diff --git a/.github/workflows/continuous-delivery.yml b/.github/workflows/continuous-delivery.yml index c3d7c89978..b7c6c5f6aa 100644 --- a/.github/workflows/continuous-delivery.yml +++ b/.github/workflows/continuous-delivery.yml @@ -11,6 +11,11 @@ on: - main - develop - release/* + pull_request: + branches: + - main + - develop + - release/* jobs: deployment_job: diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml deleted file mode 100644 index 0b7aa91c4e..0000000000 --- a/.github/workflows/continuous-integration.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: Continuous Integration - -env: - # The name of the main module repository - main_project_module: app - -on: - push: - branches: - - main - - develop - - release/* - pull_request: - branches: - - main - - develop - - release/* - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -jobs: - ci_job: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Set Up JDK - uses: actions/setup-java@v3 - with: - distribution: 'zulu' # See 'Supported distributions' for available options - java-version: '17' - cache: 'gradle' - - - name: Change wrapper permissions - run: chmod +x ./gradlew - - # Run check style - - name: Kotlin checkstyle - run: ./gradlew ktlintCheck - - # Running unit tests on app module - - name: Run app module tests - run: ./gradlew :app:testDhisDebugUnitTest - - # Running unit tests on all other modules - - name: Run all modules tests - run: ./gradlew testDebugUnitTest - - # Run Build Project - #- name: Build gradle project - # run: ./gradlew build - - deployment_job: - runs-on: ubuntu-latest - needs: ci_job - if: github.event_name == 'pull_request' && needs.ci_job.result == 'success' - steps: - - uses: actions/checkout@v3 - - # Set Current Date As Env Variable - - name: Set current date as env variable - run: echo "date_today=$(date +'%Y-%m-%d')" >> $GITHUB_ENV - - # Set Repository Name As Env Variable - - name: Set repository name as env variable - run: echo "repository_name=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_ENV - - - name: Set Up JDK - uses: actions/setup-java@v3 - with: - distribution: 'zulu' # See 'Supported distributions' for available options - java-version: '17' - cache: 'gradle' - - - name: Change wrapper permissions - run: chmod +x ./gradlew - - # Create APK Debug - - name: Build apk debug project (APK) - ${{ env.main_project_module }} module - run: ./gradlew assembleDhisDebug - - - name: Read version name from file - working-directory: ./gradle - id: read-version - run: echo "::set-output name=vName::$(grep 'vName' libs.versions.toml | awk -F' = ' '{print $2}' | tr -d '"')" - - # Upload Artifact Build - - name: Upload Android artifacts - uses: actions/upload-artifact@v3 - with: - name: ${{ env.repository_name }} - Android APK - path: ${{ env.main_project_module }}/build/outputs/apk/dhis/debug/dhis2-v${{ steps.read-version.outputs.vName }}-training.apk diff --git a/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt b/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt index f94746d2c9..e9d76fa684 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt @@ -24,7 +24,6 @@ import org.dhis2.commons.idlingresource.SearchIdlingResourceSingleton import org.dhis2.commons.prefs.Preference import org.dhis2.form.ui.idling.FormCountingIdlingResource import org.dhis2.usescases.eventsWithoutRegistration.EventIdlingResourceSingleton -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui.EventDetailIdlingResourceSingleton import org.dhis2.usescases.programEventDetail.eventList.EventListIdlingResourceSingleton import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataIdlingResourceSingleton import org.junit.After @@ -87,7 +86,6 @@ open class BaseTest { SearchIdlingResourceSingleton.countingIdlingResource, TeiDataIdlingResourceSingleton.countingIdlingResource, EventIdlingResourceSingleton.countingIdlingResource, - EventDetailIdlingResourceSingleton.countingIdlingResource, ) } @@ -100,7 +98,6 @@ open class BaseTest { SearchIdlingResourceSingleton.countingIdlingResource, TeiDataIdlingResourceSingleton.countingIdlingResource, EventIdlingResourceSingleton.countingIdlingResource, - EventDetailIdlingResourceSingleton.countingIdlingResource, ) } diff --git a/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt index c13f41c502..1bd0c7ccdb 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt @@ -40,7 +40,7 @@ class EventRegistrationRobot : BaseRobot() { } private fun clickOnNextQR() { - onView(withId(R.id.next)).perform(click()) + waitForView(withId(R.id.next)).perform(click()) } fun clickOnAllQR(listQR: Int) { 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 cf42db37d6..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 @@ -53,6 +53,7 @@ class SearchFlowTest : BaseTest() { ) val filterCounter = "1" val filterTotalCount = "2" + enableComposeForms() prepareWomanProgrammeIntentAndLaunchActivity(rule) teiFlowRobot(composeTestRule) { diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowRobot.kt index ad45d83a45..279b92be95 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowRobot.kt @@ -8,10 +8,12 @@ import org.dhis2.usescases.searchte.robot.searchTeiRobot import org.dhis2.usescases.teidashboard.robot.enrollmentRobot import org.dhis2.usescases.teidashboard.robot.eventRobot import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot +import java.text.SimpleDateFormat +import java.util.Calendar fun teiFlowRobot( composeTestRule: ComposeTestRule, - teiFlowRobot: TeiFlowRobot.() -> Unit + teiFlowRobot: TeiFlowRobot.() -> Unit, ) { TeiFlowRobot(composeTestRule).apply { teiFlowRobot() @@ -21,10 +23,10 @@ fun teiFlowRobot( class TeiFlowRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun registerTEI( - registrationModel: RegisterTEIUIModel + registrationModel: RegisterTEIUIModel, ) { val registrationDate = registrationModel.firstSpecificDate - val enrollmentDate = registrationModel.enrollmentDate + val incidentDate = getCurrentDate() searchTeiRobot(composeTestRule) { openNextSearchParameter("First name") @@ -37,12 +39,8 @@ class TeiFlowRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { clickOnEnroll() } - enrollmentRobot { - clickOnInputDate("Date of enrollment *") - selectSpecificDate(enrollmentDate.year, enrollmentDate.month, enrollmentDate.day) - clickOnAcceptInDatePicker() - clickOnInputDate("LMP Date *") - clickOnAcceptInDatePicker() + enrollmentRobot(composeTestRule) { + typeOnDateParameterWithLabel("LMP Date *", incidentDate) clickOnSaveEnrollment() } } @@ -53,10 +51,9 @@ class TeiFlowRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { clickOnMenuProgramEnrollments() } - enrollmentRobot { + enrollmentRobot(composeTestRule) { clickOnAProgramForEnrollment(composeTestRule, program) clickOnAcceptInDatePicker() - scrollToBottomProgramForm() clickOnSaveEnrollment() } } @@ -67,16 +64,16 @@ class TeiFlowRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { clickOnMenuProgramEnrollments() } - enrollmentRobot { + enrollmentRobot(composeTestRule) { waitToDebounce(1000) checkActiveAndPastEnrollmentDetails(enrollmentDetails) } } fun checkPastEventsAreClosed( - programPosition: Int + programPosition: Int, ) { - enrollmentRobot { + enrollmentRobot(composeTestRule) { clickOnEnrolledProgram(programPosition) } @@ -99,16 +96,34 @@ class TeiFlowRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun changeDueDate( cardTitle: String, - date: String, ) { teiDashboardRobot(composeTestRule) { clickOnEventGroupByStageUsingDate(cardTitle) } eventRobot(composeTestRule) { - clickOnEventReportDate() - selectSpecificDate(date) + clickOnEventDueDate() + selectSpecificDate(getCurrentDatePickerDate(), getPreviousDate()) acceptUpdateEventDate() } } + + private fun getCurrentDate(): String { + val sdf = SimpleDateFormat("ddMMYYYY") + val calendar = Calendar.getInstance() + return sdf.format(calendar.time) + } + + private fun getPreviousDate(): String { + val sdf = SimpleDateFormat("MMddYYYY") + val calendar = Calendar.getInstance() + calendar.add(Calendar.DAY_OF_MONTH, -1) + return sdf.format(calendar.time) + } + + private fun getCurrentDatePickerDate(): String { + val sdf = SimpleDateFormat("MM/dd/YYYY") + val calendar = Calendar.getInstance() + return sdf.format(calendar.time) + } } 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 7fad72127c..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 @@ -53,6 +53,7 @@ class TeiFlowTest : BaseTest() { val enrollmentListDetails = createEnrollmentList() val registerTeiDetails = createRegisterTEI() + enableComposeForms() setupCredentials() setDatePicker() prepareWomanProgrammeIntentAndLaunchActivity(ruleSearch) @@ -70,7 +71,7 @@ class TeiFlowTest : BaseTest() { EnrollmentListUIModel( ADULT_WOMAN_PROGRAM, ORG_UNIT, - "30/6/2017", + currentDate, currentDate ) @@ -118,6 +119,5 @@ class TeiFlowTest : BaseTest() { const val LASTNAME = "Stuart" const val DATE_FORMAT = "dd/M/yyyy" - const val DATE_PICKER_FORMAT = ", d MMMM" } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt index 0ee0ad8421..a0c055353b 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt @@ -1,8 +1,6 @@ package org.dhis2.usescases.searchte import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.intl.Locale import androidx.test.espresso.IdlingRegistry @@ -21,9 +19,7 @@ import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACK 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.ui.dialogs.bottomsheet.SECONDARY_BUTTON_TAG import org.dhis2.usescases.BaseTest -import org.dhis2.usescases.flow.teiFlow.TeiFlowTest import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel import org.dhis2.usescases.flow.teiFlow.entity.RegisterTEIUIModel import org.dhis2.usescases.flow.teiFlow.teiFlowRobot @@ -39,7 +35,6 @@ 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) @@ -87,7 +82,7 @@ class SearchTETest : BaseTest() { clickOnSearch() checkListOfSearchTEI( title = "First name: $firstName", - attributes = mapOf("Last name:" to lastName) + attributes = mapOf("Last name:" to lastName), ) } } @@ -136,7 +131,7 @@ class SearchTETest : BaseTest() { composeTestRule.waitForIdle() checkListOfSearchTEI( title = "First name: $firstName", - attributes = mapOf("Last name:" to lastName) + attributes = mapOf("Last name:" to lastName), ) } } @@ -186,7 +181,7 @@ class SearchTETest : BaseTest() { val enrollmentStatusFilter = context.getString(R.string.filters_title_enrollment_status) .format( context.resources.getQuantityString(R.plurals.enrollment, 1) - .capitalize(Locale.current) + .capitalize(Locale.current), ) val totalFilterCount = "2" val filterCount = "1" @@ -206,12 +201,11 @@ class SearchTETest : BaseTest() { } @Test - @Ignore("Test is successful locally but not in browserstack") fun shouldSuccessfullyFilterByEventStatusOverdue() { + enableComposeForms() val eventStatusFilter = context.getString(R.string.filters_title_event_status) val totalCount = "1" val registerTeiDetails = createRegisterTEI() - val overdueDate = getCurrentDate() val dateFormat = SimpleDateFormat(SIMPLE_DATE_FORMAT, java.util.Locale.getDefault()).format(Date()) val scheduledEventTitle = context.getString(R.string.scheduled_for) @@ -222,9 +216,7 @@ class SearchTETest : BaseTest() { teiFlowRobot(composeTestRule) { registerTEI(registerTeiDetails) - changeDueDate(scheduledEventTitle, overdueDate) - pressBack() - composeTestRule.onNodeWithTag(SECONDARY_BUTTON_TAG).performClick() + changeDueDate(scheduledEventTitle) pressBack() } @@ -235,8 +227,9 @@ class SearchTETest : BaseTest() { closeFilterRowAtField(eventStatusFilter) checkFilterCounter(totalCount) checkCountAtFilter(eventStatusFilter, totalCount) - clickOnFilter() - checkEventsAreOverdue() + } + searchTeiRobot(composeTestRule) { + checkListOfSearchTEIWithAdditionalInfo("First name: ADRIANNA", "1 day overdue") } } @@ -479,12 +472,6 @@ class SearchTETest : BaseTest() { 30 ) - private fun getCurrentDate(): String { - val sdf = SimpleDateFormat(TeiFlowTest.DATE_PICKER_FORMAT) - val calendar = Calendar.getInstance() - return sdf.format(calendar.time) - } - private val dateRegistration = createFirstSpecificDate() private val dateEnrollment = createEnrollmentDate() diff --git a/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt index 8080cc13a7..9e27f720e5 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt @@ -204,6 +204,15 @@ class SearchTeiRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { onView(withId(R.id.createButton)).perform(click()) } + fun checkListOfSearchTEIWithAdditionalInfo(title: String, additionalText: String) { + composeTestRule.onNodeWithText(title).assertIsDisplayed() + composeTestRule.onNode( + hasParent(hasTestTag("LIST_CARD_ADDITIONAL_INFO_COLUMN")) + and hasText(additionalText), + useUnmergedTree = true, + ).assertIsDisplayed() + } + private fun createAttributesList(displayListFieldsUIModel: DisplayListFieldsUIModel) = listOf( AdditionalInfoItem( key = "Last name:", diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt index b6dc9db96f..b4a9a81a75 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt @@ -1,5 +1,6 @@ package org.dhis2.usescases.teidashboard +import android.annotation.SuppressLint import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import dhis2.org.analytics.charts.data.ChartType @@ -226,6 +227,7 @@ class TeiDashboardTest : BaseTest() { } } + @SuppressLint("IgnoreWithoutReason") @Ignore @Test fun shouldOpenEventEditAndSaveSuccessfully() { @@ -271,7 +273,7 @@ class TeiDashboardTest : BaseTest() { clickOnMenuProgramEnrollments() } - enrollmentRobot { + enrollmentRobot(composeTestRule) { clickOnAProgramForEnrollment(composeTestRule, womanProgram) clickOnAcceptInDatePicker() clickOnPersonAttributes(personAttribute) diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EnrollmentRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EnrollmentRobot.kt index 12eef6b090..3451bb75d9 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EnrollmentRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EnrollmentRobot.kt @@ -1,12 +1,15 @@ package org.dhis2.usescases.teidashboard.robot +import androidx.compose.ui.test.hasAnySibling +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextReplacement import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition import androidx.test.espresso.matcher.ViewMatchers.hasDescendant @@ -24,13 +27,16 @@ import org.dhis2.usescases.teiDashboard.teiProgramList.ui.PROGRAM_TO_ENROLL import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString -fun enrollmentRobot(enrollmentRobot: EnrollmentRobot.() -> Unit) { - EnrollmentRobot().apply { +fun enrollmentRobot( + composeTestRule: ComposeTestRule, + enrollmentRobot: EnrollmentRobot.() -> Unit, +) { + EnrollmentRobot(composeTestRule).apply { enrollmentRobot() } } -class EnrollmentRobot : BaseRobot() { +class EnrollmentRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun clickOnAProgramForEnrollment(composeTestRule: ComposeTestRule, program: String) { composeTestRule.onNodeWithTag(PROGRAM_TO_ENROLL.format(program), useUnmergedTree = true) @@ -123,23 +129,13 @@ class EnrollmentRobot : BaseRobot() { ) } - fun clickOnInputDate(label: String) { - onView(withId(R.id.recyclerView)) - .perform( - actionOnItem( - hasDescendant(withText(label)), clickChildViewWithId(R.id.inputEditText) - ) - ) - } - - fun selectSpecificDate(year: Int, monthOfYear: Int, dayOfMonth: Int) { - onView(withId(R.id.datePicker)).perform( - PickerActions.setDate( - year, - monthOfYear, - dayOfMonth - ) - ) + fun typeOnDateParameterWithLabel(label: String, dateValue: String) { + composeTestRule.apply { + onNode( + hasTestTag("INPUT_DATE_TIME_TEXT_FIELD") and hasAnySibling(hasText(label)), + useUnmergedTree = true, + ).performTextReplacement(dateValue) + } } companion object { diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt index e3413e3983..2875d162b2 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt @@ -1,14 +1,17 @@ package org.dhis2.usescases.teidashboard.robot import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasAnySibling import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.performTextReplacement import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches @@ -101,24 +104,31 @@ class EventRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { onView(withId(R.id.possitive)).perform(click()) } - fun clickOnEventReportDate() { + fun clickOnEventDueDate() { composeTestRule.onNode( hasTestTag("INPUT_DATE_TIME_ACTION_BUTTON") and hasAnySibling( - hasText("Report date") + hasText("Due date") ) ).assertIsDisplayed().performClick() } - fun selectSpecificDate(date: String) { + fun selectSpecificDate(currentDate: String, date: String) { composeTestRule.onNodeWithTag("DATE_PICKER").assertIsDisplayed() - composeTestRule.onNode(hasText(date, true)).performClick() + composeTestRule.onNodeWithContentDescription( + label = "text", + substring = true, + useUnmergedTree = true, + ).performClick() + composeTestRule.onNode( + hasText(currentDate) and hasAnyAncestor(isDialog()) + ).performTextReplacement(date) } fun typeOnDateParameter(dateValue: String) { composeTestRule.apply { onNodeWithTag("INPUT_DATE_TIME_TEXT_FIELD").performClick() - onNodeWithTag("INPUT_DATE_TIME_TEXT_FIELD").performTextInput(dateValue) + onNodeWithTag("INPUT_DATE_TIME_TEXT_FIELD").performTextReplacement(dateValue) } } diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt index e953a7820e..434b519908 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt @@ -25,6 +25,7 @@ import org.dhis2.data.forms.dataentry.tablefields.spinner.SpinnerViewModel import org.dhis2.form.model.ValueStoreResult.ERROR_UPDATING_VALUE import org.dhis2.form.model.ValueStoreResult.VALUE_CHANGED import org.dhis2.form.model.ValueStoreResult.VALUE_HAS_NOT_CHANGED +import org.dhis2.usescases.datasets.dataSetTable.dataSetSection.TableDataToTableModelMapper.Companion.INDICATORS_TABLE_ID import org.hisp.dhis.android.core.arch.helpers.Result import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.dataelement.DataElement @@ -109,10 +110,12 @@ class DataValuePresenter( val updatedTableModel = mapper(tableData) val updatedTables = screenState.value.tables.map { tableModel -> - if (tableModel.id == catComboUid) { - updatedTableModel.copy(overwrittenValues = tableModel.overwrittenValues) - } else { - indicatorTables() ?: tableModel + when (tableModel.id) { + catComboUid -> updatedTableModel.copy( + overwrittenValues = tableModel.overwrittenValues, + ) + INDICATORS_TABLE_ID -> indicatorTables() ?: tableModel + else -> tableModel } } diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/TableDataToTableModelMapper.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/TableDataToTableModelMapper.kt index e361f5fba6..6ab3f171d1 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/TableDataToTableModelMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/TableDataToTableModelMapper.kt @@ -97,10 +97,14 @@ class TableDataToTableModelMapper(val mapFieldValueToUser: MapFieldValueToUser) } return TableModel( - id = "indicators", + id = INDICATORS_TABLE_ID, title = mapFieldValueToUser.resources.getString(R.string.dashboard_indicators), tableHeaderModel = tableHeader, tableRows = tableRows, ) } + + companion object { + const val INDICATORS_TABLE_ID = "indicators" + } } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt index cc317af7eb..fe94d41867 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt @@ -113,29 +113,38 @@ class EventCapturePresenterImpl( warningFields: List, eventMode: EventMode?, ) { - val eventStatus = eventStatus - if (eventStatus != EventStatus.ACTIVE) { - setUpActionByStatus(eventStatus) - } else { - val canSkipErrorFix = canSkipErrorFix( - hasErrorFields = errorFields.isNotEmpty(), - hasEmptyMandatoryFields = emptyMandatoryFields.isNotEmpty(), - hasEmptyEventCreationMandatoryFields = with(emptyMandatoryFields) { - containsValue(EventRepository.EVENT_DETAILS_SECTION_UID) || - containsValue(EventRepository.EVENT_CATEGORY_COMBO_SECTION_UID) - }, - eventMode = eventMode, - validationStrategy = eventCaptureRepository.validationStrategy(), - ) - val eventCompletionDialog = configureEventCompletionDialog.invoke( - errorFields, - emptyMandatoryFields, - warningFields, - canComplete, - onCompleteMessage, - canSkipErrorFix, - ) - view.showCompleteActions(eventCompletionDialog) + when (eventStatus) { + EventStatus.ACTIVE, EventStatus.COMPLETED -> { + var canSkipErrorFix = canSkipErrorFix( + hasErrorFields = errorFields.isNotEmpty(), + hasEmptyMandatoryFields = emptyMandatoryFields.isNotEmpty(), + hasEmptyEventCreationMandatoryFields = with(emptyMandatoryFields) { + containsValue(EventRepository.EVENT_DETAILS_SECTION_UID) || + containsValue(EventRepository.EVENT_CATEGORY_COMBO_SECTION_UID) + }, + eventMode = eventMode, + validationStrategy = eventCaptureRepository.validationStrategy(), + ) + if (eventStatus == EventStatus.COMPLETED) canSkipErrorFix = false + val eventCompletionDialog = configureEventCompletionDialog.invoke( + errorFields, + emptyMandatoryFields, + warningFields, + canComplete, + onCompleteMessage, + canSkipErrorFix, + eventStatus, + ) + + if (eventStatus == EventStatus.COMPLETED && eventCompletionDialog.fieldsWithIssues.isEmpty()) { + finishCompletedEvent() + } else { + view.showCompleteActions(eventCompletionDialog) + } + } + else -> { + setUpActionByStatus(eventStatus) + } } view.showNavigationBar() } @@ -158,13 +167,6 @@ class EventCapturePresenterImpl( private fun setUpActionByStatus(eventStatus: EventStatus) { when (eventStatus) { - EventStatus.COMPLETED -> - if (!hasExpired && !eventCaptureRepository.isEnrollmentCancelled) { - view.saveAndFinish() - } else { - view.finishDataEntry() - } - EventStatus.OVERDUE -> view.attemptToSkip() EventStatus.SKIPPED -> view.attemptToReschedule() else -> { @@ -173,6 +175,14 @@ class EventCapturePresenterImpl( } } + private fun finishCompletedEvent() { + if (!hasExpired && !eventCaptureRepository.isEnrollmentCancelled) { + view.saveAndFinish() + } else { + view.finishDataEntry() + } + } + override fun isEnrollmentOpen(): Boolean { return eventCaptureRepository.isEnrollmentOpen } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialog.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialog.kt index 3543b4641b..7e6a53f935 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialog.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialog.kt @@ -15,6 +15,7 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.model.EventCom import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.model.EventCompletionDialog import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.provider.EventCaptureResourcesProvider import org.dhis2.utils.customviews.FormBottomDialog +import org.hisp.dhis.android.core.event.EventStatus class ConfigureEventCompletionDialog( val provider: EventCaptureResourcesProvider, @@ -27,6 +28,7 @@ class ConfigureEventCompletionDialog( canComplete: Boolean, onCompleteMessage: String?, canSkipErrorFix: Boolean, + eventState: EventStatus, ): EventCompletionDialog { val dialogType = getDialogType( errorFields, @@ -34,10 +36,10 @@ class ConfigureEventCompletionDialog( warningFields, !canComplete && onCompleteMessage != null, ) - val mainButton = getMainButton(dialogType) - val secondaryButton = if (canSkipErrorFix) { + val mainButton = getMainButton(dialogType, eventState) + val secondaryButton = if (canSkipErrorFix || eventState == EventStatus.COMPLETED) { EventCompletionButtons( - SecondaryButton(provider.provideNotNow()), + SecondaryButton(if (eventState == EventStatus.COMPLETED) provider.provideSaveAnyway() else provider.provideNotNow()), FormBottomDialog.ActionType.FINISH, ) } else { @@ -45,7 +47,7 @@ class ConfigureEventCompletionDialog( } val bottomSheetDialogUiModel = BottomSheetDialogUiModel( title = getTitle(dialogType), - message = getSubtitle(dialogType), + message = getSubtitle(dialogType, eventState), iconResource = getIcon(dialogType), mainButton = mainButton.buttonStyle, secondaryButton = secondaryButton?.buttonStyle, @@ -69,10 +71,10 @@ class ConfigureEventCompletionDialog( else -> provider.provideSavedText() } - private fun getSubtitle(type: DialogType) = when (type) { + private fun getSubtitle(type: DialogType, eventState: EventStatus) = when (type) { ERROR -> provider.provideErrorInfo() MANDATORY -> provider.provideMandatoryInfo() - WARNING -> provider.provideWarningInfo() + WARNING -> if (eventState == EventStatus.COMPLETED) provider.provideWarningInfoCompletedEvent() else provider.provideWarningInfo() SUCCESSFUL -> provider.provideCompleteInfo() COMPLETE_ERROR -> provider.provideOnCompleteErrorInfo() } @@ -84,7 +86,7 @@ class ConfigureEventCompletionDialog( SUCCESSFUL -> provider.provideSavedIcon() } - private fun getMainButton(type: DialogType) = when (type) { + private fun getMainButton(type: DialogType, eventState: EventStatus) = when (type) { ERROR, MANDATORY, COMPLETE_ERROR, @@ -93,7 +95,17 @@ class ConfigureEventCompletionDialog( FormBottomDialog.ActionType.CHECK_FIELDS, ) - WARNING, + WARNING -> if (eventState == EventStatus.COMPLETED) { + EventCompletionButtons( + MainButton(provider.provideReview()), + FormBottomDialog.ActionType.CHECK_FIELDS, + ) + } else { + EventCompletionButtons( + CompleteButton, + FormBottomDialog.ActionType.COMPLETE, + ) + } SUCCESSFUL, -> EventCompletionButtons( CompleteButton, diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/provider/EventCaptureResourcesProvider.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/provider/EventCaptureResourcesProvider.kt index 439a3c8ee6..34fa0910c7 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/provider/EventCaptureResourcesProvider.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/provider/EventCaptureResourcesProvider.kt @@ -25,10 +25,14 @@ class EventCaptureResourcesProvider( fun provideWarningInfo() = resourceManager.getString(R.string.missing_warning_fields_events) + fun provideWarningInfoCompletedEvent() = resourceManager.getString(R.string.missing_warning_fields_completed_events) + fun provideReview() = R.string.review fun provideNotNow() = R.string.not_now + fun provideSaveAnyway() = R.string.save_anyway + fun provideCompleteInfo() = resourceManager.getString(R.string.event_can_be_completed) fun provideOnCompleteErrorInfo() = resourceManager.getString(R.string.event_error_on_complete) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailIdlingResourceSingleton.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailIdlingResourceSingleton.kt deleted file mode 100644 index 5a24fe2882..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailIdlingResourceSingleton.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui - -import androidx.test.espresso.idling.CountingIdlingResource - -object EventDetailIdlingResourceSingleton { - - private const val RESOURCE = "EVENT_DETAIL" - - @JvmField val countingIdlingResource = CountingIdlingResource(RESOURCE) - - fun increment() { - countingIdlingResource.increment() - } - - fun decrement() { - if (!countingIdlingResource.isIdleNow) { - countingIdlingResource.decrement() - } - } -} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt index 5d33d4db0b..e7f6233cd0 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.launch import org.dhis2.commons.extensions.truncate import org.dhis2.commons.locationprovider.LocationProvider import org.dhis2.form.data.GeometryController +import org.dhis2.usescases.eventsWithoutRegistration.EventIdlingResourceSingleton import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventCatCombo import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventCoordinates import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventDetails @@ -141,7 +142,7 @@ class EventDetailsViewModel( } private fun setUpEventDetails() { - EventDetailIdlingResourceSingleton.increment() + EventIdlingResourceSingleton.increment() viewModelScope.launch { configureEventDetails( selectedDate = eventDate.value.currentDate, @@ -154,13 +155,13 @@ class EventDetailsViewModel( .flowOn(Dispatchers.IO) .collect { _eventDetails.value = it - EventDetailIdlingResourceSingleton.decrement() + EventIdlingResourceSingleton.decrement() } } } fun setUpEventReportDate(selectedDate: Date? = null) { - EventDetailIdlingResourceSingleton.increment() + EventIdlingResourceSingleton.increment() viewModelScope.launch { configureEventReportDate(selectedDate) .flowOn(Dispatchers.IO) @@ -168,7 +169,7 @@ class EventDetailsViewModel( _eventDate.value = it setUpEventDetails() setUpOrgUnit(selectedDate = it.currentDate) - EventDetailIdlingResourceSingleton.decrement() + EventIdlingResourceSingleton.decrement() } } } @@ -195,14 +196,14 @@ class EventDetailsViewModel( } fun setUpCategoryCombo(categoryOption: Pair? = null) { - EventDetailIdlingResourceSingleton.increment() + EventIdlingResourceSingleton.increment() viewModelScope.launch { configureEventCatCombo(categoryOption) .flowOn(Dispatchers.IO) .collect { _eventCatCombo.value = it setUpEventDetails() - EventDetailIdlingResourceSingleton.decrement() + EventIdlingResourceSingleton.decrement() } } } @@ -213,7 +214,7 @@ class EventDetailsViewModel( } private fun setUpCoordinates(value: String? = "") { - EventDetailIdlingResourceSingleton.increment() + EventIdlingResourceSingleton.increment() viewModelScope.launch { configureEventCoordinates(value) .flowOn(Dispatchers.IO) @@ -233,20 +234,20 @@ class EventDetailsViewModel( ) _eventCoordinates.value = eventCoordinates setUpEventDetails() - EventDetailIdlingResourceSingleton.decrement() + EventIdlingResourceSingleton.decrement() } } } fun setUpEventTemp(status: EventTempStatus? = null, isChecked: Boolean = true) { - EventDetailIdlingResourceSingleton.increment() + EventIdlingResourceSingleton.increment() if (isChecked) { configureEventTemp(status).apply { _eventTemp.value = this setUpEventDetails() } } - EventDetailIdlingResourceSingleton.decrement() + EventIdlingResourceSingleton.decrement() } fun getSelectableDates(eventDate: EventDate): SelectableDates { diff --git a/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt b/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt index 1e4f1710a9..e50a123ff2 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt @@ -11,6 +11,7 @@ import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable import kotlinx.coroutines.async import kotlinx.coroutines.launch +import org.dhis2.R import org.dhis2.commons.Constants.PREFS_URLS import org.dhis2.commons.Constants.PREFS_USERS import org.dhis2.commons.Constants.USER_TEST_ANDROID @@ -584,6 +585,7 @@ class LoginViewModel( view.setUrl(it.serverUrl) view.setUser(it.username) displayManageAccount() + view.displayMessage(resourceManager.getString(R.string.importing_successful)) }, onFailure = { view.displayMessage(resourceManager.parseD2Error(it)) diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchTEList.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchTEList.kt index 0346a8ec12..0a35cda2e1 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchTEList.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchTEList.kt @@ -165,6 +165,11 @@ class SearchTEList : FragmentGlobalAbstract() { } } }) + lifecycleScope.launch { + liveAdapter.loadStateFlow.collectLatest { + scrollToPosition(0) + } + } }.also { recycler = it } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt index 60eb677897..9e0c5ea837 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt @@ -9,6 +9,7 @@ import org.dhis2.bindings.dp import org.dhis2.databinding.ActivitySearchBinding import org.dhis2.usescases.searchTrackEntity.SearchAnalytics import org.dhis2.usescases.searchTrackEntity.SearchList +import org.dhis2.usescases.searchTrackEntity.SearchScreenState import org.dhis2.usescases.searchTrackEntity.SearchTEScreenState import org.dhis2.usescases.searchTrackEntity.ui.BackdropManager.changeBoundsIf import org.dhis2.utils.isPortrait @@ -24,7 +25,9 @@ class SearchScreenConfigurator( if (isPortrait()) { configureListScreen(screenState) } else { - configureLandscapeAnalyticsScreen(false) + if (screenState.screenState != SearchScreenState.MAP) { + configureLandscapeAnalyticsScreen(false) + } configureLandscapeListScreen(screenState) } } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt index 4482d88eff..2dd670c8c8 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt @@ -93,13 +93,9 @@ class TEICardMapper( } private fun getTitle(item: SearchTeiModel): String { - return if (item.header != null) { - item.header!! - } else if (item.attributeValues.isEmpty()) { - "-" - } else { - val key = item.attributeValues.keys.firstOrNull() - val value = item.attributeValues.values.firstOrNull()?.value() + return item.header ?: run { + val key = item.attributeValues.keys.firstOrNull() ?: "-" + val value = item.attributeValues.values.firstOrNull()?.value() ?: "-" "$key: $value" } } diff --git a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java index ab27a4d59b..01123525d9 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java +++ b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java @@ -119,6 +119,8 @@ public void onReceive(Context context, Intent intent) { private boolean dataWorkRunning; private SettingItem settingOpened = null; + private AlertDialog deleteLocalDataDialog; + public SyncManagerFragment() { // Required empty public constructor } @@ -232,6 +234,9 @@ public void onResume() { @Override public void onPause() { super.onPause(); + if (deleteLocalDataDialog != null && deleteLocalDataDialog.isVisible()) { + deleteLocalDataDialog.dismiss(); + } context.unregisterReceiver(networkReceiver); presenter.dispose(); } @@ -272,7 +277,7 @@ private void saveTimeMeta(int time) { @Override public void deleteLocalData() { - new AlertDialog( + deleteLocalDataDialog = new AlertDialog( getString(R.string.delete_local_data), getString(R.string.delete_local_data_message), null, @@ -291,7 +296,8 @@ public void deleteLocalData() { presenter.deleteLocalData(); return null; }) - ).show(requireActivity().getSupportFragmentManager()); + ); + deleteLocalDataDialog.show(requireActivity().getSupportFragmentManager()); } @Override diff --git a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt index 5215b119dd..c5f18198b1 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt @@ -1,11 +1,13 @@ package org.dhis2.usescases.settings.ui +import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.tween @@ -19,8 +21,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Share import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -30,17 +32,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData -import com.google.accompanist.themeadapter.material3.Mdc3Theme -import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.R import org.dhis2.ui.dialogs.alert.Dhis2AlertDialogUi import org.dhis2.ui.model.ButtonUiModel @@ -48,6 +45,8 @@ import org.hisp.dhis.mobile.ui.designsystem.component.Button import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @Composable fun ExportOption( @@ -62,9 +61,7 @@ fun ExportOption( val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission(), ) { isGranted -> - if (isGranted) { - onPermissionGrantedCallback() - } else { + onPermissionGrantedCallback.takeIf { isGranted }?.invoke() ?: run { showPermissionDialog = true } } @@ -95,38 +92,29 @@ fun ExportOption( Row( modifier = Modifier .fillMaxWidth() - .height(72.dp) + .height(Spacing.Spacing72) .padding( - start = 72.dp, - top = 16.dp, - end = 16.dp, - bottom = 16.dp, + start = Spacing.Spacing48, + top = Spacing.Spacing16, + end = Spacing.Spacing16, + bottom = Spacing.Spacing16, ), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = if (displayProgress) Arrangement.Center else spacedBy(16.dp), + horizontalArrangement = getHorizontalArrangement(displayProgress), ) { Button( modifier = Modifier.weight(1f), onClick = { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU || - ContextCompat.checkSelfPermission( - context, - android.Manifest.permission.WRITE_EXTERNAL_STORAGE, - ) == PackageManager.PERMISSION_GRANTED - ) { - onDownload() - } else { - onPermissionGrantedCallback = onDownload - launcher.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) - } + onDownloadCLick(context, onDownload, launcher) + onPermissionGrantedCallback = onDownload }, style = ButtonStyle.TEXT, text = stringResource(id = R.string.download), icon = { Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_file_download), + imageVector = Icons.Filled.Download, contentDescription = "Download", - tint = MaterialTheme.colors.primary, + tint = SurfaceColor.Primary, ) }, ) @@ -134,17 +122,8 @@ fun ExportOption( Button( modifier = Modifier.weight(1f), onClick = { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU || - ContextCompat.checkSelfPermission( - context, - android.Manifest.permission.WRITE_EXTERNAL_STORAGE, - ) == PackageManager.PERMISSION_GRANTED - ) { - onShare() - } else { - onPermissionGrantedCallback = onShare - launcher.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) - } + onShareClick(context, onShare, launcher) + onPermissionGrantedCallback = onShare }, style = ButtonStyle.TEXT, text = stringResource(id = R.string.share), @@ -152,7 +131,7 @@ fun ExportOption( Icon( imageVector = Icons.Filled.Share, contentDescription = "Share", - tint = MaterialTheme.colors.primary, + tint = SurfaceColor.Primary, ) }, ) @@ -161,10 +140,10 @@ fun ExportOption( Row( modifier = Modifier .fillMaxWidth() - .height(72.dp) - .padding(16.dp), + .height(Spacing.Spacing72) + .padding(Spacing.Spacing16), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = if (displayProgress) Arrangement.Center else spacedBy(16.dp), + horizontalArrangement = getHorizontalArrangement(displayProgress), ) { ProgressIndicator(type = ProgressIndicatorType.CIRCULAR) } @@ -192,20 +171,42 @@ fun ExportOption( } } +private fun onDownloadCLick(context: Context, onSuccess: () -> Unit, launcher: ActivityResultLauncher) { + if (checkPermissionAndAndroidVersion(context)) { + onSuccess() + } else { + launcher.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + } +} + +private fun onShareClick(context: Context, onSuccess: () -> Unit, launcher: ActivityResultLauncher) { + if (checkPermissionAndAndroidVersion(context)) { + onSuccess() + } else { + launcher.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + } +} + +private fun checkPermissionAndAndroidVersion(context: Context) = + Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU || + ContextCompat.checkSelfPermission( + context, + android.Manifest.permission.WRITE_EXTERNAL_STORAGE, + ) == PackageManager.PERMISSION_GRANTED + +private fun getHorizontalArrangement(displayProgress: Boolean) = + if (displayProgress) Arrangement.Center else spacedBy(Spacing.Spacing16) + @Preview @Composable fun PreviewExportOption() { - Mdc3Theme { - ExportOption(onDownload = { }, onShare = { }, false) - } + ExportOption(onDownload = { }, onShare = { }, false) } @Preview @Composable fun PreviewExportOptionProgress() { - Mdc3Theme { - ExportOption(onDownload = { }, onShare = { }, true) - } + ExportOption(onDownload = { }, onShare = { }, true) } fun ComposeView.setExportOption( @@ -215,8 +216,6 @@ fun ComposeView.setExportOption( ) { setContent { val displayProgress by displayProgressProvider().observeAsState(false) - MdcTheme { - ExportOption(onShare, onDownload, displayProgress) - } + ExportOption(onShare, onDownload, displayProgress) } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardProgramModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardProgramModel.kt index cb2e20440a..57393ff42b 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardProgramModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardProgramModel.kt @@ -3,7 +3,6 @@ package org.dhis2.usescases.teiDashboard import org.dhis2.ui.MetadataIconData import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.EnrollmentStatus -import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramStage @@ -31,7 +30,6 @@ sealed class DashboardModel( data class DashboardEnrollmentModel( val currentEnrollment: Enrollment, val programStages: List, - val eventModels: List, override val trackedEntityInstance: TrackedEntityInstance, val trackedEntityAttributes: List>, override val trackedEntityAttributeValues: List, diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepository.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepository.kt index f8905414a4..e6d6353c20 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepository.kt @@ -29,8 +29,6 @@ interface DashboardRepository { fun getEnrollment(): Observable - fun getTEIEnrollmentEvents(programUid: String?, teiUid: String): Observable> - fun getEnrollmentEventsWithDisplay( programUid: String?, teiUid: String, diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt index 9700c6d1c8..a123aa9781 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt @@ -12,7 +12,6 @@ import org.dhis2.commons.prefs.Preference import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.ui.MetadataIconData -import org.dhis2.utils.DateUtils import org.dhis2.utils.ValueUtils import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.helpers.UidsHelper.getUidsList @@ -68,30 +67,6 @@ class DashboardRepositoryImpl( .toObservable() } - override fun getTEIEnrollmentEvents( - programUid: String?, - teiUid: String, - ): Observable> { - return d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid) - .byDeleted().isFalse - .orderByTimeline(RepositoryScope.OrderByDirection.ASC) - .get().toFlowable().flatMapIterable { events: List? -> events }.distinct() - .map { event: Event -> - var event = event - if (java.lang.Boolean.FALSE - == d2.programModule().programs().uid(programUid).blockingGet()!! - .ignoreOverdueEvents() - ) if (event.status() == EventStatus.SCHEDULE && - event.dueDate()!! - .before(DateUtils.getInstance().today) - ) { - event = updateState(event, EventStatus.OVERDUE) - } - event - }.toList() - .toObservable() - } - override fun getEnrollmentEventsWithDisplay( programUid: String?, teiUid: String, @@ -388,7 +363,6 @@ class DashboardRepositoryImpl( DashboardEnrollmentModel( getEnrollment().blockingFirst(), getProgramStages(programUid).blockingFirst(), - getTEIEnrollmentEvents(programUid, teiUid).blockingFirst(), getTrackedEntityInstance(teiUid).blockingFirst(), getAttributesMap(programUid, teiUid).blockingFirst(), getTEIAttributeValues(programUid, teiUid).blockingFirst(), diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt index 84dd97073a..72ccbcdfbd 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt @@ -9,7 +9,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import io.reactivex.Completable import io.reactivex.Flowable -import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable import io.reactivex.processors.BehaviorProcessor import kotlinx.coroutines.CoroutineScope @@ -197,32 +196,6 @@ class TEIDataPresenter( dashboardRepository.saveCatOption(eventUid, catOptionComboUid) } - fun areEventsCompleted() { - compositeDisposable.add( - dashboardRepository.getEnrollmentEventsWithDisplay(programUid, teiUid) - .flatMap { events -> - if (events.isEmpty()) { - dashboardRepository.getTEIEnrollmentEvents( - programUid, - teiUid, - ) - } else { - Observable.just(events) - } - } - .map { events -> - Observable.fromIterable(events) - .all { event -> event.status() == EventStatus.COMPLETED } - } - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe( - view.areEventsCompleted(), - Timber.Forest::d, - ), - ) - } - fun displayGenerateEvent(eventUid: String?) { compositeDisposable.add( dashboardRepository.displayGenerateEvent(eventUid) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/mapper/TeiDashboardCardMapper.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/mapper/TeiDashboardCardMapper.kt index 879877f4d9..8bc9e4b91d 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/mapper/TeiDashboardCardMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/mapper/TeiDashboardCardMapper.kt @@ -81,7 +81,9 @@ class TeiDashboardCardMapper( ?.let { val attribute = it.filterAttributes().firstOrNull() val key = attribute?.first?.displayFormName() - val value = attribute?.second?.value() + val value = attribute?.second?.value()?.takeIf { attrValue -> + attrValue.isNotEmpty() + } ?: "-" "$key: $value" } ?: "-" @@ -102,7 +104,7 @@ class TeiDashboardCardMapper( if (it.first.valueType() == ValueType.PHONE_NUMBER) { AdditionalInfoItem( key = "${it.first.displayFormName()}:", - value = it.second.value() ?: "", + value = it.second.value()?.takeIf { attrValue -> attrValue.isNotEmpty() } ?: "-", icon = { Icon( imageVector = Icons.Filled.PhoneEnabled, @@ -246,5 +248,4 @@ class TeiDashboardCardMapper( this.filter { it.first.valueType() != ValueType.IMAGE } .filter { it.first.valueType() != ValueType.COORDINATE } .filter { it.first.valueType() != ValueType.FILE_RESOURCE } - .filter { it.second.value()?.isNotEmpty() == true } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f11e971b81..c0bce83893 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -863,6 +863,7 @@ Ok Start a search to find any %s You can search or create a new %s + You have some warning messages. You have some warning messages.\nDo you want to mark this form as complete? Some fields have errors and they are not saved. \nDo you want to review the form? Do you want to mark this form as complete? @@ -976,4 +977,5 @@ Sync Show fields Hide fields + Import successful diff --git a/app/src/test/java/org/dhis2/bindings/DateExtensionsTest.kt b/app/src/test/java/org/dhis2/bindings/DateExtensionsTest.kt index 64f870f3c0..1e71a87ae0 100644 --- a/app/src/test/java/org/dhis2/bindings/DateExtensionsTest.kt +++ b/app/src/test/java/org/dhis2/bindings/DateExtensionsTest.kt @@ -14,6 +14,7 @@ import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date import java.util.Locale +import java.util.TimeZone class DateExtensionsTest { @@ -204,6 +205,16 @@ class DateExtensionsTest { assert(date.toOverdueOrScheduledUiText(resourceManager, currentDate) == "Today") } + @Test + fun `Should return 'In x days', when the scheduled date is same day but different month and same year`() { + val currentDate = currentCalendar().time + val date: Date? = currentCalendar().apply { + add(Calendar.MONTH, 2) + }.time + whenever(resourceManager.getPlural(R.plurals.schedule_days, 61, 61)) doReturn "In 61 days" + assert(date.toOverdueOrScheduledUiText(resourceManager, currentDate) == "In 61 days") + } + @Test fun `Should return 'In x days', when the current date is -x days from the scheduled date and less than 90 days`() { val currentDate = currentCalendar().time @@ -244,7 +255,10 @@ class DateExtensionsTest { assert(date.toOverdueOrScheduledUiText(resourceManager, currentDate) == "In 3 years") } - private fun currentCalendar() = Calendar.getInstance().apply { - time = "2020-03-02T00:00:00.00Z".toDate() + private fun currentCalendar(): Calendar { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")) + return Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply { + time = "2020-03-02T00:00:00.00Z".toDate() + } } } diff --git a/app/src/test/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenterTest.kt b/app/src/test/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenterTest.kt index f396e5c2fd..ee876e27ec 100644 --- a/app/src/test/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenterTest.kt @@ -18,6 +18,7 @@ import org.dhis2.data.forms.dataentry.tablefields.spinner.SpinnerViewModel import org.dhis2.data.schedulers.TrampolineSchedulerProvider import org.dhis2.form.model.StoreResult import org.dhis2.form.model.ValueStoreResult +import org.dhis2.usescases.datasets.dataSetTable.dataSetSection.TableDataToTableModelMapper.Companion.INDICATORS_TABLE_ID import org.hisp.dhis.android.core.category.CategoryCombo import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.dataelement.DataElement @@ -381,7 +382,7 @@ class DataValuePresenterTest { } val mockedIndicatorTableModel = mock { - on { id } doReturn null + on { id } doReturn INDICATORS_TABLE_ID } val mockedUpdatedTableModel = mock { @@ -426,6 +427,72 @@ class DataValuePresenterTest { verify(view).onValueProcessed() } + @Test + fun shouldUpdateValueWhenSavedForTwoTablesAndIndicators() { + val mockedTableCell = mock { + on { id } doReturn "mocked_id" + on { column } doReturn 1 + on { row } doReturn 0 + on { value } doReturn "valueToSave" + } + + val mockedTableModelA = mock { + on { id } doReturn "tableIdA" + on { hasCellWithId(any()) } doReturn true + } + + val mockedTableModelB = mock { + on { id } doReturn "tableIdB" + on { hasCellWithId(any()) } doReturn true + } + + val mockedIndicatorTableModel = mock { + on { id } doReturn INDICATORS_TABLE_ID + } + + val mockedUpdatedTableModel = mock { + on { id } doReturn "tableIdA_updated" + on { hasCellWithId(any()) } doReturn true + } + + val mockedUpdatedIndicatorTableModel = mock { + on { id } doReturn "updated_indicator" + } + + val tableStateValue = presenter.mutableTableData() + tableStateValue.value = TableScreenState( + listOf(mockedTableModelA, mockedTableModelB, mockedIndicatorTableModel), + ) + + whenever(valueStore.save(any(), any(), any(), any(), any(), any())) doReturn Flowable.just( + StoreResult( + uid = "id", + valueStoreResult = ValueStoreResult.VALUE_CHANGED, + valueStoreResultMessage = null, + ), + ) + + whenever(dataValueRepository.getDataTableModel(any())) doReturn Observable.just( + mockedDataTableModel, + ) + whenever(dataValueRepository.setTableData(any(), any())) doReturn mockedTableData + whenever(mapper.invoke(any())) doReturn mockedUpdatedTableModel + whenever( + mockedUpdatedTableModel.copy(overwrittenValues = mockedTableModel.overwrittenValues), + ) doReturn mockedUpdatedTableModel + + whenever(dataValueRepository.getDataSetIndicators()) doReturn Single.just(mockedIndicators) + whenever(mapper.map(any())) doReturn mockedUpdatedIndicatorTableModel + + presenter.onSaveValueChange(mockedTableCell) + + assertTrue(presenter.currentState().value.tables.size == 3) + assertTrue(presenter.currentState().value.tables[0].id == "tableIdA_updated") + assertTrue(presenter.currentState().value.tables[1].id == "tableIdB") + assertTrue(presenter.currentState().value.tables.last().id == "updated_indicator") + verify(view).onValueProcessed() + } + @Test fun shouldSetErrorValue() { val mockedTableCell = mock { diff --git a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterTest.kt b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterTest.kt index 51e53df2f2..b32e1a1534 100644 --- a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterTest.kt @@ -6,6 +6,7 @@ import io.reactivex.Observable import io.reactivex.Single import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.data.schedulers.TrampolineSchedulerProvider +import org.dhis2.ui.dialogs.bottomsheet.FieldWithIssue import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain.ConfigureEventCompletionDialog import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.model.EventCompletionDialog import org.hisp.dhis.android.core.common.ValidationStrategy @@ -252,7 +253,16 @@ class EventCapturePresenterTest { whenever(eventRepository.eventIntegrityCheck()) doReturn Flowable.just(false) whenever(eventRepository.eventStatus()) doReturn Flowable.just(EventStatus.COMPLETED) whenever(eventRepository.isEventEditable("eventUid")) doReturn true - + whenever( + eventRepository.validationStrategy(), + ) doReturn ValidationStrategy.ON_UPDATE_AND_INSERT + val eventCompletionDialog: EventCompletionDialog = mock() + whenever( + configureEventCompletionDialog.invoke( + emptyList(), emptyMap(), emptyList(), true, null, false, + EventStatus.COMPLETED, + ), + ) doReturn eventCompletionDialog whenever(eventRepository.isCompletedEventExpired(any())) doReturn Observable.just(true) whenever(eventRepository.isEventEditable(any())) doReturn true @@ -269,7 +279,13 @@ class EventCapturePresenterTest { whenever(eventRepository.isEventEditable("eventUid")) doReturn true presenter.init() - + whenever( + eventRepository.validationStrategy(), + ) doReturn ValidationStrategy.ON_UPDATE_AND_INSERT + val eventCompletionDialog: EventCompletionDialog = mock() + whenever( + configureEventCompletionDialog.invoke(emptyList(), emptyMap(), emptyList(), true, null, false, EventStatus.COMPLETED), + ) doReturn eventCompletionDialog whenever(eventRepository.isCompletedEventExpired(any())) doReturn Observable.just(false) whenever(eventRepository.isEventEditable(any())) doReturn true whenever(eventRepository.isEnrollmentCancelled) doReturn true @@ -325,7 +341,7 @@ class EventCapturePresenterTest { ) doReturn ValidationStrategy.ON_UPDATE_AND_INSERT val eventCompletionDialog: EventCompletionDialog = mock() whenever( - configureEventCompletionDialog.invoke(any(), any(), any(), any(), any(), any()), + configureEventCompletionDialog.invoke(any(), any(), any(), any(), any(), any(), any()), ) doReturn eventCompletionDialog whenever( eventRepository.isEnrollmentOpen, @@ -334,7 +350,37 @@ class EventCapturePresenterTest { presenter.attemptFinish( canComplete = true, onCompleteMessage = "Complete", - errorFields = emptyList(), + errorFields = listOf(mock()), + emptyMandatoryFields = emptyMap(), + warningFields = emptyList(), + ) + + verify(view).showCompleteActions(any()) + verify(view).showNavigationBar() + } + + @Test + fun `Should show completion dialog and not navigate back when event is completed and there are error fields`() { + initializeMocks() + whenever(eventRepository.eventIntegrityCheck()) doReturn Flowable.just(false) + whenever(eventRepository.eventStatus()) doReturn Flowable.just(EventStatus.COMPLETED) + whenever(eventRepository.isEventEditable("eventUid")) doReturn true + + whenever( + eventRepository.validationStrategy(), + ) doReturn ValidationStrategy.ON_UPDATE_AND_INSERT + val eventCompletionDialog = EventCompletionDialog(mock(), mock(), null, listOf(FieldWithIssue("uid", "fieldName", mock(), "message"))) + whenever( + configureEventCompletionDialog.invoke(any(), any(), any(), any(), any(), any(), any()), + ) doReturn eventCompletionDialog + whenever( + eventRepository.isEnrollmentOpen, + ) doReturn true + + presenter.attemptFinish( + canComplete = true, + onCompleteMessage = "Complete", + errorFields = listOf(FieldWithIssue("uid", "fieldName", mock(), "message")), emptyMandatoryFields = emptyMap(), warningFields = emptyList(), ) @@ -343,6 +389,35 @@ class EventCapturePresenterTest { verify(view).showNavigationBar() } + @Test + fun `Should save and finish if event is is completed and has no errors`() { + initializeMocks() + whenever(eventRepository.eventIntegrityCheck()) doReturn Flowable.just(false) + whenever(eventRepository.eventStatus()) doReturn Flowable.just(EventStatus.COMPLETED) + whenever(eventRepository.isEventEditable("eventUid")) doReturn true + + whenever( + eventRepository.validationStrategy(), + ) doReturn ValidationStrategy.ON_UPDATE_AND_INSERT + val eventCompletionDialog: EventCompletionDialog = mock() + whenever( + configureEventCompletionDialog.invoke(any(), any(), any(), any(), any(), any(), any()), + ) doReturn eventCompletionDialog + whenever( + eventRepository.isEnrollmentOpen, + ) doReturn true + + presenter.attemptFinish( + canComplete = true, + onCompleteMessage = "Complete", + errorFields = emptyList(), + emptyMandatoryFields = emptyMap(), + warningFields = emptyList(), + ) + + verify(view).saveAndFinish() + } + @Test fun `Should init note counter`() { whenever(eventRepository.noteCount) doReturnConsecutively listOf( diff --git a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialogTest.kt b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialogTest.kt index 3c151bb86f..a7b65bd709 100644 --- a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialogTest.kt +++ b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialogTest.kt @@ -3,6 +3,7 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain import org.dhis2.ui.dialogs.bottomsheet.FieldWithIssue import org.dhis2.ui.dialogs.bottomsheet.IssueType import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.provider.EventCaptureResourcesProvider +import org.hisp.dhis.android.core.event.EventStatus import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -56,6 +57,7 @@ class ConfigureEventCompletionDialogTest { canComplete = true, onCompleteMessage = null, canSkipErrorFix = true, + EventStatus.ACTIVE, ) // Then Dialog should has Error info @@ -79,6 +81,7 @@ class ConfigureEventCompletionDialogTest { canComplete = true, onCompleteMessage = null, canSkipErrorFix = true, + EventStatus.ACTIVE, ) // Then Dialog should has Error info @@ -101,6 +104,7 @@ class ConfigureEventCompletionDialogTest { canComplete = true, onCompleteMessage = null, canSkipErrorFix = true, + EventStatus.ACTIVE, ) // Then Dialog should has Error info @@ -121,6 +125,7 @@ class ConfigureEventCompletionDialogTest { canComplete = true, onCompleteMessage = null, canSkipErrorFix = true, + EventStatus.ACTIVE, ) // Then Dialog should has Error info @@ -141,6 +146,7 @@ class ConfigureEventCompletionDialogTest { canComplete = true, onCompleteMessage = WARNING_MESSAGE, canSkipErrorFix = true, + EventStatus.ACTIVE, ) // Then Dialog should has Error info @@ -161,6 +167,7 @@ class ConfigureEventCompletionDialogTest { canComplete = false, onCompleteMessage = ERROR_INFO, canSkipErrorFix = true, + EventStatus.ACTIVE, ) // Then Dialog should has Error info diff --git a/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt index 8e2b7019bb..33dcdb70c0 100644 --- a/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt @@ -455,6 +455,7 @@ class LoginViewModelTest { val mockedDatabase: File = mock() instantiateLoginViewModel() + whenever(resourceManager.getString(any())) doReturn "Import successful" whenever( userManager.d2.maintenanceModule().databaseImportExport() .importDatabase(mockedDatabase), @@ -470,6 +471,7 @@ class LoginViewModelTest { testingDispatcher.scheduler.advanceUntilIdle() verify(view).setUrl("serverUrl") verify(view).setUser("userName") + verify(view).displayMessage("Import successful") verify(view).onDbImportFinished(true) } diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenterTest.kt index f781a95e01..8d7e18bded 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenterTest.kt @@ -14,7 +14,6 @@ import org.dhis2.utils.analytics.CLICK import org.dhis2.utils.analytics.DELETE_TEI import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.EnrollmentStatus -import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramStage @@ -70,7 +69,6 @@ class TeiDashboardPresenterTest { val trackedEntityInstance = TrackedEntityInstance.builder().uid(teiUid).build() val enrollment = Enrollment.builder().uid("enrollmentUid").build() val programStages = listOf(ProgramStage.builder().uid("programStageUid").build()) - val events = listOf(Event.builder().uid("eventUid").build()) val trackedEntityAttributes = listOf( Pair( TrackedEntityAttribute.builder().uid("teiAttr").build(), @@ -95,9 +93,6 @@ class TeiDashboardPresenterTest { whenever( repository.getProgramStages(programUid), ) doReturn Observable.just(programStages) - whenever( - repository.getTEIEnrollmentEvents(programUid, teiUid), - ) doReturn Observable.just(events) whenever( repository.getAttributesMap(programUid, teiUid), ) doReturn Observable.just(trackedEntityAttributes) diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/ui/mapper/InfoBarMapperTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/ui/mapper/InfoBarMapperTest.kt index 0c53970687..144eea5fef 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/ui/mapper/InfoBarMapperTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/ui/mapper/InfoBarMapperTest.kt @@ -8,7 +8,6 @@ import org.dhis2.usescases.teiDashboard.ui.model.InfoBarType import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.EnrollmentStatus -import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramStage @@ -108,7 +107,6 @@ class InfoBarMapperTest { val model = DashboardEnrollmentModel( setEnrollment(state, status, followup), emptyList(), - emptyList(), setTei(state), attributeValues, emptyList(), diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/ui/mapper/TEIDetailMapperTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/ui/mapper/TEIDetailMapperTest.kt index dfd0c76782..ed304e65f3 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/ui/mapper/TEIDetailMapperTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/ui/mapper/TEIDetailMapperTest.kt @@ -7,7 +7,6 @@ import org.dhis2.usescases.teiDashboard.DashboardEnrollmentModel import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.EnrollmentStatus -import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramStage @@ -61,7 +60,6 @@ class TEIDetailMapperTest { val model = DashboardEnrollmentModel( setEnrollment(), emptyList(), - emptyList(), setTei(), attributeValues, emptyList(), diff --git a/commons/src/main/java/org/dhis2/commons/date/DateExtensions.kt b/commons/src/main/java/org/dhis2/commons/date/DateExtensions.kt index f2258133e6..17b532a446 100644 --- a/commons/src/main/java/org/dhis2/commons/date/DateExtensions.kt +++ b/commons/src/main/java/org/dhis2/commons/date/DateExtensions.kt @@ -104,6 +104,9 @@ fun Date?.toOverdueOrScheduledUiText( }.toPeriod(PeriodType.yearMonthDayTime()) return when { + period.days == 0 && period.months == 0 && period.years == 0 -> { + resourceManager.getString(R.string.overdue_today) + } period.years >= 1 -> { getString( resourceManager, @@ -113,7 +116,6 @@ fun Date?.toOverdueOrScheduledUiText( isOverdue, ) } - period.months >= 3 && period.years < 1 -> { getString( resourceManager, @@ -123,8 +125,7 @@ fun Date?.toOverdueOrScheduledUiText( isOverdue, ) } - - period.days in 1..89 -> { + period.days in 0..89 && period.months in 0..2 -> { val intervalDays = if (this.time > currentDay.time) { Interval(currentDay.time, this.time) } else { @@ -133,8 +134,6 @@ fun Date?.toOverdueOrScheduledUiText( getOverdueDaysString(intervalDays, isOverdue) } - - period.days == 0 -> resourceManager.getString(R.string.overdue_today) else -> { getOverdueDaysString(period.days, isOverdue) } diff --git a/form/src/main/java/org/dhis2/form/model/FieldListConfiguration.kt b/form/src/main/java/org/dhis2/form/model/FieldListConfiguration.kt new file mode 100644 index 0000000000..f74e8a8cbc --- /dev/null +++ b/form/src/main/java/org/dhis2/form/model/FieldListConfiguration.kt @@ -0,0 +1,3 @@ +package org.dhis2.form.model + +data class FieldListConfiguration(val skipProgramRules: Boolean, val finish: Boolean) diff --git a/form/src/main/java/org/dhis2/form/ui/Form.kt b/form/src/main/java/org/dhis2/form/ui/Form.kt index 7056344e33..ad2ea55870 100644 --- a/form/src/main/java/org/dhis2/form/ui/Form.kt +++ b/form/src/main/java/org/dhis2/form/ui/Form.kt @@ -20,9 +20,6 @@ import androidx.compose.material.Icon import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -31,11 +28,10 @@ import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.dhis2.commons.resources.ResourceManager import org.dhis2.form.R +import org.dhis2.form.data.EventRepository import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.FormSection import org.dhis2.form.ui.event.RecyclerViewUiEvents @@ -71,17 +67,26 @@ fun Form( } } } - val focusNext = remember { mutableStateOf(false) } LazyColumn( modifier = Modifier .fillMaxSize() - .background(Color.White, shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomStart = 0.dp, bottomEnd = 0.dp)) + .background( + Color.White, + shape = RoundedCornerShape( + topStart = Spacing.Spacing16, + topEnd = Spacing.Spacing16, + bottomStart = Spacing.Spacing0, + bottomEnd = Spacing.Spacing0, + ), + ) .clickable( - interactionSource = MutableInteractionSource(), + interactionSource = remember { + MutableInteractionSource() + }, indication = null, onClick = { focusManager.clearFocus() }, ), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp), + contentPadding = PaddingValues(horizontal = Spacing.Spacing16, vertical = Spacing.Spacing16), state = scrollState, ) { if (sections.isNotEmpty()) { @@ -101,12 +106,13 @@ fun Form( } } + val completedAndTotalFields = totalAndCompletedFields(section) Section( title = section.title, isLastSection = getNextSection(section, sections) == null, - description = if (section.fields.isNotEmpty()) section.description else null, - completedFields = section.completedFields(), - totalFields = section.fields.size, + description = sectionDescription(section), + completedFields = completedAndTotalFields.second, + totalFields = completedAndTotalFields.first, state = section.state, errorCount = section.errorCount(), warningCount = section.warningCount(), @@ -132,12 +138,7 @@ fun Form( resources = resources, focusManager = focusManager, onNextClicked = { - if (index == section.fields.size - 1) { - onNextSection() - focusNext.value = true - } else { - focusManager.moveFocus(FocusDirection.Down) - } + manageOnNextEvent(focusManager, index, section, onNextSection) }, ) } @@ -155,6 +156,39 @@ fun Form( } } +private fun manageOnNextEvent( + focusManager: FocusManager, + index: Int, + section: FormSection, + onNext: () -> Unit, +) { + if (index == section.fields.size - 1) { + onNext() + } else { + focusManager.moveFocus(FocusDirection.Down) + } +} + +private fun sectionDescription(section: FormSection): String? { + return if (section.fields.isNotEmpty()) section.description else null +} + +private fun totalAndCompletedFields(section: FormSection): Pair { + var totalFields = section.fields.size + var completedFields = section.completedFields() + if (section.uid == EventRepository.EVENT_CATEGORY_COMBO_SECTION_UID && section.fields.first().eventCategories != null) { + completedFields = section.fields.first().eventCategories?.associate { category -> + category.options.find { option -> + section.fields.first().value?.split(",")?.contains(option.uid) == true + }?.let { + category.uid to it + } ?: (category.uid to null) + }?.count { it.value != null } ?: 0 + totalFields = section.fields.first().eventCategories?.size ?: 1 + } + return Pair(totalFields, completedFields) +} + fun shouldDisplayNoFieldsWarning(sections: List): Boolean { return if (sections.size == 1) { val section = sections.first() @@ -192,22 +226,6 @@ fun NoFieldsWarning(resources: ResourceManager) { } } -private fun FocusManager.moveFocusNext(focusNext: MutableState) { - if (focusNext.value) { - this.moveFocus(FocusDirection.Next) - focusNext.value = false - } -} - -@Composable -private fun LaunchIfTrue(key: Boolean, block: suspend CoroutineScope.() -> Unit) { - LaunchedEffect(key) { - if (key) { - block() - } - } -} - private fun getNextSection(section: FormSection, sections: List): FormSection? { val currentIndex = sections.indexOf(section) if (currentIndex != -1 && currentIndex < sections.size - 1) { diff --git a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt index 4f77acb803..e3fd22ad59 100644 --- a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt +++ b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt @@ -7,6 +7,9 @@ import androidx.lifecycle.viewModelScope import com.google.gson.Gson import com.google.gson.reflect.TypeToken import kotlinx.coroutines.async +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn @@ -21,6 +24,7 @@ import org.dhis2.form.data.GeometryController import org.dhis2.form.data.GeometryParserImpl import org.dhis2.form.data.RulesUtilsProviderConfigurationError import org.dhis2.form.model.ActionType +import org.dhis2.form.model.FieldListConfiguration import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.InfoUiModel import org.dhis2.form.model.RowAction @@ -78,6 +82,11 @@ class FormViewModel( private val _pendingIntents = MutableSharedFlow() + private val fieldListChannel = Channel( + capacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + init { viewModelScope.launch { _pendingIntents @@ -92,6 +101,19 @@ class FormViewModel( .flowOn(dispatcher.io()) .collect { result -> displayResult(result) } } + + viewModelScope.launch(dispatcher.io()) { + fieldListChannel.consumeEach { fieldListConfiguration -> + val result = async { + repository.composeList(fieldListConfiguration.skipProgramRules) + } + _items.postValue(result.await()) + if (fieldListConfiguration.finish) { + runDataIntegrityCheck() + } + } + } + loadData() } @@ -648,15 +670,9 @@ class FormViewModel( private fun processCalculatedItems(skipProgramRules: Boolean = false, finish: Boolean = false) { FormCountingIdlingResource.increment() - viewModelScope.launch(dispatcher.io()) { - val result = async { - repository.composeList(skipProgramRules) - } - _items.postValue(result.await()) - if (finish) { - runDataIntegrityCheck() - } - } + fieldListChannel.trySend( + FieldListConfiguration(skipProgramRules, finish), + ) } fun updateConfigurationErrors() { diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt index 8301f6d1f4..dee4b5a5fb 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt @@ -42,17 +42,21 @@ fun ProvideInputDate( ValueType.TIME -> DateTimeActionType.TIME to TimeTransformation() else -> DateTimeActionType.DATE to DateTransformation() } - val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) + val textSelection = TextRange( + fieldUiModel.value?.length ?: 0, + ) + val yearIntRange = getYearRange(fieldUiModel) val selectableDates = getSelectableDates(fieldUiModel) var value by remember(fieldUiModel.value) { mutableStateOf( - if (fieldUiModel.value != null) { - TextFieldValue(formatStoredDateToUI(fieldUiModel.value!!, fieldUiModel.valueType), textSelection) - } else { - TextFieldValue() - }, + fieldUiModel.value?.let { value -> + TextFieldValue( + formatStoredDateToUI(value, fieldUiModel.valueType), + textSelection, + ) + } ?: TextFieldValue(), ) } @@ -69,20 +73,29 @@ fun ProvideInputDate( onNextClicked = onNextClicked, onValueChanged = { value = it ?: TextFieldValue() - intentHandler.invoke( + val formIntent = if (value.text.length == 8) { + FormIntent.OnSave( + uid = fieldUiModel.uid, + value = formatUIDateToStored(it?.text, fieldUiModel.valueType), + valueType = fieldUiModel.valueType, + ) + } else { FormIntent.OnTextChange( uid = fieldUiModel.uid, value = formatUIDateToStored(it?.text, fieldUiModel.valueType), valueType = fieldUiModel.valueType, allowFutureDates = fieldUiModel.allowFutureDates ?: true, - ), - ) + ) + } + intentHandler.invoke(formIntent) }, selectableDates = selectableDates, yearRange = yearIntRange, inputStyle = inputStyle, ), - modifier = modifier.semantics { contentDescription = formatStoredDateToUI(value.text, fieldUiModel.valueType) }, + modifier = modifier.semantics { + contentDescription = formatStoredDateToUI(value.text, fieldUiModel.valueType) + }, ) } @@ -99,27 +112,24 @@ private fun getSelectableDates(uiModel: FieldUiModel): SelectableDates { ) } } else { - uiModel.selectableDates ?: SelectableDates(initialDate = DEFAULT_MIN_DATE, endDate = DEFAULT_MAX_DATE) + uiModel.selectableDates ?: SelectableDates( + initialDate = DEFAULT_MIN_DATE, + endDate = DEFAULT_MAX_DATE, + ) } } private fun getYearRange(uiModel: FieldUiModel): IntRange { - return if (uiModel.selectableDates == null) { - if (uiModel.allowFutureDates == true) { - IntRange(1924, 2124) - } else { - IntRange( - 1924, - Calendar.getInstance()[Calendar.YEAR], - ) - } - } else { - IntRange( - uiModel.selectableDates!!.initialDate.substring(4, 8).toInt(), - uiModel.selectableDates!!.endDate.substring(4, 8).toInt(), - ) + val toYear = when (uiModel.allowFutureDates) { + true -> 2124 + else -> Calendar.getInstance()[Calendar.YEAR] } + return IntRange( + uiModel.selectableDates?.initialDate?.substring(4, 8)?.toInt() ?: 1924, + uiModel.selectableDates?.endDate?.substring(4, 8)?.toInt() ?: toYear, + ) } + private fun formatStoredDateToUI(inputDateString: String, valueType: ValueType?): String { return when (valueType) { ValueType.DATETIME -> { diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt index 6106f2072f..b34f4b071e 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.dhis2.commons.resources.ResourceManager import org.dhis2.form.extensions.autocompleteList @@ -79,14 +80,16 @@ fun FieldProvider( .onSizeChanged { intSize -> visibleArea = Rect( size = Size(intSize.width.toFloat(), intSize.height.toFloat()), - offset = Offset(0f, 20f), + offset = Offset(0f, 200f), ) } .onFocusChanged { if (it.isFocused && !fieldUiModel.focused) { scope.launch { - bringIntoViewRequester.bringIntoView(visibleArea) fieldUiModel.onItemClick() + + delay(10) + bringIntoViewRequester.bringIntoView(visibleArea) } } } diff --git a/form/src/main/res/values/strings.xml b/form/src/main/res/values/strings.xml index 88ca0e3b24..75eada1571 100644 --- a/form/src/main/res/values/strings.xml +++ b/form/src/main/res/values/strings.xml @@ -58,6 +58,7 @@ Saved! Some fields need your attention.\nDo you want to review the form? Not now + Save anyway Keep editing If you exit now all the information in the form will be discarded. Some fields have errors and they are not saved. \nIf you exit now the changes will be discarded. diff --git a/ui-components/src/main/java/org/dhis2/ui/dialogs/alert/AlertDialog.kt b/ui-components/src/main/java/org/dhis2/ui/dialogs/alert/AlertDialog.kt index 9bb7127f5c..afe0d6ed14 100644 --- a/ui-components/src/main/java/org/dhis2/ui/dialogs/alert/AlertDialog.kt +++ b/ui-components/src/main/java/org/dhis2/ui/dialogs/alert/AlertDialog.kt @@ -123,87 +123,88 @@ fun Dhis2AlertDialogUi( ) var confirmButtonClick = remember { mutableStateOf(false) } - - AlertDialog( - onDismissRequest = dismissButton.onClick, - title = { Text(text = labelText, textAlign = TextAlign.Center) }, - text = { - Column { - Text( - text = buildAnnotatedString { - append(descriptionText) - spanText?.let { - addStyle( - style = SpanStyle(MaterialTheme.colorScheme.primary), - start = descriptionText.indexOf(spanText), - end = descriptionText.indexOf(spanText) + spanText.length, - ) - } - }, - ) - animationRes?.let { - Spacer(modifier = Modifier.size(16.dp)) - if (!confirmButtonClick.value) { - LottieAnimation( - modifier = Modifier - .fillMaxWidth() - .height(200.dp), - composition = composition, - iterations = LottieConstants.IterateForever, - ) - } else { - Row( - modifier = Modifier - .fillMaxWidth() - .height(200.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - ProgressIndicator( + Dhis2Theme { + AlertDialog( + onDismissRequest = dismissButton.onClick, + title = { Text(text = labelText, textAlign = TextAlign.Center) }, + text = { + Column { + Text( + text = buildAnnotatedString { + append(descriptionText) + spanText?.let { + addStyle( + style = SpanStyle(MaterialTheme.colorScheme.primary), + start = descriptionText.indexOf(spanText), + end = descriptionText.indexOf(spanText) + spanText.length, + ) + } + }, + ) + animationRes?.let { + Spacer(modifier = Modifier.size(16.dp)) + if (!confirmButtonClick.value) { + LottieAnimation( modifier = Modifier - .width(100.dp) - .height(100.dp), - type = ProgressIndicatorType.CIRCULAR, + .fillMaxWidth() + .height(200.dp), + composition = composition, + iterations = LottieConstants.IterateForever, ) + } else { + Row( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + ProgressIndicator( + modifier = Modifier + .width(100.dp) + .height(100.dp), + type = ProgressIndicatorType.CIRCULAR, + ) + } } } } - } - }, - icon = { - iconResource?.let { - Icon( - painter = painterResource(id = iconResource), - tint = MaterialTheme.colorScheme.primary, - contentDescription = "notification alert", - ) - } - }, - confirmButton = { - TextButton( - modifier = Modifier.testTag(CONFIRM_BUTTON_TAG), - onClick = { - confirmButtonClick.value = true - animationRes?.let { - val job = Job() - val scope = CoroutineScope(job) + }, + icon = { + iconResource?.let { + Icon( + painter = painterResource(id = iconResource), + tint = MaterialTheme.colorScheme.primary, + contentDescription = "notification alert", + ) + } + }, + confirmButton = { + TextButton( + modifier = Modifier.testTag(CONFIRM_BUTTON_TAG), + onClick = { + confirmButtonClick.value = true + animationRes?.let { + val job = Job() + val scope = CoroutineScope(job) - scope.launch { - delay(5000) - confirmButton.onClick.invoke() - } - } ?: confirmButton.onClick.invoke() - }, - ) { - Text(text = confirmButton.text) - } - }, - dismissButton = { - TextButton(onClick = dismissButton.onClick) { - Text(text = dismissButton.text) - } - }, - ) + scope.launch { + delay(5000) + confirmButton.onClick.invoke() + } + } ?: confirmButton.onClick.invoke() + }, + ) { + Text(text = confirmButton.text) + } + }, + dismissButton = { + TextButton(onClick = dismissButton.onClick) { + Text(text = dismissButton.text) + } + }, + ) + } } const val CONFIRM_BUTTON_TAG = "CONFIRM_BUTTON_TAG"