Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task list sorting #60

Merged
merged 4 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
<string name="task_due_date_label_yesterday">Hier</string>
<string name="task_due_date_label_today">Aujourd'hui</string>
<string name="task_due_date_label_tomorrow">Demain</string>
<string name="task_due_date_label_past">Passée</string>
<string name="task_due_date_label_no_date">Sans date</string>
<string name="task_due_date_update_cta">Mettre à jour</string>

<string name="task_editor_sheet_edit_title">Modifier la tâche</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
<string name="task_due_date_label_yesterday">Yesterday</string>
<string name="task_due_date_label_today">Today</string>
<string name="task_due_date_label_tomorrow">Tomorrow</string>
<string name="task_due_date_label_past">Past</string>
<string name="task_due_date_label_no_date">No date</string>
<string name="task_due_date_update_cta">Update</string>

<string name="task_editor_sheet_edit_title">Edit task</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,41 @@ import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.toLocalDateTime
import net.opatry.tasks.app.ui.model.DateRange
import net.opatry.tasks.app.ui.model.TaskListUIModel
import net.opatry.tasks.app.ui.model.TaskUIModel
import net.opatry.tasks.app.ui.model.compareTo
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
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

private fun TaskListDataModel.asTaskListUIModel(): TaskListUIModel {
// TODO children
// TODO date formatter
val (completedTasks, remainingTasks) = tasks.map(TaskDataModel::asTaskUIModel).partition(TaskUIModel::isCompleted)

val taskGroups = when (sorting) {
// no grouping
TaskListSorting.Manual -> mapOf(null to remainingTasks)
TaskListSorting.DueDate -> remainingTasks
.sortedWith { o1, o2 -> o1.dateRange.compareTo(o2.dateRange) }
.groupBy { task ->
when (task.dateRange) {
// merge all overdue tasks to the same range
is DateRange.Overdue -> DateRange.Overdue(LocalDate.fromEpochDays(-1), 1)
else -> task.dateRange
}
}
}

return TaskListUIModel(
id = id,
title = title,
lastUpdate = lastUpdate.toString(),
tasks = tasks.map(TaskDataModel::asTaskUIModel)
remainingTasks = taskGroups.toMap(),
completedTasks = completedTasks,
sorting = sorting,
)
}

Expand Down Expand Up @@ -141,6 +160,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import androidx.compose.ui.unit.dp
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_list_menu_clear_all_completed_tasks
import net.opatry.tasks.resources.task_list_menu_default_list_cannot_be_deleted
Expand All @@ -71,7 +72,11 @@ enum class TaskListMenuAction {
}

@Composable
fun TaskListMenu(taskList: TaskListUIModel, expanded: Boolean, onAction: (TaskListMenuAction) -> Unit) {
fun TaskListMenu(
taskList: TaskListUIModel,
expanded: Boolean,
onAction: (TaskListMenuAction) -> Unit
) {
val allowDelete by remember(taskList.canDelete) { mutableStateOf(taskList.canDelete) }

DropdownMenu(
Expand All @@ -90,19 +95,19 @@ fun TaskListMenu(taskList: TaskListUIModel, expanded: Boolean, onAction: (TaskLi
text = {
RowWithIcon(
stringResource(Res.string.task_list_menu_sort_manual),
LucideIcons.Check.takeIf { false/*taskList.sorting == TaskListSorting.Manual*/ })
LucideIcons.Check.takeIf { taskList.sorting == TaskListSorting.Manual })
},
enabled = false, // TODO enable when sorting is implemented
enabled = taskList.sorting != TaskListSorting.Manual,
onClick = { onAction(TaskListMenuAction.SortManual) }
)

DropdownMenuItem(
text = {
RowWithIcon(
stringResource(Res.string.task_list_menu_sort_due_date),
LucideIcons.Check.takeIf { false/*taskList.sorting == TaskListSorting.Date*/ })
LucideIcons.Check.takeIf { taskList.sorting == TaskListSorting.DueDate })
},
enabled = false, // TODO enable when sorting is implemented
enabled = taskList.sorting != TaskListSorting.DueDate,
onClick = { onAction(TaskListMenuAction.SortDate) }
)

Expand Down Expand Up @@ -161,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"), showMenu) {}
}
}
}
Expand Down
Loading