feat: sort tasks by due time
ci / gitleaks (push) Successful in 8s
ci / docker (push) Successful in 3m35s

This commit is contained in:
2026-09-10 18:16:13 +08:00
parent ab8c0a5138
commit addd49363d
8 changed files with 576 additions and 58 deletions
+35 -6
View File
@@ -5,7 +5,7 @@ import {
Ellipsis, GripVertical, Inbox, ListChecks, ListTodo, Menu, Pencil, Plus, Search,
Settings, Trash2, X, Repeat2,
} from 'lucide-vue-next'
import { buildTaskRrule, defaultTaskDueAt, filterTasks, fromDateTimeLocal, groupTaskTree, moveItemWithinScope, parseTaskRrule, renderMarkdown, toDateTimeLocal, type TaskRepeatConfig } from './lib/task-utils'
import { buildTaskRrule, defaultTaskDueAt, filterTasks, fromDateTimeLocal, groupTaskTree, isSameTaskSortTier, moveItemWithinScope, parseTaskRrule, renderMarkdown, toDateTimeLocal, type TaskRepeatConfig } from './lib/task-utils'
import { beginLatestRequest, createMutationReconciler, formatApiErrorDetail, isLatestRequest, isTaskView, loadCountdownCache, nextTotalAfterLocalTaskAdd, nextTotalAfterLocalTaskRemoval, normalizeRequiredName, performTrashMutation, readStoredBoolean, readStoredNavigation, runLatestRequest, shouldToggleRowSwipe, startPrimaryWithBackground, writeStoredBoolean, writeStoredNavigation } from './lib/mvp-utils'
import { csrfHeader } from './lib/csrf'
import { createCompletionPulse } from './lib/completion-motion'
@@ -100,6 +100,7 @@ const taskPointerStart = ref<{ id: string; x: number; y: number } | null>(null)
const taskSwipeOffsets = ref<Record<string, number>>({})
const taskReorder = ref<{ id: string; startY: number; offsetY: number } | null>(null)
const taskReorderTarget = ref('')
const taskReorderBlocked = ref(false)
const taskComposeOpen = ref(false)
const composeTitle = ref('')
const composeTitleError = ref('')
@@ -622,6 +623,7 @@ function startTaskReorder(task: Task, event: PointerEvent) {
if (activeView.value === 'trash' || loading.value || query.value || totalPages.value > 1) return
taskReorder.value = { id: task.id, startY: event.clientY, offsetY: 0 }
taskReorderTarget.value = task.id
taskReorderBlocked.value = false
try { (event.currentTarget as Element).setPointerCapture(event.pointerId) } catch { /* synthetic events */ }
}
function taskById(id: string) {
@@ -636,16 +638,33 @@ function moveTaskReorder(task: Task, event: PointerEvent) {
const row = document.elementsFromPoint(event.clientX, event.clientY)
.map((element) => element.closest<HTMLElement>('[data-task-id]'))
.find((element) => element && element !== handle.closest('[data-task-id]'))
if (row?.dataset.taskId) taskReorderTarget.value = row.dataset.taskId
const target = row?.dataset.taskId ? taskById(row.dataset.taskId) : undefined
if (target && target.list_id === task.list_id && (target.parent_id ?? null) === (task.parent_id ?? null) && isSameTaskSortTier(task, target)) {
taskReorderTarget.value = target.id
taskReorderBlocked.value = false
} else {
taskReorderTarget.value = ''
taskReorderBlocked.value = Boolean(target)
}
}
async function finishTaskReorder(task: Task, event: PointerEvent) {
const drag = taskReorder.value
const targetId = taskReorderTarget.value
const blocked = taskReorderBlocked.value
taskReorder.value = null
taskReorderTarget.value = ''
taskReorderBlocked.value = false
if (blocked) {
toast('只能调整相同完成状态和截止时间档的任务顺序')
return
}
if (!drag || drag.id !== task.id || !targetId || targetId === task.id) return
const target = taskById(targetId)
if (!target || (target.parent_id ?? null) !== (task.parent_id ?? null)) return
if (!target || target.list_id !== task.list_id || (target.parent_id ?? null) !== (task.parent_id ?? null)) return
if (!isSameTaskSortTier(task, target)) {
toast('只能调整相同完成状态和截止时间档的任务顺序')
return
}
const placement = event.clientY >= drag.startY ? 'after' : 'before'
const previous = tasks.value
const previousSelected = selectedTask.value
@@ -657,12 +676,12 @@ async function finishTaskReorder(task: Task, event: PointerEvent) {
if (reordered === parent.subtasks) return
tasks.value = tasks.value.map((item) => item.id === parent.id ? { ...item, subtasks: reordered } : item)
if (selectedTask.value?.id === parent.id) selectedTask.value = { ...selectedTask.value, subtasks: reordered }
ids = reordered.map((item) => item.id)
ids = reordered.filter((item) => isSameTaskSortTier(task, item)).map((item) => item.id)
} else {
const next = moveItemWithinScope(previous, task.id, targetId, placement)
if (next === previous) return
tasks.value = next
ids = next.map((item) => item.id)
ids = next.filter((item) => isSameTaskSortTier(task, item)).map((item) => item.id)
}
try {
await api('/tasks/reorder', { method: 'PUT', body: JSON.stringify({ task_ids: ids }) })
@@ -676,6 +695,7 @@ async function finishTaskReorder(task: Task, event: PointerEvent) {
function cancelTaskReorder() {
taskReorder.value = null
taskReorderTarget.value = ''
taskReorderBlocked.value = false
}
function startTaskSwipe(task: Task, event: TouchEvent) {
@@ -771,6 +791,15 @@ function selectTaskUnlessSwiped(task: Task, toggleChildren = false) {
if (toggleChildren) toggleTaskChildren(task)
selectTask(task)
}
async function refreshTodayAfterTaskSave() {
if (activeView.value !== 'today') return
const request = beginLatestRequest('tasks')
await Promise.all([
loadTasksPage(request),
loadOverdueTasks(request),
loadTodayTaskSummary(),
])
}
async function saveTask() {
if (!selectedTask.value) return
const normalized = normalizeRequiredName(selectedTask.value.title)
@@ -793,7 +822,7 @@ async function saveTask() {
}
}
if (selectedTask.value) selectedTask.value = { ...selectedTask.value, ...updated }
if (activeView.value === 'today') void loadTodayTaskSummary()
await refreshTodayAfterTaskSave()
toast('已保存')
} catch (reason) { fail(reason) }
}