prevent adding two empty tags

This commit is contained in:
tom5079
2025-03-08 15:34:25 -08:00
parent 47d96a6ba9
commit a9cd3db27e

View File

@@ -87,17 +87,22 @@ private fun SearchQuery.toEditableStateInternal(): EditableSearchQueryState = wh
is SearchQuery.Not -> EditableSearchQueryState.Not(query.toEditableStateInternal()) is SearchQuery.Not -> EditableSearchQueryState.Not(query.toEditableStateInternal())
} }
fun SearchQuery?.toEditableState(): EditableSearchQueryState.Root fun SearchQuery?.toEditableState(): EditableSearchQueryState.Root =
= EditableSearchQueryState.Root(this?.toEditableStateInternal()) EditableSearchQueryState.Root(this?.toEditableStateInternal())
private fun EditableSearchQueryState.Tag.toSearchQueryInternal(): SearchQuery.Tag? = private fun EditableSearchQueryState.Tag.toSearchQueryInternal(): SearchQuery.Tag? =
if (namespace.value != null || tag.value.isNotBlank()) SearchQuery.Tag(namespace.value, tag.value.lowercase().trim()) else null if (namespace.value != null || tag.value.isNotBlank()) SearchQuery.Tag(
namespace.value,
tag.value.lowercase().trim()
) else null
private fun EditableSearchQueryState.And.toSearchQueryInternal(): SearchQuery.And? = private fun EditableSearchQueryState.And.toSearchQueryInternal(): SearchQuery.And? =
queries.mapNotNull { it.toSearchQueryInternal() }.let { if (it.isNotEmpty()) SearchQuery.And(it) else null } queries.mapNotNull { it.toSearchQueryInternal() }
.let { if (it.isNotEmpty()) SearchQuery.And(it) else null }
private fun EditableSearchQueryState.Or.toSearchQueryInternal(): SearchQuery.Or? = private fun EditableSearchQueryState.Or.toSearchQueryInternal(): SearchQuery.Or? =
queries.mapNotNull { it.toSearchQueryInternal() }.let { if (it.isNotEmpty()) SearchQuery.Or(it) else null } queries.mapNotNull { it.toSearchQueryInternal() }
.let { if (it.isNotEmpty()) SearchQuery.Or(it) else null }
private fun EditableSearchQueryState.Not.toSearchQueryInternal(): SearchQuery.Not? = private fun EditableSearchQueryState.Not.toSearchQueryInternal(): SearchQuery.Not? =
query.value?.toSearchQueryInternal()?.let { SearchQuery.Not(it) } query.value?.toSearchQueryInternal()?.let { SearchQuery.Not(it) }
@@ -109,51 +114,55 @@ private fun EditableSearchQueryState.toSearchQueryInternal(): SearchQuery? = whe
is EditableSearchQueryState.Not -> this.toSearchQueryInternal() is EditableSearchQueryState.Not -> this.toSearchQueryInternal()
} }
fun EditableSearchQueryState.Root.toSearchQuery(): SearchQuery? fun EditableSearchQueryState.Root.toSearchQuery(): SearchQuery? =
= query.value?.toSearchQueryInternal() query.value?.toSearchQueryInternal()
fun coalesceTags(oldTag: EditableSearchQueryState.Tag?, newTag: EditableSearchQueryState?): EditableSearchQueryState? fun coalesceTags(
= if (oldTag != null) { oldTag: EditableSearchQueryState.Tag?,
when (newTag) { newTag: EditableSearchQueryState?,
is EditableSearchQueryState.Tag, ): EditableSearchQueryState? = if (oldTag != null) {
is EditableSearchQueryState.Not -> EditableSearchQueryState.And(listOf(oldTag, newTag)) when (newTag) {
is EditableSearchQueryState.And -> newTag.apply { queries.add(oldTag) } is EditableSearchQueryState.Tag,
is EditableSearchQueryState.Or -> newTag.apply { queries.add(oldTag) } is EditableSearchQueryState.Not,
null -> oldTag -> EditableSearchQueryState.And(listOf(oldTag, newTag))
}
} else newTag is EditableSearchQueryState.And -> newTag.apply { queries.add(oldTag) }
is EditableSearchQueryState.Or -> newTag.apply { queries.add(oldTag) }
null -> oldTag
}
} else newTag
sealed interface EditableSearchQueryState { sealed interface EditableSearchQueryState {
class Tag( class Tag(
namespace: String? = null, namespace: String? = null,
tag: String = "", tag: String = "",
expanded: Boolean = false expanded: Boolean = false,
): EditableSearchQueryState { ) : EditableSearchQueryState {
val namespace = mutableStateOf(namespace) val namespace = mutableStateOf(namespace)
val tag = mutableStateOf(tag) val tag = mutableStateOf(tag)
val expanded = mutableStateOf(expanded) val expanded = mutableStateOf(expanded)
} }
class And( class And(
queries: List<EditableSearchQueryState> = emptyList() queries: List<EditableSearchQueryState> = emptyList(),
): EditableSearchQueryState { ) : EditableSearchQueryState {
val queries = queries.toMutableStateList() val queries = queries.toMutableStateList()
} }
class Or( class Or(
queries: List<EditableSearchQueryState> = emptyList() queries: List<EditableSearchQueryState> = emptyList(),
): EditableSearchQueryState { ) : EditableSearchQueryState {
val queries = queries.toMutableStateList() val queries = queries.toMutableStateList()
} }
class Not( class Not(
query: EditableSearchQueryState? = null query: EditableSearchQueryState? = null,
): EditableSearchQueryState { ) : EditableSearchQueryState {
val query = mutableStateOf(query) val query = mutableStateOf(query)
} }
class Root( class Root(
query: EditableSearchQueryState? = null query: EditableSearchQueryState? = null,
) { ) {
val query = mutableStateOf(query) val query = mutableStateOf(query)
} }
@@ -162,7 +171,7 @@ sealed interface EditableSearchQueryState {
@Composable @Composable
fun TagSuggestionList( fun TagSuggestionList(
state: EditableSearchQueryState.Tag state: EditableSearchQueryState.Tag,
) { ) {
var suggestionList: List<Suggestion>? by remember { mutableStateOf(null) } var suggestionList: List<Suggestion>? by remember { mutableStateOf(null) }
@@ -221,7 +230,7 @@ fun EditableTagChip(
.horizontalScroll(rememberScrollState()), .horizontalScroll(rememberScrollState()),
text = tag.tag.ifBlank { stringResource(R.string.search_bar_edit_tag) } text = tag.tag.ifBlank { stringResource(R.string.search_bar_edit_tag) }
) )
} },
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -323,8 +332,8 @@ fun EditableTagChip(
value = textFieldValue, value = textFieldValue,
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
autoCorrect = false,
capitalization = KeyboardCapitalization.None, capitalization = KeyboardCapitalization.None,
autoCorrectEnabled = false,
imeAction = ImeAction.Done imeAction = ImeAction.Done
), ),
leadingIcon = { leadingIcon = {
@@ -362,10 +371,11 @@ fun EditableTagChip(
onValueChange = { newTextValue -> onValueChange = { newTextValue ->
val newTag = newTextValue.text val newTag = newTextValue.text
val possibleNamespace = newTag.dropLast(1).lowercase().trim() val possibleNamespace = newTag.dropLast(1).lowercase().trim()
tag = if (namespace == null && newTag.endsWith(':') && possibleNamespace in validNamespace) { tag =
namespace = possibleNamespace if (namespace == null && newTag.endsWith(':') && possibleNamespace in validNamespace) {
"" namespace = possibleNamespace
} else newTag ""
} else newTag
selection = newTextValue.selection selection = newTextValue.selection
composition = newTextValue.composition composition = newTextValue.composition
} }
@@ -382,7 +392,7 @@ fun EditableTagChip(
@Composable @Composable
fun NewQueryChip( fun NewQueryChip(
currentQuery: EditableSearchQueryState?, currentQuery: EditableSearchQueryState?,
onNewQuery: (EditableSearchQueryState) -> Unit onNewQuery: (EditableSearchQueryState) -> Unit,
) { ) {
var opened by remember { mutableStateOf(false) } var opened by remember { mutableStateOf(false) }
@@ -391,7 +401,7 @@ fun NewQueryChip(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: ImageVector = Icons.Default.AddCircleOutline, icon: ImageVector = Icons.Default.AddCircleOutline,
text: String, text: String,
onClick: () -> Unit onClick: () -> Unit,
) { ) {
Row( Row(
modifier = modifier modifier = modifier
@@ -415,7 +425,7 @@ fun NewQueryChip(
} }
Surface(shape = RoundedCornerShape(16.dp)) { Surface(shape = RoundedCornerShape(16.dp)) {
AnimatedContent(targetState = opened, label = "add new query" ) { targetOpened -> AnimatedContent(targetState = opened, label = "add new query") { targetOpened ->
if (targetOpened) { if (targetOpened) {
Column { Column {
NewQueryRow( NewQueryRow(
@@ -426,8 +436,11 @@ fun NewQueryChip(
opened = false opened = false
} }
HorizontalDivider() HorizontalDivider()
if (currentQuery !is EditableSearchQueryState.Tag && currentQuery !is EditableSearchQueryState.And) { if (currentQuery != null && currentQuery !is EditableSearchQueryState.Tag && currentQuery !is EditableSearchQueryState.And) {
NewQueryRow(modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.search_add_query_item_tag)) { NewQueryRow(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.search_add_query_item_tag)
) {
opened = false opened = false
onNewQuery(EditableSearchQueryState.Tag(expanded = true)) onNewQuery(EditableSearchQueryState.Tag(expanded = true))
} }
@@ -489,6 +502,7 @@ fun QueryEditorQueryView(
} }
) )
} }
is EditableSearchQueryState.Or -> { is EditableSearchQueryState.Or -> {
Card( Card(
colors = CardColors( colors = CardColors(
@@ -510,7 +524,11 @@ fun QueryEditorQueryView(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text("OR", modifier = Modifier.padding(horizontal = 8.dp), style = MaterialTheme.typography.labelMedium) Text(
"OR",
modifier = Modifier.padding(horizontal = 8.dp),
style = MaterialTheme.typography.labelMedium
)
Icon( Icon(
modifier = Modifier modifier = Modifier
.size(16.dp) .size(16.dp)
@@ -520,7 +538,9 @@ fun QueryEditorQueryView(
) )
} }
state.queries.forEachIndexed { index, subQueryState -> state.queries.forEachIndexed { index, subQueryState ->
if (index != 0) { Text("+", modifier = Modifier.padding(horizontal = 8.dp)) } if (index != 0) {
Text("+", modifier = Modifier.padding(horizontal = 8.dp))
}
QueryEditorQueryView( QueryEditorQueryView(
subQueryState, subQueryState,
onQueryRemove = { state.queries.remove(it) }, onQueryRemove = { state.queries.remove(it) },
@@ -534,6 +554,7 @@ fun QueryEditorQueryView(
} }
} }
} }
is EditableSearchQueryState.And -> { is EditableSearchQueryState.And -> {
Card( Card(
colors = CardColors( colors = CardColors(
@@ -560,7 +581,12 @@ fun QueryEditorQueryView(
LaunchedEffect(newQueryExpanded) { LaunchedEffect(newQueryExpanded) {
if (!newQueryExpanded && (newQueryNamespace != null || newQueryTag.isNotBlank())) { if (!newQueryExpanded && (newQueryNamespace != null || newQueryTag.isNotBlank())) {
state.queries.add(EditableSearchQueryState.Tag(newQueryNamespace, newQueryTag)) state.queries.add(
EditableSearchQueryState.Tag(
newQueryNamespace,
newQueryTag
)
)
newQueryNamespace = null newQueryNamespace = null
newQueryTag = "" newQueryTag = ""
newQueryExpanded = true newQueryExpanded = true
@@ -573,7 +599,11 @@ fun QueryEditorQueryView(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text("AND", modifier = Modifier.padding(horizontal = 8.dp), style = MaterialTheme.typography.labelMedium) Text(
"AND",
modifier = Modifier.padding(horizontal = 8.dp),
style = MaterialTheme.typography.labelMedium
)
Icon( Icon(
modifier = Modifier modifier = Modifier
.size(16.dp) .size(16.dp)
@@ -600,6 +630,7 @@ fun QueryEditorQueryView(
} }
} }
} }
is EditableSearchQueryState.Not -> { is EditableSearchQueryState.Not -> {
var subQueryState by state.query var subQueryState by state.query
@@ -623,7 +654,11 @@ fun QueryEditorQueryView(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text("-", modifier = Modifier.padding(horizontal = 8.dp), style = MaterialTheme.typography.labelMedium) Text(
"-",
modifier = Modifier.padding(horizontal = 8.dp),
style = MaterialTheme.typography.labelMedium
)
Icon( Icon(
modifier = Modifier modifier = Modifier
.size(16.dp) .size(16.dp)
@@ -661,14 +696,14 @@ fun QueryEditorQueryView(
@Composable @Composable
fun QueryEditor( fun QueryEditor(
state: EditableSearchQueryState.Root state: EditableSearchQueryState.Root,
) { ) {
var rootQuery by state.query var rootQuery by state.query
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
var topY by remember { mutableFloatStateOf(0f) } var topY by remember { mutableFloatStateOf(0f) }
val scrollOffset = with (LocalDensity.current) { 16.dp.toPx() } val scrollOffset = with(LocalDensity.current) { 16.dp.toPx() }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -687,7 +722,10 @@ fun QueryEditor(
val topYSnapshot = topY val topYSnapshot = topY
coroutineScope.launch { coroutineScope.launch {
scrollState.animateScrollBy(target - topYSnapshot - scrollOffset, spring(stiffness = Spring.StiffnessLow)) scrollState.animateScrollBy(
target - topYSnapshot - scrollOffset,
spring(stiffness = Spring.StiffnessLow)
)
} }
} }
@@ -713,17 +751,19 @@ fun QueryEditor(
var newQueryTag by newSearchQuery.tag var newQueryTag by newSearchQuery.tag
var newQueryExpanded by newSearchQuery.expanded var newQueryExpanded by newSearchQuery.expanded
val offset = with (LocalDensity.current) { 40.dp.toPx() } val offset = with(LocalDensity.current) { 40.dp.toPx() }
LaunchedEffect(newQueryExpanded) { LaunchedEffect(newQueryExpanded) {
if (!newQueryExpanded && (newQueryNamespace != null || newQueryTag.isNotBlank())) { if (!newQueryExpanded && (newQueryNamespace != null || newQueryTag.isNotBlank())) {
rootQuery = if (rootQuerySnapshot == null) { rootQuery = if (rootQuerySnapshot == null) {
EditableSearchQueryState.Tag(newQueryNamespace, newQueryTag) EditableSearchQueryState.Tag(newQueryNamespace, newQueryTag)
} else { } else {
EditableSearchQueryState.And(listOf( EditableSearchQueryState.And(
rootQuerySnapshot, listOf(
EditableSearchQueryState.Tag(newQueryNamespace, newQueryTag) rootQuerySnapshot,
)) EditableSearchQueryState.Tag(newQueryNamespace, newQueryTag)
)
)
} }
newQueryNamespace = null newQueryNamespace = null
newQueryTag = "" newQueryTag = ""