From 332f05f16543a4a95ac1befa290858420bc9db31 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Wed, 11 Dec 2024 19:24:33 +0100 Subject: [PATCH 1/5] add result grouping --- .../probe/ui/result/ResultMeasurementCell.kt | 131 ++++++++++++------ .../org/ooni/probe/ui/result/ResultScreen.kt | 65 +++++---- .../ooni/probe/ui/result/ResultViewModel.kt | 58 +++++++- 3 files changed, 183 insertions(+), 71 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt index 829d52ab..3fd4281d 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt @@ -1,12 +1,14 @@ package org.ooni.probe.ui.result import androidx.compose.foundation.clickable +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.size import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -17,6 +19,8 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import ooniprobe.composeapp.generated.resources.Common_Collapse +import ooniprobe.composeapp.generated.resources.Common_Expand import ooniprobe.composeapp.generated.resources.Measurements_Anomaly import ooniprobe.composeapp.generated.resources.Measurements_Failed import ooniprobe.composeapp.generated.resources.Measurements_Ok @@ -24,6 +28,8 @@ import ooniprobe.composeapp.generated.resources.Modal_UploadFailed_Title import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.Snackbar_ResultsNotUploaded_Text import ooniprobe.composeapp.generated.resources.ic_cloud_off +import ooniprobe.composeapp.generated.resources.ic_keyboard_arrow_down +import ooniprobe.composeapp.generated.resources.ic_keyboard_arrow_up import ooniprobe.composeapp.generated.resources.ic_measurement_anomaly import ooniprobe.composeapp.generated.resources.ic_measurement_failed import ooniprobe.composeapp.generated.resources.ic_measurement_ok @@ -43,44 +49,15 @@ fun ResultMeasurementCell( val test = measurement.test Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .run { - if (measurement.isDone && !measurement.isMissingUpload) { - clickable { onClick(measurement.reportId!!, item.url?.url) } - } else { - alpha(0.66f) - } + modifier = Modifier.fillMaxWidth().run { + if (measurement.isDone && !measurement.isMissingUpload) { + clickable { onClick(measurement.reportId!!, item.url?.url) } + } else { + alpha(0.66f) } - .padding(16.dp), + }.padding(16.dp), ) { - val iconResource = when { - test == TestType.WebConnectivity && item.url != null -> item.url.category.icon - else -> test.iconRes - } - - val contentDescription = item.url?.category?.title?.let { stringResource(it) } ?: "" - iconResource?.let { resource -> - Icon( - painterResource(resource), - contentDescription = contentDescription, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp), - ) - } - Text( - text = if (test == TestType.WebConnectivity && item.url != null) { - item.url.url - } else if (test is TestType.Experimental) { - test.name - } else { - stringResource(test.labelRes) - }, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f).padding(end = 8.dp), - ) + TestName(test, item, modifier = Modifier.weight(1f)) if (isResultDone && measurement.isDoneAndMissingUpload) { Icon( painterResource(Res.drawable.ic_cloud_off), @@ -117,16 +94,86 @@ fun ResultMeasurementCell( }, ), tint = Color.Unspecified, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp), + modifier = Modifier.padding(end = 16.dp).size(24.dp), ) } else if (!isResultDone) { CircularProgressIndicator( - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp), + modifier = Modifier.padding(end = 16.dp).size(24.dp), ) } } } + +@Composable +private fun TestName( + test: TestType, + item: MeasurementWithUrl, + modifier: Modifier = Modifier, +) { + val iconResource = when { + test == TestType.WebConnectivity && item.url != null -> item.url.category.icon + else -> test.iconRes + } + + val contentDescription = item.url?.category?.title?.let { stringResource(it) } ?: "" + iconResource?.let { resource -> + Icon( + painterResource(resource), + contentDescription = contentDescription, + modifier = Modifier.padding(end = 16.dp).size(24.dp), + ) + } + Text( + text = if (test == TestType.WebConnectivity && item.url != null) { + item.url.url + } else if (test is TestType.Experimental) { + test.name + } else { + stringResource(test.labelRes) + }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier.padding(end = 8.dp), + ) +} + +@Composable +fun ResultGroupMeasurementCell( + item: ResultViewModel.MeasurementGroup, + isResultDone: Boolean, + onClick: (MeasurementModel.ReportId, String?) -> Unit, + onDropdownToggled: () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().alpha(0.66f).padding(horizontal = 16.dp), + ) { + TestName(item.test, item.measurements.first(), modifier = Modifier.weight(1f)) + IconButton(onClick = { onDropdownToggled() }) { + Icon( + painterResource( + if (item.isExpanded) { + Res.drawable.ic_keyboard_arrow_up + } else { + Res.drawable.ic_keyboard_arrow_down + }, + ), + contentDescription = stringResource( + if (item.isExpanded) { + Res.string.Common_Collapse + } else { + Res.string.Common_Expand + }, + ), + ) + } + } + + if (!item.isExpanded) return + + Column(modifier = Modifier.padding(start = 32.dp)) { + item.measurements.forEach { measurement -> + ResultMeasurementCell(measurement, isResultDone, onClick) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt index 3d2429a4..c611d604 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt @@ -67,6 +67,7 @@ import ooniprobe.composeapp.generated.resources.ooni_bw import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.ooni.engine.models.NetworkType +import org.ooni.probe.data.models.MeasurementWithUrl import org.ooni.probe.data.models.ResultItem import org.ooni.probe.ui.results.UploadResults import org.ooni.probe.ui.shared.TopBar @@ -143,14 +144,38 @@ fun ResultScreen( } } - items(state.result.measurements, key = { it.measurement.idOrThrow.value }) { item -> - ResultMeasurementCell( - item = item, - isResultDone = state.result.result.isDone, - onClick = { reportId, input -> - onEvent(ResultViewModel.Event.MeasurementClicked(reportId, input)) - }, - ) + items(state.groupedMeasurements, key = { item -> + var key: Any = item.toString() + when (item) { + is ResultViewModel.MeasurementGroup -> key = item.test.name + is MeasurementWithUrl -> key = item.measurement.idOrThrow.value + } + key + }) { item -> + when (item) { + is ResultViewModel.MeasurementGroup -> { + ResultGroupMeasurementCell( + item = item, + isResultDone = state.result.result.isDone, + onClick = { reportId, input -> + onEvent(ResultViewModel.Event.MeasurementClicked(reportId, input)) + }, + onDropdownToggled = { + onEvent(ResultViewModel.Event.MeasurementGroupToggled(item)) + }, + ) + } + + is MeasurementWithUrl -> { + ResultMeasurementCell( + item = item, + isResultDone = state.result.result.isDone, + onClick = { reportId, input -> + onEvent(ResultViewModel.Event.MeasurementClicked(reportId, input)) + }, + ) + } + } } } } @@ -171,8 +196,7 @@ private fun Summary(item: ResultItem) { HorizontalPager( state = pagerState, verticalAlignment = Alignment.Top, - modifier = Modifier - .padding(top = 8.dp, bottom = 16.dp) + modifier = Modifier.padding(top = 8.dp, bottom = 16.dp) .defaultMinSize(minHeight = 128.dp), ) { page -> when (page) { @@ -181,30 +205,22 @@ private fun Summary(item: ResultItem) { } } Row( - Modifier - .wrapContentHeight() - .fillMaxWidth() - .align(Alignment.BottomCenter) + Modifier.wrapContentHeight().fillMaxWidth().align(Alignment.BottomCenter) .padding(bottom = 8.dp), horizontalArrangement = Arrangement.Center, ) { repeat(pagerState.pageCount) { index -> Box( - modifier = Modifier - .padding(horizontal = 8.dp) - .padding(bottom = 8.dp) - .alpha(if (pagerState.currentPage == index) 1f else 0.33f) - .clip(CircleShape) - .background(LocalContentColor.current) - .size(12.dp), + modifier = Modifier.padding(horizontal = 8.dp).padding(bottom = 8.dp) + .alpha(if (pagerState.currentPage == index) 1f else 0.33f).clip(CircleShape) + .background(LocalContentColor.current).size(12.dp), ) } } Icon( painterResource(Res.drawable.ooni_bw), contentDescription = null, - modifier = Modifier.align(Alignment.BottomEnd) - .offset(x = 18.dp, y = 18.dp), + modifier = Modifier.align(Alignment.BottomEnd).offset(x = 18.dp, y = 18.dp), ) } } @@ -286,8 +302,7 @@ private fun SummaryNetwork(item: ResultItem) { modifier = labelModifier, ) Text( - item.network?.countryCode - ?: stringResource(Res.string.TestResults_NotAvailable), + item.network?.countryCode ?: stringResource(Res.string.TestResults_NotAvailable), modifier = valueModifier, ) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt index d481da49..714eb720 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.update import org.ooni.engine.models.TaskOrigin import org.ooni.engine.models.TestType import org.ooni.probe.data.models.MeasurementModel +import org.ooni.probe.data.models.MeasurementWithUrl import org.ooni.probe.data.models.NetTest import org.ooni.probe.data.models.ResultItem import org.ooni.probe.data.models.ResultModel @@ -36,14 +37,40 @@ class ResultViewModel( startBackgroundRun: (RunSpecification) -> Unit, ) : ViewModel() { private val events = MutableSharedFlow(extraBufferCapacity = 1) + private val expandedDescriptorsKeys = MutableStateFlow(emptyList()) - private val _state = MutableStateFlow(State(result = null)) + private val _state = MutableStateFlow(State(result = null, groupedMeasurements = emptyList())) val state = _state.asStateFlow() init { - getResult(resultId) - .onEach { result -> - _state.update { it.copy(result = result) } + combine( + getResult(resultId), + expandedDescriptorsKeys, + ::Pair, + ) + .onEach { (result, expandedDescriptorsKeys) -> + var groupedMeasurements = listOf() + result?.measurements?.let { measurements -> + groupedMeasurements = measurements.groupBy { it.measurement.test.name }.flatMap { (_, itemList) -> + when { + itemList.size == 1 -> listOf(itemList.first()) + itemList.size > 1 && itemList.size == measurements.size -> itemList + else -> { + val key = itemList.first().measurement.test + listOf( + MeasurementGroup( + test = key, + measurements = itemList, + isExpanded = expandedDescriptorsKeys.contains(key), + ), + ) + } + } + } + } + _state.update { + it.copy(result = result, groupedMeasurements = groupedMeasurements) + } if (result?.result?.isViewed == false) { markResultAsViewed(resultId) } @@ -95,6 +122,20 @@ class ResultViewModel( goToDashboard() } .launchIn(viewModelScope) + + events + .filterIsInstance() + .onEach { event -> + expandedDescriptorsKeys.update { keys -> + val key = event.measurementGroup.test + if (keys.contains(key)) { + keys - key + } else { + keys + key + } + } + } + .launchIn(viewModelScope) } fun onEvent(event: Event) { @@ -122,6 +163,7 @@ class ResultViewModel( data class State( val result: ResultItem?, + val groupedMeasurements: List, val rerunEnabled: Boolean = false, ) @@ -136,5 +178,13 @@ class ResultViewModel( data object UploadClicked : Event data object RerunClicked : Event + + data class MeasurementGroupToggled(val measurementGroup: MeasurementGroup) : Event } + + data class MeasurementGroup( + val test: TestType, + val measurements: List, + val isExpanded: Boolean = false, + ) } From 98b21833b0cd3b4c9fadbfdc83ed344d0d268c06 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Fri, 13 Dec 2024 11:59:49 +0100 Subject: [PATCH 2/5] chore: correct tests --- .../org/ooni/probe/ui/result/ResultScreenTest.kt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt index f82ce15c..49fc38e1 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt @@ -30,7 +30,10 @@ class ResultScreenTest { var title: String? = null setContent { ResultScreen( - state = ResultViewModel.State(item), + state = ResultViewModel.State( + result = item, + groupedMeasurements = emptyList(), + ), onEvent = {}, ) @@ -58,7 +61,11 @@ class ResultScreenTest { ) setContent { ResultScreen( - state = ResultViewModel.State(item, rerunEnabled = true), + state = ResultViewModel.State( + result = item, + groupedMeasurements = emptyList(), + rerunEnabled = true, + ), onEvent = events::add, ) } @@ -83,7 +90,10 @@ class ResultScreenTest { ) setContent { ResultScreen( - state = ResultViewModel.State(item), + state = ResultViewModel.State( + result = item, + groupedMeasurements = emptyList(), + ), onEvent = events::add, ) } From 2047aa7638a1655959c2820dafc24d6a6b3af333 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Wed, 18 Dec 2024 10:21:40 +0100 Subject: [PATCH 3/5] chore: update views --- .../probe/ui/result/ResultMeasurementCell.kt | 7 +++-- .../org/ooni/probe/ui/result/ResultScreen.kt | 15 +++++----- .../ooni/probe/ui/result/ResultViewModel.kt | 28 ++++++++++--------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt index 3fd4281d..dc982777 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt @@ -38,6 +38,7 @@ import org.jetbrains.compose.resources.stringResource import org.ooni.engine.models.TestType import org.ooni.probe.data.models.MeasurementModel import org.ooni.probe.data.models.MeasurementWithUrl +import org.ooni.probe.ui.result.ResultViewModel.MeasurementGroupItem.Group @Composable fun ResultMeasurementCell( @@ -139,14 +140,16 @@ private fun TestName( @Composable fun ResultGroupMeasurementCell( - item: ResultViewModel.MeasurementGroup, + item: Group, isResultDone: Boolean, onClick: (MeasurementModel.ReportId, String?) -> Unit, onDropdownToggled: () -> Unit, ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().alpha(0.66f).padding(horizontal = 16.dp), + modifier = Modifier.fillMaxWidth().alpha(0.66f).clickable { + onDropdownToggled() + }.padding(horizontal = 16.dp), ) { TestName(item.test, item.measurements.first(), modifier = Modifier.weight(1f)) IconButton(onClick = { onDropdownToggled() }) { diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt index c611d604..a29ba266 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt @@ -67,8 +67,9 @@ import ooniprobe.composeapp.generated.resources.ooni_bw import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.ooni.engine.models.NetworkType -import org.ooni.probe.data.models.MeasurementWithUrl import org.ooni.probe.data.models.ResultItem +import org.ooni.probe.ui.result.ResultViewModel.MeasurementGroupItem.Group +import org.ooni.probe.ui.result.ResultViewModel.MeasurementGroupItem.Single import org.ooni.probe.ui.results.UploadResults import org.ooni.probe.ui.shared.TopBar import org.ooni.probe.ui.shared.formatDataUsage @@ -145,15 +146,13 @@ fun ResultScreen( } items(state.groupedMeasurements, key = { item -> - var key: Any = item.toString() when (item) { - is ResultViewModel.MeasurementGroup -> key = item.test.name - is MeasurementWithUrl -> key = item.measurement.idOrThrow.value + is Group -> item.test.name + is Single -> item.measurement.measurement.idOrThrow.value } - key }) { item -> when (item) { - is ResultViewModel.MeasurementGroup -> { + is Group -> { ResultGroupMeasurementCell( item = item, isResultDone = state.result.result.isDone, @@ -166,9 +165,9 @@ fun ResultScreen( ) } - is MeasurementWithUrl -> { + is Single -> { ResultMeasurementCell( - item = item, + item = item.measurement, isResultDone = state.result.result.isDone, onClick = { reportId, input -> onEvent(ResultViewModel.Event.MeasurementClicked(reportId, input)) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt index 714eb720..d2d1e36c 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt @@ -49,16 +49,15 @@ class ResultViewModel( ::Pair, ) .onEach { (result, expandedDescriptorsKeys) -> - var groupedMeasurements = listOf() + var groupedMeasurements = listOf() result?.measurements?.let { measurements -> - groupedMeasurements = measurements.groupBy { it.measurement.test.name }.flatMap { (_, itemList) -> + groupedMeasurements = measurements.groupBy { it.measurement.test }.flatMap { (key, itemList) -> when { - itemList.size == 1 -> listOf(itemList.first()) - itemList.size > 1 && itemList.size == measurements.size -> itemList + itemList.size == 1 -> listOf(MeasurementGroupItem.Single(itemList.first())) + itemList.size > 1 && itemList.size == measurements.size -> itemList.map { MeasurementGroupItem.Single(it) } else -> { - val key = itemList.first().measurement.test listOf( - MeasurementGroup( + MeasurementGroupItem.Group( test = key, measurements = itemList, isExpanded = expandedDescriptorsKeys.contains(key), @@ -163,7 +162,7 @@ class ResultViewModel( data class State( val result: ResultItem?, - val groupedMeasurements: List, + val groupedMeasurements: List, val rerunEnabled: Boolean = false, ) @@ -179,12 +178,15 @@ class ResultViewModel( data object RerunClicked : Event - data class MeasurementGroupToggled(val measurementGroup: MeasurementGroup) : Event + data class MeasurementGroupToggled(val measurementGroup: MeasurementGroupItem.Group) : Event } - data class MeasurementGroup( - val test: TestType, - val measurements: List, - val isExpanded: Boolean = false, - ) + sealed class MeasurementGroupItem { + data class Single(val measurement: MeasurementWithUrl) : MeasurementGroupItem() + data class Group( + val test: TestType, + val measurements: List, + val isExpanded: Boolean + ) : MeasurementGroupItem() + } } From 10b914753c66f2be20f42c9bdaf7730ff27cd326 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Wed, 18 Dec 2024 11:03:50 +0100 Subject: [PATCH 4/5] chore: fix formatting --- .../kotlin/org/ooni/probe/ui/result/ResultViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt index d2d1e36c..eec5fa32 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt @@ -183,10 +183,11 @@ class ResultViewModel( sealed class MeasurementGroupItem { data class Single(val measurement: MeasurementWithUrl) : MeasurementGroupItem() + data class Group( val test: TestType, val measurements: List, - val isExpanded: Boolean + val isExpanded: Boolean, ) : MeasurementGroupItem() } } From 99dc3898af5a6d2774f1c556a7103a15db2f1ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Wed, 18 Dec 2024 11:05:30 +0000 Subject: [PATCH 5/5] Small UI tweak on the result measurement group cell --- .../org/ooni/probe/ui/result/ResultMeasurementCell.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt index dc982777..22ae36d1 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt @@ -147,9 +147,9 @@ fun ResultGroupMeasurementCell( ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().alpha(0.66f).clickable { - onDropdownToggled() - }.padding(horizontal = 16.dp), + modifier = Modifier.fillMaxWidth() + .clickable { onDropdownToggled() } + .padding(horizontal = 16.dp, vertical = 4.dp), ) { TestName(item.test, item.measurements.first(), modifier = Modifier.weight(1f)) IconButton(onClick = { onDropdownToggled() }) {