From d8a9f5ae226ed8e2f1f99195100936d490ee558a Mon Sep 17 00:00:00 2001 From: Shashwata Roy <92641125+RoyCoding8@users.noreply.github.com> Date: Fri, 30 Oct 2026 21:06:45 -0400 Subject: [PATCH] fix: expand expense members --- app/build.gradle.kts | 3 +- .../splitsmart/ui/screens/ExpenseEditor.kt | 117 +++++++++++++----- .../ui/screens/ExpenseMembersPickerTest.kt | 39 ++++++ gradle/libs.versions.toml | 2 + 4 files changed, 128 insertions(+), 33 deletions(-) create mode 100644 app/src/test/java/com/splitsmart/ui/screens/ExpenseMembersPickerTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a00a91a..e94912f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(libs.glance.appwidget); implementation(libs.glance.material3); implementation(libs.coil.compose) testImplementation(libs.junit5); testImplementation(libs.junit.vintage); testImplementation(libs.coroutines.test); testImplementation(libs.turbine) testImplementation(libs.truth); testImplementation(libs.robolectric); testImplementation(libs.room.testing) - testImplementation(libs.test.core) + testImplementation(libs.test.core); testImplementation(libs.compose.ui.test.junit4) + debugImplementation(libs.compose.ui.test.manifest) } tasks.withType { useJUnitPlatform() } diff --git a/app/src/main/java/com/splitsmart/ui/screens/ExpenseEditor.kt b/app/src/main/java/com/splitsmart/ui/screens/ExpenseEditor.kt index 6fb14e5..567a21f 100644 --- a/app/src/main/java/com/splitsmart/ui/screens/ExpenseEditor.kt +++ b/app/src/main/java/com/splitsmart/ui/screens/ExpenseEditor.kt @@ -7,6 +7,8 @@ import android.provider.MediaStore import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.OpenInFull import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -147,6 +149,7 @@ private class LongMapField( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ExpenseEditorScreen( gid: Long, @@ -591,41 +594,13 @@ fun ExpenseEditorScreen( label = { Text(t.name.lowercase().replaceFirstChar { it.uppercase() }) }) } } + ExpenseMembersPicker(rows, itemized, type, preview, cur) { touch("rows") } LazyColumn(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { items(rows, key = { it.id }) { r -> - // In itemized mode the per-row number is the participant's own item - // subtotal; outside it, a percent or shares split enters its own basis. - // Same field, so one branch picks the label. - val rowLabel = - when { - itemized -> "items" - type == SplitType.PERCENT -> "%" - type != SplitType.EQUAL -> "amt" - else -> null + ExpenseMemberRow( + r, itemized, type, preview?.get(r.id)?.let { money(it, cur) } ?: "") { + touch("rows") } - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Checkbox( - r.included, - { - r.included = it - touch("rows") - }) - Text(r.name, modifier = Modifier.weight(1f).padding(top = 12.dp)) - if (rowLabel != null) - OutlinedTextField( - r.text, - { - r.text = it - touch("rows") - }, - label = { Text(rowLabel) }, - singleLine = true, - modifier = Modifier.width(110.dp)) - else - Text( - preview?.get(r.id)?.let { money(it, cur) } ?: "", - modifier = Modifier.padding(top = 12.dp)) - } } } // In itemized mode the check keys off the rows having entries at all, not off @@ -720,3 +695,81 @@ fun ExpenseEditorScreen( } } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun ExpenseMembersPicker( + rows: List, + itemized: Boolean, + type: SplitType, + preview: Map?, + currency: String, + onChanged: () -> Unit +) { + var expanded by rememberSaveable { mutableStateOf(false) } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween) { + Text("Members", style = MaterialTheme.typography.labelLarge) + IconButton(onClick = { expanded = true }) { + Icon(Icons.Default.OpenInFull, contentDescription = "Expand members") + } + } + if (expanded) { + ModalBottomSheet( + onDismissRequest = { expanded = false }, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)) { + Column( + Modifier.fillMaxWidth().fillMaxHeight(0.9f).padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween) { + Text("Split members", style = MaterialTheme.typography.titleLarge) + TextButton(onClick = { expanded = false }) { Text("Done") } + } + LazyColumn( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp)) { + items(rows, key = { it.id }) { r -> + ExpenseMemberRow( + r, itemized, type, preview?.get(r.id)?.let { money(it, currency) } ?: "") { + onChanged() + } + } + } + } + } + } +} + +@Composable +private fun ExpenseMemberRow( + row: SplitRow, + itemized: Boolean, + type: SplitType, + previewAmount: String, + onChanged: () -> Unit +) { + val rowLabel = + when { + itemized -> "items" + type == SplitType.PERCENT -> "%" + type != SplitType.EQUAL -> "amt" + else -> null + } + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Checkbox(row.included, { row.included = it; onChanged() }) + Text(row.name, modifier = Modifier.weight(1f).padding(top = 12.dp)) + if (rowLabel != null) + OutlinedTextField( + row.text, + { row.text = it; onChanged() }, + label = { Text(rowLabel) }, + singleLine = true, + modifier = Modifier.width(110.dp)) + else Text(previewAmount, modifier = Modifier.padding(top = 12.dp)) + } +} diff --git a/app/src/test/java/com/splitsmart/ui/screens/ExpenseMembersPickerTest.kt b/app/src/test/java/com/splitsmart/ui/screens/ExpenseMembersPickerTest.kt new file mode 100644 index 0000000..5530784 --- /dev/null +++ b/app/src/test/java/com/splitsmart/ui/screens/ExpenseMembersPickerTest.kt @@ -0,0 +1,39 @@ +package com.splitsmart.ui.screens + +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasScrollToIndexAction +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToIndex +import com.splitsmart.data.SplitType +import com.splitsmart.ui.vm.SplitRow +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class ExpenseMembersPickerTest { + @get:Rule val compose = createComposeRule() + + @Test + fun expandedPickerScrollsThroughAllMembers() { + val members = (1..30).map { SplitRow(it.toLong(), "Member $it") } + compose.setContent { + MaterialTheme { + ExpenseMembersPicker(members, false, SplitType.EQUAL, null, "USD") {} + } + } + + compose.onNodeWithContentDescription("Expand members").performClick() + compose.onNodeWithText("Split members").assertIsDisplayed() + compose.onNode(hasScrollToIndexAction()).performScrollToIndex(29) + compose.onNodeWithText("Member 30").assertIsDisplayed() + compose.onNodeWithText("Done").performClick() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 397e10e..5ae279b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,8 @@ compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeB compose-ui = { module = "androidx.compose.ui:ui" } compose-material3 = { module = "androidx.compose.material3:material3" } compose-icons = { module = "androidx.compose.material:material-icons-extended" } +compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } window-size = { module = "androidx.compose.material3:material3-window-size-class" } navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" } hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }