diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9f32c010f2..334d51143ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,8 +58,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - - name: Couch Start - run: ./scripts/ci/couch-start - name: Create logs directory run: mkdir tests/logs - run: npm ci diff --git a/.gitignore b/.gitignore index e79a711369d..19a6ea80be9 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,5 @@ webapp/analyzer.report.html user-password-change.txt user-password-change.csv /tests/helm/values.yaml +/tests/e2e/visual/images/*.png + diff --git a/api/resources/translations/messages-bm.properties b/api/resources/translations/messages-bm.properties index 6bfa8a96b2f..93e15c35a66 100644 --- a/api/resources/translations/messages-bm.properties +++ b/api/resources/translations/messages-bm.properties @@ -387,7 +387,6 @@ case_id = child_birth_date = Den wolo Don/Kalo/San child_birth_outcome = Den wololen ka kunafoniw child_birth_weight = den ka banke kunafoniw jabi -clear = Ka jɔsi cleared = Don minna kunafoni dɔsila clientDdoc.version = clinic.field.children = @@ -1070,7 +1069,6 @@ select.mode.deselect.all = ka sukandilen josi select.mode.select.all = ka bɛ sukandi select.mode.start = soukandili select.mode.stop = ka datuku -select\ all = A be sugantili selection.doc.content.raw = send\ the\ following\ message\ to\ the = Nin bataki nata ci ka taa sent = Cilen @@ -1100,7 +1098,8 @@ setup.registration.title = Sɛben min bɛ maralikɛ setup.skip = Ka tɛmɛ nin yɔrɔ kan setup.start = Bara banni setup.statistics.description = -setup.statistics.title = +setup.statistics.title = +sidebar_menu.title = Tɛmɛnɛ simprints.disabled = Simprints ma lasɔrɔ. Medic Mobile ka Android baarakɛminɛ laban dɔ simprints.register = ka tɔgɔ sɛbɛn kɛ nin Simprints ye simprints.search = ka ɲinini kɛ nin Simprints ye diff --git a/api/resources/translations/messages-en.properties b/api/resources/translations/messages-en.properties index 47ae4fe7e00..cb04a0a6e72 100644 --- a/api/resources/translations/messages-en.properties +++ b/api/resources/translations/messages-en.properties @@ -403,7 +403,6 @@ case_id = Case ID child_birth_date = Child birth date child_birth_outcome = Child birth outcome child_birth_weight = Child birth weight -clear = clear cleared = Cleared timestamp clientDdoc.version = Client webapp revision clinic.field.children = People @@ -1008,7 +1007,7 @@ permission.description.can_view_uhc_stats = Allowed to view UHC stats in the Con permission.description.can_view_message_action = Display a button to send a message to the selected person. permission.description.can_view_messages = Allowed to view Messages. permission.description.can_view_messages_tab = Display the Messages tab in the app. If not set it a menu item will be shown in the app menu instead. -permission.description.can_view_old_filter_and_search = Allowed to view the old filter and search. +permission.description.can_view_old_navigation = Allowed to view old Navigation bar. permission.description.can_view_outgoing_messages = Allowed to view the Outgoing Messages screen in the admin app. permission.description.can_view_reports = Allowed to view Reports. permission.description.can_view_reports_tab = Display the Reports tab in the app. If not set it a menu item will be shown in the app menu instead. @@ -1166,6 +1165,7 @@ schedule.registration_anc_pnc = Registration Followup scheduled = Scheduled timestamp scheduled_tasks = Scheduled tasks search_bar.filter.label = Filter +search_bar.sort.label = Sort select.mode.count.plural = {{number}} records selected select.mode.count.singular = 1 record selected select.mode.delete.all = Delete all @@ -1173,7 +1173,6 @@ select.mode.deselect.all = Clear selection select.mode.select.all = Select all select.mode.start = Select select.mode.stop = Close -select\ all = select all selection.doc.content.raw = Raw report content send\ the\ following\ message\ to\ the = send the following message to the sent = Sent timestamp @@ -1188,6 +1187,7 @@ settings.restore.description = Import a new JSON file and overwrite the current settings.restore.title = Upload application code setup.language.outgoing.subtitle = Select the language in which your care coordinators would like to receive automated messages from CHT. setup.language.subtitle = Select the default language that website users of CHT will be using. Users can set individual preferences after logging in. +sidebar_menu.title = Menu sms_message.message = Incoming message sms_received = SMS message received; it will be reviewed shortly. If you were trying to submit a text form, please enter a correct form code and try again. state.cleared = cleared diff --git a/api/resources/translations/messages-es.properties b/api/resources/translations/messages-es.properties index 6d85b4e411d..2bb18b9bcf7 100644 --- a/api/resources/translations/messages-es.properties +++ b/api/resources/translations/messages-es.properties @@ -403,7 +403,6 @@ case_id = Identificación del caso child_birth_date = Fecha de nacimiento del niño child_birth_outcome = Resultado del nacimiento del niño child_birth_weight = Peso del niño al nacer -clear = Borrar cleared = Fecha y hora del borrado clientDdoc.version = Revisión del webapp en el cliente clinic.field.children = Personas @@ -1008,7 +1007,7 @@ permission.description.can_view_uhc_stats = Ver las estadísticas de UHC en la s permission.description.can_view_message_action = Mostrar el botón para enviar un mensaje a la persona seleccionada. permission.description.can_view_messages = Ver Mensajes. permission.description.can_view_messages_tab = Mostrar la pestaña de Mensajes en la aplicación. Si no lo configura, se mostrará en el menú de la aplicación. -permission.description.can_view_old_filter_and_search = Ver el diseño viejo de búsqueda y filtro. +permission.description.can_view_old_navigation = Mostrar la barra de navegación antigua. permission.description.can_view_outgoing_messages = Ver la página de mensajes salientes en la Gestión de la Aplicación. permission.description.can_view_reports = Ver Informes. permission.description.can_view_reports_tab = Mostrar la pestaña de Informes en la aplicación. Si no lo configura, se mostrará en el menú de la aplicación. @@ -1166,6 +1165,7 @@ schedule.registration_anc_pnc = Seguimiento de Registro scheduled = Fecha y hora programada scheduled_tasks = Tareas programadas search_bar.filter.label = Filtros +search_bar.sort.label = Ordenar select.mode.count.plural = {{number}} registros seleccionados select.mode.count.singular = 1 registro seleccionado select.mode.delete.all = Eliminar todos @@ -1173,7 +1173,6 @@ select.mode.deselect.all = Quitar selección select.mode.select.all = Seleccionar todos select.mode.start = Seleccionar select.mode.stop = Cerrar -select\ all = seleccionar todo selection.doc.content.raw = Contenido del informe sin procesar send\ the\ following\ message\ to\ the = enviar el siguiente mensaje a sent = Fecha y hora de envío @@ -1188,6 +1187,7 @@ settings.restore.description = Importar un nuevo archivo JSON y sobrescribir la settings.restore.title = Subir código de la aplicación setup.language.outgoing.subtitle = Seleccione el idioma en el que a sus coordinadores les gustaría recibir mensajes automáticos de CHT. setup.language.subtitle = Seleccione el idioma predeterminado que utilizarán los usuarios de CHT. Los usuarios pueden establecer preferencias individuales después de iniciar sesión. +sidebar_menu.title = Menu sms_message.message = Mensajes entrantes sms_received = Recibímos su mensaje, lo procesaremos pronto. Si intentaba mandar un formulario de texto, favor inténtelo de nuevo con un código de formulario. state.cleared = borrado diff --git a/api/resources/translations/messages-fr.properties b/api/resources/translations/messages-fr.properties index e15e1e11148..a82b00af6de 100644 --- a/api/resources/translations/messages-fr.properties +++ b/api/resources/translations/messages-fr.properties @@ -403,7 +403,6 @@ case_id = ID du cas child_birth_date = Date de naissance de l'enfant child_birth_outcome = Résultat de la naissance de l'enfant child_birth_weight = Poids de l'enfant à la naissance -clear = éffacer cleared = Date d'effacement clientDdoc.version = Version DDoc client clinic.field.children = Membres de famille @@ -1008,7 +1007,7 @@ permission.description.can_view_uhc_stats = Autorisé à afficher les statistiqu permission.description.can_view_message_action = Afficher un bouton pour envoyer un message à la personne sélectionnée. permission.description.can_view_messages = Autorisé à afficher les Messages. permission.description.can_view_messages_tab = Affichez l'onglet Messages dans l'application. S'il n'est pas défini, un élément de menu sera affiché dans le menu de l'application à la place. -permission.description.can_view_old_filter_and_search = Autorisé à afficher l'ancien filtre et à rechercher. +permission.description.can_view_old_navigation = Autorisé à afficher l'ancienne Barre de Navigation. permission.description.can_view_outgoing_messages = Autorisé à afficher l'écran des messages sortants dans l'application d'administration. permission.description.can_view_reports = Autorisé à afficher les rapports. permission.description.can_view_reports_tab = Affichez l'onglet Rapports dans l'application. S'il n'est pas défini, un élément de menu sera affiché dans le menu de l'application à la place. @@ -1166,6 +1165,7 @@ schedule.registration_anc_pnc = Suivi de l'enregistrement scheduled = Prévu scheduled_tasks = Tâches plannifiées search_bar.filter.label = Filtre +search_bar.sort.label = Trier select.mode.count.plural = {{number}} enregistrements sélectionnés select.mode.count.singular = 1 enregistrement supprimé select.mode.delete.all = Supprimer tout @@ -1173,7 +1173,6 @@ select.mode.deselect.all = Effacer la séléction select.mode.select.all = Sélectionner tout select.mode.start = Selectionner select.mode.stop = Fermer -select\ all = tout sélectionner selection.doc.content.raw = Contenu du rapport brut send\ the\ following\ message\ to\ the = envoyer le message suivant à la sent = Envoyé @@ -1188,6 +1187,7 @@ settings.restore.description = Importer un nouveau fichier JSON et écraser les settings.restore.title = Envoyer nouveaux réglages setup.language.outgoing.subtitle = Sélectionnez la langue pour les messages qui seront envoyés aux coordonnateurs de soins. setup.language.subtitle = Sélectionnez la langue que les utilisateurs de CHT utilisent par défaut. Les utilisateurs peuvent toujours définir leur préférence une fois inscrits. +sidebar_menu.title = Menu sms_message.message = Messages entrants sms_received = Merci, votre message a été bien reçu. Si vous étiez en train d'envoyer un rapport réessayez avec le bon code du rapport. state.cleared = dégagé diff --git a/api/resources/translations/messages-hi.properties b/api/resources/translations/messages-hi.properties index 0b5d81a7031..9b012194aff 100644 --- a/api/resources/translations/messages-hi.properties +++ b/api/resources/translations/messages-hi.properties @@ -387,7 +387,6 @@ case_id = child_birth_date = बच्चे के जन्म की तारीख child_birth_outcome = बच्चे के जन्म का परिणाम child_birth_weight = बच्चे के जन्म के समय वजन -clear = हटाना cleared = हटाने का समय clientDdoc.version = क्लाइंट DDoc वर्जन clinic.field.children = लोग @@ -1070,7 +1069,6 @@ select.mode.deselect.all = चयन को साफ करें select.mode.select.all = सभी का चयन करे select.mode.start = चयन select.mode.stop = बंद -select\ all = सभी का चयन करें selection.doc.content.raw = कच्चा रिपोर्ट के विवरण send\ the\ following\ message\ to\ the = यह संदेश भेजें इन को sent = भेजने का समय @@ -1101,6 +1099,7 @@ setup.skip = स्थापना के गाइड को छोड़ें setup.start = अंत setup.statistics.description = अज्ञात आंकड़ों को मेडिक मोबाइल पर सबमिट करने दें ? सॉफ्टवेयर को बेहतर बनाने और उसके प्रभाव के बारे में जानने में मदद के लिए आंकड़ों को हर महीने भेजा जायेगा । कोई भी रोगी की गोपनीय जानकारी हमारे साथ साझा नहीं की जाएगी। आप इस गाइड से किसी भी समय इन सेटिंग्स को अपडेट कर सकते हैं। setup.statistics.title = प्रभाव के डेटा साझा करे +sidebar_menu.title = मेनू simprints.disabled = इस डिवाइस पर सिमप्रिंट उपलब्ध नहीं है। सिम्पप्रिंट एकीकरण को सक्षम करने के लिए मेडिक मोबाइल एंड्रॉइड ऐप के नये संस्करण का उपयोग करें।\n simprints.register = Simprints के साथ रजिस्टर करें simprints.search = Simprints के साथ खोजें diff --git a/api/resources/translations/messages-id.properties b/api/resources/translations/messages-id.properties index 058ca793c7f..53a0b392f54 100644 --- a/api/resources/translations/messages-id.properties +++ b/api/resources/translations/messages-id.properties @@ -390,8 +390,7 @@ call = Telepon case_id = child_birth_date = Tanggal Lahir Anak child_birth_outcome = Outcome Anak dilahirkan -child_birth_weight = Berat Lahir Anak -clear = bersih +child_birth_weight = Berat Lahir Anak cleared = Waktu Dikosongkan clientDdoc.version = Versi Klien DDoc clinic.field.children = Anggota keluarga @@ -925,6 +924,7 @@ permission.description.can_view_last_visited_date = permission.description.can_view_message_action = permission.description.can_view_messages = permission.description.can_view_messages_tab = +permission.description.can_view_old_navigation = Tampilkan bilah navigasi lama permission.description.can_view_outgoing_messages = permission.description.can_view_reports = permission.description.can_view_reports_tab = @@ -1074,7 +1074,7 @@ select.mode.deselect.all = Bersihkan pilihan select.mode.select.all = Pilih semua select.mode.start = Pilih select.mode.stop = Tutup -select\ all = pilih semua +search_bar.sort.label = Urutkan selection.doc.content.raw = Isi Laporan Mentah send\ the\ following\ message\ to\ the = kirim pesan ini kepada sent = Waktu Pengiriman @@ -1104,7 +1104,8 @@ setup.registration.title = Formulir registrasi setup.skip = Melewatkan panduan pengaturan setup.start = Selesai setup.statistics.description = -setup.statistics.title = +setup.statistics.title = +sidebar_menu.title = Menu simprints.disabled = simprints.register = Daftar dengan Simprints simprints.search = Cari dengan Simprints diff --git a/api/resources/translations/messages-ne.properties b/api/resources/translations/messages-ne.properties index 9d7057b87e3..ab858cbc35b 100644 --- a/api/resources/translations/messages-ne.properties +++ b/api/resources/translations/messages-ne.properties @@ -403,7 +403,6 @@ case_id = केस आईडी child_birth_date = बच्चाको जन्म मिति child_birth_outcome = बच्चाको जन्मावस्था child_birth_weight = बच्चाको जन्म तौल -clear = मेटाउनुहोस cleared = मेटाइएको समय clientDdoc.version = ग्राहक रिभिजन clinic.field.children = परिवारका सदस्य @@ -601,9 +600,9 @@ debug.db_info.title = डाटाबेसको जानकारी debug.mode = डिबग मोड debug.mode.description = डिबग मोडले डेभ्लपरहरुलाई एप्लिकेशनमा समस्या समाधान गर्न मद्दत गर्न ब्राउजर कन्सोलमा जानकारी प्रिन्ट गर्नेछ। यो सेटिङ परिवर्तन गरेपछि, यसलाई लागू गर्न एप पुनः लोड गर्नुहोस्। debug.mode.title = डिबग मोडमा जानुहोस् -denied = अस्वीकार टाइमस्ट्याम्प debug.supported_browser = समर्थित ब्राउजर debug.supported_browser.see_requirements = आवश्यकताहरू हेर्नुहोस् +denied = अस्वीकार टाइमस्ट्याम्प display.language.accordion.title = उपलब्ध गराइएका भाषाहरू प्रयोगकर्तालाई उनीहरुको प्राथमिक भाषा छान्नको लागि देखाइने छन्। हामीले १-३ वटा भाषा मात्र प्रयोगकर्तालाई देखाउन र अरु भाषाहरू लुकाउन (disable गर्न) सल्लाह दिन्छौँ। उपलब्ध गराइएका भाषामा कुनै पनि अनुवाद छुट्नु हुँदैन। display.privacy.policies.content.type = सामग्रीको प्रकार display.privacy.policies.current = हालको नीति @@ -639,10 +638,10 @@ email.invalid = इमेल ठेगाना मिलेन empty = सन्देश​ खाली छ​ । कृपया फेरि प्रयास गर्नुहोला। enketo.constraint.invalid = अमान्य enketo.constraint.required = आवश्यक -enketo.error.max_attachment_size = अपलोड गरिएको फाइलहरूको आकार साइज सीमाभन्दा बढी छ। कृपया साना फाइलहरू अपलोड गर्नुहोस्। enketo.drawwidget.annotation = व्याख्या enketo.drawwidget.drawing = चित्र enketo.drawwidget.signature = हस्ताक्षर +enketo.error.max_attachment_size = अपलोड गरिएको फाइलहरूको आकार साइज सीमाभन्दा बढी छ। कृपया साना फाइलहरू अपलोड गर्नुहोस्। enketo.form.required = आवश्यक enketo.filepicker.file = फाइल enketo.filepicker.placeholder = फाइल अपलोड गर्नलाई यहाँ थिच्नुहोस्। (< {{maxSize}}) @@ -1008,8 +1007,8 @@ permission.description.can_view_uhc_stats = सम्पर्क संक् permission.description.can_view_message_action = चयन गरिएको व्यक्तिसँग सन्देश पठाउन बटन देखाउनुहोस्। permission.description.can_view_messages = सन्देशहरू हेर्न अनुमति दिइएको। permission.description.can_view_messages_tab = एपमा सन्देशहरू ट्याब देखाउनुहोस्। यदि सेट गरिएन भने, एप मेनूमा मेनु वस्तु देखाइनेछ। -permission.description.can_view_old_filter_and_search = पुरानो फिल्टर र खोज हेर्न अनुमति दिइएको। permission.description.can_view_outgoing_messages = व्यवस्थापक एपमा बाहिर जान सक्ने सन्देशहरू हेर्न अनुमति दिइएको। +permission.description.can_view_old_navigation = पुरानो नेभिगेशन बार हेर्न अनुमति दिइयो permission.description.can_view_reports = रिपोर्टहरू हेर्न अनुमति दिइएको। permission.description.can_view_reports_tab = एपमा रिपोर्टहरू ट्याब देखाउनुहोस्। यदि सेट गरिएन भने, एप मेनूमा मेनु वस्तु देखाइनेछ। permission.description.can_view_tasks = कार्यहरू हेर्न अनुमति दिइएको। @@ -1166,6 +1165,7 @@ schedule.registration_anc_pnc = दर्ता फलोअप scheduled = समयतालिका scheduled_tasks = कार्यतालिका search_bar.filter.label = फिल्टर +search_bar.sort.label = क्रमबद्ध गर्नुहोस् select.mode.count.plural = {{number}} रेकर्ड छानिएका छन् select.mode.count.singular = एउटा रेकर्ड छानिएको छ select.mode.delete.all = सबै मेटाउनुहोस् @@ -1173,7 +1173,6 @@ select.mode.deselect.all = छनोट हटाउनुहोस् select.mode.select.all = सबै छनोट गर्नुहोस् select.mode.start = छनोट गर्नुहोस् select.mode.stop = बन्द गर्नुहोस् -select\ all = सबै छान्नुहोस् selection.doc.content.raw = अप्रशोधित रिपोर्ट send\ the\ following\ message\ to\ the = निम्न सन्देश _____ लाई पठाउनुहोस sent = पठाइएको समय @@ -1188,6 +1187,7 @@ settings.restore.description = नयाँ JOSS फाईल आयात ग settings.restore.title = नयाँ सेटिंग्स अपलोड गर्नुहोस् setup.language.outgoing.subtitle = मेडिक मोबाइलले पठाउने सन्देशहरुको भाषा छनौट गर्नुहोस्। setup.language.subtitle = मेडिक मोबाइल वेब एप्लिकेश का प्रयोगकर्ता हरुले प्रयोग गर्ने भाषा छनौट गर्नुहोस्। +sidebar_menu.title = मेनु sms_message.message = अाएको सन्देश sms_received = सन्देश​ प्राप्त भयो। रिपोर्ट पठाउनुभएको हो भने मिलेन, ​पुन\:​ पठाउनुहोला। state.cleared = मेटाइएको diff --git a/api/resources/translations/messages-sw.properties b/api/resources/translations/messages-sw.properties index ee597730d6a..b7e0f9dae7e 100644 --- a/api/resources/translations/messages-sw.properties +++ b/api/resources/translations/messages-sw.properties @@ -203,7 +203,7 @@ Primary\ location = Eneo Processed\ number\ of\ total\ records = Kusindika {{number}}/{{total}} recodi RC\ Code = Kodi Reading\ file = Unasoma faili.. -Registration\ example = Kwa mfano, kusajili "{{name}} utatuma\: +Registration\ example = Kwa mfano, kusajili "{{name}}" utatuma\: Registration\ format = Jiandikishe kwa ajili ya huu mfuatilio wa ujumbe kwa kutuma ujumbe mfupi na muundo ufuatayo\: Registrations = Usajili Reload = Pakia upya @@ -405,7 +405,6 @@ case_id = Kitambulisho cha kesi child_birth_date = Tarehe ya kuzaliwa mtoto child_birth_outcome = Matokeo ya mtoto mzaliwa child_birth_weight = Uzani wa mtoto mzaliwa -clear = toa cleared = kumaliza wakati wa kutuma clientDdoc.version = Toleo la DDoc la Client clinic.field.children = Watu @@ -936,7 +935,7 @@ messages.schedule.postnatal.day_7_overdue = Je, {{patient_name}} ({{patient_id}} messages.schedule.postnatal.week_6 = Tafadhali ona kwamba {{patient_name}} ({{patient_id}}) ametembelea kituo cha afya kwa huduma ya wiki ya 6 baada ya kujifungua. Akihudhuria ziara iyo, tujulishe kwa 'M {{patient_id}}'. Asante\! messages.schedule.postnatal.week_6_overdue = Je, {{patient_name}} ({{patient_id}}) alihudhuria ziara ya baada ya kujifungua ya wiki ya 6? Tujulishe kwa kutuma 'M {{patient_id}}'. Shukrani\! messages.schedule.registration.followup_anc = Hujambo {{contact.name}},kumbuka kutuma fomu ya usajili mimba ya {{patient_name}} {{patient_id}} with 'P {{patient_id}} '. Asante\! -messages.schedule.registration.followup_anc_pnc = \n {{contact.name}}, Je\! {{patient_name}} {{patient_id}} anahitaji huduma? Tuma fomu aina ya P {{patient_id}} ' kusajili kama mja mzito, Fomu ya 'D {{patient_id}} '. kumsajili kwa PNC .Asante\! +messages.schedule.registration.followup_anc_pnc = \n {{contact.name}}, Je\! {{patient_name}} {{patient_id}} anahitaji huduma? Tuma fomu aina ya 'P {{patient_id}} ' kusajili kama mja mzito, Fomu ya 'D {{patient_id}} '. kumsajili kwa PNC .Asante\! messages.sent.by = Imetumwa na {{senderName}} messages.unknown.sender = Mtumaji asiyejulikana\n messages.v.report_accepted = Asante {{contact.name}},mgonjwa kwa jina {{patient_name}} ({{patient_id}}) ameshughulikiwa katika kituo cha afya. @@ -1008,7 +1007,7 @@ permission.description.can_view_uhc_stats = Inaruhusiwa kutazama takwimu za UHC permission.description.can_view_message_action = Onyesha kitufe ili kutuma ujumbe kwa mtu aliyechaguliwa. permission.description.can_view_messages = Inaruhusiwa kutazama jumbe. permission.description.can_view_messages_tab = Onyesha kichupo cha ujumbe kwenye programu. Ikiwa haijawekwa, kipengee cha menyu kitaonyeshwa kwenye menyu ya programu badala yake. -permission.description.can_view_old_filter_and_search = Inaruhusiwa kutazama kichujio cha zamani na kutafuta. +permission.description.can_view_old_navigation = Inaruhusiwa kutazama Uabiri wa zamani. permission.description.can_view_outgoing_messages = Inaruhusiwa kutazama skrini ya ujumbe unaotoka katika programu ya msimamizi. permission.description.can_view_reports = Inaruhusiwa kutazama Ripoti. permission.description.can_view_reports_tab = Onyesha kichupo cha Ripoti kwenye programu. Ikiwa haijawekwa, kipengee cha menyu kitaonyeshwa kwenye menyu ya programu badala yake. @@ -1166,6 +1165,7 @@ schedule.registration_anc_pnc = Ufuatiliaji baada ya usajili scheduled = muda wa mpangilio scheduled_tasks = Kazi iliyopangwa search_bar.filter.label = Chuja +search_bar.sort.label = Panga select.mode.count.plural = Rekodi {{number}} zimechaguliwa select.mode.count.singular = Rekodi moja imechaguliwa select.mode.delete.all = Futa yote @@ -1173,7 +1173,6 @@ select.mode.deselect.all = Vuta uchaguzi select.mode.select.all = Chagua zote select.mode.start = Chagua select.mode.stop = Funga -select\ all = chagua yote selection.doc.content.raw = Jumbe zisizo rekebishwa send\ the\ following\ message\ to\ the = Tuma ujumbe ufuatayo sent = Wakati wa kutuma @@ -1188,6 +1187,7 @@ settings.restore.description = Ingiza faili mpya ya JSON na ubadilishe mipangili settings.restore.title = Pakia msimbo wa programu setup.language.outgoing.subtitle = Chagua lugha itakayotumika kupokea jumbe za moja kwa moja na waratibisha huduma kutoka CHT. setup.language.subtitle = Chagua lugha msingi itakayo tumika kataika tuvuti ya CHT. Mtumizi anaweza badili lugha baada ya kujisajili na kuingia kwenye programu. +sidebar_menu.title = Menyu sms_message.message = Ujumbe unaoingia sms_received = Ujumbe umepokewa; utasomwa hivi punde. Kama ulikuwa unajaribu kuwasilisha ujumbe wa muundo maalum, tafadhali weka kodi sahihi ya fomu na ujaribu tena. state.cleared = yaliyotolewa diff --git a/api/src/migrations/extract-person-contacts.js b/api/src/migrations/extract-person-contacts.js index af6e262cb53..5564d632f28 100644 --- a/api/src/migrations/extract-person-contacts.js +++ b/api/src/migrations/extract-person-contacts.js @@ -136,6 +136,7 @@ const createPerson = function(id, callback) { .catch(() => { // we tried our best - log the details and exit logger.error(`Failed to restore contact on facility "${facilityId}", contact: ${JSON.stringify(oldContact)}`); + callback(); }); }; diff --git a/api/tests/integration/.mocharc.js b/api/tests/integration/.mocharc.js new file mode 100644 index 00000000000..f6fc80ed789 --- /dev/null +++ b/api/tests/integration/.mocharc.js @@ -0,0 +1,12 @@ +process.env.COUCH_URL='http://admin:pass@localhost:5984/medic'; + +module.exports = { + allowUncaught: false, + color: true, + checkLeaks: true, + fullTrace: true, + asyncOnly: false, + spec: './api/tests/integration/**/*.js', + timeout: 20000, + reporter: 'spec', +}; diff --git a/api/tests/integration/compose.yml b/api/tests/integration/compose.yml new file mode 100644 index 00000000000..d7ea81458c1 --- /dev/null +++ b/api/tests/integration/compose.yml @@ -0,0 +1,9 @@ +services: + couchdb: + build: ../../../couchdb + ports: + - "5984:5984" + environment: + - "COUCHDB_USER=admin" + - "COUCHDB_PASSWORD=pass" + - "SVC_NAME=couchdb" diff --git a/api/tests/integration/couch-start.sh b/api/tests/integration/couch-start.sh new file mode 100755 index 00000000000..4dce7823a60 --- /dev/null +++ b/api/tests/integration/couch-start.sh @@ -0,0 +1,10 @@ +#!/bin/bash -eu + +set -e + +docker compose -f ./api/tests/integration/compose.yml up -d +echo "Starting CouchDB" + +until nc -z localhost 5984; do sleep 1; done +sleep 2 +echo "CouchDB Started" diff --git a/api/tests/integration/couch-stop.sh b/api/tests/integration/couch-stop.sh new file mode 100755 index 00000000000..0afaa9bf709 --- /dev/null +++ b/api/tests/integration/couch-stop.sh @@ -0,0 +1,3 @@ +#!/bin/bash -eu +set -e +docker compose -f ./api/tests/integration/compose.yml down --remove-orphans diff --git a/api/tests/integration/migrations/add-contact-id-to-user-docs.js b/api/tests/integration/migrations/add-contact-id-to-user-docs.js index 9593dc6b708..44ad3968ec3 100644 --- a/api/tests/integration/migrations/add-contact-id-to-user-docs.js +++ b/api/tests/integration/migrations/add-contact-id-to-user-docs.js @@ -33,7 +33,6 @@ const getUserDoc = async (id) => db.users.get(id); describe('add-contact-id-to-user migration', function() { afterEach(() => { sinon.restore(); - utils.tearDown(); }); it('migrates the contact_id value from user-settings to _users for all users', async () => { diff --git a/api/tests/integration/migrations/add-meta-validate-doc-update.spec.js b/api/tests/integration/migrations/add-meta-validate-doc-update.spec.js index 5074b227982..8df07aad64b 100644 --- a/api/tests/integration/migrations/add-meta-validate-doc-update.spec.js +++ b/api/tests/integration/migrations/add-meta-validate-doc-update.spec.js @@ -53,8 +53,6 @@ const assertUserDb = (name) => { }; describe('add-meta-validate-doc-update migration', () => { - afterEach(() => utils.tearDown()); - it('should work with no user dbs', () => { return utils .initDb([]) diff --git a/api/tests/integration/migrations/add-uuid-to-scheduled-tasks.spec.js b/api/tests/integration/migrations/add-uuid-to-scheduled-tasks.spec.js index bc9a38c297c..da08b430380 100644 --- a/api/tests/integration/migrations/add-uuid-to-scheduled-tasks.spec.js +++ b/api/tests/integration/migrations/add-uuid-to-scheduled-tasks.spec.js @@ -3,10 +3,6 @@ const utils = require('./utils'); const UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/; describe('add-uuid-to-scheduled-tasks migration', function() { - afterEach(function() { - return utils.tearDown(); - }); - const FUTURE = moment().add(1, 'week').toISOString(); const PAST = moment().subtract(1, 'week').toISOString(); diff --git a/api/tests/integration/migrations/clean-up-corrupted-users.spec.js b/api/tests/integration/migrations/clean-up-corrupted-users.spec.js index 121ae0bb04d..3c2b5aa2031 100644 --- a/api/tests/integration/migrations/clean-up-corrupted-users.spec.js +++ b/api/tests/integration/migrations/clean-up-corrupted-users.spec.js @@ -1,10 +1,6 @@ const utils = require('./utils'); describe('clean-up-corrupted-users migration', function() { - afterEach(function() { - return utils.tearDown(); - }); - it('cleans up users with $promise and $resolved fields', function() { // given return utils.initDb([ diff --git a/api/tests/integration/migrations/convert-translation-messages-fix.spec.js b/api/tests/integration/migrations/convert-translation-messages-fix.spec.js index ca97301eed7..ee468684ef6 100644 --- a/api/tests/integration/migrations/convert-translation-messages-fix.spec.js +++ b/api/tests/integration/migrations/convert-translation-messages-fix.spec.js @@ -2,10 +2,6 @@ const utils = require('./utils'); const DDOC_ID = '_design/medic'; describe('convert-translation-messages-fix migration', function() { - afterEach(function() { - return utils.tearDown(); - }); - it('converts translation messages structure', () => { // given diff --git a/api/tests/integration/migrations/create-patient-contacts.spec.js b/api/tests/integration/migrations/create-patient-contacts.spec.js index 3da75fed723..02633bd5fc4 100644 --- a/api/tests/integration/migrations/create-patient-contacts.spec.js +++ b/api/tests/integration/migrations/create-patient-contacts.spec.js @@ -5,10 +5,6 @@ const migrate = function() { }; describe('create-patient-contacts migration', function() { - afterEach(function() { - return utils.tearDown(); - }); - it('should run cleanly with no registered patients', function() { return utils.initDb([]) .then(migrate) diff --git a/api/tests/integration/migrations/emit-complete.spec.js b/api/tests/integration/migrations/emit-complete.spec.js index 81014bcf0a9..d38f1b02bcb 100644 --- a/api/tests/integration/migrations/emit-complete.spec.js +++ b/api/tests/integration/migrations/emit-complete.spec.js @@ -1,5 +1,3 @@ -const sinon = require('sinon'); - const utils = require('./utils'); describe('emit-complete', function() { @@ -7,11 +5,6 @@ describe('emit-complete', function() { await utils.initDb([]); }); - afterEach(() => { - utils.tearDown(); - sinon.restore(); - }); - it('should do nothing when tasks not configured', function() { // given return utils.initSettings({ tasks: { rules: '' } }) diff --git a/api/tests/integration/migrations/extract-person-contacts.spec.js b/api/tests/integration/migrations/extract-person-contacts.spec.js index 1bd952aa0f0..40491d51de1 100644 --- a/api/tests/integration/migrations/extract-person-contacts.spec.js +++ b/api/tests/integration/migrations/extract-person-contacts.spec.js @@ -60,7 +60,6 @@ const settings = { describe('extract-person-contacts migration', () => { afterEach(() => { sinon.restore(); - return utils.tearDown(); }); it('converts and minifies a 0.4 structure into a 2.x one', async () => { diff --git a/api/tests/integration/migrations/extract-transition-seq.spec.js b/api/tests/integration/migrations/extract-transition-seq.spec.js index 51d7f79d223..ce13113edce 100644 --- a/api/tests/integration/migrations/extract-transition-seq.spec.js +++ b/api/tests/integration/migrations/extract-transition-seq.spec.js @@ -11,7 +11,6 @@ const MIGRATION = 'extract-transition-seq'; describe(`${MIGRATION} migration`, function() { before(async () => utils.initDb([])); - after(async () => utils.tearDown()); const wipe = (dbRef, docName) => { return dbRef.get(docName) diff --git a/api/tests/integration/migrations/extract-translations.spec.js b/api/tests/integration/migrations/extract-translations.spec.js index 2c22a9197db..e61fa27393d 100644 --- a/api/tests/integration/migrations/extract-translations.spec.js +++ b/api/tests/integration/migrations/extract-translations.spec.js @@ -3,7 +3,6 @@ const utils = require('./utils'); describe('extract-translations', function() { afterEach(() => { - utils.tearDown(); sinon.restore(); }); diff --git a/api/tests/integration/migrations/namespace-form-fields.spec.js b/api/tests/integration/migrations/namespace-form-fields.spec.js index a2d0cd145d4..526f89ac42a 100644 --- a/api/tests/integration/migrations/namespace-form-fields.spec.js +++ b/api/tests/integration/migrations/namespace-form-fields.spec.js @@ -1,9 +1,6 @@ const utils = require('./utils'); describe('namespace-form-fields migration', function() { - afterEach(function() { - return utils.tearDown(); - }); it('should put form fields in fields property', function() { // given diff --git a/api/tests/integration/migrations/remove-empty-parents.spec.js b/api/tests/integration/migrations/remove-empty-parents.spec.js index 98ed5eaf539..175b63bdba2 100644 --- a/api/tests/integration/migrations/remove-empty-parents.spec.js +++ b/api/tests/integration/migrations/remove-empty-parents.spec.js @@ -1,9 +1,6 @@ const utils = require('./utils'); describe('remove-empty-parents migration', function() { - afterEach(function() { - return utils.tearDown(); - }); it('should not affect well-defined parents', function() { // given diff --git a/api/tests/integration/migrations/remove-user-language.spec.js b/api/tests/integration/migrations/remove-user-language.spec.js index 9554fe5e5d7..9f9fc728008 100644 --- a/api/tests/integration/migrations/remove-user-language.spec.js +++ b/api/tests/integration/migrations/remove-user-language.spec.js @@ -1,7 +1,6 @@ const utils = require('./utils'); describe('remove-user-language migration', function() { - afterEach(() => utils.tearDown()); it('cleans up users with language field', function() { const initialUsers = [ diff --git a/api/tests/integration/migrations/test-framework.spec.js b/api/tests/integration/migrations/test-framework.spec.js index 4a78da23bf3..b08b76beaaa 100644 --- a/api/tests/integration/migrations/test-framework.spec.js +++ b/api/tests/integration/migrations/test-framework.spec.js @@ -6,10 +6,6 @@ const utils = require('./utils'); describe('migrations integration test framework', function() { - afterEach(function() { - return utils.tearDown(); - }); - describe('no-op', function() { it('should leave an empty db empty', function() { // given diff --git a/api/tests/integration/migrations/utils.js b/api/tests/integration/migrations/utils.js index 917e92a2c44..b0fc106fae4 100644 --- a/api/tests/integration/migrations/utils.js +++ b/api/tests/integration/migrations/utils.js @@ -3,7 +3,6 @@ const {promisify} = require('util'); const fs = require('fs'); const path = require('path'); const readFileAsync = promisify(fs.readFile); -const logger = require('@medic/logger'); const db = require('../../../src/db'); const { expect } = require('chai'); @@ -67,7 +66,7 @@ const matches = (expected, actual) => { const assertDb = expected => { return db - .get('medic-test').allDocs({ include_docs: true }) + .get('medic').allDocs({ include_docs: true }) .then(results => { let actual = results.rows.map(row => _.omit(row.doc, ['_rev'])); expected.sort(byId); @@ -153,31 +152,7 @@ const matchDbs = (expected, actual) => { } }; -const realMedicDb = db.medic; -const realSentinelDb = db.sentinel; -const realUsersDb = db.users; -const switchToRealDbs = () => { - db.medic = realMedicDb; - db.sentinel = realSentinelDb; - db.users = realUsersDb; -}; - -const switchToTestDbs = () => { - db.medic = new PouchDB( - realMedicDb.name.replace(/medic$/, 'medic-test') - ); - db.sentinel = new PouchDB( - realSentinelDb.name.replace(/medic-sentinel$/, 'medic-sentinel-test') - ); - db.users = new PouchDB( - realUsersDb.name.replace(/_users$/, 'users-test') - ); -}; - const initDb = content => { - - switchToTestDbs(); - return _resetDb() .then(() => { const medicPath = path.join(__dirname, '../../../../build/ddocs/medic.json'); @@ -196,31 +171,10 @@ const initDb = content => { }); }; -const _resetDb = (attempts = 0) => { - if (attempts === 3) { - return Promise.reject(new Error('Unable to reset medic-test db')); - } - - return db.exists('medic-test') - .then(exists => { - if (exists) { - return db.get('medic-test').destroy(); - } - }) - .then(() => { - return db.get('medic-test'); - }) - .catch(err => { - logger.error('Could not create "medic-test" directly after deleting, pausing and trying again'); - logger.error(err); - return new Promise(resolve => { - setTimeout(() => resolve(_resetDb(attempts + 1)), 3000); - }); - }); -}; - -const tearDown = () => { - switchToRealDbs(); +const _resetDb = async () => { + const res = await db.medic.allDocs(); + const toDelete = res.rows.map(row => ({ _id: row.id, _rev: row.value.rev, _deleted: true })); + await db.medic.bulkDocs(toDelete); }; const runMigration = migration => { @@ -273,7 +227,6 @@ module.exports = { initSettings: initSettings, getSettings: getSettings, runMigration: runMigration, - tearDown: tearDown, getDdoc: getDdoc, insertAttachment: insertAttachment, getDocStub diff --git a/config/default/app_settings.json b/config/default/app_settings.json index a7b37e2a59e..07d76c5b722 100644 --- a/config/default/app_settings.json +++ b/config/default/app_settings.json @@ -280,8 +280,7 @@ "can_upgrade": [ "program_officer" ], - "can_view_old_filter_and_search": [], - "can_view_old_action_bar": [], + "can_view_old_navigation": [], "can_default_facility_filter": [], "can_have_multiple_places": [], "can_export_devices_details": [ diff --git a/config/demo/app_settings.json b/config/demo/app_settings.json index e1e4d16d3ce..645ae7d2896 100644 --- a/config/demo/app_settings.json +++ b/config/demo/app_settings.json @@ -276,8 +276,7 @@ "can_upgrade": [ "program_officer" ], - "can_view_old_filter_and_search": [], - "can_view_old_action_bar": [] + "can_view_old_navigation": [] }, "uhc": { "contacts_default_sort": "", diff --git a/couchdb/prod.couchdb-cluster.yml b/couchdb/prod.couchdb-cluster.yml deleted file mode 100644 index 0186b9e4b31..00000000000 --- a/couchdb/prod.couchdb-cluster.yml +++ /dev/null @@ -1,74 +0,0 @@ -version: "3.3" - -networks: - cht-net: - driver: bridge - -services: - couchdb.1: - image: medicmobile/cht-couchdb:${CHT_CORE_RELEASE:-clustered-042622-test37} - volumes: - - ${DB1_DATA:-./srv1}:/opt/couchdb/data - - cht-credentials:/opt/couchdb/etc/local.d/ - environment: - - "COUCHDB_USER=${COUCHDB_USER:-admin}" - - "COUCHDB_PASSWORD=${COUCHDB_PASSWORD}" - - "COUCHDB_SECRET=${COUCHDB_SECRET:-6c1953b6-e64d-4b0c-9268-2528396f2f58}" - - "COUCHDB_UUID=${COUCHDB_UUID:-5c265815-b9e3-47f1-ba8d-c1d50495eeb2}" - - "SVC_NAME=${SVC1_NAME:-couchdb.1}" - - "CLUSTER_PEER_IPS=couchdb.2,couchdb.3" - - "COUCHDB_LOG_LEVEL=${COUCHDB_LOG_LEVEL:-error}" - restart: always - networks: - cht-net: - - couchdb.2: - image: medicmobile/cht-couchdb:${CHT_CORE_RELEASE:-clustered-042622-test37} - volumes: - - ${DB2_DATA:-./srv2}:/opt/couchdb/data - environment: - - "COUCHDB_USER=${COUCHDB_USER:-admin}" - - "COUCHDB_PASSWORD=${COUCHDB_PASSWORD}" - - "COUCHDB_SECRET=${COUCHDB_SECRET:-6c1953b6-e64d-4b0c-9268-2528396f2f58}" - - "COUCHDB_UUID=${COUCHDB_UUID:-5c265815-b9e3-47f1-ba8d-c1d50495eeb2}" - - "SVC_NAME=${SVC2_NAME:-couchdb.2}" - - "COUCHDB_LOG_LEVEL=${COUCHDB_LOG_LEVEL:-error}" - - "COUCHDB_SYNC_ADMINS_NODE=${COUCHDB_SYNC_ADMINS_NODE:-couchdb.1}" - restart: always - networks: - cht-net: - - couchdb.3: - image: medicmobile/cht-couchdb:${CHT_CORE_RELEASE:-clustered-042622-test37} - volumes: - - ${DB3_DATA:-./srv3}:/opt/couchdb/data - environment: - - "COUCHDB_USER=${COUCHDB_USER:-admin}" - - "COUCHDB_PASSWORD=${COUCHDB_PASSWORD}" - - "COUCHDB_SECRET=${COUCHDB_SECRET:-6c1953b6-e64d-4b0c-9268-2528396f2f58}" - - "COUCHDB_UUID=${COUCHDB_UUID:-5c265815-b9e3-47f1-ba8d-c1d50495eeb2}" - - "SVC_NAME=${SVC3_NAME:-couchdb.3}" - - "COUCHDB_LOG_LEVEL=${COUCHDB_LOG_LEVEL:-error}" - - "COUCHDB_SYNC_ADMINS_NODE=${COUCHDB_SYNC_ADMINS_NODE:-couchdb.1}" - restart: always - networks: - cht-net: - - haproxy: - image: medicmobile/cht-haproxy:${CHT_CORE_RELEASE:-archv3-042822-v256} - hostname: haproxy - environment: - - "HAPROXY_IP=${HAPROXY_IP:-haproxy}" - - "COUCHDB_USER=${COUCHDB_USER:-admin}" - - "COUCHDB_PASSWORD=${COUCHDB_PASSWORD}" - - "COUCHDB1_SERVER=${COUCHDB1_SERVER:-couchdb.1}" - - "COUCHDB2_SERVER=${COUCHDB2_SERVER:-couchdb.2}" - - "COUCHDB3_SERVER=${COUCHDB3_SERVER:-couchdb.3}" - - "HAPROXY_PORT=${HAPROXY_PORT:-5984}" - networks: - cht-net: - ports: - - ${HAPROXY_PORT:-5984}:${HAPROXY_PORT:-5984} - -volumes: - cht-credentials: diff --git a/couchdb/tests/compose.yml b/couchdb/tests/compose.yml index d11c704c689..bf21860a061 100644 --- a/couchdb/tests/compose.yml +++ b/couchdb/tests/compose.yml @@ -1,5 +1,3 @@ -version: "3.7" - services: couchdb.1: build: diff --git a/couchdb/tests/tests.bats b/couchdb/tests/tests.bats index aa1f0be6562..0b8b76a73aa 100644 --- a/couchdb/tests/tests.bats +++ b/couchdb/tests/tests.bats @@ -34,7 +34,7 @@ setup() { assert_output --partial '"status":"ok"' } -@test "data inserted on one couchb can be retrieved from a peer" { +@test "data inserted on one server can be retrieved from a peer" { local id id=$( =21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/sharp/node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/sharp/node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/sharp/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/sharp/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/sharp/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/shasum-object": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", diff --git a/package.json b/package.json index 33a0214b39b..97e07877fb5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "medic", - "version": "4.11.0", + "version": "4.12.0", "private": true, "license": "AGPL-3.0-only", "repository": { @@ -32,7 +32,7 @@ "-- DEV TEST SCRIPTS": "-----------------------------------------------------------------------------------------------", "integration-all-local": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && npm run ci-integration-all", "integration-sentinel-local": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && npm run ci-integration-sentinel", - "integration-api": "mocha 'api/tests/integration/**/*.js' -t 10000", + "integration-api": "./api/tests/integration/couch-start.sh && mocha --config ./api/tests/integration/.mocharc.js && ./api/tests/integration/couch-stop.sh", "integration-all-k3d-local": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && npm run ci-integration-all-k3d", "integration-sentinel-k3d-local": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && npm run ci-integration-sentinel-k3d", "integration-cht-form": "wdio run ./tests/integration/cht-form/wdio.conf.js", @@ -52,6 +52,7 @@ "unit-haproxy-healthcheck": "cd haproxy-healthcheck && make test", "unit-cht-deploy": "cd scripts/deploy && npm test", "wdio-default-mobile-local": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && wdio run ./tests/e2e/default-mobile/wdio.conf.js --suite=all", + "wdio-visual-local": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && wdio run ./tests/e2e/visual/wdio.conf.js --suite=all", "wdio-local": "export VERSION=$(node ./scripts/build/get-version.js) && ./scripts/build/build-service-images.sh && wdio run ./tests/e2e/default/wdio.conf.js", "apdex-test": "wdio run ./tests/performance/apdex-score/wdio.conf.js", "-- CI SCRIPTS ": "-----------------------------------------------------------------------------------------------", @@ -156,6 +157,7 @@ "rewire": "^7.0.0", "rosie": "^2.1.0", "sass": "^1.67.0", + "sharp": "^0.33.5", "shellcheck": "^2.2.0", "sinon": "^16.1.0", "tail": "^2.2.6", diff --git a/scripts/add-local-ip-certs-to-docker-4.x.sh b/scripts/add-local-ip-certs-to-docker-4.x.sh index 250db10c764..6da73f1bd94 100755 --- a/scripts/add-local-ip-certs-to-docker-4.x.sh +++ b/scripts/add-local-ip-certs-to-docker-4.x.sh @@ -36,6 +36,14 @@ # ./add-local-ip-certs-to-docker-4.x.sh cht-3-14 expire # +update_nginx_local_ip_tls_cert(){ + nginxContainerId=$1 + curl --retry 3 --fail --silent --show-error -o /tmp/local-ip-fullchain https://local-ip.medicmobile.org/fullchain + curl --retry 3 --fail --silent --show-error -o /tmp/local-ip-key https://local-ip.medicmobile.org/key + docker cp /tmp/local-ip-fullchain "${nginxContainerId}":/etc/nginx/private/cert.pem 1>/dev/null + docker cp /tmp/local-ip-key "${nginxContainerId}":/etc/nginx/private/key.pem 1>/dev/null +} + container="${1:-cht-nginx}" action="${2:-refresh}" @@ -52,16 +60,15 @@ if [ "$status" = "true" ]; then result="" if [ "$action" = "refresh" ]; then result="downloaded fresh local-ip.medicmobile.org" - docker exec -it "$container" bash -c "curl -s -o /etc/nginx/private/cert.pem https://local-ip.medicmobile.org/fullchain" - docker exec -it "$container" bash -c "curl -s -o /etc/nginx/private/key.pem https://local-ip.medicmobile.org/key" + update_nginx_local_ip_tls_cert "$container" elif [ "$action" = "expire" ]; then result="installed expired local-ip.medicmobile.org" - docker cp ./tls_certificates/local-ip-expired.crt "$container":/etc/nginx/private/cert.pem - docker cp ./tls_certificates/local-ip-expired.key "$container":/etc/nginx/private/key.pem + docker cp ./tls_certificates/local-ip-expired.crt "$container":/etc/nginx/private/cert.pem 1>/dev/null + docker cp ./tls_certificates/local-ip-expired.key "$container":/etc/nginx/private/key.pem 1>/dev/null elif [ "$action" = "self" ]; then result="installed self-signed" - docker cp ./tls_certificates/self-signed.crt "$container":/etc/nginx/private/cert.pem - docker cp ./tls_certificates/self-signed.key "$container":/etc/nginx/private/key.pem + docker cp ./tls_certificates/self-signed.crt "$container":/etc/nginx/private/cert.pem 1>/dev/null + docker cp ./tls_certificates/self-signed.key "$container":/etc/nginx/private/key.pem 1>/dev/null fi if [ "$result" != "" ]; then @@ -82,6 +89,6 @@ else echo echo "See this URL for more information on running containers:" echo "" - echo " https://docs.communityhealthtoolkit.org/apps/tutorials/local-setup/" + echo " https://docs.communityhealthtoolkit.org/building/local-setup/" echo "" fi diff --git a/scripts/build/cht-core.yml.template b/scripts/build/cht-core.yml.template index bf0af364a60..9cf423b6f46 100644 --- a/scripts/build/cht-core.yml.template +++ b/scripts/build/cht-core.yml.template @@ -1,5 +1,3 @@ -version: '3.9' - services: haproxy: image: {{{ repo }}}/cht-haproxy:{{ tag }} diff --git a/scripts/build/cht-couchdb-cluster.yml.template b/scripts/build/cht-couchdb-cluster.yml.template index 91e41fcc4a1..0cb08e22b68 100644 --- a/scripts/build/cht-couchdb-cluster.yml.template +++ b/scripts/build/cht-couchdb-cluster.yml.template @@ -1,5 +1,3 @@ -version: '3.9' - services: couchdb-1.local: image: {{{ repo }}}/cht-couchdb:{{ tag }} diff --git a/scripts/build/cht-couchdb-single-node.yml.template b/scripts/build/cht-couchdb-single-node.yml.template index 45e5ac38ebc..458b31f0e8c 100644 --- a/scripts/build/cht-couchdb-single-node.yml.template +++ b/scripts/build/cht-couchdb-single-node.yml.template @@ -1,5 +1,3 @@ -version: '3.9' - services: couchdb: image: {{{ repo }}}/cht-couchdb:{{ tag }} diff --git a/scripts/ci/couch-start b/scripts/ci/couch-start deleted file mode 100755 index 61c7860cbda..00000000000 --- a/scripts/ci/couch-start +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -eu - -set -e - -# start couchdb 2.x docker instance -docker run -d -p 5984:5984 -e COUCHDB_PASSWORD=pass -e COUCHDB_USER=admin -e SVC_NAME=localhost -e COUCHDB_LOG_LEVEL=debug public.ecr.aws/medic/cht-couchdb:4.2.2 -echo "Starting CouchDB" - -until nc -z localhost 5984; do sleep 1; done -sleep 2 -curl -X PUT http://admin:pass@localhost:5984/medic -curl -X PUT http://admin:pass@localhost:5984/medic-sentinel - -echo "CouchDB Started" diff --git a/scripts/docker-helper-4.x/cht-docker-compose.sh b/scripts/docker-helper-4.x/cht-docker-compose.sh index 239ca4d98d8..7c56cc8602b 100755 --- a/scripts/docker-helper-4.x/cht-docker-compose.sh +++ b/scripts/docker-helper-4.x/cht-docker-compose.sh @@ -90,7 +90,7 @@ show_help_intro() { echo " ./cht-docker-compose.sh" } -show_help_existing_stop_and_destroy() { +show_help() { echo "" echo "Start existing project" echo " ./cht-docker-compose.sh ENV-FILE.env" @@ -101,7 +101,7 @@ show_help_existing_stop_and_destroy() { echo "Stop and destroy all project data:" echo " ./cht-docker-compose.sh ENV-FILE.env destroy" echo "" - echo "https://docs.communityhealthtoolkit.org/apps/guides/hosting/4.x/app-developer/" + echo "https://docs.communityhealthtoolkit.org/hosting/4.x/app-developer/" echo "" } @@ -248,6 +248,7 @@ get_global_running_container_count(){ } get_system_and_docker_info(){ + projectURL=$(get_local_ip_url "$(get_lan_ip)") info='Service Status Container Image' services="cht-upgrade-service haproxy healthcheck api sentinel nginx couchdb" IFS=' ' read -ra servicesArray <<<"$services" @@ -261,15 +262,45 @@ get_system_and_docker_info(){ echo "---DEBUG INFO---" echo "Load: $(get_load_avg)" echo "CHT Containers: $(get_running_container_count)" - echo "Global Containers $(get_global_running_container_count)" + echo "Global Containers: $(get_global_running_container_count)" + echo "URL: $projectURL" echo echo $"$info" | column -t } +update_nginx_local_ip_tls_cert(){ + nginxContainerId=$1 + rm -f /tmp/local-ip-fullchain /tmp/local-ip-key + curl --retry 3 --fail --silent --show-error -o /tmp/local-ip-fullchain https://local-ip.medicmobile.org/fullchain + curl --retry 3 --fail --silent --show-error -o /tmp/local-ip-key https://local-ip.medicmobile.org/key + docker cp /tmp/local-ip-fullchain "${nginxContainerId}":/etc/nginx/private/cert.pem 1>/dev/null + docker cp /tmp/local-ip-key "${nginxContainerId}":/etc/nginx/private/key.pem 1>/dev/null + docker exec "$nginxContainerId" bash -c "nginx -s reload" 2>/dev/null +} + +validate_tls(){ + url=$1 + attempt=$2 + max_retries=3 + # by default curl validates TLS. If we get back 60 then TLS isn't valid: + # exitcode: https://everything.curl.dev/usingcurl/verbose/writeout.html + # 60: https://everything.curl.dev/cmdline/exitcode.html + status=$(curl --retry 3 --write-out "%{exitcode}" -qs "$url" -o /dev/null) + if [ "$status" != "0" ]; then + if [ "$attempt" -gt $max_retries ]; then + echo "false: status is $status" + else + duration_seconds=$((2 ** attempt)) + sleep $duration_seconds + validate_tls "$url" $((attempt+1)) + fi + fi +} + if [ "$(docker_not_installed)" ];then echo "" echo -e "${red}\"docker\" or \"docker compose\" is not installed or could not be found. Please install and try again!${noColor}" - show_help_existing_stop_and_destroy + show_help exit 0 fi @@ -277,7 +308,7 @@ fi if [[ -n "${1-}" ]]; then if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then show_help_intro - show_help_existing_stop_and_destroy + show_help exit 0 elif [[ -f "$1" ]]; then projectFile=$1 @@ -286,7 +317,7 @@ if [[ -n "${1-}" ]]; then else echo "" echo -e "${red}File \"$1\" doesnt exist - be sure to include \".env\" at the end!${noColor}" - show_help_existing_stop_and_destroy + show_help exit 0 fi fi @@ -419,7 +450,7 @@ if [[ -z "$projectName" ]]; then else echo "" echo -e "${red}No projects found, please initialize a new one.${noColor}" - show_help_existing_stop_and_destroy + show_help exit 1 fi fi @@ -468,9 +499,14 @@ while [[ "$running" != "true" ]]; do running=$(is_nginx_running "$nginxContainerId") done -docker exec "$nginxContainerId" bash -c "curl -s -o /etc/nginx/private/cert.pem https://local-ip.medicmobile.org/fullchain" 2>/dev/null -docker exec "$nginxContainerId" bash -c "curl -s -o /etc/nginx/private/key.pem https://local-ip.medicmobile.org/key" 2>/dev/null -docker exec "$nginxContainerId" bash -c "nginx -s reload" 2>/dev/null +# output a new line with echo and then update certs every time we run to ensure they're current +echo;update_nginx_local_ip_tls_cert "$nginxContainerId" +if [ "$(validate_tls "$projectURL" 0)" ];then + echo "" + echo -e "${red}Failed to install local-ip TLS certificate. Check for errors above and try again${noColor}" + get_system_and_docker_info + exit 0 +fi echo "" echo "" @@ -485,7 +521,7 @@ echo " Login: ${COUCHDB_USER}" echo " Password: ${COUCHDB_PASSWORD}" echo "" echo " -------------------------------------------------------- " -show_help_existing_stop_and_destroy +show_help echo "" echo -e "${green} Have a great day!${noColor} " echo "" diff --git a/tests/config.default.json b/tests/config.default.json index a041b3e43d7..d3e3d0a451b 100644 --- a/tests/config.default.json +++ b/tests/config.default.json @@ -118,8 +118,7 @@ "can_write_wealth_quintiles": [], "can_edit_verification": ["national_admin", "district_admin"], "can_view_outgoing_messages": ["national_admin"], - "can_export_all": ["national_admin"], - "can_view_old_action_bar": ["national_admin", "district_admin", "gateway", "data_entry", "analytics"] + "can_export_all": ["national_admin"] }, "uhc": { "month_start_date": 1, diff --git a/tests/e2e/default-mobile/browser-compatibility/browser-compatibility.wdio-spec.js b/tests/e2e/default-mobile/browser-compatibility/browser-compatibility.wdio-spec.js index d221027ff92..6d636d61966 100644 --- a/tests/e2e/default-mobile/browser-compatibility/browser-compatibility.wdio-spec.js +++ b/tests/e2e/default-mobile/browser-compatibility/browser-compatibility.wdio-spec.js @@ -1,26 +1,23 @@ const commonPage = require('@page-objects/default/common/common.wdio.page'); const modalPage = require('@page-objects/default/common/modal.wdio.page'); const loginPage = require('@page-objects/default/login/login.wdio.page'); -const constants = require('@constants'); -const ANDROID_VERSION = '13'; -const SUPPORTED_CHROME_VERSION = '118.0.5993.112'; -const OUTDATED_CHROME_VERSION = '74.0.5993.112'; describe('Browser Compatibility Modal', () => { - const newChromeVersion = - `Mozilla/5.0 (Linux; Android ${ANDROID_VERSION}; IN2010) AppleWebKit/537.36 (KHTML, like Gecko) ` + - `Chrome/${SUPPORTED_CHROME_VERSION} Mobile Safari/537.36`; - - const outdatedChromeVersion = - `Mozilla/5.0 (Linux; Android ${ANDROID_VERSION}; IN2010) AppleWebKit/537.36 (KHTML, like Gecko) ` + - `Chrome/${OUTDATED_CHROME_VERSION} Mobile Safari/537.36`; + const ANDROID_VERSION = '13'; + const SUPPORTED_CHROME_VERSION = '118.0.5993.112'; + const OUTDATED_CHROME_VERSION = '74.0.5993.112'; + + const EMULATE_DEVICE_SETTINGS = { + viewport: { + width: 600, + height: 960, + isMobile: true, + hasTouch: true, + } + }; beforeEach(async () => { - await loginPage.login({ - username: constants.USERNAME, - password: constants.PASSWORD, - createUser: true, - }); + await loginPage.cookieLogin(); }); afterEach(async () => { @@ -29,30 +26,20 @@ describe('Browser Compatibility Modal', () => { }); it('should not display the browser compatibility modal for updated Chrome version', async () => { - await browser.emulateDevice({ - viewport: { - width: 600, - height: 960, - isMobile: true, - hasTouch: true, - }, - userAgent: newChromeVersion, - }); + EMULATE_DEVICE_SETTINGS.userAgent = `Mozilla/5.0 (Linux; Android ${ANDROID_VERSION}; `+ + `IN2010) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${SUPPORTED_CHROME_VERSION} Mobile Safari/537.36`; + + await browser.emulateDevice(EMULATE_DEVICE_SETTINGS); await commonPage.goToBase(); await modalPage.checkModalHasClosed(); }); it('should display the browser compatibility modal for outdated Chrome version', async () => { - await browser.emulateDevice({ - viewport: { - width: 600, - height: 960, - isMobile: true, - hasTouch: true, - }, - userAgent: outdatedChromeVersion, - }); + EMULATE_DEVICE_SETTINGS.userAgent = `Mozilla/5.0 (Linux; Android ${ANDROID_VERSION}; ` + + `IN2010) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${OUTDATED_CHROME_VERSION} Mobile Safari/537.36`; + + await browser.emulateDevice(EMULATE_DEVICE_SETTINGS); await commonPage.goToBase(); const modal = await modalPage.getModalDetails(); diff --git a/tests/e2e/default-mobile/content-security-policy.wdio-spec.js b/tests/e2e/default-mobile/content-security-policy.wdio-spec.js index f9167035f82..cf2695ff3e9 100644 --- a/tests/e2e/default-mobile/content-security-policy.wdio-spec.js +++ b/tests/e2e/default-mobile/content-security-policy.wdio-spec.js @@ -8,10 +8,6 @@ describe('Content Security Policy', () => { await loginPage.cookieLogin(); }); - after(async () => { - await commonPage.logout(); - }); - // If this test fails, you've probably changed the inline telemetry script // If the change is intentional, take the hash recommended in this error and replace the telemetry hash in the // API helmet configuration diff --git a/tests/e2e/default-mobile/navigation/more-options-menu.wdio-spec.js b/tests/e2e/default-mobile/navigation/more-options-menu.wdio-spec.js new file mode 100644 index 00000000000..72214a2272d --- /dev/null +++ b/tests/e2e/default-mobile/navigation/more-options-menu.wdio-spec.js @@ -0,0 +1,117 @@ +const commonPage = require('@page-objects/default/common/common.wdio.page'); +const reportPage = require('@page-objects/default/reports/reports.wdio.page'); +const utils = require('@utils'); +const placeFactory = require('@factories/cht/contacts/place'); +const reportFactory = require('@factories/cht/reports/generic-report'); +const personFactory = require('@factories/cht/contacts/person'); +const userFactory = require('@factories/cht/users/users'); +const loginPage = require('@page-objects/default/login/login.wdio.page'); +const contactsPage = require('@page-objects/default/contacts/contacts.wdio.page'); +const sms = require('@utils/sms'); + +describe('More Options Menu - Offline User', () => { + const places = placeFactory.generateHierarchy(); + const health_center = places.get('health_center'); + let xmlReportId; + let smsReportId; + + const contact = personFactory.build({ + name: 'chw_robert', + phone: '+12068881234', + place: health_center._id, + parent: { _id: health_center._id, parent: health_center.parent }, + }); + + const offlineUser = userFactory.build({ + isOffline: true, + place: health_center._id, + contact: contact._id, + }); + + const patient = personFactory.build({ + name: 'patient_sarah', + parent: health_center + }); + + const xmlReport = reportFactory + .report() + .build( + { form: 'home_visit', content_type: 'xml' }, + { patient, submitter: contact }, + ); + + const smsReport = reportFactory + .report() + .build( + { form: 'P', patient_id: patient._id, }, + { patient, submitter: offlineUser.contact, fields: { lmp_date: 'Feb 3, 2022', patient_id: patient._id }, }, + ); + + before(async () => { + await utils.saveDocs([ ...places.values(), contact, patient ]); + xmlReportId = (await utils.saveDoc(xmlReport)).id; + smsReportId = (await utils.saveDoc(smsReport)).id; + await utils.createUsers([offlineUser]); + await loginPage.login(offlineUser); + }); + + describe('Message tab', () => { + it('should hide the kebab menu.', async () => { + await sms.sendSms('testing', contact.phone); + expect(await (await commonPage.moreOptionsMenu()).isExisting()).to.be.false; + }); + }); + + describe('Contact tab', () => { + + beforeEach(async () => { + await commonPage.goToBase(); + }); + + it('should hide the \'export\' option and ' + + 'enable the \'edit\' and \'delete\' options when a contact is opened', async () => { + await commonPage.goToPeople(); + await contactsPage.selectLHSRowByText(patient.name); + await commonPage.openMoreOptionsMenu(); + expect(await commonPage.isMenuOptionVisible('export', 'contacts')).to.be.false; + expect(await commonPage.isMenuOptionEnabled('edit', 'contacts')).to.be.true; + expect(await commonPage.isMenuOptionEnabled('delete', 'contacts')).to.be.true; + }); + + it('should hide the \'export\' and \'edit\' options and ' + + 'disable the \'delete\' option when the offline user\'s place is selected', async () => { + await commonPage.goToPeople(); + await contactsPage.selectLHSRowByText(health_center.name); + await commonPage.openMoreOptionsMenu(); + expect(await commonPage.isMenuOptionVisible('export', 'contacts')).to.be.false; + expect(await commonPage.isMenuOptionVisible('edit', 'contacts')).to.be.false; + expect(await commonPage.isMenuOptionEnabled('delete', 'contacts')).to.be.false; + }); + }); + + describe('Report tab', () => { + it('should hide the \'export\' and \'edit\' options and ' + + 'enable the \'delete\' and \'review\' options when the sms report is opened', async () => { + await commonPage.goToReports(); + expect(await (await commonPage.moreOptionsMenu()).isExisting()).to.be.false; + + await reportPage.goToReportById(smsReportId); + await commonPage.openMoreOptionsMenu(); + expect(await commonPage.isMenuOptionVisible('export', 'reports')).to.be.false; + expect(await commonPage.isMenuOptionVisible('edit', 'reports')).to.be.false; + expect(await commonPage.isMenuOptionEnabled('delete', 'reports')).to.be.true; + expect(await commonPage.isMenuOptionEnabled('review', 'report')).to.be.true; + }); + + it('should hide the \'export\' option and ' + + 'enable the \'edit\', \'delete\' and \'review\' options when the xml report is opened', async () => { + await reportPage.goToReportById(xmlReportId); + await commonPage.openMoreOptionsMenu(); + expect(await commonPage.isMenuOptionVisible('export', 'reports')).to.be.false; + expect(await commonPage.isMenuOptionEnabled('edit', 'reports')).to.be.true; + expect(await commonPage.isMenuOptionEnabled('delete', 'reports')).to.be.true; + expect(await commonPage.isMenuOptionEnabled('review', 'report')).to.be.true; + }); + }); +}); + diff --git a/tests/e2e/default-mobile/old-navigation/old-navigation.wdio-spec.js b/tests/e2e/default-mobile/old-navigation/old-navigation.wdio-spec.js new file mode 100644 index 00000000000..ffb4f25a667 --- /dev/null +++ b/tests/e2e/default-mobile/old-navigation/old-navigation.wdio-spec.js @@ -0,0 +1,102 @@ +const utils = require('@utils'); +const placeFactory = require('@factories/cht/contacts/place'); +const userFactory = require('@factories/cht/users/users'); +const personFactory = require('@factories/cht/contacts/person'); +const pregnancyFactory = require('@factories/cht/reports/pregnancy'); +const loginPage = require('@page-objects/default/login/login.wdio.page'); +const oldNavigationPage = require('@page-objects/default/old-navigation/old-navigation.wdio.page'); +const messagesPage = require('@page-objects/default/sms/messages.wdio.page'); +const commonPage = require('@page-objects/default/common/common.wdio.page'); +const taskPage = require('@page-objects/default/tasks/tasks.wdio.page'); +const reportsPage = require('@page-objects/default/reports/reports.wdio.page'); +const contactPage = require('@page-objects/default/contacts/contacts.wdio.page'); +const targetAggregatesPage = require('@page-objects/default/targets/target-aggregates.wdio.page'); + +describe('Old Navigation', () => { + const places = placeFactory.generateHierarchy(); + const healthCenter = places.get('health_center'); + + const offlineUser = userFactory.build({ place: healthCenter._id }); + + const person = personFactory.build({ + phone: '+50689999999', + parent: { _id: healthCenter._id, parent: healthCenter.parent } + }); + + const pregnancyReport = pregnancyFactory.build({ + contact: offlineUser.contact, + fields: { patient_id: person._id } + }); + + const targetsConfig = [{ id: 'test_target', type: 'count', title: 'Test target', aggregate: true }]; + + before(async () => { + await utils.saveDocs([...places.values(), person, pregnancyReport]); + await utils.createUsers([offlineUser]); + + const settings = await utils.getSettings(); + const tasks = settings.tasks; + tasks.targets.items = targetsConfig; + const permissions = settings.permissions; + permissions.can_aggregate_targets = offlineUser.roles; + permissions.can_view_old_navigation = offlineUser.roles; + await utils.updateSettings({ tasks, permissions }, true); + + await loginPage.login({username: offlineUser.username, password: offlineUser.password, loadPage: false}); + await oldNavigationPage.waitForPageLoaded(); + }); + + after(async () => { + await utils.revertSettings(true); + await utils.deleteUsers([offlineUser]); + }); + + it('should navigate to the Messages section and open a sent message', async () => { + const message = 'Navigations test'; + await messagesPage.sendMessageOnMobile(message, person.name, person.phone ); + await messagesPage.openMessage(person._id); + + const { name } = await commonPage.getHeaderTitleOnMobile(); + expect(name).to.equal(person.name); + + const messages = await messagesPage.getAmountOfMessagesByPhone(); + const { content, state } = await messagesPage.getMessageContent(messages); + expect(content).to.equal(message); + expect(state).to.equal('pending'); + }); + + it('should navigate to the Task section and open the first task listed', async () => { + await oldNavigationPage.goToTasks(); + await taskPage.openTaskById( + pregnancyReport._id, + '~pregnancy-danger-sign-follow-up~anc.pregnancy_danger_sign_followup' + ); + const { name } = await commonPage.getHeaderTitleOnMobile(); + expect(name).to.equal('Pregnancy danger sign follow-up'); + }); + + it('should navigate to the Reports section and open the first report listed', async () => { + await oldNavigationPage.goToReports(); + await reportsPage.openSelectedReport(await reportsPage.leftPanelSelectors.firstReport()); + const openReportInfo = await reportsPage.getOpenReportInfo(); + expect(openReportInfo.patientName).to.equal(person.name); + expect(openReportInfo.reportName).to.equal('Pregnancy registration'); + }); + + it('should navigate to the People section and open the created Health Center', async () => { + await oldNavigationPage.goToPeople(); + await contactPage.selectLHSRowByText(healthCenter.name); + expect(await contactPage.getContactInfoName()).to.equal(healthCenter.name); + }); + + it('should navigate to the Targets section, and open a target aggregate', async () => { + await oldNavigationPage.goToAnalytics(); + await targetAggregatesPage.goToTargetAggregates(true); + await targetAggregatesPage.openTargetDetails(targetsConfig[0]); + }); + + it('should successfully sync', async () => { + await oldNavigationPage.goToPeople(); + await oldNavigationPage.sync(); + }); +}); diff --git a/tests/e2e/default-mobile/reports/bulk-delete.wdio-spec.js b/tests/e2e/default-mobile/reports/bulk-delete.wdio-spec.js index 7b3e538aeab..f2fe440d374 100644 --- a/tests/e2e/default-mobile/reports/bulk-delete.wdio-spec.js +++ b/tests/e2e/default-mobile/reports/bulk-delete.wdio-spec.js @@ -83,6 +83,8 @@ describe('Bulk delete reports', () => { }); it('should open a selected report and a no selected report', async () => { + await commonElements.goToReports(); + const selectOne = await reportsPage.selectReports([ savedUuids[0] ]); expect(selectOne.countLabel).to.equal('1'); expect(selectOne.selectedCount).to.equal(1); diff --git a/tests/e2e/default-mobile/reports/delete.wdio-spec.js b/tests/e2e/default-mobile/reports/delete.wdio-spec.js deleted file mode 100644 index 66886bfc267..00000000000 --- a/tests/e2e/default-mobile/reports/delete.wdio-spec.js +++ /dev/null @@ -1,67 +0,0 @@ -const moment = require('moment'); -const userFactory = require('@factories/cht/users/users'); -const placeFactory = require('@factories/cht/contacts/place'); -const personFactory = require('@factories/cht/contacts/person'); -const reportFactory = require('@factories/cht/reports/generic-report'); -const commonElements = require('@page-objects/default/common/common.wdio.page'); -const reportsPage = require('@page-objects/default/reports/reports.wdio.page'); -const loginPage = require('@page-objects/default/login/login.wdio.page'); -const utils = require('@utils'); - -describe('Delete Reports', () => { - const places = placeFactory.generateHierarchy(); - const healthCenter = places.get('health_center'); - const onlineUser = userFactory.build({ place: healthCenter._id, roles: ['program_officer'] }); - const patient = personFactory.build({ parent: { _id: healthCenter._id, parent: healthCenter.parent } }); - - const today = moment(); - const reports = [ - reportFactory - .report() - .build( - { - form: 'P', - reported_date: moment([today.year(), today.month(), 1, 23, 30]).subtract(4, 'month').valueOf() - }, - { patient, submitter: onlineUser.contact, fields: { lmp_date: 'Feb 3, 2022' } }, - ), - reportFactory - .report() - .build( - { - form: 'P', - reported_date: moment([today.year(), today.month(), 12, 10, 30]).subtract(1, 'month').valueOf() - }, - { patient, submitter: onlineUser.contact, fields: { lmp_date: 'Feb 16, 2022' } }, - ), - ]; - - const savedReportIds = []; - - before(async () => { - await utils.saveDocs([ ...places.values(), patient ]); - (await utils.saveDocs(reports)).forEach(savedReport => savedReportIds.push(savedReport.id)); - await utils.createUsers([ onlineUser ]); - await loginPage.login(onlineUser); - }); - - after(async () => { - await utils.deleteUsers([onlineUser]); - await utils.revertDb([/^form:/], true); - }); - - it('Should delete report', async () => { - await commonElements.goToReports(); - await (await reportsPage.leftPanelSelectors.firstReport()).waitForDisplayed(); - - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(savedReportIds[0])).isDisplayed()).to.be.true; - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(savedReportIds[1])).isDisplayed()).to.be.true; - - await reportsPage.openReport(savedReportIds[1]); - await reportsPage.deleteReport(); - await commonElements.goToReports(); - - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(savedReportIds[0])).isDisplayed()).to.be.true; - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(savedReportIds[1])).isDisplayed()).to.be.true; - }); -}); diff --git a/tests/e2e/default-mobile/reports/send-message.wdio-spec.js b/tests/e2e/default-mobile/reports/send-message.wdio-spec.js new file mode 100644 index 00000000000..a973be680fd --- /dev/null +++ b/tests/e2e/default-mobile/reports/send-message.wdio-spec.js @@ -0,0 +1,50 @@ +const utils = require('@utils'); +const placeFactory = require('@factories/cht/contacts/place'); +const userFactory = require('@factories/cht/users/users'); +const personFactory = require('@factories/cht/contacts/person'); +const loginPage = require('@page-objects/default/login/login.wdio.page'); +const commonElements = require('@page-objects/default/common/common.wdio.page'); +const reportsPage = require('@page-objects/default/reports/reports.wdio.page'); +const pregnancyFactory = require('@factories/cht/reports/pregnancy'); + +describe('Report - Send message action', () => { + const places = placeFactory.generateHierarchy(); + const healthCenter = places.get('health_center'); + const clinic = places.get('clinic'); + + const chwContact = personFactory.build({ + phone: '+25475525759', + parent: { _id: clinic._id, parent: clinic.parent } + }); + + const onlineUser = userFactory.build({ + place: healthCenter._id, + roles: ['program_officer'], + }); + + const person = personFactory.build({ + phone: '+25475525741', + parent: { _id: clinic._id, parent: clinic.parent } + }); + + const report = pregnancyFactory.build({ + contact: chwContact, + fields: { patient_id: person._id, case_id: 'case-12' } + }); + + before(async () => { + await utils.saveDocs([...places.values(), person, chwContact, report]); + await utils.createUsers([onlineUser]); + await loginPage.login(onlineUser); + }); + + it('should display option to send message', async () => { + await commonElements.goToReports(); + const firstReport = await reportsPage.leftPanelSelectors.firstReport(); + await reportsPage.openSelectedReport(firstReport); + + expect(await commonElements.isReportActionDisplayed()).to.equal(true); + expect(await commonElements.reportsFastActionFAB().getAttribute('class')) + .to.include('fa-envelope'); + }); +}); diff --git a/tests/e2e/default-mobile/wdio.conf.js b/tests/e2e/default-mobile/wdio.conf.js index d096166094e..2f7148c262c 100644 --- a/tests/e2e/default-mobile/wdio.conf.js +++ b/tests/e2e/default-mobile/wdio.conf.js @@ -10,7 +10,7 @@ exports.config = Object.assign(wdioBaseConfig.config, { './**/*.wdio-spec.js', '../default/login/login-logout.wdio-spec.js', '../default/navigation/navigation.wdio-spec.js', - '../default/navigation/hamburger-menu.wdio-spec.js', + '../default/reports/delete.wdio-spec.js', ] }, beforeSuite: async () => { diff --git a/tests/e2e/default/contacts/fab-actionbar.wdio-spec.js b/tests/e2e/default/contacts/fab-actionbar.wdio-spec.js deleted file mode 100644 index a2a546d6edd..00000000000 --- a/tests/e2e/default/contacts/fab-actionbar.wdio-spec.js +++ /dev/null @@ -1,91 +0,0 @@ -const placeFactory = require('@factories/cht/contacts/place'); -const userFactory = require('@factories/cht/users/users'); -const personFactory = require('@factories/cht/contacts/person'); -const utils = require('@utils'); -const loginPage = require('@page-objects/default/login/login.wdio.page'); -const commonElements = require('@page-objects/default/common/common.wdio.page'); -const { genericForm } = require('@page-objects/default/contacts/contacts.wdio.page'); -const commonPage = require('@page-objects/default/common/common.wdio.page'); - -describe('FAB + Actionbar', () => { - const places = placeFactory.generateHierarchy(); - const healthCenter = places.get('health_center'); - const onlineUser = userFactory.build({ place: healthCenter._id, roles: [ 'program_officer' ] }); - const patient = personFactory.build({ parent: { _id: healthCenter._id, parent: healthCenter.parent } }); - - before(async () => { - await utils.saveDocs([ ...places.values(), patient ]); - await utils.createUsers([ onlineUser ]); - await loginPage.login(onlineUser); - }); - - afterEach(async () => { - await browser.refresh(); - await commonPage.waitForPageLoaded(); - await utils.revertSettings(false); - }); - - describe('FAB', () => { - it('should show new household and new person create option', async () => { - await commonElements.goToPeople(healthCenter._id); - const fabLabels = await commonElements.getFastActionItemsLabels(); - - expect(fabLabels).to.include.members(['New household', 'New person']); - }); - - it('should show fab when user only has can_create_places permission', async () => { - await utils.updatePermissions(onlineUser.roles, [], ['can_create_people']); - await commonElements.goToPeople(healthCenter._id); - - await commonElements.clickFastActionFAB({ waitForList: false }); - const formTitle = await genericForm.getFormTitle(); - expect(formTitle).to.equal('New household'); - }); - - it('should show fab when user only has can_create_people permission', async () => { - await utils.updatePermissions(onlineUser.roles, [], ['can_create_places']); - await commonElements.goToPeople(healthCenter._id); - - await commonElements.clickFastActionFAB({ waitForList: false }); - const formTitle = await genericForm.getFormTitle(); - expect(formTitle).to.equal('New person'); - }); - }); - - describe('Action bar', () => { - it('should show new household and new person create option', async () => { - await utils.updatePermissions(onlineUser.roles, ['can_view_old_action_bar']); - await commonElements.goToPeople(healthCenter._id); - const actionBarLabels = await commonElements.getActionBarLabels(); - - expect(actionBarLabels).to.have.members([ - 'New household', - 'New person', - 'New action', - ]); - }); - - it('should not show new person when missing permission', async () => { - await utils.updatePermissions(onlineUser.roles, ['can_view_old_action_bar'], ['can_create_people']); - await commonElements.goToPeople(healthCenter._id); - const actionBarLabels = await commonElements.getActionBarLabels(); - - expect(actionBarLabels).to.have.members([ - 'New household', - 'New action', - ]); - }); - - it('should not show new place when missing permission', async () => { - await utils.updatePermissions(onlineUser.roles, ['can_view_old_action_bar'], ['can_create_places']); - await commonElements.goToPeople(healthCenter._id); - const actionBarLabels = await commonElements.getActionBarLabels(); - - expect(actionBarLabels).to.have.members([ - 'New person', - 'New action', - ]); - }); - }); - -}); diff --git a/tests/e2e/default/contacts/fab-actions.wdio-spec.js b/tests/e2e/default/contacts/fab-actions.wdio-spec.js new file mode 100644 index 00000000000..1a803fc0678 --- /dev/null +++ b/tests/e2e/default/contacts/fab-actions.wdio-spec.js @@ -0,0 +1,52 @@ +const placeFactory = require('@factories/cht/contacts/place'); +const userFactory = require('@factories/cht/users/users'); +const personFactory = require('@factories/cht/contacts/person'); +const utils = require('@utils'); +const loginPage = require('@page-objects/default/login/login.wdio.page'); +const commonElements = require('@page-objects/default/common/common.wdio.page'); +const { genericForm } = require('@page-objects/default/contacts/contacts.wdio.page'); +const commonPage = require('@page-objects/default/common/common.wdio.page'); + +describe('FAB actions', () => { + const places = placeFactory.generateHierarchy(); + const healthCenter = places.get('health_center'); + const onlineUser = userFactory.build({ place: healthCenter._id, roles: [ 'program_officer' ] }); + const patient = personFactory.build({ parent: { _id: healthCenter._id, parent: healthCenter.parent } }); + + before(async () => { + await utils.saveDocs([ ...places.values(), patient ]); + await utils.createUsers([ onlineUser ]); + await loginPage.login(onlineUser); + }); + + afterEach(async () => { + await browser.refresh(); + await commonPage.waitForPageLoaded(); + await utils.revertSettings(false); + }); + + it('should show new household and new person create option', async () => { + await commonElements.goToPeople(healthCenter._id); + const fabLabels = await commonElements.getFastActionItemsLabels(); + + expect(fabLabels).to.include.members(['New household', 'New person']); + }); + + it('should show fab when user only has can_create_places permission', async () => { + await utils.updatePermissions(onlineUser.roles, [], ['can_create_people']); + await commonElements.goToPeople(healthCenter._id); + + await commonElements.clickFastActionFAB({ waitForList: false }); + const formTitle = await genericForm.getFormTitle(); + expect(formTitle).to.equal('New household'); + }); + + it('should show fab when user only has can_create_people permission', async () => { + await utils.updatePermissions(onlineUser.roles, [], ['can_create_places']); + await commonElements.goToPeople(healthCenter._id); + + await commonElements.clickFastActionFAB({ waitForList: false }); + const formTitle = await genericForm.getFormTitle(); + expect(formTitle).to.equal('New person'); + }); +}); diff --git a/tests/e2e/default/contacts/muting-contact-using-form.wdio-spec.js b/tests/e2e/default/contacts/muting-contact-using-form.wdio-spec.js index 327d59b7573..a9f14a036d0 100644 --- a/tests/e2e/default/contacts/muting-contact-using-form.wdio-spec.js +++ b/tests/e2e/default/contacts/muting-contact-using-form.wdio-spec.js @@ -76,5 +76,4 @@ describe('Mute/Unmute contacts using a specific form - ', () => { await modalPage.cancel(); }); - }); diff --git a/tests/e2e/default/db/initial-replication.wdio-spec.js b/tests/e2e/default/db/initial-replication.wdio-spec.js index 4d6664577eb..4acb8bfd82b 100644 --- a/tests/e2e/default/db/initial-replication.wdio-spec.js +++ b/tests/e2e/default/db/initial-replication.wdio-spec.js @@ -150,7 +150,7 @@ describe('initial-replication', () => { }); it('should support "disconnects"', async () => { - loginPage.login({ ...userAllowedDocs.user, loadPage: false }); + await loginPage.login({ ...userAllowedDocs.user, loadPage: false }); setTimeout(() => browser.refresh(), 1000); setTimeout(() => browser.refresh(), 3000); setTimeout(() => browser.refresh(), 5000); diff --git a/tests/e2e/default/db/ongoing-replication.wdio-spec.js b/tests/e2e/default/db/ongoing-replication.wdio-spec.js index cafcfa7a02b..d8f4a4685b5 100644 --- a/tests/e2e/default/db/ongoing-replication.wdio-spec.js +++ b/tests/e2e/default/db/ongoing-replication.wdio-spec.js @@ -42,7 +42,7 @@ describe('ongoing replication', function() { const isRevertingSettings = utils.revertSettings(true); if (isRevertingSettings) { await isRevertingSettings; - await commonPage.sync(true); + await commonPage.sync(true, 30000); } }); diff --git a/tests/e2e/default/navigation/navigation.wdio-spec.js b/tests/e2e/default/navigation/navigation.wdio-spec.js index 00f93282ce7..e2d3b509433 100644 --- a/tests/e2e/default/navigation/navigation.wdio-spec.js +++ b/tests/e2e/default/navigation/navigation.wdio-spec.js @@ -11,7 +11,7 @@ describe('Navigation tests', () => { }); after(async () => { - await commonPage.logout(); + await browser.deleteCookies(); }); it('should open Messages tab', async () => { @@ -50,8 +50,9 @@ describe('Navigation tests', () => { before(async () => { await loginPage.cookieLogin(); }); + after(async () => { - await commonPage.logout(); + await browser.deleteCookies(); }); it('should display tab labels, when all tabs are enabled', async () => { diff --git a/tests/e2e/default/old-navigation/old-navigation.wdio-spec.js b/tests/e2e/default/old-navigation/old-navigation.wdio-spec.js new file mode 100644 index 00000000000..ae392cdd0b5 --- /dev/null +++ b/tests/e2e/default/old-navigation/old-navigation.wdio-spec.js @@ -0,0 +1,103 @@ +const utils = require('@utils'); +const placeFactory = require('@factories/cht/contacts/place'); +const userFactory = require('@factories/cht/users/users'); +const personFactory = require('@factories/cht/contacts/person'); +const pregnancyFactory = require('@factories/cht/reports/pregnancy'); +const loginPage = require('@page-objects/default/login/login.wdio.page'); +const oldNavigationPage = require('@page-objects/default/old-navigation/old-navigation.wdio.page'); +const messagesPage = require('@page-objects/default/sms/messages.wdio.page'); +const taskPage = require('@page-objects/default/tasks/tasks.wdio.page'); +const genericForm = require('@page-objects/default/enketo/generic-form.wdio.page'); +const reportsPage = require('@page-objects/default/reports/reports.wdio.page'); +const contactPage = require('@page-objects/default/contacts/contacts.wdio.page'); +const targetAggregatesPage = require('@page-objects/default/targets/target-aggregates.wdio.page'); + +describe('Old Navigation', () => { + const places = placeFactory.generateHierarchy(); + const healthCenter = places.get('health_center'); + + const offlineUser = userFactory.build({ place: healthCenter._id }); + + const person = personFactory.build({ + phone: '+50689999999', + parent: { _id: healthCenter._id, parent: healthCenter.parent } + }); + + const pregnancyReport = pregnancyFactory.build({ + contact: offlineUser.contact, + fields: { patient_id: person._id } + }); + + const targetsConfig = [{ id: 'test_target', type: 'count', title: 'Test target', aggregate: true }]; + + before(async () => { + await utils.saveDocs([...places.values(), person, pregnancyReport]); + await utils.createUsers([offlineUser]); + + const settings = await utils.getSettings(); + const tasks = settings.tasks; + tasks.targets.items = targetsConfig; + const permissions = settings.permissions; + permissions.can_aggregate_targets = offlineUser.roles; + permissions.can_view_old_navigation = offlineUser.roles; + await utils.updateSettings({ tasks, permissions }, true); + + await loginPage.login({ ...offlineUser, loadPage: false }); + await oldNavigationPage.waitForPageLoaded(); + }); + + after(async () => { + await utils.revertSettings(true); + await utils.deleteUsers([offlineUser]); + }); + + it('should navigate to the Messages section and open a sent message', async () => { + const message = 'Navigations test'; + await oldNavigationPage.goToMessages(); + await messagesPage.sendMessage(message, person.name, person.phone ); + await messagesPage.openMessage(person._id); + + const { name, phone } = await messagesPage.getMessageHeader(); + expect(name).to.equal(person.name); + expect(phone).to.equal(person.phone); + + const messages = await messagesPage.getAmountOfMessagesByPhone(); + const { content, state } = await messagesPage.getMessageContent(messages); + expect(content).to.equal(message); + expect(state).to.equal('pending'); + }); + + it('should navigate to the Task section and open the first task listed', async () => { + await oldNavigationPage.goToTasks(); + await taskPage.openTaskById( + pregnancyReport._id, + '~pregnancy-danger-sign-follow-up~anc.pregnancy_danger_sign_followup' + ); + expect(await genericForm.getFormTitle()).to.equal('Pregnancy danger sign follow-up'); + }); + + it('should navigate to the Reports section and open the first report listed', async () => { + await oldNavigationPage.goToReports(); + await reportsPage.openSelectedReport(await reportsPage.leftPanelSelectors.firstReport()); + await oldNavigationPage.waitForPageLoaded(); + const openReportInfo = await reportsPage.getOpenReportInfo(); + expect(openReportInfo.patientName).to.equal(person.name); + expect(openReportInfo.reportName).to.equal('Pregnancy registration'); + }); + + it('should navigate to the People section and open the created Health Center', async () => { + await oldNavigationPage.goToPeople(); + await contactPage.selectLHSRowByText(healthCenter.name); + expect(await contactPage.getContactInfoName()).to.equal(healthCenter.name); + }); + + it('should navigate to the Targets section, and open a target aggregate', async () => { + await oldNavigationPage.goToAnalytics(); + await targetAggregatesPage.goToTargetAggregates(true); + await targetAggregatesPage.openTargetDetails(targetsConfig[0]); + }); + + it('should successfully sync', async () => { + await oldNavigationPage.sync(); + }); +}); diff --git a/tests/e2e/default/reports/old-filter-by-date.wdio-spec.js b/tests/e2e/default/reports/old-filter-by-date.wdio-spec.js deleted file mode 100644 index 99c53eca299..00000000000 --- a/tests/e2e/default/reports/old-filter-by-date.wdio-spec.js +++ /dev/null @@ -1,76 +0,0 @@ -const moment = require('moment'); -const utils = require('@utils'); -const commonElements = require('@page-objects/default/common/common.wdio.page'); -const reportsTab = require('@page-objects/default/reports/reports.wdio.page'); -const loginPage = require('@page-objects/default/login/login.wdio.page'); -const userFactory = require('@factories/cht/users/users'); -const placeFactory = require('@factories/cht/contacts/place'); -const personFactory = require('@factories/cht/contacts/person'); -const reportFactory = require('@factories/cht/reports/generic-report'); - -describe('Report Filter', () => { - let savedReports; - const parent = placeFactory.place().build({ _id: 'dist1', type: 'district_hospital' }); - const user = userFactory.build(); - const patient = personFactory.build({ parent: { _id: user.place._id, parent: { _id: parent._id } } }); - - const reports = [ - // one registration half an hour before the start date - reportFactory.report().build( - { form: 'P', reported_date: moment([2016, 4, 15, 23, 30]).valueOf() }, - { patient, submitter: user.contact, fields: { lmp_date: 'Feb 3, 2016' } } - ), - // one registration half an hour after the start date - reportFactory.report().build( - { form: 'P', reported_date: moment([2016, 4, 16, 0, 30]).valueOf() }, - { patient, submitter: user.contact, fields: { lmp_date: 'Feb 15, 2016' } } - ), - // one visit half an hour after the end date - reportFactory.report().build( - { form: 'V', reported_date: moment([2016, 4, 18, 0, 30]).valueOf() }, - { patient, submitter: user.contact, fields: { ok: 'Yes!' } } - ), - // one visit half an hour before the end date - reportFactory.report().build( - { form: 'V', reported_date: moment([2016, 4, 17, 23, 30]).valueOf() }, - { patient, submitter: user.contact, fields: { ok: 'Yes!' } } - ), - ]; - - beforeEach(async () => { - const settings = await utils.getSettings(); - const permissions = { - ...settings.permissions, - can_view_old_filter_and_search: [ 'chw' ] - }; - await utils.updateSettings({ permissions }, { ignoreReload: true }); - - const results = await utils.saveDocs([ parent, patient, ...reports ]); - results.splice(0, 2); // Keeping only reports - savedReports = results.map(result => result.id); - - await utils.createUsers([ user ]); - await loginPage.login(user); - await commonElements.waitForPageLoaded(); - await commonElements.goToReports(); - }); - - after(async () => { - await utils.deleteUsers([user]); - await utils.revertDb([/^form:/], true); - await utils.revertSettings(true); - }); - - it('should filter by date using the old filter and search', async () => { - await (await reportsTab.leftPanelSelectors.firstReport()).waitForDisplayed(); - - await reportsTab.filterByDate(moment('05/16/2016', 'MM/DD/YYYY'), moment('05/17/2016', 'MM/DD/YYYY')); - await commonElements.waitForPageLoaded(); - const allReports = await reportsTab.leftPanelSelectors.allReports(); - - expect(allReports.length).to.equal(2); - expect(await (await reportsTab.leftPanelSelectors.reportByUUID(savedReports[1])).isDisplayed()).to.be.true; - expect(await (await reportsTab.leftPanelSelectors.reportByUUID(savedReports[3])).isDisplayed()).to.be.true; - }); -}); - diff --git a/tests/e2e/default/reports/sidebar-filter.wdio-spec.js b/tests/e2e/default/reports/sidebar-filter.wdio-spec.js index 2b405b8d367..5600e6f79fc 100644 --- a/tests/e2e/default/reports/sidebar-filter.wdio-spec.js +++ b/tests/e2e/default/reports/sidebar-filter.wdio-spec.js @@ -1,4 +1,5 @@ const moment = require('moment'); + const utils = require('@utils'); const commonPage = require('@page-objects/default/common/common.wdio.page'); const reportsPage = require('@page-objects/default/reports/reports.wdio.page'); @@ -9,7 +10,6 @@ const personFactory = require('@factories/cht/contacts/person'); const reportFactory = require('@factories/cht/reports/generic-report'); describe('Reports Sidebar Filter', () => { - let savedReports; const places = placeFactory.generateHierarchy(); const districtHospital = places.get('district_hospital'); @@ -31,49 +31,57 @@ describe('Reports Sidebar Filter', () => { const patient = personFactory.build({ parent: healthCenterUser }); const today = moment(); + const pregnancyHealthCenter = reportFactory + .report() + .build( + { + form: 'pregnancy', + verified: true, + reported_date: moment([today.year(), today.month(), 1, 23, 30]).subtract(4, 'month').valueOf() + }, + { patient, submitter: healthCenterContact, fields: { lmp_date: 'Feb 3, 2022' } } + ); + const pregnancyDistrictHospital = reportFactory + .report() + .build( + { + form: 'pregnancy', + reported_date: moment([today.year(), today.month(), 12, 10, 30]).subtract(1, 'month').valueOf() + }, + { patient, submitter: districtHospitalContact, fields: { lmp_date: 'Feb 16, 2022' } } + ); + const visitHealthCenter = reportFactory + .report() + .build( + { + form: 'pregnancy_home_visit', + verified: false, + reported_date: moment([today.year(), today.month(), 15, 0, 30]).subtract(5, 'month').valueOf() + }, + { patient, submitter: healthCenterContact, fields: { ok: 'Yes!' } } + ); + const visitDistrictHospital = reportFactory + .report() + .build( + { + form: 'pregnancy_home_visit', + verified: true, + reported_date: moment([today.year(), today.month(), 16, 9, 10]).subtract(1, 'month').valueOf() + }, + { patient, submitter: districtHospitalContact, fields: { ok: 'Yes!' } } + ); const reports = [ - reportFactory - .report() - .build( - { - form: 'P', - reported_date: moment([today.year(), today.month(), 1, 23, 30]).subtract(4, 'month').valueOf() - }, - { patient, submitter: healthCenterContact, fields: { lmp_date: 'Feb 3, 2022' }} - ), - reportFactory - .report() - .build( - { - form: 'P', - reported_date: moment([today.year(), today.month(), 12, 10, 30]).subtract(1, 'month').valueOf() - }, - { patient, submitter: districtHospitalContact, fields: { lmp_date: 'Feb 16, 2022' }} - ), - reportFactory - .report() - .build( - { - form: 'V', - reported_date: moment([today.year(), today.month(), 15, 0, 30]).subtract(5, 'month').valueOf() - }, - { patient, submitter: healthCenterContact, fields: { ok: 'Yes!' }} - ), - reportFactory - .report() - .build( - { - form: 'V', - reported_date: moment([today.year(), today.month(), 16, 9, 10]).subtract(1, 'month').valueOf() - }, - { patient, submitter: districtHospitalContact, fields: { ok: 'Yes!' }} - ), + pregnancyHealthCenter, + pregnancyDistrictHospital, + visitHealthCenter, + visitDistrictHospital ]; before(async () => { await utils.saveDocs([ ...places.values(), patient ]); - savedReports = (await utils.saveDocs(reports)).map(result => result.id); await utils.createUsers([ districtHospitalUser, healthCenterUser ]); + const savedReports = await utils.saveDocs(reports); + savedReports.forEach((savedReport, index) => reports[index]._id = savedReport.id); }); afterEach(async () => { @@ -84,16 +92,13 @@ describe('Reports Sidebar Filter', () => { after(async () => await utils.deleteUsers([ districtHospitalUser, healthCenterUser ])); it('should filter by date', async () => { - const pregnancyDistrictHospital = savedReports[1]; - const visitDistrictHospital = savedReports[3]; - await loginPage.login(districtHospitalUser); await commonPage.waitForPageLoaded(); await commonPage.goToReports(); await (await reportsPage.leftPanelSelectors.firstReport()).waitForDisplayed(); - expect((await reportsPage.leftPanelSelectors.allReports()).length).to.equal(savedReports.length); - + expect((await reportsPage.leftPanelSelectors.allReports()).length).to.equal(reports.length); + await reportsPage.openSidebarFilter(); await reportsPage.openSidebarFilterDateAccordion(); await reportsPage.setSidebarFilterFromDate(); @@ -101,15 +106,68 @@ describe('Reports Sidebar Filter', () => { await commonPage.waitForPageLoaded(); expect((await reportsPage.leftPanelSelectors.allReports()).length).to.equal(2); - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(pregnancyDistrictHospital)) + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(pregnancyDistrictHospital._id)) + .isDisplayed()).to.be.true; + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(visitDistrictHospital._id)) .isDisplayed()).to.be.true; - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(visitDistrictHospital)).isDisplayed()).to.be.true; }); - it('should filter by user associated place when the permission to default filter is enabled', async () => { - const pregnancyHealthCenter = savedReports[0]; - const visitHealthCenter = savedReports[2]; + it('should filter by form', async () => { + await loginPage.login(districtHospitalUser); + await commonPage.waitForPageLoaded(); + + await commonPage.goToReports(); + await (await reportsPage.leftPanelSelectors.firstReport()).waitForDisplayed(); + expect((await reportsPage.leftPanelSelectors.allReports()).length).to.equal(reports.length); + + await reportsPage.openSidebarFilter(); + await reportsPage.filterByForm('Pregnancy home visit'); + await commonPage.waitForPageLoaded(); + + expect((await reportsPage.leftPanelSelectors.allReports()).length).to.equal(2); + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(visitHealthCenter._id)).isDisplayed()).to.be.true; + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(visitDistrictHospital._id)) + .isDisplayed()).to.be.true; + }); + + it('should filter by place', async () => { + await loginPage.login(districtHospitalUser); + await commonPage.waitForPageLoaded(); + + await commonPage.goToReports(); + await (await reportsPage.leftPanelSelectors.firstReport()).waitForDisplayed(); + expect((await reportsPage.leftPanelSelectors.allReports()).length).to.equal(reports.length); + await reportsPage.openSidebarFilter(); + await reportsPage.filterByFacility(districtHospital.name, healthCenter.name); + await commonPage.waitForPageLoaded(); + + expect((await reportsPage.leftPanelSelectors.allReports()).length).to.equal(2); + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(visitHealthCenter._id)).isDisplayed()).to.be.true; + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(pregnancyHealthCenter._id)) + .isDisplayed()).to.be.true; + }); + + it('should filter by status', async () => { + await loginPage.login(districtHospitalUser); + await commonPage.waitForPageLoaded(); + + await commonPage.goToReports(); + await (await reportsPage.leftPanelSelectors.firstReport()).waitForDisplayed(); + expect((await reportsPage.leftPanelSelectors.allReports()).length).to.equal(reports.length); + + await reportsPage.openSidebarFilter(); + await reportsPage.filterByStatus('Reviewed: correct'); + await commonPage.waitForPageLoaded(); + + expect((await reportsPage.leftPanelSelectors.allReports()).length).to.equal(2); + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(pregnancyHealthCenter._id)) + .isDisplayed()).to.be.true; + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(visitDistrictHospital._id)) + .isDisplayed()).to.be.true; + }); + + it('should filter by user associated place when the permission to default filter is enabled', async () => { await loginPage.login(healthCenterUser); await commonPage.waitForPageLoaded(); @@ -123,8 +181,9 @@ describe('Reports Sidebar Filter', () => { await (await reportsPage.leftPanelSelectors.firstReport()).waitForDisplayed(); expect((await reportsPage.leftPanelSelectors.allReports()).length).to.equal(2); - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(pregnancyHealthCenter)).isDisplayed()).to.be.true; - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(visitHealthCenter)).isDisplayed()).to.be.true; + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(pregnancyHealthCenter._id)) + .isDisplayed()).to.be.true; + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(visitHealthCenter._id)).isDisplayed()).to.be.true; }); }); diff --git a/tests/e2e/default/service-worker/service-worker.wdio-spec.js b/tests/e2e/default/service-worker/service-worker.wdio-spec.js index 1cf0fcabcd2..2a34ff7ceb4 100644 --- a/tests/e2e/default/service-worker/service-worker.wdio-spec.js +++ b/tests/e2e/default/service-worker/service-worker.wdio-spec.js @@ -89,6 +89,11 @@ const loginIfNeeded = async () => { }; describe('Service worker cache', () => { + const DEFAULT_TRANSLATIONS = { + 'sync.now': 'Sync now', + 'sidebar_menu.title': 'Menu', + 'sync.status.not_required': 'All reports synced', + }; before(async () => { await utils.saveDoc(district); @@ -111,34 +116,37 @@ describe('Service worker cache', () => { expect(cacheDetails.urls.sort()).to.have.members([ '/', '/audio/alert.mp3', + '/deploy-info.json', '/extension-libs', '/fontawesome-webfont.woff2', '/fonts/NotoSans-Bold.ttf', '/fonts/NotoSans-Regular.ttf', '/fonts/enketo-icons-v2.woff', '/img/cht-logo-light.png', - '/img/icon.png', '/img/icon-chw-selected.svg', '/img/icon-chw.svg', + '/img/icon-close.svg', '/img/icon-nurse-selected.svg', '/img/icon-nurse.svg', '/img/icon-pregnant-selected.svg', '/img/icon-pregnant.svg', + '/img/icon-filter.svg', + '/img/icon.png', + '/img/icon-back.svg', '/img/layers.png', + '/login/images/hide-password.svg', + '/login/images/show-password.svg', '/login/lib-bowser.js', '/login/script.js', '/login/style.css', - '/login/images/hide-password.svg', - '/login/images/show-password.svg', '/main.js', - '/deploy-info.json', '/manifest.json', '/medic/_design/medic/_rewrite/', '/medic/login', '/polyfills.js', '/runtime.js', '/scripts.js', - '/styles.css' + '/styles.css', ].sort()); }); @@ -178,6 +186,7 @@ describe('Service worker cache', () => { const waitForLogs = await utils.waitForApiLogs(utils.SW_SUCCESSFUL_REGEX); await utils.addTranslations(languageCode, { + ...DEFAULT_TRANSLATIONS, 'User Name': 'Utilizator', 'Password': 'Parola', 'login': 'Autentificare', @@ -203,6 +212,7 @@ describe('Service worker cache', () => { const waitForLogs = await utils.waitForApiLogs(utils.SW_SUCCESSFUL_REGEX); await utils.addTranslations('en', { + ...DEFAULT_TRANSLATIONS, 'ran': 'dom', 'some': 'thing', }); diff --git a/tests/e2e/default/suites.js b/tests/e2e/default/suites.js index 47be3b6ad09..7514c73f9b1 100644 --- a/tests/e2e/default/suites.js +++ b/tests/e2e/default/suites.js @@ -7,6 +7,7 @@ const SUITES = { './users/**/*.wdio-spec.js', './about/**/*.wdio-spec.js', './navigation/**/*.wdio-spec.js', + './old-navigation/**/*.wdio-spec.js', './privacy-policy/**/*.wdio-spec.js', ], workflows: [ @@ -34,4 +35,3 @@ const SUITES = { }; exports.suites = SUITES; - diff --git a/tests/e2e/default/transitions/create-user-for-contacts.replace-user.wdio-spec.js b/tests/e2e/default/transitions/create-user-for-contacts.replace-user.wdio-spec.js index aa8e6517d02..213d1e64ee0 100644 --- a/tests/e2e/default/transitions/create-user-for-contacts.replace-user.wdio-spec.js +++ b/tests/e2e/default/transitions/create-user-for-contacts.replace-user.wdio-spec.js @@ -334,8 +334,8 @@ describe('Create user for contacts', () => { expect(cookie.value).to.include(newUserSettings.name); }); - // eslint-disable-next-line max-len - it('does not assign new person as primary contact of parent place if original person was not primary', async () => { + it('does not assign new person as primary contact of parent place ' + + 'if original person was not primary', async () => { await utils.updateSettings(SETTINGS, {ignoreReload: 'sentinel'}); const district = await utils.getDoc(DISTRICT._id); district.contact = { _id: 'not-the-original-contact' }; @@ -386,7 +386,8 @@ describe('Create user for contacts', () => { expect(cookie.value).to.include(newUserSettings.name); }); - it('creates new user from latest replace_user form data if multiple are submitted before syncing', async () => { + it('creates new user from latest replace_user form data ' + + 'if multiple are submitted before syncing', async () => { await utils.updateSettings(SETTINGS, {ignoreReload: 'sentinel'}); await loginAsOfflineUser(); const originalContactId = ORIGINAL_USER.contact._id; @@ -482,8 +483,8 @@ describe('Create user for contacts', () => { basicReportsFromRemote.forEach((report, index) => expect(report).to.deep.equal(basicReports[index])); }); - // eslint-disable-next-line max-len - it('creates new user when replace_user form is submitted for contact associated with multiple users', async () => { + it('creates new user when replace_user form is submitted ' + + 'for contact associated with multiple users', async () => { await utils.updateSettings(SETTINGS, {ignoreReload: 'sentinel'}); await loginAsOfflineUser(); @@ -549,8 +550,8 @@ describe('Create user for contacts', () => { await commonPage.goToPeople(originalContactId); }); - // eslint-disable-next-line max-len - it('creates new user for the first version of a contact to sync and conflicting replacements ignored', async () => { + it('creates new user for the first version of a contact ' + + 'to sync and conflicting replacements ignored', async () => { await utils.updateSettings(SETTINGS, {ignoreReload: 'sentinel'}); await loginAsOfflineUser(); const originalContactId = ORIGINAL_USER.contact._id; diff --git a/tests/e2e/default/translations/new-language.wdio-spec.js b/tests/e2e/default/translations/new-language.wdio-spec.js index 88b8971bc9f..f184b53de10 100644 --- a/tests/e2e/default/translations/new-language.wdio-spec.js +++ b/tests/e2e/default/translations/new-language.wdio-spec.js @@ -54,12 +54,9 @@ describe('Adding new language', () => { await commonPage.goToBase(); await userSettingsElements.setLanguage(ENG_LANG_CODE); - // Add new translations await addTranslations(NEW_LANG_CODE, NEW_TRANSLATIONS); - - // Change user language + await commonPage.goToBase(); await userSettingsElements.setLanguage(NEW_LANG_CODE); - await browser.waitUntil(async () => await (await commonPage.analyticsTab()).getText() === 'Analytiks'); // Check for translations in the UI diff --git a/tests/e2e/upgrade/upgrade.wdio-spec.js b/tests/e2e/upgrade/upgrade.wdio-spec.js index 8a056fdcb95..efb0e1c6174 100644 --- a/tests/e2e/upgrade/upgrade.wdio-spec.js +++ b/tests/e2e/upgrade/upgrade.wdio-spec.js @@ -6,6 +6,7 @@ const upgradePage = require('@page-objects/upgrade/upgrade.wdio.page'); const commonPage = require('@page-objects/default/common/common.wdio.page'); const adminPage = require('@page-objects/default/admin/admin.wdio.page'); const aboutPage = require('@page-objects/default/about/about.wdio.page'); +const oldNavigationPage = require('@page-objects/default/old-navigation/old-navigation.wdio.page'); const constants = require('@constants'); const version = require('../../../scripts/build/versions'); const dataFactory = require('@factories/cht/generate'); @@ -21,7 +22,6 @@ describe('Performing an upgrade', () => { nbrPersons: 1, }); - const getDdocs = async () => { const result = await utils.requestOnMedicDb({ path: '/_all_docs', @@ -57,15 +57,13 @@ describe('Performing an upgrade', () => { if (testFrontend) { // a variety of selectors that we use in e2e tests to interact with webapp // are not compatible with older versions of the app. - await loginPage.login(docs.user); - await commonPage.logout(); + await loginPage.login({ username: docs.user.username, password: docs.user.password, loadPage: false }); + await oldNavigationPage.goToBase(); + await oldNavigationPage.logout(); } - await loginPage.cookieLogin({ - username: constants.USERNAME, - password: constants.PASSWORD, - createUser: false - }); + await loginPage.login({ username: constants.USERNAME, password: constants.PASSWORD, loadPage: false }); + await oldNavigationPage.goToBase(); }); after(async () => { @@ -124,8 +122,9 @@ describe('Performing an upgrade', () => { } await adminPage.logout(); - await loginPage.login(docs.user); - await commonPage.sync(true); + await loginPage.login({ username: docs.user.username, password: docs.user.password, loadPage: false }); + await oldNavigationPage.goToBase(); + await oldNavigationPage.sync(true); await browser.refresh(); await commonPage.waitForPageLoaded(); @@ -137,11 +136,9 @@ describe('Performing an upgrade', () => { }); it('should display upgrade page even without upgrade logs', async () => { - await loginPage.cookieLogin({ - username: constants.USERNAME, - password: constants.PASSWORD, - createUser: false - }); + if (testFrontend) { + await loginPage.login({ username: constants.USERNAME, password: constants.PASSWORD, adminApp: true }); + } await deleteUpgradeLogs(); diff --git a/tests/e2e/visual/contacts/list-view-login-visual.wdio-spec.js b/tests/e2e/visual/contacts/list-view-login-visual.wdio-spec.js new file mode 100644 index 00000000000..a6925ca6fd4 --- /dev/null +++ b/tests/e2e/visual/contacts/list-view-login-visual.wdio-spec.js @@ -0,0 +1,133 @@ +const commonPage = require('@page-objects/default/common/common.wdio.page'); +const contactPage = require('@page-objects/default/contacts/contacts.wdio.page'); +const loginPage = require('@page-objects/default/login/login.wdio.page'); +const dataFactory = require('@factories/cht/generate'); +const reportsPage = require('@page-objects/default/reports/reports.wdio.page'); +const utils = require('@utils'); + +const { resizeWindowForScreenshots, generateScreenshot } = require('@utils/screenshots'); + +describe('Contact List Page', () => { + const updateRolePermissions = async (roleValue, addPermissions, removePermissions = []) => { + const roles = [roleValue]; + const settings = await utils.getSettings(); + const permissions = await utils.getUpdatedPermissions(roles, addPermissions, removePermissions); + await utils.updateSettings( + { roles: settings.roles, permissions }, + { revert: true, ignoreReload: true, refresh: true, sync: true } + ); + }; + + const docs = dataFactory.createHierarchy({ + name: 'Janet Mwangi', + user: true, + nbrClinics: 10, + nbrPersons: 4, + useRealNames: true, + }); + + before(async () => { + await resizeWindowForScreenshots(); + await utils.saveDocs([...docs.places, ...docs.clinics, ...docs.persons, ...docs.reports]); + await utils.createUsers([docs.user]); + }); + + after(async () => { + await utils.deleteUsers([docs.user]); + await utils.revertDb([/^form:/], true); + }); + + beforeEach(async () => { + await loginPage.login(docs.user); + }); + + afterEach(async () => { + await commonPage.logout(); + }); + + describe('Log in', () => { + it('should show contacts page tab '+ + 'when can_view_contact and can_view_contacts_tab permissions are enabled', async () => { + await (await commonPage.contactsTab()).waitForDisplayed(); + await generateScreenshot('contact-page', 'tab-visible'); + await commonPage.openHamburgerMenu(); + await generateScreenshot('contact-page', 'menu-opened'); + await commonPage.closeHamburgerMenu(); + await commonPage.goToPeople(); + expect(await commonPage.isPeopleListPresent()).to.be.true; + await generateScreenshot('contact-page', 'people-list-visible'); + await commonPage.goToReports(); + await reportsPage.openFirstReport(); + await reportsPage.rightPanelSelectors.patientName().waitForClickable(); + await generateScreenshot('contact-page', 'reports-visible'); + await reportsPage.rightPanelSelectors.patientName().click(); + await contactPage.waitForContactLoaded(); + await generateScreenshot('contact-page', 'contact-loaded'); + await commonPage.goToMessages(); + }); + + it('should hide contacts page as tab and from menu option ' + + 'when can_view_contacts_tab permissions is enable but can_view_contact permission is not', async () => { + await updateRolePermissions('chw', [], ['can_view_contacts']); + await commonPage.waitForPageLoaded(); + await commonPage.goToMessages(); + await (await commonPage.contactsTab()).waitForDisplayed({ reverse: true }); + await generateScreenshot('contact-page', 'no-tab-visible'); + await commonPage.openHamburgerMenu(); + await (await commonPage.contactsButton()).waitForClickable({ reverse: true }); + await generateScreenshot('contact-page', 'no-menu-option'); + await commonPage.closeHamburgerMenu(); + await commonPage.goToReports(); + await reportsPage.openFirstReport(); + await (reportsPage.rightPanelSelectors.patientName()).waitForClickable(); + await generateScreenshot('contact-page', 'report-view-no-contacts'); + await reportsPage.rightPanelSelectors.patientName().click(); + await generateScreenshot('contact-page', 'report-no-contact-loaded'); + await commonPage.goToMessages(); + }); + + it('should hide contacts page as tab, show from menu option ' + + 'when can_view_contact permissions is enable but can_view_contact permission is not', async () => { + await updateRolePermissions('chw', ['can_view_contacts'], ['can_view_contacts_tab']); + await commonPage.waitForPageLoaded(); + await commonPage.goToMessages(); + await (await commonPage.contactsTab()).waitForDisplayed({ reverse: true }); + await generateScreenshot('contact-page', 'no-tab-visible'); + await commonPage.openHamburgerMenu(); + await (await commonPage.contactsButton()).waitForClickable(); + await generateScreenshot('contact-page', 'menu-option-visible'); + await (await commonPage.contactsButton()).click(); + expect(await commonPage.isPeopleListPresent()).to.be.true; + await commonPage.waitForPageLoaded(); + await generateScreenshot('contact-page', 'contacts-in-people-list'); + await commonPage.goToReports(); + await reportsPage.openFirstReport(); + await (reportsPage.rightPanelSelectors.patientName()).waitForClickable(); + await generateScreenshot('contact-page', 'report-view-with-contacts'); + await reportsPage.rightPanelSelectors.patientName().click(); + await contactPage.waitForContactLoaded(); + await generateScreenshot('contact-page', 'report-contact-loaded'); + await commonPage.goToMessages(); + }); + + it('should hide contacts page as a tab and from menu option ' + + 'when can_view_contact and can_view_contact permissions are disable', async () => { + await updateRolePermissions('chw', [], ['can_view_contacts_tab', 'can_view_contacts']); + await commonPage.waitForPageLoaded(); + await commonPage.goToMessages(); + await (await commonPage.contactsTab()).waitForDisplayed({ reverse: true }); + await generateScreenshot('contact-page', 'no-tab-visible-oPerms'); + await commonPage.openHamburgerMenu(); + await (await commonPage.contactsButton()).waitForClickable({ reverse: true }); + await generateScreenshot('contact-page', 'no-menu-option-no-Perms'); + await commonPage.closeHamburgerMenu(); + await commonPage.goToReports(); + await reportsPage.openFirstReport(); + await (reportsPage.rightPanelSelectors.patientName()).waitForClickable(); + await generateScreenshot('contact-page', 'report-view-no-contacts-no-perms'); + await reportsPage.rightPanelSelectors.patientName().click(); + await generateScreenshot('contact-page', 'report-no-contact-loaded-no-perms'); + await commonPage.goToMessages(); + }); + }); +}); diff --git a/tests/e2e/visual/wdio.conf.js b/tests/e2e/visual/wdio.conf.js new file mode 100644 index 00000000000..1ba9009f00a --- /dev/null +++ b/tests/e2e/visual/wdio.conf.js @@ -0,0 +1,36 @@ +const wdioBaseConfig = require('../../wdio.conf'); + +const chai = require('chai'); +chai.use(require('chai-exclude')); + +const mobileCapability = { + ...wdioBaseConfig.config.capabilities[0], + 'goog:chromeOptions': { + ...wdioBaseConfig.config.capabilities[0]['goog:chromeOptions'], + args: [ + ...wdioBaseConfig.config.capabilities[0]['goog:chromeOptions'].args, + 'window-size=450,700', + ], + }, +}; + +const desktopCapability = { + ...wdioBaseConfig.config.capabilities[0], + 'goog:chromeOptions': { + ...wdioBaseConfig.config.capabilities[0]['goog:chromeOptions'], + args: [ + ...wdioBaseConfig.config.capabilities[0]['goog:chromeOptions'].args, + 'window-size=1000,800', + ], + }, +}; + +exports.config = Object.assign(wdioBaseConfig.config, { + suites: { + all: [ + './**/*.wdio-spec.js', + ] + }, + capabilities: [mobileCapability, desktopCapability], + maxInstances: 1, +}); diff --git a/tests/factories/cht/generate.js b/tests/factories/cht/generate.js index 5fb43e46c9d..86ebbac7d13 100644 --- a/tests/factories/cht/generate.js +++ b/tests/factories/cht/generate.js @@ -4,14 +4,25 @@ const personFactory = require('@factories/cht/contacts/person'); const deliveryFactory = require('@factories/cht/reports/delivery'); const pregnancyFactory = require('@factories/cht/reports/pregnancy'); const pregnancyVisitFactory = require('@factories/cht/reports/pregnancy-visit'); + +// Fixed collection of real-world data +const FIRST_NAMES = ['Amanda', 'Beatrice', 'Dana', 'Fatima', 'Gina', 'Helen', 'Isabelle', 'Jessica', 'Ivy', 'Sara']; +const LAST_NAMES = ['Allen', 'Bass', 'Dearborn', 'Flair', 'Gorman', 'Hamburg', 'Ivanas', 'James', 'Moore', 'Taylor']; +const PHONE_NUMBERS = [ + '+256414345783', '+256414345784', '+256414345785', + '+256414345786', '+256414345787', '+256414345788', + '+256414345789', '+256414345790', '+256414345791', + '+256414345792' +]; +const PATIENT_IDS = [65421, 65422, 65423, 65424, 65425, 65426, 65427, 65428, 65429, 65430]; + const getReportContext = (patient, submitter) => { const context = { - fields: - { - patient_id: patient._id, - patient_uuid: patient._id, - patient_name: patient.name, - }, + fields: { + patient_id: patient._id, + patient_uuid: patient._id, + patient_name: patient.name, + }, }; if (submitter) { context.contact = { @@ -22,22 +33,20 @@ const getReportContext = (patient, submitter) => { return context; }; -const createData = ({ healthCenter, user, nbrClinics=10, nbrPersons=10 }) => { +const createDataWithFixedData = ({ healthCenter, user, nbrClinics = 10, nbrPersons = 10 }) => { const clinics = Array .from({ length: nbrClinics }) .map((_, idx) => placeFactory.place().build({ type: 'clinic', parent: { _id: healthCenter._id, parent: healthCenter.parent }, - name: `clinic_${idx}` + name: `clinic_${idx}`, })); const persons = [ - ...clinics.map(clinic => Array - .from({ length: nbrPersons }) - .map((_, idx) => personFactory.build({ - parent: { _id: clinic._id, parent: clinic.parent }, - name: `person_${clinic.name}_${idx}`, - }))), + ...clinics.map(clinic => Array.from({ length: nbrPersons }).map((_, idx) => personFactory.build({ + parent: { _id: clinic._id, parent: clinic.parent }, + name: `person_${clinic.name}_${idx}`, + }))), ].flat(); const reports = [ @@ -51,7 +60,81 @@ const createData = ({ healthCenter, user, nbrClinics=10, nbrPersons=10 }) => { return { clinics, reports, persons }; }; -const createHierarchy = ({ name, user=false, nbrClinics=50, nbrPersons=10 }) => { +const createClinic = (index, healthCenter) => { + const firstName = FIRST_NAMES[index % FIRST_NAMES.length]; + const lastName = LAST_NAMES[index % LAST_NAMES.length]; + const personName = `${firstName} ${lastName}`; + const personPhoneNumber = PHONE_NUMBERS[index % PHONE_NUMBERS.length]; + + const primaryContact = personFactory.build({ + name: personName, + phone: personPhoneNumber + }); + + const clinic = placeFactory.place().build({ + type: 'clinic', + parent: { _id: healthCenter._id, parent: healthCenter.parent }, + name: `${personName} Family`, + contact: primaryContact + }); + + primaryContact.parent = { _id: clinic._id, parent: clinic.parent }; + + return { clinic, primaryContact }; +}; + +const createAdditionalPersons = (nbrPersons, clinic) => { + return Array + .from({ length: nbrPersons - 1 }) + .map((_, i) => { + const additionalPersonName = `${FIRST_NAMES[i % FIRST_NAMES.length]} ${LAST_NAMES[i % LAST_NAMES.length]}`; + const additionalPhoneNumber = PHONE_NUMBERS[i % PHONE_NUMBERS.length]; + return personFactory.build({ + parent: { _id: clinic._id, parent: clinic.parent }, + name: additionalPersonName, + patient_id: PATIENT_IDS[i % PATIENT_IDS.length], + phone: additionalPhoneNumber + }); + }); +}; + +const createReportsForPerson = (person, user) => { + return [ + deliveryFactory.build(getReportContext(person, user)), + pregnancyFactory.build(getReportContext(person, user)), + pregnancyVisitFactory.build(getReportContext(person, user)) + ]; +}; + +const createDataWithRealNames = ({ healthCenter, user, nbrClinics = 10, nbrPersons = 10 }) => { + const clinicsData = Array + .from({ length: nbrClinics }) + .map((_, index) => { + const { clinic, primaryContact } = createClinic(index, healthCenter); + + const additionalPersons = createAdditionalPersons(nbrPersons, clinic); + + const allPersons = [primaryContact, ...additionalPersons]; + + return { clinic, persons: allPersons }; + }); + + const allPersons = clinicsData.flatMap(data => data.persons); + const clinicList = clinicsData.map(data => data.clinic); + + const reports = allPersons.flatMap(person => createReportsForPerson(person, user)); + + return { clinics: clinicList, reports, persons: allPersons }; +}; + +const createData = ({ healthCenter, user, nbrClinics, nbrPersons, useRealNames = false }) => { + if (useRealNames) { + return createDataWithRealNames({ healthCenter, user, nbrClinics, nbrPersons }); + } + return createDataWithFixedData({ healthCenter, user, nbrClinics, nbrPersons }); +}; + +const createHierarchy = ({ name, user = false, nbrClinics = 50, nbrPersons = 10, useRealNames = false }) => { const hierarchy = placeFactory.generateHierarchy(); const healthCenter = hierarchy.get('health_center'); user = user && userFactory.build({ place: healthCenter._id, roles: ['chw'] }); @@ -61,7 +144,8 @@ const createHierarchy = ({ name, user=false, nbrClinics=50, nbrPersons=10 }) => return place; }); - const { clinics, reports, persons } = createData({ healthCenter, nbrClinics, nbrPersons, user }); + healthCenter.name = `${name}'s Area`; + const { clinics, reports, persons } = createData({ healthCenter, nbrClinics, nbrPersons, user, useRealNames }); return { user, diff --git a/tests/integration/cht-form/default/draw-widget.wdio-spec.js b/tests/integration/cht-form/default/draw-widget.wdio-spec.js index 273fe8a116d..9fb387cbe17 100644 --- a/tests/integration/cht-form/default/draw-widget.wdio-spec.js +++ b/tests/integration/cht-form/default/draw-widget.wdio-spec.js @@ -28,6 +28,6 @@ describe('cht-form web component - Draw Widget', () => { expect(contentTypes).to.deep.equal(['image/png', 'image/png', 'image/png']); expect(doc._attachments[drawAttachmentName].data.size).to.be.closeTo(19600, 2000); expect(doc._attachments[signatureAttachmentName].data.size).to.be.closeTo(12800, 2000); - expect(doc._attachments[annotateAttachmentName].data.size).to.be.closeTo(29000, 2000); + expect(doc._attachments[annotateAttachmentName].data.size).to.be.closeTo(29000, 3000); }); }); diff --git a/tests/page-objects/default/about/about.wdio.page.js b/tests/page-objects/default/about/about.wdio.page.js index 4140ff7997d..f2baf0a89b0 100644 --- a/tests/page-objects/default/about/about.wdio.page.js +++ b/tests/page-objects/default/about/about.wdio.page.js @@ -1,7 +1,7 @@ const userName = () => $('label=User name'); const partners = () => $('.partners'); const version = () => $('[test-id="about-version"]'); -const aboutCard = () => $('div*=About'); +const aboutCard = () => $('mat-card-title*=About'); const RELOAD_BUTTON = '.about.page .mat-primary'; const getPartnerImage = async (name) => { diff --git a/tests/page-objects/default/common/common.wdio.page.js b/tests/page-objects/default/common/common.wdio.page.js index 75c23a55455..93a571c5228 100644 --- a/tests/page-objects/default/common/common.wdio.page.js +++ b/tests/page-objects/default/common/common.wdio.page.js @@ -2,11 +2,10 @@ const modalPage = require('./modal.wdio.page'); const constants = require('@constants'); const aboutPage = require('@page-objects/default/about/about.wdio.page'); -const hamburgerMenu = () => $('#header-dropdown-link'); -const userSettingsMenuOption = () => $('[test-id="user-settings-menu-option"]'); +const hamburgerMenu = () => $('aria/Application menu'); +const closeSideBarMenu = () => $('.panel-header-close'); const FAST_ACTION_TRIGGER = '.fast-action-trigger'; -const NOT_MOBILE_ONLY = 'mm-fast-action-button:not(.mobile-only)'; -const fastActionFAB = () => $(`${NOT_MOBILE_ONLY} ${FAST_ACTION_TRIGGER} .fast-action-fab-button`); +const fastActionFAB = () => $$(`${FAST_ACTION_TRIGGER} .fast-action-fab-button`); const fastActionFlat = () => $(`${FAST_ACTION_TRIGGER} .fast-action-flat-button`); const multipleActions = () => $(`${FAST_ACTION_TRIGGER}[test-id="multiple-actions-menu"]`); const FAST_ACTION_LIST_CONTAINER = '.fast-action-content-wrapper'; @@ -14,10 +13,10 @@ const fastActionListContainer = () => $(FAST_ACTION_LIST_CONTAINER); const fastActionListCloseButton = () => $(`${FAST_ACTION_LIST_CONTAINER} .panel-header .panel-header-close`); const fastActionById = (id) => $(`${FAST_ACTION_LIST_CONTAINER} .fast-action-item[test-id="${id}"]`); const fastActionItems = () => $$(`${FAST_ACTION_LIST_CONTAINER} .fast-action-item`); -const moreOptionsMenu = () => $('.more-options-menu-container>.mat-mdc-menu-trigger'); +const moreOptionsMenu = () => $('aria/Actions menu'); const hamburgerMenuItemSelector = '#header-dropdown li'; -const logoutButton = () => $(`${hamburgerMenuItemSelector} .fa-power-off`); -const syncButton = () => $(`${hamburgerMenuItemSelector} a:not(.disabled) .fa-refresh`); +const logoutButton = () => $('aria/Log out'); +const syncButton = () => $('aria/Sync now'); const messagesTab = () => $('#messages-tab'); const analyticsTab = () => $('#analytics-tab'); const taskTab = () => $('#tasks-tab'); @@ -26,13 +25,12 @@ const getMessagesButtonLabel = () => $('#messages-tab .button-label'); const getTasksButtonLabel = () => $('#tasks-tab .button-label'); const getAllButtonLabels = async () => await $$('.header .tabs .button-label'); const loaders = () => $$('.container-fluid .loader'); -const syncSuccess = () => $(`${hamburgerMenuItemSelector}.sync-status .success`); +const syncSuccess = () => $('aria/All reports synced'); const syncInProgress = () => $('*="Currently syncing"'); const syncRequired = () => $(`${hamburgerMenuItemSelector}.sync-status .required`); const jsonError = async () => (await $('pre')).getText(); - -const actionBar = () => $('.detail-actions.right-pane'); -const actionBarActions = () => $$('.detail-actions.right-pane span'); +const REPORTS_CONTENT_SELECTOR = '#reports-content'; +const reportsFastActionFAB = () => $(`${REPORTS_CONTENT_SELECTOR} .fast-action-fab-button mat-icon`); //languages const activeSnackbar = () => $('#snackbar.active'); @@ -41,22 +39,24 @@ const snackbar = () => $('#snackbar.active .snackbar-message'); const snackbarMessage = async () => (await $('#snackbar.active .snackbar-message')).getText(); const snackbarAction = () => $('#snackbar.active .snackbar-action'); -//Hamburguer menu +// Mobile +const mobileTopBarTitle = () => $('mm-navigation .ellipsis-title'); + //User settings -const USER_SETTINGS = '#header-dropdown a[routerlink="user"] i.fa-user'; +const USER_SETTINGS = 'aria/User settings'; const EDIT_PROFILE = '.user .configuration.page i.fa-user'; // Feedback or Report bug -const FEEDBACK_MENU = '#header-dropdown i.fa-bug'; +const feedbackMenuOption = () => $('aria/Report bug'); const FEEDBACK = '#feedback'; //About menu -const ABOUT_MENU = '#header-dropdown i.fa-question'; +const ABOUT_MENU = 'aria/About'; //Configuration App -const CONFIGURATION_APP_MENU = '#header-dropdown i.fa-cog'; - +const configurationAppMenuOption = () => $('aria/App Management'); const errorLog = () => $(`error-log`); +const sideBarMenuTitle = () => $('aria/Menu'); const isHamburgerMenuOpen = async () => { - return await (await $('.header .dropdown.open #header-dropdown-link')).isExisting(); + return await (await $('mat-sidenav-container.mat-drawer-container-has-open')).isExisting(); }; const openMoreOptionsMenu = async () => { @@ -78,12 +78,24 @@ const clickFastActionById = async (id) => { await (await fastActionById(id)).click(); }; +/** + * There are two FABs, one for desktop and another for mobile. This finds the visible FAB. + * @returns {Promise} + */ +const findVisibleFAB = async () => { + for (const button of await fastActionFAB()) { + if (await button.isDisplayed()) { + return button; + } + } +}; + const clickFastActionFAB = async ({ actionId, waitForList }) => { await closeHamburgerMenu(); - await (await fastActionFAB()).waitForDisplayed(); - await (await fastActionFAB()).waitForClickable(); + const fab = await findVisibleFAB(); + await fab.waitForClickable(); waitForList = waitForList === undefined ? await (await multipleActions()).isExisting() : waitForList; - await (await fastActionFAB()).click(); + await fab.click(); if (waitForList) { await clickFastActionById(actionId); } @@ -91,9 +103,9 @@ const clickFastActionFAB = async ({ actionId, waitForList }) => { const getFastActionItemsLabels = async () => { await closeHamburgerMenu(); - await (await fastActionFAB()).waitForDisplayed(); - await (await fastActionFAB()).waitForClickable(); - await (await fastActionFAB()).click(); + const fab = await findVisibleFAB(); + await fab.waitForClickable(); + await fab.click(); await browser.pause(500); await (await fastActionListContainer()).waitForDisplayed(); @@ -141,6 +153,17 @@ const closeFastActionList = async () => { await (await fastActionListCloseButton()).click(); }; +const isReportActionDisplayed = async () => { + return await browser.waitUntil(async () => { + const exists = await (await reportsFastActionFAB()).isExisting(); + if (exists) { + await (await reportsFastActionFAB()).waitForDisplayed(); + } + + return exists; + }); +}; + const isMessagesListPresent = () => { return isElementByIdPresent('message-list'); }; @@ -169,18 +192,30 @@ const isElementByIdPresent = async (elementId) => { return await (await $(`#${elementId}`)).isExisting(); }; +const getHeaderTitleOnMobile = async () => { + return { + name: await mobileTopBarTitle().getText(), + }; +}; + const openHamburgerMenu = async () => { if (!(await isHamburgerMenuOpen())) { await (await hamburgerMenu()).waitForClickable(); await (await hamburgerMenu()).click(); } + + // Adding pause here as we have to wait for sidebar nav menu animation to load + await browser.pause(500); + await (await sideBarMenuTitle()).waitForDisplayed(); }; const closeHamburgerMenu = async () => { if (await isHamburgerMenuOpen()) { - await (await hamburgerMenu()).waitForClickable(); - await (await hamburgerMenu()).click(); + await (await closeSideBarMenu()).waitForClickable(); + await (await closeSideBarMenu()).click(); } + + await (await sideBarMenuTitle()).waitForDisplayed({ reverse: true }); }; const navigateToLogoutModal = async () => { @@ -264,8 +299,7 @@ const waitForLoaderToDisappear = async (element) => { const hideSnackbar = () => { // snackbar appears in the bottom of the page for 5 seconds when certain actions are made - // for example when filling a form, or creating a contact - // and intercepts all clicks in the actionbar + // for example when filling a form, or creating a contact and intercepts all clicks in the FAB and Flat buttons // this action is temporary, and will be undone with a refresh return browser.execute(() => { // eslint-disable-next-line no-undef @@ -273,15 +307,6 @@ const hideSnackbar = () => { }); }; -const toggleActionbar = (hide) => { - // the actiobar can cover elements at the bottom of the page, making clicks land in incorrect places - return browser.execute((hide) => { - // eslint-disable-next-line no-undef - const element = window.jQuery('.detail-actions'); - hide ? element.hide() : element.show(); - }, hide); -}; - const getVisibleLoaders = async () => { const visible = []; for (const loader of await loaders()) { @@ -301,7 +326,7 @@ const waitForLoaders = async () => { }; const waitForAngularLoaded = async (timeout = 40000) => { - await (await $('#header-dropdown-link')).waitForDisplayed({ timeout }); + await (await hamburgerMenu()).waitForDisplayed({ timeout }); }; const waitForPageLoaded = async () => { @@ -322,7 +347,9 @@ const syncAndNotWaitForSuccess = async () => { const syncAndWaitForSuccess = async (timeout = 20000) => { await openHamburgerMenu(); + await (await syncButton()).waitForClickable(); await (await syncButton()).click(); + await closeReloadModal(false); await openHamburgerMenu(); if (await (await syncInProgress()).isExisting()) { await (await syncInProgress()).waitForDisplayed({ reverse: true, timeout }); @@ -345,7 +372,7 @@ const sync = async (expectReload, timeout) => { let closedModal = false; if (expectReload) { // it's possible that sync already happened organically, and we already have the reload modal - closedModal = await closeReloadModal(false, 0); + closedModal = await closeReloadModal(); } await syncAndWaitForSuccess(timeout); @@ -366,8 +393,8 @@ const syncAndWaitForFailure = async () => { const closeReloadModal = async (shouldUpdate = false, timeout = 5000) => { try { + await browser.waitUntil( async () => await modalPage.modal().isDisplayed(), { timeout: 10000, interval: 500 } ); shouldUpdate ? await modalPage.submit(timeout) : await modalPage.cancel(timeout); - await modalPage.checkModalHasClosed(); shouldUpdate && await waitForAngularLoaded(); return true; } catch (err) { @@ -377,8 +404,8 @@ const closeReloadModal = async (shouldUpdate = false, timeout = 5000) => { }; const openReportBugAndFetchProperties = async () => { - await (await $(FEEDBACK_MENU)).waitForClickable(); - await (await $(FEEDBACK_MENU)).click(); + await (await feedbackMenuOption()).waitForClickable(); + await (await feedbackMenuOption()).click(); return await modalPage.getModalDetails(); }; @@ -399,8 +426,8 @@ const openAboutMenu = async () => { }; const openUserSettings = async () => { - await (await userSettingsMenuOption()).waitForClickable(); - await (await userSettingsMenuOption()).click(); + await (await $(USER_SETTINGS)).waitForClickable(); + await (await $(USER_SETTINGS)).click(); }; const openUserSettingsAndFetchProperties = async () => { @@ -414,8 +441,8 @@ const openEditProfile = async () => { }; const openAppManagement = async () => { - await (await $(CONFIGURATION_APP_MENU)).waitForClickable(); - await (await $(CONFIGURATION_APP_MENU)).click(); + await (await configurationAppMenuOption()).waitForClickable(); + await (await configurationAppMenuOption()).click(); await (await $('.navbar-brand')).waitForDisplayed(); }; @@ -446,14 +473,6 @@ const loadNextInfiniteScrollPage = async () => { await waitForLoaderToDisappear(await $('.left-pane')); }; -const getActionBarLabels = async () => { - await (await actionBar()).waitForDisplayed(); - await (await actionBarActions())[0].waitForDisplayed(); - const items = await actionBarActions(); - const labels = await items.map(item => item.getText()); - return labels.filter(label => !!label); -}; - const getErrorLog = async () => { await errorLog().waitForDisplayed(); @@ -501,6 +520,7 @@ module.exports = { isTargetMenuItemPresent, isTargetAggregatesMenuItemPresent, openHamburgerMenu, + closeHamburgerMenu, openAboutMenu, openUserSettingsAndFetchProperties, openUserSettings, @@ -515,7 +535,6 @@ module.exports = { snackbarMessage, snackbarAction, getTextForElements, - toggleActionbar, jsonError, isMenuOptionEnabled, isMenuOptionVisible, @@ -528,6 +547,8 @@ module.exports = { loadNextInfiniteScrollPage, goToUrl, getFastActionItemsLabels, - getActionBarLabels, - getErrorLog + getErrorLog, + reportsFastActionFAB, + isReportActionDisplayed, + getHeaderTitleOnMobile, }; diff --git a/tests/page-objects/default/common/modal.wdio.page.js b/tests/page-objects/default/common/modal.wdio.page.js index 04ab8c84cd3..95bf50b8ab3 100644 --- a/tests/page-objects/default/common/modal.wdio.page.js +++ b/tests/page-objects/default/common/modal.wdio.page.js @@ -33,6 +33,7 @@ const cancel = async (timeout) => { }; module.exports = { + modal, body, submit, cancel, diff --git a/tests/page-objects/default/contacts/contacts.wdio.page.js b/tests/page-objects/default/contacts/contacts.wdio.page.js index 21a54b57107..b5d2d1b8665 100644 --- a/tests/page-objects/default/contacts/contacts.wdio.page.js +++ b/tests/page-objects/default/contacts/contacts.wdio.page.js @@ -295,10 +295,8 @@ const openFormWithWarning = async (formId) => { }; const openReport = async () => { - await commonPage.toggleActionbar(true); await (await reportsCardSelectors.rhsReportListElement()).waitForDisplayed(); await (await reportsCardSelectors.rhsReportListElement()).click(); - await commonPage.toggleActionbar(); }; const getContactCardTitle = async () => { diff --git a/tests/page-objects/default/enketo/pregnancy.wdio.page.js b/tests/page-objects/default/enketo/pregnancy.wdio.page.js index 199cb4379c7..6eccab9c2cb 100644 --- a/tests/page-objects/default/enketo/pregnancy.wdio.page.js +++ b/tests/page-objects/default/enketo/pregnancy.wdio.page.js @@ -25,7 +25,8 @@ const submitDefaultPregnancy = async (submitForm = true) => { await commonEnketoPage.selectRadioButton('If the woman has a specific upcoming ANC appointment date, ' + 'enter it here. You will receive a task three days before to remind her to attend.', 'Enter date'); await setFutureVisitDate(moment().add(2, 'day').format('YYYY-MM-DD')); - await genericForm.nextPage(); + await genericForm.formTitle().click(); + await genericForm.nextPage(1); await commonEnketoPage.selectRadioButton('Is this the woman\'s first pregnancy?', 'No'); await commonEnketoPage.selectRadioButton('Has the woman had any miscarriages or stillbirths?', 'Yes'); await genericForm.nextPage(); diff --git a/tests/page-objects/default/old-navigation/old-navigation.wdio.page.js b/tests/page-objects/default/old-navigation/old-navigation.wdio.page.js new file mode 100644 index 00000000000..f7ac9e15eca --- /dev/null +++ b/tests/page-objects/default/old-navigation/old-navigation.wdio.page.js @@ -0,0 +1,150 @@ +const commonPage = require('@page-objects/default/common/common.wdio.page'); +const modalPage = require('@page-objects/default/common/modal.wdio.page'); + +const messagesTab = () => $('#messages-tab'); +const analyticsTab = () => $('#analytics-tab'); +const taskTab = () => $('#tasks-tab'); +const hamburgerMenuItemSelector = '#header-dropdown li'; +const syncButton = () => $(`${hamburgerMenuItemSelector} a:not(.disabled) .fa-refresh`); +const hamburgerMenu = () => $('#header-dropdown-link'); +const syncInProgress = () => $('*="Currently syncing"'); +const syncSuccess = () => $(`${hamburgerMenuItemSelector}.sync-status .success`); +const loaders = () => $$('.container-fluid .loader'); + +const goToMessages = async () => { + await commonPage.goToUrl(`/#/messages`); + await (await messagesTab()).waitForDisplayed(); +}; + +const goToTasks = async () => { + await commonPage.goToUrl(`/#/tasks`); + await (await taskTab()).waitForDisplayed(); + await waitForPageLoaded(); +}; + +const goToReports = async (reportId = '') => { + await commonPage.goToUrl(`/#/reports/${reportId}`); + await waitForPageLoaded(); +}; + +const goToPeople = async (contactId = '', shouldLoad = true) => { + await commonPage.goToUrl(`/#/contacts/${contactId}`); + if (shouldLoad) { + await waitForPageLoaded(); + } +}; + +const goToAnalytics = async () => { + await commonPage.goToUrl(`/#/analytics`); + await (await analyticsTab()).waitForDisplayed(); + await waitForPageLoaded(); +}; + +const hideModalOverlay = () => { + // hides the modal overlay, so it doesn't intercept all clicks + // this action is temporary, and will be undone with a refresh + return browser.execute(() => { + const style = document.createElement('style'); + style.innerHTML = '.cdk-overlay-backdrop { display: none; }'; + document.head.appendChild(style); + }); +}; + +const isHamburgerMenuOpen = async () => { + return await (await $('.header .dropdown.open #header-dropdown-link')).isExisting(); +}; + +const openHamburgerMenu = async () => { + if (!(await isHamburgerMenuOpen())) { + await (await hamburgerMenu()).waitForClickable(); + await (await hamburgerMenu()).click(); + } +}; + +const closeHamburgerMenu = async () => { + if (await isHamburgerMenuOpen()) { + await (await hamburgerMenu()).waitForClickable(); + await (await hamburgerMenu()).click(); + } +}; + +const syncAndWaitForSuccess = async (timeout = 20000) => { + await openHamburgerMenu(); + await (await syncButton()).click(); + await openHamburgerMenu(); + if (await (await syncInProgress()).isExisting()) { + await (await syncInProgress()).waitForDisplayed({ reverse: true, timeout }); + } + await (await syncSuccess()).waitForDisplayed({ timeout }); +}; + +const sync = async (expectReload, timeout) => { + await hideModalOverlay(); + let closedModal = false; + if (expectReload) { + // it's possible that sync already happened organically, and we already have the reload modal + closedModal = await commonPage.closeReloadModal(false, 0); + } + + await syncAndWaitForSuccess(timeout); + if (expectReload && !closedModal) { + await commonPage.closeReloadModal(); + } + // sync status sometimes lies when multiple changes are fired in quick succession + await syncAndWaitForSuccess(timeout); + await closeHamburgerMenu(); +}; + +const getVisibleLoaders = async () => { + const visible = []; + for (const loader of await loaders()) { + if (await loader.isDisplayedInViewport()) { + visible.push(loader); + } + } + + return visible; +}; + +const waitForAngularLoaded = async (timeout = 40000) => { + await (await $('#header-dropdown-link')).waitForDisplayed({ timeout }); +}; + +const waitForPageLoaded = async () => { + // if we immediately check for app loaders, we might bypass the initial page load (the bootstrap loader) + // so waiting for the main page to load. + await waitForAngularLoaded(); + // ideally we would somehow target all loaders that we expect (like LHS + RHS loaders), but not all pages + // get all loaders. + do { + await commonPage.waitForLoaders(); + } while ((await getVisibleLoaders()).length > 0); +}; + +const goToBase = async () => { + await commonPage.goToUrl('/'); + await waitForPageLoaded(); +}; + +const logout = async () => { + await openHamburgerMenu(); + + await (await commonPage.logoutButton()).waitForClickable(); + await (await commonPage.logoutButton()).click(); + + await (await modalPage.body()).waitForDisplayed(); + await modalPage.submit(); + await browser.pause(100); // wait for login page js to execute +}; + +module.exports = { + goToBase, + goToMessages, + goToTasks, + goToReports, + goToPeople, + goToAnalytics, + sync, + waitForPageLoaded, + logout, +}; diff --git a/tests/page-objects/default/reports/reports.wdio.page.js b/tests/page-objects/default/reports/reports.wdio.page.js index bc3fa516eb7..bef97c85262 100644 --- a/tests/page-objects/default/reports/reports.wdio.page.js +++ b/tests/page-objects/default/reports/reports.wdio.page.js @@ -73,6 +73,12 @@ const sidebarFilterSelectors = { dateAccordionBody: () => $('#date-filter-accordion mat-panel-description'), toDate: () => $('#toDateFilter'), fromDate: () => $('#fromDateFilter'), + formAccordionHeader: () => $('#form-filter-accordion mat-expansion-panel-header'), + formAccordionBody: () => $('#form-filter-accordion mat-panel-description'), + facilityAccordionHeader: () => $('#place-filter-accordion mat-expansion-panel-header'), + facilityAccordionBody: () => $('#place-filter-accordion mat-panel-description'), + statusAccordionHeader: () => $('#status-filter-accordion mat-expansion-panel-header'), + statusAccordionBody: () => $('#status-filter-accordion mat-panel-description'), }; const deleteDialogSelectors = { @@ -260,8 +266,46 @@ const openSidebarFilterDateAccordion = async () => { return (await sidebarFilterSelectors.dateAccordionBody()).waitForDisplayed(); }; +const filterByForm = async (formName) => { + await (await sidebarFilterSelectors.formAccordionHeader()).click(); + await (await sidebarFilterSelectors.formAccordionBody()).waitForDisplayed(); + const option = sidebarFilterSelectors.formAccordionBody().$(`a*=${formName}`); + await (await option).waitForDisplayed(); + await (await option).waitForClickable(); + await (await option).click(); +}; + +const filterByStatus = async (statusOption) => { + await (await sidebarFilterSelectors.statusAccordionHeader()).click(); + await (await sidebarFilterSelectors.statusAccordionBody()).waitForDisplayed(); + const option = sidebarFilterSelectors.statusAccordionBody().$(`a*=${statusOption}`); + await (await option).waitForDisplayed(); + await (await option).waitForClickable(); + await (await option).click(); +}; + +const filterByFacility = async (parentFacility, reportFacility) => { + await (await sidebarFilterSelectors.facilityAccordionHeader()).click(); + await (await sidebarFilterSelectors.facilityAccordionBody()).waitForDisplayed(); + + const parent = sidebarFilterSelectors.facilityAccordionBody().$(`a*=${parentFacility}`); + await parent.waitForDisplayed(); + await parent.waitForClickable(); + await (await parent).click(); + + const facility = await sidebarFilterSelectors + .facilityAccordionBody() + .$('.mm-dropdown-submenu') + .$(`a*=${reportFacility}`); + await facility.waitForDisplayed(); + await facility.waitForClickable(); + const checkbox = facility.previousElement(); + await (await checkbox).click(); +}; + const setSidebarFilterDate = async (fieldPromise, calendarIdx, date) => { await (await fieldPromise).waitForDisplayed(); + await (await fieldPromise).waitForClickable(); await (await fieldPromise).click(); const dateRangePicker = `.daterangepicker:nth-of-type(${calendarIdx})`; @@ -447,6 +491,12 @@ const verifyReport = async () => { expect(validatedReport.patient).to.be.undefined; }; +const openFirstReport = async () => { + const firstReport = leftPanelSelectors.firstReport(); + await firstReport.waitForClickable(); + await openSelectedReport(firstReport); +}; + module.exports = { leftPanelSelectors, rightPanelSelectors, @@ -460,6 +510,9 @@ module.exports = { openSidebarFilterDateAccordion, setSidebarFilterFromDate, setSidebarFilterToDate, + filterByForm, + filterByFacility, + filterByStatus, setDateInput, getFieldValue, setBikDateInput, @@ -490,4 +543,5 @@ module.exports = { getReportListLoadingStatus, openSelectedReport, verifyReport, + openFirstReport, }; diff --git a/tests/page-objects/default/sms/messages.wdio.page.js b/tests/page-objects/default/sms/messages.wdio.page.js index bf0f0fbda1b..a99344f7dc3 100644 --- a/tests/page-objects/default/sms/messages.wdio.page.js +++ b/tests/page-objects/default/sms/messages.wdio.page.js @@ -78,6 +78,15 @@ const sendMessage = async (message, recipient, entryText) => { await modalPage.checkModalHasClosed(); }; +const sendMessageOnMobile = async (message, recipient, entryText) => { + await commonPage.clickFastActionFAB({ waitForList: false }); + await (await sendMessageModal()).waitForDisplayed(); + await searchSelect(recipient, entryText); + await (await messageText()).setValue(message); + await modalPage.submit(); + await modalPage.checkModalHasClosed(); +}; + const sendReplyNewRecipient = async (recipient, entryText) => { await searchSelect(recipient, entryText); await modalPage.submit(); @@ -138,6 +147,7 @@ module.exports = { getMessageHeader, getMessageContent, sendMessage, + sendMessageOnMobile, sendReplyNewRecipient, sendMessageToContact, exportMessages, diff --git a/tests/page-objects/upgrade/upgrade.wdio.page.js b/tests/page-objects/upgrade/upgrade.wdio.page.js index f6684a74c05..9868691fe2e 100644 --- a/tests/page-objects/upgrade/upgrade.wdio.page.js +++ b/tests/page-objects/upgrade/upgrade.wdio.page.js @@ -61,7 +61,7 @@ const upgradeVersion = async (branch, tag, testFrontend=true) => { await (await cancelUpgradeButton()).waitForDisplayed(); await (await deploymentInProgress()).waitForDisplayed(); - await (await deploymentInProgress()).waitForDisplayed({ reverse: true, timeout: 100000 }); + await (await deploymentInProgress()).waitForDisplayed({ reverse: true, timeout: 150000 }); if (testFrontend) { // https://github.com/medic/cht-core/issues/9186 diff --git a/tests/performance/apdex-score/README.md b/tests/performance/apdex-score/README.md index 0fac9e20b40..f05f3c01c81 100644 --- a/tests/performance/apdex-score/README.md +++ b/tests/performance/apdex-score/README.md @@ -2,13 +2,47 @@ ## Setup +#### Prerequisites + +Before continuing with the setup steps below, ensure you have a cht instance deployed and running either locally or globally - check out the [documentation](https://docs.communityhealthtoolkit.org/hosting/4.x/app-developer/) on how to do this. + +Also, make sure you have some pre-existing users and data already loaded on the app - you can use the [test-data-generator](https://github.com/medic/test-data-generator) tool to achieve this. + +Finally, ensure you have done the following installations on your machine: + +1. Install [NodeJS](https://nodejs.org/en/download) and [Java JDK](https://www.oracle.com/java/technologies/downloads/) then ensure JAVA_HOME path is correctly set up. + ``` + export JAVA_HOME=$(/usr/libexec/java_home) + ``` +2. Install and Set-up [Android studio](https://developer.android.com/studio/install) and the `adb tool` to enable you run adb commands. + - Add the Android SDK directory to your system’s ANDROID_HOME environment variable. + ``` + export ANDROID_HOME="/Users/yourpath/Library/Android/sdk/" + export PATH=$ANDROID_HOME/platform-tools:$PATH + export PATH=$ANDROID_HOME/tools:$PATH + ``` + - To set up an Android Virtual Device (AVD), open Android Studio, click on the More Actions > Virtual Device Manager button, and proceed with the virtual device creation by selecting the hardware and system image. +3. Install appium and appium doctor. + ``` + npm install -g appium@next + npm install -g appium-doctor + ``` +4. Install appium driver - `appium driver install uiautomator2` + +if you already have the CHT Android app installed just set the `appPath` value (in the capabilities section of the settings file) to an empty string. However, +If you do not have the CHT Android app installed on your mobile device, you can download the preferred [apk version](https://github.com/medic/cht-android/releases) and then set the `appPath` value to the absolute path of the apk file. + +#### Steps + 1. Enable the developer mode in your phone and enable the USB Debugger mode. -2. Connect the phone to the computer -3. Create a settings file: + - Ensure your device does not have a lock screen PIN/Passcode. +2. Connect your phone to the computer using the appropriate device cable or you can follow [these steps](https://developer.android.com/studio/run/device#wireless) to connect your device using Wi-Fi. + - Run the `adb devices` command to confirm that your device is listed among the attached devices. +3. Create a settings file or reuse one of the provided [sample settings](https://github.com/medic/cht-core/tree/master/tests/performance/apdex-score/sample-settings-files) files.
Expand to see settings file structure -```json +``` { "iterations": 1, "instanceURL": "", @@ -214,14 +248,18 @@
-- Find the android version by running `adb shell getprop | grep ro.build.version.release` -- Find the device name by running `adb shell getprop | grep ro.product.model` - -4. Set the environment variable `APDEX_TEST_SETTINGS` with the path of your settings file. - -```sh {"id":"01J2WE5FDN0D40ZJ5XA7ZHHH4Z"} -export APDEX_TEST_SETTINGS=/Users/pepe/Documents/apdex-settings.json -``` +4. Set the environment variable `APDEX_TEST_SETTINGS` with the path of your settings file (apdex-settings.json). + For example, you can use the following command but make sure to replace the path with your actual settings file location: + ``` + export APDEX_TEST_SETTINGS=/Users/pepe/Documents/apdex-settings.json + ``` + - Ensure the `apdex-settings.json` file has been updated with the correct instance url, login credentials and assertion texts (which correspond to the data in your cht instance) for page navigation, forms and other app interactions. + - Under the skip section of the settings file, set `true` for the tests you want to skip and `false` for those you want to execute. + - Update the fields for `platformVersion` and `deviceName` to match the value for your device. + - Find the android version (`platformVersion`) by running `adb shell getprop | grep ro.build.version.release`. + - Find the device name (`deviceName`) by running `adb shell getprop | grep ro.product.model`. +5. Ensure all dependencies have been properly installed - run `npm ci` from the root directory. +6. Run `npm run apdex-test` from the root directory to execute the selected tests. ## Settings file @@ -347,7 +385,7 @@ Elements to assert that are displayed in the screen. - Test how many scrolls you need by plugging the phone in the computer and run these adb commands: - Scroll down: `adb shell input swipe 500 1000 300 300` - Scroll up: `adb shell input swipe 300 300 500 1000` - - For example, if you need to run 3 times the scroll down command, then you add 3 like this: `"scrollDown": 3,` + - For example, if you need to run the scroll down command 3 times, then you add 3 as the value for scrollDown like this: `"scrollDown": 3,` - In some cases, it's necessary to unfocus a selected element, trigger a click in a label. For example: ``` @@ -362,4 +400,3 @@ Elements to assert that are displayed in the screen. - Find an element containing a text _anywhere_ in the screen: `"//*[text()[contains(.,\"Eric Patt\")]"` - Use [Appium Inspector](https://github.com/appium/appium-inspector) to help you find the XPath selectors. Sometimes it produces very long selectors but you can find a way to make them shorter. - If it fails to start after setting up with capabilities. Try running `appium server` in the terminal then run the Appium Inspector. - diff --git a/tests/performance/apdex-score/sample-settings-files/default-config-form-test.json b/tests/performance/apdex-score/sample-settings-files/default-config-form-test.json new file mode 100644 index 00000000000..58532d7eba8 --- /dev/null +++ b/tests/performance/apdex-score/sample-settings-files/default-config-form-test.json @@ -0,0 +1,419 @@ +{ + "iterations": 1, + "instanceURL": "", + "hasPrivacyPolicy": false, + "capabilities": [ + { + "platformVersion": "", + "deviceName": "", + "appPath": "", + "noReset": false + } + ], + + "skip": { + "login": false, + + "loadContactList": false, + "loadCHWArea": false, + "loadHousehold": false, + "loadPatient": false, + "loadMessageList": false, + "loadTaskList": false, + "loadAnalytics": false, + "loadReportList": false, + "searchContact": false, + "searchReport": false, + + "submitTask": true, + "createPatient": true, + "submitPatientReport": true + }, + + "users": [ + { + "type": "offline", + "role": "chw", + "username": "", + "password": "" + } + ], + + "commonElements": { + "fab": "", + "formSubmit": "", + "formNext": "", + "menuListTitle": "//*[@text=\"Tasks\"]", + "searchBox": "//android.widget.EditText" + }, + + "pages": { + "contact-list": { + "navigation": [ + { "selector": "//*[@text=\"Reports\"]" }, + { + "selector": "//*[@text=\"People\"]", + "asserts": [ { "selector": "//*[contains(@text, \"Kentwood's CHVArea\")]" } ] + } + ] + }, + "chwArea": { + "navigation": [ + { + "selector": "//*[contains(@text, \"Kentwood's CHVArea\")]", + "asserts": [ + { + "selector": "//*[@text=\"Commodities\"]" + }, + { + "selector": "//*[contains(@text, \"First Aid Kit\")]" + }, + { + "scrollDown": 2, + "selector": "//*[@text=\"Reports\"]" + } + ] + } + ], + "postTestPath": [ { "selector": "//*[@text=\"Back\"]" } ] + }, + "household": { + "navigation": [ + { "selector": "//*[@text=\"People\"]" }, + { "selector": "//*[contains(@text, \"Pattieshire's\")]", + "asserts": [ + { "selector": "//*[contains(@text, \"Primary phone\")]" }, + { "selector": "//*[contains(@text, \"People\")]" }, + { + "scrollDown": 2, + "selector": "//*[@text=\"Reports\"]" + } + ] + } + ] + }, + "patient": { + "navigation": [ + { "selector": "//*[@text=\"People\"]" }, + { "selector": "//*[contains(@text, \"Pattieshire's\")]" }, + { + "selector": "//*[contains(@text, \"Stephany Angel\")]", + "asserts": [ + { "selector": "//*[contains(@text, \"46 years\")]" }, + { "selector": "//*[contains(@text, \"Family Planning\")]" }, + { + "scrollDown": 2, + "selector": "//*[contains(@text, \"Reports\")]" + } + ] + } + ] + }, + "messages": { + "navigation": [ + { "selector": "//*[@text=\"Messages\"]", + "asserts": [ + { "selector": "//*[contains(@text, \"months ago\")]" }, + { "selector": "//*[contains(@text, \"Maegan Taylor\")]" }, + { + "scrollDown": 1, + "selector": "//*[contains(@text, \"No more messages\")]" + } + ] + }, + { "scrollUp": 1, + "selector": "//*[contains(@text, \"Maegan Taylor\")]", + "asserts": [ + { "selector": "//*[contains(@text, \"pending\")]" } + ] + } + ], + "postTestPath": [ { "selector": "//*[@text=\"Back\"]" } ] + }, + "tasks": { + "navigation": [ + { "selector": "//*[@text=\"Tasks\"]", + "asserts": [ + { "selector": "//*[contains(@text, \"Alan Patt\")]" }, + { + "scrollDown": 3, + "selector": "//*[contains(@text, \"No more tasks\")]" + } + ] + } + ] + }, + "reports": { + "navigation": [ + { "selector": "//*[@text=\"Reports\"]", + "asserts": [ + { "selector": "//*[contains(@text, \"Sexual and Gender Based Violence\")]" }, + { + "scrollDown": 15, + "selector": "//*[contains(@text, \"No more reports\")]" + } + ] + }, + { "scrollUp": 15, + "selector": "//*[@text=\"Sexual and Gender Based Violence\"]", + "asserts": [ + { "selector": "//*[contains(@text, \"Sexual and Gender Based Violence\")]" }, + { "selector": "//*[contains(@text, \"patient\")]" }, + { + "scrollDown": 2, + "selector": "//*[contains(@text, \"summary\")]" + } + ] + } + ], + "postTestPath": [ { "selector": "//*[@text=\"Back\"]" } ] + }, + "targets": { + "navigation": [ + { "selector": "//*[@text=\"Performance\"]", + "asserts": [ + { "selector": "//*[contains(@text, \"Total males\")]" }, + { "selector": "//*[contains(@text, \"Total females\")]" }, + { + "scrollDown": 1, + "selector": "//*[contains(@text, \"Active Pregnancies\")]" + }, + { "selector": "//*[contains(@text, \"Under 5\")]" }, + { "scrollDown": 2, + "selector": "//*[contains(@text, \"Malaria Treatments\")]" + }, + { + "selector": "//*[contains(@text, \"Total Referrals\")]" + } + ] + } + ] + } + }, + + "forms": { + "patientTask": { + "navigation": [{ + "scrollUp": 3, + "selector": "//*[contains(@text, \"Bergefort's\")]" + }], + "pages": [ + { + "asserts": [ + { "selector": "//*[contains(@text, \"Health Facility Notes\")]" } + ], + "fields": [ + { "selector": "//android.widget.RadioButton[@text=\"Yes\"]" }, + { "selector": "//*[contains(@text, \"health facility?\")]//android.widget.RadioButton[@text=\"Yes\"]" } + ] + }, + { + "asserts": [ + { "selector": "//*[@text=\"REFERRAL FOLLOW UP RESULTS\"]" }, + { "selector": "//*[contains(@text, \"Alan Patt\")]" } + ] + } + ], + "postSubmitAssert": [{ + "scrollDown": 7, + "selector": "//*[contains(@text, \"No more tasks\")]" + }] + }, + + "patientReport": { + "navigation": [{ + "scrollDown": 1, + "selector": "//*[@text=\"Sexual and Gender Based Violence\"]" + }], + "pages": [ + { + "asserts": [ + { "selector": "//*[contains(@text, \"Observe for signs of\")]" } + ], + "fields": [ + { "selector": "//android.widget.RadioButton[@text=\"Yes\"]" }, + { + "selector": "//android.widget.EditText", + "value": "SGBV Report" + }, + { + "scrollDown": 1, + "selector": "//*[contains(@text, \"Have you referred them to the CHA\")]//android.widget.RadioButton[@text=\"Yes\"]" + } + ], + "scrollDown": 1 + }, + { + "asserts": [ + { "selector": "//*[@text=\"SGBV REFERRAL NOTE\"]" }, + { "selector": "//*[contains(@text, \"Alan Patt\")]" } + ] + } + ], + "postSubmitAsserts": [ + { "select": "//android.widget.TextView[contains(@text, \"Submitted by Emma\")]" } + ], + "postTestPath": [ { "selector": "//*[@text=\"Back\"]" } ] + }, + + "patientContact": { + "navigation": [ + { "selector": "//*[@text=\"Add new Person\"]" } + ], + "pages": [ + { + "asserts": [ + { "selector": "//*[contains(@text, \"First name\")]" } + ], + "fields": [ + { + "id": "first_name", + "selector": "//*[@text=\"First name\"]//parent::android.view.View/android.widget.EditText", + "value": "Alan " + }, + { + "id": "last_name", + "selector": "//*[contains(@text, \"Last name\")]//parent::android.view.View/android.widget.EditText", + "value": "Patt" + }, + { + "id": "gender", + "selector": "//android.widget.RadioButton[@text=\"Male\"]" + }, + { + "id": "dob_option", + "selector": "//android.widget.RadioButton[@text=\"Date of birth with current age\"]" + }, + { + "id": "dob_age", + "scrollDown": 1, + "selector": "//*[@text=\"Age* Age in years\"]", + "keycodes": [ 9, 10 ] + }, + { + "id": "dob_months", + "selector": "//*[contains(@text, \"Months* And how many months\")]", + "keycodes": [ 10 ] + }, + { + "id": "nationality", + "selector": "//*[contains(@text,\"Kenyan?\")]//android.widget.RadioButton[@text=\"Yes\"]" + }, + { + "id": "birth_nationality", + "selector": "//*[contains(@text,\"born in Kenya?\")]//android.widget.RadioButton[@text=\"Yes\"]" + }, + { + "id": "birth_county", + "selector": "//*[contains(@text, \"County of birth*\")]", + "dropdownOption": "//android.widget.CheckedTextView[@text=\"BOMET\"]" + }, + { + "id": "county_residence", + "selector": "//android.view.View[contains(@text, \"County of residence\")]", + "dropdownOption": "//android.widget.CheckedTextView[@resource-id=\"android:id/text1\" and @text=\"BARINGO\"]" + }, + { + "id": "sub_county_residence", + "scrollDown": 1, + "selector": "//android.view.View[@text=\"Sub county*\"]", + "dropdownOption": "//android.widget.CheckedTextView[@resource-id=\"android:id/text1\" and @text=\"BARINGO NORTH\"]" + }, + { + "id": "ward_residence", + "selector": "//android.view.View[@text=\"Ward*\"]", + "dropdownOption": "//android.widget.CheckedTextView[@resource-id=\"android:id/text1\" and @text=\"BARTABWA\"]" + }, + { + "id": "village_name", + "selector": "//*[@text=\"Village\"]//parent::android.view.View/android.widget.EditText", + "keycodes": [ 35, 43, 36, 37 ] + }, + { + "id": "phone", + "selector": "//*[contains(@text,\"have a phone number?\")]//android.widget.RadioButton[@text=\"No\"]" + }, + { + "id": "passport_option", + "selector": "//android.widget.RadioButton[@text=\"Passport\"]" + }, + { + "id": "passport", + "scrollDown": 1, + "selector": "//*[@text=\"Passport\"]//parent::android.view.View/android.widget.EditText", + "value": "XXBB2233" + }, + { + "id": "next_kin", + "selector": "//*[contains(@text, \"next of kin\")]//parent::android.view.View/android.widget.EditText", + "value": "Elizabeth Kelly" + }, + { + "id": "next_kin_relationship_label", + "selector": "//*[contains(@text, \"What is Alan Patt's relationship with\")]" + }, + { + "id": "next_kin_relationship", + "selector": "//android.widget.RadioButton[@text=\"Mother\"]" + }, + { + "id": "address", + "scrollDown": 1, + "selector": "//*[contains(@text, \"Physical address\")]//parent::android.view.View/android.widget.EditText", + "value": "3752 Western Lane" + }, + { + "id": "relationship_with_owner", + "scrollDown": 1, + "selector": "//*[contains(@text,\"household head?\")]//android.widget.RadioButton[@text=\"Sibling\"]" + }, + { + "id": "disabilities", + "selector": "//*[contains(@text,\"known disability?\")]//android.widget.RadioButton[@text=\"No\"]" + }, + { + "id": "chronic_illness", + "selector": "//*[contains(@text,\"chronic illness?\")]//android.widget.RadioButton[@text=\"No\"]" + } + ] + } + ], + "postSubmitAsserts": [ + { "selector": "//*[@text=\"Alan Patt\"]" } + ], + "postTestPath": [ { "selector": "//*[@text=\"Back\"]" } ] + }, + + "patientSearch": { + "navigation": [ + { + "selector": "//*[@text=\"People\"]", + "asserts": [ { "selector": "//*[contains(@text, \"Lemketown's CHVArea\")]" } ] + }, + { "selector": "//android.widget.TextView[@text=\"\"]" } + ], + "pages": [ + { + "asserts": [ + { "selector": "//android.widget.EditText" } + ], + "fields": [ + { + "selector": "//android.widget.EditText", + "value": "Alan Pat" + }, + { "keycodes": [ 66 ] } + ] + } + ], + "postSubmitAsserts": [ + { "selector": "//*[@text=\"Alan Patt\"]" }, + { + "scrollDown": 5, + "selector": "//*[contains(@text, \"No more people\")]" } + ], + "postTestPath": [ { "selector": "(//android.widget.TextView[@text=\"\"])[2]" } ], + "postSubmitAssert": [ { "selector": "//*[contains(@text, \"Lemketown's CHVArea\")]" } ] + } + } +} \ No newline at end of file diff --git a/tests/utils/screenshots.js b/tests/utils/screenshots.js new file mode 100644 index 00000000000..0bfe1d6fcaa --- /dev/null +++ b/tests/utils/screenshots.js @@ -0,0 +1,75 @@ +/** + * This file uses the `sharp` library for image manipulation to overcome limitations + * in WebdriverIO's native screenshot and window resizing capabilities. + * + * WebdriverIO's `setWindowSize` function has a minimum width of 500px, + * which is insufficient for capturing mobile screenshots. + * And the `takeScreenshot` captures the entire window size, not just the viewport. + * + * To address this, we capture the screenshot using WebdriverIO and then use + * `sharp` to resize the image to match the desired viewport size. This ensures + * that our screenshots accurately represent the mobile view of the application. + */ +const sharp = require('sharp'); + +const MOBILE_WINDOW_WIDTH = 768; +const MOBILE_VIEWPORT_WIDTH = 320; +const MOBILE_VIEWPORT_HEIGHT = 570; +const DESKTOP_WINDOW_WIDTH = 1000; +const DESKTOP_WINDOW_HEIGHT = 820; +const HIGH_DENSITY_DISPLAY_2X = 2; + +const isMobile = async () => { + const { width } = await browser.getWindowSize(); + return width < MOBILE_WINDOW_WIDTH; +}; + +const resizeWindowForScreenshots = async () => { + if (await isMobile()) { + return await browser.emulateDevice({ + viewport: { + width: MOBILE_VIEWPORT_WIDTH, + height: MOBILE_VIEWPORT_HEIGHT, + isMobile: true, + hasTouch: true, + }, + userAgent: 'Mozilla/5.0 (Linux; Android 11; Pixel 4) ' + + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + + 'Chrome/92.0.4515.159 Mobile Safari/537.36' + }); + } + + return await browser.setWindowSize(DESKTOP_WINDOW_WIDTH, DESKTOP_WINDOW_HEIGHT); +}; + +const generateScreenshot = async (scenario, step) => { + const device = await isMobile() ? 'mobile' : 'desktop'; + + const filename = `./tests/e2e/visual/images/${scenario}-${step}-${device}.png`; + + const newScreenshot = await browser.takeScreenshot(); + let screenshotSharp = sharp(Buffer.from(newScreenshot, 'base64')); + const metadata = await screenshotSharp.metadata(); + + const isMobileDevice = await isMobile(); + const extractWidth = isMobileDevice ? Math.min(MOBILE_VIEWPORT_WIDTH*2, metadata.width) : metadata.width; + const extractHeight = isMobileDevice ? Math.min(MOBILE_VIEWPORT_HEIGHT*2, metadata.height) : metadata.height; + screenshotSharp = screenshotSharp.extract({ + width: extractWidth, + height: extractHeight, + left: 0, + top: 0, + }); + + screenshotSharp = screenshotSharp.resize( + extractWidth * HIGH_DENSITY_DISPLAY_2X, + extractHeight * HIGH_DENSITY_DISPLAY_2X + ); + + await screenshotSharp.toFile(filename); +}; + +module.exports = { + resizeWindowForScreenshots, + generateScreenshot, +}; diff --git a/webapp/src/css/about.less b/webapp/src/css/about.less index 2d0c2e3da0d..0868550075d 100644 --- a/webapp/src/css/about.less +++ b/webapp/src/css/about.less @@ -32,7 +32,6 @@ } .powered-by { - text-align: center; padding: 15px; diff --git a/webapp/src/css/badge.less b/webapp/src/css/badge.less index 07db611fa3f..b37bda83074 100644 --- a/webapp/src/css/badge.less +++ b/webapp/src/css/badge.less @@ -1,7 +1,7 @@ @import "variables"; .mm-badge-border { - border: solid 1px #fff; + border: none; } .mm-badge { @@ -12,7 +12,7 @@ margin: 5px; vertical-align: top; display: inline-block; - text-shadow: rgba(0, 0, 0, 0.5) 1px 1px 2px; + text-shadow: none; .fa { text-shadow: none; diff --git a/webapp/src/css/button.less b/webapp/src/css/button.less index 4d544586bd2..bbd561efa95 100644 --- a/webapp/src/css/button.less +++ b/webapp/src/css/button.less @@ -81,10 +81,12 @@ div.mm-button-text { .mm-navigation-menu { text-align: center; margin: 10px auto; + display: flex; + justify-content: center; li { width: 200px; vertical-align: top; - font-size: @font-extra-extra-large; + font-size: @font-XXL; margin: 20px; display: inline-block; a { diff --git a/webapp/src/css/content-list.less b/webapp/src/css/content-list.less index 501b445075b..9df79d09088 100644 --- a/webapp/src/css/content-list.less +++ b/webapp/src/css/content-list.less @@ -47,7 +47,6 @@ input[type="checkbox"] { display: none; float: left; - margin-left: -5px; margin-right: 5px; } @@ -55,7 +54,7 @@ display: flex; align-items: center; min-height: 70px; - padding: 10px 20px 10px 15px; + padding: 10px 20px 10px 8px; color: inherit; &:hover, @@ -65,8 +64,7 @@ .icon { align-self: start; - margin: 5px; - margin-left: -10px; + margin: 5px 7px 5px 0; img, svg { height: @icon-small; @@ -122,7 +120,7 @@ } .heading .visits { - font-size: @font-extra-extra-large; + font-size: @font-XXL; line-height: @font-medium; font-weight: bold; @@ -257,7 +255,7 @@ display: inline; width: 20px; height: 20px; - margin: 20px 10px; + margin: 20px 7px 20px 10px } .select-all { @@ -337,7 +335,7 @@ mm-multiselect-bar { } .bulk-delete-icon { - font-size: @font-extra-extra-large; + font-size: @font-XXL; margin-right: 15px; } diff --git a/webapp/src/css/dropdown.less b/webapp/src/css/dropdown.less index 56a1d2e1673..cf3429581e1 100644 --- a/webapp/src/css/dropdown.less +++ b/webapp/src/css/dropdown.less @@ -33,47 +33,6 @@ font-size: @font-small; } - &.with-icon li > a { - display: flex; - justify-content: space-between; - align-items: center; - vertical-align: middle; - > span { - margin-right: 10px; - } - .content { - flex-grow: 1; - overflow: hidden; - text-overflow: ellipsis; - } - img, - svg { - width: @icon-small; - height: @icon-small; - } - &:after { - color: @label-color; - content: '\f054'; // http://fontawesome.io/icon/chevron-right/ - font-family: FontAwesome; - } - } - -} - -.dropup .dropdown-menu.mm-dropdown-menu { - padding: 0; - border-top: 5px solid @label-color; - border-radius: 0; - box-shadow: 0 -6px 12px rgba(0, 0, 0, 0.4); - max-width: 90%; - margin-bottom: 1px; - - li { - border-bottom: 1px solid @separator-color; - > a { - padding: 10px; - } - } } .mm-dropdown { @@ -95,68 +54,6 @@ } } - &.multidropdown .dropdown-menu { - padding-bottom: 50px; - .filter-options ul { - margin-left: 10px; - } - > ul { - max-height: 300px; - overflow-x: hidden; - } - a:before, - li:before { - content: none; - } - li { - > a[role="menuitem"] { - padding: 5px 20px 3px 35px; - text-indent: -20px; - } - a, - &.dropdown-header { - padding-left: 10px; - padding-right: 10px; - } - &.dropdown-header { - margin-top: 10px; - font-size: @font-small; - &:first-child { - margin-top: 0; - } - } - a:before { - content: '\f096'; // http://fontawesome.io/icon/square-o/ - } - &.selected a:before { - content: '\f046'; // http://fontawesome.io/icon/check-square-o/ - } - &.disabled { - &, - a { - color: @inactive-color; - cursor: default; - } - a:hover { - background-color: inherit; - } - } - } - .dropdown-child a { - text-indent: 10px; - } - .actions { - margin: 0; - text-align: center; - position: absolute; - bottom: 0px; - left: 0; - right: 0; - border-top: 1px solid silver; - background-color: white; - } - } - &.dropdown.pair-left { padding-right: 0; .mm-button { @@ -251,32 +148,7 @@ @media (max-width: @media-mobile) { .open .mm-dropdown-menu { - top: 111px; - left: 10px; - right: 10px; - } - - .dropup .mm-dropdown-menu { - position: absolute; - top: auto; - bottom: 100%; - padding: 0; - border-top: 5px solid @label-color; - left: 0; - right: 0; - margin-bottom: 1px; - - li > a { - padding: 3px 0; - } - } -} - -@media (max-width: @media-small-mobile) { - .open .mm-dropdown-menu { - top: 101px; - } - .dropup .mm-dropdown-menu { - top: auto; + top: 0px; + left: 0px; } } diff --git a/webapp/src/css/enketo/medic.less b/webapp/src/css/enketo/medic.less index d64dd58455c..f4801f4a7df 100644 --- a/webapp/src/css/enketo/medic.less +++ b/webapp/src/css/enketo/medic.less @@ -45,7 +45,7 @@ border-bottom: 1px solid @separator-color; } h2 { - font-size: @font-extra-extra-large; + font-size: @font-XXL; } h3 { font-size: @font-extra-large; @@ -352,7 +352,7 @@ margin: 10px; } .question-label { - font-size: @font-extra-extra-large; + font-size: @font-XXL; } } .or-appearance-h2 { diff --git a/webapp/src/css/font.less b/webapp/src/css/font.less index 6c1bfb20add..5d2beb8e6a2 100644 --- a/webapp/src/css/font.less +++ b/webapp/src/css/font.less @@ -37,7 +37,7 @@ h4 { } h1 { - font-size: @font-extra-extra-large; + font-size: @font-XXL; } h2 { diff --git a/webapp/src/css/icon.less b/webapp/src/css/icon.less index ec61d82b2a4..dfcbe9b628b 100644 --- a/webapp/src/css/icon.less +++ b/webapp/src/css/icon.less @@ -9,7 +9,8 @@ text-align: center; position: relative; &, &:focus { - .button-text; + .button-text(@white); + text-shadow: none; } &:hover, &.active { .button-text-hover; @@ -18,7 +19,7 @@ .mm-icon-inverse { &, &:focus { - .button-text-inverse; + .button-text-inverse(@nav-icon-gray); } } @@ -31,15 +32,6 @@ } } -.mm-icon-caption { - font-size: @font-small; - font-weight: bold; - p { - margin-top: 5px; - margin-bottom: 5px; - } -} - .loader { margin: 10px auto; position: relative; diff --git a/webapp/src/css/inbox.less b/webapp/src/css/inbox.less index ab20a0ce97b..758447e2528 100644 --- a/webapp/src/css/inbox.less +++ b/webapp/src/css/inbox.less @@ -20,6 +20,7 @@ @import 'about'; @import 'error-log'; @import 'tooltip'; +@import 'old-nav'; .container-fluid { overflow: hidden; @@ -27,64 +28,73 @@ body { overflow-y: hidden; - .app-root.messages { - .header .tabs .selected, - .filters { + .app-root { + &.messages .tool-bar { background-color: @messages-color; } - } - .app-root.tasks { - .header .tabs .selected, - .filters { + &.tasks .tool-bar { background-color: @tasks-color; } - } - .app-root.reports { - .header .tabs .selected, - .filters { + &.reports .tool-bar { background-color: @reports-color; } - } - .app-root.analytics { - .header .tabs .selected, - .filters { + &.analytics .tool-bar { background-color: @analytics-color; } - } - .app-root.contacts { - .header .tabs .selected, - .filters { + &.contacts .tool-bar { background-color: @contacts-color; } - } - .app-root.user { - .header .tabs .selected, - .filters { + &.user .tool-bar { background-color: @admin-color; } - } - .app-root.about, - .app-root.testing, - .app-root.privacy-policy { - .header .tabs .selected, - .filters { - background-color: @help-color; + &.error .tool-bar, + &testing .tool-bar { + background-color: @error-color; + } + + &.about, + &.testing, + &.user, + &.privacy-policy { + .tool-bar { + background-color: @top-header-color; + + .ellipsis-title { + color: @text-inverse-color; + } + + .app-menu-button .mat-icon[fonticon="fa-bars"]:before { + color: @text-inverse-color; + } + } + + .partners > h3 { + margin-top: 100px; + } } - .filters { - height: 0; + + &.testing .page { + padding-bottom: 15px; + input { + margin-bottom: 15px; + } } - .page { - top: 75px; + + &.privacy-policy .page { + padding: 15px; } - .partners > h3 { - margin-top: 100px; + + &.user .page { + padding-top: 15px; } - } - .app-root.error, - .app-root.testing { - .header .tabs .selected, - .filters { - background-color: @error-color; + + &.about, + &.testing, + &.error, + &.privacy-policy { + .page { + padding-left: calc(@tab-navbar-size + 15px); + } } } } @@ -110,226 +120,115 @@ body { .row .inner { max-width: @content-width; - margin: 0 auto; } -.header { - background-color: @top-header-color; - text-align: center; - padding-top: 5px; +.app-root:not(.old-nav) mm-header { + height: 100vh; + width: @tab-navbar-size; + position: fixed; + top: @toolbar-desktop-height; + left: 0; z-index: 3; + overflow: hidden; +} + +.app-root:not(.old-nav) .header { position: relative; + display: block; + z-index: 3; + background-color: @tab-navbar-color; + text-align: center; + height: 100vh; .button-text-inverse; - .inner { - height: 60px; - } - - a { - .button-text-inverse; - outline: 0; - padding-top: 3px; - } - - .mm-icon { - margin: 0 auto; - font-size: 1.5rem; - } - - .logo { - float: left; - opacity: 0.9; - padding: 2px 10px; - img { - height: 50px; - } - } - - .extras, - .tabs { - &, - a { - height: 100%; - } - a { - display: inline-block; - } - } - + .logo, .extras { - float: right; - padding-right: 5px; - - a { - width: @header-bar-extras; - margin-top: 10px; - border-radius: @radius-size @radius-size 0 0; - - .mm-icon { - width: inherit; - } - } - - .dropdown-menu a { - border-radius: 0; - } - - .open > a { - background-color: #ffffff; - - .mm-icon { - color: @top-header-color; - } - } - } - - .dropdown ul a { - width: 100%; - height: auto; - } - - .dropdown.options.open.show { - display: inline !important; // Overriding BS' "display" that uses !important; + display: none; } - .options .dropdown-menu { - width: @header-dropdown-width; - top: 0 !important; // Overriding element.style - left: calc(-1 * (@header-dropdown-width - @header-bar-extras)) !important; // Overriding element.style - margin-top: 30px; - font-size: 0; - a { - text-shadow: none; - padding: 10px 20px; - color: #262626; - margin-top: 0; - white-space: normal; - font-size: 1rem; - } - .disabled a { - color: #999999; - } - .divider { - margin: 0; - } - .sync-status { - margin-top: 10px; - margin-bottom: 10px; - a { - padding-top: 0; - padding-bottom: 0; - color: @label-color; - &:hover { - background-color: transparent; - cursor: default; - } - } - .success { - color: @ok-color; - } - .required { - color: @error-color; - } - } + .inner { + height: 100vh; } .tabs { + display: flex; + flex-direction: column; + justify-content: flex-start; + height: 100%; + padding: 0 15px; overflow: hidden; white-space: nowrap; - .button-label { - top: -5px; - position: relative; - } - a { + .button-text-inverse(@nav-icon-gray); + outline: 0; + text-shadow: none; border-radius: @radius-size @radius-size 0 0; + padding: 0 5px; + margin-top: 30px; cursor: pointer; min-width: 80px; - padding-left: 5px; - padding-right: 5px; - .mm-icon { - display: block; - padding: 5px 0; - - svg, span:not(.mm-badge-overlay):not(.mm-badge), img { - &, &:before { - display: block; - margin: auto; - height: 24px; - max-width: 24px; - overflow: hidden; - } + &:hover, + &:focus { + text-decoration: none; + + .mm-icon, + .button-label { + color: @text-inverse-color; } } &.selected { + text-shadow: none; + .mm-icon { + border-radius: 999px; + background-color: @tab-selected-color; + } + .button-label, .mm-icon:hover, .resource-icon { - .button-text; + .button-text(@white); + text-shadow: none; } } + } - @media (hover: hover) { // Prevents sticky hover state in mobile devices. - &:hover:not(.selected) { - &.messages-tab { - &, - .mm-icon-inverse, - .mm-icon-inverse:hover, - .resource-icon svg * { - color: @messages-color; - fill: @messages-color; - } - } - - &.tasks-tab { - &, - .mm-icon-inverse, - .mm-icon-inverse:hover, - .resource-icon svg * { - color: @tasks-color; - fill: @tasks-color; - } - } - - &.reports-tab { - &, - .mm-icon-inverse, - .mm-icon-inverse:hover, - .resource-icon svg * { - color: @reports-color; - fill: @reports-color; - } - } + a .button-label { + margin-top: 3px; + position: relative; + overflow: hidden; + word-wrap: break-word; + color: @nav-icon-gray; + font-size: @font-XXS; + text-overflow: ellipsis; + } - &.analytics-tab { - &, - .mm-icon-inverse, - .mm-icon-inverse:hover, - .resource-icon svg * { - color: @analytics-color; - fill: @analytics-color; - } - } + a .mm-icon { + display: block; + padding: 8px 0; + height: 34px; + width: 54px; + margin: 0 auto; + font-size: @font-extra-large; + text-shadow: none; - &.contacts-tab { - &, - .mm-icon-inverse, - .mm-icon-inverse:hover, - .resource-icon svg * { - color: @contacts-color; - fill: @contacts-color; - } - } + svg, + span:not(.mm-badge-overlay):not(.mm-badge), + img { + &, &:before { + display: block; + margin: auto; + height: 20px; + max-width: 20px; + overflow: hidden; } } + } - &:hover, - &:focus { - text-decoration: none; - } + a .mm-badge { + margin-right: 0; } } } @@ -341,29 +240,74 @@ body { } } -.filters { - height: 58px; +.tool-bar { + display: flex; + height: @toolbar-desktop-height; white-space: nowrap; padding: 5px 0; + width: 100vw; .navigation { display: none; float: left; text-align: center; + padding-top: 3px; padding-right: 50px; a { - display: inline-block; text-align: center; - color: @text-normal-color; - font-size: @font-extra-extra-large; - float: left; - width: 50px; - line-height: 38px; + color: @filter-text-color; + width: 42px; + height: @font-extra-large; } } .inner { - position: relative; + display: flex; + justify-content: flex-end; + align-items: center; + flex-direction: row-reverse; + width: 100vw; + + mm-search-bar, + contacts-filters, + reports-filters { + flex-grow: 2; + } + + .app-menu-button { + order: 4; + min-width: 80px; + } + + .ellipsis-title { + order: 3; + min-width: 33vw; + display: inline-block; + color: @filter-text-color; + font-size: @font-XXL; + padding-left: 12px; + } + + mm-search-bar { + order: 2; + max-width: 59vw; + } + + mm-reports-more-menu { + order: 1; + } + + mm-messages-more-menu { + display: flex; + flex-grow: 2; + justify-content: flex-end; + } + + .mat-icon-filter { + margin-right: 10px; + width: @icon-extra-small; + color: @filter-icon-color; + } } .filter { @@ -395,36 +339,24 @@ body { width: 100%; } - .basic-filters, - .navigation { - padding-top: 3px; - } - - .basic-filters { - position: absolute; - top: 2px; - left: 0; - right: 0; - display: flex; - .right-side-filter { - margin-left: auto; - .open-filter-label { - color: @filter-text-color; - } - .btn.open-filter { - background-color: transparent; - margin: 0; - &:focus, - &:active { - outline: none; - border: none; - box-shadow: none; - } - .fa { - font-size: @font-extra-large; - color: @filter-icon-color; - padding: 2px 2px 0 0; - } + .right-side-filter { + margin-left: auto; + .open-filter-label { + color: @filter-text-color; + font-size: @font-large; + } + .btn.open-filter { + display: flex; + align-items: center; + background-color: transparent; + margin: 0; + outline: none; + border: none; + box-shadow: none; + .fa { + font-size: @font-extra-large; + color: @filter-icon-color; + padding: 2px 2px 0 0; } } } @@ -460,7 +392,6 @@ body { } } - .reset-filter, .sort-results { line-height: 40px; > a { @@ -468,12 +399,17 @@ body { display: inline-block; text-align: center; padding: 0 10px; + text-decoration: none; &.disabled { cursor: default; color: @inactive-color; } } + + .open-sort-label { + padding: 0 10px; + } } .right-pane { @@ -481,7 +417,7 @@ body { } h2 { - line-height: 40px; + line-height: 41px; overflow: hidden; font-weight: bold; margin: 0; @@ -489,6 +425,21 @@ body { } } +.analytics .tool-bar { + width: 100vw; + .analytics-module { + margin-left: 4px; + } +} + +mm-analytics-filters { + margin-left: auto; + + .mm-dropdown { + display: none; + } +} + .container-fluid { background-color: @background-color; position: absolute; @@ -500,12 +451,13 @@ body { .content { .page { - top: 123px; + top: @toolbar-desktop-height; bottom: 0; position: absolute; width: 100%; max-width: @content-width; - z-index: 2; + z-index: 0; + padding-left: @tab-navbar-size; } .inbox { @@ -539,7 +491,7 @@ body { text-align: center; color: #c2c2c2; text-shadow: 1px 1px 4px rgba(255, 255, 255, 1); - font-size: @font-extra-extra-large; + font-size: @font-XXL; > div { display: flex; @@ -553,6 +505,7 @@ body { .content-pane { height: 100%; } + .message-content-wrapper { height: 100%; .item-content { @@ -692,9 +645,7 @@ body { .inbox-items { padding: 0; background-color: @form-background-color; - border: 1px solid @separator-color; - border-top: 0; - border-bottom: 0; + border-right: 1px solid @separator-color; .alert { margin: 10px; @@ -706,7 +657,7 @@ body { } .loading-status { - margin: 10px; + margin: 10px 10px calc(@tab-navbar-size + 10px); text-align: center; font-size: @font-small; color: @label-color; @@ -934,6 +885,7 @@ a.fa:hover { } .inbox-items .selection-count { + display: none; padding: 5px 10px; margin: 0; background-color: @label-color; @@ -942,183 +894,33 @@ a.fa:hover { font-weight: bold; } -.inbox-items .selection-count, -.general-actions .delete-all { - display: none; -} - .tasks .warning { overflow: initial; color: @high-risk-color; } -.action-container { - overflow: visible; - position: fixed; - bottom: 0; - top: inherit; - z-index: 10; - background: transparent; - pointer-events: none; - - .actions { - max-width: 500px; - background-color: rgba(0, 0, 0, 0.65); - border-color: rgba(0, 0, 0, 0.8); - border-width: 1px; - border-style: solid; - text-align: center; - margin: 0 auto 10px; - pointer-events: auto; - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.63); - display: flex !important; // Support for old design: Overrides Bootstrap element.style - flex-direction: row; - justify-content: center; - min-height: 78px; +.reporting, +.configuration, +.testing { + background-color: #ffffff; + border: 1px solid @separator-color; + border-width: 0 1px; + overflow-y: auto; + overflow-x: hidden; +} - .dropdown-menu { - overflow-y: auto; - max-height: 350px; - width: 100%; - } +.page.scrolling { + overflow-y: auto; + overflow-x: hidden; +} -.left-pane .action-container .dropdown-menu { - top: -3px !important; // Support for old design: Overrides Bootstrap element.style +.clear-both { + clear: both; } -> * { - margin: 10px 0 0; - flex-grow: 1; - max-width: 120px; - outline: 0; - border-bottom: 5px solid transparent; - - > .mm-icon { - display: block; - height: 100%; - width: 100%; - } - } - .mm-icon { - .fa { - font-size: 30px; - } - &:hover { - text-decoration: none; - } - svg { - width: @icon-small; - height: @icon-small; - } - } - - .fa-stack { - width: 30px; - height: 30px; - line-height: inherit; - .fa-plus { - left: 8px; - top: -4px; - text-shadow: 0 0 2px rgba(255, 255, 255, 1); - font-size: 20px; - color: #000000; - } - } - - .sub-actions { - background-color: #ffffff; - height: 56px; - padding: 0; - margin: 0 0 1px 0; - border-radius: 0; - border: none; - display: flex; - max-width: none; - overflow-y: hidden; - box-shadow: 0 -6px 12px rgba(0, 0, 0, 0.4); - - .mm-icon-caption { - border: none; - line-height: 1; - color: @label-color; - max-width: none; - padding: 6px 0 0; - .verify-icon { - display: block; - margin: 0 auto 3px auto; - svg { - width: 24px; - height: 24px; - path { - fill: @inactive-color; - } - } - } - } - - .verify-error.active { - color: @report-review-error-color; - svg path { - fill: @report-review-error-color; - } - } - - .verify-valid.active { - color: @report-review-verified-color; - svg path { - fill: @report-review-verified-color; - } - } - } - } -} - -.messages .action-container .actions > :hover { - border-bottom-color: @messages-color; -} - -.reports .action-container .actions > * { - &:hover, - &.active { - border-bottom-color: @reports-color; - } -} - -.contacts .action-container .actions > :hover { - border-bottom-color: @contacts-color; -} - -.action-container .actions >.mm-icon-disabled:hover { - border-bottom-color: transparent; -} - -.reporting, -.configuration, -.testing { - background-color: #ffffff; - border: 1px solid @separator-color; - border-width: 0 1px; - overflow-y: auto; - overflow-x: hidden; -} - -.page.scrolling { - overflow-y: auto; - overflow-x: hidden; -} - -.clear-both { - clear: both; -} - -.filter-daterangepicker { - padding: 0; - margin: 0; -} - -.daterangepicker.dropdown-menu { - z-index: 1050; -} +.daterangepicker.dropdown-menu { + z-index: 1050; +} .horizontal-options { a { @@ -1176,7 +978,7 @@ mm-fast-action-button.mobile-only { .analytics { .content { .inner { - max-width: 100%; + width: 100%; .page { max-width: 100%; } @@ -1199,11 +1001,12 @@ mm-fast-action-button.mobile-only { &.sidebar-open { margin-right: 0; + z-index: initial; } } } } - .filters { + .tool-bar { .inner { max-width: @content-width; } @@ -1217,6 +1020,7 @@ mm-search-bar { .mm-search-bar-container { display: flex; + align-items: center; justify-content: flex-start; border-radius: 4px; background-color: rgba(255, 255, 255, 0.8); @@ -1241,7 +1045,7 @@ mm-search-bar { } .mm-dropdown-menu { - left: -215px !important; // Support for old design: Overrides Bootstrap element.style + left: -160px !important; // Support for old design: Overrides Bootstrap element.style margin: 0; box-shadow: 0 6px 12px rgb(0 0 0 / 40%); @@ -1279,14 +1083,20 @@ mm-search-bar { margin: 0 3px; font-size: @font-large; position: relative; + display: flex; + align-items: center; &:active { box-shadow: none; } + .open-filter-label { + margin-right: 6px; + } + .filter-counter { color: @chip-text-color; - font-size: @font-extra-extra-small; + font-size: @font-XXXXS; background: @chip-background-color; border-radius: 50%; padding: 1px 5px; @@ -1306,14 +1116,24 @@ mm-search-bar { } .more-options-menu-container { + margin-right: 20px; + .mat-icon[fonticon="fa-ellipsis-v"] { - height: 24px; + height: @more-options-icon-size-desktop; &:before { - font-size: @font-extra-extra-large; + font-size: @font-XXL; } } } +.app-menu-button .mat-icon[fonticon="fa-bars"] { + height: 26px; + &:before { + font-size: @font-XXL; + color: @filter-text-color; + } +} + .verify-report-options-wrapper { .verify-report-options-body { display: flex; @@ -1351,15 +1171,13 @@ mm-search-bar { } } -.filters.tab-bar .inner { - display: flex; - justify-content: flex-end; - align-items: center; +.app-root.sidebar-filter-active .tool-bar .inner { + .ellipsis-title { + min-width: 25vw; + } - mm-search-bar, - contacts-filters, - reports-filters { - flex-grow: 2; + mm-search-bar { + max-width: initial; } } @@ -1375,6 +1193,9 @@ mm-search-bar { right: 0; .fast-action-flat-button { width: 100%; + letter-spacing: normal; + font-size: inherit; + line-height: inherit; } } @@ -1383,7 +1204,7 @@ mm-search-bar { top: 60px; right: 5px; .fast-action-fab-button .mat-icon.fa:before { - color: @mat-invert-icon-color; + color: @mat-inverse-icon-color; } } } @@ -1424,7 +1245,7 @@ mm-search-bar { mat-icon:before { color: @mm-mat-primary; - font-size: @font-extra-extra-large; + font-size: @font-XXL; } .resource-icon { @@ -1442,10 +1263,118 @@ mm-search-bar { -webkit-line-clamp: 2; -webkit-box-orient: vertical; white-space: normal; + letter-spacing: normal; } } } +mm-sidebar-menu .mat-sidenav-container { + z-index: 4; + + .mat-drawer-backdrop { + position: fixed; + } + + .mat-drawer-inner-container { + overflow: hidden; + } + + .mat-sidenav { + width: @sidebar-menu-desktop-width; + min-height: @sidebar-menu-height; + + mat-sidenav-content { + overflow-y: auto; + height: calc(@sidebar-menu-height - @sidebar-menu-header-desktop); + } + } + + mm-panel-header { + background: @top-header-color; + height: @sidebar-menu-header-desktop; + + .panel-header { + justify-content: flex-end; + align-items: center; + flex-direction: row-reverse; + } + + .panel-header-title { + font-size: @font-XXL; + font-weight: bold; + line-height: @font-XXL; + margin-left: 15px; + color: @text-inverse-color; + letter-spacing: normal; + } + + .panel-header-close { + width: 20px; + text-align: center; + + mat-icon { + color: @mat-inverse-icon-color; + } + } + } + + .nav-section { + width: 100%; + + &:not(:first-child) { + border-top: 1px solid @gray-ultra-light; + } + } + + .nav-item { + padding: 0 20px 20px 20px; + color: @text-normal-color; + text-decoration: none; + font-size: @font-medium; + line-height: @font-extra-large; + outline: none; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + + &.success mat-icon:before, + &.success .sync-summary > span:first-child { + color: @success-state-color; + } + + &.required mat-icon:before, + &.required .sync-summary > span:first-child { + color: @failed-state-color; + } + + &.disabled mat-icon:before, + &.disabled > span { + color: @muted-state-color; + } + + .sync-summary { + display: inline-block; + vertical-align: middle; + } + + .last-sync { + font-size: @font-small; + color: @text-secondary-color; + } + + mat-icon { + text-align: center; + margin-right: 15px; + flex-shrink: 0; + } + } + + .nav-item:not(:not(.hidden) ~ .nav-item) { // The first element without .hidden class, compatible with Chrome +90 + padding-top: 20px; + } +} + @media (max-width: @content-width) { mm-search-bar { padding: 4px 8px; @@ -1453,14 +1382,124 @@ mm-search-bar { } @media (max-width: @media-mobile) { + .content .page { + padding-left: 0; + } + .fast-action-trigger:not(.embed-fast-action) { position: fixed; - bottom: 17px; + bottom: calc(@tab-navbar-size + @fab-bottom-margin); right: 18px; top: auto; } + mm-sidebar-menu .mat-sidenav-container { + .mat-sidenav { + width: @sidebar-menu-mobile-width; + } + + .mat-sidenav mat-sidenav-content { + height: calc(@sidebar-menu-height - @sidebar-menu-header-mobile); + } + + mm-panel-header { + height: @sidebar-menu-header-mobile; + .panel-header-title { + font-size: @font-extra-large; + } + } + } + + .app-root:not(.old-nav) { + &.messages, + &.tasks, + &.reports, + &.contacts { + .loading-status { + margin: 10px 10px calc(@tab-navbar-size + 10px); + } + + &.show-content .loading-status { + margin-bottom: 10px; + } + } + + &.about, + &.testing, + &.error, + &.privacy-policy { + .page { + padding-left: 15px; + } + } + + mm-header { + position: relative; + } + + .header { + display: block; + height: @tab-navbar-size; + width: 100vw; + background-color: @tab-navbar-color; + position: fixed; + bottom: 0; + .inner { + height: 100%; + } + + .tabs { + flex-direction: row; + align-items: center; + justify-content: center; + .button-label { + top: 0; + font-size: @font-XXS; + padding-top: 5px; + text-overflow: ellipsis; + margin-top: 0; + } + + a { + min-width: 65px; + margin-top: 1px; + + .mm-icon { + padding: 8px 0; + } + + &.selected + .mm-icon { + border-radius: 999px; + background-color: @tab-selected-color; + } + } + } + } + + &.select-mode { + mm-header, + .app-menu-button, + .ellipsis-title { + display: none; + } + } + + &.error { + mm-header { + display: none; + } + } + + mm-navigation { + display: flex; + align-items: center; + height: @toolbar-mobile-height; + } + } + .more-options-menu-container { + margin-right: 0; padding: 0 5px; } @@ -1488,28 +1527,36 @@ mm-search-bar { height: 60px; } } - .inbox-items .selection-count, - .general-actions .delete-all { + .inbox-items .selection-count { display: block; } .contacts h4 { padding-left: 0; } - .filters { - height: 50px; + .tool-bar { + height: @toolbar-mobile-height; padding: 0; + display: block; + .app-menu-button { + min-width: 0; + } + .navigation { - display: inline-block; + .mat-icon-close, + .mat-icon-back { + width: @icon-XXS; + height: @font-extra-large; + } } + .filter { margin: 0; } - .mm-button { - padding-left: 0; - } + .mm-button-icon { padding: 5px 0 0 5px; } + .right-pane { position: absolute; top: 0; @@ -1520,39 +1567,30 @@ mm-search-bar { } } } + h2 { display: initial; } - .ellipsis-title { - display: inline-block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - width: calc(100vw - 100px); // Give space to "more options" menu - text-align: left; - } + .dropdown, .navigation .mm-button { vertical-align: top; } + .dropdown.analytics-module { width: 80px; } + .mm-button-text { width: 0; visibility: hidden; } - .basic-filters { - left: 2px; - } + mm-freetext-filter { width: auto; .freetext-filter { width: 95%; - &.mobile-dropdown { - display: none; - } } .mobile-freetext-filter { @@ -1561,46 +1599,24 @@ mm-search-bar { padding: 10px; } } - - &.legacy { - // ToDo: Remove when upgrading contact's search design - display: inline-block; - width: auto; - } - } - .reset-filter { - padding-left: 10px; } + .sort-dropdown { position: absolute; right: 0; } } - .header { - .dropdown-menu { - margin: 0; - top: 8px; - right: 5px; - } - .tabs { - display: flex; - flex-direction: row; - a { - flex-grow: 1; - min-width: 70px; - max-width: 100px; - } - } - .logo { - display: none; - } - .extras a { - padding-top: 2px; - margin-top: 2px; - .fa-bars { - font-size: @font-extra-extra-large; - } + .analytics:not(.show-content) .tool-bar { + display: flex; + align-items: center; + } + + .analytics.show-content .tool-bar { + .navigation { + position: relative; + display: block; + width: 100vw; } } @@ -1615,7 +1631,8 @@ mm-search-bar { } .content { .page { - top: 115px; + top: @toolbar-mobile-height; + z-index: initial; } .meta { padding-bottom: 0; @@ -1650,22 +1667,6 @@ mm-search-bar { .item-content .reply .note { display: inline; } - .action-container { - .inner > div { - padding: 0; - position: absolute; - bottom: 0; - width: 100%; - .actions { - box-shadow: none; - border-width: 1px 0 0 0; - margin: 0; - padding: 0; - border-radius: 0; - max-width: 100%; - } - } - } .left-pane { left: 0; @@ -1688,36 +1689,62 @@ mm-search-bar { .app-root.show-content { .header, + .app-menu-button, + .ellipsis-title, + mm-analytics-filters, mm-multiselect-bar, mm-search-bar { display: none; } + .content .page { - top: 46px; + top: @toolbar-mobile-height; } + .left-pane { left: -100%; .fast-action-trigger:not(.embed-fast-action) { display: none; } } + .right-pane { left: 0; } - } - .filter-daterangepicker { - width: 255px; - height: 320px; - .calendar { - transition: left 500ms ease 0s; - position: absolute; - float: none; + .fast-action-trigger:not(.embed-fast-action) { + bottom: @fab-bottom-margin; } - .range_inputs { - display: none; + + .filters.tab-bar .inner { + flex-direction: initial; + } + + .tool-bar .inner { + mm-reports-more-menu, + mm-contacts-more-menu { + order: 0; + margin-left: auto; + } + + .navigation { + display: contents; + height: @toolbar-mobile-height; + padding-top: 0; + } + } + + .navigation .ellipsis-title { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: calc(@content-width - @tool-bar-title-right-margin); // Give space to "more options" menu + text-align: left; + color: @filter-text-color; } } + .show-from { .left { left: 0; @@ -1761,12 +1788,16 @@ mm-search-bar { mm-search-bar { padding: 5px; + #sort-results-dropdown { + left: -215px !important; // Overriding another rule that already has !important + } + .mm-search-bar-container { background: transparent; - justify-content: flex-end; min-height: 40px; - mm-freetext-filter.filter { + mm-freetext-filter.filter, + .open-sort-label { display: none; } @@ -1776,41 +1807,58 @@ mm-search-bar { .btn.open-filter { margin: 0; + padding: 0 5px; .filter-counter { position: absolute; - top: 3px; - right: -2px; - border: 1px solid @chip-border-color; + top: -6px; + right: 1px; } } + } + } - &.open-search { - background-color: rgba(255, 255, 255, 0.8); + .app-root.search-bar-active mm-search-bar { + width: 100%; + padding-left: 5px; + max-width: initial; - mm-freetext-filter { - display: inherit; - } + ~ .app-menu-button, + ~ .ellipsis-title { + display: none; + } - .search-bar-left-icon { - margin: 0 10px; - } + .mm-search-bar-container { + background-color: rgba(255, 255, 255, 0.8); + + mm-freetext-filter { + display: inherit; + } + + .search-bar-left-icon { + margin: 0 10px; } } } - .basic-filters { - // Support for old design: Overrides Bootstrap element.style - .mobile-freetext-filter .dropdown-menu, - .mm-status-filter .dropdown-menu { - top: 0 !important; - left: -100px !important; + .tool-bar .inner { + .app-menu-button { + min-width: initial; } - // Support for old design: Overrides Bootstrap element.style - .sort-dropdown .dropdown-menu { - top: 0 !important; - left: -230px !important; + .ellipsis-title { + text-overflow: ellipsis; + width: calc(@content-width - @tool-bar-title-right-margin); + padding-left: 0; + font-size: @font-extra-large; + } + } + + .app-root.about, + .app-root.testing, + .app-root.privacy-policy { + .page { + margin-bottom: @tab-navbar-size; } } } @@ -1819,43 +1867,14 @@ mm-search-bar { .logo { display: none; } - .filters .filter { + .tool-bar .filter { width: 17%; } .right-side-filter { padding-right: 6px; } - .header { - .tabs { - padding-left: 2px; - a { - min-width: 40px; - .button-label { - font-size: @font-extra-extra-small; - } - } - - // Use bigger font when there are 4 or less tabs. - .elements-label(a, button-label, 4, @font-extra-small); - } - .mm-badge-overlay { - right: 10%; - } - .inner { - height: 60px; - } - } - .action-container .actions { - min-height: 50px; - p { - font-size: @font-extra-extra-small; - } - .mm-icon { - min-height: 50px; - } - } .content .page { - top: 115px; + top: @toolbar-mobile-height; } .mobile-only { display: initial; @@ -1863,11 +1882,5 @@ mm-search-bar { .desktop-only { display: none; } - .app-root.about, - .app-root.testing { - .page { - top: 55px; - } - } } diff --git a/webapp/src/css/material.less b/webapp/src/css/material.less index eaaa5cdbc3e..b92c1926b67 100644 --- a/webapp/src/css/material.less +++ b/webapp/src/css/material.less @@ -7,9 +7,10 @@ html, body { } } -.mat-mdc-button, -.mat-mdc-unelevated-button { +button.mat-mdc-button, +button.mat-mdc-unelevated-button { font-size: @font-medium; + letter-spacing: normal; } .mat-mdc-button:not(:disabled).mat-primary { @@ -33,8 +34,8 @@ html, body { } .mat-icon.fa { - height: 20px; - width: 20px; + height: @icon-extra-small; + width: @icon-extra-small; &:before { font-size: @font-extra-large; color: @mat-icon-color; @@ -42,6 +43,7 @@ html, body { } .mat-mdc-menu-panel { + margin-right: 20px; .mat-mdc-menu-item { font-size: @font-medium; color: @mat-font-color; @@ -65,6 +67,10 @@ html, body { padding: 0 !important; } +.app-menu-button .mat-mdc-button-persistent-ripple { + display: none; +} + /** * Medic's custom component style */ @@ -82,12 +88,16 @@ mm-panel-header { .panel-header-title { font-size: @font-medium; + letter-spacing: normal; margin: 0; } - .panel-header-close { - font-size: @font-extra-large; - margin: 0; + .panel-header-close mat-icon { + width: @icon-XXS; + height: @icon-XXS; + margin-top: -4px; + vertical-align: middle; + color: @mat-icon-color; } } @@ -217,9 +227,10 @@ mm-panel-header { bottom: 0; cursor: default; z-index: 10000; + visibility: hidden; will-change: transform; - transform: translate3d(0, 50px, 0) rotateZ(0deg); - transition: 0.25s linear; + transform: translate3d(0, 20px, 0) rotateZ(0deg); + transition: opacity 0.25s linear, transform 0.25s linear; left: 0px; right: 0px; text-align: center; @@ -260,11 +271,33 @@ mm-panel-header { } } &.active { + visibility: visible; transform: translate3d(0, 0, 0); + transition: visibility 0s, transform 0.25s linear; } } @media (max-width: @media-mobile) { + .app-root:not(.old-nav) #snackbar { + margin: 0 18px; + bottom: calc(@tab-navbar-size + @snackbar-margin); + + .snackbar-content { + border-radius: @radius-size; + } + + &.display-above-fab { + bottom: calc(@tab-navbar-size + @fab-offset-height); + } + } + + .app-root.show-content:not(.old-nav) #snackbar { + bottom: @snackbar-margin; + &.display-above-fab { + bottom: @fab-offset-height; + } + } + .material { .card:not(.compact-card) { border-radius: 0; diff --git a/webapp/src/css/modal.less b/webapp/src/css/modal.less index 213a41fd253..4b2fecb5221 100644 --- a/webapp/src/css/modal.less +++ b/webapp/src/css/modal.less @@ -3,8 +3,9 @@ mm-modal-layout { padding: 24px; border: none; .panel-header-title { - font-size: @font-extra-extra-large; + font-size: @font-XXL; font-weight: normal; + letter-spacing: normal; } } @@ -13,6 +14,7 @@ mm-modal-layout { min-height: 50px; max-height: 75vh; padding: 0 24px; + letter-spacing: normal; textarea { margin: 0; width: 100%; diff --git a/webapp/src/css/old-nav.less b/webapp/src/css/old-nav.less new file mode 100644 index 00000000000..c2d2dae7864 --- /dev/null +++ b/webapp/src/css/old-nav.less @@ -0,0 +1,475 @@ +.app-root.old-nav { + .content .page { + padding-left: 0; + top: @old-nav-top-desktop-height; + } + + &.about .content .inner .page { + padding-top: 0; + } + + .app-menu-button { + display: none; + } + + &.messages { + .header .tabs .selected { + background-color: @messages-color; + } + } + + &.tasks { + .header .tabs .selected { + background-color: @tasks-color; + } + } + + &.reports { + .header .tabs .selected { + background-color: @reports-color; + } + } + + &.analytics { + .header .tabs .selected { + background-color: @analytics-color; + } + } + + &.contacts { + .header .tabs .selected { + background-color: @contacts-color; + } + } + + &.user { + .header .tabs .selected { + background-color: @admin-color; + } + } + + &.error, + &.testing { + .header .tabs .selected { + background-color: @error-color; + } + } + + &.about, + &.testing, + &.privacy-policy, + &.user { + .tool-bar { + height: 0; + } + + .header .tabs .selected { + background-color: @messages-color; + } + + .content { + .page { + top: @top-without-filter; + padding-left: 15px; + padding-top: 15px; + } + } + } + + &.error .page { + padding-left: 15px; + } + + .header { + background-color: @top-header-color; + text-align: center; + position: relative; + display: block; + padding-top: 5px; + .inner { + height: @top-navbar-height; + } + + .logo { + float: left; + opacity: 0.9; + padding: 2px 10px; + img { + height: 50px; + } + } + + .mm-icon { + margin: 0 auto; + + .mm-badge-overlay { + right: 15px; + } + + :before { + font-size: @font-XXL; + } + } + + a { + .button-text-inverse(@nav-icon-gray); + outline: 0; + padding-top: 6px; + + &:hover, &:active, &:focus { + text-decoration: none; + } + } + + .tabs { + .button-label { + color: @button-text-inverse-color; + } + + a { + border-radius: 10px 10px 0 0; + cursor: pointer; + min-width: 80px; + padding-left: 5px; + padding-right: 5px; + + .mm-icon { + display: block; + svg, + span:not(.mm-badge-overlay):not(.mm-badge), + img { + &, &:before { + height: 24px; + max-width: 24px; + } + } + } + + .button-label { + font-size: @font-small; + color: @button-text-inverse-color; + } + + &.selected { + .button-label, + .mm-icon:hover, + .resource-icon { + .button-text; + } + } + } + } + + .extras { + float: right; + padding-right: 5px; + a { + width: @header-bar-extras; + margin-top: 10px; + border-radius: @radius-size @radius-size 0 0; + .mm-icon { + width: inherit; + } + } + + .dropdown-menu a { + border-radius: 0; + } + + .open > a { + background-color: @white; + .mm-icon { + color: @top-header-color; + } + } + } + + .extras, + .tabs { + height: 100%; + a { + height: 100%; + display: inline-block; + } + } + + .dropdown ul a { + width: 100%; + height: auto; + } + + .dropdown.options.open.show { + display: inline !important; // Overriding BS' "display" that uses !important; + } + + .options .dropdown-menu { + width: @header-dropdown-width; + top: 0 !important; // Overriding element.style + left: calc(-1 * (@header-dropdown-width - @header-bar-extras)) !important; // Overriding element.style + margin-top: 30px; + font-size: 0; + + a { + text-shadow: none; + padding: 10px 20px; + color: @gray-ultra-darker; + margin-top: 0; + white-space: normal; + font-size: @font-medium; + } + + .disabled a { + color: @gray-disabled; + } + + .divider { + margin: 0; + } + + .sync-status { + margin-top: 10px; + margin-bottom: 10px; + + a { + padding-top: 0; + padding-bottom: 0; + color: @label-color; + + &:hover { + background-color: transparent; + cursor: default; + } + } + + .success { + color: @ok-color; + } + + .required { + color: @error-color; + } + } + } + } + + .mm-icon { + &, &:focus { + .button-text; + } + + &:hover, &.active { + .button-text-hover; + } + } + + .mm-icon-inverse { + &, &:focus { + .button-text-inverse; + } + } + + .loading-status { + margin: 10px; + } + + .tool-bar { + height: @old-nav-filters-bar-height; + + .inner { + height: @toolbar-mobile-height; + + .ellipsis-title { + display: none; + } + + mm-search-bar { + padding-left: 5px; + max-width: initial; + .fa { + color: @gray-ultra-dark; + } + + .mm-search-bar-container { + justify-content: flex-end; + } + } + mm-messages-more-menu { + display: initial; + } + } + } + + &.analytics .tool-bar .inner { + display: initial; + } + + mm-analytics-filters { + .mm-dropdown { + display: initial; + } + } + + .analytics-sidebar-filter { + display: flex; + align-items: center; + height: @toolbar-mobile-height; + } + + mm-navigation { + display: none; + } + + &.show-content { + mm-navigation { + display: flex; + align-items: center; + } + + .navigation .ellipsis-title { + display: block; + } + } + + .mm-navigation-menu { + display: block; + } + + .powered-by { + margin: 0; + } + + .mm-badge-border { + border: solid 1px @white; + margin: 0; + } +} + +@media (max-width: @media-mobile) { + .app-root.old-nav { + &.about, + &.testing, + &.privacy-policy, + &.user { + .content { + .page { + top: calc(@top-navbar-height + 5px); + margin: 0; + } + } + } + + .header { + background-color: @top-header-color; + position: relative; + .inner { + display: flex; + justify-content: space-between; + width: 100vw; + flex-direction: row-reverse; + flex-wrap: nowrap; + } + + .tabs { + display: flex; + justify-content: space-between; + width: 100%; + padding: 0 0 0 2px; + a { + min-width: 50px; + .button-label { + font-size: @font-XXXS; + } + } + + .mm-badge-overlay { + right: 0; + top: -9px; + } + + .mm-badge-border { + margin: 5px 0; + } + } + + .logo { + display: none; + } + + .dropdown-menu { + margin: 30px 0 0 0; + top: 8px; + right: 5px; + } + + .extras a { + margin-top: 2px; + .fa-bars { + font-size: @font-XXL; + } + } + + #header-dropdown-link { + padding-top: 6px; + } + } + + .tool-bar { + height: @toolbar-mobile-height; + + .ellipsis-title { + color: @text-normal-color; + } + } + + .content { + .page { + top: @old-nav-top-mobile-height; + z-index: initial; + } + } + + .fast-action-trigger:not(.embed-fast-action) { + bottom: 17px; + } + + .targets .target:last-child { + margin-bottom: 10px; + } + } + + #target-aggregates-list { + &.inbox-items { + padding-bottom: 0; + } + } +} + +@media (max-width: @media-small-mobile) { + .old-nav { + .header { + .tabs { + a { + min-width: 50px; + .button-label { + font-size: @font-XXXS; + } + } + + // Use bigger font when there are 4 or less tabs. + .elements-label(a, button-label, 4, @font-extra-small); + } + + .mm-badge-overlay-top { + top: -8px; + } + + .inner { + height: @top-navbar-height; + } + } + + .targets .target:last-child { + margin-bottom: 10px; + } + } +} diff --git a/webapp/src/css/private-vars.less b/webapp/src/css/private-vars.less index 121a5c8487a..77828cd6b1a 100644 --- a/webapp/src/css/private-vars.less +++ b/webapp/src/css/private-vars.less @@ -11,8 +11,13 @@ @gray-medium: #A0A0A0; @gray-medium-dark: #BDBEBF; @gray-dark: #777777; +@grey-darker: #545454; @gray-ultra-dark: #333333; +@gray-ultra-darker: #262626; +@gray-disabled: #999999; +@silver-gray:#A7A9AC; @black: #000000; +@medium-black: #1A1A1A; @blue: #63A2C6; @periwinkle: #7193EE; @yellow: #E9AA22; @@ -22,7 +27,7 @@ @periwinkle-highlight: #F0F4FD; @yellow-highlight: #FCF6E7; @pink-highlight: #FDF1EF; -@teal-highlight: #e4efef; +@teal-highlight: #E4EFEF; @red: #E33030; @yellow-dark: #C78330; @teal-dark: #218E7F; diff --git a/webapp/src/css/sidebar-filter.less b/webapp/src/css/sidebar-filter.less index 3810ad2d6ac..bb405d79384 100644 --- a/webapp/src/css/sidebar-filter.less +++ b/webapp/src/css/sidebar-filter.less @@ -8,11 +8,6 @@ } .sidebar-filter-active { - .inbox.page, - mm-actionbar .inner { - max-width: calc(100vw - @content-margin) !important; - } - .sidebar-filter-wrapper { position: static; right: 0; @@ -86,11 +81,12 @@ } } - .sidebar-close { - font-size: @font-extra-large; - font-weight: lighter; - color: @text-normal-color; - margin-right: -3px; + .sidebar-close mat-icon { + width: @icon-XXS; + height: @icon-XXS; + margin-top: -5px; + vertical-align: middle; + color: @mat-icon-color; } } @@ -282,7 +278,7 @@ &:before { content: '\f096'; // http://fontawesome.io/icon/square-o/ font-family: FontAwesome; - font-size: @font-extra-extra-large; + font-size: @font-XXL; color: @form-field-border; vertical-align: middle; margin: 5px 7px 5px -30px; @@ -336,7 +332,7 @@ } .accordion-facility-check { - font-size: @font-extra-extra-large; + font-size: @font-XXL; color: @checkbox-border-color; display: block; position: absolute; @@ -371,20 +367,9 @@ } } -@media (min-width: @content-width) { - .sidebar-filter-active { - mm-actionbar .inner { - margin-left: @content-margin !important; - } - } -} - @media (max-width: @media-mobile) { - .sidebar-filter-active { - .header, - .action-container { - z-index: 2; - } + .sidebar-filter-active .header { + z-index: 2; } .sidebar-filter { diff --git a/webapp/src/css/targets.less b/webapp/src/css/targets.less index c8aa58cc8c0..c4306c61840 100644 --- a/webapp/src/css/targets.less +++ b/webapp/src/css/targets.less @@ -79,7 +79,7 @@ color: @gray-dark; } label { - font-size: @font-extra-extra-large; + font-size: @font-XXL; line-height: 1; margin-bottom: 0; } @@ -186,8 +186,6 @@ color: @targets-above-goal; } } - - } } @@ -243,7 +241,13 @@ margin: 10px 12px 0 12px; } .target:last-child { - margin-bottom: 10px; + margin-bottom: calc(@tab-navbar-size + 15px); + } + } + + .page { + #target-aggregates-list.inbox-items { + padding-bottom: calc(@tab-navbar-size + 5px); } } } @@ -257,7 +261,7 @@ margin: 10px 12px 0 12px; } .target:last-child { - margin-bottom: 10px; + margin-bottom: calc(@tab-navbar-size + 15px); } } } diff --git a/webapp/src/css/theme.less b/webapp/src/css/theme.less index 00d65ac4359..1132f71705c 100644 --- a/webapp/src/css/theme.less +++ b/webapp/src/css/theme.less @@ -89,6 +89,7 @@ a { padding: 10px; .icon { float: left; + margin-right: 4px; img, svg { width: @icon-small; diff --git a/webapp/src/css/tooltip.less b/webapp/src/css/tooltip.less index c2889155c23..bb333609b6c 100644 --- a/webapp/src/css/tooltip.less +++ b/webapp/src/css/tooltip.less @@ -7,7 +7,6 @@ */ [title] { position: relative; - display: inline-flex; justify-content: left; } diff --git a/webapp/src/css/variables.less b/webapp/src/css/variables.less index a320ed73bcd..3be3de65763 100644 --- a/webapp/src/css/variables.less +++ b/webapp/src/css/variables.less @@ -3,10 +3,12 @@ /* app colors */ @mm-mat-primary: @blue-dark; @mat-icon-color: @gray-ultra-dark; -@mat-invert-icon-color: @white; +@mat-inverse-icon-color: @white; @mat-font-color: @gray-ultra-dark; @top-header-color: @gray-ultra-dark; +@tab-navbar-color: @medium-black; +@tab-selected-color: @grey-darker; @background-color: @gray-ultra-light; @target-card-background-color: @gray-ultra-lighter; @target-card-border-color: @gray-light; @@ -19,6 +21,7 @@ @button-text-inverse-color: @gray-light; @button-background-inverse-color: @white; @text-normal-color: @gray-ultra-dark; +@text-inverse-color: @white; @label-color: @gray-dark; @text-secondary-color: @gray-dark; @text-link-color: @blue-dark; @@ -41,8 +44,8 @@ @chip-text-color: @white; @chip-border-color: @white; @sidebar-background-color: @white; -@filter-icon-color: @gray-ultra-dark; -@filter-text-color: @black; +@filter-icon-color: @medium-black; +@filter-text-color: @medium-black; @filter-secondary-text-color: @gray-medium; @filter-arrow-color: @gray-medium; @progress-bar-background-color: @white; @@ -54,6 +57,7 @@ @tooltip-text: @gray-light; @target-progress-bar-border-color: @white; @targets-progress-bar-background-color: @gray-medium-dark; +@nav-icon-gray: @silver-gray; /* tab colors */ @messages-color: @blue; @@ -72,6 +76,7 @@ /* state colors */ @scheduled-state-color: @blue; @pending-state-color: @yellow; +@success-state-color: @teal-dark; @sent-state-color: @teal-dark; @muted-state-color: @gray-medium; @failed-state-color: @red; @@ -90,29 +95,52 @@ @radius-size: 10px; @radius-size-medium: 8px; @radius-size-small: 4px; -@content-width: 1170px; -@content-margin: calc((100vw - @content-width) / 2); +@content-width: 100vw; @admin-content-width: 1440px; @media-tablet: 985px; @media-mobile: 767px; @media-small-mobile: 400px; @header-dropdown-width: 250px; @header-bar-extras: 40px; +@toolbar-desktop-height: 60px; +@old-nav-filters-bar-height: 58px; +@old-nav-top-desktop-height: 123px; +@old-nav-top-mobile-height: 115px; +@toolbar-mobile-height: 50px; +@tool-bar-title-right-margin: 100px; +@top-navbar-height: 60px; +@top-without-filter: 75px; +@snackbar-margin: 15px; +@fab-bottom-margin: 15px; +@fab-height: 56px; +@fab-offset-height: @fab-height + @fab-bottom-margin + @snackbar-margin; +@sidebar-menu-height: 100vh; +@sidebar-menu-mobile-width: 85vw; +@sidebar-menu-desktop-width: 470px; +@sidebar-menu-header-mobile: @toolbar-mobile-height; +@sidebar-menu-header-desktop: @toolbar-desktop-height; +@tab-navbar-size: 80px; +@more-options-icon-size-mobile: 32px; +@more-options-icon-size-desktop: 24px; /* fonts */ @font-family-main: Noto, sans-serif; /* font sizes */ -@font-extra-extra-large: 1.5rem; +@font-XXL: 1.5rem; @font-extra-large: 1.25rem; @font-large: 1.125rem; @font-medium: 1rem; @font-small: 0.875rem; @font-extra-small: 0.8125rem; -@font-extra-extra-small: 0.625rem; +@font-XXS: 0.75rem; +@font-XXXS: 0.69rem; +@font-XXXXS: 0.625rem; -@icon-small: 30px; @icon-large: 60px; +@icon-small: 30px; +@icon-extra-small: 20px; +@icon-XXS: 17px; /* icons */ @scheduled-state-icon: '\f073'; @@ -133,8 +161,8 @@ @targets-column-gap: 24px; /* functions */ -.button-text() { - color: @button-text-color; +.button-text(@color: @button-text-color) { + color: @color; text-shadow: rgba(255, 255, 255, 0.5) 1px 1px 2px; svg { filter: drop-shadow(1px 1px 2px rgba(255, 255, 255, 0.5)); @@ -165,8 +193,8 @@ } } -.button-text-inverse() { - color: @button-text-inverse-color; +.button-text-inverse(@color: @button-text-inverse-color) { + color: @color; text-shadow: rgba(0, 0, 0, 0.5) 1px 1px 2px; svg { filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.5)); diff --git a/webapp/src/img/icon-back.svg b/webapp/src/img/icon-back.svg new file mode 100644 index 00000000000..957827ae503 --- /dev/null +++ b/webapp/src/img/icon-back.svg @@ -0,0 +1,3 @@ + + + diff --git a/webapp/src/img/icon-close.svg b/webapp/src/img/icon-close.svg new file mode 100644 index 00000000000..5acc90bf22a --- /dev/null +++ b/webapp/src/img/icon-close.svg @@ -0,0 +1,3 @@ + + + diff --git a/webapp/src/img/icon-filter.svg b/webapp/src/img/icon-filter.svg new file mode 100644 index 00000000000..9f5c531025e --- /dev/null +++ b/webapp/src/img/icon-filter.svg @@ -0,0 +1,3 @@ + + + diff --git a/webapp/src/ts/actions/global.ts b/webapp/src/ts/actions/global.ts index 6c8a7110e6b..6ced1873768 100644 --- a/webapp/src/ts/actions/global.ts +++ b/webapp/src/ts/actions/global.ts @@ -10,16 +10,9 @@ export const Actions = { setSnackbarContent: createMultiValueAction('SET_SNACKBAR_CONTENT'), setLoadingContent: createSingleValueAction('SET_LOADING_CONTENT', 'loadingContent'), setShowContent: createSingleValueAction('SET_SHOW_CONTENT', 'showContent'), - setShowActionBar: createSingleValueAction('SET_SHOW_ACTION_BAR', 'showActionBar'), setForms: createSingleValueAction('SET_FORMS', 'forms'), - setLeftActionBar: createSingleValueAction('SET_LEFT_ACTION_BAR', 'left'), - updateLeftActionBar: createSingleValueAction('UPDATE_LEFT_ACTION_BAR', 'left'), - setRightActionBar: createSingleValueAction('SET_RIGHT_ACTION_BAR', 'right'), - setRightActionBarVerified: createSingleValueAction('SET_ACTION_BAR_RIGHT_VERIFIED', 'verified'), - updateRightActionBar: createSingleValueAction('UPDATE_RIGHT_ACTION_BAR', 'right'), clearFilters: createSingleValueAction('CLEAR_FILTERS', 'skip'), setFilter: createSingleValueAction('SET_FILTER', 'filter'), - setFilters: createSingleValueAction('SET_FILTERS', 'filters'), setSidebarFilter: createSingleValueAction('SET_SIDEBAR_FILTER', 'sidebarFilter'), clearSidebarFilter: createAction('CLEAR_SIDEBAR_FILTER'), setSelectMode: createSingleValueAction('SET_SELECT_MODE', 'selectMode'), @@ -34,13 +27,17 @@ export const Actions = { setNavigation: createMultiValueAction('SET_NAVIGATION'), setPreventNavigation: createSingleValueAction('SET_PREVENT_NAVIGATION', 'preventNavigation'), deleteDocConfirm: createSingleValueAction('DELETE_DOC_CONFIRM', 'doc'), // Has Effect - setLoadingSubActionBar: createSingleValueAction('SET_LOADING_SUB_ACTION_BAR', 'loading'), + setProcessingReportVerification: createSingleValueAction('SET_PROCESSING_REPORT_VERIFICATION', 'loading'), setUnreadCount: createSingleValueAction('SET_UNREAD_COUNT', 'unreadCount'), updateUnreadCount: createSingleValueAction('UPDATE_UNREAD_COUNT', 'unreadCount'), setTranslationsLoaded: createAction('SET_TRANSLATIONS_LOADED'), setUserFacilityIds: createSingleValueAction('SET_USER_FACILITY_IDS', 'userFacilityIds'), setUserContactId: createSingleValueAction('SET_USER_CONTACT_ID', 'userContactId'), setTrainingCardFormId: createSingleValueAction('SET_TRAINING_CARD_FORM_ID', 'trainingCardFormId'), + setSidebarMenu: createSingleValueAction('SET_SIDEBAR_MENU', 'sidebarMenu'), + closeSidebarMenu: createAction('CLOSE_SIDEBAR_MENU'), + openSidebarMenu: createAction('OPEN_SIDEBAR_MENU'), + setSearchBar: createSingleValueAction('SET_SEARCH_BAR', 'searchBar'), }; export class GlobalActions { @@ -62,7 +59,7 @@ export class GlobalActions { return this.store.dispatch(Actions.setSnapshotData(snapshotData)); } - setSnackbarContent(message, action?) { + setSnackbarContent(message?, action?) { return this.store.dispatch(Actions.setSnackbarContent({ message, action })); } @@ -78,18 +75,9 @@ export class GlobalActions { return this.store.dispatch(Actions.setShowContent(showContent)); } - setShowActionBar(showActionBar) { - return this.store.dispatch(Actions.setShowActionBar(showActionBar)); - } - settingSelected() { this.store.dispatch(Actions.setLoadingContent(false)); - // todo The original code wrapped these 2 next lines in a $timeout - // I can't see a reason for this, maybe it's because of the actionbar? - // Test if the actionbar appears before the content is loaded, we might need to refactor this action into two - // actions that are called from the component and use lifecycle hooks this.setShowContent(true); - this.setShowActionBar(true); } clearFilters(skip?) { @@ -100,14 +88,14 @@ export class GlobalActions { return this.store.dispatch(Actions.setFilter(filter)); } - setFilters(filters) { - return this.store.dispatch(Actions.setFilters(filters)); - } - setSidebarFilter(sidebarFilter) { return this.store.dispatch(Actions.setSidebarFilter(sidebarFilter)); } + setSearchBar(searchBar) { + return this.store.dispatch(Actions.setSearchBar(searchBar)); + } + clearSidebarFilter() { return this.store.dispatch(Actions.clearSidebarFilter()); } @@ -128,7 +116,6 @@ export class GlobalActions { unsetComponents() { this.setShowContent(false); this.setLoadingContent(false); - this.setShowActionBar(false); this.setTitle(); } @@ -174,30 +161,6 @@ export class GlobalActions { return this.store.dispatch(Actions.deleteDocConfirm(doc)); } - setLeftActionBar(value) { - return this.store.dispatch(Actions.setLeftActionBar(value)); - } - - updateLeftActionBar(value) { - return this.store.dispatch(Actions.updateLeftActionBar(value)); - } - - setRightActionBar(value) { - return this.store.dispatch(Actions.setRightActionBar(value)); - } - - setRightActionBarVerified(verified) { - return this.store.dispatch(Actions.setRightActionBarVerified(verified)); - } - - updateRightActionBar(value) { - return this.store.dispatch(Actions.updateRightActionBar(value)); - } - - clearRightActionBar() { - return this.store.dispatch(Actions.setRightActionBar({})); - } - setPrivacyPolicyAccepted(accepted) { return this.store.dispatch(Actions.setPrivacyPolicyAccepted(accepted)); } @@ -226,8 +189,8 @@ export class GlobalActions { return this.store.dispatch(Actions.navigationCancel(nextUrl)); } - setLoadingSubActionBar(loading) { - return this.store.dispatch(Actions.setLoadingSubActionBar(loading)); + setProcessingReportVerification(loading) { + return this.store.dispatch(Actions.setProcessingReportVerification(loading)); } setUnreadCount(unreadCount) { @@ -253,4 +216,17 @@ export class GlobalActions { setTrainingCardFormId(trainingCard) { return this.store.dispatch(Actions.setTrainingCardFormId(trainingCard)); } + + setSidebarMenu(sidebarMenu) { + return this.store.dispatch(Actions.setSidebarMenu(sidebarMenu)); + } + + openSidebarMenu() { + return this.store.dispatch(Actions.openSidebarMenu()); + } + + closeSidebarMenu() { + return this.store.dispatch(Actions.closeSidebarMenu()); + } + } diff --git a/webapp/src/ts/actions/reports.ts b/webapp/src/ts/actions/reports.ts index 3ff31085e5f..20a03672cc7 100644 --- a/webapp/src/ts/actions/reports.ts +++ b/webapp/src/ts/actions/reports.ts @@ -19,12 +19,9 @@ export const Actions = { launchEditFacilityDialog: createAction('LAUNCH_EDIT_FACILITY_DIALOG'), setSelectedReportDocProperty: createMultiValueAction('SET_SELECTED_REPORT_DOC_PROPERTY'), setSelectedReportFormattedProperty: createMultiValueAction('SET_SELECTED_REPORT_FORMATTED_PROPERTY'), - updateReportsList: createSingleValueAction('UPDATE_REPORTS_LIST', 'reports'), removeReportFromList: createSingleValueAction('REMOVE_REPORT_FROM_LIST', 'report'), resetReportsList: createAction('RESET_REPORTS_LIST'), - - setRightActionBar: createAction('SET_RIGHT_ACTION_BAR_REPORTS'), setTitle: createSingleValueAction('SET_REPORTS_TITLE', 'selected'), }; @@ -77,10 +74,6 @@ export class ReportsActions { return this.store.dispatch(Actions.setVerifyingReport(verifyingReport)); } - setRightActionBar() { - return this.store.dispatch(Actions.setRightActionBar()); - } - setTitle(selected) { return this.store.dispatch(Actions.setTitle(selected)); } @@ -102,11 +95,6 @@ export class ReportsActions { this.store.dispatch(Actions.launchEditFacilityDialog()); } - toggleVerifyingReport() { - this.store.dispatch(Actions.toggleVerifyingReport()); - this.setRightActionBar(); - } - verifyReport(verified) { return this.store.dispatch(Actions.verifyReport(verified)); } diff --git a/webapp/src/ts/app.component.html b/webapp/src/ts/app.component.html index 66d25ea605b..924eaca01c6 100644 --- a/webapp/src/ts/app.component.html +++ b/webapp/src/ts/app.component.html @@ -1,7 +1,10 @@
+ [class.bootstrapped]="initialisationComplete" + [class.select-mode]="selectMode" + [class.sidebar-filter-active]="isSidebarFilterOpen" + [class.search-bar-active]="openSearch" + [class.old-nav]="hasOldNav"> +
@@ -11,15 +14,11 @@
- - +
- - + +
diff --git a/webapp/src/ts/app.component.ts b/webapp/src/ts/app.component.ts index 351d39914cf..3584ea1d8f4 100644 --- a/webapp/src/ts/app.component.ts +++ b/webapp/src/ts/app.component.ts @@ -1,4 +1,5 @@ import { ActivationEnd, ActivationStart, Router } from '@angular/router'; +import { DomSanitizer } from '@angular/platform-browser'; import { MatIconRegistry } from '@angular/material/icon'; import * as moment from 'moment'; import { AfterViewInit, Component, HostListener, NgZone, OnInit } from '@angular/core'; @@ -18,7 +19,6 @@ import { LocationService } from '@mm-services/location.service'; import { ModalService } from '@mm-services/modal.service'; import { ReloadingComponent } from '@mm-modals/reloading/reloading.component'; import { FeedbackService } from '@mm-services/feedback.service'; -import { environment } from '@mm-environments/environment'; import { FormatDateService } from '@mm-services/format-date.service'; import { XmlFormsService } from '@mm-services/xml-forms.service'; import { JsonFormsService } from '@mm-services/json-forms.service'; @@ -45,12 +45,11 @@ import { AnalyticsModulesService } from '@mm-services/analytics-modules.service' import { AnalyticsActions } from '@mm-actions/analytics'; import { TrainingCardsService } from '@mm-services/training-cards.service'; import { FormService } from '@mm-services/form.service'; -import { OLD_REPORTS_FILTER_PERMISSION } from '@mm-modules/reports/reports-filters.component'; -import { OLD_ACTION_BAR_PERMISSION } from '@mm-components/actionbar/actionbar.component'; import { BrowserDetectorService } from '@mm-services/browser-detector.service'; import { BrowserCompatibilityComponent } from '@mm-modals/browser-compatibility/browser-compatibility.component'; import { PerformanceService } from '@mm-services/performance.service'; import { UserSettings, UserSettingsService } from '@mm-services/user-settings.service'; +import { OLD_NAV_PERMISSION } from '@mm-components/header/header.component'; const SYNC_STATUS = { inProgress: { @@ -88,17 +87,22 @@ export class AppComponent implements OnInit, AfterViewInit { currentTab = ''; privacyPolicyAccepted; isSidebarFilterOpen = false; + openSearch = false; showPrivacyPolicy; selectMode; adminUrl; canLogOut; replicationStatus; androidAppVersion; - reportForms; unreadCount = {}; - useOldActionBar = false; + hasOldNav = false; initialisationComplete = false; - trainingCardFormId = false; + trainingCardFormId = ''; + private readonly SVG_ICONS = new Map([ + ['icon-close', './img/icon-close.svg'], + ['icon-filter', './img/icon-filter.svg'], + ['icon-back', './img/icon-back.svg'], + ]); constructor ( private dbSyncService:DBSyncService, @@ -114,6 +118,7 @@ export class AppComponent implements OnInit, AfterViewInit { private locationService:LocationService, private modalService:ModalService, private router:Router, + private domSanitizer: DomSanitizer, private feedbackService:FeedbackService, private formatDateService:FormatDateService, private xmlFormsService:XmlFormsService, @@ -143,15 +148,10 @@ export class AppComponent implements OnInit, AfterViewInit { ) { this.globalActions = new GlobalActions(store); this.analyticsActions = new AnalyticsActions(store); - - this.matIconRegistry.registerFontClassAlias('fontawesome', 'fa'); - this.matIconRegistry.setDefaultFontSetClass('fa'); + this.registerMaterialIcons(); moment.locale(['en']); - this.formatDateService.init(); - this.adminUrl = this.locationService.adminPath; - setBootstrapTheme('bs4'); } @@ -165,6 +165,17 @@ export class AppComponent implements OnInit, AfterViewInit { }); } + private registerMaterialIcons() { + this.matIconRegistry.registerFontClassAlias('fontawesome', 'fa'); + this.matIconRegistry.setDefaultFontSetClass('fa'); + + this.SVG_ICONS.forEach((iconPath, iconName) => { + // Disabling Sonar because we trust the SVG_ICONS defined as readonly above + const iconUrl = this.domSanitizer.bypassSecurityTrustResourceUrl(iconPath); //NoSONAR + this.matIconRegistry.addSvgIcon(iconName, iconUrl); + }); + } + private setupRouter() { const getTab = (snapshot) => { let tab; @@ -319,7 +330,7 @@ export class AppComponent implements OnInit, AfterViewInit { } ngAfterViewInit() { - this.enableOldActionBar(); + this.enableOldNav(); this.subscribeToSideFilterStore(); } @@ -381,7 +392,7 @@ export class AppComponent implements OnInit, AfterViewInit { this.updateServiceWorker.update(() => this.ngZone.run(() => this.showUpdateReady())); } else { - !environment.production && this.globalActions.setSnackbarContent(`${change.id} changed`); + console.debug(`${change.id} changed`); this.showUpdateReady(); } }, @@ -451,17 +462,19 @@ export class AppComponent implements OnInit, AfterViewInit { this.store.select(Selectors.getAndroidAppVersion), this.store.select(Selectors.getCurrentTab), this.store.select(Selectors.getSelectMode), + this.store.select(Selectors.getSearchBar), ]).subscribe(([ replicationStatus, androidAppVersion, currentTab, selectMode, + searchBar, ]) => { this.replicationStatus = replicationStatus; this.androidAppVersion = androidAppVersion; - this.currentTab = currentTab; - + this.currentTab = currentTab || ''; this.selectMode = selectMode; + this.openSearch = !!searchBar?.isOpen; }); combineLatest([ @@ -475,7 +488,7 @@ export class AppComponent implements OnInit, AfterViewInit { ]) => { this.showPrivacyPolicy = showPrivacyPolicy; this.privacyPolicyAccepted = privacyPolicyAccepted; - this.trainingCardFormId = trainingCardFormId; + this.trainingCardFormId = trainingCardFormId || ''; this.displayTrainingCards(); }); @@ -499,19 +512,13 @@ export class AppComponent implements OnInit, AfterViewInit { } private async subscribeToSideFilterStore() { - const isDisabled = !this.sessionService.isAdmin() && await this.authService.has(OLD_REPORTS_FILTER_PERMISSION); - - if (isDisabled) { - return; - } - this.store .select(Selectors.getSidebarFilter) .subscribe(({ isOpen }) => this.isSidebarFilterOpen = !!isOpen); } - private async enableOldActionBar() { - this.useOldActionBar = !this.sessionService.isAdmin() && await this.authService.has(OLD_ACTION_BAR_PERMISSION); + private async enableOldNav() { + this.hasOldNav = !this.sessionService.isAdmin() && await this.authService.has(OLD_NAV_PERMISSION); } private initForms() { @@ -556,25 +563,6 @@ export class AppComponent implements OnInit, AfterViewInit { } ); - // ToDo: remove when deprecating Action Bar Component. This subscribe gets the forms for the Add Report action. - this.xmlFormsService.subscribe( - 'AddReportMenu', - { reportForms: true }, - (err, xForms) => { - if (err) { - return console.error('Error fetching form definitions', err); - } - this.reportForms = xForms - .map((xForm) => ({ - id: xForm._id, - code: xForm.internalId, - icon: xForm.icon, - title: translateTitle(xForm.translation_key, xForm.title), - })) - .sort((a, b) => a.title - b.title); - } - ); - // Get forms for training cards and display the cards if necessary this.trainingCardsService.initTrainingCards(); }) diff --git a/webapp/src/ts/components/actionbar/actionbar.component.html b/webapp/src/ts/components/actionbar/actionbar.component.html deleted file mode 100644 index dc7c5723b3e..00000000000 --- a/webapp/src/ts/components/actionbar/actionbar.component.html +++ /dev/null @@ -1,269 +0,0 @@ - diff --git a/webapp/src/ts/components/actionbar/actionbar.component.ts b/webapp/src/ts/components/actionbar/actionbar.component.ts deleted file mode 100644 index 78236dc514d..00000000000 --- a/webapp/src/ts/components/actionbar/actionbar.component.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { combineLatest, Subscription } from 'rxjs'; -import { Store } from '@ngrx/store'; - -import { GlobalActions } from '@mm-actions/global'; -import { ReportsActions } from '@mm-actions/reports'; -import { Selectors } from '@mm-selectors/index'; - -export const OLD_ACTION_BAR_PERMISSION:string = 'can_view_old_action_bar'; - -@Component({ - selector: 'mm-actionbar', - templateUrl: './actionbar.component.html' -}) -export class ActionbarComponent implements OnInit, OnDestroy { - @Input() reportForms = []; - private subscription: Subscription = new Subscription(); - private globalActions; - private reportsActions; - - currentTab; - snapshotData; - selectMode; - selectedReportDoc; - actionBar; - showActionBar; - loadingContent; - loadingSubActionBar; - selectedContactDoc; - sidebarFilter; - - constructor(private store: Store) { - this.globalActions = new GlobalActions(store); - this.reportsActions = new ReportsActions(store); - } - - ngOnInit(): void { - const subscription = combineLatest( - this.store.select(Selectors.getActionBar), - this.store.select(Selectors.getCurrentTab), - this.store.select(Selectors.getSnapshotData), - this.store.select(Selectors.getLoadingContent), - this.store.select(Selectors.getLoadingSubActionBar), - this.store.select(Selectors.getSelectMode), - this.store.select(Selectors.getShowActionBar), - this.store.select(Selectors.getSidebarFilter), - this.store.select(Selectors.getSelectedReportDoc), - this.store.select(Selectors.getSelectedContactDoc), - ).subscribe(([ - actionBar, - currentTab, - snapshotData, - loadingContent, - loadingSubActionBar, - selectMode, - showActionBar, - sidebarFilter, - selectedReportDoc, - selectedContactDoc - ]) => { - this.currentTab = currentTab; - this.snapshotData = snapshotData; - this.selectMode = selectMode; - this.actionBar = actionBar; - this.showActionBar = showActionBar; - this.sidebarFilter = sidebarFilter; - this.loadingContent = loadingContent; - this.loadingSubActionBar = loadingSubActionBar; - this.selectedReportDoc = selectedReportDoc; - this.selectedContactDoc = selectedContactDoc; - }); - this.subscription.add(subscription); - } - - ngOnDestroy() { - this.subscription.unsubscribe(); - } - - verifyReport(reportIsVerified) { - this.reportsActions.verifyReport(reportIsVerified); - } - - toggleVerifyingReport() { - this.reportsActions.toggleVerifyingReport(); - } - - launchEditFacilityDialog() { - this.reportsActions.launchEditFacilityDialog(); - } - - deleteDoc(doc) { - this.globalActions.deleteDocConfirm(doc); - } - - trackById(idx, item) { - return item.id; - } -} diff --git a/webapp/src/ts/components/components.module.ts b/webapp/src/ts/components/components.module.ts index 59fc82de0a7..f983ff0dd06 100644 --- a/webapp/src/ts/components/components.module.ts +++ b/webapp/src/ts/components/components.module.ts @@ -9,6 +9,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatBottomSheetModule } from '@angular/material/bottom-sheet'; import { MatDialogModule } from '@angular/material/dialog'; import { MatListModule } from '@angular/material/list'; +import { MatSidenavModule } from '@angular/material/sidenav'; import { HeaderComponent } from '@mm-components/header/header.component'; import { PipesModule } from '@mm-pipes/pipes.module'; @@ -19,21 +20,16 @@ import { ReportVerifyValidIconComponent, ReportVerifyInvalidIconComponent } from '@mm-components/status-icons/status-icons.template'; -import { - MultiDropdownFilterComponent -} from '@mm-components/filters/multi-dropdown-filter/multi-dropdown-filter.component'; import { DateFilterComponent } from '@mm-components/filters/date-filter/date-filter.component'; import { FacilityFilterComponent } from '@mm-components/filters/facility-filter/facility-filter.component'; import { FormTypeFilterComponent } from '@mm-components/filters/form-type-filter/form-type-filter.component'; import { FastActionButtonComponent } from '@mm-components/fast-action-button/fast-action-button.component'; import { StatusFilterComponent } from '@mm-components/filters/status-filter/status-filter.component'; import { FreetextFilterComponent } from '@mm-components/filters/freetext-filter/freetext-filter.component'; -import { ResetFiltersComponent } from '@mm-components/filters/reset-filters/reset-filters.component'; import { SortFilterComponent } from '@mm-components/filters/sort-filter/sort-filter.component'; import { SenderComponent } from '@mm-components/sender/sender.component'; import { ReportImageComponent } from '@mm-components/report-image/report-image.component'; import { NavigationComponent } from '@mm-components/navigation/navigation.component'; -import { ActionbarComponent } from '@mm-components/actionbar/actionbar.component'; import { EnketoComponent } from '@mm-components/enketo/enketo.component'; import { SearchBarComponent } from '@mm-components/search-bar/search-bar.component'; import { MultiselectBarComponent } from '@mm-components/multiselect-bar/multiselect-bar.component'; @@ -48,6 +44,8 @@ import { MobileDetectionComponent } from '@mm-components/mobile-detection/mobile import { ErrorLogComponent } from '@mm-components/error-log/error-log.component'; import { ModalLayoutComponent } from '@mm-components/modal-layout/modal-layout.component'; import { PanelHeaderComponent } from '@mm-components/panel-header/panel-header.component'; +import { SidebarMenuComponent } from '@mm-components/sidebar-menu/sidebar-menu.component'; +import { ToolBarComponent } from '@mm-components/tool-bar/tool-bar.component'; @NgModule({ declarations: [ @@ -56,7 +54,6 @@ import { PanelHeaderComponent } from '@mm-components/panel-header/panel-header.c ContentRowListItemComponent, ReportVerifyValidIconComponent, ReportVerifyInvalidIconComponent, - MultiDropdownFilterComponent, DateFilterComponent, FacilityFilterComponent, FormTypeFilterComponent, @@ -65,12 +62,10 @@ import { PanelHeaderComponent } from '@mm-components/panel-header/panel-header.c FastActionButtonComponent, SearchBarComponent, MultiselectBarComponent, - ResetFiltersComponent, SortFilterComponent, SenderComponent, ReportImageComponent, NavigationComponent, - ActionbarComponent, EnketoComponent, AnalyticsTargetsProgressComponent, AnalyticsFilterComponent, @@ -79,6 +74,8 @@ import { PanelHeaderComponent } from '@mm-components/panel-header/panel-header.c ErrorLogComponent, ModalLayoutComponent, PanelHeaderComponent, + SidebarMenuComponent, + ToolBarComponent, ], imports: [ CommonModule, @@ -92,6 +89,7 @@ import { PanelHeaderComponent } from '@mm-components/panel-header/panel-header.c MatBottomSheetModule, MatDialogModule, MatListModule, + MatSidenavModule, BsDropdownModule, ], exports: [ @@ -108,12 +106,10 @@ import { PanelHeaderComponent } from '@mm-components/panel-header/panel-header.c SearchBarComponent, MultiselectBarComponent, FreetextFilterComponent, - ResetFiltersComponent, SortFilterComponent, SenderComponent, ReportImageComponent, NavigationComponent, - ActionbarComponent, EnketoComponent, AnalyticsTargetsProgressComponent, ErrorLogComponent, @@ -121,6 +117,8 @@ import { PanelHeaderComponent } from '@mm-components/panel-header/panel-header.c AnalyticsTargetsDetailsComponent, ModalLayoutComponent, PanelHeaderComponent, + SidebarMenuComponent, + ToolBarComponent, ] }) export class ComponentsModule { } diff --git a/webapp/src/ts/components/fast-action-button/fast-action-button.component.html b/webapp/src/ts/components/fast-action-button/fast-action-button.component.html index 0b96f0cfeab..6bafbb570fa 100644 --- a/webapp/src/ts/components/fast-action-button/fast-action-button.component.html +++ b/webapp/src/ts/components/fast-action-button/fast-action-button.component.html @@ -1,6 +1,6 @@
diff --git a/webapp/src/ts/components/fast-action-button/fast-action-button.component.ts b/webapp/src/ts/components/fast-action-button/fast-action-button.component.ts index 166ba03c0fd..05da87c9f15 100644 --- a/webapp/src/ts/components/fast-action-button/fast-action-button.component.ts +++ b/webapp/src/ts/components/fast-action-button/fast-action-button.component.ts @@ -8,9 +8,6 @@ import { filter } from 'rxjs/operators'; import { ResponsiveService } from '@mm-services/responsive.service'; import { FastAction, IconType } from '@mm-services/fast-action-button.service'; -import { OLD_ACTION_BAR_PERMISSION } from '@mm-components/actionbar/actionbar.component'; -import { SessionService } from '@mm-services/session.service'; -import { AuthService } from '@mm-services/auth.service'; import { Selectors } from '@mm-selectors/index'; @Component({ @@ -28,7 +25,6 @@ export class FastActionButtonComponent implements OnInit, OnDestroy { private bottomSheetRef: MatBottomSheetRef | undefined; selectMode = false; - useOldActionBar = false; iconTypeResource = IconType.RESOURCE; iconTypeFontAwesome = IconType.FONT_AWESOME; buttonTypeFlat = ButtonType.FLAT; @@ -36,15 +32,12 @@ export class FastActionButtonComponent implements OnInit, OnDestroy { constructor( private store: Store, private router: Router, - private authService: AuthService, - private sessionService: SessionService, private responsiveService: ResponsiveService, private matBottomSheet: MatBottomSheet, private matDialog: MatDialog, ) { } ngOnInit() { - this.checkPermissions(); this.subscribeToStore(); this.subscribeToRouter(); } @@ -67,10 +60,6 @@ export class FastActionButtonComponent implements OnInit, OnDestroy { this.subscriptions.add(selectModeSubscription); } - private async checkPermissions() { - this.useOldActionBar = !this.sessionService.isAdmin() && await this.authService.has(OLD_ACTION_BAR_PERMISSION); - } - /** * Returns a Fast Action that can be executed right away without opening the dialog or bottom sheet. */ diff --git a/webapp/src/ts/components/filters/abstract-filter.ts b/webapp/src/ts/components/filters/abstract-filter.ts deleted file mode 100644 index 935d7bbe629..00000000000 --- a/webapp/src/ts/components/filters/abstract-filter.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface AbstractFilter { - disabled; - clear():void; -} diff --git a/webapp/src/ts/components/filters/analytics-filter/analytics-filter.component.html b/webapp/src/ts/components/filters/analytics-filter/analytics-filter.component.html index c69727e0c34..906a70626d5 100644 --- a/webapp/src/ts/components/filters/analytics-filter/analytics-filter.component.html +++ b/webapp/src/ts/components/filters/analytics-filter/analytics-filter.component.html @@ -1,6 +1,5 @@ -
- - +
+ @@ -22,7 +21,7 @@
diff --git a/webapp/src/ts/components/filters/analytics-filter/analytics-filter.component.ts b/webapp/src/ts/components/filters/analytics-filter/analytics-filter.component.ts index 57c05f28317..01967891a79 100644 --- a/webapp/src/ts/components/filters/analytics-filter/analytics-filter.component.ts +++ b/webapp/src/ts/components/filters/analytics-filter/analytics-filter.component.ts @@ -14,12 +14,9 @@ import { Subscription, filter } from 'rxjs'; import { GlobalActions } from '@mm-actions/global'; import { Selectors } from '@mm-selectors/index'; -import { AuthService } from '@mm-services/auth.service'; import { SessionService } from '@mm-services/session.service'; import { TelemetryService } from '@mm-services/telemetry.service'; import { TargetAggregatesService } from '@mm-services/target-aggregates.service'; -import { OLD_REPORTS_FILTER_PERMISSION } from '@mm-modules/reports/reports-filters.component'; -import { OLD_ACTION_BAR_PERMISSION } from '@mm-components/actionbar/actionbar.component'; import { AGGREGATE_TARGETS_ID } from '@mm-services/analytics-modules.service'; @Component({ @@ -40,7 +37,6 @@ export class AnalyticsFilterComponent implements AfterContentInit, AfterContentC private store: Store, private route: ActivatedRoute, private router: Router, - private authService: AuthService, private sessionService: SessionService, private telemetryService: TelemetryService, private targetAggregatesService: TargetAggregatesService, @@ -99,17 +95,6 @@ export class AnalyticsFilterComponent implements AfterContentInit, AfterContentC this.subscriptions.add(routeSubscription); } - private checkPermissions() { - const permissions = [ - OLD_REPORTS_FILTER_PERMISSION, - OLD_ACTION_BAR_PERMISSION - ]; - - return this.authService - .has(permissions) - .then((permissions) => permissions === false); - } - private isTargetAggregates() { return this.getCurrentModuleId() === AGGREGATE_TARGETS_ID; } @@ -120,15 +105,9 @@ export class AnalyticsFilterComponent implements AfterContentInit, AfterContentC private async canDisplayFilterButton() { const isAdmin = this.sessionService.isAdmin(); - const [checkPermissions, isTargetAggregateEnabled] = await Promise.all([ - this.checkPermissions(), - this.isTargetAggregateEnabled(), - ]); - - this.showFilterButton = !isAdmin && - checkPermissions && - this.isTargetAggregates() && - isTargetAggregateEnabled; + const isTargetAggregateEnabled = await this.isTargetAggregateEnabled(); + + this.showFilterButton = !isAdmin && this.isTargetAggregates() && isTargetAggregateEnabled; } openSidebar() { diff --git a/webapp/src/ts/components/filters/date-filter/date-filter.component.ts b/webapp/src/ts/components/filters/date-filter/date-filter.component.ts index bb7ad429cf8..102278c1bf1 100644 --- a/webapp/src/ts/components/filters/date-filter/date-filter.component.ts +++ b/webapp/src/ts/components/filters/date-filter/date-filter.component.ts @@ -10,16 +10,14 @@ interface LocaleWithWeekSpec extends moment.Locale { } import { GlobalActions } from '@mm-actions/global'; -import { AbstractFilter } from '@mm-components/filters/abstract-filter'; -import { ResponsiveService } from '@mm-services/responsive.service'; import { Selectors } from '@mm-selectors/index'; @Component({ selector: 'mm-date-filter', templateUrl: './date-filter.component.html' }) -export class DateFilterComponent implements OnInit, OnDestroy, AbstractFilter, AfterViewInit { - private globalActions; +export class DateFilterComponent implements OnInit, OnDestroy, AfterViewInit { + private globalActions: GlobalActions; private subscription: Subscription = new Subscription(); inputLabel; error?: string; @@ -29,7 +27,6 @@ export class DateFilterComponent implements OnInit, OnDestroy, AbstractFilter, A }; @Input() disabled; - @Input() isRange; @Input() isStartDate; @Input() fieldId; @Output() search: EventEmitter = new EventEmitter(); @@ -37,7 +34,6 @@ export class DateFilterComponent implements OnInit, OnDestroy, AbstractFilter, A constructor( private store: Store, - private responsiveService: ResponsiveService, private datePipe: DatePipe, ) { this.globalActions = new GlobalActions(store); @@ -57,8 +53,8 @@ export class DateFilterComponent implements OnInit, OnDestroy, AbstractFilter, A ngAfterViewInit() { const datepicker:any = $(`#${this.fieldId}`).daterangepicker( { - singleDatePicker: !this.isRange, - startDate: this.isRange ? moment().subtract(1, 'months') : moment(), + singleDatePicker: true, + startDate: moment(), endDate: moment(), maxDate: moment(), opens: 'center', @@ -90,9 +86,6 @@ export class DateFilterComponent implements OnInit, OnDestroy, AbstractFilter, A }); datepicker.on('hide.daterangepicker', (element, picker) => { - if (this.isRange) { - return; - } let date = this.isStartDate ? this.dateRange.from : this.dateRange.to; if (!date) { date = moment(); @@ -104,21 +97,6 @@ export class DateFilterComponent implements OnInit, OnDestroy, AbstractFilter, A picker.setEndDate(date); } }); - - datepicker.on('mm.dateSelected.daterangepicker', (e, picker) => { - if (this.responsiveService.isMobile() && this.isRange) { - // mobile version - only show one calendar at a time - if (picker.container.is('.show-from')) { - picker.container.removeClass('show-from').addClass('show-to'); - } else { - picker.container.removeClass('show-to').addClass('show-from'); - } - } - }); - - if (this.isRange) { - $('.daterangepicker').addClass('filter-daterangepicker mm-dropdown-menu show-from'); - } } private setError(error) { @@ -137,27 +115,17 @@ export class DateFilterComponent implements OnInit, OnDestroy, AbstractFilter, A return true; } - applyFilter(dateRange, skipSearch?) { + applyFilter(dateRange) { if (!this.validateDateRange(dateRange)) { return; } this.globalActions.setFilter({ date: dateRange }); - if (skipSearch) { - // ToDo: Backward compatibility with the "reports-filters" component, remove this "skipSearch" - // once we delete that component. The new "mm-reports-sidebar-filter" doesn't need it. - return; - } - this.search.emit(); } private createDateRange(from, to) { - if (this.isRange) { - return { from, to }; - } - if (this.isStartDate) { return { ...this.dateRange, from }; } @@ -173,12 +141,12 @@ export class DateFilterComponent implements OnInit, OnDestroy, AbstractFilter, A rect.right <= (window.innerWidth || document.documentElement.clientWidth); } - clear(skipSearch?) { + clear() { if (this.disabled) { return; } - this.applyFilter(undefined, skipSearch); + this.applyFilter(undefined); } countSelected() { @@ -188,22 +156,17 @@ export class DateFilterComponent implements OnInit, OnDestroy, AbstractFilter, A setLabel(dateRange) { this.inputLabel = ''; - const divider = ' - '; const format = 'd MMM'; const dates = { from: dateRange.from ? this.datePipe.transform(dateRange.from, format) : undefined, to: dateRange.to ? this.datePipe.transform(dateRange.to, format) : undefined, }; - if (dates.from && (this.isRange || this.isStartDate)) { + if (dates.from && this.isStartDate) { this.inputLabel += dates.from; } - if (this.isRange && dates.to) { - this.inputLabel += divider; - } - - if (dates.to && (this.isRange || !this.isStartDate)) { + if (dates.to && !this.isStartDate) { this.inputLabel += dates.to; } } diff --git a/webapp/src/ts/components/filters/facility-filter/facility-filter.component.html b/webapp/src/ts/components/filters/facility-filter/facility-filter.component.html index 6fe2d705e3c..e3126b12ee6 100644 --- a/webapp/src/ts/components/filters/facility-filter/facility-filter.component.html +++ b/webapp/src/ts/components/filters/facility-filter/facility-filter.component.html @@ -1,29 +1,9 @@
- - -
    - - -
-
- -
    +
      + [ngTemplateOutletContext]="{ facility: facility, facilityDepth: 0 }">
@@ -33,40 +13,42 @@ let-facility="facility" let-facilityDepth="facilityDepth" let-parent="parent" - let-selectedParent="selectedParent" - let-filter="filter"> + let-selectedParent="selectedParent"> + diff --git a/webapp/src/ts/components/filters/facility-filter/facility-filter.component.ts b/webapp/src/ts/components/filters/facility-filter/facility-filter.component.ts index 5ab940c7f50..1f94aaba200 100644 --- a/webapp/src/ts/components/filters/facility-filter/facility-filter.component.ts +++ b/webapp/src/ts/components/filters/facility-filter/facility-filter.component.ts @@ -1,67 +1,42 @@ -import { - Component, - EventEmitter, - Output, - ViewChild, - Input, - OnInit, - NgZone, - AfterViewChecked, - AfterViewInit -} from '@angular/core'; +import { Component, EventEmitter, Output, Input, OnInit, AfterViewInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { Subscription } from 'rxjs'; -import { flatten as _flatten, sortBy as _sortBy } from 'lodash-es'; +import { sortBy as _sortBy } from 'lodash-es'; import { GlobalActions } from '@mm-actions/global'; -import { - MultiDropdownFilterComponent, - MultiDropdownFilter, -} from '@mm-components/filters/multi-dropdown-filter/multi-dropdown-filter.component'; import { PlaceHierarchyService } from '@mm-services/place-hierarchy.service'; -import { AbstractFilter } from '@mm-components/filters/abstract-filter'; import { SessionService } from '@mm-services/session.service'; import { TranslateService } from '@mm-services/translate.service'; -import { InlineFilter } from '@mm-components/filters/inline-filter'; +import { Filter } from '@mm-components/filters/filter'; import { Selectors } from '@mm-selectors/index'; @Component({ selector: 'mm-facility-filter', templateUrl: './facility-filter.component.html' }) -export class FacilityFilterComponent implements OnInit, AfterViewInit, AbstractFilter, AfterViewChecked { - private globalActions; +export class FacilityFilterComponent implements OnInit, AfterViewInit { + private globalActions: GlobalActions; private isOnlineOnly; - inlineFilter: InlineFilter; + filter: Filter; facilities: Record[] = []; - flattenedFacilities: any[] = []; displayedFacilities: Record[] = []; private totalFacilitiesDisplayed = 0; - private listHasScroll = false; private togglingFacilities = false; - private scrollEventListenerAdded = false; - private displayNewFacilityQueued = false; - private readonly MAX_LIST_HEIGHT = 300; // this is set in CSS private subscriptions: Subscription = new Subscription(); @Input() disabled; - @Input() inline; @Input() fieldId; @Output() search: EventEmitter = new EventEmitter(); - - // initialize variable to avoid change detection errors - @ViewChild(MultiDropdownFilterComponent) dropdownFilter = new MultiDropdownFilter(); constructor( private store:Store, private placeHierarchyService:PlaceHierarchyService, private translateService:TranslateService, - private ngZone:NgZone, private sessionService:SessionService, ) { this.globalActions = new GlobalActions(store); - this.inlineFilter = new InlineFilter(this.applyFilter.bind(this)); + this.filter = new Filter(this.applyFilter.bind(this)); } ngOnInit() { @@ -69,9 +44,7 @@ export class FacilityFilterComponent implements OnInit, AfterViewInit, AbstractF } ngAfterViewInit() { - if (this.inline) { - this.subscribeToSidebarStore(); - } + this.subscribeToSidebarStore(); } private subscribeToSidebarStore() { @@ -79,7 +52,7 @@ export class FacilityFilterComponent implements OnInit, AfterViewInit, AbstractF .select(Selectors.getSidebarFilter) .subscribe(sidebarFilter => { if (sidebarFilter?.isOpen && !this.facilities?.length) { - this.loadFacilities(); + return this.loadFacilities(); } }); this.subscriptions.add(subscription); @@ -96,12 +69,7 @@ export class FacilityFilterComponent implements OnInit, AfterViewInit, AbstractF .get() .then((hierarchy = []) => { this.facilities = this.sortHierarchyAndAddFacilityLabels(hierarchy); - if (this.inline) { - this.displayedFacilities = this.facilities; - } else { - this.flattenedFacilities = _flatten(this.facilities.map(facility => this.getFacilitiesRecursive(facility))); - this.displayOneMoreFacility(); - } + this.displayedFacilities = this.facilities; }) .catch(err => console.error('Error loading facilities', err)); } @@ -115,52 +83,6 @@ export class FacilityFilterComponent implements OnInit, AfterViewInit, AbstractF this.displayedFacilities = this.facilities.slice(0, this.totalFacilitiesDisplayed); } - private addOnScrollEventListener() { - if (this.scrollEventListenerAdded || !this.facilities.length) { - return; - } - - this.scrollEventListenerAdded = true; - this.ngZone.runOutsideAngular(() => { - $('#facility-dropdown-list').on('scroll', (event) => { - // the scroll event is triggered for every scrolled pixel. - // don't queue displaying another facility if the previous one hasn't yet been displayed - if (this.displayNewFacilityQueued) { - return; - } - // visible height + pixel scrolled >= total height - 100 - if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) { - this.displayNewFacilityQueued = true; - setTimeout(() => { - this.ngZone.run(() => this.displayOneMoreFacility()); - }); - } - }); - }); - } - - ngAfterViewChecked() { - if (this.inline) { - return; - } - // add the scroll event listener after we have a list element to attach it to! - this.addOnScrollEventListener(); - - // we've displayed the queued facility within this change detection cycle, next scroll should load one more - this.displayNewFacilityQueued = false; - - // keep displaying facilities until we have a scroll or we've displayed all - if (!this.listHasScroll && this.facilities.length && this.totalFacilitiesDisplayed < this.facilities.length) { - const listHeight = $('#facility-dropdown-list')[0]?.scrollHeight; - const hasScroll = listHeight > this.MAX_LIST_HEIGHT; - if (!hasScroll) { - setTimeout(() => this.displayOneMoreFacility()); - } else { - this.listHasScroll = true; - } - } - } - async setDefault(facility) { if (!facility) { // Should avoid dead-ends and apply empty filter. @@ -170,7 +92,7 @@ export class FacilityFilterComponent implements OnInit, AfterViewInit, AbstractF const descendants = await this.placeHierarchyService.getDescendants(facility._id, true); const descendantIds = descendants.map(descendant => descendant.doc._id); - this.toggle(facility._id, [ facility._id, ...descendantIds ], this.inlineFilter); + this.toggle(facility._id, [ facility._id, ...descendantIds ]); } private sortHierarchyAndAddFacilityLabels(hierarchy) { @@ -219,17 +141,17 @@ export class FacilityFilterComponent implements OnInit, AfterViewInit, AbstractF return facilities; } - private toggle(facilityId, hierarchy:string[], filter) { + private toggle(facilityId, hierarchy:string[]) { this.togglingFacilities = true; - const newToggleValue = !filter.selected.has(facilityId); + const newToggleValue = !this.filter.selected.has(facilityId); // Exclude places with already correct toggle state, then toggle the rest. hierarchy - .filter(facilityId => filter.selected.has(facilityId) !== newToggleValue) - .forEach(facilityId => filter.toggle(facilityId)); + .filter(facilityId => this.filter.selected.has(facilityId) !== newToggleValue) + .forEach(facilityId => this.filter.toggle(facilityId)); this.togglingFacilities = false; - this.applyFilter(Array.from(filter.selected)); + this.applyFilter(Array.from(this.filter.selected) as string[]); } itemLabel(facility) { @@ -245,20 +167,15 @@ export class FacilityFilterComponent implements OnInit, AfterViewInit, AbstractF return; } - if (this.inline) { - this.inlineFilter.clear(); - return; - } - - this.dropdownFilter?.clear(false); + this.filter.clear(); } countSelected() { - return this.inline && this.inlineFilter?.countSelected(); + return this.filter?.countSelected(); } - select(selectedParent, facility, filter, isCheckBox = false) { - if (!isCheckBox && this.inline) { + select(selectedParent, facility, isCheckBox = false) { + if (!isCheckBox) { facility.toggle = !facility.toggle; return; } @@ -270,6 +187,6 @@ export class FacilityFilterComponent implements OnInit, AfterViewInit, AbstractF const hierarchy = this .getFacilitiesRecursive(facility) .map(descendant => descendant.doc._id); - this.toggle(facility.doc._id, hierarchy, filter); + this.toggle(facility.doc._id, hierarchy); } } diff --git a/webapp/src/ts/components/filters/inline-filter.ts b/webapp/src/ts/components/filters/filter.ts similarity index 95% rename from webapp/src/ts/components/filters/inline-filter.ts rename to webapp/src/ts/components/filters/filter.ts index 3c46146d598..f454efb61aa 100644 --- a/webapp/src/ts/components/filters/inline-filter.ts +++ b/webapp/src/ts/components/filters/filter.ts @@ -1,4 +1,4 @@ -export class InlineFilter { +export class Filter { applyCallback:Function; selected = new Set(); diff --git a/webapp/src/ts/components/filters/form-type-filter/form-type-filter.component.html b/webapp/src/ts/components/filters/form-type-filter/form-type-filter.component.html index 86ea07e4f7c..1f0a8d24f4f 100644 --- a/webapp/src/ts/components/filters/form-type-filter/form-type-filter.component.html +++ b/webapp/src/ts/components/filters/form-type-filter/form-type-filter.component.html @@ -1,34 +1,15 @@
- - -
    - - -
-
+
- - -
  • - - {{ form.title || form.code }} - -
  • -
    diff --git a/webapp/src/ts/components/filters/form-type-filter/form-type-filter.component.ts b/webapp/src/ts/components/filters/form-type-filter/form-type-filter.component.ts index b8e569ca58e..c45cff364c8 100644 --- a/webapp/src/ts/components/filters/form-type-filter/form-type-filter.component.ts +++ b/webapp/src/ts/components/filters/form-type-filter/form-type-filter.component.ts @@ -1,49 +1,30 @@ -import { - Component, - EventEmitter, - OnDestroy, - Output, - ViewChild, - Input, - OnInit -} from '@angular/core'; +import { Component, EventEmitter, OnDestroy, Output, Input, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { sortBy as _sortBy } from 'lodash-es'; import { Subscription } from 'rxjs'; import { Selectors } from '@mm-selectors/index'; import { GlobalActions } from '@mm-actions/global'; -import { - MultiDropdownFilterComponent, - MultiDropdownFilter, -} from '@mm-components/filters/multi-dropdown-filter/multi-dropdown-filter.component'; -import { AbstractFilter } from '@mm-components/filters/abstract-filter'; -import { InlineFilter } from '@mm-components/filters/inline-filter'; +import { Filter } from '@mm-components/filters/filter'; @Component({ selector: 'mm-form-type-filter', templateUrl: './form-type-filter.component.html' }) -export class FormTypeFilterComponent implements OnDestroy, OnInit, AbstractFilter { - private globalActions; +export class FormTypeFilterComponent implements OnDestroy, OnInit { + private globalActions: GlobalActions; private formsSubscription; forms; subscriptions: Subscription = new Subscription(); - inlineFilter: InlineFilter; + filter: Filter; @Input() disabled; - @Input() inline; @Input() fieldId; @Output() search: EventEmitter = new EventEmitter(); - // initialize variable to avoid change detection errors - @ViewChild(MultiDropdownFilterComponent) dropdownFilter = new MultiDropdownFilter(); - - constructor( - private store:Store, - ) { + constructor(private store: Store) { this.globalActions = new GlobalActions(store); - this.inlineFilter = new InlineFilter(this.applyFilter.bind(this)); + this.filter = new Filter(this.applyFilter.bind(this)); } ngOnInit() { @@ -92,15 +73,10 @@ export class FormTypeFilterComponent implements OnDestroy, OnInit, AbstractFilte return; } - if (this.inline) { - this.inlineFilter.clear(); - return; - } - - this.dropdownFilter?.clear(false); + this.filter.clear(); } countSelected() { - return this.inline && this.inlineFilter?.countSelected(); + return this.filter?.countSelected(); } } diff --git a/webapp/src/ts/components/filters/freetext-filter/freetext-filter.component.html b/webapp/src/ts/components/filters/freetext-filter/freetext-filter.component.html index 65e09be8d06..75859b638e3 100644 --- a/webapp/src/ts/components/filters/freetext-filter/freetext-filter.component.html +++ b/webapp/src/ts/components/filters/freetext-filter/freetext-filter.component.html @@ -1,4 +1,4 @@ - + - - - - - - - - - - - - diff --git a/webapp/src/ts/components/filters/freetext-filter/freetext-filter.component.ts b/webapp/src/ts/components/filters/freetext-filter/freetext-filter.component.ts index 6b98cc1f0d3..a9ff0c7cd17 100644 --- a/webapp/src/ts/components/filters/freetext-filter/freetext-filter.component.ts +++ b/webapp/src/ts/components/filters/freetext-filter/freetext-filter.component.ts @@ -4,26 +4,22 @@ import { Subscription } from 'rxjs'; import { Selectors } from '@mm-selectors/index'; import { GlobalActions } from '@mm-actions/global'; -import { AbstractFilter } from '@mm-components/filters/abstract-filter'; @Component({ selector: 'mm-freetext-filter', templateUrl: './freetext-filter.component.html' }) -export class FreetextFilterComponent implements OnDestroy, OnInit, AbstractFilter { +export class FreetextFilterComponent implements OnDestroy, OnInit { private globalActions; subscription: Subscription = new Subscription(); inputText; @Input() disabled; - @Input() mobileDropdown; @Output() search: EventEmitter = new EventEmitter(); @ViewChild('freetextInput') inputElement; - constructor( - private store: Store, - ) { + constructor(private store: Store) { this.globalActions = new GlobalActions(store); } diff --git a/webapp/src/ts/components/filters/multi-dropdown-filter/multi-dropdown-filter.component.html b/webapp/src/ts/components/filters/multi-dropdown-filter/multi-dropdown-filter.component.html deleted file mode 100644 index 39e1dc7caf5..00000000000 --- a/webapp/src/ts/components/filters/multi-dropdown-filter/multi-dropdown-filter.component.html +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/webapp/src/ts/components/filters/multi-dropdown-filter/multi-dropdown-filter.component.ts b/webapp/src/ts/components/filters/multi-dropdown-filter/multi-dropdown-filter.component.ts deleted file mode 100644 index c58ec7ab6c5..00000000000 --- a/webapp/src/ts/components/filters/multi-dropdown-filter/multi-dropdown-filter.component.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; -import { debounce as _debounce } from 'lodash-es'; - -import { AbstractFilter } from '@mm-components/filters/abstract-filter'; -import { TranslateService } from '@mm-services/translate.service'; - -@Component({ - selector: 'multi-dropdown-filter', - templateUrl: './multi-dropdown-filter.component.html' -}) -export class MultiDropdownFilterComponent implements AbstractFilter, OnInit { - @Input() items; - @Input() disabled; - @Input() label; - @Input() itemLabel; - @Input() labelNoFilter; - @Input() labelFilter; - @Input() selectAllLabel; - @Input() clearLabel; - - @Output() applyFilter:EventEmitter = new EventEmitter(); - @Output() onOpen:EventEmitter = new EventEmitter(); - - selected = new Set(); - filterLabel; - - constructor(private translateService:TranslateService) { - this.apply = _debounce(this.apply, 200); - } - - ngOnInit() { - this.filterLabel = this.getLabel(); - } - - onOpenChange(open) { - if (open && this.onOpen) { - this.onOpen.emit(); - } - } - - getLabel() { - if (this.label) { - const state = { - total: this.items, - selected: this.selected, - }; - return this.translateService.get(this.label(state)); - } - - if (this.selected.size === 0 || this.selected.size === this.items.length) { - return this.translateService.get(this.labelNoFilter); - } - - if (this.selected.size === 1) { - const selectedItem = this.selected.entries().next().value[0]; - if (this.itemLabel) { - const label = this.itemLabel(selectedItem); - return label instanceof Promise ? label : Promise.resolve(label); - } - } - - return this.translateService.get(this.labelFilter, { number: this.selected.size }); - } - - private apply() { - this.filterLabel = this.getLabel(); - this.applyFilter.emit(Array.from(this.selected)); - } - - toggle(item) { - if (this.isSelected(item)) { - this.selected.delete(item); - } else { - this.selected.add(item); - } - this.apply(); - } - - isSelected(item) { - return this.selected.has(item); - } - - selectAll() { - if (this.disabled) { - return; - } - this.items.forEach(item => this.selected.add(item)); - this.apply(); - } - - clear(apply=true) { - if (this.disabled) { - return; - } - this.selected.clear(); - if (apply) { - return this.apply(); - } - - this.filterLabel = this.getLabel(); - } -} - -export class MultiDropdownFilter { - selected = new Map(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - clear(apply) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - toggle(element) {} -} diff --git a/webapp/src/ts/components/filters/reset-filters/reset-filters.component.html b/webapp/src/ts/components/filters/reset-filters/reset-filters.component.html deleted file mode 100644 index 39ec6ba0e55..00000000000 --- a/webapp/src/ts/components/filters/reset-filters/reset-filters.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/webapp/src/ts/components/filters/reset-filters/reset-filters.component.ts b/webapp/src/ts/components/filters/reset-filters/reset-filters.component.ts deleted file mode 100644 index 47ca2f64e54..00000000000 --- a/webapp/src/ts/components/filters/reset-filters/reset-filters.component.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; - -@Component({ - selector: 'reset-filters', - templateUrl: './reset-filters.component.html' -}) -export class ResetFiltersComponent { - @Input() disabled; - @Output() resetFilters: EventEmitter = new EventEmitter(); - - reset() { - this.resetFilters.emit(); - } -} diff --git a/webapp/src/ts/components/filters/sort-filter/sort-filter.component.html b/webapp/src/ts/components/filters/sort-filter/sort-filter.component.html index 01aaa7c6759..d509bb314c5 100644 --- a/webapp/src/ts/components/filters/sort-filter/sort-filter.component.html +++ b/webapp/src/ts/components/filters/sort-filter/sort-filter.component.html @@ -5,9 +5,9 @@ title="{{'contacts.results.sort' | translate}}" dropdownToggle (click)="false" - aria-controls="sort-results-dropdown" - > + aria-controls="sort-results-dropdown"> + {{ 'search_bar.sort.label' | translate }}
    + class="dropdown-menu mm-dropdown-menu">
    • diff --git a/webapp/src/ts/components/filters/status-filter/status-filter.component.html b/webapp/src/ts/components/filters/status-filter/status-filter.component.html index fe407724491..f037d4f6882 100644 --- a/webapp/src/ts/components/filters/status-filter/status-filter.component.html +++ b/webapp/src/ts/components/filters/status-filter/status-filter.component.html @@ -1,36 +1,25 @@
      - - -
        - - -
      -
      +
        + -
          - - -
        -
      +
    • + {{ 'status.review.' + status | translate }} +
    • - - -
    • - {{ 'status.review.' + status | translate }} -
    • + - -
    • - {{ 'status.sms.' + status | translate }} -
    • -
      +
    • + {{ 'status.sms.' + status | translate }} +
    • +
    +
    diff --git a/webapp/src/ts/components/filters/status-filter/status-filter.component.ts b/webapp/src/ts/components/filters/status-filter/status-filter.component.ts index 8b8f292d291..bb472ce550e 100644 --- a/webapp/src/ts/components/filters/status-filter/status-filter.component.ts +++ b/webapp/src/ts/components/filters/status-filter/status-filter.component.ts @@ -1,41 +1,27 @@ -import { Component, ViewChild, Output, EventEmitter, Input } from '@angular/core'; +import { Component, Output, EventEmitter, Input } from '@angular/core'; import { Store } from '@ngrx/store'; import { GlobalActions } from '@mm-actions/global'; -import { - MultiDropdownFilterComponent, - MultiDropdownFilter, -} from '@mm-components/filters/multi-dropdown-filter/multi-dropdown-filter.component'; -import { AbstractFilter } from '@mm-components/filters/abstract-filter'; -import { InlineFilter } from '@mm-components/filters/inline-filter'; +import { Filter } from '@mm-components/filters/filter'; @Component({ selector: 'mm-status-filter', templateUrl: './status-filter.component.html' }) -export class StatusFilterComponent implements AbstractFilter { - private globalActions; - inlineFilter: InlineFilter; +export class StatusFilterComponent { + @Input() disabled; + @Input() fieldId; + @Output() search: EventEmitter = new EventEmitter(); + private globalActions: GlobalActions; + filter: Filter; statuses = { valid: ['valid', 'invalid'], verified: ['unverified', 'errors', 'correct'], }; - allStatuses = [...this.statuses.valid, ...this.statuses.verified]; - - @Input() disabled; - @Input() inline; - @Input() fieldId; - @Output() search: EventEmitter = new EventEmitter(); - - // initialize variable to avoid change detection errors - @ViewChild(MultiDropdownFilterComponent) dropdownFilter = new MultiDropdownFilter(); - - constructor( - private store: Store, - ) { + constructor(private store: Store) { this.globalActions = new GlobalActions(store); - this.inlineFilter = new InlineFilter(this.applyFilter.bind(this)); + this.filter = new Filter(this.applyFilter.bind(this)); } private getValidStatus(statuses) { @@ -74,24 +60,15 @@ export class StatusFilterComponent implements AbstractFilter { this.search.emit(); } - getFilterLabel() { - return 'Any status'; - } - clear() { if (this.disabled) { return; } - if (this.inline) { - this.inlineFilter.clear(); - return; - } - - this.dropdownFilter?.clear(false); + this.filter.clear(); } countSelected() { - return this.inline && this.inlineFilter?.countSelected(); + return this.filter?.countSelected(); } } diff --git a/webapp/src/ts/components/header/header.component.ts b/webapp/src/ts/components/header/header.component.ts index 443ef848f44..8f686e719b4 100644 --- a/webapp/src/ts/components/header/header.component.ts +++ b/webapp/src/ts/components/header/header.component.ts @@ -5,13 +5,14 @@ import { combineLatest, Subscription } from 'rxjs'; import { Selectors } from '@mm-selectors/index'; import { SettingsService } from '@mm-services/settings.service'; import { HeaderTab, HeaderTabsService } from '@mm-services/header-tabs.service'; -import { AuthService } from '@mm-services/auth.service'; import { GlobalActions } from '@mm-actions/global'; import { ModalService } from '@mm-services/modal.service'; import { LogoutConfirmComponent } from '@mm-modals/logout/logout-confirm.component'; import { FeedbackComponent } from '@mm-modals/feedback/feedback.component'; import { DBSyncService } from '@mm-services/db-sync.service'; +export const OLD_NAV_PERMISSION = 'can_view_old_navigation'; + @Component({ selector: 'mm-header', templateUrl: './header.component.html' @@ -34,7 +35,6 @@ export class HeaderComponent implements OnInit, OnDestroy { private store: Store, private settingsService: SettingsService, private headerTabsService: HeaderTabsService, - private authService: AuthService, private modalService: ModalService, private dbSyncService: DBSyncService, ) { diff --git a/webapp/src/ts/components/navigation/navigation.component.html b/webapp/src/ts/components/navigation/navigation.component.html index 09a5e9441c1..966fb60bfdc 100644 --- a/webapp/src/ts/components/navigation/navigation.component.html +++ b/webapp/src/ts/components/navigation/navigation.component.html @@ -1,9 +1,13 @@ diff --git a/webapp/src/ts/components/panel-header/panel-header.component.html b/webapp/src/ts/components/panel-header/panel-header.component.html index cdc689998c8..13471b033c3 100644 --- a/webapp/src/ts/components/panel-header/panel-header.component.html +++ b/webapp/src/ts/components/panel-header/panel-header.component.html @@ -1,6 +1,6 @@

    {{ headerTitle | translate }}

    - +
    diff --git a/webapp/src/ts/components/search-bar/search-bar.component.html b/webapp/src/ts/components/search-bar/search-bar.component.html index f1782150e7c..10a8040741d 100644 --- a/webapp/src/ts/components/search-bar/search-bar.component.html +++ b/webapp/src/ts/components/search-bar/search-bar.component.html @@ -1,4 +1,4 @@ -
    +
    @@ -14,7 +14,7 @@ diff --git a/webapp/src/ts/components/search-bar/search-bar.component.ts b/webapp/src/ts/components/search-bar/search-bar.component.ts index 749e237f3c2..a95d189bddd 100644 --- a/webapp/src/ts/components/search-bar/search-bar.component.ts +++ b/webapp/src/ts/components/search-bar/search-bar.component.ts @@ -11,6 +11,7 @@ import { import { Store } from '@ngrx/store'; import { combineLatest, Subscription } from 'rxjs'; +import { GlobalActions } from '@mm-actions/global'; import { Selectors } from '@mm-selectors/index'; import { FreetextFilterComponent } from '@mm-components/filters/freetext-filter/freetext-filter.component'; import { ResponsiveService } from '@mm-services/responsive.service'; @@ -31,6 +32,7 @@ export class SearchBarComponent implements AfterContentInit, AfterViewInit, OnDe @Output() search: EventEmitter = new EventEmitter(); private filters; + private globalActions; subscription: Subscription = new Subscription(); activeFilters: number = 0; openSearch = false; @@ -41,7 +43,9 @@ export class SearchBarComponent implements AfterContentInit, AfterViewInit, OnDe private store: Store, private responsiveService: ResponsiveService, private searchFiltersService: SearchFiltersService, - ) { } + ) { + this.globalActions = new GlobalActions(store); + } ngAfterContentInit() { this.subscribeToStore(); @@ -55,9 +59,11 @@ export class SearchBarComponent implements AfterContentInit, AfterViewInit, OnDe const subscription = combineLatest( this.store.select(Selectors.getSidebarFilter), this.store.select(Selectors.getFilters), - ).subscribe(([sidebarFilter, filters]) => { + this.store.select(Selectors.getSearchBar), + ).subscribe(([sidebarFilter, filters, searchBar]) => { this.activeFilters = sidebarFilter?.filterCount?.total || 0; this.filters = filters; + this.openSearch = !!searchBar?.isOpen; if (!this.openSearch && this.filters?.search) { this.toggleMobileSearch(); @@ -79,9 +85,10 @@ export class SearchBarComponent implements AfterContentInit, AfterViewInit, OnDe return; } - this.openSearch = forcedValue !== undefined ? forcedValue : !this.openSearch; + const isSearchOpen = forcedValue !== undefined ? forcedValue : !this.openSearch; + this.globalActions.setSearchBar({ isOpen: isSearchOpen }); - if (this.openSearch) { + if (isSearchOpen) { // To automatically display the mobile's soft keyboard. setTimeout(() => $('.mm-search-bar-container #freetext').focus()); } @@ -100,6 +107,7 @@ export class SearchBarComponent implements AfterContentInit, AfterViewInit, OnDe } ngOnDestroy() { + this.clear(); this.subscription.unsubscribe(); } } diff --git a/webapp/src/ts/components/sidebar-menu/sidebar-menu.component.html b/webapp/src/ts/components/sidebar-menu/sidebar-menu.component.html new file mode 100644 index 00000000000..f69e702a997 --- /dev/null +++ b/webapp/src/ts/components/sidebar-menu/sidebar-menu.component.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/webapp/src/ts/components/sidebar-menu/sidebar-menu.component.ts b/webapp/src/ts/components/sidebar-menu/sidebar-menu.component.ts new file mode 100644 index 00000000000..3cee129ba70 --- /dev/null +++ b/webapp/src/ts/components/sidebar-menu/sidebar-menu.component.ts @@ -0,0 +1,167 @@ +import { Component, OnDestroy, OnInit, ViewChild, Input } from '@angular/core'; +import { MatSidenav } from '@angular/material/sidenav'; +import { NavigationStart, Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +import { Selectors } from '@mm-selectors/index'; +import { GlobalActions } from '@mm-actions/global'; +import { LocationService } from '@mm-services/location.service'; +import { DBSyncService } from '@mm-services/db-sync.service'; +import { ModalService } from '@mm-services/modal.service'; +import { LogoutConfirmComponent } from '@mm-modals/logout/logout-confirm.component'; +import { FeedbackComponent } from '@mm-modals/feedback/feedback.component'; + +@Component({ + selector: 'mm-sidebar-menu', + templateUrl: './sidebar-menu.component.html', +}) +export class SidebarMenuComponent implements OnInit, OnDestroy { + @Input() canLogOut: boolean = false; + @ViewChild('sidebar') sidebar!: MatSidenav; + private globalActions: GlobalActions; + subscriptions: Subscription = new Subscription(); + replicationStatus; + moduleOptions: MenuOption[] = []; + secondaryOptions: MenuOption[] = []; + adminAppPath: string = ''; + + constructor( + private store: Store, + private locationService: LocationService, + private dbSyncService: DBSyncService, + private modalService: ModalService, + private router: Router, + ) { + this.globalActions = new GlobalActions(store); + } + + ngOnInit() { + this.adminAppPath = this.locationService.adminPath; + this.setModuleOptions(); + this.setSecondaryOptions(); + this.subscribeToStore(); + this.subscribeToRouter(); + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } + + close() { + return this.globalActions.closeSidebarMenu(); + } + + replicate() { + if (this.replicationStatus?.current?.disableSyncButton) { + return; + } + return this.dbSyncService.sync(true); + } + + logout() { + this.modalService.show(LogoutConfirmComponent); + } + + private subscribeToRouter() { + const routerSubscription = this.router.events + .pipe(filter(event => event instanceof NavigationStart)) + .subscribe(() => this.close()); + this.subscriptions.add(routerSubscription); + } + + private subscribeToStore() { + const subscribeReplicationStatus = this.store + .select(Selectors.getReplicationStatus) + .subscribe(replicationStatus => this.replicationStatus = replicationStatus); + this.subscriptions.add(subscribeReplicationStatus); + + const subscribeSidebarMenu = this.store + .select(Selectors.getSidebarMenu) + .subscribe(sidebarMenu => this.sidebar?.toggle(sidebarMenu?.isOpen)); + this.subscriptions.add(subscribeSidebarMenu); + + const subscribePrivacyPolicy = this.store + .select(Selectors.getShowPrivacyPolicy) + .subscribe(showPrivacyPolicy => this.setSecondaryOptions(showPrivacyPolicy)); + this.subscriptions.add(subscribePrivacyPolicy); + } + + private openFeedback() { + this.modalService.show(FeedbackComponent); + } + + private setModuleOptions() { + this.moduleOptions = [ + { + routerLink: 'messages', + icon: 'fa-envelope', + translationKey: 'Messages', + hasPermissions: 'can_view_messages,!can_view_messages_tab' + }, + { + routerLink: 'tasks', + icon: 'fa-flag', + translationKey: 'Tasks', + hasPermissions: 'can_view_tasks,!can_view_tasks_tab' + }, + { + routerLink: 'reports', + icon: 'fa-list-alt', + translationKey: 'Reports', + hasPermissions: 'can_view_reports,!can_view_reports_tab' + }, + { + routerLink: 'contacts', + icon: 'fa-user', + translationKey: 'Contacts', + hasPermissions: 'can_view_contacts,!can_view_contacts_tab' + }, + { + routerLink: 'analytics', + icon: 'fa-bar-chart-o', + translationKey: 'Analytics', + hasPermissions: 'can_view_analytics,!can_view_analytics_tab', + }, + ]; + } + + private setSecondaryOptions(showPrivacyPolicy = false) { + this.secondaryOptions = [ + { + routerLink: 'about', + icon: 'fa-question', + translationKey: 'about', + canDisplay: true, + }, + { + routerLink: 'user', + icon: 'fa-user', + translationKey: 'edit.user.settings', + hasPermissions: 'can_edit_profile' + }, + { + routerLink: 'privacy-policy', + icon: 'fa-lock', + translationKey: 'privacy.policy', + canDisplay: showPrivacyPolicy, + }, + { + icon: 'fa-bug', + translationKey: 'Report Bug', + canDisplay: true, + click: () => this.openFeedback() + }, + ]; + } +} + +interface MenuOption { + icon: string; + translationKey: string; + routerLink?: string; + hasPermissions?: string; + canDisplay?: boolean; + click?: () => void; +} diff --git a/webapp/src/ts/components/snackbar/snackbar.component.html b/webapp/src/ts/components/snackbar/snackbar.component.html index 9647b71c1fb..f715522fa12 100644 --- a/webapp/src/ts/components/snackbar/snackbar.component.html +++ b/webapp/src/ts/components/snackbar/snackbar.component.html @@ -1,4 +1,4 @@ -
    +
    {{message}} {{action.label}} diff --git a/webapp/src/ts/components/snackbar/snackbar.component.ts b/webapp/src/ts/components/snackbar/snackbar.component.ts index 46ef37dcf35..9a5dfa78707 100644 --- a/webapp/src/ts/components/snackbar/snackbar.component.ts +++ b/webapp/src/ts/components/snackbar/snackbar.component.ts @@ -1,6 +1,8 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, NgZone, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { Subscription } from 'rxjs'; +import { filter, delay } from 'rxjs/operators'; import { Selectors } from '@mm-selectors/index'; import { GlobalActions } from '@mm-actions/global'; @@ -10,14 +12,15 @@ import { GlobalActions } from '@mm-actions/global'; changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './snackbar.component.html' }) -export class SnackbarComponent implements OnInit { +export class SnackbarComponent implements OnInit, OnDestroy { private subscription: Subscription = new Subscription(); private readonly SHOW_DURATION = 5000; private readonly ANIMATION_DURATION = 250; private readonly ROUND_TRIP_ANIMATION_DURATION = this.ANIMATION_DURATION * 2; + private readonly WAIT_FOR_FAB = 500; - private globalActions; + private globalActions: GlobalActions; private hideTimeout; private showNextMessageTimeout; private resetMessageTimeout; @@ -25,11 +28,13 @@ export class SnackbarComponent implements OnInit { message; action; active = false; + displayAboveFab = true; constructor( - private store:Store, - private changeDetectorRef:ChangeDetectorRef, - private ngZone:NgZone, + private store: Store, + private changeDetectorRef: ChangeDetectorRef, + private ngZone: NgZone, + private router: Router, ) { this.globalActions = new GlobalActions(store); } @@ -41,20 +46,19 @@ export class SnackbarComponent implements OnInit { } ngOnInit() { + this.subscribeToRoute(); this.changeDetectorRef.detach(); const reduxSubscription = this.store .select(Selectors.getSnackbarContent) .subscribe((snackbarContent) => { if (!snackbarContent?.message) { this.hide(); - return; } const { message, action } = snackbarContent; if (this.active) { this.queueShowMessage(message, action); - return; } @@ -64,6 +68,26 @@ export class SnackbarComponent implements OnInit { this.hide(); } + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + private subscribeToRoute() { + const subscription = this.router.events + .pipe( + delay(this.WAIT_FOR_FAB), + filter(event => event instanceof NavigationEnd), + ).subscribe(() => { + if (!this.active) { + return; + } + this.displayAboveFab = this.isFABDisplayed(); + // Snackbar is running outside Angular's zone (#6719), calling detectChanges to refresh component. + this.changeDetectorRef.detectChanges(); + }); + this.subscription.add(subscription); + } + private queueShowMessage(message, action) { clearTimeout(this.resetMessageTimeout); clearTimeout(this.showNextMessageTimeout); @@ -75,6 +99,7 @@ export class SnackbarComponent implements OnInit { } private show(message, action) { + this.displayAboveFab = this.isFABDisplayed(); clearTimeout(this.hideTimeout); this.hideTimeout = undefined; clearTimeout(this.showNextMessageTimeout); @@ -99,4 +124,8 @@ export class SnackbarComponent implements OnInit { private resetMessage() { this.globalActions.setSnackbarContent(); } + + private isFABDisplayed(): boolean { + return !!$('.fast-action-fab-button:visible').length; + } } diff --git a/webapp/src/ts/components/tool-bar/tool-bar.component.html b/webapp/src/ts/components/tool-bar/tool-bar.component.html new file mode 100644 index 00000000000..90f313dac08 --- /dev/null +++ b/webapp/src/ts/components/tool-bar/tool-bar.component.html @@ -0,0 +1,10 @@ +
    +
    + +

    {{ title | translate }}

    + + +
    +
    diff --git a/webapp/src/ts/components/tool-bar/tool-bar.component.ts b/webapp/src/ts/components/tool-bar/tool-bar.component.ts new file mode 100644 index 00000000000..cf39a071dff --- /dev/null +++ b/webapp/src/ts/components/tool-bar/tool-bar.component.ts @@ -0,0 +1,23 @@ +import { Component, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { GlobalActions } from '@mm-actions/global'; + +@Component({ + selector: 'mm-tool-bar', + templateUrl: './tool-bar.component.html' +}) +export class ToolBarComponent { + private globalActions: GlobalActions; + @Input() title: string = ''; + + constructor( + private store: Store, + ) { + this.globalActions = new GlobalActions(store); + } + + openMenu() { + this.globalActions.openSidebarMenu(); + } +} diff --git a/webapp/src/ts/effects/global.effects.ts b/webapp/src/ts/effects/global.effects.ts index 79b15eb9e9b..66a46be5d0c 100644 --- a/webapp/src/ts/effects/global.effects.ts +++ b/webapp/src/ts/effects/global.effects.ts @@ -13,7 +13,7 @@ import { NavigationConfirmComponent } from '@mm-modals/navigation-confirm/naviga @Injectable() export class GlobalEffects { - private globalActions; + private globalActions: GlobalActions; constructor( private actions$: Actions, @@ -66,7 +66,7 @@ export class GlobalEffects { .toPromise(); } - private navigate(nextUrl: string, cancelCallback: () => void) { + private navigate(nextUrl: string, cancelCallback: (() => void) | null) { try { if (nextUrl) { return this.router.navigateByUrl(nextUrl); @@ -108,4 +108,20 @@ export class GlobalEffects { }), ); }, { dispatch: false }); + + openSidebarMenu = createEffect( + ():any => this.actions$.pipe( + ofType(GlobalActionsList.openSidebarMenu), + tap(() => this.globalActions.setSidebarMenu({ isOpen: true })), + ), + { dispatch: false } + ); + + closeSidebarMenu = createEffect( + ():any => this.actions$.pipe( + ofType(GlobalActionsList.closeSidebarMenu), + tap(() => this.globalActions.setSidebarMenu({ isOpen: false })), + ), + { dispatch: false } + ); } diff --git a/webapp/src/ts/effects/reports.effects.ts b/webapp/src/ts/effects/reports.effects.ts index b2899737bb0..a7932ebe4f1 100644 --- a/webapp/src/ts/effects/reports.effects.ts +++ b/webapp/src/ts/effects/reports.effects.ts @@ -10,7 +10,6 @@ import { ReportViewModelGeneratorService } from '@mm-services/report-view-model- import { Selectors } from '@mm-selectors/index'; import { MarkReadService } from '@mm-services/mark-read.service'; import { DbService } from '@mm-services/db.service'; -import { SendMessageComponent } from '@mm-modals/send-message/send-message.component'; import { ModalService } from '@mm-services/modal.service'; import { EditReportComponent } from '@mm-modals/edit-report/edit-report.component'; import { VerifyReportComponent } from '@mm-modals/verify-report/verify-report.component'; @@ -70,7 +69,7 @@ export class ReportsEffects { this.trackOpenReport = null; } - return of(this.reportActions.setRightActionBar()); + return of(model); }), ); }, { dispatch: false }); @@ -180,50 +179,6 @@ export class ReportsEffects { ); }, { dispatch: false }); - private getContact(id) { - return this.dbService - .get() - .get(id) - .catch(err => { - // log the error but continue anyway - console.error('Error fetching contact for action bar', err); - }); - } - - setRightActionBar = createEffect(() => { - return this.actions$.pipe( - ofType(ReportActionList.setRightActionBar), - withLatestFrom( - this.store.select(Selectors.getSelectMode), - this.store.select(Selectors.getSelectedReportDoc), - this.store.select(Selectors.getVerifyingReport), - ), - tap(([, selectMode, selectedReportDoc, verifyingReport ]) => { - const model:any = {}; - const doc = !selectMode && selectedReportDoc; - if (!doc) { - return this.globalActions.setRightActionBar(model); - } - - model.verified = doc.verified; - model.type = doc.content_type; - model.verifyingReport = verifyingReport; - model.openSendMessageModal = sendTo => this.modalService.show(SendMessageComponent, { data: { to: sendTo } }); - - if (!doc.contact?._id) { - return this.globalActions.setRightActionBar(model); - } - - return this - .getContact(doc.contact._id) - .then(contact => { - model.sendTo = contact; - this.globalActions.setRightActionBar(model); - }); - }) - ); - }, { dispatch: false }); - launchEditFacilityDialog = createEffect(() => { return this.actions$.pipe( ofType(ReportActionList.launchEditFacilityDialog), @@ -249,7 +204,7 @@ export class ReportsEffects { return; } - this.globalActions.setLoadingSubActionBar(true); + this.globalActions.setProcessingReportVerification(true); const promptUserToConfirmVerification = () => { const verificationTranslationKey = verified ? 'reports.verify.valid' : 'reports.verify.invalid'; @@ -303,7 +258,6 @@ export class ReportsEffects { report.doc._id, { verified: newVerified, oldVerified }, ); - this.globalActions.setRightActionBarVerified(newVerified); }); }; @@ -316,7 +270,7 @@ export class ReportsEffects { } }) .catch(err => console.error('Error verifying message', err)) - .finally(() => this.globalActions.setLoadingSubActionBar(false)); + .finally(() => this.globalActions.setProcessingReportVerification(false)); }), ); }, { dispatch: false }); diff --git a/webapp/src/ts/modules/about/about.component.html b/webapp/src/ts/modules/about/about.component.html index ee10efa79a8..7bc77d4065b 100644 --- a/webapp/src/ts/modules/about/about.component.html +++ b/webapp/src/ts/modules/about/about.component.html @@ -1,3 +1,4 @@ +
    diff --git a/webapp/src/ts/modules/analytics/analytics-target-aggregates-sidebar-filter.component.html b/webapp/src/ts/modules/analytics/analytics-target-aggregates-sidebar-filter.component.html index a6971cc2482..550aa460fa6 100644 --- a/webapp/src/ts/modules/analytics/analytics-target-aggregates-sidebar-filter.component.html +++ b/webapp/src/ts/modules/analytics/analytics-target-aggregates-sidebar-filter.component.html @@ -11,7 +11,9 @@ - + + +