From e4982a9c2ba6724c969300bdbb2454b9d112d9f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Miguel=20Rubio?= Date: Tue, 27 Aug 2024 00:07:16 +0200 Subject: [PATCH 01/29] fix: [ANDROAPP-6392] fix typo in DateTimeUtilsTest (#289) --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 78785e629..f3cb3a214 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -version = "0.3.0-SNAPSHOT" +version = "0.4.0-SNAPSHOT" group = "org.hisp.dhis.mobile" plugins { From c6dfb422d9274159b261431d113aadadea077fa9 Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Tue, 6 Aug 2024 13:23:44 +0530 Subject: [PATCH 02/29] Disable `Save` button when no org is selected --- .../hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt index 05b151a06..424f1abe7 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt @@ -150,6 +150,7 @@ fun OrgBottomSheet( contentDescription = null, ) }, + enabled = orgTreeItems.any { it.selected }, text = doneButtonText, style = ButtonStyle.FILLED, ) From fa072130ec3916b9bcfdbeb2c6fb28c70b0944cb Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Tue, 6 Aug 2024 13:40:41 +0530 Subject: [PATCH 03/29] Fix Circle colour to `on-disabled-surface` --- .../designsystem/component/OrgBottomSheet.kt | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt index 424f1abe7..f25b0a5e7 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt @@ -230,20 +230,9 @@ fun OrgUnitSelectorItem( .padding(start = ((orgTreeItem.level - higherLevel) * 16).dp), verticalAlignment = Alignment.CenterVertically, ) { - val icon = orgTreeItemIcon(orgTreeItem) - val iconTint = if (orgTreeItem.isOpen && orgTreeItem.hasChildren) { - TextColor.OnDisabledSurface - } else if (!orgTreeItem.isOpen && !orgTreeItem.hasChildren) { - TextColor.OnDisabledSurface - } else { - TextColor.OnSurfaceVariant - } - - Icon( + OrgTreeItemIcon( modifier = Modifier.padding(Spacing.Spacing8), - painter = icon, - tint = iconTint, - contentDescription = "", + orgTreeItem = orgTreeItem, ) val clickableModifier = if (orgTreeItem.canBeSelected) { @@ -295,13 +284,31 @@ fun OrgUnitSelectorItem( } @Composable -private fun orgTreeItemIcon(orgTreeItem: OrgTreeItem): Painter { - if (!orgTreeItem.hasChildren) return provideDHIS2Icon("material_circle_outline") - - return if (orgTreeItem.isOpen) { - rememberVectorPainter(Icons.Filled.KeyboardArrowDown) +private fun OrgTreeItemIcon( + modifier: Modifier = Modifier, + orgTreeItem: OrgTreeItem +) { + if (!orgTreeItem.hasChildren) { + Icon( + modifier = modifier, + painter = provideDHIS2Icon("material_circle_outline"), + contentDescription = null, + tint = TextColor.OnDisabledSurface + ) + } else if (orgTreeItem.isOpen) { + Icon( + modifier = modifier, + painter = rememberVectorPainter(Icons.Filled.KeyboardArrowDown), + contentDescription = null, + tint = TextColor.OnDisabledSurface + ) } else { - rememberVectorPainter(Icons.AutoMirrored.Filled.KeyboardArrowRight) + Icon( + modifier = modifier, + painter = rememberVectorPainter(Icons.AutoMirrored.Filled.KeyboardArrowRight), + contentDescription = null, + tint = TextColor.OnSurfaceVariant + ) } } From e523249e3685e32f089c2826ab2c3fe9b29d55d4 Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Tue, 6 Aug 2024 14:30:48 +0530 Subject: [PATCH 04/29] Hide keyboard when search clear button is clicked --- .../hisp/dhis/mobile/ui/designsystem/component/SearchBar.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SearchBar.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SearchBar.kt index f37f5f7e5..afe75b675 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SearchBar.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SearchBar.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription @@ -71,6 +72,8 @@ fun SearchBar( val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + val containerColor = if (!isPressed) { SurfaceColor.ContainerLow } else { @@ -148,6 +151,7 @@ fun SearchBar( }, onClick = { onQueryChange.invoke("") + keyboardController?.hide() }, ) } else { From a08eba347045ead433496887e5b8747ea14a4ba0 Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Wed, 14 Aug 2024 16:25:27 +0530 Subject: [PATCH 05/29] Fix lint error --- .../mobile/ui/designsystem/component/OrgBottomSheet.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt index f25b0a5e7..9cc60e31c 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt @@ -30,7 +30,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity @@ -286,28 +285,28 @@ fun OrgUnitSelectorItem( @Composable private fun OrgTreeItemIcon( modifier: Modifier = Modifier, - orgTreeItem: OrgTreeItem + orgTreeItem: OrgTreeItem, ) { if (!orgTreeItem.hasChildren) { Icon( modifier = modifier, painter = provideDHIS2Icon("material_circle_outline"), contentDescription = null, - tint = TextColor.OnDisabledSurface + tint = TextColor.OnDisabledSurface, ) } else if (orgTreeItem.isOpen) { Icon( modifier = modifier, painter = rememberVectorPainter(Icons.Filled.KeyboardArrowDown), contentDescription = null, - tint = TextColor.OnDisabledSurface + tint = TextColor.OnDisabledSurface, ) } else { Icon( modifier = modifier, painter = rememberVectorPainter(Icons.AutoMirrored.Filled.KeyboardArrowRight), contentDescription = null, - tint = TextColor.OnSurfaceVariant + tint = TextColor.OnSurfaceVariant, ) } } From 6944cc5cfa2be4918e6794924b0c4b0efb66f12f Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Mon, 2 Sep 2024 13:00:25 +0200 Subject: [PATCH 06/29] fix: [ANDROAPP-6390]: showcase app horizontal list card screen improvements --- .../common/screens/cards/ListCardScreen.kt | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ListCardScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ListCardScreen.kt index 47a40559c..ffed8bae1 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ListCardScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ListCardScreen.kt @@ -49,37 +49,37 @@ import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor @Composable fun ListCardScreen(horizontal: Boolean) { - if (horizontal) { - LazyRow( - modifier = Modifier.heightIn(0.dp, 500.dp), - horizontalArrangement = spacedBy(4.dp), - verticalAlignment = Alignment.Top, - contentPadding = PaddingValues(vertical = 4.dp, horizontal = 16.dp), - ) { - items(count = 4) { index -> - ListCard( - listCardState = rememberListCardState( - title = ListCardTitleModel(text = "Palak Khanna, F, 61"), - lastUpdated = "5 hours", - additionalInfoColumnState = rememberAdditionalInfoColumnState( - additionalInfoList = largeItemList, - syncProgressItem = syncProgressItem(), - scrollableContent = true, + ColumnScreenContainer(title = if (horizontal) Cards.LIST_CARD_HORIZONTAL.label else Cards.LIST_CARD.label) { + if (horizontal) { + LazyRow( + modifier = Modifier.heightIn(0.dp, 500.dp), + horizontalArrangement = spacedBy(4.dp), + verticalAlignment = Alignment.Top, + contentPadding = PaddingValues(vertical = 4.dp, horizontal = 16.dp), + ) { + items(count = 4) { index -> + ListCard( + listCardState = rememberListCardState( + title = ListCardTitleModel(text = "Palak Khanna, F, 61"), + lastUpdated = "5 hours", + additionalInfoColumnState = rememberAdditionalInfoColumnState( + additionalInfoList = largeItemList, + syncProgressItem = syncProgressItem(), + scrollableContent = true, + ), + loading = false, ), - loading = false, - ), - modifier = Modifier.fillParentMaxWidth(), - listAvatar = { - Avatar( - style = AvatarStyleData.Text("$index"), - ) - }, - onCardClick = {}, - ) + modifier = Modifier.fillParentMaxWidth(), + listAvatar = { + Avatar( + style = AvatarStyleData.Text("$index"), + ) + }, + onCardClick = {}, + ) + } } - } - } else { - ColumnScreenContainer(title = Cards.LIST_CARD.label) { + } else { var showLoading1 by remember { mutableStateOf(false) } From d53ddd902cd8c4840110bcc11513b8d3d76a6f96 Mon Sep 17 00:00:00 2001 From: Ferdy Rodriguez Date: Fri, 6 Sep 2024 12:41:00 +0200 Subject: [PATCH 07/29] feat: [ANDROAPP-6390] TopBar component (#287) * Adds TopBar component and samples * refactor parameters and add tests Signed-off-by: Pablo * missing trailing commas Signed-off-by: Pablo * missing arguments Signed-off-by: Pablo * updates topbar and includes a dark version in sample app * Fix screenshot tests --------- Signed-off-by: Pablo Co-authored-by: Pablo --- .../kotlin/org/hisp/dhis/common/App.kt | 2 + .../org/hisp/dhis/common/screens/Groups.kt | 1 + .../common/screens/others/TopBarScreen.kt | 261 ++++++++++++++++++ .../ui/designsystem/TopBarSnapshotTest.kt | 102 +++++++ .../ui/designsystem/component/TopBar.kt | 92 ++++++ ...ionBarSnapShotTest_launchNavigationBar.png | 4 +- ...system_TopBarSnapshotTest_launchTopBar.png | 3 + 7 files changed, 463 insertions(+), 2 deletions(-) create mode 100644 common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/TopBarScreen.kt create mode 100644 designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/TopBarSnapshotTest.kt create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/TopBar.kt create mode 100644 designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_TopBarSnapshotTest_launchTopBar.png diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt index c62bdb5b9..66c039502 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt @@ -29,6 +29,7 @@ import org.hisp.dhis.common.screens.others.ProgressScreen import org.hisp.dhis.common.screens.others.SearchBarScreen import org.hisp.dhis.common.screens.others.SectionScreen import org.hisp.dhis.common.screens.others.TagsScreen +import org.hisp.dhis.common.screens.others.TopBarScreen import org.hisp.dhis.common.screens.parameter.ParameterSelectorScreen import org.hisp.dhis.common.screens.toggleableInputs.ToggleableInputsScreen import org.hisp.dhis.mobile.ui.designsystem.component.DropdownItem @@ -101,6 +102,7 @@ fun Main( Groups.SEARCH_BAR -> SearchBarScreen() Groups.NAVIGATION_BAR -> NavigationBarScreen() Groups.NO_GROUP_SELECTED -> NoComponentSelectedScreen() + Groups.TOP_BAR -> TopBarScreen() } } else { NoComponentSelectedScreen( diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt index 403da1712..821607631 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt @@ -18,5 +18,6 @@ enum class Groups(val label: String) { INDICATOR("Indicators"), PARAMETER_SELECTOR("Parameter selector"), NAVIGATION_BAR("Navigation Bar"), + TOP_BAR("Top Bar"), NO_GROUP_SELECTED("No group selected"), } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/TopBarScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/TopBarScreen.kt new file mode 100644 index 000000000..05e8d97d4 --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/TopBarScreen.kt @@ -0,0 +1,261 @@ +package org.hisp.dhis.common.screens.others + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.FileDownload +import androidx.compose.material.icons.outlined.Menu +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import org.hisp.dhis.common.screens.Groups +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnScreenContainer +import org.hisp.dhis.mobile.ui.designsystem.component.IconButton +import org.hisp.dhis.mobile.ui.designsystem.component.TopBar +import org.hisp.dhis.mobile.ui.designsystem.component.TopBarActionIcon +import org.hisp.dhis.mobile.ui.designsystem.component.TopBarDropdownMenuIcon +import org.hisp.dhis.mobile.ui.designsystem.component.TopBarType + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopBarScreen() { + ColumnScreenContainer( + title = Groups.TOP_BAR.label, + ) { + ColumnComponentContainer("Default") { + TopBar( + type = TopBarType.DEFAULT, + title = { + Text( + text = "Title", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + navigationIcon = { + IconButton( + onClick = { }, + icon = { + Icon( + imageVector = Icons.Outlined.Menu, + contentDescription = "Menu Button", + ) + }, + ) + }, + actions = { + TopBarActionIcon( + icon = Icons.Outlined.Share, + onClick = { }, + ) + TopBarActionIcon( + icon = Icons.Outlined.FileDownload, + onClick = { }, + ) + TopBarDropdownMenuIcon { showMenu, onDismissRequest -> + DropdownMenu( + expanded = showMenu, + onDismissRequest = onDismissRequest, + ) { + DropdownMenuItem( + text = { Text("Action 1") }, + onClick = {}, + leadingIcon = { + IconButton( + onClick = { + onDismissRequest() + }, + icon = { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "Edit Button", + ) + }, + ) + }, + ) + } + } + }, + ) + } + + ColumnComponentContainer("Back") { + TopBar( + type = TopBarType.DEFAULT, + title = { + Text( + text = "Title", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + navigationIcon = { + IconButton( + onClick = { }, + icon = { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = "Back Button", + ) + }, + ) + }, + actions = { + TopBarActionIcon( + icon = Icons.Outlined.Share, + onClick = { }, + ) + }, + ) + } + + ColumnComponentContainer("Back") { + TopBar( + type = TopBarType.DEFAULT, + title = { + Text( + text = "Title", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = Color.White, + ) + }, + navigationIcon = { + IconButton( + onClick = { }, + icon = { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = "Back Button", + tint = Color.White, + ) + }, + ) + }, + actions = { + TopBarActionIcon( + icon = Icons.Outlined.Share, + tint = Color.White, + onClick = { }, + ) + TopBarDropdownMenuIcon( + iconTint = Color.White, + ) { showMenu, onDismissRequest -> + DropdownMenu( + expanded = showMenu, + onDismissRequest = onDismissRequest, + ) { + DropdownMenuItem( + text = { Text("Action 1") }, + onClick = {}, + leadingIcon = { + IconButton( + onClick = { + onDismissRequest() + }, + icon = { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "Edit Button", + ) + }, + ) + }, + ) + } + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Black, + navigationIconContentColor = Color.White, + actionIconContentColor = Color.White, + ), + ) + } + + ColumnComponentContainer("Without Icons") { + TopBar( + type = TopBarType.DEFAULT, + title = { + Text( + text = "Title", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + navigationIcon = { + IconButton( + onClick = { }, + icon = { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = "Back Button", + ) + }, + ) + }, + actions = { + }, + ) + } + + ColumnComponentContainer("Centered") { + TopBar( + type = TopBarType.CENTERED, + title = { + Text( + text = "Title", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + navigationIcon = { + IconButton( + onClick = { }, + icon = { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = "Back Button", + ) + }, + ) + }, + actions = { + TopBarDropdownMenuIcon { showMenu, onDismissRequest -> + DropdownMenu( + expanded = showMenu, + onDismissRequest = onDismissRequest, + ) { + DropdownMenuItem( + text = { Text("Action 1") }, + onClick = {}, + leadingIcon = { + IconButton( + onClick = { + onDismissRequest() + }, + icon = { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "Edit Button", + ) + }, + ) + }, + ) + } + } + }, + ) + } + } +} diff --git a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/TopBarSnapshotTest.kt b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/TopBarSnapshotTest.kt new file mode 100644 index 000000000..9a670a4f9 --- /dev/null +++ b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/TopBarSnapshotTest.kt @@ -0,0 +1,102 @@ +package org.hisp.dhis.mobile.ui.designsystem + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.FileDownload +import androidx.compose.material.icons.outlined.Menu +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.ui.text.style.TextOverflow +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnScreenContainer +import org.hisp.dhis.mobile.ui.designsystem.component.IconButton +import org.hisp.dhis.mobile.ui.designsystem.component.TopBar +import org.hisp.dhis.mobile.ui.designsystem.component.TopBarActionIcon +import org.hisp.dhis.mobile.ui.designsystem.component.TopBarDropdownMenuIcon +import org.hisp.dhis.mobile.ui.designsystem.component.TopBarType +import org.junit.Rule +import org.junit.Test + +class TopBarSnapshotTest { + @get:Rule + val paparazzi = paparazzi() + + @OptIn(ExperimentalMaterial3Api::class) + @Test + fun launchTopBar() { + paparazzi.snapshot { + ColumnScreenContainer(title = "Top bars") { + ColumnComponentContainer(subTitle = "Default") { + TopBar( + type = TopBarType.DEFAULT, + title = { + Text( + text = "Title", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + navigationIcon = { + IconButton( + onClick = { }, + icon = { + Icon( + imageVector = Icons.Outlined.Menu, + contentDescription = "Menu Button", + ) + }, + ) + }, + actions = { + TopBarActionIcon( + icon = Icons.Outlined.Share, + onClick = { }, + ) + TopBarActionIcon( + icon = Icons.Outlined.FileDownload, + onClick = { }, + ) + TopBarDropdownMenuIcon { _, _ -> + } + }, + ) + } + + ColumnComponentContainer(subTitle = "Centered") { + TopBar( + type = TopBarType.CENTERED, + title = { + Text( + text = "Title", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + navigationIcon = { + IconButton( + onClick = { }, + icon = { + Icon( + imageVector = Icons.Outlined.Menu, + contentDescription = "Menu Button", + ) + }, + ) + }, + actions = { + TopBarActionIcon( + icon = Icons.Outlined.Share, + onClick = { }, + ) + TopBarActionIcon( + icon = Icons.Outlined.FileDownload, + onClick = { }, + ) + }, + ) + } + } + } + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/TopBar.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/TopBar.kt new file mode 100644 index 000000000..3960186bc --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/TopBar.kt @@ -0,0 +1,92 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +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.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopBar( + modifier: Modifier = Modifier, + type: TopBarType = TopBarType.DEFAULT, + navigationIcon: @Composable () -> Unit, + actions: @Composable RowScope.() -> Unit, + title: @Composable () -> Unit, + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), +) { + if (type == TopBarType.DEFAULT) { + TopAppBar( + modifier = modifier, + title = title, + navigationIcon = navigationIcon, + actions = actions, + colors = colors, + ) + } else { + CenterAlignedTopAppBar( + modifier = modifier, + title = title, + navigationIcon = navigationIcon, + actions = actions, + colors = colors, + ) + } +} + +@Composable +fun TopBarActionIcon( + icon: ImageVector, + tint: Color = Color.Unspecified, + contentDescription: String = "", + onClick: () -> Unit, +) { + IconButton( + onClick = onClick, + icon = { + Icon( + imageVector = icon, + contentDescription = contentDescription, + tint = tint, + ) + }, + ) +} + +@Composable +fun TopBarDropdownMenuIcon( + iconTint: Color = Color.Unspecified, + dropDownMenu: @Composable (showMenu: Boolean, onDismissRequest: () -> Unit) -> Unit, +) { + var showMenu by remember { mutableStateOf(false) } + + IconButton( + onClick = { showMenu = !showMenu }, + icon = { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = "More", + tint = iconTint, + ) + }, + ) + dropDownMenu(showMenu) { showMenu = false } +} + +enum class TopBarType { + DEFAULT, + CENTERED, +} diff --git a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_NavigationBarSnapShotTest_launchNavigationBar.png b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_NavigationBarSnapShotTest_launchNavigationBar.png index 228afbf63..fb8ebe0a1 100644 --- a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_NavigationBarSnapShotTest_launchNavigationBar.png +++ b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_NavigationBarSnapShotTest_launchNavigationBar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:defbf0c5c4111d0fb822c2ebe45023e2733c2dda251112154a6724adce464801 -size 34021 +oid sha256:a10ed3c7e83546fffee9ca111e2a081f47b0a43436e9b70a6c86b63387344790 +size 33980 diff --git a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_TopBarSnapshotTest_launchTopBar.png b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_TopBarSnapshotTest_launchTopBar.png new file mode 100644 index 000000000..88dcb99c6 --- /dev/null +++ b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_TopBarSnapshotTest_launchTopBar.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03c798becd02291a3829dcacdc715ed940d56bf5bdfd746b816b98978dc56e4c +size 15089 From 33912d2cf7408f7b9cd177ea1bf1dc1d530ce027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Mon, 9 Sep 2024 11:25:12 +0200 Subject: [PATCH 08/29] fix: [ANDROAPP-5394] responsive home (#296) * fix: [ANDROAPP-5394] responsive home * snapshot test --- .../screens/cards/ExpandableListCardScreen.kt | 5 +++-- .../ui/designsystem/component/BaseCard.kt | 12 +++++++---- .../ui/designsystem/component/ListCard.kt | 19 +++++++++++++----- .../mobile/ui/designsystem/component/Text.kt | 2 +- ...temColumnSnapshotTest_launchAvatarTest.png | Bin 130 -> 33643 bytes 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ExpandableListCardScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ExpandableListCardScreen.kt index e4afe9359..082575718 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ExpandableListCardScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ExpandableListCardScreen.kt @@ -8,7 +8,7 @@ import androidx.compose.material.icons.outlined.SyncDisabled import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import org.hisp.dhis.common.screens.previews.lorem_medium +import org.hisp.dhis.common.screens.previews.lorem import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem import org.hisp.dhis.mobile.ui.designsystem.component.Avatar import org.hisp.dhis.mobile.ui.designsystem.component.AvatarStyleData @@ -51,8 +51,9 @@ fun ExpandableListCardScreen() { } add( AdditionalInfoItem( - value = lorem_medium, + value = lorem, color = TextColor.OnSurfaceLight, + truncate = false, ), ) } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BaseCard.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BaseCard.kt index 0561ab189..2ad2d587a 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BaseCard.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BaseCard.kt @@ -105,7 +105,8 @@ fun BaseCard( fun ExpandableItemColumn( modifier: Modifier = Modifier, itemList: List, - itemSpacing: Dp = 16.dp, + itemSpacing: Dp = Spacing16, + contentPadding: Dp = Spacing16, itemLayout: @Composable (T, itemVerticalPadding: Dp, onSizeChanged: (IntSize) -> Unit) -> Unit, ) { val density = LocalDensity.current @@ -123,8 +124,11 @@ fun ExpandableItemColumn( val itemVerticalPadding by remember(childrenSize) { derivedStateOf { val value = if (childrenSize.size == itemCount) { - var availableHeight = - parentSize - childrenSize.values.sum() - with(density) { itemSpacing.toPx() * (itemCount - 1) } + var availableHeight = parentSize - + childrenSize.values.sum() - + with(density) { + itemSpacing.toPx() * (itemCount - 1) + contentPadding.toPx() * 2 + } if (itemCount == 1) availableHeight /= 4 with(density) { (availableHeight / (2 * itemCount)).toDp() }.takeIf { it >= 16.dp } ?: 16.dp @@ -143,7 +147,7 @@ fun ExpandableItemColumn( } }, verticalArrangement = spacedBy(itemSpacing), - contentPadding = PaddingValues(Spacing16), + contentPadding = PaddingValues(contentPadding), ) { itemList.forEachIndexed { index, item -> item { diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/ListCard.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/ListCard.kt index 4e840ce20..fa4e3e839 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/ListCard.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/ListCard.kt @@ -119,7 +119,9 @@ fun ListCard( ) listCardState.lastUpdateBasedOnLoading()?.let { ListCardLastUpdated(it) } } - listCardState.descriptionBasedOnLoading()?.let { ListCardDescription(it, Modifier) } + listCardState.descriptionBasedOnLoading()?.let { + ListCardDescription(it, Modifier.padding(bottom = Spacing.Spacing8)) + } AdditionalInfoColumn( additionalInfoColumnState = listCardState.additionalInfoColumnState, @@ -256,7 +258,7 @@ fun VerticalInfoListCard( Column( modifier = Modifier.wrapContentHeight(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = spacedBy(Spacing.Spacing4), + verticalArrangement = spacedBy(Spacing4), ) { ListCardTitle( title = listCardState.title, @@ -264,7 +266,7 @@ fun VerticalInfoListCard( .padding(bottom = if (listCardState.description?.text != null) Spacing.Spacing0 else Spacing4), ) listCardState.descriptionBasedOnLoading()?.let { - ListCardDescription(it, Modifier) + ListCardDescription(it) } listCardState.lastUpdateBasedOnLoading()?.let { ListCardLastUpdated(it) @@ -673,8 +675,14 @@ fun ProvideKeyValueItem( text = finalAnnotatedString, textAlign = TextAlign.Start, style = MaterialTheme.typography.bodyMedium, - overflow = TextOverflow.Ellipsis, - maxLines = 2, + overflow = when { + additionalInfoItem.truncate -> TextOverflow.Ellipsis + else -> TextOverflow.Clip + }, + maxLines = when { + additionalInfoItem.truncate -> 2 + else -> Int.MAX_VALUE + }, modifier = Modifier, ) @@ -811,6 +819,7 @@ data class AdditionalInfoItem( val value: String, val isConstantItem: Boolean = false, val color: Color? = null, + val truncate: Boolean = true, val action: (() -> Unit)? = null, ) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Text.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Text.kt index 9e07b66af..71269f396 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Text.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Text.kt @@ -151,7 +151,7 @@ internal fun ListCardDescription( description.text, color = description.color ?: TextColor.OnSurface, style = description.style ?: MaterialTheme.typography.bodyMedium, - modifier = modifier.padding(bottom = Spacing.Spacing8), + modifier = modifier, ) } } diff --git a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_ExpandableItemColumnSnapshotTest_launchAvatarTest.png b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_ExpandableItemColumnSnapshotTest_launchAvatarTest.png index 32085bdc374aff962ede1e16e496fecac32cea58..82d3bcdc181b005eb639f0c121b7d3f42dffac8f 100644 GIT binary patch literal 33643 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 literal 130 zcmWN?K@!3s3;@7;U%>|~hL8aL4Fw|1sC0z(;OliSdzH^<{jKVp$Jn%fw0V2VSpU~^ zUsHeSag?MlSbEE9)M$t|u}nn-7@d{OHY72-H@;X*85~#)0TKa#Gxrb^Ghz0TfjmX= MxQzBK3ehnwKhDc0kN^Mx From ec6f196e8829e70f6cfb240dc254500640090368 Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Mon, 9 Sep 2024 19:26:39 +0530 Subject: [PATCH 09/29] Androapp 6035 mobile UI create dropdown menu component (#295) * Add data class for `MenuItem` component * Add `MenuItem` component * Add `MenuItemSnapshotTest` * Add `MenuItemTest` * Fix `MenuItem` Label and supporting text truncate * Fix snapshot test * add Enrolment menu to menu item showcase * Add shadow to enrollment menu --------- Co-authored-by: Siddharth Agarwal --- .../kotlin/org/hisp/dhis/common/App.kt | 2 + .../org/hisp/dhis/common/screens/Groups.kt | 1 + .../common/screens/others/MenuItemScreen.kt | 586 ++++++++++++++++++ .../ui/designsystem/MenuItemSnapshotTest.kt | 149 +++++ .../component/menuItem/MenuItem.kt | 300 +++++++++ .../component/menuItem/MenuItemTestTags.kt | 12 + .../mobile/ui/designsystem/theme/Shadow.kt | 6 +- .../ui/designsystem/component/MenuItemTest.kt | 128 ++++ ...enuItemSnapshotTest_launchMenuItemTest.png | Bin 0 -> 24529 bytes 9 files changed, 1181 insertions(+), 3 deletions(-) create mode 100644 common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/MenuItemScreen.kt create mode 100644 designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/MenuItemSnapshotTest.kt create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItem.kt create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItemTestTags.kt create mode 100644 designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/MenuItemTest.kt create mode 100644 designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_MenuItemSnapshotTest_launchMenuItemTest.png diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt index 66c039502..4f37602d7 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt @@ -23,6 +23,7 @@ import org.hisp.dhis.common.screens.others.BadgesScreen import org.hisp.dhis.common.screens.others.ChipsScreen import org.hisp.dhis.common.screens.others.IndicatorScreen import org.hisp.dhis.common.screens.others.LegendScreen +import org.hisp.dhis.common.screens.others.MenuItemScreen import org.hisp.dhis.common.screens.others.MetadataAvatarScreen import org.hisp.dhis.common.screens.others.NavigationBarScreen import org.hisp.dhis.common.screens.others.ProgressScreen @@ -101,6 +102,7 @@ fun Main( Groups.TAGS -> TagsScreen() Groups.SEARCH_BAR -> SearchBarScreen() Groups.NAVIGATION_BAR -> NavigationBarScreen() + Groups.MENU -> MenuItemScreen() Groups.NO_GROUP_SELECTED -> NoComponentSelectedScreen() Groups.TOP_BAR -> TopBarScreen() } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt index 821607631..703580911 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt @@ -19,5 +19,6 @@ enum class Groups(val label: String) { PARAMETER_SELECTOR("Parameter selector"), NAVIGATION_BAR("Navigation Bar"), TOP_BAR("Top Bar"), + MENU("Menu"), NO_GROUP_SELECTED("No group selected"), } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/MenuItemScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/MenuItemScreen.kt new file mode 100644 index 000000000..bc27b2cfb --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/MenuItemScreen.kt @@ -0,0 +1,586 @@ +package org.hisp.dhis.common.screens.others + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowRight +import androidx.compose.material.icons.automirrored.outlined.Assignment +import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.outlined.Cancel +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.CheckCircle +import androidx.compose.material.icons.outlined.DeleteForever +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material.icons.outlined.Done +import androidx.compose.material.icons.outlined.Flag +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material.icons.outlined.Sync +import androidx.compose.material.icons.outlined.Workspaces +import androidx.compose.runtime.Composable +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.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import org.hisp.dhis.common.screens.Groups +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnScreenContainer +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItem +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemState +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemStyle +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuLeadingElement +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuTrailingElement +import org.hisp.dhis.mobile.ui.designsystem.theme.Radius +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor +import org.hisp.dhis.mobile.ui.designsystem.theme.dropShadow + +@Composable +fun MenuItemScreen() { + ColumnScreenContainer(Groups.MENU.label) { + ColumnComponentContainer( + "Enrollment dashboard menu", + ) { + var menuItems by remember { + mutableStateOf( + listOf( + MenuItemData( + label = "Refresh this record", + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Sync), + ), + MenuItemData( + label = "Mark for follow-up", + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Flag), + ), + MenuItemData( + label = "Group by stage", + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Workspaces), + ), + MenuItemData( + label = "Show help", + leadingElement = MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.HelpOutline), + ), + MenuItemData( + label = "More enrollments", + leadingElement = MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.Assignment), + ), + MenuItemData( + label = "Share", + supportingText = "Using QR code", + showDivider = true, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Share), + ), + MenuItemData( + label = "Complete", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.CheckCircle, + defaultTintColor = SurfaceColor.CustomGreen, + selectedTintColor = SurfaceColor.CustomGreen, + ), + ), + MenuItemData( + label = "Deactivate", + showDivider = true, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Cancel, + defaultTintColor = TextColor.OnDisabledSurface, + selectedTintColor = TextColor.OnDisabledSurface, + ), + ), + MenuItemData( + label = "Remove from [program]", + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.DeleteOutline), + ), + MenuItemData( + label = "Delete [TEI Type]", + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.DeleteForever), + ), + ), + ) + } + + Column( + modifier = Modifier + .dropShadow( + shape = RoundedCornerShape(Radius.XS), + blur = Spacing.Spacing2, + spread = Spacing.Spacing0, + color = Color(0x4D007DEB), + offsetY = Spacing.Spacing1, + ) + .width(270.dp) + .background(SurfaceColor.ContainerLow) + .padding(vertical = Spacing.Spacing8), + ) { + menuItems.forEachIndexed { index, menuItemData -> + MenuItem( + menuItemData = menuItemData, + ) { + menuItems = menuItems.mapIndexed { i, item -> + if (i == index) { + item.copy(state = MenuItemState.SELECTED) + } else { + item.copy(state = MenuItemState.ENABLED) + } + } + } + } + } + } + + ColumnComponentContainer("Menu list item") { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Support Text", + showDivider = true, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Support Text", + showDivider = true, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + } + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.SELECTED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.SELECTED, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + } + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.DISABLED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.DISABLED, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + } + } + } + + ColumnComponentContainer("Menu item with leading element variations") { + MenuItem( + menuItemData = MenuItemData( + label = "No Leading Element", + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Indent Leading Element", + leadingElement = MenuLeadingElement.Indent, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Icon Leading Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Indent Leading Element", + leadingElement = MenuLeadingElement.Indent, + state = MenuItemState.SELECTED, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Icon Leading Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + state = MenuItemState.SELECTED, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Indent Leading Element", + leadingElement = MenuLeadingElement.Indent, + state = MenuItemState.DISABLED, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Icon Leading Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + state = MenuItemState.DISABLED, + ), + ) {} + } + + ColumnComponentContainer("Menu item with divider") { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + showDivider = true, + ), + ) {} + } + + ColumnComponentContainer("Menu item with trailing element variations") { + MenuItem( + menuItemData = MenuItemData( + label = "No Trailing Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Icon Trailing Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Text Trailing Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Text( + text = "⌘C", + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Icon Trailing Element", + state = MenuItemState.SELECTED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Text Trailing Element", + state = MenuItemState.SELECTED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Text( + text = "⌘C", + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Icon Trailing Element", + state = MenuItemState.DISABLED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Text Trailing Element", + state = MenuItemState.DISABLED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Text( + text = "⌘C", + ), + ), + ) {} + } + + ColumnComponentContainer("Alert Menu item with leading element variations") { + MenuItem( + menuItemData = MenuItemData( + label = "No Leading Element", + style = MenuItemStyle.ALERT, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Indent Leading Element", + leadingElement = MenuLeadingElement.Indent, + style = MenuItemStyle.ALERT, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Icon Leading Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + style = MenuItemStyle.ALERT, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Indent Leading Element", + leadingElement = MenuLeadingElement.Indent, + style = MenuItemStyle.ALERT, + state = MenuItemState.SELECTED, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Icon Leading Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + style = MenuItemStyle.ALERT, + state = MenuItemState.SELECTED, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Indent Leading Element", + leadingElement = MenuLeadingElement.Indent, + style = MenuItemStyle.ALERT, + state = MenuItemState.DISABLED, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Diasbled Icon Leading Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + style = MenuItemStyle.ALERT, + state = MenuItemState.DISABLED, + ), + ) {} + } + + ColumnComponentContainer("Alert Menu item with divider") { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + showDivider = true, + style = MenuItemStyle.ALERT, + ), + ) {} + } + + ColumnComponentContainer("Alert Menu item with trailing element variations") { + MenuItem( + menuItemData = MenuItemData( + label = "No Trailing Element", + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Icon Trailing Element", + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Text Trailing Element", + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Text( + text = "⌘C", + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Icon Trailing Element", + style = MenuItemStyle.ALERT, + state = MenuItemState.SELECTED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Text Trailing Element", + style = MenuItemStyle.ALERT, + state = MenuItemState.SELECTED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Text( + text = "⌘C", + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Icon Trailing Element", + state = MenuItemState.DISABLED, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Text Trailing Element", + state = MenuItemState.DISABLED, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Text( + text = "⌘C", + ), + ), + ) {} + } + } +} diff --git a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/MenuItemSnapshotTest.kt b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/MenuItemSnapshotTest.kt new file mode 100644 index 000000000..badb7d95c --- /dev/null +++ b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/MenuItemSnapshotTest.kt @@ -0,0 +1,149 @@ +package org.hisp.dhis.mobile.ui.designsystem + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowRight +import androidx.compose.material.icons.outlined.Done +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnScreenContainer +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItem +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemState +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemStyle +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuLeadingElement +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuTrailingElement +import org.junit.Rule +import org.junit.Test + +class MenuItemSnapshotTest { + @get:Rule + val paparazzi = paparazzi() + + @Test + fun launchMenuItemTest() { + paparazzi.snapshot { + ColumnScreenContainer("Menu Item") { + ColumnComponentContainer("Menu item") { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Label", + supportingText = "Support Text", + showDivider = true, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Label", + supportingText = "Support Text", + showDivider = true, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + } + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Label", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.SELECTED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Label", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.SELECTED, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + } + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Label", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.DISABLED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Label", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.DISABLED, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + } + } + } + } + } + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItem.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItem.kt new file mode 100644 index 000000000..a07976bd9 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItem.kt @@ -0,0 +1,300 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.menuItem + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.style.TextOverflow +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_CONTAINER +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_DIVIDER +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_LEADING_ICON +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_LEADING_INDENT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_SUPPORTING_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TRAILING_ICON +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TRAILING_TEXT +import org.hisp.dhis.mobile.ui.designsystem.theme.Border +import org.hisp.dhis.mobile.ui.designsystem.theme.Outline +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor +import org.hisp.dhis.mobile.ui.designsystem.theme.hoverPointerIcon + +/** + * DHIS2 [MenuItem] Used for dropdown menu. + * @param modifier: allows a modifier to be passed externally. + * @param menuItemData: manages the [MenuItemData] + * @param onItemClick: callback to when menu item is clicked. + */ +@Composable +fun MenuItem( + modifier: Modifier = Modifier, + menuItemData: MenuItemData, + onItemClick: () -> Unit, +) { + val itemContainerBackground = when (menuItemData.state) { + MenuItemState.SELECTED -> { + if (menuItemData.style == MenuItemStyle.ALERT) { + SurfaceColor.ErrorContainer + } else { + SurfaceColor.Container + } + } + + else -> Color.Transparent + } + + Column( + modifier = modifier.testTag(MENU_ITEM_CONTAINER), + ) { + Row( + modifier = Modifier + .background(itemContainerBackground) + .alpha(if (menuItemData.state != MenuItemState.DISABLED) 1f else 0.38f) + .clickable( + enabled = menuItemData.state != MenuItemState.DISABLED, + onClick = { + onItemClick.invoke() + }, + ) + .hoverPointerIcon(menuItemData.state != MenuItemState.DISABLED) + .height(Spacing.Spacing48) + .padding(horizontal = Spacing.Spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + MenuItemLeadingElement( + leadingElement = menuItemData.leadingElement, + style = menuItemData.style, + state = menuItemData.state, + ) + Column( + modifier = Modifier.weight(1f), + ) { + Text( + maxLines = if (!menuItemData.supportingText.isNullOrEmpty()) 1 else 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.testTag(MENU_ITEM_TEXT), + style = MaterialTheme.typography.bodyLarge, + color = if (menuItemData.style == MenuItemStyle.ALERT) SurfaceColor.Error else TextColor.OnSurface, + text = menuItemData.label, + ) + if (!menuItemData.supportingText.isNullOrEmpty()) { + Text( + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.testTag(MENU_ITEM_SUPPORTING_TEXT), + style = MaterialTheme.typography.bodyMedium, + color = if (menuItemData.style == MenuItemStyle.ALERT) TextColor.OnErrorContainer else TextColor.OnSurfaceVariant, + text = menuItemData.supportingText, + ) + } + } + + MenuItemTrailingElement( + trailingElement = menuItemData.trailingElement, + state = menuItemData.state, + ) + } + if (menuItemData.showDivider) { + HorizontalDivider( + modifier = modifier + .testTag(MENU_ITEM_DIVIDER) + .padding(vertical = Spacing.Spacing8), + thickness = Border.Thin, + color = Outline.Medium, + ) + } + } +} + +@Composable +private fun MenuItemLeadingElement( + leadingElement: MenuLeadingElement? = null, + style: MenuItemStyle, + state: MenuItemState, +) { + when (leadingElement) { + is MenuLeadingElement.Indent -> { + Box( + modifier = Modifier + .testTag(MENU_ITEM_LEADING_INDENT) + .padding(end = Spacing.Spacing12) + .size(Spacing.Spacing24), + ) + } + + is MenuLeadingElement.Icon -> { + val iconTint = when (state) { + MenuItemState.SELECTED -> { + if (style == MenuItemStyle.ALERT) { + leadingElement.selectedErrorTintColor + } else { + leadingElement.selectedTintColor + } + } + + else -> if (style == MenuItemStyle.ALERT) { + leadingElement.defaultErrorTintColor + } else { + leadingElement.defaultTintColor + } + } + Icon( + imageVector = leadingElement.icon, + modifier = Modifier + .testTag(MENU_ITEM_LEADING_ICON) + .padding(end = Spacing.Spacing12) + .size(Spacing.Spacing24), + contentDescription = null, + tint = iconTint, + ) + } + + else -> {} + } +} + +@Composable +private fun MenuItemTrailingElement( + trailingElement: MenuTrailingElement? = null, + state: MenuItemState, +) { + when (trailingElement) { + is MenuTrailingElement.Icon -> { + Icon( + modifier = Modifier + .testTag(MENU_ITEM_TRAILING_ICON) + .padding(start = Spacing.Spacing12) + .size(Spacing.Spacing24), + imageVector = trailingElement.icon, + contentDescription = null, + tint = if (state == MenuItemState.SELECTED) trailingElement.selectedTintColor else trailingElement.defaultTintColor, + ) + } + + is MenuTrailingElement.Text -> { + Text( + modifier = Modifier + .testTag(MENU_ITEM_TRAILING_TEXT) + .padding(start = Spacing.Spacing12), + style = MaterialTheme.typography.bodyLarge, + color = if (state == MenuItemState.SELECTED) TextColor.OnSurface else TextColor.OnSurfaceVariant, + text = trailingElement.text, + ) + } + + else -> {} + } +} + +/** + * DHIS2 [MenuItemData], + * class to control the [MenuItem] + * @param label: controls the text to be shown. + * @param state: controls the [MenuItem] state. + * @param style: controls the [MenuItem] style. + * @param leadingElement: controls the [MenuLeadingElement]. + * @param trailingElement: controls the [MenuTrailingElement]. + * @param supportingText: controls the supporting text to be shown. + * @param showDivider: controls whether a divider should be shown. + */ +data class MenuItemData( + val label: String, + val state: MenuItemState = MenuItemState.ENABLED, + val style: MenuItemStyle = MenuItemStyle.DEFAULT, + val leadingElement: MenuLeadingElement? = null, + val trailingElement: MenuTrailingElement? = null, + val supportingText: String? = null, + val showDivider: Boolean = false, +) + +/** + * DHIS2 MenuItemState, + * enum class to control the [MenuItem] state + */ +enum class MenuItemState { + ENABLED, + SELECTED, + DISABLED, +} + +/** + * DHIS2 MenuItemStyle, + * enum class to control the [MenuItem] style + */ +enum class MenuItemStyle { + DEFAULT, + ALERT, +} + +/** + * DHIS2 [MenuLeadingElement], + * class to control the [MenuItem] leading element + */ +sealed class MenuLeadingElement { + /** + * DHIS2 [Indent], + * class to control the [MenuLeadingElement] trailing element indent. + */ + data object Indent : MenuLeadingElement() + + /** + * DHIS2 [Icon], + * class to control the [MenuLeadingElement] trailing element icon. + * @param icon: controls the icon to be shown. + * @param defaultErrorTintColor: controls the error style tint color. + * @param defaultTintColor: controls the default tint color. + * @param selectedErrorTintColor: controls the error style tint color when selected. + * @param selectedTintColor: controls the tint color when selected. + */ + data class Icon( + val icon: ImageVector, + val defaultErrorTintColor: Color = SurfaceColor.Error, + val defaultTintColor: Color = SurfaceColor.Primary, + val selectedErrorTintColor: Color = TextColor.OnErrorContainer, + val selectedTintColor: Color = TextColor.OnPrimaryContainer, + ) : MenuLeadingElement() +} + +/** + * DHIS2 [MenuTrailingElement], + * class to control the [MenuItem] trailing element + */ +sealed class MenuTrailingElement { + /** + * DHIS2 [Icon], + * class to control the [MenuTrailingElement] trailing element icon. + * @param icon: controls the icon to be shown. + * @param defaultTintColor: controls the default tint color. + * @param selectedTintColor: controls the tint color when selected. + */ + data class Icon( + val icon: ImageVector, + val defaultTintColor: Color = TextColor.OnSurfaceVariant, + val selectedTintColor: Color = TextColor.OnSurface, + ) : MenuTrailingElement() + + /** + * DHIS2 [Text], + * class to control the [MenuTrailingElement] trailing element text. + * @param text: controls the text to be shown. + */ + data class Text( + val text: String, + ) : MenuTrailingElement() +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItemTestTags.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItemTestTags.kt new file mode 100644 index 000000000..43b04b84e --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItemTestTags.kt @@ -0,0 +1,12 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.menuItem + +object MenuItemTestTags { + const val MENU_ITEM_CONTAINER = "MENU_ITEM_CONTAINER" + const val MENU_ITEM_TEXT = "MENU_ITEM_TEXT" + const val MENU_ITEM_SUPPORTING_TEXT = "MENU_ITEM_SUPPORTING_TEXT" + const val MENU_ITEM_DIVIDER = "MENU_ITEM_DIVIDER" + const val MENU_ITEM_LEADING_INDENT = "MENU_ITEM_LEADING_INDENT" + const val MENU_ITEM_LEADING_ICON = "MENU_ITEM_LEADING_ICON" + const val MENU_ITEM_TRAILING_ICON = "MENU_ITEM_TRAILING_ICON" + const val MENU_ITEM_TRAILING_TEXT = "MENU_ITEM_TRAILING_TEXT" +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Shadow.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Shadow.kt index 22affe405..14df826bc 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Shadow.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Shadow.kt @@ -98,7 +98,7 @@ internal fun Modifier.iconCardShadow( } }.padding(bottom = shadowRadius) -internal fun Modifier.dropShadow( +fun Modifier.dropShadow( shape: Shape, color: Color = SurfaceColor.Container, blur: Dp = 10.dp, @@ -112,10 +112,10 @@ internal fun Modifier.dropShadow( // Create a Paint object val paint = Paint() -// Apply specified color + // Apply specified color paint.color = color -// Check for valid blur radius + // Check for valid blur radius if (blur.toPx() > 0) { paint.asFrameworkPaint().apply { // Apply blur to the Paint diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/MenuItemTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/MenuItemTest.kt new file mode 100644 index 000000000..fdc17cd88 --- /dev/null +++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/MenuItemTest.kt @@ -0,0 +1,128 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Done +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItem +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_CONTAINER +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_DIVIDER +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_LEADING_ICON +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_LEADING_INDENT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_SUPPORTING_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TRAILING_ICON +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TRAILING_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuLeadingElement +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuTrailingElement +import org.junit.Rule +import org.junit.Test + +class MenuItemTest { + @get:Rule + val rule = createComposeRule() + + @Test + fun shouldDisplayMenuItemWithLabelCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_TEXT, true).assertExists() + } + + @Test + fun shouldDisplaySupportingTextCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Supporting Text", + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_SUPPORTING_TEXT, true).assertExists() + } + + @Test + fun shouldDisplayDividerCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + showDivider = true, + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_DIVIDER, true).assertExists() + } + + @Test + fun shouldDisplayLeadingIndentCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + leadingElement = MenuLeadingElement.Indent, + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_LEADING_INDENT, true).assertExists() + } + + @Test + fun shouldDisplayLeadingIconCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_LEADING_ICON, true).assertExists() + } + + @Test + fun shouldDisplayTrailingIconCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + trailingElement = MenuTrailingElement.Icon( + icon = Icons.Outlined.Done, + ), + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_TRAILING_ICON, true).assertExists() + } + + @Test + fun shouldDisplayTrailingTextCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + trailingElement = MenuTrailingElement.Text( + text = "Trailing Text", + ), + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_TRAILING_TEXT, true).assertExists() + } +} diff --git a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_MenuItemSnapshotTest_launchMenuItemTest.png b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_MenuItemSnapshotTest_launchMenuItemTest.png new file mode 100644 index 0000000000000000000000000000000000000000..39faab564c544d23d0186ca81013100aa20d5967 GIT binary patch literal 24529 zcmd43cT`hf7ba|1R79jGU65V{0!T;cp?5<4AvF|{PN*URQlvv5bV%q$dIuFL0YVAA z2qHmx3!%5Uyzk6Av(~IN-}kLGv*!CJIZ5t0_mqA2-p_vaPPnGJBJq9d``4~rBUVLd#&qjw$bX9sJmac-MroG+`>Qi?6AUXQ1mdp zx_+i~CSCp`__3V4JgFA?-jiF2*|*}DpWpi!K}62HJY;2u$al+68xUPick>vm|66KS z!X;F?R-WG+v27bAx^hxxUS@P#H_(2-+%V9=-k!-fudy-p;hmecb#?aFfoTx*RsB0J z-o`+h)HKd8RXKUTpV~p8LC=BT8g6X}9v~!djIpRmaR8V8F8;dA{mYi?9lKf`AMp8o z^ZV-3YFVf&<|**0;nzQx5V74*?5CGN-`N3oW_2#;n-t)e4^A!!WirdFri^&9Wx5Ua z*1wu9+@1NL%F?S3U%T8zBb3OjAwWCZa#Wy9i44LDw1@cZ>o~AK$GxvQbQfBt1D49g-6r9I9p3+LHprcdMA_Z|z7|nLLZ|KljcX3i)qvM;kN)*0!xnpW zWj2`-8Spklgd_sqR&M=kM(#JDS3SjC8bE!Voa{o92@znhv-e@5O>j^|k`%gVRTrXX z4QntMo*b1!Sug48n_0ksTYPi+)PXu)D|B%mOgLX{E4VYHH-D|KgW8tYXXt#eh24KX z3ncq<)Oh!RB=|{Jz*Gx*ue~c zt2s*_JI0M~XH@F=)`B1xei<<>FMpQ$?|VrPwP(^Ot+&nFk3Rx#FSIHQojaa0%q;>1=M zg+4v)WMPd=ltPH5+4LK~=^jik=;}I#;!70d*zwK4;ZLbqBZ(SMhrTZs z6c#l?rY9WFv_!j2&;x^J_~#wOT@iqoxikD?#k3zVd?iEb8|Z7m;*YdbDMti+>A&z8kZRzK4SO;PCAS3sOj@bdG{%Hu7j;+ z&)Q~LX=a44UEw(5ldhqXUv&3LE0eU>Cltu?4;l3L=fl+;sbFM=O3k^sf0#te8O~eH zi3f9xcW9&>s5_pN)Hp~ubU*q@wb^ofTi(3@yZqhOJU5_2p?6jZ7%xnWDvE2NO93f^5lCMwbrCf|oimU=-kBFqyrB}wx?ZqEvwk0 zn=(+A1yS87l?Lq9$I#&4oPrKssqT%T)#X{Yr zhFIqF?KOM>SG)T#(4a~%VAiX!a@J7WNJ0FNULb)r(t4K7?Tgc5!Eh);;Mm)Pr+soS z`P%u7lD!MDxGHNW(~N@?j#Vn`1un9}o~bPfTUuNm$DJ^9+_38FZP_meHE&*JTP*=6 zz^jqAHqd3IzL#zFVW$rTn#)%jZb6sZ2aDJ2%jke7uPkQ<3Bzy5rxNg;u9ie2I_RbW zvnHlDsqOAbg!BEuUp81=IA0#CD!lGAD?p3cnyR8z#B5xl7>kRlDla$lJ|SoSp@E1tZKZi0upuT1OwK%*0<8_S!4smj$TR1$0dk;%01WtAz?fh$E_SqJTdL8h7mx^~+Ty5uO)3D?4`|y{Vk}Fg&$zGM;sip6=xt9misH+69&AP6Ieo>Y~PK%m*_4 zT~LS07l~$~Im_Q|VjW0VgqM4dv~_gX)(wE&X3Ed817vRM8D0}zEPODZzE?|tew8Z` zag1|J>l?FV9ZY=U;`uI<5DdD0?OS653s$wS=Lu0&y0_&u3S+F*pC<`c9oKl*i>sD| z(zc^7Ki!X1LB>d8tqlFvd^Gm`){uDv-~`(isXQf3$>obYHdID+->T04w!got9T^ZuTtwl%f~y0KeAJ$9Hx^w%@Z_S-CH^?fQ2MeJ?o+(yCJL zP##yIV`)it-egJQ-|G3ol(17JQMy3(Se0wSIUm!0_PVy#BRR~O19m%v zfL@MV`Nl8OCwtYA3h3ye#`EiR^jgzm-12H_P=$_DM(yLYv^40q9WNA#qLxdzHQX5N zZ3X(ZCntU4zF=Tv{922j$Fw!bb4&aaH#snhLcMZA$L1_cA1{vPSuWi+G1d_ZMQx-@ z6c?3^O~>nuEKLKp)L3-xEtUp2icQserM)6}=m;Q8!*_LTAlCL-d}hjks(-RicTkrE za&z5-P6XxE*mqE#SRP&ns|}7uCnYD11m(TMZ8FHMrb22qEBtFtni*DbQ<1v!L(l#1 zCp~nTUm$bu>^Ao`bE|%h!z>vXr#ibpsWwI3O$PdIg~<;^IJd(ce2>2zCa#OVp@K{q zU6!VsoH$&PHg-uL((9I^&YwwpZ7(c>FKL@_t7;DIm*I71CwDv9SR?&zzWK4zoE1>8 zZydnbo~gmEs@5V>963t5!(i96MNp-$S4l9>yN_hN(M9Yis z7KGW~q^?dTZ~sbX8PL0h!!4#R03U~Nva*ZsT!6A5bQ%OYVa{BlWkgbZ2tFuvL`<=Lr?$d zEzmWee-3%;;X&MbG|ldV3;9l--IGueCqEc7?;V(Hl84B_|ICf(<%Dg9^tHv+0j4G& znFwlpS6>ZBZZ3*G;3>ZYxWCT%#z;}X5^@x{>K~n~5NT5`39SB1&FZ_A(_3us<24IX zLzrE>j@UTC0&df1J40QG63-t|gD4Ff&uCT*qjO+(EB&Fb$LK3k>wwpBvPd*wgb6!% zR5Sgyir`Z)K+@kXT6&0lL6<@EHcltEwlgCKF0P8$<(A{`RwZ~LK=kKS z>XqESIc#ggqEC@y$q-6*AFA6gk0~SZ$y2L7)HWyII2yPEdzh# z7u&_+9rzu}uZSGLH-G;K5Iu>xD^uTie49y(e()49vP?sO6=bSf0EZTW6W9iK2A%z1 zg$SP=rRnY9X!H=+m#Az3;<-&xN}gqyPq2E)tEG_rWi82{T1H?cUP*ZyH`ES7;1kkHCR){Yz0XXAI> z>zT+XBRHGpJvZVOeURwVnHD|z`C~%{(Wkbl5|5xUAc)nA?f~u^Rtwvr?yA$UgY|5Y z`HAxF@^16&KtSk^1>Va<%L3-%_w*}WM2kGVZ8CSn9eF#>mcn`Wj_U)xugD}kE}jU1 zY7Q46bz7NIOpNr)R?4+-NXhDkJjs-nQiw`HODLXj+pD;w1THLVJah2tF5vbG>Y8YC zR(Kp>DN;XJDZ}JZCW?k~NQf^KCQ2va8F4Y`riu2MnLxPe_tQLmKi*Hnky0*eeSNRd z+fRq3{j1V6ljWZpVP5epsqONzOW+`X(*hH`Qt@0j|BdDTFBz$;Dg2+bq|?d{4qOzJ z6gmMGaZ5S8yIV&ZcE3LlCu2-=QK*o)v{xDVnDh9Dp_F|c*Iv;xlw^BY;Rr6UIzIQ}!g(|?%N7{5r}kp~ zojosijl`|=XTXqvQ{nSSiHrb>&ChVxM{vIk{w~7<*zvGiI#5?4!;`vM6L*od2hbY^ zc{*6!U?`p{PTJ`o7PYND7r~=yzu>mme1{v*>F`!8H#cX_cHo>k@KmdJ%+qKr76dAE zYh)F4DeUo6CI1CbU4a9s7Rcedw$J%F5#f6xq~>sk$lkt+zTE{s_>v&&F+| z%Z>_G$uApX?-S|$wyY5^7u~KL1EVG%$t$Z!IF3(Z(!GPdRUT>vtrHg39-oDf`yU4C z9Q+t8wCeWkJ$9AqoINE0Gh7Bi#*4s$Tj($j%V}oL*_OB}8@0R@@yi5kcbi5TF)^zx zYn;QGPK+k@_jN3MiTV&Hn+S5xPPJlGo4AFoA@fBxp8n=1rLz6RsfNLT{sb!v=p>_x ze_&g-SD&sHJAYtp4GmQ|v74s#so8!E?(4yxuomQxyx@D7992`v^I&QD4Egy$UA~HO zVFE?`?-`GqJs`8F970C*vS)?0P?&3tN?&BGEn3#L6@wxJE^WZBdmoZ2rMjjuF^s!#mqr0PLpX=>?)Sl0JK1OV|NYjpKB9ds;> zgI&%lH-1ZzT8$<38JRn&3i25^SW*iLvL7t?a`;}*mX!%}>z=s&wFj5rmy%R!p$x4B z%G6kQwdZQ$m5K;e3m2j*$5bt|^#g#^gUodP062SvN*<65N%43az$D*u?W+j1b8>si z1FuWngL6t(488q(#&Q)zg3e*}uq6N9CjlOhTHU5V5$gbfka%;3VkM`SECg6DJEm`Q zDsN0KTiB?$Bq_@E8mdk3je{k`b$--q9@(F&Gh^A+O@27q5g$}c7X19bonvYOT;n|S z3-Y3(O_rwqLLG=v#Woj+d`Xiss0Zymh>(k=2{==8NCP>v)$Hcq9+5J(qL5(x90Up~ z$uU6ijOitc`7n1%yf28dPNox+NwU?@*)oVEs8rYKcX8;DFBB`GlTo60;@%XA0Z!Mp z^nxVs{~S;w5J&Q9xzq%!@?U=7gVdVUgDMoeBc8*mVtV7p)=^~*qkfdxEdM@=>N6Za zRSdVdPfuwE!+Mg9n{v2SK2<`Oy_2=#Bk+809uValln4M$r<%)W03m|L#l;IzzrJK-EH{YuqA+~w~_)gyk2BCwB5oiNu^ z>@CUL8Yv4dQi^#72>JFiX4jHt`r)~r1Ycq24NXLC4&$ajMRB>0HbMH5gyQm`dVD-F zL2cOU($r?csC*FTbkx9HdIQ?T%FBf*4%Me_D*4#fqARQRh!Dbwx#*bBY52sPre$Ex z=P%-PZi?$dO?}LL3pn^lzBV&iFUgCK@j;Ig@MZT^5>D0AMKxFnJN?swPx(e<2T5}4 zrPALJe{mwCCbzeDSp9OyhVAgb6E_1O08>+0ilij>--T&4u#+PdX+Xc;;_g4&ci`~f zc*fx(_JooYZo)GugxfM}Ov)^A zXdA*gS!1?-|7!6Zm_O6{KK{-u=bP(qPXZ0#Lu2mU{)>7j^pckG_cu||DzR;PdSf8# zgBy@QboZA21+66X4v)#fm(unYPe~(jK9vL!X~P!RjQ$Tx%U|VS_{V(wmCWKI8Uo{2 z@ss2%iJc7-_bZ0v-f?4qm1Um^ad8|BpRzRLE_Mec6ew(G zY|Bb&boF{|HY!Qxc-Cc(g>tu#n^o`G>oXVM0B)$Mo9owDQRnC>W}=Qrw|F$KqkJh# z`uEbrOm*FFt#B%+?H^JU(pFQ|FBkLqi$?Cr$wBAPS)`bRKq#--ZtmC5?8Q1rV=V}a zKUc~0)b#gDw?d80 z87w{G;OSkikm6P7Eu9v9fG=X?e|;j7U!XO%q!M4`>N(8_o)1m5TJ2k|hzWn9m4;2s zK(*+Xp0!@fjs0wA^woZZIIuwUCNB%-zI;lsr@m6?v9tmC2z7;6wW-&)O=1+sq?&_{k=0z8I|acini9@utRBhf5V3%*f5&I0w5 zhm5wxoL)S>_Dm0P_TArkO(G&%qi*Ad*maE~wE5u|{di1D@x2kNdakQ0X!i^PmTOQ( z@K{#3RoJF*vejk9QYVwDqWZ=4@}MG+o=X=C3gUUw^V^Sp<6{qBWzWB$Ft}Ba!#|vu z*LnFvZ+OS_Ck#E;_g}Sw`qqqBm*~14?p>N`sm^=X&M)1(ro)Xwe@9IpS$(b+R8scb zw9k8))RQ7c`r{VeTTV40!}J0D;!Pl2=wy#F|F~XMyxX-umP>6J3N?y%YTVWN<`l&P zbuB%25wHC%9X(`kmyfqpEacd}lkiVr8FO=2gRU&Nia|q8nR52AG1kwTpxd;@U_VuB zFtUzB|Dg3R1oxIQ(3Qm#-*^1ZGG*XNCD?yiIs0n$E(_%S3rFR>$gz1HJ>setNq^=8 z;;hO2F4568F<|Ao&3BAl@f*5UDZld#!3iNS9D${T>asP%j7@FsyKUF1eH`n9U0KqC zV{?0DQ!e5oG8V``4Eb2yZrvkwP<94>QL&czTD%zy4Nmtvj6HMKuYp@hXI`_oVVVLA z*SLKIDIjCLn&Xbx+2G(rkC+PO;$6jJ=4z|^4n}q>$M={=HaSPS;LRtpIR}^;F}s zJ@X>&9~Vm9db{1|`3Zq1;pm(}ZSLHftmZ@m1fYc-gan)h3^LqY8S_zp0AIzyw zIFssPReV1cJNm}yKhUa|Bw0->H9xF;%0N_eXj_DFZ4{)6{c=Ktg_Q;3_fL?2qJjQ! zm-%J9DdHQ3L)=dj{pt&9I^}d5^iHvxWr}052-S6*eE&n{l94X$rnqNUQ3#q~4;A1Z zfY?S+6&S@^TZo-(v3%R--w}p>iJl3YT+#|?OxpUzb6Q+{T94<3+yqx;lkH%i*ggez z426DQG|(difbwWCZOx9UaI8P!7NS1ISkZN%`Q8$lLv zHG996r|XvQWi)T7b+U-GG1gS;7@B}>lMZ2dJH(IR#G66uTAS-5T;Qs*J2&+d6f{qq zrFH&H>m@eskrXSdKWS{{_7fLaitHm^T(J-!d<@K*xn2-wS_?;zzY+0hGgRKW2duMw@Y_V_5C`x;7J9b!Sb@8~bwr=0} zGO@Yl+7Mk0NLYK;AQHW|aLQpdI^*s?&3vDFmlGy+@Z9=rw|OL@`sXR9_4X9Y_8{e% zJmb;kMD0EAC2QCAz4o5K9u*_wp2GUVoO|`pn<-=vp7=iVyQ`bZZXEn9rZe*S#Z94Y zb6=CBQg&~Tjf>4Av<4or1-7Zq$tv#nF73())on_Goz>D`92V%#AP~=0hx~C_V#@IC z)?il>24php;-tHZKOO2~N759wL(fUFdZc3R%xbZJ|Nb-KmC)3;Dzd?psGtibjJ7mG zwWsYl_O~N22?5_#F$lTyS9V#2PL^SD9jUy@pX9Lg_&E8@fbTXUf#qc-Y!x3gdk45; zyV0wuI2!jaoW;ef@o?!)F+XsojwRDv(}@p$hdB4+AMJ?G%Gv(i1K3?RIEVOz_amJn z)(Tr*-;u2s1wFsSKlF0-vpK#GAM0S#w23p@A>`>im6Z3H^&-^V2AxNaxu#dIrd2L} zBl0nhV{K=>R)2%H+ehO4Q>tRj#DuVcek_1>IL;5vaV|O#4vKV1d9|g#moX|W5=BgZ z5NET7MsIE}tE(`)(>Og=>=muaqQ>vlT(s2l+6I|ilqrLA``a&&g!zD+h#iOc^Hi*f zo^r_30j=qA26o#^`eHQ=!Gx5Ya`p1?nMlbBC0L`dP{{qktehR?#h!xg>O`-NmwKJE zEk=6ul$=;@63+oXqJc+9D4RpzBrj^RMs<%y+^w#Q%GRxVw*!!5BA@+WjlwyrZmQ&^ zv_)x@4!pc~anm=3!#{tdl*Perz+io)x?*Bvhs1X1jA9-tQZ>NEo_zbe$;FxXoGYnt zg%EDBJoWH zhyM+82F^bNvz4X^Op&oO{wI=(qzoF_zw76-OEe_k9KY+Xov@+W6T01Ya33ap+K{Ll z$!o6tNmTz6Y-!6w&EOE4#Z_mU0^ zu+}WimQ(on&s<=AQTKk3m^Z$KpoMC*-*aZNrVqqLx%^0H0eO7C&k>2&eMfm*yd5~w z+mWtTWmgbb2^KI>Ncx*U?}>gB=yCs5`vGz=dAN$$*u&Z65%i-maU{PxLEL@G{7@4Y zRix{^`qAor@=9_(md$$P4##dCXO_jlSxaU>V(#Dm#O`9y`Ca1dlk|v<%_#YHJ;#L3}V@ON3OiLocg5*Q1 z;pIud?Hm0ZdKX5XLL1`gSPN{2Z)=#N4S58AW;?`zj#^_@eN;b6=%Z524IRfd22=6@ zD6vB1U40ZKU5~bU7ISwizeGbq6-_g)WvP66DeH@bP|TQ>S;V)Q%FYc+qwY0nC)k`# zs9;WXF4_jcld z1@Xw=h2F5uL*-<0mWOc0pN6(%`sjX|)QsipZeqfG6f8hG&S+{5<$!1+Ba!53Rj?OH zCnr6yVzNK|Cp&U1f$|$#)wVqV#o8ElkhX96Ty6OVfY&LbtZ#?RM6;(d*m@9gtYXJ0 zpyj8#BtFh-Ur=ZjnTCvRfxxLErM-mPx>``F*B=iJLI8mGZAq(yBn#ib{qh6v{2ocF zNr$^K!q!8n8kypE%7s0<5GR9DcfZzaaO|6UiCpyo6lZLLIXl*+c?Akn0_XYli%h-} zkFovnNC#OI%_bnBXmcR`{Yy%;N@0tZ67V1_l+ce$hldDg7O;+9XERj zkWI8mH2*9miA;t!;F>dHQ)^q^ftH7^S4&CUS=;YlWYP!VAsqn6e=7@S@j_k*>O%B6 z{WQ85E+$nf0$ydf;-el!@tN!<)Z>q(MWU!CLzMR z`cCU)yiG8^FH*$%ueE?`bUcJD-^Q++i5#|gk(8~&eyK>N&j;V_$nD!2wSQSo^cM{h zy?dyVpXxiwUVhRdfp54}cOtqk4HtY9b9cKgp0#3E7alWeZPVjzLui?@Aqz6JONaFq zArS1I!#A2ve#(w&>N3Z4u*Hz&?5%?tSo%~=nvH}_3kQWSG8^JR3F8$k?eG(AH~Eky z#_^CLo4mPb%GZnUc%0i zO^p014jToLNvf`tnDaGb#H&LtpUZ^X0tZl0--A3zq}^7PGxQ?jOT1)2onvcVWdo;L zz?X10I5yrxx@W^wDn{&k7?FNeuLxUzK9O6Lf5T;uvKxl=t_QQ4&E9r%l0&LOHDv|A zxU=G9e7=Teil5f+_Pe2Mg8m>kQaxRLW#Rt%+kYIcWee`~k4<(ykPqM%Fe@j0<@fcT zfaiilYHjaV=C^+blh9&}+flJgblkGr%b8!}^XI=^kRNruOugQxqTXWeZNz%`%zTht zjS`_TZ_EB?F&v*_9y8%sjQZ0Q^-wg%E2|*N-;5!@=RBZS+@;@p!QB!{32TTXz*ZwS z9Nozrl@lhy@uh#BEo}!?L{4PjJzS?!f^G3#d?I{;!2{Q!o;L=l8wrhzzllzcD}t*e zn7O)N5q&40tN9%}bNd9g{CX-s8Xl4Ea+o{%aFJTWLp7mWEGDy&naoF>>y1!GNC|lF zjDlfmX21Sd*lLCBJ7IIaim#s^rN3fZ7WM4HRyLPzcQ@5xrfGG&cHk^PN^mdG zxw^*CB}=UW@s4W9gMo~kC(eAL<=|?(&#k2l33X+#O-eq&p~v~M-PtH}iK<#+mkoMO z&AESAOKrT=mcH8y3z&W*q#XXY+ri$-$<`xx|EYGegoex?2c84MR|!XXS&N|bK7tW! z6+95siVsLZjFR^rxizaQ{ox~nG<#}#m^k_d4ndz|=#hEhGWqTx(tFp2U}A`SR$xW4 zAOKe)r-A`in(z?Ngkp80L7_h+z@}GqrYJA<~1=5Jx9>(R#6kr z>udLT9q?N+Yuc38p8X%=yaAGdho|h`tse?IUmBXp`X*$bKi*tWqGiRbP5a!BP?dT9 zSeQw?eG>m-QIp%8BuJ*_?kQU9l`6tt#J+O6EXH?eZh z)4Be)H8&c23hfxT*Nbozac@0Jx%gK$V^+{`vugWo@J{Wc09)Etaai4%BaGH%{RY-u z6{Mi3ZTf+smXgAVZru?V*!t<;OhTd^&^bN4K4=RAXKvTG^hfb<5cTynvxh zXsN0_x=-(dv9~NbO}V->@uBFHRS$-hldpQsfx2u~M7*SRbUF58if ziZ%+413IY)GdjuC@yogi@MpiX@TXxTIgtS%*FW5Ahts|wT*hwA+da>;TzU}2+{v)!@@#BqeSGNnTvdV&hE9aJeuy9sa1?E z66nyvhQ|EC5l>0jD_7v+Ybi$ zYRMoZpB^X)ij}0FaN&I~lImBxO}nAnPz}UICuh@Mg1|FkmQi=@al6rkHQP znK==^5@c(hpBmZ5pRmxid3Pl%<{*bGlM(+6yCHcUD#l6xR}aj?pO|?^L`%ExK8=#bwAbLpH+Nbe~ys2sl7k21B(qP!0UYQz3 zs!?)(b7Fe2Y=QiAr#0~i`+&)D^M5!_!WZyUQv9dP?Yhl^7h6F%PoXHO^fnoQGeD6? zJcm00EXCro8;xyg7O$x-h4Eo}Y(T8dHj&Z@KR{H3_Pc|rq*DIS`+mS1@FwtM*7j`K z1E=1eW4U_@kl*=wVWV|Fw#Sv{2_*A*>7s(AOo7MRPA%lv4f!iYG5G=mQ5hdvYw2C6xxXUyHb=j;#Se3 z7ME&mMANrk<|60_-dlJYs@b4S)7_GkwlW9g6+_w&h~6~+l0ZJ}6SU(o-?NuHeZ>Y$ z;E!hlYYamj`BO-(7eHfG+wI+nGcKzz@b#4!U8*=~l7)_&wApz^_a=19wXgsYfZEDy zmRsj{t)po2k}SsTq9TRLZ#(%u{MW>s=}ASufZki*Q=6eL=oxYOp@}-)h^=2qQH<52 zHDHozH##XlwXW~C$tbW2h0Bty)M&g?OKDBn3dm)U#2!-ISMDDfDB%@_`JSnYEt0)% z8>S+3m@8VJ?PVxebClI6ucw*lYP?|UkGD(@y3@5Bn}rSB9Bj7I=P|!~cqL4;>R3k( zUgVMd19o>D%@x7y(Zcne>58b}{8gDzJdjxxk^kw{s)7Hmg#7&Zd?Nltv$)YUb?wa0 z>M!?$nA?zm_gS9maewJ986hmTvBabcxfa_=xZj&p@0-oq~9!NIrHs=o< zS}u9QMKUijpT~r;+8UC0Mm_kD`nB|?R1VP-EGXvFN24a%C(dI5Tz@>{Y$D?=x_Hjg zXSYwBuL5W6{(Y<^Ql>+GegK=cY0UIz>kd!PXik!%xTCI=mx)A50}`L4Q?LfA3R#>&l73O>6C(fNP~M zY2yO&UJXDX>iyJIlIPS`U31jp<*&@_S^YQ{wNxgx*3_}#q1 z4#y|?%fePRJtzG}OJ^9gvcB;ux;DQ6QafM1u(b8sxkV&80y_eD3%)Xlx_?5VOyq5XN_G zyuVuh-Wpm(3yHT6MN89g*Ja8+2QX%ox3k0&OIj3#mzNCUFR9Lu93L;q6N`(P#6H3~ ztUK9?w45~mD%snz-dMNTLl0_{WR>JNGRy+`LTwO2OS9 z#5L3XQiOxbhE+gS?yiLjg4S;xNb51(;Kt(ilcHE{smvdu2y}j9b;PmW#8@)vs_gY9a#7dEkfeZ`J+u!k_-)Re|-Xn9# z`^tg|cT1jZF)zyNfFM(=JZs~HE`t5cyhH8tUX(?`Wo*I1rW>XlCbqg<^`vsoCXYFCh$0_w6jra-cuh#z zJ2k&htq8R|fu;mkc2YBM&{$Jz(hjF(>_)&XdT#XX186L*7G zks#Q)#V~r&eJRIRK0Sc%5QSLp(&p*oSHQf~ikF|HDh7#mNHlB@m65TAm8c{HA4ZwF zvde=jE}PZTUsHJ)g&sR6@Tr6nSuhboIWa_T$+J}tb%=rRy)k)~q&z*qN4gSL_sI1^ zZfOxpS6Nx68rjiI8~2m{`fCtpcifHEEcvqdi@H7-Bq~7e5>P1(`B_xn4sZ{{Q=fY| z3s~mgwX#^IBriB(aqJQ=sR2Hs)?#BJr z-h9Kdcoq{Qxzbz>%rBiUT^r%_Jt4S3mqwfc5Y~jQO;)u>l*{2e2dCNxRy%iAd-Ua8y{vcUA~&gj3q~y$VhKWdUQ+jk@FYM?_+8UHad;#W(r zk5^E?NKbw@tH2d-6v>J)SAN0TpOZ(=sDGZpcr?Hxy&GI%BE377vxG*W;=k?wYn8KH`)J&?RHGO-XG(lcPVJS4P1Hj3v9Nm@KB=-UrAS z0o*c#ZseojY90F_o*e2L>NNdspH>dgg_2K4D9iGu%$=*IJu&M()L%08lB}%GCKrvK zoV)b@+L%MM@V;LrBEd)U51h5qCMz|on^Nqad9Fy5%{3Wkl4Y3YNQOD5w`}&v-&gHH zy?_TpH!Duox5M}}?ObypS+Teqq7${FZ%>xKK{$#| za_Vrf&EuHDI&+!b-)37KrSRl39kz=D3%)IRCWlR9}W?=KL7f3#8SGoK8cFw+%+=k2R< zH}s5bKg}38lGoPO0wB6C5;iTO&E=H2ACqfgDV4);(0~Tw-+lEPqg@Xmg^Lu!0m#pw zMx++ZgzbmP?xH$R^-|~Q2)q=&VLh($}c_eVu0zaR&)CmqD14n5{jLy0|HC zw(Ti5JT1htFV&S#Q!m|Sq4%?P}Wg;f~ zw&V}r`RWd}2H;HrJf>PFqn!#WX+Yslu+soqAR*v~fz#4di%IqRCi`=Y`Yq&CH7VRc zt$joK1r?KuGcFNE2xcOEXK)IjWNCHo(vg#|-}1T7LZ$RSGL&E?yuY7?qQy0Q*q5`^ zrL?GOP6>L{Km@yqu7~zYnGKt1yPJQn27`?%92)XW$hO<4C2Sl(pDy<(Z<@ zxc>)3X+MMeX%Pd06}%Xc;#k*Dm>7xC((z1Y#Q+{Qz;%R$4kZQtl5O<$M9;OfEd8@J zXKLebSZD?$!=N4Xdx54>hFz>>0ju9CTgY z5X2{N;0=>(w#)+&`->ky&aVN*MaKPffYphoWHRq_cnDH`Xf*-^Gui!Bw1={qi33Ug zc!E0jlX>Vg8K88sccL{S<#H^9wVf9=hXJt11Q3I$2z=efpb@v;>+7y$j+s3jG0jYv zvyQvCEgp%~LRn}&s<&)J8{mpiTTj1W9})|%cW5v34-``AEb$|!I)KMr^*zLGsE~uO z%P z_CA4sv0s75ezi8^@x>AQWLn*ru1}v7CCEs+oo~ zSS^)GfS=uUjTyf>6EN~Fla^fkQn2X+*D;!cc-xXsqa=8oMuD5z7Par~tr4@~as=x% z&c#PA*R10vcwEJK6Oc#eyS!jxWg`9MCa0??%4c*DvXM)q@8N{j2xae@Eor`z) zy$A8ts+1`N9l7sd;(A7UQ#^O?&&jRvn2Q}&8eJ}vyHv5tO{vEF3@?aT~v0g_6kgCBUU1?Nj zOMf}i?$Ne9`$tF41hHf}8&=Itxy7-4@4gEEJF)C&sO!|>XWv+YWhE+++PMJ(U@XC* zzMSxy^A(o3^0JA=jrhI>Kl*_q)xI`bKWSyNLFl?!{A|}mZyL>{SiSYFpVkR!1`Sh< zNBaj^hY5%17?_`dsd@*E_)bHq;8qImGZ1@yTFbAMSnnlbeO43VN=}sePAU72t*@WP zP*j<*RkDMuy+9JIK?Rx=Q-a00XqfS(?(W54D>(A}xtb36F}HPTV5Z|?_|SVM7qzoh zF)BZ~l|={3=@8zviy^ml-{WHAiKyAQQtzm=5wN#m(tf7N-8{j9CQ=5L3O>HRGnfts zCV)FV`Q+UzU1MIQIuP9`ec1}hyxOG^q1-FlWO!Ka`6svA$f@T4{*mksa+UfzbWRG; zO67olto{HpebJEEXm{>BEk|ek{d6dxZSC$<;WzxVA22|6O|{7A6-ddXt0263d8mfi zC&_+{fYzm#eFc$0+`Ae*T!!$^|6`M&0Hroca&VA){uI2gFA zFjGPH&;N;aDT_d-r4e7zH8nLgh5=Tz5licgk3S#$BQ1JAO!?}M0N#82|Do)0O6BFV zhfksv24=8trygF@)RJN-!(fujOba@PB$sbX-Up~zp!!i)*W^tI_*FbmueJ)5)b#!3 z{XVg_wW>CE@ft69Z6G?QCJ}*mrQ^(NS|`=@JbGR`ItHO%dm{8YNH^fmv)b_qWBMEF z4}r1L8!JBpDXCQwqb$Jk-$irgCa-b!$z}jG1qaul)4-o2$P8YGe$2dstw|UF7>Kc< zA*I^hu&SaWH2y1)Q)#m#w6?ZZR9QKk3m3v*?ua@Ee`&i)-xhm;(O;?_*4D91gnt29$10rKG;r1F&Hyhe+|OO$8t!7Oe?^jvJsj*usi= zo4Nco!1J6MB=)?#!T{sa#0nIt01@DmlRIK=3j(Hu25{<>r~r&zLI43&xl>Rfe&pgj zlR5idii>1@>|k1%EOH|!r+&9DpWNwdBHFc#=Ht%&gwvI8g7=I}n3FMIRhP5^h2U?v z1-6VCp2}i?2cLmKafvNPXQ^|~CN+l>1LR;PWpxzsb0w_hmiJk1&Dd+v+4m0QUL}yO z6&DbklvGf?d$#hK>BGe`h;ky}@Bm7vT7iVmoU@|vrNbfq0lBEKa9!Xt2m<(rfb1&# zK^u3IjC{(hnc48gm-?F0^k~3kC)MJ&5l~ege7yUYg1pyKiL8Oz2L)y4N>KgED2gs^ zm0|_onldpeu7#%B`1trLhIEZuS^9=5H{*|&`;$WYr5!Ly6inEY6}vLx|CBm#7gHt( z#jh<6?I>{+ji>sh00gdXwpE;>dS*^y&BHPtRw=FNV(wm5pd6QJa(&nZAXffWvjS#C zXG!i~wn_+V%GyVKj)j*M0u=D)ZsEo=BeG}NwL(vEH1fq(a2xjJ3VcIp4#(h^smuWX)zhMey;Z09!O|<6Nx|m zBfte$7kz+Z7x$TD$IQ}}nJZ#(Pn?%JA27)#0V;O2Z!s_F=|E~oqFa(QP__WbJy80B zM(NkP!I89mmIxF5rMeHlU!G%21;K)1yssVh9^7{#gWl-m6sV0VO_x{&l6eedfvJ3u zphu%dLy>a!yqwIkP@=KgBwG)ay(cQw=WY;&r(FIHA8!`%iuH1?ku5T+J-z$y8&NT6 z+bmKZ_zp@*0grf|pfge{Oy7qS9(dz?>)&$ZsH!H^%Rhf~Tian!X@o~@Ko{n! zq4ptbyo)RlDUp^6#f}IAyYvT(Ep>i}vbpQrY##&izxiLRd_Uf``pe$1p1s%RdG6=l*L~mFP2yDi!zA#{t%(Kt4-Ntoz#W#J*U67pfHw2kz2lj^QIfMKY=*DXa%a^vC#SP=${HH<^?H}xZpBcsBEAwJ5VH?0x*O5BK*eR$ z@MJ$=+=dG)|47^{0oFlBb#3k0vBt^FA59slfa$(%UK%ju2l?0jl>2s%{pJ~#l0o6tjGV-7j-_HeVlOb< z+ez%)z~<+~7Io&+d-tOAf;_72y6e;P8lx!vUFh)~$$^70o`g`$vv7}GU34zEUb$Z- z#ec}bBBBiDFe_%U)hyUDL~nlyE(UwGw);Is3U80NW;yJ}=(2us;D>4W_qqIYUCvb8 zAuFG4ke9=zvV7&U?%j7K|70Dc!$;KAVFm;H@L{C%9bOdWba?7@*O-vjkH)q@c}8*d zA0|Z1DL>H)g|fbwy|doN=Rp- z_X^8OpO5q1yx6AVEy-AH1i_5WH+BZhSiK{DZ{6y8*VV-{(TsL8mZJRdaHU`pEUEdU zF5;Eqe8~mP;$SaMJN?qfr!HtG zYOD=B(wjPFz5XyNDhiu&X$jxX*De~(+q1@FY~ip3s*VFAuI;>W>~wmZt)juP=>w#@B?_PdzdVz7SkqIlj> zDa5T{V=aUV77TdY4FCL({Po}WM7k0M`+qPU6#1ZYvRYvjXt z_mPfI+$GiNfUjQWI$`)kRemM~_yF*r>8|phN%V0qNdGicH!Obt67 zJF@@ljAv@t2kSgNZf%qH{qGm!lLb0ll>p#UrU6cU3Q+0^#HdE_m*4EEZQ$1}s*ev0 zY9;7jR~-0c73ou^Djt?h*VFb|tysG2-)E#>ihw$+_Sz!X?Ufp5E)j5>4H>&9UH64x zi`*O?d87^%;D_u&vJ-EdA4&UVP)#2SuGr8=J-VoQBzLF`LA$n)aj#(6oxF=jkr$f3HWEs)`#WL9z29CX1e0Q^9n0r;OYcheFh# z)*OrxdUQbnoe!b+o=S|jq$c{voXE`i42%RibYpndEGF2Hl~c`rU?jAZNC$EuTgr=l zr*N?idK;s1X)gUJTq1C-Mizc9y7YhmEw}rCZka6cVsU( zmV(MXZI57=?~X=H$8OU*T+qF!$-YxN2Q4DU#eyA}c-S>m?qTpWdCai({e?@cul38EX;En|icoJFbIey^(hIyen?h(Lbb2woUY%TAk3r z%=@F`Ih0ob$#VTDtWQeYfpgQ?p6^rY3OZ^*Fc&7@6yys~oy0Eo026Spedm@tF#gD$9KusSp`(s$deHX zK(FNVpw;MPS`lP0!`9|;aCW%h`0&)uh63#y`=hH99u3B~C^z;ts}?TPY`9R%>Cv(| zRq@(N9j`PSTvW|-x3?qEx`$Aw3to^=*x7-SjlhBOtNWgag&`@~lH{9nm7g*G_x($z z-PHX20A|}29Q8;dsv+R-DrxCTgx2u;igK%u($>kJplW^@bf%CQ^p$M+y~?;7fm$C% z^E2gNJTXcThD=?}Q=NCxBr-X{_V!dabnDtJ}w|62K4?gvRY2f2bo%7C14Bw9p~ zP)rSH2?G^etJoUWV4TYijQg{{>j-fT1_;To>uPmKC`7Bt#9!SRF(FX)`jw4xtWj1u zQmn|~oQ`*~cNO6n53h?PrGEC6c2T=Th(RL3uc8EpF2$HXt4KJIw4?dH@eO0ao&Ds8zc0x_*wFsenhU7;S38xr{~d;Gf|$M{A* zfC^XJ)^8aG2M5F8k_VFlXL)HEBLHBt$AZvaxC#I{T6o2Rr9g44m}*_zTMzS=3(IMrziBlXPFS zBTW>@L|nu=GS5?y5cYUDu;N}Z3Ftj}5a7-&hWm0@7Kr_kjhruCMG6<f*n|j|? zonGkSTl4Tx4r>6|Hj4o)o=t!oMoH2{So18?VtRezI?bg{Z{u{aLy~B>^V$)l{a7Khr9!N36k&3?ZRKBmee;YT-Us8TBzqS?YU*k3;N=A?^fvYwemd39 z_0ysSmj8uv5=fVa2uzY$9a}CM1obLtKajr&h@)z0LH^=}L-rfs$hH?4d2u**^5DhS zV=g(?I#qgarI?Vc@<_l77amwZaqYN$+0IwqG+;u_hJl)*)cf;nE2YrICV0Pq10`xh zA-MwR-~!IJ6-rBrD6aOHUuOK{wTjUW^UdIC8HBDepVvf#pasI|5hPLYUag76GXkoZ zoZ+a1XD?omyC2}Rr5<{Ch?U}u>`rfjj zlzUwji})B}oLLhAd8o!ExVuEpN&a9HZy^ATTb6G|jEn+-Kh^Y1;4hjCTm;F(cMa9n zJ_(?9;{FL67iRA;XSu7lQy~VPwwW0LY56)A0~J3^?#U`&26mt3aTbp;DY~|{9B6t$ zF;FbnbEt(h?k_N(5!3guY9N;rPv*hI(+#lRuEeT2msxxh32`Pc-YGRaRZl7u^YoD{S2|l>voeC<6cGR}d;V=@y(sIY`K$~H zZnNA$@GH{noUaXHH>(hJwo69|FkGC2K}VjRq2I!?+h|q9yP9eLJl0Ad&7=@-BosS? zXThFQpIC}IcPG6tyj`*hQg>-?89q;8en%S`XrtL?U)1LdgNm7o2~bp|%>;eXYKW(!JvjIF zg`xTRp=vH?h|}5jxm+LKzrIcR;r1}Q_P{hrOm6A;?(EjAlxfIRH2wPyTVsi8x6jwS zU^;2Cgmmp3IpiA=N#Q>#)%dprv@)sK1+Rc+l)&45)RRCD#3Jv zLcfS~+FIDyaOn>Cm?VFEn{^^Jql%A5Z03=17()t6ci=eAj>|GB;(%eV0JqZ5@SExK zx{1jGd#=$0vDlPfHuzOtf>g8R!L4_?bWNR0xI7&BZTZ{s(=pY{b?$Ntex!JA@Sfkj zqV2LZW=04E$GG@fe;gq7OUs9x!H;=kjkIq_H`iWuk@O2iaIM@KF*M13I78a5M-Ly{ zmdy0Zrk959mDVK>3fGPDRz?>2){Vgj16gea!k1sbo9+4rak}I3e~eG6E;1*1iG-?c z&u6`a--wsW*=9}ej~&^!2H>fRC$J(qbijucx;-!qIFT;E?q4MAD!_CDrJ5yyxAn|{ zl+UeS6h6#*l0qo9xMm6YM_FYQLt!D$6x(Xh=((D;3M6{1C?l<5yjf^+`v0wktkY*m z@A-2_9QMSniFn8;?DG`pKiF(!)T)f!qq}DnLoDrmQu*h*OE11u*pqnW$Xf2>Yx)0l zq`B??kD}23tB?8be$oG?uWjvod7B6rq(Zk96P7{yG*f{-zg#_G?;Ld@yCIR=)>NTW zfPtz`-1ECCRw)1H!Jjwb=acaB@9=X@_&G8DTpfNck^jXyLG+DIknAs~@(Omvk2Tuf Kvi6+!o&NzP!7EV! literal 0 HcmV?d00001 From 7b6f23cbaf4cd94545aa74de07738e7c7ea8723d Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Fri, 13 Sep 2024 14:23:17 +0530 Subject: [PATCH 10/29] Fix date format for `InputAge` component (#298) * Fix date time format for `InputAge` component * Add unit test for negative scenarios * Fix lint error --------- Co-authored-by: Siddharth Agarwal --- .../screens/actionInputs/InputAgeScreen.kt | 111 +++--- .../parameter/ParameterSelectorScreen.kt | 17 +- .../ui/designsystem/InputAgeSnapshotTest.kt | 101 +++-- .../ui/designsystem/component/InputAge.kt | 372 +++++++++++++++++- .../designsystem/component/InputDateTime.kt | 2 +- .../component/internal/DateTimeUtils.kt | 62 ++- .../component/state/InputAgeState.kt | 66 ++++ .../ui/designsystem/component/InputAgeTest.kt | 159 ++++++-- ...AgeSnapshotTest_launchInputAgeSnapshot.png | Bin 130 -> 69894 bytes 9 files changed, 721 insertions(+), 169 deletions(-) create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/state/InputAgeState.kt diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/actionInputs/InputAgeScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/actionInputs/InputAgeScreen.kt index 733b5a0ba..6af361b3c 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/actionInputs/InputAgeScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/actionInputs/InputAgeScreen.kt @@ -11,10 +11,11 @@ import org.hisp.dhis.mobile.ui.designsystem.component.AgeInputType import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer import org.hisp.dhis.mobile.ui.designsystem.component.ColumnScreenContainer import org.hisp.dhis.mobile.ui.designsystem.component.InputAge -import org.hisp.dhis.mobile.ui.designsystem.component.InputAgeModel import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState import org.hisp.dhis.mobile.ui.designsystem.component.LegendData import org.hisp.dhis.mobile.ui.designsystem.component.TimeUnitValues +import org.hisp.dhis.mobile.ui.designsystem.component.state.InputAgeData +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberInputAgeState import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @Composable @@ -24,100 +25,108 @@ fun InputAgeScreen() { ColumnComponentContainer("Input Age Component - Idle") { InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), inputType = inputType, - onValueChanged = { newInputType -> - inputType = newInputType - }, ), + onValueChanged = { newInputType -> + inputType = newInputType ?: AgeInputType.None + }, ) } ColumnComponentContainer("Input Age Component - Idle Disabled") { InputAge( - InputAgeModel( - title = "Label", - inputType = AgeInputType.None, - state = InputShellState.DISABLED, - onValueChanged = { newInputType -> - inputType = newInputType - }, + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), + inputState = InputShellState.DISABLED, ), + onValueChanged = { newInputType -> + inputType = newInputType ?: AgeInputType.None + }, ) } ColumnComponentContainer("Input Age Component - Date Of Birth") { InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), inputType = AgeInputType.DateOfBirth(TextFieldValue("01011985")), - state = InputShellState.DISABLED, - - onValueChanged = { newInputType -> - inputType = newInputType - }, + inputState = InputShellState.DISABLED, ), + onValueChanged = { newInputType -> + inputType = newInputType ?: AgeInputType.None + }, ) } ColumnComponentContainer("Input Age Component - Date Of Birth Required Error") { InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + isRequired = true, + ), inputType = AgeInputType.DateOfBirth(TextFieldValue("010")), - state = InputShellState.ERROR, - isRequired = true, - - onValueChanged = { - // no-op - }, + inputState = InputShellState.ERROR, ), + onValueChanged = { + // no-op + }, ) } ColumnComponentContainer("Input Age Component - Age Disabled") { InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), inputType = AgeInputType.Age(value = TextFieldValue("56"), unit = TimeUnitValues.YEARS), - state = InputShellState.DISABLED, - - onValueChanged = { newInputType -> - inputType = newInputType - }, + inputState = InputShellState.DISABLED, ), + onValueChanged = { newInputType -> + inputType = newInputType ?: AgeInputType.None + }, ) } ColumnComponentContainer("Input Age Component - Age Required Error") { InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + isRequired = true, + ), inputType = AgeInputType.Age(value = TextFieldValue("56"), unit = TimeUnitValues.YEARS), - state = InputShellState.ERROR, - isRequired = true, - - onValueChanged = { - // no-op - }, + inputState = InputShellState.ERROR, ), + onValueChanged = { + // no-op + }, ) } ColumnComponentContainer("Input Age Component - Legend") { InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + isRequired = true, + ), inputType = AgeInputType.Age(value = TextFieldValue("56"), unit = TimeUnitValues.YEARS), - state = InputShellState.ERROR, - isRequired = true, - - onValueChanged = { - // no-op - }, + inputState = InputShellState.ERROR, legendData = LegendData(SurfaceColor.CustomGreen, "Legend", popUpLegendDescriptionData = regularLegendList), ), + onValueChanged = { + // no-op + }, ) } } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/parameter/ParameterSelectorScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/parameter/ParameterSelectorScreen.kt index 794a3ff28..46b103fab 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/parameter/ParameterSelectorScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/parameter/ParameterSelectorScreen.kt @@ -17,7 +17,6 @@ import org.hisp.dhis.mobile.ui.designsystem.component.ColumnScreenContainer import org.hisp.dhis.mobile.ui.designsystem.component.DateTimeActionType import org.hisp.dhis.mobile.ui.designsystem.component.DropdownItem import org.hisp.dhis.mobile.ui.designsystem.component.InputAge -import org.hisp.dhis.mobile.ui.designsystem.component.InputAgeModel import org.hisp.dhis.mobile.ui.designsystem.component.InputBarCode import org.hisp.dhis.mobile.ui.designsystem.component.InputCheckBox import org.hisp.dhis.mobile.ui.designsystem.component.InputDateTime @@ -43,7 +42,9 @@ import org.hisp.dhis.mobile.ui.designsystem.component.parameter.model.ParameterS import org.hisp.dhis.mobile.ui.designsystem.component.parameter.model.ParameterSelectorItemModel.Status.CLOSED import org.hisp.dhis.mobile.ui.designsystem.component.parameter.model.ParameterSelectorItemModel.Status.FOCUSED import org.hisp.dhis.mobile.ui.designsystem.component.parameter.model.ParameterSelectorItemModel.Status.UNFOCUSED +import org.hisp.dhis.mobile.ui.designsystem.component.state.InputAgeData import org.hisp.dhis.mobile.ui.designsystem.component.state.InputDateTimeData +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberInputAgeState import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberInputDateTimeState import org.hisp.dhis.mobile.ui.designsystem.resource.provideDHIS2Icon import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @@ -134,14 +135,16 @@ fun ParameterSelectorScreen() { helper = "Optional", inputField = { InputAge( - InputAgeModel( - title = "Age parameter", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Age parameter", + inputStyle = InputStyle.ParameterInputStyle(), + ), inputType = ageInputType, - inputStyle = InputStyle.ParameterInputStyle(), - onValueChanged = { - ageInputType = it - }, ), + onValueChanged = { + ageInputType = it ?: AgeInputType.None + }, ) }, status = when (ageInputType) { diff --git a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/InputAgeSnapshotTest.kt b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/InputAgeSnapshotTest.kt index 0b5d7bf56..c4b79d1ca 100644 --- a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/InputAgeSnapshotTest.kt +++ b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/InputAgeSnapshotTest.kt @@ -4,10 +4,11 @@ import androidx.compose.ui.text.input.TextFieldValue import org.hisp.dhis.mobile.ui.designsystem.component.AgeInputType import org.hisp.dhis.mobile.ui.designsystem.component.ColumnScreenContainer import org.hisp.dhis.mobile.ui.designsystem.component.InputAge -import org.hisp.dhis.mobile.ui.designsystem.component.InputAgeModel import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState import org.hisp.dhis.mobile.ui.designsystem.component.SubTitle import org.hisp.dhis.mobile.ui.designsystem.component.TimeUnitValues +import org.hisp.dhis.mobile.ui.designsystem.component.state.InputAgeData +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberInputAgeState import org.junit.Rule import org.junit.Test @@ -22,74 +23,96 @@ class InputAgeSnapshotTest { ColumnScreenContainer { SubTitle("Input Age Component - Idle") InputAge( - InputAgeModel( - title = "Label", - inputType = AgeInputType.None, - - onValueChanged = { - }, + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), ), + onValueChanged = { + }, ) SubTitle("Input Age Component - Idle Disabled") InputAge( - InputAgeModel( - title = "Label", - inputType = AgeInputType.None, - state = InputShellState.DISABLED, - onValueChanged = { - }, + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), + inputState = InputShellState.DISABLED, ), + onValueChanged = { + }, ) - SubTitle("Input Age Component - Date Of Birth") + SubTitle("Input Age Component - Invalid Date Of Birth") InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), inputType = AgeInputType.DateOfBirth( TextFieldValue("01011985"), ), - state = InputShellState.DISABLED, - onValueChanged = { - }, + inputState = InputShellState.DISABLED, + ), + onValueChanged = { + }, + ) + + SubTitle("Input Age Component - Date Of Birth") + InputAge( + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), + inputType = AgeInputType.DateOfBirth( + TextFieldValue("1991-11-27"), + ), + inputState = InputShellState.DISABLED, ), + onValueChanged = { + }, ) SubTitle("Input Age Component - Date Of Birth Required Error") InputAge( - InputAgeModel( - title = "Label", - inputType = AgeInputType.DateOfBirth(TextFieldValue("010")), - state = InputShellState.ERROR, - isRequired = true, - onValueChanged = { - // no-op - }, + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), + inputType = AgeInputType.DateOfBirth( + TextFieldValue("010"), + ), + inputState = InputShellState.ERROR, ), + onValueChanged = { + }, ) SubTitle("Input Age Component - Age Disabled") InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), inputType = AgeInputType.Age(value = TextFieldValue("56"), unit = TimeUnitValues.YEARS), - state = InputShellState.DISABLED, - onValueChanged = { - }, + inputState = InputShellState.DISABLED, ), + onValueChanged = { + }, ) SubTitle("Input Age Component - Age Required Error") InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), inputType = AgeInputType.Age(value = TextFieldValue("56"), unit = TimeUnitValues.YEARS), - state = InputShellState.ERROR, - isRequired = true, - onValueChanged = { - // no-op - }, + inputState = InputShellState.ERROR, ), + onValueChanged = { + }, ) } } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAge.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAge.kt index 1784560f3..b8133c932 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAge.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAge.kt @@ -21,8 +21,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction @@ -36,8 +38,12 @@ import org.hisp.dhis.mobile.ui.designsystem.component.TimeUnitValues.YEARS import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateTransformation.Companion.DATE_MASK import org.hisp.dhis.mobile.ui.designsystem.component.internal.RegExValidations import org.hisp.dhis.mobile.ui.designsystem.component.internal.dateIsInRange +import org.hisp.dhis.mobile.ui.designsystem.component.internal.formatStoredDateToUI +import org.hisp.dhis.mobile.ui.designsystem.component.internal.formatUIDateToStored +import org.hisp.dhis.mobile.ui.designsystem.component.internal.getDateSupportingText +import org.hisp.dhis.mobile.ui.designsystem.component.internal.getSelectableDates import org.hisp.dhis.mobile.ui.designsystem.component.internal.isValidDate -import org.hisp.dhis.mobile.ui.designsystem.component.internal.parseStringDateToMillis +import org.hisp.dhis.mobile.ui.designsystem.component.state.InputAgeState import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2LightColorScheme import org.hisp.dhis.mobile.ui.designsystem.theme.Outline @@ -53,6 +59,8 @@ import java.util.Calendar * @param uiModel: data class [InputAgeModel] with all parameters for component. * @param modifier: optional modifier. */ +@Suppress("DEPRECATION") +@Deprecated("This component is deprecated and will be removed in the next release. Use InputAge instead.") @OptIn(ExperimentalMaterial3Api::class) @Composable fun InputAge( @@ -117,6 +125,7 @@ fun InputAge( previousInputType == None && (uiModel.inputType is DateOfBirth || uiModel.inputType is Age) -> { focusRequester.requestFocus() } + else -> { // no-op } @@ -154,6 +163,7 @@ fun InputAge( enabled = uiModel.state != InputShellState.DISABLED, ) } + is DateOfBirth, is Age -> { BasicTextField( modifier = Modifier @@ -255,7 +265,8 @@ fun InputAge( showDatePicker = false if (uiModel.inputType is DateOfBirth) { datePickerState.selectedDateMillis?.let { - val newInputType: AgeInputType = updateDateOfBirth(uiModel.inputType, TextFieldValue(getDate(it), TextRange(getDate(it).length))) + val newInputType: AgeInputType = + updateDateOfBirth(uiModel.inputType, TextFieldValue(getDate(it), TextRange(getDate(it).length))) uiModel.onValueChanged.invoke(newInputType) } } @@ -268,7 +279,6 @@ fun InputAge( ButtonStyle.TEXT, ColorStyle.DEFAULT, uiModel.cancelText ?: provideStringResource("cancel"), - ) { showDatePicker = false } @@ -296,30 +306,315 @@ fun InputAge( } } -private fun transformInputText(inputType: AgeInputType): String { - return when (inputType) { - is Age -> inputType.value.text - is DateOfBirth -> inputType.value.text - None -> "" - } -} +/** + * DHIS2 Input Age + * Input field to enter age. It will format content based on given visual + * transformation. + * component uses Material 3 [DatePicker] + * input formats supported are mentioned in the age input ui model documentation. + * [DatePicker] Input mode will always follow locale format. + * @param state: an [InputAgeState] with all the parameters for the input + * @param modifier: optional modifier. + */ -private fun getTextFieldValue(inputType: AgeInputType): TextFieldValue { - return when (inputType) { - is Age -> TextFieldValue(transformInputText(inputType), inputType.value.selection) - is DateOfBirth -> TextFieldValue(transformInputText(inputType), inputType.value.selection) - None -> TextFieldValue() +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun InputAge( + state: InputAgeState, + onValueChanged: (AgeInputType?) -> Unit, + onNextClicked: (() -> Unit)? = null, + modifier: Modifier = Modifier, +) { + val uiData = state.uiData + val inputType = state.inputType + val uiValue = remember(getTextFieldValue(inputType)) { formatStoredDateToUI(getTextFieldValue(inputType), DateTimeActionType.DATE) } + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + + val maxAgeCharLimit = 3 + var showDatePicker by rememberSaveable { mutableStateOf(false) } + + val helperText = remember(inputType) { + if (inputType is Age) { + inputType.unit.value + } else { + null + } } -} + val helperStyle = remember(inputType) { + when (inputType) { + None -> HelperStyle.NONE + is DateOfBirth -> HelperStyle.WITH_DATE_OF_BIRTH_HELPER + is Age -> HelperStyle.WITH_HELPER_AFTER + } + } + val selectableDates = uiData.selectableDates ?: SelectableDates( + MIN_DATE, + SimpleDateFormat(DATE_FORMAT).format(Calendar.getInstance().time), + ) -private fun updateDateOfBirth(inputType: DateOfBirth, newText: TextFieldValue): AgeInputType { - return if (newText.text.length <= DATE_MASK.length) { - inputType.copy(value = newText) + val datePickerState = rememberDatePickerState( + selectableDates = getSelectableDates(selectableDates), + ) + + val calendarButton: (@Composable () -> Unit)? = if (inputType is DateOfBirth) { + @Composable { + SquareIconButton( + modifier = Modifier.testTag("INPUT_AGE_OPEN_CALENDAR_BUTTON"), + icon = { + Icon( + imageVector = Icons.Filled.Event, + contentDescription = null, + ) + }, + onClick = { + focusRequester.requestFocus() + showDatePicker = !showDatePicker + }, + enabled = state.inputState != InputShellState.DISABLED, + ) + } } else { - inputType + null } + + var previousInputType by remember { mutableStateOf(inputType) } + LaunchedEffect(inputType) { + when { + previousInputType == None && (inputType is DateOfBirth || inputType is Age) -> { + focusRequester.requestFocus() + } + + else -> { + // no-op + } + } + + if (previousInputType != inputType) { + previousInputType = inputType + } + } + + val dateOutOfRangeText = "${provideStringResource("date_out_of_range")} (" + + formatStringToDate(selectableDates.initialDate) + " - " + + formatStringToDate(selectableDates.endDate) + ")" + val dateOutOfRangeItem = SupportingTextData( + text = dateOutOfRangeText, + SupportingTextState.ERROR, + ) + val incorrectDateFormatItem = SupportingTextData( + text = provideStringResource("incorrect_date_format"), + SupportingTextState.ERROR, + ) + + val supportingTextList = provideSupportingText( + inputType, + uiValue, + state.supportingText, + dateOutOfRangeItem, + incorrectDateFormatItem, + selectableDates, + ) + + InputShell( + modifier = modifier.testTag("INPUT_AGE").focusRequester(focusRequester), + title = uiData.title, + state = getInputState(supportingTextList, dateOutOfRangeItem, incorrectDateFormatItem, state.inputState), + isRequiredField = uiData.isRequired, + inputField = { + when (inputType) { + None -> { + TextButtonSelector( + modifier = Modifier.testTag("INPUT_AGE_MODE_SELECTOR"), + firstOptionText = uiData.dateOfBirthLabel ?: provideStringResource("date_birth"), + onClickFirstOption = { + onValueChanged.invoke(DateOfBirth.EMPTY) + }, + middleText = uiData.orLabel ?: provideStringResource("or"), + secondOptionText = uiData.ageLabel ?: provideStringResource("age"), + onClickSecondOption = { + onValueChanged.invoke(Age.EMPTY) + }, + enabled = state.inputState != InputShellState.DISABLED, + ) + } + + is DateOfBirth, is Age -> { + BasicTextField( + modifier = Modifier + .testTag("INPUT_AGE_TEXT_FIELD") + .fillMaxWidth(), + inputTextValue = uiValue, + helper = if (helperText != null) provideStringResource(helperText).lowercase() else null, + isSingleLine = true, + helperStyle = helperStyle, + onInputChanged = { newText -> + if ((inputType is Age && newText.text.length > maxAgeCharLimit) || + (inputType is DateOfBirth && newText.text.length > DATE_MASK.length) + ) { + return@BasicTextField + } + manageOnValueChanged(newText, inputType, onValueChanged) + }, + enabled = state.inputState != InputShellState.DISABLED, + state = state.inputState, + keyboardOptions = KeyboardOptions(imeAction = uiData.imeAction, keyboardType = KeyboardType.Number), + onNextClicked = { + if (onNextClicked != null) { + onNextClicked.invoke() + } else { + focusManager.moveFocus(FocusDirection.Down) + } + }, + ) + } + } + }, + primaryButton = { + if (inputType != None && state.inputState != InputShellState.DISABLED) { + IconButton( + modifier = Modifier.testTag("INPUT_AGE_RESET_BUTTON").padding(Spacing.Spacing0), + icon = { + Icon( + imageVector = Icons.Outlined.Cancel, + contentDescription = "Icon Button", + ) + }, + onClick = { + focusRequester.requestFocus() + onValueChanged.invoke(None) + }, + ) + } + }, + secondaryButton = calendarButton, + supportingText = { + supportingTextList.forEach { label -> + SupportingText( + label.text, + label.state, + modifier = Modifier.testTag("INPUT_AGE_SUPPORTING_TEXT"), + ) + } + }, + legend = { + if (inputType is Age) { + TimeUnitSelector( + modifier = Modifier.fillMaxWidth() + .testTag("INPUT_AGE_TIME_UNIT_SELECTOR"), + orientation = Orientation.HORIZONTAL, + optionSelected = YEARS, + enabled = state.inputState != InputShellState.DISABLED, + onClick = { timeUnit -> + onValueChanged.invoke(inputType.copy(unit = timeUnit)) + }, + ) + } + + state.legendData?.let { + Legend(it, Modifier.testTag("INPUT_AGE_LEGEND")) + } + }, + inputStyle = uiData.inputStyle, + ) + + if (showDatePicker) { + MaterialTheme( + colorScheme = DHIS2LightColorScheme.copy( + outlineVariant = Outline.Medium, + ), + ) { + DatePickerDialog( + modifier = Modifier.testTag("DATE_PICKER"), + onDismissRequest = { showDatePicker = false }, + confirmButton = { + Button( + enabled = true, + ButtonStyle.TEXT, + ColorStyle.DEFAULT, + uiData.acceptText ?: provideStringResource("ok"), + ) { + showDatePicker = false + if (inputType is DateOfBirth) { + datePickerState.selectedDateMillis?.let { + val newInputType: AgeInputType = updateDateOfBirth( + inputType, + TextFieldValue(getDate(it), TextRange(getDate(it).length)), + ) + onValueChanged.invoke(newInputType) + } + } + } + }, + colors = datePickerColors(), + dismissButton = { + Button( + enabled = true, + ButtonStyle.TEXT, + ColorStyle.DEFAULT, + uiData.cancelText ?: provideStringResource("cancel"), + ) { + showDatePicker = false + } + }, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true, + usePlatformDefaultWidth = true, + ), + ) { + DatePicker( + title = { + Text( + text = uiData.title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(start = Spacing.Spacing24, top = Spacing.Spacing24), + ) + }, + state = datePickerState, + showModeToggle = true, + modifier = Modifier.padding(Spacing.Spacing0), + ) + } + } + } +} + +private fun getInputState( + supportingTextList: List, + dateOutOfRangeItem: SupportingTextData, + incorrectDateFormatItem: SupportingTextData, + currentState: InputShellState, +): InputShellState { + return if (supportingTextList.contains(dateOutOfRangeItem) || supportingTextList.contains(incorrectDateFormatItem)) InputShellState.ERROR else currentState +} + +@Composable +private fun provideSupportingText( + inputType: AgeInputType, + uiValue: TextFieldValue, + supportingText: List?, + dateOutOfRangeItem: SupportingTextData, + incorrectDateFormatItem: SupportingTextData, + selectableDates: SelectableDates, +): List { + val supportingTextList = supportingText?.toMutableList() ?: mutableListOf() + + return (inputType as? DateOfBirth)?.value?.let { + getDateSupportingText( + uiValue = uiValue, + selectableDates = selectableDates, + actionType = DateTimeActionType.DATE, + yearRange = IntRange(MIN_YEAR, MAX_YEAR), + supportingTextList = supportingTextList, + dateOutOfRangeItem = dateOutOfRangeItem, + incorrectDateFormatItem = incorrectDateFormatItem, + ) + } ?: supportingTextList } +@Suppress("DEPRECATION") +@Deprecated("This component is deprecated and will be removed in the next release. Use InputDateTime instead.") @Composable private fun provideSupportingText( uiModel: InputAgeModel, @@ -355,7 +650,44 @@ private fun provideSupportingText( } } ?: uiModel.supportingText +private fun manageOnValueChanged(newText: TextFieldValue, inputType: AgeInputType, onValueChanged: (AgeInputType?) -> Unit) { + val allowedCharacters = RegExValidations.DATE_TIME.regex + if (allowedCharacters.containsMatchIn(newText.text) || newText.text.isBlank()) { + when (inputType) { + is Age -> onValueChanged.invoke((inputType as? Age)?.copy(value = newText)) + is DateOfBirth -> onValueChanged.invoke(DateOfBirth(formatUIDateToStored(newText, DateTimeActionType.DATE))) + None -> onValueChanged.invoke(None) + } + } +} + +private fun transformInputText(inputType: AgeInputType): String { + return when (inputType) { + is Age -> inputType.value.text + is DateOfBirth -> inputType.value.text + None -> "" + } +} + +private fun getTextFieldValue(inputType: AgeInputType): TextFieldValue { + return when (inputType) { + is Age -> TextFieldValue(transformInputText(inputType), inputType.value.selection) + is DateOfBirth -> TextFieldValue(transformInputText(inputType), inputType.value.selection) + None -> TextFieldValue() + } +} + +private fun updateDateOfBirth(inputType: DateOfBirth, newText: TextFieldValue): AgeInputType { + return if (newText.text.length <= DATE_MASK.length) { + inputType.copy(value = newText) + } else { + inputType + } +} + internal const val MIN_DATE = "10111901" +internal const val MIN_YEAR = 1901 +internal val MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) internal const val DATE_FORMAT = "ddMMYYYY" sealed interface AgeInputType { diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt index 35ff1aa58..62aa976e9 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt @@ -393,7 +393,7 @@ fun InputDateTime( } } -fun getInputState(supportingTextList: List, dateOutOfRangeItem: SupportingTextData, incorrectDateFormatItem: SupportingTextData, currentState: InputShellState): InputShellState { +private fun getInputState(supportingTextList: List, dateOutOfRangeItem: SupportingTextData, incorrectDateFormatItem: SupportingTextData, currentState: InputShellState): InputShellState { return if (supportingTextList.contains(dateOutOfRangeItem) || supportingTextList.contains(incorrectDateFormatItem)) InputShellState.ERROR else currentState } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/DateTimeUtils.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/DateTimeUtils.kt index 6bae3eb58..015542824 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/DateTimeUtils.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/DateTimeUtils.kt @@ -271,7 +271,12 @@ fun getSelectableDates(selectableDates: SelectableDates): androidx.compose.mater @Deprecated("This function is deprecated and will be removed in the next release. Use overloaded fun instead.") @Suppress("DEPRECATION") -fun getSupportingTextList(uiModel: InputDateTimeModel, dateOutOfRangeItem: SupportingTextData, incorrectHourFormatItem: SupportingTextData, incorrectDateFormatItem: SupportingTextData): List { +fun getSupportingTextList( + uiModel: InputDateTimeModel, + dateOutOfRangeItem: SupportingTextData, + incorrectHourFormatItem: SupportingTextData, + incorrectDateFormatItem: SupportingTextData, +): List { val supportingTextList = mutableListOf() uiModel.supportingText?.forEach { item -> @@ -291,6 +296,7 @@ fun getSupportingTextList(uiModel: InputDateTimeModel, dateOutOfRangeItem: Suppo uiModel.supportingText } } + DateTimeActionType.DATE_TIME -> { if (uiModel.inputTextFieldValue?.text!!.length == 12) { dateIsInRange = dateIsInRange( @@ -307,6 +313,7 @@ fun getSupportingTextList(uiModel: InputDateTimeModel, dateOutOfRangeItem: Suppo if (!isValidHourFormat) supportingTextList.add(incorrectHourFormatItem) } } + DateTimeActionType.DATE -> { if (uiModel.inputTextFieldValue?.text!!.length == 8) { dateIsInRange = dateIsInRange(parseStringDateToMillis(uiModel.inputTextFieldValue.text), uiModel.selectableDates, uiModel.format) @@ -328,7 +335,6 @@ fun getSupportingTextList( dateOutOfRangeItem: SupportingTextData, incorrectHourFormatItem: SupportingTextData, incorrectDateFormatItem: SupportingTextData, - ): List { val supportingTextList = state.supportingText?.toMutableList() ?: mutableListOf() @@ -337,22 +343,48 @@ fun getSupportingTextList( DateTimeActionType.TIME -> { getTimeSupportingTextList(uiValue, supportingTextList, incorrectHourFormatItem) } + DateTimeActionType.DATE_TIME -> { - getDateTimeSupportingTextList(uiValue, dateOutOfRangeItem, incorrectDateFormatItem, incorrectHourFormatItem, state, data, supportingTextList) + getDateTimeSupportingTextList( + uiValue, + dateOutOfRangeItem, + incorrectDateFormatItem, + incorrectHourFormatItem, + state, + data, + supportingTextList, + ) } + DateTimeActionType.DATE -> { - getDateSupportingText(uiValue, data, supportingTextList, dateOutOfRangeItem, incorrectDateFormatItem) + getDateSupportingText( + uiValue, + data.selectableDates, + data.actionType, + data.yearRange, + supportingTextList, + dateOutOfRangeItem, + incorrectDateFormatItem, + ) } } } return supportingTextList.toList() } -fun getDateSupportingText(uiValue: TextFieldValue, data: InputDateTimeData, supportingTextList: MutableList, dateOutOfRangeItem: SupportingTextData, incorrectDateFormatItem: SupportingTextData): List { +fun getDateSupportingText( + uiValue: TextFieldValue, + selectableDates: SelectableDates, + actionType: DateTimeActionType, + yearRange: IntRange, + supportingTextList: MutableList, + dateOutOfRangeItem: SupportingTextData, + incorrectDateFormatItem: SupportingTextData, +): List { if (uiValue.text.length == 8) { - val dateIsInRange = dateIsInRange(parseStringDateToMillis(uiValue.text), data.selectableDates) + val dateIsInRange = dateIsInRange(parseStringDateToMillis(uiValue.text), selectableDates) val isValidDateFormat = isValidDate(uiValue.text) - val dateIsInYearRange = yearIsInRange(uiValue.text, getDefaultFormat(data.actionType), data.yearRange) + val dateIsInYearRange = yearIsInRange(uiValue.text, getDefaultFormat(actionType), yearRange) if (!dateIsInRange || !dateIsInYearRange) supportingTextList.add(dateOutOfRangeItem) if (!isValidDateFormat) supportingTextList.add(incorrectDateFormatItem) } @@ -385,7 +417,11 @@ fun getDateTimeSupportingTextList( return supportingTextList } -fun getTimeSupportingTextList(inputTextFieldValue: TextFieldValue?, supportingTextList: MutableList, incorrectHourFormatItem: SupportingTextData): List { +fun getTimeSupportingTextList( + inputTextFieldValue: TextFieldValue?, + supportingTextList: MutableList, + incorrectHourFormatItem: SupportingTextData, +): List { if (inputTextFieldValue?.text!!.length == 4 && !isValidHourFormat(inputTextFieldValue.text)) { supportingTextList.add(incorrectHourFormatItem) } @@ -395,7 +431,10 @@ fun getTimeSupportingTextList(inputTextFieldValue: TextFieldValue?, supportingTe @Composable @OptIn(ExperimentalMaterial3Api::class) internal fun getTimePickerState(state: InputDateTimeState, uiData: InputDateTimeData): TimePickerState { - return if (state.inputTextFieldValue?.text?.isNotEmpty() == true && uiData.actionType == DateTimeActionType.TIME && isValidHourFormat(state.inputTextFieldValue?.text ?: "")) { + return if (state.inputTextFieldValue?.text?.isNotEmpty() == true && uiData.actionType == DateTimeActionType.TIME && isValidHourFormat( + state.inputTextFieldValue?.text ?: "", + ) + ) { rememberTimePickerState( initialHour = state.inputTextFieldValue!!.text.substring(0, 2) .toInt(), @@ -404,7 +443,10 @@ internal fun getTimePickerState(state: InputDateTimeState, uiData: InputDateTime ) } else if (state.inputTextFieldValue?.text?.length == 12 && isValidHourFormat(state.inputTextFieldValue!!.text.substring(8, 12))) { rememberTimePickerState( - initialHour = state.inputTextFieldValue?.text?.substring(state.inputTextFieldValue!!.text.length - 4, state.inputTextFieldValue!!.text.length - 2)!! + initialHour = state.inputTextFieldValue?.text?.substring( + state.inputTextFieldValue!!.text.length - 4, + state.inputTextFieldValue!!.text.length - 2, + )!! .toInt(), state.inputTextFieldValue!!.text.substring(state.inputTextFieldValue!!.text.length - 2, state.inputTextFieldValue!!.text.length).toInt(), is24Hour = uiData.is24hourFormat, diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/state/InputAgeState.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/state/InputAgeState.kt new file mode 100644 index 000000000..a4bfc7672 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/state/InputAgeState.kt @@ -0,0 +1,66 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.state + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.text.input.ImeAction +import org.hisp.dhis.mobile.ui.designsystem.component.AgeInputType +import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState +import org.hisp.dhis.mobile.ui.designsystem.component.InputStyle +import org.hisp.dhis.mobile.ui.designsystem.component.LegendData +import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates +import org.hisp.dhis.mobile.ui.designsystem.component.SupportingTextData + +@Stable +interface InputAgeState { + val uiData: InputAgeData + val inputType: AgeInputType + val inputState: InputShellState + val legendData: LegendData? + val supportingText: List? +} + +@Stable +internal class InputAgeStateImpl( + override val uiData: InputAgeData, + override val inputType: AgeInputType, + override val inputState: InputShellState, + override val legendData: LegendData?, + override val supportingText: List?, +) : InputAgeState + +@Composable +fun rememberInputAgeState( + inputAgeData: InputAgeData, + inputType: AgeInputType = AgeInputType.None, + inputState: InputShellState = InputShellState.UNFOCUSED, + legendData: LegendData? = null, + supportingText: List? = null, +): InputAgeState = remember( + inputType, + inputState, + legendData, + supportingText, +) { + InputAgeStateImpl( + inputAgeData, + inputType, + inputState, + legendData, + supportingText, + ) +} + +data class InputAgeData( + val title: String, + val inputStyle: InputStyle = InputStyle.DataInputStyle(), + val isRequired: Boolean = false, + val imeAction: ImeAction = ImeAction.Next, + val dateOfBirthLabel: String? = null, + val orLabel: String? = null, + val ageLabel: String? = null, + val acceptText: String? = null, + val cancelText: String? = null, + val is24hourFormat: Boolean = false, + val selectableDates: SelectableDates? = null, +) diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAgeTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAgeTest.kt index c3dbd2a6f..fcbcb815f 100644 --- a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAgeTest.kt +++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputAgeTest.kt @@ -3,11 +3,15 @@ package org.hisp.dhis.mobile.ui.designsystem.component import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.text.input.TextFieldValue +import org.hisp.dhis.mobile.ui.designsystem.component.state.InputAgeData +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberInputAgeState import org.junit.Rule import org.junit.Test import java.text.SimpleDateFormat @@ -22,12 +26,14 @@ class InputAgeTest { fun modeSelectionShouldBeShownWhenComponentIsInitialised() { rule.setContent { InputAge( - InputAgeModel( - title = "Label", - onValueChanged = { - // no-op - }, + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), ), + onValueChanged = { + // no-op + }, ) } @@ -42,13 +48,15 @@ class InputAgeTest { fun dateOfBirthFieldShouldBeShownCorrectly() { rule.setContent { InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), inputType = AgeInputType.DateOfBirth.EMPTY, - onValueChanged = { - // no-op - }, ), + onValueChanged = { + // no-op + }, ) } @@ -64,13 +72,15 @@ class InputAgeTest { var inputType by mutableStateOf(AgeInputType.None) rule.setContent { InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), inputType = AgeInputType.DateOfBirth.EMPTY, - onValueChanged = { - inputType = it - }, ), + onValueChanged = { + inputType = it ?: AgeInputType.None + }, ) } @@ -84,13 +94,15 @@ class InputAgeTest { fun ageFieldShouldBeShownCorrectly() { rule.setContent { InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), inputType = AgeInputType.Age.EMPTY, - onValueChanged = { - // no-op - }, ), + onValueChanged = { + // no-op + }, ) } @@ -106,13 +118,15 @@ class InputAgeTest { var inputType by mutableStateOf(AgeInputType.None) rule.setContent { InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), inputType = AgeInputType.Age.EMPTY, - onValueChanged = { - inputType = it - }, ), + onValueChanged = { + inputType = it ?: AgeInputType.None + }, ) } @@ -127,13 +141,15 @@ class InputAgeTest { rule.setContent { InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), inputType = inputType, - onValueChanged = { - inputType = it - }, ), + onValueChanged = { + inputType = it ?: AgeInputType.None + }, ) } @@ -156,14 +172,15 @@ class InputAgeTest { rule.setContent { InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), inputType = inputType, - onValueChanged = { - inputType = it - }, ), - + onValueChanged = { + inputType = it ?: AgeInputType.None + }, ) } @@ -178,13 +195,15 @@ class InputAgeTest { rule.setContent { InputAge( - InputAgeModel( - title = "Label", + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), inputType = inputType, - onValueChanged = { - inputType = it - }, ), + onValueChanged = { + inputType = it ?: AgeInputType.None + }, ) } @@ -206,4 +225,62 @@ class InputAgeTest { assert(newInputDaysType.value.text == "28") assert(newInputDaysType.unit == TimeUnitValues.DAYS) } + + @Test + fun shouldFormatDateCorrectly() { + rule.setContent { + InputAge( + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), + inputType = AgeInputType.DateOfBirth(TextFieldValue("1991-11-27")), + ), + onValueChanged = { + // no-op + }, + ) + } + + rule.onNodeWithTag("INPUT_AGE_TEXT_FIELD").assertExists().assertTextEquals("27/11/1991") + } + + @Test + fun shouldShowErrorForOutsideRangeDate() { + rule.setContent { + InputAge( + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), + inputType = AgeInputType.DateOfBirth(TextFieldValue("2025-11-27")), + ), + onValueChanged = { + // no-op + }, + ) + } + + rule.onNodeWithTag("INPUT_AGE_TEXT_FIELD").assertExists().assertTextEquals("27/11/2025") + rule.onNodeWithTag("INPUT_AGE_SUPPORTING_TEXT").assertExists() + } + + @Test + fun shouldWorkWithInvalidDate() { + rule.setContent { + InputAge( + state = rememberInputAgeState( + inputAgeData = InputAgeData( + title = "Label", + ), + inputType = AgeInputType.DateOfBirth(TextFieldValue("1004-9999-9999")), + ), + onValueChanged = { + // no-op + }, + ) + } + + rule.onNodeWithTag("INPUT_AGE_TEXT_FIELD").assertExists().assertTextEquals("99/99/9999") + } } diff --git a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_InputAgeSnapshotTest_launchInputAgeSnapshot.png b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_InputAgeSnapshotTest_launchInputAgeSnapshot.png index e093a22a53c3bfac9c6f780f2b609c8b6ca8ad3e..4b2f2f983aecf073d38d2dbebee5455b88eb0df8 100644 GIT binary patch literal 69894 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 B;tl`+ literal 130 zcmWN?OA^8$3;@tQr{Dq>2sEU<4WA&)sB}#2!qe;9yo=v5=1ceUJY+ZKKF-^t$@0H{ z@>1s0$ic;2PR&uZ^aLyJl^GBm1xV;2T7->}(2;Wx64}Tl6E@$th8%qjl_**c5VjO4 MWhLXw8p-3Te!12ry#N3J From 5b330cbdf37148d7efd20e87d4e8e7f496d42bc7 Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Wed, 18 Sep 2024 16:29:22 +0530 Subject: [PATCH 11/29] Androapp 6474 mobile UI update org tree bottom sheet button configuration (#299) * Update `OrgBottomSheet` Update `OrgBottomSheet` to accept doneButtonIcon and headerAlignment params and making onClear nullable * Add unit test to check clear all button logic * Update Org bottom sheet done button text to nullable --------- Co-authored-by: Siddharth Agarwal --- .../bottomSheets/OrgTreeBottomSheetScreen.kt | 39 ++++++++++++++++ .../designsystem/component/OrgBottomSheet.kt | 44 +++++++++++-------- .../component/OrgBottomSheetTest.kt | 34 ++++++++++++++ 3 files changed, 99 insertions(+), 18 deletions(-) diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/OrgTreeBottomSheetScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/OrgTreeBottomSheetScreen.kt index 627d056a6..a2a9e42d1 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/OrgTreeBottomSheetScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/OrgTreeBottomSheetScreen.kt @@ -1,5 +1,7 @@ package org.hisp.dhis.common.screens.bottomSheets +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.MoveDown import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -7,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.text.style.TextAlign import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -30,6 +33,7 @@ fun OrgTreeBottomSheetScreen() { var showTwoOrgTreeBottomSheet by rememberSaveable { mutableStateOf(false) } var showMediumOrgTreeBottomSheet by rememberSaveable { mutableStateOf(false) } var showLargeOrgTreeBottomSheet by rememberSaveable { mutableStateOf(false) } + var showTransferOrgBottomSheet by rememberSaveable { mutableStateOf(false) } if (showOneOrgTreeBottomSheet) { val orgTreeItemsRepo = remember { OrgTreeItemsFakeRepo() } @@ -115,6 +119,31 @@ fun OrgTreeBottomSheetScreen() { ) } + if (showTransferOrgBottomSheet) { + val orgTreeItemsRepo = remember { OrgTreeItemsFakeRepo() } + val oneOrgTreeItem by orgTreeItemsRepo.state.collectAsState(emptyList()) + + OrgBottomSheet( + title = "Transfer [tracked entity type]", + description = "From [current owner org. unit] to...", + orgTreeItems = oneOrgTreeItem, + doneButtonText = "Transfer", + doneButtonIcon = Icons.Outlined.MoveDown, + headerTextAlignment = TextAlign.Left, + onDismiss = { + showTransferOrgBottomSheet = false + }, + onSearch = orgTreeItemsRepo::search, + onItemClick = orgTreeItemsRepo::toggleItemExpansion, + onItemSelected = { uid, checked -> + orgTreeItemsRepo.toggleItemSelection(uid, checked) + }, + onDone = { + // no-op + }, + ) + } + ColumnScreenContainer(title = BottomSheets.ORG_TREE_BOTTOM_SHEET.label) { ColumnComponentContainer("Org Tree Bottom Sheet with single item") { Button( @@ -155,6 +184,16 @@ fun OrgTreeBottomSheetScreen() { showLargeOrgTreeBottomSheet = !showLargeOrgTreeBottomSheet } } + + ColumnComponentContainer("Transfer Org Tree Bottom Sheet") { + Button( + enabled = true, + ButtonStyle.FILLED, + text = "Show Transfer Org Tree Bottom Sheet", + ) { + showTransferOrgBottomSheet = !showTransferOrgBottomSheet + } + } } } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt index 9cc60e31c..d1631a881 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity @@ -58,7 +59,9 @@ import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor * @param description: optional description. * @param clearAllButtonText: text for clear all button. * @param doneButtonText: text for accept button. + * @param doneButtonIcon: icon for accept button. * @param noResultsFoundText: text for no results found. + * @param headerTextAlignment [Alignment] for header text. * @param icon: optional icon to be shown above the header . * @param onSearch: access to the on search event. * @param onDismiss: access to the on dismiss event. @@ -76,14 +79,16 @@ fun OrgBottomSheet( subtitle: String? = null, description: String? = null, clearAllButtonText: String = provideStringResource("clear_all"), - doneButtonText: String = provideStringResource("done"), + doneButtonText: String? = null, + doneButtonIcon: ImageVector = Icons.Filled.Check, noResultsFoundText: String = provideStringResource("no_results_found"), + headerTextAlignment: TextAlign = TextAlign.Center, icon: @Composable (() -> Unit)? = null, onSearch: ((String) -> Unit)? = null, onDismiss: () -> Unit, onItemClick: (uid: String) -> Unit, onItemSelected: (uid: String, checked: Boolean) -> Unit, - onClearAll: () -> Unit, + onClearAll: (() -> Unit)? = null, onDone: () -> Unit, ) { var searchQuery by remember { mutableStateOf("") } @@ -95,6 +100,7 @@ fun OrgBottomSheet( title = title, subtitle = subtitle, description = description, + headerTextAlignment = headerTextAlignment, icon = icon, searchQuery = searchQuery, onSearchQueryChanged = { query -> @@ -124,33 +130,35 @@ fun OrgBottomSheet( Row( verticalAlignment = Alignment.CenterVertically, ) { - Button( - modifier = Modifier.weight(1f) - .testTag("CLEAR_ALL_BUTTON"), - onClick = onClearAll, - icon = { - Icon( - imageVector = Icons.Filled.ClearAll, - contentDescription = null, - ) - }, - text = clearAllButtonText, - enabled = orgTreeItems.any { it.selected }, - ) + if (onClearAll != null) { + Button( + modifier = Modifier.weight(1f) + .testTag("CLEAR_ALL_BUTTON"), + onClick = onClearAll, + icon = { + Icon( + imageVector = Icons.Filled.ClearAll, + contentDescription = null, + ) + }, + text = clearAllButtonText, + enabled = orgTreeItems.any { it.selected }, + ) - Spacer(Modifier.requiredWidth(Spacing.Spacing16)) + Spacer(Modifier.requiredWidth(Spacing.Spacing16)) + } Button( modifier = Modifier.weight(1f), onClick = onDone, icon = { Icon( - imageVector = Icons.Filled.Check, + imageVector = doneButtonIcon, contentDescription = null, ) }, enabled = orgTreeItems.any { it.selected }, - text = doneButtonText, + text = doneButtonText ?: provideStringResource("done"), style = ButtonStyle.FILLED, ) } diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheetTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheetTest.kt index cd382796d..09d5312b1 100644 --- a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheetTest.kt +++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheetTest.kt @@ -123,4 +123,38 @@ class OrgBottomSheetTest { rule.onNodeWithTag("ORG_TREE_ITEM_CHECKBOX_Item 1").assertExists() rule.onNodeWithTag("ORG_TREE_ITEM_CHECKBOX_Item 2").assertDoesNotExist() } + + @Test + fun shouldHideClearButtonWhenOnClearAllMethodIsNotProvided() { + rule.setContent { + OrgBottomSheet( + orgTreeItems = listOf( + OrgTreeItem( + uid = "1", + label = "Item 1", + canBeSelected = true, + ), + OrgTreeItem( + uid = "2", + label = "Item 2", + canBeSelected = false, + ), + ), + onDismiss = { + // no-op + }, + onItemClick = { + // no-op + }, + onItemSelected = { _, _ -> + // no-op + }, + onDone = { + // no-op + }, + ) + } + + rule.onNodeWithTag("CLEAR_ALL_BUTTON").assertDoesNotExist() + } } From 95183bed88c7a7a8c4dccd583becb00dd8f49608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Tue, 24 Sep 2024 12:05:15 +0200 Subject: [PATCH 12/29] fix: bottom sheet description text alignment (#297) --- .../hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt index b5558dfd6..12c3adc75 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt @@ -117,7 +117,7 @@ fun BottomSheetHeader( text = description, style = MaterialTheme.typography.bodyMedium, color = TextColor.OnSurfaceLight, - textAlign = headerTextAlignment, + textAlign = TextAlign.Start, ) } } From 98b318d2e0f64b464f834dee95ec3208106a9aec Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Wed, 25 Sep 2024 15:22:17 +0530 Subject: [PATCH 13/29] Add animation to show and hide bottom sheet header (#301) Co-authored-by: Siddharth Agarwal --- .../common/screens/bottomSheets/OrgTreeBottomSheetScreen.kt | 2 +- .../dhis/mobile/ui/designsystem/component/BottomSheet.kt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/OrgTreeBottomSheetScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/OrgTreeBottomSheetScreen.kt index a2a9e42d1..348eb0df0 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/OrgTreeBottomSheetScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/OrgTreeBottomSheetScreen.kt @@ -125,7 +125,7 @@ fun OrgTreeBottomSheetScreen() { OrgBottomSheet( title = "Transfer [tracked entity type]", - description = "From [current owner org. unit] to...", + subtitle = "From [current owner org. unit] to...", orgTreeItems = oneOrgTreeItem, doneButtonText = "Transfer", doneButtonIcon = Icons.Outlined.MoveDown, diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt index 12c3adc75..26c08347f 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt @@ -1,5 +1,6 @@ package org.hisp.dhis.mobile.ui.designsystem.component +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.gestures.ScrollableState @@ -222,7 +223,9 @@ fun BottomSheetShell( ) { val hasSearch = searchQuery != null && onSearchQueryChanged != null && onSearch != null - if (showHeader) { + AnimatedVisibility( + visible = showHeader, + ) { BottomSheetHeader( title = title!!, subTitle = subtitle, From 7889fac1a2ae1d2a3243b089303ecfef358dadc9 Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 30 Sep 2024 12:59:32 +0200 Subject: [PATCH 14/29] Location search bar (#300) Signed-off-by: Pablo --- .../org/hisp/dhis/android/MainActivity.kt | 21 + common/build.gradle.kts | 2 +- .../kotlin/org/hisp/dhis/common/App.kt | 26 +- .../org/hisp/dhis/common/screens/Groups.kt | 1 + .../location/LocationSearchBarScreen.kt | 64 +++ .../LocationBarButtonSnapshotTest.kt | 23 + .../LocationBarSearchSnapshotTest.kt | 39 ++ .../component/LocationSearchBar.kt | 412 ++++++++++++++++++ .../ui/designsystem/component/SearchBar.kt | 8 +- .../internal/modifiers/ClickableWithRipple.kt | 25 ++ .../component/model/LocationItemModel.kt | 46 ++ .../resources/values-es/strings.xml | 4 + .../commonMain/resources/values/strings.xml | 4 + .../component/LocationSearchBarTest.kt | 214 +++++++++ ...SnapshotTest_launchSearchBarButtonTest.png | 3 + ...SnapshotTest_launchSearchBarButtonTest.png | 3 + 16 files changed, 889 insertions(+), 6 deletions(-) create mode 100644 common/src/commonMain/kotlin/org/hisp/dhis/common/screens/location/LocationSearchBarScreen.kt create mode 100644 designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarButtonSnapshotTest.kt create mode 100644 designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarSearchSnapshotTest.kt create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBar.kt create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/modifiers/ClickableWithRipple.kt create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/model/LocationItemModel.kt create mode 100644 designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBarTest.kt create mode 100644 designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_LocationBarButtonSnapshotTest_launchSearchBarButtonTest.png create mode 100644 designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_LocationBarSearchSnapshotTest_launchSearchBarButtonTest.png diff --git a/android/src/main/java/org/hisp/dhis/android/MainActivity.kt b/android/src/main/java/org/hisp/dhis/android/MainActivity.kt index b17de2cd7..4e9416a17 100644 --- a/android/src/main/java/org/hisp/dhis/android/MainActivity.kt +++ b/android/src/main/java/org/hisp/dhis/android/MainActivity.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.core.view.WindowCompat import org.hisp.dhis.common.App +import org.hisp.dhis.mobile.ui.designsystem.component.model.LocationItemModel class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -28,6 +29,26 @@ class MainActivity : AppCompatActivity() { .also { it.inPreferredConfig = Bitmap.Config.ARGB_8888 }, ).asImageBitmap() }, + onLocationRequest = { locationQuery, locationSearchCallback -> + + if (locationQuery.isNotBlank()) { + val fakeList = buildList { + repeat(20) { + add( + LocationItemModel.SearchResult( + "Fake Location Title #$it", + "Fake Location Address, Fake Country, Fake City", + 0.0, + 0.0, + ), + ) + } + } + locationSearchCallback(fakeList) + } else { + locationSearchCallback(emptyList()) + } + }, ) } } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 3c7df18a1..1cd8290cc 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -18,7 +18,7 @@ kotlin { implementation(compose.material3) @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) implementation(compose.components.resources) - implementation(project(":designsystem")) + api(project(":designsystem")) } commonTest.dependencies { implementation(kotlin("test")) diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt index 4f37602d7..5a90042c8 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt @@ -19,6 +19,7 @@ import org.hisp.dhis.common.screens.basicTextInputs.BasicTextInputsScreen import org.hisp.dhis.common.screens.bottomSheets.BottomSheetsScreen import org.hisp.dhis.common.screens.buttons.ButtonsScreen import org.hisp.dhis.common.screens.cards.CardsScreen +import org.hisp.dhis.common.screens.location.LocationSearchBarScreen import org.hisp.dhis.common.screens.others.BadgesScreen import org.hisp.dhis.common.screens.others.ChipsScreen import org.hisp.dhis.common.screens.others.IndicatorScreen @@ -38,21 +39,36 @@ import org.hisp.dhis.mobile.ui.designsystem.component.InputDropDown import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState import org.hisp.dhis.mobile.ui.designsystem.component.InputStyle import org.hisp.dhis.mobile.ui.designsystem.component.MetadataAvatarSize +import org.hisp.dhis.mobile.ui.designsystem.component.model.LocationItemModel import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import org.hisp.dhis.mobile.ui.designsystem.theme.Shape import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @Composable -fun App(imageBitmapLoader: (() -> ImageBitmap)? = null) { +fun App( + imageBitmapLoader: (() -> ImageBitmap)? = null, + onLocationRequest: ( + ( + locationQuery: String, + locationSearchCallback: (List) -> Unit, + ) -> Unit + )? = null, +) { DHIS2Theme { - Main(imageBitmapLoader) + Main(imageBitmapLoader, onLocationRequest) } } @Composable fun Main( imageBitmapLoader: (() -> ImageBitmap)?, + onLocationRequest: ( + ( + locationQuery: String, + locationSearchCallback: (List) -> Unit, + ) -> Unit + )?, ) { val currentScreen = remember { mutableStateOf(Groups.NO_GROUP_SELECTED) } var isComponentSelected by remember { mutableStateOf(false) } @@ -81,7 +97,8 @@ fun Main( state = InputShellState.UNFOCUSED, expanded = true, selectedItem = DropdownItem(currentScreen.value.label), - inputStyle = InputStyle.DataInputStyle().apply { backGroundColor = SurfaceColor.SurfaceBright }, + inputStyle = InputStyle.DataInputStyle() + .apply { backGroundColor = SurfaceColor.SurfaceBright }, ) when (currentScreen.value) { @@ -105,6 +122,9 @@ fun Main( Groups.MENU -> MenuItemScreen() Groups.NO_GROUP_SELECTED -> NoComponentSelectedScreen() Groups.TOP_BAR -> TopBarScreen() + Groups.LOCATION_SEARCH_BAR -> LocationSearchBarScreen { locationQuery, locationCallback -> + onLocationRequest?.invoke(locationQuery, locationCallback) + } } } else { NoComponentSelectedScreen( diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt index 703580911..e48911006 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt @@ -21,4 +21,5 @@ enum class Groups(val label: String) { TOP_BAR("Top Bar"), MENU("Menu"), NO_GROUP_SELECTED("No group selected"), + LOCATION_SEARCH_BAR("Location Search Bar"), } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/location/LocationSearchBarScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/location/LocationSearchBarScreen.kt new file mode 100644 index 000000000..ba47fc450 --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/location/LocationSearchBarScreen.kt @@ -0,0 +1,64 @@ +package org.hisp.dhis.common.screens.location + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment.Companion.TopCenter +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import org.hisp.dhis.mobile.ui.designsystem.component.LocationBar +import org.hisp.dhis.mobile.ui.designsystem.component.model.LocationItemModel + +@Composable +fun LocationSearchBarScreen( + onSearchLocation: ( + locationQuery: String, + locationSearchCallback: (List) -> Unit, + + ) -> Unit, +) { + var itemList: List by remember { + mutableStateOf(defaultLocationItems) + } + Box( + modifier = Modifier.fillMaxSize() + .background(Color.White) + .padding(16.dp), + contentAlignment = TopCenter, + ) { + LocationBar( + currentResults = itemList, + onBackClicked = {}, + onClearLocation = {}, + onSearchLocation = { locationQuery -> + onSearchLocation(locationQuery) { + itemList = it.takeIf { locationQuery.isNotBlank() } ?: defaultLocationItems + } + }, + onLocationSelected = { locationItemModel -> + }, + ) + } +} + +private val defaultLocationItems = listOf( + LocationItemModel.StoredResult( + storedTitle = "Location #1", + storedSubtitle = "Location description, location description, location description", + storedLatitude = 0.0, + storedLongitude = 0.0, + ), + LocationItemModel.StoredResult( + storedTitle = "Location #2", + storedSubtitle = "Location description, location description, location description", + storedLatitude = 0.0, + storedLongitude = 0.0, + ), +) diff --git a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarButtonSnapshotTest.kt b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarButtonSnapshotTest.kt new file mode 100644 index 000000000..284e76e84 --- /dev/null +++ b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarButtonSnapshotTest.kt @@ -0,0 +1,23 @@ +package org.hisp.dhis.mobile.ui.designsystem + +import org.hisp.dhis.mobile.ui.designsystem.component.LocationBar +import org.junit.Rule +import org.junit.Test + +class LocationBarButtonSnapshotTest { + @get:Rule + val paparazzi = paparazzi() + + @Test + fun launchSearchBarButtonTest() { + paparazzi.snapshot { + LocationBar( + currentResults = emptyList(), + onBackClicked = {}, + onClearLocation = {}, + onSearchLocation = {}, + onLocationSelected = {}, + ) + } + } +} diff --git a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarSearchSnapshotTest.kt b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarSearchSnapshotTest.kt new file mode 100644 index 000000000..39604f572 --- /dev/null +++ b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarSearchSnapshotTest.kt @@ -0,0 +1,39 @@ +package org.hisp.dhis.mobile.ui.designsystem + +import org.hisp.dhis.mobile.ui.designsystem.component.LocationBar +import org.hisp.dhis.mobile.ui.designsystem.component.SearchBarMode +import org.hisp.dhis.mobile.ui.designsystem.component.model.LocationItemModel +import org.junit.Rule +import org.junit.Test + +class LocationBarSearchSnapshotTest { + @get:Rule + val paparazzi = paparazzi() + + @Test + fun launchSearchBarButtonTest() { + paparazzi.snapshot { + LocationBar( + currentResults = listOf( + LocationItemModel.StoredResult( + "Location Item title", + "Location Item address", + 0.0, + 0.0, + ), + LocationItemModel.SearchResult( + "Location Item title 2", + "Location Item address 2", + 0.0, + 0.0, + ), + ), + mode = SearchBarMode.SEARCH, + onBackClicked = {}, + onClearLocation = {}, + onSearchLocation = {}, + onLocationSelected = {}, + ) + } + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBar.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBar.kt new file mode 100644 index 000000000..08b453a08 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBar.kt @@ -0,0 +1,412 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.filled.TravelExplore +import androidx.compose.material.icons.outlined.Cancel +import androidx.compose.material.icons.outlined.Map +import androidx.compose.material.icons.outlined.Place +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material.icons.outlined.WatchLater +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment.Companion.Center +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Alignment.Companion.Top +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.hisp.dhis.mobile.ui.designsystem.component.internal.modifiers.clickableWithRipple +import org.hisp.dhis.mobile.ui.designsystem.component.model.LocationItemModel +import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2SCustomTextStyles +import org.hisp.dhis.mobile.ui.designsystem.theme.Outline +import org.hisp.dhis.mobile.ui.designsystem.theme.Shape +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing16 +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor + +/** + * DHIS2 Location Bar modes + * BUTTON: the Location Bar is displayed as a button and shows current search query if available. + * SEARCH: The Location Bar is displayed as an input and displays available location items. + */ +enum class SearchBarMode { + BUTTON, + SEARCH, +} + +/** + * DHIS2 Location Bar. + * @param currentResults: the available location items to display before/after search. + * @param mode: the initial mode for the composable. + * @param onBackClicked: callback for when the back button is clicked. + * @param onClearLocation: callback for when the clear location button is clicked. + * @param onSearchLocation: callback for when the search location button is clicked. + * @param onLocationSelected: callback for when a location item is selected. + * @param onModeChanged: optional callback for when the mode is changed. + */ +@Composable +fun LocationBar( + currentResults: List, + mode: SearchBarMode = SearchBarMode.BUTTON, + onBackClicked: () -> Unit, + onClearLocation: () -> Unit, + onSearchLocation: (query: String) -> Unit, + onLocationSelected: (LocationItemModel) -> Unit, + onModeChanged: (currentMode: SearchBarMode) -> Unit = {}, +) { + var currentMode by remember { mutableStateOf(mode) } + var currentSearch: String by remember { mutableStateOf("") } + + LaunchedEffect(currentMode) { + onModeChanged(currentMode) + } + + when (currentMode) { + SearchBarMode.BUTTON -> LocationSearchBarButton( + currentSearch = currentSearch, + onBackClicked = onBackClicked, + onClearLocation = { + currentSearch = "" + onClearLocation() + }, + onClick = { + currentMode = SearchBarMode.SEARCH + }, + ) + + SearchBarMode.SEARCH -> LocationSearchBar( + currentSearch = currentSearch, + currentResults = currentResults, + onSearchChanged = { + currentSearch = it + onSearchLocation(currentSearch) + }, + onBackClicked = { + currentMode = SearchBarMode.BUTTON + }, + onSearch = { searchQuery -> + currentMode = SearchBarMode.BUTTON + }, + onLocationSelected = { + currentSearch = it.title + currentMode = SearchBarMode.BUTTON + onLocationSelected(it) + }, + ) + } +} + +@Composable +private fun LocationSearchBarButton( + currentSearch: String = "", + onBackClicked: () -> Unit, + onClearLocation: () -> Unit, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .testTag("SEARCH_BAR_BUTTON") + .fillMaxWidth() + .wrapContentHeight() + .clip(Shape.Full) + .clickable(onClick = onClick) + .background( + color = SurfaceColor.ContainerLow, + shape = Shape.Full, + ) + .padding(Spacing.Spacing4), + verticalAlignment = CenterVertically, + horizontalArrangement = spacedBy(4.dp), + ) { + IconButton( + style = IconButtonStyle.STANDARD, + onClick = onBackClicked, + icon = { + Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = null) + }, + ) + Text( + modifier = Modifier.weight(1f), + text = currentSearch.takeIf { it.isNotBlank() } ?: "Search location", + style = MaterialTheme.typography.bodyLarge, + color = if (currentSearch.isBlank()) { + TextColor.OnDisabledSurface + } else { + TextColor.OnSurface + }, + ) + if (currentSearch.isEmpty()) { + IconButton( + style = IconButtonStyle.STANDARD, + onClick = {}, + icon = { + Icon(imageVector = Icons.Outlined.Search, contentDescription = null) + }, + ) + } else { + IconButton( + style = IconButtonStyle.STANDARD, + onClick = onClearLocation, + icon = { + Icon(imageVector = Icons.Outlined.Cancel, contentDescription = null) + }, + ) + } + } +} + +@Composable +private fun LocationSearchBar( + currentSearch: String = "", + currentResults: List, + onSearchChanged: (String) -> Unit, + onSearch: (String) -> Unit, + onBackClicked: () -> Unit, + onLocationSelected: (LocationItemModel) -> Unit, +) { + val scrollState = rememberLazyListState() + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + var needsToFocus by remember { mutableStateOf(true) } + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = spacedBy(Spacing.Spacing16), + ) { + SearchBar( + text = currentSearch, + placeHolderText = provideStringResource("search_location"), + onActiveChange = {}, + onQueryChange = onSearchChanged, + onSearch = onSearch, + leadingIcon = { + IconButton( + style = IconButtonStyle.STANDARD, + onClick = onBackClicked, + icon = { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = null, + ) + }, + ) + }, + focusRequester = focusRequester, + ) + + LaunchedEffect(scrollState.isScrollInProgress, needsToFocus) { + if (scrollState.isScrollInProgress) { + focusRequester.freeFocus() + keyboardController?.hide() + } else if (needsToFocus) { + focusRequester.requestFocus() + needsToFocus = false + } + } + + LazyColumn(modifier = Modifier.fillMaxWidth(), state = scrollState) { + when { + currentResults.isNotEmpty() -> + itemsIndexed(items = currentResults) { index, locationItemModel -> + SearchResultLocationItem( + modifier = Modifier.testTag("LOCATION_ITEM_$index"), + locationItemModel, + ) { + onLocationSelected(locationItemModel) + } + } + + else -> + item { + NoResultsMessage(isSearching = currentSearch.isNotBlank()) + } + } + + item { + HorizontalDivider( + thickness = 1.dp, + color = Outline.Medium, + modifier = Modifier.padding(vertical = Spacing16), + ) + } + + item { + Row( + modifier = Modifier.fillMaxWidth().padding(8.dp), + ) { + Button( + modifier = Modifier.fillMaxWidth(), + style = ButtonStyle.TONAL, + icon = { + Icon( + imageVector = Icons.Outlined.Map, + contentDescription = "touch app", + ) + }, + text = provideStringResource("select_in_map"), + onClick = onBackClicked, + ) + } + } + } + } +} + +@Composable +fun LocationItem( + modifier: Modifier = Modifier, + locationItemModel: LocationItemModel, + icon: @Composable () -> Unit, + onClick: () -> Unit, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clip(Shape.Small) + .clickableWithRipple(onClick = onClick) + .padding(Spacing.Spacing8), + horizontalArrangement = spacedBy(Spacing.Spacing16), + verticalAlignment = Top, + ) { + icon() + + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = locationItemModel.title, + style = DHIS2SCustomTextStyles.titleMediumBold, + color = TextColor.OnPrimaryContainer, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = locationItemModel.subtitle, + style = MaterialTheme.typography.bodySmall, + color = TextColor.OnSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun SearchResultLocationItem( + modifier: Modifier = Modifier, + locationItemModel: LocationItemModel, + onClick: () -> Unit, +) { + val icon = when (locationItemModel) { + is LocationItemModel.StoredResult -> Icons.Outlined.WatchLater + is LocationItemModel.SearchResult -> Icons.Outlined.Place + } + val tintedColor = when (locationItemModel) { + is LocationItemModel.StoredResult -> SurfaceColor.Warning + is LocationItemModel.SearchResult -> SurfaceColor.Primary + } + + val bgColor = when (locationItemModel) { + is LocationItemModel.StoredResult -> SurfaceColor.WarningContainer + is LocationItemModel.SearchResult -> SurfaceColor.PrimaryContainer + } + + LocationItem( + modifier = modifier, + locationItemModel = locationItemModel, + icon = { + LocationItemIcon( + icon = icon, + tintedColor = tintedColor, + bgColor = bgColor, + ) + }, + onClick = onClick, + ) +} + +/** + * DHIS2 Location Item icon. + * @param icon: the ImageVector to display as an icon. + * @param tintedColor: the color to tint the icon with. + * @param bgColor: the color for the background. + */ +@Composable +fun LocationItemIcon( + icon: ImageVector, + tintedColor: Color, + bgColor: Color, +) { + Box( + modifier = Modifier.size(Spacing.Spacing40) + .clip(Shape.Full) + .background(color = bgColor, shape = Shape.Full), + contentAlignment = Center, + ) { + Icon( + imageVector = icon, + tint = tintedColor, + contentDescription = "location icon", + ) + } +} + +@Composable +private fun NoResultsMessage(isSearching: Boolean) { + val message = if (!isSearching) { + provideStringResource("no_recent_results") + } else { + provideStringResource("no_results") + } + + Column( + modifier = Modifier + .testTag(if (!isSearching) "NO_RECENT_RESULTS" else "NO_RESULTS") + .fillMaxWidth() + .padding(vertical = 64.dp), + verticalArrangement = spacedBy(16.dp), + horizontalAlignment = CenterHorizontally, + ) { + Icon( + modifier = Modifier.size(48.dp), + imageVector = Icons.Filled.TravelExplore, + tint = SurfaceColor.ContainerHighest, + contentDescription = "Travel explore", + ) + Text( + text = message, + style = MaterialTheme.typography.bodyLarge, + color = TextColor.OnSurfaceVariant, + ) + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SearchBar.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SearchBar.kt index afe75b675..5fc2d99b1 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SearchBar.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SearchBar.kt @@ -57,6 +57,8 @@ import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor * @param onQueryChange: on query change callback. * @param state: input shell state. * @param modifier: optional modifier. + * @param leadingIcon: optional leading icon to display. + * @param focusRequester: optional focus requester. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -68,10 +70,11 @@ fun SearchBar( onQueryChange: (String) -> Unit = {}, state: InputShellState = InputShellState.FOCUSED, modifier: Modifier = Modifier, + leadingIcon: @Composable (() -> Unit)? = null, + focusRequester: FocusRequester = remember { FocusRequester() }, ) { val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() - val focusRequester = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current val containerColor = if (!isPressed) { @@ -114,7 +117,7 @@ fun SearchBar( false } } - .padding(end = Spacing.Spacing4) + .padding(horizontal = Spacing.Spacing4) .semantics { contentDescription = "Search" }, @@ -138,6 +141,7 @@ fun SearchBar( color = TextColor.OnDisabledSurface, ) }, + leadingIcon = leadingIcon, trailingIcon = { if (text != "") { IconButton( diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/modifiers/ClickableWithRipple.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/modifiers/ClickableWithRipple.kt new file mode 100644 index 000000000..44053c380 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/modifiers/ClickableWithRipple.kt @@ -0,0 +1,25 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.internal.modifiers + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor + +@Composable +internal fun Modifier.clickableWithRipple( + role: Role = Role.Button, + color: Color = SurfaceColor.Primary, + onClick: () -> Unit, +): Modifier = this.then( + Modifier.clickable( + role = role, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(color = color), + onClick = onClick, + ), +) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/model/LocationItemModel.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/model/LocationItemModel.kt new file mode 100644 index 000000000..90c171a1d --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/model/LocationItemModel.kt @@ -0,0 +1,46 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.model + +sealed class LocationItemModel( + val title: String, + val subtitle: String, + val latitude: Double, + val longitude: Double, +) { + /** + * UiModel used for Location Items which are stored in cache or local database. + * @param storedTitle: the label to display. + * @param storedSubtitle: the subtitle to display. + * @param storedLatitude: the latitude of the location. + * @param storedLongitude the longitude of the location. + */ + data class StoredResult( + private val storedTitle: String, + private val storedSubtitle: String, + private val storedLatitude: Double, + private val storedLongitude: Double, + ) : LocationItemModel( + title = storedTitle, + subtitle = storedSubtitle, + latitude = storedLatitude, + longitude = storedLongitude, + ) + + /** + * UiModel used for Location Items which are provided by external apis. + * @param searchedTitle: the label to display. + * @param searchedSubtitle: the subtitle to display. + * @param searchedLatitude: the latitude of the location. + * @param searchedLongitude the longitude of the location. + */ + data class SearchResult( + private val searchedTitle: String, + private val searchedSubtitle: String, + private val searchedLatitude: Double, + private val searchedLongitude: Double, + ) : LocationItemModel( + title = searchedTitle, + subtitle = searchedSubtitle, + latitude = searchedLatitude, + longitude = searchedLongitude, + ) +} diff --git a/designsystem/src/commonMain/resources/values-es/strings.xml b/designsystem/src/commonMain/resources/values-es/strings.xml index 60a7045a4..a0b90142e 100644 --- a/designsystem/src/commonMain/resources/values-es/strings.xml +++ b/designsystem/src/commonMain/resources/values-es/strings.xml @@ -42,4 +42,8 @@ Formato de fecha incorrecto Formato de hora incorrecto No se muestran todas las opciones.\n Busca para ver más. + Buscar localización + Seleccionar en el mapa + No hay resultados recientes + No hay resultados diff --git a/designsystem/src/commonMain/resources/values/strings.xml b/designsystem/src/commonMain/resources/values/strings.xml index fcfa8e63c..b012b6a6b 100644 --- a/designsystem/src/commonMain/resources/values/strings.xml +++ b/designsystem/src/commonMain/resources/values/strings.xml @@ -42,4 +42,8 @@ Incorrect date format Incorrect time format Not all options are displayed.\n Search to see more. + Search location + Select in map + No recent results + No results diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBarTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBarTest.kt new file mode 100644 index 000000000..0076c3bd5 --- /dev/null +++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBarTest.kt @@ -0,0 +1,214 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import org.hisp.dhis.mobile.ui.designsystem.component.model.LocationItemModel +import org.junit.Rule +import org.junit.Test + +class LocationSearchBarTest { + + @get:Rule + val rule = createComposeRule() + + @Test + fun shouldDisplaySearchBarWithNoRecentResults() { + rule.setContent { + LocationBar( + currentResults = emptyList(), + onBackClicked = {}, + onClearLocation = {}, + onSearchLocation = {}, + onLocationSelected = {}, + ) + } + + with(rule) { + onNodeWithTag("SEARCH_BAR_BUTTON") + .assertIsDisplayed() + .performClick() + onNodeWithTag("SEARCH_INPUT") + .assertIsDisplayed() + onNodeWithTag("NO_RECENT_RESULTS") + .assertIsDisplayed() + } + } + + @Test + fun shouldDisplaySearchBarWithResults() { + rule.setContent { + LocationBar( + currentResults = listOf( + LocationItemModel.StoredResult( + "title", + "subtitle", + 0.0, + 0.0, + ), + ), + onBackClicked = {}, + onClearLocation = {}, + onSearchLocation = {}, + onLocationSelected = {}, + ) + } + + with(rule) { + onNodeWithTag("SEARCH_BAR_BUTTON") + .assertIsDisplayed() + .performClick() + onNodeWithTag("SEARCH_INPUT") + .assertIsDisplayed() + onNodeWithTag("LOCATION_ITEM_0") + .assertIsDisplayed() + } + } + + @Test + fun shouldDisplayNoResultsMessage() { + rule.setContent { + var items: List by remember { + mutableStateOf( + listOf( + LocationItemModel.StoredResult( + "title", + "subtitle", + 0.0, + 0.0, + ), + ), + ) + } + LocationBar( + currentResults = items, + onBackClicked = {}, + onClearLocation = {}, + onSearchLocation = { querySearch -> + items = listOf() + }, + onLocationSelected = {}, + ) + } + + with(rule) { + onNodeWithTag("SEARCH_BAR_BUTTON") + .assertIsDisplayed() + .performClick() + onNodeWithTag("SEARCH_INPUT") + .assertIsDisplayed() + .performTextInput("Hospital la") + waitForIdle() + onNodeWithTag("NO_RESULTS") + .assertIsDisplayed() + } + } + + @Test + fun shouldDisplaySearchResults() { + rule.setContent { + var items: List by remember { + mutableStateOf( + listOf( + LocationItemModel.StoredResult( + "title", + "subtitle", + 0.0, + 0.0, + ), + ), + ) + } + + LocationBar( + currentResults = items, + onBackClicked = {}, + onClearLocation = {}, + onSearchLocation = { + items = listOf( + LocationItemModel.SearchResult( + "title search result", + "subtitle search result", + 0.0, + 0.0, + ), + ) + }, + onLocationSelected = {}, + ) + } + + with(rule) { + onNodeWithTag("SEARCH_BAR_BUTTON") + .assertIsDisplayed() + .performClick() + onNodeWithTag("SEARCH_INPUT") + .assertIsDisplayed() + .performTextInput("Hospital la") + waitForIdle() + onNodeWithTag("LOCATION_ITEM_0", useUnmergedTree = true) + .assertIsDisplayed() + .assert(hasAnyDescendant(hasText("title search result"))) + } + } + + @Test + fun shouldDisplaySelectedLocationInfo() { + rule.setContent { + var items: List by remember { + mutableStateOf( + listOf( + LocationItemModel.StoredResult( + "title", + "subtitle", + 0.0, + 0.0, + ), + ), + ) + } + + LocationBar( + currentResults = items, + onBackClicked = {}, + onClearLocation = {}, + onSearchLocation = { + items = listOf( + LocationItemModel.SearchResult( + "title search result", + "subtitle search result", + 0.0, + 0.0, + ), + ) + }, + onLocationSelected = {}, + ) + } + + with(rule) { + onNodeWithTag("SEARCH_BAR_BUTTON") + .assertIsDisplayed() + .performClick() + onNodeWithTag("SEARCH_INPUT") + .assertIsDisplayed() + .performTextInput("Hospital la") + waitForIdle() + onNodeWithTag("LOCATION_ITEM_0") + .assertIsDisplayed() + .performClick() + onNodeWithTag("SEARCH_BAR_BUTTON", useUnmergedTree = true) + .assertIsDisplayed() + .assert(hasAnyDescendant(hasText("title search result"))) + } + } +} diff --git a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_LocationBarButtonSnapshotTest_launchSearchBarButtonTest.png b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_LocationBarButtonSnapshotTest_launchSearchBarButtonTest.png new file mode 100644 index 000000000..695f102bd --- /dev/null +++ b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_LocationBarButtonSnapshotTest_launchSearchBarButtonTest.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a4fa9a4ae1c48ec75abfe77699bfcf81c80f57a816f62c6442974c236d3a286 +size 11447 diff --git a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_LocationBarSearchSnapshotTest_launchSearchBarButtonTest.png b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_LocationBarSearchSnapshotTest_launchSearchBarButtonTest.png new file mode 100644 index 000000000..f97258c81 --- /dev/null +++ b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_LocationBarSearchSnapshotTest_launchSearchBarButtonTest.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ff26d23a3074d910416255857ce4a84cc845c321d5da58bcb72ba75a5fe5868 +size 25199 From 2d15707d5538fd747d5c463997d0d5dc198015c1 Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Tue, 1 Oct 2024 18:30:27 +0530 Subject: [PATCH 15/29] Hide keyboard and clear focus on keyboard search btn click (#305) Co-authored-by: Siddharth Agarwal --- .../dhis/mobile/ui/designsystem/component/SearchBar.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SearchBar.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SearchBar.kt index 5fc2d99b1..3998263a1 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SearchBar.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SearchBar.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role @@ -76,6 +77,7 @@ fun SearchBar( val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current val containerColor = if (!isPressed) { SurfaceColor.ContainerLow @@ -124,7 +126,11 @@ fun SearchBar( enabled = true, singleLine = true, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - keyboardActions = KeyboardActions(onSearch = { onSearch(text) }), + keyboardActions = KeyboardActions(onSearch = { + keyboardController?.hide() + focusManager.clearFocus() + onSearch(text) + }), interactionSource = interactionSource, textStyle = MaterialTheme.typography.bodyLarge, decorationBox = @Composable { innerTextField -> @@ -155,7 +161,6 @@ fun SearchBar( }, onClick = { onQueryChange.invoke("") - keyboardController?.hide() }, ) } else { From 6155c826db563eab400482ef4eab2c703404f403 Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Fri, 4 Oct 2024 15:52:42 +0530 Subject: [PATCH 16/29] Add DropDown menu component --- .../kotlin/org/hisp/dhis/common/App.kt | 4 +- .../common/screens/menu/DropDownMenuScreen.kt | 148 ++++++++++++++++ .../{others => menu}/MenuItemScreen.kt | 166 +++++------------- .../dhis/common/screens/menu/MenuScreen.kt | 41 +++++ .../ui/designsystem/MenuItemSnapshotTest.kt | 18 +- .../component/menu/DropDownMenu.kt | 46 +++++ .../component/{menuItem => menu}/MenuItem.kt | 29 +-- .../{menuItem => menu}/MenuItemTestTags.kt | 2 +- .../ui/designsystem/component/MenuItemTest.kt | 31 ++-- 9 files changed, 327 insertions(+), 158 deletions(-) create mode 100644 common/src/commonMain/kotlin/org/hisp/dhis/common/screens/menu/DropDownMenuScreen.kt rename common/src/commonMain/kotlin/org/hisp/dhis/common/screens/{others => menu}/MenuItemScreen.kt (74%) create mode 100644 common/src/commonMain/kotlin/org/hisp/dhis/common/screens/menu/MenuScreen.kt create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/DropDownMenu.kt rename designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/{menuItem => menu}/MenuItem.kt (90%) rename designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/{menuItem => menu}/MenuItemTestTags.kt (89%) diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt index 5a90042c8..e8ad47b1a 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt @@ -20,11 +20,11 @@ import org.hisp.dhis.common.screens.bottomSheets.BottomSheetsScreen import org.hisp.dhis.common.screens.buttons.ButtonsScreen import org.hisp.dhis.common.screens.cards.CardsScreen import org.hisp.dhis.common.screens.location.LocationSearchBarScreen +import org.hisp.dhis.common.screens.menu.MenuScreen import org.hisp.dhis.common.screens.others.BadgesScreen import org.hisp.dhis.common.screens.others.ChipsScreen import org.hisp.dhis.common.screens.others.IndicatorScreen import org.hisp.dhis.common.screens.others.LegendScreen -import org.hisp.dhis.common.screens.others.MenuItemScreen import org.hisp.dhis.common.screens.others.MetadataAvatarScreen import org.hisp.dhis.common.screens.others.NavigationBarScreen import org.hisp.dhis.common.screens.others.ProgressScreen @@ -119,7 +119,7 @@ fun Main( Groups.TAGS -> TagsScreen() Groups.SEARCH_BAR -> SearchBarScreen() Groups.NAVIGATION_BAR -> NavigationBarScreen() - Groups.MENU -> MenuItemScreen() + Groups.MENU -> MenuScreen() Groups.NO_GROUP_SELECTED -> NoComponentSelectedScreen() Groups.TOP_BAR -> TopBarScreen() Groups.LOCATION_SEARCH_BAR -> LocationSearchBarScreen { locationQuery, locationCallback -> diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/menu/DropDownMenuScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/menu/DropDownMenuScreen.kt new file mode 100644 index 000000000..0b54f180e --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/menu/DropDownMenuScreen.kt @@ -0,0 +1,148 @@ +package org.hisp.dhis.common.screens.menu + +import androidx.compose.foundation.layout.Box +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Assignment +import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.outlined.Cancel +import androidx.compose.material.icons.outlined.CheckCircle +import androidx.compose.material.icons.outlined.DeleteForever +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material.icons.outlined.Flag +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material.icons.outlined.Sync +import androidx.compose.material.icons.outlined.Workspaces +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import org.hisp.dhis.common.screens.Groups +import org.hisp.dhis.mobile.ui.designsystem.component.Button +import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnScreenContainer +import org.hisp.dhis.mobile.ui.designsystem.component.menu.DropDownMenu +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemStyle +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuLeadingElement +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor + +enum class EnrollmentMenuItem { + SYNC, + FOLLOW_UP, + GROUP_BY_STAGE, + HELP, + ENROLLMENTS, + SHARE, + DEACTIVATE, + COMPLETE, + DELETE, + REMOVE, +} + +@Composable +fun DropDownMenuScreen() { + ColumnScreenContainer(Groups.MENU.label) { + ColumnComponentContainer( + "Enrollment dashboard menu", + ) { + val enrollmentMenuItems by remember { + mutableStateOf( + listOf( + MenuItemData( + id = EnrollmentMenuItem.SYNC, + label = "Refresh this record", + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Sync), + ), + MenuItemData( + id = EnrollmentMenuItem.FOLLOW_UP, + label = "Mark for follow-up", + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Flag), + ), + MenuItemData( + id = EnrollmentMenuItem.GROUP_BY_STAGE, + label = "Group by stage", + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Workspaces), + ), + MenuItemData( + id = EnrollmentMenuItem.HELP, + label = "Show help", + leadingElement = MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.HelpOutline), + ), + MenuItemData( + id = EnrollmentMenuItem.ENROLLMENTS, + label = "More enrollments", + leadingElement = MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.Assignment), + ), + MenuItemData( + id = EnrollmentMenuItem.SHARE, + label = "Share", + supportingText = "Using QR code", + showDivider = true, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Share), + ), + MenuItemData( + id = EnrollmentMenuItem.COMPLETE, + label = "Complete", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.CheckCircle, + defaultTintColor = SurfaceColor.CustomGreen, + selectedTintColor = SurfaceColor.CustomGreen, + ), + ), + MenuItemData( + id = EnrollmentMenuItem.DEACTIVATE, + label = "Deactivate", + showDivider = true, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Cancel, + defaultTintColor = TextColor.OnDisabledSurface, + selectedTintColor = TextColor.OnDisabledSurface, + ), + ), + MenuItemData( + id = EnrollmentMenuItem.REMOVE, + label = "Remove from [program]", + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.DeleteOutline), + ), + MenuItemData( + id = EnrollmentMenuItem.DELETE, + label = "Delete [TEI Type]", + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.DeleteForever), + ), + ), + ) + } + + var selectedItemIndex by remember { mutableStateOf(null) } + var expanded by remember { mutableStateOf(false) } + + Box { + Button( + enabled = true, + ButtonStyle.FILLED, + text = "Show Dropdown menu", + ) { + expanded = !expanded + } + + DropDownMenu( + items = enrollmentMenuItems, + expanded = expanded, + selectedItemIndex = selectedItemIndex, + onDismissRequest = { + expanded = false + }, + onItemClick = { itemId -> + expanded = !expanded + selectedItemIndex = enrollmentMenuItems.indexOfFirst { it.id == itemId } + }, + ) + } + } + } +} diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/MenuItemScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/menu/MenuItemScreen.kt similarity index 74% rename from common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/MenuItemScreen.kt rename to common/src/commonMain/kotlin/org/hisp/dhis/common/screens/menu/MenuItemScreen.kt index bc27b2cfb..5db4f61c1 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/MenuItemScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/menu/MenuItemScreen.kt @@ -1,145 +1,29 @@ -package org.hisp.dhis.common.screens.others +package org.hisp.dhis.common.screens.menu -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowRight -import androidx.compose.material.icons.automirrored.outlined.Assignment -import androidx.compose.material.icons.automirrored.outlined.HelpOutline -import androidx.compose.material.icons.outlined.Cancel import androidx.compose.material.icons.outlined.Check -import androidx.compose.material.icons.outlined.CheckCircle -import androidx.compose.material.icons.outlined.DeleteForever -import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.Done -import androidx.compose.material.icons.outlined.Flag -import androidx.compose.material.icons.outlined.Share -import androidx.compose.material.icons.outlined.Sync -import androidx.compose.material.icons.outlined.Workspaces import androidx.compose.runtime.Composable -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.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import org.hisp.dhis.common.screens.Groups import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer import org.hisp.dhis.mobile.ui.designsystem.component.ColumnScreenContainer -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItem -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemData -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemState -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemStyle -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuLeadingElement -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuTrailingElement -import org.hisp.dhis.mobile.ui.designsystem.theme.Radius -import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing -import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor -import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor -import org.hisp.dhis.mobile.ui.designsystem.theme.dropShadow +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItem +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemState +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemStyle +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuLeadingElement +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuTrailingElement @Composable fun MenuItemScreen() { ColumnScreenContainer(Groups.MENU.label) { - ColumnComponentContainer( - "Enrollment dashboard menu", - ) { - var menuItems by remember { - mutableStateOf( - listOf( - MenuItemData( - label = "Refresh this record", - leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Sync), - ), - MenuItemData( - label = "Mark for follow-up", - leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Flag), - ), - MenuItemData( - label = "Group by stage", - leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Workspaces), - ), - MenuItemData( - label = "Show help", - leadingElement = MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.HelpOutline), - ), - MenuItemData( - label = "More enrollments", - leadingElement = MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.Assignment), - ), - MenuItemData( - label = "Share", - supportingText = "Using QR code", - showDivider = true, - leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Share), - ), - MenuItemData( - label = "Complete", - leadingElement = MenuLeadingElement.Icon( - icon = Icons.Outlined.CheckCircle, - defaultTintColor = SurfaceColor.CustomGreen, - selectedTintColor = SurfaceColor.CustomGreen, - ), - ), - MenuItemData( - label = "Deactivate", - showDivider = true, - leadingElement = MenuLeadingElement.Icon( - icon = Icons.Outlined.Cancel, - defaultTintColor = TextColor.OnDisabledSurface, - selectedTintColor = TextColor.OnDisabledSurface, - ), - ), - MenuItemData( - label = "Remove from [program]", - style = MenuItemStyle.ALERT, - leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.DeleteOutline), - ), - MenuItemData( - label = "Delete [TEI Type]", - style = MenuItemStyle.ALERT, - leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.DeleteForever), - ), - ), - ) - } - - Column( - modifier = Modifier - .dropShadow( - shape = RoundedCornerShape(Radius.XS), - blur = Spacing.Spacing2, - spread = Spacing.Spacing0, - color = Color(0x4D007DEB), - offsetY = Spacing.Spacing1, - ) - .width(270.dp) - .background(SurfaceColor.ContainerLow) - .padding(vertical = Spacing.Spacing8), - ) { - menuItems.forEachIndexed { index, menuItemData -> - MenuItem( - menuItemData = menuItemData, - ) { - menuItems = menuItems.mapIndexed { i, item -> - if (i == index) { - item.copy(state = MenuItemState.SELECTED) - } else { - item.copy(state = MenuItemState.ENABLED) - } - } - } - } - } - } - ColumnComponentContainer("Menu list item") { Column( modifier = Modifier.fillMaxWidth(), @@ -151,6 +35,7 @@ fun MenuItemScreen() { MenuItem( modifier = Modifier.weight(1f), menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", supportingText = "Support Text", showDivider = true, @@ -166,6 +51,7 @@ fun MenuItemScreen() { MenuItem( modifier = Modifier.weight(1f), menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", supportingText = "Support Text", showDivider = true, @@ -186,6 +72,7 @@ fun MenuItemScreen() { MenuItem( modifier = Modifier.weight(1f), menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", supportingText = "Support Text", showDivider = true, @@ -202,6 +89,7 @@ fun MenuItemScreen() { MenuItem( modifier = Modifier.weight(1f), menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", supportingText = "Support Text", showDivider = true, @@ -223,6 +111,7 @@ fun MenuItemScreen() { MenuItem( modifier = Modifier.weight(1f), menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", supportingText = "Support Text", showDivider = true, @@ -239,6 +128,7 @@ fun MenuItemScreen() { MenuItem( modifier = Modifier.weight(1f), menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", supportingText = "Support Text", showDivider = true, @@ -259,12 +149,14 @@ fun MenuItemScreen() { ColumnComponentContainer("Menu item with leading element variations") { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "No Leading Element", ), ) {} MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Indent Leading Element", leadingElement = MenuLeadingElement.Indent, ), @@ -272,6 +164,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Icon Leading Element", leadingElement = MenuLeadingElement.Icon( icon = Icons.Outlined.Check, @@ -281,6 +174,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Selected Indent Leading Element", leadingElement = MenuLeadingElement.Indent, state = MenuItemState.SELECTED, @@ -289,6 +183,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Selected Icon Leading Element", leadingElement = MenuLeadingElement.Icon( icon = Icons.Outlined.Check, @@ -299,6 +194,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Disabled Indent Leading Element", leadingElement = MenuLeadingElement.Indent, state = MenuItemState.DISABLED, @@ -307,6 +203,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Disabled Icon Leading Element", leadingElement = MenuLeadingElement.Icon( icon = Icons.Outlined.Check, @@ -319,6 +216,7 @@ fun MenuItemScreen() { ColumnComponentContainer("Menu item with divider") { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", showDivider = true, ), @@ -328,6 +226,7 @@ fun MenuItemScreen() { ColumnComponentContainer("Menu item with trailing element variations") { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "No Trailing Element", leadingElement = MenuLeadingElement.Icon( icon = Icons.Outlined.Check, @@ -337,6 +236,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Icon Trailing Element", leadingElement = MenuLeadingElement.Icon( icon = Icons.Outlined.Check, @@ -349,6 +249,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Text Trailing Element", leadingElement = MenuLeadingElement.Icon( icon = Icons.Outlined.Check, @@ -361,6 +262,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Selected Icon Trailing Element", state = MenuItemState.SELECTED, leadingElement = MenuLeadingElement.Icon( @@ -374,6 +276,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Selected Text Trailing Element", state = MenuItemState.SELECTED, leadingElement = MenuLeadingElement.Icon( @@ -387,6 +290,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Disabled Icon Trailing Element", state = MenuItemState.DISABLED, leadingElement = MenuLeadingElement.Icon( @@ -400,6 +304,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Disabled Text Trailing Element", state = MenuItemState.DISABLED, leadingElement = MenuLeadingElement.Icon( @@ -415,6 +320,7 @@ fun MenuItemScreen() { ColumnComponentContainer("Alert Menu item with leading element variations") { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "No Leading Element", style = MenuItemStyle.ALERT, ), @@ -422,6 +328,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Indent Leading Element", leadingElement = MenuLeadingElement.Indent, style = MenuItemStyle.ALERT, @@ -430,6 +337,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Icon Leading Element", leadingElement = MenuLeadingElement.Icon( icon = Icons.Outlined.Check, @@ -440,6 +348,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Selected Indent Leading Element", leadingElement = MenuLeadingElement.Indent, style = MenuItemStyle.ALERT, @@ -449,6 +358,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Selected Icon Leading Element", leadingElement = MenuLeadingElement.Icon( icon = Icons.Outlined.Check, @@ -460,6 +370,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Disabled Indent Leading Element", leadingElement = MenuLeadingElement.Indent, style = MenuItemStyle.ALERT, @@ -469,6 +380,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Diasbled Icon Leading Element", leadingElement = MenuLeadingElement.Icon( icon = Icons.Outlined.Check, @@ -482,6 +394,7 @@ fun MenuItemScreen() { ColumnComponentContainer("Alert Menu item with divider") { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", showDivider = true, style = MenuItemStyle.ALERT, @@ -492,6 +405,7 @@ fun MenuItemScreen() { ColumnComponentContainer("Alert Menu item with trailing element variations") { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "No Trailing Element", style = MenuItemStyle.ALERT, leadingElement = MenuLeadingElement.Icon( @@ -502,6 +416,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Icon Trailing Element", style = MenuItemStyle.ALERT, leadingElement = MenuLeadingElement.Icon( @@ -515,6 +430,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Text Trailing Element", style = MenuItemStyle.ALERT, leadingElement = MenuLeadingElement.Icon( @@ -528,6 +444,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Selected Icon Trailing Element", style = MenuItemStyle.ALERT, state = MenuItemState.SELECTED, @@ -542,6 +459,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Selected Text Trailing Element", style = MenuItemStyle.ALERT, state = MenuItemState.SELECTED, @@ -556,6 +474,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Disabled Icon Trailing Element", state = MenuItemState.DISABLED, style = MenuItemStyle.ALERT, @@ -570,6 +489,7 @@ fun MenuItemScreen() { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Disabled Text Trailing Element", state = MenuItemState.DISABLED, style = MenuItemStyle.ALERT, diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/menu/MenuScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/menu/MenuScreen.kt new file mode 100644 index 000000000..2d8242674 --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/menu/MenuScreen.kt @@ -0,0 +1,41 @@ +package org.hisp.dhis.common.screens.menu + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import org.hisp.dhis.common.screens.NoComponentSelectedScreen +import org.hisp.dhis.common.screens.components.GroupComponentDropDown +import org.hisp.dhis.mobile.ui.designsystem.component.DropdownItem + +@Composable +fun MenuScreen() { + val currentScreen = remember { mutableStateOf(MENU.NO_COMPONENT_SELECTED) } + + val screenDropdownItemList = mutableListOf() + MENU.entries.forEach { + if (it != MENU.NO_COMPONENT_SELECTED) { + screenDropdownItemList.add(DropdownItem(it.label)) + } + } + GroupComponentDropDown( + dropdownItems = screenDropdownItemList.toList(), + onItemSelected = { currentScreen.value = getCurrentScreen(it.label) }, + onResetButtonClicked = { currentScreen.value = MENU.NO_COMPONENT_SELECTED }, + selectedItem = DropdownItem(currentScreen.value.label), + ) + when (currentScreen.value) { + MENU.DROPDOWN_MENU -> DropDownMenuScreen() + MENU.MENU_ITEM -> MenuItemScreen() + MENU.NO_COMPONENT_SELECTED -> NoComponentSelectedScreen() + } +} + +enum class MENU(val label: String) { + DROPDOWN_MENU("Drop down menu"), + MENU_ITEM("Menu item"), + NO_COMPONENT_SELECTED("No component selected"), +} + +fun getCurrentScreen(label: String): MENU { + return MENU.entries.firstOrNull { it.label == label } ?: MENU.DROPDOWN_MENU +} diff --git a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/MenuItemSnapshotTest.kt b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/MenuItemSnapshotTest.kt index badb7d95c..a2e47ea10 100644 --- a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/MenuItemSnapshotTest.kt +++ b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/MenuItemSnapshotTest.kt @@ -11,12 +11,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer import org.hisp.dhis.mobile.ui.designsystem.component.ColumnScreenContainer -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItem -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemData -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemState -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemStyle -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuLeadingElement -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuTrailingElement +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItem +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemState +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemStyle +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuLeadingElement +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuTrailingElement import org.junit.Rule import org.junit.Test @@ -39,6 +39,7 @@ class MenuItemSnapshotTest { MenuItem( modifier = Modifier.weight(1f), menuItemData = MenuItemData( + id = "menu_item", label = "Label", supportingText = "Support Text", showDivider = true, @@ -54,6 +55,7 @@ class MenuItemSnapshotTest { MenuItem( modifier = Modifier.weight(1f), menuItemData = MenuItemData( + id = "menu_item", label = "Label", supportingText = "Support Text", showDivider = true, @@ -74,6 +76,7 @@ class MenuItemSnapshotTest { MenuItem( modifier = Modifier.weight(1f), menuItemData = MenuItemData( + id = "menu_item", label = "Label", supportingText = "Support Text", showDivider = true, @@ -90,6 +93,7 @@ class MenuItemSnapshotTest { MenuItem( modifier = Modifier.weight(1f), menuItemData = MenuItemData( + id = "menu_item", label = "Label", supportingText = "Support Text", showDivider = true, @@ -111,6 +115,7 @@ class MenuItemSnapshotTest { MenuItem( modifier = Modifier.weight(1f), menuItemData = MenuItemData( + id = "menu_item", label = "Label", supportingText = "Support Text", showDivider = true, @@ -127,6 +132,7 @@ class MenuItemSnapshotTest { MenuItem( modifier = Modifier.weight(1f), menuItemData = MenuItemData( + id = "menu_item", label = "Label", supportingText = "Support Text", showDivider = true, diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/DropDownMenu.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/DropDownMenu.kt new file mode 100644 index 000000000..c812aaa9b --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/DropDownMenu.kt @@ -0,0 +1,46 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.menu + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.hisp.dhis.mobile.ui.designsystem.theme.Radius +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor + +@Composable +fun DropDownMenu( + modifier: Modifier = Modifier, + items: List>, + expanded: Boolean = false, + selectedItemIndex: Int? = null, + onDismissRequest: () -> Unit, + onItemClick: (T) -> Unit, +) { + MaterialTheme(shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(Radius.S))) { + DropdownMenu( + modifier = modifier + .background(SurfaceColor.ContainerLow) + .widthIn(min = 270.dp), + expanded = expanded, + onDismissRequest = onDismissRequest, + ) { + items.forEachIndexed { index, item -> + MenuItem( + menuItemData = item.copy( + state = if (selectedItemIndex == index) { + MenuItemState.SELECTED + } else { + item.state + }, + ), + ) { + onItemClick(item.id) + } + } + } + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItem.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/MenuItem.kt similarity index 90% rename from designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItem.kt rename to designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/MenuItem.kt index a07976bd9..84bd3fc3e 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItem.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/MenuItem.kt @@ -1,4 +1,4 @@ -package org.hisp.dhis.mobile.ui.designsystem.component.menuItem +package org.hisp.dhis.mobile.ui.designsystem.component.menu import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -20,14 +20,14 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_CONTAINER -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_DIVIDER -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_LEADING_ICON -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_LEADING_INDENT -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_SUPPORTING_TEXT -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TEXT -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TRAILING_ICON -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TRAILING_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_CONTAINER +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_DIVIDER +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_LEADING_ICON +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_LEADING_INDENT +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_SUPPORTING_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_TRAILING_ICON +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_TRAILING_TEXT import org.hisp.dhis.mobile.ui.designsystem.theme.Border import org.hisp.dhis.mobile.ui.designsystem.theme.Outline import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing @@ -42,10 +42,10 @@ import org.hisp.dhis.mobile.ui.designsystem.theme.hoverPointerIcon * @param onItemClick: callback to when menu item is clicked. */ @Composable -fun MenuItem( +fun MenuItem( modifier: Modifier = Modifier, - menuItemData: MenuItemData, - onItemClick: () -> Unit, + menuItemData: MenuItemData, + onItemClick: (T) -> Unit, ) { val itemContainerBackground = when (menuItemData.state) { MenuItemState.SELECTED -> { @@ -69,7 +69,7 @@ fun MenuItem( .clickable( enabled = menuItemData.state != MenuItemState.DISABLED, onClick = { - onItemClick.invoke() + onItemClick(menuItemData.id) }, ) .hoverPointerIcon(menuItemData.state != MenuItemState.DISABLED) @@ -213,7 +213,8 @@ private fun MenuItemTrailingElement( * @param supportingText: controls the supporting text to be shown. * @param showDivider: controls whether a divider should be shown. */ -data class MenuItemData( +data class MenuItemData( + val id: T, val label: String, val state: MenuItemState = MenuItemState.ENABLED, val style: MenuItemStyle = MenuItemStyle.DEFAULT, diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItemTestTags.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/MenuItemTestTags.kt similarity index 89% rename from designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItemTestTags.kt rename to designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/MenuItemTestTags.kt index 43b04b84e..aca398680 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItemTestTags.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/MenuItemTestTags.kt @@ -1,4 +1,4 @@ -package org.hisp.dhis.mobile.ui.designsystem.component.menuItem +package org.hisp.dhis.mobile.ui.designsystem.component.menu object MenuItemTestTags { const val MENU_ITEM_CONTAINER = "MENU_ITEM_CONTAINER" diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/MenuItemTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/MenuItemTest.kt index fdc17cd88..5596f3194 100644 --- a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/MenuItemTest.kt +++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/MenuItemTest.kt @@ -4,18 +4,18 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Done import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItem -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemData -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_CONTAINER -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_DIVIDER -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_LEADING_ICON -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_LEADING_INDENT -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_SUPPORTING_TEXT -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TEXT -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TRAILING_ICON -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TRAILING_TEXT -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuLeadingElement -import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuTrailingElement +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItem +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_CONTAINER +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_DIVIDER +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_LEADING_ICON +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_LEADING_INDENT +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_SUPPORTING_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_TRAILING_ICON +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemTestTags.MENU_ITEM_TRAILING_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuLeadingElement +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuTrailingElement import org.junit.Rule import org.junit.Test @@ -28,6 +28,7 @@ class MenuItemTest { rule.setContent { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", ), ) {} @@ -41,6 +42,7 @@ class MenuItemTest { rule.setContent { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", supportingText = "Supporting Text", ), @@ -55,6 +57,7 @@ class MenuItemTest { rule.setContent { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", showDivider = true, ), @@ -69,6 +72,7 @@ class MenuItemTest { rule.setContent { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", leadingElement = MenuLeadingElement.Indent, ), @@ -83,6 +87,7 @@ class MenuItemTest { rule.setContent { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", leadingElement = MenuLeadingElement.Icon( icon = Icons.Outlined.Done, @@ -99,6 +104,7 @@ class MenuItemTest { rule.setContent { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", trailingElement = MenuTrailingElement.Icon( icon = Icons.Outlined.Done, @@ -115,6 +121,7 @@ class MenuItemTest { rule.setContent { MenuItem( menuItemData = MenuItemData( + id = "menu_item", label = "Menu Item", trailingElement = MenuTrailingElement.Text( text = "Trailing Text", From 18454333c56ba68310c0098141166c8029c13689 Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Fri, 4 Oct 2024 16:55:09 +0530 Subject: [PATCH 17/29] Fix shape reference --- .../mobile/ui/designsystem/component/menu/DropDownMenu.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/DropDownMenu.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/DropDownMenu.kt index c812aaa9b..062a6629e 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/DropDownMenu.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menu/DropDownMenu.kt @@ -2,13 +2,13 @@ package org.hisp.dhis.mobile.ui.designsystem.component.menu import androidx.compose.foundation.background import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.DropdownMenu import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import org.hisp.dhis.mobile.ui.designsystem.theme.Radius +import org.hisp.dhis.mobile.ui.designsystem.theme.DHISShapes +import org.hisp.dhis.mobile.ui.designsystem.theme.Shape import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @Composable @@ -20,7 +20,7 @@ fun DropDownMenu( onDismissRequest: () -> Unit, onItemClick: (T) -> Unit, ) { - MaterialTheme(shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(Radius.S))) { + MaterialTheme(shapes = DHISShapes.copy(extraSmall = Shape.Small)) { DropdownMenu( modifier = modifier .background(SurfaceColor.ContainerLow) From 17029902ea8f68ae49575fc1f47c122f5f1f4f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Mon, 7 Oct 2024 10:00:36 +0200 Subject: [PATCH 18/29] feat: [ANDROAPP-6399] selectable list card (#307) --- .../common/screens/cards/ListCardScreen.kt | 48 +++++++++++++ .../ListCardSelectableSnapshotTest.kt | 70 +++++++++++++++++++ .../ui/designsystem/component/BaseCard.kt | 30 +++++--- .../ui/designsystem/component/ListCard.kt | 14 +++- .../ui/designsystem/component/Selection.kt | 70 +++++++++++++++++++ .../component/state/ListCardState.kt | 6 ++ ...dSelectableSnapshotTest_launchListCard.png | 3 + 7 files changed, 232 insertions(+), 9 deletions(-) create mode 100644 designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/ListCardSelectableSnapshotTest.kt create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Selection.kt create mode 100644 designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_ListCardSelectableSnapshotTest_launchListCard.png diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ListCardScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ListCardScreen.kt index ffed8bae1..2ef082c0b 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ListCardScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ListCardScreen.kt @@ -40,6 +40,7 @@ import org.hisp.dhis.mobile.ui.designsystem.component.ListCard import org.hisp.dhis.mobile.ui.designsystem.component.ListCardDescriptionModel import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel import org.hisp.dhis.mobile.ui.designsystem.component.MetadataAvatarSize +import org.hisp.dhis.mobile.ui.designsystem.component.SelectionState import org.hisp.dhis.mobile.ui.designsystem.component.internal.ImageCardData import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberAdditionalInfoColumnState import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberListCardState @@ -650,6 +651,53 @@ fun ListCardScreen(horizontal: Boolean) { onCardClick = {}, ) } + + ColumnComponentContainer("Selectable list cards") { + var selectionState by remember { + mutableStateOf(SelectionState.NONE) + } + + ListCard( + listCardState = rememberListCardState( + title = ListCardTitleModel(text = "Palak Khanna, F, 61"), + lastUpdated = "5 hours", + additionalInfoColumnState = rememberAdditionalInfoColumnState( + additionalInfoList = basicAdditionalItemList.toMutableList(), + syncProgressItem = syncProgressItem(), + ), + selectionState = selectionState, + ), + listAvatar = { + Avatar( + style = AvatarStyleData.Text("P"), + ) + }, + onCardClick = {}, + onCardSelected = { selectionState = it }, + ) + var selectionState2 by remember { + mutableStateOf(SelectionState.NONE) + } + + ListCard( + listCardState = rememberListCardState( + title = ListCardTitleModel(text = "Palak Khanna, F, 61"), + lastUpdated = "5 hours", + additionalInfoColumnState = rememberAdditionalInfoColumnState( + additionalInfoList = basicAdditionalItemListWithLongKeyText.toMutableList(), + syncProgressItem = syncProgressItem(), + ), + selectionState = selectionState2, + ), + listAvatar = { + Avatar( + style = AvatarStyleData.Text("P"), + ) + }, + onCardClick = {}, + onCardSelected = { selectionState2 = it }, + ) + } } } } diff --git a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/ListCardSelectableSnapshotTest.kt b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/ListCardSelectableSnapshotTest.kt new file mode 100644 index 000000000..ba0a5647a --- /dev/null +++ b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/ListCardSelectableSnapshotTest.kt @@ -0,0 +1,70 @@ +package org.hisp.dhis.mobile.ui.designsystem + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Sync +import androidx.compose.material3.Icon +import androidx.compose.ui.Modifier +import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem +import org.hisp.dhis.mobile.ui.designsystem.component.Avatar +import org.hisp.dhis.mobile.ui.designsystem.component.AvatarStyleData +import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel +import org.hisp.dhis.mobile.ui.designsystem.component.SelectionState +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberAdditionalInfoColumnState +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberListCardState +import org.hisp.dhis.mobile.ui.designsystem.resource.provideDHIS2Icon +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.junit.Rule +import org.junit.Test + +class ListCardSelectableSnapshotTest { + + @get:Rule + val paparazzi = paparazzi() + + @Test + fun launchListCard() { + paparazzi.snapshot { + Column( + verticalArrangement = Arrangement.spacedBy(Spacing.Spacing4), + modifier = Modifier.padding(horizontal = Spacing.Spacing8), + ) { + SelectionState.entries.forEach { selectionState -> + ListCard( + listCardState = rememberListCardState( + title = ListCardTitleModel(text = "Kunal Choudary, M, 55"), + lastUpdated = "24 min", + additionalInfoColumnState = rememberAdditionalInfoColumnState( + additionalInfoList = emptyList(), + syncProgressItem = AdditionalInfoItem( + icon = { + Icon( + imageVector = Icons.Outlined.Sync, + contentDescription = "Icon Button", + tint = SurfaceColor.Primary, + ) + }, + value = "Syncing...", + color = SurfaceColor.Primary, + isConstantItem = false, + ), + ), + selectionState = selectionState, + loading = true, + ), + listAvatar = { + Avatar( + style = AvatarStyleData.Image(provideDHIS2Icon("dhis2_microscope_outline")), + ) + }, + onCardClick = {}, + ) + } + } + } + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BaseCard.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BaseCard.kt index 2ad2d587a..c64b93ed1 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BaseCard.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BaseCard.kt @@ -5,8 +5,10 @@ import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.animateContentSize import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Box @@ -53,6 +55,7 @@ import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor import org.hisp.dhis.mobile.ui.designsystem.theme.dropShadow import org.hisp.dhis.mobile.ui.designsystem.theme.hoverPointerIcon +@OptIn(ExperimentalFoundationApi::class) @Composable fun BaseCard( modifier: Modifier = Modifier, @@ -62,25 +65,36 @@ fun BaseCard( expandable: Boolean, itemVerticalPadding: Dp?, onSizeChanged: ((IntSize) -> Unit)?, + selectionMode: SelectionState, + onCardSelected: () -> Unit, content: @Composable () -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } Row( modifier = modifier - .conditional(showShadow, { - dropShadow( - RoundedCornerShape(Radius.S), - ) - }) - .background(color = TextColor.OnPrimary, shape = RoundedCornerShape(Radius.S)) + .conditional( + selectionMode != SelectionState.SELECTED && showShadow, + { dropShadow(RoundedCornerShape(Radius.S)) }, + ) + .background( + color = when (selectionMode) { + SelectionState.SELECTED -> SurfaceColor.ContainerLow + else -> SurfaceColor.SurfaceBright + }, + shape = RoundedCornerShape(Radius.S), + ) .clip(shape = RoundedCornerShape(Radius.S)) - .clickable( + .combinedClickable( role = Role.Button, interactionSource = interactionSource, indication = rememberRipple( color = SurfaceColor.Primary, ), - onClick = onCardClick, + onClick = when { + selectionMode != SelectionState.NONE -> onCardSelected + else -> onCardClick + }, + onLongClick = onCardSelected, ) .hoverPointerIcon(true) .padding(paddingValues) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/ListCard.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/ListCard.kt index fa4e3e839..60691af31 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/ListCard.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/ListCard.kt @@ -94,6 +94,7 @@ fun ListCard( actionButton: @Composable (() -> Unit)? = null, onCardClick: () -> Unit, onSizeChanged: ((IntSize) -> Unit)? = null, + onCardSelected: ((SelectionState) -> Unit)? = null, ) { BaseCard( modifier = modifier, @@ -102,6 +103,10 @@ fun ListCard( expandable = listCardState.expandable, itemVerticalPadding = listCardState.itemVerticalPadding, onSizeChanged = onSizeChanged, + selectionMode = listCardState.selectionState, + onCardSelected = { + onCardSelected?.invoke(listCardState.selectionState.changeState()) + }, paddingValues = getPaddingValues( expandable = listCardState.expandable, hasShadow = listCardState.shadow, @@ -109,7 +114,12 @@ fun ListCard( ), ) { Row(horizontalArrangement = spacedBy(Spacing.Spacing16)) { - listAvatar?.invoke() + when (listCardState.selectionState) { + SelectionState.SELECTABLE -> UnselectedItemIcon() + SelectionState.SELECTED -> SelectedItemIcon() + SelectionState.NONE -> listAvatar?.invoke() + } + Column(Modifier.fillMaxWidth().weight(1f)) { Row(horizontalArrangement = Arrangement.SpaceBetween) { ListCardTitle( @@ -247,6 +257,8 @@ fun VerticalInfoListCard( ), expandable = listCardState.expandable, itemVerticalPadding = listCardState.itemVerticalPadding, + selectionMode = SelectionState.NONE, + onCardSelected = {}, onSizeChanged = onSizeChanged, ) { Column( diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Selection.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Selection.kt new file mode 100644 index 000000000..1082bb904 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Selection.kt @@ -0,0 +1,70 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckBoxOutlineBlank +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import org.hisp.dhis.mobile.ui.designsystem.theme.Radius +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor + +enum class SelectionState { + SELECTABLE, + SELECTED, + NONE, + ; + + fun changeState(): SelectionState { + return when (this) { + SELECTABLE -> SELECTED + SELECTED -> SELECTABLE + NONE -> SELECTED + } + } +} + +@Composable +fun UnselectedItemIcon(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(Spacing.Spacing40) + .background( + color = SurfaceColor.PrimaryContainer, + shape = RoundedCornerShape(Radius.Full), + ), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Filled.CheckBoxOutlineBlank, + contentDescription = "Unselected", + tint = SurfaceColor.ContainerHighest, + ) + } +} + +@Composable +fun SelectedItemIcon(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(Spacing.Spacing40) + .background( + color = SurfaceColor.Primary, + shape = RoundedCornerShape(Radius.Full), + ), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Filled.Done, + contentDescription = "Selected", + tint = TextColor.OnPrimary, + ) + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/state/ListCardState.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/state/ListCardState.kt index 63f494b67..cbcab6589 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/state/ListCardState.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/state/ListCardState.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.unit.Dp import org.hisp.dhis.mobile.ui.designsystem.component.ListCardDescriptionModel import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel +import org.hisp.dhis.mobile.ui.designsystem.component.SelectionState @Stable interface ListCardState { @@ -17,6 +18,7 @@ interface ListCardState { val shadow: Boolean val expandable: Boolean val itemVerticalPadding: Dp? + val selectionState: SelectionState fun descriptionBasedOnLoading() = description?.takeIf { !loading } fun lastUpdateBasedOnLoading() = lastUpdated?.takeIf { !loading } @@ -32,6 +34,7 @@ internal class ListCardStateImpl( override val shadow: Boolean, override val expandable: Boolean, override val itemVerticalPadding: Dp?, + override val selectionState: SelectionState, ) : ListCardState @Composable @@ -44,12 +47,14 @@ fun rememberListCardState( shadow: Boolean = true, expandable: Boolean = false, itemVerticalPadding: Dp? = null, + selectionState: SelectionState = SelectionState.NONE, ): ListCardState = remember( description, itemVerticalPadding, loading, additionalInfoColumnState, lastUpdated, + selectionState, ) { ListCardStateImpl( title, @@ -60,5 +65,6 @@ fun rememberListCardState( shadow, expandable, itemVerticalPadding, + selectionState, ) } diff --git a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_ListCardSelectableSnapshotTest_launchListCard.png b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_ListCardSelectableSnapshotTest_launchListCard.png new file mode 100644 index 000000000..89bf620b6 --- /dev/null +++ b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_ListCardSelectableSnapshotTest_launchListCard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d090bb6e8d311ae481a4ff231623d5f2a7307150a5e2fb2717bec6f4eb0d4d59 +size 66174 From 647f86a5909badb657f4ff9364dd211d007bda51 Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 8 Oct 2024 07:55:16 +0200 Subject: [PATCH 19/29] Fixes for [ANDROAPP-6114] (#310) * allow 2 lines in location address Signed-off-by: Pablo * add autoselect item Signed-off-by: Pablo --------- Signed-off-by: Pablo --- .../location/LocationSearchBarScreen.kt | 99 +++++++++++++++---- .../component/LocationSearchBar.kt | 23 ++++- 2 files changed, 103 insertions(+), 19 deletions(-) diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/location/LocationSearchBarScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/location/LocationSearchBarScreen.kt index ba47fc450..7c265995a 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/location/LocationSearchBarScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/location/LocationSearchBarScreen.kt @@ -13,7 +13,10 @@ import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import org.hisp.dhis.common.screens.components.GroupComponentDropDown +import org.hisp.dhis.mobile.ui.designsystem.component.DropdownItem import org.hisp.dhis.mobile.ui.designsystem.component.LocationBar +import org.hisp.dhis.mobile.ui.designsystem.component.OnSearchAction import org.hisp.dhis.mobile.ui.designsystem.component.model.LocationItemModel @Composable @@ -27,24 +30,74 @@ fun LocationSearchBarScreen( var itemList: List by remember { mutableStateOf(defaultLocationItems) } - Box( - modifier = Modifier.fillMaxSize() - .background(Color.White) - .padding(16.dp), - contentAlignment = TopCenter, - ) { - LocationBar( - currentResults = itemList, - onBackClicked = {}, - onClearLocation = {}, - onSearchLocation = { locationQuery -> - onSearchLocation(locationQuery) { - itemList = it.takeIf { locationQuery.isNotBlank() } ?: defaultLocationItems - } - }, - onLocationSelected = { locationItemModel -> - }, - ) + + val currentScreen = remember { mutableStateOf(LocationSearchBarOptions.DEFAULT_BEHAVIOUR) } + val screenDropdownItemList = mutableListOf() + LocationSearchBarOptions.entries.forEach { + screenDropdownItemList.add(DropdownItem(it.label)) + } + + GroupComponentDropDown( + dropdownItems = screenDropdownItemList.toList(), + onItemSelected = { + itemList = defaultLocationItems + currentScreen.value = getCurrentScreen(it.label) + }, + onResetButtonClicked = { + itemList = defaultLocationItems + currentScreen.value = LocationSearchBarOptions.DEFAULT_BEHAVIOUR + }, + selectedItem = DropdownItem(currentScreen.value.label), + ) + + when (currentScreen.value) { + LocationSearchBarOptions.DEFAULT_BEHAVIOUR -> { + Box( + modifier = Modifier.fillMaxSize() + .background(Color.White) + .padding(16.dp), + contentAlignment = TopCenter, + ) { + LocationBar( + currentResults = itemList, + onBackClicked = {}, + onClearLocation = {}, + onSearchLocation = { locationQuery -> + onSearchLocation(locationQuery) { + itemList = + it.takeIf { locationQuery.isNotBlank() } ?: defaultLocationItems + } + }, + onLocationSelected = { locationItemModel -> + }, + ) + } + } + + LocationSearchBarOptions.AUTOSELECT_ON_ONE_ITEM_FOUND -> { + Box( + modifier = Modifier.fillMaxSize() + .background(Color.White) + .padding(16.dp), + contentAlignment = TopCenter, + ) { + LocationBar( + currentResults = itemList, + searchAction = OnSearchAction.OnOneItemSelect, + onBackClicked = {}, + onClearLocation = {}, + onSearchLocation = { locationQuery -> + onSearchLocation(locationQuery) { + itemList = + it.take(1).takeIf { locationQuery.isNotBlank() } + ?: defaultLocationItems + } + }, + onLocationSelected = { locationItemModel -> + }, + ) + } + } } } @@ -62,3 +115,13 @@ private val defaultLocationItems = listOf( storedLongitude = 0.0, ), ) + +private enum class LocationSearchBarOptions(val label: String) { + DEFAULT_BEHAVIOUR("Default behaviour"), + AUTOSELECT_ON_ONE_ITEM_FOUND("Autoselect on one item found"), +} + +private fun getCurrentScreen(label: String): LocationSearchBarOptions { + return LocationSearchBarOptions.entries.firstOrNull { it.label == label } + ?: LocationSearchBarOptions.DEFAULT_BEHAVIOUR +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBar.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBar.kt index 08b453a08..b070ab29b 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBar.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBar.kt @@ -66,6 +66,11 @@ enum class SearchBarMode { SEARCH, } +enum class OnSearchAction { + Default, + OnOneItemSelect, +} + /** * DHIS2 Location Bar. * @param currentResults: the available location items to display before/after search. @@ -80,6 +85,7 @@ enum class SearchBarMode { fun LocationBar( currentResults: List, mode: SearchBarMode = SearchBarMode.BUTTON, + searchAction: OnSearchAction = OnSearchAction.Default, onBackClicked: () -> Unit, onClearLocation: () -> Unit, onSearchLocation: (query: String) -> Unit, @@ -88,11 +94,25 @@ fun LocationBar( ) { var currentMode by remember { mutableStateOf(mode) } var currentSearch: String by remember { mutableStateOf("") } + fun selectItem(item: LocationItemModel) { + currentSearch = item.title + currentMode = SearchBarMode.BUTTON + onLocationSelected(item) + } LaunchedEffect(currentMode) { onModeChanged(currentMode) } + LaunchedEffect(searchAction, currentResults) { + if (searchAction == OnSearchAction.OnOneItemSelect) { + currentResults.filterIsInstance() + .takeIf { it.size == 1 }?.let { + selectItem(it.first()) + } + } + } + when (currentMode) { SearchBarMode.BUTTON -> LocationSearchBarButton( currentSearch = currentSearch, @@ -120,6 +140,7 @@ fun LocationBar( currentMode = SearchBarMode.BUTTON }, onLocationSelected = { + selectItem(it) currentSearch = it.title currentMode = SearchBarMode.BUTTON onLocationSelected(it) @@ -314,7 +335,7 @@ fun LocationItem( text = locationItemModel.subtitle, style = MaterialTheme.typography.bodySmall, color = TextColor.OnSurface, - maxLines = 1, + maxLines = 2, overflow = TextOverflow.Ellipsis, ) } From 668a98dae611cb6c7699fcdaf26d0e4d80b31f9b Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Fri, 11 Oct 2024 12:54:02 +0530 Subject: [PATCH 20/29] Add parameter to handle bottom sheet header view (#314) Co-authored-by: Siddharth Agarwal --- .../mobile/ui/designsystem/component/BottomSheet.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt index 26c08347f..a24dc5bb8 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt @@ -166,6 +166,7 @@ fun BottomSheetShell( headerTextAlignment: TextAlign = TextAlign.Center, scrollableContainerMinHeight: Dp = Spacing0, scrollableContainerMaxHeight: Dp = InternalSizeValues.Size386, + animateHeaderOnKeyboardAppearance: Boolean = true, onSearchQueryChanged: ((String) -> Unit)? = null, onSearch: ((String) -> Unit)? = null, onDismiss: () -> Unit, @@ -175,7 +176,15 @@ fun BottomSheetShell( val keyboardState by keyboardAsState() var isKeyboardOpen by remember { mutableStateOf(false) } - val showHeader by remember { derivedStateOf { !title.isNullOrBlank() && !isKeyboardOpen } } + val showHeader by remember { + derivedStateOf { + if (animateHeaderOnKeyboardAppearance) { + !title.isNullOrBlank() && !isKeyboardOpen + } else { + !title.isNullOrBlank() + } + } + } LaunchedEffect(keyboardState) { isKeyboardOpen = keyboardState == Keyboard.Opened From 021760f210de7b7590c1b96baa942f1c728575a1 Mon Sep 17 00:00:00 2001 From: Sasikanth Date: Mon, 14 Oct 2024 15:35:28 +0530 Subject: [PATCH 21/29] Wrap `OrgUnitSelectorItem` in a `key` (#315) This acts same as item key in lazy views. Prevents the composable from recomposing unnecessarily when the key didn't change in the control flows. Like in this `forLoop` for example. --- .../ui/designsystem/component/OrgBottomSheet.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt index d1631a881..74fd87cd0 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -199,13 +200,15 @@ private fun OrgTreeList( horizontalAlignment = Alignment.Start, ) { orgTreeItems.forEach { item -> - OrgUnitSelectorItem( - orgTreeItem = item, - higherLevel = orgTreeItems.minBy { it.level }.level, - searchQuery = searchQuery, - onItemClick = onItemClick, - onItemSelected = onItemSelected, - ) + key(item.uid) { + OrgUnitSelectorItem( + orgTreeItem = item, + higherLevel = orgTreeItems.minBy { it.level }.level, + searchQuery = searchQuery, + onItemClick = onItemClick, + onItemSelected = onItemSelected, + ) + } } } } From a6e22094e8c59866739d427c3a794a177ca26acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Miguel=20Rubio?= Date: Wed, 16 Oct 2024 14:47:36 +0200 Subject: [PATCH 22/29] docs: add TopBar documentation (#316) --- .../ui/designsystem/component/TopBar.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/TopBar.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/TopBar.kt index 3960186bc..ae36fac57 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/TopBar.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/TopBar.kt @@ -18,6 +18,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +/** + * A composable function that renders a top app bar. Depending on the [type], it can display either a + * default aligned top app bar or a center-aligned top app bar. + * + * @param modifier The [Modifier] to be applied to the TopBar. + * @param type The type of the TopBar, either [TopBarType.DEFAULT] for a default aligned top app bar or [TopBarType.CENTERED] for a center-aligned top app bar. + * @param navigationIcon A composable function that represents the navigation icon displayed in the TopBar, typically a back arrow or a menu icon. + * @param actions A composable function that represents the actions (e.g., icons, menus) displayed on the right side of the TopBar. + * @param title A composable function that represents the title content of the TopBar. + * @param colors A [TopAppBarColors] that defines the color scheme for the TopBar. Default is [TopAppBarDefaults.topAppBarColors]. + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun TopBar( @@ -47,6 +58,14 @@ fun TopBar( } } +/** + * A composable function that renders an action icon within the TopBar. + * + * @param icon The [ImageVector] representing the icon to be displayed. + * @param tint The tint color for the icon. Default is [Color.Unspecified]. + * @param contentDescription A description of the icon for accessibility purposes. + * @param onClick The callback to be invoked when the icon is clicked. + */ @Composable fun TopBarActionIcon( icon: ImageVector, @@ -66,6 +85,12 @@ fun TopBarActionIcon( ) } +/** + * A composable function that renders an icon button which toggles a dropdown menu. + * + * @param iconTint The tint color for the dropdown icon. Default is [Color.Unspecified]. + * @param dropDownMenu A composable function that renders the dropdown menu, receiving the current state (shown or hidden) and a callback to dismiss the menu. + */ @Composable fun TopBarDropdownMenuIcon( iconTint: Color = Color.Unspecified, @@ -86,6 +111,12 @@ fun TopBarDropdownMenuIcon( dropDownMenu(showMenu) { showMenu = false } } +/** + * Enum class representing the type of the TopBar. + * + * @property DEFAULT The default TopBar alignment with the title left-aligned. + * @property CENTERED A center-aligned TopBar with the title centered. + */ enum class TopBarType { DEFAULT, CENTERED, From b8db43cd3d1fc0049cf99c25d93a92763f737a83 Mon Sep 17 00:00:00 2001 From: andresmr Date: Thu, 17 Oct 2024 17:07:51 +0200 Subject: [PATCH 23/29] update docs with develop --- .../common/screens/cards/ListCardScreen.kt | 1 - .../designsystem/component/InputDateTime.kt | 261 +++++++++++++++--- 2 files changed, 221 insertions(+), 41 deletions(-) diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ListCardScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ListCardScreen.kt index 75442ebee..eb5ffa5e3 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ListCardScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/cards/ListCardScreen.kt @@ -42,7 +42,6 @@ import org.hisp.dhis.mobile.ui.designsystem.component.ListCardDescriptionModel import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel import org.hisp.dhis.mobile.ui.designsystem.component.MetadataAvatarSize import org.hisp.dhis.mobile.ui.designsystem.component.SelectionState -import org.hisp.dhis.mobile.ui.designsystem.component.internal.ImageCardData import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberAdditionalInfoColumnState import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberListCardState import org.hisp.dhis.mobile.ui.designsystem.resource.provideDHIS2Icon diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt index 4d3bdf0c4..11f067271 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt @@ -26,6 +26,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.TimePicker import androidx.compose.material3.TimePickerLayoutType import androidx.compose.material3.TimePickerState +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -53,11 +55,16 @@ import androidx.compose.ui.window.DialogProperties import org.hisp.dhis.mobile.ui.designsystem.component.internal.convertStringToTextFieldValue import org.hisp.dhis.mobile.ui.designsystem.component.internal.formatStoredDateToUI import org.hisp.dhis.mobile.ui.designsystem.component.internal.formatUIDateToStored +import org.hisp.dhis.mobile.ui.designsystem.component.internal.getDefaultFormat +import org.hisp.dhis.mobile.ui.designsystem.component.internal.getSelectableDates import org.hisp.dhis.mobile.ui.designsystem.component.internal.getSupportingTextList import org.hisp.dhis.mobile.ui.designsystem.component.internal.getTime import org.hisp.dhis.mobile.ui.designsystem.component.internal.getTimePickerState +import org.hisp.dhis.mobile.ui.designsystem.component.internal.isValidHourFormat +import org.hisp.dhis.mobile.ui.designsystem.component.internal.parseDate import org.hisp.dhis.mobile.ui.designsystem.component.internal.provideDatePickerState import org.hisp.dhis.mobile.ui.designsystem.component.internal.timePickerColors +import org.hisp.dhis.mobile.ui.designsystem.component.internal.yearIsInRange import org.hisp.dhis.mobile.ui.designsystem.component.model.DateTimeVisualTransformation import org.hisp.dhis.mobile.ui.designsystem.component.model.DateTransformation import org.hisp.dhis.mobile.ui.designsystem.component.model.RegExValidations @@ -104,8 +111,9 @@ fun InputDateTime( dateOutOfRangeText = "$dateOutOfRangeText (" + formatStringToDate( uiModel.selectableDates.initialDate, ) + " - " + - formatStringToDate(uiModel.selectableDates.endDate) + ")" - val incorrectHourFormatText = uiModel.incorrectHourFormatText ?: provideStringResource("wrong_hour_format") + formatStringToDate(uiModel.selectableDates.endDate) + ")" + val incorrectHourFormatText = + uiModel.incorrectHourFormatText ?: provideStringResource("wrong_hour_format") val incorrectHourFormatItem = SupportingTextData( text = incorrectHourFormatText, SupportingTextState.ERROR, @@ -119,13 +127,21 @@ fun InputDateTime( SupportingTextState.ERROR, ) val supportingTextList = - getSupportingTextList(uiModel, dateOutOfRangeItem, incorrectHourFormatItem, incorrectDateFormatItem) + getSupportingTextList( + uiModel, + dateOutOfRangeItem, + incorrectHourFormatItem, + incorrectDateFormatItem + ) InputShell( modifier = modifier.testTag("INPUT_DATE_TIME") .focusRequester(focusRequester), title = uiModel.title, - state = if (supportingTextList.contains(dateOutOfRangeItem) || supportingTextList.contains(incorrectDateFormatItem)) InputShellState.ERROR else uiModel.state, + state = if (supportingTextList.contains(dateOutOfRangeItem) || supportingTextList.contains( + incorrectDateFormatItem + ) + ) InputShellState.ERROR else uiModel.state, isRequiredField = uiModel.isRequired, onFocusChanged = uiModel.onFocusChanged, inputField = { @@ -134,7 +150,10 @@ fun InputDateTime( modifier = Modifier .testTag("INPUT_DATE_TIME_TEXT_FIELD") .fillMaxWidth(), - inputTextValue = TextFieldValue(uiModel.inputTextFieldValue?.text ?: "", TextRange(uiModel.inputTextFieldValue?.text?.length ?: 0)), + inputTextValue = TextFieldValue( + uiModel.inputTextFieldValue?.text ?: "", + TextRange(uiModel.inputTextFieldValue?.text?.length ?: 0) + ), isSingleLine = true, onInputChanged = { newText -> if (newText.text.length > uiModel.visualTransformation.maskLength) { @@ -147,7 +166,10 @@ fun InputDateTime( }, enabled = uiModel.state != InputShellState.DISABLED, state = uiModel.state, - keyboardOptions = KeyboardOptions(imeAction = uiModel.imeAction, keyboardType = KeyboardType.Number), + keyboardOptions = KeyboardOptions( + imeAction = uiModel.imeAction, + keyboardType = KeyboardType.Number + ), visualTransformation = uiModel.visualTransformation, onNextClicked = { if (uiModel.onNextClicked != null) { @@ -193,7 +215,8 @@ fun InputDateTime( primaryButton = { if (!uiModel.inputTextFieldValue?.text.isNullOrBlank() && uiModel.state != InputShellState.DISABLED) { IconButton( - modifier = Modifier.testTag("INPUT_DATE_TIME_RESET_BUTTON").padding(Spacing.Spacing0), + modifier = Modifier.testTag("INPUT_DATE_TIME_RESET_BUTTON") + .padding(Spacing.Spacing0), icon = { Icon( imageVector = Icons.Outlined.Cancel, @@ -275,7 +298,14 @@ fun InputDateTime( showDatePicker = false if (uiModel.actionType != DateTimeActionType.DATE_TIME) { datePickerState.selectedDateMillis?.let { - uiModel.onValueChanged(TextFieldValue(getDate(it, uiModel.format), selection = TextRange(uiModel.inputTextFieldValue?.text?.length ?: 0))) + uiModel.onValueChanged( + TextFieldValue( + getDate(it, uiModel.format), + selection = TextRange( + uiModel.inputTextFieldValue?.text?.length ?: 0 + ) + ) + ) } } else { showTimePicker = true @@ -290,7 +320,7 @@ fun InputDateTime( ColorStyle.DEFAULT, uiModel.cancelText ?: provideStringResource("cancel"), - ) { + ) { showDatePicker = false } }, @@ -305,7 +335,10 @@ fun InputDateTime( Text( text = uiModel.title, style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(start = Spacing.Spacing24, top = Spacing.Spacing24), + modifier = Modifier.padding( + start = Spacing.Spacing24, + top = Spacing.Spacing24 + ), ) }, state = datePickerState, @@ -318,24 +351,45 @@ fun InputDateTime( if (showTimePicker) { var timePickerState = rememberTimePickerState(0, 0, is24Hour = uiModel.is24hourFormat) - if (!uiModel.inputTextFieldValue?.text.isNullOrEmpty() && uiModel.actionType == DateTimeActionType.TIME && isValidHourFormat(uiModel.inputTextFieldValue?.text ?: "")) { + if (!uiModel.inputTextFieldValue?.text.isNullOrEmpty() && uiModel.actionType == DateTimeActionType.TIME && isValidHourFormat( + uiModel.inputTextFieldValue?.text ?: "" + ) + ) { timePickerState = rememberTimePickerState( initialHour = uiModel.inputTextFieldValue?.text?.substring(0, 2)!! .toInt(), - uiModel.inputTextFieldValue.text.substring(2, 4).toInt(), is24Hour = uiModel.is24hourFormat, + uiModel.inputTextFieldValue.text.substring(2, 4).toInt(), + is24Hour = uiModel.is24hourFormat, ) } else { - if (uiModel.inputTextFieldValue?.text?.length == 12 && isValidHourFormat(uiModel.inputTextFieldValue.text.substring(8, 12))) { + if (uiModel.inputTextFieldValue?.text?.length == 12 && isValidHourFormat( + uiModel.inputTextFieldValue.text.substring( + 8, + 12 + ) + ) + ) { timePickerState = rememberTimePickerState( - initialHour = uiModel.inputTextFieldValue.text.substring(uiModel.inputTextFieldValue.text.length - 4, uiModel.inputTextFieldValue.text.length - 2) + initialHour = uiModel.inputTextFieldValue.text.substring( + uiModel.inputTextFieldValue.text.length - 4, + uiModel.inputTextFieldValue.text.length - 2 + ) .toInt(), - uiModel.inputTextFieldValue.text.substring(uiModel.inputTextFieldValue.text.length - 2, uiModel.inputTextFieldValue.text.length).toInt(), is24Hour = uiModel.is24hourFormat, + uiModel.inputTextFieldValue.text.substring( + uiModel.inputTextFieldValue.text.length - 2, + uiModel.inputTextFieldValue.text.length + ).toInt(), + is24Hour = uiModel.is24hourFormat, ) } } Dialog( onDismissRequest = { showDatePicker = false }, - properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true, usePlatformDefaultWidth = true), + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true, + usePlatformDefaultWidth = true + ), ) { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -363,7 +417,7 @@ fun InputDateTime( ColorStyle.DEFAULT, uiModel.cancelText ?: provideStringResource("cancel"), - ) { + ) { showTimePicker = false } Button( @@ -374,9 +428,25 @@ fun InputDateTime( ) { showTimePicker = false if (uiModel.actionType != DateTimeActionType.DATE_TIME) { - uiModel.onValueChanged(TextFieldValue(getTime(timePickerState), selection = TextRange(uiModel.inputTextFieldValue?.text?.length ?: 0))) + uiModel.onValueChanged( + TextFieldValue( + getTime(timePickerState), + selection = TextRange( + uiModel.inputTextFieldValue?.text?.length ?: 0 + ) + ) + ) } else { - uiModel.onValueChanged(TextFieldValue(getDate(datePickerState.selectedDateMillis) + getTime(timePickerState), selection = TextRange(uiModel.inputTextFieldValue?.text?.length ?: 0))) + uiModel.onValueChanged( + TextFieldValue( + getDate(datePickerState.selectedDateMillis) + getTime( + timePickerState + ), + selection = TextRange( + uiModel.inputTextFieldValue?.text?.length ?: 0 + ) + ) + ) } } } @@ -385,11 +455,19 @@ fun InputDateTime( } } -private fun getInputState(supportingTextList: List, dateOutOfRangeItem: SupportingTextData, incorrectDateFormatItem: SupportingTextData, currentState: InputShellState): InputShellState { - return if (supportingTextList.contains(dateOutOfRangeItem) || supportingTextList.contains(incorrectDateFormatItem)) InputShellState.ERROR else currentState +private fun getInputState( + supportingTextList: List, + dateOutOfRangeItem: SupportingTextData, + incorrectDateFormatItem: SupportingTextData, + currentState: InputShellState +): InputShellState { + return if (supportingTextList.contains(dateOutOfRangeItem) || supportingTextList.contains( + incorrectDateFormatItem + ) + ) InputShellState.ERROR else currentState } -fun getActionButtonIcon(actionType: DateTimeActionType): ImageVector { +private fun getActionButtonIcon(actionType: DateTimeActionType): ImageVector { return when (actionType) { DateTimeActionType.DATE, DateTimeActionType.DATE_TIME -> Icons.Filled.Event DateTimeActionType.TIME -> Icons.Filled.Schedule @@ -418,7 +496,11 @@ fun InputDateTime( ) { val uiData = state.uiData - val uiValue = remember(state.inputTextFieldValue) { formatStoredDateToUI(state.inputTextFieldValue ?: TextFieldValue(), uiData.actionType) } + val uiValue = remember(state.inputTextFieldValue) { + formatStoredDateToUI( + state.inputTextFieldValue ?: TextFieldValue(), uiData.actionType + ) + } val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } var showDatePicker by rememberSaveable { mutableStateOf(false) } @@ -428,8 +510,9 @@ fun InputDateTime( dateOutOfRangeText = "$dateOutOfRangeText (" + formatStringToDate( uiData.selectableDates.initialDate, ) + " - " + - formatStringToDate(uiData.selectableDates.endDate) + ")" - val incorrectHourFormatTextdd = uiData.incorrectHourFormatText ?: provideStringResource("wrong_hour_format") + formatStringToDate(uiData.selectableDates.endDate) + ")" + val incorrectHourFormatTextdd = + uiData.incorrectHourFormatText ?: provideStringResource("wrong_hour_format") val incorrectHourFormatItem = SupportingTextData( text = incorrectHourFormatTextdd, SupportingTextState.ERROR, @@ -443,13 +526,25 @@ fun InputDateTime( SupportingTextState.ERROR, ) val supportingTextList = - getSupportingTextList(state, uiValue, uiData, dateOutOfRangeItem, incorrectHourFormatItem, incorrectDateFormatItem) + getSupportingTextList( + state, + uiValue, + uiData, + dateOutOfRangeItem, + incorrectHourFormatItem, + incorrectDateFormatItem + ) InputShell( modifier = modifier.testTag("INPUT_DATE_TIME") .focusRequester(focusRequester), title = uiData.title, - state = getInputState(supportingTextList, dateOutOfRangeItem, incorrectDateFormatItem, state.inputState), + state = getInputState( + supportingTextList, + dateOutOfRangeItem, + incorrectDateFormatItem, + state.inputState + ), isRequiredField = uiData.isRequired, onFocusChanged = onFocusChanged, inputField = { @@ -469,7 +564,10 @@ fun InputDateTime( }, enabled = state.inputState != InputShellState.DISABLED, state = state.inputState, - keyboardOptions = KeyboardOptions(imeAction = uiData.imeAction, keyboardType = KeyboardType.Number), + keyboardOptions = KeyboardOptions( + imeAction = uiData.imeAction, + keyboardType = KeyboardType.Number + ), visualTransformation = uiData.visualTransformation, onNextClicked = { manageOnNext(focusManager, onNextClicked) @@ -570,7 +668,14 @@ fun InputDateTime( showDatePicker = false if (uiData.actionType != DateTimeActionType.DATE_TIME) { datePickerState.selectedDateMillis?.let { - onValueChanged(TextFieldValue(getDate(it), selection = TextRange(state.inputTextFieldValue?.text?.length ?: 0))) + onValueChanged( + TextFieldValue( + getDate(it), + selection = TextRange( + state.inputTextFieldValue?.text?.length ?: 0 + ) + ) + ) } } else { showTimePicker = true @@ -585,7 +690,7 @@ fun InputDateTime( ColorStyle.DEFAULT, uiData.cancelText ?: provideStringResource("cancel"), - ) { + ) { showDatePicker = false } }, @@ -600,7 +705,10 @@ fun InputDateTime( Text( text = uiData.title, style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(start = Spacing.Spacing24, top = Spacing.Spacing24), + modifier = Modifier.padding( + start = Spacing.Spacing24, + top = Spacing.Spacing24 + ), ) }, state = datePickerState, @@ -616,7 +724,11 @@ fun InputDateTime( Dialog( onDismissRequest = { showDatePicker = false }, - properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true, usePlatformDefaultWidth = true), + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true, + usePlatformDefaultWidth = true + ), ) { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -644,7 +756,7 @@ fun InputDateTime( ColorStyle.DEFAULT, uiData.cancelText ?: provideStringResource("cancel"), - ) { + ) { showTimePicker = false } Button( @@ -654,7 +766,13 @@ fun InputDateTime( uiData.acceptText ?: provideStringResource("ok"), ) { showTimePicker = false - manageOnValueChangedFromDateTimePicker(convertStringToTextFieldValue(getTime(timePickerState)), onValueChanged, uiData.actionType, datePickerState, timePickerState) + manageOnValueChangedFromDateTimePicker( + convertStringToTextFieldValue( + getTime( + timePickerState + ) + ), onValueChanged, uiData.actionType, datePickerState, timePickerState + ) } } } @@ -663,7 +781,11 @@ fun InputDateTime( } @Composable -fun InputDateResetButton(state: InputDateTimeState, onValueChanged: (TextFieldValue?) -> Unit, focusRequester: FocusRequester) { +fun InputDateResetButton( + state: InputDateTimeState, + onValueChanged: (TextFieldValue?) -> Unit, + focusRequester: FocusRequester +) { if (!state.inputTextFieldValue?.text.isNullOrBlank() && state.inputState != InputShellState.DISABLED) { IconButton( modifier = Modifier.testTag("INPUT_DATE_TIME_RESET_BUTTON").padding(Spacing.Spacing0), @@ -697,7 +819,11 @@ fun manageOnNext(focusManager: FocusManager, onNextClicked: (() -> Unit)?) { } } -private fun manageOnValueChanged(newText: TextFieldValue, onValueChanged: (TextFieldValue?) -> Unit, actionType: DateTimeActionType) { +private fun manageOnValueChanged( + newText: TextFieldValue, + onValueChanged: (TextFieldValue?) -> Unit, + actionType: DateTimeActionType +) { val allowedCharacters = RegExValidations.DATE_TIME.regex if (allowedCharacters.containsMatchIn(newText.text) || newText.text.isBlank()) { onValueChanged.invoke(formatUIDateToStored(newText, actionType)) @@ -705,14 +831,54 @@ private fun manageOnValueChanged(newText: TextFieldValue, onValueChanged: (TextF } @OptIn(ExperimentalMaterial3Api::class) -private fun manageOnValueChangedFromDateTimePicker(newValue: TextFieldValue?, onValueChanged: (TextFieldValue?) -> Unit, actionType: DateTimeActionType, datePickerState: DatePickerState, timePickerState: TimePickerState) { +private fun manageOnValueChangedFromDateTimePicker( + newValue: TextFieldValue?, + onValueChanged: (TextFieldValue?) -> Unit, + actionType: DateTimeActionType, + datePickerState: DatePickerState, + timePickerState: TimePickerState +) { if (actionType != DateTimeActionType.DATE_TIME) { - onValueChanged(TextFieldValue(getTime(timePickerState), selection = TextRange(newValue?.text?.length ?: 0))) + onValueChanged( + TextFieldValue( + getTime(timePickerState), + selection = TextRange(newValue?.text?.length ?: 0) + ) + ) } else { - onValueChanged(TextFieldValue(getDate(datePickerState.selectedDateMillis) + getTime(timePickerState), selection = TextRange(newValue?.text?.length ?: 0))) + onValueChanged( + TextFieldValue( + getDate(datePickerState.selectedDateMillis) + getTime( + timePickerState + ), selection = TextRange(newValue?.text?.length ?: 0) + ) + ) } } +@Suppress("deprecation") +@Deprecated( + "This function is deprecated and will be removed in the next release.", + replaceWith = ReplaceWith("provideDatePickerState(state: InputDateTimeState, data: InputDateTimeData)") +) +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun provideDatePickerState(uiModel: InputDateTimeModel): DatePickerState { + return uiModel.inputTextFieldValue?.text?.takeIf { + it.isNotEmpty() && + yearIsInRange(it, getDefaultFormat(uiModel.actionType), uiModel.yearRange) + }?.let { + rememberDatePickerState( + initialSelectedDateMillis = parseStringDateToMillis( + dateString = it, + pattern = getDefaultFormat(uiModel.actionType), + ), + yearRange = uiModel.yearRange, + selectableDates = getSelectableDates(uiModel), + ) + } ?: rememberDatePickerState(selectableDates = getSelectableDates(uiModel)) +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun datePickerColors(): DatePickerColors { @@ -726,6 +892,18 @@ fun datePickerColors(): DatePickerColors { ) } +@Deprecated( + "This function is deprecated and will be removed in the near future", + replaceWith = ReplaceWith("parseStringDateToMillis(dateString: String, pattern: String)") +) +private fun parseStringDateToMillis(dateString: String, pattern: String = "ddMMyyyy"): Long { + val cal = Calendar.getInstance() + return dateString.parseDate(pattern)?.let { + cal.time = it + cal.timeInMillis + } ?: 0L +} + internal fun getDate(milliSeconds: Long?, format: String? = "ddMMyyyy"): String { val cal = Calendar.getInstance() val currentTimeZone: TimeZone = cal.getTimeZone() @@ -757,7 +935,10 @@ internal fun getDate(milliSeconds: Long?, format: String? = "ddMMyyyy"): String fun formatStringToDate(dateString: String): String { return if (dateString.length == 8) { - dateString.substring(0, 2) + "/" + dateString.substring(2, 4) + "/" + dateString.substring(4, 8) + dateString.substring(0, 2) + "/" + dateString.substring( + 2, + 4 + ) + "/" + dateString.substring(4, 8) } else { dateString } From 65d29cc36f943899bad6383beaf2f78a89e4f552 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 18 Oct 2024 10:29:30 +0200 Subject: [PATCH 24/29] Add github action to rebuild docs on change --- .github/workflows/rebuild-docs.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/rebuild-docs.yml diff --git a/.github/workflows/rebuild-docs.yml b/.github/workflows/rebuild-docs.yml new file mode 100644 index 000000000..a5b940923 --- /dev/null +++ b/.github/workflows/rebuild-docs.yml @@ -0,0 +1,18 @@ +name: 'Rebuild developer docs' + +on: + push: + branches: + - main + paths: + - 'docs/**' + +concurrency: + group: ${{ github.workflow}}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rebuild-docs: + runs-on: ubuntu-latest + steps: + - run: curl -X POST -d {} https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_DEVELOPER_DOCS_TOKEN }} From 77fb20cf83f2cab1fc1b830607c8641d6f00a71f Mon Sep 17 00:00:00 2001 From: andresmr Date: Mon, 21 Oct 2024 13:11:41 +0200 Subject: [PATCH 25/29] docs: ktlint format --- .../designsystem/component/InputDateTime.kt | 124 ++++++++++-------- 1 file changed, 69 insertions(+), 55 deletions(-) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt index 11f067271..db293b4a7 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDateTime.kt @@ -111,7 +111,7 @@ fun InputDateTime( dateOutOfRangeText = "$dateOutOfRangeText (" + formatStringToDate( uiModel.selectableDates.initialDate, ) + " - " + - formatStringToDate(uiModel.selectableDates.endDate) + ")" + formatStringToDate(uiModel.selectableDates.endDate) + ")" val incorrectHourFormatText = uiModel.incorrectHourFormatText ?: provideStringResource("wrong_hour_format") val incorrectHourFormatItem = SupportingTextData( @@ -131,7 +131,7 @@ fun InputDateTime( uiModel, dateOutOfRangeItem, incorrectHourFormatItem, - incorrectDateFormatItem + incorrectDateFormatItem, ) InputShell( @@ -139,9 +139,13 @@ fun InputDateTime( .focusRequester(focusRequester), title = uiModel.title, state = if (supportingTextList.contains(dateOutOfRangeItem) || supportingTextList.contains( - incorrectDateFormatItem + incorrectDateFormatItem, ) - ) InputShellState.ERROR else uiModel.state, + ) { + InputShellState.ERROR + } else { + uiModel.state + }, isRequiredField = uiModel.isRequired, onFocusChanged = uiModel.onFocusChanged, inputField = { @@ -152,7 +156,7 @@ fun InputDateTime( .fillMaxWidth(), inputTextValue = TextFieldValue( uiModel.inputTextFieldValue?.text ?: "", - TextRange(uiModel.inputTextFieldValue?.text?.length ?: 0) + TextRange(uiModel.inputTextFieldValue?.text?.length ?: 0), ), isSingleLine = true, onInputChanged = { newText -> @@ -168,7 +172,7 @@ fun InputDateTime( state = uiModel.state, keyboardOptions = KeyboardOptions( imeAction = uiModel.imeAction, - keyboardType = KeyboardType.Number + keyboardType = KeyboardType.Number, ), visualTransformation = uiModel.visualTransformation, onNextClicked = { @@ -302,9 +306,9 @@ fun InputDateTime( TextFieldValue( getDate(it, uiModel.format), selection = TextRange( - uiModel.inputTextFieldValue?.text?.length ?: 0 - ) - ) + uiModel.inputTextFieldValue?.text?.length ?: 0, + ), + ), ) } } else { @@ -320,7 +324,7 @@ fun InputDateTime( ColorStyle.DEFAULT, uiModel.cancelText ?: provideStringResource("cancel"), - ) { + ) { showDatePicker = false } }, @@ -337,7 +341,7 @@ fun InputDateTime( style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding( start = Spacing.Spacing24, - top = Spacing.Spacing24 + top = Spacing.Spacing24, ), ) }, @@ -352,7 +356,7 @@ fun InputDateTime( if (showTimePicker) { var timePickerState = rememberTimePickerState(0, 0, is24Hour = uiModel.is24hourFormat) if (!uiModel.inputTextFieldValue?.text.isNullOrEmpty() && uiModel.actionType == DateTimeActionType.TIME && isValidHourFormat( - uiModel.inputTextFieldValue?.text ?: "" + uiModel.inputTextFieldValue?.text ?: "", ) ) { timePickerState = rememberTimePickerState( @@ -365,19 +369,19 @@ fun InputDateTime( if (uiModel.inputTextFieldValue?.text?.length == 12 && isValidHourFormat( uiModel.inputTextFieldValue.text.substring( 8, - 12 - ) + 12, + ), ) ) { timePickerState = rememberTimePickerState( initialHour = uiModel.inputTextFieldValue.text.substring( uiModel.inputTextFieldValue.text.length - 4, - uiModel.inputTextFieldValue.text.length - 2 + uiModel.inputTextFieldValue.text.length - 2, ) .toInt(), uiModel.inputTextFieldValue.text.substring( uiModel.inputTextFieldValue.text.length - 2, - uiModel.inputTextFieldValue.text.length + uiModel.inputTextFieldValue.text.length, ).toInt(), is24Hour = uiModel.is24hourFormat, ) @@ -388,7 +392,7 @@ fun InputDateTime( properties = DialogProperties( dismissOnBackPress = true, dismissOnClickOutside = true, - usePlatformDefaultWidth = true + usePlatformDefaultWidth = true, ), ) { Column( @@ -417,7 +421,7 @@ fun InputDateTime( ColorStyle.DEFAULT, uiModel.cancelText ?: provideStringResource("cancel"), - ) { + ) { showTimePicker = false } Button( @@ -432,20 +436,20 @@ fun InputDateTime( TextFieldValue( getTime(timePickerState), selection = TextRange( - uiModel.inputTextFieldValue?.text?.length ?: 0 - ) - ) + uiModel.inputTextFieldValue?.text?.length ?: 0, + ), + ), ) } else { uiModel.onValueChanged( TextFieldValue( getDate(datePickerState.selectedDateMillis) + getTime( - timePickerState + timePickerState, ), selection = TextRange( - uiModel.inputTextFieldValue?.text?.length ?: 0 - ) - ) + uiModel.inputTextFieldValue?.text?.length ?: 0, + ), + ), ) } } @@ -459,12 +463,16 @@ private fun getInputState( supportingTextList: List, dateOutOfRangeItem: SupportingTextData, incorrectDateFormatItem: SupportingTextData, - currentState: InputShellState + currentState: InputShellState, ): InputShellState { return if (supportingTextList.contains(dateOutOfRangeItem) || supportingTextList.contains( - incorrectDateFormatItem + incorrectDateFormatItem, ) - ) InputShellState.ERROR else currentState + ) { + InputShellState.ERROR + } else { + currentState + } } private fun getActionButtonIcon(actionType: DateTimeActionType): ImageVector { @@ -498,7 +506,8 @@ fun InputDateTime( val uiValue = remember(state.inputTextFieldValue) { formatStoredDateToUI( - state.inputTextFieldValue ?: TextFieldValue(), uiData.actionType + state.inputTextFieldValue ?: TextFieldValue(), + uiData.actionType, ) } val focusManager = LocalFocusManager.current @@ -510,7 +519,7 @@ fun InputDateTime( dateOutOfRangeText = "$dateOutOfRangeText (" + formatStringToDate( uiData.selectableDates.initialDate, ) + " - " + - formatStringToDate(uiData.selectableDates.endDate) + ")" + formatStringToDate(uiData.selectableDates.endDate) + ")" val incorrectHourFormatTextdd = uiData.incorrectHourFormatText ?: provideStringResource("wrong_hour_format") val incorrectHourFormatItem = SupportingTextData( @@ -532,7 +541,7 @@ fun InputDateTime( uiData, dateOutOfRangeItem, incorrectHourFormatItem, - incorrectDateFormatItem + incorrectDateFormatItem, ) InputShell( @@ -543,7 +552,7 @@ fun InputDateTime( supportingTextList, dateOutOfRangeItem, incorrectDateFormatItem, - state.inputState + state.inputState, ), isRequiredField = uiData.isRequired, onFocusChanged = onFocusChanged, @@ -566,7 +575,7 @@ fun InputDateTime( state = state.inputState, keyboardOptions = KeyboardOptions( imeAction = uiData.imeAction, - keyboardType = KeyboardType.Number + keyboardType = KeyboardType.Number, ), visualTransformation = uiData.visualTransformation, onNextClicked = { @@ -672,9 +681,9 @@ fun InputDateTime( TextFieldValue( getDate(it), selection = TextRange( - state.inputTextFieldValue?.text?.length ?: 0 - ) - ) + state.inputTextFieldValue?.text?.length ?: 0, + ), + ), ) } } else { @@ -690,7 +699,7 @@ fun InputDateTime( ColorStyle.DEFAULT, uiData.cancelText ?: provideStringResource("cancel"), - ) { + ) { showDatePicker = false } }, @@ -707,7 +716,7 @@ fun InputDateTime( style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding( start = Spacing.Spacing24, - top = Spacing.Spacing24 + top = Spacing.Spacing24, ), ) }, @@ -727,7 +736,7 @@ fun InputDateTime( properties = DialogProperties( dismissOnBackPress = true, dismissOnClickOutside = true, - usePlatformDefaultWidth = true + usePlatformDefaultWidth = true, ), ) { Column( @@ -756,7 +765,7 @@ fun InputDateTime( ColorStyle.DEFAULT, uiData.cancelText ?: provideStringResource("cancel"), - ) { + ) { showTimePicker = false } Button( @@ -769,9 +778,13 @@ fun InputDateTime( manageOnValueChangedFromDateTimePicker( convertStringToTextFieldValue( getTime( - timePickerState - ) - ), onValueChanged, uiData.actionType, datePickerState, timePickerState + timePickerState, + ), + ), + onValueChanged, + uiData.actionType, + datePickerState, + timePickerState, ) } } @@ -784,7 +797,7 @@ fun InputDateTime( fun InputDateResetButton( state: InputDateTimeState, onValueChanged: (TextFieldValue?) -> Unit, - focusRequester: FocusRequester + focusRequester: FocusRequester, ) { if (!state.inputTextFieldValue?.text.isNullOrBlank() && state.inputState != InputShellState.DISABLED) { IconButton( @@ -822,7 +835,7 @@ fun manageOnNext(focusManager: FocusManager, onNextClicked: (() -> Unit)?) { private fun manageOnValueChanged( newText: TextFieldValue, onValueChanged: (TextFieldValue?) -> Unit, - actionType: DateTimeActionType + actionType: DateTimeActionType, ) { val allowedCharacters = RegExValidations.DATE_TIME.regex if (allowedCharacters.containsMatchIn(newText.text) || newText.text.isBlank()) { @@ -836,22 +849,23 @@ private fun manageOnValueChangedFromDateTimePicker( onValueChanged: (TextFieldValue?) -> Unit, actionType: DateTimeActionType, datePickerState: DatePickerState, - timePickerState: TimePickerState + timePickerState: TimePickerState, ) { if (actionType != DateTimeActionType.DATE_TIME) { onValueChanged( TextFieldValue( getTime(timePickerState), - selection = TextRange(newValue?.text?.length ?: 0) - ) + selection = TextRange(newValue?.text?.length ?: 0), + ), ) } else { onValueChanged( TextFieldValue( getDate(datePickerState.selectedDateMillis) + getTime( - timePickerState - ), selection = TextRange(newValue?.text?.length ?: 0) - ) + timePickerState, + ), + selection = TextRange(newValue?.text?.length ?: 0), + ), ) } } @@ -859,14 +873,14 @@ private fun manageOnValueChangedFromDateTimePicker( @Suppress("deprecation") @Deprecated( "This function is deprecated and will be removed in the next release.", - replaceWith = ReplaceWith("provideDatePickerState(state: InputDateTimeState, data: InputDateTimeData)") + replaceWith = ReplaceWith("provideDatePickerState(state: InputDateTimeState, data: InputDateTimeData)"), ) @Composable @OptIn(ExperimentalMaterial3Api::class) private fun provideDatePickerState(uiModel: InputDateTimeModel): DatePickerState { return uiModel.inputTextFieldValue?.text?.takeIf { it.isNotEmpty() && - yearIsInRange(it, getDefaultFormat(uiModel.actionType), uiModel.yearRange) + yearIsInRange(it, getDefaultFormat(uiModel.actionType), uiModel.yearRange) }?.let { rememberDatePickerState( initialSelectedDateMillis = parseStringDateToMillis( @@ -894,7 +908,7 @@ fun datePickerColors(): DatePickerColors { @Deprecated( "This function is deprecated and will be removed in the near future", - replaceWith = ReplaceWith("parseStringDateToMillis(dateString: String, pattern: String)") + replaceWith = ReplaceWith("parseStringDateToMillis(dateString: String, pattern: String)"), ) private fun parseStringDateToMillis(dateString: String, pattern: String = "ddMMyyyy"): Long { val cal = Calendar.getInstance() @@ -937,7 +951,7 @@ fun formatStringToDate(dateString: String): String { return if (dateString.length == 8) { dateString.substring(0, 2) + "/" + dateString.substring( 2, - 4 + 4, ) + "/" + dateString.substring(4, 8) } else { dateString From 48121cdbc8d32b2c724b846a31f7c0aef700957c Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Wed, 23 Oct 2024 11:50:36 +0200 Subject: [PATCH 26/29] feat: Create release start job develop --- .github/workflows/release-start.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-start.yml b/.github/workflows/release-start.yml index 7e0124512..3e4038a70 100644 --- a/.github/workflows/release-start.yml +++ b/.github/workflows/release-start.yml @@ -42,7 +42,7 @@ jobs: run: git checkout -b release/${{ inputs.release_version_name }} - name: Run Python script to update release branch version - run: python scripts/updateVersionName.py ${{ inputs.release_version_name }} + run: python .github/workflows/scripts/updateVersionName.py ${{ inputs.release_version_name }} - name: Push run: | @@ -73,7 +73,7 @@ jobs: run: git checkout -b update_version_to${{ inputs.development_version_name }} - name: Run Python script to update base branch version - run: python scripts/updateVersionName.py ${{ inputs.development_version_name }} + run: python .github/workflows/scripts/updateVersionName.py ${{ inputs.development_version_name }} - name: Commit and Push Changes run: | From 4f3b1f3113c58317ad357ad451d2f8c55f1255fc Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Wed, 23 Oct 2024 11:10:07 +0000 Subject: [PATCH 27/29] Update version to 0.4.0-SNAPSHOT --- ...temColumnSnapshotTest_launchAvatarTest.png | Bin 33643 -> 130 bytes ...AgeSnapshotTest_launchInputAgeSnapshot.png | Bin 69894 -> 130 bytes ...enuItemSnapshotTest_launchMenuItemTest.png | Bin 24529 -> 130 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_ExpandableItemColumnSnapshotTest_launchAvatarTest.png b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_ExpandableItemColumnSnapshotTest_launchAvatarTest.png index 82d3bcdc181b005eb639f0c121b7d3f42dffac8f..19201b69d6b1b621995e246ab05dfbf22f857a55 100644 GIT binary patch literal 130 zcmWN{K@x)?3;@78uiyg}NCd>+K!SytwmJ=Z=BIJJo|XOHkoeQ zSti=wMvhA6a%%6arDgD8OspyF;2ycc&tVA`+V}K|-XvL8Kct(xr4GwP`pD z-&ddK{dunI`_ADXd#*L-n4{*s#vFHqs|~Pp`3R={d~@#bT{yv^kHuA-9T9TM=) z5)LvcT|xvS4l#Z&k@k9?O8MK~mprLTxv{{PZQpL%ey?OxQYJ{RJ2gL^WK#j&rC(Kq zX$7iv{hkKd)JyiG!Luiy=S_dge9DE2#*AcRsYK@fmgD>D`A1pQFuC0*+b=>SbV`?P zuQ0iONvgfQ8?_lK6YQc__39w50q>%EP%B)wF%^2iCa?Q{Ppt86EA9o72enG$^0T*P zIZ)9gqa;r?YNkTloQ4|h??It>5^|NutzWr*DV1SW@pn$sLLc_C4WcLNXlcNJLH)6l9 zn9D=<@tJfYCQ`_l9PD=muh4VU~6~>xyW= zi_d@1o%KfW?e1Yd!ItMrdcM1x#7BkV%l=3LjVcDI$%1q>Ctz$(%nvM|(iW48 z$3H9>{yPPV$|eeXC^-Es7Z&sa=Y_*?&hKeB+j1g)LB@P)0298dwpLF8G)qlb2u__T znf8aXSPpP!$y=uBr+)(Uu-^tm8>b{P@qtHx;bwc6odtEPn3Nok{3D*BZL#7PO>&8TX`eE-{v z?{eb&vpf)YNhtmjQf~YJ=c~?hGx<0v-rtXCj=I7P<5d}<4?_BBI7X=6MB9YHX*(WZ z7$sPJiCuXc7wVKZdHqA+2Lj59LE)s}RDgL{6f8>FO8BAOFYE^HF?P{*tdG!Pw zDH7RxeYjoqJ7bp#^}ir?si_#0@OSTI3r>YAY8-4-A(cLVbX#bi;NQ}q>qwkRfBS`V z_;oim*HI1_QuSL#M|pzs1I^qkSzePo=m)#7_1=(3rs;q@`KYwww@Cwl*++5i_Fu=U z1hcv2X?-r10J>3o9kb=-x85^WiBToCdkNkA_;)%5x2Zk#`l2A^V3zH7%wa2i2Sr=K z0o4|&DYP?LV04%Jyt(|2LCII<-C~S0%c2d|{e+TktBb6bbL;Dmw|LYpMGTn!y^G$` zm{ghd2XgR^T0!f6^Dzty2eaHKD?3vd=ZQn)E@s|+uXkvrps_aLcZ!^Yfy_eiDNt;P z*0kDPEtT!-pA_m55l~zOgp^(%MID^S!u(A8s$if9Prz>tzdAsp=1oA0TPty^xECvO ziAr?7MR@^69Yf!l=Bd!Of!TY6E$?qvN&OYSHrHpk!@u--X0jH8-`KM~;>+hZaHad&pUltKffZYqUb6^N1iKxZA3H*10!rM`T~D^vc=a#K z4lw(zdz)B)q&OXnNva&GQ0gFg_qJ))SdhB3vqA}Q!QUSKBLU2DAdok5d4A6%oSa~v zjTJ*zRPzPC7Z=KdQ3p2&ATG*ZAtX~G_{R@-b|lW}`_B})LQxKOhx95yZ9d%@;(`jz zvl1V}xzbO*ImrLfY{*&yC`0unj;OJo=Rj;>_qkyiew5N1xSZiy`4o2GwNZG1Z1b)_Pk*AJcW!W^a-T}|E^s9RbXA| z++}N{RsYuW8A%e7KTh|NKj-`>#v;nYq~`oAS>N|_25UwX%g?=XAA0VzEq^1|o~G-v zPVrFNyY}~Rc_(E3kGNVXBq~~cK@&=1!OEiTUc7V45wW}T1pdjJKp-|-8de3hROZOt zj3fBU_fLjme){!Y^)>(FLN;%~2Uq<%PtHIT-WIkY%e|-xojN_OUMIrN7uBz&;hqlV zS2>OdN;}GS(k-#BLE^s>Gs1<^^jL2-CP|(+yzf{3Zjw;_kh9B6OD1CHJ6rDym!l*h z;hV7*irSZdoC++RvW$B+19DPR$E+>pV62rf&e#`4N2=&63}5xhY)3CxY8!8^jSus8 zt|k%xt5)j4(}WB8O0tRYy$7?ZMSNpU zB`RO+)ve_J^>!mmy3Z2|FXI#s{2qOkP(}q5D_UPu$E>~hStq|`Yn<#qc}gi}ZI~sl zd|w5Zlm{Lg^Rc4Ob6bAp7V+z(q4Snv7{jCq?XP=1hNi4rP^S&i%+AFm8L-kIV7;ZR zm5-=KI5v~@9$buCARN;EIzFIe_R4`B^I`BPTSUE0VuwyoRr{*%o8BlG6U2p&wi7b& zt}xo5d#%5gg)9QE_CtT7bT?B9IPV`bOQ$@2XeLz+Dpyxg3`R#IPcr;SFLai#JbhRk zl|f#9MpUZC8Q||55;<$aMo%)ThU$=RQ+st3i6p9*oygkPxs2c)MnU_R@%*92SvvZs zDt(h~L;74TVM|2FZn>9h_}yfIzeoo^=sdZsHlS`<6qKDQVTHP1T9*Ay=Snf43_B=M z7P^ql#oMg`%-7GJQ7t*A6?U8JR?}4f zSv@j>o>&o@WXK5*^5&QFmF!bM;{mHD&IQ$?#!q2yeX6Quzyp*%XHZ z1`eUmGrp1Eubw57=#)%ye<0xMZgWn=hQynyeEtFclRCxBbI{dJwDdr0k=)dheoLR5 zE%n_IC$BmS3{~jQ4i+0N`@gr7yOcma!@BbbQ7rN%qk7`dQTxa!^2`6a-7}iO zdKbb4`c%qa9kE?FyHhEKj6qK~w^fhZTlf#Z4?6otjmx>O+5Jo3pB>_KhOigKvEchYE!A~$iGI7Y^?yT&(h%x7r@BP=jC{G8 zx7dX^4?S|0w36M~6-mhdqL=tV#ZS!*9EpEo7ELzgV0!#;Q8>UMIyyZd-PvsP0HfaA z&{(XI4#`qq=s{SEz3~gCpjMKU*^Mj;iXw&yhosub-Z(AvkaYi%I0UK( zoxVF*z?H|JpI7!@YXbO*>Q-(X1tx-@EXJ24&yUGj+^JUAxFasF6B!4QCE4k&FhNF*_-8CCTGX;BEk*3Gu+nsu>ab? znMtXN+gSV5hD|7T`)9!e->9kJq|vY250_dX=NBR4ps^0*!fV8%wVR>?L@YHwY?H+; z_fJ|<&W#BEI^Y~*^ZqnJfaLP;dGjr1Z6m<)KK|RhuPl=DwS1|tkUyFBv5r9ETEUzx z>yQn$Vp?<>(OLb=&S|q7hq#U3*z$X+d+_t8+_6ULcCDJBQiQ*O=%=}8y~hmi6@=g3 zfvx%9aMrQl2!A&h&t|x2Et4n0^$s6pXX{rn>>So<(zK@Ofg%F$enVN6YZPMZ>|rVx zrL&Xx4Nejn8Ct74eOl^dzMRo3vEc)c=?BC6^!as28DQ`DRa1tT3oXMSY`|I)`e;4K z%e-##cB}R-#TF=!l&VmDu`<~U#-}|jtFJNeeq(FHdz2&ZxNUbS5~AM$c^FRu%>^wN z0(?dBh2DLJNRR?n0X|%C06<9&z0Pf_kfGjz$%TqRW8YZs4Pn*qy>qzlqA^Q$ zIQR8cg|kYYrqi`Fx}CyqV8vwtz1Y+{MF13=`| z8R#S$Cxe6s^Sl$ir;CQA%R$DcESFIir|$~7Pn14L;l@xZS=9BeD_H(|J9eS)G%(Rw z*~@E&&JqSfM*`j_F9ct*L1K3g>MM4=O9p1+8NK`~$C!sKFNddj=^iQ4%5jy}sXLz8 zHlBLDfzx6G;q<1PX)u?EgItAKop@c~dSNk)9NrIi>&rRKmfEa-u7=O~&hlac?Bk_a zw4+(UonsL%f18`X(p+ACcyq=_+{h;FI*$Dv$1zQVHy^U?;&S-2tFlD!G$ z{Js>DniFvvngx?ixkLLj9&^~`)4u6Bm2_~+1&EKGW~94jH=jpIhZBXTGg~te-R5Sx&#oxW65|Pv5B~X%v&pmYlgmQf%*4#pE{EQWWO>>x)-|f&GE!Hz5AFWp*59gZMXt2RCzmnY?kF2YIoj zP~0d5R#;|Bqc}UoM+@^Q4^EYq+X>%h|G5jzX8Sb+*1ib&-d;>5_XRs7)=uQeke(p1 zxmG+XpUCgjF6Z3sC%c(&2x)Gj`M}oi~!U)&{DWl z=_K6GbVNLzLu7tG*!aedPSe7I87D1ncGi#c*$y%>`E$6G7#__e?e-4lWjiO@K5;es z8_eXqN0gkss=B%7+qgUh=CpmP@l+%cw^ERGi!fP>_wgXa>z%spmr~85m82%RGNq6h zm2qya@q1UTZE^$$O&6}5GDaqvC<7HWTuUdu(HEGna5gOgMX8r7j5(hij!o zD<(8#Wz*9NU|^fX@5~d0+9lP{4K|XRbv4x1%x0M^zDaeB$#;KO@u}Ba0^&9^0X`8l zDqejT1&ZTMAFa?U%yD>HbpGK#oaLAix!L!jrf1q;1@RgS z(f6U&yA;Au1ebzL1s`#&+m#rY=SB=$dwZOjHe-o?$Z^YvwaLA};;lK6yP%4Xe-;TY z-;Z@2_9)VP2~K8inY#xwd>>idt}#;bp8YXj0(7`Wo`CM>%UGuC%I@JobQlPF2f&52 zp${=(*CU>md+)TN2*ad}+S0^uZ^td2po(!Pe)+UKsW%8YIrLoZCPyxjA*XqraI(?< zR}cbTFT04z;OoKJ{!@Y&s^U7mM_Ms!- z8$543u!Nt`dY`^$7pkne1Z0^XwbO9_IFFxe&(b|ip$n+wDce7LS(x;xWovKALgFN8 zx~giKHaJ2n1k>C9vK0SOo^g46*eGz}xNwh5g{?G>R^WA-hzV1x&92WpbO_Swu^k8E zT;!@5Vr1w}iF`^eqj%utp+`*(&#ga_Dsc?zbmh%~?h6YJsAG$vlB24_Q)N^m>?ihFO_jbVjlf_X0#j^a%-6baxJFO8ExV>^87p^ny9S|lGfMrgjv z2LN^*o#8-T5;Fi|E&DVOqOor}Jrdu7fXgLGuI>1-~HGS39@ zb(na+^0sl|$w@}F!jYl)YhJ%sE6>*mBxMyDpN8;AI%nnT>FVbS3Y!wkk8QY*c?7;R z)bMJ?bA%in7+<=tf993-t?&+Ie5X^es}{?98365}GWsShOJV5b&M99?k^#Vy+AJVX zjitd+lBDhls||`My{Lnj#2LK6MR-gOFD1!o*ax zB1t+f5PDu%b?D}Xlvds1PTFX>Ro~o}t6#eAzysD-Fsqd>_2{djIU*nMYAc4laGW49 zuRJ-Rl(Vq+M9{5mM?P78Oa&ob^apXRAQRIYoBM+ncv)uLwAsT)qCM`OhBXho1 zA|_nQY^IT9m%YrnPjFy9-H^m`Esfl4B2^SH0Yyy z7%R7}ch+##Pc+9#9qb74tK+ z)lM!Fj?JCLi3xaUS4kUc`0W2Cj-*w@c=y&~^l-lmGd4;ePMnsVJ3+*Dk9=<8kcvUZ7NlUvvCpLL$G(*%rS{LzCJj*;~`Y z@BL1FN~gkXvP)5ZlhYLeiB|_D;j6i|GoL-e`Uu#Y*lMpfhh4!pIbLME({&!XLjFe(7IO`lsFqnz(zd-ZR^H1{jg{Z9K! zOItP8*!1&ECO=~xwzW3VmNs3+z!Ks&Ev2P9_0!m!&ReY0Q=ZjdmbdoJQUoxru98Cu zA3y1BAHGsD4!YGCjxx#{{t@Fkyd0#}ePY?Wx=MJlUsccVO{*N}JFeu)02;b^LZt>6 z`i&%I1CNP&bg97Up#jdcxXAnk_GsyFj%82jjd!?WX5)xc^CPCh%lW}mc}K^L*gfL< zovl<4=%jpW)+Y}{vWe=CT2f6No&K$pRJuLgras*dy=CQbIi>nR{ZaGkdaJiDc9vrC z_39lzJy{MmH9r<0BqS%!h6y%!q*jj7A6VWnKg0i#&&dx zw?948?~_2p-?Fz}hp?RlE@Y)Xx!!lWj5~h%T(dOJ1(D`|`a zHDHjl`08r$wyF6viGjcM=s?GH8(F1~ez(D9)Hq=Ky5hyA{B@wr8b52hwMl?ZVxmFN zgt7JX)P7C6-?6~_CGI#e&xD`a^=$4iW%_7_oMP-Og|B>u|0ec%gM!obezlhngMrr` zf;=-??(XwE;+TR*f6x02n(l(sgP)vd^u|U~ikKH+)3knD+Nfb2sU1yb?6NG3pBcST zOZpTl^6UG%*wn;)Ivp*oV!Gp5lKq6^flA-zg37h+$^2N2W4oeoWtqf6H;L3Dy$O=l z>F%4YL0bSWIAriHTqe<`BX$b)EoV)wbZ=GN!o`lq{(y}OJ{QDL z2R4r)PsH|sN0z%|Ar($I9LgP8*g$KxC%T>ZIVYJlt*m{UwY78#TPrbG`7FBl<;$2S z9lyin1=4rshZgdni6G-e#19Pt%#It?1!+>%rOVao+Fl?Eo8cWd#ZA9t?0iuZNS#H|FZ`o$e*cCGFs+=yjaqJtb{o2Jx5V!PJuo`*b!647VBya&ZaJK3t5-mD+ z9xZg;H_K#BL2wM*#Ki!hR*pvO{8)yL+Wmz%Di!lnZ6v4!U{v&MC9VU_iarVRt%_>o zz-=IIbW0jmZ-P0}KL$cYSdnET$4f^K9=ehXyQDa_N|EebD9wIL0QC({xqJ>q z;J6yO4}!4OgYuViZp7qtZX$dclWg)0$=ST0Hn=Z?(Pb}`=%RXO9-D@~y8^WgKy1I72mFr7(zrJYFzqz=~ zghI2rznE?$MY#4O2O%7-6G2Zo0^jbj+WCG%l6%JWl&akWs8t!J z^*)ZQ8Z~~4Bh5f>?u|_q1C(9lo*t8fB@~b_GUk%wP}U0n2cu759U7_^3d$1Sd8URTI*Ef z!a*GRmiGtSX{zNW8T7}^>M_&$tKs+@Va;MNz`B@BN{U0llN(jX1B;6>>&AX6B~9nX z^AV`ITKRCPQX~Jss{6G!Md?F1AIL?sLXkrO+zgfsi0(+vI#?gtnzsB&hhKQjM13dgRnsW!%;`S zfMa#7`1D)$gz8ZVVvDEj*KX59 zb#KeV;A3(SdpYTFv7x-cfZ%rQhIsPC@8|Bn}e zd!^Aq6x|)miGZJQQ&AAgpg{o*)idbT*~UdCx2X1VRq<8294l#$?;@)N@joTn4`9NiVnqjTW}XR2 z9=?gy_H=AnbQLnGnEi2}Zc@W<{*&pvs-Lo#ouQ(2^{M^4bptl_*r>+|rHgFEt$VD} z3D!2oHoqRM6_QJ~2d3$U0fpMoF#$gw7QIU8nW}x}d<`xZJA61Na5M6Sk(D{ot7V7E z;FR>xU{5Ng2SQ0OJjq+vZu^Abhi|C&DHzgSl{RL^6+ zXFY7ay~^-9-#TM*)!xv_(A$}WtH1`e z+n*GQZT69(tu94qfG7nD$P>H1{g<}s{0czD#-z%((tU?l|LwNp72Y1qr{809zuoDg zFzB*peg2_c$ZI5CM>N~zsDK4aAgX(Ggu@`II8!R3J|?@?MFNwDZ!!84ZhnM`y7mbL z?0gW!9+}NfiwGD;MR~2L7$lPB1Y}FUGYe6u1o?K&?&xJ__1Bix9T81@mdh@ILf-w@ z4+4nOl2>kvRc@_bWd0KcM>?NRh4+6Nwr+?>EfK^uZ$~BxsWjjwe;k75h=j+Jcx|js zm2{6pFbNh-WbaM|MPg-wWicO35^pcfQ3>B;44m=#XneD$BYeU?Xj~!FT5HwMc3!ki zncn;iKW6_ZzMHUJKjBj5!W`guk;fy*QB5a7RIa~Bt5W9;@}{zC|ye+$+K+DI)Oh})c?fgJ=K74_|oxupU$K;kd`mJUrT40W2f@T z22FHsVWL+EU-^Gssqe3wiU#UU@DOv=Ib4weIDZVien`n&DqaJ zDR^K+!~py;s*B`8q0}pThQ~l8Zte8xcci!azk0&BLV^WLj}dm7``Q2SK8k(3l?vU1 zo<&$m+h`m_-x2h#tNy3k#}2#TU}qzxWJGGUATzOi(e^APZ9Yo z;pg8aq5_o^M3h)G9Vetp3#k#TwkMf~td?cXN^4*OA)LxZFEfGNT)a;K+12g<0oD`v z45^~4hG8tbXs%=!%_t0Oyw~QmO*goOPa4wbk=uhSvV%f_Pbv7<2e_Ez1!DtVv+&&qrl15z;V=M8;|2G&zD? z6N^Yhc>#L9#tHu?EQcI^qie(BkN%N6$#7a_pkAkWSFh`lrwkKcw(3h14%^}pgxN`Y zte}IGirz*sUsy#UxI8q%&_>be9&cP7LDugJ#7PoA?v_e534qVFoR*2yY}%Wb$rB{A z&j<*fP<-kiL%b2r_%V-TCax_Ez$hcv*GUP*vtKG@EE@B$j4702tQ?I)93_pbeB->5 z382bJ%~HPDnU6;}(2@m*h|__KR=DAvLZQk~3FSN@7TSn~w7O}UOAPLWF*YSqTw&=! zr8|HplEtPr@cC2V*#XZgLOsypMBt5UlB=FwM=NP3=_6pG0 zwY!4>I}9eKKS9EZrjrug$rrzxb@aYk*m3dar;9EUTbq|gDU9-z8_f}id0u25IDCm0 z9j~!4i8Erb9S@Pa2Y$(Mg_e!>K3r9-8cW`FK9Vgpvr7eqzP|n#FcEwtqYV{dmO?qu zrH}Zk`pmy3KsIV?a@m?~Vu_LMB;Mil%l4~`uBY0_-(o-ldtzPpCMiU5-4`V28IY|! z;i5DO1ygKFRl5>r5cS_{ef(b-R{P&D*i$r#!*O zb;gQ{z)yKwsf?V$Z!9)K`S0h7zA{RwJC|UqEBLg&mNM9Au>w19m@MIUd_F?;VD}^5 zkEYe_lRoKlN;kAR=N}W(Z@hv)9XFcyiulfdM!hoBgEDnQH|b>m4m_07x^S0Lv})2U2s!@wL8XxXRLSERo757qg$fini2LW8wRD9J@V#9R)Skw{g`~?aMJk}R)6EAM@#{UP-5eL5pa ztzXwSvm}g`=%h^UuZct!r#%%hZA;cRzQ-x2iAB>z!;|un7t~zlqfAa+3UZRFDJ$9c zc%hf?8yLaeH_s|RblD@PIj&nYuj$i2M(`-nU)pX2AWyh9uu;7SIwnG^7(9XR6eR%6 zMn8D~-w}FIG+?~67zmLA10YvIpR~%MB2qs4(fqV2Ve4NssblKXalaCP`+D0^*i-4@A|D-ht#c7?;5<3&lpO->!)~tlo~PH6Y`p+ zNYqn2ZsB)hY_u6QinlCWN#J(IK_0!7HuP3d%29PZ-F6qEDe{<(0OzO8l6Jp z^Ig3>l_XY5CF^M`8yDXds7TJfB-d{y2e^ow!x1W%6avS3Y*i^oV7dt9TLK?eVSHy& zyZzOA_e@(|6|ym!`|DPfNb^XyIegH!81SRE9KB4w?YMlSk8S_6T-gO}5(^1_o>7YD z`s=qckTH|Aklj@5Ualmm{}4p*JrlpvE70#Il>m#0$}zZv#pbJgB#+Q<4}A1{wdbZ@ zVJ;El?ArTvREkn@Ih*XtCIu5r!xt#FxS6Vw{qc7iBXXM}!`pZpCIyYR44~7S%#A3c zC=zj*pjYvmVn_MSKmqLxIaBwqJpobML#cOAtKW?Q{ew=4pLO!?_Vfab=jnwKmm>?i zsjz1=rG7@I6ng#RXV2J87NhFjI0|MX%@#qPh8~Z0(i<5CKvwipzDI-%<1?R3|I31* z_5XJ4_5O(!9xu06(i;$DEHslvGW1%K<8B|LhNfOo+$oX?AbrJ@HqK$V>)ou+zhO2i zC|VISQOlfH9WlZloG^ zH|{JLX}5z@A^4@>&>jX5RB+i8FL_`7IIKE_ncX=Cjph`YnOCH28uo`_`yxId5UxKe zTNH@uT2{*^N}?`U|9(h!D&6!{_IZdvO#ZwJwzMw1iei9Gv610X{5ZF|h5>UmQ2Gf4 zi~jaulzy>PjWi{IlClF&bN11y{to|5WWoDQWU1_=x38th5i!-)9-k6Ly>ouEuab^$ zD%Idv+OeGxrb~w36M;Mwjfw4V0djZxtZCMpE`8TfZR2SMQBEXcc=5V;Z>(8|9M~F1TqG*No7%Ld@czg zpjQu$``tz+^?P2{(V5&$%`{WFF|T`mexSRiGg@^32ZpMo3{?)6~vO=9NBRZv>Q&*uKVAouiUG+u#tmuI&2QsjuECJ+t z>cJQ3!OqT8sc>2%nxsng>$eJ?&|-_n6LJ^Qsa~aIf?|a0u2I zTZ{q+XFT8eFKSngd!lLpE#zJWGtj%Y+QYWG$o-#gzI%FB+UU8^4;WUdw0(dcT<&8v zmSMiia^zXHpQRlIZq@IoFP!~YPc|M)%YlDWMxK7MAqq1nU-5pO#)xJ}9ua~g9xBBJ z987Gg8x>%ULpuvc!}>WPkjYKGTaBWqI(eBg?ZdE4Jp4i6w7R3FNNJyU93$P)Tn=#; zEjH_*iWzA`g)tiS+?>pLhUzrn|DmTGd>@dwqp?hJC@h$0v*_Rs#|3S{SlyQXhsL7d zrr?@;rGtCNVqp$n>(j0tYRHklBeEpA?OxX5{p|*wOR&-xf8W&dnL#XJ;;goWyx^ua zWBTyxia)E`WuU&mpYGy2IsGhKcXJP~MfmS$7qJI&JlBcJD8-pFQTT_Ip^*}Y8^6t? z*!Xz&U{To39X0EAc1))$z}6}x=fSk~KdrgW8Yw;C{im4}ln{caFAuNgB#fe7Q*uNd znQZ&cEHzJ;6P3gPcK$EfLn8(4&4|DX%0a6p!e_omNYC?IPzCavqvFmM#IQlvnu6KT zYsNA4Kb)G{Tz)f^WdFiA*_XGUm)4P8b8n;I(ajd8X$7$qh?A<2>?%PUYv@-2)<=KZ zu>tCfet%_9stH3l7s(K5>$K{U1Lk!MzKy?j`z+WC`1tq_U8gn|S|QWXa{MLu^wk3^ z%!}P6yv@p|Ereh{^;mTQ z~2Lw?b#5GV1IA;{_@cfZQ%uRvd1JdQZ>Z0=_H2kAJHX&^)_MsDD1A=8< zj;fB9fHE-%Vxm0bAj5`ew|@K@9rvF%<=u?Ta<6hSRg!vGTJ;TB=qrSUt-Nxyd%gVp zy{}Vt-1sPc8Yz) zF=~GOE4&HQAvd24YKl}`gR+tT^o);?!u`DpoOFAFJckqF>9-SSI$l{WoN&wz*TBA> z9H77iyH@*a1urwA4_V{0%#3FeyoZ2X`XA@E`2rq|ir7(6DAq`boLzJlq{}(YhX4YT zXBz(q*t+AN`8b%WeQhrcni1_DZAoSRt0;|}2y0V)zYY=pcKX&ZGcAeNSDAY)sLG;( zjjHNxI5?i{WPkvEXllHc;uypg-d0`tLAw|at(JTcT_?GL{0S`@!@-`21|2VhkC{&$ zF{olH_kgfLr}SDl|F7StP!9@rUv?==s3T5O@i$mtNhB@DHTjxbQPQ`06*_WLH!WlT zVrHyaUI#4Gk8Y3O1S-hf(U07<%1sK0$lfmvy=FK5+i4FFQiA4&C&x}SX)3<0p}}7G zIWfm)>RHi9ya0&8B|m%smd5|mSGM;F#&6tj6$#>s%(3Y6;3UnZiff%(J!@m}gEPt2 zTR&avt-Lc9$29Q(N=ah%)|Aa`mDBP+vv1ssDynwZ~TTm{LRkfD>uu&ELVuf)cHdR((-)8K(kH7P{dIArBGZ2yMQs(sp7z7 z+*gCw5uV@mI98RmR+ef1`o4J%|Fv9whTX)Qoi@gE{Pi*;IJr%JQ|}>*vQX1A+%%cZ zai;!E#(L^kom@P-m(DbQ->id6`4Bop<4ucx3I6en%k2aA^Mu1K(;GTng2l~vR*F)V zgtMYNGMc%+oBt_axvOs?2%348DnOv>YWzMUavnGYXIMuCz5L|I5Q|g)JSumT)Q2c) z7Y0$-&PZlicPI2$(;TfBh?C^^&hq>U;LU2T~+3H%aGe?8Uw`48bAp1mRa*+t$Y~J z=$6n_TP25Md4Z^2l6vjM00?~Oo|+#Z`mfWm0L(Gn(2H04EP)8|Doe?~ra{;1ug6Z0PS%yebg)U-I&Us{q_9+UbHWcaG^ZTD{}ZWwVpmrEW*>#a07xzX|4$S$fZxW znO*>(q`N{y$AkZpQfTUaJtnxk93(6_=VppKks}cH0M6b}Fp!Y|&L`6#cSO8zieL|x z3fRT&nV|c_3(I-K%G*wWnIiKWivfg@$o<=sV>tqK!HFK|(K8IiLTdrcYkRR0w)L4w zXvAMbDd#4`42tR2HcwKDXSw$?A2VURWN|w*ZUKv-Gd?PraRKHFzu#5gl-`@8K>nn8e2g+%*UDP-LXF>n0rSN+SsY#4?T zcecS5|6%jCZ0_d)CZDs|5PSv$tq%X;oCIHZ;NU&{BvYYax10Dj`ZF+R#{=7V63%x> zxh@5=E_c%ajih*qJ**za+0BoUc9X~5LI35ZRHRJjabxsB75_uWM(d05^f7!Jqoq#6 z?GCC(11{8Ek|p9zEm0oz=Hjm_0G}{KxPbau#q{EP&X>fp3z01f;$sh8mJBEI6`pN(6XQ=hpR z8(91}Kj}K&_=W>5{_z28ggP_4G>Omec31>AY`9HZ^n+L{tN{EOi!^o|k(|U_t9VEw zG5reAgxnBddpz^l$7O_S+8^BQ)EMQa+@oNdsuM<@a9=?MQqw1}1JnNA(=*O^SW6YY zGX7K6Vkio08ZWi@^!=fcbo{!P$LK<(FEJu6T8|pwdVu!dzaMufrOf}VzWDK0WT#9> zv}Y`;mX~ALmqE{qR$>3sha0zx?;ks?2_~RUChapWDFI_+yNB zYH|B6kP zjGQn(?~>?cQ1yPPrp{(quYRW)dtntc67`y8A~^LpE&zdAuJQITitKobkhISKZkOCUffUlk%sa0!u2!g%}(?Ch8;(v)3DMS1^I-1x1YC8POV%& z_!!1NExkx8=bpb@5cUwg-2cq3HIZ3(Kk+)WkLkd;X|*3UkeovC&DiYHdIZo(`xH2@ z55Ch6vdUw~pg2Xn9qk9m6?5M`8xtrvrRMzAT#r~5sWf=eVEP&nb!O#M3`5uh~VE)!NNKap-Ta7gdFKW3sSP+({E(QZKWtv{%u4uW!S0N_Q>&a~Jo>2ozJr0FE!7hqLA12+t-_&-m`+nec z&)pYNd}5Ikk$RoSr{eQi?+1;rkgK?pDa_%%uYZan&gv{c^FhB)0&);q7H(neaWZ0`4L^%;T-f&a_gM zIISGso_5*9TkT8V=so3>VQF2fEDr1+y+O3s3iDc3#Zz52Hod2xJ$vzzT@a}h=jaVp z5}1n2t^oH()kq#spyU_*{iLdAB=o0V2l3!jI-i+~NN~`E?I$*pEr;>0ym@Tm_!vxq z{Ewj#t7m3-7{GPmgpFv4zEqJ^K zQ)w2N2R|Kb87{=apZ?WG&{L8Dq>b#vH!^XwfnL`K8Mhc35f{SUGNzC1dy@X zgAT=)8*CyTUPIL8HA4#z95D|bzVZ1oN#H`?ZR$xx<&sIPvnh>q;U_)7Jalw(j)|ix zoDojQ+$oMqZ%c0;O0Q*Zk4TSbKa?+>)(}L z72qY@p@oFghDY6x=EBfTvYhfWPe=Ep3^?(=VTv_G{*rrDnDKHo=F%oMXRSzobMAs% zcC$7q+WnZvTxHPPbK-s*i#M?6*wCnq>-)brlstW(aPIQGjlgdPo_^5lp`APLj?M1Q zWq9qteyL+%a0~38m_Z-U$71%-EY|IxKs0jnYqqn3QJnJnbK~T@iJTkJ8{$KNzrgoY z-pc*-;Dz&fFI7r}h7S9`c2<_){~u=PovZ}7n)^dNh$8H z2MbhXYl`iT?w`5R`>`&Jrd^X#c{P{eRSVa3EBI=5D#z{;YL1aQ3cu{7VdHwO@x>I$ zPEA#^;qfUip0t^c+q2?-=`%6CBvB*zmGfyDM(!W6UtFTLTl4bYsPOspZnvnj*5Ilx zRQTfS%7E&F-8KBZYhFnOc4(aN3hn?QNj2wqbpq76y}9E@|Nc8*2phKDhRFzTpN-0@ z@WKj*|BLH0-*Y_6X!!X*hx!;k=UzV1H1q!T!C-Lc{Yyr5#Xm9Yg!t{|-oMB0Zrw`s z89nh)DDqREywGTY3(&De^!hQtdu6GFdz5FrfX?c?ZikpxT#|NjYl%H|T`3AE0=)hX zV1o1g_-q_Lsiby9EitW$ahR}5(fGp>Xnx>XiL1K8^k~9PdKI-J<=Sh%CvLShzZxB{ zcYX!h(A?C2y%nWdp=-uO4P1CKD2jLW>~_83San-zSL!qwMX600jK)0~I48y6>f_HY~RbQy5W zphuehATX9wjzD<$^cwH3*B+LU3E&M>pF_txI)j5gSU~#RD#6YbiHmK9hRQW~u0z7% zj7)ToItpc)&tbRde5tIiPNj%*6Y6&=^Bf!z`TYV2{f&u=HGG7@55|YTpCSO&t5Rr0A8Kxx4G-ISvp}UsS?>Vl#^$V-loBfAfn;^)dJo4W z-}t*o#xh+#xXrm17CPe)TJ9dsBGpEIJx_%59}}+tNQl-L;tXG?$Gb3G)_Na-4bNf& zG*m5s&6K@;yW)nwnm3sH%mNpSZ`+AM*SIMA_{{9+XP{-R3#63aE^G=$7+f?mu}M+X z4QFi972IVQnt=^V=0K&a5B2xV8XQd}`+q+?_THE1TpbzQTP(c8taPNnm-KHnZ-`b7 zkIj1cE)~v@-_PP<(MY2*}0Br^?9y2$$ zfDijgYbY936Q-`Y)W>8ZU!77Y76`fR@@D#Z;=1>5xv{c#*tU4^ru@kMo2%t+OX-7h zWyqd=NePxhkC9s#Vi#I8`cLS*at*=C_f?9{!w0-IJbt<_t{_V(mfxJBL(=Xj_;b7v(U}nPQ6l} z7ip})npQGz%;M%4sO}_4VIL1t}MnVlDGg zgoYsYSEBP|C^1X zi4Y>(sfu#Zc_K2oy})wa?RX+c+@V%pPL8Eyx)B$fXV(9{rnKMV;vNl9?7L;&`KNOI zsI;>;hCdpinGHJhN<>y`a`Qv6k9QPW0!G;8%GT$;0#RZHu3fIs!~FpmD*~x>u_Y2O z;FQm4HPZ6*2PV41jqp+ zRmrrq;z2nbc1}&L@huOJD&Fvla{Ke%(7EmgSJ(K`SOV-qnvADW(xuhH`h+Q|e_cI% z&c{`Xl4TgE7Tr|L<-(J_hfgNx^&$jJsgZLO%oX8u?>>p5vhL&iOFy#}@Ec4%g5kEc18 zyyL>IMq)s3%U0JD11mwcI{8HJXYJ*cl!rck7mT~kZ2o;+5~$ZnQMnpTIe=Dyr&eJhjzM2x=?f^}82y^Egl)CXV zJuNrNMEOa*L>FTj;m~uw-WhW#>Z@eY!7IL&)qGHwp0+RFtjQfl7$KmJ@tYL}*MY01 zEzAiVP4NLh6oNs%J+!cNp}(eSC?|I6whdvoWS2D*0Y?aDpq)E&#AB2;4OVl-I?Qig z+`}yR&s~6yZ&wr9Cy7@nZ{#ZOtsPhoHFo%~@OrpSkswirfl6#deeFvYYZk-d8{-(UX)pt7H zEOex(fBu(BKVjG$CHyh*W=`Xdj^oKw(7jz}A3gc&dc>#w>HF`9l0C?Yufv_DC#+3B5OosE%eR0&lq zFV8Y~EVor}k{RAzx!tTd>`ng7qHXRX42wqrD-p4I2+W3R)lUJ3CdB#%n$?Hh#;PuP@} z^z6 zc#CrG3gmeV8Sy%6ctk&r;+%>wFNs`^39ml$XD2XvX3F}&MNTD;sgQFW>e7^}mw8#^ z{{_#uxJVzmm*g+JG>W=-75h)h{HSp4@`^2SCWZdyLnP7X`R->)?Gw@Nq0A%e1|z{V zd7ysS3=V&FQKn#kJPA{A5+7`_x|Z55Vv$llf1jfxt!SUt@9n1=JpLZsxTvjg@Yz=? zOQ0zOSj_mVkIhcwD31&8l~tCnDakw37|xG^(P2+cMo5~u#!kBNCt{TBXKPXqA7!i-Iox~Qv6<2IhX$pkon z-KZ}~0Nw*paGi71+h z3Q_C4^;agn*Vy&x^W4_`K(2aE#%}xz=-?u`+e*2kou3_mlHWtl>CXV!*e@VWD$W4y zRojmN<@{o%(8BuO5%$m?3C7$r43D0s(ci_xrU{LNVvpsMNL9$r%$J5B#1k6C%P7O90`PjKuHM zsl6I_FFZuav#LOCfB2H2qm5Z`}?+C7c)mr1$;9I879wWM^h~XHNL8 z(2S)lD>rRBxTgxT1HjT3%jSG>uev?m05LxP?Hh%bHD)nB;*Iyps7Pt5Au5zfmm^{b;K7`i(Bl>?^C>(YXZgvF55BUd!KN;@_8qk4RZ95WR>4 zcDJVs_OeKR>0g@y#+zu|VPk+1Qu+&6r!&8QpJu(}UCOsKC1==G!cuGaB z;{YH+ApI(mTpcdLFZ7Ww_nSxZ`nIbFuM~_@;=CZITF+r{}M^oc884 zgi)(nEzoybv{a`;S=euLQ7Z~tT$0D4@Pn_XY=?`hT|+r}e`eqpGrP+#FYDlZd}<#I z$t-J#tNP@7=aOm{jMvYihvv7o0&e^6_I_ABLQ5SYEk`UT4D@-~**&63qr1-E!7C?UU#MP$R1Lhp5C)^*39){VG~o(laMFJC?C z86Ba$;Y~Q&@^R^N=*@}E_Y<431^b_-oWS!*q+GpzNayO=Q_Tl;bU5p8P4)HFe~UuD zYY zS5fX|ty6(1R5WwL+E}5Qsg=`F<+0iIGq8rp34&kqz%sOsC2B552i%A63d#sp8+qz7Qa)STVP$RF zVs75m*O;1tfSM|)VrJBSxmkBxkBANyO11+j_yqU;$&>Fzkw$>wn}L_=J3to z*xENceRF3ud&U}kwOu?-M$GMxhi8=BlucJAmg#J;eFc+#rsazKupWY#PAd0hR*Yy$ zM|MxkKc6sgZN_0*@ej=GMKD2@!eJKbhTzc2Jl0KSGu5sf(yWGikNonDK1(L&68)g| zFANEb-1#Rn;u?ch99ZO8MGsri%_nA0zQSo!_7%h{oWlE4C66APL-Qks6o>jeqVZoV zqb_0u1rrR;Wo!1;5?zDq0LIyHE8Cly6*$CZZIEF0PlZ2~_&Ha)Pj)hq4#~HJn`Z~; ztFT$+pvG7e?&Oscxj0F4`{MM9peFp-N#7&yawO~<2)}zVbqMQ{%NAs1(#4+MKl7Zg zJzX6VV=9&l!Cp7gPcjO-E8S{j*bdD7Q4eC;1CN3h|{1mr&GU?!BJDSVKA zW%}93#Bl3K;&XKk3Z$6ayY7z%%?|mXS!M!B8HwkW+m@0b;~IlsWpl%SozS9kZZbbq z`e&tIUo?Sszj;9xnge7F3=MCel>0gY=?^@jApQN!tf-)sutJ}&Z`-im>?7F{_*sqn zCYMPHhQ5m>xuNb2iX@?)gU+;ZAqPXbj@G651_rjk!lKUVrKcCF}VL(vfOLf0p}8B<}3BOStY+CMf!ChslM>?4XVCWU#>vZ7msAnL5H+db9FOq zkxh;nDR$BeD>r2+TzY$a8V-}}kN5S|xxKW_cC^E4AKh4rJ5=(R(#oV3(wi_@f$3iP z+s}*Dm71!B1;R~7q;VMN)G~Ma_dHiNUo^%t?UVtp+%0EuWAq1x6`cbLpv*-2wJbRL z8w6QLHE#}-8HuIaIb%cL6@iSSx?HN{z6#m(oggJOu{ENk@*G<#?8OSu1Lo?|S#n*i zo_LV`C2e)qZd5zQt4iLfx=ajt-vov+?b8fQhu{1Jj_!Cu@}KTGZqnWB>^mZUL`Gw# zX}8F6D9uM)1N~q+d#2f1RrahWEV|Bs({s<B)4Mz&4iLm}ibDlp-hKtZ4HP$Rs-UL#>PFursv#Qxfp?OZ%j& zo7EWwv53(d^KMK2K{g>&OxbwICDKKy%G3SL!0|5DA`9h^7d!Sh>F|{iC_R8Y>ls66 zI$oROzMTEr+}7u*rd??6db}^#Pb^*VpbI*kF_NPm{6|37EGdVsPjAl)0>;}#+Z0+F zu`wv!p_d7Nvbk??E{vG3|UXXGpkZ-W)J z$+cb`N%U;5Bw_l28(nW5y$f$ZUnUx5q3UXnWq!$aX$U-Mi$|e5) zaLp)Evv97gq$|6+E{+pgP$M5^|JB-_U3;I@F_eo~cGeAFTA;yyOh2H|)j|qEGjDqv z@P@bZkiRro1w>I!3qNyLKG?~b8WY?Ar)+RNzK*qV2IoHm+mmOWJ{~n9(IaJJ2o{Yd zj}4WO+;$>FYb_B=IG|IFSb6x8&I*xI(~rP6|18Bjir3J-6D_8XCX+LybheqG?X?RP zS~QH|^hO#vhQ94w4y$Qx8n1=(EMSz4#UUt*(HjE~=T#eJeR&TO&s}qeuQo(D=td+h zE^pMIg*X}#SllRGlujhYqNcyhy5(o-5H@)vLQF370Wgmj)+X^_?vn7(I+}JaG};U* zR;4Xs0Ykx-i|i}XWx=+ncVolAa8hnSyHDnn`nKA6z}N?@pw`U`-{$GjVpW>UV@-{? zZR%&ia>i23Tb)udTdD(%c%Rh#X_mM#+!|H7YsvJjC7D(XJK(9jNO<2rT3S*?i6+e1 z@Otm&)#oi@)l>}8Z#j_VcUc-a$BNMG>0%(??H2Xbne+iFu9>Vq`!WYR3lVNPQlhTD z)?AF--FBwA>Kniza2rnk4zB4vmzTKFM_9|62f8~|=EweOxNDCq2$*n~5!3 zJrMQQk-(9QRr6~XmV<^D>CxgBYDtEa_eIUTc;5;a*0SDU3z`;bqu!GOA1_bwIcAFu zZI`hxjL-w&09l2Qt*?a^P&WKHR#*gUk1antf=YPOzL?%>eo1;JG z52Y)KD~u2Mk2umeUB;vzv@bUOe&$zZ(r1&yuiJ)yLvS+ub25fQXax?FcFP~c0jpX! z#oI_$ehcVcIkWeT%hsp0`3tNa+&i&?o$GK;o}gCG2X<;HTTnDjL|%_xT*psEc|^_* z_w&ZP(&I89ja@cM&5b+UF|>ws>SlT%tmBD2KRR7fmzl;mBxa9hcnBH}1=MuGAK{Bw znf?aUme~CBqXr$kJE8WsS~dd;grHfRD)D?;<)D0U>*Pef;F(g72UZ?Ix;c`4;k@DG zzM8&rusSg=W7OG3uR>{B7TZz7_BLW7Ls7$1Nrwt5AtcPm#AbV)G6>%w{xE!Te6n)+ zJzG!Zr^k>08 zN>i6)3>R)NdoSvC*7nHdIW%Oczt#!W8UfmgBs{S;TQ_wN+`(add@v)Et3Q8wX$Fnk zaXxsO90{r~Ua4uGX)9?RclY4!64aH-czp}{`6Hy~uX1wK`k3Qh&ve5VDyu)tQ6+Gg z$6E=_3wBX~VzE2h&F=le3i+F81%GM>Wm!=q#?s`S?8vguD)}E$|H$z2+>ho5Yshi$ zdR{~GMT+2GawOsv0A+;?8;oFMGotfT>PFTVP7WYzGgyav+25Ui5OP9dIg>qOWbY( zV{2Nstyx3Mc`o{P$%W(hI*&(8m5`oDl_7!IkMvxcjhpp!RCKS6c@PC>W>pzkL*`_Y z6NJ1=UZAUPyG%xIN(~8=dVIIcNy-?5&1x;nqS0Z4`Xv~ab-ahmS?I9Dvc?$&&+)_s zPpbNGL%QZQ>21Gla0h2mT?)^i|F)cu}2@98c z8=LQ0w!Sv=I+W!1iS2~v8J0v)oe}D17nF2-dn;YmIbEscP1F)@B&WyR#DWM)7qPGi zDdAK5-1EN4T}#J&S#zv8Hd;LpE7xNg2>A;EIP!~iAXu(S%_ML{YXRHyZ$nt2NoATw@nbtf)HM{bVc`W+``(O$H|$7eX9de0>)b# zqNWx%v2&j@oUIR^UsRabbL|9hdiLhp))=|oG1%E-2um%wVUge*{GHXG{xjLsHgomC zQpBQ2sY_mUcATb|`VbFmEw-T-af~rDwPuy5y{k-kWx~rD@ z{0!t}@X$D+L%Rjgrlbxa@x2t%1c{4XawMsK8VJ4aaR!sLC?ktsr~sMjWTA^!nma%! z2efgY&?3s{7JgK1C`WDH+PCzheMU{&wl$L}2S<3=2ahZ{z|A*-&Bnj4-0GVhmb2f# zG^a&e30S;kL#f%A({@^mKm7Y@9kFVjfGxcti_0$S5HjWlH0Z9s&w`|-dW65;-XSEq z))o=ize+2DbEB40dM-2rgKWdebew?6S(?kE=*}_q%{zV2vzj$pu4;KXbvnRAMccOf zqDc;cCZ3C{pOJQ!zQg@J(OVB@uyoGshdU-OCds+m;|q`r`r3Df>!Wup0neaYf# z4^VBHUhJ8xQR-qntv`cUR=Q2rlQHyd&-MC@0Ard-hd0{QmCJ^M|A-7KD3AiuD}%E# zXJ?D^9;JtF7-;#b`gUtR8?`0^`Yn57Mq#csT&b#S?M&>uLMn3h@BFa}kWcHsckl2Q z+R>nnSvA5~(#l3D$bSE}D?ucox3;#fFG;_spM3gJ+Ft1L@}llWVw&2KZupD9_lX&n z5+y33yQ>tqOTGgUSv}O-tT5K0kuTRHg6d_iVP*3}%9Ehtd;Kr=Uicq8=*R>ZgOeS6HI%Ffg?I5|m4 zyL11SUR|<1S}NuT^Bf!CrjCJSfja>oP56%AZm}V8HP~Yd!F4qKi9*n3>h9oX; zV&~rl`yagX;pK2ve%ZY?hdo^w)mVX+@_G*Z{N9DsX3rW4o-h!@jy z-{CNeEn|mI|9qRm+5Ys0B;bNQa{A{Vn957o^(?yPGl2&Vq9i8Vou=ZZp*NzCNnAJs5Js+s3;hzw_xyd zWz$%Ml6wNz1lwGBCBR<$Mphg$)P+J=Xl|rA{0mInDPb6?v!MF%mol5T+WYj9m3>{} z*Wby872K~~%9_uFiDA7z=EY;8qgd=&g6$-`8#sp8_|-35k&Iue7DIY1p0KA^R?rn_ zIt%YVaxW~)jRV5f#)*4>8y^?}CoeK93OIwMSn@8Z`6_Dhsqj!gZ)|X=f43`R-om-J z%qehOV*%myK8%wTEUL9lQ{y?+f;HsV{5hJj)!)u@FT)t6t!B zsGG(@A>LHlixq_0saHd^iENJ48g zou#q)LRo{i)p?DD%>muD^MXo|bxj;`)bci@or0qJSrZr>I%3wa*?TlrW#E->(hj+o z=uM_Q{Eol(C277^7OmkkM$I}oTNa`CRxF3GdE<4)Buby%0J#x4q}~&!SnA-0#cV8! zDSOoUIJ1J0yP+_eQQhz{L?4r>1?q4XuMFlrn4*5p=Te`~^E}mB5Pc&x1;eTeE#Noqo6jeOw?MNVJV{#}udf3N|)7na-fZbgtbznYqL7b6p9q%78Jxp=O zU>z7HL?xehhh+=KwrM7x8Z#>mt1Vih=Kr=#g$KL6i;8BAwC!55`fac-Rpb2wbU&Oh zxM^qc5fGBUQxdOaSMGq%whkl$l@n;SSGu?z)@g`?`q==#C0;|6aFb$xSH>ER{S9XI z$YX2k5DCV$(Z9^CKykxhJgMNyDrB4Ix8N9G6To{K*%f3R-tpyynX}s6zVr+AF?h*c zby%O-Sl%u=zZ$ukT{aZR(qtF6@%n3`S`c}R`GHV&ZmV?=dvXVX$b*S_PVB}kXuvAK z+vD~0Snt@{>cB^(?#k7slNkQ~xU_-_@$`y;1Iv zt9yck-W_}2t~_F#>XlN1QVDB!x?fMWlqfx6FQVK=d@q;lRf8x+ET9=`B%#%>HVT2= z3_NPx4i*4+)!)C1LKBAc=>8+5s{A~Bn=rHl1fzh<(5wzL(J?Q4`Sbm|99Q0UuX7Hi zl*BGZklbCMGu4yPpxr^-y~!VMo^Tg)ZahBXd;e&0t?@7UamUWX5OR zrSXVz!>R=K2;<}(6Yq|pgP8-4M;3#B6g8u6)7qk#)RV#H2F9$<>;uwY``6xuhJi^2!zuAst6a}zLi4Z&T7|aF*b4Xvmis-pj@Y=0FM)FRbyRk zn!{l|wwrsj`jY%~JQ%PH5Yqy$AP(CkZNM&kMZ0Z;SaZV-E7u9$o&Pb9PKphG(7ooS z)uwv|j!pFmvIr^Ob>+?fKw~M(G0>l^O-HQo-EgpOhX4=4iIV%>;1=PCx|O`8HnS5s zI~wS$6lWIV%dvZ~{V^rjtlO_Kws&=el9(q`;p^`ZRVvg_BeT07o0Qfu9S9i zd`wU&sKks%DRz0%v7%-?rnkfxEa!!NtN5a~Lm#Jy0AZ754KPX{v{!~)qvaFsyjFq# zkuXQ!ixhPFD>Z#~jx6!Jh&Ty!-vqT7Ww7(D!)lejHJ7|Bd>dV%D{J0>Lr|rnM`{3z z1PL-ctibc$5>gd;vASFOdC#ux`^n{h+Lr5r72PnyFXkBp!26E`&h!X!2O9d&eyALO?sq*3nJdcF)>!U!-@u!hxpBql$OcS+TsejI%j~ z+`jy&m$7oAT3JD2yDF(bZmlt3SY4{$`E2n0Ae%W{B&?D-Vv|i3V*F0tOgd*+1F~-u z{z{*XdEqCXUWbKc1(rbB41QehVbJpr(@#$cPfq(YLx&+I!GXeGwSuy>7ILEOAV3P1 zm}@7M3GoigqQLb3l#_HT1hp0_k&KK-!Lr>yeEWt;FIM95!;Ymi{5#hc-{Q^m9Ue2= zU2+h)`WXmKo@EW(Te6XhNGO*dEa7q2-E(#vnAsxHy<_d#@v{9ns;HmGs5e(3h*eW` zuUKtvzdXru3No(Mvs))m3!(yDbG_H|I&!r?M?bVv(iZmGO}~>7d1n|qUxS*#&?CV| zO7-Rb5N)uIoT(^>F=FHD)kG((KjV7$CCwtDJAXPQv~qPBIn`A1U0tnLi{IzI20FfS zR6P@OIt|D+7!KwxO3M~yLUspm2S&oV!)40HTWT0fXlJbscht<5Z;rY*k*o@6ptqbC%MoAX!7~4o{ATKtz~3v)F5F|>O&I*{jUY^uER|P#iGfHa5IkQ zcxtT4F50l6vgxS|cr;}WBFi80uUg>$zgfN@hgHdVrTR-VNjlqYW4UA4lkVNpXE!(d zZ|K|M4)dnhv`MV|#gMK8nLuaQ!|N4>vA!#nE?0hhW1c5@U(bNh>#PRUoV3HUQIZR^ zr6wWCqC)p4qo|2gI@fu`p~FKLzpb*uo+YYAx`&|@%S>A_`};EHm48U*o($rtKDgvf zG|cZ%PY2wacsbV%=cKxs!rcRk=r6`fHC=HJMRY}zm%~Yi)q8rRWz)r7^`;ryKX091 zE*tayoKeEi7z1zbe^NBpVgqx!-zF5>T5_u{checAvoNY4a(@aD3&|0%#|pm*-U-+; zIr#HWI)`(mzgH8Zg2S8(+;4dd+S9D@+2WtamyrD7_PWoLw)69ht;vdUVvSp)g8{mpwIrj`>`G;(ocx7Kw zz@ZA2bOA8djQ%l}B=#}YXUZ46tJ;gqpyw}4EQSg_l=?~+J-}XkB8nTcFlhL2@!-#u z3=Z>?m`8XK$kdFhjUg3a8V_FiQ7hiZjcny%YB=%a-CXajf+xLhLYF>XOXmb`w2bk@ zUP4C)3PBwyyVSLwo*!2YR%4oBKhGZd_^68uVh@S7@Nd+`+1ebB|KG^L_uh5E=46z- zcp>uK%uvqiB5C9a19NDQ16EQp^jxVTX?^G>4ujPFgPY6V`(P3@87jt)YbtVy zKZdBYi(?obm6DV^tv-iaw;!~(f*xNVkx(X$;xvqJOskkjl`XzCvcsmH^-ri%Bpr)2 z)X(LX1)V&JSus;?kllx?8ULI}klpvn2pXOMdkr!*c+%*GP4a*FqVreAk_L8?yilM` zQ6wSHCN+U5zeUPGl=Mfn*BSVw%Eo1taiNo`Mg{A(QPAZd3x9AfYTY6*D3TgNs^6An z9YHqM5$7c(B@Zdjy%B;$WFTI5s0+RYnVt`X%+#ha@Y-!a*cK-lmZieS{zK)oh$fN)H_fLA$})|Kwmb3b4h+Mn-EQTVqgT* zq%o$kWxUbrf}X11fD z!HvIi%!{ScQsJ1h@6OwGBcl4a60}8He^9ZvUpK4Cu&6ggo<#n=kmZZNYN~9mo!P|@{L3={$KkI$Bg(NnCI_z>6yRLCkXYa&s zybO!2li+(@F?gCFHkYJ8Nk?v!s&q7fyo$-do7lcXNe=l^nV0w$EQm(Wvu3{6d2c#q zbI1!3RKJZ1P~4f>?^8Ka|IFT$>Tlyd=X>ZxR5!Wcvecp?7?2_S6(`I&D_nNP^m0G) zQMl=AJ{2d9_1uOPQK6$k#mjQ8Tf_MU^z6-pNBF#kma~wXVW#LQ+wAw?y~oAN^h_^*A9yLmXU@{~@v~iOQP{+* zco8kscjQ$=5lG~t{U5pK%PyJ9_Z*Pxz497(WA{RfUR;)+GfIl@Y6VSLDsy@^bLP<* zh{ywIcX3V1 z9IZqD9@UdyDH$?+BJsdQ$PUYQ>n3#nRSBE`-?RVT`0slB-z5$m-LAEKdANR=kfmn^j^c5 diff --git a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_InputAgeSnapshotTest_launchInputAgeSnapshot.png b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_InputAgeSnapshotTest_launchInputAgeSnapshot.png index 4b2f2f983aecf073d38d2dbebee5455b88eb0df8..3f72419b65a00a5e1793e4a467a1235a33aedb24 100644 GIT binary patch literal 130 zcmWN?%MrpL5CG77s-OV_%V&Xf!vZVJsAL3luzG!$ch&do<0aZ!=RBmm_jP;l-2b-^ z?Rh-qJZqL0Y@-)3Tha~>TS6eEkqzpc456l|JbSW^3UYN|nL(9U!Rt&eSzt{nvta`P N-8TET3XCDz?Fa1eC!GKQ literal 69894 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 B;tl`+ diff --git a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_MenuItemSnapshotTest_launchMenuItemTest.png b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_MenuItemSnapshotTest_launchMenuItemTest.png index 39faab564c544d23d0186ca81013100aa20d5967..1ba3ca99c5a85247efd8cafcd1832ee3ae1df51b 100644 GIT binary patch literal 130 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 Kvi6+!o&NzP!7EV! From 60cf6396f60edccf5a11fb11ea59a4210e2f0a40 Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 12 Nov 2024 12:39:05 +0100 Subject: [PATCH 28/29] fix: [ANDROAPP-6622] add progress to location search (#325) Signed-off-by: Pablo Pajuelo Cabezas --- .../location/LocationSearchBarScreen.kt | 34 +++++++++++++---- .../component/LocationSearchBar.kt | 37 +++++++++++++++++++ .../resources/values-es/strings.xml | 1 + .../commonMain/resources/values/strings.xml | 1 + 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/location/LocationSearchBarScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/location/LocationSearchBarScreen.kt index 7c265995a..968898393 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/location/LocationSearchBarScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/location/LocationSearchBarScreen.kt @@ -8,11 +8,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.hisp.dhis.common.screens.components.GroupComponentDropDown import org.hisp.dhis.mobile.ui.designsystem.component.DropdownItem import org.hisp.dhis.mobile.ui.designsystem.component.LocationBar @@ -37,6 +40,8 @@ fun LocationSearchBarScreen( screenDropdownItemList.add(DropdownItem(it.label)) } + val scope = rememberCoroutineScope() + GroupComponentDropDown( dropdownItems = screenDropdownItemList.toList(), onItemSelected = { @@ -52,6 +57,8 @@ fun LocationSearchBarScreen( when (currentScreen.value) { LocationSearchBarOptions.DEFAULT_BEHAVIOUR -> { + var searching by remember { mutableStateOf(false) } + Box( modifier = Modifier.fillMaxSize() .background(Color.White) @@ -63,18 +70,26 @@ fun LocationSearchBarScreen( onBackClicked = {}, onClearLocation = {}, onSearchLocation = { locationQuery -> - onSearchLocation(locationQuery) { - itemList = - it.takeIf { locationQuery.isNotBlank() } ?: defaultLocationItems + searching = true + scope.launch { + delay(3000) + onSearchLocation(locationQuery) { + itemList = + it.takeIf { locationQuery.isNotBlank() } ?: defaultLocationItems + searching = false + } } }, onLocationSelected = { locationItemModel -> }, + searching = searching, ) } } LocationSearchBarOptions.AUTOSELECT_ON_ONE_ITEM_FOUND -> { + var searching by remember { mutableStateOf(false) } + Box( modifier = Modifier.fillMaxSize() .background(Color.White) @@ -87,14 +102,19 @@ fun LocationSearchBarScreen( onBackClicked = {}, onClearLocation = {}, onSearchLocation = { locationQuery -> - onSearchLocation(locationQuery) { - itemList = - it.take(1).takeIf { locationQuery.isNotBlank() } - ?: defaultLocationItems + searching = true + scope.launch { + onSearchLocation(locationQuery) { + itemList = + it.take(1).takeIf { locationQuery.isNotBlank() } + ?: defaultLocationItems + searching = false + } } }, onLocationSelected = { locationItemModel -> }, + searching = searching, ) } } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBar.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBar.kt index b070ab29b..a696d588f 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBar.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBar.kt @@ -66,6 +66,11 @@ enum class SearchBarMode { SEARCH, } +/** + * DHIS2 Location Bar search actions + * Default: The user will need to select a result to mark it as selected + * OnOneItemSelect: If only one item is available, it will be selected automatically + * */ enum class OnSearchAction { Default, OnOneItemSelect, @@ -75,6 +80,8 @@ enum class OnSearchAction { * DHIS2 Location Bar. * @param currentResults: the available location items to display before/after search. * @param mode: the initial mode for the composable. + * @param searchAction: How the search result selection is carried out. + * @param searching: whether the search is currently in progress. * @param onBackClicked: callback for when the back button is clicked. * @param onClearLocation: callback for when the clear location button is clicked. * @param onSearchLocation: callback for when the search location button is clicked. @@ -86,6 +93,7 @@ fun LocationBar( currentResults: List, mode: SearchBarMode = SearchBarMode.BUTTON, searchAction: OnSearchAction = OnSearchAction.Default, + searching: Boolean, onBackClicked: () -> Unit, onClearLocation: () -> Unit, onSearchLocation: (query: String) -> Unit, @@ -129,6 +137,7 @@ fun LocationBar( SearchBarMode.SEARCH -> LocationSearchBar( currentSearch = currentSearch, currentResults = currentResults, + activeSearching = searching, onSearchChanged = { currentSearch = it onSearchLocation(currentSearch) @@ -212,6 +221,7 @@ private fun LocationSearchBarButton( private fun LocationSearchBar( currentSearch: String = "", currentResults: List, + activeSearching: Boolean, onSearchChanged: (String) -> Unit, onSearch: (String) -> Unit, onBackClicked: () -> Unit, @@ -221,6 +231,7 @@ private fun LocationSearchBar( val focusRequester = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current var needsToFocus by remember { mutableStateOf(true) } + var searching by remember(currentSearch) { mutableStateOf(false) } Column( modifier = Modifier.fillMaxSize(), @@ -259,6 +270,11 @@ private fun LocationSearchBar( LazyColumn(modifier = Modifier.fillMaxWidth(), state = scrollState) { when { + activeSearching -> + item { + SearchProgressMessage() + } + currentResults.isNotEmpty() -> itemsIndexed(items = currentResults) { index, locationItemModel -> SearchResultLocationItem( @@ -431,3 +447,24 @@ private fun NoResultsMessage(isSearching: Boolean) { ) } } + +@Composable +private fun SearchProgressMessage() { + val message = provideStringResource("searching_location") + + Column( + modifier = Modifier + .testTag("SEARCHING_LOCATION") + .fillMaxWidth() + .padding(vertical = 64.dp), + verticalArrangement = spacedBy(16.dp), + horizontalAlignment = CenterHorizontally, + ) { + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR) + Text( + text = message, + style = MaterialTheme.typography.bodyLarge, + color = TextColor.OnSurfaceVariant, + ) + } +} diff --git a/designsystem/src/commonMain/resources/values-es/strings.xml b/designsystem/src/commonMain/resources/values-es/strings.xml index a0b90142e..e347b3ed5 100644 --- a/designsystem/src/commonMain/resources/values-es/strings.xml +++ b/designsystem/src/commonMain/resources/values-es/strings.xml @@ -46,4 +46,5 @@ Seleccionar en el mapa No hay resultados recientes No hay resultados + Buscando... diff --git a/designsystem/src/commonMain/resources/values/strings.xml b/designsystem/src/commonMain/resources/values/strings.xml index b012b6a6b..523a10f8c 100644 --- a/designsystem/src/commonMain/resources/values/strings.xml +++ b/designsystem/src/commonMain/resources/values/strings.xml @@ -46,4 +46,5 @@ Select in map No recent results No results + Searching... From 0ce9216b0e4b01a33740c8d94bb4f2dca60f0737 Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Tue, 19 Nov 2024 11:18:36 +0100 Subject: [PATCH 29/29] fix: update Location bar component tests and update CI (#329) --- .github/workflows/github-ci.yml | 3 ++- .../mobile/ui/designsystem/LocationBarButtonSnapshotTest.kt | 1 + .../mobile/ui/designsystem/LocationBarSearchSnapshotTest.kt | 1 + .../ui/designsystem/component/LocationSearchBarTest.kt | 5 +++++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/github-ci.yml b/.github/workflows/github-ci.yml index 744f07163..437002b7f 100644 --- a/.github/workflows/github-ci.yml +++ b/.github/workflows/github-ci.yml @@ -9,11 +9,12 @@ on: branches: - main - develop + - release/* pull_request: branches: - main - develop - + - release/* # Allows you to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarButtonSnapshotTest.kt b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarButtonSnapshotTest.kt index 284e76e84..65e221aa2 100644 --- a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarButtonSnapshotTest.kt +++ b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarButtonSnapshotTest.kt @@ -17,6 +17,7 @@ class LocationBarButtonSnapshotTest { onClearLocation = {}, onSearchLocation = {}, onLocationSelected = {}, + searching = false, ) } } diff --git a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarSearchSnapshotTest.kt b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarSearchSnapshotTest.kt index 39604f572..ccdb47055 100644 --- a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarSearchSnapshotTest.kt +++ b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/LocationBarSearchSnapshotTest.kt @@ -33,6 +33,7 @@ class LocationBarSearchSnapshotTest { onClearLocation = {}, onSearchLocation = {}, onLocationSelected = {}, + searching = false, ) } } diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBarTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBarTest.kt index 0076c3bd5..0069e0b5b 100644 --- a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBarTest.kt +++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/LocationSearchBarTest.kt @@ -30,6 +30,7 @@ class LocationSearchBarTest { onClearLocation = {}, onSearchLocation = {}, onLocationSelected = {}, + searching = false, ) } @@ -60,6 +61,7 @@ class LocationSearchBarTest { onClearLocation = {}, onSearchLocation = {}, onLocationSelected = {}, + searching = false, ) } @@ -97,6 +99,7 @@ class LocationSearchBarTest { items = listOf() }, onLocationSelected = {}, + searching = false, ) } @@ -144,6 +147,7 @@ class LocationSearchBarTest { ) }, onLocationSelected = {}, + searching = false, ) } @@ -192,6 +196,7 @@ class LocationSearchBarTest { ) }, onLocationSelected = {}, + searching = false, ) }