prevent adding two empty tags
This commit is contained in:
@@ -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 = ""
|
||||||
|
|||||||
Reference in New Issue
Block a user