From 62077840d1a212cae0b8cb4980a241f1d573a419 Mon Sep 17 00:00:00 2001 From: gabriel20xx Date: Fri, 31 Oct 2025 22:12:21 +0100 Subject: [PATCH] feat: filter out deleted fields in ListDetailScreen and webRoutes for improved data integrity --- .../com/collabtable/app/data/model/Field.kt | 15 ++-- .../app/ui/screens/ListDetailScreen.kt | 34 +++++++-- .../app/ui/screens/ListDetailViewModel.kt | 76 ++++++++++++++----- CollabTableServer/src/routes/webRoutes.ts | 9 ++- 4 files changed, 98 insertions(+), 36 deletions(-) diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/model/Field.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/model/Field.kt index cee1837..15a692a 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/model/Field.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/model/Field.kt @@ -69,11 +69,12 @@ data class Field( val isDeleted: Boolean = false, ) { fun getType(): FieldType { + val raw = fieldType.ifBlank { "TEXT" }.uppercase() return try { - FieldType.valueOf(fieldType) + FieldType.valueOf(raw) } catch (e: Exception) { - // Handle legacy field types - when (fieldType) { + // Handle legacy/synonym field types + when (raw) { "STRING" -> FieldType.TEXT "PRICE" -> FieldType.CURRENCY "AMOUNT" -> FieldType.NUMBER @@ -84,7 +85,7 @@ data class Field( } fun getDropdownOptions(): List { - return if (fieldType == "DROPDOWN" && fieldOptions.isNotBlank()) { + return if (getType() == FieldType.DROPDOWN && fieldOptions.isNotBlank()) { fieldOptions.split("|") } else { emptyList() @@ -92,7 +93,7 @@ data class Field( } fun getCurrency(): String { - return if ((fieldType == "CURRENCY" || fieldType == "PRICE") && fieldOptions.isNotBlank()) { + return if ((getType() == FieldType.CURRENCY) && fieldOptions.isNotBlank()) { fieldOptions } else { "CHF" @@ -100,7 +101,7 @@ data class Field( } fun getMaxRating(): Int { - return if (fieldType == "RATING" && fieldOptions.isNotBlank()) { + return if (getType() == FieldType.RATING && fieldOptions.isNotBlank()) { fieldOptions.toIntOrNull() ?: 5 } else { 5 @@ -108,7 +109,7 @@ data class Field( } fun getAutocompleteOptions(): List { - return if (fieldType == "AUTOCOMPLETE" && fieldOptions.isNotBlank()) { + return if (getType() == FieldType.AUTOCOMPLETE && fieldOptions.isNotBlank()) { fieldOptions.split("|") } else { emptyList() diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt index 6cccdb2..58fcdcf 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt @@ -1345,13 +1345,23 @@ fun AddFieldDialog( "CURRENCY" -> "Currency" "PERCENTAGE" -> "Percentage" "DROPDOWN" -> "Dropdown" + "AUTOCOMPLETE" -> "Autocomplete" "CHECKBOX" -> "Checkbox" + "SWITCH" -> "Switch" "URL" -> "URL" "EMAIL" -> "Email" "PHONE" -> "Phone" "DATE" -> "Date" "TIME" -> "Time" "DATETIME" -> "Date & Time" + "DURATION" -> "Duration" + "IMAGE" -> "Image" + "FILE" -> "File" + "BARCODE" -> "Barcode" + "SIGNATURE" -> "Signature" + "RATING" -> "Rating" + "COLOR" -> "Color" + "LOCATION" -> "Location" else -> "Text" }, onValueChange = {}, @@ -1599,8 +1609,9 @@ fun AddFieldDialog( TextButton( onClick = { if (fieldName.isNotBlank()) { + val normalizedType = selectedFieldType.uppercase() val options = - when (selectedFieldType) { + when (normalizedType) { "CURRENCY" -> currency.trim() "DROPDOWN" -> dropdownOptions.split(",") @@ -1615,12 +1626,12 @@ fun AddFieldDialog( "RATING" -> currency.trim().ifBlank { "5" } else -> "" } - onAdd(fieldName.trim(), selectedFieldType, options) + onAdd(fieldName.trim(), normalizedType, options) } }, enabled = fieldName.isNotBlank() && - (selectedFieldType !in listOf("DROPDOWN", "AUTOCOMPLETE") || dropdownOptions.isNotBlank()), + (selectedFieldType.uppercase() !in listOf("DROPDOWN", "AUTOCOMPLETE") || dropdownOptions.isNotBlank()), ) { Text(stringResource(R.string.add)) } @@ -1641,7 +1652,7 @@ fun EditFieldDialog( onUpdate: (String, String, String) -> Unit, ) { var name by remember { mutableStateOf(field.name) } - var selectedFieldType by remember { mutableStateOf(field.fieldType ?: "TEXT") } + var selectedFieldType by remember { mutableStateOf(field.fieldType?.uppercase() ?: "TEXT") } var dropdownOptions by remember { mutableStateOf(field.getDropdownOptions().joinToString(", ")) } var currency by remember { mutableStateOf(field.getCurrency()) } var expanded by remember { mutableStateOf(false) } @@ -1676,13 +1687,23 @@ fun EditFieldDialog( "CURRENCY", "PRICE" -> "Currency" "PERCENTAGE" -> "Percentage" "DROPDOWN" -> "Dropdown" + "AUTOCOMPLETE" -> "Autocomplete" "CHECKBOX" -> "Checkbox" + "SWITCH" -> "Switch" "URL" -> "URL" "EMAIL" -> "Email" "PHONE" -> "Phone" "DATE" -> "Date" "TIME" -> "Time" "DATETIME" -> "Date & Time" + "DURATION" -> "Duration" + "IMAGE" -> "Image" + "FILE" -> "File" + "BARCODE" -> "Barcode" + "SIGNATURE" -> "Signature" + "RATING" -> "Rating" + "COLOR" -> "Color" + "LOCATION" -> "Location" else -> "Text" }, onValueChange = {}, @@ -1929,8 +1950,9 @@ fun EditFieldDialog( confirmButton = { TextButton( onClick = { + val normalizedType = selectedFieldType.uppercase() val options = - when (selectedFieldType) { + when (normalizedType) { "CURRENCY", "PRICE" -> currency.trim() "DROPDOWN" -> dropdownOptions.split(",") @@ -1945,7 +1967,7 @@ fun EditFieldDialog( "RATING" -> currency.trim().ifBlank { "5" } else -> "" } - onUpdate(name.trim(), selectedFieldType, options) + onUpdate(name.trim(), normalizedType, options) }, enabled = name.isNotBlank() && (selectedFieldType !in listOf("DROPDOWN", "AUTOCOMPLETE") || dropdownOptions.isNotBlank()), ) { diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt index d1417ac..a5286dc 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt @@ -130,12 +130,15 @@ class ListDetailViewModel( emptyList() } - // Insert atomically to avoid intermediate inconsistent states + // Insert atomically to avoid intermediate inconsistent states and bump list.updatedAt database.withTransaction { database.fieldDao().insertField(newField) if (newValues.isNotEmpty()) { database.itemValueDao().insertValues(newValues) } + database.listDao().getListById(listId)?.let { l -> + database.listDao().updateList(l.copy(updatedAt = timestamp)) + } } performSync() @@ -144,7 +147,13 @@ class ListDetailViewModel( fun deleteField(fieldId: String) { viewModelScope.launch { - database.fieldDao().softDeleteField(fieldId, System.currentTimeMillis()) + val ts = System.currentTimeMillis() + database.withTransaction { + database.fieldDao().softDeleteField(fieldId, ts) + database.listDao().getListById(listId)?.let { l -> + database.listDao().updateList(l.copy(updatedAt = ts)) + } + } performSync() } } @@ -158,14 +167,20 @@ class ListDetailViewModel( viewModelScope.launch { val field = database.fieldDao().getFieldById(fieldId) if (field != null) { - database.fieldDao().updateField( - field.copy( - name = name, - fieldType = fieldType, - fieldOptions = fieldOptions, - updatedAt = System.currentTimeMillis(), - ), - ) + val ts = System.currentTimeMillis() + database.withTransaction { + database.fieldDao().updateField( + field.copy( + name = name, + fieldType = fieldType, + fieldOptions = fieldOptions, + updatedAt = ts, + ), + ) + database.listDao().getListById(listId)?.let { l -> + database.listDao().updateList(l.copy(updatedAt = ts)) + } + } performSync() } } @@ -198,6 +213,9 @@ class ListDetailViewModel( if (values.isNotEmpty()) { database.itemValueDao().insertValues(values) } + database.listDao().getListById(listId)?.let { l -> + database.listDao().updateList(l.copy(updatedAt = timestamp)) + } } performSync() } @@ -230,6 +248,9 @@ class ListDetailViewModel( if (values.isNotEmpty()) { database.itemValueDao().insertValues(values) } + database.listDao().getListById(listId)?.let { l -> + database.listDao().updateList(l.copy(updatedAt = timestamp)) + } } performSync() } @@ -242,12 +263,18 @@ class ListDetailViewModel( viewModelScope.launch { val itemValue = database.itemValueDao().getValueById(itemValueId) if (itemValue != null) { - database.itemValueDao().updateValue( - itemValue.copy( - value = newValue, - updatedAt = System.currentTimeMillis(), - ), - ) + val ts = System.currentTimeMillis() + database.withTransaction { + database.itemValueDao().updateValue( + itemValue.copy( + value = newValue, + updatedAt = ts, + ), + ) + database.listDao().getListById(listId)?.let { l -> + database.listDao().updateList(l.copy(updatedAt = ts)) + } + } performSync() } } @@ -255,7 +282,13 @@ class ListDetailViewModel( fun deleteItem(itemId: String) { viewModelScope.launch { - database.itemDao().softDeleteItem(itemId, System.currentTimeMillis()) + val ts = System.currentTimeMillis() + database.withTransaction { + database.itemDao().softDeleteItem(itemId, ts) + database.listDao().getListById(listId)?.let { l -> + database.listDao().updateList(l.copy(updatedAt = ts)) + } + } performSync() } } @@ -263,8 +296,13 @@ class ListDetailViewModel( fun reorderFields(reorderedFields: List) { viewModelScope.launch { val timestamp = System.currentTimeMillis() - // Use DAO-level transaction for clarity - database.fieldDao().reorderFieldsInTransaction(reorderedFields, timestamp) + // Reorder within a transaction and bump list.updatedAt + database.withTransaction { + database.fieldDao().reorderFieldsInTransaction(reorderedFields, timestamp) + database.listDao().getListById(listId)?.let { l -> + database.listDao().updateList(l.copy(updatedAt = timestamp)) + } + } performSync() } } diff --git a/CollabTableServer/src/routes/webRoutes.ts b/CollabTableServer/src/routes/webRoutes.ts index d73f468..b7b2747 100644 --- a/CollabTableServer/src/routes/webRoutes.ts +++ b/CollabTableServer/src/routes/webRoutes.ts @@ -262,7 +262,8 @@ router.get('/', (req: Request, res: Response) => { } container.innerHTML = lists.map(list => { - const listFields = fields.filter(f => f.listId === list.id); + // Only show non-deleted fields in tags and columns + const listFields = fields.filter(f => f.listId === list.id && !f.isDeleted); const listItems = items.filter(i => i.listId === list.id); return \` @@ -282,7 +283,7 @@ router.get('/', (req: Request, res: Response) => {
Fields (\${listFields.length}):
\${listFields.length > 0 - ? listFields.map(f => \`\${f.name} (\${f.fieldType})\`).join('') + ? listFields.map(f => \`\${f.name} (\${f.fieldType})\`).join('') : 'No fields' }
@@ -343,9 +344,9 @@ router.get('/web/data', async (req: Request, res: Response) => { res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); - // Exclude deleted lists and items from web UI + // Exclude deleted lists/items/fields from web UI const lists = await dbAdapter.queryAll('SELECT * FROM lists WHERE isDeleted = 0 ORDER BY updatedAt DESC'); - const fields = await dbAdapter.queryAll('SELECT * FROM fields ORDER BY listId ASC, "order" ASC'); + const fields = await dbAdapter.queryAll('SELECT * FROM fields WHERE isDeleted = 0 ORDER BY listId ASC, "order" ASC'); const items = await dbAdapter.queryAll('SELECT * FROM items WHERE isDeleted = 0 ORDER BY listId ASC, updatedAt DESC'); const values = await dbAdapter.queryAll('SELECT * FROM item_values');