feat: update field type handling and add multi-select support

This commit is contained in:
2026-03-15 17:18:00 +01:00
parent 01da9b14cf
commit 3f6b56a3c3
3 changed files with 111 additions and 30 deletions
@@ -47,6 +47,9 @@ interface FieldDao {
@Query("SELECT * FROM fields WHERE updatedAt >= :since") @Query("SELECT * FROM fields WHERE updatedAt >= :since")
suspend fun getFieldsUpdatedSince(since: Long): List<Field> suspend fun getFieldsUpdatedSince(since: Long): List<Field>
@Query("UPDATE fields SET `order` = :newOrder, updatedAt = :timestamp WHERE id = :fieldId")
suspend fun updateFieldOrder(fieldId: String, newOrder: Int, timestamp: Long)
// Reorder fields in a single transaction for clarity and consistency // Reorder fields in a single transaction for clarity and consistency
@Transaction @Transaction
suspend fun reorderFieldsInTransaction( suspend fun reorderFieldsInTransaction(
@@ -54,12 +57,7 @@ interface FieldDao {
timestamp: Long, timestamp: Long,
) { ) {
reorderedFields.forEachIndexed { index, field -> reorderedFields.forEachIndexed { index, field ->
updateField( updateFieldOrder(field.id, index, timestamp)
field.copy(
order = index,
updatedAt = timestamp,
),
)
} }
} }
} }
@@ -16,7 +16,8 @@ enum class FieldType {
PERCENTAGE, PERCENTAGE,
// Selection types // Selection types
DROPDOWN, SELECT,
MULTI_SELECT,
AUTOCOMPLETE, AUTOCOMPLETE,
CHECKBOX, CHECKBOX,
SWITCH, SWITCH,
@@ -77,6 +78,7 @@ data class Field(
} catch (e: Exception) { } catch (e: Exception) {
// Handle legacy/synonym field types // Handle legacy/synonym field types
when (raw) { when (raw) {
"DROPDOWN" -> FieldType.SELECT
"STRING" -> FieldType.TEXT "STRING" -> FieldType.TEXT
"PRICE" -> FieldType.CURRENCY "PRICE" -> FieldType.CURRENCY
"AMOUNT" -> FieldType.NUMBER "AMOUNT" -> FieldType.NUMBER
@@ -86,8 +88,8 @@ data class Field(
} }
} }
fun getDropdownOptions(): List<String> = fun getSelectOptions(): List<String> =
if (getType() == FieldType.DROPDOWN && fieldOptions.isNotBlank()) { if ((getType() == FieldType.SELECT || getType() == FieldType.MULTI_SELECT) && fieldOptions.isNotBlank()) {
fieldOptions.split("|") fieldOptions.split("|")
} else { } else {
emptyList() emptyList()
@@ -804,7 +804,8 @@ private fun getDisplayTextForMeasure(
com.collabtable.app.data.model.FieldType.TEXT, com.collabtable.app.data.model.FieldType.TEXT,
com.collabtable.app.data.model.FieldType.MULTILINE_TEXT, com.collabtable.app.data.model.FieldType.MULTILINE_TEXT,
com.collabtable.app.data.model.FieldType.NUMBER, com.collabtable.app.data.model.FieldType.NUMBER,
com.collabtable.app.data.model.FieldType.DROPDOWN, com.collabtable.app.data.model.FieldType.SELECT,
com.collabtable.app.data.model.FieldType.MULTI_SELECT,
com.collabtable.app.data.model.FieldType.AUTOCOMPLETE, com.collabtable.app.data.model.FieldType.AUTOCOMPLETE,
com.collabtable.app.data.model.FieldType.DURATION, com.collabtable.app.data.model.FieldType.DURATION,
com.collabtable.app.data.model.FieldType.LOCATION, com.collabtable.app.data.model.FieldType.LOCATION,
@@ -848,7 +849,8 @@ private fun fieldTypeToLabel(type: String): String =
"NUMBER" -> "Number" "NUMBER" -> "Number"
"CURRENCY", "PRICE" -> "Currency" "CURRENCY", "PRICE" -> "Currency"
"PERCENTAGE" -> "Percentage" "PERCENTAGE" -> "Percentage"
"DROPDOWN" -> "Dropdown" "SELECT", "Select" -> "Select"
"MULTI_SELECT" -> "Multi-select"
"AUTOCOMPLETE" -> "Autocomplete" "AUTOCOMPLETE" -> "Autocomplete"
"CHECKBOX" -> "Checkbox" "CHECKBOX" -> "Checkbox"
"SWITCH" -> "Switch" "SWITCH" -> "Switch"
@@ -1102,7 +1104,8 @@ fun ItemRow(
) )
} }
com.collabtable.app.data.model.FieldType.DROPDOWN -> { com.collabtable.app.data.model.FieldType.SELECT,
com.collabtable.app.data.model.FieldType.MULTI_SELECT -> {
Text( Text(
text = value?.value ?: "", text = value?.value ?: "",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
@@ -1576,9 +1579,16 @@ fun AddFieldDialog(
// Selection types // Selection types
DropdownMenuItem( DropdownMenuItem(
text = { Text("Dropdown") }, text = { Text("Select") },
onClick = { onClick = {
selectedFieldType = "DROPDOWN" selectedFieldType = "SELECT"
expanded = false
},
)
DropdownMenuItem(
text = { Text("Multi-select") },
onClick = {
selectedFieldType = "MULTI_SELECT"
expanded = false expanded = false
}, },
) )
@@ -1725,14 +1735,14 @@ fun AddFieldDialog(
} }
// Dropdown-specific options // Dropdown-specific options
if (selectedFieldType == "DROPDOWN") { if (selectedFieldType == "SELECT" || selectedFieldType == "MULTI_SELECT") {
OutlinedTextField( OutlinedTextField(
value = dropdownOptions, value = dropdownOptions,
onValueChange = { dropdownOptions = it }, onValueChange = { dropdownOptions = it },
label = { Text("Options (comma-separated)") }, label = { Text("Options (comma-separated)") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Option 1, Option 2, Option 3") }, placeholder = { Text("Option 1, Option 2, Option 3") },
supportingText = { Text("Enter dropdown choices separated by commas") }, supportingText = { Text("Enter select choices separated by commas") },
) )
} }
@@ -1770,7 +1780,7 @@ fun AddFieldDialog(
val options = val options =
when (normalizedType) { when (normalizedType) {
"CURRENCY" -> currency.trim() "CURRENCY" -> currency.trim()
"DROPDOWN" -> "SELECT", "MULTI_SELECT" ->
dropdownOptions dropdownOptions
.split(",") .split(",")
.map { it.trim() } .map { it.trim() }
@@ -1790,7 +1800,7 @@ fun AddFieldDialog(
}, },
enabled = enabled =
fieldName.isNotBlank() && fieldName.isNotBlank() &&
(selectedFieldType.uppercase() !in listOf("DROPDOWN", "AUTOCOMPLETE") || dropdownOptions.isNotBlank()), (selectedFieldType.uppercase() !in listOf("SELECT", "MULTI_SELECT", "AUTOCOMPLETE") || dropdownOptions.isNotBlank()),
) { ) {
Text(stringResource(R.string.add)) Text(stringResource(R.string.add))
} }
@@ -1812,7 +1822,7 @@ fun EditFieldDialog(
) { ) {
var name by remember { mutableStateOf(field.name) } var name by remember { mutableStateOf(field.name) }
var selectedFieldType by remember { mutableStateOf(field.fieldType.uppercase()) } var selectedFieldType by remember { mutableStateOf(field.fieldType.uppercase()) }
var dropdownOptions by remember { mutableStateOf(field.getDropdownOptions().joinToString(", ")) } var dropdownOptions by remember { mutableStateOf(field.getSelectOptions().joinToString(", ")) }
var currency by remember { mutableStateOf(field.getCurrency()) } var currency by remember { mutableStateOf(field.getCurrency()) }
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
// Alignment state persisted per list/field in PreferencesManager // Alignment state persisted per list/field in PreferencesManager
@@ -1902,9 +1912,16 @@ fun EditFieldDialog(
// Selection types // Selection types
DropdownMenuItem( DropdownMenuItem(
text = { Text("Dropdown") }, text = { Text("Select") },
onClick = { onClick = {
selectedFieldType = "DROPDOWN" selectedFieldType = "SELECT"
expanded = false
},
)
DropdownMenuItem(
text = { Text("Multi-select") },
onClick = {
selectedFieldType = "MULTI_SELECT"
expanded = false expanded = false
}, },
) )
@@ -2051,14 +2068,14 @@ fun EditFieldDialog(
} }
// Dropdown-specific options // Dropdown-specific options
if (selectedFieldType == "DROPDOWN") { if (selectedFieldType == "SELECT" || selectedFieldType == "MULTI_SELECT") {
OutlinedTextField( OutlinedTextField(
value = dropdownOptions, value = dropdownOptions,
onValueChange = { dropdownOptions = it }, onValueChange = { dropdownOptions = it },
label = { Text("Options (comma-separated)") }, label = { Text("Options (comma-separated)") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Option 1, Option 2, Option 3") }, placeholder = { Text("Option 1, Option 2, Option 3") },
supportingText = { Text("Enter dropdown choices separated by commas") }, supportingText = { Text("Enter select choices separated by commas") },
) )
} }
@@ -2113,7 +2130,7 @@ fun EditFieldDialog(
val options = val options =
when (normalizedType) { when (normalizedType) {
"CURRENCY", "PRICE" -> currency.trim() "CURRENCY", "PRICE" -> currency.trim()
"DROPDOWN" -> "SELECT", "MULTI_SELECT" ->
dropdownOptions dropdownOptions
.split(",") .split(",")
.map { it.trim() } .map { it.trim() }
@@ -2139,7 +2156,7 @@ fun EditFieldDialog(
} }
prefs.setColumnAlignments(field.listId, current) prefs.setColumnAlignments(field.listId, current)
}, },
enabled = name.isNotBlank() && (selectedFieldType !in listOf("DROPDOWN", "AUTOCOMPLETE") || dropdownOptions.isNotBlank()), enabled = name.isNotBlank() && (selectedFieldType !in listOf("SELECT", "MULTI_SELECT", "AUTOCOMPLETE") || dropdownOptions.isNotBlank()),
) { ) {
Text("Update") Text("Update")
} }
@@ -2306,9 +2323,9 @@ fun FieldInput(
} }
} }
com.collabtable.app.data.model.FieldType.DROPDOWN -> { com.collabtable.app.data.model.FieldType.SELECT -> {
val options = field.getDropdownOptions() val options = field.getSelectOptions()
var dropdownExpanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox( ExposedDropdownMenuBox(
expanded = dropdownExpanded, expanded = dropdownExpanded,
@@ -2345,6 +2362,60 @@ fun FieldInput(
} }
} }
com.collabtable.app.data.model.FieldType.MULTI_SELECT -> {
val options = field.getSelectOptions()
var expanded by remember { mutableStateOf(false) }
val selectedOptions = remember(value) {
value.split(", ").filter { it.isNotBlank() }.toSet()
}
ExposedDropdownMenuBox(
expanded = dropdownExpanded,
onExpandedChange = { dropdownExpanded = !dropdownExpanded },
) {
OutlinedTextField(
value = value,
onValueChange = {},
readOnly = true,
label = { Text(field.name) },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownExpanded)
},
modifier =
Modifier
.menuAnchor()
.fillMaxWidth(),
)
ExposedDropdownMenu(
expanded = dropdownExpanded,
onDismissRequest = { dropdownExpanded = false },
) {
options.forEach { option ->
val isSelected = selectedOptions.contains(option)
DropdownMenuItem(
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = isSelected, onCheckedChange = null)
Spacer(modifier = Modifier.width(8.dp))
Text(option)
}
},
onClick = {
val newSelected = if (isSelected) {
selectedOptions - option
} else {
selectedOptions + option
}
val orderedNewSelected = options.filter { it in newSelected }
onValueChange(orderedNewSelected.joinToString(", "))
},
)
}
}
}
}
com.collabtable.app.data.model.FieldType.URL -> { com.collabtable.app.data.model.FieldType.URL -> {
OutlinedTextField( OutlinedTextField(
value = value, value = value,
@@ -3070,8 +3141,10 @@ fun ColumnItem(
// Selection types // Selection types
com.collabtable.app.data.model.FieldType.CHECKBOX -> "Checkbox" com.collabtable.app.data.model.FieldType.CHECKBOX -> "Checkbox"
com.collabtable.app.data.model.FieldType.SWITCH -> "Switch" com.collabtable.app.data.model.FieldType.SWITCH -> "Switch"
com.collabtable.app.data.model.FieldType.DROPDOWN -> com.collabtable.app.data.model.FieldType.SELECT ->
"Dropdown (${field.getDropdownOptions().size} options)" "Select (${field.getSelectOptions().size} options)"
com.collabtable.app.data.model.FieldType.MULTI_SELECT ->
"Multi-select (${field.getSelectOptions().size} options)"
com.collabtable.app.data.model.FieldType.AUTOCOMPLETE -> com.collabtable.app.data.model.FieldType.AUTOCOMPLETE ->
"Autocomplete (${field.getAutocompleteOptions().size} options)" "Autocomplete (${field.getAutocompleteOptions().size} options)"
@@ -4011,3 +4084,11 @@ private fun RenameListDialog(
}, },
) )
} }