From 7c32c477dab65b84bb3c01b84b4673bd204ff26d Mon Sep 17 00:00:00 2001 From: Xavier Molloy <xavi@dhis2.org> Date: Wed, 20 Nov 2024 16:17:21 +0100 Subject: [PATCH 01/11] ci: update vName --- 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 5c12127119..2908f4b037 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ sdk = "34" minSdk = "21" vCode = "137" -vName = "3.1.0" +vName = "3.1.0.1" gradle = "8.6.1" kotlin = '2.0.20' hilt = '2.47' From abc926588898d11c2c17b48e7ad5416c3bd173d9 Mon Sep 17 00:00:00 2001 From: Xavier Molloy <xavi@dhis2.org> Date: Tue, 26 Nov 2024 15:27:58 +0100 Subject: [PATCH 02/11] fix: [ANDROAPP-6665] Clear filters when navigating back (#3895) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: [ANDROAPP-6665] Clear filters when navigating back * fix: [ANDROAPP-6665] clear filters on home Signed-off-by: Manu Muñoz <manu.munoz@dhis2.org> * fix: [ANDROAPP-6665] test Signed-off-by: Manu Muñoz <manu.munoz@dhis2.org> --------- Signed-off-by: Manu Muñoz <manu.munoz@dhis2.org> Co-authored-by: Manu Muñoz <manu.munoz@dhis2.org> --- .../main/java/org/dhis2/usescases/main/MainActivity.kt | 1 - .../main/java/org/dhis2/usescases/main/MainModule.kt | 5 ++--- .../java/org/dhis2/usescases/main/MainPresenter.kt | 10 +++------- .../java/org/dhis2/usescases/main/MainPresenterTest.kt | 8 +++++--- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt b/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt index c7a4d6679d..4ce0756b92 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt @@ -219,7 +219,6 @@ class MainActivity : } override fun onPause() { - presenter.setOpeningFilterToNone() presenter.onDetach() super.onPause() } diff --git a/app/src/main/java/org/dhis2/usescases/main/MainModule.kt b/app/src/main/java/org/dhis2/usescases/main/MainModule.kt index 95077a043c..62014e272b 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainModule.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainModule.kt @@ -6,7 +6,6 @@ import dhis2.org.analytics.charts.Charts import org.dhis2.commons.di.dagger.PerActivity import org.dhis2.commons.featureconfig.data.FeatureConfigRepository import org.dhis2.commons.filters.FilterManager -import org.dhis2.commons.filters.data.FilterRepository import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.resources.ColorUtils @@ -32,7 +31,7 @@ class MainModule(val view: MainView, private val forceToNotSynced: Boolean) { schedulerProvider: SchedulerProvider, preferences: PreferenceProvider, workManagerController: WorkManagerController, - filterRepository: FilterRepository, + filterManager: FilterManager, matomoAnalyticsController: MatomoAnalyticsController, userManager: UserManager, deleteUserData: DeleteUserData, @@ -47,7 +46,7 @@ class MainModule(val view: MainView, private val forceToNotSynced: Boolean) { schedulerProvider, preferences, workManagerController, - filterRepository, + filterManager, matomoAnalyticsController, userManager, deleteUserData, diff --git a/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt b/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt index fe2bb73058..bc88aa6a72 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt @@ -15,7 +15,6 @@ import kotlinx.coroutines.launch import org.dhis2.BuildConfig import org.dhis2.commons.Constants import org.dhis2.commons.filters.FilterManager -import org.dhis2.commons.filters.data.FilterRepository import org.dhis2.commons.matomo.Actions.Companion.BLOCK_SESSION_PIN import org.dhis2.commons.matomo.Actions.Companion.OPEN_ANALYTICS import org.dhis2.commons.matomo.Actions.Companion.QR_SCANNER @@ -57,7 +56,7 @@ class MainPresenter( private val schedulerProvider: SchedulerProvider, private val preferences: PreferenceProvider, private val workManagerController: WorkManagerController, - private val filterRepository: FilterRepository, + private val filterManager: FilterManager, private val matomoAnalyticsController: MatomoAnalyticsController, private val userManager: UserManager, private val deleteUserData: DeleteUserData, @@ -78,6 +77,7 @@ class MainPresenter( val downloadingVersion = MutableLiveData(false) fun init() { + filterManager.clearAllFilters() preferences.removeValue(Preference.CURRENT_ORG_UNIT) disposable.add( repository.user() @@ -158,7 +158,7 @@ class MainPresenter( Completable.fromCallable { workManagerController.cancelAllWork() syncStatusController.restore() - FilterManager.getInstance().clearAllFilters() + filterManager.clearAllFilters() preferences.setValue(Preference.SESSION_LOCKED, false) preferences.setValue(Preference.PIN_ENABLED, false) userManager.d2.dataStoreModule().localDataStore().value(PIN).blockingDeleteIfExist() @@ -225,10 +225,6 @@ class MainPresenter( matomoAnalyticsController.trackEvent(HOME, SETTINGS, CLICK) } - fun setOpeningFilterToNone() { - filterRepository.collapseAllFilters() - } - fun isPinStored() = repository.isPinStored() fun launchInitialDataSync() { diff --git a/app/src/test/java/org/dhis2/usescases/main/MainPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/main/MainPresenterTest.kt index ee6e872bbf..83863fac1c 100644 --- a/app/src/test/java/org/dhis2/usescases/main/MainPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/main/MainPresenterTest.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.setMain -import org.dhis2.commons.filters.data.FilterRepository +import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.matomo.Categories.Companion.HOME import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.prefs.Preference.Companion.DEFAULT_CAT_COMBO @@ -52,7 +52,7 @@ class MainPresenterTest { private val view: MainView = mock() private val preferences: PreferenceProvider = mock() private val workManagerController: WorkManagerController = mock() - private val filterRepository: FilterRepository = mock() + private val filterManager: FilterManager = mock() private val matomoAnalyticsController: MatomoAnalyticsController = mock() private val userManager: UserManager = mock() private val deleteUserData: DeleteUserData = mock() @@ -82,7 +82,7 @@ class MainPresenterTest { schedulers, preferences, workManagerController, - filterRepository, + filterManager, matomoAnalyticsController, userManager, deleteUserData, @@ -103,6 +103,7 @@ class MainPresenterTest { verify(view).renderUsername(any()) verify(preferences).setValue(DEFAULT_CAT_COMBO, "uid") verify(preferences).setValue(PREF_DEFAULT_CAT_OPTION_COMBO, "uid") + verify(filterManager).clearAllFilters() } @Test @@ -120,6 +121,7 @@ class MainPresenterTest { verify(workManagerController).cancelAllWork() verify(preferences).setValue(SESSION_LOCKED, false) verify(userManager.d2.dataStoreModule().localDataStore().value(PIN)).blockingDeleteIfExist() + verify(filterManager).clearAllFilters() verify(view).goToLogin(1, false) } From a837538f14edf1c5646246cec2039ed2846b6ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Miguel=20Rubio?= <andres@dhis2.org> Date: Wed, 27 Nov 2024 10:01:14 +0100 Subject: [PATCH 03/11] fix: [ANDROAPP-6691] Null pointer on show dialog (#3904) Signed-off-by: andresmr <andres@dhis2.org> --- .../usescases/datasets/dataSetTable/DataSetTableActivity.kt | 3 +-- .../programEventDetail/ProgramEventDetailActivity.kt | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableActivity.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableActivity.kt index b16f784076..07870ed8a1 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableActivity.kt @@ -184,9 +184,8 @@ class DataSetTableActivity : ActivityGlobalAbstract(), DataSetTableContract.View } }) .onNoConnectionListener { - val contextView = findViewById<View>(R.id.navigationBar) Snackbar.make( - contextView, + binding.root, R.string.sync_offline_check_connection, Snackbar.LENGTH_SHORT, ).show() 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 642fbe283d..3fac0ea471 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt @@ -273,9 +273,8 @@ class ProgramEventDetailActivity : } }) .onNoConnectionListener { - val contextView = findViewById<View>(R.id.navigationBar) Snackbar.make( - contextView, + binding.root, R.string.sync_offline_check_connection, Snackbar.LENGTH_SHORT, ).show() @@ -524,9 +523,8 @@ class ProgramEventDetailActivity : } }) .onNoConnectionListener { - val contextView = findViewById<View>(R.id.rootView) Snackbar.make( - contextView, + binding.root, R.string.sync_offline_check_connection, Snackbar.LENGTH_SHORT, ).show() From b16db7afa5e1df2a60619d57ffe1a7ce303c3c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tar=C3=AD?= <daniel@dhis2.org> Date: Fri, 29 Nov 2024 13:20:33 +0100 Subject: [PATCH 04/11] update SDK version for 3.1.0.1 hotfix (#3911) --- 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 2908f4b037..fe0f21ba6b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ kotlin = '2.0.20' hilt = '2.47' jacoco = '0.8.10' designSystem = "0.4.0" -dhis2sdk = "1.11.0" +dhis2sdk = "1.11.0.1-SNAPSHOT" ruleEngine = "3.0.0" expressionParser = "1.1.0" appcompat = "1.6.1" From 964ef103aeb6e5d520556471f061bd693c39fb3d Mon Sep 17 00:00:00 2001 From: Pablo <pablo@dhis2.org> Date: Fri, 29 Nov 2024 14:07:24 +0100 Subject: [PATCH 05/11] hotfix: Handle large option sets (#3901) * fix: [ANDROAPP-6653] Large option sets freeze app * update design system version * fix period selector * fix unit tests * remove commented code * fix test and remove those deprecated * fix sonarcloud warnings * remove commented code * fix: option set not searching * fix integration test * fix code smell * fix code smell * fix code smell --- .../tablefields/spinner/SpinnerViewModel.java | 16 - .../usescases/enrollment/EnrollmentModule.kt | 5 +- .../providers/InputFieldsProvider.kt | 41 ++- .../EventInitialRepositoryImpl.java | 24 +- .../SearchRepositoryImplKt.kt | 97 +++--- .../teiDashboard/TeiDashboardModule.kt | 3 +- .../dialogs/scheduling/SchedulingDialogUi.kt | 27 +- .../utils/optionset/OptionSetPresenter.kt | 23 -- .../form/data/DataEntryBaseRepository.kt | 70 +++-- .../dhis2/form/data/DataEntryRepository.kt | 11 + .../dhis2/form/data/EnrollmentRepository.kt | 43 ++- .../org/dhis2/form/data/EventRepository.kt | 50 ++-- .../org/dhis2/form/data/FormRepository.kt | 1 + .../org/dhis2/form/data/FormRepositoryImpl.kt | 151 +++++++++- .../org/dhis2/form/data/GeometryController.kt | 1 - .../form/data/RuleUtilsProviderResult.kt | 3 + .../data/metadata/EnrollmentConfiguration.kt | 30 +- .../data/metadata/FormBaseConfiguration.kt | 36 +++ .../main/java/org/dhis2/form/di/Injector.kt | 2 +- .../java/org/dhis2/form/model/ActionType.kt | 1 + .../org/dhis2/form/model/FieldUiModelImpl.kt | 8 +- .../form/model/OptionSetConfiguration.kt | 100 ++----- .../form/model/OptionSetDialogViewModel.kt | 4 +- .../java/org/dhis2/form/model/UiEventType.kt | 12 - .../main/java/org/dhis2/form/ui/FormView.kt | 282 +----------------- .../java/org/dhis2/form/ui/FormViewModel.kt | 17 ++ .../org/dhis2/form/ui/event/DialogDelegate.kt | 92 ------ .../form/ui/event/RecyclerViewUiEvents.kt | 42 --- .../dhis2/form/ui/event/UiEventFactoryImpl.kt | 103 ------- .../org/dhis2/form/ui/intent/FormIntent.kt | 6 + .../inputfield/CategorySelectorProvider.kt | 32 +- .../provider/inputfield/CheckBoxProvider.kt | 31 +- .../provider/inputfield/DropdownProvider.kt | 38 ++- .../ui/provider/inputfield/FieldProvider.kt | 11 + .../inputfield/MatrixInputProvider.kt | 20 +- .../inputfield/MatrixSequentialUtilites.kt | 2 +- .../inputfield/MultiSelectionInputProvider.kt | 31 +- .../inputfield/PeriodSelectorProvider.kt | 15 +- .../inputfield/RadioButtonProvider.kt | 31 +- .../inputfield/SequentialInputProvider.kt | 20 +- .../form/data/EnrollmentRepositoryTest.kt | 3 + .../org/dhis2/form/data/FieldUiModelTest.kt | 92 ------ .../data/FormRepositoryIntegrationTest.kt | 3 + .../dhis2/form/data/GeometryControllerTest.kt | 35 --- .../form/integration/ProgramRulesTest.kt | 94 +++--- .../model/OptionSetDialogViewModelTest.kt | 61 ---- .../dhis2/form/ui/DataEntryIntegrationTest.kt | 58 ++-- .../form/ui/event/UiEventFactoryImplTest.kt | 74 ----- gradle/libs.versions.toml | 2 +- 49 files changed, 743 insertions(+), 1211 deletions(-) delete mode 100644 form/src/main/java/org/dhis2/form/ui/event/DialogDelegate.kt delete mode 100644 form/src/test/java/org/dhis2/form/data/FieldUiModelTest.kt delete mode 100644 form/src/test/java/org/dhis2/form/ui/event/UiEventFactoryImplTest.kt diff --git a/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/spinner/SpinnerViewModel.java b/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/spinner/SpinnerViewModel.java index f44f0b2198..6c558e725f 100644 --- a/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/spinner/SpinnerViewModel.java +++ b/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/spinner/SpinnerViewModel.java @@ -12,9 +12,6 @@ @AutoValue public abstract class SpinnerViewModel extends FieldViewModel { - private List<String> optionsToHide; - private List<String> optionGroupsToHide; - @NonNull public abstract String hint(); @@ -52,17 +49,4 @@ public FieldViewModel withWarning(@NonNull String warning) { public FieldViewModel withValue(String data) { return new AutoValue_SpinnerViewModel(uid(),label(),mandatory(),data,programStageSection(),allowFutureDate(),editable(),warning(),error(),description(),dataElement(), options(), optionsList(), storeBy(), row(), column(),categoryOptionCombo(), catCombo(),hint(),optionSet()); } - - public void setOptionsToHide(List<String> optionsToHide, List<String> optionsGroupsToHide) { - this.optionsToHide = optionsToHide; - this.optionGroupsToHide = optionsGroupsToHide; - } - - public List<String> getOptionsToHide() { - return optionsToHide; - } - - public List<String> getOptionGroupsToHide() { - return optionGroupsToHide; - } } diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentModule.kt b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentModule.kt index 16790d2b35..ca121488f0 100644 --- a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentModule.kt +++ b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentModule.kt @@ -84,8 +84,7 @@ class EnrollmentModule( @PerActivity fun provideEnrollmentConfiguration( d2: D2, - metadataIconProvider: MetadataIconProvider, - ) = EnrollmentConfiguration(d2, enrollmentUid, metadataIconProvider) + ) = EnrollmentConfiguration(d2, enrollmentUid) @Provides @PerActivity @@ -93,12 +92,14 @@ class EnrollmentModule( modelFactory: FieldViewModelFactory, enrollmentFormLabelsProvider: EnrollmentFormLabelsProvider, enrollmentConfiguration: EnrollmentConfiguration, + metadataIconProvider: MetadataIconProvider, ): EnrollmentRepository { return EnrollmentRepository( fieldFactory = modelFactory, conf = enrollmentConfiguration, enrollmentMode = EnrollmentMode.valueOf(enrollmentMode.name), enrollmentFormLabelsProvider = enrollmentFormLabelsProvider, + metadataIconProvider = metadataIconProvider, ) } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt index 73acff526a..51e2978dc9 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt @@ -53,10 +53,16 @@ fun ProvideInputDate( ) { if (uiModel.showField) { Spacer(modifier = Modifier.height(16.dp)) - val textSelection = TextRange(if (uiModel.eventDate.dateValue != null) uiModel.eventDate.dateValue.length else 0) + val textSelection = + TextRange(if (uiModel.eventDate.dateValue != null) uiModel.eventDate.dateValue.length else 0) var value by remember(uiModel.eventDate.dateValue) { if (uiModel.eventDate.dateValue != null) { - mutableStateOf(TextFieldValue(formatStoredDateToUI(uiModel.eventDate.dateValue) ?: "", textSelection)) + mutableStateOf( + TextFieldValue( + formatStoredDateToUI(uiModel.eventDate.dateValue) ?: "", + textSelection, + ), + ) } else { mutableStateOf(TextFieldValue()) } @@ -66,7 +72,10 @@ fun ProvideInputDate( mutableStateOf(getInputState(uiModel.detailsEnabled)) } val yearRange = if (uiModel.selectableDates != null) { - IntRange(uiModel.selectableDates.initialDate.substring(4, 8).toInt(), uiModel.selectableDates.endDate.substring(4, 8).toInt()) + IntRange( + uiModel.selectableDates.initialDate.substring(4, 8).toInt(), + uiModel.selectableDates.endDate.substring(4, 8).toInt(), + ) } else { IntRange(1924, 2124) } @@ -90,7 +99,10 @@ fun ProvideInputDate( } }, is24hourFormat = uiModel.is24HourFormat, - selectableDates = uiModel.selectableDates ?: SelectableDates("01011924", "12312124"), + selectableDates = uiModel.selectableDates ?: SelectableDates( + "01011924", + "12312124", + ), yearRange = yearRange, ), modifier = modifier.testTag(INPUT_EVENT_INITIAL_DATE), @@ -246,15 +258,23 @@ fun ProvideCategorySelector( selectedItem = null eventCatComboUiModel.onClearCatCombo(eventCatComboUiModel.category) }, - onItemSelected = { newSelectedDropdownItem -> + onItemSelected = { _, newSelectedDropdownItem -> selectedItem = newSelectedDropdownItem.label eventCatComboUiModel.onOptionSelected(selectableOptions.firstOrNull { it.displayName() == newSelectedDropdownItem.label }) }, - dropdownItems = dropdownItems, + fetchItem = { index -> dropdownItems[index] }, + itemCount = dropdownItems.size, + onSearchOption = { /*no-op*/ }, + loadOptions = { /*no-op*/ }, + useDropDown = dropdownItems.size < 15, isRequiredField = eventCatComboUiModel.required, ) } else { - ProvideEmptyCategorySelector(modifier = modifier, name = eventCatComboUiModel.category.name, option = eventCatComboUiModel.noOptionsText) + ProvideEmptyCategorySelector( + modifier = modifier, + name = eventCatComboUiModel.category.name, + option = eventCatComboUiModel.noOptionsText, + ) } } @@ -316,10 +336,13 @@ fun ProvideEmptyCategorySelector( onResetButtonClicked = { selectedItem = "" }, - onItemSelected = { newSelectedDropdownItem -> + onItemSelected = { _, newSelectedDropdownItem -> selectedItem = newSelectedDropdownItem.label }, - dropdownItems = listOf(DropdownItem(option)), + fetchItem = { DropdownItem(option) }, + itemCount = 1, + onSearchOption = { /*no-op*/ }, + loadOptions = { /*no-op*/ }, isRequiredField = false, ) } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepositoryImpl.java index ab345fd56a..141d1af9ce 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepositoryImpl.java @@ -45,6 +45,8 @@ import io.reactivex.Flowable; import io.reactivex.Observable; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; import timber.log.Timber; public class EventInitialRepositoryImpl implements EventInitialRepository { @@ -328,22 +330,12 @@ private FieldUiModel transform(@NonNull ProgramStageDataElement stage, DataEleme if (!dataValueOptions.isEmpty()) { dataValue = option.get(0).displayName(); } - optionSetConfig = OptionSetConfiguration.Companion.config( - d2.optionModule().options().byOptionSetUid().eq(optionSet).blockingCount(), - () -> { - List<Option> options = d2.optionModule().options().byOptionSetUid().eq(optionSet).blockingGet(); - HashMap<String, MetadataIconData> metadataIconMap = new HashMap<>(); - for (Option optionItem : options) { - metadataIconMap.put( - optionItem.uid(), - metadataIconProvider.invoke(optionItem.style())); - } - - return new OptionSetConfiguration.OptionConfigData( - options, - metadataIconMap - ); - } + optionSetConfig = new OptionSetConfiguration( + null, + query -> null, + OptionSetConfiguration.Companion.optionDataFlow( + d2.optionModule().options().byOptionSetUid().eq(optionSet).getPagingData(10), + option1 -> metadataIconProvider.invoke(option1.style())) ); } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt index 6abcc4278d..69c37ee07a 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt @@ -1,7 +1,16 @@ +@file:OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) + package org.dhis2.usescases.searchTrackEntity import androidx.paging.PagingData +import androidx.paging.map +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.resources.MetadataIconProvider @@ -309,32 +318,29 @@ class SearchRepositoryImplKt( d2.trackedEntityModule().trackedEntityAttributes() .uid(programAttribute.trackedEntityAttribute()!!.uid()) .blockingGet()?.let { attribute -> - + val searchFlow = MutableStateFlow("") val optionSetConfiguration = attribute.optionSet()?.let { - OptionSetConfiguration.config( - d2.optionModule().options() - .byOptionSetUid().eq(attribute.optionSet()!!.uid()) - .blockingCount(), - ) { - val options = d2.optionModule().options() - .byOptionSetUid().eq(attribute.optionSet()!!.uid()) - .orderBySortOrder(RepositoryScope.OrderByDirection.ASC) - .blockingGet() - - val metadataIconMap = - options.associate { - it.uid() to metadataIconProvider( - it.style(), - program?.style()?.color()?.toColor() - ?: SurfaceColor.Primary, - ) - } - - OptionSetConfiguration.OptionConfigData( - options = options, - metadataIconMap = metadataIconMap, - ) - } + OptionSetConfiguration( + searchEmitter = searchFlow, + optionFlow = searchFlow.debounce(300).flatMapLatest { + d2.optionModule().options() + .byOptionSetUid().eq(attribute.optionSet()!!.uid()) + .getPagingData(10) + .map { pagingData -> + pagingData.map { option -> + OptionSetConfiguration.OptionData( + option, + metadataIconProvider( + option.style(), + program?.style()?.color()?.toColor() + ?: SurfaceColor.Primary, + ), + ) + } + } + }, + onSearch = { searchFlow.value = it }, + ) } createField( trackedEntityAttribute = attribute, @@ -359,31 +365,26 @@ class SearchRepositoryImplKt( d2.trackedEntityModule().trackedEntityAttributes() .uid(typeAttribute.trackedEntityAttribute()!!.uid()) .blockingGet()?.let { attribute -> - + val searchEmitter = MutableStateFlow("") val optionSetConfiguration = attribute.optionSet()?.let { - OptionSetConfiguration.config( - d2.optionModule().options() + OptionSetConfiguration( + searchEmitter = searchEmitter, + optionFlow = d2.optionModule().options() .byOptionSetUid().eq(attribute.optionSet()!!.uid()) - .blockingCount(), - ) { - val options = d2.optionModule().options() - .byOptionSetUid().eq(attribute.optionSet()!!.uid()) - .orderBySortOrder(RepositoryScope.OrderByDirection.ASC) - .blockingGet() - - val metadataIconMap = - options.associate { - it.uid() to metadataIconProvider( - it.style(), - SurfaceColor.Primary, - ) - } - - OptionSetConfiguration.OptionConfigData( - options = options, - metadataIconMap = metadataIconMap, - ) - } + .getPagingData(10) + .map { pagingData -> + pagingData.map { option -> + OptionSetConfiguration.OptionData( + option, + metadataIconProvider( + option.style(), + SurfaceColor.Primary, + ), + ) + } + }, + onSearch = { searchEmitter.value = it }, + ) } createField( diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt index 6e9afede90..c8f1ba6401 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt @@ -59,8 +59,7 @@ class TeiDashboardModule( @PerActivity fun provideEnrollmentConfiguration( d2: D2, - metadataIconProvider: MetadataIconProvider, - ) = enrollmentUid?.let { EnrollmentConfiguration(d2, it, metadataIconProvider) } + ) = enrollmentUid?.let { EnrollmentConfiguration(d2, it) } @Provides @PerActivity 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 374c5d1ec7..fa5e8cca27 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 @@ -161,7 +161,8 @@ private fun ButtonBlock( Column( verticalArrangement = Arrangement.spacedBy(8.dp), ) { - val eventLabel = selectedProgramStage?.displayEventLabel() ?: stringResource(R.string.event) + val eventLabel = + selectedProgramStage?.displayEventLabel() ?: stringResource(R.string.event) Button( modifier = Modifier.fillMaxWidth(), style = ButtonStyle.FILLED, @@ -233,15 +234,31 @@ fun ProvideScheduleNewEventForm( launchMode: LaunchMode, ) { if (programStages.size > 1 && launchMode !is LaunchMode.EnterEvent) { + var dropdownItems by remember { + mutableStateOf( + programStages.map { DropdownItem(it.displayName().orEmpty()) }, + ) + } InputDropDown( title = stringResource(id = R.string.program_stage), state = InputShellState.UNFOCUSED, - dropdownItems = programStages.map { DropdownItem(it.displayName().orEmpty()) }, + fetchItem = { index -> dropdownItems[index] }, + itemCount = dropdownItems.size, + onSearchOption = { query -> + dropdownItems = if (query.isNotEmpty()) { + dropdownItems.filter { it.label.contains(query) } + } else { + programStages.map { DropdownItem(it.displayName().orEmpty()) } + } + }, + useDropDown = dropdownItems.size < 15, + loadOptions = { + /*no-op*/ + }, selectedItem = DropdownItem(selectedProgramStage?.displayName().orEmpty()), onResetButtonClicked = {}, - onItemSelected = { item -> - programStages.find { it.displayName() == item.label } - ?.let { viewModel.updateStage(it) } + onItemSelected = { index, _ -> + programStages[index].let { viewModel.updateStage(it) } }, ) } diff --git a/app/src/main/java/org/dhis2/utils/optionset/OptionSetPresenter.kt b/app/src/main/java/org/dhis2/utils/optionset/OptionSetPresenter.kt index 3bcaf6253c..64f166260b 100644 --- a/app/src/main/java/org/dhis2/utils/optionset/OptionSetPresenter.kt +++ b/app/src/main/java/org/dhis2/utils/optionset/OptionSetPresenter.kt @@ -16,28 +16,16 @@ class OptionSetPresenter( val d2: D2, val schedulerProvider: SchedulerProvider, ) { - - private lateinit var optionSetOptionHandler: OptionSetOptionsHandler var disposable: CompositeDisposable = CompositeDisposable() private var optionSetUid: String? = null fun init(optionSet: FieldUiModel) { this.optionSetUid = optionSet.optionSet - this.optionSetOptionHandler = OptionSetOptionsHandler( - optionSet.optionSetConfiguration?.optionsToHide, - optionSet.optionSetConfiguration?.optionsToShow, - null, - ) getOptions() } fun init(optionSetTable: TableSpinnerViewModel) { this.optionSetUid = optionSetTable.optionSet() - this.optionSetOptionHandler = OptionSetOptionsHandler( - optionSetTable.optionsToHide, - null, - optionSetTable.optionGroupsToHide, - ) getOptions() } @@ -50,17 +38,6 @@ class OptionSetPresenter( var optionRepository = d2.optionModule().options() .byOptionSetUid().eq(optionSetUid) - val(finalOptionsToHide, finalOptionsToShow) = - optionSetOptionHandler.handleOptions() - - if (finalOptionsToShow.isNotEmpty()) { - optionRepository = optionRepository.byUid().`in`(finalOptionsToShow) - } - - if (finalOptionsToHide.isNotEmpty()) { - optionRepository = optionRepository.byUid().notIn(finalOptionsToHide) - } - if (textToSearch.isNotEmpty()) { optionRepository = optionRepository.byDisplayName().like("%$textToSearch%") } diff --git a/form/src/main/java/org/dhis2/form/data/DataEntryBaseRepository.kt b/form/src/main/java/org/dhis2/form/data/DataEntryBaseRepository.kt index 83295fea20..1e9d3d3a74 100644 --- a/form/src/main/java/org/dhis2/form/data/DataEntryBaseRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/DataEntryBaseRepository.kt @@ -1,18 +1,35 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + package org.dhis2.form.data +import androidx.compose.ui.graphics.Color +import androidx.paging.PagingData +import androidx.paging.map +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.form.data.metadata.FormBaseConfiguration import org.dhis2.form.model.FieldUiModel +import org.dhis2.form.model.OptionSetConfiguration import org.dhis2.form.model.SectionUiModelImpl import org.dhis2.form.ui.FieldViewModelFactory import org.hisp.dhis.android.core.imports.TrackerImportConflict import org.hisp.dhis.android.core.program.SectionRenderingType +import timber.log.Timber abstract class DataEntryBaseRepository( private val conf: FormBaseConfiguration, private val fieldFactory: FieldViewModelFactory, + private val metadataIconProvider: MetadataIconProvider, ) : DataEntryRepository { abstract val programUid: String? + abstract val defaultStyleColor: Color override fun firstSectionToOpen(): String? { return sectionUids().blockingFirst().firstOrNull() } @@ -41,26 +58,7 @@ abstract class DataEntryBaseRepository( optionGroupsToHide: List<String>, optionGroupsToShow: List<String>, ): FieldUiModel { - val optionsInGroupsToHide = optionsFromGroups(optionGroupsToHide) - val optionsInGroupsToShow = optionsFromGroups(optionGroupsToShow) - - val item = when { - fieldUiModel.optionSet != null -> { - fieldUiModel.apply { - this.optionSetConfiguration = - optionSetConfiguration?.updateOptionsToHideAndShow( - optionsToHide = listOf(optionsToHide, optionsInGroupsToHide).flatten(), - optionsToShow = optionsInGroupsToShow, - ) - } - } - - else -> { - fieldUiModel - } - } - - return warningMessage?.let { item.setError(it) } ?: item + return warningMessage?.let { fieldUiModel.setError(it) } ?: fieldUiModel } private fun optionsFromGroups(optionGroupUids: List<String>): List<String> { @@ -77,6 +75,38 @@ abstract class DataEntryBaseRepository( return optionsFromGroups } + override fun options( + optionSetUid: String, + optionsToHide: List<String>, + optionGroupsToHide: List<String>, + optionGroupsToShow: List<String>, + ): Pair<MutableStateFlow<String>, Flow<PagingData<OptionSetConfiguration.OptionData>>> { + val searchFlow = MutableStateFlow("") + return Pair( + searchFlow, + searchFlow.debounce(300) + .flatMapLatest { query -> + conf.options( + optionSetUid, + query, + optionsToHide, + optionGroupsToHide, + optionGroupsToShow, + ).map { pagingData -> + pagingData.map { option -> + OptionSetConfiguration.OptionData( + option, + metadataIconProvider(option.style(), defaultStyleColor), + ) + } + } + } + .catch { + Timber.e(it) + }, + ) + } + override fun dateFormatConfiguration(): String? { return conf.dateFormatConfiguration() } diff --git a/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt b/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt index 4d54b1e408..5572dfe725 100644 --- a/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt @@ -1,8 +1,12 @@ package org.dhis2.form.data +import androidx.paging.PagingData import io.reactivex.Flowable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import org.dhis2.form.model.EventMode import org.dhis2.form.model.FieldUiModel +import org.dhis2.form.model.OptionSetConfiguration import org.hisp.dhis.android.core.common.ValidationStrategy interface DataEntryRepository { @@ -37,4 +41,11 @@ interface DataEntryRepository { fun disableCollapsableSections(): Boolean? fun getSpecificDataEntryItems(uid: String): List<FieldUiModel> + + fun options( + optionSetUid: String, + optionsToHide: List<String>, + optionGroupsToHide: List<String>, + optionGroupsToShow: List<String>, + ): Pair<MutableStateFlow<String>, Flow<PagingData<OptionSetConfiguration.OptionData>>> } diff --git a/form/src/main/java/org/dhis2/form/data/EnrollmentRepository.kt b/form/src/main/java/org/dhis2/form/data/EnrollmentRepository.kt index 8e37fe7ed1..1cad5159f9 100644 --- a/form/src/main/java/org/dhis2/form/data/EnrollmentRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/EnrollmentRepository.kt @@ -4,6 +4,7 @@ import io.reactivex.Flowable import io.reactivex.Single import org.dhis2.commons.date.DateUtils import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope +import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.form.data.metadata.EnrollmentConfiguration import org.dhis2.form.model.EnrollmentMode import org.dhis2.form.model.EventMode @@ -14,6 +15,7 @@ import org.dhis2.form.ui.FieldViewModelFactory import org.dhis2.form.ui.provider.EnrollmentFormLabelsProvider import org.dhis2.form.ui.provider.inputfield.DEFAULT_MAX_DATE import org.dhis2.form.ui.provider.inputfield.DEFAULT_MIN_DATE +import org.dhis2.ui.toColor import org.hisp.dhis.android.core.arch.helpers.UidsHelper.getUidsList import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.common.ObjectStyle @@ -25,6 +27,7 @@ import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttribute import org.hisp.dhis.android.core.program.SectionRenderingType import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import timber.log.Timber import java.util.Date @@ -33,7 +36,8 @@ class EnrollmentRepository( private val conf: EnrollmentConfiguration, private val enrollmentMode: EnrollmentMode, private val enrollmentFormLabelsProvider: EnrollmentFormLabelsProvider, -) : DataEntryBaseRepository(conf, fieldFactory) { + metadataIconProvider: MetadataIconProvider, +) : DataEntryBaseRepository(conf, fieldFactory, metadataIconProvider) { override val programUid by lazy { conf.program()?.uid() @@ -43,17 +47,23 @@ class EnrollmentRepository( conf.sections() } + override val defaultStyleColor by lazy { + conf.program()?.style()?.color()?.toColor() ?: SurfaceColor.Primary + } + private fun canBeEdited(): Boolean { val selectedProgram = conf.program() val programAccess = selectedProgram?.access()?.data()?.write() == true val teTypeAccess = conf.trackedEntityType()?.access()?.data()?.write() == true return programAccess && teTypeAccess } + override fun getSpecificDataEntryItems(uid: String): List<FieldUiModel> { return when (uid) { ORG_UNIT_UID -> { getEnrollmentData() } + else -> { emptyList() } @@ -166,7 +176,17 @@ class EnrollmentRepository( var optionSetConfig: OptionSetConfiguration? = null if (!optionSet.isNullOrEmpty()) { - optionSetConfig = conf.optionSetConfig(optionSet) + val (searchEmitter, optionFlow) = options( + optionSetUid = optionSet, + optionsToHide = emptyList(), + optionGroupsToHide = emptyList(), + optionGroupsToShow = emptyList(), + ) + optionSetConfig = OptionSetConfiguration( + searchEmitter = searchEmitter, + optionFlow = optionFlow, + onSearch = { searchEmitter.value = it }, + ) } var (error, warning) = getConflictErrorsAndWarnings(attribute.uid(), dataValue) @@ -288,7 +308,12 @@ class EnrollmentRepository( private fun getEnrollmentData(): MutableList<FieldUiModel> { val enrollmentDataList = ArrayList<FieldUiModel>() - enrollmentDataList.add(getEnrollmentDataSection(conf.program()?.displayName(), conf.program()?.description())) + enrollmentDataList.add( + getEnrollmentDataSection( + conf.program()?.displayName(), + conf.program()?.description(), + ), + ) enrollmentDataList.add( getEnrollmentDateField( @@ -342,12 +367,14 @@ class EnrollmentRepository( (selectedOrgUnit?.closedDate() == null && java.lang.Boolean.FALSE == selectedProgram?.selectEnrollmentDatesInFuture()) -> { maxDate = Date(System.currentTimeMillis()) } + (selectedOrgUnit?.closedDate() != null && java.lang.Boolean.FALSE == selectedProgram?.selectEnrollmentDatesInFuture()) -> { - maxDate = if (selectedOrgUnit.closedDate()!!.before(Date(System.currentTimeMillis()))) { - selectedOrgUnit.closedDate() - } else { - Date(System.currentTimeMillis()) - } + maxDate = + if (selectedOrgUnit.closedDate()!!.before(Date(System.currentTimeMillis()))) { + selectedOrgUnit.closedDate() + } else { + Date(System.currentTimeMillis()) + } } (selectedOrgUnit?.closedDate() != null && java.lang.Boolean.TRUE == selectedProgram?.selectEnrollmentDatesInFuture()) -> { 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 05bee4fb1a..c684137015 100644 --- a/form/src/main/java/org/dhis2/form/data/EventRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/EventRepository.kt @@ -49,12 +49,12 @@ class EventRepository( private val fieldFactory: FieldViewModelFactory, private val eventUid: String, private val d2: D2, - private val metadataIconProvider: MetadataIconProvider, + metadataIconProvider: MetadataIconProvider, private val resources: ResourceManager, private val eventResourcesProvider: EventResourcesProvider, private val dateUtils: DateUtils, private val eventMode: EventMode, -) : DataEntryBaseRepository(FormBaseConfiguration(d2), fieldFactory) { +) : DataEntryBaseRepository(FormBaseConfiguration(d2), fieldFactory, metadataIconProvider) { private var event = d2.eventModule().events().uid(eventUid).blockingGet() @@ -65,7 +65,7 @@ class EventRepository( .blockingGet() } - private val defaultStyleColor by lazy { + override val defaultStyleColor by lazy { programStage?.program()?.uid()?.let { d2.program(it)?.style()?.color()?.toColor() } ?: SurfaceColor.Primary @@ -189,7 +189,8 @@ class EventRepository( } override fun validationStrategy(): ValidationStrategy? { - return d2.programModule().programStages().uid(programStage?.uid()).blockingGet()?.validationStrategy() + return d2.programModule().programStages().uid(programStage?.uid()).blockingGet() + ?.validationStrategy() } private fun getEventDetails(): MutableList<FieldUiModel> { @@ -440,7 +441,8 @@ class EventRepository( .withTrackedEntityType() .byUid().eq(programUid) .one().blockingGet()?.let { program -> - val firstAvailablePeriodDate = getFirstAvailablePeriod(event?.enrollment(), programStage) + val firstAvailablePeriodDate = + getFirstAvailablePeriod(event?.enrollment(), programStage) var minDate = dateUtils.expDate( firstAvailablePeriodDate, program.expiryDays() ?: 0, @@ -477,7 +479,11 @@ class EventRepository( } val calendar = DateUtils.getInstance().getCalendarByDate(minEventDate) - return dateUtils.getNextPeriod(programStage?.periodType(), calendar.time ?: event?.eventDate(), if (stageLastDate == null) 0 else 1) + return dateUtils.getNextPeriod( + programStage?.periodType(), + calendar.time ?: event?.eventDate(), + if (stageLastDate == null) 0 else 1, + ) } private fun getStageLastDate(): Date? { @@ -488,12 +494,14 @@ class EventRepository( .eq(enrollmentUid).byProgramStageUid() .eq(programStageUid) .byDeleted().isFalse - .orderByEventDate(RepositoryScope.OrderByDirection.DESC).blockingGet().filter { it.uid() != eventUid } + .orderByEventDate(RepositoryScope.OrderByDirection.DESC).blockingGet() + .filter { it.uid() != eventUid } val scheduleEvents = d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid).byProgramStageUid() .eq(programStageUid) .byDeleted().isFalse - .orderByDueDate(RepositoryScope.OrderByDirection.DESC).blockingGet().filter { it.uid() != eventUid } + .orderByDueDate(RepositoryScope.OrderByDirection.DESC).blockingGet() + .filter { it.uid() != eventUid } var activeDate: Date? = null var scheduleDate: Date? = null @@ -632,21 +640,17 @@ class EventRepository( .byCode() .eq(dataValue).one().blockingGet()?.displayName() } - val optionCount = - d2.optionModule().options().byOptionSetUid().eq(optionSet) - .blockingCount() - optionSetConfig = OptionSetConfiguration.config(optionCount) { - val options = d2.optionModule().options().byOptionSetUid().eq(optionSet) - .orderBySortOrder(RepositoryScope.OrderByDirection.ASC).blockingGet() - - val metadataIconMap = - options.associate { it.uid() to metadataIconProvider(it.style(), defaultStyleColor) } - - OptionSetConfiguration.OptionConfigData( - options = options, - metadataIconMap = metadataIconMap, - ) - } + val (searchEmitter, optionFlow) = options( + optionSetUid = optionSet!!, + optionsToHide = emptyList(), + optionGroupsToHide = emptyList(), + optionGroupsToShow = emptyList(), + ) + optionSetConfig = OptionSetConfiguration( + searchEmitter = searchEmitter, + optionFlow = optionFlow, + onSearch = { searchEmitter.value = it }, + ) } val fieldRendering = getValueTypeDeviceRendering(programStageDataElement) val objectStyle = getObjectStyle(de) diff --git a/form/src/main/java/org/dhis2/form/data/FormRepository.kt b/form/src/main/java/org/dhis2/form/data/FormRepository.kt index 37203fc693..d3bef68f3d 100644 --- a/form/src/main/java/org/dhis2/form/data/FormRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/FormRepository.kt @@ -32,4 +32,5 @@ interface FormRepository { fun getListFromPreferences(uid: String): MutableList<String> fun saveListToPreferences(uid: String, list: List<String>) fun activateEvent() + fun fetchOptions(id: String, optionSetUid: String) } diff --git a/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt b/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt index 12bc763b88..ac791f613d 100644 --- a/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt +++ b/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt @@ -7,6 +7,7 @@ import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.form.data.EnrollmentRepository.Companion.ENROLLMENT_DATE_UID import org.dhis2.form.model.ActionType import org.dhis2.form.model.FieldUiModel +import org.dhis2.form.model.OptionSetConfiguration import org.dhis2.form.model.RowAction import org.dhis2.form.model.SectionUiModelImpl import org.dhis2.form.model.StoreResult @@ -234,48 +235,87 @@ class FormRepositoryImpl( (itemsWithErrors.isEmpty() && itemsWithWarning.isEmpty() && mandatoryItemsWithoutValue.isEmpty()) -> { getSuccessfulResult(eventStatus) } + (itemsWithErrors.isNotEmpty()) -> { - getFieldWithErrorResult(eventStatus, itemsWithErrors, itemsWithWarning, validationStrategy, backPressed) + getFieldWithErrorResult( + eventStatus, + itemsWithErrors, + itemsWithWarning, + validationStrategy, + backPressed, + ) } + (mandatoryItemsWithoutValue.isNotEmpty()) -> { - getMissingMandatoryResult(eventStatus, itemsWithErrors, itemsWithWarning, validationStrategy, backPressed) + getMissingMandatoryResult( + eventStatus, + itemsWithErrors, + itemsWithWarning, + validationStrategy, + backPressed, + ) } + else -> { getFieldWithWarningResult(eventStatus, itemsWithWarning, validationStrategy) } } } - private fun getFieldWithWarningResult(eventStatus: EventStatus?, itemsWithWarning: List<FieldWithIssue>, validationStrategy: ValidationStrategy?): FieldsWithWarningResult { + private fun getFieldWithWarningResult( + eventStatus: EventStatus?, + itemsWithWarning: List<FieldWithIssue>, + validationStrategy: ValidationStrategy?, + ): FieldsWithWarningResult { return when (eventStatus) { EventStatus.ACTIVE -> { FieldsWithWarningResult( fieldUidWarningList = itemsWithWarning, canComplete = ruleEffectsResult?.canComplete ?: true, onCompleteMessage = ruleEffectsResult?.messageOnComplete, - eventResultDetails = EventResultDetails(formValueStore.eventState(), dataEntryRepository.eventMode(), validationStrategy), + eventResultDetails = EventResultDetails( + formValueStore.eventState(), + dataEntryRepository.eventMode(), + validationStrategy, + ), ) } + EventStatus.COMPLETED -> { FieldsWithWarningResult( fieldUidWarningList = itemsWithWarning, canComplete = false, onCompleteMessage = ruleEffectsResult?.messageOnComplete, - eventResultDetails = EventResultDetails(formValueStore.eventState(), dataEntryRepository.eventMode(), null), + eventResultDetails = EventResultDetails( + formValueStore.eventState(), + dataEntryRepository.eventMode(), + null, + ), ) } + else -> { FieldsWithWarningResult( fieldUidWarningList = itemsWithWarning, canComplete = ruleEffectsResult?.canComplete ?: false, onCompleteMessage = ruleEffectsResult?.messageOnComplete, - eventResultDetails = EventResultDetails(formValueStore.eventState(), dataEntryRepository.eventMode(), validationStrategy), + eventResultDetails = EventResultDetails( + formValueStore.eventState(), + dataEntryRepository.eventMode(), + validationStrategy, + ), ) } } } - private fun getMissingMandatoryResult(eventStatus: EventStatus?, itemsWithErrors: List<FieldWithIssue>, itemsWithWarning: List<FieldWithIssue>, validationStrategy: ValidationStrategy?, backPressed: Boolean): DataIntegrityCheckResult { + private fun getMissingMandatoryResult( + eventStatus: EventStatus?, + itemsWithErrors: List<FieldWithIssue>, + itemsWithWarning: List<FieldWithIssue>, + validationStrategy: ValidationStrategy?, + backPressed: Boolean, + ): DataIntegrityCheckResult { return when (eventStatus) { EventStatus.ACTIVE -> { MissingMandatoryResult( @@ -285,10 +325,15 @@ class FormRepositoryImpl( canComplete = ruleEffectsResult?.canComplete ?: true, onCompleteMessage = ruleEffectsResult?.messageOnComplete, allowDiscard = backPressed, - eventResultDetails = EventResultDetails(formValueStore.eventState(), dataEntryRepository.eventMode(), validationStrategy), + eventResultDetails = EventResultDetails( + formValueStore.eventState(), + dataEntryRepository.eventMode(), + validationStrategy, + ), ) } + EventStatus.COMPLETED -> { MissingMandatoryResult( mandatoryFields = mandatoryItemsWithoutValue, @@ -297,9 +342,14 @@ class FormRepositoryImpl( canComplete = false, onCompleteMessage = ruleEffectsResult?.messageOnComplete, allowDiscard = backPressed, - eventResultDetails = EventResultDetails(formValueStore.eventState(), dataEntryRepository.eventMode(), null), + eventResultDetails = EventResultDetails( + formValueStore.eventState(), + dataEntryRepository.eventMode(), + null, + ), ) } + else -> { MissingMandatoryResult( mandatoryFields = mandatoryItemsWithoutValue, @@ -308,7 +358,11 @@ class FormRepositoryImpl( canComplete = ruleEffectsResult?.canComplete ?: false, onCompleteMessage = ruleEffectsResult?.messageOnComplete, allowDiscard = backPressed, - eventResultDetails = EventResultDetails(formValueStore.eventState(), dataEntryRepository.eventMode(), validationStrategy), + eventResultDetails = EventResultDetails( + formValueStore.eventState(), + dataEntryRepository.eventMode(), + validationStrategy, + ), ) } } @@ -330,9 +384,14 @@ class FormRepositoryImpl( canComplete = ruleEffectsResult?.canComplete ?: true, onCompleteMessage = ruleEffectsResult?.messageOnComplete, allowDiscard = backPressed, - eventResultDetails = EventResultDetails(formValueStore.eventState(), dataEntryRepository.eventMode(), validationStrategy), + eventResultDetails = EventResultDetails( + formValueStore.eventState(), + dataEntryRepository.eventMode(), + validationStrategy, + ), ) } + EventStatus.COMPLETED -> { FieldsWithErrorResult( mandatoryFields = mandatoryItemsWithoutValue, @@ -341,9 +400,14 @@ class FormRepositoryImpl( canComplete = false, onCompleteMessage = ruleEffectsResult?.messageOnComplete, allowDiscard = backPressed, - eventResultDetails = EventResultDetails(formValueStore.eventState(), dataEntryRepository.eventMode(), dataEntryRepository.validationStrategy()), + eventResultDetails = EventResultDetails( + formValueStore.eventState(), + dataEntryRepository.eventMode(), + dataEntryRepository.validationStrategy(), + ), ) } + else -> { FieldsWithErrorResult( mandatoryFields = mandatoryItemsWithoutValue, @@ -352,7 +416,11 @@ class FormRepositoryImpl( canComplete = ruleEffectsResult?.canComplete ?: false, onCompleteMessage = ruleEffectsResult?.messageOnComplete, allowDiscard = backPressed, - eventResultDetails = EventResultDetails(formValueStore.eventState(), dataEntryRepository.eventMode(), validationStrategy), + eventResultDetails = EventResultDetails( + formValueStore.eventState(), + dataEntryRepository.eventMode(), + validationStrategy, + ), ) } } @@ -364,25 +432,40 @@ class FormRepositoryImpl( SuccessfulResult( canComplete = ruleEffectsResult?.canComplete ?: true, onCompleteMessage = ruleEffectsResult?.messageOnComplete, - eventResultDetails = EventResultDetails(formValueStore.eventState(), dataEntryRepository.eventMode(), dataEntryRepository.validationStrategy()), + eventResultDetails = EventResultDetails( + formValueStore.eventState(), + dataEntryRepository.eventMode(), + dataEntryRepository.validationStrategy(), + ), ) } + EventStatus.COMPLETED -> { SuccessfulResult( canComplete = false, onCompleteMessage = ruleEffectsResult?.messageOnComplete, - eventResultDetails = EventResultDetails(formValueStore.eventState(), dataEntryRepository.eventMode(), dataEntryRepository.validationStrategy()), + eventResultDetails = EventResultDetails( + formValueStore.eventState(), + dataEntryRepository.eventMode(), + dataEntryRepository.validationStrategy(), + ), ) } + else -> { SuccessfulResult( canComplete = ruleEffectsResult?.canComplete ?: false, onCompleteMessage = ruleEffectsResult?.messageOnComplete, - eventResultDetails = EventResultDetails(formValueStore.eventState(), dataEntryRepository.eventMode(), validationStrategy = dataEntryRepository.validationStrategy()), + eventResultDetails = EventResultDetails( + formValueStore.eventState(), + dataEntryRepository.eventMode(), + validationStrategy = dataEntryRepository.validationStrategy(), + ), ) } } } + override fun completedFieldsPercentage(value: List<FieldUiModel>): Float { return completionPercentage } @@ -438,6 +521,13 @@ class FormRepositoryImpl( updateValueOnList(field.uid, fieldWithNewValue.newValue, field.valueType) } } + + ruleEffectsResult?.fieldsWithOptionEffects()?.forEach { fieldWithOptionEffect -> + itemList.find { it.uid == fieldWithOptionEffect }?.let { + it.optionSet?.let { optionSetUid -> fetchOptions(it.uid, optionSetUid) } + } + } + return if (ruleEffectsResult?.fieldsToUpdate?.isNotEmpty() == true && calculationLoop < loopThreshold ) { @@ -710,6 +800,35 @@ class FormRepositoryImpl( } } + override fun fetchOptions(uid: String, optionSetUid: String) { + val (searchEmitter, flow) = dataEntryRepository.options( + optionSetUid = optionSetUid, + optionsToHide = ruleEffectsResult?.optionsToHide(uid) ?: emptyList(), + optionGroupsToHide = ruleEffectsResult?.optionGroupsToHide(uid) ?: emptyList(), + optionGroupsToShow = ruleEffectsResult?.optionGroupsToShow(uid) ?: emptyList(), + ) + + val newConf = OptionSetConfiguration( + searchEmitter = searchEmitter, + onSearch = { query -> + searchEmitter.value = query + }, + optionFlow = flow, + ) + + itemList.let { list -> + list.find { item -> + item.uid == uid + }?.let { item -> + item.optionSetConfiguration = newConf + itemList = list.updated( + list.indexOf(item), + item, + ) + } + } + } + override fun clearFocusItem() { focusedItemId = null } diff --git a/form/src/main/java/org/dhis2/form/data/GeometryController.kt b/form/src/main/java/org/dhis2/form/data/GeometryController.kt index 584abe82c3..66c87da794 100644 --- a/form/src/main/java/org/dhis2/form/data/GeometryController.kt +++ b/form/src/main/java/org/dhis2/form/data/GeometryController.kt @@ -50,7 +50,6 @@ class GeometryController(private val geometryParser: GeometryParser) { override fun recyclerViewUiEvents(uiEvent: RecyclerViewUiEvents) { when (uiEvent) { - is RecyclerViewUiEvents.RequestCurrentLocation -> currentLocation(uiEvent.uid) is RecyclerViewUiEvents.RequestLocationByMap -> mapRequest( uiEvent.uid, uiEvent.featureType.name, diff --git a/form/src/main/java/org/dhis2/form/data/RuleUtilsProviderResult.kt b/form/src/main/java/org/dhis2/form/data/RuleUtilsProviderResult.kt index 584c2e1498..d000d055e7 100644 --- a/form/src/main/java/org/dhis2/form/data/RuleUtilsProviderResult.kt +++ b/form/src/main/java/org/dhis2/form/data/RuleUtilsProviderResult.kt @@ -49,6 +49,9 @@ data class RuleUtilsProviderResult( fun optionGroupsToShow(fieldUid: String): List<String> { return optionGroupsToShow[fieldUid] ?: mutableListOf() } + + fun fieldsWithOptionEffects(): List<String> = + (optionsToHide.keys + optionGroupsToHide.keys + optionGroupsToShow.keys).toList() } data class FieldWithNewValue(val fieldUid: String, val newValue: String?) diff --git a/form/src/main/java/org/dhis2/form/data/metadata/EnrollmentConfiguration.kt b/form/src/main/java/org/dhis2/form/data/metadata/EnrollmentConfiguration.kt index fded07c53b..cf0afbd5c2 100644 --- a/form/src/main/java/org/dhis2/form/data/metadata/EnrollmentConfiguration.kt +++ b/form/src/main/java/org/dhis2/form/data/metadata/EnrollmentConfiguration.kt @@ -7,19 +7,14 @@ import org.dhis2.commons.bindings.program import org.dhis2.commons.bindings.tei import org.dhis2.commons.bindings.teiAttribute import org.dhis2.commons.bindings.trackedEntityType -import org.dhis2.commons.resources.MetadataIconProvider -import org.dhis2.form.model.OptionSetConfiguration -import org.dhis2.ui.toColor import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.organisationunit.OrganisationUnit -import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor class EnrollmentConfiguration( private val d2: D2, private val enrollmentUid: String, - private val metadataIconProvider: MetadataIconProvider, ) : FormBaseConfiguration(d2) { private val _enrollment: Enrollment? by lazy { @@ -32,10 +27,6 @@ class EnrollmentConfiguration( d2.program(it) } - private val defaultStyleColor by lazy { - program()?.style()?.color()?.toColor() ?: SurfaceColor.Primary - } - fun tei() = enrollment()?.trackedEntityInstance()?.let { d2.tei(it) } fun trackedEntityType() = d2.trackedEntityType(program()?.trackedEntityType()?.uid()!!) fun sections() = d2.programModule().programSections() @@ -43,7 +34,8 @@ class EnrollmentConfiguration( .byProgramUid().eq(enrollment()?.program()) .blockingGet() - fun orgUnit(orgUnitUid: String) = d2.organisationUnitModule().organisationUnits().uid(orgUnitUid).blockingGet() + fun orgUnit(orgUnitUid: String) = + d2.organisationUnitModule().organisationUnits().uid(orgUnitUid).blockingGet() fun programAttributes() = d2.programModule().programTrackedEntityAttributes() @@ -122,22 +114,4 @@ class EnrollmentConfiguration( fun getValue(attributeUid: String) = d2.trackedEntityModule().trackedEntityAttributeValues() .value(attributeUid, tei()?.uid()!!) .blockingGet() - - fun optionSetConfig(optionSetUid: String) = - d2.optionModule().options().byOptionSetUid().eq(optionSetUid).blockingCount() - .let { optionCount -> - OptionSetConfiguration.config(optionCount) { - val options = d2.optionModule().options() - .byOptionSetUid().eq(optionSetUid) - .orderBySortOrder(RepositoryScope.OrderByDirection.ASC) - .blockingGet() - - val metadataIconMap = options.associate { it.uid() to metadataIconProvider(it.style(), defaultStyleColor) } - - OptionSetConfiguration.OptionConfigData( - options = options, - metadataIconMap = metadataIconMap, - ) - } - } } diff --git a/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt b/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt index 9800966246..511e379d38 100644 --- a/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt +++ b/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt @@ -1,7 +1,12 @@ package org.dhis2.form.data.metadata +import androidx.paging.PagingData +import androidx.paging.filter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import org.dhis2.commons.bindings.disableCollapsableSectionsInProgram import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.option.Option open class FormBaseConfiguration(private val d2: D2) { fun optionGroups(optionGroupUids: List<String>) = d2.optionModule().optionGroups() @@ -14,4 +19,35 @@ open class FormBaseConfiguration(private val d2: D2) { fun dateFormatConfiguration() = d2.systemInfoModule().systemInfo().blockingGet()?.dateFormat() + + fun options( + optionSetUid: String, + query: String, + optionsToHide: List<String>, + optionGroupsToHide: List<String>, + optionGroupsToShow: List<String>, + ): Flow<PagingData<Option>> { + return when { + query.isEmpty() -> d2.optionModule() + .options() + .byOptionSetUid().eq(optionSetUid) + .getPagingData(10) + + else -> + d2.optionModule() + .options() + .byOptionSetUid().eq(optionSetUid) + .byDisplayName().like("%$query%") + .getPagingData(10) + }.map { pagingData -> + pagingData.filter { option -> + !optionsToHide.contains(option.uid()) && + !optionGroupsToHide.contains(option.uid()) && + ( + optionGroupsToShow.isEmpty() || + optionGroupsToShow.contains(option.uid()) + ) + } + } + } } diff --git a/form/src/main/java/org/dhis2/form/di/Injector.kt b/form/src/main/java/org/dhis2/form/di/Injector.kt index 2278271089..5055ab1035 100644 --- a/form/src/main/java/org/dhis2/form/di/Injector.kt +++ b/form/src/main/java/org/dhis2/form/di/Injector.kt @@ -141,10 +141,10 @@ object Injector { conf = EnrollmentConfiguration( provideD2(), enrollmentRecords.enrollmentUid, - metadataIconProvider, ), enrollmentMode = enrollmentRecords.enrollmentMode, enrollmentFormLabelsProvider = provideEnrollmentFormLabelsProvider(context), + metadataIconProvider = metadataIconProvider, ) } diff --git a/form/src/main/java/org/dhis2/form/model/ActionType.kt b/form/src/main/java/org/dhis2/form/model/ActionType.kt index 36d127010e..72529d5cb3 100644 --- a/form/src/main/java/org/dhis2/form/model/ActionType.kt +++ b/form/src/main/java/org/dhis2/form/model/ActionType.kt @@ -11,4 +11,5 @@ enum class ActionType { ON_CANCEL_REQUEST_COORDINATES, ON_STORE_FILE, ON_ADD_IMAGE_FINISHED, + ON_FETCH_OPTIONS, } diff --git a/form/src/main/java/org/dhis2/form/model/FieldUiModelImpl.kt b/form/src/main/java/org/dhis2/form/model/FieldUiModelImpl.kt index 46fa01a3e0..be2a049e7f 100644 --- a/form/src/main/java/org/dhis2/form/model/FieldUiModelImpl.kt +++ b/form/src/main/java/org/dhis2/form/model/FieldUiModelImpl.kt @@ -61,13 +61,7 @@ data class FieldUiModelImpl( override fun invokeUiEvent(uiEventType: UiEventType) { callback?.intent(FormIntent.OnRequestCoordinates(uid)) - if (uiEventType != UiEventType.QR_CODE && - uiEventType != UiEventType.EMAIL && - uiEventType != UiEventType.PHONE_NUMBER && - !focused - ) { - onItemClick() - } + uiEventFactory?.generateEvent(value, uiEventType, renderingType, this)?.let { callback?.recyclerViewUiEvents(it) } diff --git a/form/src/main/java/org/dhis2/form/model/OptionSetConfiguration.kt b/form/src/main/java/org/dhis2/form/model/OptionSetConfiguration.kt index 8b632b290a..54d3776403 100644 --- a/form/src/main/java/org/dhis2/form/model/OptionSetConfiguration.kt +++ b/form/src/main/java/org/dhis2/form/model/OptionSetConfiguration.kt @@ -1,93 +1,35 @@ package org.dhis2.form.model +import androidx.paging.PagingData +import androidx.paging.map +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map import org.dhis2.ui.MetadataIconData import org.hisp.dhis.android.core.option.Option -sealed class OptionSetConfiguration( - open val options: List<Option> = emptyList(), - open val optionsToHide: List<String>, - open val optionsToShow: List<String>, - open val optionMetadataIcon: Map<String, MetadataIconData>, +data class OptionSetConfiguration( + val searchEmitter: StateFlow<String>? = null, + val onSearch: (String) -> Unit, + val optionFlow: Flow<PagingData<OptionData>>, ) { - data class DefaultOptionSet( - override val options: List<Option>, - override val optionsToHide: List<String> = emptyList(), - override val optionsToShow: List<String> = emptyList(), - override val optionMetadataIcon: Map<String, MetadataIconData>, - ) : OptionSetConfiguration( - options = options, - optionsToHide = optionsToHide, - optionsToShow = optionsToShow, - optionMetadataIcon = optionMetadataIcon, - ) - - data class BigOptionSet( - override val options: List<Option>, - override val optionsToHide: List<String> = emptyList(), - override val optionsToShow: List<String> = emptyList(), - override val optionMetadataIcon: Map<String, MetadataIconData>, - ) : OptionSetConfiguration( - options = options, - optionsToHide = optionsToHide, - optionsToShow = optionsToShow, - optionMetadataIcon = optionMetadataIcon, - ) - - fun optionsToDisplay() = options.filter { option -> - when { - optionsToShow.isNotEmpty() -> - optionsToShow.contains(option.uid()) - - else -> - !optionsToHide.contains(option.uid()) - } - }.sortedBy { it.sortOrder() } - companion object { - fun config( - optionCount: Int, - optionRequestCallback: () -> OptionConfigData, - ): OptionSetConfiguration { - return when { - optionCount > 15 -> with(optionRequestCallback()) { - BigOptionSet( - options = options, - optionMetadataIcon = metadataIconMap, - ) - } - else -> with(optionRequestCallback()) { - DefaultOptionSet( - options = options, - optionMetadataIcon = metadataIconMap, + fun optionDataFlow( + flow: Flow<PagingData<Option>>, + fetchMetadataIconData: (option: Option) -> MetadataIconData, + ) = + flow.map { pagingData -> + pagingData.map { option -> + OptionData( + option = option, + metadataIconData = fetchMetadataIconData(option), ) } } - } } - data class OptionConfigData( - val options: List<Option>, - val metadataIconMap: Map<String, MetadataIconData>, + data class OptionData( + val option: Option, + val metadataIconData: MetadataIconData, ) - - fun updateOptionsToHideAndShow( - optionsToHide: List<String>, - optionsToShow: List<String>, - ): OptionSetConfiguration { - return setOptionsToShow(optionsToShow).setOptionsToHide(optionsToHide) - } - - private fun setOptionsToHide(optionsToHide: List<String>): OptionSetConfiguration { - return when (this) { - is BigOptionSet -> copy(optionsToHide = optionsToHide) - is DefaultOptionSet -> copy(optionsToHide = optionsToHide) - } - } - - private fun setOptionsToShow(optionsToShow: List<String>): OptionSetConfiguration { - return when (this) { - is BigOptionSet -> copy(optionsToShow = optionsToShow) - is DefaultOptionSet -> copy(optionsToShow = optionsToShow) - } - } } diff --git a/form/src/main/java/org/dhis2/form/model/OptionSetDialogViewModel.kt b/form/src/main/java/org/dhis2/form/model/OptionSetDialogViewModel.kt index 6b884b7130..0fca9cac59 100644 --- a/form/src/main/java/org/dhis2/form/model/OptionSetDialogViewModel.kt +++ b/form/src/main/java/org/dhis2/form/model/OptionSetDialogViewModel.kt @@ -37,8 +37,8 @@ class OptionSetDialogViewModel( searchOptionSetOption( field.optionSet, textToSearch, - field.optionSetConfiguration?.optionsToShow ?: emptyList(), - field.optionSetConfiguration?.optionsToHide ?: emptyList(), + emptyList(), + emptyList(), ) } } diff --git a/form/src/main/java/org/dhis2/form/model/UiEventType.kt b/form/src/main/java/org/dhis2/form/model/UiEventType.kt index 5e7a85142e..81eb03f23b 100644 --- a/form/src/main/java/org/dhis2/form/model/UiEventType.kt +++ b/form/src/main/java/org/dhis2/form/model/UiEventType.kt @@ -1,21 +1,9 @@ package org.dhis2.form.model enum class UiEventType { - DATE_TIME, - AGE_CALENDAR, - ORG_UNIT, - REQUEST_CURRENT_LOCATION, REQUEST_LOCATION_BY_MAP, ADD_PICTURE, - SHOW_PICTURE, - SHOW_DESCRIPTION, - COPY_TO_CLIPBOARD, - QR_CODE, - OPTION_SET, - ADD_SIGNATURE, ADD_FILE, OPEN_FILE, - EMAIL, - PHONE_NUMBER, SHARE_IMAGE, } diff --git a/form/src/main/java/org/dhis2/form/ui/FormView.kt b/form/src/main/java/org/dhis2/form/ui/FormView.kt index bb1d1e70f1..83388f2477 100644 --- a/form/src/main/java/org/dhis2/form/ui/FormView.kt +++ b/form/src/main/java/org/dhis2/form/ui/FormView.kt @@ -3,22 +3,16 @@ package org.dhis2.form.ui import android.Manifest import android.app.Activity.RESULT_OK import android.content.ActivityNotFoundException -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageManager -import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.os.Bundle -import android.text.format.DateFormat import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.DatePicker import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi @@ -32,9 +26,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.timepicker.MaterialTimePicker -import com.google.android.material.timepicker.TimeFormat.CLOCK_12H -import com.google.android.material.timepicker.TimeFormat.CLOCK_24H import com.journeyapps.barcodescanner.ScanOptions import org.dhis2.commons.ActivityResultObservable import org.dhis2.commons.ActivityResultObserver @@ -48,14 +39,9 @@ import org.dhis2.commons.date.DateUtils import org.dhis2.commons.dialogs.AlertBottomDialog import org.dhis2.commons.dialogs.CustomDialog import org.dhis2.commons.dialogs.PeriodDialog -import org.dhis2.commons.dialogs.calendarpicker.CalendarPicker -import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener -import org.dhis2.commons.dialogs.imagedetail.ImageDetailActivity import org.dhis2.commons.extensions.closeKeyboard import org.dhis2.commons.extensions.serializable -import org.dhis2.commons.extensions.truncate import org.dhis2.commons.locationprovider.LocationProvider -import org.dhis2.commons.locationprovider.LocationSettingLauncher import org.dhis2.commons.orgunitselector.OUTreeFragment import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope import org.dhis2.form.R @@ -75,9 +61,7 @@ import org.dhis2.form.model.InfoUiModel import org.dhis2.form.model.RowAction import org.dhis2.form.model.UiRenderType import org.dhis2.form.model.exception.RepositoryRecordsException -import org.dhis2.form.ui.dialog.OptionSetDialog import org.dhis2.form.ui.dialog.QRDetailBottomDialog -import org.dhis2.form.ui.event.DialogDelegate import org.dhis2.form.ui.event.RecyclerViewUiEvents import org.dhis2.form.ui.idling.FormCountingIdlingResource import org.dhis2.form.ui.intent.FormIntent @@ -92,16 +76,12 @@ import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel import org.dhis2.ui.dialogs.bottomsheet.FieldWithIssue import org.dhis2.ui.dialogs.bottomsheet.IssueType -import org.dhis2.ui.dialogs.signature.SignatureDialog import org.hisp.dhis.android.core.arch.helpers.FileResourceDirectoryHelper -import org.hisp.dhis.android.core.arch.helpers.GeometryHelper -import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.common.ValueTypeRenderingType import org.hisp.dhis.android.core.event.EventStatus import timber.log.Timber import java.io.File -import java.util.Calendar import java.util.Date class FormView : Fragment() { @@ -236,50 +216,6 @@ class FormView : Fragment() { } } - private val requestLocationPermissions = - registerForActivityResult( - ActivityResultContracts.RequestMultiplePermissions(), - ) { result -> - if (result.values.all { isGranted -> isGranted }) { - viewModel.getFocusedItemUid()?.let { - requestCurrentLocation(RecyclerViewUiEvents.RequestCurrentLocation(it)) - } - } else { - displayCoordinatesPermissionDeclined() - } - } - - private val permissionSettings = - registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ) { - val result = requireActivity().checkCallingOrSelfPermission( - Manifest.permission.ACCESS_FINE_LOCATION, - ) - if (result == PackageManager.PERMISSION_GRANTED) { - viewModel.getFocusedItemUid()?.let { - requestCurrentLocation(RecyclerViewUiEvents.RequestCurrentLocation(it)) - } - } else { - viewModel.getFocusedItemUid()?.let { - viewModel.submitIntent(FormIntent.OnCancelRequestCoordinates(it)) - } - } - } - - private val locationDisabledSettings = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (locationProvider?.hasLocationEnabled() == true) { - viewModel.getFocusedItemUid()?.let { - requestCurrentLocation(RecyclerViewUiEvents.RequestCurrentLocation(it)) - } - } else { - viewModel.getFocusedItemUid()?.let { - viewModel.submitIntent(FormIntent.OnCancelRequestCoordinates(it)) - } - } - } - private val requestStoragePermissions = registerForActivityResult( ActivityResultContracts.RequestPermission(), @@ -306,7 +242,6 @@ class FormView : Fragment() { ) } - private lateinit var dialogDelegate: DialogDelegate private lateinit var formSectionMapper: FormSectionMapper var scrollCallback: ((Boolean) -> Unit)? = null private var displayConfErrors = true @@ -334,7 +269,6 @@ class FormView : Fragment() { savedInstanceState: Bundle?, ): View { val contextWrapper = ContextThemeWrapper(context, R.style.searchFormInputText) - dialogDelegate = DialogDelegate() formSectionMapper = FormSectionMapper() FormFileProvider.init(contextWrapper.applicationContext) @@ -462,7 +396,10 @@ class FormView : Fragment() { } @Composable - private fun DialogContent(fieldsWithIssues: List<FieldWithIssue>, bottomSheetDialog: BottomSheetDialog): Unit? { + private fun DialogContent( + fieldsWithIssues: List<FieldWithIssue>, + bottomSheetDialog: BottomSheetDialog, + ): Unit? { return if (fieldsWithIssues.isEmpty()) { null } else { @@ -509,12 +446,14 @@ class FormView : Fragment() { } else { showBottomSheetDialog() } + EventStatus.SKIPPED -> { if (fieldsWithIssues.isEmpty()) { viewModel.activateEvent() } showBottomSheetDialog() } + else -> onFinishDataEntry?.invoke() } if (result.eventResultDetails.eventStatus == null && result is NotSavedResult) { @@ -528,7 +467,8 @@ class FormView : Fragment() { isEventCompleted: Boolean, bottomSheetDialog: BottomSheetDialog, ) { - val errorsInField = fieldsWithIssues.isNotEmpty() || fieldsWithIssues.any { it.issueType == IssueType.ERROR } + val errorsInField = + fieldsWithIssues.isNotEmpty() || fieldsWithIssues.any { it.issueType == IssueType.ERROR } if (errorsInField) { bottomSheetDialog.dismiss() } else if (isEventCompleted) { @@ -563,6 +503,7 @@ class FormView : Fragment() { result = result, ) } + is FieldsWithWarningResult -> formResultDialogUiProvider?.invoke( canComplete = result.canComplete, onCompleteMessage = result.onCompleteMessage, @@ -609,42 +550,13 @@ class FormView : Fragment() { .show() } - private fun displayCoordinatesPermissionDeclined() { - MaterialAlertDialogBuilder(requireContext(), R.style.DhisMaterialDialog) - .setTitle(getString(R.string.info)) - .setMessage(getString(R.string.location_permission_denied)) - .setPositiveButton(R.string.action_accept) { _, _ -> - val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - intent.data = Uri.fromParts("package", requireActivity().packageName, null) - permissionSettings.launch(intent) - } - .setNegativeButton(R.string.action_close) { _, _ -> - viewModel.getFocusedItemUid()?.let { - viewModel.submitIntent(FormIntent.OnCancelRequestCoordinates(it)) - } - } - .setCancelable(false) - .show() - } - private fun uiEventHandler(uiEvent: RecyclerViewUiEvents) { when (uiEvent) { - is RecyclerViewUiEvents.OpenCustomCalendar -> showCustomCalendar(uiEvent) - is RecyclerViewUiEvents.OpenTimePicker -> showTimePicker(uiEvent) - is RecyclerViewUiEvents.ShowDescriptionLabelDialog -> showDescriptionLabelDialog( - uiEvent, - ) - - is RecyclerViewUiEvents.RequestCurrentLocation -> requestCurrentLocation(uiEvent) is RecyclerViewUiEvents.RequestLocationByMap -> requestLocationByMap(uiEvent) is RecyclerViewUiEvents.DisplayQRCode -> displayQRImage(uiEvent) is RecyclerViewUiEvents.ScanQRCode -> requestQRScan(uiEvent) is RecyclerViewUiEvents.OpenOrgUnitDialog -> showOrgUnitDialog(uiEvent) is RecyclerViewUiEvents.AddImage -> requestAddImage(uiEvent) - is RecyclerViewUiEvents.ShowImage -> showFullPicture(uiEvent) - is RecyclerViewUiEvents.CopyToClipboard -> copyToClipboard(uiEvent.value) - is RecyclerViewUiEvents.OpenOptionSetDialog -> showOptionSetDialog(uiEvent) - is RecyclerViewUiEvents.AddSignature -> showSignatureDialog(uiEvent) is RecyclerViewUiEvents.OpenFile -> openFile(uiEvent) is RecyclerViewUiEvents.OpenFileSelector -> openFileSelector(uiEvent) is RecyclerViewUiEvents.OpenChooserIntent -> openChooserIntent(uiEvent) @@ -732,22 +644,6 @@ class FormView : Fragment() { } } - private fun copyToClipboard(value: String?) { - val clipboard = - requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - value?.let { - if (it.isNotEmpty()) { - val clip = ClipData.newPlainText("copy", it) - clipboard.setPrimaryClip(clip) - Toast.makeText( - context, - requireContext().getString(R.string.copied_text), - Toast.LENGTH_SHORT, - ).show() - } - } - } - private fun render(items: List<FieldUiModel>) { viewModel.calculateCompletedFields() viewModel.updateConfigurationErrors() @@ -762,119 +658,6 @@ class FormView : Fragment() { viewModel.submitIntent(intent) } - private fun showCustomCalendar(intent: RecyclerViewUiEvents.OpenCustomCalendar) { - val dialog = CalendarPicker(requireContext()).apply { - setTitle(intent.label) - setInitialDate(intent.date) - isFutureDatesAllowed(intent.allowFutureDates) - setListener(object : OnDatePickerListener { - override fun onNegativeClick() { - intentHandler(FormIntent.ClearValue(intent.uid)) - } - - override fun onPositiveClick(datePicker: DatePicker) { - when (intent.isDateTime) { - true -> uiEventHandler( - dialogDelegate.handleDateTimeInput( - intent.uid, - intent.label, - intent.date, - datePicker.year, - datePicker.month, - datePicker.dayOfMonth, - ), - ) - - else -> intentHandler( - dialogDelegate.handleDateInput( - intent.uid, - datePicker.year, - datePicker.month, - datePicker.dayOfMonth, - ), - ) - } - } - }) - } - dialog.show() - } - - private fun showTimePicker(intent: RecyclerViewUiEvents.OpenTimePicker) { - val calendar = Calendar.getInstance() - intent.date?.let { calendar.time = it } - val is24HourFormat = DateFormat.is24HourFormat(requireContext()) - MaterialTimePicker.Builder() - .setTheme(R.style.TimePicker) - .setTimeFormat(CLOCK_24H.takeIf { is24HourFormat } ?: CLOCK_12H) - .setHour(calendar[Calendar.HOUR_OF_DAY]) - .setMinute(calendar[Calendar.MINUTE]) - .setTitleText(intent.label) - .build().apply { - addOnPositiveButtonClickListener { - intentHandler( - dialogDelegate.handleTimeInput( - intent.uid, - if (intent.isDateTime == true) intent.date else null, - hour, - minute, - ), - ) - } - } - .show(childFragmentManager, "timePicker") - } - - private fun showDescriptionLabelDialog( - intent: RecyclerViewUiEvents.ShowDescriptionLabelDialog, - ) { - CustomDialog( - requireContext(), - intent.title, - intent.message ?: requireContext().getString(R.string.empty_description), - requireContext().getString(R.string.action_close), - null, - Constants.DESCRIPTION_DIALOG, - null, - ).show() - } - - private fun requestCurrentLocation(event: RecyclerViewUiEvents.RequestCurrentLocation) { - locationProvider?.getLastKnownLocation( - { location -> - val geometry = GeometryHelper.createPointGeometry( - location.longitude.truncate(), - location.latitude.truncate(), - ) - val intent = FormIntent.SelectLocationFromCoordinates( - event.uid, - geometry.coordinates(), - FeatureType.POINT.name, - ) - - intentHandler(intent) - }, - { - requestLocationPermissions.launch( - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), - ) - }, - { - LocationSettingLauncher.requestEnableLocationSetting( - requireContext(), - { - locationDisabledSettings.launch( - LocationSettingLauncher.locationSourceSettingIntent(), - ) - }, - { - viewModel.submitIntent(FormIntent.OnCancelRequestCoordinates(event.uid)) - }, - ) - }, - ) - } - private fun requestLocationByMap(event: RecyclerViewUiEvents.RequestLocationByMap) { onActivityForResult?.invoke() mapContent.launch( @@ -972,16 +755,6 @@ class FormView : Fragment() { .show() } - private fun showFullPicture(event: RecyclerViewUiEvents.ShowImage) { - val intent = ImageDetailActivity.intent( - title = event.label, - imagePath = event.value, - context = requireActivity(), - ) - - startActivity(intent) - } - private fun openFileSelector(event: RecyclerViewUiEvents.OpenFileSelector) { onSavePicture = { file -> intentHandler( @@ -1077,43 +850,6 @@ class FormView : Fragment() { } } - private fun showOptionSetDialog(uiEvent: RecyclerViewUiEvents.OpenOptionSetDialog) { - OptionSetDialog( - field = uiEvent.field, - onClearValue = { - intentHandler(FormIntent.ClearValue(uiEvent.field.uid)) - }, - ) { code -> - intentHandler( - FormIntent.OnSave( - uiEvent.field.uid, - code, - uiEvent.field.valueType, - ), - ) - }.show(this@FormView.childFragmentManager) - } - - private fun showSignatureDialog(uiEvent: RecyclerViewUiEvents.AddSignature) { - SignatureDialog(uiEvent.label) { - val file = File( - FileResourceDirectoryHelper.getFileResourceDirectory(requireContext()), - TEMP_FILE, - ) - file.outputStream().use { out -> - it.compress(Bitmap.CompressFormat.PNG, 85, out) - out.flush() - } - intentHandler( - FormIntent.OnStoreFile( - uiEvent.uid, - file.path, - ValueType.IMAGE, - ), - ) - }.show(this@FormView.childFragmentManager) - } - fun onBackPressed() { viewModel.runDataIntegrityCheck(backButtonPressed = true) } 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 dfa03988b6..50b9aab0b2 100644 --- a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt +++ b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt @@ -216,9 +216,18 @@ class FormViewModel( ActionType.ON_ADD_IMAGE_FINISHED -> handleOnAddImageFinishedAction(action) ActionType.ON_STORE_FILE -> handleOnStoreFileAction(action) + ActionType.ON_FETCH_OPTIONS -> handleFetchOptionsAction(action) } } + private fun handleFetchOptionsAction(action: RowAction): StoreResult { + repository.fetchOptions(action.id, action.extraData!!) + return StoreResult( + action.id, + ValueStoreResult.VALUE_CHANGED, + ) + } + private fun handleOnSaveAction(action: RowAction): StoreResult { if (action.valueType == ValueType.COORDINATE) { repository.setFieldRequestingCoordinates(action.id, false) @@ -545,6 +554,14 @@ class FormViewModel( valueType = intent.valueType, ) } + + is FormIntent.FetchOptions -> + createRowAction( + uid = intent.uid, + value = intent.value, + extraData = intent.optionSetUid, + actionType = ActionType.ON_FETCH_OPTIONS, + ) } } diff --git a/form/src/main/java/org/dhis2/form/ui/event/DialogDelegate.kt b/form/src/main/java/org/dhis2/form/ui/event/DialogDelegate.kt deleted file mode 100644 index ff848a54e0..0000000000 --- a/form/src/main/java/org/dhis2/form/ui/event/DialogDelegate.kt +++ /dev/null @@ -1,92 +0,0 @@ -package org.dhis2.form.ui.event - -import org.dhis2.commons.date.DateUtils -import org.dhis2.form.ui.intent.FormIntent -import org.hisp.dhis.android.core.common.ValueType -import java.util.Calendar -import java.util.Date - -class DialogDelegate { - - fun handleDateInput(uid: String, year: Int, month: Int, day: Int): FormIntent { - val currentCalendar = Calendar.getInstance() - val ageDate = with(currentCalendar) { - set(Calendar.YEAR, year) - set(Calendar.MONTH, month) - set(Calendar.DAY_OF_MONTH, day) - set(Calendar.HOUR_OF_DAY, 0) - set(Calendar.MINUTE, 0) - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - return@with time - } - val date = DateUtils.oldUiDateFormat().format(ageDate) - - return FormIntent.OnSave( - uid = uid, - value = date, - valueType = ValueType.DATE, - ) - } - - fun handleYearMonthDayInput(uid: String, year: Int, month: Int, day: Int): FormIntent { - val currentCalendar = Calendar.getInstance() - val ageDate = with(currentCalendar) { - add(Calendar.YEAR, year) - add(Calendar.MONTH, month) - add(Calendar.DAY_OF_MONTH, day) - set(Calendar.HOUR_OF_DAY, 0) - set(Calendar.MINUTE, 0) - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - return@with time - } - val date = DateUtils.oldUiDateFormat().format(ageDate) - - return FormIntent.OnSave( - uid = uid, - value = date, - valueType = ValueType.DATE, - ) - } - - fun handleTimeInput(uid: String, date: Date?, hourOfDay: Int, minutes: Int): FormIntent { - val currentCalendar = Calendar.getInstance() - val dateTime = with(currentCalendar) { - date?.let { time = it } - set(Calendar.HOUR_OF_DAY, hourOfDay) - set(Calendar.MINUTE, minutes) - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - return@with time - } - val dateValue = when (date) { - null -> DateUtils.timeFormat().format(dateTime) - else -> DateUtils.databaseDateFormatNoSeconds().format(dateTime) - } - return FormIntent.OnSave( - uid = uid, - value = dateValue, - valueType = date?.let { ValueType.DATETIME } ?: ValueType.TIME, - ) - } - - fun handleDateTimeInput( - uid: String, - label: String, - date: Date?, - year: Int, - month: Int, - day: Int, - ): RecyclerViewUiEvents { - val currentCalendar = Calendar.getInstance() - val dateTime = with(currentCalendar) { - date?.let { time = it } - set(Calendar.YEAR, year) - set(Calendar.MONTH, month) - set(Calendar.DAY_OF_MONTH, day) - return@with time - } - return RecyclerViewUiEvents.OpenTimePicker(uid, label, dateTime, isDateTime = true) - } -} diff --git a/form/src/main/java/org/dhis2/form/ui/event/RecyclerViewUiEvents.kt b/form/src/main/java/org/dhis2/form/ui/event/RecyclerViewUiEvents.kt index 017b751638..f28f0d830e 100644 --- a/form/src/main/java/org/dhis2/form/ui/event/RecyclerViewUiEvents.kt +++ b/form/src/main/java/org/dhis2/form/ui/event/RecyclerViewUiEvents.kt @@ -9,30 +9,6 @@ import java.util.Date sealed class RecyclerViewUiEvents { - data class OpenCustomCalendar( - val uid: String, - val label: String, - val date: Date?, - val allowFutureDates: Boolean, - val isDateTime: Boolean? = false, - ) : RecyclerViewUiEvents() - - data class OpenTimePicker( - val uid: String, - val label: String, - val date: Date?, - val isDateTime: Boolean? = false, - ) : RecyclerViewUiEvents() - - data class ShowDescriptionLabelDialog( - val title: String, - val message: String?, - ) : RecyclerViewUiEvents() - - data class RequestCurrentLocation( - val uid: String, - ) : RecyclerViewUiEvents() - data class RequestLocationByMap( val uid: String, val featureType: FeatureType, @@ -65,24 +41,6 @@ sealed class RecyclerViewUiEvents { val uid: String, ) : RecyclerViewUiEvents() - data class AddSignature( - val uid: String, - val label: String, - ) : RecyclerViewUiEvents() - - data class ShowImage( - val label: String, - val value: String, - ) : RecyclerViewUiEvents() - - data class CopyToClipboard( - val value: String?, - ) : RecyclerViewUiEvents() - - data class OpenOptionSetDialog( - val field: FieldUiModel, - ) : RecyclerViewUiEvents() - data class OpenFileSelector( val field: FieldUiModel, ) : RecyclerViewUiEvents() diff --git a/form/src/main/java/org/dhis2/form/ui/event/UiEventFactoryImpl.kt b/form/src/main/java/org/dhis2/form/ui/event/UiEventFactoryImpl.kt index ee52d3439f..f05f27c5aa 100644 --- a/form/src/main/java/org/dhis2/form/ui/event/UiEventFactoryImpl.kt +++ b/form/src/main/java/org/dhis2/form/ui/event/UiEventFactoryImpl.kt @@ -1,27 +1,13 @@ package org.dhis2.form.ui.event import android.content.Intent -import org.dhis2.commons.date.DateUtils -import org.dhis2.commons.extensions.toDate import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.UiEventType import org.dhis2.form.model.UiEventType.ADD_FILE import org.dhis2.form.model.UiEventType.ADD_PICTURE -import org.dhis2.form.model.UiEventType.ADD_SIGNATURE -import org.dhis2.form.model.UiEventType.AGE_CALENDAR -import org.dhis2.form.model.UiEventType.COPY_TO_CLIPBOARD -import org.dhis2.form.model.UiEventType.DATE_TIME -import org.dhis2.form.model.UiEventType.EMAIL import org.dhis2.form.model.UiEventType.OPEN_FILE -import org.dhis2.form.model.UiEventType.OPTION_SET -import org.dhis2.form.model.UiEventType.ORG_UNIT -import org.dhis2.form.model.UiEventType.PHONE_NUMBER -import org.dhis2.form.model.UiEventType.QR_CODE -import org.dhis2.form.model.UiEventType.REQUEST_CURRENT_LOCATION import org.dhis2.form.model.UiEventType.REQUEST_LOCATION_BY_MAP import org.dhis2.form.model.UiEventType.SHARE_IMAGE -import org.dhis2.form.model.UiEventType.SHOW_DESCRIPTION -import org.dhis2.form.model.UiEventType.SHOW_PICTURE import org.dhis2.form.model.UiRenderType import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.common.ValueType @@ -44,104 +30,15 @@ class UiEventFactoryImpl( var uiEvent: RecyclerViewUiEvents? = null try { uiEvent = when (uiEventType) { - DATE_TIME -> { - when (valueType) { - ValueType.DATE -> RecyclerViewUiEvents.OpenCustomCalendar( - uid, - label, - value?.let { DateUtils.oldUiDateFormat().parse(it) }, - allowFutureDates ?: true, - ) - - ValueType.DATETIME -> RecyclerViewUiEvents.OpenCustomCalendar( - uid, - label, - value?.let { DateUtils.databaseDateFormatNoSeconds().parse(it) }, - allowFutureDates ?: true, - isDateTime = true, - ) - - ValueType.TIME -> RecyclerViewUiEvents.OpenTimePicker( - uid, - label, - value?.let { DateUtils.timeFormat().parse(it) }, - ) - - else -> null - } - } - - AGE_CALENDAR -> RecyclerViewUiEvents.OpenCustomCalendar( - uid = uid, - label = label, - date = value?.toDate(), - allowFutureDates = allowFutureDates ?: false, - ) - - ORG_UNIT -> RecyclerViewUiEvents.OpenOrgUnitDialog( - uid, - label, - value, - fieldUiModel.orgUnitSelectorScope, - ) - - REQUEST_CURRENT_LOCATION -> RecyclerViewUiEvents.RequestCurrentLocation( - uid = uid, - ) - REQUEST_LOCATION_BY_MAP -> RecyclerViewUiEvents.RequestLocationByMap( uid = uid, featureType = getFeatureType(renderingType), value = value, ) - SHOW_DESCRIPTION -> RecyclerViewUiEvents.ShowDescriptionLabelDialog( - title = label, - message = description, - ) - ADD_PICTURE -> RecyclerViewUiEvents.AddImage(uid) - SHOW_PICTURE -> RecyclerViewUiEvents.ShowImage( - label, - value ?: "", - ) - - COPY_TO_CLIPBOARD -> RecyclerViewUiEvents.CopyToClipboard( - value = value, - ) - - QR_CODE -> { - if (value.isNullOrEmpty() && fieldUiModel.editable) { - RecyclerViewUiEvents.ScanQRCode( - uid = uid, - optionSet = optionSet, - renderingType = renderingType, - ) - } else if (value != null) { - RecyclerViewUiEvents.DisplayQRCode( - uid = uid, - optionSet = optionSet, - value = value, - renderingType = renderingType, - editable = fieldUiModel.editable, - label = label, - ) - } else { - null - } - } - - OPTION_SET -> RecyclerViewUiEvents.OpenOptionSetDialog(fieldUiModel) - ADD_SIGNATURE -> RecyclerViewUiEvents.AddSignature(uid, label) ADD_FILE -> RecyclerViewUiEvents.OpenFileSelector(fieldUiModel) OPEN_FILE -> RecyclerViewUiEvents.OpenFile(fieldUiModel) - EMAIL -> RecyclerViewUiEvents.OpenChooserIntent(Intent.ACTION_SENDTO, value, uid) - PHONE_NUMBER -> RecyclerViewUiEvents.OpenChooserIntent( - Intent.ACTION_DIAL, - value, - uid, - ) - SHARE_IMAGE -> RecyclerViewUiEvents.OpenChooserIntent( Intent.ACTION_SEND, fieldUiModel.displayName, diff --git a/form/src/main/java/org/dhis2/form/ui/intent/FormIntent.kt b/form/src/main/java/org/dhis2/form/ui/intent/FormIntent.kt index 1f37d3b459..98d2c34343 100644 --- a/form/src/main/java/org/dhis2/form/ui/intent/FormIntent.kt +++ b/form/src/main/java/org/dhis2/form/ui/intent/FormIntent.kt @@ -89,4 +89,10 @@ sealed class FormIntent { val valueType: ValueType?, val allowFutureDates: Boolean = true, ) : FormIntent() + + data class FetchOptions( + val uid: String, + val optionSetUid: String, + val value: String?, + ) : FormIntent() } diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CategorySelectorProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CategorySelectorProvider.kt index 65203b9123..221c87acf3 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CategorySelectorProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CategorySelectorProvider.kt @@ -81,7 +81,11 @@ private fun ProvideCategorySelector( } if (category.options.isNotEmpty()) { - val dropdownItems = category.options.map { DropdownItem(it.name) } + var dropdownItems by remember { + mutableStateOf( + category.options.map { DropdownItem(it.name) }, + ) + } InputDropDown( modifier = modifier, @@ -92,16 +96,26 @@ private fun ProvideCategorySelector( onResetButtonClicked = { onCategoryOptionSelected(null) }, - onItemSelected = { newSelectedItem -> + onItemSelected = { _, newSelectedItem -> onCategoryOptionSelected( category.options.firstOrNull { it.name == newSelectedItem.label }, ) }, - dropdownItems = dropdownItems, + fetchItem = { index -> dropdownItems[index] }, + itemCount = dropdownItems.size, + onSearchOption = { query -> + dropdownItems = if (query.isNotEmpty()) { + dropdownItems.filter { it.label.contains(query) } + } else { + category.options.map { DropdownItem(it.name) } + } + }, isRequiredField = fieldUiModel.mandatory, legendData = fieldUiModel.legend(), + loadOptions = { + }, ) } else { ProvideEmptyCategorySelector( @@ -122,6 +136,8 @@ fun ProvideEmptyCategorySelector( mutableStateOf("") } + val emptyItems = listOf(DropdownItem(stringResource(id = R.string.no_options))) + InputDropDown( modifier = modifier, title = name, @@ -131,11 +147,17 @@ fun ProvideEmptyCategorySelector( onResetButtonClicked = { selectedItem = "" }, - onItemSelected = { newSelectedDropdownItem -> + onItemSelected = { _, newSelectedDropdownItem -> selectedItem = newSelectedDropdownItem.label }, - dropdownItems = listOf(DropdownItem(stringResource(id = R.string.no_options))), + fetchItem = { index -> + emptyItems[index] + }, + itemCount = 1, + onSearchOption = { /*no-op*/ }, isRequiredField = false, + loadOptions = { + }, ) } diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CheckBoxProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CheckBoxProvider.kt index cb82e88131..39a5888107 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CheckBoxProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CheckBoxProvider.kt @@ -2,6 +2,7 @@ package org.dhis2.form.ui.provider.inputfield import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.paging.compose.collectAsLazyPagingItems import org.dhis2.commons.resources.ResourceManager import org.dhis2.form.R import org.dhis2.form.extensions.inputState @@ -22,14 +23,24 @@ internal fun ProvideCheckBoxInput( fieldUiModel: FieldUiModel, intentHandler: (FormIntent) -> Unit, ) { - val data = fieldUiModel.optionSetConfiguration?.optionsToDisplay()?.map { option -> - CheckBoxData( - uid = option.uid(), - checked = fieldUiModel.displayName == option.displayName(), - enabled = true, - textInput = option.displayName() ?: "", - ) - } ?: emptyList() + val dataMap = buildMap { + fieldUiModel.optionSetConfiguration?.optionFlow?.collectAsLazyPagingItems()?.let { paging -> + repeat(paging.itemCount) { index -> + val optionData = paging[index] + put( + optionData?.option?.code() ?: "", + CheckBoxData( + uid = optionData?.option?.uid() ?: "", + checked = fieldUiModel.displayName == optionData?.option?.displayName(), + enabled = true, + textInput = optionData?.option?.displayName() ?: "", + ), + ) + } + } + } + + val (codeList, data) = dataMap.toList().unzip() InputCheckBox( modifier = modifier, @@ -42,11 +53,11 @@ internal fun ProvideCheckBoxInput( legendData = fieldUiModel.legend(), isRequired = fieldUiModel.mandatory, onItemChange = { item -> + val selectedIndex = data.indexOf(item) intentHandler( FormIntent.OnSave( fieldUiModel.uid, - fieldUiModel.optionSetConfiguration?.optionsToDisplay() - ?.find { it.uid() == item.uid }?.code(), + codeList[selectedIndex], fieldUiModel.valueType, ), ) diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DropdownProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DropdownProvider.kt index 00ce8262e9..98a50668b3 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DropdownProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DropdownProvider.kt @@ -1,11 +1,13 @@ package org.dhis2.form.ui.provider.inputfield import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.paging.compose.collectAsLazyPagingItems import org.dhis2.form.extensions.inputState import org.dhis2.form.extensions.legend import org.dhis2.form.extensions.supportingText @@ -19,14 +21,27 @@ fun ProvideDropdownInput( modifier: Modifier, inputStyle: InputStyle, fieldUiModel: FieldUiModel, + fetchOptions: () -> Unit, ) { var selectedItem by remember(fieldUiModel) { mutableStateOf(DropdownItem(fieldUiModel.displayName ?: "")) } - val selectableOptions = fieldUiModel.optionSetConfiguration?.optionsToDisplay() + val optionSetConfiguration by remember(fieldUiModel) { + mutableStateOf(fieldUiModel.optionSetConfiguration) + } + + val optionsData = optionSetConfiguration?.optionFlow?.collectAsLazyPagingItems() + + val useDropdown by remember { + derivedStateOf { + optionSetConfiguration?.searchEmitter?.value?.isEmpty() == true && ( + optionsData?.itemCount + ?: 0 + ) < 15 + } + } - val dropdownItems = selectableOptions?.map { DropdownItem(it.displayName() ?: it.code() ?: "") } InputDropDown( modifier = modifier, inputStyle = inputStyle, @@ -37,14 +52,23 @@ fun ProvideDropdownInput( legendData = fieldUiModel.legend(), isRequiredField = fieldUiModel.mandatory, onResetButtonClicked = { fieldUiModel.onClear() }, - dropdownItems = dropdownItems ?: emptyList(), - onItemSelected = { newSelectedItem -> + fetchItem = { index -> + DropdownItem(optionsData?.get(index)?.option?.displayName() ?: "") + }, + onSearchOption = { query -> + fieldUiModel.optionSetConfiguration?.onSearch?.invoke(query) + }, + itemCount = optionsData?.itemCount ?: 0, + useDropDown = useDropdown, + onItemSelected = { index, newSelectedItem -> selectedItem = newSelectedItem fieldUiModel.onSave( - selectableOptions?.firstOrNull { - it.displayName() == newSelectedItem.label - }?.code(), + optionsData?.get(index)?.option?.code(), ) }, + loadOptions = fetchOptions, + onDismiss = { + fieldUiModel.optionSetConfiguration?.onSearch?.invoke("") + }, ) } 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 6599f68d7d..7b415964ef 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 @@ -113,6 +113,15 @@ fun FieldProvider( fieldUiModel = fieldUiModel, intentHandler = intentHandler, context = context, + fetchOptions = { + intentHandler( + FormIntent.FetchOptions( + fieldUiModel.uid, + fieldUiModel.optionSet!!, + value = fieldUiModel.value, + ), + ) + }, ) fieldUiModel.eventCategories != null -> ProvideCategorySelectorInput( @@ -476,6 +485,7 @@ fun ProvideByOptionSet( fieldUiModel: FieldUiModel, intentHandler: (FormIntent) -> Unit, context: Context, + fetchOptions: () -> Unit, ) { when (fieldUiModel.renderingType) { UiRenderType.HORIZONTAL_RADIOBUTTONS, @@ -527,6 +537,7 @@ fun ProvideByOptionSet( modifier = modifier, inputStyle = inputStyle, fieldUiModel = fieldUiModel, + fetchOptions = fetchOptions, ) } } diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixInputProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixInputProvider.kt index e98cf5d6ca..b926d87596 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixInputProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixInputProvider.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.paging.compose.collectAsLazyPagingItems import org.dhis2.form.extensions.inputState import org.dhis2.form.extensions.legend import org.dhis2.form.extensions.supportingText @@ -23,10 +24,21 @@ internal fun ProvideMatrixInput( context: Context, intentHandler: (FormIntent) -> Unit, ) { - val inputCardDataList = rememberInputCardList( - options = fieldUiModel.optionSetConfiguration?.optionsToDisplay(), - optionMetadataIconMap = fieldUiModel.optionSetConfiguration?.optionMetadataIcon, - ) + val inputCardDataList: MutableList<ImageCardData> = mutableListOf() + + fieldUiModel.optionSetConfiguration?.optionFlow?.collectAsLazyPagingItems()?.let { paging -> + repeat(paging.itemCount) { index -> + val optionData = paging[index] + inputCardDataList.add( + imageCardDataWithUidAndLabel( + optionData!!.metadataIconData.imageCardData, + optionData.option.code() ?: "", + optionData.option.displayName() ?: "", + ), + ) + } + } + var matrixSelectedItem by rememberSelectedOption( fieldUiModel = fieldUiModel, inputCardDataList = inputCardDataList, diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixSequentialUtilites.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixSequentialUtilites.kt index 8eaf068c1a..6fbe9b727d 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixSequentialUtilites.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixSequentialUtilites.kt @@ -29,7 +29,7 @@ fun rememberInputCardList( } ?: emptyList() } -private fun imageCardDataWithUidAndLabel( +fun imageCardDataWithUidAndLabel( imageCardData: ImageCardData, optionCode: String, label: String, diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MultiSelectionInputProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MultiSelectionInputProvider.kt index 18ea54e79b..976fda619d 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MultiSelectionInputProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MultiSelectionInputProvider.kt @@ -2,6 +2,7 @@ package org.dhis2.form.ui.provider.inputfield import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.paging.compose.collectAsLazyPagingItems import org.dhis2.form.extensions.inputState import org.dhis2.form.extensions.legend import org.dhis2.form.extensions.supportingText @@ -16,16 +17,25 @@ internal fun ProvideMultiSelectionInput( fieldUiModel: FieldUiModel, intentHandler: (FormIntent) -> Unit, ) { - val optionsToDisplay = fieldUiModel.optionSetConfiguration?.optionsToDisplay() ?: emptyList() - val data = optionsToDisplay.map { option -> - CheckBoxData( - uid = option.uid(), - checked = option.code()?.let { fieldUiModel.value?.split(",")?.contains(it) } ?: false, - enabled = true, - textInput = option.displayName() ?: "", - ) + val dataMap = buildMap { + fieldUiModel.optionSetConfiguration?.optionFlow?.collectAsLazyPagingItems()?.let { paging -> + repeat(paging.itemCount) { index -> + val optionData = paging[index] + put( + optionData?.option?.code() ?: "", + CheckBoxData( + uid = optionData?.option?.uid() ?: "", + checked = optionData?.option?.code()?.let { fieldUiModel.value?.split(",")?.contains(it) } ?: false, + enabled = true, + textInput = optionData?.option?.displayName() ?: "", + ), + ) + } + } } + val (codeList, data) = dataMap.toList().unzip() + InputMultiSelection( modifier = modifier, title = fieldUiModel.label, @@ -35,8 +45,9 @@ internal fun ProvideMultiSelectionInput( legendData = fieldUiModel.legend(), isRequired = fieldUiModel.mandatory, onItemsSelected = { - val checkedValues = it.filter { item -> item.checked }.mapNotNull { - optionsToDisplay.find { option -> option.uid() == it.uid }?.code() + val checkedValues = it.filter { item -> item.checked }.map { checkBoxData -> + val selectedIndex = data.indexOf(checkBoxData) + codeList[selectedIndex] } intentHandler( diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/PeriodSelectorProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/PeriodSelectorProvider.kt index 860ae6fbd7..18857aa51c 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/PeriodSelectorProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/PeriodSelectorProvider.kt @@ -85,6 +85,8 @@ fun ProvideEmptyPeriodSelector( mutableStateOf("") } + val options = listOf(DropdownItem(stringResource(id = R.string.no_periods))) + InputDropDown( modifier = modifier, title = name, @@ -94,10 +96,19 @@ fun ProvideEmptyPeriodSelector( onResetButtonClicked = { selectedItem = "" }, - onItemSelected = { newSelectedDropdownItem -> + onItemSelected = { _, newSelectedDropdownItem -> selectedItem = newSelectedDropdownItem.label }, - dropdownItems = listOf(DropdownItem(stringResource(id = R.string.no_periods))), + itemCount = 1, + fetchItem = { + options[it] + }, + loadOptions = { + /*no-op*/ + }, + onSearchOption = { + /*no-op*/ + }, isRequiredField = false, ) } diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/RadioButtonProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/RadioButtonProvider.kt index 5ce3379166..19a6181636 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/RadioButtonProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/RadioButtonProvider.kt @@ -2,6 +2,7 @@ package org.dhis2.form.ui.provider.inputfield import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.paging.compose.collectAsLazyPagingItems import org.dhis2.commons.resources.ResourceManager import org.dhis2.form.R import org.dhis2.form.extensions.inputState @@ -21,14 +22,24 @@ internal fun ProvideRadioButtonInput( fieldUiModel: FieldUiModel, intentHandler: (FormIntent) -> Unit, ) { - val data = fieldUiModel.optionSetConfiguration?.optionsToDisplay()?.map { option -> - RadioButtonData( - uid = option.uid(), - selected = fieldUiModel.displayName == option.displayName(), - enabled = true, - textInput = option.displayName() ?: "", - ) - } ?: emptyList() + val dataMap = buildMap { + fieldUiModel.optionSetConfiguration?.optionFlow?.collectAsLazyPagingItems()?.let { paging -> + repeat(paging.itemCount) { index -> + val optionData = paging[index] + put( + optionData?.option?.code() ?: "", + RadioButtonData( + uid = optionData?.option?.uid() ?: "", + selected = fieldUiModel.displayName == optionData?.option?.displayName(), + enabled = true, + textInput = optionData?.option?.displayName() ?: "", + ), + ) + } + } + } + + val (codeList, data) = dataMap.toList().unzip() InputRadioButton( modifier = modifier, @@ -42,11 +53,11 @@ internal fun ProvideRadioButtonInput( isRequired = fieldUiModel.mandatory, itemSelected = data.find { it.selected }, onItemChange = { item -> + val selectedIndex = data.indexOf(item) intentHandler( FormIntent.OnSave( fieldUiModel.uid, - fieldUiModel.optionSetConfiguration?.optionsToDisplay() - ?.find { it.uid() == item?.uid }?.code(), + codeList[selectedIndex], fieldUiModel.valueType, ), ) diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SequentialInputProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SequentialInputProvider.kt index d021b6d141..acdd7e235a 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SequentialInputProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SequentialInputProvider.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.paging.compose.collectAsLazyPagingItems import org.dhis2.form.extensions.inputState import org.dhis2.form.extensions.legend import org.dhis2.form.extensions.supportingText @@ -23,10 +24,21 @@ internal fun ProvideSequentialInput( context: Context, intentHandler: (FormIntent) -> Unit, ) { - val inputCardDataList = rememberInputCardList( - options = fieldUiModel.optionSetConfiguration?.optionsToDisplay(), - optionMetadataIconMap = fieldUiModel.optionSetConfiguration?.optionMetadataIcon, - ) + val inputCardDataList: MutableList<ImageCardData> = mutableListOf() + + fieldUiModel.optionSetConfiguration?.optionFlow?.collectAsLazyPagingItems()?.let { paging -> + repeat(paging.itemCount) { index -> + val optionData = paging[index] + inputCardDataList.add( + imageCardDataWithUidAndLabel( + optionData!!.metadataIconData.imageCardData, + optionData.option.code() ?: "", + optionData.option.displayName() ?: "", + ), + ) + } + } + var matrixSelectedItem by rememberSelectedOption( fieldUiModel = fieldUiModel, inputCardDataList = inputCardDataList, diff --git a/form/src/test/java/org/dhis2/form/data/EnrollmentRepositoryTest.kt b/form/src/test/java/org/dhis2/form/data/EnrollmentRepositoryTest.kt index 16a79d78d9..5ca3bb5c20 100644 --- a/form/src/test/java/org/dhis2/form/data/EnrollmentRepositoryTest.kt +++ b/form/src/test/java/org/dhis2/form/data/EnrollmentRepositoryTest.kt @@ -1,6 +1,7 @@ package org.dhis2.form.data import junit.framework.TestCase.assertTrue +import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.form.data.EnrollmentRepository.Companion.ORG_UNIT_UID import org.dhis2.form.data.metadata.EnrollmentConfiguration import org.dhis2.form.model.EnrollmentMode @@ -20,6 +21,7 @@ class EnrollmentRepositoryTest { private val conf: EnrollmentConfiguration = mock() private val enrollmentMode: EnrollmentMode = mock() private val enrolmentFormLabelsProvider: EnrollmentFormLabelsProvider = mock() + private val metadataIconProvider: MetadataIconProvider = mock() lateinit var repository: DataEntryRepository val programSection: ProgramSection = mock() @@ -56,6 +58,7 @@ class EnrollmentRepositoryTest { conf, enrollmentMode, enrolmentFormLabelsProvider, + metadataIconProvider, ) } diff --git a/form/src/test/java/org/dhis2/form/data/FieldUiModelTest.kt b/form/src/test/java/org/dhis2/form/data/FieldUiModelTest.kt deleted file mode 100644 index 1cb9a1c178..0000000000 --- a/form/src/test/java/org/dhis2/form/data/FieldUiModelTest.kt +++ /dev/null @@ -1,92 +0,0 @@ -package org.dhis2.form.data - -import org.dhis2.form.model.FieldUiModelImpl -import org.dhis2.form.model.OptionSetConfiguration -import org.hisp.dhis.android.core.common.ValueType -import org.hisp.dhis.android.core.option.Option -import org.junit.Test - -class FieldUiModelTest { - - @Test - fun `should set optionsToDisplay when there is optionsToHide`() { - val optionsToHide = listOf("1", "2") - val matrixOptionSetModel = fieldUiModel().also { - val conf = it.optionSetConfiguration - it.optionSetConfiguration = when (conf) { - is OptionSetConfiguration.BigOptionSet -> - conf.copy(optionsToHide = optionsToHide) - is OptionSetConfiguration.DefaultOptionSet -> - conf.copy(optionsToHide = optionsToHide) - null -> conf - } - } - assert( - matrixOptionSetModel.optionSetConfiguration?.optionsToDisplay() - ?.map { it.uid() } == listOf("3", "4", "5"), - ) - } - - @Test - fun `should set optionsToDisplay when there is optionsToShow`() { - val optionsInGroupToShow = listOf("1", "2") - val matrixOptionSetModel = fieldUiModel().also { - val conf = it.optionSetConfiguration - it.optionSetConfiguration = when (conf) { - is OptionSetConfiguration.BigOptionSet -> - conf.copy(optionsToShow = optionsInGroupToShow) - is OptionSetConfiguration.DefaultOptionSet -> - conf.copy(optionsToShow = optionsInGroupToShow) - null -> conf - } - } - assert( - matrixOptionSetModel.optionSetConfiguration?.optionsToDisplay() - ?.map { it.uid() } == listOf("1", "2"), - ) - } - - @Test - fun `should set optionsToDisplay when there are optionsToShow and optionsToHide`() { - val optionsToHide = listOf("1") - val optionsInGroupToShow = listOf("3", "5") - - val matrixOptionSetModel = fieldUiModel().also { - val conf = it.optionSetConfiguration - it.optionSetConfiguration = when (conf) { - is OptionSetConfiguration.BigOptionSet -> conf.copy( - optionsToHide = optionsToHide, - optionsToShow = optionsInGroupToShow, - ) - is OptionSetConfiguration.DefaultOptionSet -> conf.copy( - optionsToHide = optionsToHide, - optionsToShow = optionsInGroupToShow, - ) - null -> conf - } - } - assert( - matrixOptionSetModel.optionSetConfiguration?.optionsToDisplay() - ?.map { it.uid() } == listOf("3", "5"), - ) - } - - private fun fieldUiModel() = FieldUiModelImpl( - "uid", - label = "label", - valueType = ValueType.TEXT, - optionSetConfiguration = OptionSetConfiguration.config(5) { - OptionSetConfiguration.OptionConfigData( - listOf( - Option.builder().uid("1").build(), - Option.builder().uid("2").build(), - Option.builder().uid("3").build(), - Option.builder().uid("4").build(), - Option.builder().uid("5").build(), - ), - emptyMap(), - ) - }, - autocompleteList = null, - ) -} diff --git a/form/src/test/java/org/dhis2/form/data/FormRepositoryIntegrationTest.kt b/form/src/test/java/org/dhis2/form/data/FormRepositoryIntegrationTest.kt index fb2d09ab5c..dd5d9b4a7a 100644 --- a/form/src/test/java/org/dhis2/form/data/FormRepositoryIntegrationTest.kt +++ b/form/src/test/java/org/dhis2/form/data/FormRepositoryIntegrationTest.kt @@ -1,6 +1,7 @@ package org.dhis2.form.data import org.dhis2.commons.prefs.PreferenceProvider +import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.form.data.metadata.EnrollmentConfiguration import org.dhis2.form.model.EnrollmentMode import org.dhis2.form.model.SectionUiModelImpl @@ -43,6 +44,7 @@ class FormRepositoryIntegrationTest { on { provideEnrollmentOrgUnitLabel() } doReturn "OrgUnit label" on { provideEnrollmentDataSectionLabel(any()) } doReturn "Enrollment data" } + private val metadataIconProvider: MetadataIconProvider = mock() private val program: Program = mock { on { uid() } doReturn "programUid" @@ -173,6 +175,7 @@ class FormRepositoryIntegrationTest { conf, enrollmentMode, enrollmentFormLabelsProvider, + metadataIconProvider, ) return FormRepositoryImpl( diff --git a/form/src/test/java/org/dhis2/form/data/GeometryControllerTest.kt b/form/src/test/java/org/dhis2/form/data/GeometryControllerTest.kt index 8f326c3425..294f376828 100644 --- a/form/src/test/java/org/dhis2/form/data/GeometryControllerTest.kt +++ b/form/src/test/java/org/dhis2/form/data/GeometryControllerTest.kt @@ -1,7 +1,5 @@ package org.dhis2.form.data -import org.dhis2.form.ui.event.RecyclerViewUiEvents -import org.dhis2.form.ui.intent.FormIntent import org.hisp.dhis.android.core.common.FeatureType import org.junit.Assert.assertTrue import org.junit.Before @@ -45,37 +43,4 @@ class GeometryControllerTest { result.coordinates()?.isNotEmpty() == true, ) } - - @Test - fun `Should return coordinates callback`() { - var currentCallback: Int = -1 - val coordinateCallback = controller.getCoordinatesCallback( - { - currentCallback = 0 - }, - { currentCallback = 1 }, - { _, _, _ -> currentCallback = 2 }, - ) - - coordinateCallback.intent( - FormIntent.SaveCurrentLocation( - uid = "fieldUid", - value = null, - featureType = "none", - ), - ) - assertTrue(currentCallback == 0) - coordinateCallback.recyclerViewUiEvents( - RecyclerViewUiEvents.RequestCurrentLocation("fieldUid"), - ) - assertTrue(currentCallback == 1) - coordinateCallback.recyclerViewUiEvents( - RecyclerViewUiEvents.RequestLocationByMap( - "fieldUid", - FeatureType.POINT, - null, - ), - ) - assertTrue(currentCallback == 2) - } } diff --git a/form/src/test/java/org/dhis2/form/integration/ProgramRulesTest.kt b/form/src/test/java/org/dhis2/form/integration/ProgramRulesTest.kt index 1c5d88d8d1..610a6809fa 100644 --- a/form/src/test/java/org/dhis2/form/integration/ProgramRulesTest.kt +++ b/form/src/test/java/org/dhis2/form/integration/ProgramRulesTest.kt @@ -6,6 +6,8 @@ import io.reactivex.Flowable import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain @@ -31,9 +33,7 @@ import org.dhis2.form.ui.FormViewModel import org.dhis2.form.ui.intent.FormIntent import org.dhis2.mobileProgramRules.RuleEngineHelper import org.hisp.dhis.android.core.D2 -import org.hisp.dhis.android.core.common.ObjectWithUid import org.hisp.dhis.android.core.common.ValueType -import org.hisp.dhis.android.core.option.Option import org.hisp.dhis.android.core.program.ProgramRuleActionType import org.hisp.dhis.rules.models.RuleAction import org.hisp.dhis.rules.models.RuleEffect @@ -48,6 +48,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever class ProgramRulesTest { @@ -106,7 +107,10 @@ class ProgramRulesTest { invocationOnMock.getArgument(0) as FieldUiModel } - whenever(formValueStore.save(any(), anyOrNull(), anyOrNull())) doReturn StoreResult("", ValueStoreResult.VALUE_CHANGED) + whenever(formValueStore.save(any(), anyOrNull(), anyOrNull())) doReturn StoreResult( + "", + ValueStoreResult.VALUE_CHANGED, + ) repository = FormRepositoryImpl( formValueStore = formValueStore, @@ -356,6 +360,15 @@ class ProgramRulesTest { ), ) + whenever( + dataEntryRepository.options( + any(), + any(), + any(), + any(), + ), + ) doReturn Pair(MutableStateFlow(""), emptyFlow()) + val intent = FormIntent.OnSave( uid = "uid004", value = "value04", @@ -370,15 +383,12 @@ class ProgramRulesTest { formViewModel.submitIntent(intent) advanceUntilIdle() - val items = formViewModel.items.value ?: emptyList() - - val optionsToDisplay: List<Option> = - items.find { it.uid == "uid006" }!!.optionSetConfiguration!!.optionsToDisplay() - - assertTrue(optionsToDisplay.size == 3) - assertTrue(optionsToDisplay[0].uid() == "Option2") - assertTrue(optionsToDisplay[1].uid() == "Option3") - assertTrue(optionsToDisplay[2].uid() == "Option4") + verify(dataEntryRepository).options( + optionSetUid = "optionSetUid", + optionsToHide = emptyList(), + optionGroupsToHide = emptyList(), + optionGroupsToShow = listOf("optionGroupId"), + ) } @OptIn(ExperimentalCoroutinesApi::class) @@ -400,6 +410,15 @@ class ProgramRulesTest { ), ) + whenever( + dataEntryRepository.options( + any(), + any(), + any(), + any(), + ), + ) doReturn Pair(MutableStateFlow(""), emptyFlow()) + val intent = FormIntent.OnSave( uid = "uid004", value = "value04", @@ -408,21 +427,18 @@ class ProgramRulesTest { whenever(formValueStore.deleteOptionValueIfSelected(any(), any())) doReturn StoreResult( "uid007", - ValueStoreResult.VALUE_CHANGED, + ValueStoreResult.VALUE_HAS_NOT_CHANGED, ) formViewModel.submitIntent(intent) advanceUntilIdle() - val items = formViewModel.items.value ?: emptyList() - - val optionsToDisplay: List<Option> = - items.last().optionSetConfiguration!!.optionsToDisplay() - - optionsToDisplay.forEach { - assert(it.uid() != "Option2") - } - assert(optionsToDisplay.size == 4) + verify(dataEntryRepository).options( + optionSetUid = "optionSetUid", + optionsToHide = listOf("Option2"), + optionGroupsToHide = emptyList(), + optionGroupsToShow = emptyList(), + ) } private fun provideItemList() = listOf( @@ -486,11 +502,10 @@ class ProgramRulesTest { value = "value06", label = "field6", valueType = ValueType.MULTI_TEXT, - optionSetConfiguration = OptionSetConfiguration.DefaultOptionSet( - options = "optionSetUid".listOfOptions(), - optionsToHide = listOf(), - optionsToShow = listOf("Option2", "Option3", "Option4"), - optionMetadataIcon = mapOf(), + optionSetConfiguration = OptionSetConfiguration( + MutableStateFlow(""), + {}, + emptyFlow(), ), autocompleteList = null, programStageSection = "section2", @@ -501,31 +516,14 @@ class ProgramRulesTest { value = "value07", label = "field7", valueType = ValueType.MULTI_TEXT, - optionSetConfiguration = OptionSetConfiguration.DefaultOptionSet( - options = "optionSetUid".listOfOptions(), - optionsToHide = listOf("Option2"), - optionsToShow = listOf(), - optionMetadataIcon = mapOf(), + optionSetConfiguration = OptionSetConfiguration( + MutableStateFlow(""), + {}, + emptyFlow(), ), autocompleteList = null, programStageSection = "section2", optionSet = "optionSetUid", ), ) - - private fun String.listOfOptions(): List<Option> { - val optionSetUid = ObjectWithUid.create(this) - val options: MutableList<Option> = mutableListOf() - repeat(5) { index -> - options.add( - Option.builder() - .uid("Option$index") - .displayName("name$index") - .code("code$index") - .optionSet(optionSetUid) - .build(), - ) - } - return options.toList() - } } diff --git a/form/src/test/java/org/dhis2/form/model/OptionSetDialogViewModelTest.kt b/form/src/test/java/org/dhis2/form/model/OptionSetDialogViewModelTest.kt index 3331da2c34..b414d5713e 100644 --- a/form/src/test/java/org/dhis2/form/model/OptionSetDialogViewModelTest.kt +++ b/form/src/test/java/org/dhis2/form/model/OptionSetDialogViewModelTest.kt @@ -91,67 +91,6 @@ class OptionSetDialogViewModelTest { ) } - @Test - fun `Should search and filter options to hide`() { - val optionsToHide = listOf("Option1") - whenever(field.optionSetConfiguration) doReturn OptionSetConfiguration.DefaultOptionSet( - options = emptyList(), - optionsToHide = optionsToHide, - optionsToShow = emptyList(), - emptyMap(), - ) - viewModel.onSearchingOption("test") - testingDispatcher.scheduler.advanceUntilIdle() - assertTrue(viewModel.searchValue.value == "test") - verify(searchOptionSetOption, times(1))( - optionSetUid, - "test", - emptyList(), - optionsToHide, - ) - } - - @Test - fun `Should search and filter options to show`() { - val optionsToShow = listOf("Option1") - whenever(field.optionSetConfiguration) doReturn OptionSetConfiguration.DefaultOptionSet( - options = emptyList(), - optionsToHide = emptyList(), - optionsToShow = optionsToShow, - emptyMap(), - ) - viewModel.onSearchingOption("test") - assertTrue(viewModel.searchValue.value == "test") - testingDispatcher.scheduler.advanceUntilIdle() - verify(searchOptionSetOption, times(1))( - optionSetUid, - "test", - optionsToShow, - emptyList(), - ) - } - - @Test - fun `Should search and filter options to show and hide`() { - val optionsToShow = listOf("Option1") - val optionsToHide = listOf("Option1") - whenever(field.optionSetConfiguration) doReturn OptionSetConfiguration.DefaultOptionSet( - options = emptyList(), - optionsToHide = optionsToHide, - optionsToShow = optionsToShow, - emptyMap(), - ) - viewModel.onSearchingOption("test") - testingDispatcher.scheduler.advanceUntilIdle() - assertTrue(viewModel.searchValue.value == "test") - verify(searchOptionSetOption, times(1))( - optionSetUid, - "test", - optionsToShow, - optionsToHide, - ) - } - private val mockedOptions = mutableListOf<Option>().apply { repeat(times = 5) { index -> add( diff --git a/form/src/test/java/org/dhis2/form/ui/DataEntryIntegrationTest.kt b/form/src/test/java/org/dhis2/form/ui/DataEntryIntegrationTest.kt index 56496f09d2..5e263f6da7 100644 --- a/form/src/test/java/org/dhis2/form/ui/DataEntryIntegrationTest.kt +++ b/form/src/test/java/org/dhis2/form/ui/DataEntryIntegrationTest.kt @@ -3,9 +3,13 @@ package org.dhis2.form.ui import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.databinding.ObservableField import androidx.lifecycle.Observer +import androidx.paging.PagingData import io.reactivex.Flowable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain @@ -30,6 +34,7 @@ import org.dhis2.form.ui.provider.DisplayNameProvider import org.dhis2.form.ui.provider.LegendValueProvider import org.dhis2.form.ui.validation.FieldErrorMessageProvider import org.dhis2.mobileProgramRules.RuleEngineHelper +import org.dhis2.ui.MetadataIconData import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.option.Option import org.junit.Before @@ -253,7 +258,8 @@ class DataEntryIntegrationTest { observedItems.last().find { it.uid == "EVENT_ORG_UNIT_UID" }?.value == "g8upMTyEZGZ", ) assert( - observedItems.last().find { it.uid == "INPUT_NUMBER_WITH_LEGEND_UID" }?.legend == legendValueItem, + observedItems.last() + .find { it.uid == "INPUT_NUMBER_WITH_LEGEND_UID" }?.legend == legendValueItem, ) assert( observedItems.last().find { it.uid == "qrur9Dvnyt5" }?.value == "20", @@ -267,6 +273,7 @@ class DataEntryIntegrationTest { } private fun provideMalariaCaseRegistrationEventItems(): List<FieldUiModel> { + val optionSearchFlow = MutableStateFlow("") return listOf( SectionUiModelImpl( uid = "EVENT_DETAILS_SECTION_UID", @@ -334,24 +341,37 @@ class DataEntryIntegrationTest { label = "Gender", programStageSection = "EVENT_DATA_SECTION_UID", autocompleteList = emptyList(), - optionSetConfiguration = OptionSetConfiguration.DefaultOptionSet( - options = listOf( - Option.builder() - .uid("rBvjJYbMCVx") - .code("Male") - .displayName("Male") - .name("Male") - .sortOrder(1) - .build(), - Option.builder() - .uid("Mnp3oXrpAbK") - .code("Female") - .displayName("Female") - .name("Female") - .sortOrder(2) - .build(), - ), - optionMetadataIcon = emptyMap(), + optionSetConfiguration = OptionSetConfiguration( + optionSearchFlow, + { optionSearchFlow.value = it }, + optionSearchFlow.flatMapLatest { + flow { + PagingData.from( + listOf( + OptionSetConfiguration.OptionData( + Option.builder() + .uid("rBvjJYbMCVx") + .code("Male") + .displayName("Male") + .name("Male") + .sortOrder(1) + .build(), + MetadataIconData.defaultIcon(), + ), + OptionSetConfiguration.OptionData( + Option.builder() + .uid("Mnp3oXrpAbK") + .code("Female") + .displayName("Female") + .name("Female") + .sortOrder(2) + .build(), + MetadataIconData.defaultIcon(), + ), + ), + ) + } + }, ), valueType = ValueType.MULTI_TEXT, mandatory = true, diff --git a/form/src/test/java/org/dhis2/form/ui/event/UiEventFactoryImplTest.kt b/form/src/test/java/org/dhis2/form/ui/event/UiEventFactoryImplTest.kt deleted file mode 100644 index c30f4db510..0000000000 --- a/form/src/test/java/org/dhis2/form/ui/event/UiEventFactoryImplTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -package org.dhis2.form.ui.event - -import org.dhis2.form.model.FieldUiModelImpl -import org.dhis2.form.model.UiEventType -import org.hamcrest.MatcherAssert.assertThat -import org.hisp.dhis.android.core.common.ValueType -import org.junit.Test - -class UiEventFactoryImplTest { - - private var uiEventFactory: UiEventFactory? = null - - @Test - fun `Should return UiEvent OpenCustomCalendar for ValueType DATE`() { - uiEventFactory = provideEventForType(ValueType.DATE) - - val event = uiEventFactory?.generateEvent( - value = "2021-09-27", - uiEventType = UiEventType.DATE_TIME, - fieldUiModel = provideFieldUiModel(), - ) - assertThat( - "Event is OpenCustomCalendar", - event is RecyclerViewUiEvents.OpenCustomCalendar, - ) - } - - @Test - fun `Should return UiEvent OpenCustomCalendar for ValueType DATETIME`() { - uiEventFactory = provideEventForType(ValueType.DATETIME) - - val event = uiEventFactory?.generateEvent( - value = "2021-09-27T10:20", - uiEventType = UiEventType.DATE_TIME, - fieldUiModel = provideFieldUiModel(), - ) - assertThat( - "Event is OpenCustomCalendar", - event is RecyclerViewUiEvents.OpenCustomCalendar, - ) - } - - @Test - fun `Should return UiEvent OpenCustomCalendar for ValueType TIME`() { - uiEventFactory = provideEventForType(ValueType.TIME) - - val event = uiEventFactory?.generateEvent( - value = "10:20", - uiEventType = UiEventType.DATE_TIME, - fieldUiModel = provideFieldUiModel(), - ) - assertThat( - "Event is OpenTimePicker", - event is RecyclerViewUiEvents.OpenTimePicker, - ) - } - - private fun provideEventForType(valueType: ValueType) = UiEventFactoryImpl( - uid = "uid", - label = "label", - description = "description", - valueType = valueType, - true, - null, - ) - - private fun provideFieldUiModel() = FieldUiModelImpl( - uid = "uid", - label = "label", - valueType = ValueType.TEXT, - optionSetConfiguration = null, - autocompleteList = null, - ) -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe0f21ba6b..bbe4029d56 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ gradle = "8.6.1" kotlin = '2.0.20' hilt = '2.47' jacoco = '0.8.10' -designSystem = "0.4.0" +designSystem = "0.4.0.1-SNAPSHOT" dhis2sdk = "1.11.0.1-SNAPSHOT" ruleEngine = "3.0.0" expressionParser = "1.1.0" From f0fddc88bf6dfea2bb3b0f724073134d22945879 Mon Sep 17 00:00:00 2001 From: Xavier Molloy <xavi@dhis2.org> Date: Tue, 3 Dec 2024 12:08:36 +0100 Subject: [PATCH 06/11] build: update sdk and mobile ui to release artifacts --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bbe4029d56..187b14725f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ gradle = "8.6.1" kotlin = '2.0.20' hilt = '2.47' jacoco = '0.8.10' -designSystem = "0.4.0.1-SNAPSHOT" -dhis2sdk = "1.11.0.1-SNAPSHOT" +designSystem = "0.4.0.1" +dhis2sdk = "1.11.0.1" ruleEngine = "3.0.0" expressionParser = "1.1.0" appcompat = "1.6.1" From ffd2b1baa4eed8db78e97817315ad382ab5b7130 Mon Sep 17 00:00:00 2001 From: Xavier Molloy <xavi@dhis2.org> Date: Tue, 3 Dec 2024 12:12:05 +0100 Subject: [PATCH 07/11] build: update version code --- 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 187b14725f..9d65f0672a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] sdk = "34" minSdk = "21" -vCode = "137" +vCode = "138" vName = "3.1.0.1" gradle = "8.6.1" kotlin = '2.0.20' From c6c0d837c658165c282d34e3d21eab89f19801de Mon Sep 17 00:00:00 2001 From: Xavier Molloy <xavi@dhis2.org> Date: Tue, 3 Dec 2024 17:38:27 +0100 Subject: [PATCH 08/11] build: 3.1.0.1 release --- RELEASE.md | 59 ++++------------------------------------- whatsnew/whatsnew-en-US | 14 +++++----- 2 files changed, 11 insertions(+), 62 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index a42609a8c0..39a76ba947 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,58 +1,9 @@ +# Release notes - Android App for DHIS2 - 3.1.0.1 -## NEW FUNCTIONALITY AND WEB PARITY +### Bug -**New Capture Coordinates process:** The 3.1 version introduces a list of new features designed to enhance the capture coordinates process. These improvements aim to provide greater accuracy, flexibility, and control over location data capture. -- **Accuracy:** The capture coordinates process now includes a feature that displays the precision of the captured location. This allows users to see how accurate their location data is in real-time. This parameter can also be restricted using the Android Settings WebApp. -- **Search Functionality:** A new search functionality has been added, allowing users to look up specific locations by name or address. Users are also able to navigate through the map and perform area searches to discover other locations within a specified region. -- **Block Manual Capture:** Using the Android Settings Web App, administrators now have the option to block manual location capture. When this setting is enabled, users can only capture the current location and cannot manually select or search a different one. This ensures that location data remains consistent and accurate. +[ANDROAPP-6653](https://dhis2.atlassian.net/browse/ANDROAPP-6653) Large option sets freeze the app -[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-6330) | [Card1](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-disabled-manual-capture.png) | [Card2](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-map-accuracy.png) | [Card3](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-map-search.png) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_common_features_map_accuracy) +[ANDROAPP-6665](https://dhis2.atlassian.net/browse/ANDROAPP-6665) Filters persists when exiting the program or data set -**Improve transfers flow:** Significant enhancements to the transfer flow, aimed at making the process more user-friendly and transparent. The transfer button has been moved to a more accessible location within the three dot menu in the TEI Dashboard, ensuring that users can easily find and initiate transfers without unnecessary navigation. It also has introduced new dialogs throughout the transfer process. These dialogs provide clear, step-by-step guidance, ensuring that users understand each part of the process. - -[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-6228) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_transfers) - -**New relationship section:** Major updates have been made in the relationship tabs, enhancing both functionality and user experience. Relationship cards have been updated with the new design to offer a more intuitive and visually appealing experience. The new design emphasizes clarity and usability, making it easier to view and manage relationships at a glance. - -To prevent accidental deletions and enhance user control, a new confirmation dialog also has been added when deleting a relationship. This dialog will prompt users to confirm their action, ensuring that relationships are only deleted intentionally. - -[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-6362) | [Card1](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-relationship-sections.png) | [Card2](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-new-relationship-cards.png) | [Card3](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-relationship-deletion.png) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_common_features_relationships) - -**Sort of unique attributes in the search screen:** Aimed at aligning it with the web instance for a more consistent user experience, this version of the Android app, by default, sorts the unique attributes (QR, barcode) at the top of the list of searchable attributes. Users can quickly and easily find the attributes for a more exact search. - -[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-6039) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_unique_qrBar_search) - -**Support of biometric dialog:** An enhancement to the biometric authentication feature has been made in 3.1.0. When there is only one account configured, the user can configure biometric authentication (fingerprint or face ID). - -[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-4676) | [Documentation](https://docs.dhis2.org/en/use/android-app/android-specific-features.html#capture_app_generic_biometrics_login) - -**Line Listing improvements:** This version of the Android App introduces support for the Category Option Dimension in line listings. This enhancement enables users to apply category options directly within line listings to filter data according to precise criteria, improving data exploration and decision-making processes. This feature greatly enhances the versatility and utility of line listings, empowering users to perform more sophisticated reporting. - -Additionally, it has been improved the text alignment within the Line Listing tables to support left alignment. This enhancement ensures better readability and a cleaner presentation of data, making it easier for users to review and analyze their information quickly. - -[Jira1](https://dhis2.atlassian.net/browse/ANDROAPP-6353) | [Jira2](https://dhis2.atlassian.net/browse/ANDROAPP-6121) | [Documentation](https://docs.dhis2.org/en/use/android-app/visual-configurations.html#capture_app_visual_event_visualizations) - -## USER EXPERIENCE - -**Responsive Home Screen:** In this Android App version a new dynamic home screen that adapts to the number of programs available has been implemented. This update replaces the old static list that didn’t adjust to the screen, providing a more responsive and user-friendly interface.The responsive design makes better use of screen real estate, providing a more engaging and functional home screen layout. - -[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-5394) | [Card](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-responsive-home-screen.png) | [Documentation](https://docs.dhis2.org/en/use/android-app/android-specific-features.html#capture_app_home) - -**Scheduled events dialog:** As a continuation of the new schedule dialog introduced in the version 3.0, a new intuitive and user-friendly schedule dialog has been implemented to enhance the overall user experience, making it easier to book, reschedule, or cancel events. - -[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-6229) | [Card1](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-schedule-new.png) | [Card2](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-enter-cancel-reschedule.png) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_scheduling) - -**Improve menus and navigation bar:** A revamped of the menus and navigation bar has been made to be more user-friendly and accessible. It includes a cleaner, more modern look that improves readability and usability. These updates are designed to provide a more efficient and enjoyable user experience. - -[Jira1](https://dhis2.atlassian.net/browse/ANDROAPP-6036) | [Jira2](https://dhis2.atlassian.net/browse/ANDROAPP-6113) | [Card1](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-menu.png) | [Card2](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-navigation-bar.png) | [Documentation -](https://docs.dhis2.org/en/use/android-app/visual-configurations.html#capture_app_visual_menu_bars_update) -## CROSS PRODUCT - -**Support for customized Tracker terminology:** Some DHIS2 terminology is not familiar for the end users. For this reason, we are gradually enabling the possibility to customize it to each particular use case. In this version, the term "event" (program label context) is customizable. The admin user will be able to configure it for each program using the Maintenance App, and the Android Capture App will display the customized term instead of the generic one. - -[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-5947) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_common_features_customized_terminology) - ---- - -##### **DETAILS** -You can find the list of all new features and all bugs fixed in 3.1.0 [here.](https://dhis2.atlassian.net/projects/ANDROAPP/versions/10851/tab/release-report-all-issues) +[ANDROAPP-6691](https://dhis2.atlassian.net/browse/ANDROAPP-6691) NullPointerException: Dataset table \ No newline at end of file diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index 28fa4b8f3d..d8699e3b3a 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,8 +1,6 @@ -Capture Coordinates: Improved accuracy, search, and manual capture. -Transfer Flows: Easier access and step-by-step guidance. -Relationship Section: Redesigned module. -Unique Attribute Sorting: QR/barcodes sorted for quick searches. -Biometric Login: For single accounts. -Line Listings: Category Option filters, text improvements. -UX Updates: Dynamic home screen, revamped menus, and scheduling dialog. -Custom Tracker Terminology: Tailor terms like “event” per program. \ No newline at end of file +This is a patch version that fixes: +- ANDROAPP-6653 Large option sets freeze the app +- ANDROAPP-6665 Filters persists when exiting the program or data set +- ANDROAPP-6691 NullPointerException: Dataset table + +You can find all the details on Jira and Github. \ No newline at end of file From 2ee4dbf3b849984b1a9317deac190a70b26df0bf Mon Sep 17 00:00:00 2001 From: Pablo Pajuelo Cabezas <pablo@dhis2.org> Date: Wed, 4 Dec 2024 11:28:43 +0100 Subject: [PATCH 09/11] fix: options not hiding/showing with program rules --- .../org/dhis2/form/data/FormRepositoryImpl.kt | 18 ++++++++++-- .../data/metadata/FormBaseConfiguration.kt | 28 +++++++++++++++---- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt b/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt index ac791f613d..0895fd5083 100644 --- a/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt +++ b/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt @@ -48,6 +48,7 @@ class FormRepositoryImpl( private var runDataIntegrity: Boolean = false private var calculationLoop: Int = 0 private var backupList: List<FieldUiModel> = emptyList() + private val fieldsWithOptionEffects = mutableListOf<FieldUiModel>() private val disableCollapsableSections: Boolean? = dataEntryRepository.disableCollapsableSections() @@ -522,9 +523,22 @@ class FormRepositoryImpl( } } + fieldsWithOptionEffects.forEach { field -> + field.optionSet?.let { optionSetUid -> + fetchOptions(field.uid, optionSetUid) + } + } + + fieldsWithOptionEffects.clear() + ruleEffectsResult?.fieldsWithOptionEffects()?.forEach { fieldWithOptionEffect -> - itemList.find { it.uid == fieldWithOptionEffect }?.let { - it.optionSet?.let { optionSetUid -> fetchOptions(it.uid, optionSetUid) } + val item = itemList.find { it.uid == fieldWithOptionEffect } + + item?.let { field -> + field.optionSet?.let { optionSetUid -> + fetchOptions(field.uid, optionSetUid) + } + fieldsWithOptionEffects.add(field) } } diff --git a/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt b/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt index 511e379d38..cacc2cc5ea 100644 --- a/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt +++ b/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt @@ -41,12 +41,28 @@ open class FormBaseConfiguration(private val d2: D2) { .getPagingData(10) }.map { pagingData -> pagingData.filter { option -> - !optionsToHide.contains(option.uid()) && - !optionGroupsToHide.contains(option.uid()) && - ( - optionGroupsToShow.isEmpty() || - optionGroupsToShow.contains(option.uid()) - ) + + val optionInGroupToHide = d2.optionModule().optionGroups() + .withOptions() + .byUid().`in`(optionGroupsToHide) + .blockingGet().find { optionGroup -> + optionGroup.options()?.map { it.uid() }?.contains(option.uid()) == true + } != null + + val optionInGroupToShow = d2.optionModule().optionGroups() + .withOptions() + .byUid().`in`(optionGroupsToShow) + .blockingGet().find { optionGroup -> + optionGroup.options()?.map { it.uid() }?.contains(option.uid()) == true + } != null + + val hideOption = if (optionGroupsToShow.isEmpty()) { + optionsToHide.contains(option.uid()) || optionInGroupToHide + } else { + !optionInGroupToShow + } + + !hideOption } } } From ba3b8cdfe90b5b868560025cf24f0c710f4fdf4a Mon Sep 17 00:00:00 2001 From: Pablo Pajuelo Cabezas <pablo@dhis2.org> Date: Wed, 4 Dec 2024 12:42:17 +0100 Subject: [PATCH 10/11] fix: remove code smells --- .../data/metadata/FormBaseConfiguration.kt | 8 ++-- .../dhis2/form/data/FormRepositoryImplTest.kt | 40 ++++++++++++++++++- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt b/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt index cacc2cc5ea..6155035ccc 100644 --- a/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt +++ b/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt @@ -45,16 +45,16 @@ open class FormBaseConfiguration(private val d2: D2) { val optionInGroupToHide = d2.optionModule().optionGroups() .withOptions() .byUid().`in`(optionGroupsToHide) - .blockingGet().find { optionGroup -> + .blockingGet().any { optionGroup -> optionGroup.options()?.map { it.uid() }?.contains(option.uid()) == true - } != null + } val optionInGroupToShow = d2.optionModule().optionGroups() .withOptions() .byUid().`in`(optionGroupsToShow) - .blockingGet().find { optionGroup -> + .blockingGet().any { optionGroup -> optionGroup.options()?.map { it.uid() }?.contains(option.uid()) == true - } != null + } val hideOption = if (optionGroupsToShow.isEmpty()) { optionsToHide.contains(option.uid()) || optionInGroupToHide diff --git a/form/src/test/java/org/dhis2/form/data/FormRepositoryImplTest.kt b/form/src/test/java/org/dhis2/form/data/FormRepositoryImplTest.kt index abc6debd58..09fec26410 100644 --- a/form/src/test/java/org/dhis2/form/data/FormRepositoryImplTest.kt +++ b/form/src/test/java/org/dhis2/form/data/FormRepositoryImplTest.kt @@ -2,11 +2,14 @@ package org.dhis2.form.data import androidx.databinding.ObservableField import io.reactivex.Flowable +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.form.model.ActionType import org.dhis2.form.model.EventCategory import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.FieldUiModelImpl +import org.dhis2.form.model.OptionSetConfiguration import org.dhis2.form.model.RowAction import org.dhis2.form.model.SectionUiModelImpl import org.dhis2.form.model.StoreResult @@ -30,6 +33,7 @@ import org.junit.Before import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.atLeast import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturnConsecutively import org.mockito.kotlin.mock @@ -200,6 +204,14 @@ class FormRepositoryImplTest { mutableMapOf(Pair("field", "uid001")), ), ), + RuleEffect( + "rule2", + RuleAction( + "option1", + ProgramRuleActionType.HIDEOPTION.name, + mutableMapOf(Pair("field", "uid004")), + ), + ), ) whenever(dataEntryRepository.isEvent()) doReturn true @@ -215,12 +227,21 @@ class FormRepositoryImplTest { fieldsToUpdate = listOf(FieldWithNewValue("uid001", "newValue")), configurationErrors = emptyList(), stagesToHide = emptyList(), - optionsToHide = emptyMap(), + optionsToHide = mapOf( + "uid004" to listOf("option1"), + ), optionGroupsToHide = emptyMap(), optionGroupsToShow = emptyMap(), ) - verify(rulesUtilsProvider, times(1)).applyRuleEffects( + whenever(dataEntryRepository.options(any(), any(), any(), any()))doReturn Pair( + MutableStateFlow(""), + emptyFlow(), + ) + + repository.composeList() + + verify(rulesUtilsProvider, atLeast(1)).applyRuleEffects( any(), any(), any(), @@ -441,6 +462,21 @@ class FormRepositoryImplTest { optionSetConfiguration = null, autocompleteList = null, ), + FieldUiModelImpl( + uid = "uid004", + value = null, + label = "field4", + valueType = ValueType.TEXT, + programStageSection = "section1", + uiEventFactory = null, + optionSet = "optionSetUid", + optionSetConfiguration = OptionSetConfiguration( + MutableStateFlow(""), + {}, + emptyFlow(), + ), + autocompleteList = null, + ), ) private fun section1() = SectionUiModelImpl( From b72122ff8fa8a47b90353aa47602d5562165299b Mon Sep 17 00:00:00 2001 From: Xavier Molloy <xavi@dhis2.org> Date: Thu, 5 Dec 2024 08:41:14 +0100 Subject: [PATCH 11/11] fix: [ANDROAPP-6703] refresh selected item when inputCardData list is modified --- .../ui/provider/inputfield/FieldProvider.kt | 7 ------ .../inputfield/MatrixInputProvider.kt | 2 -- .../inputfield/MatrixSequentialUtilites.kt | 25 +------------------ .../inputfield/SequentialInputProvider.kt | 3 --- 4 files changed, 1 insertion(+), 36 deletions(-) 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 7b415964ef..2515ddf353 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 @@ -1,6 +1,5 @@ package org.dhis2.form.ui.provider.inputfield -import android.content.Context import android.content.Intent import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.focusable @@ -23,7 +22,6 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.layout.onSizeChanged -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 @@ -68,7 +66,6 @@ fun FieldProvider( focusManager: FocusManager, onNextClicked: () -> Unit, ) { - val context = LocalContext.current val bringIntoViewRequester = remember { BringIntoViewRequester() } val focusRequester = remember { FocusRequester() } var visibleArea by remember { mutableStateOf(Rect.Zero) } @@ -112,7 +109,6 @@ fun FieldProvider( inputStyle = inputStyle, fieldUiModel = fieldUiModel, intentHandler = intentHandler, - context = context, fetchOptions = { intentHandler( FormIntent.FetchOptions( @@ -484,7 +480,6 @@ fun ProvideByOptionSet( inputStyle: InputStyle, fieldUiModel: FieldUiModel, intentHandler: (FormIntent) -> Unit, - context: Context, fetchOptions: () -> Unit, ) { when (fieldUiModel.renderingType) { @@ -516,7 +511,6 @@ fun ProvideByOptionSet( inputStyle = inputStyle, fieldUiModel = fieldUiModel, intentHandler = intentHandler, - context = context, ) } @@ -526,7 +520,6 @@ fun ProvideByOptionSet( inputStyle = inputStyle, fieldUiModel = fieldUiModel, intentHandler = intentHandler, - context = context, ) } diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixInputProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixInputProvider.kt index b926d87596..ed0adf2a36 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixInputProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixInputProvider.kt @@ -1,6 +1,5 @@ package org.dhis2.form.ui.provider.inputfield -import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue @@ -21,7 +20,6 @@ internal fun ProvideMatrixInput( modifier: Modifier, inputStyle: InputStyle, fieldUiModel: FieldUiModel, - context: Context, intentHandler: (FormIntent) -> Unit, ) { val inputCardDataList: MutableList<ImageCardData> = mutableListOf() diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixSequentialUtilites.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixSequentialUtilites.kt index 6fbe9b727d..a55f3ebcad 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixSequentialUtilites.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixSequentialUtilites.kt @@ -4,31 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import org.dhis2.form.model.FieldUiModel -import org.dhis2.ui.MetadataIconData -import org.hisp.dhis.android.core.option.Option import org.hisp.dhis.mobile.ui.designsystem.component.ImageCardData -@Composable -fun rememberInputCardList( - options: List<Option>?, - optionMetadataIconMap: Map<String, MetadataIconData>?, -) = remember(options) { - options?.map { option -> - val metadataIconData = - optionMetadataIconMap?.get(option.uid()) ?: throw IllegalArgumentException() - - var icon = option.style().icon() ?: "dhis2_dhis2_logo_positive" - if (!icon.startsWith("dhis2_")) { - icon = "dhis2_$icon" - } - imageCardDataWithUidAndLabel( - metadataIconData.imageCardData, - option.code() ?: "", - option.displayName() ?: "", - ) - } ?: emptyList() -} - fun imageCardDataWithUidAndLabel( imageCardData: ImageCardData, optionCode: String, @@ -42,7 +19,7 @@ fun imageCardDataWithUidAndLabel( @Composable fun rememberSelectedOption(fieldUiModel: FieldUiModel, inputCardDataList: List<ImageCardData>) = - remember(fieldUiModel.displayName) { + remember(inputCardDataList, fieldUiModel.displayName) { mutableStateOf( inputCardDataList.find { it.uid == fieldUiModel.displayName || it.label == fieldUiModel.displayName }, ) diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SequentialInputProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SequentialInputProvider.kt index acdd7e235a..a1036ceef8 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SequentialInputProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SequentialInputProvider.kt @@ -1,6 +1,5 @@ package org.dhis2.form.ui.provider.inputfield -import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue @@ -21,7 +20,6 @@ internal fun ProvideSequentialInput( modifier: Modifier, inputStyle: InputStyle, fieldUiModel: FieldUiModel, - context: Context, intentHandler: (FormIntent) -> Unit, ) { val inputCardDataList: MutableList<ImageCardData> = mutableListOf() @@ -38,7 +36,6 @@ internal fun ProvideSequentialInput( ) } } - var matrixSelectedItem by rememberSelectedOption( fieldUiModel = fieldUiModel, inputCardDataList = inputCardDataList,