feat: Refactor DAO insert methods to use Upsert and add transaction support for field reordering
This commit is contained in:
@@ -12,10 +12,10 @@ interface FieldDao {
|
||||
@Query("SELECT * FROM fields WHERE id = :fieldId")
|
||||
suspend fun getFieldById(fieldId: String): Field?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
@Upsert
|
||||
suspend fun insertField(field: Field)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
@Upsert
|
||||
suspend fun insertFields(fields: List<Field>)
|
||||
|
||||
@Update
|
||||
@@ -30,4 +30,17 @@ interface FieldDao {
|
||||
// Get all fields updated since timestamp, including deleted ones for sync
|
||||
@Query("SELECT * FROM fields WHERE updatedAt >= :since")
|
||||
suspend fun getFieldsUpdatedSince(since: Long): List<Field>
|
||||
|
||||
// Reorder fields in a single transaction for clarity and consistency
|
||||
@Transaction
|
||||
suspend fun reorderFieldsInTransaction(reorderedFields: List<Field>, timestamp: Long) {
|
||||
reorderedFields.forEachIndexed { index, field ->
|
||||
updateField(
|
||||
field.copy(
|
||||
order = index,
|
||||
updatedAt = timestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ interface ItemDao {
|
||||
@Query("SELECT * FROM items WHERE id = :itemId")
|
||||
suspend fun getItemById(itemId: String): Item?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
@Upsert
|
||||
suspend fun insertItem(item: Item)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
@Upsert
|
||||
suspend fun insertItems(items: List<Item>)
|
||||
|
||||
@Update
|
||||
|
||||
@@ -12,10 +12,10 @@ interface ItemValueDao {
|
||||
@Query("SELECT * FROM item_values WHERE id = :valueId")
|
||||
suspend fun getValueById(valueId: String): ItemValue?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
@Upsert
|
||||
suspend fun insertValue(value: ItemValue)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
@Upsert
|
||||
suspend fun insertValues(values: List<ItemValue>)
|
||||
|
||||
@Update
|
||||
|
||||
@@ -17,10 +17,10 @@ interface ListDao {
|
||||
@Query("SELECT * FROM lists WHERE id = :listId")
|
||||
suspend fun getListById(listId: String): CollabList?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
@Upsert
|
||||
suspend fun insertList(list: CollabList)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
@Upsert
|
||||
suspend fun insertLists(lists: List<CollabList>)
|
||||
|
||||
@Update
|
||||
|
||||
+57
-38
@@ -66,6 +66,10 @@ fun ListDetailScreen(
|
||||
val fields by viewModel.fields.collectAsState()
|
||||
val items by viewModel.items.collectAsState()
|
||||
|
||||
// Use derivedStateOf to create stable references
|
||||
val stableFields by remember { derivedStateOf { fields } }
|
||||
val stableItems by remember { derivedStateOf { items } }
|
||||
|
||||
var showManageColumnsDialog by remember { mutableStateOf(false) }
|
||||
var showAddItemDialog by remember { mutableStateOf(false) }
|
||||
var itemToEdit by remember { mutableStateOf<ItemWithValues?>(null) }
|
||||
@@ -88,8 +92,8 @@ fun ListDetailScreen(
|
||||
val fieldWidths = remember { mutableStateMapOf<String, Dp>() }
|
||||
|
||||
// Initialize field widths
|
||||
LaunchedEffect(fields) {
|
||||
fields.forEach { field ->
|
||||
LaunchedEffect(stableFields) {
|
||||
stableFields.forEach { field ->
|
||||
if (!fieldWidths.containsKey(field.id)) {
|
||||
fieldWidths[field.id] = 150.dp
|
||||
}
|
||||
@@ -97,8 +101,8 @@ fun ListDetailScreen(
|
||||
}
|
||||
|
||||
// Apply filtering, sorting, and grouping
|
||||
val processedItems = remember(items, filterField, filterValue, sortField, sortAscending, groupByField) {
|
||||
var result = items
|
||||
val processedItems = remember(stableItems, stableFields, filterField, filterValue, sortField, sortAscending, groupByField) {
|
||||
var result = stableItems
|
||||
|
||||
// Apply filter
|
||||
if (filterField != null && filterValue.isNotBlank()) {
|
||||
@@ -174,7 +178,7 @@ fun ListDetailScreen(
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
if (fields.isEmpty()) {
|
||||
if (stableFields.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -329,21 +333,23 @@ fun ListDetailScreen(
|
||||
.horizontalScroll(horizontalScrollState),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
fields.forEach { field ->
|
||||
FieldHeader(
|
||||
field = field,
|
||||
width = fieldWidths[field.id] ?: 150.dp,
|
||||
onWidthChange = { delta ->
|
||||
val currentWidth = fieldWidths[field.id] ?: 150.dp
|
||||
val newWidth = (currentWidth.value + delta).coerceIn(100f, 400f)
|
||||
fieldWidths[field.id] = newWidth.dp
|
||||
}
|
||||
)
|
||||
stableFields.forEach { field ->
|
||||
key(field.id) {
|
||||
FieldHeader(
|
||||
field = field,
|
||||
width = fieldWidths[field.id] ?: 150.dp,
|
||||
onWidthChange = { delta ->
|
||||
val currentWidth = fieldWidths[field.id] ?: 150.dp
|
||||
val newWidth = (currentWidth.value + delta).coerceIn(100f, 400f)
|
||||
fieldWidths[field.id] = newWidth.dp
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Items list with synchronized scrolling
|
||||
if (items.isEmpty()) {
|
||||
if (stableItems.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
@@ -378,7 +384,7 @@ fun ListDetailScreen(
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
groupedItems.forEach { (groupName, groupItems) ->
|
||||
groupedItems.entries.forEach { (groupName, groupItems) ->
|
||||
// Show group header if grouping is enabled
|
||||
if (groupByField != null) {
|
||||
item(key = "group_$groupName") {
|
||||
@@ -397,9 +403,12 @@ fun ListDetailScreen(
|
||||
}
|
||||
|
||||
// Show items in the group
|
||||
items(groupItems, key = { it.item.id }) { itemWithValues ->
|
||||
items(
|
||||
items = groupItems,
|
||||
key = { it.item.id }
|
||||
) { itemWithValues ->
|
||||
ItemRow(
|
||||
fields = fields,
|
||||
fields = stableFields,
|
||||
fieldWidths = fieldWidths,
|
||||
itemWithValues = itemWithValues,
|
||||
scrollState = horizontalScrollState,
|
||||
@@ -415,7 +424,7 @@ fun ListDetailScreen(
|
||||
|
||||
if (showManageColumnsDialog) {
|
||||
ManageColumnsDialog(
|
||||
fields = fields,
|
||||
fields = stableFields,
|
||||
onDismiss = { showManageColumnsDialog = false },
|
||||
onAddField = { name, fieldType, fieldOptions ->
|
||||
viewModel.addField(name, fieldType, fieldOptions)
|
||||
@@ -434,7 +443,7 @@ fun ListDetailScreen(
|
||||
|
||||
if (showAddItemDialog) {
|
||||
AddItemDialog(
|
||||
fields = fields,
|
||||
fields = stableFields,
|
||||
onDismiss = { showAddItemDialog = false },
|
||||
onAdd = { fieldValues ->
|
||||
viewModel.addItemWithValues(fieldValues)
|
||||
@@ -446,7 +455,7 @@ fun ListDetailScreen(
|
||||
// Sort Dialog
|
||||
if (showSortDialog) {
|
||||
SortDialog(
|
||||
fields = fields,
|
||||
fields = stableFields,
|
||||
currentSortField = sortField,
|
||||
currentSortAscending = sortAscending,
|
||||
onDismiss = { showSortDialog = false },
|
||||
@@ -461,7 +470,7 @@ fun ListDetailScreen(
|
||||
// Group Dialog
|
||||
if (showGroupDialog) {
|
||||
GroupDialog(
|
||||
fields = fields,
|
||||
fields = stableFields,
|
||||
currentGroupByField = groupByField,
|
||||
onDismiss = { showGroupDialog = false },
|
||||
onApply = { newGroupByField ->
|
||||
@@ -474,7 +483,7 @@ fun ListDetailScreen(
|
||||
// Filter Dialog
|
||||
if (showFilterDialog) {
|
||||
FilterDialog(
|
||||
fields = fields,
|
||||
fields = stableFields,
|
||||
currentFilterField = filterField,
|
||||
currentFilterValue = filterValue,
|
||||
onDismiss = { showFilterDialog = false },
|
||||
@@ -502,7 +511,7 @@ fun ListDetailScreen(
|
||||
|
||||
itemToEdit?.let { itemWithValues ->
|
||||
EditItemDialog(
|
||||
fields = fields,
|
||||
fields = stableFields,
|
||||
itemWithValues = itemWithValues,
|
||||
onDismiss = { itemToEdit = null },
|
||||
onUpdate = { fieldValues ->
|
||||
@@ -586,6 +595,10 @@ fun ItemRow(
|
||||
scrollState: androidx.compose.foundation.ScrollState,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
// Map values by fieldId to ensure correct value-column alignment regardless of original order
|
||||
val valuesByFieldId = remember(itemWithValues.values) {
|
||||
itemWithValues.values.associateBy { it.fieldId }
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -593,19 +606,20 @@ fun ItemRow(
|
||||
.horizontalScroll(scrollState),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
fields.forEachIndexed { _, field ->
|
||||
val value = itemWithValues.values.find { it.fieldId == field.id }
|
||||
val fieldWidth = fieldWidths[field.id] ?: 150.dp
|
||||
fields.forEach { field ->
|
||||
key(field.id) {
|
||||
val value = valuesByFieldId[field.id]
|
||||
val fieldWidth = fieldWidths[field.id] ?: 150.dp
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(fieldWidth)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(fieldWidth)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
when (field.getType()) {
|
||||
com.collabtable.app.data.model.FieldType.TEXT -> {
|
||||
Text(
|
||||
@@ -1001,6 +1015,7 @@ fun ItemRow(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -2596,6 +2611,10 @@ fun EditItemDialog(
|
||||
) {
|
||||
val fieldValues = remember { mutableStateMapOf<String, String>() }
|
||||
var showDeleteConfirmation by remember { mutableStateOf(false) }
|
||||
// Map values by field to avoid linear lookups and ensure consistent mapping when fields reorder
|
||||
val valuesByFieldId = remember(itemWithValues.values) {
|
||||
itemWithValues.values.associateBy { it.fieldId }
|
||||
}
|
||||
|
||||
// Initialize field values from existing item
|
||||
LaunchedEffect(itemWithValues) {
|
||||
@@ -2647,7 +2666,7 @@ fun EditItemDialog(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(fields, key = { it.id }) { field ->
|
||||
val itemValue = itemWithValues.values.find { it.fieldId == field.id }
|
||||
val itemValue = valuesByFieldId[field.id]
|
||||
if (itemValue != null) {
|
||||
FieldInput(
|
||||
field = field,
|
||||
|
||||
+51
-37
@@ -3,12 +3,14 @@ package com.collabtable.app.ui.screens
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.room.withTransaction
|
||||
import com.collabtable.app.data.database.CollabTableDatabase
|
||||
import com.collabtable.app.data.model.*
|
||||
import com.collabtable.app.data.repository.SyncRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
@@ -35,7 +37,9 @@ class ListDetailViewModel(
|
||||
|
||||
private fun loadListData() {
|
||||
viewModelScope.launch {
|
||||
database.listDao().getListWithFields(listId).collect { listWithFields ->
|
||||
database.listDao().getListWithFields(listId)
|
||||
.debounce(75)
|
||||
.collect { listWithFields ->
|
||||
_list.value = listWithFields?.list
|
||||
_fields.value = listWithFields?.fields ?: emptyList()
|
||||
}
|
||||
@@ -90,12 +94,11 @@ class ListDetailViewModel(
|
||||
createdAt = timestamp,
|
||||
updatedAt = timestamp
|
||||
)
|
||||
database.fieldDao().insertField(newField)
|
||||
|
||||
// Create empty ItemValue entries for this new field for all existing items
|
||||
val existingItems = _items.value
|
||||
if (existingItems.isNotEmpty()) {
|
||||
val newValues = existingItems.map { itemWithValues ->
|
||||
val newValues = if (existingItems.isNotEmpty()) {
|
||||
existingItems.map { itemWithValues ->
|
||||
ItemValue(
|
||||
id = UUID.randomUUID().toString(),
|
||||
itemId = itemWithValues.item.id,
|
||||
@@ -104,7 +107,16 @@ class ListDetailViewModel(
|
||||
updatedAt = timestamp
|
||||
)
|
||||
}
|
||||
database.itemValueDao().insertValues(newValues)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
// Insert atomically to avoid intermediate inconsistent states
|
||||
database.withTransaction {
|
||||
database.fieldDao().insertField(newField)
|
||||
if (newValues.isNotEmpty()) {
|
||||
database.itemValueDao().insertValues(newValues)
|
||||
}
|
||||
}
|
||||
|
||||
performSync()
|
||||
@@ -143,19 +155,23 @@ class ListDetailViewModel(
|
||||
createdAt = timestamp,
|
||||
updatedAt = timestamp
|
||||
)
|
||||
database.itemDao().insertItem(newItem)
|
||||
|
||||
// Create empty values for each field
|
||||
val values = _fields.value.map { field ->
|
||||
ItemValue(
|
||||
id = UUID.randomUUID().toString(),
|
||||
itemId = newItem.id,
|
||||
fieldId = field.id,
|
||||
value = "",
|
||||
updatedAt = timestamp
|
||||
)
|
||||
// Insert item and its values atomically
|
||||
database.withTransaction {
|
||||
database.itemDao().insertItem(newItem)
|
||||
// Create empty values for each field
|
||||
val values = _fields.value.map { field ->
|
||||
ItemValue(
|
||||
id = UUID.randomUUID().toString(),
|
||||
itemId = newItem.id,
|
||||
fieldId = field.id,
|
||||
value = "",
|
||||
updatedAt = timestamp
|
||||
)
|
||||
}
|
||||
if (values.isNotEmpty()) {
|
||||
database.itemValueDao().insertValues(values)
|
||||
}
|
||||
}
|
||||
database.itemValueDao().insertValues(values)
|
||||
performSync()
|
||||
}
|
||||
}
|
||||
@@ -169,19 +185,23 @@ class ListDetailViewModel(
|
||||
createdAt = timestamp,
|
||||
updatedAt = timestamp
|
||||
)
|
||||
database.itemDao().insertItem(newItem)
|
||||
|
||||
// Create values for each field with the provided values
|
||||
val values = _fields.value.map { field ->
|
||||
ItemValue(
|
||||
id = UUID.randomUUID().toString(),
|
||||
itemId = newItem.id,
|
||||
fieldId = field.id,
|
||||
value = fieldValues[field.id] ?: "",
|
||||
updatedAt = timestamp
|
||||
)
|
||||
// Insert item and provided values atomically
|
||||
database.withTransaction {
|
||||
database.itemDao().insertItem(newItem)
|
||||
// Create values for each field with the provided values
|
||||
val values = _fields.value.map { field ->
|
||||
ItemValue(
|
||||
id = UUID.randomUUID().toString(),
|
||||
itemId = newItem.id,
|
||||
fieldId = field.id,
|
||||
value = fieldValues[field.id] ?: "",
|
||||
updatedAt = timestamp
|
||||
)
|
||||
}
|
||||
if (values.isNotEmpty()) {
|
||||
database.itemValueDao().insertValues(values)
|
||||
}
|
||||
}
|
||||
database.itemValueDao().insertValues(values)
|
||||
performSync()
|
||||
}
|
||||
}
|
||||
@@ -211,14 +231,8 @@ class ListDetailViewModel(
|
||||
fun reorderFields(reorderedFields: List<Field>) {
|
||||
viewModelScope.launch {
|
||||
val timestamp = System.currentTimeMillis()
|
||||
reorderedFields.forEachIndexed { index, field ->
|
||||
database.fieldDao().updateField(
|
||||
field.copy(
|
||||
order = index,
|
||||
updatedAt = timestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
// Use DAO-level transaction for clarity
|
||||
database.fieldDao().reorderFieldsInTransaction(reorderedFields, timestamp)
|
||||
performSync()
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user