Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2baef1b
feat(sensors): add search filter to sensor setting allow list dialog
danielgomezrico Mar 12, 2026
6fdce1a
refactor(sensors): extract search field composable and hide for small…
danielgomezrico Mar 13, 2026
f015d0c
Update dialog
danielgomezrico Mar 24, 2026
65d0321
fix(sensors): sort imports in lexicographic order for ktlint
danielgomezrico Mar 29, 2026
efcacd9
chore(UI): Use styles to present better data on the UI
danielgomezrico Mar 29, 2026
0c07286
style(Sensors): Add braces to if/else in filterSettingEntries
danielgomezrico Mar 29, 2026
ec55766
style(Sensors): Convert short function signatures to single-line format
danielgomezrico Apr 2, 2026
b38107c
Cleanup
danielgomezrico May 2, 2026
0f35f26
refactor(Sensors): address review feedback on multi-select bottom sheet
danielgomezrico May 3, 2026
271dca3
test(Sensors): rename filter test to match SensorDetailSettingSheet
danielgomezrico May 3, 2026
28cdedd
test(Sensors): cover joinSelectedValues, label parsing, and dialog st…
danielgomezrico May 3, 2026
5f03e84
test(common): cover HASearchField debounce behaviour
danielgomezrico May 3, 2026
9e50c02
Merge remote-tracking branch 'origin/main' into feature/search-sensor…
danielgomezrico May 4, 2026
1182eee
style(common): collapse debouncedSearchUpdate signature to single line
danielgomezrico May 4, 2026
fee247f
fix(Sensors): satisfy Compose stability lint and drop misplaced @Visi…
danielgomezrico May 4, 2026
15e0d85
refactor(Sensors): tidy SensorDetailSettingSheet per PR feedback
danielgomezrico May 17, 2026
0d6f062
refactor(Sensors): wrap multi-select sheet in HATheme
danielgomezrico May 17, 2026
e1de5eb
fix(Sensors): wrap SettingEntry list in @Immutable holder for Compose…
danielgomezrico May 18, 2026
e41294c
fix(Sensors): top-align entry list in allow-list sheet
danielgomezrico May 19, 2026
1ccc76f
refactor(Sensors): address review feedback on multi-select bottom sheet
danielgomezrico May 19, 2026
81d5992
refactor(common): expose HASearchField debounce constant for tests
danielgomezrico May 19, 2026
4c963a3
refactor(common): extract bottom-sheet scroll/fling guard into shared…
danielgomezrico May 19, 2026
2383f25
refactor(common): drop composed{} from consumeSheetScrollFling
danielgomezrico May 24, 2026
dac0dc7
chore(lint): baseline new SensorDetailSettingSheet warnings on automo…
danielgomezrico May 24, 2026
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
Expand Up @@ -28,6 +28,8 @@
import androidx.compose.material.Card
import androidx.compose.material.Checkbox
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Divider
import androidx.compose.material.LocalContentAlpha
Expand All @@ -42,6 +44,9 @@
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.contentColorFor
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
Expand Down Expand Up @@ -608,31 +613,44 @@
CircularProgressIndicator()
}
} else if (listSettingDialog) {
LazyColumn {
items(state.entries, key = { (id) -> id }) { (id, entry) ->
SensorDetailSettingRow(
label = entry,
checked = if (state.setting.valueType ==
SensorSettingType.LIST
) {
inputValue.value == id
} else {
checkedValue.contains(id)
},
multiple = state.setting.valueType != SensorSettingType.LIST,
onClick = { isChecked ->
if (state.setting.valueType == SensorSettingType.LIST) {
inputValue.value = id
onSubmit(state.copy().apply { setting.value = inputValue.value })
var searchQuery by remember { mutableStateOf("") }
val filteredEntries = remember(state.entries, searchQuery) {
filterSettingEntries(state.entries, searchQuery)
}
val showSearch = state.entries.size > 10
Column {
if (showSearch) {
SettingSearchField(
query = searchQuery,
onQueryChange = { searchQuery = it },
)
}
LazyColumn(modifier = Modifier.weight(1f)) {
items(filteredEntries, key = { (id) -> id }) { (id, entry) ->
SensorDetailSettingRow(
label = entry,
checked = if (state.setting.valueType ==
SensorSettingType.LIST
) {
inputValue.value == id
} else {
if (checkedValue.contains(id) && !isChecked) {
checkedValue.remove(id)
} else if (!checkedValue.contains(id) && isChecked) {
checkedValue.add(id)
checkedValue.contains(id)
},
multiple = state.setting.valueType != SensorSettingType.LIST,
onClick = { isChecked ->
if (state.setting.valueType == SensorSettingType.LIST) {
inputValue.value = id
onSubmit(state.copy().apply { setting.value = inputValue.value })
} else {
if (checkedValue.contains(id) && !isChecked) {
checkedValue.remove(id)
} else if (!checkedValue.contains(id) && isChecked) {
checkedValue.add(id)
}
}
}
},
)
},
)
}
}
}
} else {
Expand Down Expand Up @@ -734,6 +752,47 @@
)
}

@Composable
private fun SettingSearchField(
query: String,
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
onQueryChange: (String) -> Unit,
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
modifier: Modifier = Modifier,
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
) {
TextField(
Comment thread
danielgomezrico marked this conversation as resolved.
Outdated
value = query,
onValueChange = onQueryChange,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
Comment thread
danielgomezrico marked this conversation as resolved.
Outdated
singleLine = true,
label = { Text(stringResource(commonR.string.search)) },
leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },
trailingIcon = if (query.isNotBlank()) {
{
IconButton(onClick = { onQueryChange("") }) {
Icon(
Icons.Filled.Clear,
contentDescription = stringResource(commonR.string.clear_search),
)
}
}
} else {
null
},
)
}

/**
* Filters setting entries by matching the query against entry labels (case-insensitive).
* Returns all entries when the query is blank.
*/
internal fun filterSettingEntries(
entries: List<Pair<String, String>>,
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
query: String,
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
): List<Pair<String, String>> =
if (query.isBlank()) entries
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
else entries.filter { (_, label) -> label.contains(query.trim(), ignoreCase = true) }
Comment thread
danielgomezrico marked this conversation as resolved.
Outdated
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed
Comment thread
danielgomezrico marked this conversation as resolved.
Fixed

@Composable
fun SensorDetailSettingRow(
label: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.homeassistant.companion.android.settings.sensor.views

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class SensorDetailSettingDialogFilterTest {

private val entries = listOf(
"com.google.chrome" to "Chrome\n(com.google.chrome)",
"org.mozilla.firefox" to "Firefox\n(org.mozilla.firefox)",
"com.example.app" to "Example App\n(com.example.app)",
)

@Test
fun `Given empty query when filtering then return all entries`() {
val result = filterSettingEntries(entries, query = "")

assertEquals(entries, result)
}

@Test
fun `Given blank query when filtering then return all entries`() {
val result = filterSettingEntries(entries, query = " ")

assertEquals(entries, result)
}

@Test
fun `Given query matching app name when filtering then return matching entries`() {
val result = filterSettingEntries(entries, query = "Chrome")

assertEquals(listOf(entries[0]), result)
}

@Test
fun `Given query matching package name in label when filtering then return matching entries`() {
val result = filterSettingEntries(entries, query = "com.google")

assertEquals(listOf(entries[0]), result)
}

@Test
fun `Given case-insensitive query when filtering then return matches`() {
val result = filterSettingEntries(entries, query = "CHROME")

assertEquals(listOf(entries[0]), result)
}

@Test
fun `Given query matching no entries when filtering then return empty list`() {
val result = filterSettingEntries(entries, query = "nonexistent")

assertEquals(emptyList<Pair<String, String>>(), result)
}

@Test
fun `Given query with leading and trailing spaces when filtering then trim and match`() {
val result = filterSettingEntries(entries, query = " Chrome ")

assertEquals(listOf(entries[0]), result)
}
}
Loading