From 1356c64a484147247ca4a174b8ef23ba22a52b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Miguel=20Rubio?= Date: Tue, 18 Jun 2024 14:47:09 +0200 Subject: [PATCH 1/2] Release/3.0.0.1 (#3683) * ci: Update version name to 3.0.0.1 and version code to 134 Signed-off-by: andresmr * fix: [ANDROAPP-6195] Add missing text resource keys (#3667) * fix: [ANDROAPP-6195] Add missing text resource keys Signed-off-by: Pablo * fix: [ANDROAPP-6195] remove duplicates and typos Signed-off-by: Pablo * fix: [ANDROAPP-6195] remove duplicates Signed-off-by: Pablo * fix: [ANDROAPP-6195] add missing transifex configuration Signed-off-by: Pablo --------- Signed-off-by: Pablo * fix: [ANDROAPP-6194] Search outside the program (#3664) * fix: [ANDROAPP-6194] Send fetched list as parameter to avoid duplicated on search Signed-off-by: andresmr * fix: [ANDROAPP-6194] Send fetched list as parameter to avoid duplicated on search Signed-off-by: andresmr * fix: [ANDROAPP-6194] Add mockedWebServer response to mock get tracked entity instances Signed-off-by: andresmr --------- Signed-off-by: andresmr * fix: [ANDROAPP-6210] Manually instantiate resourceManager on BreakTheGlassBottomDialog (#3671) Signed-off-by: andresmr * fix: missing translations (#3674) * fix: missing translations Signed-off-by: Pablo * fix: duplicated resources Signed-off-by: Pablo --------- Signed-off-by: Pablo * chore: Update SDK to 1.10.0.1 (#3679) * chore: Update SDK to 1.10.0.1-SNAPSHOT Signed-off-by: andresmr * chore: Update SDK to 1.10.0.1 Signed-off-by: andresmr --------- Signed-off-by: andresmr * build: Update release notes Signed-off-by: andresmr * Add Mock response on old event when filtering by overdue Signed-off-by: andresmr * Update version name to 3.0.1 Signed-off-by: andresmr --------- Signed-off-by: andresmr Signed-off-by: Pablo Co-authored-by: Pablo --- .tx/config | 13 + RELEASE.md | 105 +---- .../mockwebserver/MockWebServerRobot.kt | 9 + .../flow/searchFlow/SearchFlowTest.kt | 14 + .../usescases/flow/teiFlow/TeiFlowTest.kt | 14 + .../dhis2/usescases/searchte/SearchTETest.kt | 57 ++- .../teilist/old_events_empty_response.json | 9 + .../old_tracked_entity_empty_response.json | 62 +++ .../searchTrackEntity/SearchRepository.java | 11 +- .../SearchRepositoryImpl.java | 31 +- .../SearchRepositoryImplKt.kt | 31 +- .../searchTrackEntity/SearchTEIViewModel.kt | 8 +- .../usescases/searchTrackEntity/SearchTEUi.kt | 389 ------------------ .../customviews/BreakTheGlassBottomDialog.kt | 13 +- app/src/main/res/values-es/strings.xml | 99 ++++- app/src/main/res/values/strings.xml | 15 +- .../searchTrackEntity/SearchRepositoryTest.kt | 90 ++++ commons/src/main/res/values-es/strings.xml | 68 ++- gradle/libs.versions.toml | 6 +- whatsnew/whatsnew-en-US | 20 +- 20 files changed, 516 insertions(+), 548 deletions(-) create mode 100644 app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json create mode 100644 app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json delete mode 100644 app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEUi.kt create mode 100644 app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt diff --git a/.tx/config b/.tx/config index 5529965f6d..d17aec75f4 100644 --- a/.tx/config +++ b/.tx/config @@ -44,3 +44,16 @@ source_lang = en type = ANDROID minimum_perc = 0 +[o:hisp-uio:p:dhis2-android-capture-app:r:ui-strings-xml] +file_filter = ui-components/src/main/res/values-/strings.xml +source_file = ui-components/src/main/res/values/strings.xml +source_lang = en +type = ANDROID +minimum_perc = 0 + +[o:hisp-uio:p:dhis2-android-capture-app:r:table-strings-xml] +file_filter = compose-table/src/main/res/values-/strings.xml +source_file = compose-table/src/main/res/values/strings.xml +source_lang = en +type = ANDROID +minimum_perc = 0 diff --git a/RELEASE.md b/RELEASE.md index f0c1788bed..19cb83d747 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,104 +1,9 @@ - - - - - - - -
-The new DHIS2 Android App allows offline data capture across all DHIS2 data models. Data and metadata are automatically synchronized whenever there is internet access, always keeping the most relevant data for the logged user in the device. -The app is compatible and we support 41, 40, 2.39. -
+# Release notes - Android App for DHIS2 - 3.0.0.1 -**Cross product** +### Bug -[**Support for customized Tracker terminology**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas?selectedIssue=ROADMAP-201)**:** Some DHIS2 terminology is not familiar for the end users. For this reason, we are gradually enabling the possibility to customize it to each particular use case. In this version, the term "event" and "enrollment" are customizable. The admin user will be able to configure it for each program using the Maintenance App, and the Android Capture App will display the customized term instead of the generic one. +[ANDROAPP-6194](https://dhis2.atlassian.net/browse/ANDROAPP-6194) Unable to search outside the program -**Documentation link:** +[ANDROAPP-6195](https://dhis2.atlassian.net/browse/ANDROAPP-6195) Missing terms in transifex -[**Multiselect value type:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-72\&issueViewSection=comments) DHIS2 already supports the introduction of multiple options for data elements for data aggregation. In this version it will also support it for individual data. The Android App will support both aggregated and individual multi select data elements from this version. - -**Documentation link:** - -[**Custom Icons**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-207): DHIS2 now supports uploading custom Icons to be used in addition to the built in Icon library. This is useful for use cases not related to health or that require very specific iconography. The Android App will render the custom icons that need to be uploaded and configured using the Maintenance DHIS2 Web App. - -**Documentation link:** - - - -**User Experience** - -[**Improvements in forms layout**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas?selectedIssue=ROADMAP-204)**:** When creating an event or an enrollment, there are a number of fields that are not data elements or attributes, for example, event date, org unit, coordinates, enrollment date, category combinations. Those elements are referred to as event/enrollment details and in previous versions they were displayed in different screens separated from the data elements or attributes. They were difficult to find when users wanted to edit or consult them. In this version the details are displayed inside the form, as the first opened section for completion. Once they are filled in, for example when the user reopens the event or enrollment form, the details section will be visible and easily available, but collapsed to leave more space for the data collection.  - -**Documentation link:** - -[**Improve TEI search user experience**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas?selectedIssue=ROADMAP-203)**:** The search form has been improved to provide a cleaner look and a more intuitive user experience. The buttons have been made more explicit for differencing search from creation. In addition the flow for searching TEIs using attributes rendered as bar / QR codes has been made more agile. If there is only one result and the attribute is unique, the app will open the TEI Dashboard directly. If there are multiple results, the app will display all the cards on the TEI list (this is equal to the current workflow), and if there are no results, the app will display the create button and allow the user to “search outside the program” if the configuration allows it. - -**Documentation link:** - - - -[**Improve TEI dashboard user experience**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas?selectedIssue=ROADMAP-205)**:** In the previous version of the application, the TEI header part of the dashboard was improved. In continuation to that effort, the bottom part, where all program stages are displayed, has been redesigned in this version. The changes include a fresh and more clean look of the list of events, with more space and less -not critical- information displayed. In addition, the button for creating new events has been moved to the top (in timeline view). - -**Documentation link:** - -[**New inputs for value types:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-293) ****The inputs for all value types have been gradually redesigned from the 2.9 version of the app. The signature input field as well as the complete legend description are included now to improve user experience at data entry. The new input fields are now displayed by default and admin users are able to opt-out to use the old forms through the Android Settings web app. - -**Documentation link:** - -[https://docs.dhis2.org/en/use/android-app/visual-configurations.html#capture\_app\_visual\_legends\_descri2ptions](https://docs.dhis2.org/en/use/android-app/visual-configurations.html#capture_app_visual_legends_descriptions) - - - -[**Improvements in tracker programs data entry flow:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-294) ****Several improvements have been made in the tracker programs user flow. An informative dialog has been added for confirmation when the user deletes a TEIs. The dialog for scheduling events after compilation has also been redesigned and improved. The selection of org. Units when the user only has access to one org. Unit for data collection has been removed and pre-filled, and lastly, the program rule “Hide program stage” behavior has been aligned with Capture web. - -**Documentation link:** - - - -[**Improve App behavior when working in offline mode:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-295) ****Some parts of the Android App are not responsive when there is no connection available (i.e. sync buttons…). The behavior is now improved and the App will inform the user that actions are not started because there is no internet connection available when buttons that require connection are tapped. - -**Documentation link:** NA - -**New functionality and Web Parity** - -[**Line listing analytics**](https://dhis2.atlassian.net/browse/ROADMAP-206): This version of the Android App includes the possibility to render and display line listing as part of the offline analytics functionalities. The line list has to be created using the Line Listing DHIS2 web App, and then configured to be displayed in Android using the Android Settings Web App (ASWA), as any other offline analytics in Android. In this case Line lists can be displayed in the home screen, and event or tracker programs (as they do not really apply to aggregated data, they are not displayed in Datasets). Users will be able to search by period, Org. unit, or any of the columns added in the Line List. - -There are some limitations to the line lists to be displayed in the Android App. The Org. units and Periods must be relative, not fixed. And there is a maximum number of columns of 15.  - -Android Local Analytics are built using local data, and wil, update instantly as more data gets collected (or downloaded) in the device. The App will display a maximum of 500 rows and will inform the user when the limit is reached. - -**Documentation link:** - -\ - - -[**Configurable basemap layer:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-209\&issueViewSection=deliver) DHIS2 supports the configuration of custom map layers for the Maps Web App. From this version of the DHIS2 Android App, those custom layers will be downloaded and rendered in the Maps. The custom layers will be presented as additional layers to the default ones. - -**Documentation link:** - -**Implementation Support** - -[**Import/Export App database:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-210) ****As part of troubleshooting, some errors can be hard to replicate and can lead to data loss because of being unable to sync. With this functionality the end user will be able to export the local database and share it with an admin who will be able to import it for troubleshooting, being able to replicate the exact environment (database, device, configuration). The exported database is encrypted and the administrator will require the user credentials to be able to access the database. - -**Documentation link:** - -[**Improve end-user config error feedback:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-296) ****In some cases configuration errors leave empty screens in the Android Capture app, either because of empty forms or because of lack of access. From this version of the Android App the app will display explicit and understandable errors to the user, who will be able to effectively communicate with the administrator to fix the problem. - -**Documentation link: NA** - -  - -**Maintenance/Performance** - -[**Improve app navigation performance for high number of TEIs:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-208) ****Implementations are more and more demanding in terms of offline need of individual records. This version of the app has been reviewed to optimize performance when there are big numbers of TEIs downloaded locally. **** - -**Documentation link: NA** - -You can find in Jira details on the [new features](https://dhis2.atlassian.net/issues/?filter=10640) and [bugs fixed](https://dhis2.atlassian.net/issues/?filter=10641) in this version. - -Remember to check the [documentation](https://www.dhis2.org/android-documentation) for detailed information of the features included in the App and how to configure DHIS2 to use it. - -Please create a [Jira](https://dhis2.atlassian.net) Issue if you find a bug or you want to propose a new functionality. [Project: Android App for DHIS2 | Component: -AndroidApp]. -
\ No newline at end of file +[ANDROAPP-6210](https://dhis2.atlassian.net/browse/ANDROAPP-6210) UninitializedPropertyAccessException on breaking the glass \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt b/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt index dc76fafbb7..cd07f23e3d 100644 --- a/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt +++ b/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt @@ -15,4 +15,13 @@ class MockWebServerRobot(private val dhis2MockServer: Dhis2MockServer) { fun addResponse(method: String, path: String, sdkResource: String, responseCode: Int = 200) { dhis2MockServer.addResponse(method, path, sdkResource, responseCode) } + + companion object { + const val API_OLD_TRACKED_ENTITY_PATH = "/api/trackedEntityInstances/query?.*" + const val API_OLD_TRACKED_ENTITY_RESPONSE = + "mocks/teilist/old_tracked_entity_empty_response.json" + const val API_OLD_EVENTS_PATH = "/api/events?.*" + const val API_OLD_EVENTS_RESPONSE = "mocks/teilist/old_events_empty_response.json" + + } } 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 5b7feb4bbc..4b26bb4f14 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 @@ -7,11 +7,14 @@ import androidx.compose.ui.text.intl.Locale import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule import org.dhis2.R +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_PATH +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_RESPONSE import org.dhis2.usescases.BaseTest import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel import org.dhis2.usescases.flow.teiFlow.entity.RegisterTEIUIModel import org.dhis2.usescases.flow.teiFlow.teiFlowRobot import org.dhis2.usescases.searchTrackEntity.SearchTEActivity +import org.hisp.dhis.android.core.mockwebserver.ResponseController import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -28,8 +31,19 @@ class SearchFlowTest : BaseTest() { private val dateRegistration = createFirstSpecificDate() private val dateEnrollment = createEnrollmentDate() + override fun setUp() { + super.setUp() + setupMockServer() + } + @Test fun shouldCreateTEIAndFilterByEnrollment() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + setDatePicker() val registerTEIDetails = createRegisterTEI() val enrollmentStatus = context.getString(R.string.filters_title_enrollment_status) 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 b59452babd..e7f603a2f5 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 @@ -4,12 +4,15 @@ 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.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_PATH +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_RESPONSE import org.dhis2.usescases.BaseTest 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.hisp.dhis.android.core.mockwebserver.ResponseController import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -33,8 +36,19 @@ class TeiFlowTest : BaseTest() { private val dateEnrollment = createEnrollmentDate() private val currentDate = getCurrentDate() + override fun setUp() { + super.setUp() + setupMockServer() + } + @Test fun shouldEnrollToSameProgramAfterClosingIt() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val totalEventsPerEnrollment = 3 val enrollmentListDetails = createEnrollmentList() val registerTeiDetails = createRegisterTEI() 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 3472dbc780..99aebeb245 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt @@ -15,6 +15,10 @@ import dispatch.android.espresso.IdlingDispatcherProviderRule import org.dhis2.R import org.dhis2.bindings.app import org.dhis2.common.idlingresources.MapIdlingResource +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_EVENTS_PATH +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_EVENTS_RESPONSE +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_PATH +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_RESPONSE import org.dhis2.commons.date.DateUtils.SIMPLE_DATE_FORMAT import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest @@ -26,13 +30,13 @@ import org.dhis2.usescases.searchte.entity.DisplayListFieldsUIModel import org.dhis2.usescases.searchte.robot.filterRobot import org.dhis2.usescases.searchte.robot.searchTeiRobot import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot +import org.hisp.dhis.android.core.mockwebserver.ResponseController import org.junit.After import org.junit.Ignore 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) @@ -55,8 +59,19 @@ class SearchTETest : BaseTest() { @get:Rule val composeTestRule = createComposeRule() + override fun setUp() { + super.setUp() + setupMockServer() + } + @Test fun shouldSuccessfullySearchByName() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val firstName = "Tim" val lastName = "Johnson" @@ -76,6 +91,12 @@ class SearchTETest : BaseTest() { @Test fun shouldShowErrorWhenCanNotFindSearchResult() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val firstName = "asdssds" prepareTestProgramRulesProgrammeIntentAndLaunchActivity(rule) @@ -91,6 +112,12 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullySearchUsingMoreThanOneField() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val firstName = "Anna" val lastName = "Jones" @@ -127,6 +154,12 @@ class SearchTETest : BaseTest() { @Test fun shouldCheckDisplayInList() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val displayInListData = createDisplayListFields() prepareTestAdultWomanProgrammeIntentAndLaunchActivity(rule) @@ -171,6 +204,16 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullyFilterByEventStatusOverdue() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_EVENTS_PATH, + API_OLD_EVENTS_RESPONSE, + ) enableComposeForms() val eventStatusFilter = context.getString(R.string.filters_title_event_status) val totalCount = "1" @@ -278,6 +321,12 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullyFilterBySync() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val teiName = "Frank" val teiLastName = "Fjordsen" val syncFilter = context.getString(R.string.action_sync) @@ -313,6 +362,12 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullySearchAndFilter() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + val name = "Anna" val lastName = "Jones" val enrollmentStatus = context.getString(R.string.filters_title_enrollment_status) diff --git a/app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json b/app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json new file mode 100644 index 0000000000..c03b0880c9 --- /dev/null +++ b/app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json @@ -0,0 +1,9 @@ +{ + "pager": { + "page": 1, + "pageCount": 1, + "total": 2, + "pageSize": 50 + }, + "events": [] +} \ No newline at end of file diff --git a/app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json b/app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json new file mode 100644 index 0000000000..e5104f914d --- /dev/null +++ b/app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json @@ -0,0 +1,62 @@ +{ + "headers": [ + { + "name": "instance", + "column": "Instance", + "type": "java.lang.String", + "hidden": false, + "meta": false + }, + { + "name": "created", + "column": "Created", + "type": "java.lang.String", + "hidden": false, + "meta": false + }, + { + "name": "lastupdated", + "column": "Last updated", + "type": "java.lang.String", + "hidden": false, + "meta": false + }, + { + "name": "ou", + "column": "Organisation unit", + "type": "java.lang.String", + "hidden": false, + "meta": false + }, + { + "name": "ouname", + "column": "Organisation unit name", + "type": "java.lang.String", + "hidden": false, + "meta": false + }, + { + "name": "te", + "column": "Tracked entity type", + "type": "java.lang.String", + "hidden": false, + "meta": false + }, + { + "name": "inactive", + "column": "Inactive", + "type": "java.lang.String", + "hidden": false, + "meta": false + } + ], + "metaData": { + "names": { + "nEenWmSyUEp": "Person" + } + }, + "width": 9, + "height": 0, + "rows": [ + ] +} \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java index ae624c091c..8691eea1fd 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java @@ -6,6 +6,7 @@ import org.dhis2.commons.data.EventViewModel; import org.dhis2.commons.data.SearchTeiModel; import org.dhis2.commons.data.tuples.Pair; +import org.dhis2.commons.filters.FilterManager; import org.dhis2.commons.filters.sorting.SortingItem; import org.dhis2.data.search.SearchParametersModel; import org.hisp.dhis.android.core.arch.call.D2Progress; @@ -17,14 +18,16 @@ import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem; import org.jetbrains.annotations.NotNull; -import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import io.reactivex.Flowable; import io.reactivex.Observable; +import kotlin.Deprecated; +@Deprecated(message = "Use SearchRepositoryKt instead") public interface SearchRepository { Observable> programsWithRegistration(String programTypeId); @@ -82,4 +85,10 @@ public interface SearchRepository { List trackedEntityTypeFields(); boolean filtersApplyOnGlobalSearch(); + + @NotNull HashSet getFetchedTeiUIDs(); + + SearchParametersModel getSavedSearchParameters(); + + FilterManager getSavedFilters(); } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java index ff0f8f0769..070bb93b60 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java @@ -24,8 +24,8 @@ import org.dhis2.commons.filters.sorting.SortingItem; import org.dhis2.commons.network.NetworkUtils; import org.dhis2.commons.reporting.CrashReportController; -import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.commons.resources.DhisPeriodUtils; +import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.commons.resources.ResourceManager; import org.dhis2.data.dhislogic.DhisEnrollmentUtils; import org.dhis2.data.forms.dataentry.SearchTEIRepository; @@ -43,7 +43,6 @@ import org.dhis2.utils.ValueUtils; import org.hisp.dhis.android.core.D2; import org.hisp.dhis.android.core.arch.call.D2Progress; -import org.hisp.dhis.android.core.arch.helpers.Result; import org.hisp.dhis.android.core.arch.helpers.UidsHelper; import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope; import org.hisp.dhis.android.core.common.FeatureType; @@ -57,7 +56,6 @@ import org.hisp.dhis.android.core.event.Event; import org.hisp.dhis.android.core.event.EventCollectionRepository; import org.hisp.dhis.android.core.event.EventStatus; -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; @@ -79,7 +77,6 @@ import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem; import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItemAttribute; import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItemHelper; -import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -606,6 +603,21 @@ public boolean filtersApplyOnGlobalSearch() { !FilterManager.getInstance().getStateFilters().isEmpty(); } + @Override + public @NotNull HashSet getFetchedTeiUIDs() { + return fetchedTeiUids; + } + + @Override + public SearchParametersModel getSavedSearchParameters() { + return savedSearchParameters; + } + + @Override + public FilterManager getSavedFilters() { + return savedFilters; + } + @Override public Observable getTrackedEntityType(String trackedEntityUid) { return d2.trackedEntityModule().trackedEntityTypes().uid(trackedEntityUid).get().toObservable(); @@ -697,17 +709,6 @@ public TeiDownloadResult download(String teiUid, @Nullable String enrollmentUid, return teiDownloader.download(teiUid, enrollmentUid, reason); } - public SearchTeiModel transformResult(Result result, @Nullable Program selectedProgram, boolean offlineOnly, SortingItem sortingItem) { - try { - return transform(result.getOrThrow(), selectedProgram, offlineOnly, sortingItem); - } catch (Exception e) { - SearchTeiModel errorModel = new SearchTeiModel(); - errorModel.onlineErrorMessage = resources.parseD2Error(e); - errorModel.onlineErrorCode = ((D2Error) e).errorCode(); - return errorModel; - } - } - @Override public SearchTeiModel transform(TrackedEntitySearchItem searchItem, @Nullable Program selectedProgram, boolean offlineOnly, SortingItem sortingItem) { if (!fetchedTeiUids.contains(searchItem.uid())) { diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt index 20e0b4b7c5..dc1a1470c7 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt @@ -30,14 +30,8 @@ class SearchRepositoryImplKt( private val metadataIconProvider: MetadataIconProvider, ) : SearchRepositoryKt { - private lateinit var savedSearchParamenters: SearchParametersModel - - private lateinit var savedFilters: FilterManager - private lateinit var trackedEntityInstanceQuery: TrackedEntitySearchCollectionRepository - private val fetchedTeiUids = HashSet() - override fun searchTrackedEntities( searchParametersModel: SearchParametersModel, isOnline: Boolean, @@ -51,26 +45,23 @@ class SearchRepositoryImplKt( isOnline: Boolean, ): TrackedEntitySearchCollectionRepository { var allowCache = false - savedSearchParamenters = searchParametersModel.copy() - savedFilters = FilterManager.getInstance().copy() - if (searchParametersModel != savedSearchParamenters || !FilterManager.getInstance() - .sameFilters(savedFilters) + if (searchParametersModel != searchRepositoryJava.savedSearchParameters || !FilterManager.getInstance() + .sameFilters(searchRepositoryJava.savedFilters) ) { trackedEntityInstanceQuery = searchRepositoryJava.getFilteredRepository(searchParametersModel) } else { - trackedEntityInstanceQuery = - searchRepositoryJava.getFilteredRepository(searchParametersModel) + searchRepositoryJava.getFilteredRepository(searchParametersModel) allowCache = true } - if (fetchedTeiUids.isNotEmpty() && searchParametersModel.selectedProgram == null) { + if (searchRepositoryJava.fetchedTeiUIDs.isNotEmpty() && searchParametersModel.selectedProgram == null) { trackedEntityInstanceQuery = - trackedEntityInstanceQuery.excludeUids().`in`(fetchedTeiUids.toList()) + trackedEntityInstanceQuery.excludeUids().`in`(searchRepositoryJava.fetchedTeiUIDs.toList()) } - val pagerFlow = if (isOnline && FilterManager.getInstance().stateFilters.isNotEmpty()) { + val pagerFlow = if (isOnline && FilterManager.getInstance().stateFilters.isEmpty()) { trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineFirst() } else { trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineOnly() @@ -131,7 +122,8 @@ class SearchRepositoryImplKt( options.associate { it.uid() to metadataIconProvider( it.style(), - program?.style()?.color()?.toColor() ?: SurfaceColor.Primary, + program?.style()?.color()?.toColor() + ?: SurfaceColor.Primary, ) } @@ -177,7 +169,12 @@ class SearchRepositoryImplKt( .blockingGet() val metadataIconMap = - options.associate { it.uid() to metadataIconProvider(it.style(), SurfaceColor.Primary) } + options.associate { + it.uid() to metadataIconProvider( + it.style(), + SurfaceColor.Primary, + ) + } OptionSetConfiguration.OptionConfigData( options = options, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt index 38441502cd..78b7e72282 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt @@ -373,7 +373,7 @@ class SearchTEIViewModel( suspend fun fetchGlobalResults() = withContext(dispatchers.io()) { val searchParametersModel = SearchParametersModel( - selectedProgram = searchRepository.getProgram(initialProgramUid), + selectedProgram = null, queryData = queryData, ) val getPagingData = searchRepositoryKt.searchTrackedEntities( @@ -927,6 +927,7 @@ class SearchTEIViewModel( ValueType.ORGANISATION_UNIT, ValueType.MULTI_TEXT -> { map[item.uid] = (item.displayName ?: "") } + ValueType.DATE, ValueType.AGE -> { item.value?.let { if (it.isNotEmpty()) { @@ -941,6 +942,7 @@ class SearchTEIViewModel( } } } + ValueType.DATETIME -> { item.value?.let { if (it.isNotEmpty()) { @@ -955,9 +957,11 @@ class SearchTEIViewModel( } } } + ValueType.BOOLEAN -> { map[item.uid] = "${item.label}: ${item.value}" } + ValueType.TRUE_ONLY -> { item.value?.let { if (it == "true") { @@ -965,9 +969,11 @@ class SearchTEIViewModel( } } } + ValueType.PERCENTAGE -> { map[item.uid] = "${item.value}%" } + else -> { map[item.uid] = (item.value ?: "") } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEUi.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEUi.kt deleted file mode 100644 index 44f045cd1d..0000000000 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEUi.kt +++ /dev/null @@ -1,389 +0,0 @@ -package org.dhis2.usescases.searchTrackEntity - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Icon -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import org.dhis2.R -import org.dhis2.usescases.searchTrackEntity.listView.SearchResult - -@Composable -fun SearchResult( - searchResultType: SearchResult.SearchResultType, - onSearchOutsideClick: () -> Unit, -) { - when (searchResultType) { - SearchResult.SearchResultType.LOADING -> - LoadingContent( - loadingDescription = stringResource(R.string.search_loading_more), - ) - - SearchResult.SearchResultType.SEARCH_OUTSIDE -> SearchOutsideProgram( - resultText = stringResource(R.string.search_no_results_in_program), - buttonText = stringResource(R.string.search_outside_action), - onSearchOutsideClick = onSearchOutsideClick, - ) - - SearchResult.SearchResultType.NO_MORE_RESULTS -> NoMoreResults() - SearchResult.SearchResultType.TOO_MANY_RESULTS -> TooManyResults() - SearchResult.SearchResultType.NO_RESULTS -> NoResults() - SearchResult.SearchResultType.SEARCH_OR_CREATE -> SearchOrCreate() - else -> { - // Nothing to do in these cases - } - } -} - -@Composable -fun SearchButton(modifier: Modifier = Modifier, onClick: () -> Unit) { - Button( - modifier = modifier, - onClick = onClick, - colors = ButtonDefaults.buttonColors(backgroundColor = Color.White), - shape = RoundedCornerShape(24.dp), - elevation = ButtonDefaults.elevation(), - ) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_search), - contentDescription = "", - tint = colorResource(id = R.color.colorPrimary), - ) - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = stringResource(id = R.string.search), - color = colorResource(id = R.color.textSecondary), - ) - } - } -} - -@Composable -fun WrappedSearchButton(onClick: () -> Unit) { - SearchButton( - modifier = Modifier - .wrapContentWidth(align = Alignment.CenterHorizontally) - .height(44.dp), - onClick = onClick, - ) -} - -@ExperimentalAnimationApi -@Composable -fun FullSearchButton(modifier: Modifier, visible: Boolean = true, onClick: () -> Unit) { - AnimatedVisibility( - modifier = modifier, - visible = visible, - enter = slideInVertically(), - exit = slideOutVertically(), - ) { - SearchButton( - modifier = Modifier - .fillMaxWidth() - .height(48.dp), - onClick = onClick, - ) - } -} - -@Composable -fun LoadingContent(loadingDescription: String) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - CircularProgressIndicator() - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = loadingDescription, - fontSize = 14.sp, - color = Color.Black.copy(alpha = 0.38f), - style = LocalTextStyle.current.copy( - lineHeight = 10.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - } -} - -@Composable -fun SearchOutsideProgram(resultText: String, buttonText: String, onSearchOutsideClick: () -> Unit) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = resultText, - fontSize = 14.sp, - color = Color.Black.copy(alpha = 0.38f), - style = LocalTextStyle.current.copy( - lineHeight = 10.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - Spacer(modifier = Modifier.size(16.dp)) - Button( - onClick = onSearchOutsideClick, - border = BorderStroke(1.dp, colorResource(id = R.color.colorPrimary)), - colors = ButtonDefaults.buttonColors( - backgroundColor = colorResource(id = R.color.white), - ), - ) { - Icon( - painter = painterResource(id = R.drawable.ic_search), - contentDescription = "", - tint = colorResource(id = R.color.colorPrimary), - ) - Spacer(modifier = Modifier.size(16.dp)) - Text(text = buttonText, color = colorResource(id = R.color.colorPrimary)) - } - } -} - -@Composable -fun NoMoreResults() { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = stringResource(R.string.string_no_more_results), - fontSize = 14.sp, - color = Color.Black.copy(alpha = 0.38f), - style = LocalTextStyle.current.copy( - lineHeight = 10.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - } -} - -@Composable -fun NoResults() { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Image( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_empty_folder), - contentDescription = "", - ) - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = stringResource(R.string.search_no_results), - fontSize = 17.sp, - color = Color.Black.copy(alpha = 0.38f), - style = LocalTextStyle.current.copy( - lineHeight = 24.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - } -} - -@Composable -fun TooManyResults() { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Image( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_too_many), - contentDescription = "", - ) - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = stringResource(R.string.search_too_many_results), - fontSize = 17.sp, - color = colorResource(id = R.color.pink_500), - style = LocalTextStyle.current.copy( - lineHeight = 24.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - Text( - text = stringResource(R.string.search_too_many_results_message), - fontSize = 17.sp, - color = Color.Black.copy(alpha = 0.38f), - style = LocalTextStyle.current.copy( - lineHeight = 24.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - } -} - -@Composable -fun SearchOrCreate() { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Image( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_searchvscreate), - contentDescription = "", - ) - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = stringResource(R.string.search_or_create), - fontSize = 17.sp, - color = Color.Black.copy(alpha = 0.38f), - style = LocalTextStyle.current.copy( - lineHeight = 24.sp, - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - } -} - -@ExperimentalAnimationApi -@Composable -fun CreateNewButton(modifier: Modifier, extended: Boolean = true, onClick: () -> Unit) { - Button( - modifier = modifier - .wrapContentWidth() - .height(56.dp), - contentPadding = PaddingValues(16.dp), - onClick = onClick, - colors = ButtonDefaults.buttonColors(backgroundColor = Color.White), - shape = RoundedCornerShape(16.dp), - elevation = ButtonDefaults.elevation(), - ) { - Icon( - modifier = Modifier.size(24.dp), - painter = painterResource(id = R.drawable.ic_add_accent), - contentDescription = "", - tint = colorResource(id = R.color.colorPrimary), - ) - AnimatedVisibility(visible = extended) { - Row { - Spacer(modifier = Modifier.size(12.dp)) - Text( - text = stringResource(R.string.search_create_new), - color = colorResource(id = R.color.colorPrimary), - ) - } - } - } -} - -@ExperimentalAnimationApi -@Preview(showBackground = true, backgroundColor = 0x2C98F0) -@Composable -fun SearchFullWidthPreview() { - FullSearchButton(modifier = Modifier) { - } -} - -@Preview(showBackground = true, backgroundColor = 0x2C98F0) -@Composable -fun SearchWrapWidthPreview() { - WrappedSearchButton { - } -} - -@ExperimentalAnimationApi -@Preview -@Composable -fun ExtendedCreateNewButtonPreview() { - CreateNewButton(modifier = Modifier) {} -} - -@ExperimentalAnimationApi -@Preview -@Composable -fun CreateNewButtonPreview() { - CreateNewButton(modifier = Modifier, extended = false) {} -} - -@Preview(showBackground = true) -@Composable -fun LoadingMoreResultsPreview() { - LoadingContent(loadingDescription = "Loading more results...") -} - -@Preview(showBackground = true) -@Composable -fun SearchOutsidePreview() { - SearchResult(searchResultType = SearchResult.SearchResultType.SEARCH_OUTSIDE) {} -} - -@Preview(showBackground = true) -@Composable -fun NoMoreResultsPreview() { - SearchResult(searchResultType = SearchResult.SearchResultType.NO_MORE_RESULTS) {} -} - -@Preview(showBackground = true) -@Composable -fun TooManyResultsPreview() { - SearchResult(searchResultType = SearchResult.SearchResultType.TOO_MANY_RESULTS) {} -} - -@Preview(showBackground = true) -@Composable -fun NoResultsPreview() { - SearchResult(searchResultType = SearchResult.SearchResultType.NO_RESULTS) {} -} - -@Preview(showBackground = true) -@Composable -fun SearchOrCreatePreview() { - SearchResult(searchResultType = SearchResult.SearchResultType.SEARCH_OR_CREATE) {} -} diff --git a/app/src/main/java/org/dhis2/utils/customviews/BreakTheGlassBottomDialog.kt b/app/src/main/java/org/dhis2/utils/customviews/BreakTheGlassBottomDialog.kt index 02d18c824b..7f68d77e86 100644 --- a/app/src/main/java/org/dhis2/utils/customviews/BreakTheGlassBottomDialog.kt +++ b/app/src/main/java/org/dhis2/utils/customviews/BreakTheGlassBottomDialog.kt @@ -1,5 +1,6 @@ package org.dhis2.utils.customviews +import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -15,13 +16,11 @@ import org.dhis2.commons.resources.ColorType import org.dhis2.commons.resources.ColorUtils import org.dhis2.commons.resources.ResourceManager import org.dhis2.databinding.BreakTheGlassBottomDialogBindingImpl -import javax.inject.Inject class BreakTheGlassBottomDialog : BottomSheetDialogFragment() { private lateinit var programUid: String - @Inject lateinit var resourceManager: ResourceManager val colorUtils: ColorUtils = ColorUtils() @@ -42,11 +41,19 @@ class BreakTheGlassBottomDialog : BottomSheetDialogFragment() { isCancelable = false } + override fun onAttach(context: Context) { + super.onAttach(context) + resourceManager = ResourceManager( + context, + colorUtils, + ) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { + ): View { return BreakTheGlassBottomDialogBindingImpl.inflate(inflater, container, false).apply { message.text = resourceManager.formatWithEnrollmentLabel( programUid, diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 732102f002..2148777b1d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -44,6 +44,8 @@ %s resultados Agregar nuevo Añadir + Introduce %s + Introduce / Salta %s Programar Remitir @@ -76,6 +78,9 @@ Buscar + Buscar / Añadir nuevo %s + Buscar %s + Añadir nuevo %s Introducir texto Introducir texto largo Introducir numero @@ -92,6 +97,8 @@ Programa suspendido Abrir + No completado + Evento completado Sólo lectura Acabar y completar Completado @@ -143,6 +150,7 @@ Total Fecha de evento más reciente + No hay opciones disponibles Listado de admisiones Programas activos Programas disponibles para inscripción @@ -153,18 +161,22 @@ Ver detalles Ver mas + Editar Seguimiento Seguimiento activado Seguimiento desactivado Desactivar Este programa no admite más eventos + No hay datos Eventos Inicio Información de la App Cerrar Sesion Bloquear sesion + Mostrar %s más... + Mostrar menos... 1 minuto 15 minutos 30 minutos @@ -190,6 +202,7 @@ Modulo SMS: SMS gateway Emisor del SMS de resultado + "Tiempo de espera de recepción del SMS " segundos numero numero @@ -224,6 +237,10 @@ Verifique los parámetros relacionados con la SMS gateway + Actualización de software + Software + Exportar base de datos + Exporta tu base de datos y compártala con su administrador Fecha de inscripción @@ -375,6 +392,7 @@ Eventos Completados Todos los eventos de este programa están completados. ¿Quiere completar el programa también? ¿ Completar también la inscripción? + Le gustaría completar el %s también? Ver Inicio @@ -471,6 +489,9 @@ Debe seleccionar al menos una unidad organizativa. Enviar datos Ayude a DHIS2 a mejorar esta app compartiendo errores asociados a su usuario. + Quiere ayudarnos a mejorar esta app? + "Si dice que sí la app mandará estadísticas anónimas y registro de errores que ayudaran a mejorar la experiencia y el rendimiento. Para más detalles, consulte nuestra política de privacidad. " + política de privacidad Comprobar campos Se perdió la conexión a internet durante la sincronización. Inténtelo mas tarde. Confirmar @@ -822,6 +843,7 @@ Demasiados resultados Intente cambiar su búsqueda para restringir los resultados. Crear nuevo + Nuevo %s Es necesario que ingrese al menos %s atributos para iniciar la búsqueda De acuerdo Empieza una búsqueda para encontrar %s @@ -841,6 +863,7 @@ Dispositivo: %s Versión del SO: %s Abrir detalles de %s + Detalles de %s Sincronizando %s... SMS enviado. Esperando respuesta del servidor. Se ha confirmado que el SMS llegó al servidor. Actualizando estado @@ -849,11 +872,85 @@ SMS no enviado. Abriendo aplicación de SMS. Por favor, no modifique el mensaje. Enviar SMS con + ¿Ha enviado el sms? Esperando confirmación manual + Sincronizando eventos + Sincronizando entidades de ratreo + Sincronizando set de datos + Sincronizando recursos + Sincronización finalizada + La sincronización por SMS está habilitada pero debe acceder a ella en cada registro individualmente. + Descargando imágenes... + Descripción del programa + Pin erróneo Inscrito en: + Sincronización necesaria + Error de sincroniación Sincronizado + Advertencia de sincronización Sincronizando + SMS enviado + ¿Quiere mandar todos sus cambios? + ¿Quiere mandar sus cambios de %s? + ¿Quiere mandar sus cambios de este %s? + ¿Quiere comprobar si existen cambios? + ¿Quiere comprobar si existen cambios en %s? + ¿Quiere comprobar si existen cambios para este %s? + ¿Quiere mandar el SMS de nuevo? Enviar Actualizar Ahora no - + Toque aquí para explorar + La versión %s de la app está ahora disponible. ¿Quiere instalarla? + Actualización de software + Más tarde + Descargar ahora + Buscar actualizaciones + Buscando actualizaciones + No hay nuevas actualizaciones + El permiso para fuentes desconocidas está deshabilitado. Habilítelo para instalar esta versión. + El permiso de almacenamiento está deshabilitado. Habilítelo para instalar esta versión. + Versión de la app + Hay algún error en la configuraciónd el programa, no se puede cargar el evento. Por favor contacte a su administrador. + Eliminar persona + Permiso de notificaciones está deshabilitado. + Permiso de notificaciones concedido. + No sincronizaco + Reintentar sincronización + Marcado para seguimiento + %s atrasado + Registrado en: + Solo vista + Programado: %s + Programado para %s + Omitido + Programas: + Sincronización existosa + Aviso en la sincronización + Error de sincronización + Marcar para seguimiento + Eliminar + Coordenadas + No tiene acceso a los datos. Contacte con su administrador. + Parece que está offline. Compruebe su conexión. + ¿Eliminar este %s? + Este %s y todos sus datos de todos los programas serán eliminados. Esta acción no puede dehacerse. + ¿Eliminar de %s? + Los datos del %s serán elimiados. Esta acción no puede deshacerse. + Programar el siguiente + Programar + Etapa + evento + %d eventos + Línea de tiempo + Limpiar búsqueda + Opcional + Base de datos descargada + Importando base de datos + Reabrir el formulario para editarlo + Mostrar más + Mostrar menos + Sincronizar + Mostrar campos + Ocultar campos + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6321e1aa7c..c0bce83893 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - + Dhis2 dhis @@ -243,7 +243,7 @@ Software update Software Export database - Export yout database and share it with your administrator + Export your database and share it with your administrator Enrollment date @@ -495,7 +495,7 @@ Send data Help DHIS2 improve this app by sharing error logs associated to your user id. Do you want to help us improve this app? - If you say yes the app will send anonymous statistics and error logs which help us improve user experience and performance.\n\nFor further details, please refer to our privacy policy. + If you say yes the app will send anonymous statistics and error logs which help us improve user experience and performance. For further details, please refer to our privacy policy. privacy policy Check fields You lost your internet connection while performing your configuration sync. Please try again later @@ -700,7 +700,7 @@ Complete anyway Missing %s %s from %s %s Event status - @string/enrollment_status + @string/enrollment_status Enrollment date There are no data. Click \"+\" to add new a new record There are no data. You don\'t have permission to add new records. @@ -925,7 +925,7 @@ %s warning %s warnings - App version %s is now available.\nDo you want to download it? + App version %s is now available. Do you want to download it? "Software update" Later Download now @@ -972,5 +972,10 @@ Database downloaded Importing database Re-open form to edit + Show more + Show less + Sync + Show fields + Hide fields Import successful diff --git a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt new file mode 100644 index 0000000000..7374d434d9 --- /dev/null +++ b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt @@ -0,0 +1,90 @@ +package org.dhis2.usescases.searchTrackEntity + +import androidx.paging.PagingData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.dhis2.commons.resources.MetadataIconProvider +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.data.search.SearchParametersModel +import org.dhis2.form.ui.FieldViewModelFactory +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchCollectionRepository +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.verify +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@OptIn(ExperimentalCoroutinesApi::class) +class SearchRepositoryTest { + private val searchRepositoryJava: SearchRepository = mock() + + private val d2: D2 = mock(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) + + private val dispatcher: DispatcherProvider = mock() + + private val fieldViewModelFactory: FieldViewModelFactory = mock() + + private val metadataIconProvider: MetadataIconProvider = mock() + + private lateinit var searchRepositoryImplKt: SearchRepositoryImplKt + + private val testDispatcher = UnconfinedTestDispatcher() + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + searchRepositoryImplKt = SearchRepositoryImplKt( + searchRepositoryJava, + d2, + dispatcher, + fieldViewModelFactory, + metadataIconProvider, + ) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `test searchTrackedEntities returns PagingData flow`() = runTest { + val searchParametersModel: SearchParametersModel = SearchParametersModel( + selectedProgram = null, + queryData = null, + ) + val trackedEntitySearchCollectionRepository: TrackedEntitySearchCollectionRepository = + mock() + val pagingDataFlow = flowOf(PagingData.empty()) + + whenever(searchRepositoryJava.getFilteredRepository(searchParametersModel)) doReturn trackedEntitySearchCollectionRepository + whenever(trackedEntitySearchCollectionRepository.allowOnlineCache()) doReturn mock() + whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false)) doReturn mock() + whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false).offlineFirst()) doReturn mock() + whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false).offlineOnly()) doReturn mock() + whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false).offlineOnly().getPagingData(10)) doReturn mock() + whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false).offlineFirst().getPagingData(10)) doReturn mock() + whenever(trackedEntitySearchCollectionRepository.getPagingData(10)) doReturn pagingDataFlow + + val result = + searchRepositoryImplKt.searchTrackedEntities(searchParametersModel, isOnline = true) + + result.collect { pagingData -> + assertTrue(pagingData is PagingData) + } + + verify(searchRepositoryJava).getFilteredRepository(searchParametersModel) +// verify(trackedEntitySearchCollectionRepository).getPagingData(10) + } +} diff --git a/commons/src/main/res/values-es/strings.xml b/commons/src/main/res/values-es/strings.xml index 5f60ed38e4..cb38784c3b 100644 --- a/commons/src/main/res/values-es/strings.xml +++ b/commons/src/main/res/values-es/strings.xml @@ -42,7 +42,69 @@ Asignado a mí Fecha Fecha de evento - Fecha de inscripción + La conexión de red no está disponible + Se requiere conexión a internet para usar mapas. + Hoy + + %d día atrasado + %d días atrasados + %d días atrasados + + + %d mes atrasado + %d meses atrasados + %d meses atrasados + + + %d año atrasado + %d años atrasados + %d años atrasados + + + + En %d día + En %d días + En %d días + + + En %d mes + En %d meses + En %d meses + + + En %d año + En %d años + En %d años + + Abrir con + Archivo descargado con éxito + Mostrar coordenadas de %s + Datos de %s + Coordenadas de %s + Fecha de %s + Estado de %s + Es necesario proveer la razón para consultar este %s en este programa protegido. Toda actividad será registrada. + Razón para consultar %s + Los datos no son editables por que el %s está completado o cancelado + %s completado + %s cancelado + Nuevo %s + Registrado en: + Eliminar solo %s + %s del programa + %s completado + %s creado + Nuevo %s + Fecha de %s + Eliminar %s + %s eliminado con éxito + Éste %s aún no ha sido creado + Éste %s ha sido omitido + ¿Está seguro de que desea eliminar éste %s? + %s actualizado + Éste %s no puede tener una fecha a futuro. Cámbielo por favor. + Error al insertar un nuevo %s + Semana %d %s a %s Estado del evento Estado de la inscripción. Seguido %s @@ -202,8 +264,12 @@ No hay suficientes atributos para realizar la búsqueda. Por favor redefina la búsqueda añadiendo más atributos. La búsqueda online solo está disponible para las unidades organizativas de búsqueda. El query contiene caracteres invalidos. Por favor contacte a su administrador para verificar la configuración del servidor (relaxedQueryChars,...). + El servidor no está disponible. Pruebe más tarde. + Está desconectado. Habilite su conexión a internet y pruebe otra vez. Este campo no está disponible para la captura por móvil. Añadir imagen + Añadir firma + Añadir archivo De acuerdo La ubicación de su dispositivo está desconectada. Por favor habilítela para capturar coordenadas. Seguir editando diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0e0aef2426..5cc97ce439 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,8 @@ ndk = "21.4.7075529" sdk = "34" minSdk = "21" -vCode = "133" -vName = "3.0" +vCode = "134" +vName = "3.0.1" kotlinCompilerExtensionVersion = "1.5.6" gradle = "8.2.2" kotlin = '1.9.21' @@ -11,7 +11,7 @@ hilt = '2.47' hiltCompiler = '1.0.0' jacoco = '0.8.10' designSystem = "0.3.0-SNAPSHOT" -dhis2sdk = "1.10.0" +dhis2sdk = "1.10.1-SNAPSHOT" ruleEngine = "3.0.0" expressionParser = "1.1.0" appcompat = "1.6.1" diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index 7be5c2dad9..19cb83d747 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,11 +1,9 @@ -- Event and enrollment label can be customized via the Maintenance App. -- Supports multiselect data elements. -- Support for custom icons. -- Event/enrollment details are now more accessible within forms. -- Improved search flow and dashboard layout. -- Redesigned input fields. -- Support line listing analytics. -- Custom map layers can be used. -- Export/import databases for troubleshooting. -- Better feedback for configuration errors. -- Better navigation for many TEIs \ No newline at end of file +# Release notes - Android App for DHIS2 - 3.0.0.1 + +### Bug + +[ANDROAPP-6194](https://dhis2.atlassian.net/browse/ANDROAPP-6194) Unable to search outside the program + +[ANDROAPP-6195](https://dhis2.atlassian.net/browse/ANDROAPP-6195) Missing terms in transifex + +[ANDROAPP-6210](https://dhis2.atlassian.net/browse/ANDROAPP-6210) UninitializedPropertyAccessException on breaking the glass \ No newline at end of file From 06e735485d8bedc8f5736aaab3bfcaa4e2268c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Miguel=20Rubio?= Date: Tue, 18 Jun 2024 14:47:30 +0200 Subject: [PATCH 2/2] Revert "Release/3.0.0.1 (#3683)" This reverts commit 1356c64a484147247ca4a174b8ef23ba22a52b27. --- .tx/config | 13 - RELEASE.md | 105 ++++- .../mockwebserver/MockWebServerRobot.kt | 9 - .../flow/searchFlow/SearchFlowTest.kt | 14 - .../usescases/flow/teiFlow/TeiFlowTest.kt | 14 - .../dhis2/usescases/searchte/SearchTETest.kt | 57 +-- .../teilist/old_events_empty_response.json | 9 - .../old_tracked_entity_empty_response.json | 62 --- .../searchTrackEntity/SearchRepository.java | 11 +- .../SearchRepositoryImpl.java | 31 +- .../SearchRepositoryImplKt.kt | 31 +- .../searchTrackEntity/SearchTEIViewModel.kt | 8 +- .../usescases/searchTrackEntity/SearchTEUi.kt | 389 ++++++++++++++++++ .../customviews/BreakTheGlassBottomDialog.kt | 13 +- app/src/main/res/values-es/strings.xml | 99 +---- app/src/main/res/values/strings.xml | 15 +- .../searchTrackEntity/SearchRepositoryTest.kt | 90 ---- commons/src/main/res/values-es/strings.xml | 68 +-- gradle/libs.versions.toml | 6 +- whatsnew/whatsnew-en-US | 20 +- 20 files changed, 548 insertions(+), 516 deletions(-) delete mode 100644 app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json delete mode 100644 app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json create mode 100644 app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEUi.kt delete mode 100644 app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt diff --git a/.tx/config b/.tx/config index d17aec75f4..5529965f6d 100644 --- a/.tx/config +++ b/.tx/config @@ -44,16 +44,3 @@ source_lang = en type = ANDROID minimum_perc = 0 -[o:hisp-uio:p:dhis2-android-capture-app:r:ui-strings-xml] -file_filter = ui-components/src/main/res/values-/strings.xml -source_file = ui-components/src/main/res/values/strings.xml -source_lang = en -type = ANDROID -minimum_perc = 0 - -[o:hisp-uio:p:dhis2-android-capture-app:r:table-strings-xml] -file_filter = compose-table/src/main/res/values-/strings.xml -source_file = compose-table/src/main/res/values/strings.xml -source_lang = en -type = ANDROID -minimum_perc = 0 diff --git a/RELEASE.md b/RELEASE.md index 19cb83d747..f0c1788bed 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,9 +1,104 @@ -# Release notes - Android App for DHIS2 - 3.0.0.1 + + + + + + + +
+The new DHIS2 Android App allows offline data capture across all DHIS2 data models. Data and metadata are automatically synchronized whenever there is internet access, always keeping the most relevant data for the logged user in the device. +The app is compatible and we support 41, 40, 2.39. +
-### Bug +**Cross product** -[ANDROAPP-6194](https://dhis2.atlassian.net/browse/ANDROAPP-6194) Unable to search outside the program +[**Support for customized Tracker terminology**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas?selectedIssue=ROADMAP-201)**:** Some DHIS2 terminology is not familiar for the end users. For this reason, we are gradually enabling the possibility to customize it to each particular use case. In this version, the term "event" and "enrollment" are customizable. The admin user will be able to configure it for each program using the Maintenance App, and the Android Capture App will display the customized term instead of the generic one. -[ANDROAPP-6195](https://dhis2.atlassian.net/browse/ANDROAPP-6195) Missing terms in transifex +**Documentation link:** -[ANDROAPP-6210](https://dhis2.atlassian.net/browse/ANDROAPP-6210) UninitializedPropertyAccessException on breaking the glass \ No newline at end of file +[**Multiselect value type:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-72\&issueViewSection=comments) DHIS2 already supports the introduction of multiple options for data elements for data aggregation. In this version it will also support it for individual data. The Android App will support both aggregated and individual multi select data elements from this version. + +**Documentation link:** + +[**Custom Icons**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-207): DHIS2 now supports uploading custom Icons to be used in addition to the built in Icon library. This is useful for use cases not related to health or that require very specific iconography. The Android App will render the custom icons that need to be uploaded and configured using the Maintenance DHIS2 Web App. + +**Documentation link:** + + + +**User Experience** + +[**Improvements in forms layout**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas?selectedIssue=ROADMAP-204)**:** When creating an event or an enrollment, there are a number of fields that are not data elements or attributes, for example, event date, org unit, coordinates, enrollment date, category combinations. Those elements are referred to as event/enrollment details and in previous versions they were displayed in different screens separated from the data elements or attributes. They were difficult to find when users wanted to edit or consult them. In this version the details are displayed inside the form, as the first opened section for completion. Once they are filled in, for example when the user reopens the event or enrollment form, the details section will be visible and easily available, but collapsed to leave more space for the data collection.  + +**Documentation link:** + +[**Improve TEI search user experience**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas?selectedIssue=ROADMAP-203)**:** The search form has been improved to provide a cleaner look and a more intuitive user experience. The buttons have been made more explicit for differencing search from creation. In addition the flow for searching TEIs using attributes rendered as bar / QR codes has been made more agile. If there is only one result and the attribute is unique, the app will open the TEI Dashboard directly. If there are multiple results, the app will display all the cards on the TEI list (this is equal to the current workflow), and if there are no results, the app will display the create button and allow the user to “search outside the program” if the configuration allows it. + +**Documentation link:** + + + +[**Improve TEI dashboard user experience**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas?selectedIssue=ROADMAP-205)**:** In the previous version of the application, the TEI header part of the dashboard was improved. In continuation to that effort, the bottom part, where all program stages are displayed, has been redesigned in this version. The changes include a fresh and more clean look of the list of events, with more space and less -not critical- information displayed. In addition, the button for creating new events has been moved to the top (in timeline view). + +**Documentation link:** + +[**New inputs for value types:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-293) ****The inputs for all value types have been gradually redesigned from the 2.9 version of the app. The signature input field as well as the complete legend description are included now to improve user experience at data entry. The new input fields are now displayed by default and admin users are able to opt-out to use the old forms through the Android Settings web app. + +**Documentation link:** + +[https://docs.dhis2.org/en/use/android-app/visual-configurations.html#capture\_app\_visual\_legends\_descri2ptions](https://docs.dhis2.org/en/use/android-app/visual-configurations.html#capture_app_visual_legends_descriptions) + + + +[**Improvements in tracker programs data entry flow:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-294) ****Several improvements have been made in the tracker programs user flow. An informative dialog has been added for confirmation when the user deletes a TEIs. The dialog for scheduling events after compilation has also been redesigned and improved. The selection of org. Units when the user only has access to one org. Unit for data collection has been removed and pre-filled, and lastly, the program rule “Hide program stage” behavior has been aligned with Capture web. + +**Documentation link:** + + + +[**Improve App behavior when working in offline mode:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-295) ****Some parts of the Android App are not responsive when there is no connection available (i.e. sync buttons…). The behavior is now improved and the App will inform the user that actions are not started because there is no internet connection available when buttons that require connection are tapped. + +**Documentation link:** NA + +**New functionality and Web Parity** + +[**Line listing analytics**](https://dhis2.atlassian.net/browse/ROADMAP-206): This version of the Android App includes the possibility to render and display line listing as part of the offline analytics functionalities. The line list has to be created using the Line Listing DHIS2 web App, and then configured to be displayed in Android using the Android Settings Web App (ASWA), as any other offline analytics in Android. In this case Line lists can be displayed in the home screen, and event or tracker programs (as they do not really apply to aggregated data, they are not displayed in Datasets). Users will be able to search by period, Org. unit, or any of the columns added in the Line List. + +There are some limitations to the line lists to be displayed in the Android App. The Org. units and Periods must be relative, not fixed. And there is a maximum number of columns of 15.  + +Android Local Analytics are built using local data, and wil, update instantly as more data gets collected (or downloaded) in the device. The App will display a maximum of 500 rows and will inform the user when the limit is reached. + +**Documentation link:** + +\ + + +[**Configurable basemap layer:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-209\&issueViewSection=deliver) DHIS2 supports the configuration of custom map layers for the Maps Web App. From this version of the DHIS2 Android App, those custom layers will be downloaded and rendered in the Maps. The custom layers will be presented as additional layers to the default ones. + +**Documentation link:** + +**Implementation Support** + +[**Import/Export App database:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-210) ****As part of troubleshooting, some errors can be hard to replicate and can lead to data loss because of being unable to sync. With this functionality the end user will be able to export the local database and share it with an admin who will be able to import it for troubleshooting, being able to replicate the exact environment (database, device, configuration). The exported database is encrypted and the administrator will require the user credentials to be able to access the database. + +**Documentation link:** + +[**Improve end-user config error feedback:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-296) ****In some cases configuration errors leave empty screens in the Android Capture app, either because of empty forms or because of lack of access. From this version of the Android App the app will display explicit and understandable errors to the user, who will be able to effectively communicate with the administrator to fix the problem. + +**Documentation link: NA** + +  + +**Maintenance/Performance** + +[**Improve app navigation performance for high number of TEIs:**](https://dhis2.atlassian.net/jira/polaris/projects/ROADMAP/ideas/view/4066207?selectedIssue=ROADMAP-208) ****Implementations are more and more demanding in terms of offline need of individual records. This version of the app has been reviewed to optimize performance when there are big numbers of TEIs downloaded locally. **** + +**Documentation link: NA** + +You can find in Jira details on the [new features](https://dhis2.atlassian.net/issues/?filter=10640) and [bugs fixed](https://dhis2.atlassian.net/issues/?filter=10641) in this version. + +Remember to check the [documentation](https://www.dhis2.org/android-documentation) for detailed information of the features included in the App and how to configure DHIS2 to use it. + +Please create a [Jira](https://dhis2.atlassian.net) Issue if you find a bug or you want to propose a new functionality. [Project: Android App for DHIS2 | Component: +AndroidApp]. +
\ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt b/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt index cd07f23e3d..dc76fafbb7 100644 --- a/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt +++ b/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt @@ -15,13 +15,4 @@ class MockWebServerRobot(private val dhis2MockServer: Dhis2MockServer) { fun addResponse(method: String, path: String, sdkResource: String, responseCode: Int = 200) { dhis2MockServer.addResponse(method, path, sdkResource, responseCode) } - - companion object { - const val API_OLD_TRACKED_ENTITY_PATH = "/api/trackedEntityInstances/query?.*" - const val API_OLD_TRACKED_ENTITY_RESPONSE = - "mocks/teilist/old_tracked_entity_empty_response.json" - const val API_OLD_EVENTS_PATH = "/api/events?.*" - const val API_OLD_EVENTS_RESPONSE = "mocks/teilist/old_events_empty_response.json" - - } } 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 4b26bb4f14..5b7feb4bbc 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 @@ -7,14 +7,11 @@ import androidx.compose.ui.text.intl.Locale import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule import org.dhis2.R -import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_PATH -import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_RESPONSE import org.dhis2.usescases.BaseTest import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel import org.dhis2.usescases.flow.teiFlow.entity.RegisterTEIUIModel import org.dhis2.usescases.flow.teiFlow.teiFlowRobot import org.dhis2.usescases.searchTrackEntity.SearchTEActivity -import org.hisp.dhis.android.core.mockwebserver.ResponseController import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -31,19 +28,8 @@ class SearchFlowTest : BaseTest() { private val dateRegistration = createFirstSpecificDate() private val dateEnrollment = createEnrollmentDate() - override fun setUp() { - super.setUp() - setupMockServer() - } - @Test fun shouldCreateTEIAndFilterByEnrollment() { - mockWebServerRobot.addResponse( - ResponseController.GET, - API_OLD_TRACKED_ENTITY_PATH, - API_OLD_TRACKED_ENTITY_RESPONSE, - ) - setDatePicker() val registerTEIDetails = createRegisterTEI() val enrollmentStatus = context.getString(R.string.filters_title_enrollment_status) 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 e7f603a2f5..b59452babd 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 @@ -4,15 +4,12 @@ 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.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_PATH -import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_RESPONSE import org.dhis2.usescases.BaseTest 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.hisp.dhis.android.core.mockwebserver.ResponseController import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -36,19 +33,8 @@ class TeiFlowTest : BaseTest() { private val dateEnrollment = createEnrollmentDate() private val currentDate = getCurrentDate() - override fun setUp() { - super.setUp() - setupMockServer() - } - @Test fun shouldEnrollToSameProgramAfterClosingIt() { - mockWebServerRobot.addResponse( - ResponseController.GET, - API_OLD_TRACKED_ENTITY_PATH, - API_OLD_TRACKED_ENTITY_RESPONSE, - ) - val totalEventsPerEnrollment = 3 val enrollmentListDetails = createEnrollmentList() val registerTeiDetails = createRegisterTEI() 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 99aebeb245..3472dbc780 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt @@ -15,10 +15,6 @@ import dispatch.android.espresso.IdlingDispatcherProviderRule import org.dhis2.R import org.dhis2.bindings.app import org.dhis2.common.idlingresources.MapIdlingResource -import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_EVENTS_PATH -import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_EVENTS_RESPONSE -import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_PATH -import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_RESPONSE import org.dhis2.commons.date.DateUtils.SIMPLE_DATE_FORMAT import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest @@ -30,13 +26,13 @@ import org.dhis2.usescases.searchte.entity.DisplayListFieldsUIModel import org.dhis2.usescases.searchte.robot.filterRobot import org.dhis2.usescases.searchte.robot.searchTeiRobot import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot -import org.hisp.dhis.android.core.mockwebserver.ResponseController import org.junit.After import org.junit.Ignore 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) @@ -59,19 +55,8 @@ class SearchTETest : BaseTest() { @get:Rule val composeTestRule = createComposeRule() - override fun setUp() { - super.setUp() - setupMockServer() - } - @Test fun shouldSuccessfullySearchByName() { - mockWebServerRobot.addResponse( - ResponseController.GET, - API_OLD_TRACKED_ENTITY_PATH, - API_OLD_TRACKED_ENTITY_RESPONSE, - ) - val firstName = "Tim" val lastName = "Johnson" @@ -91,12 +76,6 @@ class SearchTETest : BaseTest() { @Test fun shouldShowErrorWhenCanNotFindSearchResult() { - mockWebServerRobot.addResponse( - ResponseController.GET, - API_OLD_TRACKED_ENTITY_PATH, - API_OLD_TRACKED_ENTITY_RESPONSE, - ) - val firstName = "asdssds" prepareTestProgramRulesProgrammeIntentAndLaunchActivity(rule) @@ -112,12 +91,6 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullySearchUsingMoreThanOneField() { - mockWebServerRobot.addResponse( - ResponseController.GET, - API_OLD_TRACKED_ENTITY_PATH, - API_OLD_TRACKED_ENTITY_RESPONSE, - ) - val firstName = "Anna" val lastName = "Jones" @@ -154,12 +127,6 @@ class SearchTETest : BaseTest() { @Test fun shouldCheckDisplayInList() { - mockWebServerRobot.addResponse( - ResponseController.GET, - API_OLD_TRACKED_ENTITY_PATH, - API_OLD_TRACKED_ENTITY_RESPONSE, - ) - val displayInListData = createDisplayListFields() prepareTestAdultWomanProgrammeIntentAndLaunchActivity(rule) @@ -204,16 +171,6 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullyFilterByEventStatusOverdue() { - mockWebServerRobot.addResponse( - ResponseController.GET, - API_OLD_TRACKED_ENTITY_PATH, - API_OLD_TRACKED_ENTITY_RESPONSE, - ) - mockWebServerRobot.addResponse( - ResponseController.GET, - API_OLD_EVENTS_PATH, - API_OLD_EVENTS_RESPONSE, - ) enableComposeForms() val eventStatusFilter = context.getString(R.string.filters_title_event_status) val totalCount = "1" @@ -321,12 +278,6 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullyFilterBySync() { - mockWebServerRobot.addResponse( - ResponseController.GET, - API_OLD_TRACKED_ENTITY_PATH, - API_OLD_TRACKED_ENTITY_RESPONSE, - ) - val teiName = "Frank" val teiLastName = "Fjordsen" val syncFilter = context.getString(R.string.action_sync) @@ -362,12 +313,6 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullySearchAndFilter() { - mockWebServerRobot.addResponse( - ResponseController.GET, - API_OLD_TRACKED_ENTITY_PATH, - API_OLD_TRACKED_ENTITY_RESPONSE, - ) - val name = "Anna" val lastName = "Jones" val enrollmentStatus = context.getString(R.string.filters_title_enrollment_status) diff --git a/app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json b/app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json deleted file mode 100644 index c03b0880c9..0000000000 --- a/app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "pager": { - "page": 1, - "pageCount": 1, - "total": 2, - "pageSize": 50 - }, - "events": [] -} \ No newline at end of file diff --git a/app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json b/app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json deleted file mode 100644 index e5104f914d..0000000000 --- a/app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "headers": [ - { - "name": "instance", - "column": "Instance", - "type": "java.lang.String", - "hidden": false, - "meta": false - }, - { - "name": "created", - "column": "Created", - "type": "java.lang.String", - "hidden": false, - "meta": false - }, - { - "name": "lastupdated", - "column": "Last updated", - "type": "java.lang.String", - "hidden": false, - "meta": false - }, - { - "name": "ou", - "column": "Organisation unit", - "type": "java.lang.String", - "hidden": false, - "meta": false - }, - { - "name": "ouname", - "column": "Organisation unit name", - "type": "java.lang.String", - "hidden": false, - "meta": false - }, - { - "name": "te", - "column": "Tracked entity type", - "type": "java.lang.String", - "hidden": false, - "meta": false - }, - { - "name": "inactive", - "column": "Inactive", - "type": "java.lang.String", - "hidden": false, - "meta": false - } - ], - "metaData": { - "names": { - "nEenWmSyUEp": "Person" - } - }, - "width": 9, - "height": 0, - "rows": [ - ] -} \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java index 8691eea1fd..ae624c091c 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java @@ -6,7 +6,6 @@ import org.dhis2.commons.data.EventViewModel; import org.dhis2.commons.data.SearchTeiModel; import org.dhis2.commons.data.tuples.Pair; -import org.dhis2.commons.filters.FilterManager; import org.dhis2.commons.filters.sorting.SortingItem; import org.dhis2.data.search.SearchParametersModel; import org.hisp.dhis.android.core.arch.call.D2Progress; @@ -18,16 +17,14 @@ import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem; import org.jetbrains.annotations.NotNull; +import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import io.reactivex.Flowable; import io.reactivex.Observable; -import kotlin.Deprecated; -@Deprecated(message = "Use SearchRepositoryKt instead") public interface SearchRepository { Observable> programsWithRegistration(String programTypeId); @@ -85,10 +82,4 @@ public interface SearchRepository { List trackedEntityTypeFields(); boolean filtersApplyOnGlobalSearch(); - - @NotNull HashSet getFetchedTeiUIDs(); - - SearchParametersModel getSavedSearchParameters(); - - FilterManager getSavedFilters(); } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java index 070bb93b60..ff0f8f0769 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java @@ -24,8 +24,8 @@ import org.dhis2.commons.filters.sorting.SortingItem; import org.dhis2.commons.network.NetworkUtils; import org.dhis2.commons.reporting.CrashReportController; -import org.dhis2.commons.resources.DhisPeriodUtils; import org.dhis2.commons.resources.MetadataIconProvider; +import org.dhis2.commons.resources.DhisPeriodUtils; import org.dhis2.commons.resources.ResourceManager; import org.dhis2.data.dhislogic.DhisEnrollmentUtils; import org.dhis2.data.forms.dataentry.SearchTEIRepository; @@ -43,6 +43,7 @@ import org.dhis2.utils.ValueUtils; import org.hisp.dhis.android.core.D2; import org.hisp.dhis.android.core.arch.call.D2Progress; +import org.hisp.dhis.android.core.arch.helpers.Result; import org.hisp.dhis.android.core.arch.helpers.UidsHelper; import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope; import org.hisp.dhis.android.core.common.FeatureType; @@ -56,6 +57,7 @@ import org.hisp.dhis.android.core.event.Event; import org.hisp.dhis.android.core.event.EventCollectionRepository; import org.hisp.dhis.android.core.event.EventStatus; +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; @@ -77,6 +79,7 @@ import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem; import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItemAttribute; import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItemHelper; +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -603,21 +606,6 @@ public boolean filtersApplyOnGlobalSearch() { !FilterManager.getInstance().getStateFilters().isEmpty(); } - @Override - public @NotNull HashSet getFetchedTeiUIDs() { - return fetchedTeiUids; - } - - @Override - public SearchParametersModel getSavedSearchParameters() { - return savedSearchParameters; - } - - @Override - public FilterManager getSavedFilters() { - return savedFilters; - } - @Override public Observable getTrackedEntityType(String trackedEntityUid) { return d2.trackedEntityModule().trackedEntityTypes().uid(trackedEntityUid).get().toObservable(); @@ -709,6 +697,17 @@ public TeiDownloadResult download(String teiUid, @Nullable String enrollmentUid, return teiDownloader.download(teiUid, enrollmentUid, reason); } + public SearchTeiModel transformResult(Result result, @Nullable Program selectedProgram, boolean offlineOnly, SortingItem sortingItem) { + try { + return transform(result.getOrThrow(), selectedProgram, offlineOnly, sortingItem); + } catch (Exception e) { + SearchTeiModel errorModel = new SearchTeiModel(); + errorModel.onlineErrorMessage = resources.parseD2Error(e); + errorModel.onlineErrorCode = ((D2Error) e).errorCode(); + return errorModel; + } + } + @Override public SearchTeiModel transform(TrackedEntitySearchItem searchItem, @Nullable Program selectedProgram, boolean offlineOnly, SortingItem sortingItem) { if (!fetchedTeiUids.contains(searchItem.uid())) { diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt index dc1a1470c7..20e0b4b7c5 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt @@ -30,8 +30,14 @@ class SearchRepositoryImplKt( private val metadataIconProvider: MetadataIconProvider, ) : SearchRepositoryKt { + private lateinit var savedSearchParamenters: SearchParametersModel + + private lateinit var savedFilters: FilterManager + private lateinit var trackedEntityInstanceQuery: TrackedEntitySearchCollectionRepository + private val fetchedTeiUids = HashSet() + override fun searchTrackedEntities( searchParametersModel: SearchParametersModel, isOnline: Boolean, @@ -45,23 +51,26 @@ class SearchRepositoryImplKt( isOnline: Boolean, ): TrackedEntitySearchCollectionRepository { var allowCache = false + savedSearchParamenters = searchParametersModel.copy() + savedFilters = FilterManager.getInstance().copy() - if (searchParametersModel != searchRepositoryJava.savedSearchParameters || !FilterManager.getInstance() - .sameFilters(searchRepositoryJava.savedFilters) + if (searchParametersModel != savedSearchParamenters || !FilterManager.getInstance() + .sameFilters(savedFilters) ) { trackedEntityInstanceQuery = searchRepositoryJava.getFilteredRepository(searchParametersModel) } else { - searchRepositoryJava.getFilteredRepository(searchParametersModel) + trackedEntityInstanceQuery = + searchRepositoryJava.getFilteredRepository(searchParametersModel) allowCache = true } - if (searchRepositoryJava.fetchedTeiUIDs.isNotEmpty() && searchParametersModel.selectedProgram == null) { + if (fetchedTeiUids.isNotEmpty() && searchParametersModel.selectedProgram == null) { trackedEntityInstanceQuery = - trackedEntityInstanceQuery.excludeUids().`in`(searchRepositoryJava.fetchedTeiUIDs.toList()) + trackedEntityInstanceQuery.excludeUids().`in`(fetchedTeiUids.toList()) } - val pagerFlow = if (isOnline && FilterManager.getInstance().stateFilters.isEmpty()) { + val pagerFlow = if (isOnline && FilterManager.getInstance().stateFilters.isNotEmpty()) { trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineFirst() } else { trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineOnly() @@ -122,8 +131,7 @@ class SearchRepositoryImplKt( options.associate { it.uid() to metadataIconProvider( it.style(), - program?.style()?.color()?.toColor() - ?: SurfaceColor.Primary, + program?.style()?.color()?.toColor() ?: SurfaceColor.Primary, ) } @@ -169,12 +177,7 @@ class SearchRepositoryImplKt( .blockingGet() val metadataIconMap = - options.associate { - it.uid() to metadataIconProvider( - it.style(), - SurfaceColor.Primary, - ) - } + options.associate { it.uid() to metadataIconProvider(it.style(), SurfaceColor.Primary) } OptionSetConfiguration.OptionConfigData( options = options, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt index 78b7e72282..38441502cd 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt @@ -373,7 +373,7 @@ class SearchTEIViewModel( suspend fun fetchGlobalResults() = withContext(dispatchers.io()) { val searchParametersModel = SearchParametersModel( - selectedProgram = null, + selectedProgram = searchRepository.getProgram(initialProgramUid), queryData = queryData, ) val getPagingData = searchRepositoryKt.searchTrackedEntities( @@ -927,7 +927,6 @@ class SearchTEIViewModel( ValueType.ORGANISATION_UNIT, ValueType.MULTI_TEXT -> { map[item.uid] = (item.displayName ?: "") } - ValueType.DATE, ValueType.AGE -> { item.value?.let { if (it.isNotEmpty()) { @@ -942,7 +941,6 @@ class SearchTEIViewModel( } } } - ValueType.DATETIME -> { item.value?.let { if (it.isNotEmpty()) { @@ -957,11 +955,9 @@ class SearchTEIViewModel( } } } - ValueType.BOOLEAN -> { map[item.uid] = "${item.label}: ${item.value}" } - ValueType.TRUE_ONLY -> { item.value?.let { if (it == "true") { @@ -969,11 +965,9 @@ class SearchTEIViewModel( } } } - ValueType.PERCENTAGE -> { map[item.uid] = "${item.value}%" } - else -> { map[item.uid] = (item.value ?: "") } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEUi.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEUi.kt new file mode 100644 index 0000000000..44f045cd1d --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEUi.kt @@ -0,0 +1,389 @@ +package org.dhis2.usescases.searchTrackEntity + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.dhis2.R +import org.dhis2.usescases.searchTrackEntity.listView.SearchResult + +@Composable +fun SearchResult( + searchResultType: SearchResult.SearchResultType, + onSearchOutsideClick: () -> Unit, +) { + when (searchResultType) { + SearchResult.SearchResultType.LOADING -> + LoadingContent( + loadingDescription = stringResource(R.string.search_loading_more), + ) + + SearchResult.SearchResultType.SEARCH_OUTSIDE -> SearchOutsideProgram( + resultText = stringResource(R.string.search_no_results_in_program), + buttonText = stringResource(R.string.search_outside_action), + onSearchOutsideClick = onSearchOutsideClick, + ) + + SearchResult.SearchResultType.NO_MORE_RESULTS -> NoMoreResults() + SearchResult.SearchResultType.TOO_MANY_RESULTS -> TooManyResults() + SearchResult.SearchResultType.NO_RESULTS -> NoResults() + SearchResult.SearchResultType.SEARCH_OR_CREATE -> SearchOrCreate() + else -> { + // Nothing to do in these cases + } + } +} + +@Composable +fun SearchButton(modifier: Modifier = Modifier, onClick: () -> Unit) { + Button( + modifier = modifier, + onClick = onClick, + colors = ButtonDefaults.buttonColors(backgroundColor = Color.White), + shape = RoundedCornerShape(24.dp), + elevation = ButtonDefaults.elevation(), + ) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_search), + contentDescription = "", + tint = colorResource(id = R.color.colorPrimary), + ) + Spacer(modifier = Modifier.size(16.dp)) + Text( + text = stringResource(id = R.string.search), + color = colorResource(id = R.color.textSecondary), + ) + } + } +} + +@Composable +fun WrappedSearchButton(onClick: () -> Unit) { + SearchButton( + modifier = Modifier + .wrapContentWidth(align = Alignment.CenterHorizontally) + .height(44.dp), + onClick = onClick, + ) +} + +@ExperimentalAnimationApi +@Composable +fun FullSearchButton(modifier: Modifier, visible: Boolean = true, onClick: () -> Unit) { + AnimatedVisibility( + modifier = modifier, + visible = visible, + enter = slideInVertically(), + exit = slideOutVertically(), + ) { + SearchButton( + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + onClick = onClick, + ) + } +} + +@Composable +fun LoadingContent(loadingDescription: String) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularProgressIndicator() + Spacer(modifier = Modifier.size(16.dp)) + Text( + text = loadingDescription, + fontSize = 14.sp, + color = Color.Black.copy(alpha = 0.38f), + style = LocalTextStyle.current.copy( + lineHeight = 10.sp, + fontFamily = FontFamily(Font(R.font.rubik_regular)), + ), + ) + } +} + +@Composable +fun SearchOutsideProgram(resultText: String, buttonText: String, onSearchOutsideClick: () -> Unit) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = resultText, + fontSize = 14.sp, + color = Color.Black.copy(alpha = 0.38f), + style = LocalTextStyle.current.copy( + lineHeight = 10.sp, + fontFamily = FontFamily(Font(R.font.rubik_regular)), + ), + ) + Spacer(modifier = Modifier.size(16.dp)) + Button( + onClick = onSearchOutsideClick, + border = BorderStroke(1.dp, colorResource(id = R.color.colorPrimary)), + colors = ButtonDefaults.buttonColors( + backgroundColor = colorResource(id = R.color.white), + ), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_search), + contentDescription = "", + tint = colorResource(id = R.color.colorPrimary), + ) + Spacer(modifier = Modifier.size(16.dp)) + Text(text = buttonText, color = colorResource(id = R.color.colorPrimary)) + } + } +} + +@Composable +fun NoMoreResults() { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.string_no_more_results), + fontSize = 14.sp, + color = Color.Black.copy(alpha = 0.38f), + style = LocalTextStyle.current.copy( + lineHeight = 10.sp, + fontFamily = FontFamily(Font(R.font.rubik_regular)), + ), + ) + } +} + +@Composable +fun NoResults() { + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_empty_folder), + contentDescription = "", + ) + Spacer(modifier = Modifier.size(16.dp)) + Text( + text = stringResource(R.string.search_no_results), + fontSize = 17.sp, + color = Color.Black.copy(alpha = 0.38f), + style = LocalTextStyle.current.copy( + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.rubik_regular)), + ), + ) + } +} + +@Composable +fun TooManyResults() { + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_too_many), + contentDescription = "", + ) + Spacer(modifier = Modifier.size(16.dp)) + Text( + text = stringResource(R.string.search_too_many_results), + fontSize = 17.sp, + color = colorResource(id = R.color.pink_500), + style = LocalTextStyle.current.copy( + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.rubik_regular)), + ), + ) + Text( + text = stringResource(R.string.search_too_many_results_message), + fontSize = 17.sp, + color = Color.Black.copy(alpha = 0.38f), + style = LocalTextStyle.current.copy( + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.rubik_regular)), + ), + ) + } +} + +@Composable +fun SearchOrCreate() { + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_searchvscreate), + contentDescription = "", + ) + Spacer(modifier = Modifier.size(16.dp)) + Text( + text = stringResource(R.string.search_or_create), + fontSize = 17.sp, + color = Color.Black.copy(alpha = 0.38f), + style = LocalTextStyle.current.copy( + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.rubik_regular)), + ), + ) + } +} + +@ExperimentalAnimationApi +@Composable +fun CreateNewButton(modifier: Modifier, extended: Boolean = true, onClick: () -> Unit) { + Button( + modifier = modifier + .wrapContentWidth() + .height(56.dp), + contentPadding = PaddingValues(16.dp), + onClick = onClick, + colors = ButtonDefaults.buttonColors(backgroundColor = Color.White), + shape = RoundedCornerShape(16.dp), + elevation = ButtonDefaults.elevation(), + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(id = R.drawable.ic_add_accent), + contentDescription = "", + tint = colorResource(id = R.color.colorPrimary), + ) + AnimatedVisibility(visible = extended) { + Row { + Spacer(modifier = Modifier.size(12.dp)) + Text( + text = stringResource(R.string.search_create_new), + color = colorResource(id = R.color.colorPrimary), + ) + } + } + } +} + +@ExperimentalAnimationApi +@Preview(showBackground = true, backgroundColor = 0x2C98F0) +@Composable +fun SearchFullWidthPreview() { + FullSearchButton(modifier = Modifier) { + } +} + +@Preview(showBackground = true, backgroundColor = 0x2C98F0) +@Composable +fun SearchWrapWidthPreview() { + WrappedSearchButton { + } +} + +@ExperimentalAnimationApi +@Preview +@Composable +fun ExtendedCreateNewButtonPreview() { + CreateNewButton(modifier = Modifier) {} +} + +@ExperimentalAnimationApi +@Preview +@Composable +fun CreateNewButtonPreview() { + CreateNewButton(modifier = Modifier, extended = false) {} +} + +@Preview(showBackground = true) +@Composable +fun LoadingMoreResultsPreview() { + LoadingContent(loadingDescription = "Loading more results...") +} + +@Preview(showBackground = true) +@Composable +fun SearchOutsidePreview() { + SearchResult(searchResultType = SearchResult.SearchResultType.SEARCH_OUTSIDE) {} +} + +@Preview(showBackground = true) +@Composable +fun NoMoreResultsPreview() { + SearchResult(searchResultType = SearchResult.SearchResultType.NO_MORE_RESULTS) {} +} + +@Preview(showBackground = true) +@Composable +fun TooManyResultsPreview() { + SearchResult(searchResultType = SearchResult.SearchResultType.TOO_MANY_RESULTS) {} +} + +@Preview(showBackground = true) +@Composable +fun NoResultsPreview() { + SearchResult(searchResultType = SearchResult.SearchResultType.NO_RESULTS) {} +} + +@Preview(showBackground = true) +@Composable +fun SearchOrCreatePreview() { + SearchResult(searchResultType = SearchResult.SearchResultType.SEARCH_OR_CREATE) {} +} diff --git a/app/src/main/java/org/dhis2/utils/customviews/BreakTheGlassBottomDialog.kt b/app/src/main/java/org/dhis2/utils/customviews/BreakTheGlassBottomDialog.kt index 7f68d77e86..02d18c824b 100644 --- a/app/src/main/java/org/dhis2/utils/customviews/BreakTheGlassBottomDialog.kt +++ b/app/src/main/java/org/dhis2/utils/customviews/BreakTheGlassBottomDialog.kt @@ -1,6 +1,5 @@ package org.dhis2.utils.customviews -import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -16,11 +15,13 @@ import org.dhis2.commons.resources.ColorType import org.dhis2.commons.resources.ColorUtils import org.dhis2.commons.resources.ResourceManager import org.dhis2.databinding.BreakTheGlassBottomDialogBindingImpl +import javax.inject.Inject class BreakTheGlassBottomDialog : BottomSheetDialogFragment() { private lateinit var programUid: String + @Inject lateinit var resourceManager: ResourceManager val colorUtils: ColorUtils = ColorUtils() @@ -41,19 +42,11 @@ class BreakTheGlassBottomDialog : BottomSheetDialogFragment() { isCancelable = false } - override fun onAttach(context: Context) { - super.onAttach(context) - resourceManager = ResourceManager( - context, - colorUtils, - ) - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View { + ): View? { return BreakTheGlassBottomDialogBindingImpl.inflate(inflater, container, false).apply { message.text = resourceManager.formatWithEnrollmentLabel( programUid, diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2148777b1d..732102f002 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -44,8 +44,6 @@ %s resultados Agregar nuevo Añadir - Introduce %s - Introduce / Salta %s Programar Remitir @@ -78,9 +76,6 @@ Buscar - Buscar / Añadir nuevo %s - Buscar %s - Añadir nuevo %s Introducir texto Introducir texto largo Introducir numero @@ -97,8 +92,6 @@ Programa suspendido Abrir - No completado - Evento completado Sólo lectura Acabar y completar Completado @@ -150,7 +143,6 @@ Total Fecha de evento más reciente - No hay opciones disponibles Listado de admisiones Programas activos Programas disponibles para inscripción @@ -161,22 +153,18 @@ Ver detalles Ver mas - Editar Seguimiento Seguimiento activado Seguimiento desactivado Desactivar Este programa no admite más eventos - No hay datos Eventos Inicio Información de la App Cerrar Sesion Bloquear sesion - Mostrar %s más... - Mostrar menos... 1 minuto 15 minutos 30 minutos @@ -202,7 +190,6 @@ Modulo SMS: SMS gateway Emisor del SMS de resultado - "Tiempo de espera de recepción del SMS " segundos numero numero @@ -237,10 +224,6 @@ Verifique los parámetros relacionados con la SMS gateway - Actualización de software - Software - Exportar base de datos - Exporta tu base de datos y compártala con su administrador Fecha de inscripción @@ -392,7 +375,6 @@ Eventos Completados Todos los eventos de este programa están completados. ¿Quiere completar el programa también? ¿ Completar también la inscripción? - Le gustaría completar el %s también? Ver Inicio @@ -489,9 +471,6 @@ Debe seleccionar al menos una unidad organizativa. Enviar datos Ayude a DHIS2 a mejorar esta app compartiendo errores asociados a su usuario. - Quiere ayudarnos a mejorar esta app? - "Si dice que sí la app mandará estadísticas anónimas y registro de errores que ayudaran a mejorar la experiencia y el rendimiento. Para más detalles, consulte nuestra política de privacidad. " - política de privacidad Comprobar campos Se perdió la conexión a internet durante la sincronización. Inténtelo mas tarde. Confirmar @@ -843,7 +822,6 @@ Demasiados resultados Intente cambiar su búsqueda para restringir los resultados. Crear nuevo - Nuevo %s Es necesario que ingrese al menos %s atributos para iniciar la búsqueda De acuerdo Empieza una búsqueda para encontrar %s @@ -863,7 +841,6 @@ Dispositivo: %s Versión del SO: %s Abrir detalles de %s - Detalles de %s Sincronizando %s... SMS enviado. Esperando respuesta del servidor. Se ha confirmado que el SMS llegó al servidor. Actualizando estado @@ -872,85 +849,11 @@ SMS no enviado. Abriendo aplicación de SMS. Por favor, no modifique el mensaje. Enviar SMS con - ¿Ha enviado el sms? Esperando confirmación manual - Sincronizando eventos - Sincronizando entidades de ratreo - Sincronizando set de datos - Sincronizando recursos - Sincronización finalizada - La sincronización por SMS está habilitada pero debe acceder a ella en cada registro individualmente. - Descargando imágenes... - Descripción del programa - Pin erróneo Inscrito en: - Sincronización necesaria - Error de sincroniación Sincronizado - Advertencia de sincronización Sincronizando - SMS enviado - ¿Quiere mandar todos sus cambios? - ¿Quiere mandar sus cambios de %s? - ¿Quiere mandar sus cambios de este %s? - ¿Quiere comprobar si existen cambios? - ¿Quiere comprobar si existen cambios en %s? - ¿Quiere comprobar si existen cambios para este %s? - ¿Quiere mandar el SMS de nuevo? Enviar Actualizar Ahora no - Toque aquí para explorar - La versión %s de la app está ahora disponible. ¿Quiere instalarla? - Actualización de software - Más tarde - Descargar ahora - Buscar actualizaciones - Buscando actualizaciones - No hay nuevas actualizaciones - El permiso para fuentes desconocidas está deshabilitado. Habilítelo para instalar esta versión. - El permiso de almacenamiento está deshabilitado. Habilítelo para instalar esta versión. - Versión de la app - Hay algún error en la configuraciónd el programa, no se puede cargar el evento. Por favor contacte a su administrador. - Eliminar persona - Permiso de notificaciones está deshabilitado. - Permiso de notificaciones concedido. - No sincronizaco - Reintentar sincronización - Marcado para seguimiento - %s atrasado - Registrado en: - Solo vista - Programado: %s - Programado para %s - Omitido - Programas: - Sincronización existosa - Aviso en la sincronización - Error de sincronización - Marcar para seguimiento - Eliminar - Coordenadas - No tiene acceso a los datos. Contacte con su administrador. - Parece que está offline. Compruebe su conexión. - ¿Eliminar este %s? - Este %s y todos sus datos de todos los programas serán eliminados. Esta acción no puede dehacerse. - ¿Eliminar de %s? - Los datos del %s serán elimiados. Esta acción no puede deshacerse. - Programar el siguiente - Programar - Etapa - evento - %d eventos - Línea de tiempo - Limpiar búsqueda - Opcional - Base de datos descargada - Importando base de datos - Reabrir el formulario para editarlo - Mostrar más - Mostrar menos - Sincronizar - Mostrar campos - Ocultar campos -
+ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c0bce83893..6321e1aa7c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - + Dhis2 dhis @@ -243,7 +243,7 @@ Software update Software Export database - Export your database and share it with your administrator + Export yout database and share it with your administrator Enrollment date @@ -495,7 +495,7 @@ Send data Help DHIS2 improve this app by sharing error logs associated to your user id. Do you want to help us improve this app? - If you say yes the app will send anonymous statistics and error logs which help us improve user experience and performance. For further details, please refer to our privacy policy. + If you say yes the app will send anonymous statistics and error logs which help us improve user experience and performance.\n\nFor further details, please refer to our privacy policy. privacy policy Check fields You lost your internet connection while performing your configuration sync. Please try again later @@ -700,7 +700,7 @@ Complete anyway Missing %s %s from %s %s Event status - @string/enrollment_status + @string/enrollment_status Enrollment date There are no data. Click \"+\" to add new a new record There are no data. You don\'t have permission to add new records. @@ -925,7 +925,7 @@ %s warning %s warnings - App version %s is now available. Do you want to download it? + App version %s is now available.\nDo you want to download it? "Software update" Later Download now @@ -972,10 +972,5 @@ Database downloaded Importing database Re-open form to edit - Show more - Show less - Sync - Show fields - Hide fields Import successful diff --git a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt deleted file mode 100644 index 7374d434d9..0000000000 --- a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package org.dhis2.usescases.searchTrackEntity - -import androidx.paging.PagingData -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.dhis2.commons.resources.MetadataIconProvider -import org.dhis2.commons.viewmodel.DispatcherProvider -import org.dhis2.data.search.SearchParametersModel -import org.dhis2.form.ui.FieldViewModelFactory -import org.hisp.dhis.android.core.D2 -import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchCollectionRepository -import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem -import org.junit.After -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito -import org.mockito.Mockito.verify -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever - -@OptIn(ExperimentalCoroutinesApi::class) -class SearchRepositoryTest { - private val searchRepositoryJava: SearchRepository = mock() - - private val d2: D2 = mock(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) - - private val dispatcher: DispatcherProvider = mock() - - private val fieldViewModelFactory: FieldViewModelFactory = mock() - - private val metadataIconProvider: MetadataIconProvider = mock() - - private lateinit var searchRepositoryImplKt: SearchRepositoryImplKt - - private val testDispatcher = UnconfinedTestDispatcher() - - @Before - fun setup() { - Dispatchers.setMain(testDispatcher) - searchRepositoryImplKt = SearchRepositoryImplKt( - searchRepositoryJava, - d2, - dispatcher, - fieldViewModelFactory, - metadataIconProvider, - ) - } - - @After - fun tearDown() { - Dispatchers.resetMain() - } - - @Test - fun `test searchTrackedEntities returns PagingData flow`() = runTest { - val searchParametersModel: SearchParametersModel = SearchParametersModel( - selectedProgram = null, - queryData = null, - ) - val trackedEntitySearchCollectionRepository: TrackedEntitySearchCollectionRepository = - mock() - val pagingDataFlow = flowOf(PagingData.empty()) - - whenever(searchRepositoryJava.getFilteredRepository(searchParametersModel)) doReturn trackedEntitySearchCollectionRepository - whenever(trackedEntitySearchCollectionRepository.allowOnlineCache()) doReturn mock() - whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false)) doReturn mock() - whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false).offlineFirst()) doReturn mock() - whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false).offlineOnly()) doReturn mock() - whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false).offlineOnly().getPagingData(10)) doReturn mock() - whenever(trackedEntitySearchCollectionRepository.allowOnlineCache().eq(false).offlineFirst().getPagingData(10)) doReturn mock() - whenever(trackedEntitySearchCollectionRepository.getPagingData(10)) doReturn pagingDataFlow - - val result = - searchRepositoryImplKt.searchTrackedEntities(searchParametersModel, isOnline = true) - - result.collect { pagingData -> - assertTrue(pagingData is PagingData) - } - - verify(searchRepositoryJava).getFilteredRepository(searchParametersModel) -// verify(trackedEntitySearchCollectionRepository).getPagingData(10) - } -} diff --git a/commons/src/main/res/values-es/strings.xml b/commons/src/main/res/values-es/strings.xml index cb38784c3b..5f60ed38e4 100644 --- a/commons/src/main/res/values-es/strings.xml +++ b/commons/src/main/res/values-es/strings.xml @@ -42,69 +42,7 @@ Asignado a mí Fecha Fecha de evento - La conexión de red no está disponible - Se requiere conexión a internet para usar mapas. - Hoy - - %d día atrasado - %d días atrasados - %d días atrasados - - - %d mes atrasado - %d meses atrasados - %d meses atrasados - - - %d año atrasado - %d años atrasados - %d años atrasados - - - - En %d día - En %d días - En %d días - - - En %d mes - En %d meses - En %d meses - - - En %d año - En %d años - En %d años - - Abrir con - Archivo descargado con éxito - Mostrar coordenadas de %s - Datos de %s - Coordenadas de %s - Fecha de %s - Estado de %s - Es necesario proveer la razón para consultar este %s en este programa protegido. Toda actividad será registrada. - Razón para consultar %s - Los datos no son editables por que el %s está completado o cancelado - %s completado - %s cancelado - Nuevo %s - Registrado en: - Eliminar solo %s - %s del programa - %s completado - %s creado - Nuevo %s - Fecha de %s - Eliminar %s - %s eliminado con éxito - Éste %s aún no ha sido creado - Éste %s ha sido omitido - ¿Está seguro de que desea eliminar éste %s? - %s actualizado - Éste %s no puede tener una fecha a futuro. Cámbielo por favor. - Error al insertar un nuevo %s - Semana %d %s a %s + Fecha de inscripción Estado del evento Estado de la inscripción. Seguido %s @@ -264,12 +202,8 @@ No hay suficientes atributos para realizar la búsqueda. Por favor redefina la búsqueda añadiendo más atributos. La búsqueda online solo está disponible para las unidades organizativas de búsqueda. El query contiene caracteres invalidos. Por favor contacte a su administrador para verificar la configuración del servidor (relaxedQueryChars,...). - El servidor no está disponible. Pruebe más tarde. - Está desconectado. Habilite su conexión a internet y pruebe otra vez. Este campo no está disponible para la captura por móvil. Añadir imagen - Añadir firma - Añadir archivo De acuerdo La ubicación de su dispositivo está desconectada. Por favor habilítela para capturar coordenadas. Seguir editando diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5cc97ce439..0e0aef2426 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,8 @@ ndk = "21.4.7075529" sdk = "34" minSdk = "21" -vCode = "134" -vName = "3.0.1" +vCode = "133" +vName = "3.0" kotlinCompilerExtensionVersion = "1.5.6" gradle = "8.2.2" kotlin = '1.9.21' @@ -11,7 +11,7 @@ hilt = '2.47' hiltCompiler = '1.0.0' jacoco = '0.8.10' designSystem = "0.3.0-SNAPSHOT" -dhis2sdk = "1.10.1-SNAPSHOT" +dhis2sdk = "1.10.0" ruleEngine = "3.0.0" expressionParser = "1.1.0" appcompat = "1.6.1" diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index 19cb83d747..7be5c2dad9 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,9 +1,11 @@ -# Release notes - Android App for DHIS2 - 3.0.0.1 - -### Bug - -[ANDROAPP-6194](https://dhis2.atlassian.net/browse/ANDROAPP-6194) Unable to search outside the program - -[ANDROAPP-6195](https://dhis2.atlassian.net/browse/ANDROAPP-6195) Missing terms in transifex - -[ANDROAPP-6210](https://dhis2.atlassian.net/browse/ANDROAPP-6210) UninitializedPropertyAccessException on breaking the glass \ No newline at end of file +- Event and enrollment label can be customized via the Maintenance App. +- Supports multiselect data elements. +- Support for custom icons. +- Event/enrollment details are now more accessible within forms. +- Improved search flow and dashboard layout. +- Redesigned input fields. +- Support line listing analytics. +- Custom map layers can be used. +- Export/import databases for troubleshooting. +- Better feedback for configuration errors. +- Better navigation for many TEIs \ No newline at end of file