From 0db2953a4fff25953664d8284ee52c42cc90954f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Tue, 21 May 2024 10:35:51 +0200 Subject: [PATCH 01/35] refactor: [ANDROAPP-6029] remove EventDetailIdlingResourceSingleton (#3639) --- .../java/org/dhis2/usescases/BaseTest.kt | 3 --- .../ui/EventDetailIdlingResourceSingleton.kt | 20 ------------------ .../eventDetails/ui/EventDetailsViewModel.kt | 21 ++++++++++--------- 3 files changed, 11 insertions(+), 33 deletions(-) delete mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailIdlingResourceSingleton.kt 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/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 { From 8c1e94b4ba7102ed360e850994bb4fb228964e60 Mon Sep 17 00:00:00 2001 From: Xavier Molloy <44061143+xavimolloy@users.noreply.github.com> Date: Wed, 22 May 2024 19:19:58 +0200 Subject: [PATCH 02/35] fix: [ANDROAPP-6146] modify start padding for download share button (#3641) * fix: [ANDROAPP-6146] modify start padding for download share button container, change dp paddings to mobile ui Spacings, change Drawable resource Icon to Material Icon * fix: [ANDROAPP-6146] sonar code smell fixes, change material theme for mobile ui theme --- .../usescases/settings/ui/ExportOption.kt | 103 ++++++------ .../org/dhis2/ui/dialogs/alert/AlertDialog.kt | 151 +++++++++--------- 2 files changed, 127 insertions(+), 127 deletions(-) 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/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" From 1156ebffe697848f619499f242bbb90933ebc42d Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Wed, 29 May 2024 14:25:58 +0530 Subject: [PATCH 03/35] Improve scrolling to prevent overlap with save button (#3642) --- .../org/dhis2/usescases/teidashboard/TeiDashboardTest.kt | 2 ++ .../org/dhis2/form/ui/provider/inputfield/FieldProvider.kt | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) 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..7227920a0b 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() { 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) } } } From 2d3b4e7f2e5827607eceae095a31db4c415e9682 Mon Sep 17 00:00:00 2001 From: Xavier Molloy <44061143+xavimolloy@users.noreply.github.com> Date: Wed, 29 May 2024 16:02:59 +0200 Subject: [PATCH 04/35] fix: [ANDROAPP-6137] Category-Combo-section-shows-incorrect-number-of-fields (#3659) * fix: [ANDROAPP-6137] take into account number of fields if section is for event category combo, refactor for cognitive complexity * fix: [ANDROAPP-6137] femove unused methods and variable --- form/src/main/java/org/dhis2/form/ui/Form.kt | 86 ++++++++++++-------- 1 file changed, 52 insertions(+), 34 deletions(-) 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) { From ac77ef3fad62a371db458ba985e6af9f85279183 Mon Sep 17 00:00:00 2001 From: Ferdy Rodriguez Date: Thu, 30 May 2024 13:33:55 +0200 Subject: [PATCH 05/35] fix: [ANDROAPP-6174] Schedule date on tei card is formatted correctly (#3657) --- .../org/dhis2/bindings/DateExtensionsTest.kt | 18 ++++++++++++++++-- .../org/dhis2/commons/date/DateExtensions.kt | 9 ++++----- 2 files changed, 20 insertions(+), 7 deletions(-) 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/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) } From 383a448a5094ce324630219dfc348cc450ced448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Fri, 31 May 2024 15:49:07 +0200 Subject: [PATCH 06/35] fix: [ANDROAPP-6185] dashboard event list not updated (#3658) * fix: [ANDROAPP-6185] dashboard event list not updated * fix test files --- .../teiDashboard/DashboardProgramModel.kt | 2 -- .../teiDashboard/DashboardRepository.kt | 2 -- .../teiDashboard/DashboardRepositoryImpl.kt | 26 ------------------ .../teidata/TEIDataPresenter.kt | 27 ------------------- .../teiDashboard/TeiDashboardPresenterTest.kt | 5 ---- .../ui/mapper/InfoBarMapperTest.kt | 2 -- .../ui/mapper/TEIDetailMapperTest.kt | 2 -- 7 files changed, 66 deletions(-) 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/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(), From 020f6ff811cccfb4744957f37f888710dd025ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Mon, 3 Jun 2024 08:55:35 +0200 Subject: [PATCH 07/35] fix: [ANDROAPP-6088] scroll to top when update list (#3661) --- .../usescases/searchTrackEntity/listView/SearchTEList.kt | 5 +++++ 1 file changed, 5 insertions(+) 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 } From 3907c5dbb8a4fd28d31839082504b48d29c31aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Wed, 5 Jun 2024 13:11:00 +0200 Subject: [PATCH 08/35] fix: [ANDROAPP-5869] duplicated indicators table (#3666) --- .../dataSetSection/DataValuePresenter.kt | 11 +-- .../TableDataToTableModelMapper.kt | 6 +- .../dataSetSection/DataValuePresenterTest.kt | 69 ++++++++++++++++++- 3 files changed, 80 insertions(+), 6 deletions(-) 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/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 { From 46f31c3560df58ab98b4d559d64243822f503926 Mon Sep 17 00:00:00 2001 From: Ferdy Rodriguez Date: Thu, 6 Jun 2024 07:21:10 +0200 Subject: [PATCH 09/35] show import successfully message (#3663) --- app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt | 2 ++ app/src/main/res/values/strings.xml | 1 + .../test/java/org/dhis2/usescases/login/LoginViewModelTest.kt | 2 ++ 3 files changed, 5 insertions(+) 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/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63608905d2..99bb27844c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -971,4 +971,5 @@ Database downloaded Importing database Re-open form to edit + Import successful 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) } From 0cd01c9d5d7089752d6b7010958a5b624626a042 Mon Sep 17 00:00:00 2001 From: Xavier Molloy <44061143+xavimolloy@users.noreply.github.com> Date: Thu, 6 Jun 2024 10:20:15 +0200 Subject: [PATCH 10/35] feat: [ANDROAPP-6101] modify logic for saving completed events (#3645) * fix: [ANDROAPP-6101] modify logic for saving completed events and add unit tests for new logic * fix: [ANDROAPP-6101] modify complete save action to ignore validation strategy * fix: [ANDROAPP-6101] fix test * fix: [ANDROAPP-6101] allow to save with warnings, update description and secondary button text for warning * fix: [ANDROAPP-6101] fix flaky test --- .../usescases/event/EventRegistrationRobot.kt | 2 +- .../eventCapture/EventCapturePresenterImpl.kt | 70 +++++++++------- .../domain/ConfigureEventCompletionDialog.kt | 28 +++++-- .../provider/EventCaptureResourcesProvider.kt | 4 + app/src/main/res/values/strings.xml | 1 + .../eventCapture/EventCapturePresenterTest.kt | 83 ++++++++++++++++++- .../ConfigureEventCompletionDialogTest.kt | 7 ++ form/src/main/res/values/strings.xml | 1 + 8 files changed, 153 insertions(+), 43 deletions(-) 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/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/res/values/strings.xml b/app/src/main/res/values/strings.xml index 99bb27844c..6321e1aa7c 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? 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/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. From cf85f22547a6e792b12c4d6bcc367683079e9576 Mon Sep 17 00:00:00 2001 From: Ferdy Rodriguez Date: Thu, 6 Jun 2024 14:51:56 +0200 Subject: [PATCH 11/35] prevent app crash when delete local data dialog in shown and device is rotated (#3665) --- .../dhis2/usescases/settings/SyncManagerFragment.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 From 36837c91884b8e3782a2c39ed94d0c135f435fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Thu, 6 Jun 2024 17:34:48 +0200 Subject: [PATCH 12/35] test: [ANDROAPP-5987] shouldSuccessfullyFilterByEventStatusOverdue (#3656) --- .../flow/searchFlow/SearchFlowTest.kt | 1 + .../usescases/flow/teiFlow/TeiFlowRobot.kt | 49 ++++++++++++------- .../usescases/flow/teiFlow/TeiFlowTest.kt | 5 +- .../dhis2/usescases/searchte/SearchTETest.kt | 30 +++--------- .../searchte/robot/SearchTeiRobot.kt | 9 ++++ .../teidashboard/TeiDashboardTest.kt | 2 +- .../teidashboard/robot/EnrollmentRobot.kt | 38 +++++++------- .../teidashboard/robot/EventRobot.kt | 22 ++++++--- 8 files changed, 86 insertions(+), 70 deletions(-) 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 2807eded53..5b7feb4bbc 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt @@ -39,6 +39,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 5b8cf533e8..b59452babd 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt @@ -39,6 +39,7 @@ class TeiFlowTest : BaseTest() { val enrollmentListDetails = createEnrollmentList() val registerTeiDetails = createRegisterTEI() + enableComposeForms() setupCredentials() setDatePicker() prepareWomanProgrammeIntentAndLaunchActivity(ruleSearch) @@ -56,7 +57,7 @@ class TeiFlowTest : BaseTest() { EnrollmentListUIModel( ADULT_WOMAN_PROGRAM, ORG_UNIT, - "30/6/2017", + currentDate, currentDate ) @@ -104,7 +105,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 eadaad1c7b..3472dbc780 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt @@ -1,10 +1,6 @@ package org.dhis2.usescases.searchte -import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.intl.Locale import androidx.test.espresso.IdlingRegistry @@ -21,9 +17,7 @@ import org.dhis2.bindings.app import org.dhis2.common.idlingresources.MapIdlingResource import org.dhis2.commons.date.DateUtils.SIMPLE_DATE_FORMAT import org.dhis2.lazyActivityScenarioRule -import org.dhis2.ui.dialogs.bottomsheet.SECONDARY_BUTTON_TAG import org.dhis2.usescases.BaseTest -import org.dhis2.usescases.flow.teiFlow.TeiFlowTest import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel import org.dhis2.usescases.flow.teiFlow.entity.RegisterTEIUIModel import org.dhis2.usescases.flow.teiFlow.teiFlowRobot @@ -75,7 +69,7 @@ class SearchTETest : BaseTest() { clickOnSearch() checkListOfSearchTEI( title = "First name: $firstName", - attributes = mapOf("Last name:" to lastName) + attributes = mapOf("Last name:" to lastName), ) } } @@ -112,7 +106,7 @@ class SearchTETest : BaseTest() { composeTestRule.waitForIdle() checkListOfSearchTEI( title = "First name: $firstName", - attributes = mapOf("Last name:" to lastName) + attributes = mapOf("Last name:" to lastName), ) } } @@ -156,7 +150,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" @@ -176,12 +170,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) @@ -192,9 +185,7 @@ class SearchTETest : BaseTest() { teiFlowRobot(composeTestRule) { registerTEI(registerTeiDetails) - changeDueDate(scheduledEventTitle, overdueDate) - pressBack() - composeTestRule.onNodeWithTag(SECONDARY_BUTTON_TAG).performClick() + changeDueDate(scheduledEventTitle) pressBack() } @@ -205,8 +196,9 @@ class SearchTETest : BaseTest() { closeFilterRowAtField(eventStatusFilter) checkFilterCounter(totalCount) checkCountAtFilter(eventStatusFilter, totalCount) - clickOnFilter() - checkEventsAreOverdue() + } + searchTeiRobot(composeTestRule) { + checkListOfSearchTEIWithAdditionalInfo("First name: ADRIANNA", "1 day overdue") } } @@ -437,12 +429,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 7227920a0b..b4a9a81a75 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt @@ -273,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) } } From f7c75c7f9db6467a04b4888851f0a9ad59ef8979 Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 10 Jun 2024 14:19:23 +0200 Subject: [PATCH 13/35] fix: [ANDROAPP-6197] Incorrect header in TEI Dashboard card (#3670) Signed-off-by: Pablo --- .../searchTrackEntity/ui/mapper/TEICardMapper.kt | 10 +++------- .../teiDashboard/ui/mapper/TeiDashboardCardMapper.kt | 7 ++++--- 2 files changed, 7 insertions(+), 10 deletions(-) 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/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 } } From 32260615c7cfc93a1b7fb7fcfbcf433cfc15ece1 Mon Sep 17 00:00:00 2001 From: Pablo Date: Wed, 12 Jun 2024 09:54:58 +0200 Subject: [PATCH 14/35] fix: [ANDROAPP-6181] Concurrent modification exception (#3668) Signed-off-by: Pablo --- .../form/model/FieldListConfiguration.kt | 3 ++ .../java/org/dhis2/form/ui/FormViewModel.kt | 34 ++++++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 form/src/main/java/org/dhis2/form/model/FieldListConfiguration.kt 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/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() { From fb6648f95742a60d0ee7b214187ffac2c870ea69 Mon Sep 17 00:00:00 2001 From: Pablo Date: Wed, 12 Jun 2024 13:57:06 +0200 Subject: [PATCH 15/35] fix: [ANDROAPP-6179] Missing date edition warning (#3669) * fix: [ANDROAPP-6179] Missing date edition warning Signed-off-by: Pablo * fix: remove !! Signed-off-by: Pablo --------- Signed-off-by: Pablo --- .../ui/provider/inputfield/DateProvider.kt | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) 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 -> { From 8b1fe073aeef756ac2af011ffa9895c190c6ba4f Mon Sep 17 00:00:00 2001 From: Xavier Molloy <44061143+xavimolloy@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:24:55 +0200 Subject: [PATCH 16/35] fix: [ANDROAPP-6183] fix analytics screen loading on landscape for map tab (#3675) --- .../searchTrackEntity/ui/SearchScreenConfigurator.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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) } } From e202ed58145e89cd90a451228b1b1179749cdfa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Miguel=20Rubio?= Date: Thu, 13 Jun 2024 09:43:35 +0200 Subject: [PATCH 17/35] ci: [ANDROAPP-6219] Remove CI and run CD on pull request (#3678) Signed-off-by: andresmr --- .github/workflows/continuous-delivery.yml | 5 ++ .github/workflows/continuous-integration.yml | 93 -------------------- 2 files changed, 5 insertions(+), 93 deletions(-) delete mode 100644 .github/workflows/continuous-integration.yml 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 From afe0b8838b768f428478740ff4433a614d85faeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Fri, 14 Jun 2024 10:19:14 +0200 Subject: [PATCH 18/35] fix: [ANDROAPP-6182] crash when rotating scheduling dialog (#3677) --- .../teidata/TEIDataFragment.kt | 24 +++++---- .../dialogs/scheduling/SchedulingDialog.kt | 51 ++++++++++++------- .../dialogs/scheduling/SchedulingDialogUi.kt | 6 +-- .../dialogs/scheduling/SchedulingModule.kt | 13 +++-- .../dialogs/scheduling/SchedulingViewModel.kt | 32 ++++++------ .../scheduling/SchedulingViewModelFactory.kt | 6 --- .../scheduling/SchedulingViewModelTest.kt | 7 --- 7 files changed, 73 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt index 102d1c164b..fe8ec48195 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.core.app.ActivityOptionsCompat import androidx.fragment.app.activityViewModels +import androidx.fragment.app.setFragmentResultListener import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.map import androidx.recyclerview.widget.DividerItemDecoration @@ -53,7 +54,9 @@ import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.Eve import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.EventCatComboOptionSelector import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.ui.mapper.TEIEventCardMapper import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog +import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog.Companion.PROGRAM_STAGE_UID import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog.Companion.SCHEDULING_DIALOG +import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog.Companion.SCHEDULING_DIALOG_RESULT import org.dhis2.usescases.teiDashboard.ui.TeiDetailDashboard import org.dhis2.usescases.teiDashboard.ui.mapper.InfoBarMapper import org.dhis2.usescases.teiDashboard.ui.mapper.TeiDashboardCardMapper @@ -159,6 +162,16 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { setEvents(it) showLoadingProgress(false) } + + setFragmentResultListener(SCHEDULING_DIALOG_RESULT) { _, bundle -> + showToast( + resourceManager.formatWithEventLabel( + R.string.event_label_created, + bundle.getString(PROGRAM_STAGE_UID), + ), + ) + presenter.fetchEvents() + } }.root } @@ -374,18 +387,9 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { override fun displayScheduleEvent() { val model = dashboardViewModel.dashboardModel.value if (model is DashboardEnrollmentModel) { - SchedulingDialog( + SchedulingDialog.newInstance( enrollment = model.currentEnrollment, programStages = presenter.filterAvailableStages(model.programStages), - onScheduled = { programStageUid -> - showToast( - resourceManager.formatWithEventLabel( - R.string.event_label_created, - programStageUid, - ), - ) - presenter.fetchEvents() - }, ).show(parentFragmentManager, SCHEDULING_DIALOG) } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt index dff422ad1a..987a86729d 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt @@ -8,6 +8,8 @@ import android.view.ViewGroup import android.widget.DatePicker import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.core.os.bundleOf +import androidx.fragment.app.setFragmentResult import androidx.fragment.app.viewModels import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.dhis2.bindings.app @@ -20,15 +22,26 @@ import org.hisp.dhis.android.core.program.ProgramStage import java.util.Date import javax.inject.Inject -class SchedulingDialog( - val enrollment: Enrollment, - val programStages: List, - val onScheduled: (String) -> Unit, -) : BottomSheetDialogFragment() { +class SchedulingDialog : BottomSheetDialogFragment() { companion object { const val SCHEDULING_DIALOG = "SCHEDULING_DIALOG" + const val SCHEDULING_DIALOG_RESULT = "SCHEDULING_DIALOG_RESULT" + const val PROGRAM_STAGE_UID = "PROGRAM_STAGE_UID" + + fun newInstance( + enrollment: Enrollment, + programStages: List, + ): SchedulingDialog { + return SchedulingDialog().apply { + this.enrollment = enrollment + this.programStages = programStages + } + } } + var enrollment: Enrollment? = null + var programStages: List? = null + @Inject lateinit var factory: SchedulingViewModelFactory val viewModel: SchedulingViewModel by viewModels { factory } @@ -40,12 +53,7 @@ class SchedulingDialog( override fun onAttach(context: Context) { super.onAttach(context) - app().userComponent()?.plus( - SchedulingModule( - enrollment, - programStages, - ), - )?.inject(this) + app().userComponent()?.plus(SchedulingModule())?.inject(this) } override fun onCreateView( @@ -53,6 +61,18 @@ class SchedulingDialog( container: ViewGroup?, savedInstanceState: Bundle?, ): View { + enrollment?.let { + viewModel.enrollment = it + } + programStages?.let { + viewModel.programStages = it + viewModel.setInitialProgramStage(it.first()) + } + viewModel.onEventScheduled = { + setFragmentResult(SCHEDULING_DIALOG_RESULT, bundleOf(PROGRAM_STAGE_UID to it)) + dismiss() + } + viewModel.showCalendar = { showCalendarDialog() } @@ -61,11 +81,6 @@ class SchedulingDialog( showPeriodDialog() } - viewModel.onEventScheduled = { - dismiss() - onScheduled(viewModel.programStage.value.uid()) - } - return ComposeView(requireContext()).apply { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, @@ -73,8 +88,8 @@ class SchedulingDialog( setContent { SchedulingDialogUi( viewModel = viewModel, - programStages = programStages, - orgUnitUid = enrollment.organisationUnit(), + programStages = viewModel.programStages, + orgUnitUid = viewModel.enrollment.organisationUnit(), onDismiss = { dismiss() }, ) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialogUi.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialogUi.kt index 2b22cb7d37..4827df76ee 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialogUi.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialogUi.kt @@ -126,7 +126,7 @@ fun buttonTitle(scheduleNew: Boolean): String = when (scheduleNew) { fun ProvideScheduleNewEventForm( programStages: List, viewModel: SchedulingViewModel, - selectedProgramStage: ProgramStage, + selectedProgramStage: ProgramStage?, date: EventDate, catCombo: EventCatCombo, orgUnitUid: String?, @@ -136,7 +136,7 @@ fun ProvideScheduleNewEventForm( title = stringResource(id = R.string.program_stage), state = InputShellState.UNFOCUSED, dropdownItems = programStages.map { DropdownItem(it.displayName().orEmpty()) }, - selectedItem = DropdownItem(selectedProgramStage.displayName().orEmpty()), + selectedItem = DropdownItem(selectedProgramStage?.displayName().orEmpty()), onResetButtonClicked = {}, onItemSelected = { item -> programStages.find { it.displayName() == item.label } @@ -145,7 +145,7 @@ fun ProvideScheduleNewEventForm( ) } - if (willShowCalendar(selectedProgramStage.periodType())) { + if (willShowCalendar(selectedProgramStage?.periodType())) { ProvideInputDate( EventInputDateUiModel( eventDate = date, diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingModule.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingModule.kt index 80688227a8..3d5beb862b 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingModule.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingModule.kt @@ -6,14 +6,9 @@ import org.dhis2.commons.di.dagger.PerFragment import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.ResourceManager import org.hisp.dhis.android.core.D2 -import org.hisp.dhis.android.core.enrollment.Enrollment -import org.hisp.dhis.android.core.program.ProgramStage @Module -class SchedulingModule( - val enrollment: Enrollment, - val programStages: List, -) { +class SchedulingModule { @Provides @PerFragment fun provideSchedulingViewModelFactory( @@ -21,5 +16,9 @@ class SchedulingModule( resourceManager: ResourceManager, periodUtils: DhisPeriodUtils, ): SchedulingViewModelFactory = - SchedulingViewModelFactory(enrollment, programStages, d2, resourceManager, periodUtils) + SchedulingViewModelFactory( + d2, + resourceManager, + periodUtils, + ) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt index e261d52861..33f095c82d 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt @@ -26,8 +26,6 @@ import java.util.Date import java.util.Locale class SchedulingViewModel( - val enrollment: Enrollment, - val programStages: List, val d2: D2, val resourceManager: ResourceManager, val periodUtils: DhisPeriodUtils, @@ -37,12 +35,15 @@ class SchedulingViewModel( lateinit var configureEventReportDate: ConfigureEventReportDate lateinit var configureEventCatCombo: ConfigureEventCatCombo - private val _programStage: MutableStateFlow = MutableStateFlow(programStages.first()) - val programStage: StateFlow get() = _programStage + lateinit var enrollment: Enrollment + lateinit var programStages: List + + private val _programStage: MutableStateFlow = MutableStateFlow(null) + val programStage: StateFlow get() = _programStage var showCalendar: (() -> Unit)? = null var showPeriods: (() -> Unit)? = null - var onEventScheduled: (() -> Unit)? = null + var onEventScheduled: ((String) -> Unit)? = null private val _eventDate: MutableStateFlow = MutableStateFlow(EventDate()) val eventDate: StateFlow get() = _eventDate @@ -50,16 +51,12 @@ class SchedulingViewModel( private val _eventCatCombo: MutableStateFlow = MutableStateFlow(EventCatCombo()) val eventCatCombo: StateFlow get() = _eventCatCombo - init { - loadConfiguration() - } - private fun loadConfiguration() { repository = EventDetailsRepository( d2 = d2, programUid = enrollment.program().orEmpty(), eventUid = null, - programStageUid = programStage.value.uid(), + programStageUid = programStage.value?.uid(), fieldFactory = null, eventCreationType = EventCreationType.SCHEDULE, onError = resourceManager::parseD2Error, @@ -68,14 +65,14 @@ class SchedulingViewModel( creationType = EventCreationType.SCHEDULE, resourceProvider = EventDetailResourcesProvider( enrollment.program().orEmpty(), - programStage.value.uid(), + programStage.value?.uid(), resourceManager, ), repository = repository, - periodType = programStage.value.periodType(), + periodType = programStage.value?.periodType(), periodUtils = periodUtils, enrollmentId = enrollment.uid(), - scheduleInterval = programStage.value.standardInterval() ?: 0, + scheduleInterval = programStage.value?.standardInterval() ?: 0, ) configureEventCatCombo = ConfigureEventCatCombo( repository = repository, @@ -142,7 +139,7 @@ class SchedulingViewModel( } fun showPeriodDialog() { - programStage.value.periodType()?.let { + programStage.value?.periodType()?.let { showPeriods?.invoke() } } @@ -171,10 +168,15 @@ class SchedulingViewModel( ).flowOn(Dispatchers.IO) .collect { if (it != null) { - onEventScheduled?.invoke() + onEventScheduled?.invoke(programStage.value?.uid() ?: "") } } } } } + + fun setInitialProgramStage(programStage: ProgramStage) { + _programStage.value = programStage + loadConfiguration() + } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelFactory.kt index 5fc494d23a..47654437e8 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelFactory.kt @@ -5,13 +5,9 @@ import androidx.lifecycle.ViewModelProvider import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.ResourceManager import org.hisp.dhis.android.core.D2 -import org.hisp.dhis.android.core.enrollment.Enrollment -import org.hisp.dhis.android.core.program.ProgramStage @Suppress("UNCHECKED_CAST") class SchedulingViewModelFactory( - private val enrollment: Enrollment, - private val programStages: List, private val d2: D2, private val resourceManager: ResourceManager, private val periodUtils: DhisPeriodUtils, @@ -19,8 +15,6 @@ class SchedulingViewModelFactory( override fun create(modelClass: Class): T { return SchedulingViewModel( - enrollment, - programStages, d2, resourceManager, periodUtils, diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelTest.kt index 2eacb1f7e2..cf52fec2ad 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelTest.kt @@ -5,8 +5,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.setMain import org.hisp.dhis.android.core.arch.helpers.DateUtils -import org.hisp.dhis.android.core.enrollment.Enrollment -import org.hisp.dhis.android.core.program.ProgramStage import org.junit.Before import org.junit.Test import org.mockito.Mockito.spy @@ -15,9 +13,6 @@ import org.mockito.kotlin.verify class SchedulingViewModelTest { - private val enrollment = Enrollment.builder().uid("enrollmentUid").build() - private val programStage = ProgramStage.builder().uid("programStage").build() - private lateinit var schedulingViewModel: SchedulingViewModel private val testingDispatcher = StandardTestDispatcher() @@ -27,8 +22,6 @@ class SchedulingViewModelTest { fun setUp() { Dispatchers.setMain(testingDispatcher) schedulingViewModel = SchedulingViewModel( - enrollment = enrollment, - programStages = listOf(programStage), d2 = mock(), resourceManager = mock(), periodUtils = mock(), From 36c2ad53e58b5ef7cc73a1f833c499879b75c83a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Fri, 14 Jun 2024 12:54:56 +0200 Subject: [PATCH 19/35] fix: [ANDROAPP-6116] respect relationship constraints (#3680) --- .../relationships/RelationshipRepositoryImpl.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipRepositoryImpl.kt index 4723115dbb..05dfccae53 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipRepositoryImpl.kt @@ -17,7 +17,6 @@ import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.program.ProgramType -import org.hisp.dhis.android.core.relationship.RelationshipEntityType import org.hisp.dhis.android.core.relationship.RelationshipItem import org.hisp.dhis.android.core.relationship.RelationshipItemEvent import org.hisp.dhis.android.core.relationship.RelationshipItemTrackedEntityInstance @@ -55,7 +54,7 @@ class RelationshipRepositoryImpl( return d2.relationshipModule().relationshipTypes() .withConstraints() - .byConstraint(RelationshipEntityType.TRACKED_ENTITY_INSTANCE, teTypeUid) + .byAvailableForTrackedEntityInstance(config.teiUid) .get().map { relationshipTypes -> relationshipTypes.mapNotNull { relationshipType -> val secondaryTeTypeUid = when { From 44500f1adf1978e941dbef70f800f8dc9d6e2796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Miguel=20Rubio?= Date: Mon, 17 Jun 2024 16:37:54 +0200 Subject: [PATCH 20/35] build: update design system (#3690) * Update design system Signed-off-by: andresmr * SchedulingDialogUiTest wait for idle Signed-off-by: andresmr * SchedulingDialogUiTest wait for idle Signed-off-by: andresmr --------- Signed-off-by: andresmr --- .../dialogs/scheduling/SchedulingDialogUiTest.kt | 14 +++++++++++--- gradle/libs.versions.toml | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/dialogs/scheduling/SchedulingDialogUiTest.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/dialogs/scheduling/SchedulingDialogUiTest.kt index 4aa70eb56a..877eed683f 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/dialogs/scheduling/SchedulingDialogUiTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/dialogs/scheduling/SchedulingDialogUiTest.kt @@ -14,6 +14,7 @@ import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingViewModel import org.hisp.dhis.android.core.category.CategoryOption import org.hisp.dhis.android.core.program.ProgramStage import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.mockito.Mockito.mock @@ -51,7 +52,8 @@ class SchedulingDialogUiTest { @Test fun programStageInputNotDisplayedForOneStage() { - val programStages = listOf(ProgramStage.builder().uid("stageUid").displayName("PS A").build()) + val programStages = + listOf(ProgramStage.builder().uid("stageUid").displayName("PS A").build()) whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first())) composeTestRule.setContent { SchedulingDialogUi( @@ -61,7 +63,8 @@ class SchedulingDialogUiTest { ) { } } - composeTestRule.onNodeWithText("Schedule next " + programStages.first().displayName() + "?").assertExists() + composeTestRule.onNodeWithText("Schedule next " + programStages.first().displayName() + "?") + .assertExists() composeTestRule.onNodeWithText("Program stage").assertDoesNotExist() composeTestRule.onNodeWithText("Date").assertExists() composeTestRule.onNodeWithText("CatCombo *").assertExists() @@ -110,6 +113,7 @@ class SchedulingDialogUiTest { composeTestRule.onNodeWithText("Done").assertExists() } + @Ignore("Not working") @Test fun selectProgramStage() { val programStages = listOf( @@ -127,7 +131,11 @@ class SchedulingDialogUiTest { } composeTestRule.onNodeWithText("Program stage").performClick() - composeTestRule.onNodeWithTag("INPUT_DROPDOWN_MENU_ITEM_1").performClick() + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag( + testTag = "INPUT_DROPDOWN_MENU_ITEM_1", + useUnmergedTree = true + ).performClick() verify(viewModel).updateStage(programStages[1]) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f07b529e1..0e0aef2426 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ kotlin = '1.9.21' hilt = '2.47' hiltCompiler = '1.0.0' jacoco = '0.8.10' -designSystem = "0.2" +designSystem = "0.3.0-SNAPSHOT" dhis2sdk = "1.10.0" ruleEngine = "3.0.0" expressionParser = "1.1.0" From 41505347e0f985410dbcd966ca95bf52ebc35263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Tue, 18 Jun 2024 09:44:54 +0200 Subject: [PATCH 21/35] fix: [ANDROAPP-6184] crash on delete data dialog callback (#3687) --- .../org/dhis2/usescases/settings/SyncManagerFragment.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 01123525d9..d08a0b7c1e 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java +++ b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java @@ -292,8 +292,10 @@ public void deleteLocalData() { getString(R.string.action_accept), true, () -> { - analyticsHelper().setEvent(CONFIRM_DELETE_LOCAL_DATA, CLICK, CONFIRM_DELETE_LOCAL_DATA); - presenter.deleteLocalData(); + if (deleteLocalDataDialog.isAdded()) { + analyticsHelper().setEvent(CONFIRM_DELETE_LOCAL_DATA, CLICK, CONFIRM_DELETE_LOCAL_DATA); + presenter.deleteLocalData(); + } return null; }) ); From a2f02866c6e3df5691e263fd41f942e999ccfd77 Mon Sep 17 00:00:00 2001 From: andresmr Date: Tue, 18 Jun 2024 13:28:46 +0200 Subject: [PATCH 22/35] Add Mock response on old event when filtering by overdue Signed-off-by: andresmr --- .../dhis2/common/mockwebserver/MockWebServerRobot.kt | 2 ++ .../org/dhis2/usescases/searchte/SearchTETest.kt | 12 ++++++++++++ .../mocks/teilist/old_events_empty_response.json | 9 +++++++++ 3 files changed, 23 insertions(+) create mode 100644 app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json diff --git a/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt b/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt index eb2c53c7af..cd07f23e3d 100644 --- a/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt +++ b/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt @@ -20,6 +20,8 @@ class MockWebServerRobot(private val dhis2MockServer: Dhis2MockServer) { const val API_OLD_TRACKED_ENTITY_PATH = "/api/trackedEntityInstances/query?.*" const val API_OLD_TRACKED_ENTITY_RESPONSE = "mocks/teilist/old_tracked_entity_empty_response.json" + const val API_OLD_EVENTS_PATH = "/api/events?.*" + const val API_OLD_EVENTS_RESPONSE = "mocks/teilist/old_events_empty_response.json" } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt index a0c055353b..99aebeb245 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt @@ -15,6 +15,8 @@ import dispatch.android.espresso.IdlingDispatcherProviderRule import org.dhis2.R import org.dhis2.bindings.app import org.dhis2.common.idlingresources.MapIdlingResource +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_EVENTS_PATH +import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_EVENTS_RESPONSE import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_PATH import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_RESPONSE import org.dhis2.commons.date.DateUtils.SIMPLE_DATE_FORMAT @@ -202,6 +204,16 @@ class SearchTETest : BaseTest() { @Test fun shouldSuccessfullyFilterByEventStatusOverdue() { + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_TRACKED_ENTITY_PATH, + API_OLD_TRACKED_ENTITY_RESPONSE, + ) + mockWebServerRobot.addResponse( + ResponseController.GET, + API_OLD_EVENTS_PATH, + API_OLD_EVENTS_RESPONSE, + ) enableComposeForms() val eventStatusFilter = context.getString(R.string.filters_title_event_status) val totalCount = "1" diff --git a/app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json b/app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json new file mode 100644 index 0000000000..c03b0880c9 --- /dev/null +++ b/app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json @@ -0,0 +1,9 @@ +{ + "pager": { + "page": 1, + "pageCount": 1, + "total": 2, + "pageSize": 50 + }, + "events": [] +} \ No newline at end of file From 08174328b8c15fb175c17445a7f6be22b1b05772 Mon Sep 17 00:00:00 2001 From: andresmr Date: Tue, 18 Jun 2024 13:34:23 +0200 Subject: [PATCH 23/35] Update version name to 3.0.1 Signed-off-by: andresmr --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 323d723e57..5cc97ce439 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ ndk = "21.4.7075529" sdk = "34" minSdk = "21" vCode = "134" -vName = "3.0.0.1" +vName = "3.0.1" kotlinCompilerExtensionVersion = "1.5.6" gradle = "8.2.2" kotlin = '1.9.21' From 0cc90ce8be759a79477d0b1643d6ac3d1689808d Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Tue, 25 Jun 2024 15:39:03 +0200 Subject: [PATCH 24/35] ci: [CI] Ignore Compose table flaky tests (#3698) --- .../androidTest/java/org/dhis2/composetable/CellTableTest.kt | 2 ++ .../java/org/dhis2/composetable/ui/TextInputUiTest.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/compose-table/src/androidTest/java/org/dhis2/composetable/CellTableTest.kt b/compose-table/src/androidTest/java/org/dhis2/composetable/CellTableTest.kt index ecd24f51e2..7741a65550 100644 --- a/compose-table/src/androidTest/java/org/dhis2/composetable/CellTableTest.kt +++ b/compose-table/src/androidTest/java/org/dhis2/composetable/CellTableTest.kt @@ -53,6 +53,7 @@ class CellTableTest { } } + @Ignore("Flaky test, to be resolved in a separate ticket") @Test fun shouldSaveValue() { var savedValue: TableCell? = null @@ -92,6 +93,7 @@ class CellTableTest { } } + @Ignore("Flaky test, to be resolved in a separate ticket") @Test fun shouldMoveToNextRowWhenClickingNext() { tableRobot(composeTestRule) { diff --git a/compose-table/src/androidTest/java/org/dhis2/composetable/ui/TextInputUiTest.kt b/compose-table/src/androidTest/java/org/dhis2/composetable/ui/TextInputUiTest.kt index 5cb163c10b..7f35f04208 100644 --- a/compose-table/src/androidTest/java/org/dhis2/composetable/ui/TextInputUiTest.kt +++ b/compose-table/src/androidTest/java/org/dhis2/composetable/ui/TextInputUiTest.kt @@ -27,6 +27,7 @@ import org.dhis2.composetable.model.TableCell import org.dhis2.composetable.model.TextInputModel import org.dhis2.composetable.tableRobot import org.dhis2.composetable.ui.compositions.LocalInteraction +import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -65,6 +66,7 @@ class TextInputUiTest { } } + @Ignore("Flaky test, to be resolved in a separate ticket") @Test fun shouldClearFocusWhenKeyboardIsHidden() { tableRobot(composeTestRule) { From e6d031d90bffb44786ae3440788587bf47244cd9 Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Tue, 25 Jun 2024 16:21:58 +0200 Subject: [PATCH 25/35] fix: [ANDROAPP-6179] Do not save last focused item if value has not changed On focused event (#3695) --- .../java/org/dhis2/form/ui/FormViewModel.kt | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) 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 e3fd22ad59..f270ad7540 100644 --- a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt +++ b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt @@ -65,6 +65,8 @@ class FormViewModel( private val _items = MutableLiveData>() val items: LiveData> = _items + var previousActionItem: RowAction? = null + private val _savedValue = MutableLiveData() val savedValue: LiveData = _savedValue @@ -222,6 +224,7 @@ class FormViewModel( ActionType.ON_FOCUS, ActionType.ON_NEXT -> { val storeResult = saveLastFocusedItem(action) repository.setFocusedItem(action) + previousActionItem = action storeResult } @@ -300,24 +303,32 @@ class FormViewModel( } private fun saveLastFocusedItem(rowAction: RowAction) = getLastFocusedTextItem()?.let { - val error = checkFieldError(it.valueType, it.value, it.fieldMask) - if (error != null) { - val action = rowActionFromIntent( - FormIntent.OnSave(it.uid, it.value, it.valueType, it.fieldMask), - ) - repository.updateErrorList(action) + if (previousActionItem == null) previousActionItem = rowAction + if (previousActionItem?.value != it.value && previousActionItem?.id == rowAction.id) { + val error = checkFieldError(it.valueType, it.value, it.fieldMask) + if (error != null) { + val action = rowActionFromIntent( + FormIntent.OnSave(it.uid, it.value, it.valueType, it.fieldMask), + ) + repository.updateErrorList(action) + StoreResult( + rowAction.id, + ValueStoreResult.VALUE_HAS_NOT_CHANGED, + ) + } else { + checkAutoCompleteForLastFocusedItem(it) + val intent = getSaveIntent(it) + val action = rowActionFromIntent(intent) + val result = repository.save(it.uid, it.value, action.extraData) + repository.updateValueOnList(it.uid, it.value, it.valueType) + repository.updateErrorList(action) + result + } + } else { StoreResult( rowAction.id, ValueStoreResult.VALUE_HAS_NOT_CHANGED, ) - } else { - checkAutoCompleteForLastFocusedItem(it) - val intent = getSaveIntent(it) - val action = rowActionFromIntent(intent) - val result = repository.save(it.uid, it.value, action.extraData) - repository.updateValueOnList(it.uid, it.value, it.valueType) - repository.updateErrorList(action) - result } } ?: StoreResult( rowAction.id, From afda088f3b283f7aebe4101c0d0d2a9b4a947f75 Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Wed, 26 Jun 2024 09:37:05 +0200 Subject: [PATCH 26/35] fix: [ANDROAPP-6101] Do not allow to save form with errors in completed events (#3696) * fix: [ANDROAPP-6101] Do not show save anyway button if there are errors, only with warnings * ci: [ANDROAPP-6101] Ignore Compose table flaky --- .../eventCapture/domain/ConfigureEventCompletionDialog.kt | 4 ++-- .../androidTest/java/org/dhis2/composetable/CellTableTest.kt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) 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 7e6a53f935..a4b1869d06 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 @@ -37,9 +37,9 @@ class ConfigureEventCompletionDialog( !canComplete && onCompleteMessage != null, ) val mainButton = getMainButton(dialogType, eventState) - val secondaryButton = if (canSkipErrorFix || eventState == EventStatus.COMPLETED) { + val secondaryButton = if (canSkipErrorFix || dialogType == WARNING) { EventCompletionButtons( - SecondaryButton(if (eventState == EventStatus.COMPLETED) provider.provideSaveAnyway() else provider.provideNotNow()), + SecondaryButton(provider.provideNotNow()), FormBottomDialog.ActionType.FINISH, ) } else { diff --git a/compose-table/src/androidTest/java/org/dhis2/composetable/CellTableTest.kt b/compose-table/src/androidTest/java/org/dhis2/composetable/CellTableTest.kt index 7741a65550..1df4d003da 100644 --- a/compose-table/src/androidTest/java/org/dhis2/composetable/CellTableTest.kt +++ b/compose-table/src/androidTest/java/org/dhis2/composetable/CellTableTest.kt @@ -40,6 +40,7 @@ class CellTableTest { } } + @Ignore("Flaky test, to be resolved in a separate ticket") @Test fun shouldUpdateValueWhenTypingInComponent() { tableRobot(composeTestRule) { From 0e5b6c439224c3101161782d1628eb48ebecae9f Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Wed, 26 Jun 2024 11:03:13 +0200 Subject: [PATCH 27/35] fix: [ANDROAPP-6132] double tap on event/enrollment creation could generate duplicates (#3688) * test signed commit * test signed commit 5 * test signed commit 6 * test signed commit 7 * test final commit * Rename .java to .kt * fix: [ANDROAPP-6132] Create single event enforcer, implement it throughout all new enrollment/event buttons and orgUnitDialog, migrate EventInitialActivity to kt, review sonarLint issues * fix: [ANDROAPP-6132] remove debug comments * fix: [ANDROAPP-6132] wrong require condition * fix: [ANDROAPP-6132] remove deprecated date utils calls, refactor SingleEventManager to call from presenters when possible * fix: [ANDROAPP-6132] refactor OUTreeFragment and viewmodel selected Org units check * fix: [ANDROAPP-6132] ktlint * fix: [ANDROAPP-6132] fix unit test * fix: [ANDROAPP-6132] remove unused import --- .../eventInitial/EventInitialActivity.java | 403 ---------------- .../eventInitial/EventInitialActivity.kt | 434 ++++++++++++++++++ .../ProgramEventDetailActivity.kt | 33 +- .../ProgramEventDetailPresenter.kt | 5 +- .../searchTrackEntity/SearchTEActivity.java | 38 +- .../searchTrackEntity/SearchTEPresenter.java | 18 +- .../teiDashboard/DashboardProgramModel.java | 3 - .../teidata/TEIDataPresenter.kt | 10 + .../teidata/teievents/StageViewHolder.kt | 2 +- .../orgunitselector/OUTreeViewModelTest.kt | 3 +- .../org/dhis2/commons/date/DateUtils.java | 111 +++++ .../commons/orgunitselector/OUTreeFragment.kt | 30 +- .../orgunitselector/OUTreeViewModel.kt | 15 +- .../commons/schedulers/SingleEventEnforcer.kt | 24 + 14 files changed, 687 insertions(+), 442 deletions(-) delete mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.java create mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.kt create mode 100644 commons/src/main/java/org/dhis2/commons/schedulers/SingleEventEnforcer.kt diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.java deleted file mode 100644 index 2d77cda992..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.java +++ /dev/null @@ -1,403 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventInitial; - -import static org.dhis2.commons.Constants.ENROLLMENT_UID; -import static org.dhis2.commons.Constants.EVENT_CREATION_TYPE; -import static org.dhis2.commons.Constants.EVENT_PERIOD_TYPE; -import static org.dhis2.commons.Constants.ORG_UNIT; -import static org.dhis2.commons.Constants.PERMANENT; -import static org.dhis2.commons.Constants.PROGRAM_UID; -import static org.dhis2.commons.Constants.TRACKED_ENTITY_INSTANCE; -import static org.dhis2.utils.analytics.AnalyticsConstants.CLICK; -import static org.dhis2.utils.analytics.AnalyticsConstants.CREATE_EVENT; -import static org.dhis2.utils.analytics.AnalyticsConstants.DELETE_EVENT; -import static org.dhis2.utils.analytics.AnalyticsConstants.SHOW_HELP; - -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.util.SparseBooleanArray; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.databinding.DataBindingUtil; -import androidx.fragment.app.FragmentTransaction; - -import org.dhis2.App; -import org.dhis2.R; -import org.dhis2.commons.Constants; -import org.dhis2.commons.data.EventCreationType; -import org.dhis2.commons.dialogs.CustomDialog; -import org.dhis2.commons.dialogs.DialogClickListener; -import org.dhis2.commons.popupmenu.AppMenuHelper; -import org.dhis2.commons.resources.ResourceManager; -import org.dhis2.databinding.ActivityEventInitialBinding; -import org.dhis2.form.model.EventMode; -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity; -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsComponent; -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsComponentProvider; -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsModule; -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDetails; -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui.EventDetailsFragment; -import org.dhis2.usescases.general.ActivityGlobalAbstract; -import org.dhis2.usescases.qrCodes.eventsworegistration.QrEventsWORegistrationActivity; -import org.dhis2.utils.HelpManager; -import org.dhis2.utils.analytics.AnalyticsConstants; -import org.hisp.dhis.android.core.common.Geometry; -import org.hisp.dhis.android.core.enrollment.EnrollmentStatus; -import org.hisp.dhis.android.core.period.PeriodType; -import org.hisp.dhis.android.core.program.Program; -import org.hisp.dhis.android.core.program.ProgramStage; - -import java.util.Objects; - -import javax.inject.Inject; - -import io.reactivex.disposables.CompositeDisposable; -import kotlin.Unit; - -public class EventInitialActivity extends ActivityGlobalAbstract implements EventInitialContract.View, EventDetailsComponentProvider { - - @Inject - EventInitialPresenter presenter; - - @Inject - ResourceManager resourceManager; - - private ActivityEventInitialBinding binding; - - //Bundle variables - private String programUid; - private String eventUid; - private EventCreationType eventCreationType; - private String getTrackedEntityInstance; - private String enrollmentUid; - private String selectedOrgUnit; - private PeriodType periodType; - private String programStageUid; - private EnrollmentStatus enrollmentStatus; - private int eventScheduleInterval; - - private ProgramStage programStage; - private Program program; - private Boolean accessData; - private EventDetails eventDetails = new EventDetails(); - - private final CompositeDisposable disposable = new CompositeDisposable(); - - public EventInitialComponent eventInitialComponent; - - public static Bundle getBundle(String programUid, String eventUid, String eventCreationType, - String teiUid, PeriodType eventPeriodType, String orgUnit, String stageUid, - String enrollmentUid, int eventScheduleInterval, EnrollmentStatus enrollmentStatus) { - Bundle bundle = new Bundle(); - bundle.putString(Constants.PROGRAM_UID, programUid); - bundle.putString(Constants.EVENT_UID, eventUid); - bundle.putString(Constants.EVENT_CREATION_TYPE, eventCreationType); - bundle.putString(Constants.TRACKED_ENTITY_INSTANCE, teiUid); - bundle.putString(Constants.ENROLLMENT_UID, enrollmentUid); - bundle.putString(Constants.ORG_UNIT, orgUnit); - bundle.putSerializable(Constants.EVENT_PERIOD_TYPE, eventPeriodType); - bundle.putString(Constants.PROGRAM_STAGE_UID, stageUid); - bundle.putInt(Constants.EVENT_SCHEDULE_INTERVAL, eventScheduleInterval); - bundle.putSerializable(Constants.ENROLLMENT_STATUS, enrollmentStatus); - return bundle; - } - - private void initVariables() { - programUid = getIntent().getStringExtra(PROGRAM_UID); - eventUid = getIntent().getStringExtra(Constants.EVENT_UID); - eventCreationType = getIntent().getStringExtra(EVENT_CREATION_TYPE) != null ? - EventCreationType.valueOf(getIntent().getStringExtra(EVENT_CREATION_TYPE)) : - EventCreationType.DEFAULT; - getTrackedEntityInstance = getIntent().getStringExtra(TRACKED_ENTITY_INSTANCE); - enrollmentUid = getIntent().getStringExtra(ENROLLMENT_UID); - selectedOrgUnit = getIntent().getStringExtra(ORG_UNIT); - periodType = (PeriodType) getIntent().getSerializableExtra(EVENT_PERIOD_TYPE); - programStageUid = getIntent().getStringExtra(Constants.PROGRAM_STAGE_UID); - enrollmentStatus = (EnrollmentStatus) getIntent().getSerializableExtra(Constants.ENROLLMENT_STATUS); - eventScheduleInterval = getIntent().getIntExtra(Constants.EVENT_SCHEDULE_INTERVAL, 0); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - initVariables(); - eventInitialComponent = Objects.requireNonNull(((App) getApplicationContext()).userComponent()) - .plus( - new EventInitialModule(this, - eventUid, - programStageUid, - getContext()) - ); - eventInitialComponent.inject(this); - super.onCreate(savedInstanceState); - - binding = DataBindingUtil.setContentView(this, R.layout.activity_event_initial); - binding.setPresenter(presenter); - - initProgressBar(); - - Bundle bundle = new Bundle(); - bundle.putString(Constants.EVENT_UID, eventUid); - bundle.putString(Constants.EVENT_CREATION_TYPE, getIntent().getStringExtra(EVENT_CREATION_TYPE)); - bundle.putString(Constants.PROGRAM_STAGE_UID, programStageUid); - bundle.putString(PROGRAM_UID, programUid); - bundle.putSerializable(Constants.EVENT_PERIOD_TYPE, periodType); - bundle.putString(Constants.ENROLLMENT_UID, enrollmentUid); - bundle.putInt(Constants.EVENT_SCHEDULE_INTERVAL, eventScheduleInterval); - bundle.putString(Constants.ORG_UNIT, selectedOrgUnit); - bundle.putSerializable(Constants.ENROLLMENT_STATUS, enrollmentStatus); - - EventDetailsFragment eventDetailsFragment = new EventDetailsFragment(); - eventDetailsFragment.setArguments(bundle); - - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace(R.id.fragmentDetailsContainer, eventDetailsFragment).commit(); - - eventDetailsFragment.setOnEventDetailsChange(eventDetails -> { - this.eventDetails = eventDetails; - return Unit.INSTANCE; - }); - eventDetailsFragment.setOnButtonCallback(() -> { - onActionButtonClick(); - return Unit.INSTANCE; - }); - presenter.init(programUid, eventUid, selectedOrgUnit, programStageUid); - } - - private void onActionButtonClick() { - String programStageModelUid = programStage == null ? "" : programStage.uid(); - Geometry geometry = null; - if (eventDetails.getCoordinates() != null) { - geometry = Geometry.builder() - .coordinates(eventDetails.getCoordinates()) - .type(programStage.featureType()) - .build(); - } - - if (eventUid == null) { // This is a new Event - presenter.onEventCreated(); - analyticsHelper().setEvent(CREATE_EVENT, AnalyticsConstants.DATA_CREATION, CREATE_EVENT); - if (eventCreationType == EventCreationType.REFERAL && eventDetails.getTemCreate() != null && eventDetails.getTemCreate().equals(PERMANENT)) { - presenter.scheduleEventPermanent( - enrollmentUid, - getTrackedEntityInstance, - programStageModelUid, - eventDetails.getSelectedDate(), - eventDetails.getSelectedOrgUnit(), - null, - eventDetails.getCatOptionComboUid(), - geometry - ); - } else if (eventCreationType == EventCreationType.SCHEDULE || eventCreationType == EventCreationType.REFERAL) { - presenter.scheduleEvent( - enrollmentUid, - programStageModelUid, - eventDetails.getSelectedDate(), - eventDetails.getSelectedOrgUnit(), - null, - eventDetails.getCatOptionComboUid(), - geometry - ); - } else { - presenter.createEvent( - enrollmentUid, - programStageModelUid, - eventDetails.getSelectedDate(), - eventDetails.getSelectedOrgUnit(), - null, - eventDetails.getCatOptionComboUid(), - geometry, - getTrackedEntityInstance); - } - }else{ - startFormActivity(eventUid,false); - } - } - - @Override - protected void onDestroy() { - presenter.onDettach(); - disposable.dispose(); - super.onDestroy(); - } - - private void initProgressBar() { - if (eventUid != null && presenter.getCompletionPercentageVisibility()) { - binding.completion.setVisibility(View.VISIBLE); - } else { - binding.completion.setVisibility(View.GONE); - } - } - - @Override - public void setProgram(@NonNull Program program) { - this.program = program; - - setUpActivityTitle(); - } - - private void setUpActivityTitle() { - String activityTitle; - if (eventCreationType == EventCreationType.REFERAL) { - activityTitle = getString(R.string.referral); - } else { - activityTitle = eventUid == null ? - resourceManager.formatWithEventLabel(R.string.new_event_label, programStageUid, 1, false) - : program.displayName(); - } - binding.setName(activityTitle); - } - - @Override - public void onEventCreated(String eventUid) { - showToast( - resourceManager.formatWithEventLabel( - R.string.event_label_created, - programStageUid, - 1, false - )); - if (eventCreationType != EventCreationType.SCHEDULE && eventCreationType != EventCreationType.REFERAL) { - startFormActivity(eventUid, true); - } else { - finish(); - } - } - - @Override - public void onEventUpdated(String eventUid) { - startFormActivity(eventUid, false); - } - - private void startFormActivity(String eventUid, boolean isNew) { - Intent intent = new Intent(this, EventCaptureActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); - intent.putExtras(EventCaptureActivity.getActivityBundle(eventUid, programUid, isNew ? EventMode.NEW : EventMode.CHECK)); - startActivity(intent); - finish(); - } - - @Override - public void setProgramStage(ProgramStage programStage) { - this.programStage = programStage; - binding.setProgramStage(programStage); - - if (periodType == null) - periodType = programStage.periodType(); - } - - @Override - public void updatePercentage(float primaryValue) { - binding.completion.setCompletionPercentage(primaryValue); - } - - @Override - public void showProgramStageSelection() { - presenter.getProgramStage(programStageUid); - } - - @Override - public void setAccessDataWrite(Boolean canWrite) { - this.accessData = canWrite; - } - - @Override - public void showQR() { - Intent intent = new Intent(EventInitialActivity.this, QrEventsWORegistrationActivity.class); - intent.putExtra(Constants.EVENT_UID, eventUid); - startActivity(intent); - } - - @Override - public void setTutorial() { - - new Handler().postDelayed(() -> { - SparseBooleanArray stepConditions = new SparseBooleanArray(); - stepConditions.put(0, eventUid == null); - HelpManager.getInstance().show(getActivity(), HelpManager.TutorialName.EVENT_INITIAL, stepConditions); - }, 500); - } - - @Override - public void showMoreOptions(View view) { - new AppMenuHelper.Builder().menu(this, R.menu.event_menu).anchor(view) - .onMenuInflated(popupMenu -> { - popupMenu.getMenu().findItem(R.id.menu_delete).setVisible(accessData && presenter.isEnrollmentOpen()); - popupMenu.getMenu().findItem(R.id.menu_share).setVisible(eventUid != null); - return Unit.INSTANCE; - }) - .onMenuItemClicked(itemId -> { - switch (itemId) { - case R.id.showHelp: - analyticsHelper().setEvent(SHOW_HELP, CLICK, SHOW_HELP); - setTutorial(); - break; - case R.id.menu_delete: - confirmDeleteEvent(); - break; - case R.id.menu_share: - presenter.onShareClick(); - break; - default: - break; - } - return false; - }) - .build() - .show(); - } - - public void confirmDeleteEvent() { - new CustomDialog( - this, - resourceManager.formatWithEventLabel( - R.string.delete_event_label, - programStageUid, - 1, false), - resourceManager.formatWithEventLabel( - R.string.confirm_delete_event_label, - programStageUid, - 1, false), - getString(R.string.delete), - getString(R.string.cancel), - 0, - new DialogClickListener() { - @Override - public void onPositive() { - analyticsHelper().setEvent(DELETE_EVENT, CLICK, DELETE_EVENT); - presenter.deleteEvent(getTrackedEntityInstance); - } - - @Override - public void onNegative() { - // dismiss - } - } - ).show(); - } - - @Override - public void showEventWasDeleted() { - showToast(resourceManager.formatWithEventLabel( - R.string.event_label_was_deleted, - programStageUid, - 1, false - )); - finish(); - } - - @Override - public void showDeleteEventError() { - showToast(resourceManager.formatWithEventLabel( - R.string.delete_event_label_error, - programStageUid, - 1, false - )); - } - - @Nullable - @Override - public EventDetailsComponent provideEventDetailsComponent(@Nullable EventDetailsModule module) { - return eventInitialComponent.plus(module); - } -} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.kt new file mode 100644 index 0000000000..8ff12343c6 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.kt @@ -0,0 +1,434 @@ +package org.dhis2.usescases.eventsWithoutRegistration.eventInitial + +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.SparseBooleanArray +import android.view.View +import android.widget.PopupMenu +import androidx.databinding.DataBindingUtil +import io.reactivex.disposables.CompositeDisposable +import org.dhis2.App +import org.dhis2.R +import org.dhis2.commons.Constants +import org.dhis2.commons.data.EventCreationType +import org.dhis2.commons.dialogs.CustomDialog +import org.dhis2.commons.dialogs.DialogClickListener +import org.dhis2.commons.popupmenu.AppMenuHelper +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.commons.schedulers.SingleEventEnforcer +import org.dhis2.commons.schedulers.SingleEventEnforcerImpl +import org.dhis2.databinding.ActivityEventInitialBinding +import org.dhis2.form.model.EventMode +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity.Companion.getActivityBundle +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsComponent +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsComponentProvider +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsModule +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDetails +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui.EventDetailsFragment +import org.dhis2.usescases.general.ActivityGlobalAbstract +import org.dhis2.usescases.qrCodes.eventsworegistration.QrEventsWORegistrationActivity +import org.dhis2.utils.HelpManager +import org.dhis2.utils.analytics.CLICK +import org.dhis2.utils.analytics.CREATE_EVENT +import org.dhis2.utils.analytics.DATA_CREATION +import org.dhis2.utils.analytics.DELETE_EVENT +import org.dhis2.utils.analytics.SHOW_HELP +import org.hisp.dhis.android.core.common.Geometry +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.hisp.dhis.android.core.period.PeriodType +import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.android.core.program.ProgramStage +import java.util.Objects +import javax.inject.Inject + +class EventInitialActivity : + ActivityGlobalAbstract(), + EventInitialContract.View, + EventDetailsComponentProvider { + + @Inject + lateinit var presenter: EventInitialPresenter + + @Inject + lateinit var resourceManager: ResourceManager + + private lateinit var binding: ActivityEventInitialBinding + + // Bundle variables + private var programUid: String? = null + private var eventUid: String? = null + private var eventCreationType: EventCreationType? = null + private var getTrackedEntityInstance: String? = null + private var enrollmentUid: String? = null + private var selectedOrgUnit: String? = null + private var periodType: PeriodType? = null + private var programStageUid: String? = null + private var enrollmentStatus: EnrollmentStatus? = null + private var eventScheduleInterval = 0 + + private var programStage: ProgramStage? = null + private var program: Program? = null + private var accessData: Boolean? = null + private var eventDetails = EventDetails() + + private var singleEventEnforcer: SingleEventEnforcer? = null + + private val disposable = CompositeDisposable() + + var eventInitialComponent: EventInitialComponent? = null + + private fun initVariables() { + programUid = intent.getStringExtra(Constants.PROGRAM_UID) + eventUid = intent.getStringExtra(Constants.EVENT_UID) + eventCreationType = + if (intent.getStringExtra(Constants.EVENT_CREATION_TYPE) != null) { + EventCreationType.valueOf( + intent.getStringExtra(Constants.EVENT_CREATION_TYPE)!!, + ) + } else { + EventCreationType.DEFAULT + } + getTrackedEntityInstance = intent.getStringExtra(Constants.TRACKED_ENTITY_INSTANCE) + enrollmentUid = intent.getStringExtra(Constants.ENROLLMENT_UID) + selectedOrgUnit = intent.getStringExtra(Constants.ORG_UNIT) + periodType = intent.getSerializableExtra(Constants.EVENT_PERIOD_TYPE) as PeriodType? + programStageUid = intent.getStringExtra(Constants.PROGRAM_STAGE_UID) + enrollmentStatus = + intent.getSerializableExtra(Constants.ENROLLMENT_STATUS) as EnrollmentStatus? + eventScheduleInterval = intent.getIntExtra(Constants.EVENT_SCHEDULE_INTERVAL, 0) + singleEventEnforcer = SingleEventEnforcerImpl() + } + + public override fun onCreate(savedInstanceState: Bundle?) { + initVariables() + eventInitialComponent = Objects.requireNonNull((applicationContext as App).userComponent()) + ?.plus( + EventInitialModule( + this, + eventUid, + programStageUid, + context, + ), + ) + eventInitialComponent!!.inject(this) + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.activity_event_initial) + binding.setPresenter(presenter) + + initProgressBar() + + val bundle = Bundle() + bundle.putString(Constants.EVENT_UID, eventUid) + bundle.putString( + Constants.EVENT_CREATION_TYPE, + intent.getStringExtra(Constants.EVENT_CREATION_TYPE), + ) + bundle.putString(Constants.PROGRAM_STAGE_UID, programStageUid) + bundle.putString(Constants.PROGRAM_UID, programUid) + bundle.putSerializable(Constants.EVENT_PERIOD_TYPE, periodType) + bundle.putString(Constants.ENROLLMENT_UID, enrollmentUid) + bundle.putInt(Constants.EVENT_SCHEDULE_INTERVAL, eventScheduleInterval) + bundle.putString(Constants.ORG_UNIT, selectedOrgUnit) + bundle.putSerializable(Constants.ENROLLMENT_STATUS, enrollmentStatus) + + val eventDetailsFragment = EventDetailsFragment() + eventDetailsFragment.arguments = bundle + + val transaction = supportFragmentManager.beginTransaction() + transaction.replace(R.id.fragmentDetailsContainer, eventDetailsFragment).commit() + + eventDetailsFragment.onEventDetailsChange = { eventDetails: EventDetails -> + this.eventDetails = eventDetails + Unit + } + eventDetailsFragment.onButtonCallback = { + singleEventEnforcer!!.processEvent { + onActionButtonClick() + null + } + Unit + } + presenter.init(programUid, eventUid, selectedOrgUnit, programStageUid) + } + + private fun onActionButtonClick() { + val programStageModelUid = if (programStage == null) "" else programStage!!.uid() + var geometry: Geometry? = null + if (eventDetails.coordinates != null) { + geometry = Geometry.builder() + .coordinates(eventDetails.coordinates) + .type(programStage!!.featureType()) + .build() + } + + if (eventUid == null) { // This is a new Event + presenter.onEventCreated() + analyticsHelper().setEvent(CREATE_EVENT, DATA_CREATION, CREATE_EVENT) + if (eventCreationType == EventCreationType.REFERAL && eventDetails.temCreate != null && eventDetails.temCreate == Constants.PERMANENT) { + presenter.scheduleEventPermanent( + enrollmentUid, + getTrackedEntityInstance, + programStageModelUid, + eventDetails.selectedDate, + eventDetails.selectedOrgUnit, + null, + eventDetails.catOptionComboUid, + geometry, + ) + } else if (eventCreationType == EventCreationType.SCHEDULE || eventCreationType == EventCreationType.REFERAL) { + presenter.scheduleEvent( + enrollmentUid, + programStageModelUid, + eventDetails.selectedDate, + eventDetails.selectedOrgUnit, + null, + eventDetails.catOptionComboUid, + geometry, + ) + } else { + presenter.createEvent( + enrollmentUid, + programStageModelUid, + eventDetails.selectedDate, + eventDetails.selectedOrgUnit, + null, + eventDetails.catOptionComboUid, + geometry, + getTrackedEntityInstance, + ) + } + } else { + startFormActivity(eventUid!!, false) + } + } + + override fun onDestroy() { + presenter.onDettach() + disposable.dispose() + super.onDestroy() + } + + private fun initProgressBar() { + if (eventUid != null && presenter.completionPercentageVisibility) { + binding.completion.visibility = View.VISIBLE + } else { + binding.completion.visibility = View.GONE + } + } + + override fun setProgram(program: Program) { + this.program = program + + setUpActivityTitle() + } + + private fun setUpActivityTitle() { + val activityTitle = if (eventCreationType == EventCreationType.REFERAL) { + getString(R.string.referral) + } else { + if (eventUid == null) { + resourceManager.formatWithEventLabel( + R.string.new_event_label, + programStageUid, + 1, + false, + ) + } else { + program!!.displayName()!! + } + } + binding.name = activityTitle + } + + override fun onEventCreated(eventUid: String) { + showToast( + resourceManager.formatWithEventLabel( + R.string.event_label_created, + programStageUid, + 1, + false, + ), + ) + if (eventCreationType != EventCreationType.SCHEDULE && eventCreationType != EventCreationType.REFERAL) { + startFormActivity(eventUid, true) + } else { + finish() + } + } + + override fun onEventUpdated(eventUid: String) { + startFormActivity(eventUid, false) + } + + private fun startFormActivity(eventUid: String, isNew: Boolean) { + val intent = Intent( + this, + EventCaptureActivity::class.java, + ) + intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) + intent.putExtras( + getActivityBundle( + eventUid, + programUid!!, + if (isNew) EventMode.NEW else EventMode.CHECK, + ), + ) + startActivity(intent) + finish() + } + + override fun setProgramStage(programStage: ProgramStage) { + this.programStage = programStage + binding.programStage = programStage + + if (periodType == null) periodType = programStage.periodType() + } + + override fun updatePercentage(primaryValue: Float) { + binding.completion.setCompletionPercentage(primaryValue) + } + + override fun showProgramStageSelection() { + presenter.getProgramStage(programStageUid) + } + + override fun setAccessDataWrite(canWrite: Boolean) { + this.accessData = canWrite + } + + override fun showQR() { + val intent = Intent( + this@EventInitialActivity, + QrEventsWORegistrationActivity::class.java, + ) + intent.putExtra(Constants.EVENT_UID, eventUid) + startActivity(intent) + } + + override fun setTutorial() { + Handler(Looper.getMainLooper()).postDelayed({ + val stepConditions = SparseBooleanArray() + stepConditions.put(0, eventUid == null) + HelpManager.getInstance() + .show(activity, HelpManager.TutorialName.EVENT_INITIAL, stepConditions) + }, 500) + } + + override fun showMoreOptions(view: View) { + AppMenuHelper.Builder().menu(this, R.menu.event_menu).anchor(view) + .onMenuInflated { popupMenu: PopupMenu -> + popupMenu.menu.findItem(R.id.menu_delete).setVisible( + accessData!! && presenter.isEnrollmentOpen, + ) + popupMenu.menu.findItem(R.id.menu_share).setVisible(eventUid != null) + Unit + } + .onMenuItemClicked { itemId: Int? -> + when (itemId) { + R.id.showHelp -> { + analyticsHelper().setEvent(SHOW_HELP, CLICK, SHOW_HELP) + setTutorial() + } + + R.id.menu_delete -> confirmDeleteEvent() + R.id.menu_share -> presenter.onShareClick() + else -> { + // do nothing + } + } + false + } + .build() + .show() + } + + fun confirmDeleteEvent() { + CustomDialog( + this, + resourceManager.formatWithEventLabel( + R.string.delete_event_label, + programStageUid, + 1, + false, + ), + resourceManager.formatWithEventLabel( + R.string.confirm_delete_event_label, + programStageUid, + 1, + false, + ), + getString(R.string.delete), + getString(R.string.cancel), + 0, + object : DialogClickListener { + override fun onPositive() { + analyticsHelper().setEvent(DELETE_EVENT, CLICK, DELETE_EVENT) + presenter.deleteEvent(getTrackedEntityInstance) + } + + override fun onNegative() { + // dismiss + } + }, + ).show() + } + + override fun showEventWasDeleted() { + showToast( + resourceManager.formatWithEventLabel( + R.string.event_label_was_deleted, + programStageUid, + 1, + false, + ), + ) + finish() + } + + override fun showDeleteEventError() { + showToast( + resourceManager.formatWithEventLabel( + R.string.delete_event_label_error, + programStageUid, + 1, + false, + ), + ) + } + + override fun provideEventDetailsComponent(module: EventDetailsModule?): EventDetailsComponent? { + return eventInitialComponent!!.plus(module) + } + + companion object { + fun getBundle( + programUid: String?, + eventUid: String?, + eventCreationType: String?, + teiUid: String?, + eventPeriodType: PeriodType?, + orgUnit: String?, + stageUid: String?, + enrollmentUid: String?, + eventScheduleInterval: Int, + enrollmentStatus: EnrollmentStatus?, + ): Bundle { + val bundle = Bundle() + bundle.putString(Constants.PROGRAM_UID, programUid) + bundle.putString(Constants.EVENT_UID, eventUid) + bundle.putString(Constants.EVENT_CREATION_TYPE, eventCreationType) + bundle.putString(Constants.TRACKED_ENTITY_INSTANCE, teiUid) + bundle.putString(Constants.ENROLLMENT_UID, enrollmentUid) + bundle.putString(Constants.ORG_UNIT, orgUnit) + bundle.putSerializable(Constants.EVENT_PERIOD_TYPE, eventPeriodType) + bundle.putString(Constants.PROGRAM_STAGE_UID, stageUid) + bundle.putInt(Constants.EVENT_SCHEDULE_INTERVAL, eventScheduleInterval) + bundle.putSerializable(Constants.ENROLLMENT_STATUS, enrollmentStatus) + return bundle + } + } +} diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt index fed6c83568..520d0fa031 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt @@ -22,6 +22,9 @@ import org.dhis2.bindings.app import org.dhis2.bindings.clipWithRoundedCorners import org.dhis2.bindings.dp import org.dhis2.commons.Constants +import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.date.DateUtils.OnFromToSelector +import org.dhis2.commons.date.Period import org.dhis2.commons.filters.FilterItem import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.filters.FilterManager.PeriodRequest @@ -40,15 +43,17 @@ import org.dhis2.usescases.general.ActivityGlobalAbstract import org.dhis2.usescases.programEventDetail.ProgramEventDetailViewModel.EventProgramScreen import org.dhis2.usescases.programEventDetail.eventList.EventListFragment import org.dhis2.usescases.programEventDetail.eventMap.EventMapFragment -import org.dhis2.utils.DateUtils import org.dhis2.utils.analytics.DATA_CREATION import org.dhis2.utils.category.CategoryDialog import org.dhis2.utils.category.CategoryDialog.Companion.TAG +import org.dhis2.utils.customviews.RxDateDialog import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator import org.dhis2.utils.granularsync.SyncStatusDialog import org.dhis2.utils.granularsync.shouldLaunchSyncDialog import org.hisp.dhis.android.core.period.DatePeriod import org.hisp.dhis.android.core.program.Program +import timber.log.Timber +import java.util.Date import javax.inject.Inject class ProgramEventDetailActivity : @@ -318,7 +323,6 @@ class ProgramEventDetailActivity : } override fun selectOrgUnitForNewEvent() { - enableAddEventButton(false) OUTreeFragment.Builder() .showAsDialog() .singleSelection() @@ -357,17 +361,32 @@ class ProgramEventDetailActivity : override fun showPeriodRequest(periodRequest: PeriodRequest) { if (periodRequest == PeriodRequest.FROM_TO) { - DateUtils.getInstance().fromCalendarSelector(this) { datePeriod: List? -> + DateUtils.getInstance().fromCalendarSelector(this.context) { datePeriod: List? -> FilterManager.getInstance().addPeriod(datePeriod) } } else { + val onFromToSelector = + OnFromToSelector { datePeriods -> FilterManager.getInstance().addPeriod(datePeriods) } + DateUtils.getInstance().showPeriodDialog( this, - { datePeriods: List? -> - FilterManager.getInstance().addPeriod(datePeriods) - }, + onFromToSelector, true, - ) + ) { + val disposable = RxDateDialog(activity, Period.WEEKLY) + .createForFilter().show() + .subscribe( + { selectedDates: org.dhis2.commons.data.tuples.Pair?> -> + onFromToSelector.onFromToSelected( + DateUtils.getInstance().getDatePeriodListFor( + selectedDates.val1(), + selectedDates.val0(), + ), + ) + }, + { t: Throwable? -> Timber.e(t) }, + ) + } } } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt index 33ba7f44f5..10a600cba6 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt @@ -15,6 +15,8 @@ import org.dhis2.commons.matomo.Categories import org.dhis2.commons.matomo.Labels import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.schedulers.SchedulerProvider +import org.dhis2.commons.schedulers.SingleEventEnforcer +import org.dhis2.commons.schedulers.get import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.program.Program @@ -38,6 +40,7 @@ class ProgramEventDetailPresenter( val stageUid: String? get() = eventRepository.programStage().blockingGet()?.uid() + private val singleEventEnforcer = SingleEventEnforcer.get() fun init() { compositeDisposable.add( Observable.fromCallable { @@ -123,7 +126,7 @@ class ProgramEventDetailPresenter( } fun addEvent() { - view.selectOrgUnitForNewEvent() + singleEventEnforcer.processEvent { view.selectOrgUnitForNewEvent() } } fun onBackClick() { diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.java index 579cd99d6a..73d25039a5 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.java @@ -26,6 +26,8 @@ import org.dhis2.bindings.ExtensionsKt; import org.dhis2.bindings.ViewExtensionsKt; import org.dhis2.commons.Constants; +import org.dhis2.commons.date.DateUtils; +import org.dhis2.commons.date.Period; import org.dhis2.commons.featureconfig.data.FeatureConfigRepository; import org.dhis2.commons.filters.FilterItem; import org.dhis2.commons.filters.FilterManager; @@ -43,9 +45,9 @@ import org.dhis2.usescases.searchTrackEntity.listView.SearchTEList; import org.dhis2.usescases.searchTrackEntity.mapView.SearchTEMap; import org.dhis2.usescases.searchTrackEntity.ui.SearchScreenConfigurator; -import org.dhis2.utils.DateUtils; import org.dhis2.utils.OrientationUtilsKt; import org.dhis2.utils.customviews.BreakTheGlassBottomDialog; +import org.dhis2.utils.customviews.RxDateDialog; import org.dhis2.utils.granularsync.SyncStatusDialog; import org.dhis2.utils.granularsync.SyncStatusDialogNavigatorKt; import org.hisp.dhis.android.core.arch.call.D2Progress; @@ -60,6 +62,7 @@ import javax.inject.Inject; import dhis2.org.analytics.charts.ui.GroupAnalyticsFragment; +import io.reactivex.disposables.Disposable; import io.reactivex.functions.Consumer; import kotlin.Pair; import kotlin.Unit; @@ -506,12 +509,14 @@ private void observeDownload() { } private void observeLegacyInteractions() { + viewModel.getLegacyInteraction().observe(this, legacyInteraction -> { if (legacyInteraction != null) { switch (legacyInteraction.getId()) { case ON_ENROLL_CLICK -> { LegacyInteraction.OnEnrollClick interaction = (LegacyInteraction.OnEnrollClick) legacyInteraction; presenter.onEnrollClick(new HashMap<>(interaction.getQueryData())); + } case ON_ADD_RELATIONSHIP -> { LegacyInteraction.OnAddRelationship interaction = (LegacyInteraction.OnAddRelationship) legacyInteraction; @@ -672,14 +677,29 @@ public void showPeriodRequest(Pair periodR } }); } else { - DateUtils.getInstance().showPeriodDialog(this, datePeriods -> { - if (periodRequest.getSecond() == Filters.PERIOD) { - FilterManager.getInstance().addPeriod(datePeriods); - } else { - FilterManager.getInstance().addEnrollmentPeriod(datePeriods); - } - }, - true); + + DateUtils.OnFromToSelector onFromToSelector = datePeriods -> { + if (periodRequest.getSecond() == Filters.PERIOD) { + FilterManager.getInstance().addPeriod(datePeriods); + } else { + FilterManager.getInstance().addEnrollmentPeriod(datePeriods); + } + }; + + DateUtils.OnNextSelected onNextSelected = () -> { + Disposable disposable = new RxDateDialog(this, Period.WEEKLY) + .createForFilter().show() + .subscribe( + selectedDates -> onFromToSelector.onFromToSelected(DateUtils.getInstance().getDatePeriodListFor( + selectedDates.val1(), + selectedDates.val0()) + ), + Timber::e + ); + }; + + DateUtils.getInstance().showPeriodDialog(this,onFromToSelector, + true, onNextSelected); } } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEPresenter.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEPresenter.java index 0bdece83d1..2fe724992b 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEPresenter.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEPresenter.java @@ -14,7 +14,6 @@ import android.content.Intent; import android.graphics.drawable.Drawable; -import android.widget.DatePicker; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -22,8 +21,6 @@ import androidx.appcompat.content.res.AppCompatResources; import org.dhis2.R; -import org.dhis2.commons.dialogs.calendarpicker.CalendarPicker; -import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener; import org.dhis2.commons.filters.DisableHomeFiltersFromSettingsApp; import org.dhis2.commons.filters.FilterItem; import org.dhis2.commons.filters.FilterManager; @@ -37,6 +34,8 @@ import org.dhis2.commons.resources.ObjectStyleUtils; import org.dhis2.commons.resources.ResourceManager; import org.dhis2.commons.schedulers.SchedulerProvider; +import org.dhis2.commons.schedulers.SingleEventEnforcer; +import org.dhis2.commons.schedulers.SingleEventEnforcerImpl; import org.dhis2.data.service.SyncStatusController; import org.dhis2.maps.model.StageStyle; import org.dhis2.utils.analytics.AnalyticsHelper; @@ -47,12 +46,9 @@ import org.hisp.dhis.android.core.program.Program; import org.hisp.dhis.android.core.program.ProgramStage; import org.hisp.dhis.android.core.trackedentity.TrackedEntityType; -import org.jetbrains.annotations.NotNull; import java.util.ArrayList; -import java.util.Calendar; import java.util.Collections; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Objects; @@ -81,6 +77,8 @@ public class SearchTEPresenter implements SearchTEContractsModule.Presenter { private final CompositeDisposable compositeDisposable; private final TrackedEntityType trackedEntity; + SingleEventEnforcer singleEventEnforcer = new SingleEventEnforcerImpl(); + private final String trackedEntityType; private final DisableHomeFiltersFromSettingsApp disableHomeFilters; @@ -270,6 +268,13 @@ public void onBackClick() { @Override public void onEnrollClick(HashMap queryData) { + singleEventEnforcer.processEvent(() -> { + manageEnrollClick(queryData); + return Unit.INSTANCE; + }); + } + + public void manageEnrollClick(HashMap queryData) { if (selectedProgram != null) if (canCreateTei()) enroll(selectedProgram.uid(), null, queryData); @@ -279,6 +284,7 @@ public void onEnrollClick(HashMap queryData) { view.displayMessage(view.getContext().getString(R.string.search_program_not_selected)); } + private boolean canCreateTei() { boolean programAccess = selectedProgram.access().data().write() != null && selectedProgram.access().data().write(); boolean teTypeAccess = d2.trackedEntityModule().trackedEntityTypes().uid( diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardProgramModel.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardProgramModel.java index 34ea5414e7..d142a947b4 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardProgramModel.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardProgramModel.java @@ -11,16 +11,13 @@ import org.hisp.dhis.android.core.organisationunit.OrganisationUnit; import org.hisp.dhis.android.core.program.Program; import org.hisp.dhis.android.core.program.ProgramStage; -import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttribute; import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute; import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue; import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; 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 72ccbcdfbd..15a1b0b043 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 @@ -24,6 +24,8 @@ import org.dhis2.commons.data.EventViewModelType import org.dhis2.commons.data.StageSection import org.dhis2.commons.resources.D2ErrorUtils import org.dhis2.commons.schedulers.SchedulerProvider +import org.dhis2.commons.schedulers.SingleEventEnforcer +import org.dhis2.commons.schedulers.get import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.form.data.FormValueStore import org.dhis2.form.data.OptionsRepository @@ -85,6 +87,8 @@ class TEIDataPresenter( private val _events: MutableLiveData> = MutableLiveData() val events: LiveData> = _events + private val singleEventEnforcer = SingleEventEnforcer.get() + fun init() { programUid?.let { val program = d2.program(it) ?: throw NullPointerException() @@ -350,6 +354,12 @@ class TEIDataPresenter( } fun onAddNewEventOptionSelected(eventCreationType: EventCreationType, stage: ProgramStage?) { + singleEventEnforcer.processEvent { + manageAddNewEventOptionSelected(eventCreationType, stage) + } + } + + private fun manageAddNewEventOptionSelected(eventCreationType: EventCreationType, stage: ProgramStage?) { if (stage != null) { when (eventCreationType) { EventCreationType.ADDNEW -> programUid?.let { program -> diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/StageViewHolder.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/StageViewHolder.kt index a0138270a3..fd1975339a 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/StageViewHolder.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/StageViewHolder.kt @@ -22,6 +22,7 @@ import org.dhis2.commons.data.EventViewModel import org.dhis2.commons.data.StageSection import org.dhis2.commons.resources.ColorUtils import org.dhis2.commons.resources.ResourceManager +import org.dhis2.commons.schedulers.get import org.dhis2.ui.MetadataIcon import org.dhis2.ui.MetadataIconData import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TEIDataPresenter @@ -45,7 +46,6 @@ internal class StageViewHolder( val stage = eventItem.stage!! val resourceManager = ResourceManager(itemView.context, colorUtils) - composeView.setContent { Row( modifier = Modifier diff --git a/app/src/test/java/org/dhis2/usescases/orgunitselector/OUTreeViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/orgunitselector/OUTreeViewModelTest.kt index d621a86260..ea4a1f33d3 100644 --- a/app/src/test/java/org/dhis2/usescases/orgunitselector/OUTreeViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/orgunitselector/OUTreeViewModelTest.kt @@ -206,7 +206,8 @@ class OUTreeViewModelTest { whenever( repository.orgUnit(childOrgUnits[0].uid()), ) doReturn childOrgUnits[0] - val result = viewModel.getOrgUnits() + viewModel.confirmSelection() + val result = viewModel.finalSelectedOrgUnits.value assertTrue(result.size == 1) assertTrue(result.first().uid() == childOrgUnits[0].uid()) } diff --git a/commons/src/main/java/org/dhis2/commons/date/DateUtils.java b/commons/src/main/java/org/dhis2/commons/date/DateUtils.java index 70f5d4a4cf..542380e0ed 100644 --- a/commons/src/main/java/org/dhis2/commons/date/DateUtils.java +++ b/commons/src/main/java/org/dhis2/commons/date/DateUtils.java @@ -1,15 +1,25 @@ package org.dhis2.commons.date; +import android.content.Context; +import android.widget.DatePicker; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.dhis2.commons.dialogs.calendarpicker.CalendarPicker; +import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener; +import org.dhis2.commons.filters.FilterManager; import org.hisp.dhis.android.core.event.EventStatus; +import org.hisp.dhis.android.core.period.DatePeriod; import org.hisp.dhis.android.core.period.PeriodType; +import org.jetbrains.annotations.NotNull; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Calendar; import java.util.Date; +import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; @@ -769,4 +779,105 @@ public static int[] getDifference(Date startDate, Date endDate) { org.joda.time.Period interval = new org.joda.time.Period(startDate.getTime(), endDate.getTime(), org.joda.time.PeriodType.yearMonthDayTime()); return new int[]{interval.getYears(), interval.getMonths(), interval.getDays()}; } + + public void fromCalendarSelector(Context context, OnFromToSelector fromToListener) { + Date startDate = null; + if (!FilterManager.getInstance().getPeriodFilters().isEmpty()) + startDate = FilterManager.getInstance().getPeriodFilters().get(0).startDate(); + + CalendarPicker dialog = new CalendarPicker(context); + dialog.setTitle(null); + dialog.setInitialDate(startDate); + dialog.isFutureDatesAllowed(true); + dialog.setListener(new OnDatePickerListener() { + @Override + public void onNegativeClick() { + //Do nothing + } + + @Override + public void onPositiveClick(@NotNull DatePicker datePicker) { + toCalendarSelector(datePicker, context, fromToListener); + } + }); + dialog.show(); + } + + public interface OnFromToSelector { + void onFromToSelected(List datePeriods); + } + + public interface OnNextSelected { + void onNextSelector(); + } + + private void toCalendarSelector(DatePicker datePicker, Context context, OnFromToSelector fromToListener) { + Calendar fromDate = Calendar.getInstance(); + fromDate.set(datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth()); + + Date endDate = null; + if (!FilterManager.getInstance().getPeriodFilters().isEmpty()) + endDate = FilterManager.getInstance().getPeriodFilters().get(0).endDate(); + + CalendarPicker dialog = new CalendarPicker(context); + dialog.setTitle(null); + dialog.setInitialDate(endDate); + dialog.setMinDate(fromDate.getTime()); + dialog.isFutureDatesAllowed(true); + dialog.setListener(new OnDatePickerListener() { + @Override + public void onNegativeClick() { + //Do nothing + } + + @Override + public void onPositiveClick(@NotNull DatePicker datePicker) { + Calendar toDate = Calendar.getInstance(); + toDate.set(datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth()); + List dates = new ArrayList<>(); + dates.add(DatePeriod.builder().startDate(fromDate.getTime()).endDate(toDate.getTime()).build()); + fromToListener.onFromToSelected(dates); + } + }); + dialog.show(); + } + + public void showPeriodDialog(Context context, OnFromToSelector fromToListener, boolean fromOtherPeriod, OnNextSelected onNextSelected ) { + Date startDate = null; + if (!FilterManager.getInstance().getPeriodFilters().isEmpty()) + startDate = FilterManager.getInstance().getPeriodFilters().get(0).startDate(); + + + CalendarPicker dialog = new CalendarPicker(context); + dialog.setTitle("Daily"); + dialog.setInitialDate(startDate); + dialog.isFutureDatesAllowed(true); + dialog.isFromOtherPeriods(fromOtherPeriod); + dialog.setListener(new OnDatePickerListener() { + @Override + public void onNegativeClick() { + onNextSelected.onNextSelector(); + } + + @Override + public void onPositiveClick(@NotNull DatePicker datePicker) { + Calendar chosenDate = Calendar.getInstance(); + chosenDate.set(datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth()); + List dates = new ArrayList<>(); + dates.add(chosenDate.getTime()); + fromToListener.onFromToSelected(getDatePeriodListFor(dates, Period.DAILY)); + } + }); + dialog.show(); + } + + + public List getDatePeriodListFor(List selectedDates, Period period) { + List datePeriods = new ArrayList<>(); + for (Date date : selectedDates) { + Date[] startEndDates = getDateFromDateAndPeriod(date, period); + datePeriods.add(DatePeriod.builder().startDate(startEndDates[0]).endDate(startEndDates[1]).build()); + } + return datePeriods; + } } diff --git a/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeFragment.kt b/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeFragment.kt index 63bf190731..c05ca9e684 100644 --- a/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeFragment.kt +++ b/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeFragment.kt @@ -16,7 +16,9 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import com.google.accompanist.themeadapter.material3.Mdc3Theme +import kotlinx.coroutines.launch import org.dhis2.ui.dialogs.orgunit.OrgUnitSelectorActions import org.dhis2.ui.dialogs.orgunit.OrgUnitSelectorDialog import org.hisp.dhis.android.core.organisationunit.OrganisationUnit @@ -40,7 +42,7 @@ class OUTreeFragment private constructor() : DialogFragment() { } fun withPreselectedOrgUnits(preselectedOrgUnits: List) = apply { - if (singleSelection && preselectedOrgUnits.size > 1) { + require(!(singleSelection && preselectedOrgUnits.size > 1)) { throw IllegalArgumentException( "Single selection only admits one pre-selected org. unit", ) @@ -49,7 +51,7 @@ class OUTreeFragment private constructor() : DialogFragment() { } fun singleSelection() = apply { - if (preselectedOrgUnits.size > 1) { + require(preselectedOrgUnits.size <= 1) { throw IllegalArgumentException( "Single selection only admits one pre-selected org. unit", ) @@ -82,7 +84,7 @@ class OUTreeFragment private constructor() : DialogFragment() { @Inject lateinit var viewModelFactory: OUTreeViewModelFactory - private val presenter: OUTreeViewModel by viewModels { viewModelFactory } + private val viewmodel: OUTreeViewModel by viewModels { viewModelFactory } var selectionCallback: ((selectedOrgUnits: List) -> Unit) = {} @@ -119,6 +121,14 @@ class OUTreeFragment private constructor() : DialogFragment() { showAsDialog().let { showAsDialog -> showsDialog = showAsDialog } + lifecycleScope.launch { + viewmodel.finalSelectedOrgUnits.collect { + if (it.isNotEmpty()) { + selectionCallback(it) + exitOuSelection() + } + } + } } override fun onCreateView( @@ -130,24 +140,24 @@ class OUTreeFragment private constructor() : DialogFragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { Mdc3Theme { - val list by presenter.treeNodes.collectAsState() + val list by viewmodel.treeNodes.collectAsState() OrgUnitSelectorDialog( title = null, items = list, actions = object : OrgUnitSelectorActions { override val onSearch: (String) -> Unit - get() = presenter::searchByName + get() = viewmodel::searchByName override val onOrgUnitChecked: (orgUnitUid: String, isChecked: Boolean) -> Unit - get() = presenter::onOrgUnitCheckChanged + get() = viewmodel::onOrgUnitCheckChanged override val onOpenOrgUnit: (orgUnitUid: String) -> Unit - get() = presenter::onOpenChildren + get() = viewmodel::onOpenChildren override val onDoneClick: () -> Unit get() = this@OUTreeFragment::confirmOuSelection override val onCancelClick: () -> Unit get() = this@OUTreeFragment::cancelOuSelection override val onClearClick: () -> Unit - get() = presenter::clearAll + get() = viewmodel::clearAll }, ) } @@ -157,6 +167,7 @@ class OUTreeFragment private constructor() : DialogFragment() { override fun onResume() { super.onResume() + showAsDialog().takeIf { it }?.let { fixDialogSize(0.9, 0.9) } @@ -165,8 +176,7 @@ class OUTreeFragment private constructor() : DialogFragment() { private fun showAsDialog() = arguments?.getBoolean(ARG_SHOW_AS_DIALOG, false) ?: false private fun confirmOuSelection() { - selectionCallback(presenter.getOrgUnits()) - exitOuSelection() + viewmodel.confirmSelection() } private fun cancelOuSelection() { diff --git a/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeViewModel.kt b/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeViewModel.kt index fec34f6436..005c302444 100644 --- a/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeViewModel.kt +++ b/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeViewModel.kt @@ -6,6 +6,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.dhis2.commons.schedulers.SingleEventEnforcer +import org.dhis2.commons.schedulers.get import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.ui.dialogs.orgunit.OrgUnitTreeItem import org.hisp.dhis.android.core.organisationunit.OrganisationUnit @@ -19,6 +21,11 @@ class OUTreeViewModel( private val _treeNodes = MutableStateFlow(emptyList()) val treeNodes: StateFlow> = _treeNodes + private val _finalSelectedOrgUnits = MutableStateFlow(emptyList()) + val finalSelectedOrgUnits: StateFlow> = _finalSelectedOrgUnits + + private val singleEventEnforcer = SingleEventEnforcer.get() + init { fetchInitialOrgUnits() } @@ -156,7 +163,13 @@ class OUTreeViewModel( return nodesCopy } - fun getOrgUnits(): List { + private fun getOrgUnits(): List { return selectedOrgUnits.mapNotNull { uid -> repository.orgUnit(uid) } } + + fun confirmSelection() { + singleEventEnforcer.processEvent { + _finalSelectedOrgUnits.update { getOrgUnits() } + } + } } diff --git a/commons/src/main/java/org/dhis2/commons/schedulers/SingleEventEnforcer.kt b/commons/src/main/java/org/dhis2/commons/schedulers/SingleEventEnforcer.kt new file mode 100644 index 0000000000..72e7d4e347 --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/schedulers/SingleEventEnforcer.kt @@ -0,0 +1,24 @@ +package org.dhis2.commons.schedulers + +fun interface SingleEventEnforcer { + fun processEvent(event: () -> Unit) + + companion object +} + +fun SingleEventEnforcer.Companion.get(): SingleEventEnforcer = + SingleEventEnforcerImpl() + +class SingleEventEnforcerImpl : SingleEventEnforcer { + private val now: Long + get() = System.currentTimeMillis() + + private var lastEventTimeMs: Long = 0 + + override fun processEvent(event: () -> Unit) { + if (now - lastEventTimeMs >= 1200L) { + event.invoke() + } + lastEventTimeMs = now + } +} From ce20719980a2518a8e8283b5619de70331dbf52c Mon Sep 17 00:00:00 2001 From: Pablo Date: Wed, 26 Jun 2024 12:53:17 +0200 Subject: [PATCH 28/35] =?UTF-8?q?fix:=20[ANDROAPP-6193]=20App=20asks=20dev?= =?UTF-8?q?ice=20location=20permission=20after=20granti=E2=80=A6=20(#3673)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: [ANDROAPP-6193] App asks device location permission after granting it Signed-off-by: Pablo * fix: check location is enabled Signed-off-by: Pablo --------- Signed-off-by: Pablo --- .../locationprovider/LocationProviderImpl.kt | 54 ++++++------------- 1 file changed, 16 insertions(+), 38 deletions(-) diff --git a/commons/src/main/java/org/dhis2/commons/locationprovider/LocationProviderImpl.kt b/commons/src/main/java/org/dhis2/commons/locationprovider/LocationProviderImpl.kt index 4e87e45e53..1583e3b54a 100644 --- a/commons/src/main/java/org/dhis2/commons/locationprovider/LocationProviderImpl.kt +++ b/commons/src/main/java/org/dhis2/commons/locationprovider/LocationProviderImpl.kt @@ -5,32 +5,25 @@ import android.annotation.SuppressLint import android.content.Context import android.content.Context.LOCATION_SERVICE import android.content.pm.PackageManager -import android.location.Criteria import android.location.Location import android.location.LocationListener import android.location.LocationManager -import android.os.Bundle import androidx.core.app.ActivityCompat +private const val FUSED_LOCATION_PROVIDER = "fused" + class LocationProviderImpl(val context: Context) : LocationProvider { private val locationManager: LocationManager by lazy { initLocationManager() } - private val locationCriteria: Criteria by lazy { initHighAccuracyCriteria() } - private val locationProvider: String? by lazy { initLocationProvider() } + + private val locationProvider: String by lazy { initLocationProvider() } private fun initLocationManager(): LocationManager { return context.getSystemService(LOCATION_SERVICE) as LocationManager } - private fun initLocationProvider(): String? { - return locationManager.getBestProvider(locationCriteria, false) - } - - private fun initHighAccuracyCriteria(): Criteria { - return Criteria().apply { - accuracy = Criteria.ACCURACY_FINE - speedAccuracy = Criteria.ACCURACY_HIGH - } + private fun initLocationProvider(): String { + return FUSED_LOCATION_PROVIDER } private var locationListener: LocationListener? = null @@ -47,12 +40,11 @@ class LocationProviderImpl(val context: Context) : LocationProvider { onLocationDisabled() requestLocationUpdates(onNewLocation) } else { - locationManager.getLastKnownLocation(locationProvider!!).apply { + locationManager.getLastKnownLocation(locationProvider).apply { if (this != null && latitude != 0.0 && longitude != 0.0) { onNewLocation(this) - } else { - requestLocationUpdates(onNewLocation) } + requestLocationUpdates(onNewLocation) } } } @@ -60,32 +52,17 @@ class LocationProviderImpl(val context: Context) : LocationProvider { @SuppressLint("MissingPermission") private fun requestLocationUpdates(onNewLocation: (Location) -> Unit) { if (hasPermission()) { - locationListener = object : LocationListener { - override fun onLocationChanged(location: Location) { - location.let { - onNewLocation(it) - stopLocationUpdates() - } - } - override fun onProviderEnabled(provider: String) { - // Need implementation for compatibility - } - override fun onProviderDisabled(provider: String) { - // Need implementation for compatibility - } - - @Deprecated("Deprecated in Java") - override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) { - // Need implementation for compatibility - } + locationListener = LocationListener { location -> + // TODO: (To improve accuracy check that location.accuracy is less than 5m) + onNewLocation(location) + stopLocationUpdates() } locationManager.requestLocationUpdates( - 1000, + locationProvider, + 1000L, 5f, - locationCriteria, locationListener!!, - null, ) } } @@ -98,7 +75,8 @@ class LocationProviderImpl(val context: Context) : LocationProvider { } override fun hasLocationEnabled(): Boolean { - return locationProvider?.let { locationManager.isProviderEnabled(it) } ?: false + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || + locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) } override fun stopLocationUpdates() { From 8cd2d4dfa5f85d65f88416229e2a2269da585849 Mon Sep 17 00:00:00 2001 From: Ferdy Rodriguez Date: Thu, 27 Jun 2024 07:44:27 +0200 Subject: [PATCH 29/35] fix: [ANDROAPP-6187] Reloads forms when event details fields are updated when creating a new event (#3697) --- .../EventCaptureFormFragment.java | 22 +- .../EventCaptureFormPresenter.kt | 1 + .../org/dhis2/form/data/EventRepository.kt | 16 +- .../java/org/dhis2/form/model/RowAction.kt | 1 + .../java/org/dhis2/form/ui/FormViewModel.kt | 207 ++++++++++-------- 5 files changed, 142 insertions(+), 105 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.java index 131a52eac1..560eee6ff1 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.java @@ -2,6 +2,7 @@ import static org.dhis2.commons.Constants.EVENT_MODE; import static org.dhis2.commons.extensions.ViewExtensionsKt.closeKeyboard; +import static org.dhis2.form.data.EventRepository.EVENT_ORG_UNIT_UID; import static org.dhis2.usescases.eventsWithoutRegistration.eventCapture.ui.NonEditableReasonBlockKt.showNonEditableReasonMessage; import static org.dhis2.utils.granularsync.SyncStatusDialogNavigatorKt.OPEN_ERROR_LOCATION; @@ -22,6 +23,7 @@ import org.dhis2.commons.featureconfig.data.FeatureConfigRepository; import org.dhis2.commons.featureconfig.model.Feature; import org.dhis2.databinding.SectionSelectorFragmentBinding; +import org.dhis2.form.model.ActionType; import org.dhis2.form.model.EventMode; import org.dhis2.form.model.EventRecords; import org.dhis2.form.ui.FormView; @@ -29,6 +31,7 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity; import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureContract; import org.dhis2.usescases.general.FragmentGlobalAbstract; +import org.hisp.dhis.android.core.common.ValueType; import org.jetbrains.annotations.NotNull; import javax.inject.Inject; @@ -78,6 +81,13 @@ public void onAttach(@NotNull Context context) { public void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) { String eventUid = getArguments().getString(Constants.EVENT_UID, ""); EventMode eventMode = EventMode.valueOf(getArguments().getString(EVENT_MODE)); + loadForm(eventUid, eventMode); + + activity.setFormEditionListener(this); + super.onCreate(savedInstanceState); + } + + private void loadForm(String eventUid, EventMode eventMode) { formView = new FormView.Builder() .locationProvider(locationProvider) .onLoadingListener(loading -> { @@ -87,6 +97,12 @@ public void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedI activity.hideProgress(); } return Unit.INSTANCE; + }).onItemChangeListener( action -> { + if(action.isEventDetailsRow()){ + presenter.showOrHideSaveButton(); + } + return Unit.INSTANCE; + }) .onFocused(() -> { activity.hideNavigationBar(); @@ -107,8 +123,6 @@ public void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedI featureConfig.isFeatureEnable(Feature.COMPOSE_FORMS) ) .build(); - activity.setFormEditionListener(this); - super.onCreate(savedInstanceState); } @Nullable @@ -190,6 +204,8 @@ public void onReopen() { @Override public void showNonEditableMessage(@NonNull String reason, boolean canBeReOpened) { + binding.editableReasonContainer.setVisibility(View.VISIBLE); + showNonEditableReasonMessage( binding.editableReasonContainer, reason, @@ -203,6 +219,6 @@ public void showNonEditableMessage(@NonNull String reason, boolean canBeReOpened @Override public void hideNonEditableMessage() { - binding.editableReasonContainer.removeAllViews(); + binding.editableReasonContainer.setVisibility(View.GONE); } } \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormPresenter.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormPresenter.kt index 4b491b27c1..1196bbe2f8 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormPresenter.kt @@ -83,6 +83,7 @@ class EventCaptureFormPresenter( when (isEditable) { is EventEditableStatus.Editable -> { view.showSaveButton() + view.hideNonEditableMessage() } is EventEditableStatus.NonEditable -> { diff --git a/form/src/main/java/org/dhis2/form/data/EventRepository.kt b/form/src/main/java/org/dhis2/form/data/EventRepository.kt index 06eb6fda4d..f56c941247 100644 --- a/form/src/main/java/org/dhis2/form/data/EventRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/EventRepository.kt @@ -52,10 +52,7 @@ class EventRepository( private val eventMode: EventMode, ) : DataEntryBaseRepository(FormBaseConfiguration(d2), fieldFactory) { - private val event by lazy { - d2.eventModule().events().uid(eventUid) - .blockingGet() - } + private var event = d2.eventModule().events().uid(eventUid).blockingGet() private val programStage by lazy { d2.programModule() @@ -139,7 +136,7 @@ class EventRepository( override fun list(): Flowable> { return d2.programModule().programStageSections() - .byProgramStageUid().eq(event?.programStage()) + .byProgramStageUid().eq(programStage?.uid()) .withDataElements() .get() .flatMap { programStageSection -> @@ -167,7 +164,7 @@ class EventRepository( sectionUid = EVENT_DATA_SECTION_UID, sectionName = resources.formatWithEventLabel( stringResource = R.string.event_data_section_title, - programStageUid = event?.programStage(), + programStageUid = programStage?.uid(), ), description = null, isOpen = true, @@ -183,6 +180,7 @@ class EventRepository( } private fun getEventDetails(): MutableList { + event = d2.eventModule().events().uid(eventUid).blockingGet() val eventDataItems = mutableListOf() eventDataItems.apply { add(createEventDetailsSection()) @@ -459,7 +457,7 @@ class EventRepository( sectionUid = EVENT_DETAILS_SECTION_UID, sectionName = resources.formatWithEventLabel( stringResource = R.string.event_details_section_title, - programStageUid = event?.programStage(), + programStageUid = programStage?.uid(), ), description = programStage?.description(), isOpen = false, @@ -487,7 +485,7 @@ class EventRepository( return Single.fromCallable { val stageDataElements = d2.programModule().programStageDataElements().withRenderType() - .byProgramStage().eq(event?.programStage()) + .byProgramStage().eq(programStage?.uid()) .orderBySortOrder(RepositoryScope.OrderByDirection.ASC) .blockingGet() @@ -510,7 +508,7 @@ class EventRepository( ) programStageSection.dataElements()?.forEach { dataElement -> d2.programModule().programStageDataElements().withRenderType() - .byProgramStage().eq(event?.programStage()) + .byProgramStage().eq(programStage?.uid()) .byDataElement().eq(dataElement.uid()) .one().blockingGet()?.let { fields.add( diff --git a/form/src/main/java/org/dhis2/form/model/RowAction.kt b/form/src/main/java/org/dhis2/form/model/RowAction.kt index daebcc3c79..aff2efcf6a 100644 --- a/form/src/main/java/org/dhis2/form/model/RowAction.kt +++ b/form/src/main/java/org/dhis2/form/model/RowAction.kt @@ -12,4 +12,5 @@ data class RowAction( val error: Throwable? = null, val type: ActionType, val valueType: ValueType? = null, + val isEventDetailsRow: Boolean = false, ) 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 f270ad7540..26cb94fe15 100644 --- a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt +++ b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt @@ -19,6 +19,9 @@ import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.form.R import org.dhis2.form.data.DataIntegrityCheckResult +import org.dhis2.form.data.EventRepository.Companion.EVENT_COORDINATE_UID +import org.dhis2.form.data.EventRepository.Companion.EVENT_ORG_UNIT_UID +import org.dhis2.form.data.EventRepository.Companion.EVENT_REPORT_DATE_UID import org.dhis2.form.data.FormRepository import org.dhis2.form.data.GeometryController import org.dhis2.form.data.GeometryParserImpl @@ -192,113 +195,125 @@ class FormViewModel( private fun processUserAction(action: RowAction): StoreResult { return when (action.type) { - ActionType.ON_SAVE -> { - if (action.valueType == ValueType.COORDINATE) { - repository.setFieldRequestingCoordinates(action.id, false) - } + ActionType.ON_SAVE -> handleOnSaveAction(action) + ActionType.ON_FOCUS, ActionType.ON_NEXT -> handleFocusOrNextAction(action) + ActionType.ON_TEXT_CHANGE -> handleOnTextChangeAction(action) + ActionType.ON_SECTION_CHANGE -> handleOnSectionChangeAction(action) + ActionType.ON_FINISH -> handleOnFinishAction(action) + ActionType.ON_REQUEST_COORDINATES -> handleOnRequestCoordinatesAction(action) + ActionType.ON_CANCEL_REQUEST_COORDINATES -> handleOnCancelRequestCoordinatesAction(action) + ActionType.ON_ADD_IMAGE_FINISHED -> handleOnAddImageFinishedAction(action) + ActionType.ON_STORE_FILE -> handleOnStoreFileAction(action) + } + } - repository.updateErrorList(action) - if (action.error != null) { - StoreResult( - action.id, - ValueStoreResult.VALUE_HAS_NOT_CHANGED, - ) - } else { - val saveResult = repository.save(action.id, action.value, action.extraData) - if (saveResult?.valueStoreResult != ValueStoreResult.ERROR_UPDATING_VALUE) { - repository.updateValueOnList(action.id, action.value, action.valueType) - } else { - repository.updateErrorList( - action.copy( - error = Throwable(saveResult.valueStoreResultMessage), - ), - ) - } - saveResult ?: StoreResult( - action.id, - ValueStoreResult.VALUE_CHANGED, - ) - } - } + private fun handleOnSaveAction(action: RowAction): StoreResult { + if (action.valueType == ValueType.COORDINATE) { + repository.setFieldRequestingCoordinates(action.id, false) + } - ActionType.ON_FOCUS, ActionType.ON_NEXT -> { - val storeResult = saveLastFocusedItem(action) - repository.setFocusedItem(action) - previousActionItem = action - storeResult - } + repository.updateErrorList(action) + if (action.error != null) { + return StoreResult( + action.id, + ValueStoreResult.VALUE_HAS_NOT_CHANGED, + ) + } - ActionType.ON_TEXT_CHANGE -> { + val saveResult = repository.save(action.id, action.value, action.extraData) + if (saveResult?.valueStoreResult != ValueStoreResult.ERROR_UPDATING_VALUE) { + if (action.isEventDetailsRow) { + repository.fetchFormItems(openErrorLocation) + } else { repository.updateValueOnList(action.id, action.value, action.valueType) - StoreResult( - action.id, - ValueStoreResult.TEXT_CHANGING, - ) } + } else { + repository.updateErrorList( + action.copy( + error = Throwable(saveResult.valueStoreResultMessage), + ), + ) + } + return saveResult ?: StoreResult( + action.id, + ValueStoreResult.VALUE_CHANGED, + ) + } - ActionType.ON_SECTION_CHANGE -> { - repository.updateSectionOpened(action) - StoreResult( - action.id, - ValueStoreResult.VALUE_HAS_NOT_CHANGED, - ) - } + private fun handleFocusOrNextAction(action: RowAction): StoreResult { + val storeResult = saveLastFocusedItem(action) + repository.setFocusedItem(action) + previousActionItem = action + return storeResult + } - ActionType.ON_FINISH -> { - repository.setFocusedItem(action) - StoreResult( - "", - ValueStoreResult.FINISH, - ) - } + private fun handleOnTextChangeAction(action: RowAction): StoreResult { + repository.updateValueOnList(action.id, action.value, action.valueType) + return StoreResult( + action.id, + ValueStoreResult.TEXT_CHANGING, + ) + } - ActionType.ON_REQUEST_COORDINATES -> { - repository.setFieldRequestingCoordinates(action.id, true) - StoreResult( - action.id, - ValueStoreResult.VALUE_HAS_NOT_CHANGED, - ) - } + private fun handleOnSectionChangeAction(action: RowAction): StoreResult { + repository.updateSectionOpened(action) + return StoreResult( + action.id, + ValueStoreResult.VALUE_HAS_NOT_CHANGED, + ) + } - ActionType.ON_CANCEL_REQUEST_COORDINATES -> { - repository.setFieldRequestingCoordinates(action.id, false) - StoreResult( - action.id, - ValueStoreResult.VALUE_HAS_NOT_CHANGED, - ) - } + private fun handleOnFinishAction(action: RowAction): StoreResult { + repository.setFocusedItem(action) + return StoreResult( + "", + ValueStoreResult.FINISH, + ) + } - ActionType.ON_ADD_IMAGE_FINISHED -> { - repository.setFieldAddingImage(action.id, false) - StoreResult( - action.id, - ValueStoreResult.VALUE_HAS_NOT_CHANGED, - ) - } + private fun handleOnRequestCoordinatesAction(action: RowAction): StoreResult { + repository.setFieldRequestingCoordinates(action.id, true) + return StoreResult( + action.id, + ValueStoreResult.VALUE_HAS_NOT_CHANGED, + ) + } - ActionType.ON_STORE_FILE -> { - val saveResult = repository.storeFile(action.id, action.value) - when (saveResult?.valueStoreResult) { - ValueStoreResult.FILE_SAVED -> { - processUserAction( - rowActionFromIntent( - FormIntent.OnSave( - uid = action.id, - value = saveResult.uid, - valueType = action.valueType, - ), - ), - ) - } + private fun handleOnCancelRequestCoordinatesAction(action: RowAction): StoreResult { + repository.setFieldRequestingCoordinates(action.id, false) + return StoreResult( + action.id, + ValueStoreResult.VALUE_HAS_NOT_CHANGED, + ) + } - null -> StoreResult( - action.id, - ValueStoreResult.VALUE_HAS_NOT_CHANGED, - ) + private fun handleOnAddImageFinishedAction(action: RowAction): StoreResult { + repository.setFieldAddingImage(action.id, false) + return StoreResult( + action.id, + ValueStoreResult.VALUE_HAS_NOT_CHANGED, + ) + } - else -> saveResult - } + private fun handleOnStoreFileAction(action: RowAction): StoreResult { + val saveResult = repository.storeFile(action.id, action.value) + return when (saveResult?.valueStoreResult) { + ValueStoreResult.FILE_SAVED -> { + processUserAction( + rowActionFromIntent( + FormIntent.OnSave( + uid = action.id, + value = saveResult.uid, + valueType = action.valueType, + ), + ), + ) } + null -> StoreResult( + action.id, + ValueStoreResult.VALUE_HAS_NOT_CHANGED, + ) + else -> saveResult } } @@ -641,8 +656,14 @@ class FormViewModel( error = error, type = actionType, valueType = valueType, + isEventDetailsRow = isEventDetailField(uid), ) + private fun isEventDetailField(uid: String): Boolean { + val eventDetailsIds = listOf(EVENT_REPORT_DATE_UID, EVENT_ORG_UNIT_UID, EVENT_COORDINATE_UID) + return eventDetailsIds.contains(uid) + } + fun onItemsRendered() { loading.value = false } From 57abb1fa66c9a2f8751c1d0e6389ed327bb87993 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 27 Jun 2024 11:52:26 +0200 Subject: [PATCH 30/35] fix: [ANDROAPP-6225] Org. Unit dialog crashes on device rotation (#3699) Signed-off-by: Pablo --- .../java/org/dhis2/commons/orgunitselector/OUTreeFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeFragment.kt b/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeFragment.kt index c05ca9e684..9d512f9117 100644 --- a/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeFragment.kt +++ b/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeFragment.kt @@ -29,7 +29,7 @@ const val ARG_SINGLE_SELECTION = "OUTreeFragment.ARG_SINGLE_SELECTION" const val ARG_SCOPE = "OUTreeFragment.ARG_SCOPE" const val ARG_PRE_SELECTED_OU = "OUTreeFragment.ARG_PRE_SELECTED_OU" -class OUTreeFragment private constructor() : DialogFragment() { +class OUTreeFragment() : DialogFragment() { class Builder { private var showAsDialog = false From 9d81916465362d7277155833f536b45c5bfd789f Mon Sep 17 00:00:00 2001 From: Ferdy Rodriguez Date: Thu, 27 Jun 2024 13:59:27 +0200 Subject: [PATCH 31/35] fix: [ANDROAPP-6158] Shows messages when data set section is empty (#3703) --- .../dataSetSection/DataSetSectionFragment.kt | 1 + .../dataSetSection/DataValuePresenter.kt | 3 +- app/src/main/res/values/strings.xml | 1 + .../java/org/dhis2/composetable/TableRobot.kt | 37 ++++++++++++++ .../composetable/ui/DataSetTableUiTest.kt | 18 ++++++- .../dhis2/composetable/TableScreenState.kt | 6 +++ .../composetable/ui/DataSetTableScreen.kt | 49 +++++++++++++++++-- .../ui/managestock/ManageStockViewModel.kt | 2 + 8 files changed, 110 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataSetSectionFragment.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataSetSectionFragment.kt index 75d74aa135..84f22b818c 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataSetSectionFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataSetSectionFragment.kt @@ -209,6 +209,7 @@ class DataSetSectionFragment : FragmentGlobalAbstract(), DataValueContract.View onCellClick = presenterFragment::onCellClick, onEdition = presenter::editingCellValue, onSaveValue = presenterFragment::onSaveValueChange, + emptyTablesText = getString(R.string.section_misconfigured), ) } } 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 434b519908..36310c14dd 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 @@ -15,6 +15,7 @@ import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.composetable.TableConfigurationState import org.dhis2.composetable.TableScreenState +import org.dhis2.composetable.TableState import org.dhis2.composetable.actions.Validator import org.dhis2.composetable.model.TableCell import org.dhis2.composetable.model.TableModel @@ -79,7 +80,7 @@ class DataValuePresenter( .subscribe( { screenState.update { currentScreenState -> - currentScreenState.copy(tables = it.tables) + currentScreenState.copy(tables = it.tables, state = TableState.SUCCESS) } }, { Timber.e(it) }, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c0bce83893..40fb2d39e9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -978,4 +978,5 @@ Show fields Hide fields Import successful + This section is misconfigured.\nContact your administrator. diff --git a/compose-table/src/androidTest/java/org/dhis2/composetable/TableRobot.kt b/compose-table/src/androidTest/java/org/dhis2/composetable/TableRobot.kt index 43e8955d4c..06424d9d2e 100644 --- a/compose-table/src/androidTest/java/org/dhis2/composetable/TableRobot.kt +++ b/compose-table/src/androidTest/java/org/dhis2/composetable/TableRobot.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.hasParent import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -40,6 +41,7 @@ import org.dhis2.composetable.model.TextInputModel import org.dhis2.composetable.ui.DataSetTableScreen import org.dhis2.composetable.ui.DataTable import org.dhis2.composetable.ui.DrawableId +import org.dhis2.composetable.ui.EMPTY_TABLE_TEXT_TAG import org.dhis2.composetable.ui.INPUT_ERROR_MESSAGE_TEST_TAG import org.dhis2.composetable.ui.INPUT_HELPER_TEXT_TEST_TAG import org.dhis2.composetable.ui.INPUT_ICON_TEST_TAG @@ -176,6 +178,33 @@ class TableRobot( return fakeModel } + fun initEmptyTableAppScreen( + emptyTablesText: String, + ): List { + val fakeModel: List = emptyList() + composeTestRule.setContent { + val screenState = TableScreenState(fakeModel, state = TableState.SUCCESS) + + val model by remember { mutableStateOf(screenState) } + TableTheme( + tableColors = TableColors().copy(primary = MaterialTheme.colors.primary), + tableConfiguration = TableConfiguration(), + tableResizeActions = object : TableResizeActions {} + ) { + DataSetTableScreen( + tableScreenState = model, + onCellClick = { _, _, _ -> + null + }, + emptyTablesText = emptyTablesText, + onEdition = {}, + onSaveValue = {} + ) + } + } + return fakeModel + } + private fun updateValue(fakeModel: List, tableCell: TableCell): List { return fakeModel.map { tableModel -> val hasRowWithDataElement = tableModel.tableRows.find { @@ -456,4 +485,12 @@ class TableRobot( fun hideKeyboard() { keyboardHelper.hideKeyboard() } + + fun assertInfoBarIsVisible(emptyString: String) { + composeTestRule.onNode( + hasParent(hasTestTag(EMPTY_TABLE_TEXT_TAG)) + and + hasText(emptyString) + ).assertIsDisplayed() + } } \ No newline at end of file diff --git a/compose-table/src/androidTest/java/org/dhis2/composetable/ui/DataSetTableUiTest.kt b/compose-table/src/androidTest/java/org/dhis2/composetable/ui/DataSetTableUiTest.kt index 75b8fcd759..2685ac04bc 100644 --- a/compose-table/src/androidTest/java/org/dhis2/composetable/ui/DataSetTableUiTest.kt +++ b/compose-table/src/androidTest/java/org/dhis2/composetable/ui/DataSetTableUiTest.kt @@ -1,8 +1,13 @@ package org.dhis2.composetable.ui import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasParent +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule import org.dhis2.composetable.model.FakeTableModels +import org.dhis2.composetable.tableRobot import org.junit.Rule import org.junit.Test @@ -20,4 +25,15 @@ class DataSetTableUiTest { ) } } -} \ No newline at end of file + + @Test + fun shouldRenderInfoBarIfTableListIsEmpty() { + tableRobot(composeTestRule) { + initEmptyTableAppScreen( + emptyTablesText = "Section is misconfigured" + ) + + assertInfoBarIsVisible("Section is misconfigured") + } + } +} diff --git a/compose-table/src/main/java/org/dhis2/composetable/TableScreenState.kt b/compose-table/src/main/java/org/dhis2/composetable/TableScreenState.kt index 3b40f2d161..a558287e25 100644 --- a/compose-table/src/main/java/org/dhis2/composetable/TableScreenState.kt +++ b/compose-table/src/main/java/org/dhis2/composetable/TableScreenState.kt @@ -6,6 +6,7 @@ import java.util.UUID data class TableScreenState( val tables: List, val id: UUID = UUID.randomUUID(), + val state: TableState = TableState.LOADING, ) data class TableConfigurationState( @@ -17,3 +18,8 @@ data class TableConfigurationState( !overwrittenRowHeaderWidth.isNullOrEmpty() or !overwrittenColumnWidth.isNullOrEmpty() } + +enum class TableState { + LOADING, + SUCCESS, +} diff --git a/compose-table/src/main/java/org/dhis2/composetable/ui/DataSetTableScreen.kt b/compose-table/src/main/java/org/dhis2/composetable/ui/DataSetTableScreen.kt index ae0f763239..315b7e7b50 100644 --- a/compose-table/src/main/java/org/dhis2/composetable/ui/DataSetTableScreen.kt +++ b/compose-table/src/main/java/org/dhis2/composetable/ui/DataSetTableScreen.kt @@ -6,13 +6,18 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.BottomSheetScaffold import androidx.compose.material.BottomSheetValue import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.rememberBottomSheetScaffoldState import androidx.compose.material.rememberBottomSheetState import androidx.compose.runtime.Composable @@ -29,9 +34,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.dhis2.composetable.TableScreenState +import org.dhis2.composetable.TableState import org.dhis2.composetable.actions.TableInteractions import org.dhis2.composetable.actions.TextInputInteractions import org.dhis2.composetable.model.TableCell @@ -43,6 +50,9 @@ import org.dhis2.composetable.ui.compositions.LocalInteraction import org.dhis2.composetable.ui.compositions.LocalUpdatingCell import org.dhis2.composetable.ui.extensions.collapseIfExpanded import org.dhis2.composetable.ui.extensions.expandIfCollapsed +import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItemColor +import org.hisp.dhis.mobile.ui.designsystem.component.InfoBar +import org.hisp.dhis.mobile.ui.designsystem.component.InfoBarData @OptIn(ExperimentalMaterialApi::class) @Composable @@ -53,6 +63,7 @@ fun DataSetTableScreen( TableCell, updateCellValue: (TableCell) -> Unit, ) -> TextInputModel?, + emptyTablesText: String? = null, onEdition: (editing: Boolean) -> Unit, onSaveValue: (TableCell) -> Unit, bottomContent: @Composable (() -> Unit)? = null, @@ -257,7 +268,7 @@ fun DataSetTableScreen( ), ) { AnimatedVisibility( - visible = tableScreenState.tables.isEmpty(), + visible = tableScreenState.state == TableState.LOADING, enter = fadeIn(), exit = fadeOut(), ) { @@ -277,10 +288,36 @@ fun DataSetTableScreen( LocalUpdatingCell provides updatingCell, LocalInteraction provides iter, ) { - DataTable( - tableList = tableScreenState.tables, - bottomContent = bottomContent, - ) + if (tableScreenState.state == TableState.SUCCESS && tableScreenState.tables.isEmpty()) { + Column( + modifier = Modifier.fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + InfoBar( + infoBarData = InfoBarData( + text = emptyTablesText ?: "", + icon = { + Icon( + imageVector = Icons.Outlined.ErrorOutline, + contentDescription = "warning", + tint = AdditionalInfoItemColor.WARNING.color, + ) + }, + color = AdditionalInfoItemColor.WARNING.color, + backgroundColor = AdditionalInfoItemColor.WARNING.color.copy(alpha = 0.1f), + actionText = null, + onClick = {}, + ), + Modifier.testTag(EMPTY_TABLE_TEXT_TAG), + ) + } + } else { + DataTable( + tableList = tableScreenState.tables, + bottomContent = bottomContent, + ) + } } displayDescription?.let { TableDialog( @@ -295,3 +332,5 @@ fun DataSetTableScreen( } } } + +const val EMPTY_TABLE_TEXT_TAG = "EMPTY_TABLE_TEXT_TAG" diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt index 08866d35c1..d7b70a3461 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt @@ -43,6 +43,7 @@ import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.composetable.TableConfigurationState import org.dhis2.composetable.TableScreenState +import org.dhis2.composetable.TableState import org.dhis2.composetable.actions.Validator import org.dhis2.composetable.model.KeyboardInputType import org.dhis2.composetable.model.TableCell @@ -265,6 +266,7 @@ class ManageStockViewModel @Inject constructor( _screenState.postValue( TableScreenState( tables = tables, + state = TableState.SUCCESS, ), ) From fb36857e4b9c8ca3abfd49b45192909147438de2 Mon Sep 17 00:00:00 2001 From: Pablo Date: Wed, 3 Jul 2024 08:05:37 +0200 Subject: [PATCH 32/35] fix: [ANDROAPP-6131] Add progress when event list is loading (#3681) Signed-off-by: Pablo Co-authored-by: Xavier Molloy --- .../org/dhis2/usescases/event/EventTest.kt | 11 -- .../programevent/ProgramEventTest.kt | 4 +- .../programevent/robot/ProgramEventsRobot.kt | 40 +++--- .../dhis2/usescases/searchte/SearchTETest.kt | 3 +- .../searchte/robot/SearchTeiRobot.kt | 2 + .../ProgramEventDetailActivity.kt | 5 +- .../ProgramEventDetailLiveAdapter.kt | 125 ------------------ .../ProgramEventDetailPresenter.kt | 1 - .../ProgramEventDetailRepository.kt | 8 +- .../ProgramEventDetailRepositoryImpl.kt | 24 +--- .../ProgramEventDetailView.kt | 1 - .../ProgramEventDetailViewModel.kt | 2 +- .../eventList/EventListFragment.kt | 118 +++++------------ .../eventList/EventListFragmentView.kt | 8 +- .../eventList/EventListInjector.kt | 26 ++-- .../eventList/EventListPresenter.kt | 38 ------ .../eventList/EventListPresenterFactory.kt | 27 ++++ .../eventList/EventListScreen.kt | 107 +++++++++++++++ .../eventList/EventListViewModel.kt | 89 +++++++++++++ .../eventList/ui/mapper/EventCardMapper.kt | 75 ++++++----- .../searchTrackEntity/SearchTEIViewModel.kt | 6 +- .../listView/SearchTEList.kt | 6 + .../teievents/ui/mapper/TEIEventCardMapper.kt | 18 +-- .../fragment_program_event_detail_list.xml | 44 ------ .../ProgramEventDetailPresenterTest.kt | 61 +-------- commons/build.gradle.kts | 1 + .../dhis2/commons/filters/FilterManager.java | 27 +++- .../filters/FilterManagerExtensions.kt | 14 ++ gradle/libs.versions.toml | 4 + 29 files changed, 412 insertions(+), 483 deletions(-) delete mode 100644 app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailLiveAdapter.kt delete mode 100644 app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenter.kt create mode 100644 app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenterFactory.kt create mode 100644 app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListScreen.kt create mode 100644 app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListViewModel.kt delete mode 100644 app/src/main/res/layout/fragment_program_event_detail_list.xml create mode 100644 commons/src/main/java/org/dhis2/commons/filters/FilterManagerExtensions.kt diff --git a/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt b/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt index 03bb401ac3..b557dbb3b5 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt @@ -122,7 +122,6 @@ class EventTest : BaseTest() { val atenatalCare = "lxAQ7Zs9VYR" prepareProgramAndLaunchActivity(atenatalCare) - disableRecyclerViewAnimations() programEventsRobot(composeTestRule) { clickOnAddEvent() @@ -175,14 +174,4 @@ class EventTest : BaseTest() { putExtra(ProgramEventDetailActivity.EXTRA_PROGRAM_UID, programUid) }.also { eventListRule.launch(it) } } - - private fun disableRecyclerViewAnimations() { - eventListRule.getScenario().onActivity { - it.runOnUiThread { - it.supportFragmentManager.findFragmentByTag("EVENT_LIST").apply { - (this as EventListFragment).binding.recycler.itemAnimator = null - } - } - } - } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt b/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt index 68549602fa..cdabba774d 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt @@ -55,8 +55,9 @@ class ProgramEventTest : BaseTest() { clickOnFormFabButton() clickOnCompleteButton() } + composeTestRule.waitForIdle() programEventsRobot(composeTestRule) { - checkEventWasCreatedAndClosed(eventOrgUnit) + checkEventWasCreatedAndClosed("1/1/2001") } } @@ -121,6 +122,7 @@ class ProgramEventTest : BaseTest() { programEventsRobot(composeTestRule) { checkEventWasDeleted(eventDate) } + composeTestRule.waitForIdle() rule.getScenario().onActivity { context.applicationContext.deleteDatabase(DB_TO_IMPORT) } diff --git a/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt index d430956e6d..896a920c72 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt @@ -2,23 +2,21 @@ package org.dhis2.usescases.programevent.robot import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.printToLog import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withTagValue -import androidx.test.espresso.matcher.ViewMatchers.withText import org.dhis2.R import org.dhis2.common.BaseRobot -import org.hamcrest.CoreMatchers.allOf -import org.hamcrest.CoreMatchers.anyOf -import org.hamcrest.CoreMatchers.equalTo fun programEventsRobot( composeTestRule: ComposeContentTestRule, @@ -33,7 +31,7 @@ class ProgramEventsRobot(val composeTestRule: ComposeContentTestRule) : BaseRobo @OptIn(ExperimentalTestApi::class) fun clickOnEvent(eventDate: String) { - composeTestRule.waitUntilAtLeastOneExists(hasText(eventDate)) + composeTestRule.waitUntilAtLeastOneExists(hasText(eventDate),5000) composeTestRule.onNodeWithText(eventDate).performClick() } @@ -45,21 +43,21 @@ class ProgramEventsRobot(val composeTestRule: ComposeContentTestRule) : BaseRobo onView(withId(R.id.navigation_map_view)).perform(click()) } - fun checkEventWasCreatedAndClosed(eventName: String) { - waitForView( - allOf( - withId(R.id.recycler), - hasDescendant(withText(eventName)), - hasDescendant( - withTagValue( - anyOf( - equalTo(R.drawable.ic_event_status_complete), - equalTo(R.drawable.ic_event_status_complete_read) - ) + @OptIn(ExperimentalTestApi::class) + fun checkEventWasCreatedAndClosed(eventDate: String) { + composeTestRule.waitUntilAtLeastOneExists(hasTestTag("EVENT_ITEM")) + composeTestRule.onNode( + hasTestTag("EVENT_ITEM") + and + hasAnyDescendant( + hasText("Event completed") ) - ) - ) - ).check(matches(isDisplayed())) + and + hasAnyDescendant( + hasText("View only") + ), + useUnmergedTree = true + ).assertIsDisplayed() } fun checkEventIsComplete(eventDate: String) { diff --git a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt index 99aebeb245..7b7d5c1fa6 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt @@ -130,7 +130,7 @@ class SearchTETest : BaseTest() { openNextSearchParameter("Last name") typeOnNextSearchTextParameter(lastName) clickOnSearch() - composeTestRule.waitForIdle() + checkListOfSearchTEI( title = "First name: $firstName", attributes = mapOf("Last name:" to lastName), @@ -385,6 +385,7 @@ class SearchTETest : BaseTest() { openNextSearchParameter("First name") typeOnNextSearchTextParameter(name) clickOnSearch() + composeTestRule.waitForIdle() } filterRobot { 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 9e27f720e5..6cd3a9f8bf 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 @@ -124,10 +124,12 @@ class SearchTeiRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun clickOnSearch() { closeKeyboard() composeTestRule.onNodeWithTag("SEARCH_BUTTON").performClick() + composeTestRule.waitForIdle() } fun checkListOfSearchTEI(title: String, attributes: Map) { //Checks title and all attributes are displayed + composeTestRule.waitForIdle() composeTestRule.onNodeWithText(title).assertIsDisplayed() attributes.forEach { item -> item.key?.let { composeTestRule.onNodeWithText(it).assertIsDisplayed() } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt index 520d0fa031..ae135b7f5a 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt @@ -246,6 +246,7 @@ class ProgramEventDetailActivity : FilterManager.getInstance().clearCatOptCombo() FilterManager.getInstance().clearWorkingList(true) FilterManager.getInstance().clearAssignToMe() + FilterManager.getInstance().clearFlow() presenter.clearOtherFiltersIfWebAppIsConfig() } @@ -253,10 +254,6 @@ class ProgramEventDetailActivity : binding.name = programModel.displayName() } - override fun showFilterProgress() { - programEventsViewModel.setProgress(true) - } - override fun renderError(message: String) { if (activity != null) { MaterialAlertDialogBuilder(activity, R.style.MaterialDialog) diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailLiveAdapter.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailLiveAdapter.kt deleted file mode 100644 index 28304f5177..0000000000 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailLiveAdapter.kt +++ /dev/null @@ -1,125 +0,0 @@ -package org.dhis2.usescases.programEventDetail - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.testTag -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.paging.PagedListAdapter -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil -import org.dhis2.R -import org.dhis2.commons.data.EventViewModel -import org.dhis2.commons.resources.ColorUtils -import org.dhis2.databinding.ItemEventBinding -import org.dhis2.usescases.programEventDetail.eventList.ui.mapper.EventCardMapper -import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.EventViewHolder -import org.hisp.dhis.android.core.program.Program -import org.hisp.dhis.mobile.ui.designsystem.component.ListCard -import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel -import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing - -class ProgramEventDetailLiveAdapter( - private val program: Program, - private val eventViewModel: ProgramEventDetailViewModel, - private val colorUtils: ColorUtils, - private val cardMapper: EventCardMapper, - config: AsyncDifferConfig, -) : PagedListAdapter(config) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder { - val inflater = LayoutInflater.from(parent.context) - val binding = ItemEventBinding.inflate(inflater, parent, false) - return EventViewHolder( - binding, - program, - colorUtils, - { eventUid -> - eventViewModel.eventSyncClicked.value = eventUid - }, - { _, _ -> }, - { eventUid, orgUnitUid, _, _ -> - eventViewModel.eventClicked.value = Pair(eventUid, orgUnitUid) - }, - ) - } - - override fun onBindViewHolder(holder: EventViewHolder, position: Int) { - getItem(position)?.let { - val materialView = holder.itemView.findViewById(R.id.materialView) - materialView.visibility = View.GONE - val composeView = holder.itemView.findViewById(R.id.composeView) - composeView.setContent { - val card = cardMapper.map( - event = it, - editable = it.event?.uid() - ?.let { eventViewModel.isEditable(it) } ?: true, - displayOrgUnit = it.event?.program() - ?.let { program -> eventViewModel.displayOrganisationUnit(program) } - ?: true, - onSyncIconClick = { - eventViewModel.eventSyncClicked.value = it.event?.uid() - }, - onCardClick = { - it.event?.let { event -> - eventViewModel.eventClicked.value = - Pair(event.uid(), event.organisationUnit() ?: "") - } - }, - ) - Column( - modifier = Modifier - .padding( - start = Spacing.Spacing8, - end = Spacing.Spacing8, - bottom = Spacing.Spacing4, - ), - ) { - if (position == 0) { - Spacer(modifier = Modifier.size(Spacing.Spacing8)) - } - ListCard( - modifier = Modifier.testTag("EVENT_ITEM"), - listAvatar = card.avatar, - title = ListCardTitleModel(text = card.title), - lastUpdated = card.lastUpdated, - additionalInfoList = card.additionalInfo, - actionButton = card.actionButton, - expandLabelText = card.expandLabelText, - shrinkLabelText = card.shrinkLabelText, - onCardClick = card.onCardCLick, - ) - } - } - - holder.bind(it, null) { - getItem(holder.bindingAdapterPosition)?.toggleValueList() - notifyItemChanged(holder.bindingAdapterPosition) - } - } - } - - companion object { - val diffCallback: DiffUtil.ItemCallback - get() = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: EventViewModel, - newItem: EventViewModel, - ): Boolean { - return oldItem.event?.uid() == newItem.event?.uid() - } - - override fun areContentsTheSame( - oldItem: EventViewModel, - newItem: EventViewModel, - ): Boolean { - return oldItem == newItem - } - } - } -} diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt index 10a600cba6..4783654fc8 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt @@ -97,7 +97,6 @@ class ProgramEventDetailPresenter( ) compositeDisposable.add( filterManager.asFlowable().onBackpressureLatest() - .doOnNext { view.showFilterProgress() } .subscribeOn(schedulerProvider.io()) .observeOn(schedulerProvider.ui()) .subscribe( diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt index dc9d805a4b..b3d127cae6 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt @@ -1,19 +1,19 @@ package org.dhis2.usescases.programEventDetail -import androidx.lifecycle.LiveData -import androidx.paging.PagedList +import androidx.paging.PagingData import io.reactivex.Flowable import io.reactivex.Single -import org.dhis2.commons.data.EventViewModel +import kotlinx.coroutines.flow.Flow import org.dhis2.commons.data.ProgramEventViewModel import org.hisp.dhis.android.core.category.CategoryOptionCombo import org.hisp.dhis.android.core.common.FeatureType +import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.event.EventFilter import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramStage interface ProgramEventDetailRepository { - fun filteredProgramEvents(): LiveData> + fun filteredProgramEvents(): Flow> fun filteredEventsForMap(): Flowable fun program(): Single fun getAccessDataWrite(): Boolean diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt index 1d73fec3a3..7834dfca3b 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt @@ -1,14 +1,11 @@ package org.dhis2.usescases.programEventDetail -import androidx.lifecycle.LiveData -import androidx.paging.DataSource -import androidx.paging.LivePagedListBuilder -import androidx.paging.PagedList +import androidx.paging.PagingData import com.mapbox.geojson.FeatureCollection import dhis2.org.analytics.charts.Charts import io.reactivex.Flowable import io.reactivex.Single -import org.dhis2.commons.data.EventViewModel +import kotlinx.coroutines.flow.Flow import org.dhis2.commons.data.ProgramEventViewModel import org.dhis2.commons.filters.data.FilterPresenter import org.dhis2.maps.geometry.mapper.featurecollection.MapCoordinateFieldToFeatureCollection @@ -41,22 +38,11 @@ class ProgramEventDetailRepositoryImpl internal constructor( filterPresenter.filteredEventProgram(it) } - override fun filteredProgramEvents(): LiveData> { + override fun filteredProgramEvents(): Flow> { val program = program().blockingGet() ?: throw NullPointerException() - val dataSource = filterPresenter + return filterPresenter .filteredEventProgram(program) - .dataSource - .map { event -> - mapper.eventToEventViewModel(event) - } - return LivePagedListBuilder( - object : DataSource.Factory() { - override fun create(): DataSource { - return dataSource - } - }, - 20, - ).build() + .getPagingData(10) } override fun filteredEventsForMap(): Flowable { diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailView.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailView.kt index 37a03aebda..736339e7af 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailView.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailView.kt @@ -10,7 +10,6 @@ interface ProgramEventDetailView : AbstractActivityContracts.View { fun renderError(message: String) fun showHideFilter() fun setWritePermission(canWrite: Boolean) - fun showFilterProgress() fun updateFilters(totalFilters: Int) fun openOrgUnitTreeSelector() fun showPeriodRequest(periodRequest: PeriodRequest) diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt index 68e21c5fc9..b01a20e640 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt @@ -19,7 +19,7 @@ class ProgramEventDetailViewModel( val dispatcher: DispatcherProvider, val createEventUseCase: CreateEventUseCase, ) : ViewModel() { - private val progress = MutableLiveData(true) + private val progress = MutableLiveData(false) val writePermission = MutableLiveData(false) val eventSyncClicked = MutableLiveData(null) val eventClicked = MutableLiveData?>(null) diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragment.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragment.kt index 34374c972d..38dd14eef7 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragment.kt @@ -4,51 +4,33 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.layout.padding -import androidx.compose.ui.Modifier +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.test.espresso.idling.concurrent.IdlingThreadPoolExecutor -import org.dhis2.R -import org.dhis2.commons.data.EventViewModel -import org.dhis2.commons.filters.workingLists.WorkingListChipGroup import org.dhis2.commons.filters.workingLists.WorkingListViewModel import org.dhis2.commons.filters.workingLists.WorkingListViewModelFactory -import org.dhis2.commons.resources.ColorUtils -import org.dhis2.databinding.FragmentProgramEventDetailListBinding import org.dhis2.usescases.general.FragmentGlobalAbstract import org.dhis2.usescases.programEventDetail.ProgramEventDetailActivity -import org.dhis2.usescases.programEventDetail.ProgramEventDetailLiveAdapter import org.dhis2.usescases.programEventDetail.ProgramEventDetailViewModel import org.dhis2.usescases.programEventDetail.eventList.ui.mapper.EventCardMapper -import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing -import java.util.concurrent.Executors -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit import javax.inject.Inject -class EventListFragment : FragmentGlobalAbstract(), EventListFragmentView { - - lateinit var binding: FragmentProgramEventDetailListBinding - private var liveAdapter: ProgramEventDetailLiveAdapter? = null - private val programEventsViewModel: ProgramEventDetailViewModel by activityViewModels() - private var liveDataList: LiveData>? = null +class EventListFragment : FragmentGlobalAbstract() { @Inject - lateinit var presenter: EventListPresenter + lateinit var eventListViewModelFactory: EventListPresenterFactory @Inject - lateinit var colorUtils: ColorUtils + lateinit var workingListViewModelFactory: WorkingListViewModelFactory @Inject lateinit var cardMapper: EventCardMapper - @Inject - lateinit var workingListViewModelFactory: WorkingListViewModelFactory + val eventListViewModel by viewModels { eventListViewModelFactory } override fun onCreateView( inflater: LayoutInflater, @@ -56,75 +38,37 @@ class EventListFragment : FragmentGlobalAbstract(), EventListFragmentView { savedInstanceState: Bundle?, ): View { (activity as ProgramEventDetailActivity).component - ?.plus(EventListModule(this)) + ?.plus(EventListModule()) ?.inject(this) - programEventsViewModel.setProgress(true) - - val bgThreadPoolExecutor = IdlingThreadPoolExecutor( - "DiffExecutor", - 2, - 2, - 0L, - TimeUnit.MILLISECONDS, - LinkedBlockingQueue(), - Executors.defaultThreadFactory(), - ) - val config = AsyncDifferConfig.Builder(ProgramEventDetailLiveAdapter.diffCallback) - .setBackgroundThreadExecutor(bgThreadPoolExecutor) - .build() - - val program = presenter.program() ?: throw NullPointerException() - liveAdapter = - ProgramEventDetailLiveAdapter( - program, - programEventsViewModel, - colorUtils, - cardMapper, - config, - ) - return FragmentProgramEventDetailListBinding.inflate(inflater, container, false) - .apply { - binding = this - recycler.adapter = liveAdapter - configureWorkingList() - }.root - } + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + val workingListViewModel by viewModels { workingListViewModelFactory } + val programEventsViewModel by activityViewModels() + val cardClicked by eventListViewModel.onEventCardClick.collectAsState(null) + val syncClicked by eventListViewModel.onSyncClick.collectAsState(null) - override fun onResume() { - super.onResume() - programEventsViewModel.setProgress(true) - presenter.init() - } + LaunchedEffect(key1 = cardClicked) { + cardClicked?.let { + programEventsViewModel.eventClicked.value = it + } + } - override fun setLiveData(pagedListLiveData: LiveData>) { - liveDataList?.removeObservers(viewLifecycleOwner) - this.liveDataList = pagedListLiveData - liveDataList?.observe(viewLifecycleOwner) { pagedList: PagedList -> - programEventsViewModel.setProgress(false) - liveAdapter?.submitList(pagedList) { - if ((binding.recycler.adapter?.itemCount ?: 0) == 0) { - binding.emptyTeis.text = getString(R.string.empty_tei_add) - binding.emptyTeis.visibility = View.VISIBLE - binding.recycler.visibility = View.GONE - } else { - binding.emptyTeis.visibility = View.GONE - binding.recycler.visibility = View.VISIBLE + LaunchedEffect(key1 = syncClicked) { + programEventsViewModel.eventSyncClicked.value = syncClicked } - EventListIdlingResourceSingleton.decrement() + + EventListScreen( + eventListViewModel, + workingListViewModel, + ) } } } - private fun configureWorkingList() { - binding.filterLayout.apply { - setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, - ) - setContent { - val workingListViewModel by viewModels { workingListViewModelFactory } - WorkingListChipGroup(Modifier.padding(top = Spacing.Spacing16), workingListViewModel) - } - } + override fun onResume() { + super.onResume() + eventListViewModel.refreshData() } } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragmentView.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragmentView.kt index 8ef6e8f995..8a7ceed0dd 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragmentView.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragmentView.kt @@ -1,9 +1,3 @@ package org.dhis2.usescases.programEventDetail.eventList -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import org.dhis2.commons.data.EventViewModel - -interface EventListFragmentView { - fun setLiveData(pagedListLiveData: LiveData>) -} +interface EventListFragmentView diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListInjector.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListInjector.kt index 538e12dd2e..de1b9c1bf4 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListInjector.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListInjector.kt @@ -5,9 +5,10 @@ import dagger.Provides import dagger.Subcomponent import org.dhis2.commons.di.dagger.PerFragment import org.dhis2.commons.filters.FilterManager -import org.dhis2.commons.prefs.PreferenceProvider -import org.dhis2.commons.schedulers.SchedulerProvider +import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.usescases.programEventDetail.ProgramEventDetailRepository +import org.dhis2.usescases.programEventDetail.ProgramEventMapper +import org.dhis2.usescases.programEventDetail.eventList.ui.mapper.EventCardMapper @PerFragment @Subcomponent(modules = [EventListModule::class]) @@ -16,23 +17,22 @@ interface EventListComponent { } @Module -class EventListModule( - val view: EventListFragmentView, -) { +class EventListModule { @Provides @PerFragment - fun providePresenter( + fun providePresenterFactory( filterManager: FilterManager, programEventDetailRepository: ProgramEventDetailRepository, - preferences: PreferenceProvider, - schedulers: SchedulerProvider, - ): EventListPresenter { - return EventListPresenter( - view, + dispatcher: DispatcherProvider, + mapper: ProgramEventMapper, + cardMapper: EventCardMapper, + ): EventListPresenterFactory { + return EventListPresenterFactory( filterManager, programEventDetailRepository, - preferences, - schedulers, + dispatcher, + mapper, + cardMapper, ) } } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenter.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenter.kt deleted file mode 100644 index e2439f9514..0000000000 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenter.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.dhis2.usescases.programEventDetail.eventList - -import io.reactivex.disposables.CompositeDisposable -import org.dhis2.commons.filters.FilterManager -import org.dhis2.commons.prefs.PreferenceProvider -import org.dhis2.commons.schedulers.SchedulerProvider -import org.dhis2.commons.schedulers.defaultSubscribe -import org.dhis2.usescases.programEventDetail.ProgramEventDetailRepository -import org.hisp.dhis.android.core.program.Program -import timber.log.Timber - -class EventListPresenter( - val view: EventListFragmentView, - val filterManager: FilterManager, - val eventRepository: ProgramEventDetailRepository, - val preferences: PreferenceProvider, - val schedulerProvider: SchedulerProvider, -) { - - val disposable = CompositeDisposable() - - fun init() { - disposable.add( - filterManager.asFlowable().startWith(filterManager) - .doOnEach { EventListIdlingResourceSingleton.increment() } - .map { eventRepository.filteredProgramEvents() } - .defaultSubscribe( - schedulerProvider, - { view.setLiveData(it) }, - { Timber.e(it) }, - ), - ) - } - - fun program(): Program? { - return eventRepository.program().blockingGet() - } -} diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenterFactory.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenterFactory.kt new file mode 100644 index 0000000000..3e342c0c31 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenterFactory.kt @@ -0,0 +1,27 @@ +package org.dhis2.usescases.programEventDetail.eventList + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.usescases.programEventDetail.ProgramEventDetailRepository +import org.dhis2.usescases.programEventDetail.ProgramEventMapper +import org.dhis2.usescases.programEventDetail.eventList.ui.mapper.EventCardMapper + +class EventListPresenterFactory( + private val filterManager: FilterManager, + private val programEventDetailRepository: ProgramEventDetailRepository, + private val dispatchers: DispatcherProvider, + private val mapper: ProgramEventMapper, + private val cardMapper: EventCardMapper, +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return EventListViewModel( + filterManager, + programEventDetailRepository, + dispatchers, + mapper, + cardMapper, + ) as T + } +} diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListScreen.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListScreen.kt new file mode 100644 index 0000000000..6a448b8adf --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListScreen.kt @@ -0,0 +1,107 @@ +package org.dhis2.usescases.programEventDetail.eventList + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.paging.LoadState +import androidx.paging.compose.collectAsLazyPagingItems +import org.dhis2.R +import org.dhis2.commons.filters.workingLists.WorkingListChipGroup +import org.dhis2.commons.filters.workingLists.WorkingListViewModel +import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing + +@Composable +fun EventListScreen( + eventListViewModel: EventListViewModel, + workingListViewModel: WorkingListViewModel, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(horizontal = 8.dp), + verticalArrangement = Arrangement.Absolute.spacedBy(Spacing.Spacing4), + ) { + WorkingListChipGroup( + Modifier.padding(top = Spacing.Spacing16), + workingListViewModel, + ) + val events = eventListViewModel.eventList.collectAsLazyPagingItems() + when (events.loadState.refresh) { + is LoadState.Error -> { + // no-op + } + LoadState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center, + ) { + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR) + } + } + + is LoadState.NotLoading -> { + if (events.itemCount < 1) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 42.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(id = R.string.empty_tei_add), + ) + } + } else { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + verticalArrangement = Arrangement.Absolute.spacedBy(4.dp), + ) { + items(count = events.itemCount) { index -> + val card = events[index]!! + ListCard( + modifier = Modifier.testTag("EVENT_ITEM"), + listAvatar = card.avatar, + title = ListCardTitleModel(text = card.title), + lastUpdated = card.lastUpdated, + additionalInfoList = card.additionalInfo, + actionButton = card.actionButton, + expandLabelText = card.expandLabelText, + shrinkLabelText = card.shrinkLabelText, + onCardClick = card.onCardCLick, + ) + + if (index == events.itemCount - 1) { + Spacer(modifier = Modifier.padding(100.dp)) + } + } + } + } + } + } + EventListIdlingResourceSingleton.decrement() + } +} diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListViewModel.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListViewModel.kt new file mode 100644 index 0000000000..921d8d269a --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListViewModel.kt @@ -0,0 +1,89 @@ +package org.dhis2.usescases.programEventDetail.eventList + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.map +import io.reactivex.disposables.CompositeDisposable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.ui.model.ListCardUiModel +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.usescases.programEventDetail.ProgramEventDetailRepository +import org.dhis2.usescases.programEventDetail.ProgramEventMapper +import org.dhis2.usescases.programEventDetail.eventList.ui.mapper.EventCardMapper + +class EventListViewModel( + val filterManager: FilterManager, + val eventRepository: ProgramEventDetailRepository, + val dispatchers: DispatcherProvider, + val mapper: ProgramEventMapper, + val cardMapper: EventCardMapper, +) : ViewModel() { + + val disposable = CompositeDisposable() + private var _onSyncClick: MutableStateFlow = MutableStateFlow(null) + val onSyncClick: Flow = _onSyncClick + private var _onEventCardClick: MutableStateFlow?> = MutableStateFlow(null) + val onEventCardClick: Flow?> = _onEventCardClick + + private var _eventList: Flow> = + filterManager.asFlow(viewModelScope) + .flatMapLatest { + EventListIdlingResourceSingleton.increment() + eventRepository.filteredProgramEvents() + .map { pagingData -> + pagingData.map { event -> + withContext(dispatchers.io()) { + val eventModel = mapper.eventToEventViewModel(event) + cardMapper.map( + event = eventModel, + editable = eventRepository.isEventEditable(event.uid()), + displayOrgUnit = event.program()?.let { program -> + eventRepository.displayOrganisationUnit(program) + } ?: true, + onSyncIconClick = { + onSyncIconClick( + eventModel.event?.uid(), + ) + }, + onCardClick = { + eventModel.event?.let { event -> + onEventCardClick( + Pair( + event.uid(), + event.organisationUnit() ?: "", + ), + ) + } + }, + ) + } + } + }.flowOn(dispatchers.io()) + }.flowOn(dispatchers.io()) + + val eventList = _eventList + + private fun onSyncIconClick(eventUid: String?) { + viewModelScope.launch { + _onSyncClick.emit(eventUid) + } + } + + private fun onEventCardClick(eventUidAndOrgUnit: Pair) { + viewModelScope.launch { + _onEventCardClick.emit(eventUidAndOrgUnit) + } + } + + fun refreshData() { + filterManager.publishData() + } +} diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/ui/mapper/EventCardMapper.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/ui/mapper/EventCardMapper.kt index 309a05642c..f410ad11c8 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/ui/mapper/EventCardMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/ui/mapper/EventCardMapper.kt @@ -48,6 +48,8 @@ class EventCardMapper( additionalInfo = getAdditionalInfoList(event, editable, displayOrgUnit), actionButton = { ProvideSyncButton( + syncButtonLabel = resourceManager.getString(R.string.sync), + retryButtonLabel = resourceManager.getString(R.string.sync_retry), state = event.event?.aggregatedSyncState(), onSyncIconClick = onSyncIconClick, ) @@ -231,40 +233,6 @@ class EventCardMapper( item?.let { list.add(it) } } - @Composable - private fun ProvideSyncButton(state: State?, onSyncIconClick: () -> Unit) { - val buttonText = when (state) { - State.TO_POST, - State.TO_UPDATE, - -> { - resourceManager.getString(R.string.sync) - } - - State.ERROR, - State.WARNING, - -> { - resourceManager.getString(R.string.sync_retry) - } - - else -> null - } - buttonText?.let { - Button( - style = ButtonStyle.TONAL, - text = it, - icon = { - Icon( - imageVector = Icons.Outlined.Sync, - contentDescription = it, - tint = TextColor.OnPrimaryContainer, - ) - }, - onClick = { onSyncIconClick() }, - modifier = Modifier.fillMaxWidth(), - ) - } - } - private fun checkSyncStatus( list: MutableList, state: State?, @@ -333,3 +301,42 @@ class EventCardMapper( item?.let { list.add(it) } } } + +@Composable +fun ProvideSyncButton( + syncButtonLabel: String, + retryButtonLabel: String, + state: State?, + onSyncIconClick: () -> Unit, +) { + val buttonText = when (state) { + State.TO_POST, + State.TO_UPDATE, + -> { + syncButtonLabel + } + + State.ERROR, + State.WARNING, + -> { + retryButtonLabel + } + + else -> null + } + buttonText?.let { + Button( + style = ButtonStyle.TONAL, + text = it, + icon = { + Icon( + imageVector = Icons.Outlined.Sync, + contentDescription = it, + tint = TextColor.OnPrimaryContainer, + ) + }, + onClick = { onSyncIconClick() }, + modifier = Modifier.fillMaxWidth(), + ) + } +} diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt index 78b7e72282..3f5d68b3e7 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt @@ -445,7 +445,11 @@ class SearchTEIViewModel( SearchScreenState.LIST -> { SearchIdlingResourceSingleton.increment() setListScreen() - _refreshData.postValue(Unit) + fetchListResults { flow -> + flow?.let { + _refreshData.postValue(Unit) + } + } } SearchScreenState.MAP -> { 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 0a35cda2e1..dc2278538f 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 @@ -29,6 +29,7 @@ import org.dhis2.bindings.dp import org.dhis2.commons.dialogs.imagedetail.ImageDetailActivity import org.dhis2.commons.filters.workingLists.WorkingListViewModel import org.dhis2.commons.filters.workingLists.WorkingListViewModelFactory +import org.dhis2.commons.idlingresource.SearchIdlingResourceSingleton import org.dhis2.commons.resources.ColorUtils import org.dhis2.databinding.FragmentSearchListBinding import org.dhis2.usescases.general.FragmentGlobalAbstract @@ -151,6 +152,11 @@ class SearchTEList : FragmentGlobalAbstract() { addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + SearchIdlingResourceSingleton.decrement() + } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { + SearchIdlingResourceSingleton.increment() + } if (!recyclerView.canScrollVertically(DIRECTION_DOWN)) { viewModel.isScrollingDown.value = false } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt index e55f23b607..35857df8f6 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt @@ -343,11 +343,12 @@ class TEIEventCardMapper( icon = { Icon( imageVector = Icons.Outlined.Edit, - contentDescription = resourceManager.getString(R.string.enter_event_data).format( - event.stage?.eventLabel() ?: resourceManager.getString( - R.string.event, + contentDescription = resourceManager.getString(R.string.enter_event_data) + .format( + event.stage?.eventLabel() ?: resourceManager.getString( + R.string.event, + ), ), - ), tint = TextColor.OnPrimaryContainer, ) }, @@ -365,11 +366,12 @@ class TEIEventCardMapper( icon = { Icon( imageVector = Icons.Outlined.Edit, - contentDescription = resourceManager.getString(R.string.enter_event_data).format( - event.stage?.eventLabel() ?: resourceManager.getString( - R.string.event, + contentDescription = resourceManager.getString(R.string.enter_event_data) + .format( + event.stage?.eventLabel() ?: resourceManager.getString( + R.string.event, + ), ), - ), tint = TextColor.OnPrimaryContainer, ) }, diff --git a/app/src/main/res/layout/fragment_program_event_detail_list.xml b/app/src/main/res/layout/fragment_program_event_detail_list.xml deleted file mode 100644 index f326932c95..0000000000 --- a/app/src/main/res/layout/fragment_program_event_detail_list.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/test/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenterTest.kt index 8244e9de58..daf4bd2e6e 100644 --- a/app/src/test/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenterTest.kt @@ -1,36 +1,19 @@ package org.dhis2.usescases.programEventDetail import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.MutableLiveData -import androidx.paging.PagedList -import com.mapbox.geojson.BoundingBox -import com.mapbox.geojson.Feature -import com.mapbox.geojson.FeatureCollection -import io.reactivex.Flowable import io.reactivex.Single import io.reactivex.android.plugins.RxAndroidPlugins import io.reactivex.schedulers.Schedulers -import org.dhis2.commons.data.EventViewModel -import org.dhis2.commons.data.EventViewModelType -import org.dhis2.commons.data.ProgramEventViewModel -import org.dhis2.commons.data.tuples.Pair import org.dhis2.commons.filters.DisableHomeFiltersFromSettingsApp import org.dhis2.commons.filters.FilterItem import org.dhis2.commons.filters.FilterManager -import org.dhis2.commons.filters.Filters -import org.dhis2.commons.filters.data.FilterPresenter import org.dhis2.commons.filters.data.FilterRepository -import org.dhis2.commons.filters.sorting.SortingItem -import org.dhis2.commons.filters.sorting.SortingStatus import org.dhis2.commons.filters.workingLists.EventFilterToWorkingListItemMapper import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.data.schedulers.TrampolineSchedulerProvider -import org.dhis2.ui.MetadataIconData import org.hisp.dhis.android.core.category.CategoryCombo import org.hisp.dhis.android.core.category.CategoryOptionCombo -import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.program.Program -import org.hisp.dhis.android.core.program.ProgramStage import org.junit.After import org.junit.Assert.assertTrue import org.junit.Before @@ -54,7 +37,6 @@ class ProgramEventDetailPresenterTest { private val scheduler = TrampolineSchedulerProvider() private val filterManager: FilterManager = FilterManager.getInstance() private val workingListMapper: EventFilterToWorkingListItemMapper = mock() - private val filterPresenter: FilterPresenter = mock() private val disableHomeFilters: DisableHomeFiltersFromSettingsApp = mock() private val matomoAnalyticsController: MatomoAnalyticsController = mock() @@ -83,49 +65,10 @@ class ProgramEventDetailPresenterTest { @Test fun `Should init screen`() { val program = Program.builder().uid("programUid").build() - val catOptionComboPair = Pair.create(dummyCategoryCombo(), dummyListCatOptionCombo()) - - val eventViewModel = EventViewModel( - EventViewModelType.EVENT, - ProgramStage.builder().uid("stageUid").build(), - Event.builder().uid("event").build(), - eventCount = 0, - lastUpdate = null, - isSelected = true, - canAddNewEvent = true, - orgUnitName = "orgUnit", - catComboName = "catComboName", - dataElementValues = emptyList(), - groupedByStage = false, - valueListIsOpen = false, - displayDate = "2/01/2021", - nameCategoryOptionCombo = "Category Option Combo", - metadataIconData = MetadataIconData.defaultIcon(), - ) - val events = - MutableLiveData>().also { - it.value?.add(eventViewModel) - } - - val mapEvents = Triple>( - FeatureCollection.fromFeature(Feature.fromGeometry(null)), - BoundingBox.fromLngLats(0.0, 0.0, 0.0, 0.0), - listOf(), - ) - val mapData = ProgramEventMapData( - mutableListOf(), - mutableMapOf("key" to FeatureCollection.fromFeature(Feature.fromGeometry(null))), - BoundingBox.fromLngLats(0.0, 0.0, 0.0, 0.0), - ) - filterManager.sortingItem = SortingItem(Filters.ORG_UNIT, SortingStatus.NONE) + whenever(repository.getAccessDataWrite()) doReturn true whenever(repository.program()) doReturn Single.just(program) - whenever( - repository.filteredProgramEvents(), - ) doReturn events - whenever( - repository.filteredEventsForMap(), - ) doReturn Flowable.just(mapData) + presenter.init() verify(view).setWritePermission(true) verify(view).setProgram(program) diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index 20ecdd06a0..ba60975d2a 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -92,6 +92,7 @@ dependencies { api(libs.androidx.compose.preview) api(libs.androidx.compose.ui) api(libs.androidx.compose.livedata) + api(libs.androidx.compose.paging) api(libs.google.material) api(libs.google.gson) diff --git a/commons/src/main/java/org/dhis2/commons/filters/FilterManager.java b/commons/src/main/java/org/dhis2/commons/filters/FilterManager.java index 61fc1f7cf7..db35d904bd 100644 --- a/commons/src/main/java/org/dhis2/commons/filters/FilterManager.java +++ b/commons/src/main/java/org/dhis2/commons/filters/FilterManager.java @@ -33,17 +33,28 @@ import io.reactivex.processors.PublishProcessor; import kotlin.Pair; import kotlin.collections.CollectionsKt; +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.flow.Flow; +import kotlinx.coroutines.flow.MutableSharedFlow; +import kotlinx.coroutines.flow.MutableStateFlow; public class FilterManager implements Serializable { public void publishData() { filterProcessor.onNext(this); + if (scope != null) { + FilterManagerExtensionsKt.emit(this, scope, filterFlow); + } } public void setCatComboAdapter(CatOptCombFilterAdapter adapter) { this.catComboAdapter = adapter; } + public void clearFlow() { + this.scope = null; + } + public enum PeriodRequest { FROM_TO, OTHER } @@ -91,6 +102,9 @@ public enum PeriodRequest { ); private FlowableProcessor filterProcessor; + private MutableSharedFlow filterFlow; + + private CoroutineScope scope; private FlowableProcessor ouTreeProcessor; private FlowableProcessor> periodRequestProcessor; private FlowableProcessor catOptComboRequestProcessor; @@ -137,7 +151,7 @@ public void reset() { eventStatusFilters = new ArrayList<>(); enrollmentStatusFilters = new ArrayList<>(); assignedFilter = false; - followUpFilter =false; + followUpFilter = false; sortingItem = null; ouFiltersApplied = new ObservableField<>(0); @@ -151,6 +165,7 @@ public void reset() { followUpFilterApplied = new ObservableField<>(0); filterProcessor = PublishProcessor.create(); + filterFlow = FilterManagerExtensionsKt.initFlow(this); ouTreeProcessor = PublishProcessor.create(); periodRequestProcessor = PublishProcessor.create(); catOptComboRequestProcessor = PublishProcessor.create(); @@ -258,7 +273,7 @@ public void addEnrollmentStatus(boolean remove, EnrollmentStatus enrollmentStatu boolean changed = true; if (remove) { enrollmentStatusFilters.remove(enrollmentStatus); - } else if (!enrollmentStatusFilters.contains(enrollmentStatus)){ + } else if (!enrollmentStatusFilters.contains(enrollmentStatus)) { enrollmentStatusFilters.clear(); enrollmentStatusFilters.add(enrollmentStatus); observableEnrollmentStatus.set(enrollmentStatus); @@ -296,7 +311,7 @@ public void addOrgUnit(OrganisationUnit ou) { publishData(); } - public void addOrgUnits(List ouList){ + public void addOrgUnits(List ouList) { ouFilters.clear(); ouFilters.addAll(ouList); liveDataOUFilter.setValue(ouFilters); @@ -348,9 +363,15 @@ public FlowableProcessor getOuTreeProcessor() { } public Flowable asFlowable() { + this.scope = null; return filterProcessor; } + public Flow asFlow(CoroutineScope scope) { + this.scope = scope; + return filterFlow; + } + public FlowableProcessor> getPeriodRequest() { return periodRequestProcessor; } diff --git a/commons/src/main/java/org/dhis2/commons/filters/FilterManagerExtensions.kt b/commons/src/main/java/org/dhis2/commons/filters/FilterManagerExtensions.kt new file mode 100644 index 0000000000..cfae6733a2 --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/filters/FilterManagerExtensions.kt @@ -0,0 +1,14 @@ +package org.dhis2.commons.filters + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlin.random.Random + +fun FilterManager.initFlow() = MutableStateFlow(0) +fun FilterManager.emit(scope: CoroutineScope, flow: MutableSharedFlow) { + scope.launch { + flow.emit(Random.nextInt()) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5cc97ce439..4eeafd87b7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,8 @@ dynamicanimation = "1.0.0" viewpager2 = "1.0.0" recyclerview = "1.3.1" compose = "1.5.4" +composePaging = "3.3.0" +composeLifecycle ="2.8.1" composeTheme = "1.2.1" composeConstraintLayout = "1.0.1" activityCompose = "1.8.2" @@ -122,6 +124,8 @@ androidx-compose-livedata = { group = "androidx.compose.runtime", name = "runtim androidx-compose-uitooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose" } androidx-compose-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose" } androidx-compose-viewbinding = { group = "androidx.compose.ui", name = "ui-viewbinding", version.ref = "compose" } +androidx-compose-paging = {group = "androidx.paging", name="paging-compose", version.ref="composePaging"} +androidx-compose-lifecycle = {group= "androidx.lifecycle", name ="lifecycle-runtime-compose", version.ref="composeLifecycle"} androidx-coreKtx = { group = "androidx.core", name = "core-ktx", version.ref = "corektx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" } From e9acb51e81fd6c8c679ba0f1b6f4d7020d47f21d Mon Sep 17 00:00:00 2001 From: Ferdy Rodriguez Date: Thu, 4 Jul 2024 11:50:45 +0200 Subject: [PATCH 33/35] changes calls to datasets to background thread and now uses kotlin's flows rather than rxjava's subscribeOn to prevent ANR (#3707) --- .../datasetList/DataSetListViewModel.kt | 58 +++++++++---------- .../DataSetListViewModelFactory.kt | 1 - .../datasetlist/DataSetListViewModelTest.kt | 7 --- 3 files changed, 28 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModel.kt b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModel.kt index 9eb943de26..e9a611cfec 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModel.kt @@ -4,14 +4,16 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.async +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.withContext import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.matomo.Actions import org.dhis2.commons.matomo.Categories import org.dhis2.commons.matomo.Labels import org.dhis2.commons.matomo.MatomoAnalyticsController -import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.usescases.datasets.datasetDetail.DataSetDetailModel import org.dhis2.usescases.datasets.datasetDetail.DataSetDetailRepository @@ -20,11 +22,9 @@ import timber.log.Timber class DataSetListViewModel( private val dataSetDetailRepository: DataSetDetailRepository, - schedulerProvider: SchedulerProvider, val filterManager: FilterManager, val matomoAnalyticsController: MatomoAnalyticsController, dispatcher: DispatcherProvider, - ) : ViewModel() { private val _datasets = MutableLiveData>() @@ -38,36 +38,34 @@ class DataSetListViewModel( val selectedSync = MutableLiveData>() init { - viewModelScope.launch(dispatcher.io()) { - val datasets = async { - filterManager.asFlowable() - .startWith(filterManager) - .flatMap { filterManager: FilterManager -> - dataSetDetailRepository.dataSetGroups( - filterManager.orgUnitUidsFilters, - filterManager.periodFilters, - filterManager.stateFilters, - filterManager.catOptComboFilters, - ).subscribeOn(schedulerProvider.io()) - } - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe({ + filterManager.asFlowable() + .startWith(filterManager) + .flatMap { filterManager: FilterManager -> + dataSetDetailRepository.dataSetGroups( + filterManager.orgUnitUidsFilters, + filterManager.periodFilters, + filterManager.stateFilters, + filterManager.catOptComboFilters, + ) + } + .asFlow() + .catch { Timber.d(it) } + .collectLatest { + withContext(dispatcher.ui()) { _datasets.value = it - }) { t: Throwable? -> Timber.d(t) } - } - val permissions = async { - dataSetDetailRepository.canWriteAny() - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe({ + } + } + + dataSetDetailRepository.canWriteAny() + .asFlow() + .catch { Timber.d(it) } + .collectLatest { + withContext(dispatcher.ui()) { _canWrite.value = it - }) { t: Throwable? -> Timber.e(t) } - } - datasets.await() - permissions.await() + } + } } } diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModelFactory.kt index 7bce936c93..cd7e07ef33 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModelFactory.kt @@ -21,7 +21,6 @@ class DataSetListViewModelFactory( override fun create(modelClass: Class): T { return DataSetListViewModel( dataSetDetailRepository, - schedulerProvider, filterManager, matomoAnalyticsController, dispatchers, diff --git a/app/src/test/java/org/dhis2/usescases/datasets/datasetDetail/datasetlist/DataSetListViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/datasets/datasetDetail/datasetlist/DataSetListViewModelTest.kt index 916d15ce09..f331c17039 100644 --- a/app/src/test/java/org/dhis2/usescases/datasets/datasetDetail/datasetlist/DataSetListViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/datasets/datasetDetail/datasetlist/DataSetListViewModelTest.kt @@ -15,7 +15,6 @@ import org.dhis2.commons.matomo.Categories import org.dhis2.commons.matomo.Labels import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.viewmodel.DispatcherProvider -import org.dhis2.data.schedulers.TrampolineSchedulerProvider import org.dhis2.usescases.datasets.datasetDetail.DataSetDetailModel import org.dhis2.usescases.datasets.datasetDetail.DataSetDetailRepository import org.dhis2.usescases.datasets.datasetDetail.datasetList.DataSetListViewModel @@ -37,11 +36,9 @@ class DataSetListViewModelTest { private lateinit var viewModel: DataSetListViewModel private val repository: DataSetDetailRepository = mock() - private val scheduler = TrampolineSchedulerProvider() private val filterManager: FilterManager = mock() private val matomoAnalyticsController: MatomoAnalyticsController = mock() - @OptIn(ExperimentalCoroutinesApi::class) private val testingDispatcher = StandardTestDispatcher() private val filterProcessor: FlowableProcessor = PublishProcessor.create() @@ -56,7 +53,6 @@ class DataSetListViewModelTest { whenever(repository.canWriteAny()) doReturn Flowable.just(true) viewModel = DataSetListViewModel( repository, - scheduler, filterManager, matomoAnalyticsController, object : DispatcherProvider { @@ -75,7 +71,6 @@ class DataSetListViewModelTest { ) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun `Should get the list of dataSet`() { val dataSets = listOf(dummyDataSet(), dummyDataSet(), dummyDataSet()) @@ -84,7 +79,6 @@ class DataSetListViewModelTest { ) doReturn Flowable.just(dataSets) viewModel = DataSetListViewModel( repository, - scheduler, filterManager, matomoAnalyticsController, object : DispatcherProvider { @@ -105,7 +99,6 @@ class DataSetListViewModelTest { assert(viewModel.datasets.value == dataSets) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun `Should get write permissions`() { whenever(repository.canWriteAny()) doReturn Flowable.just(true) From 2dac14da56fa8ee52e1a19b60888bf885b4c9e3e Mon Sep 17 00:00:00 2001 From: Ferdy Rodriguez Date: Mon, 8 Jul 2024 14:06:34 +0200 Subject: [PATCH 34/35] fix: [ANDROAPP-5808] GS1 QR code is show as QR Code if value does not have GS1 prefix value. (#3709) * GS1 QR code is show as QR Code if value does not have GS1 prefix value. * Fix failing test --- .../ui/provider/inputfield/AgeProviderTest.kt | 6 +++--- .../form/ui/dialog/QRImageControllerImpl.kt | 21 +++++++++++-------- .../form/dialog/QRImageControllerTest.kt | 11 +++++++++- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt b/form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt index 6160f1321d..1b37987cdf 100644 --- a/form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt +++ b/form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt @@ -31,9 +31,9 @@ class AgeProviderTest { const val INPUT_AGE_OPEN_CALENDAR_BUTTON = "INPUT_AGE_OPEN_CALENDAR_BUTTON" const val INPUT_AGE_TIME_UNIT_SELECTOR = "INPUT_AGE_TIME_UNIT_SELECTOR" const val INPUT_AGE_TEXT_FIELD = "INPUT_AGE_TEXT_FIELD" - const val RADIO_BUTTON_months = "RADIO_BUTTON_months" - const val RADIO_BUTTON_days = "RADIO_BUTTON_days" - const val RADIO_BUTTON_years = "RADIO_BUTTON_years" + const val RADIO_BUTTON_months = "RADIO_BUTTON_MONTHS" + const val RADIO_BUTTON_days = "RADIO_BUTTON_DAYS" + const val RADIO_BUTTON_years = "RADIO_BUTTON_YEARS" const val AGE_SELECTOR_TEXT = "6 years" const val INPUT_AGE_TEST_TAG = "INPUT_AGE" const val FIELD_UI_MODEL_UID = "FieldUIModelUid" diff --git a/form/src/main/java/org/dhis2/form/ui/dialog/QRImageControllerImpl.kt b/form/src/main/java/org/dhis2/form/ui/dialog/QRImageControllerImpl.kt index f2d771135b..d472c5e54b 100644 --- a/form/src/main/java/org/dhis2/form/ui/dialog/QRImageControllerImpl.kt +++ b/form/src/main/java/org/dhis2/form/ui/dialog/QRImageControllerImpl.kt @@ -17,15 +17,18 @@ class QRImageControllerImpl( ) : QRImageController { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - fun getWriterFromRendering(value: String, renderingType: UiRenderType) = when { - value.startsWith(GS1Elements.GS1_d2_IDENTIFIER.element) -> Pair( - DataMatrixWriter(), - BarcodeFormat.DATA_MATRIX, - ) - renderingType == UiRenderType.QR_CODE -> Pair(QRCodeWriter(), BarcodeFormat.QR_CODE) - renderingType == UiRenderType.BAR_CODE -> Pair(Code128Writer(), BarcodeFormat.CODE_128) - else -> throw IllegalArgumentException() - } + fun getWriterFromRendering(value: String, renderingType: UiRenderType) = + when (renderingType) { + UiRenderType.GS1_DATAMATRIX -> + if (value.startsWith(GS1Elements.GS1_d2_IDENTIFIER.element)) { + Pair(DataMatrixWriter(), BarcodeFormat.DATA_MATRIX) + } else { + Pair(QRCodeWriter(), BarcodeFormat.QR_CODE) + } + UiRenderType.QR_CODE -> Pair(QRCodeWriter(), BarcodeFormat.QR_CODE) + UiRenderType.BAR_CODE -> Pair(Code128Writer(), BarcodeFormat.CODE_128) + else -> throw IllegalArgumentException() + } private fun formattedContent(value: String) = value.removePrefix(GS1Elements.GS1_d2_IDENTIFIER.element) diff --git a/form/src/test/java/org/dhis2/form/dialog/QRImageControllerTest.kt b/form/src/test/java/org/dhis2/form/dialog/QRImageControllerTest.kt index cef23732ef..086304cbd1 100644 --- a/form/src/test/java/org/dhis2/form/dialog/QRImageControllerTest.kt +++ b/form/src/test/java/org/dhis2/form/dialog/QRImageControllerTest.kt @@ -15,12 +15,21 @@ class QRImageControllerTest { @Test fun shouldReturnDataMatrixWriter() { val testValue = "]d2\u001D01084700069915412110081996195256\u001D10DXB2005\u001D17220228" - controller.getWriterFromRendering(testValue, UiRenderType.QR_CODE).let { (writer, format) -> + controller.getWriterFromRendering(testValue, UiRenderType.GS1_DATAMATRIX).let { (writer, format) -> assertTrue(writer is DataMatrixWriter) assertTrue(format == BarcodeFormat.DATA_MATRIX) } } + @Test + fun shouldReturnQRWriterWhenValueDoesNotHaveGS1Prefix() { + val testValue = "01084700069915412110081996195256\u001D10DXB2005\u001D17220228" + controller.getWriterFromRendering(testValue, UiRenderType.GS1_DATAMATRIX).let { (writer, format) -> + assertTrue(writer is QRCodeWriter) + assertTrue(format == BarcodeFormat.QR_CODE) + } + } + @Test fun shouldReturnQRWriter() { val testValue = "qrValue" From cd8943412dd4eb2f5efacf703c78ac80476a3d60 Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Tue, 9 Jul 2024 08:55:59 +0200 Subject: [PATCH 35/35] fix: [ANDROAPPP-5753] formatting issues with attribute values on tei card dashboard (#3702) --- .../usescases/flow/teiFlow/TeiFlowTest.kt | 8 +- .../programevent/ProgramEventTest.kt | 8 +- .../programevent/robot/ProgramEventsRobot.kt | 4 +- .../dhis2/usescases/searchte/SearchTETest.kt | 2 +- .../org/dhis2/bindings/StringExtensions.kt | 4 +- .../dataSetSection/MapFieldValueToUser.kt | 2 +- .../SearchRepositoryImpl.java | 10 +- .../searchTrackEntity/SearchTEIViewModel.kt | 36 +-- .../teiDashboard/DashboardRepositoryImpl.kt | 232 ++++++++++-------- .../TeiProgramListRepositoryImpl.java | 5 +- .../main/java/org/dhis2/utils/ValueUtils.java | 52 ---- .../main/java/org/dhis2/utils/ValueUtils.kt | 88 +++++++ .../data/dhislogic/DhisPeriodUtilsTest.kt | 4 +- .../domain/ConfigureEventReportDateTest.kt | 16 +- .../SearchTEIViewModelTest.kt | 4 +- .../dhis2/commons/bindings/ValueExtensions.kt | 7 +- .../dhis2/commons/data/SearchTeiModel.java | 3 - .../org/dhis2/commons/date/DateExtensions.kt | 2 +- .../org/dhis2/commons/date/DateUtils.java | 4 +- .../commons/extensions/StringExtensions.kt | 48 +++- .../commons/resources/DhisPeriodUtils.kt | 2 +- 21 files changed, 312 insertions(+), 229 deletions(-) delete mode 100644 app/src/main/java/org/dhis2/utils/ValueUtils.java create mode 100644 app/src/main/java/org/dhis2/utils/ValueUtils.kt diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt index e7f603a2f5..acfe77dd6c 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 @@ -6,6 +6,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_PATH import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_RESPONSE +import org.dhis2.commons.date.DateUtils import org.dhis2.usescases.BaseTest import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel import org.dhis2.usescases.flow.teiFlow.entity.EnrollmentListUIModel @@ -16,7 +17,6 @@ import org.hisp.dhis.android.core.mockwebserver.ResponseController import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import java.text.SimpleDateFormat import java.util.Date @@ -95,9 +95,9 @@ class TeiFlowTest : BaseTest() { ) private fun getCurrentDate(): String { - val sdf = SimpleDateFormat(DATE_FORMAT) + val sdf = DateUtils.uiDateFormat() val dateFormat = sdf.format(Date()) - return dateFormat.removePrefix("0") + return dateFormat } private fun prepareWomanProgrammeIntentAndLaunchActivity(ruleSearch: ActivityTestRule) { @@ -117,7 +117,5 @@ class TeiFlowTest : BaseTest() { const val ORG_UNIT = "Ngelehun CHC" const val NAME = "Marta" const val LASTNAME = "Stuart" - - const val DATE_FORMAT = "dd/M/yyyy" } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt b/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt index cdabba774d..4f12841ae7 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt @@ -57,13 +57,13 @@ class ProgramEventTest : BaseTest() { } composeTestRule.waitForIdle() programEventsRobot(composeTestRule) { - checkEventWasCreatedAndClosed("1/1/2001") + checkEventWasCreatedAndClosed() } } @Test fun shouldOpenExistingEvent() { - val eventDate = "15/3/2020" + val eventDate = "15/03/2020" val eventOrgUnit = "Ngelehun CHC" prepareProgramAndLaunchActivity(antenatalCare) @@ -81,7 +81,7 @@ class ProgramEventTest : BaseTest() { @Ignore("Flaky test, will be look om issue ANDROAPP-6030") @Test fun shouldCompleteAnEventAndReopenIt() { - val eventDate = "15/3/2020" + val eventDate = "15/03/2020" prepareProgramAndLaunchActivity(antenatalCare) @@ -107,7 +107,7 @@ class ProgramEventTest : BaseTest() { @Test fun shouldDeleteEvent() { - val eventDate = "15/3/2020" + val eventDate = "15/03/2020" prepareProgramAndLaunchActivity(antenatalCare) diff --git a/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt index 896a920c72..9be1dff635 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt @@ -7,9 +7,7 @@ import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.printToLog import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches @@ -44,7 +42,7 @@ class ProgramEventsRobot(val composeTestRule: ComposeContentTestRule) : BaseRobo } @OptIn(ExperimentalTestApi::class) - fun checkEventWasCreatedAndClosed(eventDate: String) { + fun checkEventWasCreatedAndClosed() { composeTestRule.waitUntilAtLeastOneExists(hasTestTag("EVENT_ITEM")) composeTestRule.onNode( hasTestTag("EVENT_ITEM") 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 7b7d5c1fa6..df1d7a387e 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt @@ -435,7 +435,7 @@ class SearchTETest : BaseTest() { private fun createDisplayListFields() = DisplayListFieldsUIModel( "Sarah", "Thompson", - "2001-01-01", + "01/01/2001", "sarah@gmail.com", "Main street 1", "56", diff --git a/app/src/main/java/org/dhis2/bindings/StringExtensions.kt b/app/src/main/java/org/dhis2/bindings/StringExtensions.kt index 026140e282..9e2dd7797f 100644 --- a/app/src/main/java/org/dhis2/bindings/StringExtensions.kt +++ b/app/src/main/java/org/dhis2/bindings/StringExtensions.kt @@ -1,8 +1,8 @@ package org.dhis2.bindings import android.content.Context +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.date.toDateSpan -import org.dhis2.utils.DateUtils import timber.log.Timber import java.util.Date @@ -38,7 +38,7 @@ fun String.toDate(): Date { } if (date == null) { try { - date = DateUtils.databaseDateFormatMillis().parse(this) + date = DateUtils.databaseDateFormat().parse(this) } catch (e: Exception) { Timber.d("wrong format") } diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/MapFieldValueToUser.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/MapFieldValueToUser.kt index ea27653e45..b122f9478f 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/MapFieldValueToUser.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/MapFieldValueToUser.kt @@ -2,9 +2,9 @@ package org.dhis2.usescases.datasets.dataSetTable.dataSetSection import org.dhis2.R import org.dhis2.bindings.toDate +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.resources.ResourceManager import org.dhis2.data.forms.dataentry.tablefields.FieldViewModel -import org.dhis2.utils.DateUtils import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.dataelement.DataElement diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java index 070bb93b60..98a36d230a 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java @@ -368,7 +368,7 @@ private void setAttributeValue(SearchTeiModel searchTei, TrackedEntitySearchItem String value = attribute.getValue(); String transformedValue; if (value != null) { - transformedValue = ValueUtils.transformValue(d2, value, attribute.getValueType(), attribute.getOptionSet()); + transformedValue = ValueUtils.Companion.transformValue(d2, value, attribute.getValueType(), attribute.getOptionSet()); } else { transformedValue = sortingValueSetter.getUnknownLabel(); } @@ -409,8 +409,8 @@ private void setOverdueEvents(@NonNull SearchTeiModel tei, Program selectedProgr if (count > 0) { tei.setHasOverdue(true); - Date scheduleDate = scheduleList.size() > 0 ? scheduleList.get(0).dueDate() : null; - Date overdueDate = overdueList.size() > 0 ? overdueList.get(0).dueDate() : null; + Date scheduleDate = !scheduleList.isEmpty() ? scheduleList.get(0).dueDate() : null; + Date overdueDate = !overdueList.isEmpty() ? overdueList.get(0).dueDate() : null; Date dateToShow = null; if (scheduleDate != null && overdueDate != null) { if (scheduleDate.before(overdueDate)) { @@ -530,8 +530,8 @@ private List getTETypeAttributeUids(String teTypeUid) { } private int getTeiDefaultRes(TrackedEntityInstance tei) { - TrackedEntityType teiType = d2.trackedEntityModule().trackedEntityTypes().uid(tei.trackedEntityType()).blockingGet(); - return resources.getObjectStyleDrawableResource(teiType.style().icon(), R.drawable.photo_temp_gray); + TrackedEntityType teiTypeValues = d2.trackedEntityModule().trackedEntityTypes().uid(tei.trackedEntityType()).blockingGet(); + return resources.getObjectStyleDrawableResource(teiTypeValues.style().icon(), R.drawable.photo_temp_gray); } private List getTrackedEntityAttributesForRelationship(TrackedEntityInstance tei, Program selectedProgram) { diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt index 3f5d68b3e7..2f580bc979 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt @@ -18,7 +18,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.dhis2.R import org.dhis2.commons.data.SearchTeiModel -import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.extensions.toFriendlyDate +import org.dhis2.commons.extensions.toFriendlyDateTime +import org.dhis2.commons.extensions.toPercentage import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.idlingresource.SearchIdlingResourceSingleton import org.dhis2.commons.network.NetworkUtils @@ -38,7 +40,6 @@ import org.hisp.dhis.android.core.arch.helpers.Result import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.maintenance.D2ErrorCode import timber.log.Timber -import java.text.ParseException const val TEI_TYPE_SEARCH_MAX_RESULTS = 5 @@ -223,7 +224,9 @@ class SearchTEIViewModel( SearchScreenState.LIST -> setListScreen() SearchScreenState.MAP -> setMapScreen() SearchScreenState.ANALYTICS -> setAnalyticsScreen() - else -> {} + else -> { + // no-op + } } } @@ -927,6 +930,7 @@ class SearchTEIViewModel( val map = mutableMapOf() uiState.items.filter { !it.value.isNullOrEmpty() } .forEach { item -> + when (item.valueType) { ValueType.ORGANISATION_UNIT, ValueType.MULTI_TEXT -> { map[item.uid] = (item.displayName ?: "") @@ -934,31 +938,13 @@ class SearchTEIViewModel( ValueType.DATE, ValueType.AGE -> { item.value?.let { - if (it.isNotEmpty()) { - val date = try { - DateUtils.oldUiDateFormat().parse(it) - } catch (e: ParseException) { - null - } - map[item.uid] = date?.let { - DateUtils.uiDateFormat().format(date) - } ?: it - } + map[item.uid] = it.toFriendlyDate() } } ValueType.DATETIME -> { item.value?.let { - if (it.isNotEmpty()) { - val date = try { - DateUtils.databaseDateFormatNoSeconds().parse(it) - } catch (e: ParseException) { - null - } - map[item.uid] = date?.let { - DateUtils.uiDateTimeFormat().format(date) - } ?: it - } + map[item.uid] = it.toFriendlyDateTime() } } @@ -975,7 +961,9 @@ class SearchTEIViewModel( } ValueType.PERCENTAGE -> { - map[item.uid] = "${item.value}%" + item.value?.let { + map[item.uid] = it.toPercentage() + } } else -> { 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 a123aa9781..d81101aa2d 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt @@ -97,53 +97,42 @@ class DashboardRepositoryImpl( teiUid, ) .map> { attributesValues: List -> - val formattedValues: MutableList = - java.util.ArrayList() - for (attributeValue in attributesValues) { - if (attributeValue.value() != null) { - val attribute = - d2.trackedEntityModule().trackedEntityAttributes() - .uid(attributeValue.trackedEntityAttribute()).blockingGet() - if (attribute!!.valueType() != ValueType.IMAGE) { - formattedValues.add( - ValueUtils.transform( - d2, - attributeValue, - attribute!!.valueType(), - if (attribute!!.optionSet() != null) { - attribute!!.optionSet()!! - .uid() - } else { - null - }, - ), - ) - } - } else { - formattedValues.add( - TrackedEntityAttributeValue.builder() - .trackedEntityAttribute(attributeValue.trackedEntityAttribute()) - .trackedEntityInstance(teiUid) - .value("") - .build(), - ) - } - } + val formattedValues = formatProgramAttributeValues(attributesValues) formattedValues }.toObservable() } else { val teType = d2.trackedEntityModule().trackedEntityInstances().uid(teiUid).blockingGet()!! .trackedEntityType() - val attributeValues: MutableList = java.util.ArrayList() - for (attributeValue in teiAttributesProvider.getValuesFromTrackedEntityTypeAttributes( - teType, - teiUid, - )) { - val attribute = d2.trackedEntityModule().trackedEntityAttributes() - .uid(attributeValue.trackedEntityAttribute()).blockingGet() - if (attribute!!.valueType() != ValueType.IMAGE && attributeValue.value() != null) { - attributeValues.add( + + val attributeValues = mapTeiTypeAttributeValues( + teiAttributesProvider.getValuesFromTrackedEntityTypeAttributes( + teType, + teiUid, + ), + ) + if (attributeValues.isEmpty()) { + formatProgramAttributeValuesByTrackedEntity( + attributeValues, + teiAttributesProvider.getValuesFromProgramTrackedEntityAttributes( + teType, + teiUid, + ), + ) + } + Observable.just(attributeValues) + } + } + + private fun formatProgramAttributeValues(list: List): MutableList { + val formattedValues: MutableList = mutableListOf() + for (attributeValue in list) { + if (attributeValue.value() != null) { + val attribute = + d2.trackedEntityModule().trackedEntityAttributes() + .uid(attributeValue.trackedEntityAttribute()).blockingGet() + if (attribute!!.valueType() != ValueType.IMAGE) { + formattedValues.add( ValueUtils.transform( d2, attributeValue, @@ -157,31 +146,103 @@ class DashboardRepositoryImpl( ), ) } + } else { + formattedValues.add( + TrackedEntityAttributeValue.builder() + .trackedEntityAttribute(attributeValue.trackedEntityAttribute()) + .trackedEntityInstance(teiUid) + .value("") + .build(), + ) } - if (attributeValues.isEmpty()) { - for (attributeValue in teiAttributesProvider.getValuesFromProgramTrackedEntityAttributes( - teType, - teiUid, - )) { - val attribute = d2.trackedEntityModule().trackedEntityAttributes() - .uid(attributeValue.trackedEntityAttribute()).blockingGet() - attributeValues.add( - ValueUtils.transform( - d2, - attributeValue, - attribute!!.valueType(), - if (attribute.optionSet() != null) { - attribute.optionSet()!! - .uid() - } else { - null - }, + } + return formattedValues + } + + private fun formatProgramAttributeValuesByTrackedEntity( + formattedList: MutableList, + list: List, + ): MutableList { + for (attributeValue in list) { + val attribute = d2.trackedEntityModule().trackedEntityAttributes() + .uid(attributeValue.trackedEntityAttribute()).blockingGet() + formattedList.add( + ValueUtils.transform( + d2, + attributeValue, + attribute!!.valueType(), + if (attribute.optionSet() != null) { + attribute.optionSet()!! + .uid() + } else { + null + }, + ), + ) + } + return formattedList + } + + private fun mapTeiTypeAttributeValues(list: List): MutableList { + val attributeValues: MutableList = mutableListOf() + for (attributeValue in list) { + val attribute = d2.trackedEntityModule().trackedEntityAttributes() + .uid(attributeValue.trackedEntityAttribute()).blockingGet() + if (attribute!!.valueType() != ValueType.IMAGE && attributeValue.value() != null) { + attributeValues.add( + ValueUtils.transform( + d2, + attributeValue, + attribute.valueType(), + if (attribute.optionSet() != null) { + attribute.optionSet()!! + .uid() + } else { + null + }, + ), + ) + } + } + + return attributeValues + } + + private fun mapRelationShipTypes(list: List, teType: String): MutableList> { + val relTypeList: MutableList> = + java.util.ArrayList() + for (relationshipType in list) { + if (relationshipType.fromConstraint() != null && relationshipType.fromConstraint()!! + .trackedEntityType() != null && relationshipType.fromConstraint()!! + .trackedEntityType()!!.uid() == teType + ) { + if (relationshipType.toConstraint() != null && relationshipType.toConstraint()!! + .trackedEntityType() != null + ) { + relTypeList.add( + Pair.create( + relationshipType, + relationshipType.toConstraint()!! + .trackedEntityType()!!.uid(), ), ) } + } else if (relationshipType.bidirectional()!! && relationshipType.toConstraint() != null && relationshipType.toConstraint()!! + .trackedEntityType() != null && relationshipType.toConstraint()!! + .trackedEntityType()!! + .uid() == teType && relationshipType.fromConstraint() != null && + relationshipType.fromConstraint()!!.trackedEntityType() != null + ) { + relTypeList.add( + Pair.create( + relationshipType, + relationshipType.fromConstraint()!! + .trackedEntityType()!!.uid(), + ), + ) } - Observable.just(attributeValues) } + return relTypeList } override fun setFollowUp(enrollmentUid: String?): Boolean { @@ -228,8 +289,8 @@ class DashboardRepositoryImpl( return d2.systemInfoModule().systemInfo().get().toObservable() .map(Function { obj: SystemInfo -> obj.version() }) .flatMap { version: String? -> - if (version == "2.29") { - return@flatMap d2.relationshipModule().relationshipTypes() + return@flatMap if (version == "2.29") { + d2.relationshipModule().relationshipTypes() .get().toObservable() .flatMapIterable { list: List? -> list } .map> { relationshipType: RelationshipType? -> @@ -239,45 +300,10 @@ class DashboardRepositoryImpl( ) }.toList().toObservable() } else { - return@flatMap d2.relationshipModule() + d2.relationshipModule() .relationshipTypes().withConstraints().get() .map>> { relationshipTypes: List -> - val relTypeList: MutableList> = - java.util.ArrayList() - for (relationshipType in relationshipTypes) { - if (relationshipType.fromConstraint() != null && relationshipType.fromConstraint()!! - .trackedEntityType() != null && relationshipType.fromConstraint()!! - .trackedEntityType()!!.uid() == teType - ) { - if (relationshipType.toConstraint() != null && relationshipType.toConstraint()!! - .trackedEntityType() != null - ) { - relTypeList.add( - Pair.create( - relationshipType, - relationshipType.toConstraint()!! - .trackedEntityType()!!.uid(), - ), - ) - } - } else if (relationshipType.bidirectional()!! && relationshipType.toConstraint() != null && relationshipType.toConstraint()!! - .trackedEntityType() != null && relationshipType.toConstraint()!! - .trackedEntityType()!! - .uid() == teType - ) { - if (relationshipType.fromConstraint() != null && relationshipType.fromConstraint()!! - .trackedEntityType() != null - ) { - relTypeList.add( - Pair.create( - relationshipType, - relationshipType.fromConstraint()!! - .trackedEntityType()!!.uid(), - ), - ) - } - } - } + val relTypeList = mapRelationShipTypes(relationshipTypes, teType) relTypeList.toList() }.toObservable() } @@ -412,7 +438,7 @@ class DashboardRepositoryImpl( .trackedEntityInstances() .uid(teiUid) .blockingGet() - ?.state() == State.TO_POST + ?.aggregatedSyncState() == State.TO_POST val hasAuthority = d2.userModule() .authorities() .byName().eq("F_TEI_CASCADE_DELETE") @@ -433,7 +459,7 @@ class DashboardRepositoryImpl( val local = d2.enrollmentModule() .enrollments() .uid(enrollmentUid) - .blockingGet()!!.state() == State.TO_POST + .blockingGet()!!.aggregatedSyncState() == State.TO_POST val hasAuthority = d2.userModule() .authorities() .byName().eq("F_ENROLLMENT_CASCADE_DELETE") @@ -451,9 +477,9 @@ class DashboardRepositoryImpl( enrollmentObjectRepository.blockingGet()!!.status()!!, ) enrollmentObjectRepository.blockingDelete() - !d2.enrollmentModule().enrollments().byTrackedEntityInstance().eq(teiUid) + d2.enrollmentModule().enrollments().byTrackedEntityInstance().eq(teiUid) .byDeleted().isFalse - .byStatus().eq(EnrollmentStatus.ACTIVE).blockingGet().isEmpty() + .byStatus().eq(EnrollmentStatus.ACTIVE).blockingGet().isNotEmpty() } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java index 973e128a50..ece43b5b34 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java @@ -2,20 +2,17 @@ import androidx.annotation.NonNull; -import org.dhis2.R; +import org.dhis2.commons.date.DateUtils; import org.dhis2.commons.resources.MetadataIconProvider; -import org.dhis2.commons.resources.ResourceManager; import org.dhis2.usescases.main.program.ProgramDownloadState; import org.dhis2.usescases.main.program.ProgramViewModel; import org.dhis2.usescases.main.program.ProgramViewModelMapper; -import org.dhis2.utils.DateUtils; import org.hisp.dhis.android.core.D2; import org.hisp.dhis.android.core.common.State; import org.hisp.dhis.android.core.enrollment.EnrollmentCreateProjection; import org.hisp.dhis.android.core.enrollment.EnrollmentStatus; import org.hisp.dhis.android.core.organisationunit.OrganisationUnit; import org.hisp.dhis.android.core.program.Program; -import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor; import java.util.ArrayList; import java.util.Collections; diff --git a/app/src/main/java/org/dhis2/utils/ValueUtils.java b/app/src/main/java/org/dhis2/utils/ValueUtils.java deleted file mode 100644 index 5952d55ef5..0000000000 --- a/app/src/main/java/org/dhis2/utils/ValueUtils.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.dhis2.utils; - -import org.hisp.dhis.android.core.D2; -import org.hisp.dhis.android.core.common.ValueType; -import org.hisp.dhis.android.core.option.Option; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue; - -import java.util.Objects; - -/** - * QUADRAM. Created by ppajuelo on 25/09/2018. - */ - -public class ValueUtils { - - private ValueUtils() { - throw new IllegalStateException("Utility class"); - } - - public static TrackedEntityAttributeValue transform(D2 d2, TrackedEntityAttributeValue attributeValue, ValueType valueType, String optionSetUid) { - String transformedValue = transformValue(d2, attributeValue.value(), valueType, optionSetUid); - - if (!Objects.equals(transformedValue, attributeValue.value())) { - return attributeValue.toBuilder() - .value(transformedValue) - .build(); - } else { - return attributeValue; - } - } - - public static String transformValue(D2 d2, String value, ValueType valueType, String optionSetUid) { - String teAttrValue = value; - if (valueType.equals(ValueType.ORGANISATION_UNIT)) { - if (!d2.organisationUnitModule().organisationUnits().byUid().eq(value).blockingIsEmpty()) { - String orgUnitName = d2.organisationUnitModule().organisationUnits() - .byUid().eq(value) - .one().blockingGet().displayName(); - teAttrValue = orgUnitName; - } - } else if (optionSetUid != null) { - String optionCode = value; - if (optionCode != null) { - Option option = d2.optionModule().options().byOptionSetUid().eq(optionSetUid).byCode().eq(optionCode).one().blockingGet(); - if (option != null && (Objects.equals(option.code(), optionCode) || Objects.equals(option.name(), optionCode))) { - teAttrValue = option.displayName(); - } - } - } - return teAttrValue; - } -} diff --git a/app/src/main/java/org/dhis2/utils/ValueUtils.kt b/app/src/main/java/org/dhis2/utils/ValueUtils.kt new file mode 100644 index 0000000000..ff41fc0a4f --- /dev/null +++ b/app/src/main/java/org/dhis2/utils/ValueUtils.kt @@ -0,0 +1,88 @@ +package org.dhis2.utils + +import org.dhis2.commons.extensions.toFriendlyDate +import org.dhis2.commons.extensions.toFriendlyDateTime +import org.dhis2.commons.extensions.toPercentage +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue + +/** + * QUADRAM. Created by ppajuelo on 25/09/2018. + */ +class ValueUtils private constructor() { + init { + throw IllegalStateException("Utility class") + } + + companion object { + fun transform( + d2: D2, + attributeValue: TrackedEntityAttributeValue, + valueType: ValueType?, + optionSetUid: String?, + ): TrackedEntityAttributeValue { + val transformedValue = + transformValue(d2, attributeValue.value(), valueType, optionSetUid) + + return if (transformedValue != attributeValue.value()) { + attributeValue.toBuilder() + .value(transformedValue) + .build() + } else { + attributeValue + } + } + + fun transformValue( + d2: D2, + value: String?, + valueType: ValueType?, + optionSetUid: String?, + ): String? { + var teAttrValue = value + when (valueType) { + ValueType.ORGANISATION_UNIT -> { + if (!d2.organisationUnitModule().organisationUnits().byUid().eq(value) + .blockingIsEmpty() + ) { + val orgUnitName = d2.organisationUnitModule().organisationUnits() + .byUid().eq(value) + .one().blockingGet()!!.displayName()!! + teAttrValue = orgUnitName + } + } + ValueType.DATE, ValueType.AGE -> { + teAttrValue = teAttrValue?.toFriendlyDate() + } + ValueType.DATETIME -> { + teAttrValue = teAttrValue?.toFriendlyDateTime() + } + + ValueType.PERCENTAGE -> { + teAttrValue = teAttrValue?.toPercentage() + } + else -> { + teAttrValue = transformOptionSet(optionSetUid, d2, value) + } + } + return teAttrValue + } + + private fun transformOptionSet(optionSetUid: String?, d2: D2, value: String?): String? { + var teAttrValue = value + if (optionSetUid != null) { + val optionCode = value + if (optionCode != null) { + val option = + d2.optionModule().options().byOptionSetUid().eq(optionSetUid).byCode() + .eq(optionCode).one().blockingGet() + if (option != null && (option.code() == optionCode || option.name() == optionCode)) { + teAttrValue = option.displayName() + } + } + } + return teAttrValue + } + } +} diff --git a/app/src/test/java/org/dhis2/data/dhislogic/DhisPeriodUtilsTest.kt b/app/src/test/java/org/dhis2/data/dhislogic/DhisPeriodUtilsTest.kt index 1dfe083851..1116d9637e 100644 --- a/app/src/test/java/org/dhis2/data/dhislogic/DhisPeriodUtilsTest.kt +++ b/app/src/test/java/org/dhis2/data/dhislogic/DhisPeriodUtilsTest.kt @@ -47,7 +47,7 @@ class DhisPeriodUtilsTest { .build() Assert.assertEquals( - "11/1/2019", + "11/01/2019", periodUtils.getPeriodUIString(null, testDate, Locale.ENGLISH), ) } @@ -66,7 +66,7 @@ class DhisPeriodUtilsTest { .build() Assert.assertEquals( - "11/1/2019", + "11/01/2019", periodUtils.getPeriodUIString( PeriodType.Daily, testDate, diff --git a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDateTest.kt b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDateTest.kt index c354f8702f..4b521c9ce6 100644 --- a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDateTest.kt +++ b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDateTest.kt @@ -45,7 +45,7 @@ class ConfigureEventReportDateTest { ) // And has a concrete date - val expectedDate = "14/2/2022" + val expectedDate = "14/02/2022" val event: Event = mock { on { eventDate() } doReturn DateUtils.uiDateFormat().parse(expectedDate) @@ -97,7 +97,7 @@ class ConfigureEventReportDateTest { periodUtils = periodUtils, ) - val tomorrow = "16/2/2022" + val tomorrow = "16/02/2022" whenever( periodUtils.getPeriodUIString(any(), any(), any()), @@ -122,8 +122,8 @@ class ConfigureEventReportDateTest { scheduleInterval = 6, ) - val lastEventDate = "13/2/2022" - val nextEventDate = "19/2/2022" + val lastEventDate = "13/02/2022" + val nextEventDate = "19/02/2022" whenever( repository.getStageLastDate(ENROLLMENT_ID), ) doReturn DateUtils.uiDateFormat().parse(lastEventDate) @@ -147,8 +147,8 @@ class ConfigureEventReportDateTest { scheduleInterval = 6, ) - val lastEventDate = "13/2/2022" - val nextEventDate = "19/2/2022" + val lastEventDate = "13/02/2022" + val nextEventDate = "19/02/2022" whenever( repository.getStageLastDate(ENROLLMENT_ID), ) doReturn null @@ -180,8 +180,8 @@ class ConfigureEventReportDateTest { enrollmentId = ENROLLMENT_ID, ) - val lastEventDate = "13/2/2022" - val nextEventDate = "15/2/2022" + val lastEventDate = "13/02/2022" + val nextEventDate = "15/02/2022" whenever( repository.getStageLastDate(ENROLLMENT_ID), ) doReturn null diff --git a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModelTest.kt index 143ca66547..73e510c82b 100644 --- a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModelTest.kt @@ -638,8 +638,8 @@ class SearchTEIViewModelTest { val expectedMap = mapOf( "uid1" to "Friendly OrgUnit Name", "uid2" to "Male", - "uid3" to "21/2/2024", - "uid4" to "21/2/2024 01:00", + "uid3" to "21/02/2024", + "uid4" to "21/02/2024 - 01:00", "uid5" to "Boolean: false", "uid6" to "Yes Only", "uid7" to "Text value", diff --git a/commons/src/main/java/org/dhis2/commons/bindings/ValueExtensions.kt b/commons/src/main/java/org/dhis2/commons/bindings/ValueExtensions.kt index 85c0602f01..48586b7653 100644 --- a/commons/src/main/java/org/dhis2/commons/bindings/ValueExtensions.kt +++ b/commons/src/main/java/org/dhis2/commons/bindings/ValueExtensions.kt @@ -1,6 +1,7 @@ package org.dhis2.bindings import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.extensions.toPercentage import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue @@ -71,13 +72,13 @@ fun checkValueTypeValue(d2: D2, valueType: ValueType?, value: String): String { ValueType.IMAGE, ValueType.FILE_RESOURCE -> d2.fileResourceModule().fileResources().uid(value).blockingGet()?.path() ?: "" - ValueType.DATE -> + ValueType.DATE, ValueType.AGE -> DateUtils.uiDateFormat().format( DateUtils.oldUiDateFormat().parse(value) ?: "", ) ValueType.DATETIME -> - DateUtils.dateTimeFormat().format( + DateUtils.uiDateTimeFormat().format( DateUtils.databaseDateFormatNoSeconds().parse(value) ?: "", ) @@ -86,6 +87,8 @@ fun checkValueTypeValue(d2: D2, valueType: ValueType?, value: String): String { DateUtils.timeFormat().parse(value) ?: "", ) + ValueType.PERCENTAGE -> value.toPercentage() + else -> value } } diff --git a/commons/src/main/java/org/dhis2/commons/data/SearchTeiModel.java b/commons/src/main/java/org/dhis2/commons/data/SearchTeiModel.java index d3c9597a70..ba8d33d93c 100644 --- a/commons/src/main/java/org/dhis2/commons/data/SearchTeiModel.java +++ b/commons/src/main/java/org/dhis2/commons/data/SearchTeiModel.java @@ -1,7 +1,5 @@ package org.dhis2.commons.data; -import androidx.compose.ui.graphics.Color; - import org.dhis2.commons.data.tuples.Trio; import org.dhis2.ui.MetadataIconData; import org.hisp.dhis.android.core.enrollment.Enrollment; @@ -9,7 +7,6 @@ import org.hisp.dhis.android.core.program.Program; import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue; import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance; -import org.hisp.dhis.mobile.ui.designsystem.component.internal.ImageCardData; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; 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 17b532a446..e8e0275fed 100644 --- a/commons/src/main/java/org/dhis2/commons/date/DateExtensions.kt +++ b/commons/src/main/java/org/dhis2/commons/date/DateExtensions.kt @@ -155,4 +155,4 @@ private fun getString( } fun Date?.toUi(): String? = - this?.let { SimpleDateFormat("d/M/yyyy", Locale.getDefault()).format(this) } + this?.let { DateUtils.uiDateFormat().format(this) } diff --git a/commons/src/main/java/org/dhis2/commons/date/DateUtils.java b/commons/src/main/java/org/dhis2/commons/date/DateUtils.java index 542380e0ed..362c9b89eb 100644 --- a/commons/src/main/java/org/dhis2/commons/date/DateUtils.java +++ b/commons/src/main/java/org/dhis2/commons/date/DateUtils.java @@ -43,10 +43,10 @@ public static DateUtils getInstance() { public static final String WEEKLY_FORMAT_EXPRESSION = "w yyyy"; public static final String MONTHLY_FORMAT_EXPRESSION = "MMM yyyy"; public static final String YEARLY_FORMAT_EXPRESSION = "yyyy"; - public static final String SIMPLE_DATE_FORMAT = "d/M/yyyy"; + public static final String SIMPLE_DATE_FORMAT = "dd/MM/yyyy"; public static final String TIME_12H_EXPRESSION = "hh:mm a"; public static final String UI_LIBRARY_FORMAT = "ddMMyyyy"; - public static final String SIMPLE_DATE_TIME_FORMAT = "d/M/yyyy HH:mm"; + public static final String SIMPLE_DATE_TIME_FORMAT = "dd/MM/yyyy - HH:mm"; public Date[] getDateFromDateAndPeriod(Date date, Period period) { switch (period) { diff --git a/commons/src/main/java/org/dhis2/commons/extensions/StringExtensions.kt b/commons/src/main/java/org/dhis2/commons/extensions/StringExtensions.kt index fcf9df3202..f14ca437ea 100644 --- a/commons/src/main/java/org/dhis2/commons/extensions/StringExtensions.kt +++ b/commons/src/main/java/org/dhis2/commons/extensions/StringExtensions.kt @@ -2,27 +2,29 @@ package org.dhis2.commons.extensions import org.dhis2.commons.date.DateUtils import timber.log.Timber +import java.text.ParseException import java.util.Date fun String.toDate(): Date? { + val wrongFormat = "wrong format" var date: Date? = null try { date = DateUtils.databaseDateFormat().parse(this) } catch (e: Exception) { - Timber.d("wrong format") + Timber.d(wrongFormat) } if (date == null) { try { date = DateUtils.dateTimeFormat().parse(this) } catch (e: Exception) { - Timber.d("wrong format") + Timber.d(wrongFormat) } } if (date == null) { try { date = DateUtils.uiDateFormat().parse(this) } catch (e: Exception) { - Timber.d("wrong format") + Timber.d(wrongFormat) } } @@ -30,9 +32,47 @@ fun String.toDate(): Date? { try { date = DateUtils.oldUiDateFormat().parse(this) } catch (e: Exception) { - Timber.d("wrong format") + Timber.d(wrongFormat) } } return date } + +fun String.toPercentage(): String { + return "$this%" +} + +fun String.toFriendlyDate(): String { + return if (this.isNotEmpty()) { + var formattedDate = this + val date = try { + DateUtils.oldUiDateFormat().parse(this) + } catch (e: ParseException) { + null + } + date?.let { + formattedDate = DateUtils.uiDateFormat().format(date) + } + formattedDate + } else { + this + } +} + +fun String.toFriendlyDateTime(): String { + return if (this.isNotEmpty()) { + var formattedDate = this + val date = try { + DateUtils.databaseDateFormatNoSeconds().parse(this) + } catch (e: ParseException) { + null + } + date?.let { + formattedDate = DateUtils.uiDateTimeFormat().format(date) + } + formattedDate + } else { + this + } +} diff --git a/commons/src/main/java/org/dhis2/commons/resources/DhisPeriodUtils.kt b/commons/src/main/java/org/dhis2/commons/resources/DhisPeriodUtils.kt index 153a851d34..a9e53c2be2 100644 --- a/commons/src/main/java/org/dhis2/commons/resources/DhisPeriodUtils.kt +++ b/commons/src/main/java/org/dhis2/commons/resources/DhisPeriodUtils.kt @@ -11,7 +11,7 @@ import java.util.regex.Pattern const val DATE_FORMAT_EXPRESSION = "yyyy-MM-dd" const val MONTHLY_FORMAT_EXPRESSION = "MMM yyyy" const val YEARLY_FORMAT_EXPRESSION = "yyyy" -const val SIMPLE_DATE_FORMAT = "d/M/yyyy" +const val SIMPLE_DATE_FORMAT = "dd/MM/yyyy" class DhisPeriodUtils( d2: D2,