Skip to content

Commit

Permalink
Merge pull request #4052 from element-hq/feature/bma/licenceFilter
Browse files Browse the repository at this point in the history
Add a quick filter on the open source licence screen.
  • Loading branch information
bmarty authored Dec 17, 2024
2 parents e85aa50 + 1446524 commit b7dd490
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/

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

sealed interface DependencyLicensesListEvent {
data class SetFilter(val filter: String) : DependencyLicensesListEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,43 @@ class DependencyLicensesListPresenter @Inject constructor(
var licenses by remember {
mutableStateOf<AsyncData<ImmutableList<DependencyLicenseItem>>>(AsyncData.Loading())
}
var filteredLicenses by remember {
mutableStateOf<AsyncData<ImmutableList<DependencyLicenseItem>>>(AsyncData.Loading())
}
var filter by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
runCatching {
licenses = AsyncData.Success(licensesProvider.provides().toPersistentList())
}.onFailure {
licenses = AsyncData.Failure(it)
}
}
return DependencyLicensesListState(licenses = licenses)
LaunchedEffect(filter, licenses.dataOrNull()) {
val data = licenses.dataOrNull()
val safeFilter = filter.trim()
if (data != null && safeFilter.isNotEmpty()) {
filteredLicenses = AsyncData.Success(data.filter {
it.safeName.contains(safeFilter, ignoreCase = true) ||
it.groupId.contains(safeFilter, ignoreCase = true) ||
it.artifactId.contains(safeFilter, ignoreCase = true)
}.toPersistentList())
} else {
filteredLicenses = licenses
}
}

fun handleEvent(dependencyLicensesListEvent: DependencyLicensesListEvent) {
when (dependencyLicensesListEvent) {
is DependencyLicensesListEvent.SetFilter -> {
filter = dependencyLicensesListEvent.filter
}
}
}

return DependencyLicensesListState(
licenses = filteredLicenses,
filter = filter,
eventSink = ::handleEvent,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ import kotlinx.collections.immutable.ImmutableList

data class DependencyLicensesListState(
val licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
val filter: String,
val eventSink: (DependencyLicensesListEvent) -> Unit,
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,49 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
import io.element.android.features.licenses.impl.model.License
import io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

open class DependencyLicensesListStateProvider : PreviewParameterProvider<DependencyLicensesListState> {
override val values: Sequence<DependencyLicensesListState>
get() = sequenceOf(
DependencyLicensesListState(
aDependencyLicensesListState(
licenses = AsyncData.Loading()
),
DependencyLicensesListState(
aDependencyLicensesListState(
licenses = AsyncData.Failure(Exception("Failed to load licenses"))
),
DependencyLicensesListState(
aDependencyLicensesListState(
licenses = AsyncData.Success(
persistentListOf(
aDependencyLicenseItem(),
aDependencyLicenseItem(name = null),
)
)
)
),
aDependencyLicensesListState(
licenses = AsyncData.Success(
persistentListOf(
aDependencyLicenseItem(),
aDependencyLicenseItem(name = null),
)
),
filter = "a filter",
),
)
}

private fun aDependencyLicensesListState(
licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
filter: String = "",
): DependencyLicensesListState {
return DependencyLicensesListState(
licenses = licenses,
filter = filter,
eventSink = {},
)
}

internal fun aDependencyLicenseItem(
name: String? = "A dependency",
) = DependencyLicenseItem(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,36 @@

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

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings

@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun DependencyLicensesListView(
state: DependencyLicensesListState,
Expand All @@ -48,48 +53,64 @@ fun DependencyLicensesListView(
)
},
) { contentPadding ->
LazyColumn(
Column(
modifier = Modifier
.padding(contentPadding)
.padding(horizontal = 16.dp)
) {
when (state.licenses) {
is AsyncData.Failure -> item {
Text(
text = stringResource(CommonStrings.common_error),
modifier = Modifier.padding(16.dp)
)
}
AsyncData.Uninitialized,
is AsyncData.Loading -> item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 64.dp)
) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
if (state.licenses.isSuccess()) {
// Search field
OutlinedTextField(
value = state.filter,
onValueChange = { state.eventSink(DependencyLicensesListEvent.SetFilter(it)) },
leadingIcon = {
Icon(
imageVector = CompoundIcons.Search(),
contentDescription = null,
)
},
modifier = Modifier.fillMaxWidth(),
)
}
LazyColumn {
when (state.licenses) {
is AsyncData.Failure -> item {
Text(
text = stringResource(CommonStrings.common_error),
modifier = Modifier.padding(16.dp)
)
}
}
is AsyncData.Success -> items(state.licenses.data) { license ->
ListItem(
headlineContent = { Text(license.safeName) },
supportingContent = {
Text(
buildString {
append(license.groupId)
append(":")
append(license.artifactId)
append(":")
append(license.version)
}
AsyncData.Uninitialized,
is AsyncData.Loading -> item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 64.dp)
) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
},
onClick = {
onOpenLicense(license)
}
)
}
is AsyncData.Success -> items(state.licenses.data) { license ->
ListItem(
headlineContent = { Text(license.safeName) },
supportingContent = {
Text(
buildString {
append(license.groupId)
append(":")
append(license.artifactId)
append(":")
append(license.version)
}
)
},
onClick = {
onOpenLicense(license)
}
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class DependencyLicensesListPresenterTest {
val finalState = awaitItem()
assertThat(finalState.licenses.isSuccess()).isTrue()
assertThat(finalState.licenses.dataOrNull()).isEmpty()
assertThat(finalState.filter).isEqualTo("")
}
}

Expand All @@ -54,6 +55,40 @@ class DependencyLicensesListPresenterTest {
}
}

@Test
fun `present - initial state, one license, set filter`() = runTest {
val anItem = aDependencyLicenseItem()
val presenter = createPresenter {
listOf(anItem)
}
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.licenses).isInstanceOf(AsyncData.Loading::class.java)
val loadedState = awaitItem()
assertThat(loadedState.licenses.isSuccess()).isTrue()
assertThat(loadedState.licenses.dataOrNull()!!.size).isEqualTo(1)
loadedState.eventSink(DependencyLicensesListEvent.SetFilter("dep"))
awaitItem().let { state ->
assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(1)
assertThat(state.filter).isEqualTo("dep")
}
loadedState.eventSink(DependencyLicensesListEvent.SetFilter("bleh"))
skipItems(1)
awaitItem().let { state ->
assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(0)
assertThat(state.filter).isEqualTo("bleh")
}
loadedState.eventSink(DependencyLicensesListEvent.SetFilter(""))
skipItems(1)
awaitItem().let { state ->
assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(1)
assertThat(state.filter).isEqualTo("")
}
}
}

private fun createPresenter(
provideResult: () -> List<DependencyLicenseItem>
) = DependencyLicensesListPresenter(
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b7dd490

Please sign in to comment.