Skip to content

Commit ea59ce1

Browse files
committed
Add a quick filter on the open source licence screen.
1 parent 66ca329 commit ea59ce1

File tree

6 files changed

+161
-40
lines changed

6 files changed

+161
-40
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.licenses.impl.list
9+
10+
sealed interface DependencyLicensesListEvent {
11+
data class SetFilter(val filter: String) : DependencyLicensesListEvent
12+
}

features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,43 @@ class DependencyLicensesListPresenter @Inject constructor(
2929
var licenses by remember {
3030
mutableStateOf<AsyncData<ImmutableList<DependencyLicenseItem>>>(AsyncData.Loading())
3131
}
32+
var filteredLicenses by remember {
33+
mutableStateOf<AsyncData<ImmutableList<DependencyLicenseItem>>>(AsyncData.Loading())
34+
}
35+
var filter by remember { mutableStateOf("") }
3236
LaunchedEffect(Unit) {
3337
runCatching {
3438
licenses = AsyncData.Success(licensesProvider.provides().toPersistentList())
3539
}.onFailure {
3640
licenses = AsyncData.Failure(it)
3741
}
3842
}
39-
return DependencyLicensesListState(licenses = licenses)
43+
LaunchedEffect(filter, licenses.dataOrNull()) {
44+
val data = licenses.dataOrNull()
45+
val safeFilter = filter.trim()
46+
if (data != null && safeFilter.isNotEmpty()) {
47+
filteredLicenses = AsyncData.Success(data.filter {
48+
it.safeName.contains(safeFilter, ignoreCase = true) ||
49+
it.groupId.contains(safeFilter, ignoreCase = true) ||
50+
it.artifactId.contains(safeFilter, ignoreCase = true)
51+
}.toPersistentList())
52+
} else {
53+
filteredLicenses = licenses
54+
}
55+
}
56+
57+
fun handleEvent(dependencyLicensesListEvent: DependencyLicensesListEvent) {
58+
when (dependencyLicensesListEvent) {
59+
is DependencyLicensesListEvent.SetFilter -> {
60+
filter = dependencyLicensesListEvent.filter
61+
}
62+
}
63+
}
64+
65+
return DependencyLicensesListState(
66+
licenses = filteredLicenses,
67+
filter = filter,
68+
eventSink = ::handleEvent,
69+
)
4070
}
4171
}

features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ import kotlinx.collections.immutable.ImmutableList
1313

1414
data class DependencyLicensesListState(
1515
val licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
16+
val filter: String,
17+
val eventSink: (DependencyLicensesListEvent) -> Unit,
1618
)

features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,49 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
1111
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
1212
import io.element.android.features.licenses.impl.model.License
1313
import io.element.android.libraries.architecture.AsyncData
14+
import kotlinx.collections.immutable.ImmutableList
1415
import kotlinx.collections.immutable.persistentListOf
1516

1617
open class DependencyLicensesListStateProvider : PreviewParameterProvider<DependencyLicensesListState> {
1718
override val values: Sequence<DependencyLicensesListState>
1819
get() = sequenceOf(
19-
DependencyLicensesListState(
20+
aDependencyLicensesListState(
2021
licenses = AsyncData.Loading()
2122
),
22-
DependencyLicensesListState(
23+
aDependencyLicensesListState(
2324
licenses = AsyncData.Failure(Exception("Failed to load licenses"))
2425
),
25-
DependencyLicensesListState(
26+
aDependencyLicensesListState(
2627
licenses = AsyncData.Success(
2728
persistentListOf(
2829
aDependencyLicenseItem(),
2930
aDependencyLicenseItem(name = null),
3031
)
3132
)
32-
)
33+
),
34+
aDependencyLicensesListState(
35+
licenses = AsyncData.Success(
36+
persistentListOf(
37+
aDependencyLicenseItem(),
38+
aDependencyLicenseItem(name = null),
39+
)
40+
),
41+
filter = "a filter",
42+
),
3343
)
3444
}
3545

46+
private fun aDependencyLicensesListState(
47+
licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
48+
filter: String = "",
49+
): DependencyLicensesListState {
50+
return DependencyLicensesListState(
51+
licenses = licenses,
52+
filter = filter,
53+
eventSink = {},
54+
)
55+
}
56+
3657
internal fun aDependencyLicenseItem(
3758
name: String? = "A dependency",
3859
) = DependencyLicenseItem(

features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,36 @@
77

88
package io.element.android.features.licenses.impl.list
99

10+
import androidx.compose.foundation.ExperimentalFoundationApi
1011
import androidx.compose.foundation.layout.Box
12+
import androidx.compose.foundation.layout.Column
1113
import androidx.compose.foundation.layout.fillMaxWidth
1214
import androidx.compose.foundation.layout.padding
1315
import androidx.compose.foundation.lazy.LazyColumn
1416
import androidx.compose.foundation.lazy.items
1517
import androidx.compose.material3.ExperimentalMaterial3Api
18+
import androidx.compose.material3.OutlinedTextField
1619
import androidx.compose.runtime.Composable
1720
import androidx.compose.ui.Alignment
1821
import androidx.compose.ui.Modifier
1922
import androidx.compose.ui.res.stringResource
2023
import androidx.compose.ui.tooling.preview.PreviewParameter
2124
import androidx.compose.ui.unit.dp
25+
import io.element.android.compound.tokens.generated.CompoundIcons
2226
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
2327
import io.element.android.libraries.architecture.AsyncData
2428
import io.element.android.libraries.designsystem.components.button.BackButton
2529
import io.element.android.libraries.designsystem.preview.ElementPreview
2630
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
2731
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
32+
import io.element.android.libraries.designsystem.theme.components.Icon
2833
import io.element.android.libraries.designsystem.theme.components.ListItem
2934
import io.element.android.libraries.designsystem.theme.components.Scaffold
3035
import io.element.android.libraries.designsystem.theme.components.Text
3136
import io.element.android.libraries.designsystem.theme.components.TopAppBar
3237
import io.element.android.libraries.ui.strings.CommonStrings
3338

34-
@OptIn(ExperimentalMaterial3Api::class)
39+
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
3540
@Composable
3641
fun DependencyLicensesListView(
3742
state: DependencyLicensesListState,
@@ -48,48 +53,64 @@ fun DependencyLicensesListView(
4853
)
4954
},
5055
) { contentPadding ->
51-
LazyColumn(
56+
Column(
5257
modifier = Modifier
5358
.padding(contentPadding)
5459
.padding(horizontal = 16.dp)
5560
) {
56-
when (state.licenses) {
57-
is AsyncData.Failure -> item {
58-
Text(
59-
text = stringResource(CommonStrings.common_error),
60-
modifier = Modifier.padding(16.dp)
61-
)
62-
}
63-
AsyncData.Uninitialized,
64-
is AsyncData.Loading -> item {
65-
Box(
66-
modifier = Modifier
67-
.fillMaxWidth()
68-
.padding(top = 64.dp)
69-
) {
70-
CircularProgressIndicator(
71-
modifier = Modifier.align(Alignment.Center)
61+
if (state.licenses.isSuccess()) {
62+
// Search field
63+
OutlinedTextField(
64+
value = state.filter,
65+
onValueChange = { state.eventSink(DependencyLicensesListEvent.SetFilter(it)) },
66+
leadingIcon = {
67+
Icon(
68+
imageVector = CompoundIcons.Search(),
69+
contentDescription = null,
70+
)
71+
},
72+
modifier = Modifier.fillMaxWidth(),
73+
)
74+
}
75+
LazyColumn {
76+
when (state.licenses) {
77+
is AsyncData.Failure -> item {
78+
Text(
79+
text = stringResource(CommonStrings.common_error),
80+
modifier = Modifier.padding(16.dp)
7281
)
7382
}
74-
}
75-
is AsyncData.Success -> items(state.licenses.data) { license ->
76-
ListItem(
77-
headlineContent = { Text(license.safeName) },
78-
supportingContent = {
79-
Text(
80-
buildString {
81-
append(license.groupId)
82-
append(":")
83-
append(license.artifactId)
84-
append(":")
85-
append(license.version)
86-
}
83+
AsyncData.Uninitialized,
84+
is AsyncData.Loading -> item {
85+
Box(
86+
modifier = Modifier
87+
.fillMaxWidth()
88+
.padding(top = 64.dp)
89+
) {
90+
CircularProgressIndicator(
91+
modifier = Modifier.align(Alignment.Center)
8792
)
88-
},
89-
onClick = {
90-
onOpenLicense(license)
9193
}
92-
)
94+
}
95+
is AsyncData.Success -> items(state.licenses.data) { license ->
96+
ListItem(
97+
headlineContent = { Text(license.safeName) },
98+
supportingContent = {
99+
Text(
100+
buildString {
101+
append(license.groupId)
102+
append(":")
103+
append(license.artifactId)
104+
append(":")
105+
append(license.version)
106+
}
107+
)
108+
},
109+
onClick = {
110+
onOpenLicense(license)
111+
}
112+
)
113+
}
93114
}
94115
}
95116
}

features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenterTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class DependencyLicensesListPresenterTest {
3333
val finalState = awaitItem()
3434
assertThat(finalState.licenses.isSuccess()).isTrue()
3535
assertThat(finalState.licenses.dataOrNull()).isEmpty()
36+
assertThat(finalState.filter).isEqualTo("")
3637
}
3738
}
3839

@@ -54,6 +55,40 @@ class DependencyLicensesListPresenterTest {
5455
}
5556
}
5657

58+
@Test
59+
fun `present - initial state, one license, set filter`() = runTest {
60+
val anItem = aDependencyLicenseItem()
61+
val presenter = createPresenter {
62+
listOf(anItem)
63+
}
64+
moleculeFlow(RecompositionMode.Immediate) {
65+
presenter.present()
66+
}.test {
67+
val initialState = awaitItem()
68+
assertThat(initialState.licenses).isInstanceOf(AsyncData.Loading::class.java)
69+
val loadedState = awaitItem()
70+
assertThat(loadedState.licenses.isSuccess()).isTrue()
71+
assertThat(loadedState.licenses.dataOrNull()!!.size).isEqualTo(1)
72+
loadedState.eventSink(DependencyLicensesListEvent.SetFilter("dep"))
73+
awaitItem().let { state ->
74+
assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(1)
75+
assertThat(state.filter).isEqualTo("dep")
76+
}
77+
loadedState.eventSink(DependencyLicensesListEvent.SetFilter("bleh"))
78+
skipItems(1)
79+
awaitItem().let { state ->
80+
assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(0)
81+
assertThat(state.filter).isEqualTo("bleh")
82+
}
83+
loadedState.eventSink(DependencyLicensesListEvent.SetFilter(""))
84+
skipItems(1)
85+
awaitItem().let { state ->
86+
assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(1)
87+
assertThat(state.filter).isEqualTo("")
88+
}
89+
}
90+
}
91+
5792
private fun createPresenter(
5893
provideResult: () -> List<DependencyLicenseItem>
5994
) = DependencyLicensesListPresenter(

0 commit comments

Comments
 (0)