diff --git a/.cypress-cucumber-preprocessorrc.json b/.cypress-cucumber-preprocessorrc.json index f1975a9f5e..b935815d07 100644 --- a/.cypress-cucumber-preprocessorrc.json +++ b/.cypress-cucumber-preprocessorrc.json @@ -1,3 +1,3 @@ { "nonGlobalStepDefinitions": true -} \ No newline at end of file +} diff --git a/.env b/.env index 77a6b672f3..d41c6a2f62 100644 --- a/.env +++ b/.env @@ -1,3 +1,5 @@ REACT_APP_CACHE_VERSION=$npm_package_cacheVersion REACT_APP_SERVER_VERSION=$npm_package_serverVersion REACT_APP_VERSION=$npm_package_version +# TODO: Remove this when upgrading to react-scripts 5 (and webpack 5) +NODE_OPTIONS=--openssl-legacy-provider diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 6e92b4aae2..d1cf66c452 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -78,18 +78,17 @@ jobs: needs: prerequisites if: contains(fromJson(needs.prerequisites.outputs.json-labels), github.event.label.name) runs-on: ubuntu-latest - container: cypress/browsers:node14.7.0-chrome84 strategy: fail-fast: false matrix: versions: ${{ fromJSON(needs.prerequisites.outputs.versions) }} containers: ${{ fromJSON(needs.prerequisites.outputs.matrix-containers) }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x - name: compute-instance-url id: instance-url @@ -98,7 +97,7 @@ jobs: version: ${{ matrix.versions }} - name: Cypress run - uses: cypress-io/github-action@v4 + uses: cypress-io/github-action@v6 with: record: true parallel: true @@ -115,6 +114,7 @@ jobs: CYPRESS_dhis2InstanceVersion: ${{matrix.versions}} CYPRESS_dhis2Username: ${{ secrets.CYPRESS_DHIS2_USERNAME }} CYPRESS_dhis2Password: ${{ secrets.CYPRESS_DHIS2_PASSWORD }} + NODE_OPTIONS: "--openssl-legacy-provider" call-e2e-tests-result: needs: cypress diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index bbef3256d9..ddea1ad671 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -4,22 +4,22 @@ on: pull_request: types: [ labeled ] -jobs: +jobs: preview: runs-on: ubuntu-latest if: (github.event.label.name == 'preview' || github.event.label.name == 'testing') && !github.event.push.repository.fork && github.actor != 'dependabot[bot]' steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x - uses: actions/cache@v2 id: yarn-cache with: path: '**/node_modules' key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - + - name: Install if: steps.yarn-cache.outputs.cache-hit != 'true' run: yarn install --frozen-lockfile diff --git a/.github/workflows/verify-app.yml b/.github/workflows/verify-app.yml index afe87ad0dd..4877b7ceed 100644 --- a/.github/workflows/verify-app.yml +++ b/.github/workflows/verify-app.yml @@ -20,10 +20,10 @@ jobs: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[skip ci]')" steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x - uses: actions/cache@v2 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) @@ -40,10 +40,10 @@ jobs: needs: install if: "!contains(github.event.head_commit.message, '[skip ci]')" steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x - uses: actions/cache@v2 id: yarn-cache @@ -59,10 +59,10 @@ jobs: if: "!contains(github.event.head_commit.message, '[skip ci]')" needs: install steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x - uses: actions/cache@v2 id: yarn-cache @@ -78,10 +78,10 @@ jobs: needs: install if: "!contains(github.event.head_commit.message, '[skip ci]')" steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x - uses: actions/cache@v2 id: yarn-cache @@ -91,7 +91,7 @@ jobs: - name: Unit tests run: yarn test - + instance-version: runs-on: ubuntu-latest needs: [lint, flow, unit-tests] @@ -105,11 +105,10 @@ jobs: instance-url: ${{ secrets.CYPRESS_DHIS2_INSTANCES_BASE_URL }}/ca-test-dev username: ${{ secrets.CYPRESS_DHIS2_USERNAME }} password: ${{ secrets.CYPRESS_DHIS2_PASSWORD }} - + cypress-dev: runs-on: ubuntu-latest needs: instance-version - container: cypress/browsers:node14.7.0-chrome84 strategy: # when one test fails, DO NOT cancel the other # containers, because this will kill Cypress processes @@ -119,13 +118,13 @@ jobs: matrix: containers: [1, 2, 3, 4, 5] steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x - name: Cypress run - uses: cypress-io/github-action@v4 + uses: cypress-io/github-action@v6 with: record: true parallel: true @@ -142,16 +141,17 @@ jobs: CYPRESS_dhis2InstanceVersion: ${{ needs.instance-version.outputs.version }} CYPRESS_dhis2Username: ${{ secrets.CYPRESS_DHIS2_USERNAME }} CYPRESS_dhis2Password: ${{ secrets.CYPRESS_DHIS2_PASSWORD }} + NODE_OPTIONS: "--openssl-legacy-provider" build: runs-on: ubuntu-latest needs: [lint, flow, unit-tests] if: "!contains(github.event.head_commit.message, '[skip ci]')" steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x - uses: actions/cache@v2 id: yarn-cache @@ -175,13 +175,13 @@ jobs: needs: build if: "!github.event.push.repository.fork && !contains(github.event.head_commit.message, '[skip ci]') && github.actor != 'dependabot[bot]'" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: token: ${{ env.GH_TOKEN }} - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x - uses: actions/download-artifact@v2 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index ddcfc2c8a2..cc2c8c1be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +# [100.42.0](https://github.com/dhis2/capture-app/compare/v100.41.4...v100.42.0) (2023-10-24) + + +### Features + +* [DHIS2-12361] Tracked Entity Relationships widget ([cafed8d](https://github.com/dhis2/capture-app/commit/cafed8d8aac3fe9955739b1a8ee2cdde57722967)) + +## [100.41.4](https://github.com/dhis2/capture-app/compare/v100.41.3...v100.41.4) (2023-10-22) + + +### Bug Fixes + +* **translations:** sync translations from transifex (master) ([c82ce9f](https://github.com/dhis2/capture-app/commit/c82ce9f69159878f5ca9cc2e6ca9944544099368)) + +## [100.41.3](https://github.com/dhis2/capture-app/compare/v100.41.2...v100.41.3) (2023-10-16) + + +### Bug Fixes + +* [DHIS2-15782] navigation with program stage filter ([#3431](https://github.com/dhis2/capture-app/issues/3431)) ([8f7a017](https://github.com/dhis2/capture-app/commit/8f7a017c5860104620c6b7fc8c20916b00037302)) + ## [100.41.2](https://github.com/dhis2/capture-app/compare/v100.41.1...v100.41.2) (2023-10-12) diff --git a/config/eslint/alias.js b/config/eslint/alias.js index 553e8b60a6..345a65607b 100644 --- a/config/eslint/alias.js +++ b/config/eslint/alias.js @@ -14,4 +14,4 @@ if (fileExists(path.resolve(__dirname, '../../src/core_modules/capture-core'))) alias['capture-core-utils'] = path.resolve(__dirname, '../../src/core_modules/capture-core-utils'); } -module.exports = alias; \ No newline at end of file +module.exports = alias; diff --git a/cypress.config.js b/cypress.config.js new file mode 100644 index 0000000000..837af5437d --- /dev/null +++ b/cypress.config.js @@ -0,0 +1,35 @@ +const { chromeAllowXSiteCookies } = require('@dhis2/cypress-plugins'); +const { defineConfig } = require('cypress'); +const getCypressEnvVariables = require('./cypress/support/getCypressEnvVariables'); +const cucumberPreprocessor = require('./cypress/support/cucumberPreprocessor'); + +async function setupNodeEvents(on, config) { + await chromeAllowXSiteCookies(on); + await cucumberPreprocessor(on, config); + + config.env = getCypressEnvVariables(config); + return config; +} + +module.exports = defineConfig({ + video: true, + dhis2_datatest_prefix: 'dhis2-capture', + chromeWebSecurityComment: + 'chromeWebSecurity should removed once https://github.com/cypress-io/cypress/issues/4220 is fixed', + chromeWebSecurity: false, + defaultCommandTimeout: 25000, + projectId: '322xnh', + experimentalFetchPolyfill: true, + retries: { + runMode: 3, + }, + env: { + dhis2DataTestPrefix: 'capture-app', + networkMode: 'live', + }, + e2e: { + setupNodeEvents, + baseUrl: 'http://localhost:3000', + specPattern: 'cypress/e2e/**/*.feature', + }, +}); diff --git a/cypress.json b/cypress.json deleted file mode 100644 index 1520cffbe2..0000000000 --- a/cypress.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "baseUrl": "http://localhost:3000", - "video": true, - "testFiles": "**/*.feature", - "dhis2_datatest_prefix": "dhis2-capture", - "chromeWebSecurityComment": "chromeWebSecurity should removed once https://github.com/cypress-io/cypress/issues/4220 is fixed", - "chromeWebSecurity": false, - "defaultCommandTimeout": 25000, - "projectId": "322xnh", - "experimentalFetchPolyfill": true, - "retries": { - "runMode": 3 - }, - "env": { - "dhis2DataTestPrefix": "capture-app", - "networkMode": "live" - } -} \ No newline at end of file diff --git a/cypress/integration/AllAccessibleRecordsPage.feature b/cypress/e2e/AllAccessibleRecordsPage.feature similarity index 100% rename from cypress/integration/AllAccessibleRecordsPage.feature rename to cypress/e2e/AllAccessibleRecordsPage.feature diff --git a/cypress/integration/AllAccessibleRecordsPage/index.js b/cypress/e2e/AllAccessibleRecordsPage/index.js similarity index 96% rename from cypress/integration/AllAccessibleRecordsPage/index.js rename to cypress/e2e/AllAccessibleRecordsPage/index.js index a9468a7018..dfd48e73ef 100644 --- a/cypress/integration/AllAccessibleRecordsPage/index.js +++ b/cypress/e2e/AllAccessibleRecordsPage/index.js @@ -1,3 +1,5 @@ +import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'; + import '../sharedSteps'; Given('the user is on the the main page', () => { diff --git a/cypress/integration/EnrollmentAddEventPage/EnrollmentAddEventPageForm.feature b/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm.feature similarity index 100% rename from cypress/integration/EnrollmentAddEventPage/EnrollmentAddEventPageForm.feature rename to cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm.feature diff --git a/cypress/integration/EnrollmentAddEventPage/EnrollmentAddEventPageForm/index.js b/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm/index.js similarity index 93% rename from cypress/integration/EnrollmentAddEventPage/EnrollmentAddEventPageForm/index.js rename to cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm/index.js index c7b9294952..9791f3f190 100644 --- a/cypress/integration/EnrollmentAddEventPage/EnrollmentAddEventPageForm/index.js +++ b/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm/index.js @@ -1,3 +1,4 @@ +import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'; import '../sharedSteps'; const showAllEventsInProgramStage = () => { @@ -58,19 +59,6 @@ When(/^you click the checkbox number (.*)$/, (eq) => { .click(); }); -When(/^you click the (.*) button/, (buttonText) => { - cy.intercept({ - method: 'POST', - url: '**/tracker?async=false', - }).as('postEvents'); - - cy.get('[data-test="dhis2-uicore-button"]') - .contains(buttonText) - .click(); - - cy.wait('@postEvents'); -}); - When(/^you click the button to (.*) without post request/, (buttonText) => { cy.get('[data-test="dhis2-uicore-button"]') .contains(buttonText) diff --git a/cypress/integration/EnrollmentAddEventPage/EnrollmentAddEventPageNavigation.feature b/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageNavigation.feature similarity index 100% rename from cypress/integration/EnrollmentAddEventPage/EnrollmentAddEventPageNavigation.feature rename to cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageNavigation.feature diff --git a/cypress/integration/EnrollmentAddEventPage/EnrollmentAddEventPageNavigation/index.js b/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageNavigation/index.js similarity index 88% rename from cypress/integration/EnrollmentAddEventPage/EnrollmentAddEventPageNavigation/index.js rename to cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageNavigation/index.js index 593b33077a..b2f298297d 100644 --- a/cypress/integration/EnrollmentAddEventPage/EnrollmentAddEventPageNavigation/index.js +++ b/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageNavigation/index.js @@ -1,3 +1,4 @@ +import { Given, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; import '../sharedSteps'; Given(/^you land on the enrollment (.*) page by having typed (.*)$/, (_, url) => { diff --git a/cypress/integration/EnrollmentAddEventPage/ProgramStageSelector.feature b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector.feature similarity index 100% rename from cypress/integration/EnrollmentAddEventPage/ProgramStageSelector.feature rename to cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector.feature diff --git a/cypress/integration/EnrollmentAddEventPage/ProgramStageSelector/index.js b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/index.js similarity index 89% rename from cypress/integration/EnrollmentAddEventPage/ProgramStageSelector/index.js rename to cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/index.js index ddb02bd66d..6c8eac1dac 100644 --- a/cypress/integration/EnrollmentAddEventPage/ProgramStageSelector/index.js +++ b/cypress/e2e/EnrollmentAddEventPage/ProgramStageSelector/index.js @@ -1,3 +1,5 @@ +import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'; + Given('you land on the EnrollmentEventNew-page without a stageId', () => { cy.visit('/#/enrollmentEventNew?programId=IpHINAT79UW&orgUnitId=DiszpKrYNg8&teiId=x2kJgpb0XQC&enrollmentId=RiNIt1yJoge'); }); diff --git a/cypress/integration/EnrollmentAddEventPage/sharedSteps.js b/cypress/e2e/EnrollmentAddEventPage/sharedSteps.js similarity index 83% rename from cypress/integration/EnrollmentAddEventPage/sharedSteps.js rename to cypress/e2e/EnrollmentAddEventPage/sharedSteps.js index 855e6e90f1..9be509e28c 100644 --- a/cypress/integration/EnrollmentAddEventPage/sharedSteps.js +++ b/cypress/e2e/EnrollmentAddEventPage/sharedSteps.js @@ -1,3 +1,5 @@ +import { Then, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; + Then(/^you see the following (.*)$/, (message) => { cy.contains(message); }); diff --git a/cypress/integration/EnrollmentEditEventPage/EnrollmentEditEventPageForm.feature b/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageForm.feature similarity index 100% rename from cypress/integration/EnrollmentEditEventPage/EnrollmentEditEventPageForm.feature rename to cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageForm.feature diff --git a/cypress/integration/EnrollmentEditEventPage/EnrollmentEditEventPageForm/index.js b/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageForm/index.js similarity index 97% rename from cypress/integration/EnrollmentEditEventPage/EnrollmentEditEventPageForm/index.js rename to cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageForm/index.js index 3651052988..3c808d775d 100644 --- a/cypress/integration/EnrollmentEditEventPage/EnrollmentEditEventPageForm/index.js +++ b/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageForm/index.js @@ -1,3 +1,4 @@ +import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'; import { getCurrentYear } from '../../../support/date'; import '../../sharedSteps'; diff --git a/cypress/integration/EnrollmentEditEventPage/EnrollmentEditEventPageNavigation.feature b/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageNavigation.feature similarity index 100% rename from cypress/integration/EnrollmentEditEventPage/EnrollmentEditEventPageNavigation.feature rename to cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageNavigation.feature diff --git a/cypress/integration/EnrollmentEditEventPage/EnrollmentEditEventPageNavigation/index.js b/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageNavigation/index.js similarity index 92% rename from cypress/integration/EnrollmentEditEventPage/EnrollmentEditEventPageNavigation/index.js rename to cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageNavigation/index.js index a51cbe9816..76fbb1915c 100644 --- a/cypress/integration/EnrollmentEditEventPage/EnrollmentEditEventPageNavigation/index.js +++ b/cypress/e2e/EnrollmentEditEventPage/EnrollmentEditEventPageNavigation/index.js @@ -1,3 +1,5 @@ +import { Given, When, Then, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; + Given(/^you land on the enrollment event page by having typed (.*)$/, (url) => { cy.visit(url); cy.get('[data-test="person-selector-container"]').contains('Person'); diff --git a/cypress/integration/EnrollmentPage/BreakingTheGlass.feature b/cypress/e2e/EnrollmentPage/BreakingTheGlass.feature similarity index 93% rename from cypress/integration/EnrollmentPage/BreakingTheGlass.feature rename to cypress/e2e/EnrollmentPage/BreakingTheGlass.feature index a3600593a6..ac67d53256 100644 --- a/cypress/integration/EnrollmentPage/BreakingTheGlass.feature +++ b/cypress/e2e/EnrollmentPage/BreakingTheGlass.feature @@ -1,5 +1,7 @@ Feature: Breaking the glass page + # TODO - Flaky tests should be fixed by TECH-1662 + @skip Scenario: User with search scope access tries to access an enrollment in a protected program Given the tei created by this test is cleared from the database And you opt temporarily in on new enrollment dashboard in Child programme and WHO RMNCH Tracker diff --git a/cypress/integration/EnrollmentPage/BreakingTheGlass/index.js b/cypress/e2e/EnrollmentPage/BreakingTheGlass/index.js similarity index 97% rename from cypress/integration/EnrollmentPage/BreakingTheGlass/index.js rename to cypress/e2e/EnrollmentPage/BreakingTheGlass/index.js index 70a2fdad96..120dc0a0dc 100644 --- a/cypress/integration/EnrollmentPage/BreakingTheGlass/index.js +++ b/cypress/e2e/EnrollmentPage/BreakingTheGlass/index.js @@ -1,3 +1,4 @@ +import { Given, When, Then, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; import '../sharedSteps'; Given('the tei created by this test is cleared from the database', () => { @@ -89,6 +90,7 @@ And('you log out', () => { }); And('you log in as tracker2 user', () => { + cy.clearCookies(); cy.visit('/').then(() => { cy.get('#j_username').type('tracker2'); cy.get('#j_password').type('Tracker@123'); diff --git a/cypress/integration/EnrollmentPage/EnrollmentPageNavigation.feature b/cypress/e2e/EnrollmentPage/EnrollmentPageNavigation.feature similarity index 100% rename from cypress/integration/EnrollmentPage/EnrollmentPageNavigation.feature rename to cypress/e2e/EnrollmentPage/EnrollmentPageNavigation.feature diff --git a/cypress/integration/EnrollmentPage/EnrollmentPageNavigation/index.js b/cypress/e2e/EnrollmentPage/EnrollmentPageNavigation/index.js similarity index 97% rename from cypress/integration/EnrollmentPage/EnrollmentPageNavigation/index.js rename to cypress/e2e/EnrollmentPage/EnrollmentPageNavigation/index.js index 518d2416c3..bdc1d2f189 100644 --- a/cypress/integration/EnrollmentPage/EnrollmentPageNavigation/index.js +++ b/cypress/e2e/EnrollmentPage/EnrollmentPageNavigation/index.js @@ -1,3 +1,4 @@ +import { Given, When, Then, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; import '../../sharedSteps'; Given('you are on an enrollment page', () => { @@ -105,10 +106,6 @@ When('you reset the tei selection', () => { Then('you are navigated to the main page', () => { cy.url().should('include', `${Cypress.config().baseUrl}/#/?orgUnitId=UgYg0YW7ZIh&programId=IpHINAT79UW`); }); -When('you reset the program selection', () => { - cy.get('[data-test="program-selector-container-clear-icon"]') - .click(); -}); Then('you see message explaining you need to select a program', () => { cy.url().should('not.include', 'programId'); diff --git a/cypress/integration/EnrollmentPage/EnrollmentQuickActions.feature b/cypress/e2e/EnrollmentPage/EnrollmentQuickActions.feature similarity index 100% rename from cypress/integration/EnrollmentPage/EnrollmentQuickActions.feature rename to cypress/e2e/EnrollmentPage/EnrollmentQuickActions.feature diff --git a/cypress/integration/EnrollmentPage/EnrollmentQuickActions/index.js b/cypress/e2e/EnrollmentPage/EnrollmentQuickActions/index.js similarity index 93% rename from cypress/integration/EnrollmentPage/EnrollmentQuickActions/index.js rename to cypress/e2e/EnrollmentPage/EnrollmentQuickActions/index.js index 76142ded00..23ff019cf3 100644 --- a/cypress/integration/EnrollmentPage/EnrollmentQuickActions/index.js +++ b/cypress/e2e/EnrollmentPage/EnrollmentQuickActions/index.js @@ -1,3 +1,5 @@ +import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'; + Given('you are on an enrollment page with stage available', () => { cy.visit('/#/enrollment?programId=ur1Edk5Oe2n&orgUnitId=UgYg0YW7ZIh&teiId=zmgVvEZ91Kg&enrollmentId=xRnBV5aJDeF'); cy.get('[data-test="enrollment-page-content"]') diff --git a/cypress/integration/EnrollmentPage/HiddenProgramStage.feature b/cypress/e2e/EnrollmentPage/HiddenProgramStage.feature similarity index 100% rename from cypress/integration/EnrollmentPage/HiddenProgramStage.feature rename to cypress/e2e/EnrollmentPage/HiddenProgramStage.feature diff --git a/cypress/integration/EnrollmentPage/HiddenProgramStage/index.js b/cypress/e2e/EnrollmentPage/HiddenProgramStage/index.js similarity index 77% rename from cypress/integration/EnrollmentPage/HiddenProgramStage/index.js rename to cypress/e2e/EnrollmentPage/HiddenProgramStage/index.js index f9e5cf69c2..6147d114cc 100644 --- a/cypress/integration/EnrollmentPage/HiddenProgramStage/index.js +++ b/cypress/e2e/EnrollmentPage/HiddenProgramStage/index.js @@ -1,3 +1,4 @@ +import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'; import moment from 'moment'; const cleanUpIfApplicable = () => { @@ -7,13 +8,15 @@ const cleanUpIfApplicable = () => { ) .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'); + // TODO - Cypress does not handle optional chaining - figure out why + const enrollment = body.enrollments && body.enrollments.find(e => e.enrollment === 'fmhIsWXVDmS'); + const event = enrollment && enrollment.events && enrollment + .events + .find(e => e.programStage === 'PFDfvmGpsR3'); if (!event) { return null; } - return cy - .buildApiUrl('events', event.event) + return cy.buildApiUrl('events', event.event) .then(eventUrl => cy.request('DELETE', eventUrl)); }); diff --git a/cypress/integration/EnrollmentPage/StagesAndEventsWidget.feature b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget.feature similarity index 100% rename from cypress/integration/EnrollmentPage/StagesAndEventsWidget.feature rename to cypress/e2e/EnrollmentPage/StagesAndEventsWidget.feature diff --git a/cypress/integration/EnrollmentPage/StagesAndEventsWidget/index.js b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/index.js similarity index 89% rename from cypress/integration/EnrollmentPage/StagesAndEventsWidget/index.js rename to cypress/e2e/EnrollmentPage/StagesAndEventsWidget/index.js index 13c5dc60eb..7d867b0437 100644 --- a/cypress/integration/EnrollmentPage/StagesAndEventsWidget/index.js +++ b/cypress/e2e/EnrollmentPage/StagesAndEventsWidget/index.js @@ -1,3 +1,4 @@ +import { Given, When, Then, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; import { getCurrentYear } from '../../../support/date'; import '../sharedSteps'; @@ -22,7 +23,6 @@ Then('the stages and events widget should be closed', () => { cy.get('[data-test="stages-and-events-widget"]') .within(() => { cy.get('[data-test="widget-contents"]') - .children() .should('not.exist'); cy.contains('Birth').should('not.exist'); }); @@ -114,23 +114,29 @@ Then('the default list should be displayed', () => { }); When(/^you sort list asc by (.*)$/, (columnName) => { - cy.get('[data-test="stages-and-events-widget"]') - .find('[data-test="widget-contents"]') + cy.get('[data-test="stages-and-events-widget"]').should('exist') + .find('[data-test="widget-contents"]').should('exist') .find('[data-test="stage-content"]') + .should('exist') .eq(2) .find('thead') + .should('exist') .find('th') + .should('exist') + .contains(columnName) + .parent() + .should('exist') .within(() => { - cy.contains('th', columnName) - .find('button') - .click(); - cy.wait(100); - cy.contains('th', columnName) - .find('button') - .click(); + cy.get('button').should('exist') // Use cy.get() instead of cy.find() + .click() + .then(() => { + // Perform further actions or assertions if needed + cy.get('button').should('exist').click(); + }); }); }); + Then(/^the sorted list by (.*) asc should be displayed$/, () => { const rows = [ '07-13|Bumbeh MCHP', diff --git a/cypress/integration/EnrollmentPage/sharedSteps.js b/cypress/e2e/EnrollmentPage/sharedSteps.js similarity index 79% rename from cypress/integration/EnrollmentPage/sharedSteps.js rename to cypress/e2e/EnrollmentPage/sharedSteps.js index 8dea95f553..8f8e2992ca 100644 --- a/cypress/integration/EnrollmentPage/sharedSteps.js +++ b/cypress/e2e/EnrollmentPage/sharedSteps.js @@ -1,3 +1,5 @@ +import { Given } from '@badeball/cypress-cucumber-preprocessor'; + Given('you open the enrollment page', () => { cy.visit('#/enrollment?enrollmentId=wBU0RAsYjKE'); }); diff --git a/cypress/integration/MainPage.feature b/cypress/e2e/MainPage.feature similarity index 95% rename from cypress/integration/MainPage.feature rename to cypress/e2e/MainPage.feature index c95a65b9ff..70a5430db6 100644 --- a/cypress/integration/MainPage.feature +++ b/cypress/e2e/MainPage.feature @@ -1,6 +1,6 @@ Feature: User interacts with Main page - Scenario: The Working list is displayed + Scenario: The Working list is displayed Given you are in the main page with no selections made And the user selects the program Child Programme And the user selects the org unit Ngelehun CHC @@ -29,7 +29,7 @@ Feature: User interacts with Main page Then the current url is /#/?orgUnitId=DiszpKrYNg8&programId=M3xtLkYBlKI&selectedTemplateId=PpGINOT00UX Scenario: You are redirected to create a custom working list - Given you are in the search page with Ngelehun and MNCH / PNC context + Given you are in the search page with Ngelehun and MNCH PNC context And the search form is displayed When the user clicks the element containing the text: Create saved list Then the current url is /#/?orgUnitId=DiszpKrYNg8&programId=uy2gU8kT1jF&selectedTemplateId=uy2gU8kT1jF-default @@ -41,4 +41,4 @@ Feature: User interacts with Main page When you opt in to use the new enrollment Dashboard for Child Programme Then you see the opt out component for Child Programme When you opt out to use the new enrollment Dashboard for Child Programme - Then you see the opt in component for Child Programme \ No newline at end of file + Then you see the opt in component for Child Programme diff --git a/cypress/integration/MainPage/index.js b/cypress/e2e/MainPage/index.js similarity index 89% rename from cypress/integration/MainPage/index.js rename to cypress/e2e/MainPage/index.js index 60fcc41f37..f004659122 100644 --- a/cypress/integration/MainPage/index.js +++ b/cypress/e2e/MainPage/index.js @@ -1,6 +1,7 @@ +import { Given, Then, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; import '../sharedSteps'; -Given('you are in the search page with Ngelehun and MNCH / PNC context', () => { +Given('you are in the search page with Ngelehun and MNCH PNC context', () => { cy.visit('/#/search?orgUnitId=DiszpKrYNg8&programId=uy2gU8kT1jF'); }); diff --git a/cypress/integration/NewEventThroughAddRelationship.feature b/cypress/e2e/NewEventThroughAddRelationship.feature similarity index 100% rename from cypress/integration/NewEventThroughAddRelationship.feature rename to cypress/e2e/NewEventThroughAddRelationship.feature diff --git a/cypress/integration/NewEventThroughAddRelationship/index.js b/cypress/e2e/NewEventThroughAddRelationship/index.js similarity index 96% rename from cypress/integration/NewEventThroughAddRelationship/index.js rename to cypress/e2e/NewEventThroughAddRelationship/index.js index f0fb89eb86..d525b595a4 100644 --- a/cypress/integration/NewEventThroughAddRelationship/index.js +++ b/cypress/e2e/NewEventThroughAddRelationship/index.js @@ -1,3 +1,4 @@ +import { When, Then } from '@badeball/cypress-cucumber-preprocessor'; import { v4 as uuid } from 'uuid'; import '../sharedSteps'; @@ -15,8 +16,7 @@ When('you add data to the form', () => { }); When('you submit the form', () => { - cy.server(); - cy.route('POST', '**/tracker?async=false').as('postData'); + cy.intercept('POST', '**/tracker?async=false').as('postData'); cy.get('[data-test="dhis2-uicore-splitbutton-button"]') .click(); }); @@ -24,7 +24,7 @@ When('you submit the form', () => { Then('the event should be sent to the server successfully', () => { cy.wait('@postData', { timeout: 30000 }) .then((result) => { - expect(result.status).to.equal(200); + expect(result.response.statusCode).to.equal(200); // clean up const id = result.response.body.bundleReport.typeReportMap.EVENT.objectReports[0].uid; cy.buildApiUrl('events', id) diff --git a/cypress/integration/NewPage.feature b/cypress/e2e/NewPage.feature similarity index 98% rename from cypress/integration/NewPage.feature rename to cypress/e2e/NewPage.feature index 85aaa709d3..220ca7e971 100644 --- a/cypress/integration/NewPage.feature +++ b/cypress/e2e/NewPage.feature @@ -1,9 +1,19 @@ Feature: User creates a new entries from the registration page + Scenario: Viewing the registration page without any selections Given you are on the default registration page Then there should be informative message explaining you need to select an organisation unit + @v>=41 + Scenario: New person in Tracker Program > Filling the Allergies with multiple options + Given you are in the WHO RMNCH program registration page + When you fill in multiple Allergies options + Then you can see the multiple selections in the form + And you fill the WHO RMNCH program registration form with its required unique values + And you click the save person submit button + Then you are navigated to the WHO RMNCH program in Tracker Capture app + Scenario: Viewing the registration page with incomplete program categories selection Given you are in the main page with no selections made And you select org unit @@ -108,10 +118,10 @@ Feature: User creates a new entries from the registration page ### New Person Scenario: New person > Submitting the form with unique name navigates you to the user dashboard - Given you are in the Person registration page - When you fill in a unique first name - And you click the save person submit button - Then you are navigated to the Tracker Capture + Given you are in the Person registration page + When you fill in a unique first name + And you click the save person submit button + Then you are navigated to the Tracker Capture Scenario: New person > Submitting the form from the duplicates modal navigates you to the user dashboard Given you are in the Person registration page @@ -166,14 +176,6 @@ Feature: User creates a new entries from the registration page When you submit the form again from the duplicates modal Then you are navigated to the WHO RMNCH program in Tracker Capture app -@v>=41 - Scenario: New person in Tracker Program > Filling the Allergies with multiple options - Given you are in the WHO RMNCH program registration page - When you fill in multiple Allergies options - Then you can see the multiple selections in the form - And you fill the WHO RMNCH program registration form with its required unique values - And you click the save person submit button - Then you are navigated to the WHO RMNCH program in Tracker Capture app Scenario: New person in Tracker Program > Submitting the form shows a list with duplicates Given you are in Child programme registration page diff --git a/cypress/integration/NewPage/index.js b/cypress/e2e/NewPage/index.js similarity index 99% rename from cypress/integration/NewPage/index.js rename to cypress/e2e/NewPage/index.js index 4cc6af0afb..901452f440 100644 --- a/cypress/integration/NewPage/index.js +++ b/cypress/e2e/NewPage/index.js @@ -1,3 +1,4 @@ +import { Given, When, Then, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; import moment from 'moment'; import { getCurrentYear } from '../../support/date'; import '../sharedSteps'; diff --git a/cypress/integration/ScopeSelector.feature b/cypress/e2e/ScopeSelector.feature similarity index 98% rename from cypress/integration/ScopeSelector.feature rename to cypress/e2e/ScopeSelector.feature index ec80a5d2b0..7610b86994 100644 --- a/cypress/integration/ScopeSelector.feature +++ b/cypress/e2e/ScopeSelector.feature @@ -28,9 +28,10 @@ Feature: User uses the ScopeSelector to navigate When you select both org unit and program Malaria case registration Then you should see the table - Scenario: Main page > Url with invalid program id - Given you land on a main page with an invalid program id - Then you should see error message +# TODO - App crashes on invalid program id (DHIS2-16010) +# Scenario: Main page > Url with invalid program id +# Given you land on a main page with an invalid program id +# Then you should see error message Scenario: Main page > Url with invalid org unit id Given you land on a main page with an invalid org unit id diff --git a/cypress/integration/ScopeSelector/index.js b/cypress/e2e/ScopeSelector/index.js similarity index 97% rename from cypress/integration/ScopeSelector/index.js rename to cypress/e2e/ScopeSelector/index.js index 79fa8f698c..3daaa87f6e 100644 --- a/cypress/integration/ScopeSelector/index.js +++ b/cypress/e2e/ScopeSelector/index.js @@ -1,3 +1,4 @@ +import { Given, When, Then, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; import { getCurrentYear } from '../../support/date'; import '../sharedSteps'; @@ -231,18 +232,6 @@ Then('you are navigated to the search page with the same org unit and program Ch cy.url().should('eq', `${Cypress.config().baseUrl}/#/search?orgUnitId=DiszpKrYNg8&programId=IpHINAT79UW`); }); -Then('there should be visible a title with Child Program', () => { - cy.get('[data-test="search-page-content"]') - .contains('person in program: Child Programme') - .should('exist'); -}); - -And('there should be Child Programme domain forms visible to search with', () => { - cy.get('[data-test="search-page-content"]') - .find('[data-test="capture-ui-input"]') - .should('have.length', 1); -}); - const selectedChildProgram = ['Program', 'Child Programme']; const selectedMalariaProgram = ['Program', 'Malaria case diagnosis']; const selectedEventProgram = ['Program', 'Antenatal care visit']; diff --git a/cypress/integration/SearchForDuplicatesThroughAddRelationship.feature b/cypress/e2e/SearchForDuplicatesThroughAddRelationship.feature similarity index 100% rename from cypress/integration/SearchForDuplicatesThroughAddRelationship.feature rename to cypress/e2e/SearchForDuplicatesThroughAddRelationship.feature diff --git a/cypress/integration/SearchForDuplicatesThroughAddRelationship/index.js b/cypress/e2e/SearchForDuplicatesThroughAddRelationship/index.js similarity index 94% rename from cypress/integration/SearchForDuplicatesThroughAddRelationship/index.js rename to cypress/e2e/SearchForDuplicatesThroughAddRelationship/index.js index 9110c3f6d2..388903e301 100644 --- a/cypress/integration/SearchForDuplicatesThroughAddRelationship/index.js +++ b/cypress/e2e/SearchForDuplicatesThroughAddRelationship/index.js @@ -1,3 +1,4 @@ +import { When, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; import '../sharedSteps'; And('you fill in the first name with values that have duplicates', () => { diff --git a/cypress/integration/SearchPage.feature b/cypress/e2e/SearchPage.feature similarity index 100% rename from cypress/integration/SearchPage.feature rename to cypress/e2e/SearchPage.feature diff --git a/cypress/integration/SearchPage/index.js b/cypress/e2e/SearchPage/index.js similarity index 99% rename from cypress/integration/SearchPage/index.js rename to cypress/e2e/SearchPage/index.js index 93d0212d34..b322ef00bc 100644 --- a/cypress/integration/SearchPage/index.js +++ b/cypress/e2e/SearchPage/index.js @@ -1,3 +1,4 @@ +import { Given, Then, When, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; import '../sharedSteps'; Given('you are on the default search page', () => { diff --git a/cypress/integration/SearchThroughAddRelationship.feature b/cypress/e2e/SearchThroughAddRelationship.feature similarity index 100% rename from cypress/integration/SearchThroughAddRelationship.feature rename to cypress/e2e/SearchThroughAddRelationship.feature diff --git a/cypress/integration/SearchThroughAddRelationship/index.js b/cypress/e2e/SearchThroughAddRelationship/index.js similarity index 97% rename from cypress/integration/SearchThroughAddRelationship/index.js rename to cypress/e2e/SearchThroughAddRelationship/index.js index 8839e685a6..4d46ed2b22 100644 --- a/cypress/integration/SearchThroughAddRelationship/index.js +++ b/cypress/e2e/SearchThroughAddRelationship/index.js @@ -1,3 +1,4 @@ +import { When, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; import '../sharedSteps'; And('you select search scope TB program', () => { diff --git a/cypress/integration/SmokeTests.feature b/cypress/e2e/SmokeTests.feature similarity index 100% rename from cypress/integration/SmokeTests.feature rename to cypress/e2e/SmokeTests.feature diff --git a/cypress/integration/SmokeTests/index.js b/cypress/e2e/SmokeTests/index.js similarity index 85% rename from cypress/integration/SmokeTests/index.js rename to cypress/e2e/SmokeTests/index.js index eff621b5e0..7deac3868d 100644 --- a/cypress/integration/SmokeTests/index.js +++ b/cypress/e2e/SmokeTests/index.js @@ -1,3 +1,6 @@ +import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'; +import '../sharedSteps'; + Then('you should see the app main selections', () => { cy.get('[data-test="org-unit-selector-container"]'); cy.get('[data-test="program-selector-container"]'); @@ -36,12 +39,6 @@ When('you sign in', () => { .submit(); }); -Then('you should see the app main selections', () => { - cy.get('[data-test="org-unit-selector-container"]'); - - cy.get('[data-test="program-selector-container"]'); -}); - Then('you should see the header bar', () => { cy.get('[data-test="headerbar-title"]'); }); diff --git a/cypress/integration/StageEventListPage/StageEventListNavigation.feature b/cypress/e2e/StageEventListPage/StageEventListNavigation.feature similarity index 100% rename from cypress/integration/StageEventListPage/StageEventListNavigation.feature rename to cypress/e2e/StageEventListPage/StageEventListNavigation.feature diff --git a/cypress/integration/StageEventListPage/StageEventListNavigation/index.js b/cypress/e2e/StageEventListPage/StageEventListNavigation/index.js similarity index 83% rename from cypress/integration/StageEventListPage/StageEventListNavigation/index.js rename to cypress/e2e/StageEventListPage/StageEventListNavigation/index.js index b160ccb7f6..2aa919ca13 100644 --- a/cypress/integration/StageEventListPage/StageEventListNavigation/index.js +++ b/cypress/e2e/StageEventListPage/StageEventListNavigation/index.js @@ -1,3 +1,5 @@ +import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'; + Given('you are visiting Stage Event List Page by url', () => { cy.visit('/#/enrollment/stageEvents?enrollmentId=wBU0RAsYjKE&stageId=A03MvHHogjR&orgUnitId=DiszpKrYNg8&programId=IpHINAT79UW'); }); diff --git a/cypress/integration/TopBarActions.feature b/cypress/e2e/TopBarActions.feature similarity index 100% rename from cypress/integration/TopBarActions.feature rename to cypress/e2e/TopBarActions.feature diff --git a/cypress/integration/TopBarActions/index.js b/cypress/e2e/TopBarActions/index.js similarity index 91% rename from cypress/integration/TopBarActions/index.js rename to cypress/e2e/TopBarActions/index.js index 36a5c77f8e..24321c87fc 100644 --- a/cypress/integration/TopBarActions/index.js +++ b/cypress/e2e/TopBarActions/index.js @@ -1,3 +1,4 @@ +import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'; import '../sharedSteps'; Given(/^you land on a enrollment page domain by having typed (.*)$/, (url) => { diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollment/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js similarity index 95% rename from cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollment/index.js rename to cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js index 4584e1ba2a..676a73b3bb 100644 --- a/cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollment/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollment/index.js @@ -1,4 +1,5 @@ -const { getCurrentYear } = require('../../../support/date'); +import { When, Then } from '@badeball/cypress-cucumber-preprocessor'; +import { getCurrentYear } from '../../../support/date'; When('you click the enrollment widget toggle open close button', () => { cy.get('[data-test="widget-enrollment"]').within(() => { @@ -8,7 +9,7 @@ When('you click the enrollment widget toggle open close button', () => { Then('the enrollment widget should be closed', () => { cy.get('[data-test="widget-enrollment"]').within(() => { - cy.get('[data-test="widget-contents"]').children().should('not.exist'); + cy.get('[data-test="widget-contents"]').should('not.exist'); }); }); diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollmentComment/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollmentComment/index.js similarity index 90% rename from cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollmentComment/index.js rename to cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollmentComment/index.js index 1d0c464bc9..1e97d61e19 100644 --- a/cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollmentComment/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollmentComment/index.js @@ -1,8 +1,9 @@ +import { When, Then } from '@badeball/cypress-cucumber-preprocessor'; + Then('the stages and events should be loaded', () => { cy.contains('Stages and Events').should('exist'); }); - When(/^you fill in the comment: (.*)$/, (comment) => { cy.get('[data-test="enrollment-comment-widget"]').within(() => { cy.get('[data-test="comment-textfield"]').type(comment); diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetEventComment/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEventComment/index.js similarity index 92% rename from cypress/integration/WidgetsForEnrollmentPages/WidgetEventComment/index.js rename to cypress/e2e/WidgetsForEnrollmentPages/WidgetEventComment/index.js index 96211b0803..83fbe2c68d 100644 --- a/cypress/integration/WidgetsForEnrollmentPages/WidgetEventComment/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEventComment/index.js @@ -1,3 +1,5 @@ +import { When, Then } from '@badeball/cypress-cucumber-preprocessor'; + Then('the enrollment widget should be loaded', () => { cy.contains('The enrollment event data could not be found').should('not.exist'); }); diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetProfile/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetProfile/index.js similarity index 95% rename from cypress/integration/WidgetsForEnrollmentPages/WidgetProfile/index.js rename to cypress/e2e/WidgetsForEnrollmentPages/WidgetProfile/index.js index 8ac1a3f29b..31370a0606 100644 --- a/cypress/integration/WidgetsForEnrollmentPages/WidgetProfile/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetProfile/index.js @@ -1,3 +1,4 @@ +import { Then } from '@badeball/cypress-cucumber-preprocessor'; import '../../sharedSteps'; Then('the profile details should be displayed', () => { @@ -16,7 +17,6 @@ Then('the widget profile should be closed', () => { cy.get('[data-test="profile-widget"]') .within(() => { cy.get('[data-test="widget-contents"]') - .children() .should('not.exist'); }); }); diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetTab/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetTab/index.js similarity index 86% rename from cypress/integration/WidgetsForEnrollmentPages/WidgetTab/index.js rename to cypress/e2e/WidgetsForEnrollmentPages/WidgetTab/index.js index 085b0950ba..036671a649 100644 --- a/cypress/integration/WidgetsForEnrollmentPages/WidgetTab/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetTab/index.js @@ -1,4 +1,5 @@ -const { getCurrentYear } = require('../../../support/date'); +import { When, Then, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; +import { getCurrentYear } from '../../../support/date'; Then(/you should see tabs: (.*)/, (tabNames) => { const tabs = tabNames.split(','); diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage.feature b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage.feature similarity index 100% rename from cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage.feature rename to cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage.feature diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/index.js similarity index 100% rename from cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/index.js rename to cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/index.js diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard.feature b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard.feature similarity index 100% rename from cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard.feature rename to cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard.feature diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard/index.js similarity index 96% rename from cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard/index.js rename to cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard/index.js index 6ffc2f3caa..c0fa3ca18e 100644 --- a/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentDashboard/index.js @@ -1,3 +1,4 @@ +import { When, Then } from '@badeball/cypress-cucumber-preprocessor'; import moment from 'moment'; import '../sharedSteps'; import '../WidgetEnrollment'; diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent.feature b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent.feature similarity index 100% rename from cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent.feature rename to cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent.feature diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/index.js similarity index 100% rename from cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/index.js rename to cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/index.js diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEventSchedule.feature b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEventSchedule.feature similarity index 100% rename from cypress/integration/WidgetsForEnrollmentPages/WidgetsForEventSchedule.feature rename to cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEventSchedule.feature diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEventSchedule/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEventSchedule/index.js similarity index 93% rename from cypress/integration/WidgetsForEnrollmentPages/WidgetsForEventSchedule/index.js rename to cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEventSchedule/index.js index 6cc1222f24..1b606aa3c0 100644 --- a/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEventSchedule/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEventSchedule/index.js @@ -1,3 +1,4 @@ +import { When, Then } from '@badeball/cypress-cucumber-preprocessor'; import { getCurrentYear } from '../../../support/date'; import '../sharedSteps'; import '../WidgetTab'; diff --git a/cypress/integration/WidgetsForEnrollmentPages/sharedSteps.js b/cypress/e2e/WidgetsForEnrollmentPages/sharedSteps.js similarity index 88% rename from cypress/integration/WidgetsForEnrollmentPages/sharedSteps.js rename to cypress/e2e/WidgetsForEnrollmentPages/sharedSteps.js index f3773fc011..7cea912966 100644 --- a/cypress/integration/WidgetsForEnrollmentPages/sharedSteps.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/sharedSteps.js @@ -1,3 +1,5 @@ +import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'; + Given(/^you land on the enrollment (.*) page by having typed (.*)$/, (_, url) => { cy.visit(url); }); diff --git a/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsDev.feature b/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsDev.feature similarity index 100% rename from cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsDev.feature rename to cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsDev.feature diff --git a/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsDev/index.js b/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsDev/index.js similarity index 75% rename from cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsDev/index.js rename to cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsDev/index.js index b1b8f6c79a..e1d79d283a 100644 --- a/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsDev/index.js +++ b/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsDev/index.js @@ -1,8 +1,22 @@ +import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'; import '../../sharedSteps'; +beforeEach(() => { + // Disable cache for chromium browsers to force the api to be called + if (Cypress.browser.family === 'chromium') { + Cypress.automation('remote:debugger:protocol', { + command: 'Network.enable', + params: {}, + }); + Cypress.automation('remote:debugger:protocol', { + command: 'Network.setCacheDisabled', + params: { cacheDisabled: true }, + }); + } +}); + Given('you open the main page with Ngelehun and malaria case context', () => { - cy.server(); - cy.route('GET', '**/tracker/events**').as('getDefaultEvents'); + cy.intercept('GET', '**/tracker/events**').as('getDefaultEvents'); cy.visit('#/?programId=VBqh0ynB2wv&orgUnitId=DiszpKrYNg8'); }); @@ -11,15 +25,15 @@ Then('events should be retrieved from the api using the default query args', () cy.wait('@getDefaultEvents', { timeout: 40000 }).as('result'); cy.get('@result') - .its('status') + .its('response.statusCode') .should('equal', 200); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'pageSize=15'); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'page=1'); cy.get('@result').its('response.body.instances').as('events'); @@ -51,7 +65,7 @@ When('you select the working list called events assigned to anyone', () => { .contains('Events assigned to anyone') .should('exist'); - cy.route('GET', '**/tracker/events**').as('getEventsAssignedToAnyone'); + cy.intercept('GET', '**/tracker/events**').as('getEventsAssignedToAnyone'); cy.get('[data-test="workinglists-template-selector-chips-container"]') .contains('Events assigned to anyone') @@ -62,29 +76,29 @@ Then('events assigned to anyone should be retrieved from the api', () => { cy.wait('@getEventsAssignedToAnyone', { timeout: 40000 }).as('result'); cy.get('@result') - .its('status') + .its('response.statusCode') .should('equal', 200); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'assignedUserMode=ANY'); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'page=1'); cy.get('@result').its('response.body.instances').as('events'); }); When('you apply the assignee filter', () => { - cy.route('GET', '**/tracker/events**').as('getEventsAssignedToAnyone'); + cy.intercept('GET', '**/tracker/events**').as('getEventsAssignedToAnyone'); cy.get('[data-test="list-view-filter-apply-button"]') .click(); }); When('you apply the status filter', () => { - cy.route('GET', '**/tracker/events**').as('getActiveEventsAssignedToAnyone'); + cy.intercept('GET', '**/tracker/events**').as('getActiveEventsAssignedToAnyone'); cy.get('[data-test="list-view-filter-apply-button"]') .click(); @@ -94,68 +108,48 @@ Then('active events that are assigned to anyone should be retrieved from the api cy.wait('@getActiveEventsAssignedToAnyone', { timeout: 40000 }).as('result'); cy.get('@result') - .its('status') + .its('response.statusCode') .should('equal', 200); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'assignedUserMode=ANY'); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'status=ACTIVE'); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'page=1'); cy.get('@result').its('response.body.instances').as('events'); }); -When('you set the age filter to 10-20', () => { - cy.get('[data-test="event-working-lists"]') - .contains('Age (years)') - .click(); - - cy.get('[data-test="list-view-filter-contents"]') - .find('input[placeholder="Min"]') - .type('10'); - - cy.get('[data-test="list-view-filter-contents"]') - .find('input[placeholder="Max"]') - .type('20'); -}); - When('you apply the current filter on the event working list', () => { - cy.route('GET', '**/tracker/events**').as('getEvents'); + cy.intercept('GET', '**/tracker/events**').as('getEvents'); cy.get('[data-test="list-view-filter-apply-button"]') .click(); }); -Then('the age filter button should show that the filter is in effect', () => { - cy.get('[data-test="event-working-lists"]') - .contains('Age (years): 10 to 20') - .should('exist'); -}); - Then('events where age is between 10 and 20 should be retrieved from the api', () => { cy.wait('@getEvents', { timeout: 40000 }).as('result'); cy.get('@result') - .its('status') + .its('response.statusCode') .should('equal', 200); cy.get('@result') - .its('url') + .its('response.url') .should('match', /filter=.*10/); cy.get('@result') - .its('url') + .its('response.url') .should('match', /filter=.*20/); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'page=1'); cy.get('@result').its('response.body.instances').as('events'); @@ -165,7 +159,7 @@ When('you click the next page button on the event working list', () => { cy.get('[data-test="search-pagination-next-page"]') .should('exist'); - cy.route('GET', '**/tracker/events**').as('getEvents'); + cy.intercept('GET', '**/tracker/events**').as('getEvents'); cy.get('[data-test="search-pagination-next-page"]') .click(); @@ -175,21 +169,21 @@ Then('new events should be retrieved from the api', () => { cy.wait('@getEvents', { timeout: 40000 }).as('result'); cy.get('@result') - .its('status') + .its('response.statusCode') .should('equal', 200); cy.get('@result').its('response.body.instances').as('events'); }); When('you click the previous page button on the event working list', () => { - cy.route('GET', '**/tracker/events**').as('getEvents'); + cy.intercept('GET', '**/tracker/events**').as('getEvents'); cy.get('[data-test="search-pagination-previous-page"]') .click(); }); When('you click the first page button on the event working list', () => { - cy.route('GET', '**/tracker/events**').as('getEvents'); + cy.intercept('GET', '**/tracker/events**').as('getEvents'); cy.get('[data-test="search-pagination-first-page"]') .click(); @@ -199,7 +193,7 @@ When('you change rows per page to 50', () => { cy.get('div[data-test="rows-per-page-selector"]') .should('exist'); - cy.route('GET', '**/tracker/events**').as('getEvents'); + cy.intercept('GET', '**/tracker/events**').as('getEvents'); cy.get('div[data-test="rows-per-page-selector"]') .click() @@ -211,15 +205,15 @@ Then('an event batch capped at 50 records should be retrieved from the api', () cy.wait('@getEvents', { timeout: 40000 }).as('result'); cy.get('@result') - .its('status') + .its('response.statusCode') .should('equal', 200); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'pageSize=50'); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'page=1'); cy.get('@result').its('response.body.instances').as('events'); @@ -230,7 +224,7 @@ When('you click the report date column header', () => { .contains('Report date') .should('exist'); - cy.route('GET', '**/tracker/events**').as('getEvents'); + cy.intercept('GET', '**/tracker/events**').as('getEvents'); cy.get('[data-test="dhis2-uicore-tableheadercellaction"]') .eq(0) @@ -243,15 +237,15 @@ Then('events should be retrieved from the api ordered ascendingly by report date cy.wait('@getEvents', { timeout: 40000 }).as('resultAsc'); cy.get('@resultAsc') - .its('status') + .its('response.statusCode') .should('equal', 200); cy.get('@resultAsc') - .its('url') + .its('response.url') .should('match', /order=.*asc/); cy.get('@resultAsc') - .its('url') + .its('response.url') .should('include', 'page=1'); cy.get('@resultAsc').its('response.body.instances').as('events'); diff --git a/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsUser.feature b/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser.feature similarity index 99% rename from cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsUser.feature rename to cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser.feature index 529b1f43d4..46f86686fc 100644 --- a/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsUser.feature +++ b/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser.feature @@ -117,4 +117,4 @@ Given you open the main page with Ngelehun and Inpatient morbidity and mortality When you set the date of admission filter And you save the view as toDeleteWorkingList When you delete the name toDeleteWorkingList -Then the custom events working list is deleted \ No newline at end of file +Then the custom events working list is deleted diff --git a/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js b/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js similarity index 94% rename from cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js rename to cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js index cacda7707f..d16c2a84d7 100644 --- a/cypress/integration/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js +++ b/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js @@ -1,3 +1,4 @@ +import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'; import { v4 as uuid } from 'uuid'; import '../../sharedSteps'; import '../../../sharedSteps'; @@ -106,26 +107,6 @@ Then('the list should display active events that are assigned to anyone', () => }); }); -When('you set the age filter to 10-20', () => { - cy.get('[data-test="event-working-lists"]') - .contains('Age (years)') - .click(); - - cy.get('[data-test="list-view-filter-contents"]') - .find('input[placeholder="Min"]') - .type('10'); - - cy.get('[data-test="list-view-filter-contents"]') - .find('input[placeholder="Max"]') - .type('20'); -}); - -Then('the age filter button should show that the filter is in effect', () => { - cy.get('[data-test="event-working-lists"]') - .contains('Age (years): 10 to 20') - .should('exist'); -}); - Then('the list should display events where age is between 10 and 20', () => { const rows = [ '14 Male', @@ -274,7 +255,7 @@ Then('the list should display data ordered descendingly by report date', () => { cy.get('input[placeholder="To"]').click(); cy.contains('Update') - .click(); + .click({ force: true }); const rows = combineDataAndYear(lastYear, { '01-01': ['14 Female'], @@ -439,8 +420,7 @@ When(/^you save the view as (.*)$/, (name) => { cy.get('[data-test="view-name-content"]') .type(name); - cy.server(); - cy.route('POST', '**/eventFilters**').as('newEventFilter'); + cy.intercept('POST', '**/eventFilters**').as('newEventFilter'); cy.get('button') .contains('Save') @@ -479,7 +459,7 @@ Then('the admission filter should be in effect', () => { // clean up cy.get('@newEventResult') .then((result) => { - expect(result.status).to.equal(201); + expect(result.response.statusCode).to.equal(201); const id = result.response.body.response.uid; cy.buildApiUrl('eventFilters', id) .then((eventFiltersUrl) => { @@ -515,8 +495,7 @@ When('you delete the name toDeleteWorkingList', () => { .click(); cy.contains('Delete view') .click(); - cy.server(); - cy.route('DELETE', '**/eventFilters/**').as('deleteEventFilters'); + cy.intercept('DELETE', '**/eventFilters/**').as('deleteEventFilters'); cy.get('button') .contains('Confirm') .click(); diff --git a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsDev.feature b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsDev.feature similarity index 100% rename from cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsDev.feature rename to cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsDev.feature diff --git a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsDev/index.js b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsDev/index.js similarity index 71% rename from cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsDev/index.js rename to cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsDev/index.js index d7bbe85138..56989dd920 100644 --- a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsDev/index.js +++ b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsDev/index.js @@ -1,15 +1,28 @@ +import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'; import '../../sharedSteps'; +beforeEach(() => { + // Disable cache for chromium browsers to force the api to be called + if (Cypress.browser.family === 'chromium') { + Cypress.automation('remote:debugger:protocol', { + command: 'Network.enable', + params: {}, + }); + Cypress.automation('remote:debugger:protocol', { + command: 'Network.setCacheDisabled', + params: { cacheDisabled: true }, + }); + } +}); + Given('you open the main page with Ngelehun and child programme context', () => { - cy.server(); - cy.route('GET', '**/tracker/trackedEntities**').as('getDefaultTeis'); + cy.intercept('GET', '**/tracker/trackedEntities**').as('getDefaultTeis'); cy.visit('#/?programId=IpHINAT79UW&orgUnitId=DiszpKrYNg8'); }); Given('you open the main page with Ngelehun and Malaria focus investigation context', () => { - cy.server(); - cy.route('GET', '**/tracker/trackedEntities**').as('getDefaultTeis'); + cy.intercept('GET', '**/tracker/trackedEntities**').as('getDefaultTeis'); cy.visit('#/?programId=M3xtLkYBlKI&orgUnitId=DiszpKrYNg8'); }); @@ -18,15 +31,15 @@ Then('teis should be retrieved from the api using the default query args', () => cy.wait('@getDefaultTeis', { timeout: 40000 }).as('result'); cy.get('@result') - .its('status') - .should('equal', 200); + .its('response.statusCode') + .should('eq', 200); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'pageSize=15'); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'page=1'); cy.get('@result').its('response.body.instances').as('teis'); @@ -46,7 +59,7 @@ When('you select the working list called Active enrollments', () => { .contains('Active enrollments') .should('exist'); - cy.route('GET', '**/tracker/trackedEntities**').as('getTeisWithEnrollmentStatusActive'); + cy.intercept('GET', '**/tracker/trackedEntities**').as('getTeisWithEnrollmentStatusActive'); cy.get('[data-test="workinglists-template-selector-chips-container"]') .contains('Active enrollments') @@ -55,7 +68,7 @@ When('you select the working list called Active enrollments', () => { When('you apply the enrollment status filter', () => { - cy.route('GET', '**/tracker/trackedEntities**').as('getTeisWithEnrollmentStatusActive'); + cy.intercept('GET', '**/tracker/trackedEntities**').as('getTeisWithEnrollmentStatusActive'); cy.get('[data-test="list-view-filter-apply-button"]') .click(); @@ -65,22 +78,22 @@ Then('teis with an active enrollment should be retrieved from the api', () => { cy.wait('@getTeisWithEnrollmentStatusActive', { timeout: 40000 }).as('result'); cy.get('@result') - .its('status') - .should('equal', 200); + .its('response.statusCode') + .should('eq', 200); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'programStatus=ACTIVE'); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'page=1'); cy.get('@result').its('response.body.instances').as('teis'); }); When('you apply the assignee filter', () => { - cy.route('GET', '**/tracker/trackedEntities**').as('getTeisStatusAndAssigneeFilter'); + cy.intercept('GET', '**/tracker/trackedEntities**').as('getTeisStatusAndAssigneeFilter'); cy.get('[data-test="list-view-filter-apply-button"]') .click(); @@ -90,26 +103,26 @@ Then('teis with active enrollments and unassigned events should be retrieved fro cy.wait('@getTeisStatusAndAssigneeFilter', { timeout: 40000 }).as('result'); cy.get('@result') - .its('status') - .should('equal', 200); + .its('response.statusCode') + .should('eq', 200); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'programStatus=ACTIVE'); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'assignedUserMode=NONE'); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'page=1'); cy.get('@result').its('response.body.instances').as('teis'); }); When('you apply the current filter on the tei working list', () => { - cy.route('GET', '**/tracker/trackedEntities**').as('getTeis'); + cy.intercept('GET', '**/tracker/trackedEntities**').as('getTeis'); cy.get('[data-test="list-view-filter-apply-button"]') .click(); @@ -119,15 +132,15 @@ Then('teis with a first name containing John should be retrieved from the api', cy.wait('@getTeis', { timeout: 40000 }).as('result'); cy.get('@result') - .its('status') - .should('equal', 200); + .its('response.statusCode') + .should('eq', 200); cy.get('@result') - .its('url') + .its('response.url') .should('match', /filter=.*John/); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'page=1'); cy.get('@result').its('response.body.instances').as('teis'); @@ -158,7 +171,7 @@ When('you click the next page button on the tei working list', () => { cy.get('[data-test="search-pagination-next-page"]') .should('exist'); - cy.route('GET', '**/tracker/trackedEntities**').as('getTeis'); + cy.intercept('GET', '**/tracker/trackedEntities**').as('getTeis'); cy.get('[data-test="search-pagination-next-page"]') .click(); @@ -168,15 +181,15 @@ Then('new teis should be retrieved from the api', () => { cy.wait('@getTeis', { timeout: 40000 }).as('result'); cy.get('@result') - .its('status') - .should('equal', 200); + .its('response.statusCode') + .should('eq', 200); cy.get('@result').its('response.body.instances').as('teis'); }); When('you click the previous page button on the tei working list', () => { - cy.route('GET', '**/tracker/trackedEntities**').as('getTeis'); + cy.intercept('GET', '**/tracker/trackedEntities**').as('getTeis'); cy.get('[data-test="search-pagination-previous-page"]') .click(); @@ -186,7 +199,7 @@ When('you change rows per page to 50', () => { cy.get('div[data-test="rows-per-page-selector"]') .should('exist'); - cy.route('GET', '**/tracker/trackedEntities**').as('getTeis'); + cy.intercept('GET', '**/tracker/trackedEntities**').as('getTeis'); cy.get('div[data-test="rows-per-page-selector"]') .click() @@ -198,22 +211,22 @@ Then('a tei batch capped at 50 records should be retrieved from the api', () => cy.wait('@getTeis', { timeout: 40000 }).as('result'); cy.get('@result') - .its('status') - .should('equal', 200); + .its('response.statusCode') + .should('eq', 200); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'pageSize=50'); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'page=1'); cy.get('@result').its('response.body.instances').as('teis'); }); When('you click the first page button on the tei working list', () => { - cy.route('GET', '**/tracker/trackedEntities**').as('getTeis'); + cy.intercept('GET', '**/tracker/trackedEntities**').as('getTeis'); cy.get('[data-test="search-pagination-first-page"]') .click(); @@ -224,7 +237,7 @@ When('you click the first name column header', () => { .contains('First name') .should('exist'); - cy.route('GET', '**/tracker/trackedEntities**').as('getTeis'); + cy.intercept('GET', '**/tracker/trackedEntities**').as('getTeis'); cy.get('[data-test="dhis2-uicore-tableheadercellaction"]') .eq(0) @@ -235,15 +248,15 @@ Then('teis should be retrieved from the api ordered ascendingly by first name', cy.wait('@getTeis', { timeout: 40000 }).as('result'); cy.get('@result') - .its('status') - .should('equal', 200); + .its('response.statusCode') + .should('eq', 200); cy.get('@result') - .its('url') + .its('response.url') .should('match', /order=.*asc/); cy.get('@result') - .its('url') + .its('response.url') .should('include', 'page=1'); cy.get('@result').its('response.body.instances').as('teis'); diff --git a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser.feature b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser.feature similarity index 97% rename from cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser.feature rename to cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser.feature index d3f07adfe2..b0bdfef1e0 100644 --- a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser.feature +++ b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser.feature @@ -186,6 +186,13 @@ And you select the events scheduled today And you apply the current filter Then you see the selected option in the scheduledAt filter +@v>=39 +Scenario: The program stage working list configureation is kept when navigating +Given you open the main page with Ngelehun and WHO RMNCH Tracker context and configure a program stage working list +When you open an enrollment event from the working list +And you go back using the browser button +Then the program stage working list is loaded + @v>=40 Scenario: The user creates, updates and deletes a Program stage custom working list Given you open the main page with Ngelehun and Malaria case diagnosis and Household investigation context diff --git a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js similarity index 92% rename from cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js rename to cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js index 7e5825c8a1..1c1574e25d 100644 --- a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js +++ b/cypress/e2e/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js @@ -1,3 +1,4 @@ +import { Given, When, Then, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; import { v4 as uuid } from 'uuid'; import '../../sharedSteps'; import '../../../sharedSteps'; @@ -6,7 +7,7 @@ const cleanUpIfApplicable = (programId) => { cy.buildApiUrl(`programStageWorkingLists?filter=program.id:eq:${programId}&fields=id,displayName`) .then(url => cy.request(url)) .then(({ body }) => { - const workingList = body.programStageWorkingLists?.find(e => e.displayName === 'Custom Program stage list'); + const workingList = body.programStageWorkingLists && body.programStageWorkingLists.find(e => e.displayName === 'Custom Program stage list'); if (!workingList) { return null; } @@ -167,21 +168,6 @@ When('you set the enrollment date to a relative range', () => { .type('1000'); }); -When('you apply the current filter', () => { - cy.get('[data-test="list-view-filter-apply-button"]') - .click(); -}); - -When('you set the enrollment status filter to active', () => { - cy.get('[data-test="tei-working-lists"]') - .contains('Enrollment status') - .click(); - - cy.get('[data-test="list-view-filter-contents"]') - .contains('Active') - .click(); -}); - When('you set the event status filter to completed', () => { cy.get('[data-test="tei-working-lists"]') .contains('Event status') @@ -192,17 +178,6 @@ When('you set the event status filter to completed', () => { .click(); }); -When(/^you set the first name filter to (.*)$/, (name) => { - cy.get('[data-test="tei-working-lists"]') - .contains('First name') - .click(); - - cy.get('[data-test="list-view-filter-contents"]') - .find('input') - .type(name) - .blur(); -}); - When('you set the WHOMCH Smoking filter to No', () => { cy.get('[data-test="tei-working-lists"]') .within(() => { @@ -227,12 +202,6 @@ When('you set the assginee filter to none', () => { .click(); }); -Then('the enrollment status filter button should show that the active filter is in effect', () => { - cy.get('[data-test="tei-working-lists"]') - .contains('Enrollment status: Active') - .should('exist'); -}); - Then('the assignee filter button should show that unassigned filter is in effect', () => { cy.get('[data-test="tei-working-lists"]') .contains('Assigned to: None') @@ -453,8 +422,8 @@ When('you save the list with the name My custom list', () => { .click(); cy.get('[data-test="view-name-content"]') .type('My custom list'); - cy.server(); - cy.route('POST', '**/trackedEntityInstanceFilters**').as('newTrackedEntityInstanceFilters'); + + cy.intercept('POST', '**/trackedEntityInstanceFilters**').as('newTrackedEntityInstanceFilters'); cy.get('button') .contains('Save') .click(); @@ -468,8 +437,8 @@ When('you save the list with the name Custom Program stage list', () => { .click(); cy.get('[data-test="view-name-content"]') .type('Custom Program stage list'); - cy.server(); - cy.route('POST', '**/programStageWorkingLists**').as('newProgramStageWorkingLists'); + + cy.intercept('POST', '**/programStageWorkingLists**').as('newProgramStageWorkingLists'); cy.get('button') .contains('Save') .click(); @@ -479,8 +448,8 @@ When('you save the list with the name Custom Program stage list', () => { When('you update the list with the name My custom list', () => { cy.get('[data-test="list-view-menu-button"]') .click(); - cy.server(); - cy.route('PUT', '**/trackedEntityInstanceFilters/**').as('editTrackedEntityInstanceFilters'); + + cy.intercept('PUT', '**/trackedEntityInstanceFilters/**').as('editTrackedEntityInstanceFilters'); cy.contains('Update view') .click(); cy.wait('@editTrackedEntityInstanceFilters', { timeout: 30000 }); @@ -489,8 +458,8 @@ When('you update the list with the name My custom list', () => { When('you update the list with the name Custom Program stage list', () => { cy.get('[data-test="list-view-menu-button"]') .click(); - cy.server(); - cy.route('PUT', '**/programStageWorkingLists/**').as('editProgramStageWorkingLists'); + + cy.intercept('PUT', '**/programStageWorkingLists/**').as('editProgramStageWorkingLists'); cy.contains('Update view') .click(); cy.wait('@editProgramStageWorkingLists', { timeout: 30000 }); @@ -508,8 +477,8 @@ When('you delete the name My custom list', () => { .click(); cy.contains('Delete view') .click(); - cy.server(); - cy.route('DELETE', '**/trackedEntityInstanceFilters/**').as('deleteTrackedEntityInstanceFilters'); + + cy.intercept('DELETE', '**/trackedEntityInstanceFilters/**').as('deleteTrackedEntityInstanceFilters'); cy.get('button') .contains('Confirm') .click(); @@ -521,8 +490,8 @@ When('you delete the name Custom Program stage list', () => { .click(); cy.contains('Delete view') .click(); - cy.server(); - cy.route('DELETE', '**/programStageWorkingLists/**').as('deleteProgramStageWorkingLists'); + + cy.intercept('DELETE', '**/programStageWorkingLists/**').as('deleteProgramStageWorkingLists'); cy.get('button') .contains('Confirm') .click(); @@ -748,15 +717,48 @@ Then('the working list configuration was kept', () => { .should('exist'); }); -Then('the working list configuration was kept', () => { +Then('the program stage custom working list filters are loaded', () => { cy.get('[data-test="tei-working-lists"]') - .contains('Event status: Completed') - .should('exist'); + .find('[data-test="more-filters"]') + .should('have.length', 2); }); -Then('the program stage custom working list filters are loaded', () => { +Given('you open the main page with Ngelehun and WHO RMNCH Tracker context and configure a program stage working list', () => { + cy.visit('#/?programId=WSGAb5XwJ3Y&orgUnitId=DiszpKrYNg8'); + cy.get('[data-test="template-selector-create-list"]') + .click(); + + cy.get('[data-test="tei-working-lists"]') + .within(() => { + cy.contains('More filters') + .click(); + cy.contains('Program stage') + .click(); + }); + + cy.get('[data-test="list-view-filter-contents"]') + .contains('Postpartum care visit') + .click(); + + cy.get('[data-test="list-view-filter-apply-button"]') + .click(); +}); + +When('you open an enrollment event from the working list', () => { + cy.contains('Linda') + .click(); +}); + +When('you go back using the browser button', () => { + cy.go('back'); +}); + +Then('the program stage working list is loaded', () => { cy.get('[data-test="tei-working-lists"]') .find('[data-test="more-filters"]') .should('have.length', 2); -}); + cy.get('[data-test="tei-working-lists"]') + .contains('WHOMCH Hemoglobin value') + .should('exist'); +}); diff --git a/cypress/integration/WorkingLists/sharedSteps.js b/cypress/e2e/WorkingLists/sharedSteps.js similarity index 96% rename from cypress/integration/WorkingLists/sharedSteps.js rename to cypress/e2e/WorkingLists/sharedSteps.js index 5c359d2569..c28bdb5ccb 100644 --- a/cypress/integration/WorkingLists/sharedSteps.js +++ b/cypress/e2e/WorkingLists/sharedSteps.js @@ -1,3 +1,5 @@ +import { When, Then } from '@badeball/cypress-cucumber-preprocessor'; + Then('for an event program the page navigation should show that you are on the first page', () => { cy.get('[data-test="event-working-lists"]') .contains('Page 1') @@ -106,14 +108,14 @@ When('you set the assginee filter to none', () => { .click(); }); -When('you set the first name filter to John', () => { +When(/^you set the first name filter to (.*)$/, (name) => { cy.get('[data-test="tei-working-lists"]') .contains('First name') .click(); cy.get('[data-test="list-view-filter-contents"]') .find('input') - .type('John') + .type(name) .blur(); }); diff --git a/cypress/integration/sharedSteps.js b/cypress/e2e/sharedSteps.js similarity index 93% rename from cypress/integration/sharedSteps.js rename to cypress/e2e/sharedSteps.js index 7b8e7a3bc9..16967a0c21 100644 --- a/cypress/integration/sharedSteps.js +++ b/cypress/e2e/sharedSteps.js @@ -1,3 +1,18 @@ +import { + Given, + When, + Then, + defineStep as And, Before, +} from '@badeball/cypress-cucumber-preprocessor'; +import { filterInstanceVersion, shouldClearCookies } from '../support/tagUtils'; + +Before(function callback() { + filterInstanceVersion(() => this.skip()); + if (shouldClearCookies()) { + cy.clearAllCookies(); + } +}); + Given('you are in the main page with no selections made', () => { cy.visit('/#/'); cy.get('[data-test="new-event-button"]') @@ -186,8 +201,7 @@ Then(/^you see the opt out component for (.*)$/, (program) => { }); When(/^you opt out to use the new enrollment Dashboard for (.*)$/, (program) => { - cy.server(); - cy.route('PUT', '**/dataStore/capture/useNewDashboard').as('optOutEnrollmentDashboard'); + cy.intercept('PUT', '**/dataStore/capture/useNewDashboard').as('optOutEnrollmentDashboard'); cy.contains('[data-test="dhis2-uicore-button"]', `Opt out for ${program}`).click(); cy.wait('@optOutEnrollmentDashboard', { timeout: 30000 }); }); diff --git a/cypress/integration/common/index.js b/cypress/integration/common/index.js deleted file mode 100644 index c104ab1095..0000000000 --- a/cypress/integration/common/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Before } from 'cypress-cucumber-preprocessor/steps'; -import { filterInstanceVersion, shouldLogin } from '../../support/tagUtils'; - - -let skip; -Before(() => { - filterInstanceVersion(skip); - shouldLogin() && cy.loginThroughForm(); -}); - -beforeEach(function callback() { - /* - Cypress automatically clears all cookies between tests except cross domain cookies. - When we are working with a remote instance, we need to clear the login cookie associated with the remote domain. - Currently, calling clearCookies with domain:null is a workaround to get rid of the cross domain cookies. - Related Cypress issue https://github.com/cypress-io/cypress/issues/408 - */ - cy.clearCookies({ domain: null }); - skip = this.skip.bind(this); -}); diff --git a/cypress/plugins/ignore-x-frame-headers/README.md b/cypress/plugins/ignore-x-frame-headers/README.md deleted file mode 100644 index b3df5337a5..0000000000 --- a/cypress/plugins/ignore-x-frame-headers/README.md +++ /dev/null @@ -1,5 +0,0 @@ -We include this plugin as a fix to the [issue]( https://github.com/cypress-io/cypress/issues/4220). - -When the issue is resolved we can remove: -1. the plugin from the `plugins/index.js` -2. the chromeWebSecurity from the `cypress.json` file \ No newline at end of file diff --git a/cypress/plugins/ignore-x-frame-headers/background.js b/cypress/plugins/ignore-x-frame-headers/background.js deleted file mode 100644 index 236bfe801f..0000000000 --- a/cypress/plugins/ignore-x-frame-headers/background.js +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable */ -const HEADERS_TO_STRIP_LOWERCASE = [ - 'content-security-policy', - 'x-frame-options', -]; - -chrome.webRequest.onHeadersReceived.addListener( - details => ({ - responseHeaders: details.responseHeaders.filter(header => HEADERS_TO_STRIP_LOWERCASE.indexOf(header.name.toLowerCase()) < 0), - }), { - urls: [''], - }, ['blocking', 'responseHeaders']); - diff --git a/cypress/plugins/ignore-x-frame-headers/icon128.png b/cypress/plugins/ignore-x-frame-headers/icon128.png deleted file mode 100644 index c0a240b2f3..0000000000 Binary files a/cypress/plugins/ignore-x-frame-headers/icon128.png and /dev/null differ diff --git a/cypress/plugins/ignore-x-frame-headers/icon16.png b/cypress/plugins/ignore-x-frame-headers/icon16.png deleted file mode 100644 index b6448e9114..0000000000 Binary files a/cypress/plugins/ignore-x-frame-headers/icon16.png and /dev/null differ diff --git a/cypress/plugins/ignore-x-frame-headers/icon48.png b/cypress/plugins/ignore-x-frame-headers/icon48.png deleted file mode 100644 index f0b40c731f..0000000000 Binary files a/cypress/plugins/ignore-x-frame-headers/icon48.png and /dev/null differ diff --git a/cypress/plugins/ignore-x-frame-headers/manifest.json b/cypress/plugins/ignore-x-frame-headers/manifest.json deleted file mode 100644 index ceb41f11da..0000000000 --- a/cypress/plugins/ignore-x-frame-headers/manifest.json +++ /dev/null @@ -1,24 +0,0 @@ -{ -"update_url": "https://clients2.google.com/service/update2/crx", - - "manifest_version": 2, - "name": "Ignore X-Frame headers", - "version": "1.1.1", - - "description": "Drops X-Frame-Options and Content-Security-Policy HTTP response headers, allowing all pages to be iframed.", - "icons": { - "16": "icon16.png", - "48": "icon48.png", - "128": "icon128.png" - }, - - "background": { - "scripts": ["background.js"] - }, - - "permissions": [ - "webRequest", - "webRequestBlocking", - "" - ] -} diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js deleted file mode 100644 index a79fe99d49..0000000000 --- a/cypress/plugins/index.js +++ /dev/null @@ -1,25 +0,0 @@ -const { - chromeAllowXSiteCookies, - cucumberPreprocessor, -} = require('@dhis2/cypress-plugins'); - -const getCypressEnvVariables = require('./getCypressEnvVariables'); -const path = require('path'); - -module.exports = (on, config) => { - chromeAllowXSiteCookies(on); - cucumberPreprocessor(on, config); - on('before:browser:launch', (browser, launchOptions) => { - if (browser.family === 'chromium' && browser.name !== 'electron') { - launchOptions.extensions.push(path.join(__dirname, '/ignore-x-frame-headers')); - - launchOptions.args.push('--disable-features=SameSiteByDefaultCookies,CookiesWithoutSameSiteMustBeSecure,SameSiteDefaultChecksMethodRigorously'); - } - - return launchOptions; - }); - - // Add additional plugins here - config.env = getCypressEnvVariables(config); - return config; -}; diff --git a/cypress/support/cucumberPreprocessor.js b/cypress/support/cucumberPreprocessor.js new file mode 100644 index 0000000000..53e2010cbc --- /dev/null +++ b/cypress/support/cucumberPreprocessor.js @@ -0,0 +1,31 @@ +const preprocessor = require('@badeball/cypress-cucumber-preprocessor'); +const webpack = require('@cypress/webpack-preprocessor'); + +module.exports = async function cucumberPreprocessor(on, config) { + // This is required for the preprocessor to be able to generate JSON reports after each run, and more, + await preprocessor.addCucumberPreprocessorPlugin(on, config); + + on( + 'file:preprocessor', + webpack({ + webpackOptions: { + resolve: { + extensions: ['.ts', '.js'], + }, + module: { + rules: [ + { + test: /\.feature$/, + use: [ + { + loader: '@badeball/cypress-cucumber-preprocessor/dist/bundler-utils/webpack', + options: config, + }, + ], + }, + ], + }, + }, + }), + ); +}; diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js new file mode 100644 index 0000000000..031f323471 --- /dev/null +++ b/cypress/support/e2e.js @@ -0,0 +1,4 @@ +import { enableAutoLogin } from '@dhis2/cypress-commands'; +import './commands'; + +enableAutoLogin(); diff --git a/cypress/plugins/getCypressEnvVariables.js b/cypress/support/getCypressEnvVariables/index.js similarity index 100% rename from cypress/plugins/getCypressEnvVariables.js rename to cypress/support/getCypressEnvVariables/index.js diff --git a/cypress/support/index.js b/cypress/support/index.js deleted file mode 100644 index 2af960ceb6..0000000000 --- a/cypress/support/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import './commands'; - diff --git a/cypress/support/tagUtils/filterInstanceVersion.js b/cypress/support/tagUtils/filterInstanceVersion.js index 8ec2b8870d..68ae9ea14d 100644 --- a/cypress/support/tagUtils/filterInstanceVersion.js +++ b/cypress/support/tagUtils/filterInstanceVersion.js @@ -1,5 +1,5 @@ export const filterInstanceVersion = (skip) => { - const { tags } = window.testState.currentScenario; + const { tags } = window.testState.pickle; if (!tags || !tags.length) { return; } @@ -26,8 +26,15 @@ export const filterInstanceVersion = (skip) => { .some((versionTag) => { const version = Number(versionTag[2]); const operator = versionTag[1] || '='; - return operation[operator]?.(currentInstanceVersion, version) ?? false; + + if (!operation[operator] || !currentInstanceVersion) { + return false; + } + + return operation[operator](currentInstanceVersion, version); }); - !shouldRun && skip(); + if (!shouldRun) { + skip(); + } }; diff --git a/cypress/support/tagUtils/index.js b/cypress/support/tagUtils/index.js index e2c3a66227..7471fd0a2f 100644 --- a/cypress/support/tagUtils/index.js +++ b/cypress/support/tagUtils/index.js @@ -1,2 +1,2 @@ export { filterInstanceVersion } from './filterInstanceVersion'; -export { shouldLogin } from './shouldLogin'; +export { shouldClearCookies } from './shouldClearCookies'; diff --git a/cypress/support/tagUtils/shouldClearCookies.js b/cypress/support/tagUtils/shouldClearCookies.js new file mode 100644 index 0000000000..a0691ce760 --- /dev/null +++ b/cypress/support/tagUtils/shouldClearCookies.js @@ -0,0 +1,10 @@ +export const shouldClearCookies = () => { + const { tags } = window.testState.pickle; + + if (!tags || !tags.length) { + return false; + } + + return tags + .some(({ name }) => name === '@skip-login'); +}; diff --git a/cypress/support/tagUtils/shouldLogin.js b/cypress/support/tagUtils/shouldLogin.js deleted file mode 100644 index d5307db163..0000000000 --- a/cypress/support/tagUtils/shouldLogin.js +++ /dev/null @@ -1,9 +0,0 @@ -export const shouldLogin = () => { - const { tags } = window.testState.currentScenario; - if (!tags || !tags.length) { - return true; - } - - return tags - .every(({ name }) => name !== '@skip-login'); -}; diff --git a/docs/user/resources/images/enrollment-dash-relationship-widget-add-choose.png b/docs/user/resources/images/enrollment-dash-relationship-widget-add-choose.png new file mode 100644 index 0000000000..59c052056b Binary files /dev/null and b/docs/user/resources/images/enrollment-dash-relationship-widget-add-choose.png differ diff --git a/docs/user/resources/images/enrollment-dash-relationship-widget-add-existing.png b/docs/user/resources/images/enrollment-dash-relationship-widget-add-existing.png new file mode 100644 index 0000000000..467adef42c Binary files /dev/null and b/docs/user/resources/images/enrollment-dash-relationship-widget-add-existing.png differ diff --git a/docs/user/resources/images/enrollment-dash-relationship-widget-add-new.png b/docs/user/resources/images/enrollment-dash-relationship-widget-add-new.png new file mode 100644 index 0000000000..6e64a2f58d Binary files /dev/null and b/docs/user/resources/images/enrollment-dash-relationship-widget-add-new.png differ diff --git a/docs/user/resources/images/enrollment-dash-relationship-widget-add.png b/docs/user/resources/images/enrollment-dash-relationship-widget-add.png new file mode 100644 index 0000000000..78c0e82f83 Binary files /dev/null and b/docs/user/resources/images/enrollment-dash-relationship-widget-add.png differ diff --git a/docs/user/resources/images/enrollment-dash-relationship-widget.png b/docs/user/resources/images/enrollment-dash-relationship-widget.png new file mode 100644 index 0000000000..baf3c48666 Binary files /dev/null and b/docs/user/resources/images/enrollment-dash-relationship-widget.png differ diff --git a/docs/user/using-the-capture-app.md b/docs/user/using-the-capture-app.md index b335b3e7ce..138568e172 100644 --- a/docs/user/using-the-capture-app.md +++ b/docs/user/using-the-capture-app.md @@ -986,6 +986,39 @@ The enrollment comment widget displays comments and allows addition of comments, By clicking in the text field, you will be able to enter new text and see action buttons **Save comment** and **Cancel**. Note that Enrollment comments are attributed to a user and cannot be deleted. +### Relationship widget + +The Relationships widget on the enrollment dashboard is used for viewing the record’s linked relationships to other records. +The number next to the title signifies the total number of relationships + +![](resources/images/enrollment-dash-relationship-widget.png) + +For tracked entity instance relationships, the key attributes shown in the widget are the attributes that have been selected to be displayed on the relationship type page in Maintenance. + +If no attributes are selected, it will just show a row per record with tracked entity type name and relationship creation date. + +When clicking a tracked entity instance you should be taken to the Enrollment Dashboard. If the relationship type includes a program, you should be taken to the latest enrollment for that program. If no program is specified, you should still be sent to the enrollment dashboard, but without a program. + +Click the **Add new** button to add a new relationship. Adding a new relationship opens a dialog where you can select the applicable relationship type. + +![](resources/images/enrollment-dash-relationship-widget-add.png) + +Choose between linking to an existing tracked entity instance or creating a new one. + +![](resources/images/enrollment-dash-relationship-widget-add-choose.png) + +#### Existing tracked entity instance + +Use the search form to find any existing record to link to. + +![](resources/images/enrollment-dash-relationship-widget-add-existing.png) + +#### New tracked entity instance + +Use the form to create a new record and link. + +![](resources/images/enrollment-dash-relationship-widget-add-new.png) + ### Tracked entity instance profile widget On the enrollment dashboard, you can view the tracked entity instance profile widget. Inside the profile widget you can view the key attributes values. @@ -1226,4 +1259,4 @@ The attribute option combo selector will be displayed when you are adding or cha Example from new Tracker event: -![](resources/images/attribute-option-combo-tracker.png) \ No newline at end of file +![](resources/images/attribute-option-combo-tracker.png) diff --git a/i18n/en.pot b/i18n/en.pot index 06e8de8d33..2396905e88 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2023-09-29T14:14:34.330Z\n" -"PO-Revision-Date: 2023-09-29T14:14:34.330Z\n" +"POT-Creation-Date: 2023-09-12T06:24:49.265Z\n" +"PO-Revision-Date: 2023-09-12T06:24:49.265Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -947,6 +947,18 @@ msgstr "Event could not be loaded" msgid "Organisation unit could not be loaded" msgstr "Organisation unit could not be loaded" +msgid "Selected program" +msgstr "Selected program" + +msgid "Search {{uniqueAttrName}}" +msgstr "Search {{uniqueAttrName}}" + +msgid "Search by attributes" +msgstr "Search by attributes" + +msgid "Could not retrieve metadata. Please try again later." +msgstr "Could not retrieve metadata. Please try again later." + msgid "Possible duplicates found" msgstr "Possible duplicates found" @@ -1010,9 +1022,6 @@ msgstr "Search {{name}}" msgid "Search by {{name}}" msgstr "Search by {{name}}" -msgid "Search by attributes" -msgstr "Search by attributes" - msgid "all programs" msgstr "all programs" @@ -1070,12 +1079,6 @@ msgstr "Missing search criteria" msgid "Results found" msgstr "Results found" -msgid "Selected program" -msgstr "Selected program" - -msgid "Search {{uniqueAttrName}}" -msgstr "Search {{uniqueAttrName}}" - msgid "Saved lists in this program" msgstr "Saved lists in this program" @@ -1359,6 +1362,42 @@ msgstr "{{ scheduledEvents }} scheduled" msgid "Stages and Events" msgstr "Stages and Events" +msgid "New TEI Relationship" +msgstr "New TEI Relationship" + +msgid "Missing implementation step" +msgstr "Missing implementation step" + +msgid "Go back without saving relationship" +msgstr "Go back without saving relationship" + +msgid "New Relationship" +msgstr "New Relationship" + +msgid "Link to an existing {{tetName}}" +msgstr "Link to an existing {{tetName}}" + +msgid "An error occurred while adding the relationship" +msgstr "An error occurred while adding the relationship" + +msgid "Something went wrong while loading relationships. Please try again later." +msgstr "Something went wrong while loading relationships. Please try again later." + +msgid "{{trackedEntityTypeName}} relationships" +msgstr "{{trackedEntityTypeName}} relationships" + +msgid "To open this relationship, please wait until saving is complete" +msgstr "To open this relationship, please wait until saving is complete" + +msgid "Type" +msgstr "Type" + +msgid "Created date" +msgstr "Created date" + +msgid "Program stage name" +msgstr "Program stage name" + msgid "Working list could not be loaded" msgstr "Working list could not be loaded" diff --git a/i18n/es.po b/i18n/es.po index b74921f8ef..cd154429ff 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -17,7 +17,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2023-09-04T07:07:59.195Z\n" +"POT-Creation-Date: 2023-09-29T14:14:34.330Z\n" "PO-Revision-Date: 2019-06-27 07:31+0000\n" "Last-Translator: Enzo Nicolas Rossi , 2023\n" "Language-Team: Spanish (https://app.transifex.com/hisp-uio/teams/100509/es/)\n" @@ -172,6 +172,15 @@ msgstr "Coordenada" msgid "Enrollment" msgstr "Inscripción" +msgid "Complete event" +msgstr "Completar el evento" + +msgid "{{ stageName }} - Basic info" +msgstr "" + +msgid "{{ stageName }} - Status" +msgstr "" + msgid "Please select {{categoryName}}" msgstr "Seleccione {{categoryName}}" @@ -192,15 +201,18 @@ msgstr "Cancelar" msgid "Metadata error. see log for details" msgstr "Error de metadatos. ver registro para más detalles" +msgid "{{ stageName }} - Details" +msgstr "" + +msgid "{{ stageName }} - {{ sectionName }}" +msgstr "" + msgid "Assigned user" msgstr "Usuario asignado" msgid "Search for user" msgstr "Buscar usuario" -msgid "Complete event" -msgstr "Completar el evento" - msgid "Basic info" msgstr "Información básica" @@ -438,13 +450,13 @@ msgid "Days in the future" msgstr "Días en el futuro" msgid "From" -msgstr "De" +msgstr "Desde" msgid "Days in the past" msgstr "Días en el pasado" msgid "To" -msgstr "Para" +msgstr "Hasta" msgid "This week" msgstr "Esta semana" @@ -791,7 +803,7 @@ msgid "There was an error loading the page" msgstr "Hubo un error al cargar la página" msgid "Choose a registering unit to start reporting" -msgstr "Elija una unidad de registro para comenzar a informar" +msgstr "Seleccione una unidad de registro para comenzar a informar" msgid "There are no feedbacks for this event" msgstr "No hay comentarios para este evento." @@ -866,7 +878,7 @@ msgid "View enrollment" msgstr "Ver inscripción" msgid "Create for" -msgstr "Crear para" +msgstr "Crear" msgid "" "You can also choose a program from the top bar and create in that program" diff --git a/i18n/nb.po b/i18n/nb.po index 036767eb33..59b9da637b 100644 --- a/i18n/nb.po +++ b/i18n/nb.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2023-09-04T07:07:59.195Z\n" +"POT-Creation-Date: 2023-09-29T14:14:34.330Z\n" "PO-Revision-Date: 2019-06-27 07:31+0000\n" "Last-Translator: Karoline Tufte Lien , 2023\n" "Language-Team: Norwegian Bokmål (https://app.transifex.com/hisp-uio/teams/100509/nb/)\n" @@ -82,6 +82,7 @@ msgstr "Feil" msgid "" "Plugins are not yet available - Please contact your system administrator" msgstr "" +"Plugins er ikke tilgjengelig ennå - Ta kontakt med systemadministratoren din" msgid "This value is validating" msgstr "Denne verdien validerer" @@ -158,8 +159,17 @@ msgstr "Koordinater" msgid "Enrollment" msgstr "Registrering" +msgid "Complete event" +msgstr "Fullfør hendelse" + +msgid "{{ stageName }} - Basic info" +msgstr "{{ stageName }} - Grunnleggende informasjon" + +msgid "{{ stageName }} - Status" +msgstr "{{ stageName }} - Status" + msgid "Please select {{categoryName}}" -msgstr "" +msgstr "Velg {{categoryName}}" msgid "A future date is not allowed" msgstr "En fremtidig dato er ikke tillatt" @@ -176,15 +186,18 @@ msgstr "Avbryt" msgid "Metadata error. see log for details" msgstr "Metadata feil, se logg for detaljer" +msgid "{{ stageName }} - Details" +msgstr "{{ stageName }} - Detaljer" + +msgid "{{ stageName }} - {{ sectionName }}" +msgstr "{{ stageName }} - {{ sectionName }}" + msgid "Assigned user" msgstr "Tildelt programbruker" msgid "Search for user" msgstr "Søk etter bruker" -msgid "Complete event" -msgstr "Fullfør hendelse" - msgid "Basic info" msgstr "Grunnleggende informasjon" @@ -281,7 +294,7 @@ msgstr "" "relasjon" msgid "Yes, discard changes" -msgstr "" +msgstr "Ja, forkast endringer" msgid "No, cancel" msgstr "Nei, avbryt" @@ -385,6 +398,9 @@ 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 "" +"Denne hendelsen har ulagrede endringer. Hvis du forlater denne siden uten å " +"lagre, mister du disse endringene. Er du sikker på at du vil forkaste " +"ulagrede endringer?" msgid "No events to display" msgstr "Ingen hendelser å vise" @@ -498,7 +514,7 @@ msgid "Type to filter options" msgstr "Skriv for å filtrere alternativer" msgid "No match found" -msgstr "" +msgstr "Ingen treff funnet" msgid "Search" msgstr "Søk" @@ -597,13 +613,13 @@ msgid "Write comment" msgstr "Skriv kommentar" msgid "was blanked out and hidden by your last action" -msgstr "" +msgstr "ble tømt og skjult av den siste handlingen din" msgid "Notice" -msgstr "" +msgstr "Merknad" msgid "Close the notice" -msgstr "" +msgstr "Lukk merknaden" msgid "Use new Enrollment dashboard for {{programName}}" msgstr "Bruk nytt registreringsdashbord for {{programName}}" @@ -619,18 +635,30 @@ msgid "" "functionality in Capture is ongoing and will be added in upcoming app " "releases." msgstr "" +"Ved å klikke på \"meld på\" nedenfor, vil du begynne å bruke det nye " +"registreringsdashbordet i Capture-appen for dette Tracker-programmet. For " +"øyeblikket er det noe funksjonalitet fra Tracker Capture som ennå ikke er " +"lagt til, inkludert relasjons- og henvisningsfunksjonalitet. Arbeidet med å " +"inkludere denne Tracker-funksjonaliteten i Capture pågår og vil bli lagt til" +" i kommende apputgivelser." msgid "" "The core team appreciates any feedback on this new functionality which is " "currently being beta tested, please report any issues and feedback in the " "DHIS2 JIRA project." msgstr "" +"Kjerneteamet setter pris på all tilbakemelding på denne nye funksjonaliteten" +" som for øyeblikket blir betatestet, vennligst rapporter eventuelle " +"problemer og tilbakemeldinger i DHIS2 JIRA-prosjektet." msgid "" "Click the button below to opt-in to the new enrollment dashboard " "functionality in the Capture app (beta) for this Tracker program for all " "users." msgstr "" +"Klikk på knappen nedenfor for å melde deg på den nye funksjonen for " +"registreringsdashbord i Capture-appen (beta) for dette Tracker-programmet " +"for alle brukere." msgid "Yes, opt in" msgstr "Ja, meld på" @@ -653,6 +681,8 @@ msgstr "" msgid "" "An error occurred while fetching enrollments. Please enter a valid url." msgstr "" +"Det oppsto en feil under henting av registreringer. Vennligst skriv inn en " +"gyldig url." msgid "Enrollment Dashboard" msgstr "Registreringsdashbord" @@ -694,19 +724,19 @@ msgstr "" "{{programName}} har kategorier. Velg alle kategorier for å se dashbordet." msgid "Invalid enrollment id {{enrollmentId}}." -msgstr "" +msgstr "Ugyldig registrerings-ID {{enrollmentId}}" msgid "Choose an enrollment to view the dashboard." msgstr "Velg en registrering for å se dashbordet." msgid "There are no active enrollments." -msgstr "" +msgstr "Det er ingen aktive registreringer." msgid "Add new enrollment for {{teiDisplayName}} in this program." -msgstr "" +msgstr "Legg til ny registrering for {{teiDisplayName}} i dette programmet." msgid "No access to program owner." -msgstr "" +msgstr "Ingen tilgang til programeier." msgid "{{teiDisplayName}} is not enrolled in this program." msgstr "{{teiDisplayName}} er ikke registrert i dette programmet." @@ -774,7 +804,7 @@ msgid "Refer" msgstr "Henvis" msgid "You can't add any more {{ programStageName }} events" -msgstr "" +msgstr "Du kan ikke legge til flere {{ programStageName }}-hendelser" msgid "Cancel without saving" msgstr "Avbryt uten å lagre" @@ -839,7 +869,7 @@ msgid "New Enrollment in program{{escape}} {{programName}}" msgstr "Ny registrering i programmet{{escape}} {{programName}}" msgid "Save {{trackedEntityTypeName}}" -msgstr "" +msgstr "Lagre {{trackedEntityTypeName}}" msgid "Save {{trackedEntityName}}" msgstr "Lagre {{trackedEntityName}}" @@ -957,13 +987,13 @@ msgid "Add relationship" msgstr "Legg til relasjon" msgid "No results found for " -msgstr "" +msgstr "Ingen resultater funnet for" msgid "Registering unit" msgstr "Registreringsenhet" msgid "Choose a registering unit" -msgstr "" +msgstr "Velg en registreringsenhet" msgid "Clear selection" msgstr "Fjern valg" @@ -972,13 +1002,13 @@ msgid "No programs available." msgstr "Ingen programmer tilgjengelig." msgid "Search for a program" -msgstr "" +msgstr "Søk for et program" msgid "Some programs are being filtered by the chosen registering unit" -msgstr "" +msgstr "Noen programmer blir filtrert av den valgte registreringsenheten" msgid "Show all programs" -msgstr "" +msgstr "Vis alle programmer" msgid "Choose a program" msgstr "Velg et program" @@ -1055,7 +1085,7 @@ msgid "Cannot search in all programs" msgstr "Kan ikke søke i alle programmer" msgid "Missing search criteria" -msgstr "" +msgstr "Manglende søkekriterier" msgid "Results found" msgstr "Resultater funnet" @@ -1096,23 +1126,27 @@ msgid "open the Tracker Capture app" msgstr "åpne Tracker Capture app" msgid "This program is protected" -msgstr "" +msgstr "Dette programmet er beskyttet" msgid "Reason to check for enrollments" -msgstr "" +msgstr "Grunn til å sjekke for registreringer" msgid "" "Describe the reason you are checking for enrollments in this protected " "program" msgstr "" +"Beskriv årsaken til at du ser etter registreringer i dette beskyttede " +"programmet" msgid "Check for enrollments" -msgstr "" +msgstr "Se etter registreringer" msgid "" "You must provide a reason to check for enrollments in this protected " "program. All activity will be logged." msgstr "" +"Du må oppgi en grunn for å se etter registreringer i dette beskyttede " +"programmet. All aktivitet vil bli logget." msgid "Save comment" msgstr "Lagre kommentar" @@ -1159,6 +1193,8 @@ msgstr "Merk for oppfølging" msgid "Existing dates for auto-generated events will not be updated." msgstr "" +"Eksisterende datoer for automatisk genererte hendelser vil ikke bli " +"oppdatert." msgid "Enrollment date" msgstr "Registreringsdato" @@ -1322,7 +1358,7 @@ msgid "New {{ eventName }} event" msgstr "Ny {{ eventName }} hendelse" msgid "To open this event, please wait until saving is complete" -msgstr "" +msgstr "For å åpne denne hendelsen, vennligst vent til lagringen er fullført" msgid "Show {{ rest }} more" msgstr "Vis {{ rest }} flere" @@ -1388,19 +1424,19 @@ msgid "Choose a program stage to filter by {{label}}" msgstr "Velg en programfase å filtrere etter {{label}}" msgid "Active enrollments" -msgstr "" +msgstr "Aktive registreringer" msgid "Completed enrollments" -msgstr "" +msgstr "Fullførte registreringer" msgid "Cancelled enrollments" -msgstr "" +msgstr "Avbrutte registreringer" msgid "Working list could not be updated" msgstr "Arbeidslisten kunne ikke oppdateres" msgid "an error occurred loading the working lists" -msgstr "" +msgstr "det oppstod en feil ved lasting av arbeidslistene" msgid "an error occurred loading Tracked entity instance lists" msgstr "det oppstod en feil ved lasting av lister over sporede enheter" @@ -1535,7 +1571,7 @@ msgid "Set area" msgstr "Sett område" msgid "Area on map saved" -msgstr "" +msgstr "Området på kartet er lagret" msgid "Compatibility mode" msgstr "Kompabilitetsmodus" diff --git a/package.json b/package.json index 64230da5bf..99bb762f2f 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "capture-app", "homepage": ".", - "version": "100.41.2", - "cacheVersion": "6", + "version": "100.42.0", + "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", "private": true, @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.41.2", + "@dhis2/rules-engine-javascript": "100.42.0", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", @@ -93,17 +93,19 @@ "@babel/plugin-transform-flow-strip-types": "^7.16.7", "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.16.7", + "@badeball/cypress-cucumber-preprocessor": "17.2.1", + "@cypress/webpack-preprocessor": "^6.0.0", "@dhis2/cli-app-scripts": "^9.0.1", "@dhis2/cli-helpers-engine": "^3.2.1", "@dhis2/cli-style": "^10.4.1", "@dhis2/cli-utils-cypress": "^9.0.2", - "@dhis2/cypress-commands": "^8.0.6", - "@dhis2/cypress-plugins": "^9.0.2", + "@dhis2/cypress-commands": "^10.0.3", + "@dhis2/cypress-plugins": "^10.0.3", "@typescript-eslint/eslint-plugin": "^4.33.0", "@vercel/ncc": "^0.34.0", + "babel-plugin-module-resolver": "^5.0.0", "concurrently": "^7.0.0", - "cypress": "^7", - "cypress-cucumber-preprocessor": "^4", + "cypress": "12", "docdash": "^1.2.0", "dotenv": "^16.0.3", "enzyme": "^3.11.0", diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index 88cc2da714..96f533f035 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.41.2", + "version": "100.42.0", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { diff --git a/scripts/startAppForCypress.js b/scripts/startAppForCypress.js index 819aebef8d..3b0607f109 100644 --- a/scripts/startAppForCypress.js +++ b/scripts/startAppForCypress.js @@ -45,6 +45,8 @@ const env = Object acc.REACT_APP_DHIS2_BASE_URL = allEnvVariables[key]; } else if (key.toUpperCase() === 'CYPRESS_DHIS2APIVERSION') { acc.REACT_APP_DHIS2_API_VERSION = allEnvVariables[key]; + } else if (key.toUpperCase() === 'NODE_OPTIONS') { + acc[key] = allEnvVariables[key]; } return acc; }, { BROWSER: 'none' }); diff --git a/src/components/App/AppPages.component.js b/src/components/App/AppPages.component.js index 2203baf361..0af49adb99 100644 --- a/src/components/App/AppPages.component.js +++ b/src/components/App/AppPages.component.js @@ -9,17 +9,21 @@ import { EnrollmentPage } from 'capture-core/components/Pages/Enrollment'; import { StageEventListPage } from 'capture-core/components/Pages/StageEvent'; import { EnrollmentEditEventPage } from 'capture-core/components/Pages/EnrollmentEditEvent'; import { EnrollmentAddEventPage } from 'capture-core/components/Pages/EnrollmentAddEvent'; +import { ReactQueryDevtools } from 'react-query/devtools'; export const AppPages = () => ( - - - - - - - - - - - + <> + + + + + + + + + + + + + ); diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js index 55106542d8..2da9aba5b1 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js @@ -5,29 +5,38 @@ import { useSelector } from 'react-redux'; import { EnrollmentRegistrationEntryComponent } from './EnrollmentRegistrationEntry.component'; import type { OwnProps } from './EnrollmentRegistrationEntry.types'; import { useLifecycle } from './hooks'; -import { useCurrentOrgUnitId } from '../../../hooks/useCurrentOrgUnitId'; import { useCoreOrgUnit } from '../../../metadataRetrieval/coreOrgUnit'; import { dataEntryHasChanges } from '../../DataEntry/common/dataEntryHasChanges'; +import { + useBuildEnrollmentPayload, +} from './hooks/useBuildEnrollmentPayload'; export const EnrollmentRegistrationEntry: ComponentType = ({ selectedScopeId, id, saveButtonText, trackedEntityInstanceAttributes, + orgUnitId, + teiId, onSave, ...passOnProps }) => { - const orgUnitId = useCurrentOrgUnitId(); const { orgUnit, error } = useCoreOrgUnit(orgUnitId); const { - teiId, ready, skipDuplicateCheck, firstStageMetaData, formId, enrollmentMetadata, formFoundation, - } = useLifecycle(selectedScopeId, id, trackedEntityInstanceAttributes, orgUnit); + } = useLifecycle(selectedScopeId, id, trackedEntityInstanceAttributes, orgUnit, teiId, selectedScopeId); + const { buildTeiWithEnrollment } = useBuildEnrollmentPayload({ + programId: selectedScopeId, + dataEntryId: id, + orgUnitId, + teiId, + trackedEntityTypeId: enrollmentMetadata?.trackedEntityType?.id, + }); const isUserInteractionInProgress: boolean = useSelector( state => @@ -41,10 +50,16 @@ export const EnrollmentRegistrationEntry: ComponentType = ({ const isSavingInProgress = useSelector(({ possibleDuplicates, newPage }) => possibleDuplicates.isLoading || possibleDuplicates.isUpdating || !!newPage.uid); + if (error) { return error.errorComponent; } + const onSaveWithEnrollment = () => { + const teiWithEnrollment = buildTeiWithEnrollment(); + onSave(teiWithEnrollment); + }; + return ( = ({ orgUnit={orgUnit} isUserInteractionInProgress={isUserInteractionInProgress} isSavingInProgress={isSavingInProgress} - onSave={() => onSave(formFoundation, firstStageMetaData)} + onSave={onSaveWithEnrollment} /> ); }; diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js index f1924c00b3..d58a42aff2 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js @@ -8,8 +8,32 @@ import type { ExistingUniqueValueDialogActionsComponent } from '../withErrorMess import type { InputAttribute } from './hooks/useFormValues'; import { RenderFoundation, ProgramStage } from '../../../metaData'; +export type EnrollmentPayload = {| + trackedEntity: string, + trackedEntityType: string, + orgUnit: string, + geometry: any, + enrollments: [ + {| + occurredAt: string, + orgUnit: string, + program: string, + status: string, + enrolledAt: string, + events: Array<{ + orgUnit: string, + }>, + attributes: Array<{ + attribute: string, + value: any, + }>, + |} + ] +|} + export type OwnProps = $ReadOnly<{| id: string, + orgUnitId: string, selectedScopeId: string, fieldOptions?: Object, onSave: SaveForDuplicateCheck, diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildEnrollmentPayload.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildEnrollmentPayload.js new file mode 100644 index 0000000000..95778162b0 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildEnrollmentPayload.js @@ -0,0 +1,188 @@ +// @flow +import { useSelector } from 'react-redux'; +import { getDataEntryKey } from '../../../DataEntry/common/getDataEntryKey'; +import { + getTrackerProgramThrowIfNotFound, + Section, +} from '../../../../metaData'; +import type { RenderFoundation } from '../../../../metaData'; +import { convertClientToServer, convertFormToClient } from '../../../../converters'; +import { + convertDataEntryValuesToClientValues, +} from '../../../DataEntry/common/convertDataEntryValuesToClientValues'; +import { capitalizeFirstLetter } from '../../../../../capture-core-utils/string'; +import { generateUID } from '../../../../utils/uid/generateUID'; +import { + useBuildFirstStageRegistration, +} from './useBuildFirstStageRegistration'; +import { + useMetadataForRegistrationForm, +} from '../../common/TEIAndEnrollment/useMetadataForRegistrationForm'; +import { + useMergeFormFoundationsIfApplicable, +} from './useMergeFormFoundationsIfApplicable'; +import { + deriveAutoGenerateEvents, + deriveFirstStageDuringRegistrationEvent, +} from '../../../Pages/New/RegistrationDataEntry/helpers'; +import { FEATURETYPE } from '../../../../constants'; +import type { EnrollmentPayload } from '../EnrollmentRegistrationEntry.types'; + +type DataEntryReduxConverterProps = { + programId: string; + dataEntryId: string; + itemId?: string; + orgUnitId: string; + teiId: ?string; + trackedEntityTypeId: string; +}; + +function getClientValuesForFormData(formValues: Object, formFoundation: RenderFoundation) { + const clientValues = formFoundation.convertValues(formValues, convertFormToClient); + return clientValues; +} + +function getServerValuesForMainValues( + values: Object, + meta: Object, + formFoundation: RenderFoundation, +) { + const clientValues = convertDataEntryValuesToClientValues( + values, + meta, + formFoundation, + ) || {}; + + // potientally run this through a server to client converter for enrollment, the same way as for event + const serverValues = Object + .keys(clientValues) + .reduce((acc, key) => { + const value = clientValues[key]; + const type = meta[key].type; + acc[key] = convertClientToServer(value, type); + return acc; + }, {}); + + return serverValues; +} + +function getPossibleTetFeatureTypeKey(serverValues: Object) { + return Object + .keys(serverValues) + .find(key => key.startsWith('FEATURETYPE_')); +} + +function buildGeometryProp(key: string, serverValues: Object) { + if (!serverValues[key]) { + return undefined; + } + const type = capitalizeFirstLetter(key.replace('FEATURETYPE_', '').toLocaleLowerCase()); + return { + type, + coordinates: serverValues[key], + }; +} + +const geometryType = formValuesKey => Object.values(FEATURETYPE).find(geometryKey => geometryKey === formValuesKey); + +const deriveAttributesFromFormValues = (formValues = {}) => + Object.keys(formValues) + .filter(key => !geometryType(key)) + .map<{ attribute: string, value: ?any }>(key => ({ attribute: key, value: formValues[key] })); + +export const useBuildEnrollmentPayload = ({ + programId, + dataEntryId, + itemId = 'newEnrollment', + orgUnitId, + teiId, + trackedEntityTypeId, +}: DataEntryReduxConverterProps) => { + const dataEntryKey = getDataEntryKey(dataEntryId, itemId); + const formValues = useSelector(({ formsValues }) => formsValues[dataEntryKey]); + const dataEntryFieldValues = useSelector(({ dataEntriesFieldsValue }) => dataEntriesFieldsValue[dataEntryKey]); + const dataEntryFieldsMeta = useSelector(({ dataEntriesFieldsMeta }) => dataEntriesFieldsMeta[dataEntryKey]); + const { formFoundation: scopeFormFoundation } = useMetadataForRegistrationForm({ selectedScopeId: programId }); + const { firstStageMetaData } = useBuildFirstStageRegistration(programId); + const { formFoundation } = useMergeFormFoundationsIfApplicable(scopeFormFoundation, firstStageMetaData); + + const buildTeiWithEnrollment = (): EnrollmentPayload => { + if (!formFoundation) throw Error('form foundation object not found'); + const firstStage = firstStageMetaData && firstStageMetaData.stage; + const clientValues = getClientValuesForFormData(formValues, formFoundation); + const serverValuesForFormValues = formFoundation.convertAndGroupBySection(clientValues, convertClientToServer); + const serverValuesForMainValues = getServerValuesForMainValues( + dataEntryFieldValues, + dataEntryFieldsMeta, + formFoundation, + ); + const { enrolledAt, occurredAt } = serverValuesForMainValues; + + const { stages } = getTrackerProgramThrowIfNotFound(programId); + + const attributeCategoryOptionsId = 'attributeCategoryOptions'; + const attributeCategoryOptions = Object.keys(serverValuesForMainValues) + .filter(key => key.startsWith(attributeCategoryOptionsId)) + .reduce((acc, key) => { + const categoryId = key.split('-')[1]; + acc[categoryId] = serverValuesForMainValues[key]; + return acc; + }, {}); + + const formServerValues = serverValuesForFormValues[Section.groups.ENROLLMENT]; + const currentEventValues = serverValuesForFormValues[Section.groups.EVENT]; + + + const firstStageDuringRegistrationEvent = deriveFirstStageDuringRegistrationEvent({ + firstStageMetadata: firstStage, + programId, + orgUnitId, + currentEventValues, + fieldsValue: dataEntryFieldValues, + attributeCategoryOptions, + }); + + const autoGenerateEvents = deriveAutoGenerateEvents({ + firstStageMetadata: firstStage, + stages, + enrolledAt, + occurredAt, + programId, + orgUnitId, + attributeCategoryOptions, + }); + + const allEventsToBeCreated = firstStageDuringRegistrationEvent + ? [firstStageDuringRegistrationEvent, ...autoGenerateEvents] + : autoGenerateEvents; + + const enrollment = { + program: programId, + status: 'ACTIVE', + orgUnit: orgUnitId, + occurredAt, + enrolledAt, + attributes: deriveAttributesFromFormValues(formServerValues), + events: allEventsToBeCreated, + }; + + const tetFeatureTypeKey = getPossibleTetFeatureTypeKey(serverValuesForFormValues); + let geometry; + if (tetFeatureTypeKey) { + geometry = buildGeometryProp(tetFeatureTypeKey, serverValuesForFormValues); + delete serverValuesForFormValues[tetFeatureTypeKey]; + } + + return { + trackedEntity: teiId || generateUID(), + orgUnit: orgUnitId, + trackedEntityType: trackedEntityTypeId, + geometry, + enrollments: [enrollment], + }; + }; + + return { + buildTeiWithEnrollment, + }; +}; diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js index 0c4a280bf6..4a84d7ccb7 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js @@ -4,7 +4,6 @@ import { useEffect, useRef } from 'react'; import type { OrgUnit } from '@dhis2/rules-engine-javascript'; import { startNewEnrollmentDataEntryInitialisation } from '../EnrollmentRegistrationEntry.actions'; import { scopeTypes, getProgramThrowIfNotFound } from '../../../../metaData'; -import { useLocationQuery } from '../../../../utils/routing'; import { useScopeInfo } from '../../../../hooks/useScopeInfo'; import { useFormValues } from './index'; import type { InputAttribute } from './useFormValues'; @@ -18,8 +17,9 @@ export const useLifecycle = ( dataEntryId: string, trackedEntityInstanceAttributes?: Array, orgUnit: ?OrgUnit, + teiId: ?string, + programId: string, ) => { - const { teiId, programId } = useLocationQuery(); const dataEntryReadyRef = useRef(false); const dispatch = useDispatch(); const program = programId && getProgramThrowIfNotFound(programId); diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js index a77292e3af..4c3b8badab 100644 --- a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js @@ -3,7 +3,6 @@ import { useDispatch, useSelector } from 'react-redux'; import React, { useEffect, useMemo } from 'react'; import type { ComponentType } from 'react'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; -import { useCurrentOrgUnitId } from '../../../hooks/useCurrentOrgUnitId'; import { Enrollment, scopeTypes } from '../../../metaData'; import { startNewTeiDataEntryInitialisation } from './TeiRegistrationEntry.actions'; import type { OwnProps } from './TeiRegistrationEntry.types'; @@ -11,11 +10,11 @@ import { TeiRegistrationEntryComponent } from './TeiRegistrationEntry.component' import { useFormValuesFromSearchTerms } from './hooks/useFormValuesFromSearchTerms'; import { dataEntryHasChanges } from '../../DataEntry/common/dataEntryHasChanges'; import { useMetadataForRegistrationForm } from '../common/TEIAndEnrollment/useMetadataForRegistrationForm'; +import { useBuildTeiPayload } from './hooks/useBuildTeiPayload'; -const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId) => { +const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId, orgUnitId) => { const dispatch = useDispatch(); const { scopeType, trackedEntityName } = useScopeInfo(selectedScopeId); - const selectedOrgUnitId = useCurrentOrgUnitId(); const { formId, formFoundation } = useMetadataForRegistrationForm({ selectedScopeId }); const formValues = useFormValuesFromSearchTerms(); const registrationFormReady = !!formId; @@ -24,18 +23,18 @@ const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId) => { if (registrationFormReady && scopeType === scopeTypes.TRACKED_ENTITY_TYPE) { dispatch( startNewTeiDataEntryInitialisation( - { selectedOrgUnitId, selectedScopeId, dataEntryId, formFoundation, formValues }, + { selectedOrgUnitId: orgUnitId, selectedScopeId, dataEntryId, formFoundation, formValues }, )); } }, [ scopeType, dataEntryId, selectedScopeId, - selectedOrgUnitId, registrationFormReady, formFoundation, formValues, dispatch, + orgUnitId, ]); return { @@ -44,13 +43,18 @@ const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId) => { }; -export const TeiRegistrationEntry: ComponentType = ({ selectedScopeId, id, ...rest }) => { - const { trackedEntityName } = useInitialiseTeiRegistration(selectedScopeId, id); +export const TeiRegistrationEntry: ComponentType = ({ selectedScopeId, id, orgUnitId, onSave, ...rest }) => { + const { trackedEntityName } = useInitialiseTeiRegistration(selectedScopeId, id, orgUnitId); const ready = useSelector(({ dataEntries }) => (!!dataEntries[id])); const dataEntry = useSelector(({ dataEntries }) => (dataEntries[id])); const { registrationMetaData: teiRegistrationMetadata, } = useMetadataForRegistrationForm({ selectedScopeId }); + const { buildTeiWithoutEnrollment } = useBuildTeiPayload({ + trackedEntityTypeId: selectedScopeId, + dataEntryId: id, + orgUnitId, + }); const dataEntryKey = useMemo(() => { if (dataEntry) { @@ -68,14 +72,21 @@ export const TeiRegistrationEntry: ComponentType = ({ selectedScopeId, return null; } + const onSaveWithoutEnrollment = () => { + const teiPayload = buildTeiWithoutEnrollment(); + onSave(teiPayload); + }; + return ( ); diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js index d73d49c473..43afd9adc8 100644 --- a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js @@ -2,23 +2,27 @@ import type { Node } from 'react'; import type { RegistrationFormMetadata } from '../common/TEIAndEnrollment/useMetadataForRegistrationForm/types'; import type { RenderCustomCardActions } from '../../CardList'; -import type { SaveForDuplicateCheck } from '../common/TEIAndEnrollment/DuplicateCheckOnSave'; import type { ExistingUniqueValueDialogActionsComponent } from '../withErrorMessagePostProcessor'; +import type { + TeiPayload, +} from '../../Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; export type OwnProps = $ReadOnly<{| id: string, + orgUnitId: string, selectedScopeId: string, saveButtonText: string, fieldOptions?: Object, - onSave: SaveForDuplicateCheck, + onSave: (TeiPayload) => void, duplicatesReviewPageSize: number, isSavingInProgress?: boolean, renderDuplicatesCardActions?: RenderCustomCardActions, - renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForDuplicateCheck) => Node, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: (TeiPayload) => void) => Node, ExistingUniqueValueDialogActions: ExistingUniqueValueDialogActionsComponent, |}>; type ContainerProps = {| + orgUnitId: string, teiRegistrationMetadata: RegistrationFormMetadata, ready: boolean, trackedEntityName: string, @@ -37,9 +41,9 @@ type PropsAddedInHOC = {| |}; type PropsRemovedInHOC = {| renderDuplicatesCardActions?: RenderCustomCardActions, - renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForDuplicateCheck) => Node, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: (TeiPayload) => void) => Node, duplicatesReviewPageSize: number, - onSave: SaveForDuplicateCheck, + onSave: (TeiPayload) => void, |}; export type PlainProps = {| diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/hooks/useBuildTeiPayload.js b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/hooks/useBuildTeiPayload.js new file mode 100644 index 0000000000..0c4cd6da0d --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/hooks/useBuildTeiPayload.js @@ -0,0 +1,81 @@ +// @flow +import { useSelector } from 'react-redux'; +import { useMetadataForRegistrationForm } from '../../common/TEIAndEnrollment/useMetadataForRegistrationForm'; +import type { RenderFoundation } from '../../../../metaData'; +import { convertClientToServer, convertFormToClient } from '../../../../converters'; +import { capitalizeFirstLetter } from '../../../../../capture-core-utils/string'; +import { generateUID } from '../../../../utils/uid/generateUID'; +import { getDataEntryKey } from '../../../DataEntry/common/getDataEntryKey'; +import type { + TeiPayload, +} from '../../../Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; + +type Props = { + trackedEntityTypeId: string, + dataEntryId: string, + orgUnitId: string, + itemId?: string, +}; + +function getClientValuesForFormData(formValues: Object, formFoundation: RenderFoundation) { + return formFoundation.convertValues(formValues, convertFormToClient); +} + +function getPossibleTetFeatureTypeKey(serverValues: Object) { + return Object + .keys(serverValues) + .find(key => key.startsWith('FEATURETYPE_')); +} + +function buildGeometryProp(key: string, serverValues: Object) { + if (!serverValues[key]) { + return undefined; + } + const type = capitalizeFirstLetter(key.replace('FEATURETYPE_', '').toLocaleLowerCase()); + return { + type, + coordinates: serverValues[key], + }; +} + +export const useBuildTeiPayload = ({ + trackedEntityTypeId, + dataEntryId, + itemId = 'newTei', + orgUnitId, +}: Props) => { + const dataEntryKey = getDataEntryKey(dataEntryId, itemId); + const { formFoundation } = useMetadataForRegistrationForm({ selectedScopeId: trackedEntityTypeId }); + const formValues = useSelector(({ formsValues }) => formsValues[dataEntryKey]); + + const buildTeiWithoutEnrollment = (): TeiPayload => { + if (!formFoundation) throw Error('form foundation object not found'); + const clientValues = getClientValuesForFormData(formValues, formFoundation); + const serverValuesForFormValues = formFoundation.convertValues(clientValues, convertClientToServer); + + // $FlowFixMe + const attributes = Object.keys(serverValuesForFormValues) + .map(key => ({ + attribute: key, + value: serverValuesForFormValues[key], + })); + + const tetFeatureTypeKey = getPossibleTetFeatureTypeKey(serverValuesForFormValues); + let geometry; + if (tetFeatureTypeKey) { + geometry = buildGeometryProp(tetFeatureTypeKey, serverValuesForFormValues); + delete serverValuesForFormValues[tetFeatureTypeKey]; + } + + return { + attributes, + trackedEntity: generateUID(), + orgUnit: orgUnitId, + trackedEntityType: trackedEntityTypeId, + geometry, + enrollments: [], + }; + }; + + return { buildTeiWithoutEnrollment }; +}; diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/types/duplicateCheckOnSave.types.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/types/duplicateCheckOnSave.types.js index a727db9ec2..f55dc39c0e 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/types/duplicateCheckOnSave.types.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/types/duplicateCheckOnSave.types.js @@ -1,7 +1,7 @@ // @flow -import { ProgramStage, RenderFoundation } from '../../../../../../metaData'; +import type { EnrollmentPayload } from '../../../../EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types'; +import type { TeiPayload } from '../../../../../Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; export type SaveForDuplicateCheck = ( - formFoundation?: RenderFoundation, - firstStageMetaData?: { stage: ProgramStage }, + teiWithEnrollment: EnrollmentPayload | TeiPayload, ) => void; diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/useDuplicateCheckerOnSave.types.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/useDuplicateCheckerOnSave.types.js index 98525d23d5..d74476bfee 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/useDuplicateCheckerOnSave.types.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/useDuplicateCheckerOnSave.types.js @@ -1,9 +1,8 @@ // @flow import { type InputSearchGroup } from '../../../../../metaData'; -import type { SaveForDuplicateCheck } from './types'; export type Input = {| - onSave: SaveForDuplicateCheck, + onSave: () => void, hasDuplicate: ?boolean, onResetPossibleDuplicates: () => void, onReviewDuplicates: (duplicatesReviewPageSize: number) => void, diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/withDuplicateCheckOnSave.types.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/withDuplicateCheckOnSave.types.js index 87a3d4cffc..560dd0e9a2 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/withDuplicateCheckOnSave.types.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/withDuplicateCheckOnSave.types.js @@ -2,16 +2,15 @@ import type { Node } from 'react'; import type { Enrollment, TeiRegistration } from '../../../../../metaData'; import type { RenderCustomCardActions } from '../../../../CardList'; -import type { SaveForDuplicateCheck } from './types'; export type Props = { id: string, selectedScopeId: string, - onSave: SaveForDuplicateCheck, + onSave: () => void, enrollmentMetadata?: Enrollment, teiRegistrationMetadata?: TeiRegistration, duplicatesReviewPageSize: number, renderDuplicatesCardActions?: RenderCustomCardActions, - renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForDuplicateCheck) => Node, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: () => void) => Node, skipDuplicateCheck: ?boolean, }; diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js index 49a7b484be..0238087811 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js @@ -30,6 +30,7 @@ export const useEnrollmentFormFoundation = ({ locale, }: Props) => { const { data: enrollment, isLoading, error } = useIndexedDBQuery( + // $FlowFixMe - QueryKey can be undefined ['enrollmentForm', program?.id], () => buildEnrollmentForm({ // $FlowFixMe - Flow does not understand that the values are not null here diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js index ae3d4c3e5a..05a173cfc8 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js @@ -26,6 +26,7 @@ export const useTrackedEntityTypeCollection = ({ locale, }: Props): ReturnValues => { const { data: trackedEntityAttributes } = useIndexedDBQuery( + // $FlowFixMe - QueryKey can be undefined ['trackedEntityAttributes', trackedEntityType?.id], () => getTrackedEntityAttributes( trackedEntityType @@ -40,6 +41,7 @@ export const useTrackedEntityTypeCollection = ({ ); const { data: trackedEntityTypeCollection } = useIndexedDBQuery( + // $FlowFixMe - QueryKey can be undefined ['trackedEntityTypeCollection', trackedEntityType?.id], () => buildTrackedEntityTypeCollection({ // $FlowFixMe diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js index 99857025f9..6d905e0188 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js @@ -207,3 +207,4 @@ export const openEnrollmentPageEpic = (action$: InputObservable, store: ReduxSto }, ), ); + 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 0759fcd558..05be58a16e 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 @@ -1,5 +1,5 @@ // @flow -import React, { type ComponentType } from 'react'; +import React, { type ComponentType, useState, useCallback } from 'react'; import withStyles from '@material-ui/core/styles/withStyles'; import { spacersNum, spacers, colors } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; @@ -13,8 +13,15 @@ import { WidgetError } from '../../../WidgetErrorAndWarning/WidgetError'; import { WidgetIndicator } from '../../../WidgetIndicator'; import { WidgetEnrollmentComment } from '../../../WidgetEnrollmentComment'; import { EnrollmentQuickActions } from './EnrollmentQuickActions'; +import { + TrackedEntityRelationshipsWrapper, +} from '../../common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper'; +import { AddRelationshipRefWrapper } from '../../EnrollmentEditEvent/AddRelationshipRefWrapper'; const getStyles = () => ({ + container: { + position: 'relative', + }, columns: { display: 'flex', }, @@ -59,68 +66,96 @@ export const EnrollmentPageDefaultPlain = ({ hideWidgets, classes, onEventClick, + onLinkedRecordClick, onUpdateTeiAttributeValues, onUpdateEnrollmentDate, onUpdateIncidentDate, onEnrollmentError, ruleEffects, -}: PlainProps) => ( - <> -
{i18n.t('Enrollment Dashboard')}
-
-
- - -
-
- - - - {!hideWidgets.indicator && ( - - )} - {!hideWidgets.feedback && ( - - )} - - {enrollmentId !== 'AUTO' && } +}: PlainProps) => { + const [mainContentVisible, setMainContentVisibility] = useState(true); + const [addRelationShipContainerElement, setAddRelationshipContainerElement] = + useState(undefined); + + const toggleVisibility = useCallback(() => setMainContentVisibility(current => !current), []); + + return ( + <> + +
+
{i18n.t('Enrollment Dashboard')}
+
+
+ + +
+
+ {addRelationShipContainerElement && + {}} + onOpenAddRelationship={toggleVisibility} + onCloseAddRelationship={toggleVisibility} + teiId={teiId} + onLinkedRecordClick={onLinkedRecordClick} + /> + } + + + + {!hideWidgets.indicator && ( + + )} + {!hideWidgets.feedback && ( + + )} + + {enrollmentId !== 'AUTO' && } +
+
-
- -); + + ); +}; export const EnrollmentPageDefaultComponent: ComponentType = withStyles( 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 71e75fa2b0..9e37366cad 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 @@ -28,12 +28,14 @@ import { } from './hooks'; import { buildUrlQueryString, useLocationQuery } from '../../../../utils/routing'; import { useFilteredWidgetData } from './hooks/useFilteredWidgetData'; +import { useLinkedRecordClick } from '../../common/TEIRelationshipsWidget'; export const EnrollmentPageDefault = () => { const history = useHistory(); const dispatch = useDispatch(); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); const { orgUnit, error } = useCoreOrgUnit(orgUnitId); + const { onLinkedRecordClick } = useLinkedRecordClick(); const program = useTrackerProgram(programId); const { @@ -102,6 +104,7 @@ export const EnrollmentPageDefault = () => { }; const onEnrollmentError = message => dispatch(showEnrollmentError({ message })); + if (error) { return error.errorComponent; } @@ -122,6 +125,7 @@ export const EnrollmentPageDefault = () => { widgetEffects={outputEffects} hideWidgets={hideWidgets} onEventClick={onEventClick} + onLinkedRecordClick={onLinkedRecordClick} onUpdateTeiAttributeValues={onUpdateTeiAttributeValues} onUpdateEnrollmentDate={onUpdateEnrollmentDate} onUpdateIncidentDate={onUpdateIncidentDate} 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 4e88f4a39b..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,12 +1,13 @@ // @flow import { typeof effectActions } from '@dhis2/rules-engine-javascript'; -import type { Program } from 'capture-core/metaData'; +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'; import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { LinkedRecordClick } from '../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; export type Props = {| - program: Program, + program: TrackerProgram, enrollmentId: string, teiId: string, events: ?Array, @@ -20,6 +21,7 @@ export type Props = {| onCreateNew: (stageId: string) => void, 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, diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.js index f7abac3990..6466d5f93b 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.js @@ -57,87 +57,88 @@ const EnrollmentAddEventPagePain = ({ ready, classes, ...passOnProps -}: Props) => (
-
{i18n.t('Enrollment{{escape}} New Event', { escape: ':' })}
- {(() => { - if (pageFailure) { +}: Props) => ( +
+
{i18n.t('Enrollment{{escape}} New Event', { escape: ':' })}
+ {(() => { + if (pageFailure) { + return ( +
+ {i18n.t('There was an error loading the page')} +
+ ); + } else if (!orgUnitId) { + return ( + + {i18n.t('Choose a registering unit to start reporting')} + + ); + } else if (!ready) { + return null; + } + return (
- {i18n.t('There was an error loading the page')} -
- ); - } else if (!orgUnitId) { - return ( - - {i18n.t('Choose a registering unit to start reporting')} - - ); - } else if (!ready) { - return null; - } - - return ( -
-
-
-
- {!stageId ? - +
+
+ {!stageId ? + + : + + } +
+
+
+ + + {!hideWidgets.feedback && ( + - : - - } -
-
-
- - - {!hideWidgets.feedback && ( - - )} - {!hideWidgets.indicator && ( - - )} - - +
-
- ); - })()} -
); + ); + })()} +
); export const EnrollmentAddEventPageDefaultComponent: ComponentType<$Diff> = withStyles(styles)(EnrollmentAddEventPagePain); diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js new file mode 100644 index 0000000000..7235bb1a14 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js @@ -0,0 +1,21 @@ +// @flow +import React, { useEffect, useRef } from 'react'; + +type Props = { + setRelationshipRef: (HTMLDivElement) => void, +} + +export const AddRelationshipRefWrapper = ({ setRelationshipRef }: Props) => { + const renderRelationshipRef = useRef(undefined); + + // Extracting the logic to separate component because of the OrgUnitFetcher + useEffect(() => { + if (renderRelationshipRef.current) { + setRelationshipRef(renderRelationshipRef.current); + } + }, [setRelationshipRef]); + + return ( +
+ ); +}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js new file mode 100644 index 0000000000..7400ed7ff3 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js @@ -0,0 +1,3 @@ +// @flow + +export { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper.component'; 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 1add434b21..6137c49b32 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 @@ -1,5 +1,5 @@ // @flow -import React from 'react'; +import React, { useCallback, useState } from 'react'; import type { ComponentType } from 'react'; import i18n from '@dhis2/d2-i18n'; import { spacersNum } from '@dhis2/ui'; @@ -18,12 +18,19 @@ import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; import { WidgetEventComment } from '../../WidgetEventComment'; import { OrgUnitFetcher } from '../../OrgUnitFetcher'; import { TopBar } from './TopBar.container'; +import { + TrackedEntityRelationshipsWrapper, +} from '../common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper'; +import { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper'; import { NoticeBox } from '../../NoticeBox'; const styles = ({ typography }) => ({ page: { margin: spacersNum.dp16, }, + addRelationshipContainer: { + margin: spacersNum.dp16, + }, columns: { display: 'flex', }, @@ -52,6 +59,7 @@ const EnrollmentEditEventPagePain = ({ programStage, teiId, enrollmentId, + trackedEntityTypeId, programId, enrollmentsAsOptions, trackedEntityName, @@ -62,6 +70,7 @@ const EnrollmentEditEventPagePain = ({ onAddNew, classes, onGoBack, + onLinkedRecordClick, orgUnitId, eventDate, scheduleDate, @@ -71,85 +80,111 @@ const EnrollmentEditEventPagePain = ({ onEnrollmentSuccess, onCancelEditEvent, onHandleScheduleSave, -}: PlainProps) => ( - - -
-
- {mode === dataEntryKeys.VIEW - ? i18n.t('Enrollment{{escape}} View Event', { escape: ':' }) - : i18n.t('Enrollment{{escape}} Edit Event', { escape: ':' })} +}: PlainProps) => { + const [mainContentVisible, setMainContentVisible] = useState(true); + const [addRelationShipContainerElement, setAddRelationShipContainerElement] = useState(undefined); + + const toggleVisibility = useCallback(() => setMainContentVisible(current => !current), []); + + return ( + + +
+
-
-
- {pageStatus === pageStatuses.DEFAULT && programStage && ( - +
+ {mode === dataEntryKeys.VIEW + ? i18n.t('Enrollment{{escape}} View Event', { escape: ':' }) + : i18n.t('Enrollment{{escape}} Edit Event', { escape: ':' })} +
+
+
+ {pageStatus === pageStatuses.DEFAULT && programStage && ( + + )} + {pageStatus === pageStatuses.MISSING_DATA && ( + {i18n.t('The enrollment event data could not be found')} + )} + {pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED && ( + + {i18n.t('Choose a registering unit to start reporting')} + + )} +
+
+ + + + {!hideWidgets.feedback && ( + + )} + {!hideWidgets.indicator && ( + + )} + {addRelationShipContainerElement && + {}} + onLinkedRecordClick={onLinkedRecordClick} + /> + } + + - )} - {pageStatus === pageStatuses.MISSING_DATA && ( - {i18n.t('The enrollment event data could not be found')} - )} - {pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED && ( - - {i18n.t('Choose a registering unit to start reporting')} - - )} -
-
- - - - {!hideWidgets.feedback && ( - - )} - {!hideWidgets.indicator && ( - - )} - - +
+
- -
-
-); + + ); +}; export const EnrollmentEditEventPageComponent: ComponentType<$Diff> = withStyles(styles)(EnrollmentEditEventPagePain); diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js index b6a27e92bf..82ee3b9360 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js @@ -21,6 +21,7 @@ import { useEvent } from './hooks'; import type { Props } from './EnrollmentEditEventPage.types'; import { LoadingMaskForPage } from '../../LoadingMasks'; import { cleanUpDataEntry } from '../../DataEntry'; +import { useLinkedRecordClick } from '../common/TEIRelationshipsWidget'; import { pageKeys } from '../../App/withAppUrlSync'; import { withErrorMessageHandler } from '../../../HOC'; @@ -90,6 +91,8 @@ const EnrollmentEditEventPageWithContextPlain = ({ const history = useHistory(); const dispatch = useDispatch(); + const { onLinkedRecordClick } = useLinkedRecordClick(); + useEffect(() => () => { dispatch(cleanUpDataEntry(dataEntryIds.ENROLLMENT_EVENT)); }, [dispatch]); @@ -116,6 +119,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ } }, [initMode, enrollmentId, eventId, orgUnitId, history]); + const { enrollment: enrollmentSite } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const onGoBack = () => history.push(`/enrollment?${buildUrlQueryString({ enrollmentId })}`); @@ -123,10 +127,9 @@ const EnrollmentEditEventPageWithContextPlain = ({ dispatch(updateEnrollmentEvents(eventId, eventData)); history.push(`enrollment?${buildUrlQueryString({ enrollmentId })}`); }; - const enrollmentSite = useCommonEnrollmentDomainData(teiId, enrollmentId, programId).enrollment; const { teiDisplayName } = useTeiDisplayName(teiId, programId); // $FlowFixMe - const trackedEntityName = program?.trackedEntityType?.name; + const { name: trackedEntityName, id: trackedEntityTypeId } = program?.trackedEntityType; const enrollmentsAsOptions = buildEnrollmentsAsOptions([enrollmentSite || {}], programId); const event = enrollmentSite?.events?.find(item => item.event === eventId); const eventDate = getEventDate(event); @@ -135,6 +138,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ const dataEntryKey = `${dataEntryIds.ENROLLMENT_EVENT}-${currentPageMode}`; const outputEffects = useWidgetDataFromStore(dataEntryKey); + const pageStatus = getPageStatus({ orgUnitId, enrollmentSite, @@ -154,6 +158,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ hideWidgets={hideWidgets} teiId={teiId} enrollmentId={enrollmentId} + trackedEntityTypeId={trackedEntityTypeId} enrollmentsAsOptions={enrollmentsAsOptions} teiDisplayName={teiDisplayName} trackedEntityName={trackedEntityName} @@ -162,6 +167,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ onAddNew={onAddNew} orgUnitId={orgUnitId} eventDate={eventDate} + onLinkedRecordClick={onLinkedRecordClick} onEnrollmentError={onEnrollmentError} onEnrollmentSuccess={onEnrollmentSuccess} eventStatus={event?.status} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index 8e16b09325..8f269fed00 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -1,6 +1,7 @@ // @flow import type { ProgramStage } from '../../../metaData'; import type { WidgetEffects, HideWidgets } from '../common/EnrollmentOverviewDomain'; +import type { LinkedRecordClick } from '../../WidgetsRelationship/WidgetTrackedEntityRelationship'; export type PlainProps = {| programStage: ?ProgramStage, @@ -9,6 +10,7 @@ export type PlainProps = {| teiId: string, enrollmentId: string, programId: string, + trackedEntityTypeId: string, mode: string, orgUnitId: string, trackedEntityName: string, @@ -19,6 +21,7 @@ export type PlainProps = {| onDelete: () => void, onAddNew: () => void, onGoBack: () => void, + onLinkedRecordClick: LinkedRecordClick, onEnrollmentError: (message: string) => void, onEnrollmentSuccess: () => void, onCancelEditEvent: (isScheduled: boolean) => void, diff --git a/src/core_modules/capture-core/components/Pages/MainPage/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.js b/src/core_modules/capture-core/components/Pages/MainPage/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.js index 5589098844..f791f50b93 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.js +++ b/src/core_modules/capture-core/components/Pages/MainPage/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.js @@ -39,6 +39,7 @@ type Props = {| |} const WithoutOrgUnitSelectedMessagePlain = ({ programId, setShowAccessible, classes }: Props) => { + // TODO - this hook breaks the app when the program is not found const { program, programType } = useProgramInfo(programId); const IncompleteSelectionMessage = useMemo(() => (programType === programTypes.TRACKER_PROGRAM ? ( i18n.t('Or see all records accessible to you in {{program}} ', { diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.js index e716f71741..6beb555bc6 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.js @@ -1,7 +1,12 @@ // @flow -import type { ProgramStage, RenderFoundation } from '../../../../metaData'; import { actionCreator } from '../../../../actions/actions.utils'; import { effectMethods } from '../../../../trackerOffline'; +import type { + EnrollmentPayload, +} from '../../../DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types'; +import type { + TeiPayload, +} from '../../common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; export const registrationFormActionTypes = { NEW_TRACKED_ENTITY_INSTANCE_SAVE_START: 'StartSavingNewTrackedEntityInstance', @@ -16,8 +21,8 @@ export const registrationFormActionTypes = { }; // without enrollment -export const startSavingNewTrackedEntityInstance = (formFoundation: RenderFoundation) => - actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE_START)({ formFoundation }); +export const startSavingNewTrackedEntityInstance = (teiPayload: TeiPayload) => + actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE_START)({ teiPayload }); export const saveNewTrackedEntityInstance = (candidateForRegistration: any) => actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE)( @@ -41,11 +46,9 @@ export const saveNewTrackedEntityInstance = (candidateForRegistration: any) => ); // with enrollment -export const startSavingNewTrackedEntityInstanceWithEnrollment = (formFoundation: RenderFoundation, teiId: string, uid: string, firstStage?: ProgramStage) => +export const startSavingNewTrackedEntityInstanceWithEnrollment = (enrollmentPayload: EnrollmentPayload, uid: string) => actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_START)({ - formFoundation, - teiId, - firstStage, + enrollmentPayload, uid, }); diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js index cf14636450..d5d956f29d 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js @@ -15,9 +15,7 @@ import { ResultsPageSizeContext } from '../../shared-contexts'; import { navigateToEnrollmentOverview } from '../../../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions'; import { useLocationQuery } from '../../../../utils/routing'; import { EnrollmentRegistrationEntryWrapper } from '../EnrollmentRegistrationEntryWrapper.component'; -import { - useMetadataForRegistrationForm, -} from '../../../DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm'; +import { useCurrentOrgUnitInfo } from '../../../../hooks/useCurrentOrgUnitInfo'; const getStyles = ({ typography }) => ({ container: { @@ -99,7 +97,7 @@ const RegistrationDataEntryPlain = ({ const { resultsPageSize } = useContext(ResultsPageSizeContext); const { scopeType, programName, trackedEntityName } = useScopeInfo(selectedScopeId); const titleText = useScopeTitleText(selectedScopeId); - const { formFoundation } = useMetadataForRegistrationForm({ selectedScopeId }); + const { id: reduxOrgUnitId } = useCurrentOrgUnitInfo(); const handleRegistrationScopeSelection = (id) => { setScopeId(id); @@ -178,10 +176,10 @@ const RegistrationDataEntryPlain = ({ - onSaveWithEnrollment(customFormFoundation, firstStageMetaData?.stage) - } + onSave={onSaveWithEnrollment} saveButtonText={(trackedEntityTypeNameLC: string) => i18n.t('Save {{trackedEntityTypeName}}', { trackedEntityTypeName: trackedEntityTypeNameLC, interpolation: { escapeValue: false }, @@ -233,11 +231,12 @@ const RegistrationDataEntryPlain = ({ onSaveWithoutEnrollment(formFoundation)} + onSave={onSaveWithoutEnrollment} duplicatesReviewPageSize={resultsPageSize} renderDuplicatesDialogActions={renderDuplicatesDialogActions} renderDuplicatesCardActions={renderDuplicatesCardActions} diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.container.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.container.js index 41f5a864d5..8aeda04f1f 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.container.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.container.js @@ -26,15 +26,15 @@ export const RegistrationDataEntry: ComponentType = ({ const { teiId } = useLocationQuery(); const dispatchOnSaveWithoutEnrollment = useCallback( - (formFoundation) => { dispatch(startSavingNewTrackedEntityInstance(formFoundation)); }, + (teiPayload) => { dispatch(startSavingNewTrackedEntityInstance(teiPayload)); }, [dispatch]); const dispatchOnSaveWithEnrollment = useCallback( - (formFoundation, firstStage) => { + (enrollmentPayload) => { const uid = uuid(); - dispatch(startSavingNewTrackedEntityInstanceWithEnrollment(formFoundation, teiId, uid, firstStage)); + dispatch(startSavingNewTrackedEntityInstanceWithEnrollment(enrollmentPayload, uid)); }, - [dispatch, teiId]); + [dispatch]); const dataEntryIsReady = useSelector(({ dataEntries }) => (!!dataEntries[dataEntryId])); diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.js index 004adb75f7..be02acbcb5 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.js @@ -1,59 +1,31 @@ // @flow import { ofType } from 'redux-observable'; -import { pipe } from 'capture-core-utils'; import { flatMap, map } from 'rxjs/operators'; import { of, EMPTY } from 'rxjs'; -import { FEATURETYPE, dataEntryKeys } from 'capture-core/constants'; +import { dataEntryKeys } from 'capture-core/constants'; import { registrationFormActionTypes, saveNewTrackedEntityInstance, saveNewTrackedEntityInstanceWithEnrollment, } from './RegistrationDataEntry.actions'; -import { getTrackerProgramThrowIfNotFound, dataElementTypes, Section } from '../../../../metaData'; +import { getTrackerProgramThrowIfNotFound } from '../../../../metaData'; import { navigateToEnrollmentOverview, } from '../../../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions'; -import { convertFormToClient, convertClientToServer } from '../../../../converters'; import { buildUrlQueryString, shouldUseNewDashboard } from '../../../../utils/routing'; import { - deriveAutoGenerateEvents, - deriveFirstStageDuringRegistrationEvent, getStageWithOpenAfterEnrollment, - standardGeoJson, PAGES, } from './helpers'; -const convertFn = pipe(convertFormToClient, convertClientToServer); - -const geometryType = formValuesKey => Object.values(FEATURETYPE).find(geometryKey => geometryKey === formValuesKey); - -const deriveAttributesFromFormValues = (formValues = {}) => - Object.keys(formValues) - .filter(key => !geometryType(key)) - .map(key => ({ attribute: key, value: formValues[key] })); - -const deriveGeometryFromFormValues = (formValues = {}) => - Object.keys(formValues) - .filter(key => geometryType(key)) - .reduce((acc, currentKey) => (standardGeoJson(formValues[currentKey])), undefined); - -export const startSavingNewTrackedEntityInstanceEpic: Epic = (action$: InputObservable, store: ReduxStore) => +export const startSavingNewTrackedEntityInstanceEpic: Epic = (action$: InputObservable) => action$.pipe( ofType(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE_START), map((action) => { - const { currentSelections: { orgUnitId, trackedEntityTypeId }, formsValues } = store.value; - const values = formsValues['newPageDataEntryId-newTei']; - const formFoundation = action.payload?.formFoundation; - const formServerValues = formFoundation?.convertValues(values, convertFn); + const { teiPayload } = action.payload; return saveNewTrackedEntityInstance( { - trackedEntities: [{ - attributes: deriveAttributesFromFormValues(formServerValues), - geometry: deriveGeometryFromFormValues(values), - enrollments: [], - orgUnit: orgUnitId, - trackedEntityType: trackedEntityTypeId, - }], + trackedEntities: [teiPayload], }); }), ); @@ -80,78 +52,26 @@ export const startSavingNewTrackedEntityInstanceWithEnrollmentEpic: Epic = ( action$.pipe( ofType(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_START), map((action) => { - const formId = 'newPageDataEntryId-newEnrollment'; - const { currentSelections: { orgUnitId, programId }, formsValues, dataEntriesFieldsValue } = store.value; + const { currentSelections: { programId } } = store.value; const { dataStore, userDataStore, temp } = store.value.useNewDashboard; - const { formFoundation, teiId: trackedEntity, firstStage: firstStageMetadata, uid } = action.payload; - const fieldsValue = dataEntriesFieldsValue[formId] || {}; - const { occurredAt, enrolledAt, geometry } = fieldsValue; - const attributeCategoryOptionsId = 'attributeCategoryOptions'; - const attributeCategoryOptions = Object.keys(fieldsValue) - .filter(key => key.startsWith(attributeCategoryOptionsId)) - .reduce((acc, key) => { - const categoryId = key.split('-')[1]; - acc[categoryId] = fieldsValue[key]; - return acc; - }, {}); - const { trackedEntityType, stages } = getTrackerProgramThrowIfNotFound(programId); - const currentFormData = formsValues[formId] || {}; + const { enrollmentPayload, uid } = action.payload; + const { stages, useFirstStageDuringRegistration } = getTrackerProgramThrowIfNotFound(programId); + const shouldRedirect = shouldUseNewDashboard(userDataStore, dataStore, temp, programId); const { stageWithOpenAfterEnrollment, redirectTo } = getStageWithOpenAfterEnrollment( stages, - firstStageMetadata, + useFirstStageDuringRegistration, shouldRedirect, ); - const convertedValues = formFoundation.convertAndGroupBySection(currentFormData, convertFn); - const formServerValues = convertedValues[Section.groups.ENROLLMENT]; - const currentEventValues = convertedValues[Section.groups.EVENT]; - - const firstStageDuringRegistrationEvent = deriveFirstStageDuringRegistrationEvent({ - firstStageMetadata, - programId, - orgUnitId, - currentEventValues, - fieldsValue, - attributeCategoryOptions, - }); - const autoGenerateEvents = deriveAutoGenerateEvents({ - stages, - enrolledAt, - occurredAt, - programId, - orgUnitId, - firstStageMetadata, - attributeCategoryOptions, - }); - const allEventsToBeCreated = firstStageDuringRegistrationEvent - ? [firstStageDuringRegistrationEvent, ...autoGenerateEvents] - : autoGenerateEvents; - const eventIndex = allEventsToBeCreated.findIndex( + const eventIndex = enrollmentPayload.enrollments[0]?.events.findIndex( eventsToBeCreated => eventsToBeCreated.programStage === stageWithOpenAfterEnrollment?.id, ); return saveNewTrackedEntityInstanceWithEnrollment({ candidateForRegistration: { trackedEntities: [ - { - geometry: deriveGeometryFromFormValues(currentFormData), - enrollments: [ - { - geometry: standardGeoJson(geometry), - occurredAt: convertFn(occurredAt, dataElementTypes.DATE), - enrolledAt: convertFn(enrolledAt, dataElementTypes.DATE), - program: programId, - orgUnit: orgUnitId, - attributes: deriveAttributesFromFormValues(formServerValues), - status: 'ACTIVE', - events: allEventsToBeCreated, - }, - ], - orgUnit: orgUnitId, - trackedEntityType: trackedEntityType.id, - ...(trackedEntity && { trackedEntity }), - }, + enrollmentPayload, ], }, redirectTo, diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveAutoGenerateEvents.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveAutoGenerateEvents.js index a38c30e4e5..e36a6e10e6 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveAutoGenerateEvents.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveAutoGenerateEvents.js @@ -24,7 +24,7 @@ export const deriveAutoGenerateEvents = ({ occurredAt: string, programId: string, orgUnitId: string, - firstStageMetadata: ProgramStage, + firstStageMetadata: ?ProgramStage, attributeCategoryOptions: { [categoryId: string]: string } | string, }) => { // in case we have a program that does not have an incident date (occurredAt), such as Malaria case diagnosis, diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveFirstStageDuringRegistrationEvent.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveFirstStageDuringRegistrationEvent.js index bf40f45135..2e4f952f92 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveFirstStageDuringRegistrationEvent.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveFirstStageDuringRegistrationEvent.js @@ -16,7 +16,7 @@ export const deriveFirstStageDuringRegistrationEvent = ({ fieldsValue, attributeCategoryOptions, }: { - firstStageMetadata: ProgramStage, + firstStageMetadata: ?ProgramStage, programId: string, orgUnitId: string, currentEventValues?: { [id: string]: any }, diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/getStageWithOpenAfterEnrollment.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/getStageWithOpenAfterEnrollment.js index 26bd03daad..23d9d1a610 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/getStageWithOpenAfterEnrollment.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/getStageWithOpenAfterEnrollment.js @@ -12,7 +12,7 @@ export const PAGES = { // when the event will not be created redirect to enrollmentEventNew export const getStageWithOpenAfterEnrollment = ( stages: Map, - firstStageMetadata: ProgramStage, + useFirstStageDuringRegistration: boolean, shouldRedirect: boolean, ) => { const stagesArray = [...stages.values()]; @@ -22,8 +22,8 @@ export const getStageWithOpenAfterEnrollment = ( if (shouldRedirect && firstStageWithOpenAfterEnrollment) { // event will be created during first stage registration if ( - firstStageMetadata && - firstStageMetadata.id === firstStageWithOpenAfterEnrollment.id + useFirstStageDuringRegistration + && stagesArray[0].id === firstStageWithOpenAfterEnrollment.id ) { return PAGES.enrollmentEventEdit; } diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js index 0de7c46415..19d78a8c31 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js @@ -6,6 +6,7 @@ import { DATA_ENTRY_ID } from '../../registerTei.const'; import enrollmentClasses from './enrollment.module.css'; import { EnrollmentRegistrationEntry } from '../../../../../DataEntries'; import type { Props } from './dataEntryEnrollment.types'; +import { useLocationQuery } from '../../../../../../utils/routing'; const NewEnrollmentRelationshipPlain = ({ @@ -17,13 +18,15 @@ const NewEnrollmentRelationshipPlain = renderDuplicatesCardActions, ExistingUniqueValueDialogActions, }: Props) => { + const { orgUnitId, teiId } = useLocationQuery(); const fieldOptions = { theme, fieldLabelMediaBasedClass: enrollmentClasses.fieldLabelMediaBased }; - return ( i18n.t('Save new {{trackedEntityTypeName}} and link', { trackedEntityTypeName, diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.component.js b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.component.js index a7f1b1bea7..70f6bc76a2 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.component.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.component.js @@ -6,38 +6,41 @@ import { DATA_ENTRY_ID } from '../../registerTei.const'; import teiClasses from './trackedEntityInstance.module.css'; import { TeiRegistrationEntry } from '../../../../../DataEntries'; import type { Props } from './dataEntryTrackedEntityInstance.types'; +import { useCurrentOrgUnitInfo } from '../../../../../../hooks/useCurrentOrgUnitInfo'; const RelationshipTrackedEntityInstancePlain = - ({ - theme, - onSave, - teiRegistrationMetadata = {}, - duplicatesReviewPageSize, - renderDuplicatesDialogActions, - renderDuplicatesCardActions, - ExistingUniqueValueDialogActions, - }: Props) => { - const fieldOptions = { theme, fieldLabelMediaBasedClass: teiClasses.fieldLabelMediaBased }; - const { trackedEntityType } = teiRegistrationMetadata || {}; - const trackedEntityTypeNameLC = trackedEntityType.name.toLocaleLowerCase(); + ({ + theme, + onSave, + teiRegistrationMetadata = {}, + duplicatesReviewPageSize, + renderDuplicatesDialogActions, + renderDuplicatesCardActions, + ExistingUniqueValueDialogActions, + }: Props) => { + const { id: orgUnitId } = useCurrentOrgUnitInfo(); + const fieldOptions = { theme, fieldLabelMediaBasedClass: teiClasses.fieldLabelMediaBased }; + const { trackedEntityType } = teiRegistrationMetadata || {}; + const trackedEntityTypeNameLC = trackedEntityType.name.toLocaleLowerCase(); - return ( - // $FlowFixMe - flow error will be resolved when rewriting relationship metadata fetching - - ); - }; + return ( + // $FlowFixMe - flow error will be resolved when rewriting relationship metadata fetching + + ); + }; export const RelationshipTrackedEntityInstance = withTheme()(RelationshipTrackedEntityInstancePlain); diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js index a3ac0f0ae5..20f5463e75 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js @@ -3,16 +3,18 @@ import type { Node } from 'react'; import type { TeiRegistration } from '../../../../../../metaData'; import type { RenderCustomCardActions } from '../../../../../CardList'; import type { - SaveForEnrollmentAndTeiRegistration, ExistingUniqueValueDialogActionsComponent, } from '../../../../../DataEntries'; +import type { + TeiPayload, +} from '../../../../common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; export type Props = {| theme: Theme, - onSave: SaveForEnrollmentAndTeiRegistration, + onSave: (TeiPayload) => void, teiRegistrationMetadata?: TeiRegistration, duplicatesReviewPageSize: number, renderDuplicatesCardActions?: RenderCustomCardActions, - renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForEnrollmentAndTeiRegistration) => Node, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: (TeiPayload) => void) => Node, ExistingUniqueValueDialogActions: ExistingUniqueValueDialogActionsComponent, |}; diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.container.js b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.container.js index 991425a334..cf9de6695b 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.container.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.container.js @@ -34,6 +34,7 @@ export const RegisterTei = ({ onLink, onSave, onGetUnsavedAttributeValues }: Own trackedEntityName={trackedEntityName} newRelationshipProgramId={newRelationshipProgramId} error={error} - />); + /> + ); }; diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component.js b/src/core_modules/capture-core/components/Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component.js index b748347358..3cced06ccf 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component.js @@ -13,6 +13,7 @@ import { SearchResultsHeader } from '../../../../SearchResultsHeader'; import { type SearchGroup } from '../../../../../metaData'; import { ResultsPageSizeContext } from '../../../shared-contexts'; import type { ListItem } from '../../../../CardList/CardList.types'; +import { convertClientValuesToServer } from '../../../../../converters/helpers/clientToServer'; const SearchResultsPager = withNavigation()(Pagination); @@ -77,7 +78,8 @@ class TeiRelationshipSearchResultsPlain extends React.Component { } onAddRelationship = (item) => { - this.props.onAddRelationship(item.id, item.values); + const serverValues = convertClientValuesToServer(item.values, this.props.searchGroup.searchForm); + this.props.onAddRelationship(item.id, serverValues); } renderResults = () => { diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index fb48b2a555..6ee03b475f 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -1,5 +1,4 @@ // @flow - export type DataValue = { dataElement: string, value: string, @@ -49,6 +48,7 @@ export type AttributeValue = {| value: string, |}; + export type Output = {| error?: any, enrollment?: EnrollmentData, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js new file mode 100644 index 0000000000..2dbf0c7a20 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js @@ -0,0 +1,42 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { withTheme } from '@material-ui/core/styles'; +import { DATA_ENTRY_ID } from '../../registerTei.const'; +import enrollmentClasses from './enrollment.module.css'; +import { EnrollmentRegistrationEntry } from '../../../../../../DataEntries'; +import type { Props } from './dataEntryEnrollment.types'; + +const NewEnrollmentRelationshipPlain = + ({ + theme, + onSave, + programId, + orgUnitId, + duplicatesReviewPageSize, + renderDuplicatesDialogActions, + renderDuplicatesCardActions, + ExistingUniqueValueDialogActions, + }: Props) => { + const fieldOptions = { theme, fieldLabelMediaBasedClass: enrollmentClasses.fieldLabelMediaBased }; + + return ( + i18n.t('Save new {{trackedEntityTypeName}} and link', { + trackedEntityTypeName, + interpolation: { escapeValue: false }, + })} + onSave={onSave} + duplicatesReviewPageSize={duplicatesReviewPageSize} + renderDuplicatesDialogActions={renderDuplicatesDialogActions} + renderDuplicatesCardActions={renderDuplicatesCardActions} + ExistingUniqueValueDialogActions={ExistingUniqueValueDialogActions} + /> + ); + }; + +export const NewEnrollmentRelationship = withTheme()(NewEnrollmentRelationshipPlain); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.container.js new file mode 100644 index 0000000000..507e09e61b --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.container.js @@ -0,0 +1,23 @@ +// @flow +import { connect } from 'react-redux'; +import { makeEnrollmentMetadataSelector } from './enrollment.selectors'; +import { NewEnrollmentRelationship } from './DataEntryEnrollment.component'; + +const makeMapStateToProps = () => { + const enrollmentMetadataSelector = makeEnrollmentMetadataSelector(); + + const mapStateToProps = (state: ReduxState) => { + const enrollmentMetadata = enrollmentMetadataSelector(state); + + return { + enrollmentMetadata, + programId: state.newRelationshipRegisterTei.programId, + orgUnitId: state.newRelationshipRegisterTei.orgUnit.id, + }; + }; + // $FlowFixMe[not-an-object] automated comment + return mapStateToProps; +}; + +// $FlowFixMe +export const DataEntryEnrollment = connect(makeMapStateToProps, () => ({}))(NewEnrollmentRelationship); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/dataEntryEnrollment.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/dataEntryEnrollment.types.js new file mode 100644 index 0000000000..de3f6cee6f --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/dataEntryEnrollment.types.js @@ -0,0 +1,20 @@ +// @flow +import type { Node } from 'react'; +import type { Enrollment } from '../../../../../../../metaData'; +import type { RenderCustomCardActions } from '../../../../../../CardList'; +import type { + SaveForEnrollmentAndTeiRegistration, + ExistingUniqueValueDialogActionsComponent, +} from '../../../../../../DataEntries'; + +export type Props = {| + theme: Theme, + programId: string, + orgUnitId: string, + enrollmentMetadata?: Enrollment, + onSave: SaveForEnrollmentAndTeiRegistration, + duplicatesReviewPageSize: number, + renderDuplicatesCardActions?: RenderCustomCardActions, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForEnrollmentAndTeiRegistration) => Node, + ExistingUniqueValueDialogActions: ExistingUniqueValueDialogActionsComponent, +|}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.module.css b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.module.css new file mode 100644 index 0000000000..953ddeae59 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.module.css @@ -0,0 +1,11 @@ +@media screen and (max-width: 811px) and (min-width: 564px) { + .fieldLabelMediaBased { + padding-top: 0px !important; + } +} + +@media screen and (max-width: 451px) { + .fieldLabelMediaBased { + padding-top: 0px !important; + } +} diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.selectors.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.selectors.js new file mode 100644 index 0000000000..fd9331f6a6 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.selectors.js @@ -0,0 +1,23 @@ +// @flow +import { createSelector } from 'reselect'; +import type { TrackerProgram } from '../../../../../../../metaData'; +import { getProgramFromProgramIdThrowIfNotFound } from '../../../../../../../metaData'; + +const programIdSelector = state => state.newRelationshipRegisterTei.programId; + +// $FlowFixMe +export const makeEnrollmentMetadataSelector = () => createSelector( + programIdSelector, + (programId: string) => { + let program: TrackerProgram; + try { + // $FlowFixMe[incompatible-type] automated comment + program = getProgramFromProgramIdThrowIfNotFound(programId); + } catch (error) { + return null; + } + + // $FlowFixMe + return program.enrollment; + }, +); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/index.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/index.js new file mode 100644 index 0000000000..0ecadd4fab --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/index.js @@ -0,0 +1,2 @@ +// @flow +export { DataEntryEnrollment } from './DataEntryEnrollment.container'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.actions.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.actions.js new file mode 100644 index 0000000000..8cb9a9b5fa --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.actions.js @@ -0,0 +1,14 @@ +// @flow +import { actionCreator } from '../../../../../../actions/actions.utils'; + +export const actionTypes = { + DATA_ENTRY_OPEN: 'NewRelationshipRegisterTeiDataEntryOpen', + DATA_ENTRY_OPEN_CANCELLED: 'NewRelationshopRegisterTeiDataEntryOpenCancelled', + DATA_ENTRY_OPEN_FAILED: 'NewRelationshopRegisterTeiDataEntryOpenFailed', +}; + +export const openDataEntry = () => + actionCreator(actionTypes.DATA_ENTRY_OPEN)(); + +export const openDataEntryFailed = (errorMessage: string) => + actionCreator(actionTypes.DATA_ENTRY_OPEN_FAILED)({ errorMessage }); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js new file mode 100644 index 0000000000..292a1d1530 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js @@ -0,0 +1,37 @@ +// @flow +import * as React from 'react'; +import { DataEntryEnrollment } from './Enrollment'; +import { DataEntryTrackedEntityInstance } from './TrackedEntityInstance'; + +type Props = { + showDataEntry: boolean, + programId: string, + onSaveWithoutEnrollment: () => void, + onSaveWithEnrollment: () => void, +}; + +export class RegisterTeiDataEntryComponent extends React.Component { + render() { + const { showDataEntry, programId, onSaveWithoutEnrollment, onSaveWithEnrollment, ...passOnProps } = this.props; + + if (!showDataEntry) { + return null; + } + + if (programId) { + return ( + + ); + } + + return ( + + ); + } +} diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.container.js new file mode 100644 index 0000000000..e5e7d50e76 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.container.js @@ -0,0 +1,20 @@ +// @flow +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { RegisterTeiDataEntryComponent } from './RegisterTeiDataEntry.component'; +import { withErrorMessageHandler } from '../../../../../../HOC/withErrorMessageHandler'; + +const mapStateToProps = (state: ReduxState) => ({ + showDataEntry: state.newRelationshipRegisterTei.orgUnit, + error: state.newRelationshipRegisterTei.dataEntryError, + programId: state.newRelationshipRegisterTei.programId, +}); + +const mapDispatchToProps = () => ({}); + +export const RegisterTeiDataEntry = + compose( + // $FlowFixMe[missing-annot] automated comment + connect(mapStateToProps, mapDispatchToProps), + withErrorMessageHandler(), + )(RegisterTeiDataEntryComponent); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.js new file mode 100644 index 0000000000..c455e3616d --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.js @@ -0,0 +1,52 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { withTheme } from '@material-ui/core'; +import { DATA_ENTRY_ID } from '../../registerTei.const'; +import teiClasses from './trackedEntityInstance.module.css'; +import { TeiRegistrationEntry } from '../../../../../../DataEntries'; +import type { Props } from './dataEntryTrackedEntityInstance.types'; +import { getTeiRegistrationMetadata } from './tei.selectors'; +import { useLocationQuery } from '../../../../../../../utils/routing'; + +const RelationshipTrackedEntityInstancePlain = + ({ + theme, + onSave, + trackedEntityTypeId, + duplicatesReviewPageSize, + renderDuplicatesDialogActions, + renderDuplicatesCardActions, + ExistingUniqueValueDialogActions, + }: Props) => { + const { orgUnitId } = useLocationQuery(); + const fieldOptions = { theme, fieldLabelMediaBasedClass: teiClasses.fieldLabelMediaBased }; + const teiRegistrationMetadata = getTeiRegistrationMetadata(trackedEntityTypeId); + const { trackedEntityType } = teiRegistrationMetadata || {}; + const trackedEntityTypeNameLC = trackedEntityType.name.toLocaleLowerCase(); + + if (!teiRegistrationMetadata && !teiRegistrationMetadata?.form) { + return null; + } + + return ( + // $FlowFixMe - flow error will be resolved when rewriting relationship metadata fetching + + ); + }; + +export const DataEntryTrackedEntityInstance = withTheme()(RelationshipTrackedEntityInstancePlain); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js new file mode 100644 index 0000000000..b4f9617052 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js @@ -0,0 +1,30 @@ +// @flow +import type { Node } from 'react'; +import type { TeiRegistration } from '../../../../../../../metaData'; +import type { RenderCustomCardActions } from '../../../../../../CardList'; +import type { + ExistingUniqueValueDialogActionsComponent, +} from '../../../../../../DataEntries'; + +export type TeiPayload = {| + trackedEntity: string, + trackedEntityType: string, + enrollments: [], + orgUnit: string, + geometry: ?{ coordinates: any, type: any }, + attributes: Array<{| + attribute: string, + value: any, + |}>, +|} + +export type Props = {| + theme: Theme, + trackedEntityTypeId: string, + onSave: TeiPayload => void, + teiRegistrationMetadata?: TeiRegistration, + duplicatesReviewPageSize: number, + renderDuplicatesCardActions?: RenderCustomCardActions, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: (TeiPayload) => void) => Node, + ExistingUniqueValueDialogActions: ExistingUniqueValueDialogActionsComponent, +|}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/index.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/index.js new file mode 100644 index 0000000000..5bb8975389 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/index.js @@ -0,0 +1,2 @@ +// @flow +export { DataEntryTrackedEntityInstance } from './DataEntryTrackedEntityInstance'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/tei.selectors.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/tei.selectors.js new file mode 100644 index 0000000000..2363e24584 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/tei.selectors.js @@ -0,0 +1,18 @@ +// @flow +import log from 'loglevel'; +import { errorCreator } from 'capture-core-utils'; +import type { TrackedEntityType } from '../../../../../../../metaData'; +import { getTrackedEntityTypeThrowIfNotFound } from '../../../../../../../metaData'; + +// $FlowFixMe +export const getTeiRegistrationMetadata = (TETypeId: string) => { + let TEType: TrackedEntityType; + try { + TEType = getTrackedEntityTypeThrowIfNotFound(TETypeId); + } catch (error) { + log.error(errorCreator('Could not get TrackedEntityType for id')({ TETypeId })); + return null; + } + + return TEType.teiRegistration; +}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/trackedEntityInstance.module.css b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/trackedEntityInstance.module.css new file mode 100644 index 0000000000..8d899a1886 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/trackedEntityInstance.module.css @@ -0,0 +1,11 @@ +@media screen and (max-width: 811px) and (min-width: 564px) { + .fieldLabelMediaBased { + padding-top: 0px !important; + } +} + +@media screen and (max-width: 451px) { + .fieldLabelMediaBased { + padding-top: 0px !important; + } +} \ No newline at end of file diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js new file mode 100644 index 0000000000..9a9a7070af --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js @@ -0,0 +1,130 @@ +// @flow +import React, { type ComponentType, useContext, useCallback } from 'react'; +import { compose } from 'redux'; +import { withStyles } from '@material-ui/core/styles'; +import i18n from '@dhis2/d2-i18n'; +import { Button } from '@dhis2/ui'; +import { RegisterTeiDataEntry } from './DataEntry/RegisterTeiDataEntry.container'; +import { RegistrationSection } from './RegistrationSection'; +import { DataEntryWidgetOutput } from '../../../../DataEntryWidgetOutput/DataEntryWidgetOutput.container'; +import { ResultsPageSizeContext } from '../../../shared-contexts'; +import type { ComponentProps } from './RegisterTei.types'; +import { withErrorMessageHandler } from '../../../../../HOC'; + +const getStyles = () => ({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + leftContainer: { + flexGrow: 10, + flexBasis: 0, + margin: 8, + }, +}); + +const CardListButton = (({ teiId, values, handleOnClick }) => ( + +)); + +const DialogButtons = ({ onCancel, onSave, trackedEntityName }) => ( + <> + +
+ +
+ +); + +const RegisterTeiPlain = ({ + dataEntryId, + onLink, + onSaveWithoutEnrollment, + onSaveWithEnrollment, + onGetUnsavedAttributeValues, + trackedEntityName, + trackedEntityTypeId, + selectedScopeId, + classes, +}: ComponentProps) => { + const { resultsPageSize } = useContext(ResultsPageSizeContext); + + const renderDuplicatesCardActions = useCallback(({ item }) => ( + + ), [onLink]); + + const renderDuplicatesDialogActions = useCallback((onCancel, onSaveArgument) => ( + + ), [trackedEntityName]); + + const ExistingUniqueValueDialogActions = useCallback(({ teiId, attributeValues }) => ( + + ), [onLink]); + + return ( +
+
+ + +
+ + + } + /> +
+ ); +}; + +export const RegisterTeiComponent: ComponentType<$Diff> = + compose( + withErrorMessageHandler(), + withStyles(getStyles), + )(RegisterTeiPlain); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.container.js new file mode 100644 index 0000000000..773d0c2778 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.container.js @@ -0,0 +1,34 @@ +// @flow +import React from 'react'; +import { useSelector } from 'react-redux'; +import { RegisterTeiComponent } from './RegisterTei.component'; +import type { ContainerProps } from './RegisterTei.types'; +import { useScopeInfo } from '../../../../../hooks'; + +export const RegisterTei = ({ + onLink, + onSave, + onGetUnsavedAttributeValues, + trackedEntityTypeId, + suggestedProgramId, +}: ContainerProps) => { + const dataEntryId = 'relationship'; + const error = useSelector(({ newRelationshipRegisterTei }) => (newRelationshipRegisterTei.error)); + const selectedScopeId = suggestedProgramId || trackedEntityTypeId; + const { trackedEntityName } = useScopeInfo(selectedScopeId); + + return ( + + ); +}; + diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.types.js new file mode 100644 index 0000000000..0de2b570ba --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.types.js @@ -0,0 +1,23 @@ +// @flow +export type SharedProps = {| + onLink: (teiId: string, values: Object) => void, + onGetUnsavedAttributeValues?: ?Function, + trackedEntityTypeId: string, +|}; + +export type ContainerProps = {| + suggestedProgramId: string, + onSave: (teiPayload: Object) => void, + ...SharedProps, +|}; + +export type ComponentProps = {| + selectedScopeId: string, + error: string, + dataEntryId: string, + trackedEntityName: ?string, + onSaveWithEnrollment: () => void, + onSaveWithoutEnrollment: () => void, + ...SharedProps, + ...CssClasses, +|}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/ProgramSelector/ComposedProgramSelector.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/ProgramSelector/ComposedProgramSelector.component.js new file mode 100644 index 0000000000..f815b36498 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/ProgramSelector/ComposedProgramSelector.component.js @@ -0,0 +1,157 @@ +// @flow +import * as React from 'react'; +import { withStyles } from '@material-ui/core/styles'; +import i18n from '@dhis2/d2-i18n'; +import { LinkButton } from '../../../../../../Buttons/LinkButton.component'; +import { ProgramFilterer } from '../../../../../../ProgramFilterer'; +import type { Program } from '../../../../../../../metaData'; +import { TrackerProgram } from '../../../../../../../metaData'; +import { + VirtualizedSelectField, + withSelectTranslations, + withFocusSaver, + withDefaultFieldContainer, + withLabel, + withFilterProps, +} from '../../../../../../FormFields/New'; +import { NonBundledDhis2Icon } from '../../../../../../NonBundledDhis2Icon'; + +const getStyles = (theme: Theme) => ({ + iconContainer: { + display: 'flex', + alignItems: 'center', + paddingRight: 5, + }, + icon: { + width: 22, + height: 22, + borderRadius: 2, + }, + isFilteredContainer: { + fontSize: 12, + color: theme.palette.grey.dark, + paddingTop: 5, + }, + isFilteredLink: { + paddingLeft: 2, + backgroundColor: 'inherit', + }, +}); + +type Option = { + label: string, + value: string, + iconLeft?: ?React.Node, +}; + +type Props = { + orgUnitIds: ?Array, + value: string, + trackedEntityTypeId: string, + classes: Object, + onUpdateSelectedProgram: (programId: string) => void, + onClearFilter: () => void, +}; + +class ProgramSelector extends React.Component { + baseLineFilter = (program: Program) => { + const { trackedEntityTypeId } = this.props; + + const isValid = program instanceof TrackerProgram && + program.trackedEntityType.id === trackedEntityTypeId && + program.access.data.write; + + return isValid; + } + + getOptionsFromPrograms = (programs: Array): Array