Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cb5bf1f
refactor navigation to support nested destinations
kaushikrw Sep 25, 2025
1b744e6
add paging, symbology display for features
kaushikrw Sep 27, 2025
89ad3e1
change feature list title
kaushikrw Sep 29, 2025
910173d
add association creation
kaushikrw Sep 30, 2025
5f13c39
add asset type selection
kaushikrw Oct 1, 2025
001f568
add final screens
kaushikrw Oct 2, 2025
c11befb
optimize imports and extract strings
kaushikrw Oct 2, 2025
4ae0d02
update association creation logic
kaushikrw Oct 3, 2025
73ee9fc
extract strings
kaushikrw Oct 3, 2025
54c47ea
updated ignored api classes
kaushikrw Oct 3, 2025
1675cde
Add filter/search functionality
puneet-pdx Oct 6, 2025
65b6483
revert logic to determine query result size from paging params
puneet-pdx Oct 6, 2025
3d75ad7
add ignore for "com.arcgismaps.toolkit.featureforms.internal.utils.C…
puneet-pdx Oct 6, 2025
87305b2
remove paging
kaushikrw Oct 6, 2025
eb76516
Merge branch 'kaushik/forms/una-add-feature-1' into puneet/1418_Searc…
puneet-pdx Oct 7, 2025
89a6476
CR : Puneet
kaushikrw Oct 7, 2025
1e7d28a
rename routes
kaushikrw Oct 7, 2025
1ab4071
rename more routes
kaushikrw Oct 7, 2025
650ec50
move filtering logic to the composable from viewModel
puneet-pdx Oct 7, 2025
50ea358
update SearchBar UI
puneet-pdx Oct 7, 2025
482ba70
Merge branch 'kaushik/forms/una-add-feature-1' into puneet/1418_Searc…
puneet-pdx Oct 7, 2025
79c3709
Merge branch 'feature-branches/forms' into puneet/1418_SearchImplemen…
puneet-pdx Oct 7, 2025
2751695
abstract filter logic to viewmodel
puneet-pdx Oct 8, 2025
9ba1395
update doc
puneet-pdx Oct 8, 2025
bd8ffa7
address code review comments
puneet-pdx Oct 8, 2025
96c3cdb
update padding
puneet-pdx Oct 8, 2025
8d7180b
don't clear sources filter in selectSource
puneet-pdx Oct 8, 2025
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
3 changes: 2 additions & 1 deletion toolkit/featureforms/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ apiValidation {
"com.arcgismaps.toolkit.featureforms.internal.screens.ComposableSingletons\$UNAssociationsFilterResultScreenKt",
"com.arcgismaps.toolkit.featureforms.internal.screens.ComposableSingletons\$CreateAssociationScreenKt",
"com.arcgismaps.toolkit.featureforms.internal.screens.ComposableSingletons\$SelectAssociatedFeatureScreenKt",
"com.arcgismaps.toolkit.featureforms.internal.screens.ComposableSingletons\$SelectNetworkSourceScreenKt"
"com.arcgismaps.toolkit.featureforms.internal.screens.ComposableSingletons\$SelectNetworkSourceScreenKt",
"com.arcgismaps.toolkit.featureforms.internal.utils.ComposableSingletons\$SearchBarKt"
)

ignoredClasses.addAll(composableSingletons)
Expand Down
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the filter texts are stale and not reset when coming back to a screen.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.arcgismaps.toolkit.featureforms.internal.components.utilitynetwork

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.ViewModel
Expand All @@ -36,7 +37,11 @@ import com.arcgismaps.utilitynetworks.UtilityAssociationsFilter
import com.arcgismaps.utilitynetworks.UtilityAssociationsFilterType
import com.arcgismaps.utilitynetworks.UtilityTerminal
import com.arcgismaps.utilitynetworks.UtilityTerminalConfiguration
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

/**
Expand All @@ -57,6 +62,27 @@ internal class AddAssociationFromSourceViewModel(
private val onAssociationAdded: suspend () -> Unit
) : ViewModel() {

private val _featureSourcesFilterText: MutableState<String> = mutableStateOf("")
/**
* The current text used to filter the list of feature sources. This can be set via
* [setFeatureSourcesFilterText].
*/
val featureSourcesFilterText by _featureSourcesFilterText

private val _assetTypesFilterText: MutableState<String> = mutableStateOf("")
/**
* The current text used to filter the list of asset types. This can be set via
* [setAssetTypesFilterText].
*/
val assetTypesFilterText by _assetTypesFilterText

private val _associatedFeaturesFilterText: MutableState<String> = mutableStateOf("")
/**
* The current text used to filter the list of associated feature candidates. This can be set
* via [setAssociatedFeaturesFilterText].
*/
val associatedFeaturesFilterText by _associatedFeaturesFilterText

private val _featureSources: MutableState<List<UtilityAssociationFeatureSource>> =
mutableStateOf(emptyList())

Expand Down Expand Up @@ -106,6 +132,57 @@ internal class AddAssociationFromSourceViewModel(
val newAssociationOptions: NewAssociationOptions?
get() = _newAssociationOptions.value

/**
* A flow that emits a list of [UtilityAssociationFeatureSource]s based on the current search text.
*/
val filteredFeatureSources: StateFlow<List<UtilityAssociationFeatureSource>> = snapshotFlow {
featureSourcesFilterText to featureSources
}.map { (text, _) ->
return@map if (text.isNotEmpty()) {
filterFeatureSources(text)
} else {
_featureSources.value
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(1000),
initialValue = _featureSources.value
)

/**
* A flow that emits a list of [UtilityAssetType]s based on the current search text.
*/
val filteredAssetTypes: StateFlow<List<UtilityAssetType>> = snapshotFlow {
assetTypesFilterText to selectedSource
}.map { (text, _) ->
return@map if (text.isNotEmpty()) {
filterAssetTypes(text)
} else {
_selectedSource.value?.assetTypes ?: emptyList()
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(1000),
initialValue = _selectedSource.value?.assetTypes ?: emptyList()
)

/**
* A flow that emits a list of [UtilityAssociationFeatureCandidate]s based on the current search text.
*/
val filteredFeatureCandidates: StateFlow<List<UtilityAssociationFeatureCandidate>> = snapshotFlow {
associatedFeaturesFilterText to _featureCandidatesUiState.value
}.map { (text, _) ->
return@map if (text.isNotEmpty()) {
filterFeatureCandidates(text)
} else {
_featureCandidatesUiState.value.candidates
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(1000),
initialValue = _featureCandidatesUiState.value.candidates
)

init {
viewModelScope.launch {
// Whenever the selected source or asset type changes, reload the feature candidates
Expand Down Expand Up @@ -144,6 +221,58 @@ internal class AddAssociationFromSourceViewModel(
}
}

/**
* Filters the list of [UtilityAssociationFeatureSource] objects based on the provided
* [filterString], returning only those whose names contain the filter string (case-insensitive).
*/
private fun filterFeatureSources(filterString: String): List<UtilityAssociationFeatureSource> {
return _featureSources.value.filter { featureSource ->
featureSource.name.contains(filterString, ignoreCase = true)
}
}

/**
* Filters the list of [UtilityAssetType] objects from the currently selected source based on
* the provided [filterString], returning only those whose names contain the filter string
* (case-insensitive).
*/
private fun filterAssetTypes(filterString: String): List<UtilityAssetType> {
return _selectedSource.value?.assetTypes?.filter { assetType ->
assetType.name.contains(filterString, ignoreCase = true)
} ?: emptyList()
}

/**
* Filters the list of [UtilityAssociationFeatureCandidate] objects based on the provided
* [filterString], returning only those whose titles contain the filter string (case-insensitive).
*/
private fun filterFeatureCandidates(filterString: String): List<UtilityAssociationFeatureCandidate> {
return _featureCandidatesUiState.value.candidates.filter { candidate ->
candidate.title.contains(filterString, ignoreCase = true)
}
}

/**
* Sets the text for [featureSourcesFilterText].
*/
fun setFeatureSourcesFilterText(text: String) {
_featureSourcesFilterText.value = text
}

/**
* Sets the text for [assetTypesFilterText].
*/
fun setAssetTypesFilterText(text: String) {
_assetTypesFilterText.value = text
}

/**
* Sets the text for [associatedFeaturesFilterText].
*/
fun setAssociatedFeaturesFilterText(text: String) {
_associatedFeaturesFilterText.value = text
}

/**
* Fetches the list of [UtilityAssociationFeatureSource] objects that can be used to create
* new associations for the given [filter] and stores them in the [featureSources] map.
Expand Down Expand Up @@ -305,13 +434,17 @@ internal class AddAssociationFromSourceViewModel(
*/
fun selectSource(source: UtilityAssociationFeatureSource?) {
_selectedSource.value = source
// Clear previously set filter text
setAssetTypesFilterText("")
}

/**
* Sets the currently selected [UtilityAssetType].
*/
fun selectAssetType(assetType: UtilityAssetType?) {
_selectedAssetType.value = assetType
// Clear previously set filter text
setAssociatedFeaturesFilterText("")
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.arcgismaps.toolkit.featureforms.R
import com.arcgismaps.toolkit.featureforms.internal.components.utilitynetwork.AddAssociationFromSourceViewModel
import com.arcgismaps.toolkit.featureforms.internal.utils.SearchBar

@Composable
internal fun SelectAssetTypeScreen(
Expand All @@ -48,14 +50,20 @@ internal fun SelectAssetTypeScreen(
modifier: Modifier = Modifier
) {
val networkSource = viewModel.selectedSource
val assetTypes = networkSource?.assetTypes ?: emptyList()
val filteredAssetTypes = viewModel.filteredAssetTypes.collectAsState().value

Column(modifier = modifier) {
AddWorkflowTopBar(
title = "${networkSource?.name}",
subTitle = "",
onBackPressed = onBackPressed,
modifier = Modifier.fillMaxWidth(),
)
SearchBar(
value = viewModel.assetTypesFilterText,
onValueChange = { viewModel.setAssetTypesFilterText(it) },
placeholder = stringResource(R.string.search)
)
Row(
modifier = Modifier
.fillMaxWidth()
Expand All @@ -68,7 +76,7 @@ internal fun SelectAssetTypeScreen(
style = MaterialTheme.typography.labelSmall
)
Text(
text = stringResource(R.string.count, assetTypes.count()),
text = stringResource(R.string.count, filteredAssetTypes.count()),
style = MaterialTheme.typography.labelSmall
)
}
Expand All @@ -80,7 +88,7 @@ internal fun SelectAssetTypeScreen(
color = MaterialTheme.colorScheme.surfaceBright
) {
LazyColumn {
itemsIndexed(assetTypes) { index, assetType ->
itemsIndexed(filteredAssetTypes) { index, assetType ->
ListItem(
modifier = Modifier.clickable {
viewModel.selectAssetType(assetType)
Expand All @@ -96,7 +104,7 @@ internal fun SelectAssetTypeScreen(
containerColor = MaterialTheme.colorScheme.surfaceBright,
)
)
if (index < assetTypes.count() - 1) {
if (index < filteredAssetTypes.count() - 1) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
Expand All @@ -70,6 +70,7 @@ import com.arcgismaps.mapping.layers.SubtypeFeatureLayer
import com.arcgismaps.mapping.symbology.Symbol
import com.arcgismaps.toolkit.featureforms.R
import com.arcgismaps.toolkit.featureforms.internal.components.utilitynetwork.AddAssociationFromSourceViewModel
import com.arcgismaps.toolkit.featureforms.internal.utils.SearchBar
import com.arcgismaps.toolkit.featureforms.internal.utils.SharedImageLoader
import kotlinx.coroutines.launch

Expand All @@ -90,9 +91,9 @@ internal fun SelectAssociatedFeatureScreen(
val scope = rememberCoroutineScope()
val lazyListState = rememberLazyListState()
val state by viewModel.featureCandidatesUiState
val count = rememberSaveable(state) {
state.candidates.count()
}
val filteredCandidates = viewModel.filteredFeatureCandidates.collectAsState().value

val count = filteredCandidates.count()
val title = if (state.isLoading){
stringResource(R.string.loading2)
} else {
Expand All @@ -110,6 +111,12 @@ internal fun SelectAssociatedFeatureScreen(
onBackPressed = onBackPressed,
modifier = Modifier.fillMaxWidth(),
)
SearchBar(
value = viewModel.associatedFeaturesFilterText,
onValueChange = { viewModel.setAssociatedFeaturesFilterText(it) },
placeholder = stringResource(R.string.search_features)
)

AnimatedContent(
targetState = state
) { state ->
Expand Down Expand Up @@ -146,7 +153,7 @@ internal fun SelectAssociatedFeatureScreen(
autoHide = true
)
) {
itemsIndexed(state.candidates) { index, item ->
itemsIndexed(filteredCandidates) { index, item ->
ListItem(
modifier = Modifier.clickable {
scope.launch {
Expand Down Expand Up @@ -189,7 +196,7 @@ internal fun SelectAssociatedFeatureScreen(
containerColor = MaterialTheme.colorScheme.surfaceBright,
)
)
if (index < state.candidates.count() - 1) {
if (index < filteredCandidates.count() - 1) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh
Expand Down Expand Up @@ -316,7 +323,7 @@ internal fun Modifier.verticalScrollbar(
if (scrollbarHeight >= size.height) return@drawWithContent
// Calculate the Y offset of the scrollbar
val scrollBarOffsetY = (size.height / totalHeight) *
(state.firstVisibleItemIndex * itemHeight + state.firstVisibleItemScrollOffset)
(state.firstVisibleItemIndex * itemHeight + state.firstVisibleItemScrollOffset)
// Calculate the X offset of the scrollbar
val scrollBarOffsetX = size.width + width.toPx() - offsetX.toPx()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.arcgismaps.toolkit.featureforms.R
import com.arcgismaps.toolkit.featureforms.internal.components.utilitynetwork.AddAssociationFromSourceViewModel
import com.arcgismaps.toolkit.featureforms.internal.utils.SearchBar

@Composable
internal fun SelectNetworkSourceScreen(
Expand All @@ -53,7 +55,7 @@ internal fun SelectNetworkSourceScreen(
onBackPressed: () -> Unit,
modifier: Modifier = Modifier
) {
val sources = viewModel.featureSources
val filteredSources = viewModel.filteredFeatureSources.collectAsState().value
val lazyListState = rememberLazyListState()
Column(
modifier = modifier,
Expand All @@ -66,8 +68,13 @@ internal fun SelectNetworkSourceScreen(
onBackPressed = onBackPressed,
modifier = Modifier.fillMaxWidth(),
)
SearchBar(
value = viewModel.featureSourcesFilterText,
onValueChange = { viewModel.setFeatureSourcesFilterText(it) },
placeholder = stringResource(R.string.search)
)
Text(
text = stringResource(R.string.count, sources.count()),
text = stringResource(R.string.count, filteredSources.count()),
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 16.dp)
)
Expand All @@ -79,7 +86,7 @@ internal fun SelectNetworkSourceScreen(
color = MaterialTheme.colorScheme.surfaceBright
) {
LazyColumn(state = lazyListState) {
itemsIndexed(sources) { index, source ->
itemsIndexed(filteredSources) { index, source ->
ListItem(
modifier = Modifier.clickable {
viewModel.selectSource(source)
Expand All @@ -95,7 +102,7 @@ internal fun SelectNetworkSourceScreen(
containerColor = MaterialTheme.colorScheme.surfaceBright,
)
)
if (index < sources.count() - 1) {
if (index < filteredSources.count() - 1) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh
Expand Down
Loading