From d64dd5750c57447223b682fc71d0021c47cc53fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Mon, 1 Jul 2024 15:31:56 +0200 Subject: [PATCH] fix: [ANDROAPP-6104] do not allow futures dates in age by default (#261) --- .../ui/designsystem/component/InputAge.kt | 47 +++++++++++++++++-- .../designsystem/component/InputDateTime.kt | 6 +-- .../ui/designsystem/component/InputAgeTest.kt | 28 +++++++++++ 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAge.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAge.kt index bcc75de3c..3bfbedc99 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAge.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAge.kt @@ -39,6 +39,8 @@ import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2LightColorScheme import org.hisp.dhis.mobile.ui.designsystem.theme.Outline import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import java.text.SimpleDateFormat +import java.util.Calendar /** * DHIS2 Input Age component wraps DHIS2 [InputShell]. @@ -71,12 +73,16 @@ fun InputAge( is Age -> HelperStyle.WITH_HELPER_AFTER } } + val selectableDates = uiModel.selectableDates ?: SelectableDates( + MIN_DATE, + SimpleDateFormat(DATE_FORMAT).format(Calendar.getInstance().time), + ) val focusRequester = remember { FocusRequester() } val datePickerState = rememberDatePickerState( selectableDates = object : androidx.compose.material3.SelectableDates { override fun isSelectableDate(utcTimeMillis: Long): Boolean { - return dateIsInRange(utcTimeMillis, uiModel.selectableDates) + return dateIsInRange(utcTimeMillis, selectableDates) } }, ) @@ -118,10 +124,15 @@ fun InputAge( } } + val supportingText = provideSupportingText(uiModel, selectableDates) + InputShell( modifier = modifier.testTag("INPUT_AGE").focusRequester(focusRequester), title = uiModel.title, - state = uiModel.state, + state = when (supportingText) { + uiModel.supportingText -> uiModel.state + else -> InputShellState.ERROR + }, isRequiredField = uiModel.isRequired, inputField = { when (uiModel.inputType) { @@ -193,7 +204,7 @@ fun InputAge( }, secondaryButton = calendarButton, supportingText = { - uiModel.supportingText?.forEach { label -> + supportingText?.forEach { label -> SupportingText( label.text, label.state, @@ -309,6 +320,34 @@ private fun updateDateOfBirth(inputType: DateOfBirth, newText: TextFieldValue): } } +@Composable +private fun provideSupportingText( + uiModel: InputAgeModel, + selectableDates: SelectableDates, +): List? = + (uiModel.inputType as? DateOfBirth)?.value?.text?.let { + if ( + it.length == DATE_FORMAT.length && + !dateIsInRange(parseStringDateToMillis(it), selectableDates) + ) { + val dateOutOfRangeText = "${provideStringResource("date_out_of_range")} (" + + formatStringToDate(selectableDates.initialDate) + " - " + + formatStringToDate(selectableDates.endDate) + ")" + + listOf( + SupportingTextData( + text = dateOutOfRangeText, + SupportingTextState.ERROR, + ), + ).plus(uiModel.supportingText ?: listOf()) + } else { + uiModel.supportingText + } + } ?: uiModel.supportingText + +internal const val MIN_DATE = "10111901" +internal const val DATE_FORMAT = "ddMMYYYY" + sealed interface AgeInputType { data object None : AgeInputType @@ -364,5 +403,5 @@ data class InputAgeModel( val acceptText: String? = null, val cancelText: String? = null, val onValueChanged: (AgeInputType) -> Unit, - val selectableDates: SelectableDates = SelectableDates("10111901", "12112124"), + val selectableDates: SelectableDates? = null, ) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt index e0fcc5b2f..85afa9e2c 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt @@ -402,7 +402,7 @@ fun getSelectableDates(uiModel: InputDateTimeModel): androidx.compose.material3. } } -private fun getSupportingTextList(uiModel: InputDateTimeModel, dateOutOfRangeItem: SupportingTextData, incorrectHourFormatItem: SupportingTextData): List { +fun getSupportingTextList(uiModel: InputDateTimeModel, dateOutOfRangeItem: SupportingTextData, incorrectHourFormatItem: SupportingTextData): List { val supportingTextList = mutableListOf() uiModel.supportingText?.forEach { item -> @@ -547,7 +547,7 @@ private fun getTime(timePickerState: TimePickerState, format: String? = "HHmm"): return formater.format(cal.time) } -private fun parseStringDateToMillis(dateString: String, pattern: String = "ddMMyyyy", locale: Locale = Locale.getDefault()): Long { +fun parseStringDateToMillis(dateString: String, pattern: String = "ddMMyyyy", locale: Locale = Locale.getDefault()): Long { return if (dateString.isNotEmpty()) { val cal = Calendar.getInstance() val sdf = SimpleDateFormat(pattern, locale) @@ -564,7 +564,7 @@ data class SelectableDates( val endDate: String, ) -private fun formatStringToDate(dateString: String): String { +fun formatStringToDate(dateString: String): String { return if (dateString.length == 8) { dateString.substring(0, 2) + "/" + dateString.substring(2, 4) + "/" + dateString.substring(4, 8) } else { diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAgeTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAgeTest.kt index 3b327004c..fbffaf18c 100644 --- a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAgeTest.kt +++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAgeTest.kt @@ -9,6 +9,8 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput import org.junit.Rule import org.junit.Test +import java.text.SimpleDateFormat +import java.util.Calendar class InputAgeTest { @@ -148,4 +150,30 @@ class InputAgeTest { rule.onNodeWithTag("INPUT_AGE_OPEN_CALENDAR_BUTTON").assertDoesNotExist() rule.onNodeWithTag("INPUT_AGE_TIME_UNIT_SELECTOR").assertDoesNotExist() } + + @Test + fun shouldShowErrorMessageWhenAgeIsOnFuture() { + val calendar = Calendar.getInstance().apply { + add(Calendar.DAY_OF_MONTH, 1) + } + val futureDate = SimpleDateFormat(DATE_FORMAT).format(calendar.time) + var inputType by mutableStateOf(AgeInputType.DateOfBirth.EMPTY) + + rule.setContent { + InputAge( + InputAgeModel( + title = "Label", + inputType = inputType, + onValueChanged = { + inputType = it + }, + ), + + ) + } + + rule.onNodeWithTag("INPUT_AGE_TEXT_FIELD").performTextInput(futureDate) + + rule.onNodeWithTag("INPUT_AGE_SUPPORTING_TEXT").assertExists() + } }