From fa9e920b55db2e7047293f885375487e47ee3d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Miguel=20Rubio?= Date: Fri, 22 Mar 2024 12:34:34 +0100 Subject: [PATCH 01/12] Create publish-libraries.yml --- .github/workflows/publish-libraries.yml | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/publish-libraries.yml diff --git a/.github/workflows/publish-libraries.yml b/.github/workflows/publish-libraries.yml new file mode 100644 index 0000000000..94b5ccb317 --- /dev/null +++ b/.github/workflows/publish-libraries.yml @@ -0,0 +1,32 @@ +# This is a basic workflow that is manually triggered + +name: Publish libraries + +# Controls when the action will run. Workflow runs when manually triggered using the UI +# or API. +on: + workflow_dispatch: + # Inputs the workflow accepts. + inputs: + name: + # Friendly description to be shown in the UI instead of 'name' + description: 'Person to greet' + # Default value if no value is explicitly provided + default: 'World' + # Input has to be provided for the workflow to run + required: true + # The data type of the input + type: string + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "greet" + greet: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Runs a single command using the runners shell + - name: Send greeting + run: echo "Hello ${{ inputs.name }}" From a080207e2a960e6fd3be1e0d9be0b7aab85c646c Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 8 Apr 2024 11:40:51 +0200 Subject: [PATCH 02/12] Create build-release-candidate.yml --- .github/workflows/build-release-candidate.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/workflows/build-release-candidate.yml diff --git a/.github/workflows/build-release-candidate.yml b/.github/workflows/build-release-candidate.yml new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/.github/workflows/build-release-candidate.yml @@ -0,0 +1 @@ + From 33d2b7403b4c3a7862849c04dc1a89f5118a74bd Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 8 Apr 2024 11:43:57 +0200 Subject: [PATCH 03/12] Update build-release-candidate.yml --- .github/workflows/build-release-candidate.yml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/build-release-candidate.yml b/.github/workflows/build-release-candidate.yml index 8b13789179..f6bd368099 100644 --- a/.github/workflows/build-release-candidate.yml +++ b/.github/workflows/build-release-candidate.yml @@ -1 +1,26 @@ +name: Build Release Candidate +on: + workflow_dispatch: + # Inputs the workflow accepts. + inputs: + name: + # Friendly description to be shown in the UI instead of 'name' + description: 'Person to greet' + # Default value if no value is explicitly provided + default: 'World' + # Input has to be provided for the workflow to run + required: true + # The data type of the input + type: string +jobs: + # This workflow contains a single job called "greet" + greet: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Runs a single command using the runners shell + - name: Send greeting + run: echo "Hello ${{ inputs.name }}" From 144f8bed7eac41c16b8a97a4f6f006b096620c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Miguel=20Rubio?= Date: Thu, 11 Apr 2024 11:04:25 +0200 Subject: [PATCH 04/12] Update mobile-ui library version to 0.1-SNAPSHOT (#3579) Signed-off-by: andresmr --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 11cc6f62bd..ff295f775f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ kotlin = '1.9.21' hilt = '2.47' hiltCompiler = '1.0.0' jacoco = '0.8.10' -designSystem = "1.0-20240126.103542-129" +designSystem = "0.1-SNAPSHOT" dhis2sdk = "1.9.1" ruleEngine = "2.1.9" appcompat = "1.6.1" From 8f4b63de7b12d6b6dced411aa67d680b75601232 Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 29 Apr 2024 13:42:44 +0200 Subject: [PATCH 05/12] Release/2.10 (#3611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Androapp 5724 remove organization unit in tei event ds card if user only has access to one org unit (#3459) * add logic to show organisation unit organisation unit in TEI list, DataSet and tracker program list will only be shown when user has access to more than one organisation unit. * fix lint error * move `displayOrgUnitName` logic to ui layer --------- Co-authored-by: Siddharth Agarwal * [ANDROAPP-5887] update SDK (#3471) * feat: [ANDROAPP-5830] update mobile UI version to 0.2-20240103.112017-2 (#3461) * fix: [ANDROAPP-5830] update mobile-ui version 0.2-20240103.112017-2 * feat: [ANDROAPP-5830] add to do's and bare minimun for compilation * ANDROAPP-5829-adapt-input-image-to-new-mobile-ui-functionality (#3462) * Handle `Intent.ACTION_SEND` in form view for sharing files * Open chooser intent when share button is clicked in `ImageInput` * Open chooser intent when share button is clicked in `InputSignature`` * Move `getBitmap` extension to commons module * Move `FormFileProvider` to commons module * Add image detail activity Currently we don't have a working "save image" functionality. So, it didn't make sense to copy the implementation that is not working. So for the time being I marked it as TODO * Launch `ImageDetailActivity` instead of `ImageDetailBottomDialog` * Remove `ImageDetailBottomDialog` * feat: [ANDROAPP-5848] Adapt Input dropdown to new functionality (#3465) * feat: [ANDROAPP-5848] remove old option set dialog, recycler event and deprecated code. refactor OptionSetConfiguration to default adapt input dropdown to new functionality * feat: [ANDROAPP-5848] remove deprecated code * fix: [ANDROAPP-5848] remove deprecated code * fix: [ANDROAPP-5848] ktlint fix * fix: [ANDROAPP-5848] dismiss keyboard on dismiss popup * fix: [ANDROAPP-5848] add old pop in again to maintain old form logic * feat: [ANDROAPP-5848] update mobile ui to version 0.2-20240123.112704-7 * fix: [ANDROAPP-5848] sonar fix * fix: [ANDROAPP-5848] sonar fix --------- Co-authored-by: Sasikanth Miriyampalli * [ANDROAPP-5891] Implement new signature for compose form and maintain current for old one. (#3475) * feat: [ANDROAPP-5568] hide program stage behavior (#3464) * feat: [ANDROAPP-5523] change error access message at home (#3467) * [Update develop with main] conflict resolution * [Update develop with main] fix test * [Update release notes] Correct typos and delete unnecessary release.info file (#3480) * [ANDROAPP-5760] DashboardRepositoryImpl refactor to kotlin, new test implementation start * Move shouldSuccessfullyCreateANewEvent to unit test * Move shouldNotBeAbleToCreateNewEventsWhenFull to unit tests * Move shouldSuccessfullySyncAChangedTEI to unit tests * fix test Signed-off-by: Pablo * fix test Signed-off-by: Pablo * [ANDROAPP-5645] Close enrollment data section if already completed (#3484) --------- Signed-off-by: Pablo * feat: [ANDROAPP-5683] legend description (#3486) * fix: [ANDROAPP-5900] adapt Input providers to new TextFieldValue usage * fix: [ANDROAPP-5900] update mobile ui 0.2-20240206.071329-16 * fix: [ANDROAPP-5900] set initial selection to value length * updates paging on search to paging3 * update search tests * fix code smells * updates sdk and prevents scroll to top when paginating * Updates sdk * update and adapt design library version * fix tests * adds helpertext to table's input field (#3487) * show confirmation dialog show confirmation dialog before deleting TEI or removing enrolments * show/hide menu items Show TEI Dashboard menu items based on correspondent authorities * update confirm delete dialog design Older BottomSheetDialog has now been replaced by mobile-ui library `BottomSheetSheel` component * Set button color style in delete bottom sheet dialog * Run code formatting * Fix delete button callback not working in `DeleteBottomSheetDialog` * [ANDROAPP-5894] Adapt enrollment label to be configurable from server (#3493) Signed-off-by: Pablo * Adds a no connection listener to Sync Dialog to show snackbar to users * resolve code smell. now dialog uses SAM interface for the NoConnectionListener * feat: [ANDROAPP-5896] configurable event label (#3495) * cache some complex operations (#3494) Signed-off-by: Pablo * [ANDROAPP-5857] image download implementation (#3498) * download image files to external directory Signed-off-by: Pablo * download qr/barcode files to external directory Signed-off-by: Pablo * show message when file is downloaded Signed-off-by: Pablo --------- Signed-off-by: Pablo * feat: [ANDROAPP-5802] schedule events after completion (#3483) * update: [ANDROAPP-4826] design system version updated * add: [ANDROAPP-4826] String added * add: [ANDROAPP-4826] Infobar added, warning message field added * update: [ANDROAPP-4826] visual changes when there is no fields in the section * update: [ANDROAPP-4826] code formatted, visual changes to description * delete: [ANDROAPP-4826] Dummy section deleted in FormSectionMapper * update: [ANDROAPP-4826] the InfoBar doesn't appears when the form is loading * update: [ANDROAPP-4826] libs updated * [ANDROAPP-4826] Show no fields warnings if one section and is NO_HEADER * [ANDROAPP-4826] Refactor section warning generation to mapper * [ANDROAPP-4826] Refactor section warning generation to mapper * [ANDROAPP-5901] Move create button in timeline view (#3490) * [ANDROAPP-5800] Replace xml view by ComposeView * [ANDROAPP-5800] Set up SearchParameter structure * [ANDROAPP-5800] Implement SearchParametersRepository * [ANDROAPP-5800] update search query * [ANDROAPP-500] Remove deprecated steps in walkthrough * [ANDROAPP-500] Implement search and clear button * [ANDROAPP-500] Implement SnackBar for min attributes search * [ANDROAPP-500] Refactor search and clear button to SearchTEIViewModel * [ANDROAPP-500] rebase develop and update library * [ANDROAPP-500] show min attributes message * [ANDROAPP-5800] handle focus * [ANDROAPP-5800] update design library * [ANDROAPP-5800] clear search parameters * [ANDROAPP-5800] Use fieldUiModel for parameter provider * [ANDROAPP-5800] insert inputstyle as parameter * [ANDROAPP-5800] text range to textInput model * [ANDROAPP-5800] update display name values * [ANDROAPP-5800] show org unit selector * [ANDROAPP-5800] remove SearchRepository.kt * [ANDROAPP-5800] fix tests * [ANDROAPP-5800] fix tests * [ANDROAPP-5800] Add composeTestRule to functional tests * [ANDROAPP-5800] Update design library version * [ANDROAPP-5800] Remove SearchParametersRepository.kt and use SearchRepositoryKt * [ANDROAPP-5800] provide icons based on valueType * [ANDROAPP-5800] Implement QR and Barcode scanning * [ANDROAPP-5800] fix code smells * [ANDROAPP-5800] clear focus on clear search * [ANDROAPP-5800] close keyboard om search and clear * [ANDROAPP-5800] close empty fields when clear search * [ANDROAPP-5800] request focus when item is opened * [ANDROAPP-5800] move focus to next item * [ANDROAPP-5800] enable disable search button on empty parameters * [ANDROAPP-5805] clear search button visibility * [ANDROAPP-5805] Adapt top bar when searching * [ANDROAPP-5805] Add replay to mutable search flow in order to emit the first value * [ANDROAPP-5805] Remove connected rounded corners on landscape * [ANDROAPP-5805] update ProgramEventTest to remove check on org unit when is not visible * [ANDROAPP-5800] configure toolbar for landscape * [ANDROAPP-5800] remove unused program variable * [ANDROAPP-5800] remove needs force update parameter from Form only used for search * [ANDROAPP-5800] Remember ScanContract on SearchParametersScreen * [ANDROAPP-5800] Clear focus when closing search * [ANDROAPP-5800] Avoid double updating Age field * [ANDROAPP-5800] Not loop on next click when gets the last item * [ANDROAPP-5800] set rounded corners on show filters landscape * [ANDROAPP-5800] fix code smell * feat: [ANDROAPP-5794] Date Selectors (#3501) * feat: [ANDROAPP-5794] Implement new InputDate Date and TimePicker functionalities * feat: [ANDROAPP-5794] add selectable dates and refactor code * feat: [ANDROAPP-5794] update design system version * feat: [ANDROAPP-5794] update input date in scheduling dialog * feat: [ANDROAPP-5794] adapt new design for implementations with periods * feat: [ANDROAPP-5794] remove old calendar from event details * feat: [ANDROAPP-5794] fix test and add year range control * feat: [ANDROAPP-5794] fix ui test and reformat default values * feat: [ANDROAPP-5794] sonar fix * feat: [ANDROAPP-5794] ktlint * feat: [ANDROAPP-5794] ktlint * feat: [ANDROAPP-5794] update date value on reset button click * fix: [ANDROAPP-5802] scheduling dialog not displayed (#3507) * [ANDROAPP-1540] Multi selection for option set in programs (#3500) Signed-off-by: Pablo * fix(translations): sync translations from transifex (develop) WARNING: This automated sync from transifex removed more lines than it added. Please check carefully before merging! * fix(translations): sync translations from transifex (develop) * fix: CI fix for new date picker selection (#3511) * fix: CI fix for new date picker selection * fix: readapt to avoid flakiness * fix: remove log printing for datepicker * fix: ignore test * [ANDROAPP-4457] Configurable basemaps (#3509) Signed-off-by: Pablo * Update ActivityTestRule on SyncActivityTest * Add function to get single tracked entity in `SearchRepositoryKt` * Add `OnQrCodeScannedFormIntent` * Return `OnQrCodeScanned` instead of `OnSave` when QR code is scanned * Update `SearchRepositoryKt#trackedEntity` function to return list of TEIs * Open TEI dashboard when a TEI is found after QR code scanning * Update ActivityTestRule on ProgramEventTest * feat: [ANDROAPP-5919] multiselection on dataset * update design library * Grouped stage events ui logic updated * show all event stages * add toggle button to grouped stage events * update show logic for timeline events * fix code styling * fix toggle button click Earlier when clicking on a particular stage show more buttons, all the stage events were expanding. This commit fixes this. * update toggle button design * update stage item ui * update design for timeline and grouped events * fix lint error * fix roboto test * fix ui test for completed events * fix click for event and stages item * fix `SEARCHTETEST` * fix roboto test * ignore depreciated test * Ignore test WIP * fix sonar test * fix lint error * fix left margin for event card * remove data element value null or empty value * fix text style for stage item title and descr * fix color for skipped element * fix metadata avatar for stage and event item * fix lint error * fix test notation * [ANDROAPP-5941] Use multiplatform rule engine (#3505) * Rename .java to .kt Signed-off-by: Pablo * update rule engine Signed-off-by: Pablo * update rule engine Signed-off-by: Pablo * Rename .java to .kt Signed-off-by: Pablo * use multiplatform rule engine Signed-off-by: Pablo * rule engine module Signed-off-by: Pablo * set null enrollment uid Signed-off-by: Pablo * fix dagger Signed-off-by: Pablo * fix tests Signed-off-by: Pablo * fix LMIS test Signed-off-by: Pablo * ktlint Signed-off-by: Pablo * fix tests Signed-off-by: Pablo * add desugar lib Signed-off-by: Pablo * removed unused test Signed-off-by: Pablo * fix code smells Signed-off-by: Pablo * remove duplicated dependencies Signed-off-by: Pablo * fix displayText Signed-off-by: Pablo * fix test Signed-off-by: Pablo --------- Signed-off-by: Pablo * [ANDROAPP-2474] Import/Export database (#3503) Signed-off-by: Pablo * fix(translations): sync translations from transifex (develop) * fix FormTest (#3523) * fix FormTest Signed-off-by: Pablo * remove commented code Signed-off-by: Pablo --------- Signed-off-by: Pablo * Set compose forms as default * verifying signed commit * fix: [ANDROAPP-5802] scheduling program stages (#3518) * fix: [ANDROAPP-5802] scheduling program stages * fix: [ANDROAPP-5802] scheduling program stages * fix black screen when qr is read (#3524) Signed-off-by: Pablo * Update SearchTETest (#3525) * Update SearchTETest with lazyActivityScenario and composeTestRule * fix shouldShowErrorWhenSyncEventFails * revert Jenkinsfile changes * shouldShowErrorWhenSyncEventFails click on event item * feat: [ANDROAPP-5930] move details information to the registration form (#3512) * feat: [ANDROAPP-5930] Initial commit: move enrollement date to formulary, check org unit open/close dates, modify enrollment screen * feat: [ANDROAPP-5930] update allowed dates based on selected Org Unit correctly * feat: [ANDROAPP-5930] remove unnecessary code and comments * feat: [ANDROAPP-5930] amend sonar fix for failed test * feat: [ANDROAPP-5930] correct flaky test and adapt to new enrollment flow * feat: [ANDROAPP-5930] readapt instrumentation tests to new enrollment flow * fix: [ANDROAPP-5930] sonar fix * fix: [ANDROAPP-5930] format style and add Unit tests for EnrollmentRepository class * fix: [ANDROAPP-5800] search page corrections (#3517) * fix: [ANDROAPP-5800] filter by equals instead of contains if parameter is option set, order search params * fix: [ANDROAPP-5800] correct value change when switching between age, month year inputs * fix: [ANDROAPP-5800] lint check * [ANDROAPP-5911] Fix paging3 mapping (#3526) Co-authored-by: Manu Muñoz Signed-off-by: Pablo * [ANDROAPP-5123] Support for custom icons (#3510) Co-authored-by: Manu Muñoz --------- Signed-off-by: Pablo * [ANDROAPP-5309] Line listing in local analytics (#3516) Signed-off-by: Pablo Co-authored-by: manu * fix: [ANDROAPP-5976] crash after discard or delete event (#3533) * feat: [ANDROAPP-5952] Update design system and adapt InputAge to new … (#3531) * feat: [ANDROAPP-5952] Update design system and adapt InputAge to new implementation, add tests for component * fix: [ANDROAPP-5952] take timezone into account for date setting * fix: [ANDROAPP-5952] code clean up * fix: [ANDROAPP-5952] update design system * fix: [ANDROAPP-5952] fix test * Androapp 5996 error in timeline view when there are no events created (#3527) * fix groupByStage value set method * fix timeline events recycler view when no events * fix the event count in timeline header * revert get grouping change --------- Co-authored-by: Siddharth Agarwal * fix: [ANDROAPP-5988] crash when returning to dashboard event list (#3540) * fix feedback on delete program enrollments (#3537) * fix feedback on delete program enrollments * fix lint error --------- Co-authored-by: Siddharth Agarwal * fix event card item margins (#3536) * fix event card item margins * fix lint error --------- Co-authored-by: Siddharth Agarwal * ANDROAPP-5799-search-enroll-button (#3535) * Load TE type name when search view model is created * Use new FAB component for `CreateNewButton` * Update search button UI * Add old search button composable * Remove `SearchTEListScreen.kt` * Add `Add new $teType` button * Rename `SearchButton_Old` to `SearchButtonWithQuery` * Show create TEI FAB if there is query data * Display old search button when there is search query data * Use `CoordinatorLayout` in `fragment_search_list` layout Rather than listening to scroll state changes manually, we can just leverage the inbuilt scroll behaviour of coordinator which is smoother. We are using a fake appbar to wrap the header content essentially. * Remove animated visibility from `FullSearchButtonAndWorkingList` * Use immutable map in `FullSearchButtonAndWorkingList` composable * Update design of search bar with query data * Show search bar with query data and create new entity button in landscape * Add icon to search button * Use lower case TE type name in the create button * Update working list chip group spacing * fix: [ANDROAPP-5982] "All [TE Type]" search and relationship search crash (#3542) * [ANDROAPP-5123] Do not use app MetadataIcon (#3539) * refactor custom icons Signed-off-by: Pablo * fix program metadata icon Signed-off-by: Pablo * fix tests Signed-off-by: Pablo * fix eventi Signed-off-by: Pablo --------- Signed-off-by: Pablo * [ANDROAPP-5309] fix table dimensions (#3541) * fix table dimensions Signed-off-by: Pablo * design comments Signed-off-by: Pablo --------- Signed-off-by: Pablo Co-authored-by: Andrés Miguel Rubio * fix: [ANDROAPP-5930] remove old unnecessary header and modify edit en… (#3538) * fix: [ANDROAPP-5930] remove old unnecessary header and modify edit enrollment corners and header * fix: [ANDROAPP-5930] wait for idle in test * fix: [ANDROAPP-5930] refactor methods * [ANDROAPP-5952] input age prompt stays at beginning (#3543) * refactor custom icons Signed-off-by: Pablo * fix: [ANDROAPP-5930] update design system and adapt input for textfieldValue * fix: [ANDROAPP-5930] add null checks for value format * fix: [ANDROAPP-5952] correct test * fix: [ANDROAPP-5952] amend rebase conflic resolution update design system --------- Signed-off-by: Pablo Co-authored-by: Pablo * [ANDROAPP-5805] Move details information to Event Data entry (#3528) * [ANDROAPP-5805] Simplify if else on EventDate * [ANDROAPP-5805] Remove MviIntent interface * [ANDROAPP-5805] Remove nullable parameters forced by using search * [ANDROAPP-5805] Add Event details section with report date * [ANDROAPP-5805] Add Event Org unit in details * [ANDROAPP-5805] Add Event Coordinates in details * [ANDROAPP-5805] Add Event Category Combo in details * [ANDROAPP-5805] Create Eventwithout registration and navigate to Dataentry * [ANDROAPP-5805] Add CreateEventUseCase * [ANDROAPP-5805] update saving attribute option combo * [ANDROAPP-5805] move PeriodUtils to common * [ANDROAPP-5805] Implement period selector for events * [ANDROAPP-5805] Set EventMode and allow org unit edition on creation * [ANDROAPP-5805] Add eventCatCombo parameter to equals method in FieldUiModelImpl * [ANDROAPP-5805] Remove Event Detail section * [ANDROAPP-5805] Remove date on CreateEventUseCase * [ANDROAPP-5805] load selected category option combo * [ANDROAPP-5805] fix test dependencies * [ANDROAPP-5805] fix ProgramEventTest * [ANDROAPP-5805] fix EventTest * [ANDROAPP-5805] ktlintFormat after rebasing custom icons * [ANDROAPP-5805] Add editable reason to EventCaptureForm * [ANDROAPP-5931] Collapse event details and cat combo sections if completed (#3530) Signed-off-by: Pablo * [ANDROAPP-5805] fix collapsible when no coordinates * [ANDROAPP-5805] Fix ProgramEventTest * [ANDROAPP-5805] Fix FormTest * [ANDROAPP-5805] Fix EventTest * [ANDROAPP-5805] Fix ProgramEventTest * [ANDROAPP-5805] Add idlingResource to EventCaptureFormPresenter * [ANDROAPP-5805] Handle remove org unit when creating event * [ANDROAPP-5805] Handle adding cat combo * [ANDROAPP-5805] Handle adding cat combo * [ANDROAPP-5805] fix code smells * [ANDROAPP-5805] fix code smells --------- Signed-off-by: Pablo Co-authored-by: Pablo * Change start padding of event item when grouped (#3546) * Update android sdk 1.10.0 snapshot 36 (#3547) * fix: [ANDROAPP-5923] All enrollment screen crash (#3534) * prevents crash on All enrollments screen * removes deleted view tags from test * removes useless non-null access * Add idilingResource to event tests (#3550) * Update release start job (#3549) * release_start workflow * Update release-start.yml --------- Co-authored-by: manu * Update version to 2.10 * fix: [ANDROAPP-6003] multiselection checked values (#3545) * fix: [ANDROAPP-5981] the app crashes randomly when scheduling new events in tracker program (#3521) * fix: [ANDROAPP-5981] filter by distinct events when updating to avoid duplicate calls to same event * fix: [ANDROAPP-5981] remove unnecessary subscription to observer, remove NonNullAsserted !! assertions for option codes and org unit codes * fix: [ANDROAPP-5981] code smells and small refactor * fix: [ANDROAPP-5981] update with requested changes * fix: [ANDROAPP-5981] ignore flaky test * [ANDROAPP-6010] defect event details catcombo scope fails when modifying org unit (#3555) * [ANDROAPP-6010] Adapt Event toolbar to new design * [ANDROAPP-6010] Set ProgramCaptureScope to event orgunit * [ANDROAPP-6010] check category combos for mandatory fields * fix: [ANDROAPP-6006] update changes on dashboard (#3554) * Add IdlingResource when completing an event (#3556) * Update android sdk 1.10.0 snapshot 37 (#3560) Co-authored-by: Andrés Miguel Rubio * fix icon size and alignment of program stage item (#3552) Co-authored-by: Siddharth Agarwal * fi: [ANDROAPP-6004] All {TE Type} search now show too many result. (#3553) * Update android sdk 1.10.0 snapshot 38 (#3562) * Update android sdk 1.10.0 snapshot 38 * Adapt unit tests * fix: [ANDROAPP-6017] only searched fields on searched bar (#3559) * update desyng system (#3544) Signed-off-by: Pablo * add anr reports in sentry (#3532) Signed-off-by: Pablo * [ANDROAPP-6007] Use default color in metadata icons (#3557) * use default color Signed-off-by: Pablo * fix unit tests Signed-off-by: Pablo * remove unused Signed-off-by: Pablo --------- Signed-off-by: Pablo * fix: [ANDROAPP-6024] table resize dimensions (#3566) * [ANDROAPP-6023] Scheduled event missbehavior (#3565) * [ANDROAPP-6023] Remove Event initial step when scheduling and event * [ANDROAPP-6023] add event form aesthetics rounded corners * [ANDROAPP-6023] set entollment from schedule and by generation after create an enrollment as new events. * [ANDROAPP-6023] Set EventMode Schedule when entering data scheduling an event * [ANDROAPP-6023] Remove unused EventCaptureInitial package * [ANDROAPP-6023] Change Enter/Cancel button text by Enter/Skip * [ANDROAPP-6023] Set rounded corners on EventCaptureActivity sections * fix: [ANDROAPP-6008] user-friendly name on search parameters (#3563) * updates Dashboard only when new tei dashboard card is shown (#3567) * fix: [ANDROAPP-6013] don't save events without date or catCombo (#3569) * fix: [ANDROAPP-6040] Remove scroll to first empty item functionality (#3571) * fix: [ANDROAPP-6040] Remove scroll to first empty item functionality * fix: [ANDROAPP-6040] adapt test now that automatic scroll does not occur * [Androapp-5763] cropped number when data element name wraps into second line (#3570) * feat: [ANDROAPP-5763] implement Indicator Input throughout app * [ANDROAPP-5763] Use biggerOrEquals for filtering legends Signed-off-by: andresmr * [ANDROAPP-5763] Set SurfaceColor.Container as default color Signed-off-by: andresmr * [ANDROAPP-5763] Remove xml from IndicatorViewHolder Signed-off-by: andresmr * [ANDROAPP-5763] Replace test robots Signed-off-by: andresmr * [ANDROAPP-5763] Remove uncommented test code Signed-off-by: andresmr * [ANDROAPP-5763] Move spacing to vertical arrangement Signed-off-by: andresmr --------- Signed-off-by: andresmr Co-authored-by: Xavier Molloy * fix: [ANDROAPP-6038] improved management for Cancel event in OU dialog (#3572) * fix: [ANDROAPP-6038] enable add new button when exiting canceling OU dialog * fix: [ANDROAPP-6038] small code refactor * Update design library to 0.2-20240405.110637-52 (#3575) Signed-off-by: andresmr * [ANDROAPP-6086] set today es event date when creating new event (#3577) Signed-off-by: andresmr * fix: [ANDROAPP-6033] modify padding between ListCard components, add superior padding if item is first one in order to improve superior border visibility (#3568) * fix: [ANDROAPP-6011] freeze getting dashboard events (#3574) * fix: [ANDROAPP-6011] freeze getting dashboard events * fix test * fix test * access localdatastore in background thread (#3580) Signed-off-by: Pablo * Create build-release-candidate.yml (#3573) * Create build-release-candidate.yml * Update build-release-candidate.yml * buid release candidate signing config Signed-off-by: Pablo * set keystore path Signed-off-by: Pablo * set missing env variables Signed-off-by: Pablo * ktlint Signed-off-by: Pablo * ignore schedule event test --------- Signed-off-by: Pablo Co-authored-by: manu * [ANDROAPP-6015] update design system (#3585) Signed-off-by: andresmr * fix: [ANDROAPP-6091] days to scheduled day (#3578) * update version name (#3584) * fix: [ANDROAPP-6053] ANR on dataset list (#3583) * [ANDROAPP-5712] Update sync dialog when reopened (#3564) * Update sync dialog when reopened Signed-off-by: Pablo * wait for compose to idle Signed-off-by: Pablo * use indeterminate progress in granular sync Signed-off-by: Pablo * wait for the data to be loaded Signed-off-by: Pablo --------- Signed-off-by: Pablo * Get period step on background (#3582) Signed-off-by: Pablo * fix: [ANDROAPP-6110] crash in program default metadata icon (#3594) * fix: Improve performance in home and event program screen (analytics tab) (#3595) * Improve performance in home screen (analytics tab) * Improve performance in program event screen (analytics tab) * Refactor condition * Restore deleted code * fix: [ANDROAPP-6103] month value in scheduling dialog (#3590) * fix component focus requester (#3587) Co-authored-by: Siddharth Agarwal * [ANDROAPP-6106] Replace enrollment label with custom one (#3592) * replace enrollment label with custom one Signed-off-by: Pablo * replace enrollment label with custom one in complete dialog Signed-off-by: Pablo * Restore enrollment list label Signed-off-by: Pablo --------- Signed-off-by: Pablo * Fix IndexOutOfBounds exception in SyncStat (#3597) Signed-off-by: Pablo * fix: [ANDROAPP-5992] check program stage filters in WorkingListScope (#3600) * fix: [ANDROAPP-5992] check program stage filters in WorkingListScope * fix: [ANDROAPP-5992] check program stage filters in WorkingListScope * [ANDROAPP-6012] Display validation errors (#3599) * [ANDROAPP-6012] Display validation errors Signed-off-by: Pablo * apply validation only for date value dates Signed-off-by: Pablo --------- Signed-off-by: Pablo * Skip OU selection for single option in event creation (#3588) * skip OU selection when count is 1 * fix lint error * Fix nullable programUid for OURepository * Fix TEIPresenter Test * Code refactored:move functions to repository class * Fix lint error --------- Co-authored-by: Siddharth Agarwal * fix: [ANDROAPP-6081] set current date for new enrollment (#3603) * [ANDROAPP-5949] OpenID login button overlaps with "Manage accounts" (#3606) Signed-off-by: Pablo * fix: [ANDROAPP-6119] verify array size before accessing index (#3602) Co-authored-by: Xavier Molloy * [ANDROAPP-5787] Use search scope in a referral (#3608) * [ANDROAPP-5787] Use search scope in a referral Signed-off-by: Pablo * [ANDROAPP-5787] add test Signed-off-by: Pablo * search scope must return search and capture org units Signed-off-by: Pablo * search scope must return search and capture org units Signed-off-by: Pablo * fix test Signed-off-by: Pablo --------- Signed-off-by: Pablo * browserstack build tag (#3610) * fix: [ANDROAPP-6123] key text in list cards (#3614) Co-authored-by: DavidAparicioAlbaAsenjo <137989685+DavidAparicioAlbaAsenjo@users.noreply.github.com> * [ANDROAPP-5869] Provide different keys for fixedStickyHeader (#3607) Signed-off-by: Pablo * fix: [ANDROAPP-5883] periods when orgunit is closed (#3613) * [ANDROAPP-5757] Field with error can't be disabled (#3609) Signed-off-by: Pablo * [ANDROAPP-6126] Date parse exception when searching by unfinished date string (#3604) Signed-off-by: Pablo * fix: [ANDROAPP-6062] clear search fields when required attributes to search (#3581) * fix: [ANDROAPP-6062] clear search fields when required attributes to search * add clear test --------- Signed-off-by: Pablo Signed-off-by: andresmr Co-authored-by: Siddharth Agarwal Co-authored-by: Siddharth Agarwal Co-authored-by: Andrés Miguel Rubio Co-authored-by: Xavier Molloy <44061143+xavimolloy@users.noreply.github.com> Co-authored-by: Sasikanth Miriyampalli Co-authored-by: Manu Muñoz Co-authored-by: Xavier Molloy Co-authored-by: FerdyRod Co-authored-by: = Co-authored-by: dhis2-bot Co-authored-by: manu Co-authored-by: FerdyRod Co-authored-by: Víctor García Co-authored-by: GitHub Actions Bot Co-authored-by: DavidAparicioAlbaAsenjo <137989685+DavidAparicioAlbaAsenjo@users.noreply.github.com> --- .github/workflows/build-release-candidate.yml | 65 +- .github/workflows/release-start.yml | 45 +- Jenkinsfile | 3 + app/build.gradle.kts | 16 +- .../org/dhis2/LazyActivityScenarioRule.kt | 68 ++ .../org/dhis2/common/filters/FiltersRobot.kt | 3 +- .../dhis2/common/matchers/ChartMatchers.kt | 3 +- .../java/org/dhis2/usescases/BaseTest.kt | 23 +- .../org/dhis2/usescases/UseCaseTestsSuite.kt | 5 +- .../org/dhis2/usescases/event/EventIntents.kt | 45 +- .../usescases/event/EventRegistrationRobot.kt | 60 +- .../org/dhis2/usescases/event/EventTest.kt | 70 +- .../eventDetails/EventInitialTest.kt | 26 +- .../org/dhis2/usescases/filters/FilterTest.kt | 54 +- .../flow/searchFlow/SearchFlowRobot.kt | 1 - .../flow/searchFlow/SearchFlowTest.kt | 12 +- .../usescases/flow/syncFlow/SyncFlowTest.kt | 119 +-- .../usescases/flow/syncFlow/SyncIntents.kt | 29 +- .../robot/EventWithoutRegistrationRobot.kt | 51 +- .../flow/syncFlow/robot/SyncFlowRobot.kt | 37 +- .../usescases/flow/teiFlow/TeiFlowRobot.kt | 74 +- .../usescases/flow/teiFlow/TeiFlowTest.kt | 21 +- .../org/dhis2/usescases/form/FormIntents.kt | 43 +- .../org/dhis2/usescases/form/FormRobot.kt | 66 +- .../java/org/dhis2/usescases/form/FormTest.kt | 278 ++---- .../usescases/main/program/ProgramUiTest.kt | 8 +- .../programevent/ProgramEventTest.kt | 131 ++- .../programevent/robot/ProgramEventsRobot.kt | 112 +-- .../usescases/searchte/SearchTEIntents.kt | 24 +- .../dhis2/usescases/searchte/SearchTETest.kt | 139 +-- .../searchte/robot/SearchTeiRobot.kt | 118 +-- .../dhis2/usescases/sync/SyncActivityTest.kt | 25 +- .../teidashboard/TeiDashboardIntents.kt | 127 ++- .../TeiDashboardMobileActivityTest.kt | 197 +++++ .../teidashboard/TeiDashboardTest.kt | 127 +-- .../TeiDashboardTestNoComposable.kt | 26 +- .../scheduling/SchedulingDialogUiTest.kt | 134 +++ .../teidashboard/robot/EnrollmentRobot.kt | 102 ++- .../teidashboard/robot/EventRobot.kt | 100 +-- .../teidashboard/robot/IndicatorsRobot.kt | 64 +- .../teidashboard/robot/TeiDashboardRobot.kt | 412 ++++----- .../org/dhis2/bindings/ContextExtensions.kt | 2 +- .../utils/granularsync/GranularSyncModule.kt | 2 +- .../org/dhis2/bindings/ContextExtensions.kt | 2 +- .../utils/granularsync/GranularSyncModule.kt | 2 +- .../assets/databases/dhis_test.db | Bin 6692864 -> 6692864 bytes .../org/dhis2/bindings/ContextExtensions.kt | 2 +- .../utils/granularsync/GranularSyncModule.kt | 2 +- app/src/main/AndroidManifest.xml | 10 +- app/src/main/java/org/dhis2/App.java | 5 +- app/src/main/java/org/dhis2/AppComponent.java | 1 - app/src/main/java/org/dhis2/AppModule.kt | 7 - .../java/org/dhis2/bindings/Bindings.java | 316 +------ .../bindings/ValidationStrategyExtensions.kt | 9 - .../java/org/dhis2/bindings/ViewExtensions.kt | 56 +- .../dhis2/data/dhislogic/DhisEventUtils.kt | 20 - .../data/forms/EnrollmentFormRepository.java | 100 --- .../data/forms/EnrollmentFormRepository.kt | 92 ++ .../org/dhis2/data/forms/EventRepository.java | 106 ++- .../org/dhis2/data/forms/FormRepository.java | 6 +- .../EnrollmentRuleEngineRepository.java | 170 ---- .../forms/dataentry/RuleEngineRepository.java | 20 - .../dhis2/data/forms/dataentry/ValueStore.kt | 5 - .../data/forms/dataentry/ValueStoreImpl.kt | 63 -- .../dhis2/data/server/ServerComponent.java | 9 +- .../org/dhis2/data/server/ServerModule.kt | 25 +- .../dhis2/data/service/SyncGranularWorker.kt | 62 +- .../dhis2/data/service/SyncPresenterImpl.kt | 13 +- .../org/dhis2/data/user/UserComponent.java | 5 + .../dataSetTable/DataSetTableActivity.kt | 8 + .../dataSetTable/DataSetTableModule.java | 2 +- .../dataSetTable/DataSetTablePresenter.kt | 2 +- .../DataSetTableViewModelFactory.kt | 2 +- .../dataSetDetail/DataSetDetailFragment.kt | 33 +- .../dataSetSection/DataValuePresenter.kt | 1 + .../dataSetSection/DataValueRepository.kt | 18 +- .../dataSetSection/MapFieldValueToUser.kt | 3 + .../TableDataToTableModelMapper.kt | 2 + .../datasetDetail/DataSetDetailActivity.java | 11 + .../datasetDetail/DataSetDetailModule.java | 2 +- .../DataSetDetailRepositoryImpl.java | 6 +- .../datasetList/DataSetListAdapter.kt | 39 +- .../datasetList/DataSetListFragment.kt | 12 +- .../datasetList/DataSetListViewModel.kt | 5 +- .../DataSetInitialActivity.java | 6 +- .../datasetInitial/DataSetInitialPresenter.kt | 5 +- .../development/DevelopmentActivity.java | 69 +- .../usescases/development/RuleValidation.kt | 4 +- .../enrollment/EnrollmentActivity.kt | 93 +- .../enrollment/EnrollmentFormRepository.kt | 5 - .../EnrollmentFormRepositoryImpl.kt | 108 +-- .../usescases/enrollment/EnrollmentModule.kt | 23 +- .../enrollment/EnrollmentPresenterImpl.kt | 29 +- .../usescases/enrollment/EnrollmentView.kt | 1 - .../events/ScheduledEventActivity.kt | 174 ++-- .../events/ScheduledEventContract.kt | 6 +- .../usescases/events/ScheduledEventModule.kt | 4 +- .../events/ScheduledEventPresenterImpl.kt | 39 +- .../EventIdlingResourceSingleton.kt | 24 + .../eventCapture/EventCaptureActivity.kt | 209 +++-- .../eventCapture/EventCaptureContract.java | 152 ---- .../eventCapture/EventCaptureContract.kt | 89 ++ .../eventCapture/EventCaptureFieldProvider.kt | 271 ------ .../eventCapture/EventCaptureModule.java | 12 +- .../EventCapturePagerAdapter.java | 42 +- .../eventCapture/EventCapturePresenterImpl.kt | 74 +- .../EventCaptureRepositoryImpl.java | 35 +- .../EventRuleEngineRepository.java | 92 -- .../domain/ConfigureEventCompletionDialog.kt | 2 +- .../eventCapture/domain/ReOpenEventUseCase.kt | 23 + .../EventCaptureFormFragment.java | 34 +- .../EventCaptureFormModule.kt | 20 + .../EventCaptureFormPresenter.kt | 88 +- .../EventCaptureFormView.kt | 3 + .../EventCaptureInitialComponent.kt | 8 - .../EventCaptureInitialFragment.kt | 5 - .../EventCaptureInitialModule.kt | 6 - .../injection/EventDispatchers.kt | 12 + .../model/EventCaptureInitialInfo.kt | 2 - .../eventCapture/ui/NonEditableReasonBlock.kt | 103 +++ .../data/EventDetailsRepository.kt | 68 +- .../domain/ConfigureEventCatCombo.kt | 6 +- .../domain/ConfigureEventDetails.kt | 12 +- .../domain/ConfigureEventReportDate.kt | 23 +- .../injection/EventDetailsModule.kt | 17 +- .../models/EventCatComboUiModel.kt | 1 - .../eventDetails/models/EventDetails.kt | 8 +- .../models/EventInputDateUiModel.kt | 9 +- .../providers/EventDetailResourcesProvider.kt | 23 +- .../providers/InputFieldsProvider.kt | 285 +++--- .../eventDetails/ui/EventDetailsFragment.kt | 118 ++- .../eventDetails/ui/EventDetailsViewModel.kt | 51 +- .../eventInitial/EventInitialActivity.java | 45 +- .../eventInitial/EventInitialContract.java | 2 + .../eventInitial/EventInitialModule.java | 47 +- .../eventInitial/EventInitialPresenter.java | 2 +- .../EventInitialRepositoryImpl.java | 45 +- .../dhis2/usescases/login/LoginActivity.kt | 71 +- .../dhis2/usescases/login/LoginContracts.kt | 1 + .../org/dhis2/usescases/login/LoginModule.kt | 13 + .../dhis2/usescases/login/LoginViewModel.kt | 56 +- .../usescases/login/LoginViewModelFactory.kt | 6 + .../login/SyncIsPerformedInteractor.kt | 27 +- .../login/accounts/AccountRepository.kt | 12 + .../login/accounts/AccountsActivity.kt | 59 +- .../login/accounts/AccountsViewModel.kt | 27 + .../login/accounts/ui/AccountsScreen.kt | 16 +- .../dhis2/usescases/login/ui/LoginScreen.kt | 158 ++++ .../usescases/main/HomeRepositoryImpl.kt | 2 +- .../org/dhis2/usescases/main/MainActivity.kt | 9 + .../org/dhis2/usescases/main/MainPresenter.kt | 6 +- .../usescases/main/program/ProgramFragment.kt | 20 +- .../usescases/main/program/ProgramModule.kt | 3 + .../main/program/ProgramPresenter.kt | 9 +- .../main/program/ProgramRepositoryImpl.kt | 8 +- .../dhis2/usescases/main/program/ProgramUi.kt | 121 ++- .../usescases/main/program/ProgramView.kt | 2 - .../main/program/ProgramViewModelMapper.kt | 22 +- .../ProgramEventDetailActivity.kt | 96 +- .../ProgramEventDetailLiveAdapter.kt | 44 +- .../ProgramEventDetailModule.kt | 37 + .../ProgramEventDetailPresenter.kt | 2 +- .../ProgramEventDetailRepository.kt | 1 + .../ProgramEventDetailRepositoryImpl.kt | 8 +- .../ProgramEventDetailView.kt | 2 +- .../ProgramEventDetailViewModel.kt | 37 + .../ProgramEventDetailViewModelFactory.kt | 11 +- .../programEventDetail/ProgramEventMapper.kt | 24 +- .../eventList/EventListFragment.kt | 1 + .../EventListIdlingResourceSingleton.kt | 25 + .../eventList/EventListPresenter.kt | 1 + .../eventList/ui/mapper/EventCardMapper.kt | 14 +- .../usecase/CreateEventUseCase.kt | 39 + .../programStageSelection/ProgramStageData.kt | 9 + .../ProgramStageSelectionActivity.kt | 98 ++- .../ProgramStageSelectionAdapter.kt | 16 +- .../ProgramStageSelectionInjector.kt | 33 +- .../ProgramStageSelectionPresenter.kt | 72 +- .../ProgramStageSelectionRepositoryImpl.kt | 121 +-- .../ProgramStageSelectionView.kt | 5 +- .../ProgramStageSelectionViewHolder.kt | 32 +- .../searchTrackEntity/SearchJavaToCompose.kt | 6 +- .../searchTrackEntity/SearchRepository.java | 28 +- .../SearchRepositoryImpl.java | 308 ++++--- .../SearchRepositoryImplKt.kt | 226 +++++ .../searchTrackEntity/SearchRepositoryKt.kt | 19 + .../searchTrackEntity/SearchTEActivity.java | 197 +++-- .../SearchTEContractsModule.java | 11 +- .../searchTrackEntity/SearchTEIViewModel.kt | 627 ++++++++++--- .../searchTrackEntity/SearchTEModule.java | 100 ++- .../searchTrackEntity/SearchTEPresenter.java | 59 +- .../searchTrackEntity/SearchTEScreenState.kt | 11 - .../usescases/searchTrackEntity/SearchTEUi.kt | 111 +-- .../SearchTeiViewModelFactory.kt | 10 +- .../adapters/BaseTeiViewHolder.kt | 4 +- .../adapters/SearchAdapterDiffCallback.kt | 8 + .../adapters/SearchTeiLiveAdapter.kt | 48 +- .../listView/SearchResult.kt | 2 +- .../listView/SearchResultHolder.kt | 8 +- .../listView/SearchTEList.kt | 153 ++-- .../searchTrackEntity/mapView/SearchTEMap.kt | 17 +- .../SearchParametersScreen.kt | 335 +++++++ .../model/SearchParametersUiState.kt | 30 + .../provider/ParameterSelectorItemProvider.kt | 126 +++ .../ui/SearchScreenConfigurator.kt | 56 +- .../ui/SearchTEListScreen.kt | 99 --- .../searchTrackEntity/ui/SearchTEUi.kt | 424 ++++----- .../ui/mapper/TEICardMapper.kt | 40 +- .../dhis2/usescases/settings/SettingItem.kt | 1 + .../settings/SyncManagerFragment.java | 248 ++++-- .../settings/SyncManagerPresenter.kt | 64 +- .../settings/models/ExportDbModel.kt | 11 + .../usescases/settings/ui/ExportOption.kt | 222 +++++ .../teiDashboard/DashboardProgramModel.kt | 92 ++ .../teiDashboard/DashboardRepository.kt | 23 +- .../teiDashboard/DashboardRepositoryImpl.java | 499 ----------- .../teiDashboard/DashboardRepositoryImpl.kt | 631 +++++++++++++ .../teiDashboard/DashboardViewModel.kt | 149 +++- .../teiDashboard/DashboardViewModelFactory.kt | 3 + .../teiDashboard/TeiDashboardContracts.java | 20 +- .../TeiDashboardMobileActivity.kt | 206 +++-- .../teiDashboard/TeiDashboardModule.kt | 31 +- .../teiDashboard/TeiDashboardPresenter.java | 138 +-- .../adapters/DashboardPagerAdapter.kt | 1 - .../indicators/BaseIndicatorRepository.kt | 103 ++- .../indicators/EventIndicatorRepository.kt | 22 +- .../indicators/IndicatorRepository.kt | 14 +- .../indicators/IndicatorsFragment.kt | 39 +- .../indicators/IndicatorsModule.kt | 8 +- .../indicators/IndicatorsPresenter.kt | 26 +- .../indicators/TrackerAnalyticsRepository.kt | 26 +- .../relationships/RelationshipFragment.kt | 2 +- .../relationships/RelationshipModule.java | 6 +- .../RelationshipRepositoryImpl.kt | 14 +- .../relationships/RelationshipViewHolder.kt | 10 +- .../teidata/DashboardProgramAdapter.kt | 6 +- .../teidata/DashboardProgramViewHolder.kt | 42 +- .../teidata/EventCreationOptionsMapper.kt | 27 +- .../teidata/TEIDataContracts.kt | 40 +- .../teidata/TEIDataFragment.kt | 654 +++++++------- .../teidata/TEIDataModule.kt | 78 +- .../teidata/TEIDataPresenter.kt | 418 ++++----- .../teidata/TeiDataContractHandler.kt | 56 ++ .../teidata/TeiDataRepository.kt | 4 + .../teidata/TeiDataRepositoryImpl.kt | 210 ++++- .../teidata/teievents/EventAdapter.kt | 140 ++- .../teidata/teievents/EventViewHolder.java | 26 +- .../teidata/teievents/StageViewHolder.kt | 155 ++-- .../ToggleStageEventsButtonHolder.kt | 51 ++ .../teievents/ui/mapper/TEIEventCardMapper.kt | 386 ++++++++ .../dialogs/scheduling/SchedulingComponent.kt | 10 + .../dialogs/scheduling/SchedulingDialog.kt | 117 +++ .../dialogs/scheduling/SchedulingDialogUi.kt | 195 +++++ .../dialogs/scheduling/SchedulingModule.kt | 25 + .../dialogs/scheduling/SchedulingViewModel.kt | 180 ++++ .../scheduling/SchedulingViewModelFactory.kt | 29 + .../teiProgramList/EnrollmentViewModel.java | 15 +- .../teiProgramList/TeiProgramListAdapter.java | 2 +- .../TeiProgramListEnrollmentViewHolder.kt | 7 +- .../TeiProgramListInteractor.java | 4 +- .../teiProgramList/TeiProgramListModule.java | 11 +- .../TeiProgramListRepositoryImpl.java | 41 +- .../teiProgramList/ui/EnrollToProgram.kt | 11 +- .../teiDashboard/ui/NewEventOptionsMenu.kt | 6 + .../teiDashboard/ui/TeiDetailDashboard.kt | 54 +- .../teiDashboard/ui/TimelineEventsHeader.kt | 61 ++ .../teiDashboard/ui/mapper/InfoBarMapper.kt | 95 +- .../ui/mapper/TeiDashboardCardMapper.kt | 204 +++-- .../ui/model/TimelineEventsHeaderModel.kt | 9 + .../TroubleshootingInjector.kt | 8 +- .../TroubleshootingRepository.kt | 170 ++-- .../troubleshooting/ui/TroubleshootingUi.kt | 1 + .../ui/TroubleshootingUiPreview.kt | 21 +- .../java/org/dhis2/utils/D2EditionMapper.kt | 33 - .../main/java/org/dhis2/utils/DateUtils.java | 5 + .../java/org/dhis2/utils/HelpManager.java | 71 +- .../category/CategoyOptionComboSource.kt | 2 +- .../customviews/BreakTheGlassBottomDialog.kt | 21 + .../dhis2/utils/customviews/CatOptionPopUp.kt | 4 +- .../utils/customviews/OptionSetCellPopUp.java | 96 -- .../dhis2/utils/customviews/PeriodAdapter.kt | 3 +- .../customviews/PeriodDialogInputPeriod.java | 6 +- .../navigationbar/NavigationPage.kt | 1 - .../NavigationPageConfigurator.kt | 2 - .../granularsync/GranularSyncPresenter.kt | 34 +- .../granularsync/GranularSyncRepository.kt | 2 +- .../utils/granularsync/SyncStatusDialog.kt | 43 +- .../granularsync/SyncStatusDialogNavigator.kt | 3 +- .../granularsync/SyncStatusDialogProvider.kt | 3 + app/src/main/res/drawable/ic_import_db.xml | 9 + .../main/res/drawable/ic_settings_export.xml | 35 + .../layout-land/activity_dashboard_mobile.xml | 8 - .../main/res/layout-land/activity_search.xml | 57 +- .../layout-land/event_details_fragment.xml | 2 +- .../res/layout-land/fragment_tei_data.xml | 62 +- .../res/layout/activity_dashboard_mobile.xml | 9 - .../res/layout/activity_event_capture.xml | 29 +- app/src/main/res/layout/activity_login.xml | 47 +- app/src/main/res/layout/activity_search.xml | 48 +- .../layout/break_the_glass_bottom_dialog.xml | 2 - .../main/res/layout/development_activity.xml | 58 +- .../main/res/layout/enrollment_activity.xml | 18 +- .../res/layout/event_details_fragment.xml | 2 +- .../main/res/layout/fragment_indicators.xml | 4 +- app/src/main/res/layout/fragment_notes.xml | 6 +- app/src/main/res/layout/fragment_program.xml | 10 - .../res/layout/fragment_relationships.xml | 6 +- .../main/res/layout/fragment_search_list.xml | 40 +- app/src/main/res/layout/fragment_settings.xml | 77 +- app/src/main/res/layout/fragment_tei_data.xml | 73 +- .../layout/fragment_tei_data_card_front.xml | 217 +---- .../res/layout/item_dashboard_program.xml | 6 +- app/src/main/res/layout/item_dataset.xml | 5 +- app/src/main/res/layout/item_event.xml | 5 +- .../res/layout/item_search_tracked_entity.xml | 5 +- .../main/res/layout/item_stage_section.xml | 109 --- .../layout/item_tei_programs_enrollment.xml | 6 +- .../item_tei_programs_enrollment_inactive.xml | 6 +- .../res/layout/section_selector_fragment.xml | 12 +- app/src/main/res/menu/dashboard_menu.xml | 4 +- .../main/res/menu/dashboard_menu_group.xml | 4 +- app/src/main/res/menu/dashboard_tei_menu.xml | 2 +- app/src/main/res/values-ar/strings.xml | 26 - app/src/main/res/values-b+uz+Cyrl/strings.xml | 17 - app/src/main/res/values-b+uz+Latn/strings.xml | 24 - app/src/main/res/values-ckb/strings.xml | 2 - app/src/main/res/values-cs/strings.xml | 29 - app/src/main/res/values-es/strings.xml | 29 - app/src/main/res/values-fr/strings.xml | 28 - app/src/main/res/values-id/strings.xml | 27 - app/src/main/res/values-km/strings.xml | 12 - app/src/main/res/values-lo/strings.xml | 26 - app/src/main/res/values-nb/strings.xml | 25 - app/src/main/res/values-nl/strings.xml | 29 - app/src/main/res/values-prs/strings.xml | 1 - app/src/main/res/values-ps/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 25 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sv/strings.xml | 12 - app/src/main/res/values-tg/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 29 - app/src/main/res/values-ur/strings.xml | 1 - app/src/main/res/values-uz/strings.xml | 27 - app/src/main/res/values-vi/strings.xml | 29 - app/src/main/res/values-zh-rCN/strings.xml | 25 - app/src/main/res/values-zh/strings.xml | 29 - app/src/main/res/values/strings.xml | 71 +- .../org/dhis2/bindings/DateExtensionsTest.kt | 87 +- .../org/dhis2/bindings/RuleExtensionsTest.kt | 20 +- .../dhis2/bindings/TEICardExtensionsTest.kt | 12 +- .../CategoryOptionExtensionsKtTest.kt | 2 + .../data/dhislogic/DhisPeriodUtilsTest.kt | 1 + .../dhis2/data/filter/FilterRepositoryTest.kt | 4 +- .../EnrollmentRuleEngineRepositoryTest.kt | 19 - .../dhis2/data/services/SyncPresenterTest.kt | 23 + .../MapTeiEventsToFeatureCollectionTest.kt | 2 + .../map/mocks/RelationshipViewModelDummy.kt | 5 +- .../dataSetTable/DataSetTablePresenterTest.kt | 2 +- .../DataSetDetailRepositoryTest.kt | 2 +- .../EnrollmentFormRepositoryTest.kt | 16 - .../enrollment/EnrollmentPresenterImplTest.kt | 83 +- .../events/ScheduledEventPresenterImplTest.kt | 14 +- .../eventCapture/EventCapturePresenterTest.kt | 86 +- .../EventCaptureRepositoryImplTest.kt | 88 -- .../eventCapture/EventIntegrationTest.kt | 206 ++++- .../domain/ReOpenEventUseCaseTest.kt | 65 ++ .../EventCaptureFormPresenterTest.kt | 27 +- .../EventDetailsIntegrationTest.kt | 10 +- .../data/EventDetailsRepositoryTest.kt | 60 ++ .../domain/ConfigureEventDetailsTest.kt | 17 +- .../domain/ConfigureEventReportDateTest.kt | 2 +- .../eventInitial/EventInitialPresenterTest.kt | 3 +- .../EventInitialRepositoryImplTest.kt | 32 +- .../usescases/login/LoginViewModelTest.kt | 84 +- .../login/SyncIsPerformedInteractorTest.kt | 51 ++ .../main/program/ProgramPresenterTest.kt | 13 +- .../main/program/ProgramRepositoryImplTest.kt | 8 + .../ProgramEventDetailPresenterTest.kt | 4 +- .../ProgramEventMapperTest.kt | 13 +- .../eventList/mapper/EventCardMapperTest.kt | 3 + .../usecase/CreateEventUseCaseTest.kt | 120 +++ .../ProgramStageSelectionPresenterTest.kt | 141 ++- .../SearchTEIViewModelTest.kt | 216 ++++- .../ui/mapper/TEICardMapperTest.kt | 13 +- .../settings/SyncManagerPresenterTest.kt | 26 + .../DashboardRepositoryImplTest.kt | 7 +- .../teiDashboard/DashboardViewModelTest.kt | 190 ++++ .../teiDashboard/TeiDashboardPresenterTest.kt | 229 +---- .../data/TeiDataPresenterTest.kt | 311 ++++++- .../EventIndicatorRepositoryTest.kt | 43 +- .../TrackerAnalyticsRepositoryTest.kt | 60 +- .../scheduling/SchedulingViewModelTest.kt | 53 ++ .../TeiProgramListPresenterTest.kt | 9 +- .../TeiProgramListRepositoryImplTest.kt | 5 +- .../ui/mapper/InfoBarMapperTest.kt | 60 +- .../ui/mapper/TEIDetailMapperTest.kt | 49 +- .../dhis2/utils/filters/FilterManagerTest.kt | 3 + .../granularsync/GranularSyncPresenterTest.kt | 180 ++++ build.gradle.kts | 1 + commons/build.gradle.kts | 2 + commons/src/main/AndroidManifest.xml | 5 +- .../org/dhis2/commons/bindings/Bindings.kt | 26 +- .../dhis2/commons/bindings/FileExtensions.kt | 9 +- .../org/dhis2/commons/bindings/Permissions.kt | 11 + .../dhis2/commons/bindings/SdkExtensions.kt | 7 + .../commons/bindings/TEICardExtensions.kt | 35 +- .../dhis2/commons/bindings/ValueExtensions.kt | 7 +- .../dhis2/commons/data/EnrollmentIconData.kt | 3 + .../org/dhis2/commons/data/EventViewModel.kt | 17 + .../dhis2/commons/data/EventViewModelType.kt | 1 + .../org/dhis2/commons/data/FileHandler.kt | 65 ++ .../dhis2/commons}/data/FormFileProvider.kt | 2 +- .../commons/data/RelationshipViewModel.kt | 3 +- .../dhis2/commons/data/SearchTeiModel.java | 40 +- .../org/dhis2/commons/data/StageSection.kt | 1 + .../org/dhis2/commons/date/DateExtensions.kt | 73 +- .../org/dhis2/commons/date/DateUtils.java | 184 ++++ .../dhis2/commons/dialogs}/PeriodDialog.java | 21 +- .../imagedetail/ImageDetailActivity.kt | 91 ++ .../imagedetail/ImageDetailBottomDialog.kt | 83 -- .../extensions}/CategoryOptionExtensions.kt | 2 +- .../commons/extensions/PictureBindings.kt | 9 + .../data/FeatureConfigRepositoryImpl.kt | 26 +- .../dhis2/commons/filters/FilterResources.kt | 18 +- .../commons/filters/data/FilterRepository.kt | 4 +- .../filters/data/TeiWorkingListScope.kt | 6 + .../dhis2/commons/filters/di/FilterModule.kt | 12 +- .../workingLists/WorkingListChipGroup.kt | 4 +- .../commons/orgunitselector/OUTreeFragment.kt | 15 +- .../dhis2/commons/resources/D2ErrorUtils.kt | 9 +- .../commons/resources}/DhisPeriodUtils.kt | 2 +- .../commons/resources/MetadataIconProvider.kt | 70 ++ .../commons/resources/ResourceManager.kt | 71 ++ .../commons/rules/RuleEngineContextData.kt | 11 + .../commons/rules/RuleEngineExtensions.kt | 12 + .../commons/sync/OnNoConnectionListener.kt | 5 + .../commons/sync/SyncComponentProvider.kt | 1 + .../java/org/dhis2/commons/sync/SyncDialog.kt | 2 + .../dhis2/commons/ui/model/ListCardUiModel.kt | 3 +- .../main/res/drawable/ic_navigate_before.xml | 9 + .../main/res/drawable/ic_navigate_next.xml | 9 + .../src/main/res/layout/dialog_period.xml | 0 .../navigation_dashboard_landscape_menu.xml | 7 - .../res/menu/navigation_dashboard_menu.xml | 7 - .../main/res/menu/navigation_event_menu.xml | 5 - commons/src/main/res/values-ar/strings.xml | 2 - .../src/main/res/values-b+uz+Cyrl/strings.xml | 1 - .../src/main/res/values-b+uz+Latn/strings.xml | 2 - commons/src/main/res/values-cs/strings.xml | 2 - commons/src/main/res/values-es/strings.xml | 2 - commons/src/main/res/values-fr/strings.xml | 2 - commons/src/main/res/values-id/strings.xml | 2 - commons/src/main/res/values-lo/strings.xml | 3 - commons/src/main/res/values-nb/strings.xml | 2 - commons/src/main/res/values-nl/strings.xml | 2 - commons/src/main/res/values-pt/strings.xml | 2 - commons/src/main/res/values-uk/strings.xml | 2 - commons/src/main/res/values-uz/strings.xml | 2 - commons/src/main/res/values-vi/strings.xml | 2 - .../src/main/res/values-zh-rCN/strings.xml | 2 - commons/src/main/res/values-zh/strings.xml | 2 - commons/src/main/res/values/strings.xml | 68 +- compose-table/build.gradle.kts | 1 + .../java/org/dhis2/composetable/TableRobot.kt | 9 + .../composetable/data/TableInputUiData.kt | 3 +- .../dhis2/composetable/ui/TextInputUiTest.kt | 23 +- .../model/ItemColumnHeaderUiState.kt | 2 + .../org/dhis2/composetable/model/TableCell.kt | 1 + .../composetable/model/TextInputModel.kt | 3 + .../org/dhis2/composetable/ui/HeaderCell.kt | 4 +- .../org/dhis2/composetable/ui/ItemValues.kt | 12 +- .../composetable/ui/MultiOptionSelector.kt | 43 + .../org/dhis2/composetable/ui/TableCell.kt | 30 +- .../dhis2/composetable/ui/TableDimensions.kt | 29 +- .../org/dhis2/composetable/ui/TableHeader.kt | 18 +- .../org/dhis2/composetable/ui/TableItemRow.kt | 7 +- .../org/dhis2/composetable/ui/TableTheme.kt | 4 +- .../org/dhis2/composetable/ui/TextInput.kt | 34 + .../ui/extensions/LazyListScopeExtensions.kt | 4 +- dhis2-mobile-program-rules/.gitignore | 1 + dhis2-mobile-program-rules/build.gradle.kts | 41 + dhis2-mobile-program-rules/consumer-rules.pro | 0 dhis2-mobile-program-rules/proguard-rules.pro | 21 + .../src/main/AndroidManifest.xml | 4 + .../mobileProgramRules/EvaluationType.kt | 6 + .../RuleEngineExtensions.kt | 549 ++++++++++++ .../mobileProgramRules/RuleEngineHelper.kt | 133 +++ .../mobileProgramRules/RulesRepository.kt | 340 +++++++ .../maps/carousel/CarouselEventHolder.kt | 27 +- .../dhis2/maps/carousel/CarouselTeiHolder.kt | 4 +- .../org/dhis2/maps/layer/MapLayerDialog.kt | 2 +- .../org/dhis2/maps/layer/MapLayerManager.kt | 9 +- .../maps/layer/basemaps/BaseMapHolder.kt | 3 + .../dhis2/maps/layer/basemaps/BaseMapStyle.kt | 11 +- .../org/dhis2/maps/managers/MapManager.kt | 5 +- .../maps/mapper/EventToEventUiComponent.kt | 1 + .../dhis2/maps/model/EventUiComponentModel.kt | 2 + .../maps/usecases/MapStyleConfiguration.kt | 39 +- .../src/main/res/values-ar/strings.xml | 3 +- .../src/main/res/values-b+uz+Latn/strings.xml | 1 - .../src/main/res/values-cs/strings.xml | 1 - .../src/main/res/values-es/strings.xml | 1 - .../src/main/res/values-fr/strings.xml | 1 - .../src/main/res/values-nb/strings.xml | 1 - .../src/main/res/values-nl/strings.xml | 3 +- .../src/main/res/values-pt/strings.xml | 1 - .../src/main/res/values-uk/strings.xml | 1 - .../src/main/res/values-uz/strings.xml | 1 - .../src/main/res/values-vi/strings.xml | 1 - .../src/main/res/values-zh-rCN/strings.xml | 1 - .../src/main/res/values-zh/strings.xml | 1 - .../src/main/res/values/strings.xml | 1 - .../usecases/MapStyleConfigurationTest.kt | 37 + .../java/dhis2/org/analytics/charts/Charts.kt | 15 +- .../org/analytics/charts/ChartsRepository.kt | 14 +- .../analytics/charts/ChartsRepositoryImpl.kt | 324 +++++-- .../analytics/charts/DhisAnalyticCharts.kt | 23 +- .../charts/bindings/LineListingExtensions.kt | 144 +++ .../dhis2/org/analytics/charts/data/Chart.kt | 13 +- .../org/analytics/charts/data/ChartType.kt | 9 + .../dhis2/org/analytics/charts/data/Graph.kt | 45 +- .../org/analytics/charts/data/GraphFilters.kt | 53 ++ .../org/analytics/charts/di/ChartsInjector.kt | 8 +- .../mappers/AnalyticsTeiSettingsToGraph.kt | 7 +- .../charts/mappers/DataElementToGraph.kt | 7 +- .../mappers/DimensionalResponseToPieData.kt | 3 +- .../mappers/GraphCoordinatesToBarEntry.kt | 2 +- .../charts/mappers/GraphCoordinatesToEntry.kt | 4 +- .../mappers/GraphCoordinatesToPieEntry.kt | 2 +- .../charts/mappers/GraphToBarChart.kt | 2 +- .../charts/mappers/GraphToIndicator.kt | 55 ++ .../charts/mappers/GraphToLineChart.kt | 2 +- .../charts/mappers/GraphToRadarData.kt | 8 +- .../analytics/charts/mappers/GraphToTable.kt | 273 ++++-- .../analytics/charts/mappers/GraphToValue.kt | 53 -- .../charts/mappers/ProgramIndicatorToGraph.kt | 7 +- .../charts/mappers/VisualizationToGraph.kt | 115 ++- .../providers/AnalyticsFilterProvider.kt | 216 ++++- .../providers/ChartCoordinatesProviderImpl.kt | 11 +- .../providers/PeriodStepProviderImpl.kt | 75 +- .../RuleEngineNutritionDataProviderImpl.kt | 3 +- .../analytics/charts/ui/AnalyticsAdapter.kt | 37 +- .../org/analytics/charts/ui/AnalyticsModel.kt | 327 ++++--- .../analytics/charts/ui/ChartViewHolder.kt | 31 +- .../charts/ui/GroupAnalyticsFragment.kt | 50 +- .../charts/ui/GroupAnalyticsViewModel.kt | 40 +- .../charts/ui/IndicatorViewHolder.kt | 90 +- .../charts/ui/dialog/SearchColumnDialog.kt | 158 ++++ .../src/main/res/layout/item_chart.xml | 7 +- .../src/main/res/layout/item_indicator.xml | 82 -- .../src/main/res/menu/chart_menu.xml | 8 +- .../src/main/res/menu/search_column_menu.xml | 19 + .../src/main/res/values/strings.xml | 1 + .../analytics/charts/ChartsRepositoryTest.kt | 77 +- .../org/analytics/charts/data/GraphTest.kt | 8 +- .../mappers/GraphCoordinatesToEntryTest.kt | 11 +- .../providers/AnalyticsFilterProviderTest.kt | 12 +- .../providers/PeriodStepProviderImplTest.kt | 25 +- .../analytics/charts/ui/AnalyticsModelTest.kt | 18 +- form/build.gradle.kts | 1 + .../ui/provider/inputfield/AgeProviderTest.kt | 189 ++++ .../CategorySelectorProviderTest.kt | 128 +++ .../provider/inputfield/DateProviderTest.kt | 34 +- .../inputfield/FieldUiModelFactory.kt | 10 +- form/src/main/AndroidManifest.xml | 1 + .../org/dhis2/form/bindings/RuleExtensions.kt | 366 ++++---- .../form/data/DataEntryBaseRepository.kt | 19 +- .../dhis2/form/data/DataEntryRepository.kt | 5 + .../dhis2/form/data/EnrollmentRepository.kt | 348 ++++---- .../data/EnrollmentRuleEngineRepository.kt | 113 --- .../org/dhis2/form/data/EventRepository.kt | 493 ++++++++++- .../form/data/EventRuleEngineRepository.kt | 96 -- .../org/dhis2/form/data/FormRepository.kt | 1 + .../org/dhis2/form/data/FormRepositoryImpl.kt | 103 ++- .../org/dhis2/form/data/FormValueStore.kt | 175 +++- .../dhis2/form/data/RuleEngineRepository.kt | 7 - .../org/dhis2/form/data/RulesRepository.kt | 150 ++-- .../dhis2/form/data/RulesUtilsProviderImpl.kt | 263 +++--- .../org/dhis2/form/data/SearchRepository.kt | 116 --- .../data/metadata/EnrollmentConfiguration.kt | 143 +++ .../data/metadata/FormBaseConfiguration.kt | 17 + .../main/java/org/dhis2/form/di/Injector.kt | 159 ++-- .../form/extensions/FieldUiModelExtensions.kt | 4 +- .../java/org/dhis2/form/model/ActionType.kt | 1 - .../org/dhis2/form/model/EventCategory.kt | 7 + .../dhis2/form/model/EventCategoryOption.kt | 6 + .../java/org/dhis2/form/model}/EventMode.kt | 3 +- .../java/org/dhis2/form/model/FieldUiModel.kt | 9 + .../org/dhis2/form/model/FieldUiModelImpl.kt | 8 + .../dhis2/form/model/FormRepositoryRecords.kt | 17 +- .../java/org/dhis2/form/model/FormSection.kt | 1 + .../java/org/dhis2/form/model/LegendValue.kt | 8 +- .../form/model/OptionSetConfiguration.kt | 32 +- .../org/dhis2/form/model/PeriodSelector.kt | 10 + .../org/dhis2/form/model/RuleActionError.kt | 8 - .../dhis2/form/model/SectionUiModelImpl.kt | 32 +- .../java/org/dhis2/form/model/UiEventType.kt | 1 + .../main/java/org/dhis2/form/mvi/MviIntent.kt | 3 - .../org/dhis2/form/ui/DataEntryAdapter.kt | 22 +- .../dhis2/form/ui/FieldViewModelFactory.kt | 16 +- .../form/ui/FieldViewModelFactoryImpl.kt | 60 +- form/src/main/java/org/dhis2/form/ui/Form.kt | 115 ++- .../main/java/org/dhis2/form/ui/FormView.kt | 121 +-- .../dhis2/form/ui/FormViewFragmentFactory.kt | 2 - .../java/org/dhis2/form/ui/FormViewModel.kt | 33 +- .../dhis2/form/ui/binding/PictureBindings.kt | 8 +- .../form/ui/dialog/QRDetailBottomDialog.kt | 40 +- .../form/ui/event/RecyclerViewUiEvents.kt | 9 + .../dhis2/form/ui/event/UiEventFactoryImpl.kt | 6 + .../org/dhis2/form/ui/intent/FormIntent.kt | 13 +- .../dhis2/form/ui/mapper/FormSectionMapper.kt | 12 +- .../form/ui/provider/DisplayNameProvider.kt | 2 + .../ui/provider/DisplayNameProviderImpl.kt | 48 +- .../provider/EnrollmentFormLabelsProvider.kt | 22 +- .../ui/provider/LegendValueProviderImpl.kt | 17 + .../ui/provider/inputfield/AgeProvider.kt | 157 ++-- .../inputfield/CategorySelectorProvider.kt | 157 ++++ .../provider/inputfield/CheckBoxProvider.kt | 18 +- .../inputfield/CoordinatesProvider.kt | 4 - .../ui/provider/inputfield/DateProvider.kt | 142 +-- .../provider/inputfield/DropdownProvider.kt | 102 +-- .../ui/provider/inputfield/FieldProvider.kt | 827 ++++++++++-------- .../provider/inputfield/ImageInputProvider.kt | 15 +- .../InputsForTextValueTypeProvider.kt | 49 +- .../inputfield/MatrixInputProvider.kt | 49 +- .../inputfield/MatrixSequentialUtilites.kt | 49 ++ .../inputfield/MultiSelectionInputProvider.kt | 54 ++ .../inputfield/PeriodSelectorProvider.kt | 61 ++ .../inputfield/RadioButtonProvider.kt | 12 +- .../inputfield/SequentialInputProvider.kt | 49 +- .../provider/inputfield/SignatureProvider.kt | 51 +- .../ui/provider/inputfield/SwitchProvider.kt | 3 + .../inputfield/UnitIntervalInputProvider.kt | 15 +- .../ui/style/FormUiModelColorFactoryImpl.kt | 25 +- .../ui/style/LongTextUiColorFactoryImpl.kt | 21 +- form/src/main/res/values-ar/strings.xml | 5 - .../src/main/res/values-b+uz+Latn/strings.xml | 5 - form/src/main/res/values-cs/strings.xml | 5 - form/src/main/res/values-es/strings.xml | 5 - form/src/main/res/values-fr/strings.xml | 5 - form/src/main/res/values-id/strings.xml | 5 - form/src/main/res/values-lo/strings.xml | 4 - form/src/main/res/values-nb/strings.xml | 5 - form/src/main/res/values-nl/strings.xml | 5 - form/src/main/res/values-pt/strings.xml | 5 - form/src/main/res/values-uk/strings.xml | 5 - form/src/main/res/values-uz/strings.xml | 5 - form/src/main/res/values-vi/strings.xml | 5 - form/src/main/res/values-zh-rCN/strings.xml | 5 - form/src/main/res/values-zh/strings.xml | 7 +- form/src/main/res/values/strings.xml | 13 +- .../form/data/EnrollmentRepositoryTest.kt | 109 +++ .../dhis2/form/data/EventRepositoryTest.kt | 206 +++++ .../org/dhis2/form/data/FieldUiModelTest.kt | 15 +- .../dhis2/form/data/FormRepositoryImplTest.kt | 88 +- .../data/FormRepositoryIntegrationTest.kt | 193 ++++ .../org/dhis2/form/data/FormValueStoreTest.kt | 130 ++- .../form/data/RulesUtilsProviderImplTest.kt | 279 ++++-- .../model/OptionSetDialogViewModelTest.kt | 3 + .../form/ui/FieldViewModelFactoryImplTest.kt | 29 +- .../provider/DisplayNameProviderImplTest.kt | 3 + gradle/libs.versions.toml | 18 +- release.info | 3 - scripts/browserstackJenkins.sh | 5 +- scripts/browserstackJenkinsCompose.sh | 3 +- scripts/browserstackJenkinsForm.sh | 3 +- scripts/generateReleaseNotes.py | 7 +- settings.gradle.kts | 1 + .../android/rtsm/services/StockManagerImpl.kt | 12 +- .../rules/RuleValidationHelperImpl.kt | 137 +-- .../android/rtsm/ui/home/HomeActivity.kt | 2 + .../android/rtsm/ui/home/HomeViewModel.kt | 5 + .../ui/managestock/ManageStockViewModel.kt | 18 +- .../dhis2/android/rtsm/utils/DebugUtils.kt | 36 +- .../android/rtsm/utils/RuleEngineHelper.kt | 29 +- .../android/rtsm/utils/RuleExtensions.kt | 146 ++-- .../android/rtsm/services/ProgramRuleTests.kt | 269 +++--- .../main/java/org/dhis2/ui/MetadataIcon.kt | 78 +- .../java/org/dhis2/ui/MetadataIconData.kt | 32 +- .../bottomsheet/BottomSheetDialogContent.kt | 2 +- .../bottomsheet/DeleteBottomSheetDialog.kt | 105 +++ .../dialogs/bottomsheet/DialogButtonStyle.kt | 2 +- .../src/main/java/org/dhis2/ui/theme/Type.kt | 14 + .../main/res/drawable/image_not_supported.xml | 4 + 685 files changed, 21964 insertions(+), 12874 deletions(-) create mode 100644 app/src/androidTest/java/org/dhis2/LazyActivityScenarioRule.kt create mode 100644 app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt create mode 100644 app/src/androidTest/java/org/dhis2/usescases/teidashboard/dialogs/scheduling/SchedulingDialogUiTest.kt delete mode 100644 app/src/main/java/org/dhis2/bindings/ValidationStrategyExtensions.kt delete mode 100644 app/src/main/java/org/dhis2/data/dhislogic/DhisEventUtils.kt delete mode 100644 app/src/main/java/org/dhis2/data/forms/EnrollmentFormRepository.java create mode 100644 app/src/main/java/org/dhis2/data/forms/EnrollmentFormRepository.kt delete mode 100644 app/src/main/java/org/dhis2/data/forms/dataentry/EnrollmentRuleEngineRepository.java delete mode 100644 app/src/main/java/org/dhis2/data/forms/dataentry/RuleEngineRepository.java create mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/EventIdlingResourceSingleton.kt delete mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureContract.java create mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureContract.kt delete mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureFieldProvider.kt delete mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventRuleEngineRepository.java create mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ReOpenEventUseCase.kt delete mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventInitialFragment/EventCaptureInitialComponent.kt delete mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventInitialFragment/EventCaptureInitialFragment.kt delete mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventInitialFragment/EventCaptureInitialModule.kt create mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/injection/EventDispatchers.kt create mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/ui/NonEditableReasonBlock.kt create mode 100644 app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt create mode 100644 app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListIdlingResourceSingleton.kt create mode 100644 app/src/main/java/org/dhis2/usescases/programEventDetail/usecase/CreateEventUseCase.kt create mode 100644 app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageData.kt create mode 100644 app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt create mode 100644 app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryKt.kt create mode 100644 app/src/main/java/org/dhis2/usescases/searchTrackEntity/searchparameters/SearchParametersScreen.kt create mode 100644 app/src/main/java/org/dhis2/usescases/searchTrackEntity/searchparameters/model/SearchParametersUiState.kt create mode 100644 app/src/main/java/org/dhis2/usescases/searchTrackEntity/searchparameters/provider/ParameterSelectorItemProvider.kt delete mode 100644 app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEListScreen.kt create mode 100644 app/src/main/java/org/dhis2/usescases/settings/models/ExportDbModel.kt create mode 100644 app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt create mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardProgramModel.kt delete mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.java create mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt create mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataContractHandler.kt create mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ToggleStageEventsButtonHolder.kt create mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt create mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingComponent.kt create mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt create mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialogUi.kt create mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingModule.kt create mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt create mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelFactory.kt create mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TimelineEventsHeader.kt create mode 100644 app/src/main/java/org/dhis2/usescases/teiDashboard/ui/model/TimelineEventsHeaderModel.kt delete mode 100644 app/src/main/java/org/dhis2/utils/D2EditionMapper.kt delete mode 100644 app/src/main/java/org/dhis2/utils/customviews/OptionSetCellPopUp.java create mode 100644 app/src/main/res/drawable/ic_import_db.xml create mode 100644 app/src/main/res/drawable/ic_settings_export.xml delete mode 100644 app/src/main/res/layout/item_stage_section.xml delete mode 100644 app/src/test/java/org/dhis2/data/forms/dataentry/EnrollmentRuleEngineRepositoryTest.kt create mode 100644 app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ReOpenEventUseCaseTest.kt create mode 100644 app/src/test/java/org/dhis2/usescases/programEventDetail/usecase/CreateEventUseCaseTest.kt create mode 100644 app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt create mode 100644 app/src/test/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelTest.kt create mode 100644 commons/src/main/java/org/dhis2/commons/bindings/Permissions.kt create mode 100644 commons/src/main/java/org/dhis2/commons/data/FileHandler.kt rename {form/src/main/java/org/dhis2/form => commons/src/main/java/org/dhis2/commons}/data/FormFileProvider.kt (88%) rename {app/src/main/java/org/dhis2/utils/customviews => commons/src/main/java/org/dhis2/commons/dialogs}/PeriodDialog.java (91%) create mode 100644 commons/src/main/java/org/dhis2/commons/dialogs/imagedetail/ImageDetailActivity.kt delete mode 100644 commons/src/main/java/org/dhis2/commons/dialogs/imagedetail/ImageDetailBottomDialog.kt rename {app/src/main/java/org/dhis2/data/dhislogic => commons/src/main/java/org/dhis2/commons/extensions}/CategoryOptionExtensions.kt (94%) create mode 100644 commons/src/main/java/org/dhis2/commons/extensions/PictureBindings.kt rename {app/src/main/java/org/dhis2/data/dhislogic => commons/src/main/java/org/dhis2/commons/resources}/DhisPeriodUtils.kt (99%) create mode 100644 commons/src/main/java/org/dhis2/commons/resources/MetadataIconProvider.kt create mode 100644 commons/src/main/java/org/dhis2/commons/rules/RuleEngineContextData.kt create mode 100644 commons/src/main/java/org/dhis2/commons/rules/RuleEngineExtensions.kt create mode 100644 commons/src/main/java/org/dhis2/commons/sync/OnNoConnectionListener.kt create mode 100644 commons/src/main/res/drawable/ic_navigate_before.xml create mode 100644 commons/src/main/res/drawable/ic_navigate_next.xml rename {app => commons}/src/main/res/layout/dialog_period.xml (100%) create mode 100644 compose-table/src/main/java/org/dhis2/composetable/ui/MultiOptionSelector.kt create mode 100644 dhis2-mobile-program-rules/.gitignore create mode 100644 dhis2-mobile-program-rules/build.gradle.kts create mode 100644 dhis2-mobile-program-rules/consumer-rules.pro create mode 100644 dhis2-mobile-program-rules/proguard-rules.pro create mode 100644 dhis2-mobile-program-rules/src/main/AndroidManifest.xml create mode 100644 dhis2-mobile-program-rules/src/main/java/org/dhis2/mobileProgramRules/EvaluationType.kt create mode 100644 dhis2-mobile-program-rules/src/main/java/org/dhis2/mobileProgramRules/RuleEngineExtensions.kt create mode 100644 dhis2-mobile-program-rules/src/main/java/org/dhis2/mobileProgramRules/RuleEngineHelper.kt create mode 100644 dhis2-mobile-program-rules/src/main/java/org/dhis2/mobileProgramRules/RulesRepository.kt create mode 100644 dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/bindings/LineListingExtensions.kt create mode 100644 dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/data/GraphFilters.kt create mode 100644 dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/mappers/GraphToIndicator.kt delete mode 100644 dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/mappers/GraphToValue.kt create mode 100644 dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/dialog/SearchColumnDialog.kt delete mode 100644 dhis_android_analytics/src/main/res/layout/item_indicator.xml create mode 100644 dhis_android_analytics/src/main/res/menu/search_column_menu.xml create mode 100644 form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt create mode 100644 form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/CategorySelectorProviderTest.kt delete mode 100644 form/src/main/java/org/dhis2/form/data/EnrollmentRuleEngineRepository.kt delete mode 100644 form/src/main/java/org/dhis2/form/data/EventRuleEngineRepository.kt delete mode 100644 form/src/main/java/org/dhis2/form/data/RuleEngineRepository.kt delete mode 100644 form/src/main/java/org/dhis2/form/data/SearchRepository.kt create mode 100644 form/src/main/java/org/dhis2/form/data/metadata/EnrollmentConfiguration.kt create mode 100644 form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt create mode 100644 form/src/main/java/org/dhis2/form/model/EventCategory.kt create mode 100644 form/src/main/java/org/dhis2/form/model/EventCategoryOption.kt rename {app/src/main/java/org/dhis2/utils => form/src/main/java/org/dhis2/form/model}/EventMode.kt (51%) create mode 100644 form/src/main/java/org/dhis2/form/model/PeriodSelector.kt delete mode 100644 form/src/main/java/org/dhis2/form/model/RuleActionError.kt delete mode 100644 form/src/main/java/org/dhis2/form/mvi/MviIntent.kt create mode 100644 form/src/main/java/org/dhis2/form/ui/provider/inputfield/CategorySelectorProvider.kt create mode 100644 form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixSequentialUtilites.kt create mode 100644 form/src/main/java/org/dhis2/form/ui/provider/inputfield/MultiSelectionInputProvider.kt create mode 100644 form/src/main/java/org/dhis2/form/ui/provider/inputfield/PeriodSelectorProvider.kt create mode 100644 form/src/test/java/org/dhis2/form/data/EnrollmentRepositoryTest.kt create mode 100644 form/src/test/java/org/dhis2/form/data/EventRepositoryTest.kt create mode 100644 form/src/test/java/org/dhis2/form/data/FormRepositoryIntegrationTest.kt delete mode 100644 release.info create mode 100644 ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/DeleteBottomSheetDialog.kt create mode 100644 ui-components/src/main/res/drawable/image_not_supported.xml diff --git a/.github/workflows/build-release-candidate.yml b/.github/workflows/build-release-candidate.yml index f6bd368099..df7609ec4a 100644 --- a/.github/workflows/build-release-candidate.yml +++ b/.github/workflows/build-release-candidate.yml @@ -1,18 +1,17 @@ +# This is a basic workflow that is manually triggered + name: Build Release Candidate +env: + # The name of the main module repository + main_project_module: app + +# Controls when the action will run. Workflow runs when manually triggered using the UI +# or API. on: workflow_dispatch: - # Inputs the workflow accepts. - inputs: - name: - # Friendly description to be shown in the UI instead of 'name' - description: 'Person to greet' - # Default value if no value is explicitly provided - default: 'World' - # Input has to be provided for the workflow to run - required: true - # The data type of the input - type: string + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "greet" greet: @@ -22,5 +21,45 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: # Runs a single command using the runners shell - - name: Send greeting - run: echo "Hello ${{ inputs.name }}" + - uses: actions/checkout@v3 + + # Set Repository Name As Env Variable + - name: Set repository name as env variable + run: echo "repository_name=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_ENV + + - name: Set Up JDK + uses: actions/setup-java@v3 + with: + distribution: 'zulu' # See 'Supported distributions' for available options + java-version: '17' + cache: 'gradle' + + - name: Change wrapper permissions + run: chmod +x ./gradlew + + - name: Decode Keystore + id: decode_keystore + uses: timheuer/base64-to-file@v1 + with: + fileName: 'dhis_keystore.jks' + encodedString: ${{ secrets.KEYSTORE }} + - name: build prod + run: ./gradlew app:assembleDhisRelease + env: + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + SIGNING_KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + SIGNING_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + SIGNING_STORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + SIGNING_KEYSTORE_PATH: ${{ steps.decode_keystore.outputs.filePath }} + + - name: Read version name from file + working-directory: ./gradle + id: read-version + run: echo "::set-output name=vName::$(grep 'vName' libs.versions.toml | awk -F' = ' '{print $2}' | tr -d '"')" + + # Upload Artifact Build + - name: Upload Android artifacts + uses: actions/upload-artifact@v3 + with: + name: ${{ env.repository_name }} - Android APK + path: ${{ env.main_project_module }}/build/outputs/apk/dhis/release/dhis2-v${{ steps.read-version.outputs.vName }}-dhis-release.apk diff --git a/.github/workflows/release-start.yml b/.github/workflows/release-start.yml index bc85cca7c0..538e250e51 100644 --- a/.github/workflows/release-start.yml +++ b/.github/workflows/release-start.yml @@ -39,15 +39,6 @@ jobs: git config user.name "GitHub Actions Bot" git config user.email "" - - name: Run Python script to update base branch version - run: python scripts/updateVersionName.py ${{ inputs.development_version_name }} - - - name: Commit and Push Changes - run: | - git add . - git commit -m "Update version to ${{ inputs.development_version_name }}" - git push - # override vName with new version - name: Create release branch run: git checkout -b release/${{ inputs.release_version_name }} @@ -60,3 +51,39 @@ jobs: git add . git commit -m "Update version to ${{ inputs.release_version_name }}" git push origin release/${{ inputs.release_version_name }} + + update_version: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.12.1 + + - name: setup git config + run: | + # setup the username and email. + git config user.name "GitHub Actions Bot" + git config user.email "" + + - name: Create release branch + run: git checkout -b update_version_to${{ inputs.development_version_name }} + + - name: Run Python script to update base branch version + run: python scripts/updateVersionName.py ${{ inputs.development_version_name }} + + - name: Commit and Push Changes + run: | + git add . + git commit -m "Update version to ${{ inputs.development_version_name }}" + git push origin update_version_to${{ inputs.development_version_name }} + + - name: create pull request + run: gh pr create -B develop -H update_version_to${{ inputs.development_version_name }} --title 'Merge update_version_to${{ inputs.development_version_name }} into develop' --body 'Created by Github action' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Jenkinsfile b/Jenkinsfile index c9ac08f86d..5c8ba67d08 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -55,6 +55,7 @@ pipeline { BROWSERSTACK = credentials('android-browserstack') form_apk = sh(returnStdout: true, script: 'find form/build/outputs -iname "*.apk" | sed -n 1p') form_apk_path = "${env.WORKSPACE}/${form_apk}" + buildTag = "${env.GIT_BRANCH} - form" } steps { dir("${env.WORKSPACE}/scripts"){ @@ -71,6 +72,7 @@ pipeline { BROWSERSTACK = credentials('android-browserstack') compose_table_apk = sh(returnStdout: true, script: 'find compose-table/build/outputs -iname "*.apk" | sed -n 1p') compose_table_apk_path = "${env.WORKSPACE}/${compose_table_apk}" + buildTag = "${env.GIT_BRANCH} - table" } steps { dir("${env.WORKSPACE}/scripts"){ @@ -89,6 +91,7 @@ pipeline { test_apk = sh(returnStdout: true, script: 'find app/build/outputs -iname "*.apk" | sed -n 2p') app_apk_path = "${env.WORKSPACE}/${app_apk}" test_apk_path = "${env.WORKSPACE}/${test_apk}" + buildTag = "${env.GIT_BRANCH}" } steps { dir("${env.WORKSPACE}/scripts"){ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1057264be8..535bb1ac95 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,6 +35,17 @@ android { } } + signingConfigs { + create("release"){ + keyAlias = System.getenv("SIGNING_KEY_ALIAS") + keyPassword = System.getenv("SIGNING_KEY_PASSWORD") + System.getenv("SIGNING_KEYSTORE_PATH")?.let {path-> + storeFile = file(path) + } + storePassword = System.getenv("SIGNING_STORE_PASSWORD") + } + } + testOptions { execution = "ANDROIDX_TEST_ORCHESTRATOR" unitTests { @@ -143,6 +154,7 @@ android { getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" ) + signingConfig = signingConfigs.getByName("release") buildConfigField("int", "MATOMO_ID", "1") buildConfigField("String", "BUILD_DATE", "\"" + getBuildDate() + "\"") buildConfigField("String", "GIT_SHA", "\"" + getCommitHash() + "\"") @@ -226,6 +238,7 @@ dependencies { implementation(project(":dhis2_android_maps")) implementation(project(":compose-table")) implementation(project(":stock-usecase")) + implementation(project(":dhis2-mobile-program-rules")) implementation(libs.security.conscrypt) implementation(libs.security.rootbeer) @@ -255,7 +268,6 @@ dependencies { implementation(libs.analytics.customactivityoncrash) implementation(platform(libs.dispatcher.dispatchBOM)) implementation(libs.dispatcher.dispatchCore) - implementation(libs.dhis2.mobile.designsystem) coreLibraryDesugaring(libs.desugar) @@ -307,4 +319,4 @@ dependencies { androidTestImplementation(libs.test.compose.ui.test) androidTestImplementation(libs.test.hamcrest) androidTestImplementation(libs.dispatcher.dispatchEspresso) -} +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/LazyActivityScenarioRule.kt b/app/src/androidTest/java/org/dhis2/LazyActivityScenarioRule.kt new file mode 100644 index 0000000000..4b70ab3220 --- /dev/null +++ b/app/src/androidTest/java/org/dhis2/LazyActivityScenarioRule.kt @@ -0,0 +1,68 @@ +package org.dhis2 + +import android.app.Activity +import android.content.Intent +import androidx.test.core.app.ActivityScenario +import org.junit.rules.ExternalResource + +class LazyActivityScenarioRule : ExternalResource { + + constructor(launchActivity: Boolean, startActivityIntentSupplier: () -> Intent) { + this.launchActivity = launchActivity + scenarioSupplier = { ActivityScenario.launch(startActivityIntentSupplier()) } + } + + constructor(launchActivity: Boolean, startActivityIntent: Intent) { + this.launchActivity = launchActivity + scenarioSupplier = { ActivityScenario.launch(startActivityIntent) } + } + + constructor(launchActivity: Boolean, startActivityClass: Class) { + this.launchActivity = launchActivity + scenarioSupplier = { ActivityScenario.launch(startActivityClass) } + } + + private var launchActivity: Boolean + + private var scenarioSupplier: () -> ActivityScenario + + private var scenario: ActivityScenario? = null + + private var scenarioLaunched: Boolean = false + + override fun before() { + if (launchActivity) { + launch() + } + } + + override fun after() { + scenario?.close() + } + + fun launch(newIntent: Intent? = null) { + if (scenarioLaunched) throw IllegalStateException("Scenario has already been launched!") + + newIntent?.let { scenarioSupplier = { ActivityScenario.launch(it) } } + + scenario = scenarioSupplier() + scenarioLaunched = true + } + + fun getScenario(): ActivityScenario = checkNotNull(scenario) +} + +inline fun lazyActivityScenarioRule( + launchActivity: Boolean = true, + noinline intentSupplier: () -> Intent +): LazyActivityScenarioRule = + LazyActivityScenarioRule(launchActivity, intentSupplier) + +inline fun lazyActivityScenarioRule( + launchActivity: Boolean = true, + intent: Intent? = null +): LazyActivityScenarioRule = if (intent == null) { + LazyActivityScenarioRule(launchActivity, A::class.java) +} else { + LazyActivityScenarioRule(launchActivity, intent) +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/common/filters/FiltersRobot.kt b/app/src/androidTest/java/org/dhis2/common/filters/FiltersRobot.kt index ca436902ab..de1e6693b7 100644 --- a/app/src/androidTest/java/org/dhis2/common/filters/FiltersRobot.kt +++ b/app/src/androidTest/java/org/dhis2/common/filters/FiltersRobot.kt @@ -10,6 +10,7 @@ import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withId import org.dhis2.R import org.dhis2.common.BaseRobot @@ -55,7 +56,7 @@ class FiltersRobot : BaseRobot() { } fun selectNotSyncedState() { - onView(withId(R.id.stateNotSynced)).perform(click()) + onView( withId(R.id.stateNotSynced)).perform(click()) } fun acceptDateSelected() { diff --git a/app/src/androidTest/java/org/dhis2/common/matchers/ChartMatchers.kt b/app/src/androidTest/java/org/dhis2/common/matchers/ChartMatchers.kt index beadcb5fe5..5b80f30c8d 100644 --- a/app/src/androidTest/java/org/dhis2/common/matchers/ChartMatchers.kt +++ b/app/src/androidTest/java/org/dhis2/common/matchers/ChartMatchers.kt @@ -27,11 +27,10 @@ class ChartMatchers { return when (chartType){ ChartType.LINE_CHART -> view is LineChart ChartType.BAR_CHART -> view is BarChart - ChartType.TABLE -> view is ComposeView + ChartType.TABLE, ChartType.LINE_LISTING -> view is ComposeView ChartType.SINGLE_VALUE -> view.findViewById(R.id.singleValueTitle) != null ChartType.NUTRITION -> view is LineChart ChartType.RADAR -> view is RadarChart - ChartType.RADAR -> view is RadarChart ChartType.PIE_CHART -> view is PieChart } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt b/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt index f1d8e26584..5f17dd4292 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt @@ -23,6 +23,9 @@ import org.dhis2.commons.idlingresource.CountingIdlingResourceSingleton import org.dhis2.commons.idlingresource.SearchIdlingResourceSingleton import org.dhis2.commons.prefs.Preference import org.dhis2.form.ui.idling.FormCountingIdlingResource +import org.dhis2.usescases.eventsWithoutRegistration.EventIdlingResourceSingleton +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui.EventDetailIdlingResourceSingleton +import org.dhis2.usescases.programEventDetail.eventList.EventListIdlingResourceSingleton import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataIdlingResourceSingleton import org.junit.After import org.junit.Before @@ -51,7 +54,7 @@ open class BaseTest { android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.CAMERA ) - }else { + } else { GrantPermissionRule.grant( android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.CAMERA, @@ -78,20 +81,26 @@ open class BaseTest { private fun registerCountingIdlingResource() { IdlingRegistry.getInstance().register( + EventListIdlingResourceSingleton.countingIdlingResource, CountingIdlingResourceSingleton.countingIdlingResource, FormCountingIdlingResource.countingIdlingResource, SearchIdlingResourceSingleton.countingIdlingResource, - TeiDataIdlingResourceSingleton.countingIdlingResource + TeiDataIdlingResourceSingleton.countingIdlingResource, + EventIdlingResourceSingleton.countingIdlingResource, + EventDetailIdlingResourceSingleton.countingIdlingResource, ) } private fun unregisterCountingIdlingResource() { IdlingRegistry.getInstance() .unregister( + EventListIdlingResourceSingleton.countingIdlingResource, CountingIdlingResourceSingleton.countingIdlingResource, FormCountingIdlingResource.countingIdlingResource, SearchIdlingResourceSingleton.countingIdlingResource, - TeiDataIdlingResourceSingleton.countingIdlingResource + TeiDataIdlingResourceSingleton.countingIdlingResource, + EventIdlingResourceSingleton.countingIdlingResource, + EventDetailIdlingResourceSingleton.countingIdlingResource, ) } @@ -129,7 +138,7 @@ open class BaseTest { preferencesRobot.saveValue(Preference.DATE_PICKER, true) } - private fun closeKeyboard(){ + private fun closeKeyboard() { BaseRobot().closeKeyboard() } @@ -163,6 +172,12 @@ open class BaseTest { preferencesRobot.saveValue(Feature.COMPOSE_FORMS.name, false) } + + fun enableComposeForms() { + preferencesRobot.saveValue("SET_FROM_DEVELOPMENT", true) + preferencesRobot.saveValue(Feature.COMPOSE_FORMS.name, true) + } + companion object { @ClassRule @JvmField diff --git a/app/src/androidTest/java/org/dhis2/usescases/UseCaseTestsSuite.kt b/app/src/androidTest/java/org/dhis2/usescases/UseCaseTestsSuite.kt index 0b60fdfb25..a0e85ac6b7 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/UseCaseTestsSuite.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/UseCaseTestsSuite.kt @@ -5,7 +5,6 @@ import org.dhis2.usescases.datasets.DataSetTest import org.dhis2.usescases.enrollment.EnrollmentTest import org.dhis2.usescases.event.EventTest import org.dhis2.usescases.filters.FilterTest -import org.dhis2.usescases.form.FormTest import org.dhis2.usescases.jira.JiraTest import org.dhis2.usescases.login.LoginTest import org.dhis2.usescases.main.MainTest @@ -15,6 +14,7 @@ import org.dhis2.usescases.searchte.SearchTETest import org.dhis2.usescases.settings.SettingsTest import org.dhis2.usescases.sync.SyncActivityTest import org.dhis2.usescases.teidashboard.TeiDashboardTest +import org.dhis2.usescases.teidashboard.dialogs.scheduling.SchedulingDialogUiTest import org.junit.runner.RunWith import org.junit.runners.Suite @@ -33,6 +33,7 @@ import org.junit.runners.Suite SearchTETest::class, SettingsTest::class, SyncActivityTest::class, - TeiDashboardTest::class + TeiDashboardTest::class, + SchedulingDialogUiTest::class, ) class UseCaseTestsSuite diff --git a/app/src/androidTest/java/org/dhis2/usescases/event/EventIntents.kt b/app/src/androidTest/java/org/dhis2/usescases/event/EventIntents.kt index 5b5998535d..0b8ab3b41e 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/event/EventIntents.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/event/EventIntents.kt @@ -1,7 +1,10 @@ package org.dhis2.usescases.event import android.content.Intent -import androidx.test.rule.ActivityTestRule +import androidx.test.core.app.ApplicationProvider +import org.dhis2.LazyActivityScenarioRule +import org.dhis2.commons.Constants +import org.dhis2.form.model.EventMode import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity import org.dhis2.usescases.eventsWithoutRegistration.eventInitial.EventInitialActivity import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity @@ -22,33 +25,47 @@ const val PROGRAM_STAGE_TO_SHARE = "EPEcjy3FWmI" const val TEI_TO_UPDATE_UID = "LxMVYhJm3Jp" const val ENROLLMENT_TO_UPDATE_UID = "awZ5RHoJin5" -fun prepareEventDetailsIntentAndLaunchActivity(rule: ActivityTestRule) { - Intent().apply { +fun prepareEventDetailsIntentAndLaunchActivity(rule: LazyActivityScenarioRule) { + Intent( + ApplicationProvider.getApplicationContext(), + EventCaptureActivity::class.java, + ).apply { putExtra(PROGRAM_UID, PROGRAM_XX_TRACKER_UID) putExtra(EVENT_UID, EVENT_DETAILS_UID) - }.also { rule.launchActivity(it) } + putExtra(Constants.EVENT_MODE, EventMode.CHECK) + + }.also { rule.launch(it) } } -fun prepareEventToDeleteIntentAndLaunchActivity(ruleTeiDashboard: ActivityTestRule) { - Intent().apply { +fun prepareEventToDeleteIntentAndLaunchActivity(ruleTeiDashboard: LazyActivityScenarioRule) { + Intent( + ApplicationProvider.getApplicationContext(), + TeiDashboardMobileActivity::class.java, + ).apply { putExtra(PROGRAM_UID, PROGRAM_TB_UID) putExtra(TEI_UID, TEI_EVENT_TO_DELETE_UID) putExtra(ENROLLMENT_UID, ENROLLMENT_EVENT_DELETE_UID) - }.also { ruleTeiDashboard.launchActivity(it) } + }.also { ruleTeiDashboard.launch(it) } } -fun prepareEventToShareIntentAndLaunchActivity(ruleEventDetail: ActivityTestRule) { - Intent().apply { +fun prepareEventToShareIntentAndLaunchActivity(ruleEventDetail: LazyActivityScenarioRule) { + Intent( + ApplicationProvider.getApplicationContext(), + EventInitialActivity::class.java, + ).apply { putExtra(PROGRAM_UID, PROGRAM_TB_UID) putExtra(EVENT_UID, EVENT_TO_SHARE_UID) putExtra(PROGRAM_STAGE_UID, PROGRAM_STAGE_TO_SHARE) - }.also { ruleEventDetail.launchActivity(it) } + }.also { ruleEventDetail.launch(it) } } -fun prepareEventToUpdateIntentAndLaunchActivity(ruleTeiDashboard: ActivityTestRule) { - Intent().apply { +fun prepareEventToUpdateIntentAndLaunchActivity(ruleTeiDashboard: LazyActivityScenarioRule) { + Intent( + ApplicationProvider.getApplicationContext(), + TeiDashboardMobileActivity::class.java, + ).apply { putExtra(PROGRAM_UID, PROGRAM_TB_UID) putExtra(TEI_UID, TEI_TO_UPDATE_UID) putExtra(ENROLLMENT_UID, ENROLLMENT_TO_UPDATE_UID) - }.also { ruleTeiDashboard.launchActivity(it) } -} \ No newline at end of file + }.also { ruleTeiDashboard.launch(it) } +} diff --git a/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt index 56cb29fa1d..c13f41c502 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt @@ -3,17 +3,15 @@ package org.dhis2.usescases.event import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performScrollTo import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withSubstring import androidx.test.espresso.matcher.ViewMatchers.withText import org.dhis2.R import org.dhis2.common.BaseRobot import org.dhis2.common.matchers.hasCompletedPercentage -import org.dhis2.usescases.event.entity.EventDetailsUIModel -import org.hamcrest.CoreMatchers.allOf fun eventRegistrationRobot(eventRegistrationRobot: EventRegistrationRobot.() -> Unit) { EventRegistrationRobot().apply { @@ -23,19 +21,6 @@ fun eventRegistrationRobot(eventRegistrationRobot: EventRegistrationRobot.() -> class EventRegistrationRobot : BaseRobot() { - fun checkEventFormDetails(eventDetails: EventDetailsUIModel) { - onView(withId(R.id.programStageName)).check(matches(withText(eventDetails.programStage))) - onView(withId(R.id.completion)).check(matches(hasCompletedPercentage(eventDetails.completedPercentage))) - onView(withId(R.id.eventSecundaryInfo)).check( - matches( - allOf( - withSubstring(eventDetails.eventDate), - withSubstring(eventDetails.orgUnit) - ) - ) - ) - } - fun openMenuMoreOptions() { onView(withId(R.id.moreOptions)).perform(click()) } @@ -44,16 +29,10 @@ class EventRegistrationRobot : BaseRobot() { onView(withText(R.string.delete)).perform(click()) } - fun clickOnDetails() { - onView(withId(R.id.navigation_details)).perform(click()) - } - - fun checkEventDetails(eventDetails: EventDetailsUIModel, composeTestRule: ComposeTestRule) { - onView(withId(R.id.detailsStageName)).check(matches(withText(eventDetails.programStage))) - onView(withId(R.id.completion)).check(matches(hasCompletedPercentage(eventDetails.completedPercentage))) - - composeTestRule.onNodeWithText(formatStoredDateToUI(eventDetails.eventDate)).assertIsDisplayed() - composeTestRule.onNodeWithText(eventDetails.orgUnit).assertIsDisplayed() + fun checkEventDataEntryIsOpened(completion: Int, email: String, composeTestRule: ComposeTestRule) { + onView(withId(R.id.completion)).check(matches(hasCompletedPercentage(completion))) + composeTestRule.onNodeWithText(email).performScrollTo() + composeTestRule.onNodeWithText(email).assertIsDisplayed() } fun clickOnShare() { @@ -77,34 +56,7 @@ class EventRegistrationRobot : BaseRobot() { onView(withId(R.id.possitive)).perform(click()) } - fun clickLocationButton() { - onView(withId(R.id.location1)).perform(click()) - } - - fun selectOrgUnit(orgUnitName: String) { - onView(withId(R.id.org_unit)).perform(click()) - onView(withText(orgUnitName)).perform(click()) - } - fun clickNextButton() { waitForView(withId(R.id.action_button)).perform(click()) } - - private fun formatStoredDateToUI(dateValue: String): String { - val components = dateValue.split("/") - - val year = components[2] - val month = if (components[1].length == 1) { - "0${components[1]}" - } else { - components[1] - } - val day = if (components[0].length == 1) { - "0${components[0]}" - } else { - components[0] - } - - return "$day/$month/$year" - } -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt b/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt index 02c8bc8ba3..03bb401ac3 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt @@ -3,9 +3,8 @@ package org.dhis2.usescases.event import android.content.Intent import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.ActivityTestRule +import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest -import org.dhis2.usescases.event.entity.EventDetailsUIModel import org.dhis2.usescases.event.entity.EventStatusUIModel import org.dhis2.usescases.event.entity.ProgramStageUIModel import org.dhis2.usescases.event.entity.TEIProgramStagesUIModel @@ -23,34 +22,34 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class EventTest: BaseTest() { +class EventTest : BaseTest() { @get:Rule - val rule = ActivityTestRule(EventCaptureActivity::class.java, false, false) + val rule = lazyActivityScenarioRule(launchActivity = false) @get:Rule - val ruleTeiDashboard = ActivityTestRule(TeiDashboardMobileActivity::class.java, false, false) + val ruleTeiDashboard = + lazyActivityScenarioRule(launchActivity = false) @get:Rule - val ruleEventDetail = ActivityTestRule(EventInitialActivity::class.java, false, false) + val ruleEventDetail = lazyActivityScenarioRule(launchActivity = false) @get:Rule - val eventListRule = ActivityTestRule(ProgramEventDetailActivity::class.java, false, false) + val eventListRule = lazyActivityScenarioRule(launchActivity = false) @get:Rule val composeTestRule = createComposeRule() + @Ignore @Test fun shouldDeleteEventWhenClickOnDeleteInsideSpecificEvent() { - val tbVisit = "TB visit" val tbVisitDate = "31/12/2019" val tbProgramStages = createProgramStageModel() prepareEventToDeleteIntentAndLaunchActivity(ruleTeiDashboard) - teiDashboardRobot { - clickOnStageGroup(tbVisit) - clickOnEventGroupByStage(tbVisitDate) + teiDashboardRobot(composeTestRule) { + clickOnEventGroupByStageUsingDate(tbVisitDate) } eventRegistrationRobot { @@ -59,21 +58,22 @@ class EventTest: BaseTest() { clickOnDeleteDialog() } - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { checkEventWasDeletedStageGroup(tbProgramStages) } } @Test fun shouldShowEventDetailsWhenClickOnDetailsInsideSpecificEvent() { - val eventDetails = createEventDetails() + val completion = 92 + val email = "mail@mail.com" + + enableComposeForms() prepareEventDetailsIntentAndLaunchActivity(rule) eventRegistrationRobot { - checkEventFormDetails(eventDetails) - clickOnDetails() - checkEventDetails(eventDetails, composeTestRule) + checkEventDataEntryIsOpened(completion, email, composeTestRule) } } @@ -99,18 +99,18 @@ class EventTest: BaseTest() { prepareEventToUpdateIntentAndLaunchActivity(ruleTeiDashboard) - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnStageGroup(labMonitoring) clickOnEventGroupByStage(eventDate) } - eventRobot { + eventRobot(composeTestRule) { fillRadioButtonForm(radioFormLength) clickOnFormFabButton() - clickOnCompleteButton(composeTestRule) + clickOnCompleteButton() } - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnStageGroup(labMonitoring) checkEventStateStageGroup(labMonitoringStatus) } @@ -124,22 +124,22 @@ class EventTest: BaseTest() { prepareProgramAndLaunchActivity(atenatalCare) disableRecyclerViewAnimations() - programEventsRobot { + programEventsRobot(composeTestRule) { clickOnAddEvent() } eventRegistrationRobot { clickNextButton() } - eventRobot { + eventRobot(composeTestRule) { typeOnRequiredEventForm("125", 1) clickOnFormFabButton() - checkSecondaryButtonNotVisible(composeTestRule) + checkSecondaryButtonNotVisible() } } - private val tbVisitProgramStage = createTbVisitStageModel() - private val labMonitoringProgramStage = createLabMonitoringStageModel() - private val sputumProgramStage = createSputumStageModel() + private val tbVisitProgramStage = createTbVisitStageModel() + private val labMonitoringProgramStage = createLabMonitoringStageModel() + private val sputumProgramStage = createSputumStageModel() private val labMonitoringStatus = createEventStatusDetails() private fun createProgramStageModel() = TEIProgramStagesUIModel( @@ -163,13 +163,6 @@ class EventTest: BaseTest() { "4 events" ) - private fun createEventDetails() = EventDetailsUIModel( - "Alfa", - 91, - "1/3/2020", - "OU TEST PARENT" - ) - private fun createEventStatusDetails() = EventStatusUIModel( "Lab monitoring", "Event Completed", @@ -180,14 +173,15 @@ class EventTest: BaseTest() { private fun prepareProgramAndLaunchActivity(programUid: String) { Intent().apply { putExtra(ProgramEventDetailActivity.EXTRA_PROGRAM_UID, programUid) - }.also { eventListRule.launchActivity(it) } + }.also { eventListRule.launch(it) } } private fun disableRecyclerViewAnimations() { - val activity = eventListRule.activity - activity.runOnUiThread { - activity.supportFragmentManager.findFragmentByTag("EVENT_LIST").apply { - (this as EventListFragment).binding.recycler.itemAnimator = null + eventListRule.getScenario().onActivity { + it.runOnUiThread { + it.supportFragmentManager.findFragmentByTag("EVENT_LIST").apply { + (this as EventListFragment).binding.recycler.itemAnimator = null + } } } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventInitialTest.kt b/app/src/androidTest/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventInitialTest.kt index 71dcd45b2f..2b0733d650 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventInitialTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventInitialTest.kt @@ -3,6 +3,7 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventDetails import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag @@ -11,8 +12,8 @@ import org.dhis2.commons.data.EventCreationType import org.dhis2.commons.date.DateUtils import org.dhis2.commons.locationprovider.LocationProvider import org.dhis2.commons.prefs.PreferenceProvider +import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.ResourceManager -import org.dhis2.data.dhislogic.DhisPeriodUtils import org.dhis2.form.data.GeometryController import org.dhis2.form.data.GeometryParserImpl import org.dhis2.form.model.FieldUiModel @@ -47,6 +48,9 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import java.text.SimpleDateFormat import java.util.Date +import org.dhis2.commons.resources.MetadataIconProvider +import org.dhis2.ui.MetadataIconData +import org.mockito.kotlin.any @ExperimentalCoroutinesApi class EventInitialTest { @@ -75,6 +79,10 @@ class EventInitialTest { } + private val metadataIconProvider:MetadataIconProvider = mock{ + on { invoke(any()) }doReturn MetadataIconData.defaultIcon() + } + private lateinit var viewModel: EventDetailsViewModel @@ -91,6 +99,7 @@ class EventInitialTest { on { displayName() } doReturn PROGRAM_STAGE_NAME on { executionDateLabel() } doReturn EXECUTION_DATE on { generatedByEnrollmentDate() } doReturn true + on { uid() } doReturn "programStage" } private val catCombo: CategoryCombo = mock { on { uid() } doReturn CAT_COMBO_UID @@ -152,9 +161,10 @@ class EventInitialTest { resourcesProvider = provideEventResourcesProvider(), creationType = eventCreationType, enrollmentStatus = enrollmentStatus, + metadataIconProvider = metadataIconProvider ) - private fun provideEventResourcesProvider() = EventDetailResourcesProvider(resourceManager) + private fun provideEventResourcesProvider() = EventDetailResourcesProvider(PROGRAM_UID, programStage.uid(), resourceManager) private fun createOrUpdateEventDetails() = CreateOrUpdateEventDetails( repository = eventDetailsRepository, @@ -223,8 +233,8 @@ class EventInitialTest { EventInputDateUiModel( eventDate = date, detailsEnabled = details.enabled, - onDateClick = { viewModel.onDateClick() }, - onDateSet = { dateValues -> + onDateClick = {} , + onDateSelected = { dateValues -> viewModel.onDateSet(dateValues.year, dateValues.month, dateValues.day) }, onClear = { viewModel.onClearEventReportDate() }, @@ -256,8 +266,8 @@ class EventInitialTest { EventInputDateUiModel( eventDate = date, detailsEnabled = details.enabled, - onDateClick = { viewModel.onDateClick() }, - onDateSet = { dateValues -> + onDateClick = {}, + onDateSelected = { dateValues -> viewModel.onDateSet(dateValues.year, dateValues.month, dateValues.day) }, onClear = { viewModel.onClearEventReportDate() }, @@ -286,15 +296,13 @@ class EventInitialTest { val catCombo by viewModel.eventCatCombo.collectAsState() ProvideCategorySelector( - modifier = Modifier, + modifier = Modifier.testTag(EMPTY_CATEGORY_SELECTOR), eventCatComboUiModel = EventCatComboUiModel( EventCategory("UID", "NO OPTIONS ", 0, emptyList()), eventCatCombo = catCombo, detailsEnabled = details.enabled, currentDate = date.currentDate, selectedOrgUnit = details.selectedOrgUnit, - onShowCategoryDialog = { - }, onClearCatCombo = { }, onOptionSelected = { diff --git a/app/src/androidTest/java/org/dhis2/usescases/filters/FilterTest.kt b/app/src/androidTest/java/org/dhis2/usescases/filters/FilterTest.kt index 6826737ec6..966881f270 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/filters/FilterTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/filters/FilterTest.kt @@ -16,7 +16,7 @@ import org.junit.Ignore import org.junit.Rule import org.junit.Test -class FilterTest: BaseTest() { +class FilterTest : BaseTest() { @get:Rule val rule = ActivityTestRule(MainActivity::class.java, false, false) @@ -37,16 +37,16 @@ class FilterTest: BaseTest() { filterRobotCommon { openFilterAtPosition(0) clickOnFromToDateOption() - selectDate(2020,6,15) + selectDate(2020, 6, 15) acceptDateSelected() - selectDate(2020,11,7) + selectDate(2020, 11, 7) acceptDateSelected() } homeRobot { openFilters() - checkItemsInProgram(composeTestRule,3,"Child Programme", "3") - checkItemsInProgram(composeTestRule,5, "Contraceptives Voucher Program", "5") - checkItemsInProgram(composeTestRule,26, "Mortality < 5 years", "4") + checkItemsInProgram(composeTestRule, 3, "Child Programme", "3") + checkItemsInProgram(composeTestRule, 5, "Contraceptives Voucher Program", "5") + checkItemsInProgram(composeTestRule, 26, "Mortality < 5 years", "4") } cleanLocalDatabase() } @@ -68,15 +68,15 @@ class FilterTest: BaseTest() { } homeRobot { openFilters() - checkItemsInProgram(composeTestRule,3,"Child Programme", "0") - checkItemsInProgram(composeTestRule,41, "XX TEST EVENT FULL", "2") - checkItemsInProgram(composeTestRule,43, "XX TEST TRACKER PROGRAM", "4") + checkItemsInProgram(composeTestRule, 3, "Child Programme", "0") + checkItemsInProgram(composeTestRule, 41, "XX TEST EVENT FULL", "2") + checkItemsInProgram(composeTestRule, 43, "XX TEST TRACKER PROGRAM", "4") } cleanLocalDatabase() } @Test - fun checkTreeOrgUnitFilter(){ + fun checkTreeOrgUnitFilter() { startActivity() setupCredentials() @@ -87,34 +87,35 @@ class FilterTest: BaseTest() { filterRobotCommon { openFilterAtPosition(1) clickOnOrgUnitTree() - orgUnitSelectorRobot(composeTestRule){ + orgUnitSelectorRobot(composeTestRule) { selectTreeOrgUnit("OU TEST PARENT") } } homeRobot { openFilters() - checkItemsInProgram(composeTestRule,3,"Child Programme", "0") - checkItemsInProgram(composeTestRule,41, "XX TEST EVENT FULL", "2") - checkItemsInProgram(composeTestRule,43, "XX TEST TRACKER PROGRAM", "4") + checkItemsInProgram(composeTestRule, 3, "Child Programme", "0") + checkItemsInProgram(composeTestRule, 41, "XX TEST EVENT FULL", "2") + checkItemsInProgram(composeTestRule, 43, "XX TEST TRACKER PROGRAM", "4") } cleanLocalDatabase() } + @Ignore("Undeterministic") @Test fun checkSyncFilter() { setupCredentials() startActivity() homeRobot { - openProgramByPosition(composeTestRule,0) + openProgramByPosition(composeTestRule, 0) waitToDebounce(700) } - eventWithoutRegistrationRobot { + eventWithoutRegistrationRobot(composeTestRule) { clickOnEventAtPosition(0) } - eventRobot { + eventRobot(composeTestRule) { clickOnFormFabButton() - clickOnCompleteButton(composeTestRule) + clickOnCompleteButton() pressBack() } homeRobot { @@ -126,8 +127,9 @@ class FilterTest: BaseTest() { } homeRobot { openFilters() - checkItemsInProgram(composeTestRule,0,"Antenatal care visit", "1") - checkItemsInProgram(composeTestRule,3,"Child Programme", "0") + waitToDebounce(1000) + checkItemsInProgram(composeTestRule, 0, "Antenatal care visit", "1") + checkItemsInProgram(composeTestRule, 3, "Child Programme", "0") } cleanLocalDatabase() } @@ -139,13 +141,13 @@ class FilterTest: BaseTest() { startActivity() homeRobot { - openProgramByPosition(composeTestRule,41) + openProgramByPosition(composeTestRule, 41) } - eventWithoutRegistrationRobot { + eventWithoutRegistrationRobot(composeTestRule) { clickOnEventAtPosition(0) } - formRobot { - clickOnSelectOption("ZZ TEST RULE ACTIONS A", 1,"Hide Field", 1) + formRobot(composeTestRule) { + clickOnSelectOption(1, 1) pressBack() pressBack() pressBack() @@ -164,9 +166,9 @@ class FilterTest: BaseTest() { } homeRobot { openFilters() - checkItemsInProgram(composeTestRule,37,"TB program", "0") + checkItemsInProgram(composeTestRule, 37, "TB program", "0") waitToDebounce(700) - checkItemsInProgram(composeTestRule,41, "XX TEST EVENT FULL", "1") + checkItemsInProgram(composeTestRule, 41, "XX TEST EVENT FULL", "1") waitToDebounce(700) } cleanLocalDatabase() diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowRobot.kt index f4d9832ca8..da09cd4963 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowRobot.kt @@ -2,7 +2,6 @@ package org.dhis2.usescases.flow.searchFlow import org.dhis2.common.BaseRobot import org.dhis2.usescases.searchte.robot.filterRobot -import org.dhis2.usescases.searchte.robot.searchTeiRobot fun searchFlowRobot(searchFlowRobot: SearchFlowRobot.() -> Unit) { SearchFlowRobot().apply { diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt index 41d93c24c2..2807eded53 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt @@ -1,6 +1,9 @@ package org.dhis2.usescases.flow.searchFlow import android.content.Intent +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.text.capitalize +import androidx.compose.ui.text.intl.Locale import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule import org.dhis2.R @@ -19,6 +22,9 @@ class SearchFlowTest : BaseTest() { @get:Rule val rule = ActivityTestRule(SearchTEActivity::class.java, false, false) + @get:Rule + val composeTestRule = createComposeRule() + private val dateRegistration = createFirstSpecificDate() private val dateEnrollment = createEnrollmentDate() @@ -27,11 +33,15 @@ class SearchFlowTest : BaseTest() { setDatePicker() val registerTEIDetails = createRegisterTEI() val enrollmentStatus = context.getString(R.string.filters_title_enrollment_status) + .format( + context.resources.getQuantityString(R.plurals.enrollment, 1) + .capitalize(Locale.current) + ) val filterCounter = "1" val filterTotalCount = "2" prepareWomanProgrammeIntentAndLaunchActivity(rule) - teiFlowRobot { + teiFlowRobot(composeTestRule) { registerTEI(registerTEIDetails) pressBack() } diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt index 61a935da81..458845c14f 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt @@ -6,10 +6,10 @@ import androidx.compose.ui.test.performClick import androidx.lifecycle.MutableLiveData import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.ActivityTestRule import androidx.work.Data import androidx.work.WorkInfo import org.dhis2.AppTest +import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest import org.dhis2.usescases.datasets.dataSetTableRobot import org.dhis2.usescases.datasets.datasetDetail.DataSetDetailActivity @@ -26,19 +26,20 @@ import org.junit.Test import org.junit.runner.RunWith import syncFlowRobot import java.util.UUID +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity @RunWith(AndroidJUnit4::class) class SyncFlowTest : BaseTest() { @get:Rule - val ruleDataSet = ActivityTestRule(DataSetDetailActivity::class.java, false, false) + val ruleDataSet = lazyActivityScenarioRule(launchActivity = false) @get:Rule - val ruleSearch = ActivityTestRule(SearchTEActivity::class.java, false, false) + val ruleSearch = lazyActivityScenarioRule(launchActivity = false) @get:Rule val ruleEventWithoutRegistration = - ActivityTestRule(ProgramEventDetailActivity::class.java, false, false) + lazyActivityScenarioRule(launchActivity = false) @get:Rule val composeTestRule = createComposeRule() @@ -51,42 +52,6 @@ class SyncFlowTest : BaseTest() { ApplicationProvider.getApplicationContext().mutableWorkInfoStatuses } - @Test - fun shouldSuccessfullySyncAChangedTEI() { - val teiName = "Scott" - val teiLastName = "Kelley" - - prepareTBProgrammeIntentAndLaunchActivity(ruleSearch) - searchTeiRobot { - clickOnOpenSearch() - typeAttributeAtPosition(teiName, 0) - typeAttributeAtPosition(teiLastName, 1) - clickOnSearch() - clickOnTEI(teiName, teiLastName) - } - - teiDashboardRobot { - clickOnGroupEventByName(TB_VISIT) - clickOnEventWith(TB_VISIT_EVENT_DATE, ORG_UNIT) - } - - eventRobot { - clickOnUpdate() - } - - teiDashboardRobot { - composeTestRule.onNodeWithText("Sync").performClick() - } - syncFlowRobot { - waitToDebounce(500) - clickOnSyncButton(composeTestRule) - workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.RUNNING))) - workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.SUCCEEDED))) - checkSyncWasSuccessfully(composeTestRule) - } - cleanLocalDatabase() - } - @Test fun shouldShowErrorWhenTEISyncFails() { val teiName = "Lars" @@ -94,59 +59,59 @@ class SyncFlowTest : BaseTest() { prepareTBProgrammeIntentAndLaunchActivity(ruleSearch) - searchTeiRobot { + searchTeiRobot(composeTestRule) { clickOnOpenSearch() - typeAttributeAtPosition(teiName, 0) - typeAttributeAtPosition(teiLastName, 1) + openNextSearchParameter("First name") + typeOnNextSearchTextParameter(teiName) + openNextSearchParameter("Last name") + typeOnNextSearchTextParameter(teiLastName) clickOnSearch() clickOnTEI(teiName, teiLastName) } - teiDashboardRobot { - clickOnGroupEventByName(LAB_MONITORING) - clickOnEventWith(LAB_MONITORING_EVENT_DATE, ORG_UNIT) + teiDashboardRobot(composeTestRule) { + clickOnEventWith(LAB_MONITORING_EVENT_DATE) } - eventRobot { + eventRobot(composeTestRule) { fillRadioButtonForm(4) clickOnFormFabButton() - clickOnCompleteButton(composeTestRule) + clickOnCompleteButton() } - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { composeTestRule.onNodeWithText("Sync").performClick() } - syncFlowRobot { + syncFlowRobot(composeTestRule) { waitToDebounce(500) - clickOnSyncButton(composeTestRule) + clickOnSyncButton() workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.RUNNING))) workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.FAILED))) - checkSyncFailed(composeTestRule) + checkSyncFailed() } cleanLocalDatabase() } - @Ignore("Indeterminate (flaky)") @Test fun shouldSuccessfullySyncSavedEvent() { prepareMalariaEventIntentAndLaunchActivity(ruleEventWithoutRegistration) - eventWithoutRegistrationRobot { + eventWithoutRegistrationRobot(composeTestRule) { clickOnEventAtPosition(0) } - eventRobot { + eventRobot(composeTestRule) { clickOnFormFabButton() - clickOnCompleteButton(composeTestRule) + clickOnCompleteButton() } - syncFlowRobot { - clickOnEventToSync(0) - clickOnSyncButton(composeTestRule) + syncFlowRobot(composeTestRule) { + clickOnEventToSync() + clickOnSyncButton() workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.RUNNING))) workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.SUCCEEDED))) - checkSyncWasSuccessfully(composeTestRule) + checkSyncWasSuccessfully() } cleanLocalDatabase() } @@ -155,21 +120,21 @@ class SyncFlowTest : BaseTest() { fun shouldShowErrorWhenSyncEventFails() { prepareMalariaEventIntentAndLaunchActivity(ruleEventWithoutRegistration) - eventWithoutRegistrationRobot { + eventWithoutRegistrationRobot(composeTestRule) { clickOnEventAtPosition(1) } - eventRobot { + eventRobot(composeTestRule) { clickOnFormFabButton() - clickOnCompleteButton(composeTestRule) + clickOnCompleteButton() } - syncFlowRobot { - clickOnEventToSync(1) - clickOnSyncButton(composeTestRule) + syncFlowRobot(composeTestRule) { + clickOnEventToSync() + clickOnSyncButton() workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.RUNNING))) workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.FAILED))) - checkSyncFailed(composeTestRule) + checkSyncFailed() } cleanLocalDatabase() } @@ -179,6 +144,7 @@ class SyncFlowTest : BaseTest() { prepareFacilityDataSetIntentAndLaunchActivity(ruleDataSet) dataSetRobot { + composeTestRule.waitForIdle() clickOnDataSetAtPosition(0) } @@ -197,12 +163,15 @@ class SyncFlowTest : BaseTest() { clickOnNegativeButton() } - syncFlowRobot { + syncFlowRobot(composeTestRule) { clickOnDataSetToSync(0) - clickOnSyncButton(composeTestRule) + clickOnSyncButton() workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.RUNNING))) + composeTestRule.waitForIdle() workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.SUCCEEDED))) - checkSyncWasSuccessfully(composeTestRule) //sync failed + composeTestRule.waitForIdle() + waitToDebounce(3000) + checkSyncWasSuccessfully() //sync failed } cleanLocalDatabase() } @@ -230,12 +199,12 @@ class SyncFlowTest : BaseTest() { clickOnNegativeButton() } - syncFlowRobot { + syncFlowRobot(composeTestRule) { clickOnDataSetToSync(1) - clickOnSyncButton(composeTestRule) + clickOnSyncButton() workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.RUNNING))) workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.FAILED))) - checkSyncFailed(composeTestRule) + checkSyncFailed() } cleanLocalDatabase() } @@ -253,10 +222,6 @@ class SyncFlowTest : BaseTest() { } companion object { - const val ORG_UNIT = "Ngelehun CHC" - const val TB_VISIT = "TB visit" - const val TB_VISIT_EVENT_DATE = "3/7/2019" - const val LAB_MONITORING = "Lab monitoring" const val LAB_MONITORING_EVENT_DATE = "28/6/2020" } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncIntents.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncIntents.kt index b0f3019c14..83b391ceb4 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncIntents.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncIntents.kt @@ -1,31 +1,40 @@ package org.dhis2.usescases.flow.syncFlow import android.content.Intent -import androidx.test.rule.ActivityTestRule +import androidx.test.core.app.ApplicationProvider +import org.dhis2.LazyActivityScenarioRule import org.dhis2.usescases.datasets.datasetDetail.DataSetDetailActivity import org.dhis2.usescases.programEventDetail.ProgramEventDetailActivity import org.dhis2.usescases.searchTrackEntity.SearchTEActivity import org.dhis2.usescases.searchte.SearchTETest -fun prepareTBProgrammeIntentAndLaunchActivity(ruleSearch: ActivityTestRule) { - Intent().apply { +fun prepareTBProgrammeIntentAndLaunchActivity(ruleSearch: LazyActivityScenarioRule) { + Intent( + ApplicationProvider.getApplicationContext(), + SearchTEActivity::class.java, + ).apply { putExtra(PROGRAM_UID, TB_PROGRAM_UID_VALUE) putExtra(SearchTETest.CHILD_TE_TYPE, SearchTETest.CHILD_TE_TYPE_VALUE) - }.also { ruleSearch.launchActivity(it) } + }.also { ruleSearch.launch(it) } } -fun prepareMalariaEventIntentAndLaunchActivity(ruleSearch: ActivityTestRule) { - Intent().apply { putExtra(PROGRAM_UID, ANTENATAL_PROGRAM_UID_VALUE) }.also { ruleSearch.launchActivity(it) } +fun prepareMalariaEventIntentAndLaunchActivity(ruleSearch: LazyActivityScenarioRule) { + Intent( + ApplicationProvider.getApplicationContext(), + ProgramEventDetailActivity::class.java, + ).apply { putExtra(PROGRAM_UID, ANTENATAL_PROGRAM_UID_VALUE) }.also { ruleSearch.launch(it) } } -fun prepareFacilityDataSetIntentAndLaunchActivity(ruleSearch: ActivityTestRule) { - Intent().apply { putExtra(DATASET_UID, MNCH_DATASET_UID_VALUE) }.also { ruleSearch.launchActivity(it) } +fun prepareFacilityDataSetIntentAndLaunchActivity(ruleSearch: LazyActivityScenarioRule) { + Intent( + ApplicationProvider.getApplicationContext(), + DataSetDetailActivity::class.java, + ).apply { putExtra(DATASET_UID, MNCH_DATASET_UID_VALUE) } + .also { ruleSearch.launch(it) } } const val PROGRAM_UID = "PROGRAM_UID" -const val MALARIA_PROGRAM_UID_VALUE = "VBqh0ynB2wv" const val ANTENATAL_PROGRAM_UID_VALUE = "lxAQ7Zs9VYR" const val DATASET_UID = "DATASET_UID" -const val FACILITY_DATASET_UID_VALUE = "V8MHeZHIrcP" const val MNCH_DATASET_UID_VALUE = "EKWVBc5C0ms" const val TB_PROGRAM_UID_VALUE = "ur1Edk5Oe2n" \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/robot/EventWithoutRegistrationRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/robot/EventWithoutRegistrationRobot.kt index 78b18c3914..61106af524 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/robot/EventWithoutRegistrationRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/robot/EventWithoutRegistrationRobot.kt @@ -1,44 +1,33 @@ package org.dhis2.usescases.flow.syncFlow.robot -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem -import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition -import androidx.test.espresso.contrib.RecyclerViewActions.scrollTo -import androidx.test.espresso.matcher.ViewMatchers.hasDescendant -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText -import org.dhis2.R +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.printToLog +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.contrib.RecyclerViewActions import org.dhis2.common.BaseRobot -import org.dhis2.common.viewactions.clickChildViewWithId import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.EventViewHolder -import org.hamcrest.CoreMatchers.allOf -fun eventWithoutRegistrationRobot(eventWithoutRegistrationRobot: EventWithoutRegistrationRobot.() -> Unit) { - EventWithoutRegistrationRobot().apply { +fun eventWithoutRegistrationRobot( + composeTestRule: ComposeTestRule, + eventWithoutRegistrationRobot: EventWithoutRegistrationRobot.() -> Unit +) { + EventWithoutRegistrationRobot(composeTestRule).apply { eventWithoutRegistrationRobot() } } -class EventWithoutRegistrationRobot : BaseRobot() { - - fun clickOnEvent() { - onView(withId(R.id.recycler)).perform( - scrollTo( - allOf(hasDescendant(withText("teiName")), hasDescendant(withText("teiLastName"))) - ), - actionOnItem( - allOf(hasDescendant(withText("teiName")), hasDescendant(withText("teiLastName"))), clickChildViewWithId(R.id.status_icon) - ) - ) - } +class EventWithoutRegistrationRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { + @OptIn(ExperimentalTestApi::class) fun clickOnEventAtPosition(position: Int) { - onView(withId(R.id.recycler)) - .perform(actionOnItemAtPosition(position, click())) - } - - fun clickOnSaveFab() { - onView(withId(R.id.action_button)).perform(click()) + composeTestRule.waitUntilAtLeastOneExists(hasTestTag("EVENT_ITEM")) + composeTestRule + .onAllNodesWithTag("EVENT_ITEM", true)[position] + .performClick() } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/robot/SyncFlowRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/robot/SyncFlowRobot.kt index 4072ae657d..ff74718f41 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/robot/SyncFlowRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/robot/SyncFlowRobot.kt @@ -1,7 +1,9 @@ +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assert import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.espresso.Espresso.onView import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition @@ -13,40 +15,37 @@ import org.dhis2.common.viewactions.clickChildViewWithId import org.dhis2.ui.dialogs.bottomsheet.MAIN_BUTTON_TAG import org.dhis2.ui.dialogs.bottomsheet.TITLE import org.dhis2.usescases.datasets.datasetDetail.datasetList.DataSetListViewHolder -import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.EventViewHolder -fun syncFlowRobot(syncFlowRobot: SyncFlowRobot.() -> Unit) { - SyncFlowRobot().apply { +fun syncFlowRobot( + composeTestRule: ComposeTestRule, + syncFlowRobot: SyncFlowRobot.() -> Unit) { + SyncFlowRobot(composeTestRule).apply { syncFlowRobot() } } -class SyncFlowRobot : BaseRobot() { +class SyncFlowRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { - fun clickOnSyncButton(composeTestRule: ComposeContentTestRule) { - composeTestRule.onNodeWithTag(MAIN_BUTTON_TAG).performClick() + fun clickOnSyncButton() { + composeTestRule.onNodeWithTag(MAIN_BUTTON_TAG, useUnmergedTree = true).performClick() } - fun checkSyncWasSuccessfully(composeTestRule: ComposeContentTestRule) { + fun checkSyncWasSuccessfully() { val expectedTitle = InstrumentationRegistry.getInstrumentation() .targetContext.getString(R.string.sync_dialog_title_not_synced) - composeTestRule.onNodeWithTag(TITLE).assert(hasText(expectedTitle)) + composeTestRule.onNodeWithTag(TITLE, useUnmergedTree = true).assert(hasText(expectedTitle)) } - fun checkSyncFailed(composeTestRule: ComposeContentTestRule) { + fun checkSyncFailed() { val expectedTitle = InstrumentationRegistry.getInstrumentation() .targetContext.getString(R.string.sync_dialog_title_not_synced) - composeTestRule.onNodeWithTag(TITLE).assert(hasText(expectedTitle)) + composeTestRule.onNodeWithTag(TITLE, useUnmergedTree = true).assert(hasText(expectedTitle)) } - fun clickOnEventToSync(position: Int) { - onView(withId(R.id.recycler)) - .perform( - actionOnItemAtPosition( - position, - clickChildViewWithId(R.id.sync_icon) - ) - ) + @OptIn(ExperimentalTestApi::class) + fun clickOnEventToSync() { + composeTestRule.waitUntilAtLeastOneExists(hasText("Sync")) + composeTestRule.onNodeWithText("Sync", useUnmergedTree = true).performClick() } fun clickOnDataSetToSync(position: Int) { diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowRobot.kt index 0f0ef7320a..ad45d83a45 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowRobot.kt @@ -1,61 +1,68 @@ package org.dhis2.usescases.flow.teiFlow -import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeTestRule import org.dhis2.common.BaseRobot -import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel -import org.dhis2.usescases.searchte.robot.searchTeiRobot import org.dhis2.usescases.flow.teiFlow.entity.EnrollmentListUIModel import org.dhis2.usescases.flow.teiFlow.entity.RegisterTEIUIModel +import org.dhis2.usescases.searchte.robot.searchTeiRobot import org.dhis2.usescases.teidashboard.robot.enrollmentRobot import org.dhis2.usescases.teidashboard.robot.eventRobot import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot -fun teiFlowRobot(teiFlowRobot: TeiFlowRobot.() -> Unit) { - TeiFlowRobot().apply { +fun teiFlowRobot( + composeTestRule: ComposeTestRule, + teiFlowRobot: TeiFlowRobot.() -> Unit +) { + TeiFlowRobot(composeTestRule).apply { teiFlowRobot() } } -class TeiFlowRobot : BaseRobot() { +class TeiFlowRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { - fun registerTEI(registrationModel: RegisterTEIUIModel) { + fun registerTEI( + registrationModel: RegisterTEIUIModel + ) { val registrationDate = registrationModel.firstSpecificDate val enrollmentDate = registrationModel.enrollmentDate - searchTeiRobot { - typeAttributeAtPosition(registrationModel.name, 0) - typeAttributeAtPosition(registrationModel.lastName, 1) - clickOnDateField() - selectSpecificDate(registrationDate.year, registrationDate.month, registrationDate.day) - acceptDate() + searchTeiRobot(composeTestRule) { + openNextSearchParameter("First name") + typeOnNextSearchTextParameter(registrationModel.name) + openNextSearchParameter("Last name") + typeOnNextSearchTextParameter(registrationModel.lastName) + openNextSearchParameter("Date of birth") + typeOnDateParameter("${registrationDate.day}0${registrationDate.month}${registrationDate.year}") clickOnSearch() clickOnEnroll() - selectSpecificDate(enrollmentDate.year, enrollmentDate.month, enrollmentDate.day) - acceptDate() } enrollmentRobot { + clickOnInputDate("Date of enrollment *") + selectSpecificDate(enrollmentDate.year, enrollmentDate.month, enrollmentDate.day) + clickOnAcceptInDatePicker() + clickOnInputDate("LMP Date *") + clickOnAcceptInDatePicker() clickOnSaveEnrollment() } } - fun enrollToProgram(composeTestRule: ComposeTestRule, program: String) { - teiDashboardRobot { + fun enrollToProgram(program: String) { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnMenuProgramEnrollments() } enrollmentRobot { clickOnAProgramForEnrollment(composeTestRule, program) - clickOnAcceptEnrollmentDate() + clickOnAcceptInDatePicker() scrollToBottomProgramForm() clickOnSaveEnrollment() } } fun checkActiveAndPastEnrollmentDetails(enrollmentDetails: EnrollmentListUIModel) { - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnMenuProgramEnrollments() } @@ -67,41 +74,40 @@ class TeiFlowRobot : BaseRobot() { } fun checkPastEventsAreClosed( - composeTestRule: ComposeContentTestRule, - totalEvents: Int, programPosition: Int ) { enrollmentRobot { clickOnEnrolledProgram(programPosition) } - teiDashboardRobot { - checkCompleteStateInfoBarIsDisplay(composeTestRule) + teiDashboardRobot(composeTestRule) { checkCanNotAddEvent() - checkAllEventsAreClosed(totalEvents) + checkAllEventsAreClosed() } } - fun closeEnrollmentAndCheckEvents(totalEvents: Int) { - teiDashboardRobot { + fun closeEnrollmentAndCheckEvents() { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnTimelineEvents() clickOnMenuMoreOptions() clickOnMenuComplete() checkCanNotAddEvent() - checkAllEventsAreClosed(totalEvents) + checkAllEventsAreClosed() } } - fun changeDueDate(date: DateRegistrationUIModel, programStage: String, orgUnit: String, composeTestRule: ComposeTestRule) { - teiDashboardRobot { - clickOnStageGroup(programStage) - clickOnEventGroupByStageUsingOU(orgUnit) + fun changeDueDate( + cardTitle: String, + date: String, + ) { + teiDashboardRobot(composeTestRule) { + clickOnEventGroupByStageUsingDate(cardTitle) } - eventRobot { - clickOnEventDueDate(composeTestRule) - selectSpecificDate(date.year, date.month, date.day) + eventRobot(composeTestRule) { + clickOnEventReportDate() + selectSpecificDate(date) acceptUpdateEventDate() } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt index d6d4a28036..5b8cf533e8 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt @@ -5,11 +5,11 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule import org.dhis2.usescases.BaseTest -import org.dhis2.usescases.searchTrackEntity.SearchTEActivity -import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel import org.dhis2.usescases.flow.teiFlow.entity.EnrollmentListUIModel import org.dhis2.usescases.flow.teiFlow.entity.RegisterTEIUIModel +import org.dhis2.usescases.searchTrackEntity.SearchTEActivity +import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -18,7 +18,7 @@ import java.util.Date @RunWith(AndroidJUnit4::class) -class TeiFlowTest: BaseTest() { +class TeiFlowTest : BaseTest() { @get:Rule val rule = ActivityTestRule(TeiDashboardMobileActivity::class.java, false, false) @@ -34,9 +34,8 @@ class TeiFlowTest: BaseTest() { private val currentDate = getCurrentDate() @Test - fun shouldEnrollToSameProgramAfterClosedIt() { + fun shouldEnrollToSameProgramAfterClosingIt() { val totalEventsPerEnrollment = 3 - val pastProgramPosition = 4 val enrollmentListDetails = createEnrollmentList() val registerTeiDetails = createRegisterTEI() @@ -44,12 +43,12 @@ class TeiFlowTest: BaseTest() { setDatePicker() prepareWomanProgrammeIntentAndLaunchActivity(ruleSearch) - teiFlowRobot { + teiFlowRobot(composeTestRule) { registerTEI(registerTeiDetails) - closeEnrollmentAndCheckEvents(totalEventsPerEnrollment) - enrollToProgram(composeTestRule, ADULT_WOMAN_PROGRAM) + closeEnrollmentAndCheckEvents() + enrollToProgram(ADULT_WOMAN_PROGRAM) checkActiveAndPastEnrollmentDetails(enrollmentListDetails) - checkPastEventsAreClosed(composeTestRule, totalEventsPerEnrollment, pastProgramPosition) + checkPastEventsAreClosed(totalEventsPerEnrollment) } } @@ -80,7 +79,7 @@ class TeiFlowTest: BaseTest() { 30 ) - private fun getCurrentDate() :String { + private fun getCurrentDate(): String { val sdf = SimpleDateFormat(DATE_FORMAT) val dateFormat = sdf.format(Date()) return dateFormat.removePrefix("0") @@ -105,5 +104,7 @@ class TeiFlowTest: BaseTest() { const val LASTNAME = "Stuart" const val DATE_FORMAT = "dd/M/yyyy" + const val DATE_PICKER_FORMAT = ", d MMMM" + } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/form/FormIntents.kt b/app/src/androidTest/java/org/dhis2/usescases/form/FormIntents.kt index 60327c03f4..c7c55ef64d 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/form/FormIntents.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/form/FormIntents.kt @@ -1,41 +1,20 @@ package org.dhis2.usescases.form -import android.content.Intent -import androidx.test.rule.ActivityTestRule +import androidx.test.core.app.ApplicationProvider +import org.dhis2.LazyActivityScenarioRule +import org.dhis2.form.model.EventMode import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity -import org.dhis2.usescases.searchTrackEntity.SearchTEActivity -import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity -const val EVENT_UID = "EVENT_UID" const val PROGRAM_UID = "PROGRAM_UID" -const val TEI_UID = "TEI_UID" -const val ENROLLMENT_UID = "ENROLLMENT_UID" -const val PROGRAM_STAGE_UID = "PROGRAM_STAGE_UID" -const val TE_UID = "TRACKED_ENTITY_UID" - const val PROGRAM_XX_PROGRAM_RULES = "jIT6KcSZiAN" -const val EVENT_GAMMA = "lwwdP4k1SnI" -const val PROGRAM_STAGE_GAMMA = "wtf7lKvaoY8" -const val TEI_BLA = "bxEs3ZwGcZg" -const val TRACKED_ENTITY_TYPE = "nEenWmSyUEp" +const val EVENT_GAMMA = "MIZVQnTD4HW" -fun prepareEventProgramRuleIntentAndLaunchActivity(rule: ActivityTestRule) { - Intent().apply { - putExtra(PROGRAM_UID, PROGRAM_XX_PROGRAM_RULES) - putExtra(EVENT_UID, EVENT_GAMMA) - }.also { rule.launchActivity(it) } -} -fun prepareTEIIntentAndLaunchActivity(ruleTeiDashboard: ActivityTestRule) { - Intent().apply { - putExtra(PROGRAM_UID, PROGRAM_XX_PROGRAM_RULES) - putExtra(TEI_UID, TEI_BLA) - }.also { ruleTeiDashboard.launchActivity(it) } +fun prepareIntentAndLaunchEventActivity(rule: LazyActivityScenarioRule) { + EventCaptureActivity.intent( + ApplicationProvider.getApplicationContext(), + EVENT_GAMMA, + PROGRAM_XX_PROGRAM_RULES, + EventMode.CHECK + ).also { rule.launch(it) } } - -fun startSearchActivity(ruleSearch: ActivityTestRule) { - Intent().apply { - putExtra(PROGRAM_UID, PROGRAM_XX_PROGRAM_RULES) - putExtra(TE_UID, TRACKED_ENTITY_TYPE) - }.also { ruleSearch.launchActivity(it) } -} \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/form/FormRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/form/FormRobot.kt index 58573f1142..e42dfdab16 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/form/FormRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/form/FormRobot.kt @@ -2,18 +2,16 @@ package org.dhis2.usescases.form import android.app.Activity import android.view.MenuItem +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onFirst -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.onNodeWithText import androidx.test.espresso.Espresso.onData import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup import androidx.test.espresso.matcher.RootMatchers.withDecorView @@ -27,37 +25,28 @@ import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.atPosition import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.hasItem import org.dhis2.common.viewactions.clickChildViewWithId import org.dhis2.common.viewactions.scrollToBottomRecyclerView -import org.dhis2.common.viewactions.scrollToPositionRecyclerview import org.dhis2.form.ui.FormViewHolder -import org.dhis2.ui.dialogs.bottomsheet.SECONDARY_BUTTON_TAG -import org.dhis2.usescases.form.FormTest.Companion.NO_ACTION import org.dhis2.usescases.form.FormTest.Companion.NO_ACTION_POSITION -import org.hamcrest.CoreMatchers.`is` import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anything import org.hamcrest.CoreMatchers.instanceOf +import org.hamcrest.CoreMatchers.`is` import org.hamcrest.CoreMatchers.not -fun formRobot(formRobot: FormRobot.() -> Unit) { - FormRobot().apply { +fun formRobot( + composeTestRule: ComposeTestRule, + formRobot: FormRobot.() -> Unit +) { + FormRobot(composeTestRule).apply { formRobot() } } -class FormRobot : BaseRobot() { +class FormRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { - private fun clickOnASpecificSection(sectionLabel: String) { - val perform = onView(withId(R.id.recyclerView)) - .perform( - actionOnItem( - allOf( - hasDescendant(withText(sectionLabel)), hasDescendant( - withId(R.id.openIndicator) - ) - ), click() - ) - ) + fun clickOnASpecificSection(sectionLabel: String) { + onView(withText(sectionLabel)).perform(click()) } private fun clickOnSpinner(position: Int) { @@ -69,20 +58,16 @@ class FormRobot : BaseRobot() { ) } - fun typeOnSearchInput(searchWord: String) { - onView(withId(R.id.txtSearch)).perform(typeText(searchWord)) - } - - private fun selectAction(action: String, position: Int) { + private fun selectAction(position: Int) { onData(anything()) .inRoot(isPlatformPopup()) .atPosition(position) .perform(click()) } - fun resetToNoAction(label: String, position: Int) { + fun resetToNoAction(position: Int) { clickOnSpinner(position) - selectAction(NO_ACTION, NO_ACTION_POSITION) + selectAction(NO_ACTION_POSITION) } fun checkHiddenField(label: String) { @@ -91,7 +76,6 @@ class FormRobot : BaseRobot() { } fun checkHiddenSection(label: String) { - clickOnASpecificSection(label) onView(withId(R.id.recyclerView)) .check(matches(not(hasItem(withText(label))))) } @@ -125,10 +109,8 @@ class FormRobot : BaseRobot() { } fun checkIndicatorIsDisplayed(name: String, value: String) { - onView(withId(R.id.indicator_name)) - .check(matches(allOf(isDisplayed(), withText(name)))) - onView(withId(R.id.indicator_value)) - .check(matches(allOf(isDisplayed(), withText(value)))) + composeTestRule.onNodeWithText(name).assertIsDisplayed() + composeTestRule.onNodeWithText(value).assertIsDisplayed() } fun checkLabel(label: String, position: Int) { @@ -144,7 +126,7 @@ class FormRobot : BaseRobot() { clickOnSpinner(position) onView(allOf(instanceOf(MenuItem::class.java), hasDescendant(withText(label)))) .check(doesNotExist()) - selectAction("", 0) + selectAction(0) } fun checkDisplayedOption(label: String, position: Int, activity: Activity) { @@ -152,26 +134,18 @@ class FormRobot : BaseRobot() { onView(withText(label)) .inRoot(withDecorView(not(`is`(activity.window.decorView)))) .check(matches(isDisplayed())) - selectAction("", 0) - } - - fun clickOnNotNow(composeTestRule: ComposeTestRule) { - composeTestRule.onNodeWithTag(SECONDARY_BUTTON_TAG).performClick() + selectAction(0) } - fun clickOnSelectOption(label: String, position: Int, option: String, optionPosition: Int) { + fun clickOnSelectOption(position: Int, optionPosition: Int) { clickOnSpinner(position) - selectAction(option, optionPosition) + selectAction(optionPosition) } fun scrollToBottomForm() { onView(withId(R.id.recyclerView)).perform(scrollToBottomRecyclerView()) } - fun scrollToPositionForm(position: Int) { - onView(withId(R.id.recyclerView)).perform(scrollToPositionRecyclerview(position)) - } - fun goToAnalytics() { onView(withId(R.id.navigation_analytics)).perform(click()) } diff --git a/app/src/androidTest/java/org/dhis2/usescases/form/FormTest.kt b/app/src/androidTest/java/org/dhis2/usescases/form/FormTest.kt index 0047e6f375..06c74900a8 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/form/FormTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/form/FormTest.kt @@ -1,34 +1,22 @@ package org.dhis2.usescases.form +import android.util.Log import androidx.compose.ui.test.junit4.createComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.ActivityTestRule +import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity -import org.dhis2.usescases.orgunitselector.orgUnitSelectorRobot -import org.dhis2.usescases.searchTrackEntity.SearchTEActivity -import org.dhis2.usescases.searchte.robot.searchTeiRobot -import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity -import org.dhis2.usescases.teidashboard.robot.enrollmentRobot -import org.dhis2.usescases.teidashboard.robot.eventRobot -import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot import org.junit.After import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class FormTest : BaseTest() { +const val firstSectionPosition = 2 - @get:Rule - val rule = ActivityTestRule(EventCaptureActivity::class.java, false, false) +class FormTest : BaseTest() { @get:Rule - val ruleTeiDashboard = ActivityTestRule(TeiDashboardMobileActivity::class.java, false, false) + val ruleEvent = lazyActivityScenarioRule(launchActivity = false) - @get:Rule - val ruleSearch = ActivityTestRule(SearchTEActivity::class.java, false, false) @get:Rule val composeTestRule = createComposeRule() @@ -39,176 +27,168 @@ class FormTest : BaseTest() { super.teardown() } - @Ignore("indeterminate test") + @Ignore("When added Event details section the test fails, is commented to be refactored with new form in a specific issue") @Test - fun shouldSuccessfullyUseForm() { - val rulesFirstSection = "ZZ TEST RULE ACTIONS A" - val firstSectionPosition = 1 - initTest() + fun shouldApplyProgramRules() { + prepareIntentAndLaunchEventActivity(ruleEvent) + + formRobot(composeTestRule) { + clickOnASpecificSection("Gamma Rules A") + } + applyHideField() + applyHideSection() + applyShowWarning() + applyShowError() + applySetMandatoryField() + applyHideOption() + applyHideOptionGroup() + applyShowOptionGroup() + applyAssignValue() + applyDisplayText() + applyDisplayKeyValue() + applyWarningOnComplete() + applyErrorOnComplete() + } - formRobot { + private fun applyHideField() { + formRobot(composeTestRule) { clickOnSelectOption( - rulesFirstSection, firstSectionPosition, - HIDE_FIELD, HIDE_FIELD_POSITION ) checkHiddenField("ZZ TEST LONGTEST") } + } - formRobot { - resetToNoAction(rulesFirstSection, firstSectionPosition) + private fun applyHideSection() { + formRobot(composeTestRule) { + resetToNoAction(firstSectionPosition) clickOnSelectOption( - rulesFirstSection, firstSectionPosition, - HIDE_SECTION, HIDE_SECTION_POSITION ) checkHiddenSection("Gamma Rules A") } + } - formRobot { - resetToNoAction(rulesFirstSection, firstSectionPosition) + private fun applyShowWarning() { + formRobot(composeTestRule) { + resetToNoAction(firstSectionPosition) clickOnSelectOption( - rulesFirstSection, firstSectionPosition, - SHOW_WARNING, SHOW_WARNING_POSITION ) checkWarningIsShown() } + } - formRobot { - resetToNoAction(rulesFirstSection, firstSectionPosition) + private fun applyShowError() { + formRobot(composeTestRule) { + resetToNoAction(firstSectionPosition) clickOnSelectOption( - rulesFirstSection, firstSectionPosition, - SHOW_ERROR, SHOW_ERROR_POSITION ) checkErrorIsShown() } + } - formRobot { + private fun applySetMandatoryField() { + formRobot(composeTestRule) { val nonMandatoryLabel = "ZZ TEST NUMBER" val mandatoryLabel = "ZZ TEST NUMBER *" - val position = 4 - resetToNoAction(rulesFirstSection, firstSectionPosition) + val position = 5 + resetToNoAction(firstSectionPosition) checkLabel(nonMandatoryLabel, position) clickOnSelectOption( - rulesFirstSection, firstSectionPosition, - MANDATORY_FIELD, MANDATORY_FIELD_POSITION ) checkLabel(mandatoryLabel, position) } + } - formRobot { - resetToNoAction(rulesFirstSection, firstSectionPosition) + private fun applyHideOption() { + formRobot(composeTestRule) { + resetToNoAction(firstSectionPosition) clickOnSelectOption( - rulesFirstSection, firstSectionPosition, - HIDE_OPTION, HIDE_OPTION_POSITION ) checkHiddenOption("North", OPTION_SET_FIELD_POSITION) } + } - formRobot { - resetToNoAction(rulesFirstSection, firstSectionPosition) + private fun applyHideOptionGroup() { + formRobot(composeTestRule) { + resetToNoAction(firstSectionPosition) clickOnSelectOption( - rulesFirstSection, firstSectionPosition, - HIDE_OPTION_GROUP, HIDE_OPTION_GROUP_POSITION ) checkHiddenOption("North", OPTION_SET_FIELD_POSITION) checkHiddenOption("West", OPTION_SET_FIELD_POSITION) } + } - formRobot { - resetToNoAction(rulesFirstSection, firstSectionPosition) + private fun applyShowOptionGroup() { + formRobot(composeTestRule) { + resetToNoAction(firstSectionPosition) clickOnSelectOption( - rulesFirstSection, firstSectionPosition, - SHOW_OPTION_GROUP, SHOW_OPTION_POSITION ) - checkDisplayedOption("North", OPTION_SET_FIELD_POSITION, ruleSearch.activity) - checkDisplayedOption("West", OPTION_SET_FIELD_POSITION, ruleSearch.activity) + + val activity = waitForActivityScenario() + checkDisplayedOption("North", OPTION_SET_FIELD_POSITION, activity) + checkDisplayedOption("West", OPTION_SET_FIELD_POSITION, activity) } } - @Ignore("Indeterminate (flaky)") - @Test - fun shouldApplyAssignAction() { - val rulesFirstSection = "ZZ TEST RULE ACTIONS A" - val firstSectionPosition = 1 - initTest() - - formRobot { - resetToNoAction(rulesFirstSection, firstSectionPosition) + private fun applyAssignValue() { + formRobot(composeTestRule) { + resetToNoAction(firstSectionPosition) clickOnSelectOption( - rulesFirstSection, firstSectionPosition, - ASSIGN_VALUE, ASSIGN_VALUE_POSITION ) checkValueWasAssigned(ASSIGNED_VALUE_TEXT) } } - @Test - fun shouldApplyIndicatorRelatedActions() { - val rulesFirstSection = "ZZ TEST RULE ACTIONS A" - val firstSectionPosition = 1 - initTest() - - formRobot { - resetToNoAction(rulesFirstSection, firstSectionPosition) + private fun applyDisplayText() { + formRobot(composeTestRule) { + resetToNoAction(firstSectionPosition) clickOnSelectOption( - rulesFirstSection, firstSectionPosition, - DISPLAY_TEXT, DISPLAY_TEXT_POSITION ) pressBack() goToAnalytics() - waitToDebounce(3000) checkIndicatorIsDisplayed("Info", "Current Option Selected: DT") goToDataEntry() } + } - formRobot { - resetToNoAction(rulesFirstSection, firstSectionPosition) + private fun applyDisplayKeyValue() { + formRobot(composeTestRule) { + resetToNoAction(firstSectionPosition) clickOnSelectOption( - rulesFirstSection, firstSectionPosition, - DISPLAY_KEY, DISPLAY_KEY_POSITION ) pressBack() goToAnalytics() - waitToDebounce(3000) checkIndicatorIsDisplayed("Current Option", "DKVP") goToDataEntry() } } - @Ignore("indeterminate test") - @Test - fun shouldApplyWarningAndErrorOnComplete() { - val rulesFirstSection = "ZZ TEST RULE ACTIONS A" - val firstSectionPosition = 1 - initTest() - - formRobot { - resetToNoAction(rulesFirstSection, firstSectionPosition) + private fun applyWarningOnComplete() { + formRobot(composeTestRule) { + resetToNoAction(firstSectionPosition) clickOnSelectOption( - rulesFirstSection, firstSectionPosition, - WARNING_COMPLETE, WARNING_COMPLETE_POSITION ) scrollToBottomForm() @@ -217,13 +197,13 @@ class FormTest : BaseTest() { checkPopUpWithMessageOnCompleteIsShown("WARNING_ON_COMPLETE", composeTestRule) pressBack() } + } - formRobot { - resetToNoAction(rulesFirstSection, firstSectionPosition) + private fun applyErrorOnComplete() { + formRobot(composeTestRule) { + resetToNoAction(firstSectionPosition) clickOnSelectOption( - rulesFirstSection, firstSectionPosition, - ERROR_COMPLETE, ERROR_COMPLETE_POSITION ) scrollToBottomForm() @@ -234,119 +214,33 @@ class FormTest : BaseTest() { } } - @Ignore("Indeterminate test") - @Test - fun shouldApplyOptionRelatedActions() { - val rulesFirstSection = "ZZ TEST RULE ACTIONS A" - val firstSectionPosition = 1 - startSearchActivity(ruleSearch) - - searchTeiRobot { - clickOnOpenSearch() - typeAttributeAtPosition("optionGroup", 1) - clickOnSearch() - clickOnEnroll() - orgUnitSelectorRobot(composeTestRule) { - selectTreeOrgUnit("Ngelehun CHC") - } - acceptDate() - } - - enrollmentRobot { - clickOnPersonAttributesUsingButton("Attributes - Person") - scrollToBottomProgramForm() - clickOnInputDate("DD TEST AGE *") - clickOnDatePicker() - clickOnAcceptEnrollmentDate() - clickOnInputDate("DD TEST DATE *") - clickOnAcceptEnrollmentDate() - clickOnSaveEnrollment() - } - - eventRobot { - clickOnUpdate() - waitToDebounce(3000) - } - - formRobot { - resetToNoAction(rulesFirstSection, firstSectionPosition) - clickOnSelectOption( - rulesFirstSection, - firstSectionPosition, - HIDE_OPTION_GROUP, - HIDE_OPTION_GROUP_POSITION - ) - checkHiddenOption("North", OPTION_SET_FIELD_POSITION) - checkHiddenOption("West", OPTION_SET_FIELD_POSITION) - } - } - - private fun initTest() { - - startSearchActivity(ruleSearch) - - searchTeiRobot { - clickOnOpenSearch() - typeAttributeAtPosition("abc", 1) - clickOnSearch() - clickOnEnroll() - orgUnitSelectorRobot(composeTestRule) { - selectTreeOrgUnit("Ngelehun CHC") - } - acceptDate() + private fun waitForActivityScenario(): EventCaptureActivity { + var activity: EventCaptureActivity? = null + ruleEvent.getScenario().onActivity { + activity = it } - - enrollmentRobot { - waitToDebounce(500) - clickOnPersonAttributes("Attributes - Person") - scrollToBottomProgramForm() - clickOnInputDate("DD TEST AGE *") - clickOnDatePicker() - clickOnAcceptEnrollmentDate() - clickOnInputDate("DD TEST DATE *") - clickOnAcceptEnrollmentDate() - clickOnSaveEnrollment() - } - - eventRobot { - clickOnUpdate() - waitToDebounce(3000) + while (activity == null) { + Log.d("FormTest", "Waiting for activity to be initialized") } + return activity!! } companion object { - const val NO_ACTION = "No Action" const val NO_ACTION_POSITION = 0 - const val HIDE_FIELD = "Hide Field" const val HIDE_FIELD_POSITION = 1 - const val HIDE_SECTION = "Hide Section" const val HIDE_SECTION_POSITION = 2 - const val HIDE_OPTION = "Hide Option" const val HIDE_OPTION_POSITION = 3 - const val OPTION_SET_FIELD_POSITION = 5 - const val HIDE_OPTION_GROUP = "Hide Option Group" + const val OPTION_SET_FIELD_POSITION = 6 const val HIDE_OPTION_GROUP_POSITION = 4 - const val ASSIGN_VALUE = "Assign Value" const val ASSIGN_VALUE_POSITION = 5 const val ASSIGNED_VALUE_TEXT = "Result for current event" - const val SHOW_WARNING = "Show Warning" const val SHOW_WARNING_POSITION = 6 - const val WARNING_COMPLETE = "Warning on Complete" const val WARNING_COMPLETE_POSITION = 7 - const val SHOW_ERROR = "Show Error" const val SHOW_ERROR_POSITION = 8 - const val ERROR_COMPLETE = "Error on Complete" const val ERROR_COMPLETE_POSITION = 9 - const val MANDATORY_FIELD = "Make Field Mandatory" const val MANDATORY_FIELD_POSITION = 10 - const val DISPLAY_TEXT = "Display Text" const val DISPLAY_TEXT_POSITION = 11 - const val DISPLAY_KEY = "Display Key/Value Pair" const val DISPLAY_KEY_POSITION = 12 - const val HIDE_PROGRAM_STAGE = "Hide Program Stage" - const val HIDE_PROGRAM_STAGE_POSITION = 13 - const val SHOW_OPTION_GROUP = "Show Option Group" const val SHOW_OPTION_POSITION = 14 } - } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/main/program/ProgramUiTest.kt b/app/src/androidTest/java/org/dhis2/usescases/main/program/ProgramUiTest.kt index e2f5a8ab76..580871fb50 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/main/program/ProgramUiTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/main/program/ProgramUiTest.kt @@ -10,7 +10,9 @@ import androidx.compose.ui.test.performClick import androidx.test.platform.app.InstrumentationRegistry import org.dhis2.R import org.dhis2.ui.MetadataIconData +import org.dhis2.ui.toColor import org.hisp.dhis.android.core.common.State +import org.hisp.dhis.mobile.ui.designsystem.component.internal.ImageCardData import org.junit.Rule import org.junit.Test @@ -82,9 +84,9 @@ class ProgramUiTest { ProgramViewModel( uid = "qweqwe", title = "Program title", - metadataIconData = MetadataIconData( - programColor = Color.parseColor("#00BCD4"), - iconResource = R.drawable.ic_info + MetadataIconData( + imageCardData = ImageCardData.IconCardData("", "", "ic_info", "#00BCD4".toColor()), + color = "#00BCD4".toColor(), ), count = 12, type = "type", diff --git a/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt b/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt index 6ec2122d91..68549602fa 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt @@ -3,24 +3,26 @@ package org.dhis2.usescases.programevent import android.Manifest import android.content.Intent import androidx.compose.ui.test.junit4.createComposeRule -import androidx.test.rule.ActivityTestRule +import androidx.test.core.app.ApplicationProvider import org.dhis2.AppTest.Companion.DB_TO_IMPORT +import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest -import org.dhis2.usescases.event.eventRegistrationRobot +import org.dhis2.usescases.orgunitselector.orgUnitSelectorRobot import org.dhis2.usescases.programEventDetail.ProgramEventDetailActivity -import org.dhis2.usescases.programEventDetail.eventList.EventListFragment import org.dhis2.usescases.programevent.robot.programEventsRobot import org.dhis2.usescases.teidashboard.robot.eventRobot +import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test class ProgramEventTest : BaseTest() { - private val atenatalCare = "lxAQ7Zs9VYR" + private val antenatalCare = "lxAQ7Zs9VYR" private val informationCampaign = "q04UBOqq3rp" @get:Rule - val rule = ActivityTestRule(ProgramEventDetailActivity::class.java, false, false) + val rule = lazyActivityScenarioRule(launchActivity = false) @get:Rule val composeTestRule = createComposeRule() @@ -29,33 +31,32 @@ class ProgramEventTest : BaseTest() { return arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) } + @Before + override fun setUp() { + super.setUp() + enableComposeForms() + } + @Test fun shouldCreateNewEventAndCompleteIt() { val eventOrgUnit = "Ngelehun CHC" - prepareProgramAndLaunchActivity(atenatalCare) - disableRecyclerViewAnimations() + prepareProgramAndLaunchActivity(antenatalCare) - programEventsRobot { + programEventsRobot(composeTestRule) { clickOnAddEvent() } - eventRegistrationRobot { - clickNextButton() + orgUnitSelectorRobot(composeTestRule) { + selectTreeOrgUnit(eventOrgUnit) } - eventRobot { + eventRobot(composeTestRule) { + typeOnDateParameter( + dateValue = "01012001", + ) clickOnFormFabButton() - clickOnCompleteButton(composeTestRule) - } - programEventsRobot { - checkEventWasCreatedAndClosed(eventOrgUnit, 0) + clickOnCompleteButton() } - } - - private fun disableRecyclerViewAnimations() { - val activity = rule.activity - activity.runOnUiThread { - activity.supportFragmentManager.findFragmentByTag("EVENT_LIST").apply { - (this as EventListFragment).binding.recycler.itemAnimator = null - } + programEventsRobot(composeTestRule) { + checkEventWasCreatedAndClosed(eventOrgUnit) } } @@ -64,88 +65,65 @@ class ProgramEventTest : BaseTest() { val eventDate = "15/3/2020" val eventOrgUnit = "Ngelehun CHC" - prepareProgramAndLaunchActivity(atenatalCare) + prepareProgramAndLaunchActivity(antenatalCare) - programEventsRobot { - clickOnEvent(eventDate, eventOrgUnit) + programEventsRobot(composeTestRule) { + clickOnEvent(eventDate) } - eventRobot { - checkDetails(eventDate, eventOrgUnit) + eventRobot(composeTestRule) { + openEventDetailsSection() + checkEventDetails(eventDate, eventOrgUnit) } } + @Ignore("Flaky test, will be look om issue ANDROAPP-6030") @Test fun shouldCompleteAnEventAndReopenIt() { val eventDate = "15/3/2020" - val eventOrgUnit = "Ngelehun CHC" - prepareProgramAndLaunchActivity(atenatalCare) - disableRecyclerViewAnimations() + prepareProgramAndLaunchActivity(antenatalCare) - programEventsRobot { - clickOnEvent(eventDate, eventOrgUnit) + programEventsRobot(composeTestRule) { + clickOnEvent(eventDate) } - eventRobot { + eventRobot(composeTestRule) { clickOnFormFabButton() - clickOnCompleteButton(composeTestRule) - waitToDebounce(400) + clickOnCompleteButton() } - programEventsRobot { - checkEventIsComplete(eventDate, eventOrgUnit) - clickOnEvent(eventDate, eventOrgUnit) + programEventsRobot(composeTestRule) { + checkEventIsComplete(eventDate) + clickOnEvent(eventDate) } - eventRobot { - clickOnDetails() + eventRobot(composeTestRule) { clickOnReopen() - pressBack() - } - - programEventsRobot { - waitToDebounce(800) - checkEventIsOpen(eventDate, eventOrgUnit) - } - } - - @Test - fun shouldOpenDetailsOfExistingEvent() { - val eventDate = "15/3/2020" - val eventOrgUnit = "Ngelehun CHC" - - prepareProgramAndLaunchActivity(atenatalCare) - - programEventsRobot { - clickOnEvent(eventDate, eventOrgUnit) - } - eventRobot { - clickOnDetails() - checkEventDetails(eventDate, eventOrgUnit, composeTestRule) + checkEventIsOpen() } } @Test fun shouldDeleteEvent() { val eventDate = "15/3/2020" - val eventOrgUnit = "Ngelehun CHC" - prepareProgramAndLaunchActivity(atenatalCare) - disableRecyclerViewAnimations() + prepareProgramAndLaunchActivity(antenatalCare) - programEventsRobot { - clickOnEvent(eventDate, eventOrgUnit) + programEventsRobot(composeTestRule) { + clickOnEvent(eventDate) } - eventRobot { + eventRobot(composeTestRule) { openMenuMoreOptions() clickOnDelete() clickOnDeleteDialog() } - programEventsRobot { - checkEventWasDeleted(eventDate, eventOrgUnit) + programEventsRobot(composeTestRule) { + checkEventWasDeleted(eventDate) + } + rule.getScenario().onActivity { + context.applicationContext.deleteDatabase(DB_TO_IMPORT) } - rule.activity.application.deleteDatabase(DB_TO_IMPORT) } @Test @@ -153,15 +131,18 @@ class ProgramEventTest : BaseTest() { prepareProgramAndLaunchActivity(informationCampaign) - programEventsRobot { + programEventsRobot(composeTestRule) { clickOnMap() checkMapIsDisplayed() } } private fun prepareProgramAndLaunchActivity(programUid: String) { - Intent().apply { + Intent( + ApplicationProvider.getApplicationContext(), + ProgramEventDetailActivity::class.java, + ).apply { putExtra(ProgramEventDetailActivity.EXTRA_PROGRAM_UID, programUid) - }.also { rule.launchActivity(it) } + }.also { rule.launch(it) } } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt index db3e8a81cb..d430956e6d 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt @@ -1,43 +1,40 @@ package org.dhis2.usescases.programevent.robot +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.RecyclerViewActions -import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem -import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withTagValue +import androidx.test.espresso.matcher.ViewMatchers.withText import org.dhis2.R import org.dhis2.common.BaseRobot -import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.hasItem -import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.EventViewHolder import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.equalTo -import org.hamcrest.CoreMatchers.not -fun programEventsRobot(programEventsRobot: ProgramEventsRobot.() -> Unit) { - ProgramEventsRobot().apply { +fun programEventsRobot( + composeTestRule: ComposeContentTestRule, + programEventsRobot: ProgramEventsRobot.() -> Unit +) { + ProgramEventsRobot(composeTestRule).apply { programEventsRobot() } } -class ProgramEventsRobot : BaseRobot() { +class ProgramEventsRobot(val composeTestRule: ComposeContentTestRule) : BaseRobot() { - fun clickOnEvent(eventDate: String, eventOrgUnit: String) { - onView(withId(R.id.recycler)).perform( - RecyclerViewActions.scrollTo( - allOf( - hasDescendant(withText(eventDate)), - hasDescendant(withText(eventOrgUnit)) - ) - ), - actionOnItem( - allOf( - hasDescendant(withText(eventDate)), - hasDescendant(withText(eventOrgUnit)) - ), click() - ) - ) + @OptIn(ExperimentalTestApi::class) + fun clickOnEvent(eventDate: String) { + composeTestRule.waitUntilAtLeastOneExists(hasText(eventDate)) + composeTestRule.onNodeWithText(eventDate).performClick() } fun clickOnAddEvent() { @@ -48,7 +45,7 @@ class ProgramEventsRobot : BaseRobot() { onView(withId(R.id.navigation_map_view)).perform(click()) } - fun checkEventWasCreatedAndClosed(eventName: String, position: Int) { + fun checkEventWasCreatedAndClosed(eventName: String) { waitForView( allOf( withId(R.id.recycler), @@ -65,68 +62,13 @@ class ProgramEventsRobot : BaseRobot() { ).check(matches(isDisplayed())) } - fun checkEventIsComplete(eventDate: String, eventOrgUnit: String) { - onView(withId(R.id.recycler)) - .check( - matches( - allOf( - hasItem( - allOf( - hasDescendant(withText(eventDate)), - hasDescendant(withText(eventOrgUnit)), - hasDescendant( - withTagValue( - anyOf( - equalTo(R.drawable.ic_event_status_complete), - equalTo(R.drawable.ic_event_status_complete_read) - ) - ) - ) - ) - ) - ) - ) - ) - } - - fun checkEventIsOpen(eventDate: String, eventOrgUnit: String) { - onView(withId(R.id.recycler)) - .check( - matches( - allOf( - hasItem( - allOf( - hasDescendant(withText(eventDate)), - hasDescendant(withText(eventOrgUnit)), - hasDescendant( - withTagValue( - anyOf( - equalTo(R.drawable.ic_event_status_open), - equalTo(R.drawable.ic_event_status_open_read) - ) - ) - ) - ) - ) - ) - ) - ) + fun checkEventIsComplete(eventDate: String) { + composeTestRule.onNodeWithText(eventDate).assertIsDisplayed() + composeTestRule.onNodeWithText("Event completed").assertIsDisplayed() } - fun checkEventWasDeleted(eventDate: String, eventOrgUnit: String) { - onView(withId(R.id.recycler)) - .check( - matches( - not( - hasItem( - allOf( - hasDescendant(withText(eventDate)), - hasDescendant(withText(eventOrgUnit)) - ) - ) - ) - ) - ) + fun checkEventWasDeleted(eventDate: String) { + composeTestRule.onNodeWithText(eventDate).assertDoesNotExist() } fun checkMapIsDisplayed() { diff --git a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTEIntents.kt b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTEIntents.kt index ab7517fa2a..e13224bf4d 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTEIntents.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTEIntents.kt @@ -1,7 +1,8 @@ package org.dhis2.usescases.searchte import android.content.Intent -import androidx.test.rule.ActivityTestRule +import androidx.test.core.app.ApplicationProvider +import org.dhis2.LazyActivityScenarioRule import org.dhis2.usescases.searchTrackEntity.SearchTEActivity private const val PROGRAM_UID = "PROGRAM_UID" @@ -16,25 +17,32 @@ private const val ADULT_WOMAN_TE_TYPE_VALUE = "nEenWmSyUEp" private const val TB_TE_TYPE_VALUE = "nEenWmSyUEp" private const val CHILD_TE_TYPE = "TRACKED_ENTITY_UID" -fun prepareChildProgrammeIntentAndLaunchActivity(ruleSearch: ActivityTestRule) { +fun prepareChildProgrammeIntentAndLaunchActivity(ruleSearch: LazyActivityScenarioRule) { startSearchActivity(CHILD_PROGRAM_UID_VALUE, CHILD_TE_TYPE_VALUE, ruleSearch) } -fun prepareTestProgramRulesProgrammeIntentAndLaunchActivity(ruleSearch: ActivityTestRule) { +fun prepareTestProgramRulesProgrammeIntentAndLaunchActivity(ruleSearch: LazyActivityScenarioRule) { startSearchActivity(XX_TEST_PROGRAM_RULES_UID_VALUE, PROGRAM_RULES_TE_TYPE_VALUE, ruleSearch) } -fun prepareTestAdultWomanProgrammeIntentAndLaunchActivity(ruleSearch: ActivityTestRule) { +fun prepareTestAdultWomanProgrammeIntentAndLaunchActivity(ruleSearch: LazyActivityScenarioRule) { startSearchActivity(ADULT_WOMAN_PROGRAM_UID_VALUE, ADULT_WOMAN_TE_TYPE_VALUE, ruleSearch) } -fun prepareTBIntentAndLaunchActivity(ruleSearch: ActivityTestRule) { +fun prepareTBIntentAndLaunchActivity(ruleSearch: LazyActivityScenarioRule) { startSearchActivity(TB_PROGRAM_UID_VALUE, TB_TE_TYPE_VALUE, ruleSearch) } -fun startSearchActivity(programUID: String?, teType: String, ruleSearch: ActivityTestRule) { - Intent().apply { +fun startSearchActivity( + programUID: String?, + teType: String, + ruleSearch: LazyActivityScenarioRule +) { + Intent( + ApplicationProvider.getApplicationContext(), + SearchTEActivity::class.java, + ).apply { putExtra(PROGRAM_UID, programUID) putExtra(CHILD_TE_TYPE, teType) - }.also { ruleSearch.launchActivity(it) } + }.also { ruleSearch.launch(it) } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt index b7cf9e5d5f..eadaad1c7b 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt @@ -1,13 +1,16 @@ package org.dhis2.usescases.searchte +import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.text.capitalize +import androidx.compose.ui.text.intl.Locale import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingResourceTimeoutException import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation -import androidx.test.rule.ActivityTestRule import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until @@ -16,6 +19,8 @@ import dispatch.android.espresso.IdlingDispatcherProviderRule import org.dhis2.R import org.dhis2.bindings.app import org.dhis2.common.idlingresources.MapIdlingResource +import org.dhis2.commons.date.DateUtils.SIMPLE_DATE_FORMAT +import org.dhis2.lazyActivityScenarioRule import org.dhis2.ui.dialogs.bottomsheet.SECONDARY_BUTTON_TAG import org.dhis2.usescases.BaseTest import org.dhis2.usescases.flow.teiFlow.TeiFlowTest @@ -33,13 +38,14 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import java.text.SimpleDateFormat +import java.util.Calendar import java.util.Date @RunWith(AndroidJUnit4::class) class SearchTETest : BaseTest() { @get:Rule - val rule = ActivityTestRule(SearchTEActivity::class.java, false, false) + val rule = lazyActivityScenarioRule(launchActivity = false) private var mapIdlingResource: MapIdlingResource? = null @@ -58,29 +64,32 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullySearchByName() { val firstName = "Tim" - val firstNamePosition = 0 - val orgUnit = "Ngelehun CHC" + val lastName = "Johnson" prepareChildProgrammeIntentAndLaunchActivity(rule) - searchTeiRobot { + searchTeiRobot(composeTestRule) { clickOnOpenSearch() - typeAttributeAtPosition(firstName, firstNamePosition) + openNextSearchParameter("First name") + typeOnNextSearchTextParameter(firstName) clickOnSearch() - checkListOfSearchTEI(firstName, orgUnit) + checkListOfSearchTEI( + title = "First name: $firstName", + attributes = mapOf("Last name:" to lastName) + ) } } @Test fun shouldShowErrorWhenCanNotFindSearchResult() { val firstName = "asdssds" - val firstNamePosition = 1 prepareTestProgramRulesProgrammeIntentAndLaunchActivity(rule) - searchTeiRobot { + searchTeiRobot(composeTestRule) { clickOnOpenSearch() - typeAttributeAtPosition(firstName, firstNamePosition) + openNextSearchParameter("First name") + typeOnNextSearchTextParameter(firstName) clickOnSearch() checkNoSearchResult() } @@ -89,18 +98,22 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullySearchUsingMoreThanOneField() { val firstName = "Anna" - val firstNamePosition = 0 val lastName = "Jones" - val lastNamePosition = 1 prepareChildProgrammeIntentAndLaunchActivity(rule) - searchTeiRobot { + searchTeiRobot(composeTestRule) { clickOnOpenSearch() - typeAttributeAtPosition(firstName, firstNamePosition) - typeAttributeAtPosition(lastName, lastNamePosition) + openNextSearchParameter("First name") + typeOnNextSearchTextParameter(firstName) + openNextSearchParameter("Last name") + typeOnNextSearchTextParameter(lastName) clickOnSearch() - checkListOfSearchTEI(firstName, lastName) + composeTestRule.waitForIdle() + checkListOfSearchTEI( + title = "First name: $firstName", + attributes = mapOf("Last name:" to lastName) + ) } } @@ -111,7 +124,7 @@ class SearchTETest : BaseTest() { prepareChildProgrammeIntentAndLaunchActivity(rule) - searchTeiRobot { + searchTeiRobot(composeTestRule) { clickOnProgramSpinner() selectAProgram(tbProgram) checkProgramHasChanged(tbProgram) @@ -120,23 +133,19 @@ class SearchTETest : BaseTest() { @Test fun shouldCheckDisplayInList() { - val birthdaySearch = createDateOfBirthSearch() val displayInListData = createDisplayListFields() - val namePosition = 0 - val lastNamePosition = 1 - setDatePicker() prepareTestAdultWomanProgrammeIntentAndLaunchActivity(rule) - searchTeiRobot { - typeAttributeAtPosition(displayInListData.name, namePosition) - typeAttributeAtPosition(displayInListData.lastName, lastNamePosition) - clickOnDateField() - selectSpecificDate(birthdaySearch.year, birthdaySearch.month, birthdaySearch.day) - acceptDate() + searchTeiRobot(composeTestRule) { + openNextSearchParameter("First name") + typeOnNextSearchTextParameter(displayInListData.name) + openNextSearchParameter("Last name") + typeOnNextSearchTextParameter(displayInListData.lastName) + openNextSearchParameter("Date of birth") + typeOnDateParameter("01012001") clickOnSearch() checkFieldsFromDisplayList( - composeTestRule, displayInListData, ) } @@ -145,6 +154,10 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullyFilterByEnrollmentStatusActive() { val enrollmentStatusFilter = context.getString(R.string.filters_title_enrollment_status) + .format( + context.resources.getQuantityString(R.plurals.enrollment, 1) + .capitalize(Locale.current) + ) val totalFilterCount = "2" val filterCount = "1" @@ -163,21 +176,23 @@ class SearchTETest : BaseTest() { } @Test + @Ignore("Test is successful locally but not in browserstack") fun shouldSuccessfullyFilterByEventStatusOverdue() { val eventStatusFilter = context.getString(R.string.filters_title_event_status) val totalCount = "1" - - val programStage = "PNC Visit" - val orgUnit = "Ngelehun CHC" val registerTeiDetails = createRegisterTEI() - val overdueDate = createOverdueDate() + val overdueDate = getCurrentDate() + val dateFormat = + SimpleDateFormat(SIMPLE_DATE_FORMAT, java.util.Locale.getDefault()).format(Date()) + val scheduledEventTitle = context.getString(R.string.scheduled_for) + .format(dateFormat) setDatePicker() prepareTestAdultWomanProgrammeIntentAndLaunchActivity(rule) - teiFlowRobot { + teiFlowRobot(composeTestRule) { registerTEI(registerTeiDetails) - changeDueDate(overdueDate, programStage, orgUnit, composeTestRule) + changeDueDate(scheduledEventTitle, overdueDate) pressBack() composeTestRule.onNodeWithTag(SECONDARY_BUTTON_TAG).performClick() pressBack() @@ -273,21 +288,21 @@ class SearchTETest : BaseTest() { fun shouldSuccessfullyFilterBySync() { val teiName = "Frank" val teiLastName = "Fjordsen" - val firstNamePosition = 0 - val lastNamePosition = 1 val syncFilter = context.getString(R.string.action_sync) val totalCount = "1" prepareChildProgrammeIntentAndLaunchActivity(rule) - searchTeiRobot { + searchTeiRobot(composeTestRule) { clickOnOpenSearch() - typeAttributeAtPosition(teiName, firstNamePosition) - typeAttributeAtPosition(teiLastName, lastNamePosition) + openNextSearchParameter("First name") + typeOnNextSearchTextParameter(teiName) + openNextSearchParameter("Last name") + typeOnNextSearchTextParameter(teiLastName) clickOnSearch() clickOnTEI(teiName, teiLastName) } - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnMenuReOpen() pressBack() @@ -308,16 +323,20 @@ class SearchTETest : BaseTest() { fun shouldSuccessfullySearchAndFilter() { val name = "Anna" val lastName = "Jones" - val namePosition = 0 val enrollmentStatus = context.getString(R.string.filters_title_enrollment_status) + .format( + context.resources.getQuantityString(R.plurals.enrollment, 1) + .capitalize(Locale.current) + ) val totalCount = "2" val totalFilterCount = "1" prepareChildProgrammeIntentAndLaunchActivity(rule) - searchTeiRobot { + searchTeiRobot(composeTestRule) { clickOnOpenSearch() - typeAttributeAtPosition(name, namePosition) + openNextSearchParameter("First name") + typeOnNextSearchTextParameter(name) clickOnSearch() } @@ -332,8 +351,11 @@ class SearchTETest : BaseTest() { checkTEIsAreOpen() } - searchTeiRobot { - checkListOfSearchTEI(name, lastName) + searchTeiRobot(composeTestRule) { + checkListOfSearchTEI( + title = "First name: $name", + attributes = mapOf("Last name:" to lastName) + ) } } @@ -343,7 +365,7 @@ class SearchTETest : BaseTest() { prepareChildProgrammeIntentAndLaunchActivity(rule) - searchTeiRobot { + searchTeiRobot(composeTestRule) { clickOnShowMap() try { val device = UiDevice.getInstance(getInstrumentation()) @@ -362,12 +384,6 @@ class SearchTETest : BaseTest() { } } - private fun createDateOfBirthSearch() = DateRegistrationUIModel( - 2001, - 1, - 1 - ) - private fun createDisplayListFields() = DisplayListFieldsUIModel( "Sarah", "Thompson", @@ -421,25 +437,14 @@ class SearchTETest : BaseTest() { 30 ) - private fun getSplitCurrentDate(): DateRegistrationUIModel { - val sdf = SimpleDateFormat(TeiFlowTest.DATE_FORMAT) - val dateFormat = sdf.format(Date()) - val splitDate: Array = dateFormat.removePrefix("0").split("/").toTypedArray() - val day = splitDate[0].toInt() - val month = splitDate[1].toInt() - val year = splitDate[2].toInt() - return DateRegistrationUIModel(year, month, day) + private fun getCurrentDate(): String { + val sdf = SimpleDateFormat(TeiFlowTest.DATE_PICKER_FORMAT) + val calendar = Calendar.getInstance() + return sdf.format(calendar.time) } - private fun createOverdueDate() = DateRegistrationUIModel( - currentDate.year, - currentDate.month - 1, - currentDate.day - ) - private val dateRegistration = createFirstSpecificDate() private val dateEnrollment = createEnrollmentDate() - private val currentDate = getSplitCurrentDate() companion object { const val PROGRAM_UID = "PROGRAM_UID" diff --git a/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt index 6713922c09..8080cc13a7 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt @@ -1,18 +1,22 @@ package org.dhis2.usescases.searchte.robot import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.hasParent +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onLast +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.recyclerview.widget.RecyclerView +import androidx.compose.ui.test.performTextInput import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition import androidx.test.espresso.contrib.RecyclerViewActions.scrollTo -import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText @@ -21,7 +25,6 @@ import org.dhis2.common.BaseRobot import org.dhis2.common.matchers.RecyclerviewMatchers import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.hasItem import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.hasNoMoreResultsInProgram -import org.dhis2.common.viewactions.clickChildViewWithId import org.dhis2.common.viewactions.openSpinnerPopup import org.dhis2.common.viewactions.typeChildViewWithId import org.dhis2.usescases.searchTrackEntity.adapters.SearchTEViewHolder @@ -30,21 +33,18 @@ import org.dhis2.usescases.searchte.entity.DisplayListFieldsUIModel import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem -import org.hisp.dhis.mobile.ui.designsystem.component.ListCard -fun searchTeiRobot(searchTeiRobot: SearchTeiRobot.() -> Unit) { - SearchTeiRobot().apply { +fun searchTeiRobot( + composeTestRule: ComposeTestRule, + searchTeiRobot: SearchTeiRobot.() -> Unit +) { + SearchTeiRobot(composeTestRule).apply { searchTeiRobot() } } -class SearchTeiRobot : BaseRobot() { - - fun closeSearchForm() { - waitToDebounce(2500) - onView(withId(R.id.close_filter)).perform(click()) - } +class SearchTeiRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun clickOnTEI(teiName: String, teiLastName: String) { waitForView( @@ -86,6 +86,22 @@ class SearchTeiRobot : BaseRobot() { ) } + fun openNextSearchParameter(parameterValue: String) { + composeTestRule.onNodeWithText(parameterValue).performClick() + } + + fun typeOnNextSearchTextParameter(parameterValue: String) { + composeTestRule.apply { + onAllNodesWithTag("INPUT_TEXT_FIELD").onLast().performTextInput(parameterValue) + } + } + + fun typeOnDateParameter(dateValue: String) { + composeTestRule.apply { + onNodeWithTag("INPUT_DATE_TIME_TEXT_FIELD").performTextInput(dateValue) + } + } + fun typeAttributeAtPosition(searchWord: String, position: Int) { onView(withId(R.id.recyclerView)) .perform( @@ -105,47 +121,21 @@ class SearchTeiRobot : BaseRobot() { closeKeyboard() } - fun clickOnDateField() { - onView(withId(R.id.recyclerView)) - .perform( - actionOnItemAtPosition( - 2, - clickChildViewWithId(R.id.inputEditText) - ) - ) - } - - fun selectSpecificDate(year: Int, monthOfYear: Int, dayOfMonth: Int) { - onView(withId(R.id.datePicker)).perform( - PickerActions.setDate( - year, - monthOfYear, - dayOfMonth - ) - ) - } - - fun acceptDate() { - onView(withId(R.id.acceptBtn)).perform(click()) - } - fun clickOnSearch() { closeKeyboard() - onView(withId(R.id.searchButton)).perform(click()) + composeTestRule.onNodeWithTag("SEARCH_BUTTON").performClick() } - fun checkListOfSearchTEI(firstSearchWord: String, secondSearchWord: String) { - onView(withId(R.id.scrollView)) - .check( - matches( - RecyclerviewMatchers.allElementsWithHolderTypeHave( - SearchTEViewHolder::class.java, allOf( - hasDescendant(withText(firstSearchWord)), - hasDescendant(withText(secondSearchWord)) - ) - ) - ) - ) + fun checkListOfSearchTEI(title: String, attributes: Map) { + //Checks title and all attributes are displayed + composeTestRule.onNodeWithText(title).assertIsDisplayed() + attributes.forEach { item -> + item.key?.let { composeTestRule.onNodeWithText(it).assertIsDisplayed() } + composeTestRule.onNode( + hasParent(hasTestTag("LIST_CARD_ADDITIONAL_INFO_COLUMN")) + and hasText(item.value), useUnmergedTree = true + ).assertIsDisplayed() + } } fun checkNoSearchResult() { @@ -177,21 +167,12 @@ class SearchTeiRobot : BaseRobot() { fun checkFieldsFromDisplayList( - composeTestRule: ComposeContentTestRule, displayListFieldsUIModel: DisplayListFieldsUIModel ) { //Given the title is the first attribute val title = "First name: ${displayListFieldsUIModel.name}" val displayedAttributes = createAttributesList(displayListFieldsUIModel) - composeTestRule.setContent { - ListCard( - title = title, - additionalInfoList = displayedAttributes, - onCardClick = { } - ) - } - //When we expand all attribute list composeTestRule.onNodeWithText("Show more").performClick() @@ -199,7 +180,10 @@ class SearchTeiRobot : BaseRobot() { composeTestRule.onNodeWithText(title).assertIsDisplayed() displayedAttributes.forEach { item -> item.key?.let { composeTestRule.onNodeWithText(it).assertIsDisplayed() } - composeTestRule.onNodeWithText(item.value).assertIsDisplayed() + composeTestRule.onNode( + hasParent(hasTestTag("LIST_CARD_ADDITIONAL_INFO_COLUMN")) + and hasText(item.value), useUnmergedTree = true + ).assertIsDisplayed() } } @@ -207,10 +191,6 @@ class SearchTeiRobot : BaseRobot() { onView(withId(R.id.navigation_map_view)).perform(click()) } - fun swipeCarouselToLeft() { - onView(withId(R.id.map_carousel)).perform(scrollToPosition(3)) - } - fun checkCarouselTEICardInfo(firstName: String) { onView(withId(R.id.map_carousel)) .check(matches(hasItem(hasDescendant(withText(firstName))))) @@ -220,14 +200,6 @@ class SearchTeiRobot : BaseRobot() { onView(withId(R.id.openSearchButton)).perform(click()) } - fun clickOnShowMoreFilters() { - onView(withId(R.id.search_filter_general)).perform(click()) - } - - fun clickOnAcceptButton() { - onView(withId(R.id.accept_button)).perform(click()) - } - fun clickOnEnroll() { onView(withId(R.id.createButton)).perform(click()) } diff --git a/app/src/androidTest/java/org/dhis2/usescases/sync/SyncActivityTest.kt b/app/src/androidTest/java/org/dhis2/usescases/sync/SyncActivityTest.kt index 712a27362b..a89404a409 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/sync/SyncActivityTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/sync/SyncActivityTest.kt @@ -2,13 +2,13 @@ package org.dhis2.usescases.sync import androidx.lifecycle.MutableLiveData import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.rules.activityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.ActivityTestRule import androidx.work.Data import androidx.work.WorkInfo import org.dhis2.AppTest -import org.dhis2.usescases.BaseTest import org.dhis2.commons.Constants +import org.dhis2.usescases.BaseTest import org.junit.Before import org.junit.Rule import org.junit.Test @@ -20,8 +20,7 @@ class SyncActivityTest : BaseTest() { private lateinit var workInfoStatusLiveData: MutableLiveData> @get:Rule - val syncRule = ActivityTestRule(SyncActivity::class.java, false, false) - + val rule = activityScenarioRule() @Before override fun setUp() { @@ -32,7 +31,6 @@ class SyncActivityTest : BaseTest() { @Test fun shouldShowMetadataErrorDialog() { - startSyncActivity() syncRobot { waitUntilActivityVisible() @@ -47,7 +45,6 @@ class SyncActivityTest : BaseTest() { @Test fun shouldCompleteSyncProcess() { - startSyncActivity() enableIntents() syncRobot { @@ -62,10 +59,6 @@ class SyncActivityTest : BaseTest() { } } - private fun startSyncActivity() { - syncRule.launchActivity(null) - } - private fun mockedMetaWorkInfo(state: WorkInfo.State): WorkInfo { return WorkInfo( UUID.randomUUID(), @@ -77,17 +70,5 @@ class SyncActivityTest : BaseTest() { 0 ) } - - private fun mockedDataWorkInfo(state: WorkInfo.State): WorkInfo { - return WorkInfo( - UUID.randomUUID(), - state, - Data.EMPTY, - arrayListOf(Constants.DATA_NOW), - Data.EMPTY, - 0, - 0 - ) - } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardIntents.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardIntents.kt index fe3011fd31..82305a577f 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardIntents.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardIntents.kt @@ -1,7 +1,8 @@ package org.dhis2.usescases.teidashboard import android.content.Intent -import androidx.test.rule.ActivityTestRule +import androidx.test.core.app.ApplicationProvider +import org.dhis2.LazyActivityScenarioRule import org.dhis2.usescases.searchTrackEntity.SearchTEActivity import org.dhis2.usescases.searchte.SearchTETest import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity @@ -37,23 +38,40 @@ private const val TEI_UID_VALUE_ANALYTICS = "wsk89u7zquT" private const val ENROLLMENT_UID_VALUE_ANALYTICS = "ITyaPVATEwc" fun prepareTeiCompletedProgrammeAndLaunchActivity( - rule: ActivityTestRule + rule: LazyActivityScenarioRule ) { - startTeiDashboardActivity(CHILD_PROGRAM_UID_VALUE, TEI_UID_VALUE_COMPLETED, ENROLLMENT_VALUE_COMPLETED, rule) + startTeiDashboardActivity( + CHILD_PROGRAM_UID_VALUE, + TEI_UID_VALUE_COMPLETED, + ENROLLMENT_VALUE_COMPLETED, + rule + ) } fun prepareTeiOpenedForReferralProgrammeAndLaunchActivity( - rule: ActivityTestRule + rule: LazyActivityScenarioRule ) { - startTeiDashboardActivity(TB_PROGRAM_UID, TEI_UID_VALUE_OPEN_REFERRAL, ENROLLMENT_VALUE_OPEN_REFERRAL, rule) + startTeiDashboardActivity( + TB_PROGRAM_UID, + TEI_UID_VALUE_OPEN_REFERRAL, + ENROLLMENT_VALUE_OPEN_REFERRAL, + rule + ) } -fun prepareTeiOpenedProgrammeAndLaunchActivity(rule: ActivityTestRule) { - startTeiDashboardActivity(CHILD_PROGRAM_UID_VALUE, TEI_UID_VALUE_OPENED, ENROLLMENT_VALUE_OPENED, rule) +fun prepareTeiOpenedProgrammeAndLaunchActivity( + rule: LazyActivityScenarioRule +) { + startTeiDashboardActivity( + CHILD_PROGRAM_UID_VALUE, + TEI_UID_VALUE_OPENED, + ENROLLMENT_VALUE_OPENED, + rule + ) } fun prepareTeiOpenedForCompleteProgrammeAndLaunchActivity( - rule: ActivityTestRule + rule: LazyActivityScenarioRule ) { startTeiDashboardActivity( CHILD_PROGRAM_UID_VALUE, @@ -64,7 +82,7 @@ fun prepareTeiOpenedForCompleteProgrammeAndLaunchActivity( } fun prepareTeiWithExistingNoteAndLaunchActivity( - rule: ActivityTestRule + rule: LazyActivityScenarioRule ) { startTeiDashboardActivity( CHILD_PROGRAM_UID_VALUE, @@ -75,60 +93,107 @@ fun prepareTeiWithExistingNoteAndLaunchActivity( } fun prepareTeiOpenedWithFullEventsAndLaunchActivity( - rule: ActivityTestRule + rule: LazyActivityScenarioRule ) { - startTeiDashboardActivity(CHILD_PROGRAM_UID_VALUE, TEI_UID_VALUE_OPENED_FULL, ENROLLMENT_VALUE_OPENED_FULL, rule) + startTeiDashboardActivity( + CHILD_PROGRAM_UID_VALUE, + TEI_UID_VALUE_OPENED_FULL, + ENROLLMENT_VALUE_OPENED_FULL, + rule + ) } -fun prepareTeiToDeleteAndLaunchActivity(rule: ActivityTestRule) { - startTeiDashboardActivity(CHILD_PROGRAM_UID_VALUE, TEI_UID_VALUE_TO_DELETE, ENROLLMENT_VALUE_TO_DELETE, rule) +fun prepareTeiToDeleteAndLaunchActivity( + rule: LazyActivityScenarioRule +) { + startTeiDashboardActivity( + CHILD_PROGRAM_UID_VALUE, + TEI_UID_VALUE_TO_DELETE, + ENROLLMENT_VALUE_TO_DELETE, + rule + ) } fun prepareTeiOpenedWithNoPreviousEventProgrammeAndLaunchActivity( - rule: ActivityTestRule + rule: LazyActivityScenarioRule ) { - startTeiDashboardActivity(TB_PROGRAM_UID, TEI_UID_VALUE_TO_SCHEDULE, ENROLLMENT_VALUE_TO_SCHEDULE, rule) + startTeiDashboardActivity( + TB_PROGRAM_UID, + TEI_UID_VALUE_TO_SCHEDULE, + ENROLLMENT_VALUE_TO_SCHEDULE, + rule + ) } fun prepareTeiToCreateANewEventAndLaunchActivity( - rule: ActivityTestRule + rule: LazyActivityScenarioRule ) { - startTeiDashboardActivity(TB_PROGRAM_UID, TEI_UID_VALUE_TO_CREATE_EVENT, ENROLLMENT_VALUE_TO_CREATE_EVENT, rule) + startTeiDashboardActivity( + TB_PROGRAM_UID, + TEI_UID_VALUE_TO_CREATE_EVENT, + ENROLLMENT_VALUE_TO_CREATE_EVENT, + rule + ) } -fun prepareTeiOpenedToEditAndLaunchActivity(rule: ActivityTestRule) { - startTeiDashboardActivity(TB_PROGRAM_UID, TEI_UID_VALUE_TO_EDIT_EVENT, ENROLLMENT_VALUE_TO_EDIT_EVENT, rule) +fun prepareTeiOpenedToEditAndLaunchActivity( + rule: LazyActivityScenarioRule +) { + startTeiDashboardActivity( + TB_PROGRAM_UID, + TEI_UID_VALUE_TO_EDIT_EVENT, + ENROLLMENT_VALUE_TO_EDIT_EVENT, + rule + ) } fun prepareTeiToEnrollToOtherProgramAndLaunchActivity( - rule: ActivityTestRule + rule: LazyActivityScenarioRule ) { - startTeiDashboardActivity(CHILD_PROGRAM_UID_VALUE, TEI_UID_VALUE_TO_ENROLL, ENROLLMENT_VALUE_TO_ENROLL, rule) + startTeiDashboardActivity( + CHILD_PROGRAM_UID_VALUE, + TEI_UID_VALUE_TO_ENROLL, + ENROLLMENT_VALUE_TO_ENROLL, + rule + ) } fun prepareTeiForAnalyticsAndLaunchActivity( - rule: ActivityTestRule -){ - startTeiDashboardActivity(TB_PROGRAM_UID, TEI_UID_VALUE_ANALYTICS, ENROLLMENT_UID_VALUE_ANALYTICS, rule) + rule: LazyActivityScenarioRule +) { + startTeiDashboardActivity( + TB_PROGRAM_UID, + TEI_UID_VALUE_ANALYTICS, + ENROLLMENT_UID_VALUE_ANALYTICS, + rule + ) } fun startTeiDashboardActivity( programUID: String?, teiUID: String, enrollmentUID: String?, - rule: ActivityTestRule + rule: LazyActivityScenarioRule ) { - Intent().apply { + Intent( + ApplicationProvider.getApplicationContext(), + TeiDashboardMobileActivity::class.java, + ).apply { putExtra(PROGRAM_UID, programUID) putExtra(TEI_UID, teiUID) - putExtra(ENROLLMENT_UID,enrollmentUID) - }.also { rule.launchActivity(it) } + putExtra(ENROLLMENT_UID, enrollmentUID) + }.also { rule.launch(it) } } -fun prepareChildProgrammeIntentAndLaunchActivity(ruleSearch: ActivityTestRule) { - Intent().apply { +fun prepareChildProgrammeIntentAndLaunchActivity( + ruleSearch: LazyActivityScenarioRule +) { + Intent( + ApplicationProvider.getApplicationContext(), + SearchTEActivity::class.java, + ).apply { putExtra(SearchTETest.PROGRAM_UID, SearchTETest.CHILD_PROGRAM_UID_VALUE) putExtra(SearchTETest.CHILD_TE_TYPE, SearchTETest.CHILD_TE_TYPE_VALUE) - }.also { ruleSearch.launchActivity(it) } + }.also { ruleSearch.launch(it) } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt new file mode 100644 index 0000000000..5a221799fb --- /dev/null +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt @@ -0,0 +1,197 @@ +package org.dhis2.usescases.teidashboard + +import android.view.View +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.rules.activityScenarioRule +import dhis2.org.analytics.charts.Charts +import io.reactivex.Observable +import org.dhis2.R +import org.dhis2.android.rtsm.utils.NetworkUtils +import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.prefs.PreferenceProvider +import org.dhis2.commons.resources.MetadataIconProvider +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.ui.MetadataIconData +import org.dhis2.ui.ThemeManager +import org.dhis2.usescases.teiDashboard.DashboardRepositoryImpl +import org.dhis2.usescases.teiDashboard.DashboardViewModel +import org.dhis2.usescases.teiDashboard.TeiAttributesProvider +import org.dhis2.usescases.teiDashboard.TeiDashboardContracts +import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity +import org.dhis2.utils.analytics.AnalyticsHelper +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance +import org.hisp.dhis.android.core.trackedentity.TrackedEntityType +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.util.Calendar + +class TeiDashboardMobileActivityTest { + + @get:Rule + val activityScenarioRule = activityScenarioRule() + + @get:Rule + val composeRule = createAndroidComposeRule() + + + private lateinit var viewModel: DashboardViewModel + + private val d2: D2 = Mockito.mock(D2::class.java, Mockito.RETURNS_DEEP_STUBS) + private val resources: ResourceManager = mock() + private val charts: Charts = mock() + private val teiAttributesProvider: TeiAttributesProvider = mock() + private val preferences: PreferenceProvider = mock() + + private val metadataIconProvider: MetadataIconProvider = mock { + on { invoke(any()) }doReturn MetadataIconData.defaultIcon() + } + + private var repository: DashboardRepositoryImpl = mock { + + } + + private var dispatcher: DispatcherProvider = mock() + var tei = Observable.just( + TrackedEntityInstance.builder() + .uid(TEI_Uid) + .created(Calendar.getInstance().time) + .lastUpdated(Calendar.getInstance().time) + .organisationUnit(ORG_UNIT_UID) + .trackedEntityType(TETYPE_NAME) + .build() + ) + + private val teType: TrackedEntityType = mock() + + private val analyticsHelper = mock { + } + + private val themeManager: ThemeManager = mock() + private val presenter: TeiDashboardContracts.Presenter = mock() + private val filterManager: FilterManager = mock() + private val networkUtils: NetworkUtils = mock() + + companion object { + const val ENROLLMENT_UID = "enrollmentUid" + const val TEI_Uid = "TEIUid" + const val PROGRAM_UID = "programUid" + const val TETYPE_NAME = "TETypeName" + const val INITIAL_ORG_UNIT_UID = "initialOrgUnitUid" + const val PROGRAM_STAGE_NAME = "Marvellous Program Stage" + const val EXECUTION_DATE = "Date of Marvellous Program Stage" + const val ORG_UNIT_UID = "orgUnitUid" + const val ENROLLMENT_VALUE_WITH_NOTE = "EnrollmentValueWithNote" + const val TEI_UID_VALUE_WITH_NOTE = "TeiUidValueWithNote" + const val CHILD_PROGRAM_UID_VALUE = "childProgramUid" + } + + private fun initViewModel() { + viewModel = DashboardViewModel( + repository, + analyticsHelper, + dispatcher, + ) + + } + + private fun setUp() { + initRepository() + initViewModel() + } + + private fun initRepository() { + repository = DashboardRepositoryImpl( + d2, + charts, + TEI_Uid, + PROGRAM_UID, + ENROLLMENT_UID, + teiAttributesProvider, + preferences, + metadataIconProvider + ) + + + } + + + @Test + fun shouldSuccessfullyInitializeTeiDashBoardMobileActivity() { + setUp() + whenever(repository.getTETypeName()) doReturn TETYPE_NAME + whenever(repository.getTrackedEntityInstance("")) doReturn mock() + whenever { + repository.getTrackedEntityInstance("").flatMap { tei: TrackedEntityInstance -> + d2.trackedEntityModule().trackedEntityTypes() + .uid(tei.trackedEntityType()) + .get() + .toObservable() + } + } doReturn mock() + whenever { + repository.getTrackedEntityInstance("").flatMap { tei: TrackedEntityInstance -> + d2.trackedEntityModule().trackedEntityTypes() + .uid(tei.trackedEntityType()) + .get() + .toObservable() + }.blockingFirst() + } doReturn { teType } + whenever( + presenter.teType + ) doReturn TETYPE_NAME + + whenever( + repository.getTETypeName() + ) doReturn TETYPE_NAME + whenever( + d2.trackedEntityModule() + ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances() + ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances().byUid() + ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances().byUid().eq("") + ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances().byUid().eq("").one() + ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances().byUid().eq("").one() + .blockingGet() + ) doReturn mock() + + whenever( + d2.trackedEntityModule().trackedEntityTypes() + ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityTypes().uid("") + ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityTypes().uid("").get() + ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityTypes().uid("").get().toObservable() + ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityTypes().uid("").get().toObservable() + ) doReturn mock() + + + ActivityScenario.launch(TeiDashboardMobileActivity::class.java).onActivity { activity -> + + val showMoreOptions = activity.findViewById(R.id.moreOptions) + showMoreOptions.performClick() + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt index 7e6ab907dd..4557f174bf 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt @@ -2,9 +2,9 @@ package org.dhis2.usescases.teidashboard import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.ActivityTestRule import dhis2.org.analytics.charts.data.ChartType import org.dhis2.R +import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest import org.dhis2.usescases.searchTrackEntity.SearchTEActivity import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity @@ -16,6 +16,7 @@ import org.dhis2.usescases.teidashboard.robot.eventRobot import org.dhis2.usescases.teidashboard.robot.indicatorsRobot import org.dhis2.usescases.teidashboard.robot.noteRobot import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -24,20 +25,21 @@ import org.junit.runner.RunWith class TeiDashboardTest : BaseTest() { @get:Rule - val rule = ActivityTestRule(TeiDashboardMobileActivity::class.java, false, false) + val rule = lazyActivityScenarioRule(launchActivity = false) @get:Rule - val ruleSearch = ActivityTestRule(SearchTEActivity::class.java, false, false) + val ruleSearch = lazyActivityScenarioRule(launchActivity = false) @get:Rule val composeTestRule = createComposeRule() + @Test fun shouldSuccessfullyCreateANoteWhenClickCreateNote() { setupCredentials() prepareTeiCompletedProgrammeAndLaunchActivity(rule) - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { goToNotes() } @@ -53,7 +55,7 @@ class TeiDashboardTest : BaseTest() { fun shouldNotCreateANoteWhenClickClear() { prepareTeiCompletedProgrammeAndLaunchActivity(rule) - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { goToNotes() } @@ -70,7 +72,7 @@ class TeiDashboardTest : BaseTest() { fun shouldOpenNotesDetailsWhenClickOnNote() { prepareTeiWithExistingNoteAndLaunchActivity(rule) - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { goToNotes() } @@ -84,7 +86,7 @@ class TeiDashboardTest : BaseTest() { fun shouldReactivateTEIWhenClickReOpenWithProgramCompletedEvents() { prepareTeiCompletedProgrammeAndLaunchActivity(rule) - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnTimelineEvents() clickOnMenuMoreOptions() @@ -97,14 +99,14 @@ class TeiDashboardTest : BaseTest() { fun shouldShowInactiveProgramWhenClickDeactivate() { prepareTeiOpenedProgrammeAndLaunchActivity(rule) - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnTimelineEvents() clickOnMenuMoreOptions() clickOnMenuDeactivate() - checkCancelledStateInfoBarIsDisplay(composeTestRule) + checkCancelledStateInfoBarIsDisplay() checkCanNotAddEvent() - checkAllEventsAreInactive(1) + checkAllEventsAreClosed() } } @@ -112,14 +114,14 @@ class TeiDashboardTest : BaseTest() { fun shouldCompleteProgramWhenClickComplete() { prepareTeiOpenedForCompleteProgrammeAndLaunchActivity(rule) - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnTimelineEvents() clickOnMenuMoreOptions() clickOnMenuComplete() - checkCompleteStateInfoBarIsDisplay(composeTestRule) + checkCompleteStateInfoBarIsDisplay() checkCanNotAddEvent() - checkAllEventsAreClosed(1) + checkAllEventsAreClosed() } } @@ -127,7 +129,7 @@ class TeiDashboardTest : BaseTest() { fun shouldShowQRWhenClickOnShare() { prepareTeiCompletedProgrammeAndLaunchActivity(rule) - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnShareButton() clickOnNextQR() @@ -138,26 +140,24 @@ class TeiDashboardTest : BaseTest() { fun shouldMakeAReferral() { prepareTeiOpenedForReferralProgrammeAndLaunchActivity(rule) - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnTimelineEvents() clickOnFab() clickOnReferral() clickOnFirstReferralEvent() - clickOnReferralOption( - composeTestRule, - context.getString(R.string.one_time) - ) + clickOnReferralOption(context.getString(R.string.one_time)) clickOnReferralNextButton() checkEventWasCreated(LAB_MONITORING) } } + @Ignore("To fix in ANDROAPP-6109") @Test fun shouldSuccessfullyScheduleAnEvent() { prepareTeiOpenedWithNoPreviousEventProgrammeAndLaunchActivity(rule) - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnTimelineEvents() clickOnFab() @@ -168,44 +168,34 @@ class TeiDashboardTest : BaseTest() { } } - @Test - fun shouldNotBeAbleToCreateNewEventsWhenFull() { - prepareTeiOpenedWithFullEventsAndLaunchActivity(rule) - - teiDashboardRobot { - clickOnMenuMoreOptions() - clickOnTimelineEvents() - checkCanNotAddEvent() - } - } - @Test fun shouldOpenEventAndSaveSuccessfully() { setupCredentials() prepareTeiOpenedProgrammeAndLaunchActivity(rule) - val babyPostNatal = 0 - teiDashboardRobot { + val babyPostNatal = "Baby Postnatal" + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnTimelineEvents() - clickOnEventWithPosition(babyPostNatal) + clickOnEventWithTitle(babyPostNatal) } - eventRobot { + eventRobot(composeTestRule) { scrollToBottomForm() clickOnFormFabButton() - clickOnNotNow(composeTestRule) + clickOnNotNow() } } + @Ignore("This is checking xml instead of compose. Update mobile library with test tags.") @Test fun shouldShowCorrectInfoWhenOpenTEI() { prepareTeiCompletedProgrammeAndLaunchActivity(rule) val upperInformation = createExpectedUpperInformation() - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { checkUpperInfo(upperInformation) } } @@ -216,7 +206,7 @@ class TeiDashboardTest : BaseTest() { val enrollmentFullDetails = createExpectedEnrollmentInformation() - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnSeeDetails() checkFullDetails(enrollmentFullDetails) } @@ -226,64 +216,37 @@ class TeiDashboardTest : BaseTest() { fun shouldShowIndicatorsDetailsWhenClickOnIndicatorsTab() { prepareTeiCompletedProgrammeAndLaunchActivity(rule) - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { goToAnalytics() } - indicatorsRobot { + indicatorsRobot(composeTestRule) { checkDetails("0", "4817") } } - @Test - fun shouldSuccessfullyCreateANewEvent() { - prepareTeiToCreateANewEventAndLaunchActivity(rule) - - teiDashboardRobot { - clickOnMenuMoreOptions() - clickOnTimelineEvents() - clickOnFab() - clickOnCreateNewEvent() - clickOnFirstReferralEvent() - waitToDebounce(2000) - clickOnReferralNextButton() - waitToDebounce(600) - } - - eventRobot { - fillRadioButtonForm(4) - clickOnFormFabButton() - clickOnNotNow(composeTestRule) - } - - teiDashboardRobot { - checkEventWasCreatedAndOpen(LAB_MONITORING, 0) - } - } - + @Ignore @Test fun shouldOpenEventEditAndSaveSuccessfully() { prepareTeiOpenedToEditAndLaunchActivity(rule) - val labMonitoring = 2 - - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnTimelineEvents() - clickOnEventWithPosition(labMonitoring) + clickOnEventWith(LAB_MONITORING) waitToDebounce(600) } - eventRobot { + eventRobot(composeTestRule) { waitToDebounce(600) fillRadioButtonForm(4) clickOnFormFabButton() - clickOnCompleteButton(composeTestRule) + clickOnCompleteButton() waitToDebounce(600) } - teiDashboardRobot { - checkEventWasCreatedAndClosed(LAB_MONITORING, 2) + teiDashboardRobot(composeTestRule) { + checkEventWasCreatedAndClosed(LAB_MONITORING) } } @@ -300,7 +263,7 @@ class TeiDashboardTest : BaseTest() { setDatePicker() prepareTeiToEnrollToOtherProgramAndLaunchActivity(rule) - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnTimelineEvents() clickOnMenuMoreOptions() @@ -309,16 +272,16 @@ class TeiDashboardTest : BaseTest() { enrollmentRobot { clickOnAProgramForEnrollment(composeTestRule, womanProgram) - clickOnAcceptEnrollmentDate() + clickOnAcceptInDatePicker() clickOnPersonAttributes(personAttribute) waitToDebounce(5000) clickOnCalendarItem() - clickOnAcceptEnrollmentDate() + clickOnAcceptInDatePicker() scrollToBottomProgramForm() clickOnSaveEnrollment() } - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { waitToDebounce(1000) clickOnMenuMoreOptions() clickOnTimelineEvents() @@ -336,11 +299,11 @@ class TeiDashboardTest : BaseTest() { setupCredentials() prepareTeiForAnalyticsAndLaunchActivity(rule) - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { goToAnalytics() } - indicatorsRobot { + indicatorsRobot(composeTestRule) { checkGraphIsRendered(chartName) } @@ -376,9 +339,5 @@ class TeiDashboardTest : BaseTest() { const val LAB_MONITORING = "Lab monitoring" const val LAB_MONITORING_SCHEDULE_DATE = "10/9/2019" - - const val API_TEI_1_RESPONSE_OK = "mocks/teilist/teilist_1.json" - const val API_TEI_2_RESPONSE_OK = "mocks/teilist/teilist_2.json" - const val API_TEI_3_RESPONSE_OK = "mocks/teilist/teilist_3.json" } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTestNoComposable.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTestNoComposable.kt index eb75346c45..eb436cbe01 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTestNoComposable.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTestNoComposable.kt @@ -1,7 +1,8 @@ package org.dhis2.usescases.teidashboard +import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.ActivityTestRule +import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest import org.dhis2.usescases.searchTrackEntity.SearchTEActivity import org.dhis2.usescases.searchte.robot.searchTeiRobot @@ -16,7 +17,10 @@ import org.junit.runner.RunWith class TeiDashboardTestNoComposable : BaseTest() { @get:Rule - val ruleSearch = ActivityTestRule(SearchTEActivity::class.java, false, false) + val ruleSearch = lazyActivityScenarioRule(launchActivity = false) + + @get:Rule + val composeTestRule = createComposeRule() @Ignore @Test @@ -30,11 +34,11 @@ class TeiDashboardTestNoComposable : BaseTest() { setupCredentials() prepareChildProgrammeIntentAndLaunchActivity(ruleSearch) - searchTeiRobot { + searchTeiRobot(composeTestRule) { clickOnTEI(teiName, teiLastName) } - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { goToRelationships() } @@ -45,7 +49,7 @@ class TeiDashboardTestNoComposable : BaseTest() { waitToDebounce(500) } - searchTeiRobot { + searchTeiRobot(composeTestRule) { clickOnOpenSearch() typeAttributeAtPosition(relationshipName, 0) typeAttributeAtPosition(relationshipLastName, 1) @@ -69,7 +73,7 @@ class TeiDashboardTestNoComposable : BaseTest() { setupCredentials() prepareChildProgrammeIntentAndLaunchActivity(ruleSearch) - searchTeiRobot { + searchTeiRobot(composeTestRule) { clickOnOpenSearch() typeAttributeAtPosition(teiName, firstNamePosition) typeAttributeAtPosition(teiLastName, lastNamePosition) @@ -78,12 +82,12 @@ class TeiDashboardTestNoComposable : BaseTest() { //scrollToTEIandClick() } - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnMenuDeleteTEI() } - searchTeiRobot { + searchTeiRobot(composeTestRule) { checkTEIsDelete(teiName, teiLastName) } } @@ -98,7 +102,7 @@ class TeiDashboardTestNoComposable : BaseTest() { setupCredentials() prepareChildProgrammeIntentAndLaunchActivity(ruleSearch) - searchTeiRobot { + searchTeiRobot(composeTestRule) { clickOnOpenSearch() typeAttributeAtPosition(teiName, firstNamePosition) typeAttributeAtPosition(teiLastName, lastNamePosition) @@ -107,12 +111,12 @@ class TeiDashboardTestNoComposable : BaseTest() { clickOnTEI(teiName, teiLastName) } - teiDashboardRobot { + teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnMenuDeleteEnrollment() } - searchTeiRobot { + searchTeiRobot(composeTestRule) { checkTEIsDelete(teiName, teiLastName) } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/dialogs/scheduling/SchedulingDialogUiTest.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/dialogs/scheduling/SchedulingDialogUiTest.kt new file mode 100644 index 0000000000..4aa70eb56a --- /dev/null +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/dialogs/scheduling/SchedulingDialogUiTest.kt @@ -0,0 +1,134 @@ +package org.dhis2.usescases.teidashboard.dialogs.scheduling + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import kotlinx.coroutines.flow.MutableStateFlow +import org.dhis2.composetable.test.TestActivity +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCategory +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate +import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialogUi +import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingViewModel +import org.hisp.dhis.android.core.category.CategoryOption +import org.hisp.dhis.android.core.program.ProgramStage +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class SchedulingDialogUiTest { + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + private val viewModel: SchedulingViewModel = mock() + + @Before + fun setUp() { + whenever(viewModel.eventDate).thenReturn(MutableStateFlow(EventDate(label = "Date"))) + whenever(viewModel.eventCatCombo).thenReturn( + MutableStateFlow( + EventCatCombo( + categories = listOf( + EventCategory( + uid = "uid", + name = "CatCombo", + optionsSize = 2, + options = listOf( + CategoryOption.builder().uid("uidA").displayName("optionA").build(), + CategoryOption.builder().uid("uidB").displayName("optionB").build(), + ), + ), + ), + ), + ), + ) + } + + @Test + fun programStageInputNotDisplayedForOneStage() { + val programStages = listOf(ProgramStage.builder().uid("stageUid").displayName("PS A").build()) + whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first())) + composeTestRule.setContent { + SchedulingDialogUi( + programStages = programStages, + viewModel = viewModel, + orgUnitUid = "orgUnitUid", + ) { + } + } + composeTestRule.onNodeWithText("Schedule next " + programStages.first().displayName() + "?").assertExists() + composeTestRule.onNodeWithText("Program stage").assertDoesNotExist() + composeTestRule.onNodeWithText("Date").assertExists() + composeTestRule.onNodeWithText("CatCombo *").assertExists() + composeTestRule.onNodeWithText("Schedule").assertExists() + } + + @Test + fun programStageInputDisplayedForMoreThanOneStages() { + val programStages = listOf( + ProgramStage.builder().uid("stageUidA").displayName("PS A").build(), + ProgramStage.builder().uid("stageUidB").displayName("PS B").build(), + ) + whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first())) + composeTestRule.setContent { + SchedulingDialogUi( + programStages = programStages, + viewModel = viewModel, + orgUnitUid = "orgUnitUid", + ) { + } + } + composeTestRule.onNodeWithText("Schedule next event?").assertExists() + composeTestRule.onNodeWithText("Program stage").assertExists() + } + + @Test + fun inputFieldsShouldNotBeDisplayedWhenAnsweringNo() { + val programStages = listOf( + ProgramStage.builder().uid("stageUidA").displayName("PS A").build(), + ProgramStage.builder().uid("stageUidB").displayName("PS B").build(), + ) + whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first())) + composeTestRule.setContent { + SchedulingDialogUi( + programStages = programStages, + viewModel = viewModel, + orgUnitUid = "orgUnitUid", + ) { + } + } + composeTestRule.onNodeWithText("No").performClick() + + composeTestRule.onNodeWithText("Program stage").assertDoesNotExist() + composeTestRule.onNodeWithText("Date").assertDoesNotExist() + composeTestRule.onNodeWithText("CatCombo *").assertDoesNotExist() + composeTestRule.onNodeWithText("Done").assertExists() + } + + @Test + fun selectProgramStage() { + val programStages = listOf( + ProgramStage.builder().uid("stageUidA").displayName("PS A").build(), + ProgramStage.builder().uid("stageUidB").displayName("PS B").build(), + ) + whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first())) + composeTestRule.setContent { + SchedulingDialogUi( + programStages = programStages, + viewModel = viewModel, + orgUnitUid = "orgUnitUid", + ) { + } + } + + composeTestRule.onNodeWithText("Program stage").performClick() + composeTestRule.onNodeWithTag("INPUT_DROPDOWN_MENU_ITEM_1").performClick() + + verify(viewModel).updateStage(programStages[1]) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EnrollmentRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EnrollmentRobot.kt index d773566b76..e55593ce66 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EnrollmentRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EnrollmentRobot.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.test.performClick import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition import androidx.test.espresso.matcher.ViewMatchers.hasDescendant @@ -16,10 +17,9 @@ import org.dhis2.common.BaseRobot import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.atPosition import org.dhis2.common.viewactions.clickChildViewWithId import org.dhis2.common.viewactions.scrollToBottomRecyclerView -import org.dhis2.common.viewactions.typeChildViewWithId import org.dhis2.form.ui.FormViewHolder -import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.DashboardProgramViewHolder import org.dhis2.usescases.flow.teiFlow.entity.EnrollmentListUIModel +import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.DashboardProgramViewHolder import org.dhis2.usescases.teiDashboard.teiProgramList.ui.PROGRAM_TO_ENROLL import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString @@ -37,7 +37,7 @@ class EnrollmentRobot : BaseRobot() { .performClick() } - fun clickOnAcceptEnrollmentDate() { + fun clickOnAcceptInDatePicker() { waitForView(withId(R.id.acceptBtn)).perform(click()) } @@ -46,23 +46,12 @@ class EnrollmentRobot : BaseRobot() { } fun clickOnPersonAttributes(attribute: String) { - onView(withId(R.id.recyclerView)) - .perform(actionOnItem( - hasDescendant(withText(containsString(attribute))), clickChildViewWithId(R.id.section_details))) - } - - fun clickOnPersonAttributesUsingButton(attribute: String){ - onView(withId(R.id.recyclerView)) - .perform(actionOnItem( - hasDescendant(withText(containsString(attribute))), clickChildViewWithId(R.id.sectionButton) - )) - } - - fun typeOnRequiredTextField(text: String, position: Int) { onView(withId(R.id.recyclerView)) .perform( - actionOnItemAtPosition( - position, typeChildViewWithId(text, R.id.input_editText)) + actionOnItem( + hasDescendant(withText(containsString(attribute))), + clickChildViewWithId(R.id.section_details) + ) ) } @@ -72,22 +61,59 @@ class EnrollmentRobot : BaseRobot() { fun clickOnCalendarItem() { onView(withId(R.id.recyclerView)) - .perform(actionOnItem( - hasDescendant(withText(containsString(DATE_OF_BIRTH))), clickChildViewWithId(R.id.inputEditText))) + .perform( + actionOnItem( + hasDescendant(withText(containsString(DATE_OF_BIRTH))), + clickChildViewWithId(R.id.inputEditText) + ) + ) } fun checkActiveAndPastEnrollmentDetails(enrollmentListUIModel: EnrollmentListUIModel) { - checkHeaderAndProgramDetails(enrollmentListUIModel, ACTIVE_PROGRAMS, 1, 2, enrollmentListUIModel.currentEnrollmentDate) - checkHeaderAndProgramDetails(enrollmentListUIModel, PAST_PROGRAMS, 3, 4, enrollmentListUIModel.pastEnrollmentDate) + checkHeaderAndProgramDetails( + enrollmentListUIModel, + ACTIVE_PROGRAMS, + 1, + 2, + enrollmentListUIModel.currentEnrollmentDate + ) + checkHeaderAndProgramDetails( + enrollmentListUIModel, + PAST_PROGRAMS, + 3, + 4, + enrollmentListUIModel.pastEnrollmentDate + ) } - private fun checkHeaderAndProgramDetails(enrollmentListUIModel: EnrollmentListUIModel, programStatus: String, headerPosition: Int, programPosition: Int, enrollmentDay: String) { - onView(withId(R.id.recycler)).check(matches(atPosition(headerPosition, withText(programStatus)))) - onView(withId(R.id.recycler)).check(matches(allOf(atPosition(programPosition, allOf( - hasDescendant(withText(enrollmentListUIModel.program)), - hasDescendant(withText(enrollmentListUIModel.orgUnit)), - hasDescendant(withText(enrollmentDay)) - ))))) + private fun checkHeaderAndProgramDetails( + enrollmentListUIModel: EnrollmentListUIModel, + programStatus: String, + headerPosition: Int, + programPosition: Int, + enrollmentDay: String + ) { + onView(withId(R.id.recycler)).check( + matches( + atPosition( + headerPosition, + withText(programStatus) + ) + ) + ) + onView(withId(R.id.recycler)).check( + matches( + allOf( + atPosition( + programPosition, allOf( + hasDescendant(withText(enrollmentListUIModel.program)), + hasDescendant(withText(enrollmentListUIModel.orgUnit)), + hasDescendant(withText(enrollmentDay)) + ) + ) + ) + ) + ) } fun clickOnEnrolledProgram(position: Int) { @@ -99,18 +125,26 @@ class EnrollmentRobot : BaseRobot() { fun clickOnInputDate(label: String) { onView(withId(R.id.recyclerView)) - .perform(actionOnItem( - hasDescendant(withText(label)), clickChildViewWithId(R.id.inputEditText))) + .perform( + actionOnItem( + hasDescendant(withText(label)), clickChildViewWithId(R.id.inputEditText) + ) + ) } - fun clickOnDatePicker() { - waitForView(withId(R.id.date_picker)).perform(click()) + fun selectSpecificDate(year: Int, monthOfYear: Int, dayOfMonth: Int) { + onView(withId(R.id.datePicker)).perform( + PickerActions.setDate( + year, + monthOfYear, + dayOfMonth + ) + ) } companion object { const val ACTIVE_PROGRAMS = "Active programs" const val PAST_PROGRAMS = "Past programs" - const val ENROLL = "ENROLL" const val DATE_OF_BIRTH = "Date of birth" } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt index 625f39c53a..e3413e3983 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt @@ -1,21 +1,22 @@ package org.dhis2.usescases.teidashboard.robot import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnySibling +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withSubstring import androidx.test.espresso.matcher.ViewMatchers.withText import org.dhis2.R import org.dhis2.common.BaseRobot -import org.dhis2.common.matchers.clickOnTab import org.dhis2.common.matchers.hasCompletedPercentage import org.dhis2.common.viewactions.clickChildViewWithId import org.dhis2.common.viewactions.scrollToBottomRecyclerView @@ -24,37 +25,40 @@ import org.dhis2.form.ui.FormViewHolder import org.dhis2.ui.dialogs.bottomsheet.MAIN_BUTTON_TAG import org.dhis2.ui.dialogs.bottomsheet.SECONDARY_BUTTON_TAG import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.DashboardProgramViewHolder -import org.hamcrest.CoreMatchers.allOf -fun eventRobot(eventRobot: EventRobot.() -> Unit) { - EventRobot().apply { +fun eventRobot( + composeTestRule: ComposeTestRule, + eventRobot: EventRobot.() -> Unit +) { + EventRobot(composeTestRule).apply { eventRobot() } } -class EventRobot : BaseRobot() { +class EventRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun scrollToBottomForm() { onView(withId(R.id.recyclerView)).perform(scrollToBottomRecyclerView()) } fun clickOnFormFabButton() { - onView(withId(R.id.actionButton)).perform(click()) + waitForView(withId(R.id.actionButton)).perform(click()) } - fun clickOnNotNow(composeTestRule: ComposeTestRule) { + + fun clickOnNotNow() { composeTestRule.onNodeWithTag(SECONDARY_BUTTON_TAG).performClick() } - fun clickOnCompleteButton(composeTestRule: ComposeTestRule) { + fun clickOnCompleteButton() { composeTestRule.onNodeWithTag(MAIN_BUTTON_TAG).performClick() } - fun checkSecondaryButtonNotVisible(composeTestRule: ComposeTestRule) { + fun checkSecondaryButtonNotVisible() { composeTestRule.onNodeWithTag(SECONDARY_BUTTON_TAG).assertDoesNotExist() } fun clickOnReopen() { - onView(withId(R.id.reopenButton)).perform(click()) + composeTestRule.onNodeWithTag("REOPEN_BUTTON").performClick() } fun fillRadioButtonForm(numberFields: Int) { @@ -72,20 +76,8 @@ class EventRobot : BaseRobot() { } } - fun clickOnChangeDate() { - onView(withText(R.string.change_event_date)).perform(click()) - } - - fun clickOnEditDate() { - onView(withId(R.id.date)).perform(click()) - } - fun acceptUpdateEventDate() { - onView(withId(R.id.acceptBtn)).perform(click()) - } - - fun clickOnUpdate() { - onView(withId(R.id.action_button)).perform(click()) + composeTestRule.onNodeWithText("OK", true).performClick() } fun typeOnRequiredEventForm(text: String, position: Int) { @@ -97,34 +89,10 @@ class EventRobot : BaseRobot() { ) } - fun clickOnFutureAlertDialog(){ - clickOnChangeDate() - clickOnEditDate() - acceptUpdateEventDate() - clickOnUpdate() - } - - fun checkDetails(eventDate: String, eventOrgUnit: String) { - onView(withId(R.id.eventSecundaryInfo)).check(matches( - allOf( - withSubstring(eventDate), - withSubstring(eventOrgUnit) - ) - )) - } - - fun clickOnNotesTab() { - onView(clickOnTab(1)).perform(click()) - } - fun openMenuMoreOptions() { onView(withId(R.id.moreOptions)).perform(click()) } - fun clickOnDetails() { - onView(withId(R.id.navigation_details)).perform(click()) - } - fun clickOnDelete() { onView(withText(R.string.delete)).perform(click()) } @@ -133,22 +101,42 @@ class EventRobot : BaseRobot() { onView(withId(R.id.possitive)).perform(click()) } - fun clickOnEventDueDate(composeTestRule: ComposeTestRule) { - composeTestRule.onNodeWithTag("INPUT_DATE_TIME_TEXT").assertIsDisplayed() - composeTestRule.onNodeWithTag("INPUT_DATE_TIME_TEXT").performClick() + fun clickOnEventReportDate() { + composeTestRule.onNode( + hasTestTag("INPUT_DATE_TIME_ACTION_BUTTON") and hasAnySibling( + hasText("Report date") + ) + ).assertIsDisplayed().performClick() + + } + fun selectSpecificDate(date: String) { + composeTestRule.onNodeWithTag("DATE_PICKER").assertIsDisplayed() + composeTestRule.onNode(hasText(date, true)).performClick() } - fun selectSpecificDate(year: Int, monthOfYear: Int, dayOfMonth: Int) { - onView(withId(R.id.datePicker)).perform(PickerActions.setDate(year, monthOfYear, dayOfMonth)) + fun typeOnDateParameter(dateValue: String) { + composeTestRule.apply { + onNodeWithTag("INPUT_DATE_TIME_TEXT_FIELD").performClick() + onNodeWithTag("INPUT_DATE_TIME_TEXT_FIELD").performTextInput(dateValue) + } } - fun checkEventDetails(eventDate: String, eventOrgUnit: String, composeTestRule: ComposeTestRule) { + fun checkEventDetails(eventDate: String, eventOrgUnit: String) { onView(withId(R.id.completion)).check(matches(hasCompletedPercentage(100))) - composeTestRule.onNodeWithText(formatStoredDateToUI(eventDate)).assertIsDisplayed() + val formattedDate = formatStoredDateToUI(eventDate) + composeTestRule.onNodeWithText(formattedDate).assertIsDisplayed() composeTestRule.onNodeWithText(eventOrgUnit).assertIsDisplayed() } + fun openEventDetailsSection() { + composeTestRule.onNodeWithText("Event details").performClick() + } + + fun checkEventIsOpen() { + composeTestRule.onNodeWithTag("REOPEN_BUTTON").assertDoesNotExist() + } + private fun formatStoredDateToUI(dateValue: String): String { val components = dateValue.split("/") diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/IndicatorsRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/IndicatorsRobot.kt index 62f4c50948..5a6af744fe 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/IndicatorsRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/IndicatorsRobot.kt @@ -1,73 +1,41 @@ package org.dhis2.usescases.teidashboard.robot +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithText import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.hasDescendant -import androidx.test.espresso.matcher.ViewMatchers.hasSibling -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import org.dhis2.R import org.dhis2.common.BaseRobot import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.atPosition -import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.isNotEmpty -import org.hamcrest.CoreMatchers.allOf -fun indicatorsRobot(indicatorsRobot: IndicatorsRobot.() -> Unit) { - IndicatorsRobot().apply { +fun indicatorsRobot( + composeTestRule: ComposeTestRule, + indicatorsRobot: IndicatorsRobot.() -> Unit +) { + IndicatorsRobot(composeTestRule).apply { indicatorsRobot() } } -class IndicatorsRobot : BaseRobot() { +class IndicatorsRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun checkDetails(yellowFeverIndicator: String, weightIndicator: String) { - onView(withId(R.id.indicators_recycler)).check( - matches( - allOf( - isDisplayed(), isNotEmpty(), - atPosition( - 2, - hasDescendant( - allOf( - withText(yellowFeverIndicator), - hasSibling( - allOf( - withId(R.id.indicator_name), - withText("Measles + Yellow fever doses") - ) - ) - ) - ) - ) - ) - ) - ) + composeTestRule.onNodeWithText(yellowFeverIndicator).assertIsDisplayed() + composeTestRule.onNodeWithText(weightIndicator).assertIsDisplayed() + } + fun checkGraphIsRendered(chartName: String) { onView(withId(R.id.indicators_recycler)).check( matches( - allOf( - isDisplayed(), isNotEmpty(), - atPosition( - 1, - hasDescendant( - allOf( - withText(weightIndicator), - hasSibling( - allOf( - withId(R.id.indicator_name), - withText("Average weight (g)") - ) - ) - ) - ) - ) + atPosition( + 1, + hasDescendant(withText(chartName)) ) ) ) } - - fun checkGraphIsRendered(chartName:String){ - onView(withId(R.id.indicators_recycler)).check(matches(atPosition(1, hasDescendant(withText(chartName))))) - } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt index 4b9540e94e..dc50165dbe 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt @@ -3,7 +3,10 @@ package org.dhis2.usescases.teidashboard.robot import android.content.Context import android.view.View import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -17,7 +20,6 @@ import androidx.test.espresso.matcher.BoundedMatcher import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.isEnabled import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withTagValue import androidx.test.espresso.matcher.ViewMatchers.withText @@ -27,18 +29,16 @@ import org.dhis2.common.BaseRobot import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.atPosition import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.hasItem import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.isNotEmpty -import org.dhis2.common.matchers.isToast import org.dhis2.common.viewactions.clickChildViewWithId import org.dhis2.usescases.event.entity.EventStatusUIModel import org.dhis2.usescases.event.entity.TEIProgramStagesUIModel import org.dhis2.usescases.programStageSelection.ProgramStageSelectionViewHolder import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.DashboardProgramViewHolder import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.EventViewHolder -import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.StageViewHolder import org.dhis2.usescases.teiDashboard.ui.STATE_INFO_BAR_TEST_TAG +import org.dhis2.usescases.teiDashboard.ui.TEST_ADD_EVENT_BUTTON import org.dhis2.usescases.teidashboard.entity.EnrollmentUIModel import org.dhis2.usescases.teidashboard.entity.UpperEnrollmentUIModel -import org.dhis2.utils.dialFloatingActionButton.FAB_ID import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.equalTo @@ -46,23 +46,22 @@ import org.hamcrest.CoreMatchers.not import org.hamcrest.Description import org.hamcrest.Matcher -fun teiDashboardRobot(teiDashboardRobot: TeiDashboardRobot.() -> Unit) { - TeiDashboardRobot().apply { +fun teiDashboardRobot( + composeTestRule: ComposeTestRule, + teiDashboardRobot: TeiDashboardRobot.() -> Unit +) { + TeiDashboardRobot(composeTestRule).apply { teiDashboardRobot() } } -class TeiDashboardRobot : BaseRobot() { +class TeiDashboardRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun goToNotes() { onView(withId(R.id.navigation_notes)).perform(click()) Thread.sleep(500) } - fun clickOnSync() { - onView(withId(R.id.syncButton)).perform(click()) - } - fun goToRelationships() { onView(withId(R.id.navigation_relationships)).perform(click()) Thread.sleep(500) @@ -81,73 +80,28 @@ class TeiDashboardRobot : BaseRobot() { onView(withText(R.string.re_open)).perform(click()) } - fun checkCancelledStateInfoBarIsDisplay(composeTestRule: ComposeTestRule) { + fun checkCancelledStateInfoBarIsDisplay() { composeTestRule.onNodeWithTag(STATE_INFO_BAR_TEST_TAG).assertIsDisplayed() composeTestRule.onNodeWithText("Enrollment cancelled").assertIsDisplayed() } - fun checkCanAddEvent() { - onView(withId(FAB_ID)).check(matches(allOf(isDisplayed(), isEnabled()))).perform(click()) - val targetContext: Context = InstrumentationRegistry.getInstrumentation().targetContext - val addNewTag = targetContext.resources.getString(R.string.add_new) - onView(withTagValue(equalTo(addNewTag))).check(matches(isDisplayed())) - } - - fun clickOnEventWithPosition(position: Int) { - onView(withId(R.id.tei_recycler)) - .perform(actionOnItemAtPosition(position, click())) - } - - fun clickOnEventWith(eventDate: String, orgUnit: String) { - onView(withId(R.id.tei_recycler)) - .perform( - actionOnItem( - allOf( - hasDescendant(withText(eventDate)), hasDescendant( - withText(orgUnit) - ) - ), click() - ) - ) - } - - fun clickOnEventWith(eventName: String, eventStatus: Int, date: String) { - onView(withId(R.id.tei_recycler)) - .perform( - actionOnItem( - allOf( - hasDescendant(withText(eventName)), - hasDescendant(withText(eventStatus)), - hasDescendant(withText(date)) - ), - click() - ) - ) + fun clickOnEventWithTitle(title: String) { + composeTestRule.onNodeWithText(title).performClick() } - fun clickOnGroupEventByName(name: String) { - onView(withId(R.id.tei_recycler)) - .perform( - actionOnItem( - hasDescendant(withText(name)), - click() - ) - ) + fun clickOnEventWith(searchParam: String) { + composeTestRule.onAllNodesWithText(searchParam, useUnmergedTree = true).onFirst() + .performClick() } fun clickOnFab() { - onView(withId(FAB_ID)).perform(click()) + composeTestRule.onNodeWithTag(TEST_ADD_EVENT_BUTTON, useUnmergedTree = true).performClick() } fun clickOnReferral() { val targetContext: Context = InstrumentationRegistry.getInstrumentation().targetContext val referalTag = targetContext.resources.getString(R.string.referral) - onView(withTagValue(equalTo(referalTag))).perform(click()) - } - - fun checkCannotAddMoreEventToastIsShown() { - onView(withText(R.string.program_not_allow_events)).inRoot(isToast()) - .check(matches(isDisplayed())) + composeTestRule.onNodeWithTag(referalTag).performClick() } fun clickOnFirstReferralEvent() { @@ -156,7 +110,7 @@ class TeiDashboardRobot : BaseRobot() { .perform(actionOnItemAtPosition(0, click())) } - fun clickOnReferralOption(composeTestRule: ComposeTestRule, oneTime: String) { + fun clickOnReferralOption(oneTime: String) { composeTestRule.onNodeWithText(oneTime).performClick() } @@ -224,14 +178,15 @@ class TeiDashboardRobot : BaseRobot() { onView(withText(R.string.complete)).perform(click()) } - fun checkCompleteStateInfoBarIsDisplay(composeTestRule: ComposeTestRule) { + fun checkCompleteStateInfoBarIsDisplay() { composeTestRule.onNodeWithTag(STATE_INFO_BAR_TEST_TAG).assertIsDisplayed() composeTestRule.onNodeWithText("Enrollment completed").assertIsDisplayed() } fun checkCanNotAddEvent() { - onView(withId(FAB_ID)).check(matches(not(isDisplayed()))) + composeTestRule.onNodeWithTag(TEST_ADD_EVENT_BUTTON, useUnmergedTree = true) + .assertDoesNotExist() } fun clickOnShareButton() { @@ -252,10 +207,6 @@ class TeiDashboardRobot : BaseRobot() { } fun checkUpperInfo(upperInformation: UpperEnrollmentUIModel) { - onView(withId(R.id.incident_date)) - .check(matches(withText(upperInformation.incidentDate))) - onView(withId(R.id.enrollment_date)) - .check(matches(withText(upperInformation.enrollmentDate))) onView(withId(R.id.org_unit)) .check(matches(withText(upperInformation.orgUnit))) } @@ -265,15 +216,75 @@ class TeiDashboardRobot : BaseRobot() { } fun checkFullDetails(enrollmentUIModel: EnrollmentUIModel) { - onView(withId(R.id.recyclerView)).check(matches(not(recyclerChildViews(hasItem(hasDescendant(withText(enrollmentUIModel.enrollmentDate))))))) + onView(withId(R.id.recyclerView)).check( + matches( + not( + recyclerChildViews( + hasItem( + hasDescendant( + withText(enrollmentUIModel.enrollmentDate) + ) + ) + ) + ) + ) + ) - onView(withId(R.id.recyclerView)).check(matches(not(recyclerChildViews(hasItem(hasDescendant(withText(enrollmentUIModel.birthday))))))) + onView(withId(R.id.recyclerView)).check( + matches( + not( + recyclerChildViews( + hasItem( + hasDescendant( + withText(enrollmentUIModel.birthday) + ) + ) + ) + ) + ) + ) - onView(withId(R.id.recyclerView)).check(matches(not(recyclerChildViews(hasItem(hasDescendant(withText(enrollmentUIModel.orgUnit))))))) + onView(withId(R.id.recyclerView)).check( + matches( + not( + recyclerChildViews( + hasItem( + hasDescendant( + withText(enrollmentUIModel.orgUnit) + ) + ) + ) + ) + ) + ) - onView(withId(R.id.recyclerView)).check(matches(not(recyclerChildViews(hasItem(hasDescendant(withText(enrollmentUIModel.latitude))))))) + onView(withId(R.id.recyclerView)).check( + matches( + not( + recyclerChildViews( + hasItem( + hasDescendant( + withText(enrollmentUIModel.latitude) + ) + ) + ) + ) + ) + ) - onView(withId(R.id.recyclerView)).check(matches(not(recyclerChildViews(hasItem(hasDescendant(withText(enrollmentUIModel.longitude))))))) + onView(withId(R.id.recyclerView)).check( + matches( + not( + recyclerChildViews( + hasItem( + hasDescendant( + withText(enrollmentUIModel.longitude) + ) + ) + ) + ) + ) + ) onView(withId(R.id.recyclerView)) @@ -286,11 +297,47 @@ class TeiDashboardRobot : BaseRobot() { waitToDebounce(2000) - onView(withId(R.id.recyclerView)).check(matches(not(recyclerChildViews(hasItem(hasDescendant(withText(enrollmentUIModel.name))))))) + onView(withId(R.id.recyclerView)).check( + matches( + not( + recyclerChildViews( + hasItem( + hasDescendant( + withText(enrollmentUIModel.name) + ) + ) + ) + ) + ) + ) - onView(withId(R.id.recyclerView)).check(matches(not(recyclerChildViews(hasItem(hasDescendant(withText(enrollmentUIModel.lastName))))))) + onView(withId(R.id.recyclerView)).check( + matches( + not( + recyclerChildViews( + hasItem( + hasDescendant( + withText(enrollmentUIModel.lastName) + ) + ) + ) + ) + ) + ) - onView(withId(R.id.recyclerView)).check(matches(not(recyclerChildViews(hasItem(hasDescendant(withText(enrollmentUIModel.sex))))))) + onView(withId(R.id.recyclerView)).check( + matches( + not( + recyclerChildViews( + hasItem( + hasDescendant( + withText(enrollmentUIModel.sex) + ) + ) + ) + ) + ) + ) } @@ -306,81 +353,45 @@ class TeiDashboardRobot : BaseRobot() { val adapter: RecyclerView.Adapter = recyclerView.adapter as RecyclerView.Adapter for (position in 0..( - hasDescendant(withText(programStageName)), - click() - ) - ) + composeTestRule.onNodeWithText(programStageName).performClick() } fun clickOnEventGroupByStage(eventDate: String) { @@ -614,27 +532,7 @@ class TeiDashboardRobot : BaseRobot() { ) } - fun clickOnEventGroupByStageUsingOU(orgUnit: String) { - onView(withId(R.id.tei_recycler)) - .perform( - actionOnItem( - hasDescendant( - allOf( - withText(orgUnit), - withId(R.id.organisationUnit) - ) - ), click() - ) - ) - } - - fun checkProgramStageIsHidden(stageName: String) { - onView(withId(R.id.tei_recycler)) - .check(matches(not(hasItem(hasDescendant(withText(stageName)))))) - } - - companion object { - const val OPEN_EVENT_STATUS = R.string.event_open - const val OVERDUE_EVENT_STATUS = R.string.event_overdue + fun clickOnEventGroupByStageUsingDate(dueDate: String) { + composeTestRule.onNodeWithText(dueDate).performClick() } } diff --git a/app/src/dhis/java/org/dhis2/bindings/ContextExtensions.kt b/app/src/dhis/java/org/dhis2/bindings/ContextExtensions.kt index 177f8e5df1..b9d36fd878 100644 --- a/app/src/dhis/java/org/dhis2/bindings/ContextExtensions.kt +++ b/app/src/dhis/java/org/dhis2/bindings/ContextExtensions.kt @@ -11,7 +11,7 @@ fun Context.buildInfo(): String { return if (BuildConfig.BUILD_TYPE == "release") { "v${BuildConfig.VERSION_NAME}" } else { - "v${BuildConfig.VERSION_NAME} : ${BuildConfig.BUILD_DATE} : ${BuildConfig.GIT_SHA} " + "v${BuildConfig.VERSION_NAME} : ${BuildConfig.GIT_SHA} " } } diff --git a/app/src/dhis/java/org/dhis2/utils/granularsync/GranularSyncModule.kt b/app/src/dhis/java/org/dhis2/utils/granularsync/GranularSyncModule.kt index cd515b6703..9a42713a10 100644 --- a/app/src/dhis/java/org/dhis2/utils/granularsync/GranularSyncModule.kt +++ b/app/src/dhis/java/org/dhis2/utils/granularsync/GranularSyncModule.kt @@ -31,11 +31,11 @@ import dagger.Provides import kotlinx.coroutines.Dispatchers import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.resources.ColorUtils +import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.sync.SyncContext import org.dhis2.commons.viewmodel.DispatcherProvider -import org.dhis2.data.dhislogic.DhisPeriodUtils import org.dhis2.data.dhislogic.DhisProgramUtils import org.dhis2.data.service.workManager.WorkManagerController import org.hisp.dhis.android.core.D2 diff --git a/app/src/dhisPlayServices/java/org/dhis2/bindings/ContextExtensions.kt b/app/src/dhisPlayServices/java/org/dhis2/bindings/ContextExtensions.kt index 1699dcb3e7..caa840d62e 100644 --- a/app/src/dhisPlayServices/java/org/dhis2/bindings/ContextExtensions.kt +++ b/app/src/dhisPlayServices/java/org/dhis2/bindings/ContextExtensions.kt @@ -10,7 +10,7 @@ fun Context.buildInfo(): String { return if (BuildConfig.BUILD_TYPE == "release") { "v${BuildConfig.VERSION_NAME}" } else { - "v${BuildConfig.VERSION_NAME} : ${BuildConfig.BUILD_DATE} : ${BuildConfig.GIT_SHA} " + "v${BuildConfig.VERSION_NAME} : ${BuildConfig.GIT_SHA} " } } diff --git a/app/src/dhisPlayServices/java/org/dhis2/utils/granularsync/GranularSyncModule.kt b/app/src/dhisPlayServices/java/org/dhis2/utils/granularsync/GranularSyncModule.kt index 6ed0cf8816..44ec6a4b02 100644 --- a/app/src/dhisPlayServices/java/org/dhis2/utils/granularsync/GranularSyncModule.kt +++ b/app/src/dhisPlayServices/java/org/dhis2/utils/granularsync/GranularSyncModule.kt @@ -31,11 +31,11 @@ import dagger.Provides import kotlinx.coroutines.Dispatchers import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.resources.ColorUtils +import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.sync.SyncContext import org.dhis2.commons.viewmodel.DispatcherProvider -import org.dhis2.data.dhislogic.DhisPeriodUtils import org.dhis2.data.dhislogic.DhisProgramUtils import org.dhis2.data.service.workManager.WorkManagerController import org.hisp.dhis.android.core.D2 diff --git a/app/src/dhisUITesting/assets/databases/dhis_test.db b/app/src/dhisUITesting/assets/databases/dhis_test.db index 8523d26aa7e2dd7a4e6265239298a1d071802d15..a14b48ebcccdb4cff6cfeeddfffd3e0eba0b1547 100644 GIT binary patch delta 770 zcmW;K%TE(g6bA4!ZKs`?-kE8k53s0|kv>GM3R-b|;S00^R>X>mqGH?-cW!i3YBJr} zM4d%R0ecfoT$mU`gWb6BA8@6KD>c!C7;QAsgt+u8i{H)pR^QD%H~)6tnTJ=xL%$-E zD6NvTsx7NCETsIbN4fAp@)SCS02$r(k0%j3TWzzas?0X3pIk)r)mAH?J%xlAA|q-0 zDg(AzX}B{k#2&kM!K_U#sDeA=T+g$MW^I14nH}r-#gbY3xWt44x42+&hsB*1cUfGt zxZC0$i%S-lEv~%gUSV2ieiD^mq+DJ76d5 zf* z&cOtnudj(Y?_Hh!CXe!fbVIwNF01akFQ5Cun%^s29vsfL2Imr%BZx(sCQ&ca)3mL-MWynrxyOW(o#KMUQF5LMMSY4 zpWjc}(u3b>Atd-oBxrx0ZFUW3a`AZ5_TT$a;EW55i;PQ*%Zw|GyBJp)cQdXru0P`* zVce>3^99?Onh~YGvX=@}EHpSlO2d*MG&$XrzEQ(g-^8UW*NUb=E2d$5{}#iXpHzu8 zYCbYP8UfnxnbUviL)xG_qdr#ME}t?bFH3jCS7M9skW?Mq_@Z1jZ^+_Ihcm61K~&#V z%xUF+aZNFcHN^ri_@Dt+fFBwm08OwGf)IjcXn|G;LmRAuc32G&SOaSz3LUTx*24za z2%BItbV3Zaz*g7>+o22MumcjX6OzykDM-UE*bP0f2YO*I?1Mho4+o$h2H+qZfqs;V6v2F&Ks8kb$vEP04z$|9hCK-y&ATylOn9Un|~x_C1wndYI_+4vaK9ZKZ6% wiM<|kJD-A7SCXBHL|1XCl!X&;5>CNsI0I+l9OPgeCg40=fQyx-(q!)LU#egOAOHXW diff --git a/app/src/dhisUITesting/java/org/dhis2/bindings/ContextExtensions.kt b/app/src/dhisUITesting/java/org/dhis2/bindings/ContextExtensions.kt index 9c26109728..982b706a05 100644 --- a/app/src/dhisUITesting/java/org/dhis2/bindings/ContextExtensions.kt +++ b/app/src/dhisUITesting/java/org/dhis2/bindings/ContextExtensions.kt @@ -11,7 +11,7 @@ fun Context.buildInfo(): String { return if (BuildConfig.BUILD_TYPE == "release") { "v${BuildConfig.VERSION_NAME}" } else { - "v${BuildConfig.VERSION_NAME} : ${BuildConfig.BUILD_DATE} : ${BuildConfig.GIT_SHA} " + "v${BuildConfig.VERSION_NAME} : ${BuildConfig.GIT_SHA} " } } diff --git a/app/src/dhisUITesting/java/org/dhis2/utils/granularsync/GranularSyncModule.kt b/app/src/dhisUITesting/java/org/dhis2/utils/granularsync/GranularSyncModule.kt index cd515b6703..9a42713a10 100644 --- a/app/src/dhisUITesting/java/org/dhis2/utils/granularsync/GranularSyncModule.kt +++ b/app/src/dhisUITesting/java/org/dhis2/utils/granularsync/GranularSyncModule.kt @@ -31,11 +31,11 @@ import dagger.Provides import kotlinx.coroutines.Dispatchers import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.resources.ColorUtils +import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.sync.SyncContext import org.dhis2.commons.viewmodel.DispatcherProvider -import org.dhis2.data.dhislogic.DhisPeriodUtils import org.dhis2.data.dhislogic.DhisProgramUtils import org.dhis2.data.service.workManager.WorkManagerController import org.hisp.dhis.android.core.D2 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8412df191c..aa604adfc6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -78,7 +78,9 @@ + android:configChanges="keyboardHidden|screenSize" + android:windowSoftInputMode="stateAlwaysHidden" + /> @@ -87,7 +89,7 @@ + android:windowSoftInputMode="stateAlwaysHidden" /> @@ -114,7 +116,7 @@ + android:windowSoftInputMode="adjustResize|stateAlwaysHidden" /> + android:windowSoftInputMode="adjustResize|stateAlwaysHidden" /> { options.setDsn(BuildConfig.SENTRY_DSN); + options.setAnrReportInDebug(true); // Add a callback that will be used before the event is sent to Sentry. // With this callback, you can modify the event or, when returning null, also discard the event. diff --git a/app/src/main/java/org/dhis2/AppComponent.java b/app/src/main/java/org/dhis2/AppComponent.java index 8d8028d6e1..4e59e8e475 100644 --- a/app/src/main/java/org/dhis2/AppComponent.java +++ b/app/src/main/java/org/dhis2/AppComponent.java @@ -48,7 +48,6 @@ ValidatorModule.class, CrashReportModule.class, LocationModule.class, - FilterModule.class, DispatcherModule.class, FeatureConfigModule.class, NetworkUtilsModule.class, diff --git a/app/src/main/java/org/dhis2/AppModule.kt b/app/src/main/java/org/dhis2/AppModule.kt index 0888eead1e..049b720c6f 100644 --- a/app/src/main/java/org/dhis2/AppModule.kt +++ b/app/src/main/java/org/dhis2/AppModule.kt @@ -4,7 +4,6 @@ import android.content.Context import dagger.Module import dagger.Provides import org.dhis2.commons.resources.ColorUtils -import org.dhis2.commons.resources.ResourceManager import javax.inject.Singleton @Module @@ -15,12 +14,6 @@ class AppModule(private val application: App) { return application } - @Provides - @Singleton - fun resources(): ResourceManager { - return ResourceManager(application, colorUtils()) - } - @Provides @Singleton fun colorUtils(): ColorUtils { diff --git a/app/src/main/java/org/dhis2/bindings/Bindings.java b/app/src/main/java/org/dhis2/bindings/Bindings.java index c500345e52..f61f6b15e4 100644 --- a/app/src/main/java/org/dhis2/bindings/Bindings.java +++ b/app/src/main/java/org/dhis2/bindings/Bindings.java @@ -1,41 +1,28 @@ package org.dhis2.bindings; -import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.Typeface; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.os.Build; import android.util.TypedValue; import android.view.View; -import android.widget.AdapterView; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.appcompat.content.res.AppCompatResources; -import androidx.appcompat.widget.AppCompatSpinner; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.databinding.BindingAdapter; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.google.android.material.floatingactionbutton.FloatingActionButton; - import org.dhis2.R; import org.dhis2.commons.animations.ViewAnimationsKt; -import org.dhis2.commons.filters.CatOptionComboFilter; -import org.dhis2.commons.data.ProgramEventViewModel; -import org.dhis2.utils.CatComboAdapter; import org.dhis2.utils.DateUtils; import org.dhis2.utils.NetworkUtils; -import org.hisp.dhis.android.core.category.CategoryOptionCombo; -import org.hisp.dhis.android.core.common.State; import org.hisp.dhis.android.core.enrollment.Enrollment; import org.hisp.dhis.android.core.enrollment.EnrollmentStatus; import org.hisp.dhis.android.core.event.Event; @@ -44,9 +31,7 @@ import org.hisp.dhis.android.core.program.Program; import org.hisp.dhis.android.core.program.ProgramStage; -import java.util.ArrayList; import java.util.Date; -import java.util.List; public class Bindings { @@ -55,10 +40,8 @@ public class Bindings { public static void setDrawableEnd(TextView textView, Drawable drawable) { textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - if (drawable instanceof AnimatedVectorDrawable) - ((AnimatedVectorDrawable) drawable).start(); - } + if (drawable instanceof AnimatedVectorDrawable) + ((AnimatedVectorDrawable) drawable).start(); } @BindingAdapter(value = {"initGrid", "spanCount"}, requireAll = false) @@ -73,19 +56,6 @@ public static void setLayoutManager(RecyclerView recyclerView, boolean horizonta } - @BindingAdapter("spanSize") - public static void setSpanSize(RecyclerView recyclerView, boolean setSpanSize) { - if (recyclerView.getLayoutManager() instanceof GridLayoutManager) { - ((GridLayoutManager) recyclerView.getLayoutManager()).setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { - @Override - public int getSpanSize(int position) { - int itemViewType = recyclerView.getAdapter().getItemViewType(position); - return (itemViewType == 4 || itemViewType == 8) ? 2 : 1; - } - }); - } - } - @BindingAdapter("enrolmentIcon") public static void setEnrolmentIcon(ImageView view, EnrollmentStatus status) { Drawable lock; @@ -133,27 +103,6 @@ public static void setEnrolmentText(TextView view, EnrollmentStatus status) { view.setText(text); } - public static String enrollmentText(Context context, EnrollmentStatus status) { - String text; - if (status == null) - status = EnrollmentStatus.ACTIVE; - switch (status) { - case ACTIVE: - text = context.getString(R.string.event_open); - break; - case COMPLETED: - text = context.getString(R.string.completed); - break; - case CANCELLED: - text = context.getString(R.string.cancelled); - break; - default: - text = context.getString(R.string.read_only); - break; - } - return text; - } - @BindingAdapter(value = {"eventStatusIcon", "enrollmentStatusIcon", "eventProgramStage", "eventProgram"}, requireAll = false) public static void setEventIcon(ImageView view, Event event, Enrollment enrollment, ProgramStage eventProgramStage, Program program) { if (event != null) { @@ -204,220 +153,16 @@ public static void setEventIcon(ImageView view, Event event, Enrollment enrollme } } - @BindingAdapter(value = {"eventStatusText", "enrollmentStatus", "eventProgramStage", "eventProgram"}) - public static void setEventText(TextView view, Event event, Enrollment enrollment, ProgramStage eventProgramStage, Program program) { - if (event != null) { - EventStatus status = event.status(); - EnrollmentStatus enrollmentStatus = enrollment.status(); - if (status == null) - status = EventStatus.ACTIVE; - if (enrollmentStatus == null) - enrollmentStatus = EnrollmentStatus.ACTIVE; - - - if (enrollmentStatus == EnrollmentStatus.ACTIVE) { - switch (status) { - case ACTIVE: - Date eventDate = event.eventDate(); - if (eventProgramStage.periodType() != null && eventProgramStage.periodType().name().contains(PeriodType.Weekly.name())) - eventDate = DateUtils.getInstance().getNextPeriod(eventProgramStage.periodType(), eventDate, 0, true); - if (DateUtils.getInstance().isEventExpired(eventDate, null, event.status(), program.completeEventsExpiryDays(), eventProgramStage.periodType() != null ? eventProgramStage.periodType() : program.expiryPeriodType(), program.expiryDays())) { - view.setText(view.getContext().getString(R.string.event_expired)); - } else { - view.setText(view.getContext().getString(R.string.event_open)); - } - break; - case COMPLETED: - if (DateUtils.getInstance().isEventExpired(null, event.completedDate(), program.completeEventsExpiryDays())) { - view.setText(view.getContext().getString(R.string.event_expired)); - } else { - view.setText(view.getContext().getString(R.string.event_completed)); - } - break; - case SCHEDULE: - if (DateUtils.getInstance().isEventExpired( - event.dueDate(), - null, - status, - program.completeEventsExpiryDays(), - eventProgramStage.periodType() != null ? eventProgramStage.periodType() : program.expiryPeriodType(), - program.expiryDays())) { - view.setText(view.getContext().getString(R.string.event_expired)); - } else { - view.setText(view.getContext().getString(R.string.event_schedule)); - } - break; - case SKIPPED: - view.setText(view.getContext().getString(R.string.event_skipped)); - break; - case OVERDUE: - view.setText(R.string.event_overdue); - break; - default: - view.setText(view.getContext().getString(R.string.read_only)); - break; - } - } else if (enrollmentStatus == EnrollmentStatus.COMPLETED) { - view.setText(view.getContext().getString(R.string.program_completed)); - } else { //EnrollmentStatus = CANCELLED - view.setText(view.getContext().getString(R.string.program_inactive)); - } - } - } - - @BindingAdapter(value = {"eventColor", "eventProgramStage", "eventProgram"}) - public static void setEventColor(View view, Event event, ProgramStage programStage, Program program) { - if (event != null) { - int bgColor; - if (DateUtils.getInstance().isEventExpired(null, event.completedDate(), program.completeEventsExpiryDays())) { - bgColor = R.drawable.item_event_dark_gray_ripple; - } else if (event.status() != null) { - switch (event.status()) { - case ACTIVE: - Date eventDate = event.eventDate(); - if (programStage.periodType() != null && programStage.periodType().name().contains(PeriodType.Weekly.name())) - eventDate = DateUtils.getInstance().getNextPeriod(programStage.periodType(), eventDate, 0, true); - if (DateUtils.getInstance().isEventExpired(eventDate, null, event.status(), program.completeEventsExpiryDays(), programStage.periodType() != null ? programStage.periodType() : program.expiryPeriodType(), program.expiryDays())) { - bgColor = R.drawable.item_event_dark_gray_ripple; - } else - bgColor = R.drawable.item_event_yellow_ripple; - break; - case COMPLETED: - if (DateUtils.getInstance().isEventExpired(null, event.completedDate(), program.completeEventsExpiryDays())) { - bgColor = R.drawable.item_event_dark_gray_ripple; - } else - bgColor = R.drawable.item_event_gray_ripple; - break; - case SCHEDULE: - if (DateUtils.getInstance().isEventExpired( - event.dueDate(), - null, - event.status(), - program.completeEventsExpiryDays(), - programStage.periodType() != null ? programStage.periodType() : program.expiryPeriodType(), - program.expiryDays())) { - bgColor = R.drawable.item_event_dark_gray_ripple; - } else - bgColor = R.drawable.item_event_green_ripple; - break; - case VISITED: - case SKIPPED: - default: - bgColor = R.drawable.item_event_red_ripple; - break; - } - } else { - bgColor = R.drawable.item_event_red_ripple; - } - view.setBackground(AppCompatResources.getDrawable(view.getContext(), bgColor)); - } - } - - @BindingAdapter("eventWithoutRegistrationStatusText") - public static void setEventWithoutRegistrationStatusText(TextView textView, ProgramEventViewModel event) { - switch (event.eventStatus()) { - case ACTIVE: - if (event.isExpired()) { - textView.setText(textView.getContext().getString(R.string.event_editing_expired)); - } else { - textView.setText(textView.getContext().getString(R.string.event_open)); - } - break; - case COMPLETED: - if (event.isExpired()) { - textView.setText(textView.getContext().getString(R.string.event_editing_expired)); - } else { - textView.setText(textView.getContext().getString(R.string.event_completed)); - } - break; - case SKIPPED: - textView.setText(textView.getContext().getString(R.string.event_editing_expired)); - break; - default: - textView.setText(textView.getContext().getString(R.string.read_only)); - break; - } - } - - - - @BindingAdapter("stateText") - public static void setStateText(TextView textView, State state) { - switch (state) { - case TO_POST: - textView.setText(textView.getContext().getString(R.string.state_to_post)); - break; - case TO_UPDATE: - textView.setText(textView.getContext().getString(R.string.state_to_update)); - break; - case ERROR: - textView.setText(textView.getContext().getString(R.string.state_error)); - break; - case SYNCED: - textView.setText(textView.getContext().getString(R.string.state_synced)); - break; - default: - break; - } - } - - @BindingAdapter("fromResBgColor") - public static void setFromResBgColor(View view, int color) { - String tintedColor; - - ArrayList rgb = new ArrayList<>(); - rgb.add(Color.red(color) / 255.0d); - rgb.add(Color.green(color) / 255.0d); - rgb.add(Color.blue(color) / 255.0d); - - Double r = null; - Double g = null; - Double b = null; - for (Double c : rgb) { - if (c <= 0.03928d) - c = c / 12.92d; - else - c = Math.pow(((c + 0.055d) / 1.055d), 2.4d); - - if (r == null) - r = c; - else if (g == null) - g = c; - else - b = c; - } - - double L = 0.2126d * r + 0.7152d * g + 0.0722d * b; - - - if (L > 0.179d) - tintedColor = "#000000"; // bright colors - black font - else - tintedColor = "#FFFFFF"; // dark colors - white font - - if (view instanceof TextView) { - ((TextView) view).setTextColor(Color.parseColor(tintedColor)); - } - if (view instanceof ImageView) { - Drawable drawable = ((ImageView) view).getDrawable(); - if (drawable != null) - drawable.setColorFilter(Color.parseColor(tintedColor), PorterDuff.Mode.SRC_IN); - ((ImageView) view).setImageDrawable(drawable); - } - } @BindingAdapter("imageBackground") public static void setImageBackground(ImageView imageView, Drawable drawable) { TypedValue typedValue = new TypedValue(); TypedArray a = imageView.getContext().obtainStyledAttributes(typedValue.data, new int[]{R.attr.colorPrimaryDark}); - TypedArray b = imageView.getContext().obtainStyledAttributes(typedValue.data, new int[]{R.attr.colorPrimaryLight}); int colorPrimaryDark = a.getColor(0, 0); - int colorPrimaryLight = b.getColor(0, 0); int px = (int) (1 * Resources.getSystem().getDisplayMetrics().density); ((GradientDrawable) drawable.mutate()).setStroke(px, colorPrimaryDark); - //((GradientDrawable) drawable.mutate()).setColor(colorPrimaryLight); imageView.setBackground(drawable); @@ -454,36 +199,6 @@ public static void setNetworkVisibility(View view, boolean checkNetwork) { } } - @BindingAdapter(value = {"catComboAdapterData", "catComboAdapterTitle"}) - public static void setCatComboAdapter(AppCompatSpinner spinner, List catComboAdapterData, String catComboAdapterTitle) { - CatComboAdapter spinnerAdapter = new CatComboAdapter(spinner.getContext(), - R.layout.spinner_layout, - R.id.spinner_text, - catComboAdapterData != null ? catComboAdapterData : new ArrayList<>(), - catComboAdapterTitle, - R.color.white_faf); - - spinner.setAdapter(spinnerAdapter); - } - - @BindingAdapter("onCatComboSelected") - public static void setOnCatComboSelected(AppCompatSpinner spinner, CatOptionComboFilter itemFilter) { - spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int position, long l) { - if (position != 0) { - itemFilter.selectCatOptionCombo(position - 1); - spinner.setSelection(0); - } - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - - } - }); - } - @BindingAdapter(value = {"dataSetStatus"}) public static void setDataSetStatusIcon(ImageView view, Boolean isComplete) { int drawableResource = isComplete ? R.drawable.ic_event_status_complete : R.drawable.ic_event_status_open; @@ -496,38 +211,11 @@ public static void setDataSetStatusIcon(ImageView view, Boolean isComplete) { view.setTag(drawableResource); } - @BindingAdapter("textStyle") - public static void setTextStyle(TextView textView, int style) { - switch (style) { - case Typeface.BOLD: - textView.setTypeface(null, Typeface.BOLD); - break; - default: - textView.setTypeface(null, Typeface.NORMAL); - break; - - } - } - @BindingAdapter("clipCorners") public static void setClipCorners(View view, int cornerRadiusInDp) { ViewExtensionsKt.clipWithRoundedCorners(view, ExtensionsKt.getDp(cornerRadiusInDp)); } - @BindingAdapter("clipAllCorners") - public static void setAllClipCorners(View view, int cornerRadiusInDp) { - ViewExtensionsKt.clipWithAllRoundedCorners(view, ExtensionsKt.getDp(cornerRadiusInDp)); - } - - @BindingAdapter("fabVisibility") - public static void setFabVisibility(FloatingActionButton fab, boolean isVisible) { - if (isVisible) { - fab.show(); - } else { - fab.hide(); - } - } - @BindingAdapter("viewVisibility") public static void setViewVisibility(View view, boolean isVisible) { if (isVisible) { diff --git a/app/src/main/java/org/dhis2/bindings/ValidationStrategyExtensions.kt b/app/src/main/java/org/dhis2/bindings/ValidationStrategyExtensions.kt deleted file mode 100644 index 63df251fb5..0000000000 --- a/app/src/main/java/org/dhis2/bindings/ValidationStrategyExtensions.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.dhis2.bindings - -import org.hisp.dhis.android.core.common.ValidationStrategy - -fun ValidationStrategy.canSkipErrorFix(hasErrorFields: Boolean, hasEmptyMandatoryFields: Boolean) = - when (this) { - ValidationStrategy.ON_COMPLETE -> true - ValidationStrategy.ON_UPDATE_AND_INSERT -> !hasErrorFields && !hasEmptyMandatoryFields - } diff --git a/app/src/main/java/org/dhis2/bindings/ViewExtensions.kt b/app/src/main/java/org/dhis2/bindings/ViewExtensions.kt index 0ccb1d7069..90318177fb 100644 --- a/app/src/main/java/org/dhis2/bindings/ViewExtensions.kt +++ b/app/src/main/java/org/dhis2/bindings/ViewExtensions.kt @@ -1,8 +1,6 @@ package org.dhis2.bindings import android.graphics.Outline -import android.os.Build -import android.util.TypedValue import android.view.View import android.view.ViewOutlineProvider import android.view.inputmethod.EditorInfo @@ -11,15 +9,7 @@ import android.widget.ListPopupWindow import android.widget.Spinner import android.widget.TextView import com.google.android.material.floatingactionbutton.FloatingActionButton -import com.tbuonomo.viewpagerdotsindicator.R import org.dhis2.commons.extensions.closeKeyboard -import java.lang.Exception - -fun View.getThemePrimaryColor(): Int { - val value = TypedValue() - context.theme.resolveAttribute(R.attr.colorPrimary, value, true) - return value.data -} fun View.onFocusRemoved(onFocusRemovedCallback: () -> Unit) { setOnFocusChangeListener { view, hasFocus -> @@ -42,37 +32,33 @@ fun TextView.clearFocusOnDone() { } fun View.clipWithRoundedCorners(curvedRadio: Int = 16.dp) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - outlineProvider = object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - outline.setRoundRect( - 0, - 0, - view.width, - view.height + curvedRadio, - curvedRadio.toFloat(), - ) - } + outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect( + 0, + 0, + view.width, + view.height + curvedRadio, + curvedRadio.toFloat(), + ) } - clipToOutline = true } + clipToOutline = true } -fun View.clipWithAllRoundedCorners(curvedRadio: Int = 16.dp) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - outlineProvider = object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - outline.setRoundRect( - 0, - 0, - view.width, - view.height, - curvedRadio.toFloat(), - ) - } +fun View.clipWithTopRightRoundedCorner(curvedRadio: Int = 16.dp) { + outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect( + 0 - curvedRadio, + 0, + view.width, + view.height + curvedRadio, + curvedRadio.toFloat(), + ) } - clipToOutline = true } + clipToOutline = true } fun Spinner.overrideHeight(desiredHeight: Int) { diff --git a/app/src/main/java/org/dhis2/data/dhislogic/DhisEventUtils.kt b/app/src/main/java/org/dhis2/data/dhislogic/DhisEventUtils.kt deleted file mode 100644 index 27aac9e132..0000000000 --- a/app/src/main/java/org/dhis2/data/dhislogic/DhisEventUtils.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.dhis2.data.dhislogic - -import org.hisp.dhis.android.core.D2 -import org.hisp.dhis.android.core.common.FeatureType -import javax.inject.Inject - -class DhisEventUtils @Inject constructor(val d2: D2) { - fun newEventNeedsExtraInfo(eventUid: String): Boolean { - val event = d2.eventModule().events().uid(eventUid) - .blockingGet() - val stage = d2.programModule().programStages().uid(event?.programStage()) - .blockingGet() - val program = d2.programModule().programs().uid(stage?.program()?.uid()) - .blockingGet() - val hasCoordinates = stage?.featureType() != null && stage.featureType() != FeatureType.NONE - val hasNonDefaultCatCombo = d2.categoryModule().categoryCombos() - .uid(program?.categoryComboUid()).blockingGet()?.isDefault != true - return hasCoordinates || hasNonDefaultCatCombo - } -} diff --git a/app/src/main/java/org/dhis2/data/forms/EnrollmentFormRepository.java b/app/src/main/java/org/dhis2/data/forms/EnrollmentFormRepository.java deleted file mode 100644 index fd392115e5..0000000000 --- a/app/src/main/java/org/dhis2/data/forms/EnrollmentFormRepository.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.dhis2.data.forms; - -import androidx.annotation.NonNull; - -import org.dhis2.form.data.RulesRepository; -import org.hisp.dhis.android.core.D2; -import org.hisp.dhis.android.core.enrollment.Enrollment; -import org.hisp.dhis.rules.RuleEngine; -import org.hisp.dhis.rules.RuleEngineContext; -import org.hisp.dhis.rules.models.TriggerEnvironment; - -import io.reactivex.Flowable; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; - -@SuppressWarnings({ - "PMD.AvoidDuplicateLiterals" -}) -public class EnrollmentFormRepository implements FormRepository { - - @NonNull - private Flowable cachedRuleEngineFlowable; - - @NonNull - private final String enrollmentUid; - private final String enrollmentOrgUnitUid; - private final D2 d2; - private final RulesRepository rulesRepository; - - public EnrollmentFormRepository(@NonNull RulesRepository rulesRepository, - @NonNull String enrollmentUid, - @NonNull D2 d2) { - this.d2 = d2; - this.enrollmentUid = enrollmentUid; - this.rulesRepository = rulesRepository; - if (!enrollmentUid.isEmpty()) { - enrollmentOrgUnitUid = d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet().organisationUnit(); - } else { - enrollmentOrgUnitUid = ""; - } - // We don't want to rebuild RuleEngine on each request, since metadata of - // the event is not changing throughout lifecycle of FormComponent. - this.cachedRuleEngineFlowable = enrollmentProgram() - .switchMap(program -> Single.zip( - rulesRepository.rulesNew(program, null).subscribeOn(Schedulers.io()), - rulesRepository.ruleVariables(program).subscribeOn(Schedulers.io()), - rulesRepository.enrollmentEvents(enrollmentUid).subscribeOn(Schedulers.io()), - rulesRepository.queryConstants().subscribeOn(Schedulers.io()), - rulesRepository.supplementaryData(enrollmentOrgUnitUid).subscribeOn(Schedulers.io()), - (rules, variables, events, constants, supplementaryData) -> { - RuleEngine.Builder builder = RuleEngineContext.builder() - .rules(rules) - .ruleVariables(variables) - .supplementaryData(supplementaryData) - .constantsValue(constants) - .build().toEngineBuilder(); - builder.triggerEnvironment(TriggerEnvironment.ANDROIDCLIENT); - builder.events(events); - return builder.build(); - }).toFlowable()) - .cacheWithInitialCapacity(1); - } - - @Override - public Flowable restartRuleEngine() { - String orgUnit = d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet().organisationUnit(); - return this.cachedRuleEngineFlowable = enrollmentProgram() - .switchMap(program -> Single.zip( - rulesRepository.rulesNew(program, null), - rulesRepository.ruleVariables(program), - rulesRepository.enrollmentEvents(enrollmentUid), - rulesRepository.queryConstants(), - rulesRepository.supplementaryData(orgUnit), - (rules, variables, events, constants, supplementaryData) -> { - RuleEngine.Builder builder = RuleEngineContext.builder() - .rules(rules) - .ruleVariables(variables) - .supplementaryData(supplementaryData) - .constantsValue(constants) - .build().toEngineBuilder(); - builder.triggerEnvironment(TriggerEnvironment.ANDROIDCLIENT); - builder.events(events); - return builder.build(); - }).toFlowable()) - .cacheWithInitialCapacity(1); - } - - @NonNull - @Override - public Flowable ruleEngine() { - return cachedRuleEngineFlowable; - } - - @NonNull - private Flowable enrollmentProgram() { - return d2.enrollmentModule().enrollments().uid(enrollmentUid).get() - .map(Enrollment::program) - .toFlowable(); - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/data/forms/EnrollmentFormRepository.kt b/app/src/main/java/org/dhis2/data/forms/EnrollmentFormRepository.kt new file mode 100644 index 0000000000..a97016ff3e --- /dev/null +++ b/app/src/main/java/org/dhis2/data/forms/EnrollmentFormRepository.kt @@ -0,0 +1,92 @@ +package org.dhis2.data.forms + +import io.reactivex.Flowable +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import org.dhis2.commons.rules.RuleEngineContextData +import org.dhis2.form.data.RulesRepository +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.rules.api.RuleEngineContext + +class EnrollmentFormRepository( + private val rulesRepository: RulesRepository, + private val enrollmentUid: String, + private val d2: D2, +) : FormRepository { + private var cachedRuleEngineFlowable: Flowable + private var enrollmentOrgUnitUid: String? = null + + init { + enrollmentOrgUnitUid = if (enrollmentUid.isNotEmpty()) { + d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet()!! + .organisationUnit() + } else { + "" + } + // We don't want to rebuild RuleEngine on each request, since metadata of + // the event is not changing throughout lifecycle of FormComponent. + cachedRuleEngineFlowable = enrollmentProgram() + .switchMap { program -> + Single.zip( + rulesRepository.rulesNew(program, null).subscribeOn(Schedulers.io()), + rulesRepository.ruleVariables(program).subscribeOn(Schedulers.io()), + rulesRepository.enrollmentEvents(enrollmentUid).subscribeOn(Schedulers.io()), + rulesRepository.queryConstants().subscribeOn(Schedulers.io()), + rulesRepository.supplementaryData(enrollmentOrgUnitUid!!) + .subscribeOn(Schedulers.io()), + ) { rules, variables, events, constants, supplementaryData -> + + val ruleEngineContext = RuleEngineContext( + rules, + variables, + supplementaryData, + constants, + ) + RuleEngineContextData( + ruleEngineContext = ruleEngineContext, + ruleEnrollment = null, + ruleEvents = events, + ) + }.toFlowable() + } + .cacheWithInitialCapacity(1) + } + + override fun restartRuleEngine(): Flowable { + val orgUnit = d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet()!! + .organisationUnit() + return enrollmentProgram() + .switchMap { program -> + Single.zip( + rulesRepository.rulesNew(program, null), + rulesRepository.ruleVariables(program), + rulesRepository.enrollmentEvents(enrollmentUid), + rulesRepository.queryConstants(), + rulesRepository.supplementaryData(orgUnit!!), + ) { rules, variables, events, constants, supplementaryData -> + val ruleEngineContext = RuleEngineContext( + rules, + variables, + supplementaryData, + constants, + ) + RuleEngineContextData( + ruleEngineContext = ruleEngineContext, + ruleEnrollment = null, + ruleEvents = events, + ) + }.toFlowable() + } + .cacheWithInitialCapacity(1).also { cachedRuleEngineFlowable = it } + } + + override fun ruleEngine(): Flowable { + return cachedRuleEngineFlowable + } + + private fun enrollmentProgram(): Flowable { + return d2.enrollmentModule().enrollments().uid(enrollmentUid).get() + .map { it.program()!! } + .toFlowable() + } +} diff --git a/app/src/main/java/org/dhis2/data/forms/EventRepository.java b/app/src/main/java/org/dhis2/data/forms/EventRepository.java index 9642b67e40..ff5e99b0fa 100644 --- a/app/src/main/java/org/dhis2/data/forms/EventRepository.java +++ b/app/src/main/java/org/dhis2/data/forms/EventRepository.java @@ -3,31 +3,23 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.dhis2.commons.rules.RuleEngineContextData; import org.dhis2.form.data.RulesRepository; import org.hisp.dhis.android.core.D2; -import org.hisp.dhis.rules.RuleEngine; -import org.hisp.dhis.rules.RuleEngineContext; -import org.hisp.dhis.rules.models.TriggerEnvironment; +import org.hisp.dhis.rules.api.RuleEngineContext; import io.reactivex.Flowable; import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; -import static android.text.TextUtils.isEmpty; - -@SuppressWarnings({ - "PMD.AvoidDuplicateLiterals" -}) public class EventRepository implements FormRepository { private final String programUid; private final String orgUnit; @NonNull - private Flowable cachedRuleEngineFlowable; + private Flowable cachedRuleEngineFlowable; - private RuleEngine ruleEngine = null; + private RuleEngineContextData ruleEngineContextData = null; @Nullable private final String eventUid; @@ -44,63 +36,63 @@ public EventRepository( // We don't want to rebuild RuleEngine on each request, since metadata of // the event is not changing throughout lifecycle of FormComponent. this.cachedRuleEngineFlowable = Single.zip( - rulesRepository.rulesNew(programUid, eventUid), - rulesRepository.ruleVariables(programUid), - rulesRepository.otherEvents(this.eventUid), - rulesRepository.enrollment(this.eventUid), - rulesRepository.queryConstants(), - rulesRepository.supplementaryData(orgUnit), - (rules, variables, events, enrollment, constants, supplementaryData) -> { + rulesRepository.rulesNew(programUid, eventUid), + rulesRepository.ruleVariables(programUid), + rulesRepository.otherEvents(this.eventUid), + rulesRepository.enrollment(this.eventUid), + rulesRepository.queryConstants(), + rulesRepository.supplementaryData(orgUnit), + (rules, variables, events, enrollment, constants, supplementaryData) -> { + RuleEngineContext ruleEngineContext = new RuleEngineContext( + rules, + variables, + supplementaryData, + constants + ); - RuleEngine.Builder builder = RuleEngineContext.builder() - .rules(rules) - .ruleVariables(variables) - .constantsValue(constants) - .supplementaryData(supplementaryData) - .build().toEngineBuilder(); - builder.triggerEnvironment(TriggerEnvironment.ANDROIDCLIENT); - builder.events(events); - if (!isEmpty(enrollment.enrollment())) - builder.enrollment(enrollment); - return builder.build(); - }) - .doOnSuccess(ruleEngine -> { - this.ruleEngine = ruleEngine; - Timber.tag("ROGRAMRULEREPOSITORY").d("RULE ENGINE READY AT %s", Thread.currentThread().getName()); - }).toFlowable() + return new RuleEngineContextData( + ruleEngineContext, + enrollment.getEnrollment().isEmpty() ? null : enrollment, + events + ); + }) + .doOnSuccess(contextData -> this.ruleEngineContextData = contextData) + .toFlowable() .cacheWithInitialCapacity(1); } @Override - public Flowable restartRuleEngine() { + public Flowable restartRuleEngine() { return this.cachedRuleEngineFlowable = Single.zip( - rulesRepository.rulesNew(programUid,eventUid).subscribeOn(Schedulers.io()), - rulesRepository.ruleVariables(programUid).subscribeOn(Schedulers.io()), - rulesRepository.otherEvents(eventUid).subscribeOn(Schedulers.io()), - rulesRepository.enrollment(eventUid).subscribeOn(Schedulers.io()), - rulesRepository.queryConstants().subscribeOn(Schedulers.io()), - rulesRepository.supplementaryData(orgUnit).subscribeOn(Schedulers.io()), - (rules, variables, events, enrollment, constants, supplementaryData) -> { + rulesRepository.rulesNew(programUid, eventUid), + rulesRepository.ruleVariables(programUid), + rulesRepository.otherEvents(this.eventUid), + rulesRepository.enrollment(this.eventUid), + rulesRepository.queryConstants(), + rulesRepository.supplementaryData(orgUnit), + (rules, variables, events, enrollment, constants, supplementaryData) -> { + RuleEngineContext ruleEngineContext = new RuleEngineContext( + rules, + variables, + supplementaryData, + constants + ); - RuleEngine.Builder builder = RuleEngineContext.builder() - .rules(rules) - .ruleVariables(variables) - .constantsValue(constants) - .supplementaryData(supplementaryData) - .build().toEngineBuilder(); - builder.triggerEnvironment(TriggerEnvironment.ANDROIDCLIENT); - builder.events(events); - if (!isEmpty(enrollment.enrollment())) - builder.enrollment(enrollment); - return builder.build(); - }).toFlowable() + return new RuleEngineContextData( + ruleEngineContext, + enrollment.getEnrollment().isEmpty() ? null : enrollment, + events + ); + }) + .doOnSuccess(contextData -> this.ruleEngineContextData = contextData) + .toFlowable() .cacheWithInitialCapacity(1); } @NonNull @Override - public Flowable ruleEngine() { - return ruleEngine != null ? Flowable.just(ruleEngine) : cachedRuleEngineFlowable; + public Flowable ruleEngine() { + return ruleEngineContextData != null ? Flowable.just(ruleEngineContextData) : cachedRuleEngineFlowable; } } \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/data/forms/FormRepository.java b/app/src/main/java/org/dhis2/data/forms/FormRepository.java index 8564ccc6c7..78425ea42f 100644 --- a/app/src/main/java/org/dhis2/data/forms/FormRepository.java +++ b/app/src/main/java/org/dhis2/data/forms/FormRepository.java @@ -2,15 +2,15 @@ import androidx.annotation.NonNull; -import org.hisp.dhis.rules.RuleEngine; +import org.dhis2.commons.rules.RuleEngineContextData; import io.reactivex.Flowable; public interface FormRepository { - Flowable restartRuleEngine(); + Flowable restartRuleEngine(); @NonNull - Flowable ruleEngine(); + Flowable ruleEngine(); } \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/data/forms/dataentry/EnrollmentRuleEngineRepository.java b/app/src/main/java/org/dhis2/data/forms/dataentry/EnrollmentRuleEngineRepository.java deleted file mode 100644 index 3e3ac211c6..0000000000 --- a/app/src/main/java/org/dhis2/data/forms/dataentry/EnrollmentRuleEngineRepository.java +++ /dev/null @@ -1,170 +0,0 @@ -package org.dhis2.data.forms.dataentry; - -import androidx.annotation.NonNull; - -import org.dhis2.data.forms.FormRepository; -import org.dhis2.form.bindings.RuleExtensionsKt; -import org.dhis2.utils.Result; -import org.hisp.dhis.android.core.D2; -import org.hisp.dhis.android.core.enrollment.Enrollment; -import org.hisp.dhis.android.core.organisationunit.OrganisationUnit; -import org.hisp.dhis.android.core.program.Program; -import org.hisp.dhis.android.core.program.ProgramRule; -import org.hisp.dhis.android.core.program.ProgramRuleAction; -import org.hisp.dhis.android.core.program.ProgramRuleActionType; -import org.hisp.dhis.android.core.program.ProgramRuleVariable; -import org.hisp.dhis.rules.RuleEngine; -import org.hisp.dhis.rules.models.Rule; -import org.hisp.dhis.rules.models.RuleAttributeValue; -import org.hisp.dhis.rules.models.RuleEffect; -import org.hisp.dhis.rules.models.RuleEnrollment; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import io.reactivex.Flowable; - -public final class EnrollmentRuleEngineRepository implements RuleEngineRepository { - - @NonNull - private final FormRepository formRepository; - - @NonNull - private final String enrollmentUid; - - private RuleEnrollment.Builder ruleEnrollmentBuilder; - - @NonNull - private final D2 d2; - - private Map> attributeRules = new HashMap<>(); - - private List mandatoryRules; - - public EnrollmentRuleEngineRepository(@NonNull FormRepository formRepository, - @NonNull String enrollmentUid, @NotNull D2 d2) { - this.d2 = d2; - this.formRepository = formRepository; - this.enrollmentUid = enrollmentUid; - - if (!enrollmentUid.isEmpty()) { - initData(); - } - - } - - public void initData() { - Enrollment enrollment = d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet(); - OrganisationUnit ou = d2.organisationUnitModule().organisationUnits().uid(enrollment.organisationUnit()) - .blockingGet(); - Program program = d2.programModule().programs().uid(enrollment.program()) - .blockingGet(); - - ruleEnrollmentBuilder = RuleEnrollment.builder().enrollment(enrollment.uid()) - .incidentDate(enrollment.incidentDate() == null ? enrollment.enrollmentDate() : enrollment.incidentDate()) - .enrollmentDate(enrollment.enrollmentDate()) - .status(RuleEnrollment.Status.valueOf(enrollment.status().name())) - .organisationUnit(enrollment.organisationUnit()).organisationUnitCode(ou.code()) - .programName(program.displayName()); - - loadAttrRules(program.uid()); - } - - private void loadAttrRules(String programUid) { - List rules = d2.programModule().programRules().byProgramUid().eq(programUid) - .withProgramRuleActions().blockingGet(); - mandatoryRules = new ArrayList<>(); - Iterator ruleIterator = rules.iterator(); - while (ruleIterator.hasNext()) { - ProgramRule rule = ruleIterator.next(); - if (rule.condition() == null || rule.programStage() != null) - ruleIterator.remove(); - else - for (ProgramRuleAction action : rule.programRuleActions()) - if (action.programRuleActionType() == ProgramRuleActionType.HIDEFIELD - || action.programRuleActionType() == ProgramRuleActionType.HIDEPROGRAMSTAGE - || action.programRuleActionType() == ProgramRuleActionType.HIDESECTION - || action.programRuleActionType() == ProgramRuleActionType.ASSIGN - || action.programRuleActionType() == ProgramRuleActionType.SHOWWARNING - || action.programRuleActionType() == ProgramRuleActionType.SHOWERROR - || action.programRuleActionType() == ProgramRuleActionType.DISPLAYKEYVALUEPAIR - || action.programRuleActionType() == ProgramRuleActionType.DISPLAYTEXT - || action.programRuleActionType() == ProgramRuleActionType.HIDEOPTIONGROUP - || action.programRuleActionType() == ProgramRuleActionType.HIDEOPTION - || action.programRuleActionType() == ProgramRuleActionType.SHOWOPTIONGROUP - || action.programRuleActionType() == ProgramRuleActionType.SETMANDATORYFIELD) - if (!mandatoryRules.contains(rule)) - mandatoryRules.add(rule); - } - - List variables = d2.programModule().programRuleVariables().byProgramUid().eq(programUid) - .blockingGet(); - Iterator variableIterator = variables.iterator(); - while (variableIterator.hasNext()) { - ProgramRuleVariable variable = variableIterator.next(); - if (variable.trackedEntityAttribute() == null) - variableIterator.remove(); - } - List finalMandatoryRules = RuleExtensionsKt.toRuleList(mandatoryRules); - for (ProgramRuleVariable variable : variables) { - if (variable.trackedEntityAttribute() != null - && !attributeRules.containsKey(variable.trackedEntityAttribute().uid())) - attributeRules.put(variable.trackedEntityAttribute().uid(), finalMandatoryRules); - for (ProgramRule rule : rules) { - if (rule.condition().contains(variable.displayName()) - || actionsContainsAttr(rule.programRuleActions(), variable.displayName())) { - if (attributeRules.get(variable.trackedEntityAttribute().uid()) == null) - attributeRules.put(variable.trackedEntityAttribute().uid(), finalMandatoryRules); - attributeRules.get(variable.trackedEntityAttribute().uid()).add(RuleExtensionsKt.toRuleEngineObject(rule)); - } - } - } - } - - private boolean actionsContainsAttr(List programRuleActions, String variableName) { - boolean actionContainsDe = false; - for (ProgramRuleAction ruleAction : programRuleActions) { - if (ruleAction.data() != null && ruleAction.data().contains(variableName)) - actionContainsDe = true; - } - return actionContainsDe; - } - - @Override - public Flowable updateRuleEngine() { - return this.formRepository.restartRuleEngine(); - } - - @NonNull - @Override - public Flowable> calculate() { - return queryAttributeValues() - .map(ruleAttributeValues -> ruleEnrollmentBuilder.attributeValues(ruleAttributeValues) - .build()) - .switchMap(enrollment -> formRepository.ruleEngine() - .switchMap(ruleEngine -> Flowable.fromCallable(ruleEngine.evaluate(enrollment))) - .map(Result::success) - .onErrorReturn(error -> Result.failure(new Exception(error)))); - } - - @NonNull - @Override - public Flowable> reCalculate() { - initData(); - return calculate(); - } - - @NonNull - private Flowable> queryAttributeValues() { - return d2.enrollmentModule().enrollments().uid(enrollmentUid).get() - .flatMap(enrollment -> d2.trackedEntityModule().trackedEntityAttributeValues() - .byTrackedEntityInstance().eq(enrollment.trackedEntityInstance()).get() - .map(list -> - RuleExtensionsKt.toRuleAttributeValue(list, d2, enrollment.program()))).toFlowable(); - } -} - diff --git a/app/src/main/java/org/dhis2/data/forms/dataentry/RuleEngineRepository.java b/app/src/main/java/org/dhis2/data/forms/dataentry/RuleEngineRepository.java deleted file mode 100644 index 87a1f40a2f..0000000000 --- a/app/src/main/java/org/dhis2/data/forms/dataentry/RuleEngineRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.dhis2.data.forms.dataentry; - -import org.dhis2.utils.Result; -import org.hisp.dhis.rules.RuleEngine; -import org.hisp.dhis.rules.models.RuleEffect; - -import androidx.annotation.NonNull; -import io.reactivex.Flowable; - -public interface RuleEngineRepository { - - Flowable updateRuleEngine(); - - @NonNull - Flowable> calculate(); - - @NonNull - Flowable> reCalculate(); - -} diff --git a/app/src/main/java/org/dhis2/data/forms/dataentry/ValueStore.kt b/app/src/main/java/org/dhis2/data/forms/dataentry/ValueStore.kt index 0ba894599e..4381d11c82 100644 --- a/app/src/main/java/org/dhis2/data/forms/dataentry/ValueStore.kt +++ b/app/src/main/java/org/dhis2/data/forms/dataentry/ValueStore.kt @@ -18,11 +18,6 @@ interface ValueStore { fun deleteOptionValues(optionCodeValuesToDelete: List) fun deleteOptionValueIfSelected(field: String, optionUid: String): StoreResult - fun deleteOptionValueIfSelectedInGroup( - field: String, - optionGroupUid: String, - isInGroup: Boolean, - ): StoreResult fun overrideProgram(programUid: String?) fun validate(dataElementUid: String, value: String?): ValidatorResult diff --git a/app/src/main/java/org/dhis2/data/forms/dataentry/ValueStoreImpl.kt b/app/src/main/java/org/dhis2/data/forms/dataentry/ValueStoreImpl.kt index ebd4b20bd7..8ec8091e42 100644 --- a/app/src/main/java/org/dhis2/data/forms/dataentry/ValueStoreImpl.kt +++ b/app/src/main/java/org/dhis2/data/forms/dataentry/ValueStoreImpl.kt @@ -258,37 +258,6 @@ class ValueStoreImpl( } } - override fun deleteOptionValueIfSelectedInGroup( - field: String, - optionGroupUid: String, - isInGroup: Boolean, - ): StoreResult { - val optionsInGroup = - d2.optionModule().optionGroups() - .withOptions() - .uid(optionGroupUid) - .blockingGet() - ?.options() - ?.map { d2.optionModule().options().uid(it.uid()).blockingGet()?.code()!! } - ?: arrayListOf() - return when (entryMode) { - EntryMode.DE -> deleteDataElementValueIfNotInGroup( - field, - optionsInGroup, - isInGroup, - ) - EntryMode.ATTR -> deleteAttributeValueIfNotInGroup( - field, - optionsInGroup, - isInGroup, - ) - EntryMode.DV, - -> throw IllegalArgumentException( - "DataValues can't be saved using these arguments. Use the other one.", - ) - } - } - private fun deleteDataElementValue(field: String, optionUid: String): StoreResult { val option = d2.optionModule().options().uid(optionUid).blockingGet() val possibleValues = arrayListOf(option?.name(), option?.code()).filterNotNull() @@ -317,38 +286,6 @@ class ValueStoreImpl( } } - private fun deleteDataElementValueIfNotInGroup( - field: String, - optionCodesToShow: List, - isInGroup: Boolean, - ): StoreResult { - val valueRepository = - d2.trackedEntityModule().trackedEntityDataValues().value(recordUid, field) - return if (valueRepository.blockingExists() && - optionCodesToShow.contains(valueRepository.blockingGet()?.value()) == isInGroup - ) { - save(field, null).blockingFirst() - } else { - StoreResult(field, ValueStoreResult.VALUE_HAS_NOT_CHANGED) - } - } - - private fun deleteAttributeValueIfNotInGroup( - field: String, - optionCodesToShow: List, - isInGroup: Boolean, - ): StoreResult { - val valueRepository = - d2.trackedEntityModule().trackedEntityAttributeValues().value(field, recordUid) - return if (valueRepository.blockingExists() && - optionCodesToShow.contains(valueRepository.blockingGet()?.value()) == isInGroup - ) { - save(field, null).blockingFirst() - } else { - StoreResult(field, ValueStoreResult.VALUE_HAS_NOT_CHANGED) - } - } - override fun deleteOptionValues(optionCodeValuesToDelete: List) { when (entryMode) { EntryMode.DE -> deleteOptionValuesForEvents(optionCodeValuesToDelete) diff --git a/app/src/main/java/org/dhis2/data/server/ServerComponent.java b/app/src/main/java/org/dhis2/data/server/ServerComponent.java index 3fca991351..cbe4ed9d70 100644 --- a/app/src/main/java/org/dhis2/data/server/ServerComponent.java +++ b/app/src/main/java/org/dhis2/data/server/ServerComponent.java @@ -3,11 +3,12 @@ import androidx.annotation.NonNull; import org.dhis2.commons.di.dagger.PerServer; -import org.dhis2.data.dhislogic.DhisPeriodUtils; -import org.dhis2.data.user.UserComponent; -import org.dhis2.data.user.UserModule; +import org.dhis2.commons.filters.di.FilterModule; import org.dhis2.commons.orgunitselector.OUTreeComponent; import org.dhis2.commons.orgunitselector.OUTreeModule; +import org.dhis2.commons.resources.DhisPeriodUtils; +import org.dhis2.data.user.UserComponent; +import org.dhis2.data.user.UserModule; import org.dhis2.ui.ThemeManager; import org.dhis2.usescases.login.accounts.AccountsComponent; import org.dhis2.usescases.login.accounts.AccountsModule; @@ -22,7 +23,7 @@ import dhis2.org.analytics.charts.Charts; @PerServer -@Subcomponent(modules = {ServerModule.class}) +@Subcomponent(modules = {ServerModule.class, FilterModule.class}) public interface ServerComponent extends Charts.Dependencies { @NonNull diff --git a/app/src/main/java/org/dhis2/data/server/ServerModule.kt b/app/src/main/java/org/dhis2/data/server/ServerModule.kt index 5bc7468309..f78931d708 100644 --- a/app/src/main/java/org/dhis2/data/server/ServerModule.kt +++ b/app/src/main/java/org/dhis2/data/server/ServerModule.kt @@ -1,6 +1,7 @@ package org.dhis2.data.server import android.content.Context +import android.content.ContextWrapper import dagger.Module import dagger.Provides import dhis2.org.analytics.charts.Charts @@ -14,8 +15,10 @@ import org.dhis2.commons.filters.data.GetFiltersApplyingWebAppConfig import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.reporting.CrashReportController import org.dhis2.commons.resources.ColorUtils +import org.dhis2.commons.resources.DhisPeriodUtils +import org.dhis2.commons.resources.MetadataIconProvider +import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider -import org.dhis2.data.dhislogic.DhisPeriodUtils import org.dhis2.data.service.SyncStatusController import org.dhis2.data.service.VersionRepository import org.dhis2.form.data.FileController @@ -147,6 +150,26 @@ class ServerModule { ) } + @Provides + @PerServer + fun metadataIconProvider( + d2: D2, + ): MetadataIconProvider { + return MetadataIconProvider(d2) + } + + @Provides + @PerServer + fun provideResourceManager( + context: Context, + themeManager: ThemeManager, + colorUtils: ColorUtils, + ): ResourceManager { + val contextWrapper = ContextWrapper(context) + contextWrapper.setTheme(themeManager.getAppTheme()) + return ResourceManager(contextWrapper, colorUtils) + } + companion object { @JvmStatic fun getD2Configuration(context: Context): D2Configuration { diff --git a/app/src/main/java/org/dhis2/data/service/SyncGranularWorker.kt b/app/src/main/java/org/dhis2/data/service/SyncGranularWorker.kt index 96122ae22f..02a769c62e 100644 --- a/app/src/main/java/org/dhis2/data/service/SyncGranularWorker.kt +++ b/app/src/main/java/org/dhis2/data/service/SyncGranularWorker.kt @@ -25,10 +25,18 @@ package org.dhis2.data.service +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Context +import android.content.pm.ServiceInfo +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.work.ForegroundInfo import androidx.work.Worker import androidx.work.WorkerParameters import org.dhis2.App +import org.dhis2.R import org.dhis2.commons.Constants.ATTRIBUTE_OPTION_COMBO import org.dhis2.commons.Constants.CATEGORY_OPTION_COMBO import org.dhis2.commons.Constants.CONFLICT_TYPE @@ -36,9 +44,10 @@ import org.dhis2.commons.Constants.ORG_UNIT import org.dhis2.commons.Constants.PERIOD_ID import org.dhis2.commons.Constants.UID import org.dhis2.commons.sync.ConflictType -import java.util.Objects import javax.inject.Inject +private const val GRANULAR_CHANNEL = "sync_granular_notification" +private const val SYNC_GRANULAR_ID = 8071988 class SyncGranularWorker( context: Context, workerParams: WorkerParameters, @@ -48,12 +57,18 @@ class SyncGranularWorker( internal lateinit var presenter: SyncPresenter override fun doWork(): Result { - Objects.requireNonNull((applicationContext as App).userComponent())!! - .plus(SyncGranularRxModule()).inject(this) + (applicationContext as App).userComponent() + ?.plus(SyncGranularRxModule())?.inject(this) val uid = inputData.getString(UID) ?: return Result.failure() val conflictType = inputData.getString(CONFLICT_TYPE)?.let { ConflictType.valueOf(it) } + triggerNotification( + title = applicationContext.getString(R.string.app_name), + content = applicationContext.getString(R.string.syncing_data), + progress = 0, + ) + val result = when (conflictType) { ConflictType.PROGRAM -> { presenter.blockSyncGranularProgram(uid) @@ -79,6 +94,47 @@ class SyncGranularWorker( ) else -> Result.failure() } + + cancelNotification() return result } + + private fun triggerNotification(title: String, content: String, progress: Int) { + val notificationManager = + applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val mChannel = NotificationChannel( + GRANULAR_CHANNEL, + "GranularSync", + NotificationManager.IMPORTANCE_HIGH, + ) + notificationManager.createNotificationChannel(mChannel) + } + val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder( + applicationContext, + GRANULAR_CHANNEL, + ) + .setSmallIcon(R.drawable.ic_sync) + .setContentTitle(title) + .setContentText(content) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setAutoCancel(false) + .setProgress(100, progress, true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + setForegroundAsync( + ForegroundInfo( + SYNC_GRANULAR_ID, + notificationBuilder.build(), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0, + ), + ) + } + + private fun cancelNotification() { + val notificationManager = NotificationManagerCompat.from( + applicationContext, + ) + notificationManager.cancel(SYNC_GRANULAR_ID) + } } diff --git a/app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt b/app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt index fcdc44fd2a..9e1acd36e8 100644 --- a/app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt +++ b/app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt @@ -29,6 +29,7 @@ import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.call.D2Progress import org.hisp.dhis.android.core.arch.call.D2ProgressStatus import org.hisp.dhis.android.core.common.State +import org.hisp.dhis.android.core.fileresource.FileResourceDomainType import org.hisp.dhis.android.core.imports.TrackerImportConflict import org.hisp.dhis.android.core.program.ProgramType import org.hisp.dhis.android.core.settings.GeneralSettings @@ -211,6 +212,12 @@ class SyncPresenterImpl( ).andThen( d2.mapsModule().mapLayersDownloader().downloadMetadata(), + ).andThen( + Completable.fromObservable( + d2.fileResourceModule().fileResourceDownloader() + .byDomainType().eq(FileResourceDomainType.ICON) + .download(), + ), ).blockingAwait() } @@ -240,7 +247,9 @@ class SyncPresenterImpl( if (d2.systemInfoModule().versionManager().isGreaterThan(DHISVersion.V2_32)) { syncStatusController.initDownloadMedia() Completable.fromObservable( - d2.fileResourceModule().fileResourceDownloader().download(), + d2.fileResourceModule().fileResourceDownloader() + .byDomainType().eq(FileResourceDomainType.DATA_VALUE) + .download(), ).blockingAwait() } } @@ -305,6 +314,7 @@ class SyncPresenterImpl( SyncResult.SYNC -> { ListenableWorker.Result.success() } + SyncResult.ERROR -> { val trackerImportConflicts = messageTrackerImportConflict(teiUid) val mergeDateConflicts = ArrayList() @@ -320,6 +330,7 @@ class SyncPresenterImpl( .build() ListenableWorker.Result.failure(data) } + SyncResult.INCOMPLETE -> { val data = Data.Builder() .putStringArray("incomplete", arrayOf("INCOMPLETE")) diff --git a/app/src/main/java/org/dhis2/data/user/UserComponent.java b/app/src/main/java/org/dhis2/data/user/UserComponent.java index 4aa6e247e2..b562c2e0ce 100644 --- a/app/src/main/java/org/dhis2/data/user/UserComponent.java +++ b/app/src/main/java/org/dhis2/data/user/UserComponent.java @@ -72,6 +72,8 @@ import org.dhis2.usescases.teiDashboard.TeiDashboardModule; import org.dhis2.usescases.teiDashboard.dashboardfragments.relationships.RelationshipComponent; import org.dhis2.usescases.teiDashboard.dashboardfragments.relationships.RelationshipModule; +import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingComponent; +import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingModule; import org.dhis2.usescases.teiDashboard.teiProgramList.TeiProgramListComponent; import org.dhis2.usescases.teiDashboard.teiProgramList.TeiProgramListModule; import org.dhis2.utils.optionset.OptionSetComponent; @@ -204,4 +206,7 @@ public interface UserComponent { @NonNull SessionComponent plus(PinModule pinModule); + + @NonNull + SchedulingComponent plus(SchedulingModule schedulingModule); } diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableActivity.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableActivity.kt index f4ac9f4095..c0f162192b 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableActivity.kt @@ -172,6 +172,14 @@ class DataSetTableActivity : ActivityGlobalAbstract(), DataSetTableContract.View if (hasChanged) presenter.updateData() } }) + .onNoConnectionListener { + val contextView = findViewById(R.id.navigationBar) + Snackbar.make( + contextView, + R.string.sync_offline_check_connection, + Snackbar.LENGTH_SHORT, + ).show() + } .show(DATAVALUE_SYNC) } diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableModule.java b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableModule.java index 3f2677d58d..42dd58171e 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableModule.java +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableModule.java @@ -4,9 +4,9 @@ import androidx.lifecycle.ViewModelStore; import org.dhis2.commons.di.dagger.PerActivity; +import org.dhis2.commons.resources.DhisPeriodUtils; import org.dhis2.commons.resources.ResourceManager; import org.dhis2.commons.viewmodel.DispatcherProvider; -import org.dhis2.data.dhislogic.DhisPeriodUtils; import org.dhis2.usescases.datasets.datasetInitial.DataSetInitialRepository; import org.dhis2.usescases.datasets.datasetInitial.DataSetInitialRepositoryImpl; import org.dhis2.utils.analytics.AnalyticsHelper; diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTablePresenter.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTablePresenter.kt index 23ed4d1c1a..5528118c89 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTablePresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTablePresenter.kt @@ -13,8 +13,8 @@ import kotlinx.coroutines.withContext import org.dhis2.commons.matomo.Actions import org.dhis2.commons.matomo.Categories import org.dhis2.commons.matomo.Labels +import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.viewmodel.DispatcherProvider -import org.dhis2.data.dhislogic.DhisPeriodUtils import org.dhis2.usescases.datasets.dataSetTable.dataSetSection.DataSetSection import org.dhis2.utils.analytics.AnalyticsHelper import org.dhis2.utils.validationrules.ValidationRuleResult diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableViewModelFactory.kt index 24488d03bb..f14996ff71 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableViewModelFactory.kt @@ -3,8 +3,8 @@ package org.dhis2.usescases.datasets.dataSetTable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import io.reactivex.processors.FlowableProcessor +import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.viewmodel.DispatcherProvider -import org.dhis2.data.dhislogic.DhisPeriodUtils import org.dhis2.usescases.datasets.dataSetTable.DataSetTableContract.View import org.dhis2.utils.analytics.AnalyticsHelper diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetDetail/DataSetDetailFragment.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetDetail/DataSetDetailFragment.kt index 58ce1afe55..cf0fa562b4 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetDetail/DataSetDetailFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetDetail/DataSetDetailFragment.kt @@ -9,12 +9,10 @@ import android.view.ViewGroup import io.reactivex.Flowable import org.dhis2.R import org.dhis2.commons.date.toDateSpan -import org.dhis2.commons.resources.ColorType import org.dhis2.commons.resources.ColorUtils -import org.dhis2.commons.resources.ResourceManager -import org.dhis2.data.dhislogic.DhisPeriodUtils +import org.dhis2.commons.resources.DhisPeriodUtils +import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.databinding.FragmentDatasetDetailBinding -import org.dhis2.ui.MetadataIconData import org.dhis2.ui.setUpMetadataIcon import org.dhis2.usescases.datasets.dataSetTable.DataSetTableActivity import org.dhis2.usescases.general.FragmentGlobalAbstract @@ -22,6 +20,7 @@ import org.hisp.dhis.android.core.common.ObjectStyle import org.hisp.dhis.android.core.dataset.DataSetInstance import org.hisp.dhis.android.core.period.Period import org.hisp.dhis.android.core.period.PeriodType +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import java.util.Date import java.util.Locale import javax.inject.Inject @@ -47,6 +46,9 @@ class DataSetDetailFragment private constructor() : FragmentGlobalAbstract(), Da @Inject lateinit var colorUtils: ColorUtils + @Inject + lateinit var metadataIconProvider: MetadataIconProvider + companion object { @JvmStatic fun create(dataSetUid: String, accessWrite: Boolean): DataSetDetailFragment { @@ -146,24 +148,11 @@ class DataSetDetailFragment private constructor() : FragmentGlobalAbstract(), Da } override fun setStyle(style: ObjectStyle?) { - val color = colorUtils.getColorFrom( - style?.color(), - colorUtils.getPrimaryColor( - mContext, - ColorType.PRIMARY, - ), - ) - val imageResource = ResourceManager(mContext, colorUtils).getObjectStyleDrawableResource( - style?.icon(), - R.drawable.ic_default_outline, - ) - - binding.composeDataSetIcon.setUpMetadataIcon( - MetadataIconData( - programColor = color, - iconResource = imageResource, - ), - ) + style?.let { + binding.composeDataSetIcon.setUpMetadataIcon( + metadataIconProvider(style, SurfaceColor.Primary), + ) + } } override fun observeReopenChanges(): Flowable { diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt index 5182a469ea..e953a7820e 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt @@ -153,6 +153,7 @@ class DataValuePresenter( id = cell.id ?: "", mainLabel = dataElement?.displayFormName() ?: "-", secondaryLabels = repository.getCatOptComboOptions(ids[1]), + helperText = dataElement?.description(), currentValue = cell.value, keyboardInputType = inputType, error = errors[cell.id], diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValueRepository.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValueRepository.kt index fee52a250c..20bca865b6 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValueRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValueRepository.kt @@ -612,13 +612,7 @@ class DataValueRepository( isNumber = dataElement.valueType()!!.isNumeric } - val options = dataElement.optionSetUid()?.let { - d2.optionModule().options() - .byOptionSetUid().eq(it) - .orderBySortOrder(RepositoryScope.OrderByDirection.ASC) - .blockingGet() - .map { option -> "${option.code()}_${option.displayName()}" } - } ?: emptyList() + val options = getOptionsForOptionSet(dataElement.optionSetUid()) for ( categoryOptionCombo in categorOptionCombos @@ -1011,7 +1005,7 @@ class DataValueRepository( null, dataElement.displayDescription(), dataElement.uid(), - emptyList(), + getOptionsForOptionSet(dataElement.optionSetUid()), "android", 0, 0, @@ -1035,4 +1029,12 @@ class DataValueRepository( fun getDataSetInfo(): Triple { return Triple(periodId, orgUnitUid, attributeOptionComboUid) } + + private fun getOptionsForOptionSet(optionSetUid: String?) = optionSetUid?.let { + d2.optionModule().options() + .byOptionSetUid().eq(it) + .orderBySortOrder(RepositoryScope.OrderByDirection.ASC) + .blockingGet() + .map { option -> "${option.code()}_${option.displayName()}" } + } ?: emptyList() } diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/MapFieldValueToUser.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/MapFieldValueToUser.kt index 7a406a75f1..ea27653e45 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/MapFieldValueToUser.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/MapFieldValueToUser.kt @@ -48,6 +48,9 @@ class MapFieldValueToUser( field.value() } } + ValueType.MULTI_TEXT -> field.value()?.split(", ")?.map { code -> + field.options().find { it.contains(code) }?.split("_")?.get(1) + }?.joinToString(", ") else -> field.value() } } diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/TableDataToTableModelMapper.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/TableDataToTableModelMapper.kt index f2c92e5d75..e361f5fba6 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/TableDataToTableModelMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/TableDataToTableModelMapper.kt @@ -8,6 +8,7 @@ import org.dhis2.composetable.model.TableHeaderCell import org.dhis2.composetable.model.TableHeaderRow import org.dhis2.composetable.model.TableModel import org.dhis2.composetable.model.TableRowModel +import org.hisp.dhis.android.core.common.ValueType import java.util.SortedMap class TableDataToTableModelMapper(val mapFieldValueToUser: MapFieldValueToUser) { @@ -50,6 +51,7 @@ class TableDataToTableModelMapper(val mapFieldValueToUser: MapFieldValueToUser) mandatory = field.mandatory(), error = field.error(), warning = field.warning(), + isMultiText = dataElement.valueType() == ValueType.MULTI_TEXT, ) }.toMap(), isLastRow = rowIndex == (tableData.rows()!!.size - 1), diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailActivity.java b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailActivity.java index 353e038802..e0529c293a 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailActivity.java +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailActivity.java @@ -16,6 +16,8 @@ import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; +import com.google.android.material.snackbar.Snackbar; + import org.dhis2.App; import org.dhis2.R; import org.dhis2.bindings.ExtensionsKt; @@ -26,6 +28,7 @@ import org.dhis2.commons.filters.FiltersAdapter; import org.dhis2.commons.orgunitselector.OUTreeFragment; import org.dhis2.commons.sync.ConflictType; +import org.dhis2.commons.sync.OnNoConnectionListener; import org.dhis2.databinding.ActivityDatasetDetailBinding; import org.dhis2.ui.ThemeManager; import org.dhis2.usescases.datasets.datasetDetail.datasetList.DataSetListFragment; @@ -240,10 +243,18 @@ public void setProgress(boolean active) { @Override public void showGranularSync() { presenter.trackDataSetGranularSync(); + View contextView = findViewById(R.id.navigationBar); new SyncStatusDialog.Builder() .withContext(this, null) .withSyncContext(new SyncContext.DataSet(dataSetUid)) .onDismissListener(hasChanged -> presenter.refreshList()) + .onNoConnectionListener(() -> + Snackbar.make( + contextView, + R.string.sync_offline_check_connection, + Snackbar.LENGTH_SHORT + ).show() + ) .show("DATASET_SYNC"); } } diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailModule.java b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailModule.java index 890a19107b..f59584a82e 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailModule.java +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailModule.java @@ -36,10 +36,10 @@ import org.dhis2.commons.filters.FiltersAdapter; import org.dhis2.commons.filters.data.FilterRepository; import org.dhis2.commons.matomo.MatomoAnalyticsController; +import org.dhis2.commons.resources.DhisPeriodUtils; import org.dhis2.commons.resources.ResourceManager; import org.dhis2.commons.schedulers.SchedulerProvider; import org.dhis2.commons.viewmodel.DispatcherProvider; -import org.dhis2.data.dhislogic.DhisPeriodUtils; import org.dhis2.usescases.datasets.datasetDetail.datasetList.mapper.DatasetCardMapper; import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator; import org.hisp.dhis.android.core.D2; diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailRepositoryImpl.java index 8f9af478f3..82119871d1 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailRepositoryImpl.java @@ -1,7 +1,8 @@ package org.dhis2.usescases.datasets.datasetDetail; import static org.dhis2.data.dhislogic.AuthoritiesKt.AUTH_DATAVALUE_ADD; -import org.dhis2.data.dhislogic.DhisPeriodUtils; + +import org.dhis2.commons.resources.DhisPeriodUtils; import org.hisp.dhis.android.core.D2; import org.hisp.dhis.android.core.arch.helpers.UidsHelper; import org.hisp.dhis.android.core.category.CategoryCombo; @@ -16,14 +17,15 @@ import org.hisp.dhis.android.core.period.Period; import org.hisp.dhis.android.core.period.PeriodType; import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationsSetting; + import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Objects; + import dhis2.org.analytics.charts.Charts; import io.reactivex.Flowable; -import timber.log.Timber; public class DataSetDetailRepositoryImpl implements DataSetDetailRepository { diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListAdapter.kt b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListAdapter.kt index ba611acf5e..235945ccaf 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListAdapter.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListAdapter.kt @@ -3,6 +3,11 @@ package org.dhis2.usescases.datasets.datasetDetail.datasetList import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter @@ -12,6 +17,8 @@ import org.dhis2.databinding.ItemDatasetBinding import org.dhis2.usescases.datasets.datasetDetail.DataSetDetailModel import org.dhis2.usescases.datasets.datasetDetail.datasetList.mapper.DatasetCardMapper import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing class DataSetListAdapter( val viewModel: DataSetListViewModel, @@ -47,16 +54,28 @@ class DataSetListAdapter( viewModel.openDataSet(it) }, ) - ListCard( - listAvatar = card.avatar, - title = card.title, - lastUpdated = card.lastUpdated, - additionalInfoList = card.additionalInfo, - actionButton = card.actionButton, - expandLabelText = card.expandLabelText, - shrinkLabelText = card.shrinkLabelText, - onCardClick = card.onCardCLick, - ) + Column( + modifier = Modifier + .padding( + start = Spacing.Spacing8, + end = Spacing.Spacing8, + bottom = Spacing.Spacing4, + ), + ) { + if (position == 0) { + Spacer(modifier = Modifier.size(Spacing.Spacing8)) + } + ListCard( + listAvatar = card.avatar, + title = ListCardTitleModel(text = card.title), + lastUpdated = card.lastUpdated, + additionalInfoList = card.additionalInfo, + actionButton = card.actionButton, + expandLabelText = card.expandLabelText, + shrinkLabelText = card.shrinkLabelText, + onCardClick = card.onCardCLick, + ) + } } holder.bind(it, viewModel) diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListFragment.kt b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListFragment.kt index 2e75a4831b..476e90bae2 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListFragment.kt @@ -5,6 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.viewModels +import com.google.android.material.snackbar.Snackbar import org.dhis2.R import org.dhis2.commons.Constants import org.dhis2.commons.sync.OnDismissListener @@ -132,7 +133,16 @@ class DataSetListFragment : FragmentGlobalAbstract() { viewModel.updateData() } } - }).show(FRAGMENT_TAG) + }) + .onNoConnectionListener { + val contextView = activity.findViewById(R.id.navigationBar) + Snackbar.make( + contextView, + R.string.sync_offline_check_connection, + Snackbar.LENGTH_SHORT, + ).show() + } + .show(FRAGMENT_TAG) } companion object { diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModel.kt b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModel.kt index 3439625446..9eb943de26 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModel.kt @@ -23,7 +23,7 @@ class DataSetListViewModel( schedulerProvider: SchedulerProvider, val filterManager: FilterManager, val matomoAnalyticsController: MatomoAnalyticsController, - private val dispatcher: DispatcherProvider, + dispatcher: DispatcherProvider, ) : ViewModel() { @@ -45,13 +45,12 @@ class DataSetListViewModel( filterManager.asFlowable() .startWith(filterManager) .flatMap { filterManager: FilterManager -> - dataSetDetailRepository.dataSetGroups( filterManager.orgUnitUidsFilters, filterManager.periodFilters, filterManager.stateFilters, filterManager.catOptComboFilters, - ) + ).subscribeOn(schedulerProvider.io()) } .subscribeOn(schedulerProvider.io()) .observeOn(schedulerProvider.ui()) diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetInitial/DataSetInitialActivity.java b/app/src/main/java/org/dhis2/usescases/datasets/datasetInitial/DataSetInitialActivity.java index 135ac22508..a18a4602d6 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetInitial/DataSetInitialActivity.java +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetInitial/DataSetInitialActivity.java @@ -11,10 +11,11 @@ import org.dhis2.App; import org.dhis2.R; import org.dhis2.commons.Constants; +import org.dhis2.commons.dialogs.PeriodDialog; +import org.dhis2.commons.extensions.CategoryOptionExtensionsKt; import org.dhis2.commons.orgunitselector.OUTreeFragment; import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope; -import org.dhis2.data.dhislogic.CategoryOptionExtensionsKt; -import org.dhis2.data.dhislogic.DhisPeriodUtils; +import org.dhis2.commons.resources.DhisPeriodUtils; import org.dhis2.data.dhislogic.OrganisationUnitExtensionsKt; import org.dhis2.databinding.ActivityDatasetInitialBinding; import org.dhis2.databinding.ItemCategoryComboBinding; @@ -22,7 +23,6 @@ import org.dhis2.usescases.general.ActivityGlobalAbstract; import org.dhis2.utils.category.CategoryDialog; import org.dhis2.utils.customviews.CategoryOptionPopUp; -import org.dhis2.utils.customviews.PeriodDialog; import org.dhis2.utils.customviews.PeriodDialogInputPeriod; import org.hisp.dhis.android.core.category.Category; import org.hisp.dhis.android.core.category.CategoryOption; diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetInitial/DataSetInitialPresenter.kt b/app/src/main/java/org/dhis2/usescases/datasets/datasetInitial/DataSetInitialPresenter.kt index 3db2599ce5..77a554d44e 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetInitial/DataSetInitialPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetInitial/DataSetInitialPresenter.kt @@ -4,14 +4,13 @@ import io.reactivex.Flowable import io.reactivex.disposables.CompositeDisposable import io.reactivex.functions.BiFunction import org.dhis2.commons.data.tuples.Pair +import org.dhis2.commons.extensions.inDateRange +import org.dhis2.commons.extensions.inOrgUnit import org.dhis2.commons.schedulers.SchedulerProvider -import org.dhis2.data.dhislogic.inDateRange -import org.dhis2.data.dhislogic.inOrgUnit import org.hisp.dhis.android.core.category.CategoryOption import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.period.PeriodType import timber.log.Timber -import java.util.ArrayList class DataSetInitialPresenter( private val view: DataSetInitialContract.View, diff --git a/app/src/main/java/org/dhis2/usescases/development/DevelopmentActivity.java b/app/src/main/java/org/dhis2/usescases/development/DevelopmentActivity.java index e7f0664292..ce34945dc9 100644 --- a/app/src/main/java/org/dhis2/usescases/development/DevelopmentActivity.java +++ b/app/src/main/java/org/dhis2/usescases/development/DevelopmentActivity.java @@ -1,6 +1,7 @@ package org.dhis2.usescases.development; import android.os.Bundle; +import android.widget.Toast; import androidx.annotation.Nullable; import androidx.databinding.DataBindingUtil; @@ -11,10 +12,12 @@ import org.dhis2.R; import org.dhis2.commons.featureconfig.ui.FeatureConfigView; import org.dhis2.databinding.DevelopmentActivityBinding; -import org.dhis2.ui.dialogs.signature.SignatureDialog; import org.dhis2.usescases.general.ActivityGlobalAbstract; import org.hisp.dhis.android.core.D2; import org.hisp.dhis.android.core.D2Manager; +import org.hisp.dhis.android.core.common.ValueType; +import org.hisp.dhis.android.core.fileresource.FileResource; +import org.hisp.dhis.android.core.fileresource.FileResourceDomain; import java.io.BufferedReader; import java.io.IOException; @@ -25,8 +28,6 @@ import java.io.Writer; import java.util.List; -import kotlin.Unit; - public class DevelopmentActivity extends ActivityGlobalAbstract { private int count; @@ -41,8 +42,61 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { loadIconsDevTools(); loadCrashControl(); loadFeatureConfig(); - loadSignature(); loadConflicts(); + loadMultiText(); + loadCustomIcons(); + } + + private void loadCustomIcons() { + binding.forceCustomIcon.setOnClickListener(view -> { + D2 d2 = D2Manager.getD2(); + FileResource fileResource = d2.fileResourceModule().fileResources() + .byDomain().eq(FileResourceDomain.DATA_VALUE) + .one().blockingGet(); + if (fileResource != null) { + String uidToInsert = fileResource.uid(); + d2.databaseAdapter().execSQL( + String.format( + "INSERT INTO CustomIcon (\"key\", \"fileResourceUid\", \"href\") VALUES (\"%s\",\"%s\",\"%s\")", + uidToInsert, + uidToInsert, + uidToInsert + ) + ); + d2.databaseAdapter().execSQL( + String.format("UPDATE Program SET icon = \"%s\"", uidToInsert) + ); + d2.databaseAdapter().execSQL( + String.format("UPDATE DataSet SET icon = \"%s\"", uidToInsert) + ); + d2.databaseAdapter().execSQL( + String.format("UPDATE ProgramStage SET icon = \"%s\"", uidToInsert) + ); + d2.databaseAdapter().execSQL( + String.format("UPDATE Option SET icon = \"%s\"", uidToInsert) + ); + } else { + Toast.makeText(this, "No file resource found. Add an image in a form and retry", Toast.LENGTH_SHORT).show(); + } + + }); + } + + private void loadMultiText() { + D2 d2 = D2Manager.getD2(); + boolean hasMultiText = !d2.dataElementModule().dataElements().byValueType().eq(ValueType.MULTI_TEXT).blockingIsEmpty(); + binding.multitext.setText(hasMultiText ? "REVERT" : "FORCE MULTITEXT"); + binding.multitext.setOnClickListener(view -> { + if (hasMultiText) { + d2.databaseAdapter().execSQL( + "UPDATE DataElement SET valueType = \"TEXT\" WHERE valueType = \"MULTI_TEXT\" AND optionSet IS NOT null" + ); + } else { + d2.databaseAdapter().execSQL( + "UPDATE DataElement SET valueType = \"MULTI_TEXT\" WHERE valueType = \"TEXT\" AND optionSet IS NOT null" + ); + } + }); } private void loadConflicts() { @@ -189,13 +243,6 @@ private void loadFeatureConfig() { }); } - private void loadSignature() { - binding.signature.setOnClickListener(view -> { - new SignatureDialog("Signature", bitmap -> Unit.INSTANCE).show(getSupportFragmentManager(), "Signature"); - } - ); - } - @Override public void onBackPressed() { setResult(RESULT_OK); diff --git a/app/src/main/java/org/dhis2/usescases/development/RuleValidation.kt b/app/src/main/java/org/dhis2/usescases/development/RuleValidation.kt index 2bafc23c34..5ca8f80ea6 100644 --- a/app/src/main/java/org/dhis2/usescases/development/RuleValidation.kt +++ b/app/src/main/java/org/dhis2/usescases/development/RuleValidation.kt @@ -16,7 +16,7 @@ data class RuleValidation( val conditionError: String? = null, val actionsError: List? = null, ) { - fun uid() = rule?.uid() + fun uid() = rule?.uid fun hasError() = conditionError != null || actionsError != null fun errors(): List { return mutableListOf().apply { @@ -25,5 +25,5 @@ data class RuleValidation( } } - fun title() = rule?.name() ?: rule?.uid() ?: "-" + fun title() = rule?.name ?: rule?.uid ?: "-" } diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentActivity.kt b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentActivity.kt index 09696b45b0..e9f318a053 100644 --- a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentActivity.kt @@ -3,29 +3,26 @@ package org.dhis2.usescases.enrollment import android.app.Activity import android.content.Context import android.content.Intent -import android.graphics.Color import android.os.Bundle import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.databinding.DataBindingUtil -import com.bumptech.glide.Glide -import com.bumptech.glide.load.resource.bitmap.CircleCrop -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.dhis2.App import org.dhis2.R -import org.dhis2.commons.Constants import org.dhis2.commons.Constants.ENROLLMENT_UID import org.dhis2.commons.Constants.PROGRAM_UID import org.dhis2.commons.Constants.TEI_UID import org.dhis2.commons.data.TeiAttributesInfo -import org.dhis2.commons.dialogs.imagedetail.ImageDetailBottomDialog +import org.dhis2.commons.dialogs.imagedetail.ImageDetailActivity import org.dhis2.commons.featureconfig.data.FeatureConfigRepository import org.dhis2.commons.featureconfig.model.Feature +import org.dhis2.commons.resources.ResourceManager import org.dhis2.databinding.EnrollmentActivityBinding import org.dhis2.form.data.GeometryController import org.dhis2.form.data.GeometryParserImpl import org.dhis2.form.model.EnrollmentRecords +import org.dhis2.form.model.EventMode import org.dhis2.form.ui.FormView import org.dhis2.form.ui.provider.EnrollmentResultDialogUiProvider import org.dhis2.maps.views.MapSelectorActivity @@ -34,14 +31,11 @@ import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel import org.dhis2.ui.dialogs.bottomsheet.DialogButtonStyle import org.dhis2.usescases.events.ScheduledEventActivity import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity -import org.dhis2.usescases.eventsWithoutRegistration.eventInitial.EventInitialActivity import org.dhis2.usescases.general.ActivityGlobalAbstract import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity -import org.dhis2.utils.EventMode import org.dhis2.utils.granularsync.OPEN_ERROR_LOCATION import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.enrollment.EnrollmentStatus -import java.io.File import javax.inject.Inject class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { @@ -51,6 +45,9 @@ class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { private var forRelationship: Boolean = false private lateinit var formView: FormView + @Inject + lateinit var resourceManager: ResourceManager + @Inject lateinit var presenter: EnrollmentPresenterImpl @@ -195,35 +192,15 @@ class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { if (presenter.isEventScheduleOrSkipped(eventUid)) { val scheduleEventIntent = ScheduledEventActivity.getIntent(this, eventUid) openEventForResult.launch(scheduleEventIntent) - } else if (presenter.openInitial(eventUid)) { - val bundle = EventInitialActivity.getBundle( - presenter.getProgram()?.uid(), - eventUid, - null, - presenter.getEnrollment()!!.trackedEntityInstance(), - null, - presenter.getEnrollment()!!.organisationUnit(), - presenter.getEventStage(eventUid), - presenter.getEnrollment()!!.uid(), - 0, - presenter.getEnrollment()!!.status(), - ) - val eventInitialIntent = Intent(abstracContext, EventInitialActivity::class.java) - eventInitialIntent.putExtras(bundle) - startActivityForResult(eventInitialIntent, RQ_EVENT) } else { val eventCreationIntent = Intent(abstracContext, EventCaptureActivity::class.java) eventCreationIntent.putExtras( EventCaptureActivity.getActivityBundle( eventUid, presenter.getProgram()?.uid() ?: "", - EventMode.CHECK, + EventMode.NEW, ), ) - eventCreationIntent.putExtra( - Constants.TRACKED_ENTITY_INSTANCE, - presenter.getEnrollment()!!.trackedEntityInstance(), - ) startActivityForResult(eventCreationIntent, RQ_EVENT) } } @@ -312,49 +289,30 @@ class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { /*region TEI*/ override fun displayTeiInfo(teiInfo: TeiAttributesInfo) { if (mode != EnrollmentMode.NEW) { - binding.title.visibility = View.GONE - binding.teiDataHeader.root.visibility = View.VISIBLE - - binding.teiDataHeader.mainAttributes.apply { - text = teiInfo.teiMainLabel(getString(R.string.tracked_entity_type_details)) - setTextColor(Color.WHITE) - } - when (val secondaryLabel = teiInfo.teiSecondaryLabel()) { - null -> binding.teiDataHeader.secundaryAttribute.visibility = View.GONE - else -> { - binding.teiDataHeader.secundaryAttribute.text = secondaryLabel - binding.teiDataHeader.secundaryAttribute.setTextColor(Color.WHITE) - } - } - - if (teiInfo.profileImage.isEmpty()) { - binding.teiDataHeader.teiImage.visibility = View.GONE - binding.teiDataHeader.imageSeparator.visibility = View.GONE - } else { - Glide.with(this).load(File(teiInfo.profileImage)) - .transition(DrawableTransitionOptions.withCrossFade()) - .transform(CircleCrop()) - .into(binding.teiDataHeader.teiImage) - binding.teiDataHeader.teiImage.setOnClickListener { - presenter.onTeiImageHeaderClick() - } - } + binding.title.text = + resourceManager.defaultEnrollmentLabel( + programUid = presenter.getProgram()?.uid()!!, + true, + 1, + ) } else { - binding.title.visibility = View.VISIBLE - binding.teiDataHeader.root.visibility = View.GONE binding.title.text = - String.format(getString(R.string.enroll_in), presenter.getProgram()?.displayName()) + resourceManager.formatWithEnrollmentLabel( + programUid = presenter.getProgram()?.uid()!!, + R.string.new_enrollment, + 1, + ) } } override fun displayTeiPicture(picturePath: String) { - ImageDetailBottomDialog( - null, - File(picturePath), - ).show( - supportFragmentManager, - ImageDetailBottomDialog.TAG, + val intent = ImageDetailActivity.intent( + context = this, + title = null, + imagePath = picturePath, ) + + startActivity(intent) } /*endregion*/ /*region ACCESS*/ @@ -372,9 +330,6 @@ class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { binding.enrollmentStatus = status } - override fun showStatusOptions(currentStatus: EnrollmentStatus) { - } - /*endregion*/ override fun requestFocus() { diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepository.kt b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepository.kt index 19c0882ee8..b20dd134a7 100644 --- a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepository.kt @@ -1,14 +1,9 @@ package org.dhis2.usescases.enrollment -import io.reactivex.Flowable import io.reactivex.Single -import org.hisp.dhis.rules.RuleEngine -import org.hisp.dhis.rules.models.RuleEffect interface EnrollmentFormRepository { - fun ruleEngine(): Flowable - fun calculate(): Flowable>> fun generateEvents(): Single> fun getProfilePicture(): String fun getProgramStageUidFromEvent(eventUi: String): String? diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepositoryImpl.kt index d2463541ac..8ab48b2bfb 100644 --- a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepositoryImpl.kt @@ -1,36 +1,23 @@ package org.dhis2.usescases.enrollment -import io.reactivex.Flowable import io.reactivex.Single -import org.dhis2.bindings.blockingGetCheck import org.dhis2.bindings.profilePicturePath import org.dhis2.data.dhislogic.DhisEnrollmentUtils -import org.dhis2.form.bindings.toRuleAttributeValue -import org.dhis2.form.data.RulesRepository import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.repositories.`object`.ReadOnlyOneObjectRepositoryFinalImpl import org.hisp.dhis.android.core.enrollment.EnrollmentObjectRepository import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstanceObjectRepository -import org.hisp.dhis.rules.RuleEngine -import org.hisp.dhis.rules.RuleEngineContext -import org.hisp.dhis.rules.models.RuleAttributeValue -import org.hisp.dhis.rules.models.RuleEffect -import org.hisp.dhis.rules.models.RuleEnrollment -import org.hisp.dhis.rules.models.TriggerEnvironment class EnrollmentFormRepositoryImpl( val d2: D2, - rulesRepository: RulesRepository, - private val enrollmentRepository: EnrollmentObjectRepository, - private val programRepository: ReadOnlyOneObjectRepositoryFinalImpl, + enrollmentRepository: EnrollmentObjectRepository, + programRepository: ReadOnlyOneObjectRepositoryFinalImpl, teiRepository: TrackedEntityInstanceObjectRepository, private val enrollmentService: DhisEnrollmentUtils, ) : EnrollmentFormRepository { - private var cachedRuleEngineFlowable: Flowable - private var ruleEnrollmentBuilder: RuleEnrollment.Builder private var programUid: String = programRepository.blockingGet()?.uid() ?: throw NullPointerException() private var enrollmentUid: String = @@ -38,101 +25,10 @@ class EnrollmentFormRepositoryImpl( private val tei: TrackedEntityInstance = teiRepository.blockingGet() ?: throw NullPointerException() - init { - this.cachedRuleEngineFlowable = - Single.zip( - rulesRepository.rulesNew(programUid), - rulesRepository.ruleVariables(programUid), - rulesRepository.enrollmentEvents( - enrollmentRepository.blockingGet()?.uid() ?: "", - ), - rulesRepository.queryConstants(), - rulesRepository.supplementaryData( - enrollmentRepository.blockingGet()?.organisationUnit() ?: "", - ), - { rules, variables, events, constants, supplData -> - val builder = RuleEngineContext.builder() - .rules(rules) - .ruleVariables(variables) - .supplementaryData(supplData) - .constantsValue(constants) - .build().toEngineBuilder() - builder.triggerEnvironment(TriggerEnvironment.ANDROIDCLIENT) - builder.events(events) - builder.build() - }, - ).toFlowable() - .cacheWithInitialCapacity(1) - - this.ruleEnrollmentBuilder = RuleEnrollment.builder() - .enrollment(enrollmentRepository.blockingGet()?.uid()) - .incidentDate( - if (enrollmentRepository.blockingGet()?.incidentDate() == null) { - enrollmentRepository.blockingGet()?.enrollmentDate() - } else { - enrollmentRepository.blockingGet()?.incidentDate() - }, - ) - .enrollmentDate(enrollmentRepository.blockingGet()?.enrollmentDate()) - .status( - RuleEnrollment.Status.valueOf(enrollmentRepository.blockingGet()?.status()!!.name), - ) - .organisationUnit(enrollmentRepository.blockingGet()?.organisationUnit()) - .organisationUnitCode( - d2.organisationUnitModule().organisationUnits().uid( - enrollmentRepository.blockingGet()?.organisationUnit(), - ).blockingGet()?.code(), - ) - .programName(programRepository.blockingGet()?.displayName()) - } - - override fun ruleEngine(): Flowable { - return cachedRuleEngineFlowable - } - override fun generateEvents(): Single> { return Single.fromCallable { enrollmentService.generateEnrollmentEvents(enrollmentUid) } } - override fun calculate(): Flowable>> { - return queryAttributes() - .map { ruleEnrollmentBuilder.attributeValues(it).build() } - .switchMap { ruleEnrollment -> - ruleEngine().flatMap { ruleEngine -> - Flowable.fromCallable(ruleEngine.evaluate(ruleEnrollment)) - } - .map { - Result.success(it) - } - .onErrorReturn { - Result.failure(Exception(it)) - } - } - } - - private fun queryAttributes(): Flowable> { - return programRepository.get() - .map { program -> - d2.programModule().programTrackedEntityAttributes().byProgram().eq(program.uid()) - .blockingGet() - .filter { - d2.trackedEntityModule().trackedEntityAttributeValues() - .value( - it.trackedEntityAttribute()!!.uid(), - enrollmentRepository.blockingGet()?.trackedEntityInstance()!!, - ) - .blockingExists() - }.mapNotNull { - d2.trackedEntityModule().trackedEntityAttributeValues() - .value( - it.trackedEntityAttribute()!!.uid(), - enrollmentRepository.blockingGet()?.trackedEntityInstance()!!, - ) - .blockingGetCheck(d2, it.trackedEntityAttribute()!!.uid()) - }.toRuleAttributeValue(d2, program.uid()) - }.toFlowable() - } - override fun getProfilePicture() = tei.profilePicturePath(d2, programUid) override fun getProgramStageUidFromEvent(eventUi: String) = diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentModule.kt b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentModule.kt index 136a6dfff4..5f11242dfe 100644 --- a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentModule.kt +++ b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentModule.kt @@ -12,6 +12,8 @@ import org.dhis2.commons.network.NetworkUtils import org.dhis2.commons.prefs.PreferenceProviderImpl import org.dhis2.commons.reporting.CrashReportController import org.dhis2.commons.resources.ColorUtils +import org.dhis2.commons.resources.DhisPeriodUtils +import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.data.dhislogic.DhisEnrollmentUtils @@ -20,7 +22,7 @@ import org.dhis2.data.forms.dataentry.SearchTEIRepositoryImpl import org.dhis2.data.forms.dataentry.ValueStore import org.dhis2.data.forms.dataentry.ValueStoreImpl import org.dhis2.form.data.EnrollmentRepository -import org.dhis2.form.data.RulesRepository +import org.dhis2.form.data.metadata.EnrollmentConfiguration import org.dhis2.form.data.metadata.FileResourceConfiguration import org.dhis2.form.data.metadata.OptionSetConfiguration import org.dhis2.form.data.metadata.OrgUnitConfiguration @@ -87,11 +89,11 @@ class EnrollmentModule( d2: D2, modelFactory: FieldViewModelFactory, enrollmentFormLabelsProvider: EnrollmentFormLabelsProvider, + metadataIconProvider: MetadataIconProvider, ): EnrollmentRepository { return EnrollmentRepository( fieldFactory = modelFactory, - enrollmentUid = enrollmentUid, - d2 = d2, + conf = EnrollmentConfiguration(d2, enrollmentUid, metadataIconProvider), enrollmentMode = EnrollmentMode.valueOf(enrollmentMode.name), enrollmentFormLabelsProvider = enrollmentFormLabelsProvider, ) @@ -115,12 +117,12 @@ class EnrollmentModule( d2: D2, resourceManager: ResourceManager, colorUtils: ColorUtils, + periodUtils: DhisPeriodUtils, ): FieldViewModelFactory { return FieldViewModelFactoryImpl( - false, UiStyleProviderImpl( - FormUiModelColorFactoryImpl(activityContext, true, colorUtils), - LongTextUiColorFactoryImpl(activityContext, true, colorUtils), + FormUiModelColorFactoryImpl(activityContext, colorUtils), + LongTextUiColorFactoryImpl(activityContext, colorUtils), true, ), LayoutProviderImpl(), @@ -129,6 +131,7 @@ class EnrollmentModule( OptionSetConfiguration(d2), OrgUnitConfiguration(d2), FileResourceConfiguration(d2), + periodUtils, ), UiEventTypesProviderImpl(), KeyboardActionProviderImpl(), @@ -204,17 +207,10 @@ class EnrollmentModule( return SearchTEIRepositoryImpl(d2, DhisEnrollmentUtils(d2)) } - @Provides - @PerActivity - internal fun rulesRepository(d2: D2): RulesRepository { - return RulesRepository(d2) - } - @Provides @PerActivity fun formRepository( d2: D2, - rulesRepository: RulesRepository, enrollmentRepository: EnrollmentObjectRepository, programRepository: ReadOnlyOneObjectRepositoryFinalImpl, teiRepository: TrackedEntityInstanceObjectRepository, @@ -222,7 +218,6 @@ class EnrollmentModule( ): EnrollmentFormRepository { return EnrollmentFormRepositoryImpl( d2, - rulesRepository, enrollmentRepository, programRepository, teiRepository, diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentPresenterImpl.kt b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentPresenterImpl.kt index e0bfac9128..a87db3bff1 100644 --- a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentPresenterImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentPresenterImpl.kt @@ -19,7 +19,6 @@ import org.dhis2.utils.analytics.AnalyticsHelper import org.dhis2.utils.analytics.DELETE_AND_BACK import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.repositories.`object`.ReadOnlyOneObjectRepositoryFinalImpl -import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.common.Geometry import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.enrollment.Enrollment @@ -50,7 +49,6 @@ class EnrollmentPresenterImpl( private val eventCollectionRepository: EventCollectionRepository, private val teiAttributesProvider: TeiAttributesProvider, ) { - private var finishing: Boolean = false private val disposable = CompositeDisposable() private val backButtonProcessor: FlowableProcessor = PublishProcessor.create() private var hasShownIncidentDateEditionWarning = false @@ -168,6 +166,7 @@ class EnrollmentPresenterImpl( ), ) } + EnrollmentActivity.EnrollmentMode.CHECK -> view.setResultAndFinish() } } @@ -178,29 +177,12 @@ class EnrollmentPresenterImpl( view.showDateEditionWarning() } } - if (finishing) { - view.performSaveClick() - } - finishing = false } fun backIsClicked() { backButtonProcessor.onNext(true) } - fun openInitial(eventUid: String): Boolean { - val catComboUid = getProgram()?.categoryComboUid() - val event = d2.eventModule().events().uid(eventUid).blockingGet() - val stage = d2.programModule().programStages().uid(event?.programStage()).blockingGet() - val needsCatCombo = programRepository.blockingGet()?.categoryComboUid() != null && - d2.categoryModule().categoryCombos().uid(catComboUid) - .blockingGet()?.isDefault == false - val needsCoordinates = - stage?.featureType() != null && stage.featureType() != FeatureType.NONE - - return needsCatCombo || needsCoordinates - } - fun getEnrollment(): Enrollment? { return enrollmentObjectRepository.blockingGet() } @@ -224,8 +206,6 @@ class EnrollmentPresenterImpl( } } - fun hasAccess() = getProgram()?.access()?.data()?.write() ?: false - fun saveEnrollmentGeometry(geometry: Geometry?) { enrollmentObjectRepository.setGeometry(geometry) } @@ -258,13 +238,6 @@ class EnrollmentPresenterImpl( } } - fun setFinishing() { - finishing = true - } - - fun getEventStage(eventUid: String) = - enrollmentFormRepository.getProgramStageUidFromEvent(eventUid) - fun showOrHideSaveButton() { val teiUid = teiRepository.blockingGet()?.uid() ?: "" val programUid = getProgram()?.uid() ?: "" diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentView.kt b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentView.kt index bc0c10c7b0..9a953b3bd7 100644 --- a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentView.kt +++ b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentView.kt @@ -9,7 +9,6 @@ interface EnrollmentView : AbstractActivityContracts.View { fun setAccess(access: Boolean?) fun renderStatus(status: EnrollmentStatus) - fun showStatusOptions(currentStatus: EnrollmentStatus) fun setSaveButtonVisible(visible: Boolean) diff --git a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt index de8bc64028..28952bf12a 100644 --- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt @@ -4,32 +4,28 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View -import android.widget.DatePicker import androidx.compose.foundation.layout.Column +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.databinding.DataBindingUtil import org.dhis2.App import org.dhis2.R -import org.dhis2.commons.data.EventCreationType -import org.dhis2.commons.dialogs.calendarpicker.CalendarPicker -import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener +import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.dialogs.PeriodDialog import org.dhis2.databinding.ActivityEventScheduledBinding +import org.dhis2.form.model.EventMode import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventInputDateUiModel import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideInputDate -import org.dhis2.usescases.eventsWithoutRegistration.eventInitial.EventInitialActivity +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvidePeriodSelector +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.willShowCalendar import org.dhis2.usescases.general.ActivityGlobalAbstract -import org.dhis2.utils.DateUtils -import org.dhis2.utils.EventMode -import org.dhis2.utils.customviews.PeriodDialog import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.event.EventStatus import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramStage -import java.util.Calendar -import java.util.Date import javax.inject.Inject const val EXTRA_EVENT_UID = "EVENT_UID" @@ -98,7 +94,6 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract. override fun setStage(programStage: ProgramStage, event: Event) { this.stage = programStage binding.programStage = programStage - binding.scheduledEventFieldContainer.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { @@ -108,32 +103,73 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract. ?: getString(R.string.report_date), dateValue = "", ) - - ProvideInputDate( - EventInputDateUiModel( - eventDate = eventDate, - allowsManualInput = false, - detailsEnabled = true, - onDateClick = { setEvenDateListener(programStage.periodType()) }, - onDateSet = {}, - onClear = {}, - ), - + val dueDate = EventDate( + label = programStage.dueDateLabel() ?: getString(R.string.due_date), + dateValue = DateUtils.uiDateFormat().format(event.dueDate() ?: ""), ) - if (programStage.hideDueDate() == false) { - val dueDate = EventDate( - label = programStage.dueDateLabel() ?: getString(R.string.due_date), - dateValue = DateUtils.uiDateFormat().format(event.dueDate() ?: ""), - ) + + if (willShowCalendar(programStage.periodType())) { ProvideInputDate( EventInputDateUiModel( - eventDate = dueDate, + eventDate = eventDate, + allowsManualInput = false, + detailsEnabled = true, + onDateClick = {}, + onDateSelected = { date -> + presenter.setEventDate( + presenter.formatDateValues( + date, + ), + ) + }, + selectableDates = presenter.getSelectableDates(program, false), + ), + ) + + if (programStage.hideDueDate() == false) { + ProvideInputDate( + EventInputDateUiModel( + eventDate = dueDate, + allowsManualInput = false, + detailsEnabled = true, + onDateClick = {}, + onDateSelected = { date -> + presenter.setDueDate(presenter.formatDateValues(date)) + }, + selectableDates = presenter.getSelectableDates(program, true), + ), + ) + } + } else { + ProvidePeriodSelector( + uiModel = EventInputDateUiModel( + eventDate = eventDate, detailsEnabled = true, - onDateClick = { setDueDateListener(programStage.periodType()) }, - onDateSet = {}, - onClear = {}, + onDateClick = { showEventDatePeriodDialog(programStage.periodType()) }, + onDateSelected = {}, + onClear = { }, + required = true, + showField = true, + selectableDates = presenter.getSelectableDates(program, false), ), + modifier = Modifier, ) + + if (programStage.hideDueDate() == false) { + ProvidePeriodSelector( + uiModel = EventInputDateUiModel( + eventDate = dueDate, + detailsEnabled = true, + onDateClick = { showDueDatePeriodDialog(programStage.periodType()) }, + onDateSelected = {}, + onClear = { }, + required = true, + showField = true, + selectableDates = presenter.getSelectableDates(program, false), + ), + modifier = Modifier, + ) + } } } } @@ -145,10 +181,8 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract. binding.name = program.displayName() } - private fun setEvenDateListener(periodType: PeriodType?) { - if (periodType == null) { - showCustomCalendar(false) - } else { + private fun showEventDatePeriodDialog(periodType: PeriodType?) { + if (periodType != null) { var minDate = DateUtils.getInstance().expDate(null, program.expiryDays()!!, periodType) val lastPeriodDate = @@ -174,10 +208,8 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract. } } - private fun setDueDateListener(periodType: PeriodType?) { - if (periodType == null) { - showCustomCalendar(true) - } else { + private fun showDueDatePeriodDialog(periodType: PeriodType?) { + if (periodType != null) { var minDate = DateUtils.getInstance().expDate(null, program.expiryDays()!!, periodType) val lastPeriodDate = @@ -203,73 +235,11 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract. } } - private fun showCustomCalendar(isDueDate: Boolean) { - val dialog = CalendarPicker(this) - - if (isDueDate) { - dialog.setInitialDate(event.dueDate()) - dialog.setScheduleInterval(stage.standardInterval() ?: 0) - } - - if (program.expiryPeriodType() != null) { - val minDate = DateUtils.getInstance().expDate( - null, - program.expiryDays() ?: 0, - program.expiryPeriodType(), - ) - dialog.setMinDate(minDate) - } - - if (!isDueDate) { - dialog.setMaxDate(Date(System.currentTimeMillis() - 1000)) - } - dialog.setListener(object : OnDatePickerListener { - override fun onNegativeClick() { - dialog.dismiss() - } - - override fun onPositiveClick(datePicker: DatePicker) { - val date = Calendar.getInstance().apply { - set(Calendar.YEAR, datePicker.year) - set(Calendar.MONTH, datePicker.month) - set(Calendar.DAY_OF_MONTH, datePicker.dayOfMonth) - set(Calendar.HOUR, 0) - set(Calendar.MINUTE, 0) - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - }.time - if (isDueDate) { - presenter.setDueDate(date) - } else { - presenter.setEventDate(date) - } - } - }) - dialog.show() - } - - override fun openInitialActivity() { - val bundle = EventInitialActivity.getBundle( - program.uid(), - event.uid(), - EventCreationType.DEFAULT.name, - presenter.getEventTei(), - stage.periodType(), - presenter.getEnrollment()?.organisationUnit(), - stage.uid(), - event.enrollment(), - stage.standardInterval() ?: 0, - presenter.getEnrollment()?.status(), - ) - startActivity(Intent(this, EventInitialActivity::class.java).apply { putExtras(bundle) }) - finish() - } - override fun openFormActivity() { val bundle = EventCaptureActivity.getActivityBundle( event.uid(), program.uid(), - EventMode.CHECK, + EventMode.SCHEDULE, ) Intent(activity, EventCaptureActivity::class.java).apply { putExtras(bundle) diff --git a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt index 0bf7b898a4..dfc435ff34 100644 --- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt +++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt @@ -1,11 +1,13 @@ package org.dhis2.usescases.events +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.InputDateValues import org.dhis2.usescases.general.AbstractActivityContracts import org.hisp.dhis.android.core.category.CategoryOption import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramStage +import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates import java.util.Date class ScheduledEventContract { @@ -14,7 +16,6 @@ class ScheduledEventContract { fun setEvent(event: Event) fun setStage(programStage: ProgramStage, event: Event) fun setProgram(program: Program) - fun openInitialActivity() fun openFormActivity() } @@ -22,11 +23,14 @@ class ScheduledEventContract { fun init() fun finish() fun setEventDate(date: Date) + fun formatDateValues(date: InputDateValues): Date fun setDueDate(date: Date) + fun getDateFormatConfiguration(): String? fun skipEvent() fun setCatOptionCombo(catComboUid: String, arrayList: ArrayList) fun onBackClick() fun getEventTei(): String fun getEnrollment(): Enrollment? + fun getSelectableDates(program: Program, isDueDate: Boolean): SelectableDates? } } diff --git a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventModule.kt b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventModule.kt index 8f54a87090..ff27377572 100644 --- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventModule.kt +++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventModule.kt @@ -3,7 +3,6 @@ package org.dhis2.usescases.events import dagger.Module import dagger.Provides import org.dhis2.commons.di.dagger.PerActivity -import org.dhis2.data.dhislogic.DhisEventUtils import org.hisp.dhis.android.core.D2 @Module @@ -13,8 +12,7 @@ class ScheduledEventModule(val eventUid: String, val view: ScheduledEventContrac @PerActivity internal fun providePresenter( d2: D2, - eventUtils: DhisEventUtils, ): ScheduledEventContract.Presenter { - return ScheduledEventPresenterImpl(view, d2, eventUid, eventUtils) + return ScheduledEventPresenterImpl(view, d2, eventUid) } } diff --git a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt index 407a2991c5..fc8cebc39c 100644 --- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt @@ -3,20 +3,27 @@ package org.dhis2.usescases.events import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable -import org.dhis2.data.dhislogic.DhisEventUtils +import org.dhis2.commons.date.DateUtils +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.DEFAULT_MAX_DATE +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.DEFAULT_MIN_DATE +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.InputDateValues import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.helpers.UidsHelper import org.hisp.dhis.android.core.category.CategoryOption import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates import timber.log.Timber +import java.text.SimpleDateFormat +import java.util.Calendar import java.util.Date +import java.util.Locale class ScheduledEventPresenterImpl( val view: ScheduledEventContract.View, val d2: D2, val eventUid: String, - val eventUtils: DhisEventUtils, ) : ScheduledEventContract.Presenter { private lateinit var disposable: CompositeDisposable @@ -75,11 +82,33 @@ class ScheduledEventPresenterImpl( override fun setEventDate(date: Date) { d2.eventModule().events().uid(eventUid).setEventDate(date) d2.eventModule().events().uid(eventUid).setStatus(EventStatus.ACTIVE) - if (eventUtils.newEventNeedsExtraInfo(eventUid)) { - view.openInitialActivity() + view.openFormActivity() + } + + override fun formatDateValues(date: InputDateValues): Date { + val calendar = Calendar.getInstance() + calendar[date.year, date.month - 1, date.day, 0, 0] = 0 + calendar[Calendar.MILLISECOND] = 0 + return calendar.time + } + + override fun getDateFormatConfiguration(): String? { + return d2.systemInfoModule().systemInfo().blockingGet()?.dateFormat() + } + + override fun getSelectableDates(program: Program, isDueDate: Boolean): SelectableDates { + val minDate = if (program.expiryPeriodType() != null) { + DateUtils.getInstance().expDate( + null, + program.expiryDays() ?: 0, + program.expiryPeriodType(), + ) } else { - view.openFormActivity() + null } + val minDateString = if (minDate == null) null else SimpleDateFormat("ddMMyyyy", Locale.US).format(minDate) + val maxDateString = if (isDueDate) DEFAULT_MAX_DATE else SimpleDateFormat("ddMMyyyy", Locale.US).format(Date(System.currentTimeMillis() - 1000)) + return SelectableDates(minDateString ?: DEFAULT_MIN_DATE, maxDateString) } override fun setDueDate(date: Date) { diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/EventIdlingResourceSingleton.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/EventIdlingResourceSingleton.kt new file mode 100644 index 0000000000..1093cdfc8e --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/EventIdlingResourceSingleton.kt @@ -0,0 +1,24 @@ +package org.dhis2.usescases.eventsWithoutRegistration + +import androidx.test.espresso.idling.CountingIdlingResource + +object EventIdlingResourceSingleton { + private const val RESOURCE = "Event" + + @JvmField + val countingIdlingResource = CountingIdlingResource(RESOURCE) + + fun increment() { + if (countingIdlingResource.isIdleNow) { + countingIdlingResource.increment() + } + countingIdlingResource.dumpStateToLogs() + } + + fun decrement() { + if (!countingIdlingResource.isIdleNow) { + countingIdlingResource.decrement() + } + countingIdlingResource.dumpStateToLogs() + } +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt index f2cdb99aa2..0c58a25de5 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt @@ -13,7 +13,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import org.dhis2.R import org.dhis2.bindings.app @@ -22,8 +21,10 @@ import org.dhis2.commons.dialogs.AlertBottomDialog import org.dhis2.commons.dialogs.CustomDialog import org.dhis2.commons.dialogs.DialogClickListener import org.dhis2.commons.popupmenu.AppMenuHelper +import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.sync.SyncContext import org.dhis2.databinding.ActivityEventCaptureBinding +import org.dhis2.form.model.EventMode import org.dhis2.ui.ErrorFieldList import org.dhis2.ui.ThemeManager import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog @@ -38,31 +39,26 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.Even import org.dhis2.usescases.eventsWithoutRegistration.eventInitial.EventInitialActivity import org.dhis2.usescases.general.ActivityGlobalAbstract import org.dhis2.usescases.teiDashboard.dashboardfragments.relationships.MapButtonObservable -import org.dhis2.utils.EventMode import org.dhis2.utils.analytics.CLICK import org.dhis2.utils.analytics.DELETE_EVENT import org.dhis2.utils.analytics.SHOW_HELP import org.dhis2.utils.customviews.FormBottomDialog import org.dhis2.utils.customviews.FormBottomDialog.Companion.instance import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator -import org.dhis2.utils.customviews.navigationbar.setInitialPage import org.dhis2.utils.granularsync.OPEN_ERROR_LOCATION import org.dhis2.utils.granularsync.SyncStatusDialog import org.dhis2.utils.granularsync.shouldLaunchSyncDialog import javax.inject.Inject -const val EXTRA_DETAILS_AS_FIRST_PAGE = "EXTRA_DETAILS_AS_FIRST_PAGE" - class EventCaptureActivity : ActivityGlobalAbstract(), EventCaptureContract.View, MapButtonObservable, EventDetailsComponentProvider { - private var binding: ActivityEventCaptureBinding? = null + private lateinit var binding: ActivityEventCaptureBinding - @JvmField @Inject - var presenter: EventCaptureContract.Presenter? = null + override lateinit var presenter: EventCaptureContract.Presenter @JvmField @Inject @@ -74,6 +70,9 @@ class EventCaptureActivity : private var isEventCompleted = false private var eventMode: EventMode? = null + @Inject + lateinit var resourceManager: ResourceManager + @JvmField var eventCaptureComponent: EventCaptureComponent? = null var programUid: String? = null @@ -93,28 +92,22 @@ class EventCaptureActivity : themeManager!!.setProgramTheme(intent.getStringExtra(Constants.PROGRAM_UID)!!) super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_event_capture) - binding?.presenter = presenter - val navigationInitialPage = - if (intent.getBooleanExtra(EXTRA_DETAILS_AS_FIRST_PAGE, false)) { - 0 - } else { - 1 - } + binding.presenter = presenter eventMode = intent.getSerializableExtra(Constants.EVENT_MODE) as EventMode? - setUpViewPagerAdapter(navigationInitialPage) - setUpNavigationBar(navigationInitialPage) + setUpViewPagerAdapter() + setUpNavigationBar() showProgress() - presenter!!.initNoteCounter() - presenter!!.init() - binding?.syncButton?.setOnClickListener { showSyncDialog() } + presenter.initNoteCounter() + presenter.init() + binding.syncButton.setOnClickListener { showSyncDialog() } if (intent.shouldLaunchSyncDialog()) { showSyncDialog() } } - private fun setUpViewPagerAdapter(initialPage: Int) { - binding!!.eventViewPager.isUserInputEnabled = false + private fun setUpViewPagerAdapter() { + binding.eventViewPager.isUserInputEnabled = false adapter = EventCapturePagerAdapter( this, intent.getStringExtra(Constants.PROGRAM_UID), @@ -122,16 +115,16 @@ class EventCaptureActivity : pageConfigurator!!.displayAnalytics(), pageConfigurator!!.displayRelationships(), intent.getBooleanExtra(OPEN_ERROR_LOCATION, false), + eventMode, ) - binding!!.eventViewPager.adapter = adapter - binding!!.eventViewPager.setCurrentItem(initialPage, false) - binding!!.eventViewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() { + binding.eventViewPager.adapter = adapter + binding.eventViewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() { override fun onPageSelected(position: Int) { super.onPageSelected(position) if (position == 0 && eventMode !== EventMode.NEW) { - binding!!.syncButton.visibility = View.VISIBLE + binding.syncButton.visibility = View.VISIBLE } else { - binding!!.syncButton.visibility = View.GONE + binding.syncButton.visibility = View.GONE } if (position != 1) { hideProgress() @@ -140,17 +133,16 @@ class EventCaptureActivity : }) } - private fun setUpNavigationBar(initialPage: Int) { - binding!!.navigationBar.setInitialPage(initialPage) - binding!!.navigationBar.pageConfiguration(pageConfigurator!!) - binding!!.navigationBar.setOnNavigationItemSelectedListener { item: MenuItem -> - binding!!.eventViewPager.currentItem = adapter!!.getDynamicTabIndex(item.itemId) + private fun setUpNavigationBar() { + binding.navigationBar.pageConfiguration(pageConfigurator!!) + binding.navigationBar.setOnItemSelectedListener { item: MenuItem -> + binding.eventViewPager.currentItem = adapter!!.getDynamicTabIndex(item.itemId) true } } fun openDetails() { - binding?.navigationBar?.selectItemAt(0) + binding.navigationBar.selectItemAt(0) } fun openForm() { @@ -159,16 +151,16 @@ class EventCaptureActivity : it.dismiss() } } - binding?.navigationBar?.selectItemAt(1) + binding.navigationBar.selectItemAt(1) } override fun onResume() { super.onResume() - presenter!!.refreshTabCounters() + presenter.refreshTabCounters() } override fun onDestroy() { - presenter!!.onDettach() + presenter.onDettach() super.onDestroy() } @@ -185,7 +177,7 @@ class EventCaptureActivity : } private fun finishEditMode() { - if (binding!!.navigationBar.isHidden()) { + if (binding.navigationBar.isHidden()) { showNavigationBar() } else { attemptFinish() @@ -206,33 +198,29 @@ class EventCaptureActivity : { /*Unused*/ }, - { presenter!!.deleteEvent() }, + { presenter.deleteEvent() }, ) dialog.show(supportFragmentManager, AlertBottomDialog::class.java.simpleName) } else if (isFormScreen()) { - presenter?.emitAction(EventCaptureAction.ON_BACK) + presenter.emitAction(EventCaptureAction.ON_BACK) } else { finishDataEntry() } } private fun isFormScreen(): Boolean { - return adapter?.isFormScreenShown(binding?.eventViewPager?.currentItem) == true + return adapter?.isFormScreenShown(binding.eventViewPager.currentItem) == true } override fun updatePercentage(primaryValue: Float) { - binding!!.completion.setCompletionPercentage(primaryValue) - if (!presenter!!.completionPercentageVisibility) { - binding!!.completion.visibility = View.GONE + binding.completion.setCompletionPercentage(primaryValue) + if (!presenter.getCompletionPercentageVisibility()) { + binding.completion.visibility = View.GONE } } - override fun showCompleteActions( - canComplete: Boolean, - emptyMandatoryFields: Map, - eventCompletionDialog: EventCompletionDialog, - ) { - if (binding!!.navigationBar.selectedItemId == R.id.navigation_data_entry) { + override fun showCompleteActions(eventCompletionDialog: EventCompletionDialog) { + if (binding.navigationBar.selectedItemId == R.id.navigation_data_entry) { val dialog = BottomSheetDialog( bottomSheetDialogUiModel = eventCompletionDialog.bottomSheetDialogUiModel, onMainButtonClicked = { @@ -255,15 +243,15 @@ class EventCaptureActivity : } } - override fun SaveAndFinish() { + override fun saveAndFinish() { displayMessage(getString(R.string.saved)) setAction(FormBottomDialog.ActionType.FINISH) } override fun attemptToSkip() { instance - .setAccessDataWrite(presenter!!.canWrite()) - .setIsExpired(presenter!!.hasExpired()) + .setAccessDataWrite(presenter.canWrite()) + .setIsExpired(presenter.hasExpired()) .setSkip(true) .setListener { actionType: FormBottomDialog.ActionType -> setAction(actionType) } .show(supportFragmentManager, SHOW_OPTIONS) @@ -271,8 +259,8 @@ class EventCaptureActivity : override fun attemptToReschedule() { instance - .setAccessDataWrite(presenter!!.canWrite()) - .setIsExpired(presenter!!.hasExpired()) + .setAccessDataWrite(presenter.canWrite()) + .setIsExpired(presenter.hasExpired()) .setReschedule(true) .setListener { actionType: FormBottomDialog.ActionType -> setAction(actionType) } .show(supportFragmentManager, SHOW_OPTIONS) @@ -282,25 +270,31 @@ class EventCaptureActivity : when (actionType) { FormBottomDialog.ActionType.COMPLETE -> { isEventCompleted = true - presenter!!.completeEvent(false) + presenter.completeEvent(false) } - FormBottomDialog.ActionType.COMPLETE_ADD_NEW -> presenter!!.completeEvent(true) + + FormBottomDialog.ActionType.COMPLETE_ADD_NEW -> presenter.completeEvent(true) FormBottomDialog.ActionType.FINISH_ADD_NEW -> restartDataEntry() - FormBottomDialog.ActionType.SKIP -> presenter!!.skipEvent() + FormBottomDialog.ActionType.SKIP -> presenter.skipEvent() FormBottomDialog.ActionType.RESCHEDULE -> { // Do nothing } + FormBottomDialog.ActionType.CHECK_FIELDS -> { // Do nothing } + FormBottomDialog.ActionType.FINISH -> finishDataEntry() FormBottomDialog.ActionType.NONE -> { // Do nothing } } } - override fun showSnackBar(messageId: Int) { - val mySnackbar = - Snackbar.make(binding!!.root, messageId, BaseTransientBottomBar.LENGTH_SHORT) - mySnackbar.show() + override fun showSnackBar(messageId: Int, programStage: String) { + showToast( + resourceManager.formatWithEventLabel( + messageId, + programStage, + ), + ) } override fun restartDataEntry() { @@ -322,31 +316,16 @@ class EventCaptureActivity : finish() } - override fun renderInitialInfo( - stageName: String, - eventDate: String, - orgUnit: String, - catOption: String, - ) { - binding!!.programStageName.text = stageName - val eventDataString = StringBuilder(String.format("%s | %s", eventDate, orgUnit)) - if (catOption != null && !catOption.isEmpty()) { - eventDataString.append(String.format(" | %s", catOption)) - } - binding!!.eventSecundaryInfo.text = eventDataString - } - - override fun getPresenter(): EventCaptureContract.Presenter { - return presenter!! + override fun renderInitialInfo(stageName: String) { + binding.programStageName.text = stageName } override fun showMoreOptions(view: View) { AppMenuHelper.Builder().menu(this, R.menu.event_menu).anchor(view) .onMenuInflated { popupMenu: PopupMenu -> popupMenu.menu.findItem(R.id.menu_delete).isVisible = - presenter!!.canWrite() && presenter!!.isEnrollmentOpen + presenter.canWrite() && presenter.isEnrollmentOpen() popupMenu.menu.findItem(R.id.menu_share).isVisible = false - Unit } .onMenuItemClicked { itemId: Int? -> when (itemId) { @@ -354,6 +333,7 @@ class EventCaptureActivity : analyticsHelper().setEvent(SHOW_HELP, CLICK, SHOW_HELP) showTutorial(false) } + R.id.menu_delete -> confirmDeleteEvent() else -> { // Do nothing } @@ -369,59 +349,72 @@ class EventCaptureActivity : } private fun confirmDeleteEvent() { - CustomDialog( - this, - getString(R.string.delete_event), - getString(R.string.confirm_delete_event), - getString(R.string.delete), - getString(R.string.cancel), - 0, - object : DialogClickListener { - override fun onPositive() { - analyticsHelper().setEvent(DELETE_EVENT, CLICK, DELETE_EVENT) - presenter!!.deleteEvent() - } + presenter.programStage().let { + CustomDialog( + this, + resourceManager.formatWithEventLabel( + R.string.delete_event_label, + programStageUid = it, + ), + resourceManager.formatWithEventLabel( + R.string.confirm_delete_event_label, + programStageUid = it, + ), + getString(R.string.delete), + getString(R.string.cancel), + 0, + object : DialogClickListener { + override fun onPositive() { + analyticsHelper().setEvent(DELETE_EVENT, CLICK, DELETE_EVENT) + presenter.deleteEvent() + } - override fun onNegative() { - // dismiss - } - }, - ).show() + override fun onNegative() { + // dismiss + } + }, + ).show() + } } override fun showEventIntegrityAlert() { MaterialAlertDialogBuilder(this, R.style.DhisMaterialDialog) .setTitle(R.string.conflict) - .setMessage(R.string.event_date_in_future_message) + .setMessage( + resourceManager.formatWithEventLabel( + R.string.event_label_date_in_future_message, + programStageUid = presenter.programStage(), + ), + ) .setPositiveButton( R.string.change_event_date, - ) { _, _ -> binding!!.navigationBar.selectItemAt(0) } + ) { _, _ -> binding.navigationBar.selectItemAt(0) } .setNegativeButton(R.string.go_back) { _, _ -> back() } .setCancelable(false) .show() } override fun updateNoteBadge(numberOfNotes: Int) { - binding!!.navigationBar.updateBadge(R.id.navigation_notes, numberOfNotes) + binding.navigationBar.updateBadge(R.id.navigation_notes, numberOfNotes) } override fun showProgress() { - runOnUiThread { binding!!.toolbarProgress.show() } + runOnUiThread { binding.toolbarProgress.show() } } override fun hideProgress() { Handler(Looper.getMainLooper()).postDelayed( - { runOnUiThread { binding!!.toolbarProgress.hide() } }, + { runOnUiThread { binding.toolbarProgress.hide() } }, 1000, ) } override fun showNavigationBar() { - binding!!.navigationBar.show() + binding.navigationBar.show() } override fun hideNavigationBar() { - binding!!.navigationBar.hide() + binding.navigationBar.hide() } override fun relationshipMap(): LiveData { @@ -444,6 +437,14 @@ class EventCaptureActivity : SyncStatusDialog.Builder() .withContext(this) .withSyncContext(SyncContext.Event(eventUid!!)) + .onNoConnectionListener { + val contextView = findViewById(R.id.navigationBar) + Snackbar.make( + contextView, + R.string.sync_offline_check_connection, + Snackbar.LENGTH_SHORT, + ).show() + } .show("EVENT_SYNC") } @@ -463,13 +464,11 @@ class EventCaptureActivity : context: Context, eventUid: String, programUid: String, - openDetailsAsFirstPage: Boolean, eventMode: EventMode, ): Intent { return Intent(context, EventCaptureActivity::class.java).apply { putExtra(Constants.EVENT_UID, eventUid) putExtra(Constants.PROGRAM_UID, programUid) - putExtra(EXTRA_DETAILS_AS_FIRST_PAGE, openDetailsAsFirstPage) putExtra(Constants.EVENT_MODE, eventMode) } } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureContract.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureContract.java deleted file mode 100644 index d45ad8e5b3..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureContract.java +++ /dev/null @@ -1,152 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventCapture; - -import androidx.lifecycle.LiveData; - -import org.dhis2.ui.dialogs.bottomsheet.FieldWithIssue; -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.model.EventCompletionDialog; -import org.dhis2.usescases.general.AbstractActivityContracts; -import org.hisp.dhis.android.core.common.ValidationStrategy; -import org.hisp.dhis.android.core.event.EventStatus; -import org.hisp.dhis.android.core.organisationunit.OrganisationUnit; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Date; -import java.util.List; -import java.util.Map; - -import io.reactivex.Flowable; -import io.reactivex.Observable; -import io.reactivex.Single; - -public class EventCaptureContract { - - public interface View extends AbstractActivityContracts.View { - - void renderInitialInfo(String stageName, String eventDate, String orgUnit, String catOption); - - EventCaptureContract.Presenter getPresenter(); - - void updatePercentage(float primaryValue); - - void showCompleteActions( - boolean canComplete, - Map emptyMandatoryFields, - EventCompletionDialog eventCompletionDialog); - - void restartDataEntry(); - - void finishDataEntry(); - - void SaveAndFinish(); - - void showSnackBar(int messageId); - - void attemptToSkip(); - - void attemptToReschedule(); - - void showEventIntegrityAlert(); - - void updateNoteBadge(int numberOfNotes); - - void goBack(); - - void showProgress(); - - void hideProgress(); - - void showNavigationBar(); - - void hideNavigationBar(); - } - - public interface Presenter extends AbstractActivityContracts.Presenter { - - LiveData observeActions(); - - void init(); - - void onBackClick(); - - void attemptFinish(boolean canComplete, - @Nullable String onCompleteMessage, - List errorFields, - Map emptyMandatoryFields, - List warningFields); - - boolean isEnrollmentOpen(); - - void completeEvent(boolean addNew); - - void deleteEvent(); - - void skipEvent(); - - void rescheduleEvent(Date time); - - boolean canWrite(); - - boolean hasExpired(); - - void initNoteCounter(); - - void refreshTabCounters(); - - void hideProgress(); - - void showProgress(); - - boolean getCompletionPercentageVisibility(); - - void emitAction(@NotNull EventCaptureAction onBack); - } - - public interface EventCaptureRepository { - - Flowable eventIntegrityCheck(); - - Flowable programStageName(); - - Flowable eventDate(); - - Flowable orgUnit(); - - Flowable catOption(); - - Observable completeEvent(); - - Flowable eventStatus(); - - boolean isEnrollmentOpen(); - - Observable deleteEvent(); - - Observable updateEventStatus(EventStatus skipped); - - Observable rescheduleEvent(Date time); - - Observable programStage(); - - boolean getAccessDataWrite(); - - boolean isEnrollmentCancelled(); - - boolean isEventEditable(String eventUid); - - Single canReOpenEvent(); - - Observable isCompletedEventExpired(String eventUid); - - Single getNoteCount(); - - boolean showCompletionPercentage(); - - boolean hasAnalytics(); - - boolean hasRelationships(); - - ValidationStrategy validationStrategy(); - } - -} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureContract.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureContract.kt new file mode 100644 index 0000000000..91e9fff76c --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureContract.kt @@ -0,0 +1,89 @@ +package org.dhis2.usescases.eventsWithoutRegistration.eventCapture + +import androidx.lifecycle.LiveData +import io.reactivex.Flowable +import io.reactivex.Observable +import io.reactivex.Single +import org.dhis2.form.model.EventMode +import org.dhis2.ui.dialogs.bottomsheet.FieldWithIssue +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.model.EventCompletionDialog +import org.dhis2.usescases.general.AbstractActivityContracts +import org.hisp.dhis.android.core.common.ValidationStrategy +import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.organisationunit.OrganisationUnit +import java.util.Date + +class EventCaptureContract { + interface View : AbstractActivityContracts.View { + fun renderInitialInfo(stageName: String) + val presenter: Presenter + fun updatePercentage(primaryValue: Float) + fun showCompleteActions(eventCompletionDialog: EventCompletionDialog) + + fun restartDataEntry() + fun finishDataEntry() + fun saveAndFinish() + fun showSnackBar(messageId: Int, programStage: String) + fun attemptToSkip() + fun attemptToReschedule() + fun showEventIntegrityAlert() + fun updateNoteBadge(numberOfNotes: Int) + fun goBack() + fun showProgress() + fun hideProgress() + fun showNavigationBar() + fun hideNavigationBar() + } + + interface Presenter : AbstractActivityContracts.Presenter { + fun observeActions(): LiveData + fun init() + fun onBackClick() + fun attemptFinish( + canComplete: Boolean, + onCompleteMessage: String?, + errorFields: List, + emptyMandatoryFields: Map, + warningFields: List, + eventMode: EventMode? = null, + ) + + fun isEnrollmentOpen(): Boolean + fun completeEvent(addNew: Boolean) + fun deleteEvent() + fun skipEvent() + fun rescheduleEvent(time: Date) + fun canWrite(): Boolean + fun hasExpired(): Boolean + fun initNoteCounter() + fun refreshTabCounters() + fun hideProgress() + fun showProgress() + fun getCompletionPercentageVisibility(): Boolean + fun emitAction(onBack: EventCaptureAction) + fun programStage(): String + } + + interface EventCaptureRepository { + fun eventIntegrityCheck(): Flowable + fun programStageName(): Flowable + fun orgUnit(): Flowable + fun completeEvent(): Observable + fun eventStatus(): Flowable + val isEnrollmentOpen: Boolean + fun deleteEvent(): Observable + fun updateEventStatus(skipped: EventStatus): Observable + fun rescheduleEvent(time: Date): Observable + fun programStage(): Observable + val accessDataWrite: Boolean + val isEnrollmentCancelled: Boolean + fun isEventEditable(eventUid: String): Boolean + fun canReOpenEvent(): Single + fun isCompletedEventExpired(eventUid: String): Observable + val noteCount: Single + fun showCompletionPercentage(): Boolean + fun hasAnalytics(): Boolean + fun hasRelationships(): Boolean + fun validationStrategy(): ValidationStrategy + } +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureFieldProvider.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureFieldProvider.kt deleted file mode 100644 index b032f0f8ac..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureFieldProvider.kt +++ /dev/null @@ -1,271 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventCapture - -import io.reactivex.Flowable -import io.reactivex.processors.FlowableProcessor -import org.dhis2.bindings.blockingGetValueCheck -import org.dhis2.bindings.userFriendlyValue -import org.dhis2.commons.resources.ResourceManager -import org.dhis2.form.model.FieldUiModel -import org.dhis2.form.model.LegendValue -import org.dhis2.form.model.OptionSetConfiguration -import org.dhis2.form.model.RowAction -import org.dhis2.form.ui.FieldViewModelFactory -import org.hisp.dhis.android.core.D2 -import org.hisp.dhis.android.core.arch.helpers.UidsHelper.getUidsList -import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope -import org.hisp.dhis.android.core.common.FeatureType -import org.hisp.dhis.android.core.common.ObjectStyle -import org.hisp.dhis.android.core.common.ValueType -import org.hisp.dhis.android.core.dataelement.DataElement -import org.hisp.dhis.android.core.event.Event -import org.hisp.dhis.android.core.program.ProgramStageDataElement -import org.hisp.dhis.android.core.program.ProgramStageSection - -class EventCaptureFieldProvider( - private val d2: D2, - private val fieldFactory: FieldViewModelFactory, - private val resourceManager: ResourceManager, -) { - - fun provideEventFields( - event: Event, - programStageSections: List, - isEventEditable: Boolean, - actionProcessor: FlowableProcessor, - cachedFields: List, - ): Flowable> { - return if (cachedFields.isNotEmpty()) { - updateEventFields(event, cachedFields, isEventEditable) - } else { - provideEventFields(event, programStageSections, isEventEditable, actionProcessor) - } - } - - private fun provideEventFields( - event: Event, - programStageSections: List, - isEventEditable: Boolean, - actionProcessor: FlowableProcessor, - ): Flowable> { - return Flowable.just(sortedStageDataElements(event.programStage()!!)) - .flatMapIterable { list -> list } - .map { programStageDataElement -> - stageDataElementToFieldViewModel( - programStageDataElement, - event.uid(), - programStageSections, - isEventEditable, - actionProcessor, - ) - }.toList().toFlowable() - } - - private fun updateEventFields( - event: Event, - fields: List, - isEventEditable: Boolean, - ): Flowable> { - return Flowable.just(fields) - .flatMapIterable { list -> list } - .map { fieldViewModel -> - - val de = dataElement(fieldViewModel.uid) - - val (rawValue, friendlyValue) = dataValue( - event.uid(), - fieldViewModel.uid, - fieldViewModel.valueType == ValueType.ORGANISATION_UNIT, - ) - - val error = checkConflicts( - event.uid(), - fieldViewModel.uid, - rawValue, - ) - - val legend = if (fieldViewModel.legend != null) { - getColorByLegend(rawValue, de) - } else { - null - } - - val updatedFieldViewModel = fieldViewModel.setValue(friendlyValue) - .setEditable(fieldViewModel.editable || isEventEditable) - .setLegend(legend) - .apply { - if (error.isNotEmpty()) { - setError(error) - } else { - setError(null) - } - } - - updatedFieldViewModel - }.toList().toFlowable() - } - - private fun sortedStageDataElements(stageUid: String): List { - val stageDataElements = stageDataElements(stageUid) - val stageSections = stageSections(stageUid) - if (stageSections.isNotEmpty()) { - val dataElementsOrder = arrayListOf() - stageSections.forEach { section -> - dataElementsOrder.addAll(getUidsList(section.dataElements()!!)) - } - stageDataElements.toMutableList().sortWith( - Comparator { de1: ProgramStageDataElement, de2: ProgramStageDataElement -> - val pos1 = dataElementsOrder.indexOf(de1.dataElement()!!.uid()) - val pos2 = dataElementsOrder.indexOf(de2.dataElement()!!.uid()) - pos1.compareTo(pos2) - }, - ) - } - return stageDataElements - } - - private fun stageDataElementToFieldViewModel( - programStageDataElement: ProgramStageDataElement, - eventUid: String, - programStageSections: List, - isEventEditable: Boolean, - actionProcessor: FlowableProcessor, - ): FieldUiModel { - val de = dataElement(programStageDataElement.dataElement()!!.uid()) - - val programStageSection: ProgramStageSection? = - programStageSections.firstOrNull { section -> - getUidsList(section.dataElements()!!).contains(de?.uid()) - } - - val optionSet = de?.optionSetUid() - - val (rawValue, friendlyValue) = dataValue( - eventUid, - de?.uid() ?: "", - de?.valueType() == ValueType.ORGANISATION_UNIT, - ) - - val optionSetConfiguration = options(optionSet) - - val error: String = checkConflicts(eventUid, de?.uid() ?: "", rawValue) - - val fieldViewModel: FieldUiModel = - fieldFactory.create( - de?.uid() ?: "", - de?.formName() ?: de?.displayName() ?: "", - de?.valueType()!!, - programStageDataElement.compulsory() == true, - de.optionSetUid(), - friendlyValue, - programStageSection?.uid(), - programStageDataElement.allowFutureDate() == true, - isEventEditable, - programStageSection?.renderType()?.mobile()?.type(), - de.displayDescription(), - programStageDataElement.renderType()?.mobile(), - de.style() ?: ObjectStyle.builder().build(), - de.fieldMask(), - optionSetConfiguration, - FeatureType.POINT, - ) - - return if (error.isNotEmpty()) { - fieldViewModel.setError(error) - } else { - fieldViewModel - } - } - - private fun stageDataElements(stageUid: String) = d2.programModule().programStageDataElements() - .byProgramStage().eq(stageUid) - .withRenderType().blockingGet() - - private fun stageSections(stageUid: String) = d2.programModule().programStageSections() - .byProgramStageUid().eq(stageUid) - .withDataElements() - .blockingGet() - - private fun dataElement(dataElementUid: String) = d2.dataElementModule().dataElements() - .withLegendSets() - .uid(dataElementUid) - .blockingGet() - - private fun dataValue( - eventUid: String, - dataElementUid: String, - isValueTypeOrgUnit: Boolean, - ): Pair { - val valueRepository = d2.trackedEntityModule().trackedEntityDataValues() - .value(eventUid, dataElementUid) - return if (valueRepository.blockingExists()) { - val value = valueRepository.blockingGet()?.value() - var friendlyValue = - valueRepository.blockingGetValueCheck(d2, dataElementUid).userFriendlyValue(d2) - if (value != null && isValueTypeOrgUnit) { - friendlyValue = "%s_ou_%s".format(value, friendlyValue) - } - Pair(value, friendlyValue) - } else { - Pair(null, null) - } - } - - private fun checkConflicts(eventUid: String, dataElementUid: String, value: String?): String { - val conflicts = d2.importModule().trackerImportConflicts() - .byEventUid().eq(eventUid) - .blockingGet() - - return conflicts.firstOrNull { conflict -> - conflict.event() == eventUid && - conflict.dataElement() == dataElementUid && - conflict.value() == value - }?.displayDescription() ?: "" - } - - private fun getColorByLegend(value: String?, dataElement: DataElement?): LegendValue? { - return if (value == null || dataElement == null) { - null - } else { - try { - if (dataElement.valueType()!!.isNumeric && - dataElement.legendSets() != null && - dataElement.legendSets()!!.isNotEmpty() - ) { - val legendSet = dataElement.legendSets()!![0] - var legend = - d2.legendSetModule().legends() - .byStartValue().smallerThan(java.lang.Double.valueOf(value)) - .byEndValue().biggerThan(java.lang.Double.valueOf(value)) - .byLegendSet().eq(legendSet.uid()) - .one() - .blockingGet() - if (legend == null) { - legend = d2.legendSetModule().legends() - .byEndValue().eq(java.lang.Double.valueOf(value)) - .byLegendSet().eq(legendSet.uid()) - .one() - .blockingGet() - } - if (legend != null) { - return LegendValue( - resourceManager.getColorFrom(legend.color()), - legend.displayName(), - ) - } - } - null - } catch (e: Exception) { - null - } - } - } - - private fun options(optionSetUid: String?): OptionSetConfiguration? = optionSetUid?.let { - OptionSetConfiguration.config( - d2.optionModule().options().byOptionSetUid().eq(it).blockingCount(), - ) { - d2.optionModule().options().byOptionSetUid().eq(it) - .orderBySortOrder(RepositoryScope.OrderByDirection.ASC).blockingGet() - } - } -} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.java index 3f2f03514d..c7b9f31d8d 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.java @@ -16,9 +16,10 @@ import org.dhis2.data.dhislogic.DhisEnrollmentUtils; import org.dhis2.data.forms.EventRepository; import org.dhis2.data.forms.FormRepository; -import org.dhis2.data.forms.dataentry.RuleEngineRepository; import org.dhis2.data.forms.dataentry.SearchTEIRepository; import org.dhis2.data.forms.dataentry.SearchTEIRepositoryImpl; +import org.dhis2.mobileProgramRules.EvaluationType; +import org.dhis2.mobileProgramRules.RuleEngineHelper; import org.dhis2.form.data.FileController; import org.dhis2.form.data.FormValueStore; import org.dhis2.form.data.RulesRepository; @@ -81,8 +82,12 @@ RulesRepository rulesRepository(@NonNull D2 d2) { @Provides @PerActivity - RuleEngineRepository ruleEngineRepository(D2 d2, FormRepository formRepository) { - return new EventRuleEngineRepository(d2, formRepository, eventUid); + RuleEngineHelper ruleEngineRepository(D2 d2) { + if(eventUid == null) return null; + return new RuleEngineHelper( + new EvaluationType.Event(eventUid), + new org.dhis2.mobileProgramRules.RulesRepository(d2) + ); } @Provides @@ -107,6 +112,7 @@ FormValueStore valueStore( eventUid, EntryMode.DE, null, + null, crashReportController, networkUtils, resourceManager, diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.java index de871519f3..e8e044d83e 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.java @@ -1,7 +1,6 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventCapture; import static org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.IndicatorsFragmentKt.VISUALIZATION_TYPE; -import static org.dhis2.commons.Constants.PROGRAM_UID; import android.os.Bundle; @@ -12,35 +11,33 @@ import androidx.viewpager2.adapter.FragmentStateAdapter; import org.dhis2.R; +import org.dhis2.form.model.EventMode; import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventCaptureFragment.EventCaptureFormFragment; -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui.EventDetailsFragment; import org.dhis2.usescases.notes.NotesFragment; import org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.IndicatorsFragment; import org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.VisualizationType; import org.dhis2.usescases.teiDashboard.dashboardfragments.relationships.RelationshipFragment; -import org.dhis2.commons.Constants; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; -import kotlin.Unit; - public class EventCapturePagerAdapter extends FragmentStateAdapter { private final String programUid; private final String eventUid; private final List pages; - private EventCaptureFormFragment formFragment; private final boolean shouldOpenErrorSection; + private final EventMode eventMode; + public boolean isFormScreenShown(@Nullable Integer currentItem) { - return currentItem!=null && pages.get(currentItem) == EventPageType.DATA_ENTRY; + return currentItem != null && pages.get(currentItem) == EventPageType.DATA_ENTRY; } private enum EventPageType { - DETAILS, DATA_ENTRY, ANALYTICS, RELATIONSHIPS, NOTES + DATA_ENTRY, ANALYTICS, RELATIONSHIPS, NOTES } public EventCapturePagerAdapter(FragmentActivity fragmentActivity, @@ -48,15 +45,16 @@ public EventCapturePagerAdapter(FragmentActivity fragmentActivity, String eventUid, boolean displayAnalyticScreen, boolean displayRelationshipScreen, - boolean openErrorSection + boolean openErrorSection, + EventMode eventMode ) { super(fragmentActivity); this.programUid = programUid; this.eventUid = eventUid; this.shouldOpenErrorSection = openErrorSection; + this.eventMode = eventMode; pages = new ArrayList<>(); - pages.add(EventPageType.DETAILS); pages.add(EventPageType.DATA_ENTRY); if (displayAnalyticScreen) { @@ -70,9 +68,7 @@ public EventCapturePagerAdapter(FragmentActivity fragmentActivity, } public int getDynamicTabIndex(@IntegerRes int tabClicked) { - if (tabClicked == R.id.navigation_details) { - return pages.indexOf(EventPageType.DETAILS); - } else if (tabClicked == R.id.navigation_data_entry) { + if (tabClicked == R.id.navigation_data_entry) { return pages.indexOf(EventPageType.DATA_ENTRY); } else if (tabClicked == R.id.navigation_analytics) { return pages.indexOf(EventPageType.ANALYTICS); @@ -89,22 +85,12 @@ public int getDynamicTabIndex(@IntegerRes int tabClicked) { public Fragment createFragment(int position) { switch (pages.get(position)) { default: - case DETAILS: - Bundle bundle = new Bundle(); - bundle.putString(Constants.EVENT_UID, eventUid); - bundle.putString(PROGRAM_UID, programUid); - EventDetailsFragment eventDetailsFragment = new EventDetailsFragment(); - eventDetailsFragment.setArguments(bundle); - eventDetailsFragment.setOnEventReopened(() -> { - if (formFragment != null) { - formFragment.onReopen(); - } - return Unit.INSTANCE; - }); - return eventDetailsFragment; case DATA_ENTRY: - formFragment = EventCaptureFormFragment.newInstance(eventUid, shouldOpenErrorSection); - return formFragment; + return EventCaptureFormFragment.newInstance( + eventUid, + shouldOpenErrorSection, + eventMode + ); case ANALYTICS: Fragment indicatorFragment = new IndicatorsFragment(); Bundle arguments = new Bundle(); diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt index 048c540d06..cc317af7eb 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt @@ -7,16 +7,19 @@ import io.reactivex.Flowable import io.reactivex.disposables.CompositeDisposable import io.reactivex.processors.PublishProcessor import org.dhis2.R -import org.dhis2.bindings.canSkipErrorFix import org.dhis2.commons.prefs.Preference import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.schedulers.defaultSubscribe +import org.dhis2.form.data.EventRepository +import org.dhis2.form.model.EventMode import org.dhis2.ui.dialogs.bottomsheet.FieldWithIssue +import org.dhis2.usescases.eventsWithoutRegistration.EventIdlingResourceSingleton import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureContract.EventCaptureRepository import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain.ConfigureEventCompletionDialog import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.model.EventCaptureInitialInfo import org.hisp.dhis.android.core.common.Unit +import org.hisp.dhis.android.core.common.ValidationStrategy import org.hisp.dhis.android.core.event.EventStatus import timber.log.Timber import java.util.Date @@ -55,9 +58,7 @@ class EventCapturePresenterImpl( compositeDisposable.add( Flowable.zip( eventCaptureRepository.programStageName(), - eventCaptureRepository.eventDate(), eventCaptureRepository.orgUnit(), - eventCaptureRepository.catOption(), ::EventCaptureInitialInfo, ).defaultSubscribe( schedulerProvider, @@ -68,9 +69,6 @@ class EventCapturePresenterImpl( ) view.renderInitialInfo( initialInfo.programStageName, - initialInfo.eventDate, - initialInfo.organisationUnit.displayName(), - initialInfo.categoryOption, ) }, Timber::e, @@ -113,15 +111,21 @@ class EventCapturePresenterImpl( errorFields: List, emptyMandatoryFields: Map, warningFields: List, + eventMode: EventMode?, ) { val eventStatus = eventStatus if (eventStatus != EventStatus.ACTIVE) { setUpActionByStatus(eventStatus) } else { - val validationStrategy = eventCaptureRepository.validationStrategy() - val canSkipErrorFix = validationStrategy.canSkipErrorFix( + val canSkipErrorFix = canSkipErrorFix( hasErrorFields = errorFields.isNotEmpty(), hasEmptyMandatoryFields = emptyMandatoryFields.isNotEmpty(), + hasEmptyEventCreationMandatoryFields = with(emptyMandatoryFields) { + containsValue(EventRepository.EVENT_DETAILS_SECTION_UID) || + containsValue(EventRepository.EVENT_CATEGORY_COMBO_SECTION_UID) + }, + eventMode = eventMode, + validationStrategy = eventCaptureRepository.validationStrategy(), ) val eventCompletionDialog = configureEventCompletionDialog.invoke( errorFields, @@ -131,20 +135,32 @@ class EventCapturePresenterImpl( onCompleteMessage, canSkipErrorFix, ) - view.showCompleteActions( - canComplete && eventCaptureRepository.isEnrollmentOpen, - emptyMandatoryFields, - eventCompletionDialog, - ) + view.showCompleteActions(eventCompletionDialog) } view.showNavigationBar() } + private fun canSkipErrorFix( + hasErrorFields: Boolean, + hasEmptyMandatoryFields: Boolean, + hasEmptyEventCreationMandatoryFields: Boolean, + eventMode: EventMode?, + validationStrategy: ValidationStrategy, + ): Boolean { + return when (validationStrategy) { + ValidationStrategy.ON_COMPLETE -> when (eventMode) { + EventMode.NEW -> !hasEmptyEventCreationMandatoryFields + else -> true + } + ValidationStrategy.ON_UPDATE_AND_INSERT -> !hasErrorFields && !hasEmptyMandatoryFields + } + } + private fun setUpActionByStatus(eventStatus: EventStatus) { when (eventStatus) { EventStatus.COMPLETED -> if (!hasExpired && !eventCaptureRepository.isEnrollmentCancelled) { - view.SaveAndFinish() + view.saveAndFinish() } else { view.finishDataEntry() } @@ -162,11 +178,13 @@ class EventCapturePresenterImpl( } override fun completeEvent(addNew: Boolean) { + EventIdlingResourceSingleton.increment() compositeDisposable.add( eventCaptureRepository.completeEvent() .defaultSubscribe( schedulerProvider, - { + onNext = { + EventIdlingResourceSingleton.decrement() if (addNew) { view.restartDataEntry() } else { @@ -174,23 +192,35 @@ class EventCapturePresenterImpl( view.finishDataEntry() } }, - Timber::e, + onError = { + EventIdlingResourceSingleton.decrement() + Timber.e(it) + }, ), ) } override fun deleteEvent() { + val programStage = programStage() + EventIdlingResourceSingleton.increment() compositeDisposable.add( eventCaptureRepository.deleteEvent() .defaultSubscribe( schedulerProvider, - { result -> + onNext = { result -> + EventIdlingResourceSingleton.decrement() if (result) { - view.showSnackBar(R.string.event_was_deleted) + view.showSnackBar(R.string.event_label_was_deleted, programStage) } }, - Timber::e, - view::finishDataEntry, + onError = { + EventIdlingResourceSingleton.decrement() + Timber.e(it) + }, + onComplete = { + EventIdlingResourceSingleton.decrement() + view.finishDataEntry() + }, ), ) } @@ -200,7 +230,7 @@ class EventCapturePresenterImpl( eventCaptureRepository.updateEventStatus(EventStatus.SKIPPED) .defaultSubscribe( schedulerProvider, - { view.showSnackBar(R.string.event_was_skipped) }, + { view.showSnackBar(R.string.event_label_was_skipped, programStage()) }, Timber::e, view::finishDataEntry, ), @@ -268,4 +298,6 @@ class EventCapturePresenterImpl( private val eventStatus: EventStatus get() = eventCaptureRepository.eventStatus().blockingFirst() + + override fun programStage(): String = eventCaptureRepository.programStage().blockingFirst() } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java index 1f37e458a7..a27e7d0aeb 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java @@ -2,7 +2,6 @@ import org.dhis2.commons.bindings.SdkExtensionsKt; import org.dhis2.data.dhislogic.AuthoritiesKt; -import org.dhis2.utils.DateUtils; import org.hisp.dhis.android.core.D2; import org.hisp.dhis.android.core.common.BaseIdentifiableObject; import org.hisp.dhis.android.core.common.ValidationStrategy; @@ -70,31 +69,17 @@ public Flowable programStageName() { } @Override - public Flowable eventDate() { - Event currentEvent = getCurrentEvent(); + public Flowable orgUnit() { return Flowable.just( - currentEvent.eventDate() != null ? DateUtils.uiDateFormat().format(currentEvent.eventDate()) : "" + Objects.requireNonNull( + d2.organisationUnitModule() + .organisationUnits() + .uid(getCurrentEvent().organisationUnit()) + .blockingGet() + ) ); } - @Override - public Flowable orgUnit() { - return Flowable.just(d2.organisationUnitModule().organisationUnits().uid(getCurrentEvent().organisationUnit()).blockingGet()); - } - - - @Override - public Flowable catOption() { - return Flowable.just(d2.categoryModule().categoryOptionCombos().uid(getCurrentEvent().attributeOptionCombo())) - .map(categoryOptionComboRepo -> { - if (categoryOptionComboRepo.blockingGet() == null) - return ""; - else - return categoryOptionComboRepo.blockingGet().displayName(); - }) - .map(displayName -> displayName.equals("default") ? "" : displayName); - } - @Override public Observable completeEvent() { return Observable.fromCallable(() -> { @@ -160,8 +145,8 @@ public Single canReOpenEvent() { @Override public Observable isCompletedEventExpired(String eventUid) { return d2.eventModule().eventService().getEditableStatus(eventUid).map(editionStatus -> { - if (editionStatus instanceof EventEditableStatus.NonEditable) { - return ((EventEditableStatus.NonEditable) editionStatus).getReason() == EventNonEditableReason.EXPIRED; + if (editionStatus instanceof EventEditableStatus.NonEditable nonEditableStatus) { + return nonEditableStatus.getReason() == EventNonEditableReason.EXPIRED; } else { return false; } @@ -174,7 +159,7 @@ public Flowable eventIntegrityCheck() { return Flowable.just(currentEvent).map(event -> (event.status() == EventStatus.COMPLETED || event.status() == EventStatus.ACTIVE) && - event.eventDate() != null && !event.eventDate().after(new Date()) + (event.eventDate() == null || !event.eventDate().after(new Date())) ); } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventRuleEngineRepository.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventRuleEngineRepository.java deleted file mode 100644 index fca800ca8e..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventRuleEngineRepository.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventCapture; - -import androidx.annotation.NonNull; - -import org.dhis2.data.forms.FormRepository; -import org.dhis2.data.forms.dataentry.RuleEngineRepository; -import org.dhis2.form.bindings.RuleExtensionsKt; -import org.dhis2.utils.Result; -import org.hisp.dhis.android.core.D2; -import org.hisp.dhis.android.core.event.Event; -import org.hisp.dhis.android.core.organisationunit.OrganisationUnit; -import org.hisp.dhis.android.core.program.ProgramStage; -import org.hisp.dhis.rules.RuleEngine; -import org.hisp.dhis.rules.models.RuleDataValue; -import org.hisp.dhis.rules.models.RuleEffect; -import org.hisp.dhis.rules.models.RuleEvent; - -import java.util.List; - -import io.reactivex.Flowable; - -public final class EventRuleEngineRepository implements RuleEngineRepository { - - D2 d2; - FormRepository formRepository; - String eventUid; - private RuleEvent.Builder eventBuilder; - - public EventRuleEngineRepository(D2 d2, FormRepository formRepository, String eventUid) { - this.d2 = d2; - this.formRepository = formRepository; - this.eventUid = eventUid; - - initData(); - } - - public void initData() { - eventBuilder = RuleEvent.builder(); - if (eventUid != null) { - Event currentEvent = d2.eventModule().events().withTrackedEntityDataValues().uid(eventUid).blockingGet(); - ProgramStage currentStage = d2.programModule().programStages().uid(currentEvent.programStage()).blockingGet(); - OrganisationUnit ou = d2.organisationUnitModule().organisationUnits().uid(currentEvent.organisationUnit()).blockingGet(); - - eventBuilder - .event(currentEvent.uid()) - .programStage(currentEvent.programStage()) - .programStageName(currentStage.displayName()) - .status(RuleEvent.Status.valueOf(currentEvent.status().name())) - .eventDate(currentEvent.eventDate()) - .dueDate(currentEvent.dueDate() != null ? currentEvent.dueDate() : currentEvent.eventDate()) - .organisationUnit(currentEvent.organisationUnit()) - .organisationUnitCode(ou.code()); - } - } - - @Override - public Flowable updateRuleEngine() { - return this.formRepository.restartRuleEngine(); - } - - @NonNull - @Override - public Flowable> calculate() { - return queryDataValues(eventUid) - .switchMap(dataValues -> - formRepository.ruleEngine() - .flatMap(ruleEngine -> - Flowable.fromCallable( - ruleEngine.evaluate( - eventBuilder.dataValues(dataValues).build() - )) - .map(Result::success) - .onErrorReturn(error->Result.failure(new Exception(error))) - - ) - ); - } - - @NonNull - @Override - public Flowable> reCalculate() { - initData(); - return calculate(); - } - - @NonNull - private Flowable> queryDataValues(String eventUid) { - return d2.eventModule().events().uid(eventUid).get() - .flatMap(event -> d2.trackedEntityModule().trackedEntityDataValues().byEvent().eq(eventUid).byValue().isNotNull().get() - .map(values -> RuleExtensionsKt.toRuleDataValue(values, event, d2.dataElementModule().dataElements(), d2.programModule().programRuleVariables(), d2.optionModule().options()))).toFlowable(); - } -} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialog.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialog.kt index c85784aa50..3543b4641b 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialog.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialog.kt @@ -96,7 +96,7 @@ class ConfigureEventCompletionDialog( WARNING, SUCCESSFUL, -> EventCompletionButtons( - CompleteButton(), + CompleteButton, FormBottomDialog.ActionType.COMPLETE, ) } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ReOpenEventUseCase.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ReOpenEventUseCase.kt new file mode 100644 index 0000000000..6bda1c586f --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ReOpenEventUseCase.kt @@ -0,0 +1,23 @@ +package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain + +import kotlinx.coroutines.withContext +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.maintenance.D2Error + +class ReOpenEventUseCase( + private val dispatcher: DispatcherProvider, + private val d2: D2, +) { + suspend operator fun invoke( + eventUid: String, + ): Result = withContext(dispatcher.io()) { + try { + d2.eventModule().events().uid(eventUid).setStatus(EventStatus.ACTIVE) + Result.success(Unit) + } catch (error: D2Error) { + Result.failure(error) + } + } +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.java index f870d21b5a..131a52eac1 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.java @@ -1,6 +1,8 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventCaptureFragment; +import static org.dhis2.commons.Constants.EVENT_MODE; import static org.dhis2.commons.extensions.ViewExtensionsKt.closeKeyboard; +import static org.dhis2.usescases.eventsWithoutRegistration.eventCapture.ui.NonEditableReasonBlockKt.showNonEditableReasonMessage; import static org.dhis2.utils.granularsync.SyncStatusDialogNavigatorKt.OPEN_ERROR_LOCATION; import android.content.Context; @@ -20,6 +22,7 @@ import org.dhis2.commons.featureconfig.data.FeatureConfigRepository; import org.dhis2.commons.featureconfig.model.Feature; import org.dhis2.databinding.SectionSelectorFragmentBinding; +import org.dhis2.form.model.EventMode; import org.dhis2.form.model.EventRecords; import org.dhis2.form.ui.FormView; import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureAction; @@ -45,11 +48,16 @@ public class EventCaptureFormFragment extends FragmentGlobalAbstract implements private SectionSelectorFragmentBinding binding; private FormView formView; - public static EventCaptureFormFragment newInstance(String eventUid, Boolean openErrorSection) { + public static EventCaptureFormFragment newInstance( + String eventUid, + Boolean openErrorSection, + EventMode eventMode + ) { EventCaptureFormFragment fragment = new EventCaptureFormFragment(); Bundle args = new Bundle(); args.putString(Constants.EVENT_UID, eventUid); args.putBoolean(OPEN_ERROR_LOCATION, openErrorSection); + args.putString(EVENT_MODE, eventMode.name()); fragment.setArguments(args); return fragment; } @@ -68,6 +76,8 @@ public void onAttach(@NotNull Context context) { @Override public void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) { + String eventUid = getArguments().getString(Constants.EVENT_UID, ""); + EventMode eventMode = EventMode.valueOf(getArguments().getString(EVENT_MODE)); formView = new FormView.Builder() .locationProvider(locationProvider) .onLoadingListener(loading -> { @@ -87,11 +97,11 @@ public void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedI return Unit.INSTANCE; }) .onDataIntegrityResult(result -> { - presenter.handleDataIntegrityResult(result); + presenter.handleDataIntegrityResult(result, eventMode); return Unit.INSTANCE; }) .factory(activity.getSupportFragmentManager()) - .setRecords(new EventRecords(getArguments().getString(Constants.EVENT_UID))) + .setRecords(new EventRecords(eventUid, eventMode)) .openErrorLocation(getArguments().getBoolean(OPEN_ERROR_LOCATION, false)) .useComposeForm( featureConfig.isFeatureEnable(Feature.COMPOSE_FORMS) @@ -177,4 +187,22 @@ public void showSaveButton() { public void onReopen() { formView.reload(); } + + @Override + public void showNonEditableMessage(@NonNull String reason, boolean canBeReOpened) { + showNonEditableReasonMessage( + binding.editableReasonContainer, + reason, + canBeReOpened, + () -> { + presenter.reOpenEvent(); + return Unit.INSTANCE; + } + ); + } + + @Override + public void hideNonEditableMessage() { + binding.editableReasonContainer.removeAllViews(); + } } \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormModule.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormModule.kt index 125ceab3dc..e6fd091a46 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormModule.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormModule.kt @@ -3,7 +3,10 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventCaptureF import dagger.Module import dagger.Provides import org.dhis2.commons.di.dagger.PerFragment +import org.dhis2.commons.resources.ResourceManager import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureContract +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain.ReOpenEventUseCase +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.injection.EventDispatchers import org.hisp.dhis.android.core.D2 @Module @@ -17,12 +20,29 @@ class EventCaptureFormModule( fun providePresenter( activityPresenter: EventCaptureContract.Presenter, d2: D2, + resourceManager: ResourceManager, + reOpenEventUseCase: ReOpenEventUseCase, + eventDispatchers: EventDispatchers, ): EventCaptureFormPresenter { return EventCaptureFormPresenter( view, activityPresenter, d2, eventUid, + resourceManager, + reOpenEventUseCase, + eventDispatchers, ) } + + @Provides + @PerFragment + fun provideReOpenEventUseCase( + d2: D2, + eventDispatchers: EventDispatchers, + ) = ReOpenEventUseCase(eventDispatchers, d2) + + @Provides + @PerFragment + fun provideEventDispatchers() = EventDispatchers() } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormPresenter.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormPresenter.kt index 245b30c3f4..4b491b27c1 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormPresenter.kt @@ -1,23 +1,39 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventCaptureFragment +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.dhis2.R +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.data.dhislogic.AUTH_ALL +import org.dhis2.data.dhislogic.AUTH_UNCOMPLETE_EVENT import org.dhis2.form.data.DataIntegrityCheckResult import org.dhis2.form.data.FieldsWithErrorResult import org.dhis2.form.data.FieldsWithWarningResult import org.dhis2.form.data.MissingMandatoryResult import org.dhis2.form.data.NotSavedResult import org.dhis2.form.data.SuccessfulResult +import org.dhis2.form.model.EventMode +import org.dhis2.usescases.eventsWithoutRegistration.EventIdlingResourceSingleton import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureContract +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain.ReOpenEventUseCase import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.event.EventEditableStatus +import org.hisp.dhis.android.core.event.EventNonEditableReason +import org.hisp.dhis.android.core.event.EventStatus class EventCaptureFormPresenter( private val view: EventCaptureFormView, private val activityPresenter: EventCaptureContract.Presenter, private val d2: D2, private val eventUid: String, + private val resourceManager: ResourceManager, + private val reOpenEventUseCase: ReOpenEventUseCase, + private val dispatcherProvider: DispatcherProvider, ) { - fun handleDataIntegrityResult(result: DataIntegrityCheckResult) { + fun handleDataIntegrityResult(result: DataIntegrityCheckResult, eventMode: EventMode? = null) { when (result) { is FieldsWithErrorResult -> activityPresenter.attemptFinish( result.canComplete, @@ -25,21 +41,27 @@ class EventCaptureFormPresenter( result.fieldUidErrorList, result.mandatoryFields, result.warningFields, + eventMode, ) + is FieldsWithWarningResult -> activityPresenter.attemptFinish( result.canComplete, result.onCompleteMessage, emptyList(), emptyMap(), result.fieldUidWarningList, + eventMode, ) + is MissingMandatoryResult -> activityPresenter.attemptFinish( result.canComplete, result.onCompleteMessage, result.errorFields, result.mandatoryFields, result.warningFields, + eventMode, ) + is SuccessfulResult -> activityPresenter.attemptFinish( result.canComplete, result.onCompleteMessage, @@ -47,6 +69,7 @@ class EventCaptureFormPresenter( emptyMap(), emptyList(), ) + NotSavedResult -> { // Nothing to do in this case } @@ -56,10 +79,65 @@ class EventCaptureFormPresenter( fun showOrHideSaveButton() { val isEditable = d2.eventModule().eventService().getEditableStatus(eventUid = eventUid).blockingGet() - if (isEditable is EventEditableStatus.Editable) { - view.showSaveButton() - } else { - view.hideSaveButton() + + when (isEditable) { + is EventEditableStatus.Editable -> { + view.showSaveButton() + } + + is EventEditableStatus.NonEditable -> { + view.hideSaveButton() + configureNonEditableMessage(isEditable.reason) + } + } + } + + private fun configureNonEditableMessage(eventNonEditableReason: EventNonEditableReason) { + val (reason, canBeReOpened) = when (eventNonEditableReason) { + EventNonEditableReason.BLOCKED_BY_COMPLETION -> resourceManager.getString(R.string.blocked_by_completion) to canReopen() + EventNonEditableReason.EXPIRED -> resourceManager.getString(R.string.edition_expired) to false + EventNonEditableReason.NO_DATA_WRITE_ACCESS -> resourceManager.getString(R.string.edition_no_write_access) to false + EventNonEditableReason.EVENT_DATE_IS_NOT_IN_ORGUNIT_RANGE -> resourceManager.getString(R.string.event_date_not_in_orgunit_range) to false + EventNonEditableReason.NO_CATEGORY_COMBO_ACCESS -> resourceManager.getString(R.string.edition_no_catcombo_access) to false + EventNonEditableReason.ENROLLMENT_IS_NOT_OPEN -> resourceManager.formatWithEnrollmentLabel( + d2.eventModule().events().uid(eventUid).blockingGet()?.program(), + R.string.edition_enrollment_is_no_open_V2, + 1, + ) to false + + EventNonEditableReason.ORGUNIT_IS_NOT_IN_CAPTURE_SCOPE -> resourceManager.getString(R.string.edition_orgunit_capture_scope) to false + } + view.showNonEditableMessage(reason, canBeReOpened) + } + + fun reOpenEvent() { + EventIdlingResourceSingleton.increment() + CoroutineScope(dispatcherProvider.ui()).launch { + reOpenEventUseCase(eventUid).fold( + onSuccess = { + view.onReopen() + view.showSaveButton() + view.hideNonEditableMessage() + EventIdlingResourceSingleton.decrement() + }, + onFailure = { error -> + resourceManager.parseD2Error(error) + EventIdlingResourceSingleton.decrement() + }, + ) } } + + private fun canReopen(): Boolean = getEvent()?.let { + it.status() == EventStatus.COMPLETED && hasReopenAuthority() + } ?: false + + private fun getEvent(): Event? { + return d2.eventModule().events().uid(eventUid).blockingGet() + } + + private fun hasReopenAuthority(): Boolean = d2.userModule().authorities() + .byName().`in`(AUTH_UNCOMPLETE_EVENT, AUTH_ALL) + .one() + .blockingExists() } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormView.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormView.kt index 7563836259..faf19d9935 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormView.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormView.kt @@ -5,4 +5,7 @@ interface EventCaptureFormView { fun hideSaveButton() fun showSaveButton() fun onReopen() + fun showNonEditableMessage(reason: String, canBeReOpened: Boolean) + fun hideNonEditableMessage() + fun displayMessage(errorMessage: String) } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventInitialFragment/EventCaptureInitialComponent.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventInitialFragment/EventCaptureInitialComponent.kt deleted file mode 100644 index c2dc482ebc..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventInitialFragment/EventCaptureInitialComponent.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventInitialFragment - -import dagger.Subcomponent - -@Subcomponent(modules = [EventCaptureInitialModule::class]) -interface EventCaptureInitialComponent { - fun inject(fragment: EventCaptureInitialFragment) -} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventInitialFragment/EventCaptureInitialFragment.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventInitialFragment/EventCaptureInitialFragment.kt deleted file mode 100644 index ee69557db7..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventInitialFragment/EventCaptureInitialFragment.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventInitialFragment - -import org.dhis2.usescases.general.FragmentGlobalAbstract - -class EventCaptureInitialFragment : FragmentGlobalAbstract() diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventInitialFragment/EventCaptureInitialModule.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventInitialFragment/EventCaptureInitialModule.kt deleted file mode 100644 index 3a5d2ef621..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventInitialFragment/EventCaptureInitialModule.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventInitialFragment - -import dagger.Module - -@Module -class EventCaptureInitialModule diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/injection/EventDispatchers.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/injection/EventDispatchers.kt new file mode 100644 index 0000000000..81a4a8c295 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/injection/EventDispatchers.kt @@ -0,0 +1,12 @@ +package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.injection + +import kotlinx.coroutines.Dispatchers +import org.dhis2.commons.viewmodel.DispatcherProvider + +class EventDispatchers : DispatcherProvider { + override fun io() = Dispatchers.IO + + override fun computation() = Dispatchers.Default + + override fun ui() = Dispatchers.Main +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/model/EventCaptureInitialInfo.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/model/EventCaptureInitialInfo.kt index cb20466399..aee1cf565d 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/model/EventCaptureInitialInfo.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/model/EventCaptureInitialInfo.kt @@ -4,7 +4,5 @@ import org.hisp.dhis.android.core.organisationunit.OrganisationUnit data class EventCaptureInitialInfo( val programStageName: String, - val eventDate: String, val organisationUnit: OrganisationUnit, - val categoryOption: String, ) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/ui/NonEditableReasonBlock.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/ui/NonEditableReasonBlock.kt new file mode 100644 index 0000000000..096d468a1a --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/ui/NonEditableReasonBlock.kt @@ -0,0 +1,103 @@ +package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.LockOpen +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import org.dhis2.R +import org.hisp.dhis.mobile.ui.designsystem.component.Button +import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor + +@Composable +fun NonEditableReasonBlock( + reason: String, + canBeReopened: Boolean, + onReopenClick: () -> Unit, +) { + Box( + modifier = Modifier + .background(SurfaceColor.Container), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(Spacing.Spacing10), + modifier = Modifier + .padding( + start = Spacing.Spacing16, + top = Spacing.Spacing8, + end = Spacing.Spacing16, + bottom = Spacing.Spacing8, + ), + ) { + Text( + text = reason, + color = TextColor.OnSurfaceVariant, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + ) + if (canBeReopened) { + Button( + modifier = Modifier + .fillMaxWidth() + .testTag("REOPEN_BUTTON"), + text = stringResource(id = R.string.re_open_to_edit), + style = ButtonStyle.FILLED, + icon = { + Icon( + imageVector = Icons.Outlined.LockOpen, + contentDescription = stringResource(id = R.string.re_open_to_edit), + tint = TextColor.OnPrimary, + ) + }, + ) { + onReopenClick() + } + } + } + } +} + +@Preview +@Composable +fun NonEditableReasonBlockPreview() { + NonEditableReasonBlock( + reason = "This data is not editable because it is marked as completed.", + canBeReopened = true, + onReopenClick = { + }, + ) +} + +fun showNonEditableReasonMessage( + composeView: ComposeView, + reason: String, + canBeReopened: Boolean, + onReopenClick: () -> Unit, +) { + composeView.setContent { + NonEditableReasonBlock( + reason = reason, + canBeReopened = canBeReopened, + onReopenClick = { + onReopenClick() + }, + ) + } +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/data/EventDetailsRepository.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/data/EventDetailsRepository.kt index 0029f6ac3d..a70ef1c12c 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/data/EventDetailsRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/data/EventDetailsRepository.kt @@ -1,11 +1,14 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.data import io.reactivex.Observable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.dhis2.commons.data.EventCreationType +import org.dhis2.commons.date.DateUtils import org.dhis2.data.dhislogic.AUTH_ALL import org.dhis2.data.dhislogic.AUTH_UNCOMPLETE_EVENT import org.dhis2.form.model.FieldUiModel import org.dhis2.form.ui.FieldViewModelFactory -import org.dhis2.utils.DateUtils import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope import org.hisp.dhis.android.core.category.CategoryCombo @@ -17,6 +20,7 @@ import org.hisp.dhis.android.core.common.ObjectStyle import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.event.Event +import org.hisp.dhis.android.core.event.EventCreateProjection import org.hisp.dhis.android.core.event.EventEditableStatus import org.hisp.dhis.android.core.event.EventObjectRepository import org.hisp.dhis.android.core.event.EventStatus @@ -24,6 +28,7 @@ import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramStage +import java.util.Calendar import java.util.Date class EventDetailsRepository( @@ -31,7 +36,8 @@ class EventDetailsRepository( private val programUid: String, private val eventUid: String?, private val programStageUid: String?, - private val fieldFactory: FieldViewModelFactory, + private val fieldFactory: FieldViewModelFactory?, + private val eventCreationType: EventCreationType, private val onError: (Throwable) -> String?, ) { @@ -42,6 +48,10 @@ class EventDetailsRepository( .blockingGet() } + fun getDateFormatConfiguration(): String? { + return d2.systemInfoModule().systemInfo().blockingGet()?.dateFormat() + } + fun getObjectStyle(): ObjectStyle? { val programStage = getProgramStage() val program = getProgram() @@ -140,10 +150,15 @@ class EventDetailsRepository( return d2.organisationUnitModule().organisationUnits() .byProgramUids(listOf(programUid)) .byParentUid().eq(parentUid) - .byOrganisationUnitScope(OrganisationUnit.Scope.SCOPE_DATA_CAPTURE) + .byOrganisationUnitScope(getOrgUnitScope()) .blockingGet() } + private fun getOrgUnitScope() = when (eventCreationType) { + EventCreationType.REFERAL -> OrganisationUnit.Scope.SCOPE_TEI_SEARCH + else -> OrganisationUnit.Scope.SCOPE_DATA_CAPTURE + } + fun getOrganisationUnit(orgUnitUid: String): OrganisationUnit? { return d2.organisationUnitModule().organisationUnits() .byUid() @@ -152,10 +167,18 @@ class EventDetailsRepository( } fun getOrganisationUnits(): List { - return d2.organisationUnitModule().organisationUnits() - .byOrganisationUnitScope(OrganisationUnit.Scope.SCOPE_DATA_CAPTURE) - .byProgramUids(listOf(programUid)) - .blockingGet() + val scope = getOrgUnitScope() + return when (scope) { + OrganisationUnit.Scope.SCOPE_DATA_CAPTURE -> + d2.organisationUnitModule().organisationUnits() + .byOrganisationUnitScope(OrganisationUnit.Scope.SCOPE_DATA_CAPTURE) + .byProgramUids(listOf(programUid)) + .blockingGet() + OrganisationUnit.Scope.SCOPE_TEI_SEARCH -> + d2.organisationUnitModule().organisationUnits() + .byProgramUids(listOf(programUid)) + .blockingGet() + } } fun getGeometryModel(): FieldUiModel { @@ -173,7 +196,7 @@ class EventDetailsRepository( d2.eventModule().events().uid(eventUid).blockingGet()?.geometry()?.coordinates() } - return fieldFactory.create( + return fieldFactory!!.create( id = "", label = "", valueType = ValueType.COORDINATE, @@ -325,4 +348,33 @@ class EventDetailsRepository( ), ) } + + fun scheduleEvent( + enrollmentUid: String?, + dueDate: Date, + orgUnitUid: String?, + categoryOptionComboUid: String?, + ): Flow = flow { + val cal = Calendar.getInstance() + cal.time = dueDate + cal[Calendar.HOUR_OF_DAY] = 0 + cal[Calendar.MINUTE] = 0 + cal[Calendar.SECOND] = 0 + cal[Calendar.MILLISECOND] = 0 + + val uid = d2.eventModule().events().blockingAdd( + EventCreateProjection.builder() + .enrollment(enrollmentUid) + .program(programUid) + .programStage(programStageUid) + .organisationUnit(orgUnitUid) + .attributeOptionCombo(categoryOptionComboUid) + .build(), + ) + val eventRepository = d2.eventModule().events().uid(uid) + eventRepository.setDueDate(cal.time) + eventRepository.setStatus(EventStatus.SCHEDULE) + + emit(uid) + } } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventCatCombo.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventCatCombo.kt index 093f8a971b..0eac233e8c 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventCatCombo.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventCatCombo.kt @@ -19,9 +19,9 @@ class ConfigureEventCatCombo( repository.catCombo().apply { val categories = getCategories(this?.categories()) val categoryOptions = getCategoryOptions() - val catComboUid = getCatComboUid(this?.uid() ?: "", this?.isDefault ?: false) - val catComboDisplayName = getCatComboDisplayName(this?.uid() ?: "") updateSelectedOptions(categoryOption, categories, categoryOptions) + val catComboUid = getCatOptionComboUid(this?.uid() ?: "", this?.isDefault ?: false) + val catComboDisplayName = getCatComboDisplayName(this?.uid() ?: "") return flowOf( EventCatCombo( @@ -53,7 +53,7 @@ class ConfigureEventCatCombo( } } - private fun getCatComboUid(categoryComboUid: String, isDefault: Boolean): String? { + private fun getCatOptionComboUid(categoryComboUid: String, isDefault: Boolean): String? { if (isDefault) { return repository.getCatOptionCombos( categoryComboUid, diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetails.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetails.kt index e2ad0490f2..4026469dd4 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetails.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetails.kt @@ -4,6 +4,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import org.dhis2.commons.data.EventCreationType import org.dhis2.commons.data.EventCreationType.REFERAL +import org.dhis2.commons.resources.MetadataIconProvider +import org.dhis2.ui.toColor import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.data.EventDetailsRepository import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider @@ -13,6 +15,7 @@ import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.event.EventEditableStatus.Editable import org.hisp.dhis.android.core.event.EventEditableStatus.NonEditable import org.hisp.dhis.android.core.event.EventStatus.OVERDUE +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import java.util.Date class ConfigureEventDetails( @@ -20,6 +23,7 @@ class ConfigureEventDetails( private val resourcesProvider: EventDetailResourcesProvider, private val creationType: EventCreationType, private val enrollmentStatus: EnrollmentStatus?, + private val metadataIconProvider: MetadataIconProvider, ) { operator fun invoke( @@ -38,11 +42,17 @@ class ConfigureEventDetails( ) val storedEvent = repository.getEvent() val programStage = repository.getProgramStage() + val program = repository.getProgram() return flowOf( EventDetails( name = programStage?.displayName(), description = programStage?.displayDescription(), - style = repository.getObjectStyle(), + metadataIconData = programStage?.style()?.let { + metadataIconProvider( + programStage.style(), + program?.style()?.color()?.toColor() ?: SurfaceColor.Primary, + ) + }, enabled = isEnable(storedEvent), isEditable = isEditable(), editableReason = getEditableReason(), diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDate.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDate.kt index f410379261..c464e213ab 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDate.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDate.kt @@ -7,7 +7,7 @@ import org.dhis2.commons.data.EventCreationType.ADDNEW import org.dhis2.commons.data.EventCreationType.DEFAULT import org.dhis2.commons.data.EventCreationType.SCHEDULE import org.dhis2.commons.date.DateUtils -import org.dhis2.data.dhislogic.DhisPeriodUtils +import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.data.EventDetailsRepository import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider @@ -44,10 +44,7 @@ class ConfigureEventReportDate( } private fun isActive(): Boolean { - if (creationType == SCHEDULE && getProgramStage()?.hideDueDate() == true) { - return false - } - return true + return !(creationType == SCHEDULE && getProgramStage()?.hideDueDate() == true) } private fun getLabel(): String { @@ -55,6 +52,7 @@ class ConfigureEventReportDate( return when (creationType) { SCHEDULE -> programStage?.dueDateLabel() ?: resourceProvider.provideDueDate() + else -> { programStage?.executionDateLabel() ?: resourceProvider.provideEventDate() } @@ -73,6 +71,7 @@ class ConfigureEventReportDate( when { periodType != null -> periodUtils.getPeriodUIString(periodType, date, Locale.getDefault()) + else -> DateUtils.uiDateFormat().format(date) } } @@ -87,7 +86,7 @@ class ConfigureEventReportDate( } else { val calendar = DateUtils.getInstance().calendar calendar.add(DAY_OF_YEAR, getScheduleInterval()) - org.dhis2.utils.DateUtils.getInstance().getNextPeriod( + DateUtils.getInstance().getNextPeriod( null, calendar.time, 0, @@ -132,19 +131,19 @@ class ConfigureEventReportDate( if (periodType == null) { if (program.expiryPeriodType() != null) { val expiryDays = program.expiryDays() ?: 0 - return org.dhis2.utils.DateUtils.getInstance().expDate( + return DateUtils.getInstance().expDate( null, expiryDays, program.expiryPeriodType(), ) } } else { - var minDate = org.dhis2.utils.DateUtils.getInstance().expDate( + var minDate = DateUtils.getInstance().expDate( null, program.expiryDays() ?: 0, periodType, ) - val lastPeriodDate = org.dhis2.utils.DateUtils.getInstance().getNextPeriod( + val lastPeriodDate = DateUtils.getInstance().getNextPeriod( periodType, minDate, -1, @@ -152,14 +151,14 @@ class ConfigureEventReportDate( ) if (lastPeriodDate.after( - org.dhis2.utils.DateUtils.getInstance().getNextPeriod( + DateUtils.getInstance().getNextPeriod( program.expiryPeriodType(), minDate, 0, ), ) ) { - minDate = org.dhis2.utils.DateUtils.getInstance() + minDate = DateUtils.getInstance() .getNextPeriod(periodType, lastPeriodDate, 0) } return minDate @@ -174,6 +173,7 @@ class ConfigureEventReportDate( ADDNEW, DEFAULT, -> Date(System.currentTimeMillis() - 1000) + else -> null } } else { @@ -181,6 +181,7 @@ class ConfigureEventReportDate( ADDNEW, DEFAULT, -> DateUtils.getInstance().today + else -> null } } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt index c1f6ff7c02..daf4ee8309 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt @@ -6,12 +6,12 @@ import dagger.Provides import org.dhis2.commons.data.EventCreationType import org.dhis2.commons.di.dagger.PerFragment import org.dhis2.commons.locationprovider.LocationProvider -import org.dhis2.commons.network.NetworkUtils import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.prefs.PreferenceProviderImpl import org.dhis2.commons.resources.ColorUtils +import org.dhis2.commons.resources.DhisPeriodUtils +import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager -import org.dhis2.data.dhislogic.DhisPeriodUtils import org.dhis2.form.data.GeometryController import org.dhis2.form.data.GeometryParserImpl import org.dhis2.form.data.metadata.FileResourceConfiguration @@ -61,7 +61,7 @@ class EventDetailsModule( fun provideEventDetailResourceProvider( resourceManager: ResourceManager, ): EventDetailResourcesProvider { - return EventDetailResourcesProvider(resourceManager) + return EventDetailResourcesProvider(programUid, programStageUid, resourceManager) } @Provides @@ -75,19 +75,19 @@ class EventDetailsModule( fun provideEventDetailsRepository( d2: D2, resourceManager: ResourceManager, - networkUtils: NetworkUtils, colorUtils: ColorUtils, + periodUtils: DhisPeriodUtils, ): EventDetailsRepository { return EventDetailsRepository( d2 = d2, programUid = programUid, eventUid = eventUid, programStageUid = programStageUid, + eventCreationType = eventCreationType, fieldFactory = FieldViewModelFactoryImpl( - false, UiStyleProviderImpl( - FormUiModelColorFactoryImpl(context, true, colorUtils), - LongTextUiColorFactoryImpl(context, true, colorUtils), + FormUiModelColorFactoryImpl(context, colorUtils), + LongTextUiColorFactoryImpl(context, colorUtils), true, ), LayoutProviderImpl(), @@ -96,6 +96,7 @@ class EventDetailsModule( OptionSetConfiguration(d2), OrgUnitConfiguration(d2), FileResourceConfiguration(d2), + periodUtils, ), UiEventTypesProviderImpl(), KeyboardActionProviderImpl(), @@ -116,6 +117,7 @@ class EventDetailsModule( geometryController: GeometryController, locationProvider: LocationProvider, eventDetailResourcesProvider: EventDetailResourcesProvider, + metadataIconProvider: MetadataIconProvider, ): EventDetailsViewModelFactory { return EventDetailsViewModelFactory( ConfigureEventDetails( @@ -123,6 +125,7 @@ class EventDetailsModule( resourcesProvider = resourcesProvider, creationType = eventCreationType, enrollmentStatus = enrollmentStatus, + metadataIconProvider = metadataIconProvider, ), ConfigureEventReportDate( creationType = eventCreationType, diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventCatComboUiModel.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventCatComboUiModel.kt index 2554c5f580..845fd968c5 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventCatComboUiModel.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventCatComboUiModel.kt @@ -9,7 +9,6 @@ data class EventCatComboUiModel( val detailsEnabled: Boolean, val currentDate: Date?, val selectedOrgUnit: String?, - val onShowCategoryDialog: (EventCategory) -> Unit, val onClearCatCombo: (EventCategory) -> Unit, val onOptionSelected: (CategoryOption?) -> Unit, val required: Boolean = false, diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventDetails.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventDetails.kt index 6b715ead9b..616a675c53 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventDetails.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventDetails.kt @@ -1,12 +1,12 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models -import org.hisp.dhis.android.core.common.ObjectStyle +import org.dhis2.ui.MetadataIconData import java.util.Date data class EventDetails( val name: String? = null, val description: String? = null, - val style: ObjectStyle? = null, + val metadataIconData: MetadataIconData? = null, val enabled: Boolean = true, val isEditable: Boolean = true, val editableReason: String? = null, @@ -19,4 +19,6 @@ data class EventDetails( val isActionButtonVisible: Boolean = false, val actionButtonText: String? = null, val canReopen: Boolean = false, -) +) { + fun getIcon() = metadataIconData +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventInputDateUiModel.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventInputDateUiModel.kt index c7f92b621c..601766d697 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventInputDateUiModel.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventInputDateUiModel.kt @@ -1,14 +1,17 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.InputDateValues +import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates data class EventInputDateUiModel( val eventDate: EventDate, val detailsEnabled: Boolean, - val onDateClick: () -> Unit, + val onDateClick: (() -> Unit)?, val allowsManualInput: Boolean = true, - val onDateSet: (InputDateValues) -> Unit, - val onClear: () -> Unit, + val onDateSelected: (InputDateValues) -> Unit?, + val onClear: (() -> Unit)? = null, val required: Boolean = false, val showField: Boolean = true, + val is24HourFormat: Boolean = true, + val selectableDates: SelectableDates? = null, ) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/EventDetailResourcesProvider.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/EventDetailResourcesProvider.kt index f1230cd7f2..d661fc0341 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/EventDetailResourcesProvider.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/EventDetailResourcesProvider.kt @@ -5,11 +5,16 @@ import org.dhis2.commons.resources.ResourceManager import org.hisp.dhis.android.core.event.EventNonEditableReason class EventDetailResourcesProvider( + private val programUid: String, + private val programStage: String?, private val resourceManager: ResourceManager, ) { fun provideDueDate() = resourceManager.getString(R.string.due_date) - fun provideEventDate() = resourceManager.getString(R.string.event_date) + fun provideEventDate() = resourceManager.formatWithEventLabel( + R.string.event_label_date, + programStage, + ) fun provideEditionStatus(reason: EventNonEditableReason): String { return when (reason) { @@ -24,7 +29,11 @@ class EventDetailResourcesProvider( EventNonEditableReason.NO_CATEGORY_COMBO_ACCESS -> resourceManager.getString(R.string.edition_no_catcombo_access) EventNonEditableReason.ENROLLMENT_IS_NOT_OPEN -> - resourceManager.getString(R.string.edition_enrollment_is_no_open) + resourceManager.formatWithEnrollmentLabel( + programUid, + R.string.edition_enrollment_is_no_open_V2, + 1, + ) EventNonEditableReason.ORGUNIT_IS_NOT_IN_CAPTURE_SCOPE -> resourceManager.getString(R.string.edition_orgunit_capture_scope) } @@ -36,9 +45,15 @@ class EventDetailResourcesProvider( fun provideButtonCheck() = resourceManager.getString(R.string.check_event) - fun provideEventCreatedMessage() = resourceManager.getString(R.string.event_updated) + fun provideEventCreatedMessage() = resourceManager.formatWithEventLabel( + R.string.event_label_updated, + programStage, + ) - fun provideEventCreationError() = resourceManager.getString(R.string.failed_insert_event) + fun provideEventCreationError() = resourceManager.formatWithEventLabel( + R.string.failed_insert_event_label, + programStage, + ) fun provideReOpened() = resourceManager.getString(R.string.re_opened) } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt index 69c297b4a4..3c0719ea6c 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt @@ -1,26 +1,22 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height -import androidx.compose.material.DropdownMenu -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ExposedDropdownMenuBox -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import org.dhis2.R +import org.dhis2.commons.extensions.inDateRange +import org.dhis2.commons.extensions.inOrgUnit import org.dhis2.commons.resources.ResourceManager -import org.dhis2.data.dhislogic.inDateRange -import org.dhis2.data.dhislogic.inOrgUnit import org.dhis2.form.model.UiEventType import org.dhis2.form.model.UiRenderType import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatComboUiModel @@ -29,16 +25,19 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventIn import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventOrgUnit import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTemp import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTempStatus -import org.dhis2.utils.category.CategoryDialog.Companion.DEFAULT_COUNT_LIMIT import org.hisp.dhis.android.core.arch.helpers.GeometryHelper import org.hisp.dhis.android.core.arch.helpers.Result import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.common.Geometry import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.mobile.ui.designsystem.component.Coordinates -import org.hisp.dhis.mobile.ui.designsystem.component.DateTimeActionIconType +import org.hisp.dhis.mobile.ui.designsystem.component.DateTimeActionType +import org.hisp.dhis.mobile.ui.designsystem.component.DropdownInputField +import org.hisp.dhis.mobile.ui.designsystem.component.DropdownItem import org.hisp.dhis.mobile.ui.designsystem.component.InputCoordinate import org.hisp.dhis.mobile.ui.designsystem.component.InputDateTime +import org.hisp.dhis.mobile.ui.designsystem.component.InputDateTimeModel import org.hisp.dhis.mobile.ui.designsystem.component.InputDropDown import org.hisp.dhis.mobile.ui.designsystem.component.InputOrgUnit import org.hisp.dhis.mobile.ui.designsystem.component.InputPolygon @@ -46,9 +45,8 @@ import org.hisp.dhis.mobile.ui.designsystem.component.InputRadioButton import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState import org.hisp.dhis.mobile.ui.designsystem.component.Orientation import org.hisp.dhis.mobile.ui.designsystem.component.RadioButtonData +import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateTransformation -import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor -import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor import java.time.LocalDate import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException @@ -60,38 +58,47 @@ fun ProvideInputDate( ) { if (uiModel.showField) { Spacer(modifier = Modifier.height(16.dp)) + val textSelection = TextRange(if (uiModel.eventDate.dateValue != null) uiModel.eventDate.dateValue.length else 0) var value by remember(uiModel.eventDate.dateValue) { - mutableStateOf(uiModel.eventDate.dateValue?.let { formatStoredDateToUI(it) }) + if (uiModel.eventDate.dateValue != null) { + mutableStateOf(TextFieldValue(formatStoredDateToUI(uiModel.eventDate.dateValue) ?: "", textSelection)) + } else { + mutableStateOf(TextFieldValue()) + } } var state by remember { mutableStateOf(getInputState(uiModel.detailsEnabled)) } - + val yearRange = if (uiModel.selectableDates != null) { + IntRange(uiModel.selectableDates.initialDate.substring(4, 8).toInt(), uiModel.selectableDates.endDate.substring(4, 8).toInt()) + } else { + IntRange(1924, 2124) + } InputDateTime( - title = uiModel.eventDate.label ?: "", - allowsManualInput = uiModel.allowsManualInput, - value = value, - actionIconType = DateTimeActionIconType.DATE, - onActionClicked = uiModel.onDateClick, - state = state, - visualTransformation = DateTransformation(), - onValueChanged = { - value = it - state = getInputShellStateBasedOnValue(it) - manageActionBasedOnValue(uiModel, it) - }, - isRequired = uiModel.required, - modifier = modifier.testTag(INPUT_EVENT_INITIAL_DATE), - onFocusChanged = { focused -> - if (!focused) { - value?.let { - if (!isValid(it)) { - state = InputShellState.ERROR - } + InputDateTimeModel( + title = uiModel.eventDate.label ?: "", + allowsManualInput = uiModel.allowsManualInput, + inputTextFieldValue = value, + actionType = DateTimeActionType.DATE, + state = state, + visualTransformation = DateTransformation(), + onValueChanged = { + value = it ?: TextFieldValue() + state = getInputShellStateBasedOnValue(it?.text) + it?.let { it1 -> manageActionBasedOnValue(uiModel, it1.text) } + }, + isRequired = uiModel.required, + onFocusChanged = { focused -> + if (!focused && !isValid(value.text)) { + state = InputShellState.ERROR } - } - }, + }, + is24hourFormat = uiModel.is24HourFormat, + selectableDates = uiModel.selectableDates ?: SelectableDates("01011924", "12312124"), + yearRange = yearRange, + ), + modifier = modifier.testTag(INPUT_EVENT_INITIAL_DATE), ) } } @@ -129,10 +136,10 @@ fun getInputShellStateBasedOnValue(dateString: String?): InputShellState { fun manageActionBasedOnValue(uiModel: EventInputDateUiModel, dateString: String) { if (dateString.isEmpty()) { - uiModel.onClear() + uiModel.onClear?.invoke() } else if (isValid(dateString) && isValidDateFormat(dateString)) { formatUIDateToStored(dateString)?.let { dateValues -> - uiModel.onDateSet(dateValues) + uiModel.onDateSelected(dateValues) } } } @@ -207,20 +214,20 @@ fun ProvideOrgUnit( } } -@OptIn(ExperimentalMaterialApi::class) @Composable fun ProvideCategorySelector( modifier: Modifier = Modifier, eventCatComboUiModel: EventCatComboUiModel, ) { - var selectedItem by remember { - mutableStateOf( - eventCatComboUiModel.eventCatCombo.selectedCategoryOptions[eventCatComboUiModel.category.uid]?.displayName() - ?: eventCatComboUiModel.eventCatCombo.categoryOptions?.get(eventCatComboUiModel.category.uid)?.displayName(), - ) + var selectedItem by with(eventCatComboUiModel) { + remember(this) { + mutableStateOf( + eventCatCombo.selectedCategoryOptions[category.uid]?.displayName() + ?: eventCatCombo.categoryOptions?.get(category.uid)?.displayName(), + ) + } } - var expanded by remember { mutableStateOf(false) } val selectableOptions = eventCatComboUiModel.category.options .filter { option -> option.access().data().write() @@ -229,75 +236,71 @@ fun ProvideCategorySelector( }.filter { option -> option.inOrgUnit(eventCatComboUiModel.selectedOrgUnit) } + val dropdownItems = selectableOptions.map { DropdownItem(it.displayName() ?: it.code() ?: "") } Spacer(modifier = Modifier.height(16.dp)) - if (selectableOptions.isNotEmpty()) { - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = {}, - ) { - InputDropDown( - modifier = modifier.testTag(CATEGORY_SELECTOR), - title = eventCatComboUiModel.category.name, - state = getInputState(eventCatComboUiModel.detailsEnabled), - selectedItem = selectedItem, - onResetButtonClicked = { - selectedItem = null - eventCatComboUiModel.onClearCatCombo(eventCatComboUiModel.category) - }, - onArrowDropDownButtonClicked = { - expanded = !expanded - }, - isRequiredField = eventCatComboUiModel.required, - ) - if (expanded) { - if (eventCatComboUiModel.category.optionsSize > DEFAULT_COUNT_LIMIT) { - eventCatComboUiModel.onShowCategoryDialog(eventCatComboUiModel.category) - expanded = false - } else { - DropdownMenu( - modifier = modifier.exposedDropdownSize(), - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { - if (selectableOptions.isNotEmpty()) { - selectableOptions.forEach { option -> - val isSelected = option.displayName() == selectedItem - DropdownMenuItem( - modifier = Modifier.background( - when { - isSelected -> SurfaceColor.PrimaryContainer - else -> Color.Transparent - }, - ), - content = { - Text( - text = option.displayName() ?: option.code() ?: "", - color = when { - isSelected -> TextColor.OnPrimaryContainer - else -> TextColor.OnSurface - }, - ) - }, - onClick = { - expanded = false - selectedItem = option.displayName() - eventCatComboUiModel.onOptionSelected(option) - }, - ) - } - } - } - } - } - } + if (selectableOptions.isNotEmpty()) { + InputDropDown( + modifier = modifier, + title = eventCatComboUiModel.category.name, + state = getInputState(eventCatComboUiModel.detailsEnabled), + selectedItem = DropdownItem(selectedItem ?: ""), + onResetButtonClicked = { + selectedItem = null + eventCatComboUiModel.onClearCatCombo(eventCatComboUiModel.category) + }, + onItemSelected = { newSelectedDropdownItem -> + selectedItem = newSelectedDropdownItem.label + eventCatComboUiModel.onOptionSelected(selectableOptions.firstOrNull { it.displayName() == newSelectedDropdownItem.label }) + }, + dropdownItems = dropdownItems, + isRequiredField = eventCatComboUiModel.required, + ) } else { ProvideEmptyCategorySelector(modifier = modifier, name = eventCatComboUiModel.category.name, option = eventCatComboUiModel.noOptionsText) } } -@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ProvidePeriodSelector( + modifier: Modifier = Modifier, + uiModel: EventInputDateUiModel, +) { + var selectedItem by with(uiModel) { + remember(this) { + mutableStateOf( + uiModel.eventDate.dateValue, + ) + } + } + val state = getInputState(uiModel.detailsEnabled) + + Spacer(modifier = Modifier.height(16.dp)) + + DropdownInputField( + modifier = modifier, + title = uiModel.eventDate.label ?: "", + state = state, + selectedItem = DropdownItem(selectedItem ?: ""), + onResetButtonClicked = { + selectedItem = null + uiModel.onClear?.let { it() } + }, + onDropdownIconClick = { + uiModel.onDateClick?.invoke() + }, + isRequiredField = uiModel.required, + legendData = null, + onFocusChanged = {}, + supportingTextData = null, + focusRequester = remember { + FocusRequester() + }, + expanded = false, + ) +} + @Composable fun ProvideEmptyCategorySelector( modifier: Modifier = Modifier, @@ -308,55 +311,21 @@ fun ProvideEmptyCategorySelector( mutableStateOf("") } - var expanded by remember { mutableStateOf(false) } Spacer(modifier = Modifier.height(16.dp)) - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = {}, - ) { - InputDropDown( - modifier = modifier.testTag(EMPTY_CATEGORY_SELECTOR), - title = name, - state = InputShellState.UNFOCUSED, - selectedItem = selectedItem, - onResetButtonClicked = { - selectedItem = "" - }, - onArrowDropDownButtonClicked = { - expanded = !expanded - }, - isRequiredField = true, - ) - - DropdownMenu( - modifier = modifier.exposedDropdownSize(), - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { - val isSelected = option == selectedItem - DropdownMenuItem( - modifier = Modifier.background( - when { - isSelected -> SurfaceColor.PrimaryContainer - else -> Color.Transparent - }, - ), - content = { - Text( - text = option, - color = when { - isSelected -> TextColor.OnPrimaryContainer - else -> TextColor.OnSurface - }, - ) - }, - onClick = { - expanded = false - selectedItem = option - }, - ) - } - } + InputDropDown( + modifier = modifier, + title = name, + state = InputShellState.UNFOCUSED, + selectedItem = DropdownItem(selectedItem), + onResetButtonClicked = { + selectedItem = "" + }, + onItemSelected = { newSelectedDropdownItem -> + selectedItem = newSelectedDropdownItem.label + }, + dropdownItems = listOf(DropdownItem(option)), + isRequiredField = false, + ) } private fun getInputState(enabled: Boolean) = if (enabled) { @@ -471,6 +440,12 @@ fun ProvideRadioButtons( } } +fun willShowCalendar(periodType: PeriodType?): Boolean { + return (periodType == null || periodType == PeriodType.Daily) +} + const val INPUT_EVENT_INITIAL_DATE = "INPUT_EVENT_INITIAL_DATE" const val EMPTY_CATEGORY_SELECTOR = "EMPTY_CATEGORY_SELECTOR" const val CATEGORY_SELECTOR = "CATEGORY_SELECTOR" +const val DEFAULT_MIN_DATE = "12111924" +const val DEFAULT_MAX_DATE = "12112124" diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt index 4a3f291399..036a3fe9f7 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt @@ -6,12 +6,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.DatePicker import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier import androidx.databinding.DataBindingUtil import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope @@ -26,8 +26,7 @@ import org.dhis2.commons.Constants.ORG_UNIT import org.dhis2.commons.Constants.PROGRAM_STAGE_UID import org.dhis2.commons.Constants.PROGRAM_UID import org.dhis2.commons.data.EventCreationType -import org.dhis2.commons.dialogs.calendarpicker.CalendarPicker -import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener +import org.dhis2.commons.dialogs.PeriodDialog import org.dhis2.commons.locationprovider.LocationSettingLauncher import org.dhis2.commons.orgunitselector.OUTreeFragment import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope @@ -38,7 +37,6 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.Even import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsModule import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatComboUiModel -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCategory import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCoordinates import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDetails @@ -50,11 +48,9 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.Prov import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideEmptyCategorySelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideInputDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideOrgUnit +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvidePeriodSelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideRadioButtons import org.dhis2.usescases.general.FragmentGlobalAbstract -import org.dhis2.utils.category.CategoryDialog -import org.dhis2.utils.category.CategoryDialog.Companion.TAG -import org.dhis2.utils.customviews.PeriodDialog import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.period.PeriodType @@ -168,10 +164,6 @@ class EventDetailsFragment : FragmentGlobalAbstract() { } } - viewModel.showCalendar = { - showCalendarDialog() - } - viewModel.showPeriods = { showPeriodDialog() } @@ -240,19 +232,41 @@ class EventDetailsFragment : FragmentGlobalAbstract() { eventTemp: EventTemp, ) { Column { - ProvideInputDate( - EventInputDateUiModel( - eventDate = date, - detailsEnabled = details.enabled, - onDateClick = { viewModel.onDateClick() }, - onDateSet = { dateValues -> - viewModel.onDateSet(dateValues.year, dateValues.month - 1, dateValues.day) - }, - onClear = { viewModel.onClearEventReportDate() }, - required = true, - showField = date.active, - ), - ) + if (viewModel.getPeriodType() == null || (viewModel.getPeriodType() != null && viewModel.getPeriodType() == PeriodType.Daily)) { + ProvideInputDate( + EventInputDateUiModel( + eventDate = date, + detailsEnabled = details.enabled, + onDateClick = {}, + onDateSelected = { dateValues -> + viewModel.onDateSet( + dateValues.year, + dateValues.month - 1, + dateValues.day, + ) + }, + onClear = { viewModel.onClearEventReportDate() }, + required = true, + showField = date.active, + selectableDates = viewModel.getSelectableDates(date), + ), + ) + } else { + ProvidePeriodSelector( + uiModel = EventInputDateUiModel( + eventDate = date, + detailsEnabled = details.enabled, + onDateClick = { showPeriodDialog() }, + onDateSelected = {}, + onClear = { viewModel.onClearEventReportDate() }, + required = true, + showField = date.active, + selectableDates = viewModel.getSelectableDates(date), + ), + modifier = Modifier, + ) + } + ProvideOrgUnit( orgUnit = orgUnit, detailsEnabled = details.enabled, @@ -274,9 +288,6 @@ class EventDetailsFragment : FragmentGlobalAbstract() { detailsEnabled = details.enabled, currentDate = date.currentDate, selectedOrgUnit = details.selectedOrgUnit, - onShowCategoryDialog = { - showCategoryDialog(it) - }, onClearCatCombo = { viewModel.onClearCatCombo() }, @@ -291,7 +302,10 @@ class EventDetailsFragment : FragmentGlobalAbstract() { ) } } else if (!catCombo.isDefault) { - ProvideEmptyCategorySelector(name = catCombo.displayName ?: getString(R.string.cat_combo), option = getString(R.string.no_options)) + ProvideEmptyCategorySelector( + name = catCombo.displayName ?: getString(R.string.cat_combo), + option = getString(R.string.no_options), + ) } ProvideCoordinates( coordinates = coordinates, @@ -311,27 +325,6 @@ class EventDetailsFragment : FragmentGlobalAbstract() { } } - private fun showCalendarDialog() { - val dialog = CalendarPicker(requireContext()) - dialog.setInitialDate(viewModel.eventDate.value.currentDate) - dialog.setMinDate(viewModel.eventDate.value.minDate) - dialog.setMaxDate(viewModel.eventDate.value.maxDate) - dialog.isFutureDatesAllowed(viewModel.eventDate.value.allowFutureDates) - dialog.setListener( - object : OnDatePickerListener { - override fun onNegativeClick() {} - override fun onPositiveClick(datePicker: DatePicker) { - viewModel.onDateSet( - datePicker.year, - datePicker.month, - datePicker.dayOfMonth, - ) - } - }, - ) - dialog.show() - } - private fun showPeriodDialog() { PeriodDialog() .setPeriod(viewModel.eventDate.value.periodType) @@ -353,7 +346,20 @@ class EventDetailsFragment : FragmentGlobalAbstract() { ) .singleSelection() .orgUnitScope( - OrgUnitSelectorScope.ProgramCaptureScope(viewModel.eventOrgUnit.value.programUid!!), + when (getEventCreationType(requireArguments().getString(EVENT_CREATION_TYPE))) { + EventCreationType.REFERAL -> + OrgUnitSelectorScope.ProgramSearchScope( + viewModel.eventOrgUnit.value.programUid!!, + ) + + EventCreationType.DEFAULT, + EventCreationType.ADDNEW, + EventCreationType.SCHEDULE, + -> + OrgUnitSelectorScope.ProgramCaptureScope( + viewModel.eventOrgUnit.value.programUid!!, + ) + }, ) .onSelection { selectedOrgUnits -> viewModel.setUpOrgUnit(selectedOrgUnit = selectedOrgUnits.firstOrNull()?.uid()) @@ -366,18 +372,6 @@ class EventDetailsFragment : FragmentGlobalAbstract() { showInfoDialog(getString(R.string.error), getString(R.string.no_org_units)) } - private fun showCategoryDialog(category: EventCategory) { - CategoryDialog( - CategoryDialog.Type.CATEGORY_OPTIONS, - category.uid, - true, - viewModel.eventDate.value.currentDate, - ) { categoryOption -> - val selectedOption = Pair(category.uid, categoryOption) - viewModel.setUpCategoryCombo(selectedOption) - }.show(requireActivity().supportFragmentManager, TAG) - } - private fun getEventCreationType(typeString: String?): EventCreationType { return typeString?.let { EventCreationType.valueOf(it) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt index 614e1232a8..5d33d4db0b 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt @@ -24,13 +24,20 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDe import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventOrgUnit import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTemp import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTempStatus +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.DEFAULT_MAX_DATE +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.DEFAULT_MIN_DATE import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider import org.hisp.dhis.android.core.arch.helpers.GeometryHelper import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.common.Geometry import org.hisp.dhis.android.core.period.PeriodType +import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates +import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date +import java.util.GregorianCalendar +import java.util.Locale +import java.util.TimeZone class EventDetailsViewModel( private val configureEventDetails: ConfigureEventDetails, @@ -47,7 +54,6 @@ class EventDetailsViewModel( private val resourcesProvider: EventDetailResourcesProvider, ) : ViewModel() { - var showCalendar: (() -> Unit)? = null var showPeriods: (() -> Unit)? = null var showOrgUnits: (() -> Unit)? = null var showNoOrgUnits: (() -> Unit)? = null @@ -168,7 +174,7 @@ class EventDetailsViewModel( } fun onClearEventReportDate() { - _eventDate.value = eventDate.value.copy(currentDate = null) + _eventDate.value = eventDate.value.copy(currentDate = null, dateValue = null) setUpEventDetails() } @@ -243,23 +249,48 @@ class EventDetailsViewModel( EventDetailIdlingResourceSingleton.decrement() } - fun onDateClick() { + fun getSelectableDates(eventDate: EventDate): SelectableDates { + return if (eventDate.allowFutureDates) { + SelectableDates(DEFAULT_MIN_DATE, DEFAULT_MAX_DATE) + } else { + val currentDate = + SimpleDateFormat("ddMMyyyy", Locale.US).format(Date(System.currentTimeMillis())) + SelectableDates(DEFAULT_MIN_DATE, currentDate) + } + } + + fun showPeriodDialog() { periodType?.let { showPeriods?.invoke() - } ?: showCalendar?.invoke() + } } fun onDateSet(year: Int, month: Int, day: Int) { val calendar = Calendar.getInstance() calendar[year, month, day, 0, 0] = 0 calendar[Calendar.MILLISECOND] = 0 + + val currentTimeZone: TimeZone = calendar.getTimeZone() + val currentDt: Calendar = GregorianCalendar(currentTimeZone, Locale.getDefault()) + + var gmtOffset: Int = currentTimeZone.getOffset( + currentDt[Calendar.ERA], + currentDt[Calendar.YEAR], + currentDt[Calendar.MONTH], + currentDt[Calendar.DAY_OF_MONTH], + currentDt[Calendar.DAY_OF_WEEK], + currentDt[Calendar.MILLISECOND], + ) + gmtOffset /= (60 * 60 * 1000) + calendar.add(Calendar.HOUR_OF_DAY, +gmtOffset) val selectedDate = calendar.time + setUpEventReportDate(selectedDate) } fun onOrgUnitClick() { if (!eventOrgUnit.value.fixed) { - if (eventOrgUnit.value.orgUnits.isNullOrEmpty()) { + if (eventOrgUnit.value.orgUnits.isEmpty()) { showNoOrgUnits?.invoke() } else { showOrgUnits?.invoke() @@ -323,6 +354,10 @@ class EventDetailsViewModel( } } + fun getPeriodType(): PeriodType? { + return periodType + } + fun onReopenClick() { configureEventDetails.reopenEvent().mockSafeFold( onSuccess = { @@ -353,16 +388,16 @@ inline fun Result.mockSafeFold( if ((value as Result<*>).isSuccess) { valueNotNull::class.java.getDeclaredField("value").let { it.isAccessible = true - it.get(value) as T + it[value] as T }.let(onSuccess) } else { valueNotNull::class.java.getDeclaredField("value").let { it.isAccessible = true - it.get(value) + it[value] }.let { failure -> failure!!::class.java.getDeclaredField("exception").let { it.isAccessible = true - it.get(failure) as Exception + it[failure] as Exception } }.let(onFailure) } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.java index 0f3852d7ce..3aed5a82aa 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.java @@ -25,11 +25,14 @@ import org.dhis2.App; import org.dhis2.R; +import org.dhis2.commons.Constants; import org.dhis2.commons.data.EventCreationType; import org.dhis2.commons.dialogs.CustomDialog; import org.dhis2.commons.dialogs.DialogClickListener; import org.dhis2.commons.popupmenu.AppMenuHelper; +import org.dhis2.commons.resources.ResourceManager; import org.dhis2.databinding.ActivityEventInitialBinding; +import org.dhis2.form.model.EventMode; import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity; import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsComponent; import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsComponentProvider; @@ -38,8 +41,6 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui.EventDetailsFragment; import org.dhis2.usescases.general.ActivityGlobalAbstract; import org.dhis2.usescases.qrCodes.eventsworegistration.QrEventsWORegistrationActivity; -import org.dhis2.commons.Constants; -import org.dhis2.utils.EventMode; import org.dhis2.utils.HelpManager; import org.dhis2.utils.analytics.AnalyticsConstants; import org.hisp.dhis.android.core.common.Geometry; @@ -60,6 +61,9 @@ public class EventInitialActivity extends ActivityGlobalAbstract implements Even @Inject EventInitialPresenter presenter; + @Inject + ResourceManager resourceManager; + private ActivityEventInitialBinding binding; //Bundle variables @@ -239,14 +243,22 @@ private void setUpActivityTitle() { activityTitle = program.displayName() + " - " + getString(R.string.referral); } else { - activityTitle = eventUid == null ? program.displayName() + " - " + getString(R.string.new_event) : program.displayName(); + activityTitle = eventUid == null ? + program.displayName() + " - " + + resourceManager.formatWithEventLabel(R.string.new_event_label, programStageUid, 1, false) + : program.displayName(); } binding.setName(activityTitle); } @Override public void onEventCreated(String eventUid) { - showToast(getString(R.string.event_created)); + showToast( + resourceManager.formatWithEventLabel( + R.string.event_label_created, + programStageUid, + 1, false + )); if (eventCreationType != EventCreationType.SCHEDULE && eventCreationType != EventCreationType.REFERAL) { startFormActivity(eventUid, true); } else { @@ -340,8 +352,14 @@ public void showMoreOptions(View view) { public void confirmDeleteEvent() { new CustomDialog( this, - getString(R.string.delete_event), - getString(R.string.confirm_delete_event), + resourceManager.formatWithEventLabel( + R.string.delete_event_label, + programStageUid, + 1, false), + resourceManager.formatWithEventLabel( + R.string.confirm_delete_event_label, + programStageUid, + 1, false), getString(R.string.delete), getString(R.string.cancel), 0, @@ -362,10 +380,23 @@ public void onNegative() { @Override public void showEventWasDeleted() { - showToast(getString(R.string.event_was_deleted)); + showToast(resourceManager.formatWithEventLabel( + R.string.event_label_was_deleted, + programStageUid, + 1, false + )); finish(); } + @Override + public void showDeleteEventError() { + showToast(resourceManager.formatWithEventLabel( + R.string.delete_event_label_error, + programStageUid, + 1, false + )); + } + @Nullable @Override public EventDetailsComponent provideEventDetailsComponent(@Nullable EventDetailsModule module) { diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialContract.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialContract.java index 4c190b31f0..50c0bda4ae 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialContract.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialContract.java @@ -29,5 +29,7 @@ public interface View extends AbstractActivityContracts.View { void showQR(); void showEventWasDeleted(); + + void showDeleteEventError(); } } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialModule.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialModule.java index 038527c669..949159bca4 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialModule.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialModule.java @@ -11,11 +11,12 @@ import org.dhis2.commons.prefs.PreferenceProvider; import org.dhis2.commons.prefs.PreferenceProviderImpl; import org.dhis2.commons.resources.ColorUtils; +import org.dhis2.commons.resources.MetadataIconProvider; +import org.dhis2.commons.resources.DhisPeriodUtils; import org.dhis2.commons.resources.ResourceManager; import org.dhis2.commons.schedulers.SchedulerProvider; import org.dhis2.data.forms.EventRepository; import org.dhis2.data.forms.FormRepository; -import org.dhis2.data.forms.dataentry.RuleEngineRepository; import org.dhis2.form.data.RulesRepository; import org.dhis2.form.data.RulesUtilsProvider; import org.dhis2.form.data.metadata.FileResourceConfiguration; @@ -33,8 +34,9 @@ import org.dhis2.form.ui.provider.UiStyleProviderImpl; import org.dhis2.form.ui.style.FormUiModelColorFactoryImpl; import org.dhis2.form.ui.style.LongTextUiColorFactoryImpl; +import org.dhis2.mobileProgramRules.EvaluationType; +import org.dhis2.mobileProgramRules.RuleEngineHelper; import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventFieldMapper; -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventRuleEngineRepository; import org.dhis2.utils.analytics.AnalyticsHelper; import org.hisp.dhis.android.core.D2; @@ -88,12 +90,17 @@ EventFieldMapper provideFieldMapper(Context context, FieldViewModelFactory field @Provides @PerActivity - FieldViewModelFactory fieldFactory(Context context, D2 d2, ResourceManager resourceManager, ColorUtils colorUtils) { + FieldViewModelFactory fieldFactory( + Context context, + D2 d2, + ResourceManager resourceManager, + ColorUtils colorUtils, + DhisPeriodUtils periodUtils + ) { return new FieldViewModelFactoryImpl( - false, new UiStyleProviderImpl( - new FormUiModelColorFactoryImpl(activityContext, true, colorUtils), - new LongTextUiColorFactoryImpl(activityContext, true, colorUtils), + new FormUiModelColorFactoryImpl(activityContext, colorUtils), + new LongTextUiColorFactoryImpl(activityContext, colorUtils), true ), new LayoutProviderImpl(), @@ -101,7 +108,8 @@ FieldViewModelFactory fieldFactory(Context context, D2 d2, ResourceManager resou new DisplayNameProviderImpl( new OptionSetConfiguration(d2), new OrgUnitConfiguration(d2), - new FileResourceConfiguration(d2) + new FileResourceConfiguration(d2), + periodUtils ), new UiEventTypesProviderImpl(), new KeyboardActionProviderImpl(), @@ -123,15 +131,28 @@ RulesRepository rulesRepository(@NonNull D2 d2) { @Provides @PerActivity - EventInitialRepository eventDetailRepository(D2 d2, - @NonNull FieldViewModelFactory fieldViewModelFactory, - RuleEngineRepository ruleEngineRepository) { - return new EventInitialRepositoryImpl(eventUid, stageUid, d2, fieldViewModelFactory, ruleEngineRepository); + EventInitialRepository eventDetailRepository( + D2 d2, + @NonNull FieldViewModelFactory fieldViewModelFactory, + @Nullable RuleEngineHelper ruleEngineHelper, + MetadataIconProvider metadataIconProvider + ) { + return new EventInitialRepositoryImpl(eventUid, + stageUid, + d2, + fieldViewModelFactory, + ruleEngineHelper, + metadataIconProvider); } @Provides @PerActivity - RuleEngineRepository ruleEngineRepository(D2 d2, FormRepository formRepository) { - return new EventRuleEngineRepository(d2, formRepository, eventUid); + @Nullable + RuleEngineHelper ruleEngineRepository(D2 d2) { + if (eventUid == null) return null; + return new RuleEngineHelper( + new EvaluationType.Event(eventUid), + new org.dhis2.mobileProgramRules.RulesRepository(d2) + ); } } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialPresenter.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialPresenter.java index b1aec976b0..28a7035617 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialPresenter.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialPresenter.java @@ -144,7 +144,7 @@ public void deleteEvent(String trackedEntityInstance) { eventInitialRepository.deleteEvent(eventId, trackedEntityInstance); view.showEventWasDeleted(); } else - view.displayMessage(view.getContext().getString(R.string.delete_event_error)); + view.showDeleteEventError(); } public boolean isEnrollmentOpen() { diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepositoryImpl.java index 7b3d36fbf0..010541dad7 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepositoryImpl.java @@ -3,11 +3,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.data.forms.FormSectionViewModel; -import org.dhis2.data.forms.dataentry.RuleEngineRepository; import org.dhis2.form.model.FieldUiModel; import org.dhis2.form.model.OptionSetConfiguration; import org.dhis2.form.ui.FieldViewModelFactory; +import org.dhis2.mobileProgramRules.RuleEngineHelper; +import org.dhis2.ui.MetadataIconData; import org.dhis2.utils.Result; import org.hisp.dhis.android.core.D2; import org.hisp.dhis.android.core.arch.helpers.UidsHelper; @@ -38,9 +40,9 @@ import java.util.Calendar; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; -import io.reactivex.Completable; import io.reactivex.Flowable; import io.reactivex.Observable; import timber.log.Timber; @@ -48,22 +50,26 @@ public class EventInitialRepositoryImpl implements EventInitialRepository { private final FieldViewModelFactory fieldFactory; - private final RuleEngineRepository ruleEngineRepository; + @Nullable + private final RuleEngineHelper ruleEngineHelper; private final String eventUid; private final D2 d2; private final String stageUid; + private final MetadataIconProvider metadataIconProvider; EventInitialRepositoryImpl(String eventUid, String stageUid, D2 d2, FieldViewModelFactory fieldFactory, - RuleEngineRepository ruleEngineRepository) { + @Nullable RuleEngineHelper ruleEngineHelper, + MetadataIconProvider metadataIconProvider) { this.eventUid = eventUid; this.stageUid = stageUid; this.d2 = d2; this.fieldFactory = fieldFactory; - this.ruleEngineRepository = ruleEngineRepository; + this.ruleEngineHelper = ruleEngineHelper; + this.metadataIconProvider = metadataIconProvider; } @NonNull @@ -295,7 +301,11 @@ public Flowable> list() { @Override public Flowable> calculate() { - return ruleEngineRepository.calculate(); + if (ruleEngineHelper != null) { + return Flowable.just(ruleEngineHelper.evaluate()).map(Result::success); + } else { + return Flowable.just(Result.success(new ArrayList<>())); + } } @NonNull @@ -311,15 +321,29 @@ private FieldUiModel transform(@NonNull ProgramStageDataElement stage, DataEleme String formName = dataElement.displayFormName(); String description = dataElement.displayDescription(); + OptionSetConfiguration optionSetConfig = null; - if(optionSet!=null){ + if (optionSet != null) { List