diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d88ab41f9..9bbd699a93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,87 @@ +# [100.39.0](https://github.com/dhis2/capture-app/compare/v100.38.0...v100.39.0) (2023-09-07) + + +### Features + +* [DHIS2-13343] hidden program stage rule effect ([#3406](https://github.com/dhis2/capture-app/issues/3406)) ([4ef2973](https://github.com/dhis2/capture-app/commit/4ef2973b71d6376f99db07e70bc4d51facb8018e)) + +# [100.38.0](https://github.com/dhis2/capture-app/compare/v100.37.0...v100.38.0) (2023-09-06) + + +### Features + +* [DHIS2-14334] edit enrollment date ([#3350](https://github.com/dhis2/capture-app/issues/3350)) ([9dd1b6a](https://github.com/dhis2/capture-app/commit/9dd1b6a046e94021ae4e63a67a3d6b16a099212d)) + +# [100.37.0](https://github.com/dhis2/capture-app/compare/v100.36.0...v100.37.0) (2023-08-22) + + +### Features + +* [DHIS2-15299] escape value for attribute filter ([#3403](https://github.com/dhis2/capture-app/issues/3403)) ([5db743e](https://github.com/dhis2/capture-app/commit/5db743eaf7f67bbb4a9391ac9ade25317e4d3642)) + +# [100.36.0](https://github.com/dhis2/capture-app/compare/v100.35.9...v100.36.0) (2023-08-22) + + +### Features + +* [DHIS2-15229] search for MULTI_TEXT ([#3395](https://github.com/dhis2/capture-app/issues/3395)) ([c5d9a7d](https://github.com/dhis2/capture-app/commit/c5d9a7d57547153dd62cf36c8e3323685f43978f)) + +## [100.35.9](https://github.com/dhis2/capture-app/compare/v100.35.8...v100.35.9) (2023-08-22) + + +### Bug Fixes + +* [DHIS2-15700] Option sets not working in TEI Registration ([2e47477](https://github.com/dhis2/capture-app/commit/2e4747719151b042d3539fe61a612ebd0ac122d4)) + +## [100.35.8](https://github.com/dhis2/capture-app/compare/v100.35.7...v100.35.8) (2023-08-22) + + +### Bug Fixes + +* [DHIS2-15492] transition of tooltip enabled state ([#3381](https://github.com/dhis2/capture-app/issues/3381)) ([adb6b32](https://github.com/dhis2/capture-app/commit/adb6b32f1bffc5020ca34ab5c7568b5a323a8002)) + +## [100.35.7](https://github.com/dhis2/capture-app/compare/v100.35.6...v100.35.7) (2023-08-15) + + +### Bug Fixes + +* [DHIS2-15525] show formNames in the enrollment dashboard ([#3402](https://github.com/dhis2/capture-app/issues/3402)) ([c02c3ee](https://github.com/dhis2/capture-app/commit/c02c3ee46ed4209215946636682ec86c97c9d905)) + +## [100.35.6](https://github.com/dhis2/capture-app/compare/v100.35.5...v100.35.6) (2023-08-11) + + +### Bug Fixes + +* [TECH-1623] rename TEI "Launchpad McQuack" -> "Breaking TheGlass" ([#3400](https://github.com/dhis2/capture-app/issues/3400)) ([7f26237](https://github.com/dhis2/capture-app/commit/7f262372499b98d9fd8071c7b8069238ad3a1d15)) + +## [100.35.5](https://github.com/dhis2/capture-app/compare/v100.35.4...v100.35.5) (2023-08-03) + + +### Bug Fixes + +* [DHIS2-15365] remove orgUnit and ouMode query args ([#3398](https://github.com/dhis2/capture-app/issues/3398)) ([ef35eb3](https://github.com/dhis2/capture-app/commit/ef35eb300fa61343576957e6734a2767dc37b4ec)) + +## [100.35.4](https://github.com/dhis2/capture-app/compare/v100.35.3...v100.35.4) (2023-08-01) + + +### Bug Fixes + +* **translations:** sync translations from transifex (master) ([343e5da](https://github.com/dhis2/capture-app/commit/343e5daa158faf257cdf1277ff8f7a3e0e5d24ec)) + +## [100.35.3](https://github.com/dhis2/capture-app/compare/v100.35.2...v100.35.3) (2023-07-27) + + +### Bug Fixes + +* **translations:** sync translations from transifex (master) ([4568f0f](https://github.com/dhis2/capture-app/commit/4568f0f3da3bc979a64bf3650c8e658756f2a580)) + +## [100.35.2](https://github.com/dhis2/capture-app/compare/v100.35.1...v100.35.2) (2023-07-26) + + +### Bug Fixes + +* **translations:** sync translations from transifex (master) ([085b3a3](https://github.com/dhis2/capture-app/commit/085b3a3eb5e1fe67cc66d1ef741dde64dabfdc7e)) + ## [100.35.1](https://github.com/dhis2/capture-app/compare/v100.35.0...v100.35.1) (2023-07-25) diff --git a/cypress/integration/EnrollmentPage/BreakingTheGlass/index.js b/cypress/integration/EnrollmentPage/BreakingTheGlass/index.js index b460d47b33..45ad92f517 100644 --- a/cypress/integration/EnrollmentPage/BreakingTheGlass/index.js +++ b/cypress/integration/EnrollmentPage/BreakingTheGlass/index.js @@ -1,7 +1,7 @@ import '../sharedSteps'; Given('the tei created by this test is cleared from the database', () => { - cy.buildApiUrl('tracker', 'trackedEntities?filter=w75KJ2mc4zz:like:Launchpad&filter=zDhUuAYrxNC:like:McQuack&trackedEntityType=nEenWmSyUEp&page=1&pageSize=5&ouMode=ACCESSIBLE') + cy.buildApiUrl('tracker', 'trackedEntities?filter=w75KJ2mc4zz:like:Breaking&filter=zDhUuAYrxNC:like:TheGlass&trackedEntityType=nEenWmSyUEp&page=1&pageSize=5&ouMode=ACCESSIBLE') .then(url => cy.request(url)) .then(({ body }) => body.instances.forEach(({ trackedEntity }) => @@ -24,12 +24,12 @@ And('you create a new tei in Child programme from Ngelehun CHC', () => { cy.get('[data-test="d2-form-component"]') .find('[data-test="capture-ui-input"]') .eq(0) - .type('Launchpad') + .type('Breaking') .blur(); cy.get('[data-test="d2-form-component"]') .find('[data-test="capture-ui-input"]') .eq(1) - .type('McQuack') + .type('TheGlass') .blur(); clickSave(); @@ -61,7 +61,7 @@ And('you enroll the tei from Njandama MCHP', () => { .click(); cy.get('[data-test="enrollment-page-content"]') - .contains('Enroll Launchpad McQuack in this program') + .contains('Enroll Breaking TheGlass in this program') .click(); cy.get('[data-test="d2-form-component"]') @@ -97,7 +97,7 @@ And('you log in as tracker2 user', () => { And('you select the new tei', () => { cy.visit('/#/?orgUnitId=DiszpKrYNg8&programId=IpHINAT79UW'); - cy.contains('Launchpad') + cy.contains('Breaking') .click(); // Wait for enrollment dashboard cy.get('[data-test="profile-widget"]') diff --git a/cypress/integration/EnrollmentPage/HiddenProgramStage.feature b/cypress/integration/EnrollmentPage/HiddenProgramStage.feature new file mode 100644 index 0000000000..4e6e427e3e --- /dev/null +++ b/cypress/integration/EnrollmentPage/HiddenProgramStage.feature @@ -0,0 +1,7 @@ +Feature: Hidden program stage + + Scenario: The user cannot add an event in a hidden program stage + Given you add an enrollment event that will result in a rule effect to hide a program stage + Then the New Postpartum care visit event button is disabled in the stages and events widget + And and an error is show in the Postpartum care visit stage + And the Postpartum care visit button is disabled in the enrollmentEventNew page diff --git a/cypress/integration/EnrollmentPage/HiddenProgramStage/index.js b/cypress/integration/EnrollmentPage/HiddenProgramStage/index.js new file mode 100644 index 0000000000..edf6b833bd --- /dev/null +++ b/cypress/integration/EnrollmentPage/HiddenProgramStage/index.js @@ -0,0 +1,66 @@ +import moment from 'moment'; + +const cleanUpIfApplicable = () => { + cy.buildApiUrl( + 'tracker', + 'trackedEntities/uW8Y7AIcRKA?program=WSGAb5XwJ3Y&fields=enrollments', + ) + .then(url => cy.request(url)) + .then(({ body }) => { + const enrollment = body.enrollments?.find(e => e.enrollment === 'fmhIsWXVDmS'); + const event = enrollment?.events?.find(e => e.programStage === 'PFDfvmGpsR3'); + if (!event) { + return null; + } + return cy + .buildApiUrl('events', event.event) + .then(eventUrl => + cy.request('DELETE', eventUrl)); + }); +}; + +Given('you add an enrollment event that will result in a rule effect to hide a program stage', () => { + cleanUpIfApplicable(); + cy.visit( + '/#/enrollmentEventNew?enrollmentId=fmhIsWXVDmS&orgUnitId=s7SLtx8wmRA&programId=WSGAb5XwJ3Y&stageId=PFDfvmGpsR3&teiId=uW8Y7AIcRKA', + ); + + cy.get('[data-test="capture-ui-input"]') + .eq(0) + .type(moment().format('YYYY-MM-DD')) + .blur(); + + cy + .get('[data-test="virtualized-select"]') + .eq(6) + .click() + .contains('Termination of pregnancy') + .click(); + + cy.contains('[data-test="dhis2-uicore-button"]', 'Save without completing').click(); +}); + +Then('the New Postpartum care visit event button is disabled in the stages and events widget', () => { + cy.contains('[data-test="create-new-button"]', 'New Postpartum care visit event') + .should('be.disabled'); +}); + +Then('and an error is show in the Postpartum care visit stage', () => { + cy.visit( + '/#/enrollmentEventNew?enrollmentId=fmhIsWXVDmS&orgUnitId=s7SLtx8wmRA&programId=WSGAb5XwJ3Y&teiId=uW8Y7AIcRKA&stageId=bbKtnxRZKEP', + ); + cy.contains('[data-test="dhis2-uicore-button"]', 'Complete') + .should('be.disabled'); + cy.contains('[data-test="dhis2-uicore-button"]', 'Save without completing') + .should('be.disabled'); + cy.contains('[data-test="dhis2-uicore-noticebox-content"]', 'You can\'t add any more Postpartum care visit events') + .should('exist'); +}); + +Then('the Postpartum care visit button is disabled in the enrollmentEventNew page', () => { + cy.visit( + '/#/enrollmentEventNew?enrollmentId=fmhIsWXVDmS&orgUnitId=s7SLtx8wmRA&programId=WSGAb5XwJ3Y&teiId=uW8Y7AIcRKA', + ); + + cy.contains('[data-test="program-stage-selector-button"]', 'Postpartum care visit').should('be.disabled'); +}); diff --git a/cypress/integration/SearchPage.feature b/cypress/integration/SearchPage.feature index ea30752589..0f98fdfdfc 100644 --- a/cypress/integration/SearchPage.feature +++ b/cypress/integration/SearchPage.feature @@ -37,14 +37,6 @@ Feature: User interacts with Search page # And you click search # Then you should see no results found - Scenario: Searching using attributes in Tracker Program throws error - Given you are on the default search page - When you select the search domain Malaria Case diagnosis - And you expand the attributes search area - And you fill in the first name with values that will return an error - And you click search - Then there should be an generic error message - Scenario: Searching using attributes in Tracker Program is invalid because no terms typed Given you are on the default search page When you select the search domain Malaria Case diagnosis diff --git a/cypress/integration/SearchPage/index.js b/cypress/integration/SearchPage/index.js index 40868ea394..93d0212d34 100644 --- a/cypress/integration/SearchPage/index.js +++ b/cypress/integration/SearchPage/index.js @@ -151,19 +151,6 @@ When('for Person you fill in values that will return less than 5 results', () => .blur(); }); -When('you fill in the first name with values that will return an error', () => { - cy.get('[data-test="form-attributes"]') - .find('[data-test="capture-ui-input"]') - .first() - .type(',,,,') - .blur(); -}); - -Then('there should be an generic error message', () => { - cy.get('[data-test="general-purpose-error-mesage"]') - .should('exist'); -}); - When('you dont fill in any of the values', () => { cy.get('[data-test="form-attributes"]') .find('[data-test="capture-ui-input"]') diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollment/index.js b/cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollment/index.js index 26146624d7..4584e1ba2a 100644 --- a/cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollment/index.js +++ b/cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollment/index.js @@ -19,18 +19,18 @@ Then('the enrollment widget should be opened', () => { }); Then('the user sees the enrollment date', () => { - cy.get('[data-test="widget-enrollment"]').within(() => { + cy.get('[data-test="widget-enrollment-enrollment-date"]').within(() => { cy.get('[data-test="widget-enrollment-icon-calendar"]').should('exist'); - cy.get('[data-test="widget-enrollment-enrollment-date"]') - .contains(`Date of enrollment ${getCurrentYear()}-08-01`) + cy.get('[data-test="widget-enrollment-date"]') + .contains(`Date of enrollment: ${getCurrentYear()}-08-01`) .should('exist'); }); }); Then('the user sees the incident date', () => { - cy.get('[data-test="widget-enrollment"]').within(() => { - cy.get('[data-test="widget-enrollment-incident-date"]') - .contains(`Date of birth ${getCurrentYear()}-08-01`) + cy.get('[data-test="widget-enrollment-incident-date"]').within(() => { + cy.get('[data-test="widget-enrollment-date"]') + .contains(`Date of birth: ${getCurrentYear()}-08-01`) .should('exist'); }); }); diff --git a/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsDev/index.js b/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsDev/index.js index a200fcf728..b1b8f6c79a 100644 --- a/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsDev/index.js +++ b/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsDev/index.js @@ -38,7 +38,7 @@ Then('the list should display the events retrieved from the api', () => { cy.get('[data-test="event-working-lists"]') .find('tr') .each(($teiRow, index) => { - const rowId = $teiRow.get(0).getAttribute('id'); + const rowId = $teiRow.get(0).getAttribute('data-test'); if (index > 1) { expect(rowId).to.equal(teis[index - 1].event); } @@ -232,25 +232,27 @@ When('you click the report date column header', () => { cy.route('GET', '**/tracker/events**').as('getEvents'); - cy.get('[data-test="online-list-table"]') - .contains('Report date') + cy.get('[data-test="dhis2-uicore-tableheadercellaction"]') + .eq(0) + .click() .click(); }); Then('events should be retrieved from the api ordered ascendingly by report date', () => { - cy.wait('@getEvents', { timeout: 40000 }).as('result'); + cy.wait('@getEvents', { timeout: 40000 }).as('resultDefault'); + cy.wait('@getEvents', { timeout: 40000 }).as('resultAsc'); - cy.get('@result') + cy.get('@resultAsc') .its('status') .should('equal', 200); - cy.get('@result') + cy.get('@resultAsc') .its('url') .should('match', /order=.*asc/); - cy.get('@result') + cy.get('@resultAsc') .its('url') .should('include', 'page=1'); - cy.get('@result').its('response.body.instances').as('events'); + cy.get('@resultAsc').its('response.body.instances').as('events'); }); diff --git a/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js b/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js index ed72014a7f..cacda7707f 100644 --- a/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js +++ b/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js @@ -256,8 +256,9 @@ Then('the list should display 10 rows of data', () => { }); When('you click the report date column header', () => { - cy.get('[data-test="online-list-table"]') - .contains('Report date') + cy.get('[data-test="dhis2-uicore-tableheadercellaction"]') + .eq(0) + .click() .click(); }); @@ -364,8 +365,9 @@ When('you change the sharing settings', () => { When('you update the working list', () => { - cy.get('[data-test="online-list-table"]') - .contains('Report date') + cy.get('[data-test="dhis2-uicore-tableheadercellaction"]') + .eq(0) + .click() .click(); cy.get('[data-test="list-view-menu-button"]') diff --git a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsDev/index.js b/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsDev/index.js index 1c8cacb865..d7bbe85138 100644 --- a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsDev/index.js +++ b/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsDev/index.js @@ -146,7 +146,7 @@ Then('the list should display the teis retrieved from the api', () => { cy.get('[data-test="tei-working-lists"]') .find('tr') .each(($teiRow, index) => { - const rowId = $teiRow.get(0).getAttribute('id'); + const rowId = $teiRow.get(0).getAttribute('data-test'); if (index > 1) { expect(rowId).to.equal(teis[index - 1].trackedEntity); } @@ -226,8 +226,8 @@ When('you click the first name column header', () => { cy.route('GET', '**/tracker/trackedEntities**').as('getTeis'); - cy.get('[data-test="online-list-table"]') - .contains('First name') + cy.get('[data-test="dhis2-uicore-tableheadercellaction"]') + .eq(0) .click(); }); diff --git a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js b/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js index db0dfacfb6..6426baf38c 100644 --- a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js +++ b/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js @@ -62,7 +62,6 @@ Given('you open the main page with Ngelehun and Malaria case diagnosis and House Then('the default working list should be displayed', () => { const names = [ - 'John', 'Filona', 'Gertrude', 'Frank', @@ -77,6 +76,7 @@ Then('the default working list should be displayed', () => { 'Julia', 'Elizabeth', 'Donald', + 'Wayne', ]; cy.get('[data-test="tei-working-lists"]') @@ -276,7 +276,6 @@ Then('the registering unit should display in the list', () => { Then('the list should display data for the second page', () => { const names = [ - 'Wayne', 'Johnny', 'Donna', 'Sharon', @@ -291,6 +290,7 @@ Then('the list should display data for the second page', () => { 'Noah', 'Emily', 'Lily', + 'Olvia', ]; cy.get('[data-test="tei-working-lists"]') @@ -307,7 +307,6 @@ Then('the list should display data for the second page', () => { Then('the list should display 10 rows of data', () => { const names = [ - 'John', 'Filona', 'Gertrud', 'Frank', @@ -317,6 +316,7 @@ Then('the list should display 10 rows of data', () => { 'Alan', 'Heather', 'Andrea', + 'Donald', ]; cy.get('[data-test="tei-working-lists"]') @@ -332,20 +332,21 @@ Then('the list should display 10 rows of data', () => { }); When('you click the first name column header', () => { - cy.get('[data-test="online-list-table"]') - .contains('First name') + cy.get('[data-test="dhis2-uicore-tableheadercellaction"]') + .eq(0) .click(); }); When('you click the last name column header', () => { - cy.get('[data-test="online-list-table"]') - .contains('Last name') + cy.get('[data-test="dhis2-uicore-tableheadercellaction"]') + .eq(2) .click(); }); When('you click the WHOMCH Smoking column header', () => { - cy.get('[data-test="online-list-table"]') - .contains('WHOMCH Smoking') + cy.get('[data-test="dhis2-uicore-tableheadercellaction"]') + .eq(6) + .click() .click(); }); diff --git a/cypress/integration/WorkingLists/sharedSteps.js b/cypress/integration/WorkingLists/sharedSteps.js index 4dd206ad9b..5c359d2569 100644 --- a/cypress/integration/WorkingLists/sharedSteps.js +++ b/cypress/integration/WorkingLists/sharedSteps.js @@ -75,8 +75,9 @@ Then('the pagination for the tei working list should show the second page', () = }); Then('the sort arrow should indicate ascending order', () => { - cy.get('[data-test="data-table-asc-sort-icon"]') - .should('exist'); + cy.get('[data-test="table-row"]').within(() => { + cy.get('[data-test="table-row-asc"]').should('exist'); + }); }); Then('the enrollment status filter button should show that the active filter is in effect', () => { @@ -133,8 +134,9 @@ When('you click the first page button', () => { }); Then('the sort arrow should indicate descending order', () => { - cy.get('[data-test="data-table-desc-sort-icon"]') - .should('exist'); + cy.get('[data-test="table-row"]').within(() => { + cy.get('[data-test="table-row-desc"]').should('exist'); + }); }); Then('rows per page should be set to 15', () => { diff --git a/i18n/en.pot b/i18n/en.pot index f69fbe22a0..f2f3189897 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -373,9 +373,6 @@ msgstr "Some operations are still runnning. Please wait.." msgid "Operations running" msgstr "Operations running" -msgid "Sort" -msgstr "Sort" - msgid "" "This event has unsaved changes. Leaving this page without saving will lose " "these changes. Are you sure you want to discard unsaved changes?" @@ -775,8 +772,8 @@ msgstr "Schedule" msgid "Refer" msgstr "Refer" -msgid "You can’t add any more {{ programStageName }} events" -msgstr "You can’t add any more {{ programStageName }} events" +msgid "You can't add any more {{ programStageName }} events" +msgstr "You can't add any more {{ programStageName }} events" msgid "Cancel without saving" msgstr "Cancel without saving" @@ -1164,6 +1161,9 @@ msgstr "Remove mark for follow-up" msgid "Mark for follow-up" 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 "Enrollment date" msgstr "Enrollment date" @@ -1536,9 +1536,6 @@ msgstr "Error editing the event, the changes made were not saved" msgid "Set coordinate" msgstr "Set coordinate" -msgid "Page {{currentPage}}" -msgstr "Page {{currentPage}}" - msgid "Date" msgstr "Date" @@ -1557,6 +1554,9 @@ msgstr "To date" msgid "To time" msgstr "To time" +msgid "Page {{currentPage}}" +msgstr "Page {{currentPage}}" + msgid "Delete polygon" msgstr "Delete polygon" diff --git a/i18n/lo.po b/i18n/lo.po index aff6d396ab..768265b14c 100644 --- a/i18n/lo.po +++ b/i18n/lo.po @@ -2,15 +2,15 @@ # Translators: # phil_dhis2, 2022 # Thuy Nguyen , 2022 -# Saysamone Sibounma, 2023 # Viktor Varland , 2023 +# Saysamone Sibounma, 2023 # msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2023-06-27T06:20:33.460Z\n" +"POT-Creation-Date: 2023-06-01T08:11:59.116Z\n" "PO-Revision-Date: 2019-06-27 07:31+0000\n" -"Last-Translator: Viktor Varland , 2023\n" +"Last-Translator: Saysamone Sibounma, 2023\n" "Language-Team: Lao (https://app.transifex.com/hisp-uio/teams/100509/lo/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -69,6 +69,16 @@ msgstr "Date of enrollment" msgid "Last updated" msgstr "ປັບປຸງ​ຄັ້ງຫລ້າສຸດ" +msgid "error encountered during field validation" +msgstr "" + +msgid "error" +msgstr "" + +msgid "" +"Plugins are not yet available - Please contact your system administrator" +msgstr "" + msgid "This value is validating" msgstr "" @@ -463,7 +473,7 @@ msgid "Delete" msgstr "ລົບ" msgid "Select file" -msgstr "" +msgstr "ເລືອກຟາຍ" msgid "No" msgstr "ບໍ່ແມນ" @@ -474,6 +484,12 @@ msgstr "" msgid "Select image" msgstr "" +msgid "Type to filter options" +msgstr "" + +msgid "No match found" +msgstr "" + msgid "Search" msgstr "ຄົນຫາ" @@ -803,6 +819,9 @@ msgstr "" msgid "New Enrollment in program{{escape}} {{programName}}" msgstr "" +msgid "Save {{trackedEntityTypeName}}" +msgstr "" + msgid "Save {{trackedEntityName}}" msgstr "" @@ -956,10 +975,6 @@ msgstr "" msgid "Choose a type to start searching" msgstr "" -msgid "Fill in at least {{count}} attribute to search" -msgid_plural "Fill in at least {{count}} attribute to search" -msgstr[0] "" - msgid "Search {{name}}" msgstr "" @@ -1200,22 +1215,12 @@ msgid "" "The scheduled date matches the suggested date, but can be changed if needed." msgstr "" -msgid "The scheduled date is {{count}} days {{position}} the suggested date." -msgid_plural "" -"The scheduled date is {{count}} days {{position}} the suggested date." -msgstr[0] "" - msgid "after" msgstr "" msgid "before" msgstr "" -msgid "There are {{count}} scheduled event in {{orgUnitName}} on this day." -msgid_plural "" -"There are {{count}} scheduled event in {{orgUnitName}} on this day." -msgstr[0] "" - msgid "" "Scheduling an event in {{stageName}} for {{programName}} in {{orgUnitName}}" msgstr "" @@ -1259,9 +1264,6 @@ msgstr "" msgid "Try again or contact your system administrator for support" msgstr "" -msgid "tracked entity instance" -msgstr "" - msgid "Fix errors in the form to continue." msgstr "" @@ -1274,6 +1276,9 @@ msgstr "" msgid "Edit" msgstr "ແກ້ໄຂ" +msgid "tracked entity instance" +msgstr "" + msgid "New {{ eventName }} event" msgstr "" @@ -1484,12 +1489,6 @@ msgstr "ເຖີງວັນທີ່" msgid "To time" msgstr "" -msgid "error encountered during field validation" -msgstr "" - -msgid "error" -msgstr "" - msgid "Delete polygon" msgstr "" diff --git a/i18n/uz.po b/i18n/uz.po index db80881838..2165735667 100644 --- a/i18n/uz.po +++ b/i18n/uz.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2023-06-27T06:20:33.460Z\n" +"POT-Creation-Date: 2023-06-01T08:11:59.116Z\n" "PO-Revision-Date: 2019-06-27 07:31+0000\n" "Last-Translator: Ibatov , 2023\n" "Language-Team: Uzbek (Cyrillic) (https://app.transifex.com/hisp-uio/teams/100509/uz@Cyrl/)\n" @@ -72,6 +72,16 @@ msgstr "Рўйхатга киритилган сана" msgid "Last updated" msgstr "Охирги янгиланган" +msgid "error encountered during field validation" +msgstr "майдонни текшириш вақтида хатолик юз берди" + +msgid "error" +msgstr "хато" + +msgid "" +"Plugins are not yet available - Please contact your system administrator" +msgstr "" + msgid "This value is validating" msgstr "Ушбу қиймат тасдиқланмоқда" @@ -489,6 +499,12 @@ msgstr "Расм юкланмоқда" msgid "Select image" msgstr "Расмни танланг" +msgid "Type to filter options" +msgstr "" + +msgid "No match found" +msgstr "" + msgid "Search" msgstr "Излаш" @@ -827,6 +843,9 @@ msgstr "" msgid "New Enrollment in program{{escape}} {{programName}}" msgstr "" +msgid "Save {{trackedEntityTypeName}}" +msgstr "" + msgid "Save {{trackedEntityName}}" msgstr "" @@ -984,10 +1003,6 @@ msgstr "" msgid "Choose a type to start searching" msgstr "Қидирув турини танланг ва қидиришни бошланг" -msgid "Fill in at least {{count}} attribute to search" -msgid_plural "Fill in at least {{count}} attribute to search" -msgstr[0] "" - msgid "Search {{name}}" msgstr "{{name}} орқали қидириш" @@ -1125,7 +1140,7 @@ msgid "Mark as cancelled" msgstr "" msgid "Mark incomplete" -msgstr "" +msgstr "Тугалланмаган деб белгилансин" msgid "Delete enrollment" msgstr "Рўйхатдан ўтказиш ўчирилсин " @@ -1233,22 +1248,12 @@ msgid "" "The scheduled date matches the suggested date, but can be changed if needed." msgstr "" -msgid "The scheduled date is {{count}} days {{position}} the suggested date." -msgid_plural "" -"The scheduled date is {{count}} days {{position}} the suggested date." -msgstr[0] "" - msgid "after" msgstr "кейин" msgid "before" msgstr "олдин" -msgid "There are {{count}} scheduled event in {{orgUnitName}} on this day." -msgid_plural "" -"There are {{count}} scheduled event in {{orgUnitName}} on this day." -msgstr[0] "" - msgid "" "Scheduling an event in {{stageName}} for {{programName}} in {{orgUnitName}}" msgstr "" @@ -1292,9 +1297,6 @@ msgstr "" msgid "Try again or contact your system administrator for support" msgstr "" -msgid "tracked entity instance" -msgstr "кузатилаётган объект намунаси" - msgid "Fix errors in the form to continue." msgstr "" @@ -1308,6 +1310,9 @@ msgstr "" msgid "Edit" msgstr "Таҳрирлаш" +msgid "tracked entity instance" +msgstr "кузатилаётган объект намунаси" + msgid "New {{ eventName }} event" msgstr "" @@ -1518,12 +1523,6 @@ msgstr "Шу кунгача" msgid "To time" msgstr "Шу вақтгача" -msgid "error encountered during field validation" -msgstr "майдонни текшириш вақтида хатолик юз берди" - -msgid "error" -msgstr "хато" - msgid "Delete polygon" msgstr "Полигон (кўпбурчак) ни ўчириб ташлаш" diff --git a/i18n/zh.po b/i18n/zh.po index db9d95f508..0253648916 100644 --- a/i18n/zh.po +++ b/i18n/zh.po @@ -79,7 +79,7 @@ msgstr "错误" msgid "" "Plugins are not yet available - Please contact your system administrator" -msgstr "" +msgstr "插件尚不可用 - 请联系您的系统管理员" msgid "This value is validating" msgstr "值正在被验证" @@ -274,7 +274,7 @@ msgid "" msgstr "离开本页将丢掉你的新关系的选择" msgid "Yes, discard changes" -msgstr "" +msgstr "是的,放弃更改" msgid "No, cancel" msgstr "不,取消" @@ -376,7 +376,7 @@ msgstr "排序" 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 "" +msgstr "此事件有未保存的更改。离开此页面而不保存将丢失这些更改。您确定要放弃未保存的更改吗?" msgid "No events to display" msgstr "无事件可显示" @@ -589,13 +589,13 @@ msgid "Write comment" msgstr "写备注" msgid "was blanked out and hidden by your last action" -msgstr "" +msgstr "被你的最后一个动作清空并隐藏" msgid "Notice" -msgstr "" +msgstr "注意" msgid "Close the notice" -msgstr "" +msgstr "关闭通知" msgid "Use new Enrollment dashboard for {{programName}}" msgstr "为 {{programName}} 使用新的注册仪表板" @@ -825,7 +825,7 @@ msgid "New Enrollment in program{{escape}} {{programName}}" msgstr "项目{{escape}} {{programName}}的新报名" msgid "Save {{trackedEntityTypeName}}" -msgstr "" +msgstr "保存 {{trackedEntityTypeName}}" msgid "Save {{trackedEntityName}}" msgstr "保存{{trackedEntityName}}" diff --git a/package.json b/package.json index ba1ffcd1d5..92d62348e1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.35.1", + "version": "100.39.0", "cacheVersion": "5", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,15 +10,15 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.35.1", - "@dhis2/app-runtime": "^3.8.0", + "@dhis2/rules-engine-javascript": "100.39.0", + "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", "@dhis2/d2-ui-app": "^2.0.0", "@dhis2/d2-ui-org-unit-tree": "^7.3.3", "@dhis2/d2-ui-rich-text": "^7.4.0", "@dhis2/d2-ui-sharing-dialog": "^7.3.3", - "@dhis2/ui": "^8.7.7", + "@dhis2/ui": "^8.13.8", "@joakim_sm/react-infinite-calendar": "^2.4.2", "@material-ui/core": "3.9.4", "@material-ui/icons": "3", @@ -131,6 +131,7 @@ }, "resolutions": { "@babel/preset-react": "7.16.7", + "@js-temporal/polyfill": "0.4.3", "core-js": "2.5.7" }, "browserslist": { diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index 13092f4d03..760850a4a9 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.35.1", + "version": "100.39.0", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { diff --git a/packages/rules-engine/src/processors/rulesEffectsProcessor/rulesEffectsProcessor.js b/packages/rules-engine/src/processors/rulesEffectsProcessor/rulesEffectsProcessor.js index da59fe6d94..15951dbc57 100644 --- a/packages/rules-engine/src/processors/rulesEffectsProcessor/rulesEffectsProcessor.js +++ b/packages/rules-engine/src/processors/rulesEffectsProcessor/rulesEffectsProcessor.js @@ -12,6 +12,7 @@ import type { IConvertOutputRulesEffectsValue, AssignOutputEffect, HideOutputEffect, + HideProgramStageEffect, MessageEffect, GeneralErrorEffect, GeneralWarningEffect, @@ -207,6 +208,17 @@ export function getRulesEffectsProcessor( }; } + function processHideProgramStage(effect: ProgramRuleEffect): ?HideProgramStageEffect { + if (!effect.programStageId) { + return null; + } + + return { + type: effectActions.HIDE_PROGRAM_STAGE, + id: effect.programStageId, + }; + } + function processMakeCompulsory(effect: ProgramRuleEffect): Array { return createEffectsForConfiguredDataTypes(effect, () => ({ type: effectActions.MAKE_COMPULSORY, @@ -267,6 +279,7 @@ export function getRulesEffectsProcessor( [effectActions.SHOW_WARNING]: processShowWarning, [effectActions.SHOW_ERROR_ONCOMPLETE]: processShowErrorOnComplete, [effectActions.SHOW_WARNING_ONCOMPLETE]: processShowWarningOnComplete, + [effectActions.HIDE_PROGRAM_STAGE]: processHideProgramStage, [effectActions.HIDE_SECTION]: processHideSection, [effectActions.MAKE_COMPULSORY]: processMakeCompulsory, [effectActions.DISPLAY_TEXT]: processDisplayText, diff --git a/packages/rules-engine/src/rulesEngine.types.js b/packages/rules-engine/src/rulesEngine.types.js index e2edca905b..decb2154a0 100644 --- a/packages/rules-engine/src/rulesEngine.types.js +++ b/packages/rules-engine/src/rulesEngine.types.js @@ -30,6 +30,10 @@ export type HideOutputEffect = OutputEffect & { }; +export type HideProgramStageEffect = OutputEffect & { + +}; + export type MessageEffect = OutputEffect & { message: string, }; diff --git a/src/core_modules/capture-core/components/ConditionalTooltip/ConditionalTooltip.component.js b/src/core_modules/capture-core/components/ConditionalTooltip/ConditionalTooltip.component.js new file mode 100644 index 0000000000..e2df643a07 --- /dev/null +++ b/src/core_modules/capture-core/components/ConditionalTooltip/ConditionalTooltip.component.js @@ -0,0 +1,31 @@ +// @flow +import React from 'react'; +import { Tooltip } from '@dhis2/ui'; + +type Props = { + enabled: boolean, + wrapperClassName?: string, + children: any, +}; + +export const ConditionalTooltip = (props: Props) => { + const { enabled, wrapperClassName, 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/ConditionalTooltip/index.js b/src/core_modules/capture-core/components/ConditionalTooltip/index.js new file mode 100644 index 0000000000..dacd281a12 --- /dev/null +++ b/src/core_modules/capture-core/components/ConditionalTooltip/index.js @@ -0,0 +1 @@ +export { ConditionalTooltip } from './ConditionalTooltip.component'; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/withMainButton.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/withMainButton.js index 097d88a8dd..8b18d2f201 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/withMainButton.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/withMainButton.js @@ -2,7 +2,8 @@ import * as React from 'react'; import { connect } from 'react-redux'; import i18n from '@dhis2/d2-i18n'; -import { Tooltip, Button } from '@dhis2/ui'; +import { Button } from '@dhis2/ui'; +import { ConditionalTooltip } from 'capture-core/components/ConditionalTooltip'; import { newEventSaveTypes } from './newEventSaveTypes'; import { getDataEntryKey } from '../../../../DataEntry/common/getDataEntryKey'; import { type RenderFoundation } from '../../../../../metaData'; @@ -86,29 +87,21 @@ const getMainButton = (InnerComponent: React.ComponentType) => const primary = buttons[0]; const secondaries = buttons.slice(1); return ( - - {({ onMouseOver, onMouseOut, ref }) => ( -
{ - if (divRef && !hasWriteAccess) { - divRef.onmouseover = onMouseOver; - divRef.onmouseout = onMouseOut; - ref.current = divRef; - } - }} + +
+ - - {primary.text} - -
- )} - + {primary.text} + +
+ ); } diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/useMetadataForRegistrationForm.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/useMetadataForRegistrationForm.js index d018aa4c1d..97488cf7fd 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/useMetadataForRegistrationForm.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/useMetadataForRegistrationForm.js @@ -33,7 +33,9 @@ export const useMetadataForRegistrationForm = ({ selectedScopeId }: Props) => { .map(({ trackedEntityAttributeId }) => trackedEntityAttributeId); } if (scopeType === scopeTypes.TRACKED_ENTITY_TYPE && trackedEntityType) { - return trackedEntityType.trackedEntityTypeAttributes.map(({ id }) => id); + return trackedEntityType + .trackedEntityTypeAttributes + .map(({ trackedEntityAttributeId }) => trackedEntityAttributeId); } return undefined; }, [program, scopeType, trackedEntityType]); diff --git a/src/core_modules/capture-core/components/DataTable/SortLabelWrapper.component.js b/src/core_modules/capture-core/components/DataTable/SortLabelWrapper.component.js deleted file mode 100644 index 73fded30e6..0000000000 --- a/src/core_modules/capture-core/components/DataTable/SortLabelWrapper.component.js +++ /dev/null @@ -1,141 +0,0 @@ -// @flow -import * as React from 'react'; -import { IconArrowDown16, IconArrowUp16, Tooltip } from '@dhis2/ui'; -import { withStyles } from '@material-ui/core/styles'; -import classNames from 'classnames'; - -import i18n from '@dhis2/d2-i18n'; -import { SortLabel, sortLabelDirections } from 'capture-ui'; - -const styles = () => ({ - iconBase: { - color: '#3a796f', - }, - enabledIcon: { - cursor: 'pointer', - }, -}); - -type Props = { - children?: ?React.Node, - classes: { - iconBase: string, - enabledIcon: string, - }, - disabled?: ?boolean, -}; - -class SortLabelWrapperPlain extends React.Component { - getIconClickHandler = ( - direction: $Values, - onSort: (direction: $Values) => void) => - () => { - if (!this.props.disabled) { - onSort(direction); - } - } - - getActiveIcons = (direction?: ?$Values, - onSort: (direction: $Values) => void, - ) => { - const isDisabled = this.props.disabled; - const classes = this.props.classes; - - const icon = - direction === sortLabelDirections.ASC ? - (
- -
) : - (
- -
); - - if (this.props.disabled) { - return ( - - {icon} - - ); - } - - return ( - - - {icon} - - - ); - } - - getIcons = ( - isActive: boolean, - direction?: ?$Values, - onSort: (direction: $Values, - ) => void) => { - if (isActive) { - return this.getActiveIcons(direction, onSort); - } - return ( -
- ); - } - - render() { - return ( - // $FlowFixMe[cannot-spread-inexact] automated comment - - { - (() => { - if (this.props.disabled) { - return ( - - {this.props.children} - - ); - } - return ( - - - {this.props.children} - - - ); - })() - } - - ); - } -} - -/** - * A wrapper for the d2-ui/dataTable/sortLabel component. Adds sort tooltip and icons - * @alias SortLabelWrapper - * @memberof DataTable - */ -export const SortLabelWrapper = withStyles(styles)(SortLabelWrapperPlain); diff --git a/src/core_modules/capture-core/components/List/OfflineList/OfflineList.component.js b/src/core_modules/capture-core/components/List/OfflineList/OfflineList.component.js index 69d7b9c760..8955ce20e3 100644 --- a/src/core_modules/capture-core/components/List/OfflineList/OfflineList.component.js +++ b/src/core_modules/capture-core/components/List/OfflineList/OfflineList.component.js @@ -3,62 +3,17 @@ import React, { Component } from 'react'; import { withStyles } from '@material-ui/core/styles'; import classNames from 'classnames'; import i18n from '@dhis2/d2-i18n'; - -import { - Table, - Row, - Cell, - HeaderCell, - Head, - Body, - sortLabelDirections, - sorLabelPlacements, -} from 'capture-ui'; -import { SortLabelWrapper } from '../../DataTable/SortLabelWrapper.component'; +import { DataTableHead, DataTable, DataTableBody, DataTableRow, DataTableCell, DataTableColumnHeader } from '@dhis2/ui'; import { dataElementTypes } from '../../../metaData'; -const styles = theme => ({ - loaderContainer: { - display: 'flex', - justifyContent: 'center', - }, - container: { - borderColor: theme.palette.type === 'light' - ? theme.palette.dividerLighter - : theme.palette.dividerDarker, - borderWidth: '1px', - borderStyle: 'solid', - }, - topBarContainer: { - display: 'flex', - justifyContent: 'space-between', - }, +const styles = () => ({ tableContainer: { overflow: 'auto', }, - optionsIcon: { - color: theme.palette.primary.main, - }, - table: {}, - row: {}, - cell: { - padding: `${theme.spacing.unit / 2}px ${theme.spacing.unit * 7}px ${theme.spacing.unit / - 2}px ${theme.spacing.unit * 3}px`, - '&:last-child': { - paddingRight: theme.spacing.unit * 3, + headerAlign: { + '&>span.container': { + alignItems: 'flex-end', }, - borderBottomColor: theme.palette.type === 'light' - ? theme.palette.dividerLighter - : theme.palette.dividerDarker, - }, - bodyCell: { - fontSize: theme.typography.pxToRem(13), - color: theme.palette.text.primary, - }, - headerCell: { - fontSize: theme.typography.pxToRem(12), - color: theme.palette.text.secondary, - fontWeight: theme.typography.fontWeightMedium, }, }); @@ -70,37 +25,17 @@ type Column = { }; type Props = { - dataSource: Array<{id: string, [elementId: string]: any}>, + dataSource: Array<{ id: string, [elementId: string]: any }>, columns: ?Array, classes: { - loaderContainer: string, - container: string, - topBarContainer: string, + headerAlign: string, tableContainer: string, - optionsIcon: string, - table: string, - cell: string, - headerCell: string, - bodyCell: string, - footerCell: string, - row: string, }, rowIdKey: string, - sortById: string, - sortByDirection: string, noItemsText: ?string, }; class Index extends Component { - static defaultProps = { - rowIdKey: 'id', - }; - static typesWithAscendingInitialDirection = [ - // todo (report lgmt) - dataElementTypes.TEXT, - dataElementTypes.LONG_TEXT, - ]; - static typesWithRightPlacement = [ dataElementTypes.NUMBER, dataElementTypes.INTEGER, @@ -110,122 +45,61 @@ class Index extends Component { ]; renderHeaderRow(visibleColumns: Array) { - const sortById = this.props.sortById; - const sortByDirection = this.props.sortByDirection; - - const headerCells = visibleColumns - .map(column => ( - - - {column.header} - - - )); + const { classes } = this.props; - return ( - ( + - {headerCells} - - ); + {column.header} + + )); + + return {headerCells}; } renderRows(visibleColumns: Array) { - const { dataSource, classes, noItemsText, rowIdKey } = this.props; + const { dataSource, noItemsText, rowIdKey } = this.props; if (!dataSource || dataSource.length === 0) { const columnsCount = visibleColumns.length; return ( - - - {noItemsText || i18n.t('No items to display')} - - + + {noItemsText || i18n.t('No items to display')} + ); } - return dataSource - .map((row) => { - const cells = visibleColumns - .map(column => ( - -
- {row[column.id]} -
-
- )); + return dataSource.map((row) => { + const cells = visibleColumns.map(column => ( + + {row[column.id]} + + )); - return ( - - {cells} - - ); - }); + return ( + + {cells} + + ); + }); } render() { const { columns, classes } = this.props; - - const visibleColumns = columns ? - columns - .filter(column => column.visible) : []; + const visibleColumns = columns ? columns.filter(column => column.visible) : []; return ( -
-
-
- - - {this.renderHeaderRow(visibleColumns)} - - - {this.renderRows(visibleColumns)} - -
-
+
+ + {this.renderHeaderRow(visibleColumns)} + {this.renderRows(visibleColumns)} +
); } diff --git a/src/core_modules/capture-core/components/List/OnlineList/OnlineList.component.js b/src/core_modules/capture-core/components/List/OnlineList/OnlineList.component.js index 8d8fc3ad59..21b085e8d0 100644 --- a/src/core_modules/capture-core/components/List/OnlineList/OnlineList.component.js +++ b/src/core_modules/capture-core/components/List/OnlineList/OnlineList.component.js @@ -2,68 +2,23 @@ import * as React from 'react'; import i18n from '@dhis2/d2-i18n'; -import { CircularLoader } from '@dhis2/ui'; +import { DataTableHead, DataTable, DataTableBody, DataTableRow, DataTableCell, DataTableColumnHeader } from '@dhis2/ui'; import classNames from 'classnames'; import { withStyles } from '@material-ui/core/styles'; -import { - Table, - Head, - Body, - Row, - Cell, - HeaderCell, - sortLabelDirections, - sorLabelPlacements, -} from 'capture-ui'; -import { SortLabelWrapper } from '../../DataTable/SortLabelWrapper.component'; import { dataElementTypes } from '../../../metaData'; import type { OptionSet } from '../../../metaData'; - -const getStyles = (theme: Theme) => ({ +const getStyles = () => ({ tableContainer: { overflowX: 'auto', }, - table: {}, - row: {}, loadingRow: { height: 100, }, - dataRow: { - cursor: 'pointer', - '&:hover': { - backgroundColor: '#F1FBFF', - }, - }, - - cell: { - padding: `${theme.spacing.unit / 2}px ${theme.spacing.unit * 7}px ${theme.spacing.unit / - 2}px ${theme.spacing.unit * 3}px`, - '&:last-child': { - paddingRight: theme.spacing.unit * 3, + headerAlign: { + '&>span.container': { + alignItems: 'flex-end', }, - borderBottomColor: theme.palette.type === 'light' - ? theme.palette.dividerLighter - : theme.palette.dividerDarker, - }, - bodyCell: { - fontSize: theme.typography.pxToRem(13), - color: theme.palette.text.primary, - }, - staticHeaderCell: { - width: 1, - }, - headerCell: { - fontSize: theme.typography.pxToRem(12), - color: theme.palette.text.secondary, - // $FlowFixMe - fontWeight: theme.typography.fontWeightMedium, - }, - loadingCell: { - textAlign: 'center', - }, - loader: { - display: 'inline-block', }, }); @@ -90,33 +45,12 @@ type Props = { customEndCellBodyStyle?: ?Object, classes: { tableContainer: string, - table: string, - cell: string, - headerCell: string, - bodyCell: string, - loadingCell: string, - sortLabelChilden: string, loadingRow: string, - row: string, - dataRow: string, - loader: string, - } -} - + headerAlign: string, + }, +}; class Index extends React.Component { - columnHeaderInstances: Array; - constructor(props: Props) { - super(props); - this.columnHeaderInstances = []; - } - static typesWithAscendingInitialDirection = [ - dataElementTypes.TEXT, - dataElementTypes.LONG_TEXT, - dataElementTypes.USERNAME, - 'ASSIGNEE', - ]; - static typesWithRightPlacement = [ dataElementTypes.NUMBER, dataElementTypes.INTEGER, @@ -124,181 +58,107 @@ class Index extends React.Component { dataElementTypes.INTEGER_NEGATIVE, dataElementTypes.INTEGER_ZERO_OR_POSITIVE, ]; - getSortHandler = (id: string) => (direction: string) => { - this.props.onSort(id, direction); - } - setColumnWidth(columnInstance: any, index: number) { - if (columnInstance && !this.props.updating) { - this.columnHeaderInstances[index] = columnInstance; - } - } + getSortHandler = + (id: string) => + ({ direction }: { direction: string }) => { + this.props.onSort(id, direction); + }; getCustomEndCellHeader = () => { - const { getCustomEndCellHeader, getCustomEndCellBody, customEndCellHeaderStyle, classes } = this.props; + const { getCustomEndCellHeader, getCustomEndCellBody, customEndCellHeaderStyle } = this.props; - return getCustomEndCellBody ? - ( - - {getCustomEndCellHeader && getCustomEndCellHeader(this.props)} - - ) : - null; - } + return getCustomEndCellBody ? ( + + {getCustomEndCellHeader && getCustomEndCellHeader(this.props)} + + ) : null; + }; getCustomEndCellBody = (row: Object, customEndCellBodyProps: Object) => { - const { getCustomEndCellBody, customEndCellBodyStyle, classes } = this.props; + const { getCustomEndCellBody, customEndCellBodyStyle } = this.props; - return getCustomEndCellBody ? - ( - - {getCustomEndCellBody(row, customEndCellBodyProps)} - - ) : - null; - } + return getCustomEndCellBody ? ( + + {getCustomEndCellBody(row, customEndCellBodyProps)} + + ) : null; + }; renderHeaderRow(visibleColumns: Array) { - const sortById = this.props.sortById; - const sortByDirection = this.props.sortByDirection; - - const headerCells = visibleColumns - .map((column, index) => ( - { this.setColumnWidth(instance, index); }} - key={column.id} - className={classNames(this.props.classes.cell, this.props.classes.headerCell)} - style={{ width: this.props.updating && this.columnHeaderInstances.length - 1 >= index ? this.columnHeaderInstances[index].clientWidth : 'auto' }} - > - - {column.header} - - - )); + const { classes, sortById, sortByDirection } = this.props; + + const headerCells = visibleColumns.map(column => ( + + {column.header} + + )); return ( - + {headerCells} {this.getCustomEndCellHeader()} - + ); } renderBody(visibleColumns: Array) { - const { classes, getCustomEndCellBody, updating } = this.props; + const { getCustomEndCellBody, updating, classes } = this.props; const columnsCount = visibleColumns.length + (getCustomEndCellBody ? 1 : 0); - return updating ? - ( - - - - - - ) : this.renderRows(visibleColumns, columnsCount); + return updating ? ( + + ) : ( + this.renderRows(visibleColumns, columnsCount) + ); } renderRows(visibleColumns: Array, columnsCount: number) { - const { dataSource, classes, rowIdKey, ...customEndCellBodyProps } = this.props; + const { dataSource, rowIdKey, ...customEndCellBodyProps } = this.props; if (!dataSource || dataSource.length === 0) { return ( - - - {i18n.t('No items to display')} - - + + {i18n.t('No items to display')} + ); } - return ( - - { - dataSource - .map((row) => { - const cells = visibleColumns - .map(column => ( - -
- {row[column.id]} -
-
- )); - return ( - this.props.onRowClick(row)} - > - {cells} - {this.getCustomEndCellBody(row, customEndCellBodyProps)} - - ); - }) - } -
- ); + return dataSource.map((row) => { + const cells = visibleColumns.map(column => ( + this.props.onRowClick(row)} + > + {row[column.id]} + + )); + return ( + + {cells} + {this.getCustomEndCellBody(row, customEndCellBodyProps)} + + ); + }); } render() { - const { classes, columns } = this.props; + const { classes, columns, updating } = this.props; const visibleColumns = columns ? columns.filter(column => column.visible) : []; return ( -
- - - {this.renderHeaderRow(visibleColumns)} - - - {this.renderBody(visibleColumns)} - -
+
+ + {this.renderHeaderRow(visibleColumns)} + {this.renderBody(visibleColumns)} +
); } diff --git a/src/core_modules/capture-core/components/ListView/Filters/FilterButton/FilterButtonMain.component.js b/src/core_modules/capture-core/components/ListView/Filters/FilterButton/FilterButtonMain.component.js index ec4f54e83a..6541747f96 100644 --- a/src/core_modules/capture-core/components/ListView/Filters/FilterButton/FilterButtonMain.component.js +++ b/src/core_modules/capture-core/components/ListView/Filters/FilterButton/FilterButtonMain.component.js @@ -2,7 +2,8 @@ import React, { Component } from 'react'; import { withStyles } from '@material-ui/core/styles'; import Popover from '@material-ui/core/Popover'; -import { IconChevronDown16, IconChevronUp16, Tooltip, Button } from '@dhis2/ui'; +import { IconChevronDown16, IconChevronUp16, Button } from '@dhis2/ui'; +import { ConditionalTooltip } from 'capture-core/components/ConditionalTooltip'; import { ActiveFilterButton } from './ActiveFilterButton.component'; import { FilterSelectorContents } from '../Contents'; import type { UpdateFilter, ClearFilter, RemoveFilter } from '../../types'; @@ -165,34 +166,22 @@ class FilterButtonMainPlain extends Component { renderWithoutAppliedFilter() { const { selectorVisible, classes, title, disabled, tooltipContent } = this.props; - return disabled ? ( - - {({ onMouseOver, onMouseOut, ref }) => ( -
{ - if (divRef && disabled) { - divRef.onmouseover = onMouseOver; - divRef.onmouseout = onMouseOut; - ref.current = divRef; - } - }} - > - -
- )} -
- ) : ( - + return ( + + + ); } diff --git a/src/core_modules/capture-core/components/ListView/withEndColumnMenu/RowMenu.component.js b/src/core_modules/capture-core/components/ListView/withEndColumnMenu/RowMenu.component.js index 22d8cf593c..28778a4ef7 100644 --- a/src/core_modules/capture-core/components/ListView/withEndColumnMenu/RowMenu.component.js +++ b/src/core_modules/capture-core/components/ListView/withEndColumnMenu/RowMenu.component.js @@ -2,12 +2,11 @@ import * as React from 'react'; import { Manager, Popper, Reference } from 'react-popper'; import ClickAwayListener from '@material-ui/core/ClickAwayListener'; -import { IconMore24 } from '@dhis2/ui'; +import { spacers, IconMore24, colors } from '@dhis2/ui'; import Grow from '@material-ui/core/Grow'; import Paper from '@material-ui/core/Paper'; import MenuList from '@material-ui/core/MenuList'; import MenuItem from '@material-ui/core/MenuItem'; -import IconButton from '@material-ui/core/IconButton'; import withStyles from '@material-ui/core/styles/withStyles'; import type { Props, State } from './rowMenu.types'; @@ -21,12 +20,19 @@ const styles = theme => ({ popperContainer: { zIndex: 100, }, - iconContainer: { - position: 'relative', - }, - icon: { - position: 'absolute', - marginTop: '-24px', + iconButton: { + display: 'flex', + borderRadius: '50%', + border: 'none', + cursor: 'pointer', + background: 'transparent', + padding: spacers.dp12, + marginTop: `-${spacers.dp12}`, + marginBottom: `-${spacers.dp12}`, + color: colors.grey600, + '&:hover': { + background: colors.grey400, + }, }, }); @@ -105,15 +111,14 @@ class Index extends React.Component { return (
- - +
); } diff --git a/src/core_modules/capture-core/components/ListView/withEndColumnMenu/rowMenu.types.js b/src/core_modules/capture-core/components/ListView/withEndColumnMenu/rowMenu.types.js index 4b11a4ff82..6c05417157 100644 --- a/src/core_modules/capture-core/components/ListView/withEndColumnMenu/rowMenu.types.js +++ b/src/core_modules/capture-core/components/ListView/withEndColumnMenu/rowMenu.types.js @@ -7,8 +7,7 @@ export type Props = { menuList: string, popperContainerHidden: string, popperContainer: string, - iconContainer: string, - icon: string, + iconButton: string, }, row: DataSourceItem, customRowMenuContents?: CustomRowMenuContents, diff --git a/src/core_modules/capture-core/components/Notes/Notes.component.js b/src/core_modules/capture-core/components/Notes/Notes.component.js index cf0bf7e8f5..bb1fc61c8d 100644 --- a/src/core_modules/capture-core/components/Notes/Notes.component.js +++ b/src/core_modules/capture-core/components/Notes/Notes.component.js @@ -3,9 +3,10 @@ import * as React from 'react'; import { Editor, Parser } from '@dhis2/d2-ui-rich-text'; import { withStyles } from '@material-ui/core'; -import { colors, spacersNum, Tooltip, Menu, MenuItem, Button } from '@dhis2/ui'; +import { colors, spacersNum, Menu, MenuItem, Button } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import { withFocusSaver } from 'capture-ui'; +import { ConditionalTooltip } from 'capture-core/components/ConditionalTooltip'; import { TextField } from '../FormFields/New'; import type { Note } from './notes.types'; @@ -181,26 +182,18 @@ class NotesPlain extends React.Component { className={classes.newNoteButtonContainer} data-test="new-comment-button" > - + +
); } diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js index a1f8c3250a..5f29c4daac 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js @@ -22,6 +22,7 @@ export const enrollmentPageActionTypes = { DELETE_ENROLLMENT: 'EnrollmentPage.DeleteEnrollment', UPDATE_TEI_DISPLAY_NAME: 'EnrollmentPage.UpdateTeiDisplayName', + UPDATE_ENROLLMENT_DATE: 'EnrollmentPage.UpdateEnrollmentDate', }; export const fetchEnrollmentPageInformation = () => @@ -73,3 +74,6 @@ export const updateTeiDisplayName = (teiDisplayName: string) => actionCreator(enrollmentPageActionTypes.UPDATE_TEI_DISPLAY_NAME)({ teiDisplayName, }); + +export const updateEnrollmentDate = ({ enrollmentId, enrollmentDate }: { enrollmentId: string, enrollmentDate: string }) => + actionCreator(enrollmentPageActionTypes.UPDATE_ENROLLMENT_DATE)({ enrollmentId, enrollmentDate }); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 624ad10170..f810251f17 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -66,7 +66,10 @@ export const EnrollmentPageDefaultPlain = ({ onEventClick, onLinkedRecordClick, onUpdateTeiAttributeValues, + onUpdateEnrollmentDate, + onUpdateIncidentDate, onEnrollmentError, + ruleEffects, }: PlainProps) => { const [mainContentVisible, setMainContentVisibility] = useState(true); const [addRelationShipContainerElement, setAddRelationshipContainerElement] = @@ -87,6 +90,7 @@ export const EnrollmentPageDefaultPlain = ({
@@ -138,6 +143,8 @@ export const EnrollmentPageDefaultPlain = ({ programId={program.id} onDelete={onDelete} onAddNew={onAddNew} + onUpdateEnrollmentDate={onUpdateEnrollmentDate} + onUpdateIncidentDate={onUpdateIncidentDate} onError={onEnrollmentError} />}
diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 637c9d2fc9..d7f3ccdf85 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -7,9 +7,17 @@ import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { useCommonEnrollmentDomainData, + useRuleEffects, updateEnrollmentAttributeValues, + updateEnrollmentDate, + updateIncidentDate, showEnrollmentError, } from '../../common/EnrollmentOverviewDomain'; +import { + updateEnrollmentDate as updateTopBarEnrollmentDate, + deleteEnrollment, + updateTeiDisplayName, +} from '../EnrollmentPage.actions'; import { useTrackerProgram } from '../../../../hooks/useTrackerProgram'; import { useRulesEngineOrgUnit } from '../../../../hooks/useRulesEngineOrgUnit'; import { EnrollmentPageDefaultComponent } from './EnrollmentPageDefault.component'; @@ -17,10 +25,8 @@ import { useProgramMetadata, useHideWidgetByRuleLocations, useProgramStages, - useRuleEffects, } from './hooks'; import { buildUrlQueryString, useLocationQuery } from '../../../../utils/routing'; -import { deleteEnrollment, updateTeiDisplayName } from '../EnrollmentPage.actions'; import { useFilteredWidgetData } from './hooks/useFilteredWidgetData'; import { useLinkedRecordClick } from '../../common/TEIRelationshipsWidget'; @@ -84,6 +90,15 @@ export const EnrollmentPageDefault = () => { dispatch(updateTeiDisplayName(teiDisplayName)); }, [dispatch]); + const onUpdateEnrollmentDate = useCallback((enrollmentDate) => { + dispatch(updateEnrollmentDate(enrollmentDate)); + dispatch(updateTopBarEnrollmentDate({ enrollmentId, enrollmentDate })); + }, [dispatch, enrollmentId]); + + const onUpdateIncidentDate = useCallback((incidentDate) => { + dispatch(updateIncidentDate(incidentDate)); + }, [dispatch]); + const onAddNew = () => { history.push(`/new?${buildUrlQueryString({ orgUnitId, programId, teiId })}`); }; @@ -112,7 +127,10 @@ export const EnrollmentPageDefault = () => { onEventClick={onEventClick} onLinkedRecordClick={onLinkedRecordClick} onUpdateTeiAttributeValues={onUpdateTeiAttributeValues} + onUpdateEnrollmentDate={onUpdateEnrollmentDate} + onUpdateIncidentDate={onUpdateIncidentDate} onEnrollmentError={onEnrollmentError} + ruleEffects={ruleEffects} /> ); }; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index b5c1efcc5a..143b9b8c28 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -1,4 +1,5 @@ // @flow +import { typeof effectActions } from '@dhis2/rules-engine-javascript'; import type { TrackerProgram } from 'capture-core/metaData'; import type { Stage } from 'capture-core/components/WidgetStagesAndEvents/types/common.types'; import type { WidgetEffects, HideWidgets } from '../../common/EnrollmentOverviewDomain'; @@ -21,7 +22,10 @@ export type Props = {| onEventClick: (eventId: string) => void, onUpdateTeiAttributeValues: (attributes: Array<{ [key: string]: string }>, teiDisplayName: string) => void, onLinkedRecordClick: LinkedRecordClick, + onUpdateEnrollmentDate: (enrollmentDate: string) => void, + onUpdateIncidentDate: (incidentDate: string) => void, onEnrollmentError: (message: string) => void, + ruleEffects?: Array<{id: string, type: $Values}>; |}; export type PlainProps = {| diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.js index 2853c9986e..e5621d7449 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.js @@ -19,7 +19,7 @@ const styles = { }; -const EnrollmentQuickActionsComponent = ({ stages, events, classes }) => { +const EnrollmentQuickActionsComponent = ({ stages, events, ruleEffects, classes }) => { const [open, setOpen] = useState(true); const history = useHistory(); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); @@ -33,10 +33,20 @@ const EnrollmentQuickActionsComponent = ({ stages, events, classes }) => { return mutatedStage; }), [events, stages]); + const hiddenProgramStageRuleEffects = useMemo( + () => ruleEffects?.filter(ruleEffect => ruleEffect.type === 'HIDEPROGRAMSTAGE'), + [ruleEffects], + ); + const noStageAvailable = useMemo( - () => stagesWithEventCount.every(programStage => - (!programStage.repeatable && programStage.eventCount > 0), - ), [stagesWithEventCount]); + () => + stagesWithEventCount.every( + programStage => + (!programStage.repeatable && programStage.eventCount > 0) || + hiddenProgramStageRuleEffects?.find(ruleEffect => ruleEffect.id === programStage.id), + ), + [stagesWithEventCount, hiddenProgramStageRuleEffects], + ); const onNavigationFromQuickActions = (tab: string) => { history.push(`/enrollmentEventNew?${buildUrlQueryString({ programId, teiId, enrollmentId, orgUnitId, tab })}`); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.js index 6cf8e002c5..7727a05019 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.js @@ -1,8 +1,9 @@ // @flow import React from 'react'; import i18n from '@dhis2/d2-i18n'; -import { Button, spacers, Tooltip } from '@dhis2/ui'; +import { Button, spacers } from '@dhis2/ui'; import { withStyles } from '@material-ui/core'; +import { ConditionalTooltip } from 'capture-core/components/ConditionalTooltip'; import type { QuickActionButtonTypes } from './QuickActionButton.types'; const styles = { @@ -14,30 +15,20 @@ const styles = { }; const QuickActionButtonPlain = ({ icon, label, onClickAction, dataTest, disable, classes }: QuickActionButtonTypes) => ( - - { ({ onMouseOver, onMouseOut, ref }) => ( -
{ - if (btnRef && disable) { - btnRef.onmouseover = onMouseOver; - btnRef.onmouseout = onMouseOut; - ref.current = btnRef; - } - }} - > - -
)} -
); + + ); export const QuickActionButton = withStyles(styles)(QuickActionButtonPlain); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js index 3b0cdd75eb..049aa3b303 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/index.js @@ -3,5 +3,4 @@ export { useTeiAttributes } from './useTeiAttributes'; export { useProgramMetadata } from './useProgramMetadata'; export { useHideWidgetByRuleLocations } from './useHideWidgetByRuleLocations'; export { useProgramStages } from './useProgramStages'; -export { useRuleEffects } from './useRuleEffects'; -export type { UseRuleEffectsInput } from './useRuleEffects.types'; + diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js index 085a79bd15..d319df06d1 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useProgramMetadata.js @@ -8,7 +8,7 @@ const query = { id: ({ id }) => id, params: { fields: - ['programStages[id,repeatable,hideDueDate,programStageDataElements[displayInReports,dataElement[id,valueType,displayName,optionSet[options[code,name]]]'], + ['programStages[id,repeatable,hideDueDate,programStageDataElements[displayInReports,dataElement[id,valueType,displayName,displayFormName,optionSet[options[code,name]]]'], }, }, }; 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 d146c6e3a9..3c8b50285a 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 @@ -32,6 +32,7 @@ export const useProgramStages = (program: Program, programStages?: Array (
{programStages.map((programStage) => { - const disableStage = !programStage.repeatable && programStage.eventCount > 0; + const disableStage = + (!programStage.repeatable && programStage.eventCount > 0) || programStage.hiddenProgramStage; return (
- + {programStage.displayName} + +
); })} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.js index cbe6b8dbd6..1bd16b7a80 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.js @@ -6,22 +6,33 @@ import log from 'loglevel'; import { ProgramStageSelectorComponent } from './ProgramStageSelector.component'; import { Widget } from '../../../Widget'; import { errorCreator } from '../../../../../capture-core-utils'; -import { useCommonEnrollmentDomainData } from '../../common/EnrollmentOverviewDomain'; +import { useCommonEnrollmentDomainData, useRuleEffects } from '../../common/EnrollmentOverviewDomain'; import type { Props } from './ProgramStageSelector.types'; import { useProgramFromIndexedDB } from '../../../../utils/cachedDataHooks/useProgramFromIndexedDB'; import { useLocationQuery, buildUrlQueryString } from '../../../../utils/routing'; +import { useRulesEngineOrgUnit } from '../../../../hooks/useRulesEngineOrgUnit'; +import { useTrackerProgram } from '../../../../hooks/useTrackerProgram'; export const ProgramStageSelector = ({ programId, orgUnitId, teiId, enrollmentId }: Props) => { const history = useHistory(); const { tab } = useLocationQuery(); - const { error: enrollmentsError, enrollment } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); + const { error: enrollmentsError, enrollment, attributeValues } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { program, isLoading: programLoading, isError: programError, } = useProgramFromIndexedDB(programId); + const { orgUnit } = useRulesEngineOrgUnit(orgUnitId); + const programRules = useTrackerProgram(programId); + + const ruleEffects = useRuleEffects({ + orgUnit, + program: programRules, + apiEnrollment: enrollment, + apiAttributeValues: attributeValues, + }); useEffect(() => { if (enrollmentsError || programError) { @@ -42,9 +53,12 @@ export const ProgramStageSelector = ({ programId, orgUnitId, teiId, enrollmentId displayName: currentStage.displayName, style: currentStage.style, repeatable: currentStage.repeatable, + hiddenProgramStage: ruleEffects?.find( + ruleEffect => ruleEffect.type === 'HIDEPROGRAMSTAGE' && ruleEffect.id === currentStage.id, + ), }); return accStage; - }, []), [enrollment?.events, program?.programStages, programLoading]); + }, []), [enrollment?.events, program?.programStages, programLoading, ruleEffects]); const onSelectProgramStage = (newStageId: string) => history.push(`enrollmentEventNew?${buildUrlQueryString({ diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index 6876f31002..cf244a8b1e 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -171,6 +171,7 @@ const EnrollmentEditEventPagePain = ({ teiId={teiId} enrollmentId={enrollmentId} programId={programId} + readOnlyMode onDelete={onDelete} onAddNew={onAddNew} onError={onEnrollmentError} diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.js index 0af4ee3050..4416248dcf 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.js @@ -3,8 +3,9 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { dataEntryIds, dataEntryKeys } from 'capture-core/constants'; import { withStyles } from '@material-ui/core/'; -import { spacers, IconFileDocument24, Tooltip, Button } from '@dhis2/ui'; +import { spacers, IconFileDocument24, Button } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; +import { ConditionalTooltip } from 'capture-core/components/ConditionalTooltip'; import { ViewEventSection } from '../Section/ViewEventSection.component'; import { ViewEventSectionHeader } from '../Section/ViewEventSectionHeader.component'; import { EditEventDataEntry } from '../../../WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container'; @@ -96,26 +97,20 @@ const EventDetailsSectionPlain = (props: Props) => {
- + +
}
); diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js index 6cdbe894ba..beb3832f89 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js @@ -4,6 +4,8 @@ import { actionCreator } from '../../../../actions/actions.utils'; export const enrollmentSiteActionTypes = { COMMON_ENROLLMENT_SITE_DATA_SET: 'EnrollmentSite.SetCommonData', + UPDATE_ENROLLMENT_DATE: 'Enrollment.UpdateEnrollmentDate', + UPDATE_INCIDENT_DATE: 'Enrollment.UpdateIncidentDate', UPDATE_ENROLLMENT_EVENTS: 'Enrollment.UpdateEnrollmentEvents', UPDATE_ENROLLMENT_EVENTS_WITHOUT_ID: 'Enrollment.UpdateEnrollmentEventsWithoutId', UPDATE_ENROLLMENT_ATTRIBUTE_VALUES: 'Enrollment.UpdateEnrollmentAttributeValues', @@ -18,6 +20,16 @@ export const enrollmentSiteActionTypes = { export const setCommonEnrollmentSiteData = (enrollment: ApiEnrollment, attributeValues: ApiAttributeValues) => actionCreator(enrollmentSiteActionTypes.COMMON_ENROLLMENT_SITE_DATA_SET)({ enrollment, attributeValues }); +export const updateEnrollmentDate = (enrollmentDate: string) => + actionCreator(enrollmentSiteActionTypes.UPDATE_ENROLLMENT_DATE)({ + enrollmentDate, + }); + +export const updateIncidentDate = (incidentDate: string) => + actionCreator(enrollmentSiteActionTypes.UPDATE_INCIDENT_DATE)({ + incidentDate, + }); + export const updateEnrollmentEvents = (eventId: string, eventData: Object) => actionCreator(enrollmentSiteActionTypes.UPDATE_ENROLLMENT_EVENTS)({ eventId, diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.js index e7a9b66f5b..067f6b41cd 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.js @@ -2,6 +2,8 @@ export type { HideWidgets, WidgetEffects } from './enrollmentOverviewDomain.types'; export { enrollmentSiteActionTypes, + updateEnrollmentDate, + updateIncidentDate, updateEnrollmentEvents, commitEnrollmentEvent, rollbackEnrollmentEvent, @@ -10,3 +12,4 @@ export { showEnrollmentError, } from './enrollment.actions'; export { useCommonEnrollmentDomainData } from './useCommonEnrollmentDomainData'; +export { useRuleEffects } from './useRuleEffects'; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/index.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/index.js new file mode 100644 index 0000000000..b8f1e7545a --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/index.js @@ -0,0 +1,3 @@ +// @flow +export { useRuleEffects } from './useRuleEffects'; +export type * from './useRuleEffects.types'; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRuleEffects.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js similarity index 94% rename from src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRuleEffects.js rename to src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js index bc38ec206c..74f6a96735 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRuleEffects.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js @@ -79,13 +79,16 @@ export const useRuleEffects = ({ orgUnit, program, apiEnrollment, apiAttributeVa useEffect(() => { if (orgUnit && attributeValues && enrollmentData && otherEvents) { - setRuleEffects(getApplicableRuleEffectsForTrackerProgram({ + const effects = getApplicableRuleEffectsForTrackerProgram({ program, orgUnit, otherEvents, attributeValues, enrollmentData, - }, true)); + }, true); + if (Array.isArray(effects)) { + setRuleEffects(effects); + } } }, [attributeValues, enrollmentData, orgUnit, otherEvents, program]); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRuleEffects.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.types.js similarity index 81% rename from src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRuleEffects.types.js rename to src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.types.js index e2a28b85f3..3ce69affc9 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/hooks/useRuleEffects.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.types.js @@ -4,7 +4,7 @@ import type { TrackerProgram } from 'capture-core/metaData'; import type { EnrollmentData, AttributeValue, -} from '../../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +} from '../useCommonEnrollmentDomainData'; export type UseRuleEffectsInput = {| orgUnit?: ?OrgUnit, 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 f1a26604b0..515eee121d 100644 --- a/src/core_modules/capture-core/components/PossibleDuplicatesDialog/possibleDuplicatesDialog.epics.js +++ b/src/core_modules/capture-core/components/PossibleDuplicatesDialog/possibleDuplicatesDialog.epics.js @@ -9,13 +9,14 @@ import { duplicatesForReviewRetrievalFailed, } from './possibleDuplicatesDialog.actions'; import { - scopeTypes, getScopeFromScopeId, EventProgram, TrackerProgram, TrackedEntityType, + scopeTypes, getScopeFromScopeId, EventProgram, TrackerProgram, TrackedEntityType, dataElementTypes, } from '../../metaData'; import { getDataEntryKey } from '../DataEntry/common/getDataEntryKey'; import { convertFormToClient, convertClientToServer } from '../../converters'; import { getTrackedEntityInstances } from '../../trackedEntityInstances/trackedEntityInstanceRequests'; import { getAttributesFromScopeId } from '../../metaData/helpers'; import { searchGroupDuplicateActionTypes } from '../../components/Pages/NewRelationship/RegisterTei'; +import { escapeString } from '../../utils/escapeString'; function getGroupElementsFromScopeId(scopeId: ?string) { if (!scopeId) { @@ -65,7 +66,8 @@ export const loadSearchGroupDuplicatesForReviewEpic = ( return null; } const serverValue = element.convertValue(value, pipeD2(convertFormToClient, convertClientToServer)); - return `${element.id}:${element.optionSet ? 'eq' : 'like'}:${serverValue}`; + const hasOptionSet = element.optionSet && element.type !== dataElementTypes.MULTI_TEXT; + return `${element.id}:${hasOptionSet ? 'eq' : 'like'}:${escapeString(serverValue)}`; }) .filter(f => f); diff --git a/src/core_modules/capture-core/components/Relationships/Relationships.component.js b/src/core_modules/capture-core/components/Relationships/Relationships.component.js index eb2e6589fd..1ee7f8af6c 100644 --- a/src/core_modules/capture-core/components/Relationships/Relationships.component.js +++ b/src/core_modules/capture-core/components/Relationships/Relationships.component.js @@ -4,7 +4,8 @@ import * as React from 'react'; import classNames from 'classnames'; import i18n from '@dhis2/d2-i18n'; import { IconButton, withStyles } from '@material-ui/core'; -import { IconArrowRight16, IconCross24, Tooltip, Button } from '@dhis2/ui'; +import { IconArrowRight16, IconCross24, Button } from '@dhis2/ui'; +import { ConditionalTooltip } from 'capture-core/components/ConditionalTooltip'; import type { RelationshipType } from '../../metaData'; import type { Relationship, Entity } from './relationships.types'; @@ -61,6 +62,10 @@ const getStyles = (theme: Theme) => ({ relationshipHighlight: { animation: 'background-fade 2.5s forwards', }, + tooltip: { + display: 'inline-flex', + borderRadius: '100%', + }, }); const fromNames = { @@ -78,6 +83,7 @@ type Props = { arrowIcon: string, relationshipActions: string, relationshipHighlight: string, + tooltip: string, addButtonContainer: string, }, relationships: Array, @@ -133,25 +139,18 @@ class RelationshipsPlain extends React.Component {
- - {({ onMouseOver, onMouseOut, ref }) => ( -
{ - if (divRef && !canDelete) { - divRef.onmouseover = onMouseOver; - divRef.onmouseout = onMouseOut; - ref.current = divRef; - } - }} - > - { onRemoveRelationship(relationship.clientId); }} - disabled={!canDelete} - > - - -
- )} -
+ + { onRemoveRelationship(relationship.clientId); }} + disabled={!canDelete} + > + + +
); @@ -194,27 +193,19 @@ class RelationshipsPlain extends React.Component {
- + +
diff --git a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.epics.js b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.epics.js index f9aa25f38e..9f50c5e821 100644 --- a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.epics.js +++ b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.epics.js @@ -17,7 +17,6 @@ import { getTrackedEntityInstances, } from '../../../trackedEntityInstances/trackedEntityInstanceRequests'; import { - type DataElement, dataElementTypes, getTrackedEntityTypeThrowIfNotFound, getTrackerProgramThrowIfNotFound, @@ -29,11 +28,12 @@ import { } from '../../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions'; import { dataElementConvertFunctions } from './SearchFormElementConverter/SearchFormElementConverter'; import type { QuerySingleResource } from '../../../utils/api/api.types'; +import { escapeString } from '../../../utils/escapeString'; const getFiltersForUniqueIdSearchQuery = (formValues) => { const fieldId = Object.keys(formValues)[0]; - return [`${fieldId}:eq:${formValues[fieldId]}`]; + return [`${fieldId}:eq:${escapeString(formValues[fieldId])}`]; }; const searchViaUniqueIdStream = ({ @@ -67,11 +67,6 @@ const searchViaUniqueIdStream = ({ catchError(() => of(showErrorViewOnSearchBox())), ); -export const deriveFilterKeyword = (fieldId: string, attributes: Array): ("eq" | "like") => { - const hasOptionSet = Boolean(attributes.find(({ id, optionSet }) => (id === fieldId) && (optionSet))); - return hasOptionSet ? 'eq' : 'like'; -}; - const getFiltersForAttributesSearchQuery = (formValues, attributes) => Object.keys(formValues) .filter(fieldId => formValues[fieldId]) .filter((fieldId) => { diff --git a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchFormElementConverter/SearchFormElementConverter.js b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchFormElementConverter/SearchFormElementConverter.js index a07af2134b..5153fc5454 100644 --- a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchFormElementConverter/SearchFormElementConverter.js +++ b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchFormElementConverter/SearchFormElementConverter.js @@ -1,19 +1,20 @@ // @flow import { pipe as pipeD2 } from '../../../../../capture-core-utils'; import { convertClientToServer, convertFormToClient } from '../../../../converters'; -import { type DataElement } from '../../../../metaData'; +import { type DataElement, dataElementTypes } from '../../../../metaData'; +import { escapeString } from '../../../../utils/escapeString'; type FormValues = { [key: string]: any} const derivedFilterKeyword = (dataElement) => { - const hasOptionSet = !!dataElement.optionSet; + const hasOptionSet = dataElement.optionSet && dataElement.type !== dataElementTypes.MULTI_TEXT; return hasOptionSet ? 'eq' : 'like'; }; const convertString = (formValues: string, dataElement: DataElement) => { const sanitizedString = formValues.trim(); const convertedString = (dataElement.convertValue(sanitizedString, pipeD2(convertFormToClient, convertClientToServer))); - return `${dataElement.id}:${derivedFilterKeyword(dataElement)}:${convertedString}`; + return `${dataElement.id}:${derivedFilterKeyword(dataElement)}:${escapeString(convertedString)}`; }; const convertRange = (formValues: FormValues, dataElement: DataElement) => { @@ -38,7 +39,7 @@ const convertAge = (formValues: FormValues, dataElement: DataElement) => { const convertFile = (formValues: FormValues, dataElement: DataElement) => { const convertedFileName = (dataElement.convertValue(formValues, pipeD2(convertFormToClient, convertClientToServer))); - return `${dataElement.id}:${derivedFilterKeyword(dataElement)}:${convertedFileName}`; + return `${dataElement.id}:${derivedFilterKeyword(dataElement)}:${escapeString(convertedFileName)}`; }; const convertBoolean = (formValues: boolean, dataElement: DataElement) => { diff --git a/src/core_modules/capture-core/components/TeiSearch/serverToFilters.js b/src/core_modules/capture-core/components/TeiSearch/serverToFilters.js index e9da576888..afbf8a2b49 100644 --- a/src/core_modules/capture-core/components/TeiSearch/serverToFilters.js +++ b/src/core_modules/capture-core/components/TeiSearch/serverToFilters.js @@ -1,21 +1,28 @@ // @flow import { type DataElement, dataElementTypes } from '../../metaData'; +import { escapeString } from '../../utils/escapeString'; type RangeValue = { from: number, to: number, } -const equals = (value: any, elementId: string) => `${elementId}:eq:${value}`; -const like = (value: any, elementId: string) => `${elementId}:like:${value}`; +const equals = (value: any, elementId: string) => `${elementId}:eq:${escapeString(value)}`; +const like = (value: any, elementId: string) => `${elementId}:like:${escapeString(value)}`; -function convertRange(value: RangeValue, elementId: string) { - return `${elementId}:ge:${value.from}:le:${value.to}`; -} +const convertRange = (value: RangeValue, { id: elementId }: DataElement) => ( + `${elementId}:ge:${value.from}:le:${value.to}` +); + +const convertString = (value: any, metaElement: DataElement) => { + const hasOptionSet = metaElement.optionSet && metaElement.type !== dataElementTypes.MULTI_TEXT; + return hasOptionSet ? equals(value, metaElement.id) : like(value, metaElement.id); +}; const valueConvertersForType = { - [dataElementTypes.TEXT]: like, + [dataElementTypes.TEXT]: convertString, + [dataElementTypes.MULTI_TEXT]: convertString, [dataElementTypes.NUMBER_RANGE]: convertRange, [dataElementTypes.DATE_RANGE]: convertRange, [dataElementTypes.DATETIME_RANGE]: convertRange, @@ -27,7 +34,7 @@ export function convertValue(value: any, type: $Keys, m return value; } // $FlowFixMe dataElementTypes flow error - return valueConvertersForType[type] ? valueConvertersForType[type](value, metaElement.id) : equals(value, metaElement.id); + return valueConvertersForType[type] ? valueConvertersForType[type](value, metaElement) : equals(value, metaElement.id); } export function convertValueToEqual(value: any, type: $Keys, metaElement: DataElement) { 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 e83fdfda17..2ce98fcb38 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 @@ -3,6 +3,7 @@ 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', @@ -18,13 +19,6 @@ const enrollmentDelete = { 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 Actions = ({ enrollment = {}, diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddNew/AddNew.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddNew/AddNew.component.js index e576a3ec0e..e4ec61be1b 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddNew/AddNew.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddNew/AddNew.component.js @@ -1,7 +1,8 @@ // @flow -import { IconAdd16, MenuItem, Tooltip } from '@dhis2/ui'; +import { IconAdd16, MenuItem } from '@dhis2/ui'; import React from 'react'; import i18n from '@dhis2/d2-i18n'; +import { ConditionalTooltip } from 'capture-core/components/ConditionalTooltip'; import type { Props } from './addNew.types'; export const AddNew = ({ canAddNew, onlyEnrollOnce, tetName, onAddNew }: Props) => { @@ -9,27 +10,18 @@ export const AddNew = ({ canAddNew, onlyEnrollOnce, tetName, onAddNew }: Props) return null; } - return ( - {({ onMouseOver, onMouseOut, ref }) => ( -
{ - if (divRef && onlyEnrollOnce) { - divRef.onmouseover = onMouseOver; - divRef.onmouseout = onMouseOut; - ref.current = divRef; - } - }} - > - } - label={i18n.t('Add new')} - disabled={onlyEnrollOnce} - /> -
- )} -
); + return ( + + } + label={i18n.t('Add new')} + disabled={onlyEnrollOnce} + /> + ); }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js new file mode 100644 index 0000000000..f3c7f36617 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js @@ -0,0 +1,151 @@ +// @flow +import React, { useState, useCallback } from 'react'; +import moment from 'moment'; +import { + Button, + CalendarInput, + IconCalendar16, + IconEdit16, + colors, + spacersNum, +} from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; +import { withStyles } from '@material-ui/core'; +import { convertValue as convertValueClientToView } from '../../../converters/clientToView'; +import { dataElementTypes } from '../../../metaData'; + +type Props = { + date: string, + dateLabel: string, + editEnabled: boolean, + displayAutoGeneratedEventWarning: boolean, + onSave: (string) => void, + ...CssClasses, +} + +const styles = { + editButton: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + cursor: 'pointer', + border: 'none', + borderRadius: '3px', + background: 'transparent', + color: colors.grey600, + padding: 0, + marginLeft: '2px', + '&:focus': { + outline: 'none', + background: colors.grey200, + color: colors.grey800, + }, + '&:hover': { + background: colors.grey200, + color: colors.grey800, + }, + }, + calendar: { + paddingTop: '6px', + }, + inputField: { + maxWidth: '200px', + }, + buttonStrip: { + display: 'flex', + gap: `${spacersNum.dp4}px`, + margin: `${spacersNum.dp4}px 0`, + }, + note: { + fontSize: '12px', + color: colors.grey700, + }, +}; + +const DateComponentPlain = ({ + date, + dateLabel, + editEnabled, + displayAutoGeneratedEventWarning, + onSave, + classes, +}: Props) => { + const [editMode, setEditMode] = useState(false); + const [selectedDate, setSelectedDate] = useState(); + const dateChangeHandler = useCallback(({ calendarDateString }) => { + setSelectedDate(calendarDateString); + }, [setSelectedDate]); + const displayDate = String(convertValueClientToView(date, dataElementTypes.DATE)); + + const onOpenEdit = () => { + // CalendarInput component only supports the YYYY-MM-DD format + setSelectedDate(moment(date).format('YYYY-MM-DD')); + setEditMode(true); + }; + const saveHandler = () => { + // CalendarInput component only supports the YYYY-MM-DD format + if (selectedDate) { + const newDate = moment.utc(selectedDate, 'YYYY-MM-DD').format('YYYY-MM-DDTHH:mm:ss.SSS'); + if (newDate !== date) { + onSave(newDate); + } + } + setEditMode(false); + }; + + return editMode ? ( +
+
+ +
+
+ + +
+ {displayAutoGeneratedEventWarning && ( +
+ {i18n.t('Existing dates for auto-generated events will not be updated.')} +
+ )} +
+ ) : ( +
+ + + + {dateLabel}{': '} + {displayDate} + {editEnabled && + + } +
+ ); +}; + +export const Date = withStyles(styles)(DateComponentPlain); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Date/index.js b/src/core_modules/capture-core/components/WidgetEnrollment/Date/index.js new file mode 100644 index 0000000000..5fc124daa8 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Date/index.js @@ -0,0 +1,2 @@ +// @flow +export { Date } from './Date.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 977793676c..15ebdb7c41 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, - IconCalendar16, IconLocation16, colors, Tag, @@ -20,6 +19,7 @@ import { Status } from './Status'; import { convertValue as convertValueServerToClient } from '../../converters/serverToClient'; import { convertValue as convertValueClientToView } from '../../converters/clientToView'; import { dataElementTypes } from '../../metaData'; +import { Date } from './Date'; import { Actions } from './Actions'; const styles = { @@ -52,11 +52,15 @@ export const WidgetEnrollmentPlain = ({ ownerOrgUnit = {}, refetchEnrollment, refetchTEI, - error, + initError, loading, canAddNew, + editDateEnabled, + displayAutoGeneratedEventWarning, onDelete, onAddNew, + updateEnrollmentDate, + updateIncidentDate, onError, onSuccess, }: PlainProps) => { @@ -72,13 +76,13 @@ export const WidgetEnrollmentPlain = ({ onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} open={open} > - {error && ( + {initError && (
{i18n.t('Enrollment widget could not be loaded. Please try again later')}
)} {loading && } - {!error && !loading && ( + {!initError && !loading && (
{enrollment.followUp && ( @@ -89,28 +93,28 @@ export const WidgetEnrollmentPlain = ({
-
- - - - {getEnrollmentDateLabel(program)}{' '} - {convertValueClientToView( - convertValueServerToClient(enrollment.enrolledAt, dataElementTypes.DATE), - dataElementTypes.DATE, - )} -
+ + + {program.displayIncidentDate && ( -
- - - - {getIncidentDateLabel(program)}{' '} - {convertValueClientToView( - convertValueServerToClient(enrollment.occurredAt, dataElementTypes.DATE), - dataElementTypes.DATE, - )} -
+ + + )}
diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.js b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.js index 8660f704f4..38296cfa32 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.js @@ -10,8 +10,30 @@ import { useProgram } from './hooks/useProgram'; import type { Props } from './enrollment.types'; import { plainStatus } from './constants/status.const'; -export const WidgetEnrollment = ({ teiId, enrollmentId, programId, onDelete, onAddNew, onError, onSuccess }: Props) => { - const { error: errorEnrollment, enrollment, refetch: refetchEnrollment } = useEnrollment(enrollmentId); +export const WidgetEnrollment = ({ + teiId, + enrollmentId, + programId, + readOnlyMode = false, + onDelete, + onAddNew, + onUpdateEnrollmentDate, + onUpdateIncidentDate, + onError, + onSuccess, +}: Props) => { + const { + enrollment, + updateEnrollmentDate, + updateIncidentDate, + error: errorEnrollment, + refetch: refetchEnrollment, + } = useEnrollment({ + enrollmentId, + onUpdateEnrollmentDate, + onUpdateIncidentDate, + onError, + }); const { error: errorProgram, program } = useProgram(programId); const { error: errorOwnerOrgUnit, @@ -23,6 +45,7 @@ export const WidgetEnrollment = ({ teiId, enrollmentId, programId, onDelete, onA const canAddNew = enrollments .filter(item => item.program === programId) .every(item => item.status !== plainStatus.ACTIVE); + const containsAutoGeneratedEvent = program && program.programStages.some(({ autoGenerateEvent }) => autoGenerateEvent); const error = errorEnrollment || errorProgram || errorOwnerOrgUnit || errorOrgUnit; if (error) { @@ -33,6 +56,8 @@ export const WidgetEnrollment = ({ teiId, enrollmentId, programId, onDelete, onA diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.js b/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.js index a3eacd0fdb..d4e14794b8 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.js @@ -5,8 +5,11 @@ export type Props = {| teiId: string, enrollmentId: string, programId: string, + readOnlyMode?: boolean, onDelete: () => void, onAddNew: () => void, + onUpdateEnrollmentDate?: (enrollmentDate: string) => void, + onUpdateIncidentDate?: (enrollmentDate: string) => void, onError?: (message: string) => void, onSuccess?: () => void, |}; @@ -17,9 +20,13 @@ export type PlainProps = {| ownerOrgUnit: Object, refetchEnrollment: QueryRefetchFunction, refetchTEI: QueryRefetchFunction, - error?: FetchError, + initError?: FetchError, loading: boolean, canAddNew: boolean, + editDateEnabled: boolean, + displayAutoGeneratedEventWarning: boolean, + updateEnrollmentDate: (enrollmentDate: string) => void, + updateIncidentDate: (incidentDate: string) => void, onDelete: () => void, onAddNew: () => void, onError?: (message: string) => void, diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useEnrollment.js b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useEnrollment.js index 87ae5120c4..e9b2d305fd 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useEnrollment.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useEnrollment.js @@ -1,8 +1,23 @@ // @flow -import { useMemo, useEffect } from 'react'; +import { useMemo, useEffect, useState } from 'react'; import { useDataQuery } from '@dhis2/app-runtime'; +import { useUpdateEnrollment } from './useUpdateEnrollment'; + +type Props = { + enrollmentId: string, + onUpdateEnrollmentDate?: (date: string) => void, + onUpdateIncidentDate?: (date: string) => void, + onError?: (error: any) => void, +} + +export const useEnrollment = ({ + enrollmentId, + onUpdateEnrollmentDate, + onUpdateIncidentDate, + onError, +}: Props) => { + const [enrollment, setEnrollment] = useState(); -export const useEnrollment = (enrollmentId: string) => { const { error, loading, data, refetch } = useDataQuery( useMemo( () => ({ @@ -20,5 +35,33 @@ export const useEnrollment = (enrollmentId: string) => { enrollmentId && refetch({ variables: { enrollmentId } }); }, [refetch, enrollmentId]); - return { error, refetch, enrollment: !loading && data?.enrollment }; + useEffect(() => { + if (data) { + setEnrollment(data.enrollment); + } + }, [setEnrollment, data]); + + const updateEnrollmentDate = useUpdateEnrollment({ + enrollment, + setEnrollment, + propertyName: 'enrolledAt', + updateHandler: onUpdateEnrollmentDate, + onError, + }); + + const updateIncidentDate = useUpdateEnrollment({ + enrollment, + setEnrollment, + propertyName: 'occurredAt', + updateHandler: onUpdateIncidentDate, + onError, + }); + + return { + error, + refetch, + enrollment: !loading && enrollment, + updateEnrollmentDate, + updateIncidentDate, + }; }; 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 937f5644c6..fec7939f6d 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,incidentDateLabel,enrollmentDateLabel,onlyEnrollOnce,trackedEntityType[displayName]', + 'displayIncidentDate,incidentDateLabel,enrollmentDateLabel,onlyEnrollOnce,trackedEntityType[displayName],programStages[autoGenerateEvent],access', ], }, }, diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useUpdateEnrollment.js b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useUpdateEnrollment.js new file mode 100644 index 0000000000..fd7c96f827 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useUpdateEnrollment.js @@ -0,0 +1,42 @@ +// @flow +import { useCallback } from 'react'; +import { useDataMutation } from '@dhis2/app-runtime'; +import { processErrorReports } from '../processErrorReports'; + +const enrollmentUpdate = { + resource: 'tracker?async=false&importStrategy=UPDATE', + type: 'create', + data: enrollment => ({ + enrollments: [enrollment], + }), +}; + +export const useUpdateEnrollment = ({ + enrollment, + setEnrollment, + propertyName, + updateHandler, + onError, +}: { + enrollment: any, + setEnrollment: (enrollment: any) => void, + propertyName: string, + updateHandler?: (value: any) => void, + onError?: (error: any) => void, +}) => { + const [updateEnrollmentMutation] = useDataMutation(enrollmentUpdate, { + onError: (e) => { + setEnrollment(enrollment); + updateHandler && updateHandler(enrollment[propertyName]); + onError && onError(processErrorReports(e)); + }, + }); + + return useCallback((value: string) => { + const updatedEnrollment = { ...enrollment }; + updatedEnrollment[propertyName] = value; + setEnrollment(updatedEnrollment); + updateEnrollmentMutation(updatedEnrollment); + updateHandler && updateHandler(value); + }, [enrollment, setEnrollment, propertyName, updateHandler, updateEnrollmentMutation]); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/processErrorReports.js b/src/core_modules/capture-core/components/WidgetEnrollment/processErrorReports.js new file mode 100644 index 0000000000..d60fa40b31 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/processErrorReports.js @@ -0,0 +1,8 @@ +// @flow +export const processErrorReports = (error: any) => { + // $FlowFixMe[prop-missing] + const errorReports = error?.details?.validationReport?.errorReports; + return errorReports?.length > 0 + ? errorReports.reduce((acc, errorReport) => `${acc} ${errorReport.message}`, '') + : error.message; +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/ErrorText.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/ErrorText.component.js new file mode 100644 index 0000000000..a885a48a6d --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/ErrorText.component.js @@ -0,0 +1,20 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { NoticeBox } from '@dhis2/ui'; +import type { Props } from './ErrorText.types'; + +export const ErrorText = ({ stageName }: Props) => ( + <> +
+ + + {i18n.t("You can't add any more {{ programStageName }} events", { + programStageName: stageName, + interpolation: { escapeValue: false }, + })} + + +
+ +); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/ErrorText.types.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/ErrorText.types.js new file mode 100644 index 0000000000..07785c7e1a --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/ErrorText.types.js @@ -0,0 +1,5 @@ +// @flow + +export type Props = {| + stageName: string, +|}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/index.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/index.js new file mode 100644 index 0000000000..2b26086840 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/index.js @@ -0,0 +1,2 @@ +// @flow +export { ErrorText } from './ErrorText.component'; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/FinishButtons.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/FinishButtons.component.js index ae5f972510..1c0a36c1c8 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/FinishButtons.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/FinishButtons.component.js @@ -3,6 +3,7 @@ import React, { type ComponentType } from 'react'; import i18n from '@dhis2/d2-i18n'; import { withStyles } from '@material-ui/core'; import { Button, spacersNum } from '@dhis2/ui'; +import { ConditionalTooltip } from 'capture-core/components/ConditionalTooltip'; import { withCancelButton } from '../../DataEntry/withCancelButton'; import { addEventSaveTypes } from '../DataEntry/addEventSaveTypes'; import type { InputProps, Props } from './finishButtons.types'; @@ -17,26 +18,43 @@ const styles = { }, }; -const FinishButtonsPlain = ({ onSave, cancelButton, classes }: Props) => ( +const FinishButtonsPlain = ({ onSave, cancelButton, hiddenProgramStage, stageName, classes }: Props) => (
- + +
- + +
{cancelButton}
); -export const FinishButtons: ComponentType = - withCancelButton()(withStyles(styles)(FinishButtonsPlain)); +export const FinishButtons: ComponentType = withCancelButton()(withStyles(styles)(FinishButtonsPlain)); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/finishButtons.types.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/finishButtons.types.js index dd3acb8e32..079708e30a 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/finishButtons.types.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/finishButtons.types.js @@ -6,10 +6,14 @@ export type InputProps = {| onSave: (saveType: $Keys) => void, onCancel: () => void, id: string, + hiddenProgramStage: boolean, + stageName: string, |}; export type Props = {| onSave: (saveType: $Keys) => void, cancelButton: Element, + hiddenProgramStage: boolean, + stageName: string, ...CssClasses, |}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js index ed59d6dc4e..4f784d2b9a 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js @@ -6,6 +6,7 @@ import { Widget } from '../../Widget'; import { DataEntry } from '../DataEntry'; import { FinishButtons } from '../FinishButtons'; import { SavingText } from '../SavingText'; +import { ErrorText } from '../ErrorText'; import type { Props } from './validated.types'; const styles = () => ({ @@ -23,6 +24,7 @@ const ValidatedPlain = ({ onSave, onCancel, orgUnit, + hiddenProgramStage, id, ...passOnProps }: Props) => ( @@ -46,12 +48,14 @@ const ValidatedPlain = ({ onSave={onSave} onCancel={onCancel} id={id} - /> - + {hiddenProgramStage ? ( + + ) : ( + + )}
)} diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js index 3d85e67cbf..c0245b477f 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js @@ -1,6 +1,6 @@ // @flow import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { withAskToCreateNew, withSaveHandler } from '../../DataEntry'; import { useLifecycle } from './useLifecycle'; import { useClientFormattedRulesExecutionDependencies } from './useClientFormattedRulesExecutionDependencies'; @@ -29,6 +29,11 @@ export const Validated = ({ }: ContainerProps) => { const dataEntryId = 'enrollmentEvent'; const itemId = 'newEvent'; + const rulesEffectsHiddenProgram = useSelector( + ({ rulesEffectsHiddenProgramStageDesc }) => + rulesEffectsHiddenProgramStageDesc && rulesEffectsHiddenProgramStageDesc[`${dataEntryId}-${itemId}`], + ); + const hiddenProgramStage = rulesEffectsHiddenProgram && rulesEffectsHiddenProgram[stage.id]; const rulesExecutionDependenciesClientFormatted = useClientFormattedRulesExecutionDependencies(rulesExecutionDependencies, program); @@ -127,6 +132,7 @@ export const Validated = ({ programName={program.name} orgUnit={orgUnit} rulesExecutionDependenciesClientFormatted={rulesExecutionDependenciesClientFormatted} + hiddenProgramStage={hiddenProgramStage} /> ); }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js index a5194d88af..932e61a6d8 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js @@ -25,5 +25,6 @@ export type Props = {| formRef: (formInstance: any) => void, dataEntryFieldRef: (instance: any, id: string) => void, rulesExecutionDependenciesClientFormatted: RulesExecutionDependenciesClientFormatted, + hiddenProgramStage: boolean, ...CssClasses, |}; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withMainButton.js b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withMainButton.js index 34e93f3184..42491ef4ab 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withMainButton.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withMainButton.js @@ -2,7 +2,8 @@ import * as React from 'react'; import { connect } from 'react-redux'; import i18n from '@dhis2/d2-i18n'; -import { Tooltip, Button } from '@dhis2/ui'; +import { Button } from '@dhis2/ui'; +import { ConditionalTooltip } from 'capture-core/components/ConditionalTooltip'; import { type RenderFoundation } from '../../../metaData'; type Props = { @@ -19,23 +20,18 @@ const getMainButton = (InnerComponent: React.ComponentType) => return this.innerInstance; } renderMainButton = (hasWriteAccess: boolean) => ( - - {({ onMouseOver, onMouseOut, ref }) => ( - )} - + + + ) render() { diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js index 6cba9acf96..4c4196fcfe 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js @@ -2,9 +2,10 @@ import React, { type ComponentType } from 'react'; import { dataEntryIds, dataEntryKeys } from 'capture-core/constants'; import { useDispatch } from 'react-redux'; -import { spacersNum, Button, colors, IconEdit24, IconArrowLeft24, Tooltip } from '@dhis2/ui'; +import { spacersNum, Button, colors, IconEdit24, IconArrowLeft24 } from '@dhis2/ui'; import { withStyles } from '@material-ui/core'; import i18n from '@dhis2/d2-i18n'; +import { ConditionalTooltip } from 'capture-core/components/ConditionalTooltip'; import { useEnrollmentEditEventPageMode, useRulesEngineOrgUnit, useAvailableProgramStages } from 'capture-core/hooks'; import type { Props } from './widgetEventEdit.types'; import { startShowEditEventDataEntry } from './WidgetEventEdit.actions'; @@ -39,6 +40,7 @@ const styles = { borderBottomWidth: 0, }, button: { margin: spacersNum.dp8 }, + tooltip: { display: 'inline-flex' }, }; export const WidgetEventEditPlain = ({ @@ -75,32 +77,23 @@ export const WidgetEventEditPlain = ({ {currentPageMode === dataEntryKeys.VIEW && ( - - {({ onMouseOver, onMouseOut, ref }) => ( -
{ - if (btnRef && !eventAccess?.write) { - btnRef.onmouseover = onMouseOver; - btnRef.onmouseout = onMouseOut; - ref.current = btnRef; - } - }} +
+ + -
- )} - + + {i18n.t('Edit event')} + + +
)}
+ value + .replace(new RegExp(valueToEscape.SLASH, 'g'), `${escape}${valueToEscape.SLASH}`) + .replace(new RegExp(valueToEscape.COLON, 'g'), `${escape}${valueToEscape.COLON}`) + .replace(new RegExp(valueToEscape.COMMA, 'g'), `${escape}${valueToEscape.COMMA}`); diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/helpers/index.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/helpers/index.js index 5782f0f6e7..9a9b94144a 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/helpers/index.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/helpers/index.js @@ -15,3 +15,4 @@ export { export { GEOMETRY, getFeatureType, getDataElement, getLabel } from './geometry'; export { convertClientToView } from './convertClientToView'; export { isNotValidOptionSet } from './isNotValidOptionSet'; +export { escapeString } from './escapeString'; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.js index b50d0134a2..f8a8908b20 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.js @@ -4,6 +4,7 @@ import cx from 'classnames'; import i18n from '@dhis2/d2-i18n'; import { withStyles } from '@material-ui/core'; import { spacersNum, colors, IconAdd16, Button } from '@dhis2/ui'; +import { ConditionalTooltip } from 'capture-core/components/ConditionalTooltip'; import { StageOverview } from './StageOverview'; import type { Props } from './stage.types'; import { Widget } from '../../../Widget'; @@ -23,11 +24,15 @@ const styles = { alignItems: 'center', }, }; +const hideProgramStage = (ruleEffects, stageId) => ( + Boolean(ruleEffects?.find(ruleEffect => ruleEffect.type === 'HIDEPROGRAMSTAGE' && ruleEffect.id === stageId)) +); - -export const StagePlain = ({ stage, events, classes, className, onCreateNew, ...passOnProps }: Props) => { +export const StagePlain = ({ stage, events, classes, className, onCreateNew, ruleEffects, ...passOnProps }: Props) => { const [open, setOpenStatus] = useState(true); const { id, name, icon, description, dataElements, hideDueDate, repeatable } = stage; + const hiddenProgramStage = hideProgramStage(ruleEffects, id); + return (
: } + /> : ( + + + + )}
); 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 c76e617668..6d5da181fa 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 @@ -16,6 +16,7 @@ import { colors, IconAdd16, Tooltip, } from '@dhis2/ui'; +import { ConditionalTooltip } from 'capture-core/components/ConditionalTooltip'; import { sortDataFromEvent } from './hooks/sortFuntions'; import { useComputeDataFromEvent, useComputeHeaderColumn, formatRowForView } from './hooks/useEventList'; import { DEFAULT_NUMBER_OF_ROW, SORT_DIRECTION } from './hooks/constants'; @@ -68,6 +69,7 @@ const StageDetailPlain = (props: Props) => { onEventClick, onViewAll, onCreateNew, + hiddenProgramStage, classes } = props; const defaultSortState = { columnName: 'status', @@ -146,17 +148,20 @@ const StageDetailPlain = (props: Props) => { key={id} onClick={() => !row.pendingApiResponse && onEventClick(row.id)} ref={(tableCell) => { - if (tableCell && row.pendingApiResponse) { - tableCell.onmouseover = onMouseOver; - tableCell.onmouseout = onMouseOut; - ref.current = tableCell; + if (tableCell) { + if (row.pendingApiResponse) { + tableCell.onmouseover = onMouseOver; + tableCell.onmouseout = onMouseOut; + ref.current = tableCell; + } else { + tableCell.onmouseover = null; + tableCell.onmouseout = null; + } } }} >
- { // $FlowFixMe - row[id] - } + {row[id]}
)} @@ -210,37 +215,35 @@ const StageDetailPlain = (props: Props) => { >{i18n.t('Go to full {{ eventName }}', { eventName, interpolation: { escapeValue: false } })} : null); const renderCreateNewButton = () => { - const shouldDisableCreateNew = !repeatable && events.length > 0; + const shouldDisableCreateNew = (!repeatable && events.length > 0) || hiddenProgramStage; - return (); + + + ); }; return ( diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js index ab03558397..ab47615397 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.js @@ -113,13 +113,13 @@ const useComputeDataFromEvent = (dataElements: Array, events: const useComputeHeaderColumn = (dataElements: Array, hideDueDate: boolean, formFoundation: Object) => { const headerColumns = useMemo(() => { const dataElementHeaders = dataElements.reduce((acc, currDataElement) => { - const { id, name, type, optionSet } = currDataElement; + const { id, name, formName, type, optionSet } = currDataElement; if (!acc.find(item => item.id === id)) { if (isNotValidOptionSet(type, optionSet)) { log.error(errorCreator(MULIT_TEXT_WITH_NO_OPTIONS_SET)({ currDataElement })); return acc; } - acc.push({ id, header: name, type, sortDirection: SORT_DIRECTION.DEFAULT }); + acc.push({ id, header: formName || name, type, sortDirection: SORT_DIRECTION.DEFAULT }); } return acc; }, []); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.js index ac5c02c897..a5edeeab75 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/stageDetail.types.js @@ -8,6 +8,7 @@ import type { StageDataElement, StageCommonProps } from '../../../types/common.t hideDueDate?: boolean, repeatable?: boolean, stageId: string, + hiddenProgramStage?: boolean, ...CssClasses, |}; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js index 53e97b9ebe..a3b0c008d4 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js @@ -1,4 +1,5 @@ // @flow +import { typeof effectActions } from '@dhis2/rules-engine-javascript'; import type { Icon } from 'capture-core/metaData'; import { dataElementTypes, Option } from '../../../metaData'; @@ -8,6 +9,7 @@ type StageOptions = { export type StageDataElement = { id: string, name: string, + formName: string, type: $Keys, options?: StageOptions, optionSet?: { options: Array