diff --git a/CHANGELOG.md b/CHANGELOG.md index 80b6da0707..a28036713d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,59 @@ +## [100.45.2](https://github.com/dhis2/capture-app/compare/v100.45.1...v100.45.2) (2023-11-29) + + +### Bug Fixes + +* [DHIS2-15693] Rules not triggered on program update ([#3472](https://github.com/dhis2/capture-app/issues/3472)) ([2dbca1e](https://github.com/dhis2/capture-app/commit/2dbca1efe36ed0e166d4aea803505c30c79cb35d)) + +## [100.45.1](https://github.com/dhis2/capture-app/compare/v100.45.0...v100.45.1) (2023-11-26) + + +### Bug Fixes + +* **translations:** sync translations from transifex (master) ([8f28703](https://github.com/dhis2/capture-app/commit/8f28703ae8b56cfee283ec1ce0ec2b13dfd91e30)) + +# [100.45.0](https://github.com/dhis2/capture-app/compare/v100.44.7...v100.45.0) (2023-11-20) + + +### Features + +* [DHIS2-13237] Enrollment coordinates in enrollment widget ([#3141](https://github.com/dhis2/capture-app/issues/3141)) ([2f2e52c](https://github.com/dhis2/capture-app/commit/2f2e52c3103e9cb48e77766701a9a5fc9af6ad48)) + +## [100.44.7](https://github.com/dhis2/capture-app/compare/v100.44.6...v100.44.7) (2023-11-19) + + +### Bug Fixes + +* **translations:** sync translations from transifex (master) ([46fb227](https://github.com/dhis2/capture-app/commit/46fb2270b21c4dec57f4447232608d8dd3cfb9af)) + +## [100.44.6](https://github.com/dhis2/capture-app/compare/v100.44.5...v100.44.6) (2023-11-16) + + +### Bug Fixes + +* [DHIS2-15967] allow enrolling TEIs with mandatory TET attributes ([#3455](https://github.com/dhis2/capture-app/issues/3455)) ([cc87a8a](https://github.com/dhis2/capture-app/commit/cc87a8a993001d5b4cea8493a84a6798f481cc5a)) + +## [100.44.5](https://github.com/dhis2/capture-app/compare/v100.44.4...v100.44.5) (2023-11-10) + + +### Bug Fixes + +* [TECH-1627] use only valid combinations of orgUnit and ouMode ([#3405](https://github.com/dhis2/capture-app/issues/3405)) ([20d05f1](https://github.com/dhis2/capture-app/commit/20d05f1602840e8ac1241ee305b796d58f616802)) + +## [100.44.4](https://github.com/dhis2/capture-app/compare/v100.44.3...v100.44.4) (2023-11-10) + + +### Bug Fixes + +* [DHIS2-15383] align mandatory behaviour for all value types ([#3413](https://github.com/dhis2/capture-app/issues/3413)) ([b0eddc7](https://github.com/dhis2/capture-app/commit/b0eddc76b674ceb20df0eb5bcd0672a3c42d3954)) + +## [100.44.3](https://github.com/dhis2/capture-app/compare/v100.44.2...v100.44.3) (2023-11-10) + + +### Bug Fixes + +* [DHIS2-15814] missing orgunit names ([#3449](https://github.com/dhis2/capture-app/issues/3449)) ([488f8c0](https://github.com/dhis2/capture-app/commit/488f8c090680ad47ed693214478a7ef2919b82e7)) + ## [100.44.2](https://github.com/dhis2/capture-app/compare/v100.44.1...v100.44.2) (2023-11-05) diff --git a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget.feature b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget.feature index 480c827c0a..cc60abeef8 100644 --- a/cypress/e2e/EnrollmentPage/StagesAndEventsWidget.feature +++ b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget.feature @@ -6,13 +6,16 @@ Feature: User interacts with Stages and Events Widget Scenario: User can close the Stages and Events Widget Given you open the enrollment page + And the program stages should be displayed When you click the stages and events widget toggle open close button Then the stages and events widget should be closed Scenario: User can close and reopen the Stages and Events Widget Given you open the enrollment page + And the program stages should be displayed + When you click the stages and events widget toggle open close button + Then the stages and events widget should be closed When you click the stages and events widget toggle open close button - And you click the stages and events widget toggle open close button Then the program stages should be displayed Scenario: User can view the list of events @@ -21,15 +24,11 @@ Feature: User interacts with Stages and Events Widget And you see the first 5 events in the table And you see buttons in the footer list - Scenario: User can view more events + Scenario: User can view more events and then view less Given you open the enrollment page which has multiples events and stages When you click show more button in stages&event list Then more events should be displayed And reset button should be displayed - - Scenario: User can reset events - Given you open the enrollment page which has multiples events and stages - When you click show more button in stages&event list And you click reset button Then there should be 5 rows in the table diff --git a/cypress/e2e/MainPage.feature b/cypress/e2e/MainPage.feature index 70a5430db6..980d9fd1e2 100644 --- a/cypress/e2e/MainPage.feature +++ b/cypress/e2e/MainPage.feature @@ -42,3 +42,15 @@ Feature: User interacts with Main page Then you see the opt out component for Child Programme When you opt out to use the new enrollment Dashboard for Child Programme Then you see the opt in component for Child Programme + + @v<41 + Scenario: The icon is rendered as an svg + Given you are in the main page with no selections made + When you select Child Programme + Then the icon is rendered as an svg + + @v>=41 + Scenario: The icon is rendered as a custom icon + Given you are in the main page with no selections made + When you select Child Programme + Then the icon is rendered as a custom icon diff --git a/cypress/e2e/MainPage/index.js b/cypress/e2e/MainPage/index.js index f004659122..6090ac6fb5 100644 --- a/cypress/e2e/MainPage/index.js +++ b/cypress/e2e/MainPage/index.js @@ -20,6 +20,18 @@ And('you can load the view with the name Events assigned to me', () => { }); }); +Then('the icon is rendered as a custom icon', () => { + cy.get('[alt="child_program_positive"]') + .invoke('attr', 'src') + .should('match', /\/icons\/child_program_positive\/icon$/); +}); + +Then('the icon is rendered as an svg', () => { + cy.get('[alt="child_program_positive"]') + .invoke('attr', 'src') + .should('match', /\/icons\/child_program_positive\/icon.svg$/); +}); + Then('the TEI working list is displayed', () => { cy.get('[data-test="tei-working-lists"]').within(() => { cy.contains('Rows per page').should('exist'); diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js index 676a73b3bb..be1a87296a 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js @@ -15,7 +15,7 @@ Then('the enrollment widget should be closed', () => { Then('the enrollment widget should be opened', () => { cy.get('[data-test="widget-enrollment"]').within(() => { - cy.get('[data-test="widget-contents"]').children().should('exist'); + cy.get('[data-test="widget-enrollment-contents"]').children().should('exist'); }); }); diff --git a/i18n/cs.po b/i18n/cs.po index 4e2e113dd9..171ec3d5b0 100644 --- a/i18n/cs.po +++ b/i18n/cs.po @@ -43,7 +43,7 @@ msgstr "" "mějte však na paměti, že se tím zavřou další verze." msgid "View {{programName}} dashboard" -msgstr "" +msgstr "Zobrazit {{programName}} ovládací panel" msgid "View dashboard" msgstr "Zobrazit ovládací panel" @@ -80,7 +80,7 @@ msgstr "chyba" msgid "" "Plugins are not yet available - Please contact your system administrator" -msgstr "" +msgstr "Pluginy zatím nejsou k dispozici – obraťte se na správce systému" msgid "This value is validating" msgstr "Tato hodnota se ověřuje" @@ -161,13 +161,13 @@ msgid "Complete event" msgstr "Dokončit událost" msgid "{{ stageName }} - Basic info" -msgstr "" +msgstr "{{ stageName }} - Základní informace" msgid "{{ stageName }} - Status" -msgstr "" +msgstr "{{ stageName }} - Stav" msgid "Please select {{categoryName}}" -msgstr "" +msgstr "Vyberte prosím {{categoryName}}" msgid "A future date is not allowed" msgstr "Budoucí datum není povoleno" @@ -185,10 +185,10 @@ msgid "Metadata error. see log for details" msgstr "Chyba metadat. viz log pro podrobnosti" msgid "{{ stageName }} - Details" -msgstr "" +msgstr "{{ stageName }} - Podrobnosti" msgid "{{ stageName }} - {{ sectionName }}" -msgstr "" +msgstr "{{ stageName }} - {{ sectionName }}" msgid "Assigned user" msgstr "Přiřazený uživatel" @@ -395,6 +395,8 @@ msgid "" "This event has unsaved changes. Leaving this page without saving will lose " "these changes. Are you sure you want to discard unsaved changes?" msgstr "" +"Tato událost obsahuje neuložené změny. Opuštěním této stránky bez uložení " +"tyto změny ztratíte. Opravdu chcete zahodit neuložené změny?" msgid "No events to display" msgstr "Žádné události k zobrazení" @@ -534,6 +536,9 @@ msgstr "začněte psát pro vyhledávání" msgid "suggestions could not be retrieved" msgstr "návrhy nelze načíst" +msgid "No results found" +msgstr "Nenalezeny žádné výsledky" + msgid "No items to display" msgstr "Žádné položky k zobrazení" @@ -607,13 +612,13 @@ msgid "Write comment" msgstr "Napsat komentář" msgid "was blanked out and hidden by your last action" -msgstr "" +msgstr "byla vymazána a skryta vaší poslední akcí" msgid "Notice" -msgstr "" +msgstr "Oznámení" msgid "Close the notice" -msgstr "" +msgstr "Zavřít oznámení" msgid "Use new Enrollment dashboard for {{programName}}" msgstr "Použít nový ovládací panel zápisu pro {{programName}}" @@ -629,18 +634,29 @@ msgid "" "functionality in Capture is ongoing and will be added in upcoming app " "releases." msgstr "" +"Kliknutím na přihlášení níže začnete používat nový ovládací panel registrace" +" v aplikaci Capture pro tento program Tracker. V současné době existuje " +"určitá funkce z aplikace Tracker Capture, která ještě nebyla přidána, včetně" +" funkcí vztahů a doporučení. Práce na zahrnutí této funkce sledování do " +"Capture pokračují a budou přidány v nadcházejících vydáních aplikace." msgid "" "The core team appreciates any feedback on this new functionality which is " "currently being beta tested, please report any issues and feedback in the " "DHIS2 JIRA project." msgstr "" +"Základní tým oceňuje jakoukoli zpětnou vazbu k této nové funkcionalitě, " +"která je v současné době beta testována, nahlaste prosím jakékoli problémy a" +" zpětnou vazbu v projektu DHIS2 JIRA." msgid "" "Click the button below to opt-in to the new enrollment dashboard " "functionality in the Capture app (beta) for this Tracker program for all " "users." msgstr "" +"Kliknutím na tlačítko níže se přihlásíte k nové funkci ovládacího panelu " +"registrace v aplikaci Capture (beta) pro tento program Tracker pro všechny " +"uživatele." msgid "Yes, opt in" msgstr "Ano, přihlásit se" @@ -662,6 +678,7 @@ msgstr "" msgid "" "An error occurred while fetching enrollments. Please enter a valid url." msgstr "" +"Při načítání registrací došlo k chybě. Zadejte prosím platnou adresu URL." msgid "Enrollment Dashboard" msgstr "Ovládací panel zápisu" @@ -704,19 +721,19 @@ msgstr "" "vyberte všechny kategorie." msgid "Invalid enrollment id {{enrollmentId}}." -msgstr "" +msgstr "Neplatné id zápisu {{enrollmentId}}." msgid "Choose an enrollment to view the dashboard." msgstr "Chcete-li zobrazit ovládací panel, vyberte zápis." msgid "There are no active enrollments." -msgstr "" +msgstr "Nejsou žádné aktivní zápisy." msgid "Add new enrollment for {{teiDisplayName}} in this program." -msgstr "" +msgstr "Přidejte nový zápis pro {{teiDisplayName}} v tomto programu." msgid "No access to program owner." -msgstr "" +msgstr "Žádný přístup k vlastníkovi programu." msgid "{{teiDisplayName}} is not enrolled in this program." msgstr "{{teiDisplayName}} není v tomto programu zapsán." @@ -783,7 +800,7 @@ msgid "Refer" msgstr "Odkazovat" msgid "You can't add any more {{ programStageName }} events" -msgstr "" +msgstr "Nemůžete přidat žádné další události {{ programStageName }}" msgid "Cancel without saving" msgstr "Zrušit bez uložení" @@ -847,7 +864,7 @@ msgid "New Enrollment in program{{escape}} {{programName}}" msgstr "Nový zápis do programu{{escape}} {{programName}}" msgid "Save {{trackedEntityTypeName}}" -msgstr "" +msgstr "Uložit {{trackedEntityTypeName}}" msgid "Save {{trackedEntityName}}" msgstr "Uložit {{trackedEntityName}}" @@ -956,14 +973,11 @@ msgid "Search by attributes" msgstr "Hledání podle atributů" msgid "Could not retrieve metadata. Please try again later." -msgstr "" +msgstr "Nepodařilo se načíst metadata. Prosím zkuste to znovu později." msgid "Possible duplicates found" msgstr "Byly nalezeny možné duplikáty" -msgid "No results found" -msgstr "Nenalezeny žádné výsledky" - msgid "An error occurred loading possible duplicates" msgstr "Při načítání možných duplikátů došlo k chybě" @@ -977,28 +991,28 @@ msgid "Add relationship" msgstr "Přidat vztah" msgid "No results found for " -msgstr "" +msgstr "Pro dotaz nebyly nalezeny žádné výsledky" msgid "Registering unit" msgstr "Registrační jednotka" msgid "Choose a registering unit" -msgstr "" +msgstr "Vyberte registrační jednotku" msgid "Clear selection" -msgstr "" +msgstr "Vymazat výběr" msgid "No programs available." msgstr "Nejsou k dispozici žádné programy." msgid "Search for a program" -msgstr "" +msgstr "Vyhledat program" msgid "Some programs are being filtered by the chosen registering unit" -msgstr "" +msgstr "Některé programy jsou filtrovány vybranou registrační jednotkou" msgid "Show all programs" -msgstr "" +msgstr "Zobrazit všechny programy" msgid "Choose a program" msgstr "Vyberte program" @@ -1074,7 +1088,7 @@ msgid "Cannot search in all programs" msgstr "Nelze vyhledávat ve všech programech" msgid "Missing search criteria" -msgstr "" +msgstr "Chybí kritéria vyhledávání" msgid "Results found" msgstr "Nalezeny výsledky" @@ -1113,20 +1127,22 @@ msgid "This program is protected" msgstr "Tento program je chráněn" msgid "Reason to check for enrollments" -msgstr "" +msgstr "Důvod ke kontrole zápisů" msgid "" "Describe the reason you are checking for enrollments in this protected " "program" -msgstr "" +msgstr "Popište důvod, proč kontrolujete zápisy v tomto chráněném programu" msgid "Check for enrollments" -msgstr "" +msgstr "Zkontrolujte zápisy" msgid "" "You must provide a reason to check for enrollments in this protected " "program. All activity will be logged." msgstr "" +"Pro kontrolu zápisů v tomto chráněném programu musíte uvést důvod. Veškerá " +"aktivita bude zaznamenána." msgid "Save comment" msgstr "Uložit komentář" @@ -1173,6 +1189,7 @@ msgstr "Označit pro další sledování" msgid "Existing dates for auto-generated events will not be updated." msgstr "" +"Stávající data pro automaticky generované události nebudou aktualizována." msgid "Enrollment date" msgstr "Datum zápisu" @@ -1336,7 +1353,7 @@ msgid "New {{ eventName }} event" msgstr "Nová událost {{ eventName }}" msgid "To open this event, please wait until saving is complete" -msgstr "" +msgstr "Chcete-li otevřít tuto událost, počkejte na dokončení ukládání" msgid "Show {{ rest }} more" msgstr "Zobrazit {{rest}} více" @@ -1366,38 +1383,38 @@ msgid "Stages and Events" msgstr "Fáze a události" msgid "New TEI Relationship" -msgstr "" +msgstr "Nový vztah TEI" msgid "Missing implementation step" -msgstr "" +msgstr "Chybí krok implementace" msgid "Go back without saving relationship" -msgstr "" +msgstr "Vraťte se zpět bez uložení vztahu" msgid "New Relationship" -msgstr "" +msgstr "Nový vztah" msgid "Link to an existing {{tetName}}" -msgstr "" +msgstr "Odkaz na existující {{tetName}}" msgid "An error occurred while adding the relationship" -msgstr "" +msgstr "Při přidávání vztahu došlo k chybě" msgid "" "Something went wrong while loading relationships. Please try again later." -msgstr "" +msgstr "Při načítání vztahů se něco pokazilo. Prosím zkuste to znovu později." msgid "{{trackedEntityTypeName}} relationships" -msgstr "" +msgstr "{{trackedEntityTypeName}} vztahy" msgid "To open this relationship, please wait until saving is complete" -msgstr "" +msgstr "Chcete-li otevřít tento vztah, počkejte na dokončení ukládání" msgid "Type" -msgstr "" +msgstr "Typ" msgid "Created date" -msgstr "" +msgstr "Datum vytvoření" msgid "Program stage name" msgstr "Název fáze programu" @@ -1436,19 +1453,19 @@ msgid "Choose a program stage to filter by {{label}}" msgstr "Vyberte fázi programu, kterou chcete filtrovat podle {{label}}" msgid "Active enrollments" -msgstr "" +msgstr "Aktivní zápisy" msgid "Completed enrollments" -msgstr "" +msgstr "Dokončené zápisy" msgid "Cancelled enrollments" -msgstr "" +msgstr "Zrušené zápisy" msgid "Working list could not be updated" msgstr "Pracovní seznam nelze aktualizovat" msgid "an error occurred loading the working lists" -msgstr "" +msgstr "došlo k chybě při načítání pracovních seznamů" msgid "an error occurred loading Tracked entity instance lists" msgstr "došlo k chybě při načítání seznamů instancí trasovaných entit" @@ -1583,7 +1600,7 @@ msgid "Set area" msgstr "Nastavit oblast" msgid "Area on map saved" -msgstr "" +msgstr "Oblast na mapě byla uložena" msgid "Compatibility mode" msgstr "Režim kompatibility" diff --git a/i18n/en.pot b/i18n/en.pot index 5078cc26d8..0dd6fab89a 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -1176,6 +1176,33 @@ msgstr "Mark for follow-up" msgid "Existing dates for auto-generated events will not be updated." msgstr "Existing dates for auto-generated events will not be updated." +msgid "Latitude" +msgstr "Latitude" + +msgid "Longitude" +msgstr "Longitude" + +msgid "Edit" +msgstr "Edit" + +msgid "Set coordinates" +msgstr "Set coordinates" + +msgid "Coordinates" +msgstr "Coordinates" + +msgid "Delete polygon" +msgstr "Delete polygon" + +msgid "Close without saving" +msgstr "Close without saving" + +msgid "Finish drawing before saving" +msgstr "Finish drawing before saving" + +msgid "Set area" +msgstr "Set area" + msgid "Enrollment date" msgstr "Enrollment date" @@ -1200,6 +1227,12 @@ msgstr "Last updated {{date}}" msgid "Cancelled" msgstr "Cancelled" +msgid "Add coordinates" +msgstr "Add coordinates" + +msgid "Add area" +msgstr "Add area" + msgid "Comments about this enrollment" msgstr "Comments about this enrollment" @@ -1323,9 +1356,6 @@ msgstr "Profile widget could not be loaded. Please try again later" msgid "{{TETName}} profile" msgstr "{{TETName}} profile" -msgid "Edit" -msgstr "Edit" - msgid "tracked entity instance" msgstr "tracked entity instance" @@ -1572,12 +1602,6 @@ msgstr "To time" msgid "Page {{currentPage}}" msgstr "Page {{currentPage}}" -msgid "Delete polygon" -msgstr "Delete polygon" - -msgid "Set area" -msgstr "Set area" - msgid "Area on map saved" msgstr "Area on map saved" diff --git a/i18n/uz_UZ_Cyrl.po b/i18n/uz_UZ_Cyrl.po index 5e07eebebb..620b5591f9 100644 --- a/i18n/uz_UZ_Cyrl.po +++ b/i18n/uz_UZ_Cyrl.po @@ -177,6 +177,7 @@ msgstr "" msgid "Saving a {{trackedEntityName}} in {{programName}} in {{orgUnitName}}." msgstr "" +"Сақланмоқда {{trackedEntityName}} да {{programName}} да {{orgUnitName}}." msgid "Cancel" msgstr "Бекор қилиш" @@ -212,7 +213,7 @@ msgid "Assignee" msgstr "Ваколат берилган шахс" msgid "Saving to {{programName}} in {{orgUnitName}}" -msgstr "" +msgstr "Сақланади {{programName}} да {{orgUnitName}}" msgid "" "This is not an event program or the metadata is corrupt. See log for " @@ -250,7 +251,7 @@ msgid "Finish" msgstr "Тугатиш" msgid "Save without completing" -msgstr "" +msgstr "Тугалланмасдан сақлаш" msgid "Complete" msgstr "Тўлдириш" @@ -512,7 +513,7 @@ msgid "Type to filter options" msgstr "" msgid "No match found" -msgstr "" +msgstr "Ҳеч нима топилмади" msgid "Search" msgstr "Излаш" @@ -538,6 +539,9 @@ msgstr "қидириш учун ёзишни бошланг" msgid "suggestions could not be retrieved" msgstr "таклифлар қабул қилинмади" +msgid "No results found" +msgstr "Ҳеч қандай натижа топилмади" + msgid "No items to display" msgstr "Кўрсатиладиган ахборот йўқ" @@ -554,7 +558,7 @@ msgid "Update" msgstr "Янгилаш" msgid "Reset filter" -msgstr "" +msgstr "Фильтрни қайта созлаш" msgid "Remove filter" msgstr "Фильтрни олиб ташлаш" @@ -670,36 +674,38 @@ msgid "" msgstr "" msgid "Enrollment Dashboard" -msgstr "" +msgstr "Қайд этиш панели" msgid "No indicator output for this enrollment yet" -msgstr "" +msgstr "Ушбу қайд этишда индикатор ахбороти мавжуд эмас" msgid "No feedback for this enrollment yet" -msgstr "" +msgstr "Ушбу қайд этишда қайта алоқа ахбороти мавжуд эмас" msgid "Quick actions" -msgstr "" +msgstr "Тезкор харакатлар" msgid "New Event" -msgstr "" +msgstr "Янги Ҳодиса/Ҳолат" msgid "Schedule an event" -msgstr "" +msgstr "Ҳодиса/Ҳолатни режалаш" msgid "Make referral" msgstr "Йўналиш яратинг" msgid "No available program stages" -msgstr "" +msgstr "Дастур босқичлари мавжуд эмас" msgid "Program stage not found" -msgstr "" +msgstr "Дастур босқичлари топилмади" msgid "" "Choose a program to add new or see existing enrollments for " "{{teiDisplayName}}" msgstr "" +" {{teiDisplayName}} учун янги рўйхатга ёки мавжуд рўйхатга олишларини кўриш " +"учун дастурни танланг." msgid "" "{{programName}} has categories. Choose all categories to view dashboard." @@ -710,7 +716,7 @@ msgid "Invalid enrollment id {{enrollmentId}}." msgstr "" msgid "Choose an enrollment to view the dashboard." -msgstr "" +msgstr "бошқарув панелини кўриш учун рўйхатдан ўтишни танланг." msgid "There are no active enrollments." msgstr "" @@ -748,19 +754,19 @@ msgid "View working list in this program." msgstr "Ушбу дастурдаги ишчи рўйхатни кўриб чиқиш" msgid "Page is missing required values from URL" -msgstr "" +msgstr "URL саҳифасида керакли қийматлар йўқ" msgid "Program is not valid" -msgstr "" +msgstr "Дастур яроқли эмас" msgid "Org unit is not valid with current program" -msgstr "" +msgstr "Жорий дастур учун ташкилий бирлик яроқли эмас" msgid "There was an error opening the Page" -msgstr "" +msgstr "Саҳифани очишда хатолик мавжуд" msgid "Enrollment{{escape}} New Event" -msgstr "" +msgstr "Рўйхатга олишда {{escape}} Янги Ҳодиса/Ҳолат" msgid "There was an error loading the page" msgstr "" @@ -967,9 +973,6 @@ msgstr "" msgid "Possible duplicates found" msgstr "Бўлиши мумкин бўлган нусхалар топилди" -msgid "No results found" -msgstr "Ҳеч қандай натижа топилмади" - msgid "An error occurred loading possible duplicates" msgstr "Эҳтимолий нусхаларни юклашда хатолик юз берди" @@ -1174,6 +1177,33 @@ msgstr "Кузатув учун белгиланг" msgid "Existing dates for auto-generated events will not be updated." msgstr "" +msgid "Latitude" +msgstr "Кенглик" + +msgid "Longitude" +msgstr "" + +msgid "Edit" +msgstr "Таҳрирлаш" + +msgid "Set coordinates" +msgstr "" + +msgid "Coordinates" +msgstr "Координаталар" + +msgid "Delete polygon" +msgstr "Полигон (кўпбурчак) ни ўчириб ташлаш" + +msgid "Close without saving" +msgstr "" + +msgid "Finish drawing before saving" +msgstr "" + +msgid "Set area" +msgstr "Ҳудудни белгиланг" + msgid "Enrollment date" msgstr "Қайд этилган сана" @@ -1199,6 +1229,12 @@ msgstr " {{date}}да охирги марта янгиланган" msgid "Cancelled" msgstr "Бекор қилинди" +msgid "Add coordinates" +msgstr "" + +msgid "Add area" +msgstr "" + msgid "Comments about this enrollment" msgstr "" @@ -1322,9 +1358,6 @@ msgstr "" msgid "{{TETName}} profile" msgstr "" -msgid "Edit" -msgstr "Таҳрирлаш" - msgid "tracked entity instance" msgstr "кузатилаётган объект намунаси" @@ -1572,12 +1605,6 @@ msgstr "Шу вақтгача" msgid "Page {{currentPage}}" msgstr "{{currentPage}} саҳифаси" -msgid "Delete polygon" -msgstr "Полигон (кўпбурчак) ни ўчириб ташлаш" - -msgid "Set area" -msgstr "Ҳудудни белгиланг" - msgid "Area on map saved" msgstr "" diff --git a/package.json b/package.json index 8638a37b0c..a3a3e55966 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.44.2", + "version": "100.45.2", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.44.2", + "@dhis2/rules-engine-javascript": "100.45.2", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index 79c2dd89a8..360a957889 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.44.2", + "version": "100.45.2", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { diff --git a/packages/rules-engine/src/services/VariableService/variableService.types.js b/packages/rules-engine/src/services/VariableService/variableService.types.js index 5529c58a43..0a0b7e65d2 100644 --- a/packages/rules-engine/src/services/VariableService/variableService.types.js +++ b/packages/rules-engine/src/services/VariableService/variableService.types.js @@ -20,7 +20,6 @@ type EventMain = { +programStageId?: string, +programStageName?: string, +orgUnitId?: string, - +orgUnitName?: string, +trackedEntityInstanceId?: string, +enrollmentId?: string, +enrollmentStatus?: string, diff --git a/src/core_modules/capture-core-utils/featuresSupport/support.js b/src/core_modules/capture-core-utils/featuresSupport/support.js index 0b31bf6dd9..207c929c21 100644 --- a/src/core_modules/capture-core-utils/featuresSupport/support.js +++ b/src/core_modules/capture-core-utils/featuresSupport/support.js @@ -2,12 +2,14 @@ export const FEATURES = Object.freeze({ programStageWorkingList: 'programStageWorkingList', storeProgramStageWorkingList: 'storeProgramStageWorkingList', + customIcons: 'customIcons', }); // The first minor version that supports the feature const MINOR_VERSION_SUPPORT = Object.freeze({ [FEATURES.programStageWorkingList]: 39, [FEATURES.storeProgramStageWorkingList]: 40, + [FEATURES.customIcons]: 41, }); export const hasAPISupportForFeature = (minorVersion: string, featureName: string) => diff --git a/src/core_modules/capture-core/components/CardList/CardListItem.component.js b/src/core_modules/capture-core/components/CardList/CardListItem.component.js index d2542d3c1b..328926bdb8 100644 --- a/src/core_modules/capture-core/components/CardList/CardListItem.component.js +++ b/src/core_modules/capture-core/components/CardList/CardListItem.component.js @@ -13,6 +13,7 @@ import { searchScopes } from '../SearchBox'; import { enrollmentTypes } from './CardList.constants'; import { ListEntry } from './ListEntry.component'; import { dataElementTypes, getTrackerProgramThrowIfNotFound } from '../../metaData'; +import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import type { ListItem, RenderCustomCardActions } from './CardList.types'; @@ -96,24 +97,24 @@ const deriveEnrollmentType = return enrollmentTypes.DONT_SHOW_TAG; }; -const deriveEnrollmentOrgUnitAndDate = (enrollments, enrollmentType, currentProgramId): {orgUnitName?: string, enrolledAt?: string} => { +const deriveEnrollmentOrgUnitIdAndDate = (enrollments, enrollmentType, currentProgramId): {orgUnitId?: string, enrolledAt?: string} => { if (!enrollments?.length) { return {}; } if (!currentProgramId && enrollments.length) { - const { orgUnitName, enrolledAt } = enrollments[0]; + const { orgUnit: orgUnitId, enrolledAt } = enrollments[0]; return { - orgUnitName, + orgUnitId, enrolledAt, }; } - const { orgUnitName, enrolledAt } = + const { orgUnit: orgUnitId, enrolledAt } = enrollments .filter(({ program }) => program === currentProgramId) .filter(({ status }) => status === enrollmentType) .sort((a, b) => moment.utc(a.lastUpdated).diff(moment.utc(b.lastUpdated)))[0] || {}; - return { orgUnitName, enrolledAt }; + return { orgUnitId, enrolledAt }; }; const deriveProgramFromEnrollment = (enrollments, currentSearchScopeType) => { @@ -137,7 +138,8 @@ const CardListItemIndex = ({ }: Props) => { const enrollments = item.tei ? item.tei.enrollments : []; const enrollmentType = deriveEnrollmentType(enrollments, currentProgramId); - const { orgUnitName, enrolledAt } = deriveEnrollmentOrgUnitAndDate(enrollments, enrollmentType, currentProgramId); + const { orgUnitId, enrolledAt } = deriveEnrollmentOrgUnitIdAndDate(enrollments, enrollmentType, currentProgramId); + const { displayName: orgUnitName } = useOrgUnitName(orgUnitId); const program = enrollments && enrollments.length ? deriveProgramFromEnrollment(enrollments, currentSearchScopeType) : undefined; diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js index d58a42aff2..3e4ea25e82 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js @@ -8,11 +8,17 @@ import type { ExistingUniqueValueDialogActionsComponent } from '../withErrorMess import type { InputAttribute } from './hooks/useFormValues'; import { RenderFoundation, ProgramStage } from '../../../metaData'; +type TrackedEntityAttributes = Array<{ + attribute: string, + value: any, +}>; + export type EnrollmentPayload = {| trackedEntity: string, trackedEntityType: string, orgUnit: string, geometry: any, + attributes: TrackedEntityAttributes, enrollments: [ {| occurredAt: string, @@ -23,10 +29,7 @@ export type EnrollmentPayload = {| events: Array<{ orgUnit: string, }>, - attributes: Array<{ - attribute: string, - value: any, - }>, + attributes: TrackedEntityAttributes, |} ] |} diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildEnrollmentPayload.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildEnrollmentPayload.js index 95778162b0..d6ae0f8adf 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildEnrollmentPayload.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildEnrollmentPayload.js @@ -156,13 +156,15 @@ export const useBuildEnrollmentPayload = ({ ? [firstStageDuringRegistrationEvent, ...autoGenerateEvents] : autoGenerateEvents; + const attributes = deriveAttributesFromFormValues(formServerValues); + const enrollment = { program: programId, status: 'ACTIVE', orgUnit: orgUnitId, occurredAt, enrolledAt, - attributes: deriveAttributesFromFormValues(formServerValues), + attributes, events: allEventsToBeCreated, }; @@ -177,6 +179,7 @@ export const useBuildEnrollmentPayload = ({ trackedEntity: teiId || generateUID(), orgUnit: orgUnitId, trackedEntityType: trackedEntityTypeId, + attributes, geometry, enrollments: [enrollment], }; diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js index 4a84d7ccb7..a9b0eda796 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js @@ -45,7 +45,7 @@ export const useLifecycle = ( }); useEffect(() => { dataEntryReadyRef.current = false; - }, [teiId]); + }, [teiId, selectedScopeId]); useEffect(() => { if ( diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.component.js b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.component.js index 7b61054364..3e2ea0d8a9 100644 --- a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.component.js @@ -9,7 +9,7 @@ import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { scopeTypes } from '../../../metaData'; import { TrackedEntityInstanceDataEntry } from '../TrackedEntityInstance'; import { useCurrentOrgUnitId } from '../../../hooks/useCurrentOrgUnitId'; -import { useCoreOrgUnit } from '../../../metadataRetrieval/coreOrgUnit'; +import { useOrgUnitName } from '../../../metadataRetrieval/orgUnitName'; import type { Props, PlainProps } from './TeiRegistrationEntry.types'; import { DiscardDialog } from '../../Dialogs/DiscardDialog.component'; import { withSaveHandler } from '../../DataEntry'; @@ -56,8 +56,7 @@ const TeiRegistrationEntryPlain = const { scopeType } = useScopeInfo(selectedScopeId); const { formId, formFoundation } = useMetadataForRegistrationForm({ selectedScopeId }); const orgUnitId = useCurrentOrgUnitId(); - const { orgUnit } = useCoreOrgUnit(orgUnitId); // Tony: [DHIS2-15814] Change this to new hook - const orgUnitName = orgUnit ? orgUnit.name : ''; + const { displayName: orgUnitName } = useOrgUnitName(orgUnitId); const handleOnCancel = () => { if (!isUserInteractionInProgress) { diff --git a/src/core_modules/capture-core/components/FormFields/File/D2File.component.js b/src/core_modules/capture-core/components/FormFields/File/D2File.component.js index a3c449e82a..0b87faba5f 100644 --- a/src/core_modules/capture-core/components/FormFields/File/D2File.component.js +++ b/src/core_modules/capture-core/components/FormFields/File/D2File.component.js @@ -28,6 +28,10 @@ type Props = { mutate: (data: any) => Promise } +type State = { + fileSelectorOpen: boolean, +}; + const styles = theme => ({ horizontalContainer: { display: 'flex', @@ -74,10 +78,18 @@ const styles = theme => ({ }, }); -class D2FilePlain extends Component { +class D2FilePlain extends Component { hiddenFileSelectorRef: any; + fileSelectorOpen: boolean; + constructor(props: Props) { + super(props); + this.state = { + fileSelectorOpen: false, + }; + } handleFileChange = (e: Object) => { + this.setState((state) => { state.fileSelectorOpen = false; }); e.preventDefault(); const file = e.target.files[0]; e.target.value = null; @@ -100,12 +112,23 @@ class D2FilePlain extends Component { } handleButtonClick = () => { this.hiddenFileSelectorRef.click(); + this.setState((state) => { state.fileSelectorOpen = true; }); + } + + handleCancel = () => { + this.setState((state) => { state.fileSelectorOpen = false; }); } handleRemoveClick = () => { this.props.onBlur(null); } + handleBlur = () => { + if (!this.state.fileSelectorOpen) { + this.props.onBlur(this.getFileUrl()); + } + } + getFileUrl = () => { const value = this.props.value; if (value) { @@ -122,7 +145,7 @@ class D2FilePlain extends Component { const containerClass = isVertical ? classes.verticalContainer : classes.horizontalContainer; const selectedFileTextContainerClass = isVertical ? classes.verticalSelectedFileTextContainer : classes.horizontalSelectedFileTextContainer; return ( -
+
{ this.hiddenFileSelectorRef = hiddenFileSelector; }} onChange={e => this.handleFileChange(e)} + onCancel={this.handleCancel} // eslint-disable-line react/no-unknown-property /> { (() => { diff --git a/src/core_modules/capture-core/components/FormFields/Image/D2Image.component.js b/src/core_modules/capture-core/components/FormFields/Image/D2Image.component.js index 5f9458930b..d9682c56b5 100644 --- a/src/core_modules/capture-core/components/FormFields/Image/D2Image.component.js +++ b/src/core_modules/capture-core/components/FormFields/Image/D2Image.component.js @@ -27,6 +27,10 @@ type Props = { mutate: (data: any) => Promise } +type State = { + imageSelectorOpen: boolean, +}; + const styles = theme => ({ horizontalContainer: { display: 'flex', @@ -72,10 +76,18 @@ const styles = theme => ({ }, }); -class D2ImagePlain extends Component { +class D2ImagePlain extends Component { hiddenimageSelectorRef: any; + imageSelectorOpen: boolean; + constructor(props: Props) { + super(props); + this.state = { + imageSelectorOpen: false, + }; + } handleImageChange = (e: Object) => { + this.setState((state) => { state.imageSelectorOpen = false; }); e.preventDefault(); const image = e.target.files[0]; e.target.value = null; @@ -98,13 +110,24 @@ class D2ImagePlain extends Component { } handleButtonClick = () => { this.hiddenimageSelectorRef.click(); + this.setState((state) => { state.imageSelectorOpen = true; }); + } + + handleCancel = () => { + this.setState((state) => { state.imageSelectorOpen = false; }); } handleRemoveClick = () => { this.props.onBlur(null); } - getimageUrl = () => { + handleBlur = () => { + if (!this.state.imageSelectorOpen) { + this.props.onBlur(this.getImageUrl()); + } + } + + getImageUrl = () => { const value = this.props.value; if (value) { return value.url || inMemoryFileStore.get(value.value); @@ -116,13 +139,13 @@ class D2ImagePlain extends Component { const { value, classes, asyncUIState, orientation, disabled } = this.props; const isVertical = orientation === orientations.VERTICAL; const isUploading = asyncUIState && asyncUIState.loading; - const imageUrl = this.getimageUrl(); + const imageUrl = this.getImageUrl(); // $FlowFixMe[prop-missing] automated comment const containerClass = isVertical ? classes.verticalContainer : classes.horizontalContainer; // $FlowFixMe[prop-missing] automated comment const selectedImageTextContainerClass = isVertical ? classes.verticalSelectedImageTextContainer : classes.horizontalSelectedImageTextContainer; return ( -
+
{ this.hiddenimageSelectorRef = hiddenimageSelector; }} onChange={e => this.handleImageChange(e)} + onCancel={this.handleCancel} // eslint-disable-line react/no-unknown-property /> { (() => { diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/MultiSelectField/MultiSelectField.component.js b/src/core_modules/capture-core/components/FormFields/New/Fields/MultiSelectField/MultiSelectField.component.js index 1057e0ee76..6cb7c2db71 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/MultiSelectField/MultiSelectField.component.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/MultiSelectField/MultiSelectField.component.js @@ -39,22 +39,27 @@ const MultiSelectFieldComponentPlain = (props: Props) => { onSelect(multiSelectSelected.join(MULTI_TEXT_SEPARATOR)); }; + const handleBlur = () => { + onBlur(selected ? selected.join(MULTI_TEXT_SEPARATOR) : null); + }; + return ( - - {options.map(option => ( - - ))} - +
+ + {options.map(option => ( + + ))} + +
); }; diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/MultiSelectField/MultiSelectField.types.js b/src/core_modules/capture-core/components/FormFields/New/Fields/MultiSelectField/MultiSelectField.types.js index 286daf7be7..c708a8b3a7 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/MultiSelectField/MultiSelectField.types.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/MultiSelectField/MultiSelectField.types.js @@ -11,7 +11,7 @@ type MultiSelectOptionConfig = { export type Props = { onSelect: (value?: string) => void, onFocus: () => void, - onBlur: () => void, + onBlur: (value: ?string) => void, options: Array, value?: string, translations: { diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/MultiSelectField/withFocusHandler.js b/src/core_modules/capture-core/components/FormFields/New/Fields/MultiSelectField/withFocusHandler.js index 378623435f..cdbf756da5 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/MultiSelectField/withFocusHandler.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/MultiSelectField/withFocusHandler.js @@ -7,7 +7,7 @@ type Props = { onSetFocus: () => void, onRemoveFocus: () => void, inFocus: boolean, - onBlur?: ?(event: SyntheticEvent) => void, + onBlur?: (value: ?string) => void, onFocus: () => void, classes: { inputWrapperFocused: string, @@ -15,10 +15,11 @@ type Props = { } }; -export const withFocusHandler = () => (InnerCompnent: React.ComponentType) => +export const withFocusHandler = () => (InnerComponent: React.ComponentType) => class FocusHandlerHOC extends React.Component { - handleRemoveFocus = () => { + handleRemoveFocus = (value: string) => { this.props.onRemoveFocus(); + this.props.onBlur && this.props.onBlur(value); } handleFocus = () => { @@ -40,7 +41,7 @@ export const withFocusHandler = () => (InnerCompnent: React.ComponentType) className={classNames(defaultClasses.inputWrapper, inputWrapper)} > {/* $FlowFixMe[cannot-spread-inexact] automated comment */} - ({ type Props = { onSelectClick: (selectedOrgUnit: Object) => void, + onBlur: (selectedOrgUnit: Object) => void, selected?: ?string, maxTreeHeight?: ?number, disabled?: ?boolean, @@ -56,6 +57,7 @@ type Props = { const OrgUnitFieldPlain = (props: Props) => { const { onSelectClick, + onBlur, classes, selected, maxTreeHeight, @@ -136,10 +138,15 @@ const OrgUnitFieldPlain = (props: Props) => { setSearchText(event.currentTarget.value); }; + const handleBlur = () => { + onBlur && onBlur(null); + }; + const styles = maxTreeHeight ? { maxHeight: maxTreeHeight, overflowY: 'auto' } : null; return (
{ ); } + onBlur = () => { + this.props.onSelect(this.props.value); + } + render() { const { options, @@ -220,6 +224,7 @@ class OptionsSelectVirtualizedPlain extends React.Component { >
{/* $FlowFixMe[cannot-spread-inexact] automated comment */} { focusSelectedInput.current = true; }; + const handleBlur = () => { + // $FlowExpectedError + onSet(value); + }; + if (value) { return ( { } return ( -
+
{ + const supportCustomIcons = useFeature(FEATURES.customIcons); const { baseUrl, apiVersion } = useConfig(); - const source = name && buildUrl(baseUrl, `api/${apiVersion}/icons/${name}/icon.svg`); + let source; + + if (name) { + source = buildUrl(baseUrl, `api/${apiVersion}/icons/${name}/icon`); + // Append .svg to source if supportCustomIcons is false (feature flag v41) + source = supportCustomIcons ? source : `${source}.svg`; + } return ( { const [mainContentVisible, setMainContentVisibility] = useState(true); const [addRelationShipContainerElement, setAddRelationshipContainerElement] = - useState(undefined); + useState(undefined); const toggleVisibility = useCallback(() => setMainContentVisibility(current => !current), []); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramStages.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramStages.js index 3c8b50285a..7d692e9867 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramStages.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramStages.js @@ -1,11 +1,12 @@ // @flow +import { useMemo } from 'react'; import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; import i18n from '@dhis2/d2-i18n'; import type { apiProgramStage } from 'capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types'; import { Program } from '../../../../../metaData'; -export const useProgramStages = (program: Program, programStages?: Array) => { +export const useProgramStages = (program: Program, programStages?: Array) => useMemo(() => { const stages = []; if (program && programStages) { program.stages.forEach((item) => { @@ -48,4 +49,4 @@ export const useProgramStages = (program: Program, programStages?: Array} |}; export type AttributeValue = {| diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js index 74f6a96735..d8ff56e1b7 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js @@ -33,7 +33,6 @@ const useEventsData = (enrollment, program) => { programId: event.program, programStageId: event.programStage, orgUnitId: event.orgUnit, - orgUnitName: event.orgUnitName, trackedEntityInstanceId: event.trackedEntityInstance, enrollmentId: event.enrollment, enrollmentStatus: event.enrollmentStatus, diff --git a/src/core_modules/capture-core/components/PossibleDuplicatesDialog/possibleDuplicatesDialog.epics.js b/src/core_modules/capture-core/components/PossibleDuplicatesDialog/possibleDuplicatesDialog.epics.js index 515eee121d..78098fa714 100644 --- a/src/core_modules/capture-core/components/PossibleDuplicatesDialog/possibleDuplicatesDialog.epics.js +++ b/src/core_modules/capture-core/components/PossibleDuplicatesDialog/possibleDuplicatesDialog.epics.js @@ -47,7 +47,6 @@ export const loadSearchGroupDuplicatesForReviewEpic = ( payload: { page, pageSize, - orgUnitId, selectedScopeId, scopeType, dataEntryId, @@ -73,7 +72,6 @@ export const loadSearchGroupDuplicatesForReviewEpic = ( const contextParam = scopeType === scopeTypes.TRACKER_PROGRAM ? { program: selectedScopeId } : { trackedEntityType: selectedScopeId }; const queryArgs = { - ou: orgUnitId, ouMode: 'ACCESSIBLE', pageSize, page, diff --git a/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js b/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js index 18557db76b..0aba4ccd71 100644 --- a/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js +++ b/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js @@ -3,15 +3,15 @@ import React, { type ComponentType, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { ScopeSelectorComponent } from './ScopeSelector.component'; import type { OwnProps } from './ScopeSelector.types'; -import { useOrganizationUnit } from './hooks'; +import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { resetOrgUnitIdFromScopeSelector } from './ScopeSelector.actions'; -const deriveReadiness = (lockedSelectorLoads, selectedOrgUnitId, selectedOrgUnitName) => { +const deriveReadiness = (lockedSelectorLoads, selectedOrgUnitId, selectedOrgUnitName, displayName) => { // because we want the orgUnit to be fetched and stored // before allowing the user to view the locked selector - if (selectedOrgUnitId && selectedOrgUnitName) { - return true; + if (selectedOrgUnitId && (!selectedOrgUnitName || selectedOrgUnitName !== displayName)) { + return false; } return !lockedSelectorLoads; }; @@ -32,21 +32,20 @@ export const ScopeSelector: ComponentType = ({ children, }) => { const dispatch = useDispatch(); - const { refetch: refetchOrganisationUnit, displayName } = useOrganizationUnit(); - const [selectedOrgUnit, setSelectedOrgUnit] = useState({ name: displayName, id: selectedOrgUnitId }); + const [selectedOrgUnit, setSelectedOrgUnit] = useState({ name: undefined, id: selectedOrgUnitId }); + const { displayName } = useOrgUnitName(selectedOrgUnit.id); useEffect(() => { - const missName = !selectedOrgUnit.name; - const hasDifferentId = selectedOrgUnit.id !== selectedOrgUnitId; - - selectedOrgUnitId && - (hasDifferentId || missName) && - refetchOrganisationUnit({ variables: { selectedOrgUnitId } }); - }, [selectedOrgUnitId]); // eslint-disable-line react-hooks/exhaustive-deps + if (displayName && selectedOrgUnit.name !== displayName) { + setSelectedOrgUnit(prevSelectedOrgUnit => ({ ...prevSelectedOrgUnit, name: displayName })); + } + }, [displayName, selectedOrgUnit, setSelectedOrgUnit]); useEffect(() => { - displayName && setSelectedOrgUnit(prevSelectedOrgUnit => ({ ...prevSelectedOrgUnit, name: displayName })); - }, [displayName, setSelectedOrgUnit]); + if (selectedOrgUnitId && !selectedOrgUnit.id) { + selectedOrgUnitId && setSelectedOrgUnit(prevSelectedOrgUnit => ({ ...prevSelectedOrgUnit, id: selectedOrgUnitId })); + } + }, [selectedOrgUnitId, selectedOrgUnit, setSelectedOrgUnit]); const handleSetOrgUnit = (orgUnitId, orgUnitObject) => { setSelectedOrgUnit(orgUnitObject); @@ -59,7 +58,7 @@ export const ScopeSelector: ComponentType = ({ previousOrgUnitId: app.previousOrgUnitId, } )); - const ready = deriveReadiness(lockedSelectorLoads, selectedOrgUnitId, selectedOrgUnit.name); + const ready = deriveReadiness(lockedSelectorLoads, selectedOrgUnit.id, selectedOrgUnit.name, displayName); return ( { - const { data, refetch } = useDataQuery( - useMemo( - () => ({ - organisationUnits: { - resource: 'organisationUnits', - id: ({ variables: { selectedOrgUnitId: id } }) => id, - params: { - fields: ['displayName'], - }, - }, - }), - [], - ), - { - lazy: true, - }, - ); - - return { - displayName: data?.organisationUnits?.displayName, - refetch, - }; -}; diff --git a/src/core_modules/capture-core/components/TeiSearch/epics/teiSearch.epics.js b/src/core_modules/capture-core/components/TeiSearch/epics/teiSearch.epics.js index 4b0fd58a51..c0b3e7321c 100644 --- a/src/core_modules/capture-core/components/TeiSearch/epics/teiSearch.epics.js +++ b/src/core_modules/capture-core/components/TeiSearch/epics/teiSearch.epics.js @@ -34,9 +34,9 @@ import { getSearchFormId } from '../getSearchFormId'; import type { QuerySingleResource } from '../../../utils/api/api.types'; const getOuQueryArgs = (orgUnit: ?Object, orgUnitScope: string) => - (orgUnitScope !== 'ACCESSIBLE' ? - { orgUnit: orgUnit && orgUnit.id, ouMode: orgUnitScope } : - { ouMode: orgUnitScope }); + (['ACCESSIBLE', 'CAPTURE', 'ALL'].includes(orgUnitScope) ? + { ouMode: orgUnitScope } : + { orgUnit: orgUnit && orgUnit.id, ouMode: orgUnitScope }); const getContextQueryArgs = (programId: ?string, trackedEntityTypeId: string) => (programId ? { program: programId } : { trackedEntityType: trackedEntityTypeId }); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.js index 4d07c56a61..0c56de8cb1 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.component.js @@ -8,8 +8,10 @@ import { Complete } from './Complete'; import { Delete } from './Delete'; import { Followup } from './Followup'; import { AddNew } from './AddNew'; +import { AddLocation } from './AddLocation'; import type { PlainProps } from './actions.types'; import { LoadingMaskForButton } from '../../LoadingMasks'; +import { MapModal } from '../MapModal'; const styles = { actions: { @@ -35,13 +37,14 @@ export const ActionsPlain = ({ onlyEnrollOnce, classes, }: PlainProps) => { - const [open, setOpen] = useState(false); + const [isOpenActions, setOpenActions] = useState(false); + const [isOpenMap, setOpenMap] = useState(false); const handleOnUpdate = (arg) => { - setOpen(prev => !prev); + setOpenActions(false); onUpdate(arg); }; const handleOnDelete = (arg) => { - setOpen(prev => !prev); + setOpenActions(false); onDelete(arg); }; @@ -53,8 +56,8 @@ export const ActionsPlain = ({ small disabled={loading} className={classes.actions} - open={open} - onClick={() => setOpen(prev => !prev)} + open={isOpenActions} + onClick={() => setOpenActions(prev => !prev)} component={ loading ? null : ( @@ -72,6 +75,13 @@ export const ActionsPlain = ({ enrollment={enrollment} onUpdate={handleOnUpdate} /> + { + setOpenMap(true); + setOpenActions(false); + }} + /> + ) } @@ -94,6 +105,11 @@ export const ActionsPlain = ({ {i18n.t('We are processing your request.')}
)} + {isOpenMap && } ); }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.js b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.js index 2ce98fcb38..1e3bab189e 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.js @@ -1,24 +1,8 @@ // @flow -import { useDataMutation } from '@dhis2/app-runtime'; import React from 'react'; import { ActionsComponent } from './Actions.component'; import type { Props } from './actions.types'; -import { processErrorReports } from '../processErrorReports'; - -const enrollmentUpdate = { - resource: 'tracker?async=false&importStrategy=UPDATE', - type: 'create', - data: enrollment => ({ - enrollments: [enrollment], - }), -}; -const enrollmentDelete = { - resource: 'tracker?async=false&importStrategy=DELETE', - type: 'create', - data: enrollment => ({ - enrollments: [enrollment], - }), -}; +import { useUpdateEnrollment, useDeleteEnrollment } from '../dataMutation/dataMutation'; export const Actions = ({ enrollment = {}, @@ -29,31 +13,8 @@ export const Actions = ({ onSuccess, ...passOnProps }: Props) => { - const [updateMutation, { loading: updateLoading }] = useDataMutation( - enrollmentUpdate, - { - onComplete: () => { - refetchEnrollment(); - refetchTEI(); - onSuccess && onSuccess(); - }, - onError: (e) => { - onError && onError(processErrorReports(e)); - }, - }, - ); - const [deleteMutation, { loading: deleteLoading }] = useDataMutation( - enrollmentDelete, - { - onComplete: () => { - onDelete(); - onSuccess && onSuccess(); - }, - onError: (e) => { - onError && onError(processErrorReports(e)); - }, - }, - ); + const { updateMutation, updateLoading } = useUpdateEnrollment(refetchEnrollment, refetchTEI, onError); + const { deleteMutation, deleteLoading } = useDeleteEnrollment(onDelete, onError); return ( { + const label = useGeometryLabel(enrollment); + + if (!label) { + return null; + } + + return ( + } + label={label} + onClick={() => setOpenMap(true)} + /> + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddLocation/addLocation.types.js b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddLocation/addLocation.types.js new file mode 100644 index 0000000000..d004713319 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddLocation/addLocation.types.js @@ -0,0 +1,6 @@ +// @flow + +export type Props = {| + enrollment: Object, + setOpenMap: (toogle: boolean) => void, +|}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddLocation/index.js b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddLocation/index.js new file mode 100644 index 0000000000..bdf4bdca7c --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddLocation/index.js @@ -0,0 +1,2 @@ +// @flow +export { AddLocation } from './AddLocation.component'; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.js new file mode 100644 index 0000000000..cd9940c11a --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.component.js @@ -0,0 +1,256 @@ +// @flow +import React, { useState, useMemo } from 'react'; +import classNames from 'classnames'; +import i18n from '@dhis2/d2-i18n'; +import { IconCross24, spacers, Modal, ModalTitle, ModalContent, ModalActions, Button, ButtonStrip } from '@dhis2/ui'; +import { ReactLeafletSearch } from 'react-leaflet-search-unpolyfilled'; +import { Map, TileLayer, Marker, withLeaflet } from 'react-leaflet'; +import { withStyles } from '@material-ui/core'; +import type { CoordinatesProps } from './Coordinates.types'; +import { CoordinateInput } from '../../../../../capture-ui/internal/CoordinateInput/CoordinateInput.component'; +import { isEqual } from '../../../../utils/valueEqualityChecker'; +import { isValidCoordinate } from './coordinate.validator'; +import { convertCoordinatesToServer } from './converters'; + +const styles = (theme: Theme) => ({ + modalContent: { + width: '100%', + }, + map: { + width: '100%', + height: 'calc(100vh - 380px)', + }, + inputWrapper: { + paddingTop: spacers.dp8, + display: 'flex', + }, + inputContent: { + flexGrow: 1, + }, + fieldButton: { + height: '42px !important', + width: 42, + borderRadius: '0 !important', + }, + errorContainer: { + backgroundColor: theme.palette.error.lighter, + color: theme.palette.error.main, + }, +}); + +const WrappedLeafletSearch = withLeaflet(ReactLeafletSearch); + +const CoordinatesPlain = ({ + classes, + center: initialCenter, + setOpen, + defaultValues, + onSetCoordinates, +}: CoordinatesProps) => { + const [position, setPosition] = useState(defaultValues); + const [center, setCenter] = useState(initialCenter); + const [tempLatitude, setTempLatitude] = useState(position?.[0]); + const [tempLongitude, setTempLongitude] = useState(position?.[1]); + const [isEditing, setEditing] = useState(!defaultValues); + const [isValid, setValid] = useState(true); + const hasErrors = useMemo(() => { + const changed = !isEqual(position, defaultValues); + return changed && !isValid; + }, [position, defaultValues, isValid]); + + const resetToDefaultValues = () => { + setCenter(initialCenter); + setPosition(defaultValues); + if (defaultValues) { + setTempLatitude(defaultValues[0]); + setTempLongitude(defaultValues[1]); + setEditing(false); + } else { + setTempLatitude(null); + setTempLongitude(null); + } + }; + + const onHandleMapClicked = (mapCoordinates) => { + if (isEditing) { + const { lat, lng } = mapCoordinates.latlng; + const newPosition: [number, number] = [lat, lng]; + setValid(true); + setPosition(newPosition); + setTempLatitude(lat); + setTempLongitude(lng); + } + }; + + const onSearch = (searchPosition: any) => { + setCenter(searchPosition); + setValid(true); + setTempLatitude(searchPosition[0]); + setTempLongitude(searchPosition[1]); + setPosition(searchPosition); + }; + + const renderMap = () => ( + { + if (ref?.leafletElement) { + ref.leafletElement.invalidateSize(); + } + }} + className={classes.map} + onClick={onHandleMapClicked} + > + + + {position && } + + ); + + const renderLatitude = () => ( + { + if (!latitude) { + return; + } + const longitude = tempLongitude || (position?.[1] ? position[1] : undefined); + if (!longitude) { + return; + } + if (!isValidCoordinate({ longitude: Number(longitude), latitude: Number(latitude) })) { + setPosition(null); + setValid(false); + return; + } + setValid(true); + const newPosition = [Number(latitude), longitude]; + setPosition(newPosition); + setCenter(newPosition); + }} + onChange={(latitude) => { + setTempLatitude(latitude); + }} + /> + ); + + const renderLongitude = () => ( + { + if (!longitude) { + return; + } + const latitude = tempLatitude || (position?.[1] ? position[0] : undefined); + if (!latitude) { + return; + } + if (!isValidCoordinate({ longitude: Number(longitude), latitude: Number(latitude) })) { + setPosition(null); + setValid(false); + return; + } + setValid(true); + const newPosition = [latitude, Number(longitude)]; + setPosition(newPosition); + setCenter(newPosition); + }} + onChange={(longitude) => { + setTempLongitude(longitude); + }} + /> + ); + + const renderFieldButton = () => ( +
+ {!isEditing ? ( + + ) : ( +
+ ); + + const renderActions = () => ( + + + + + ); + + return ( + + {i18n.t('Coordinates')} + + {renderMap()} +
+
+
{renderLatitude()}
+
{renderLongitude()}
+ {renderFieldButton()} +
+ {hasErrors && ( +
{i18n.t('Please provide valid coordinates')}
+ )} +
+
+ {renderActions()} +
+ ); +}; +export const Coordinates = withStyles(styles)(CoordinatesPlain); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.js new file mode 100644 index 0000000000..56e3c5f689 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/Coordinates.types.js @@ -0,0 +1,10 @@ +// @flow + +export type CoordinatesProps = { + center: ?[number, number], + setOpen: (open: boolean) => void, + onSetCoordinates: (coordinates: ?[number, number] | ?Array<[number, number]>) => void, + defaultValues?: ?[number, number], + ...CssClasses, +} + diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/converters.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/converters.js new file mode 100644 index 0000000000..357a034be3 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/converters.js @@ -0,0 +1,11 @@ +// @flow + +export const convertCoordinatesToServer = (coordinates?: Array | null): ?[number, number] => { + if (!coordinates || !coordinates[0]) { + return null; + } + + const lng: number = coordinates[0][1]; + const lat: number = coordinates[0][0]; + return [lng, lat]; +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/coordinate.validator.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/coordinate.validator.js new file mode 100644 index 0000000000..1d37df3233 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/coordinate.validator.js @@ -0,0 +1,32 @@ +// @flow + +type Location = { + longitude: number, + latitude: number, +}; + +function isNumValid(num) { + if (typeof num === 'number') { + return true; + } else if (typeof num === 'string') { + return num.match(/[^0-9.,-]+/) === null; + } + + return false; +} + +export const isValidCoordinate = (value: Location) => { + if (!value) { + return false; + } + + const { longitude, latitude } = value; + if (!isNumValid(latitude) || !isNumValid(longitude)) { + return false; + } + + const ld = parseInt(longitude, 10); + const lt = parseInt(latitude, 10); + + return ld >= -180 && ld <= 180 && lt >= -90 && lt <= 90; +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/index.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/index.js new file mode 100644 index 0000000000..8d4c2efc14 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Coordinates/index.js @@ -0,0 +1,3 @@ +// @flow +export { Coordinates } from './Coordinates.component'; + diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.component.js new file mode 100644 index 0000000000..355490f756 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.component.js @@ -0,0 +1,27 @@ +// @flow +import React from 'react'; +import { dataElementTypes } from '../../../metaData'; +import type { MapModalComponentProps } from './MapModal.types'; +import { Coordinates } from './Coordinates'; +import { Polygon } from './Polygon'; + +export const MapModal = ({ type, center, setOpen, onSetCoordinates, defaultValues }: MapModalComponentProps) => ( + <> + {type === dataElementTypes.COORDINATE && ( + + )} + {type === dataElementTypes.POLYGON && ( + + )} + +); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.container.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.container.js new file mode 100644 index 0000000000..b39d141b1d --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.container.js @@ -0,0 +1,32 @@ +// @flow +import React, { useCallback } from 'react'; +import { useGeometry } from '../hooks/useGeometry'; +import type { MapModalProps } from './MapModal.types'; +import { MapModal as MapModalComponent } from './MapModal.component'; + +const DEFAULT_CENTER = [51.505, -0.09]; + +export const MapModal = ({ + enrollment, + onUpdate, + setOpenMap, + defaultValues, + center, +}: MapModalProps) => { + const { geometryType, dataElementType } = useGeometry(enrollment); + + const onSetCoordinates = useCallback((coordinates) => { + const geometry = coordinates ? { type: geometryType, coordinates } : null; + onUpdate({ ...enrollment, geometry }); + }, [enrollment, geometryType, onUpdate]); + + return ( + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.js new file mode 100644 index 0000000000..80a273420b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/MapModal.types.js @@ -0,0 +1,18 @@ +// @flow +import { dataElementTypes } from '../../../metaData'; + +export type MapModalComponentProps = { + center: ?[number, number], + type: typeof dataElementTypes.COORDINATE | typeof dataElementTypes.POLYGON, + defaultValues?: ?Array> | ?[number, number], + setOpen: (open: boolean) => void, + onSetCoordinates: (coordinates: ?[number, number] | ?Array<[number, number]>) => void, +} + +export type MapModalProps = {| + center?: ?[number, number], + enrollment: Object, + onUpdate: (arg: Object) => void, + setOpenMap: (toggle: boolean) => void, + defaultValues?: ?Array> | ?[number, number], +|}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/ConditionalTooltip.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/ConditionalTooltip.component.js new file mode 100644 index 0000000000..1250a792d2 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/ConditionalTooltip.component.js @@ -0,0 +1,29 @@ +// @flow +import React from 'react'; +import { Tooltip } from '@dhis2/ui'; + +type Props = { + enabled: boolean, + children: any, +}; + +export const ConditionalTooltip = (props: Props) => { + const { enabled, children, ...passOnProps } = props; + + return enabled ? + ( + { ({ onMouseOver, onMouseOut, ref }) => ( + { + if (btnRef) { + btnRef.onpointerenter = onMouseOver; + btnRef.onpointerleave = onMouseOut; + ref.current = btnRef; + } + }} + > + {children} + + )} + ) : children; +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/DeleteControl.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/DeleteControl.component.js new file mode 100644 index 0000000000..3dd18b0e96 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/DeleteControl.component.js @@ -0,0 +1,52 @@ +// @flow +import React, { useEffect, useState, useCallback } from 'react'; +import ReactDOM from 'react-dom'; +import i18n from '@dhis2/d2-i18n'; +import classNames from 'classnames'; +import L, { Control } from 'leaflet'; +import { withLeaflet } from 'react-leaflet'; + +type Props = { + onClick: () => void, + disabled?: ?boolean, + leaflet: typeof Control, +}; + +const DeleteControlPlain = ({ onClick, disabled, leaflet }: Props) => { + const [leafletElement, setLeafletElement] = useState(); + const onHandleClick = useCallback(() => !disabled && onClick(), [disabled, onClick]); + + useEffect(() => { + const deleteControl = L.control({ position: 'topright' }); + const text = i18n.t('Delete polygon'); + const jsx = ( +
+ {/* eslint-disable-next-line */} + +
+ ); + + deleteControl.onAdd = () => { + const div = L.DomUtil.create('div', ''); + ReactDOM.render(jsx, div); + return div; + }; + setLeafletElement(deleteControl); + }, [onHandleClick, disabled]); + + useEffect(() => { + leafletElement && leafletElement.addTo(leaflet.map); + }, [leafletElement, leaflet.map]); + + useEffect(() => () => leafletElement && leafletElement.remove(), [leafletElement]); + + return null; +}; + +export const DeleteControl = withLeaflet(DeleteControlPlain); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.js new file mode 100644 index 0000000000..d1c2eeefe1 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.component.js @@ -0,0 +1,225 @@ +// @flow +import React, { useState, useRef } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { Modal, ModalTitle, ModalContent, ModalActions, Button, ButtonStrip } from '@dhis2/ui'; +import { ReactLeafletSearch } from 'react-leaflet-search-unpolyfilled'; +import { Map, TileLayer, FeatureGroup, withLeaflet } from 'react-leaflet'; +import { EditControl } from 'react-leaflet-draw'; +import L from 'leaflet'; +import { withStyles } from '@material-ui/core'; +import type { PolygonProps, FeatureCollection } from './Polygon.types'; +import { convertPolygonToServer } from './converters'; +import { DeleteControl } from './DeleteControl.component'; +import { ConditionalTooltip } from './ConditionalTooltip.component'; + +const styles = () => ({ + modalContent: { + width: '100%', + }, + map: { + width: '100%', + height: 'calc(100vh - 380px)', + }, + setAreaButton: { + marginLeft: '5px', + }, +}); + +const coordsToFeatureCollection = (inputCoordinates: any): ?FeatureCollection => { + if (!inputCoordinates) { + return null; + } + const list = inputCoordinates[0].length > 2 ? inputCoordinates[0] : inputCoordinates.map(c => [c[1], c[0]]); + + return { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [list], + }, + }, + ], + }; +}; + +const drawing = { + STARTED: 'STARTED', + FINISHED: 'FINISHED', +}; + +const WrappedLeafletSearch = withLeaflet(ReactLeafletSearch); + +const PolygonPlain = ({ + classes, + center: initialCenter, + setOpen, + defaultValues, + onSetCoordinates, +}: PolygonProps) => { + const [polygonArea, setPolygonArea] = useState(defaultValues); + const [center, setCenter] = useState(initialCenter); + const [drawingState, setDrawingState] = useState(undefined); + const prevDrawingState = useRef(undefined); + + const resetToDefaultValues = () => { + setCenter(initialCenter); + setPolygonArea(defaultValues); + }; + + const onMapPolygonCreated = (e: any) => { + const polygonCoordinates = e.layer.toGeoJSON().geometry.coordinates[0].map(c => [c[1], c[0]]); + setPolygonArea(polygonCoordinates); + setDrawingState(drawing.FINISHED); + prevDrawingState.current = drawing.FINISHED; + }; + + const onMapPolygonDelete = () => { + setPolygonArea(null); + setDrawingState(drawing.FINISHED); + prevDrawingState.current = drawing.FINISHED; + }; + + const onSearch = (searchPosition: any) => { + setCenter(searchPosition); + }; + + const getFeatureCollection = () => (Array.isArray(polygonArea) ? coordsToFeatureCollection(polygonArea) : null); + + const renderMap = () => ( + { + if (ref?.leafletElement) { + ref.leafletElement.invalidateSize(); + if (ref.contextValue && polygonArea) { + const { map } = ref.contextValue; + map?.fitBounds(polygonArea); + } + } + }} + className={classes.map} + > + + + { + onFeatureGroupReady(reactFGref, getFeatureCollection()); + }} + > + setDrawingState(drawing.STARTED)} + onDrawStop={() => setDrawingState(prevDrawingState.current)} + draw={{ + rectangle: false, + polyline: false, + circle: false, + marker: false, + circlemarker: false, + }} + edit={{ + remove: false, + edit: false, + }} + /> + + + + ); + + const onFeatureGroupReady = (reactFGref: any, featureCollection: ?FeatureCollection) => { + if (!reactFGref) { + return; + } + if (featureCollection) { + const leafletGeoJSON = new L.GeoJSON(featureCollection); + const leafletFG = reactFGref.leafletElement; + leafletFG.clearLayers(); + + leafletGeoJSON.eachLayer((layer) => { + leafletFG.addLayer(layer); + }); + } else { + const leafletFG = reactFGref.leafletElement; + leafletFG.clearLayers(); + } + }; + + const renderActions = () => ( + + {!drawingState && ( + + )} + {drawingState && ( + <> + + + + + + )} + + ); + + return ( + + {i18n.t('Area')} + {renderMap()} + {renderActions()} + + ); +}; + +export const Polygon = withStyles(styles)(PolygonPlain); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.js new file mode 100644 index 0000000000..024be1b432 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/Polygon.types.js @@ -0,0 +1,24 @@ +// @flow + +type Feature = { + type: string, + properties: Object, + geometry: { + type: string, + coordinates: Array | number>>, + }, +} + +export type FeatureCollection = { + type: string, + features: Array, +}; + +export type PolygonProps = { + center: ?[number, number], + setOpen: (open: boolean) => void, + onSetCoordinates: (coordinates: ?[number, number] | ?Array<[number, number]>) => void, + defaultValues?: ?Array>, + ...CssClasses, +} + diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/converters.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/converters.js new file mode 100644 index 0000000000..15556ee8c2 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/converters.js @@ -0,0 +1,8 @@ +// @flow + +export const convertPolygonToServer = (coordinates?: Array> | null): ?Array<[number, number]> => { + if (!coordinates) { + return null; + } + return Array<[number, number]>(coordinates.map(c => (c ? [c[1], c[0]] : null))); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/index.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/index.js new file mode 100644 index 0000000000..04659c427b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/Polygon/index.js @@ -0,0 +1,3 @@ +// @flow +export { Polygon } from './Polygon.component'; + diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/index.js b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/index.js new file mode 100644 index 0000000000..41266d1ca4 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MapModal/index.js @@ -0,0 +1,3 @@ +// @flow +export { MapModal } from './MapModal.container'; + diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.component.js new file mode 100644 index 0000000000..a8d3304847 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.component.js @@ -0,0 +1,80 @@ +// @flow +import React, { useState } from 'react'; +import { Map, TileLayer, Marker, Polygon } from 'react-leaflet'; +import { withStyles } from '@material-ui/core'; +import { dataElementTypes } from '../../../metaData'; +import { MapModal } from '../MapModal'; +import type { MiniMapProps } from './MiniMap.types'; +import { convertToClientCoordinates } from './converters'; +import { useUpdateEnrollment } from '../dataMutation/dataMutation'; + +const styles = () => ({ + mapContainer: { + width: 150, + height: 120, + }, + map: { + width: '100%', + height: '100%', + }, +}); + +const MiniMapPlain = ({ + coordinates, + geometryType, + enrollment, + refetchEnrollment, + refetchTEI, + onError, + classes, +}: MiniMapProps) => { + const [isOpenMap, setOpenMap] = useState(false); + const { updateMutation } = useUpdateEnrollment(refetchEnrollment, refetchTEI, onError); + const clientValues = convertToClientCoordinates(coordinates, geometryType); + const center = geometryType === dataElementTypes.COORDINATE ? clientValues : clientValues[0]; + const onMapReady = (mapRef) => { + if (mapRef?.contextValue && geometryType === dataElementTypes.POLYGON) { + const { map } = mapRef.contextValue; + map?.fitBounds(clientValues); + } + }; + + return ( + <> +
+ { + onMapReady(mapRef); + }} + center={center} + className={classes.map} + zoom={11} + zoomControl={false} + attributionControl={false} + key="minimap" + onClick={() => { + setOpenMap(true); + }} + > + + {geometryType === dataElementTypes.COORDINATE && } + {geometryType === dataElementTypes.POLYGON && } + +
+ {isOpenMap && ( + + )} + + ); +}; + +export const MiniMap = withStyles(styles)(MiniMapPlain); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.js b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.js new file mode 100644 index 0000000000..b905c28783 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/MiniMap.types.js @@ -0,0 +1,13 @@ +// @flow +import type { QueryRefetchFunction } from '@dhis2/app-runtime'; +import { dataElementTypes } from '../../../metaData'; + +export type MiniMapProps = { + coordinates: Array>, + enrollment: any, + refetchEnrollment: QueryRefetchFunction, + refetchTEI: QueryRefetchFunction, + onError?: (message: string) => void, + geometryType: typeof dataElementTypes.COORDINATE | typeof dataElementTypes.POLYGON, + ...CssClasses +} diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/converters.js b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/converters.js new file mode 100644 index 0000000000..498738ef13 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/converters.js @@ -0,0 +1,10 @@ +// @flow +import { dataElementTypes } from '../../../metaData'; + +export const convertToClientCoordinates = (coordinates: any[], type: $Values) => { + if (type === dataElementTypes.COORDINATE) { + return [coordinates[1], coordinates[0]]; + } + + return coordinates[0].map(coord => [coord[1], coord[0]]); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/index.js b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/index.js new file mode 100644 index 0000000000..c1e7fa0e6f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/MiniMap/index.js @@ -0,0 +1,2 @@ +// @flow +export { MiniMap } from './MiniMap.component'; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js index 78c7f1cb08..018a6e7798 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js @@ -4,7 +4,6 @@ import moment from 'moment'; import { IconClock16, IconDimensionOrgUnit16, - IconLocation16, colors, Tag, spacersNum, @@ -16,11 +15,11 @@ import { LoadingMaskElementCenter } from '../LoadingMasks'; import { Widget } from '../Widget'; import type { PlainProps } from './enrollment.types'; import { Status } from './Status'; -import { convertValue as convertValueServerToClient } from '../../converters/serverToClient'; -import { convertValue as convertValueClientToView } from '../../converters/clientToView'; import { dataElementTypes } from '../../metaData'; +import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { Date } from './Date'; import { Actions } from './Actions'; +import { MiniMap } from './MiniMap'; const styles = { enrollment: { @@ -68,6 +67,7 @@ export const WidgetEnrollmentPlain = ({ const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); const geometryType = getGeometryType(enrollment?.geometry?.type); + const { displayName: orgUnitName } = useOrgUnitName(enrollment.orgUnit); return (
@@ -84,7 +84,7 @@ export const WidgetEnrollmentPlain = ({ )} {loading && } {!initError && !loading && ( -
+
{enrollment.followUp && ( @@ -125,7 +125,7 @@ export const WidgetEnrollmentPlain = ({ {i18n.t('Started at {{orgUnitName}}', { - orgUnitName: enrollment.orgUnitName, + orgUnitName, interpolation: { escapeValue: false }, })}
@@ -150,13 +150,14 @@ export const WidgetEnrollmentPlain = ({ {enrollment.geometry && (
- - - - {convertValueClientToView( - convertValueServerToClient(enrollment.geometry.coordinates, geometryType), - geometryType, - )} +
)} item.program === programId) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/dataMutation/dataMutation.js b/src/core_modules/capture-core/components/WidgetEnrollment/dataMutation/dataMutation.js new file mode 100644 index 0000000000..b43c8f318f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/dataMutation/dataMutation.js @@ -0,0 +1,67 @@ +// @flow +import { useDataMutation, type QueryRefetchFunction } from '@dhis2/app-runtime'; + + +const enrollmentUpdate = { + resource: 'tracker?async=false&importStrategy=UPDATE', + type: 'create', + data: enrollment => ({ + enrollments: [enrollment], + }), +}; + +const enrollmentDelete = { + resource: 'tracker?async=false&importStrategy=DELETE', + type: 'create', + data: enrollment => ({ + enrollments: [enrollment], + }), +}; + +const processErrorReports = (error) => { + // $FlowFixMe[prop-missing] + const errorReports = error?.details?.validationReport?.errorReports; + return errorReports?.length > 0 + ? errorReports.reduce((acc, errorReport) => `${acc} ${errorReport.message}`, '') + : error.message; +}; + + +export const useUpdateEnrollment = ( + refetchEnrollment: QueryRefetchFunction, + refetchTEI: QueryRefetchFunction, + onError?: ?(message: string) => void, +) => { + const [updateMutation, { loading: updateLoading }] = useDataMutation( + enrollmentUpdate, + { + onComplete: () => { + refetchEnrollment(); + refetchTEI(); + }, + onError: (e) => { + onError && onError(processErrorReports(e)); + }, + }, + ); + return { + updateMutation, updateLoading, + }; +}; + +export const useDeleteEnrollment = ( + onDelete: () => void, + onError?: ?(message: string) => void, +) => { + const [deleteMutation, { loading: deleteLoading }] = useDataMutation( + enrollmentDelete, + { + onComplete: onDelete, + onError: (e) => { + onError && onError(processErrorReports(e)); + }, + }, + ); + return { deleteMutation, deleteLoading }; +}; + diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useGeometry.js b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useGeometry.js new file mode 100644 index 0000000000..3569c211db --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useGeometry.js @@ -0,0 +1,39 @@ +// @flow +import i18n from '@dhis2/d2-i18n'; +import { dataElementTypes } from '../../../metaData'; +import { useProgram } from './useProgram'; + +export const useGeometry = (enrollment: { program: string }) => { + const { + program: { featureType }, + } = useProgram(enrollment.program); + + if (featureType === 'POINT') { + return { + geometryType: 'Point', + dataElementType: dataElementTypes.COORDINATE, + }; + } + + return { + geometryType: 'Polygon', + dataElementType: dataElementTypes.POLYGON, + }; +}; + +export const useGeometryLabel = (enrollment: { program: string, geometry: { type: string } }) => { + const { + program: { featureType }, + error, + } = useProgram(enrollment.program); + + if (error || !featureType || !['POINT', 'POLYGON'].includes(featureType) || enrollment.geometry?.type) { + return undefined; + } + + if (featureType === 'POINT') { + return i18n.t('Add coordinates'); + } + + return i18n.t('Add area'); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useOrganizationUnit.js b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useOrganizationUnit.js deleted file mode 100644 index 875e86796b..0000000000 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useOrganizationUnit.js +++ /dev/null @@ -1,32 +0,0 @@ -// @flow -import { useMemo } from 'react'; -import { useDataQuery } from '@dhis2/app-runtime'; - -export const useOrganizationUnit = (ownerOrgUnit: string | boolean) => { - const { error, loading, data, refetch, called } = useDataQuery( - useMemo( - () => ({ - organisationUnits: { - resource: 'organisationUnits', - id: ({ variables: { ownerOrgUnit: id } }) => id, - params: { - fields: ['displayName'], - }, - }, - }), - [], - ), - { - lazy: true, - }, - ); - - if (ownerOrgUnit && !called) { - refetch({ variables: { ownerOrgUnit } }); - } - - return { - error, - displayName: !loading && data?.organisationUnits?.displayName, - }; -}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.js b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.js index 4d643cb140..de41c3692e 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.js @@ -10,7 +10,7 @@ export const useProgram = (programId: string) => { resource: `programs/${programId}`, params: { fields: [ - 'displayIncidentDate,displayIncidentDateLabel,displayEnrollmentDateLabel,onlyEnrollOnce,trackedEntityType[displayName],programStages[autoGenerateEvent],access', + 'displayIncidentDate,displayIncidentDateLabel,displayEnrollmentDateLabel,onlyEnrollOnce,trackedEntityType[displayName],programStages[autoGenerateEvent],access,featureType', ], }, }, @@ -18,5 +18,5 @@ export const useProgram = (programId: string) => { [programId], ), ); - return { error, program: !loading && data?.program }; + return { error, loading, program: data?.program }; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/common.types.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/common.types.js index ccdcea6c45..791ff8fa85 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/common.types.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/common.types.js @@ -25,7 +25,6 @@ export type EnrollmentEvent = {| programId: string, programStageId: string, orgUnitId: string, - orgUnitName: string, trackedEntityInstanceId: string, enrollmentId: string, enrollmentStatus: string, diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/scheduleDate.types.js b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/scheduleDate.types.js index 84d5f037e3..e181165e6f 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/scheduleDate.types.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/scheduleDate.types.js @@ -12,6 +12,6 @@ export type Props = {| eventCountInOrgUnit: number, suggestedScheduleDate?: ?string, hideDueDate?: boolean, - orgUnit: Object, + orgUnit: { id: string, name: string }, ...CssClasses, |}; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js index 367ab395bb..489ce42dbc 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js @@ -4,7 +4,7 @@ import i18n from '@dhis2/d2-i18n'; import { useDispatch } from 'react-redux'; import moment from 'moment'; import { getProgramAndStageForProgram, TrackerProgram, getProgramEventAccess } from '../../metaData'; -import { useOrganisationUnit } from '../../dataQueries'; +import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { useLocationQuery } from '../../utils/routing'; import type { ContainerProps } from './widgetEventSchedule.types'; import { WidgetEventScheduleComponent } from './WidgetEventSchedule.component'; @@ -35,7 +35,7 @@ export const WidgetEventSchedule = ({ }: ContainerProps) => { const { program, stage } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); const dispatch = useDispatch(); - const { orgUnit } = useOrganisationUnit(orgUnitId, 'displayName'); + const orgUnit = { id: orgUnitId, name: useOrgUnitName(orgUnitId).displayName }; const { programStageScheduleConfig } = useScheduleConfigFromProgramStage(stageId); const { programConfig } = useScheduleConfigFromProgram(programId); const suggestedScheduleDate = useDetermineSuggestedScheduleDate({ diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useEvents.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useEvents.js index 868e1c5889..0c900af9a4 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useEvents.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useEvents.js @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import { convertValue } from '../../../../converters/serverToClient'; import { dataElementTypes } from '../../../../metaData'; +import { useOrgUnitNames } from '../../../../metadataRetrieval/orgUnitName'; const convertDate = date => convertValue(date, dataElementTypes.DATE); @@ -14,16 +15,26 @@ const getClientFormattedDataValuesAsObject = (dataValues, elementsById) => return acc; }, {}); -export const useEvents = (enrollment: any, elementsById: Array) => - useMemo( +const getOrgUnitIds = (enrollment: any): Array => + (enrollment ? enrollment.events.reduce((acc, event) => { + if (event.orgUnit) { + acc.push(event.orgUnit); + } + return acc; + }, []) : []); + +export const useEvents = (enrollment: any, elementsById: Array) => { + const orgUnitIds = useMemo(() => getOrgUnitIds(enrollment), [enrollment]); + const { orgUnitNames } = useOrgUnitNames(orgUnitIds); + return useMemo( () => - enrollment && + enrollment && orgUnitNames && enrollment.events.map(event => ({ eventId: event.event, programId: event.program, programStageId: event.programStage, orgUnitId: event.orgUnit, - orgUnitName: event.orgUnitName, + orgUnitName: orgUnitNames[event.orgUnit], trackedEntityInstanceId: event.trackedEntityInstance, enrollmentId: event.enrollment, enrollmentStatus: event.enrollmentStatus, @@ -32,5 +43,6 @@ export const useEvents = (enrollment: any, elementsById: Array) => dueDate: convertDate(event.dueDate), ...getClientFormattedDataValuesAsObject(event.dataValues, elementsById), })), - [elementsById, enrollment], + [elementsById, enrollment, orgUnitNames], ); +}; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js index 6d5da181fa..0cc28bc98c 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.js @@ -103,6 +103,11 @@ const StageDetailPlain = (props: Props) => { onCreateNew(stageId); }, [onCreateNew, stageId]); + const handleShowMore = useCallback(() => { + const nextRowIndex = Math.min(events.length, displayedRowNumber + DEFAULT_NUMBER_OF_ROW); + setDisplayedRowNumber(nextRowIndex); + }, [events, displayedRowNumber, setDisplayedRowNumber]); + function renderHeader() { const headerCells = headerColumns .map(column => ( @@ -181,16 +186,14 @@ const StageDetailPlain = (props: Props) => { } function renderFooter() { - const renderShowMoreButton = () => (events.length > DEFAULT_NUMBER_OF_ROW + const renderShowMoreButton = () => (dataSource && !loading + && events.length > DEFAULT_NUMBER_OF_ROW && displayedRowNumber < events.length ?