From 4a0e5833438df38cce92867ae3f5f158d5101cf2 Mon Sep 17 00:00:00 2001 From: Olivier Patry Date: Tue, 15 Oct 2024 17:43:44 +0200 Subject: [PATCH] Store task list sorting in DB --- .../3.json | 194 ++++++++++++++++++ .../opatry/tasks/app/ui/TaskListsViewModel.kt | 15 +- .../tasks/app/ui/component/TaskListMenu.kt | 13 +- .../tasks/app/ui/model/TaskListUIModel.kt | 3 + .../tasks/app/ui/screen/taskListsPane.kt | 2 + .../opatry/tasks/app/ui/screen/tasksPane.kt | 28 +-- .../net/opatry/tasks/data/TasksAppDatabase.kt | 11 +- .../net/opatry/tasks/data/TaskListDao.kt | 3 + .../net/opatry/tasks/data/TaskRepository.kt | 40 +++- .../tasks/data/entity/TaskListEntity.kt | 9 +- .../tasks/data/model/TaskListDataModel.kt | 2 + 11 files changed, 283 insertions(+), 37 deletions(-) create mode 100644 tasks-app-shared/schemas/net.opatry.tasks.data.TasksAppDatabase/3.json diff --git a/tasks-app-shared/schemas/net.opatry.tasks.data.TasksAppDatabase/3.json b/tasks-app-shared/schemas/net.opatry.tasks.data.TasksAppDatabase/3.json new file mode 100644 index 00000000..0284e20b --- /dev/null +++ b/tasks-app-shared/schemas/net.opatry.tasks.data.TasksAppDatabase/3.json @@ -0,0 +1,194 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "17de8549f80d34ddcf3b7baff29a9f31", + "entities": [ + { + "tableName": "task_list", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `remote_id` TEXT, `etag` TEXT NOT NULL DEFAULT '', `title` TEXT NOT NULL, `update_date` TEXT NOT NULL, `sorting` TEXT NOT NULL DEFAULT 'UserDefined')", + "fields": [ + { + "fieldPath": "id", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdateDate", + "columnName": "update_date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sorting", + "columnName": "sorting", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'UserDefined'" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "local_id" + ] + } + }, + { + "tableName": "task", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `remote_id` TEXT, `parent_list_local_id` INTEGER NOT NULL, `etag` TEXT NOT NULL DEFAULT '', `title` TEXT NOT NULL, `due_date` TEXT, `update_date` TEXT NOT NULL, `completion_date` TEXT, `notes` TEXT NOT NULL DEFAULT '', `is_completed` INTEGER NOT NULL, `position` TEXT NOT NULL, `parent_local_id` INTEGER, `remote_parent_id` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "parentListLocalId", + "columnName": "parent_list_local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dueDate", + "columnName": "due_date", + "affinity": "TEXT" + }, + { + "fieldPath": "lastUpdateDate", + "columnName": "update_date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "completionDate", + "columnName": "completion_date", + "affinity": "TEXT" + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isCompleted", + "columnName": "is_completed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentTaskLocalId", + "columnName": "parent_local_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "parentTaskRemoteId", + "columnName": "remote_parent_id", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "local_id" + ] + } + }, + { + "tableName": "user", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `remote_id` TEXT, `name` TEXT NOT NULL, `email` TEXT, `avatar_url` TEXT, `is_signed_in` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT" + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatar_url", + "affinity": "TEXT" + }, + { + "fieldPath": "isSignedIn", + "columnName": "is_signed_in", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '17de8549f80d34ddcf3b7baff29a9f31')" + ] + } +} \ No newline at end of file diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt index fc8b4120..e8e10a04 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt @@ -39,6 +39,7 @@ import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.toLocalDateTime import net.opatry.tasks.app.ui.model.TaskListUIModel import net.opatry.tasks.app.ui.model.TaskUIModel +import net.opatry.tasks.data.TaskListSorting import net.opatry.tasks.data.TaskRepository import net.opatry.tasks.data.model.TaskDataModel import net.opatry.tasks.data.model.TaskListDataModel @@ -52,7 +53,8 @@ private fun TaskListDataModel.asTaskListUIModel(): TaskListUIModel { id = id, title = title, lastUpdate = lastUpdate.toString(), - tasks = tasks.map(TaskDataModel::asTaskUIModel) + tasks = tasks.map(TaskDataModel::asTaskUIModel), + sorting = sorting, ) } @@ -141,6 +143,17 @@ class TaskListsViewModel( } } + fun sortBy(taskList: TaskListUIModel, sorting: TaskListSorting) { + viewModelScope.launch { + try { + taskRepository.sortTasksBy(taskList.id, sorting) + } catch (e: Exception) { + println("Error while sorting task list: $e") + // TODO error handling + } + } + } + fun createTask(taskList: TaskListUIModel, title: String, notes: String = "", dueDate: LocalDate? = null) { viewModelScope.launch { try { diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt index 00b2b9f1..ad45f763 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt @@ -49,9 +49,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import net.opatry.tasks.app.ui.model.TaskListUIModel -import net.opatry.tasks.app.ui.screen.TaskListSorting import net.opatry.tasks.app.ui.tooling.TaskfolioPreview import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview +import net.opatry.tasks.data.TaskListSorting import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.task_list_menu_clear_all_completed_tasks import net.opatry.tasks.resources.task_list_menu_default_list_cannot_be_deleted @@ -75,7 +75,6 @@ enum class TaskListMenuAction { fun TaskListMenu( taskList: TaskListUIModel, expanded: Boolean, - sorting: TaskListSorting = TaskListSorting.Manual, onAction: (TaskListMenuAction) -> Unit ) { val allowDelete by remember(taskList.canDelete) { mutableStateOf(taskList.canDelete) } @@ -96,9 +95,9 @@ fun TaskListMenu( text = { RowWithIcon( stringResource(Res.string.task_list_menu_sort_manual), - LucideIcons.Check.takeIf { sorting == TaskListSorting.Manual }) + LucideIcons.Check.takeIf { taskList.sorting == TaskListSorting.Manual }) }, - enabled = sorting != TaskListSorting.Manual, + enabled = taskList.sorting != TaskListSorting.Manual, onClick = { onAction(TaskListMenuAction.SortManual) } ) @@ -106,9 +105,9 @@ fun TaskListMenu( text = { RowWithIcon( stringResource(Res.string.task_list_menu_sort_due_date), - LucideIcons.Check.takeIf { sorting == TaskListSorting.DueDate }) + LucideIcons.Check.takeIf { taskList.sorting == TaskListSorting.DueDate }) }, - enabled = sorting != TaskListSorting.DueDate, + enabled = taskList.sorting != TaskListSorting.DueDate, onClick = { onAction(TaskListMenuAction.SortDate) } ) @@ -167,7 +166,7 @@ private fun TaskListMenuPreview() { ) { IconButton(onClick = { showMenu = true }) { Icon(LucideIcons.EllipsisVertical, null) - TaskListMenu(TaskListUIModel(0L, "My task list", "TODO DATE", tasks = emptyList()), showMenu) {} + TaskListMenu(TaskListUIModel(0L, "My task list", "TODO DATE", tasks = emptyList(), sorting = TaskListSorting.Manual), showMenu) {} } } } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt index 4ec52fe5..97d76ac0 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt @@ -22,12 +22,15 @@ package net.opatry.tasks.app.ui.model +import net.opatry.tasks.data.TaskListSorting + data class TaskListUIModel( val id: Long, val title: String, val lastUpdate: String, val tasks: List, + val sorting: TaskListSorting, ) { val isEmpty: Boolean = tasks.isEmpty() val hasCompletedTasks: Boolean = tasks.any { it.isCompleted } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt index 357c5c1c..9ebbcafc 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt @@ -53,6 +53,7 @@ import net.opatry.tasks.app.ui.component.RowWithIcon import net.opatry.tasks.app.ui.model.TaskListUIModel import net.opatry.tasks.app.ui.tooling.TaskfolioPreview import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview +import net.opatry.tasks.data.TaskListSorting import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.task_lists_screen_add_task_list_cta import org.jetbrains.compose.resources.stringResource @@ -136,6 +137,7 @@ private fun TaskListRowScaffold( title = title, "TODO DATE", tasks = emptyList(), + sorting = TaskListSorting.Manual, ), isSelected = isSelected, onClick = {} diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt index 4d2e1d00..73292324 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt @@ -128,6 +128,7 @@ import net.opatry.tasks.app.ui.model.TaskUIModel import net.opatry.tasks.app.ui.model.compareTo import net.opatry.tasks.app.ui.tooling.TaskfolioPreview import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview +import net.opatry.tasks.data.TaskListSorting import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.dialog_cancel import net.opatry.tasks.resources.task_due_date_label_days_ago @@ -169,10 +170,6 @@ import net.opatry.tasks.resources.task_lists_screen_empty_list_title import org.jetbrains.compose.resources.stringResource import kotlin.math.abs -enum class TaskListSorting { - Manual, - DueDate, -} @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -182,7 +179,6 @@ fun TaskListDetail( onNavigateTo: (TaskListUIModel?) -> Unit ) { val taskLists by viewModel.taskLists.collectAsState(emptyList()) - var taskSorting by remember { mutableStateOf(TaskListSorting.Manual) } // TODO extract a smart state for all this mess var taskOfInterest by remember { mutableStateOf(null) } @@ -237,12 +233,12 @@ fun TaskListDetail( IconButton(onClick = { showTaskListActions = true }) { Icon(LucideIcons.EllipsisVertical, null) } - TaskListMenu(taskList, showTaskListActions, taskSorting) { action -> + TaskListMenu(taskList, showTaskListActions) { action -> showTaskListActions = false when (action) { TaskListMenuAction.Dismiss -> Unit - TaskListMenuAction.SortManual -> taskSorting = TaskListSorting.Manual - TaskListMenuAction.SortDate -> taskSorting = TaskListSorting.DueDate + TaskListMenuAction.SortManual -> viewModel.sortBy(taskList, TaskListSorting.Manual) + TaskListMenuAction.SortDate -> viewModel.sortBy(taskList, TaskListSorting.DueDate) TaskListMenuAction.Rename -> showRenameTaskListDialog = true TaskListMenuAction.ClearCompletedTasks -> showClearTaskListCompletedTasksDialog = true TaskListMenuAction.Delete -> showDeleteTaskListDialog = true @@ -275,7 +271,6 @@ fun TaskListDetail( TasksColumn( taskLists, taskList, - taskSorting, onToggleCompletionState = viewModel::toggleTaskCompletionState, onEditTask = { taskOfInterest = it @@ -582,7 +577,6 @@ fun TaskListDetail( fun TasksColumn( taskLists: List, taskList: TaskListUIModel, - taskSorting: TaskListSorting, onToggleCompletionState: (TaskUIModel) -> Unit, onEditTask: (TaskUIModel) -> Unit, onUpdateDueDate: (TaskUIModel) -> Unit, @@ -599,7 +593,7 @@ fun TasksColumn( // FIXME remember computation & derived states val (completedTasks, remainingTasks) = taskList.tasks.partition(TaskUIModel::isCompleted) - val taskGroups = when (taskSorting) { + val taskGroups = when (taskList.sorting) { // no grouping TaskListSorting.Manual -> mapOf(null to remainingTasks) TaskListSorting.DueDate -> remainingTasks @@ -651,7 +645,7 @@ fun TasksColumn( RemainingTaskRow( taskLists, task, - simpleDisplay = taskSorting == TaskListSorting.DueDate + showDate = taskList.sorting == TaskListSorting.Manual || dateRange is DateRange.Overdue ) { action -> when (action) { TaskAction.ToggleCompletion -> onToggleCompletionState(task) @@ -718,7 +712,6 @@ fun TasksColumn( @Composable fun DateRange?.toColor(): Color = when (this) { is DateRange.Overdue -> MaterialTheme.colorScheme.error - is DateRange.Today -> MaterialTheme.colorScheme.primary is DateRange.Later, DateRange.None, @@ -773,7 +766,7 @@ private fun RemainingTaskRow( taskLists: List, task: TaskUIModel, modifier: Modifier = Modifier, - simpleDisplay: Boolean = false, + showDate: Boolean = true, onAction: (TaskAction) -> Unit, ) { var showContextualMenu by remember { mutableStateOf(false) } @@ -781,10 +774,7 @@ private fun RemainingTaskRow( Row(modifier.clickable(onClick = { onAction(TaskAction.Edit) })) { IconButton( onClick = { onAction(TaskAction.ToggleCompletion) }, - modifier = if (simpleDisplay) - Modifier - else - Modifier.padding(start = 36.dp * task.indent) + modifier = Modifier.padding(start = 36.dp * task.indent) ) { Icon(LucideIcons.Circle, null, tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)) } @@ -808,7 +798,7 @@ private fun RemainingTaskRow( maxLines = 2 ) } - if (!simpleDisplay && task.dueDate != null) { + if (showDate && task.dueDate != null) { AssistChip( onClick = { onAction(TaskAction.UpdateDueDate) }, shape = MaterialTheme.shapes.large, diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/data/TasksAppDatabase.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/data/TasksAppDatabase.kt index 553db297..87f72f55 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/data/TasksAppDatabase.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/data/TasksAppDatabase.kt @@ -41,6 +41,12 @@ object Converters { @TypeConverter fun instantToString(instant: Instant?): String? = instant?.toString() + + @TypeConverter + fun sortingFromString(value: String?): TaskListEntity.Sorting? = value?.let(TaskListEntity.Sorting::valueOf) + + @TypeConverter + fun sortingToString(sorting: TaskListEntity.Sorting?): String? = sorting?.name } @Database( @@ -49,9 +55,10 @@ object Converters { TaskEntity::class, UserEntity::class, ], - version = 2, + version = 3, autoMigrations = [ - AutoMigration(from = 1, to = 2) + AutoMigration(from = 1, to = 2), // add user table + AutoMigration(from = 2, to = 3), // add sorting column in task_list table ], ) @ConstructedBy(TasksAppDatabaseConstructor::class) diff --git a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskListDao.kt b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskListDao.kt index 78933f83..ad71d1f7 100644 --- a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskListDao.kt +++ b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskListDao.kt @@ -61,4 +61,7 @@ LEFT JOIN task ON task_list.local_id = task.parent_list_local_id ORDER BY task_l @Query("DELETE FROM task_list WHERE remote_id IS NOT NULL AND remote_id NOT IN (:validRemoteIds)") suspend fun deleteStaleTaskLists(validRemoteIds: List) + + @Query("UPDATE task_list SET sorting = :sorting WHERE local_id = :taskListId") + suspend fun sortTasksBy(taskListId: Long, sorting: TaskListEntity.Sorting) } \ No newline at end of file diff --git a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt index aad70b58..3c173c85 100644 --- a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt +++ b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt @@ -42,13 +42,19 @@ import net.opatry.tasks.data.entity.TaskListEntity import net.opatry.tasks.data.model.TaskDataModel import net.opatry.tasks.data.model.TaskListDataModel -private fun TaskList.asTaskListEntity(localId: Long?): TaskListEntity { +enum class TaskListSorting { + Manual, + DueDate, +} + +private fun TaskList.asTaskListEntity(localId: Long?, sorting: TaskListEntity.Sorting): TaskListEntity { return TaskListEntity( id = localId ?: 0, remoteId = id, etag = etag, title = title, lastUpdateDate = updatedDate, + sorting = sorting, ) } @@ -71,14 +77,21 @@ private fun Task.asTaskEntity(parentLocalId: Long, localId: Long?): TaskEntity { } private fun TaskListEntity.asTaskListDataModel(tasks: List): TaskListDataModel { - val sortedTasks = sortTasks(tasks).map { (task, indent) -> - task.asTaskDataModel(indent) + val (sorting, sortedTasks) = when (sorting) { + TaskListEntity.Sorting.UserDefined -> TaskListSorting.Manual to sortTasksManualOrdering(tasks).map { (task, indent) -> + task.asTaskDataModel(indent) + } + + TaskListEntity.Sorting.DueDate -> TaskListSorting.DueDate to sortTasksDateOrdering(tasks).map { task -> + task.asTaskDataModel(0) + } } return TaskListDataModel( id = id, title = title, lastUpdate = lastUpdateDate, tasks = sortedTasks, + sorting = sorting ) } @@ -109,7 +122,7 @@ private fun TaskEntity.asTask(): Task { ) } -fun sortTasks(tasks: List): List> { +fun sortTasksManualOrdering(tasks: List): List> { // Step 1: Create a map of tasks by their IDs for easy lookup val taskMap = tasks.associateBy { it.remoteId ?: it.id.toString() }.toMutableMap() // FIXME local data only? @@ -150,6 +163,10 @@ fun sortTasks(tasks: List): List> { return sortedTasks } +fun sortTasksDateOrdering(tasks: List): List { + return tasks.sortedBy(TaskEntity::dueDate) +} + class TaskRepository( private val taskListDao: TaskListDao, private val taskDao: TaskDao, @@ -188,8 +205,8 @@ class TaskRepository( // - check new ones // - etc. val existingEntity = taskListDao.getByRemoteId(remoteTaskList.id) - val finalLocalId = taskListDao.insert(remoteTaskList.asTaskListEntity(existingEntity?.id)) - println("task list ${remoteTaskList.etag} vs ${existingEntity?.etag}) with final local id $finalLocalId") + val updatedEntity = remoteTaskList.asTaskListEntity(existingEntity?.id, existingEntity?.sorting ?: TaskListEntity.Sorting.UserDefined) + val finalLocalId = taskListDao.insert(updatedEntity) taskListIds[finalLocalId] = remoteTaskList.id } taskListDao.deleteStaleTaskLists(remoteTaskLists.map(TaskList::id)) @@ -237,7 +254,7 @@ class TaskRepository( } } if (taskList != null) { - taskListDao.insert(taskList.asTaskListEntity(taskListId)) + taskListDao.insert(taskList.asTaskListEntity(taskListId, TaskListEntity.Sorting.UserDefined)) } } @@ -304,6 +321,15 @@ class TaskRepository( } } + suspend fun sortTasksBy(taskListId: Long, sorting: TaskListSorting) { + val dbSorting = when (sorting) { + TaskListSorting.Manual -> TaskListEntity.Sorting.UserDefined + TaskListSorting.DueDate -> TaskListEntity.Sorting.DueDate + } + // no update date change, it's a local only information unrelated to remote tasks + taskListDao.sortTasksBy(taskListId, dbSorting) + } + suspend fun createTask(taskListId: Long, title: String, notes: String = "", dueDate: Instant? = null) { val now = Clock.System.now() val taskEntity = TaskEntity( diff --git a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/entity/TaskListEntity.kt b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/entity/TaskListEntity.kt index 25a60fac..1613eeea 100644 --- a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/entity/TaskListEntity.kt +++ b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/entity/TaskListEntity.kt @@ -41,5 +41,12 @@ data class TaskListEntity( val title: String, @ColumnInfo(name = "update_date") val lastUpdateDate: Instant, -) + @ColumnInfo(name = "sorting", defaultValue = "UserDefined") // tightly coupled to converters & enum/string mapping + val sorting: Sorting = Sorting.UserDefined, +) { + enum class Sorting { + UserDefined, + DueDate, + } +} diff --git a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/model/TaskListDataModel.kt b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/model/TaskListDataModel.kt index 5886cdd1..6c2212f6 100644 --- a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/model/TaskListDataModel.kt +++ b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/model/TaskListDataModel.kt @@ -23,10 +23,12 @@ package net.opatry.tasks.data.model import kotlinx.datetime.Instant +import net.opatry.tasks.data.TaskListSorting data class TaskListDataModel( val id: Long, val title: String, val lastUpdate: Instant, val tasks: List, + val sorting: TaskListSorting, )