Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a quick filter on the open source licence screen. #4052

Merged
merged 2 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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 = {},

Check warning on line 53 in features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L53 was not covered by tests
)
}

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 @@
)
},
) { 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)

Check warning on line 110 in features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L110 was not covered by tests
}
)
}
}
}
}
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.
Loading