diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 2572407..820e3d2 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -99,7 +99,6 @@ const todayHabitTotal = ref(0) const todayHabitCompleted = ref(0) const totalPages = computed(() => Math.max(1, Math.ceil(totalTasks.value / pageSize))) const expandedFolders = ref(new Set()) -const collapsedTaskIds = ref(new Set()) const justCompletedTaskIds = ref(new Set()) const completionExitingTaskIds = ref(new Set()) function setTaskCompletionExiting(id: string, active: boolean) { @@ -398,7 +397,6 @@ async function loadRestoredView() { if (activeView.value === 'trash') await loadTrash() else if (isTaskView(activeView.value)) { await loadAll() - if (activeView.value === 'tasks' || activeView.value === 'upcoming') collapseLoadedTaskChildren() } else { tasks.value = []; totalTasks.value = 0 } } @@ -611,7 +609,6 @@ async function switchView(view: View, listId?: string) { else if (!isTaskView(view)) tasks.value = [] else { await loadAll() - if (view === 'tasks' || view === 'upcoming') collapseLoadedTaskChildren() } } async function loadTodayView() { @@ -814,21 +811,11 @@ function cancelTaskSwipe(task?: Task) { taskSwipeStart.value = null if (task) taskSwipeOffsets.value[task.id] = 0 } -function collapseLoadedTaskChildren() { - collapsedTaskIds.value = new Set(tasks.value.filter((task) => task.subtasks?.length).map((task) => task.id)) -} -function toggleTaskChildren(task: Task) { - if (!task.subtasks?.length) return - const next = new Set(collapsedTaskIds.value) - next.has(task.id) ? next.delete(task.id) : next.add(task.id) - collapsedTaskIds.value = next -} -function selectTaskUnlessSwiped(task: Task, toggleChildren = false) { +function selectTaskUnlessSwiped(task: Task) { if (suppressTaskClickId === task.id) { suppressTaskClickId = '' return } - if (toggleChildren) toggleTaskChildren(task) selectTask(task) } async function refreshTodayAfterTaskSave() { @@ -1376,7 +1363,6 @@ onUnmounted(() => {
@@ -1389,10 +1375,9 @@ onUnmounted(() => {
-
{{node.task.title}}{{node.subtasks.filter(t=>t.completed).length}}/{{node.subtasks.length}}{{['','低','中','高'][node.task.priority]}}
+
{{node.task.title}}{{node.subtasks.filter(t=>t.completed).length}}/{{node.subtasks.length}}{{['','低','中','高'][node.task.priority]}}
-
{{subtask.title}}
{{ query ? '没有匹配的任务' : hiddenCompletedTaskCount > 0 ? '已完成任务已隐藏' : '这里还很安静' }}{{query?'换个关键词试试':hiddenCompletedTaskCount > 0 ? '开启“显示已完成”即可查看。':'写下第一件想完成的小事吧'}}
diff --git a/frontend/src/style.test.ts b/frontend/src/style.test.ts index d89eae8..a4abfe7 100644 --- a/frontend/src/style.test.ts +++ b/frontend/src/style.test.ts @@ -11,38 +11,31 @@ const completedFilterPill = readFileSync('src/components/CompletedFilterPill.vue const taskDueDisplay = readFileSync('src/components/TaskDueDisplay.vue', 'utf8') describe('unified task due display', () => { - it('uses the shared display for overdue, ordinary parent, and child task rows', () => { + it('uses the shared display for overdue and ordinary parent task rows', () => { expect(app).toContain("import TaskDueDisplay from './components/TaskDueDisplay.vue'") - expect(app.match(/ { + it('keeps overdue subtasks out of the list while preserving their parent progress summary', () => { const overdue = app.slice(app.indexOf('
t.completed).length') + expect(overdue).toContain('{{node.subtasks.length}}') }) - it('places every due display in a right tail before stable actions', () => { - expect(app.match(/ { + expect(app.match(/') - const ordinaryChildren = app.slice(app.indexOf('
', app.indexOf('
') - expect(app).toContain('') + expect(app).not.toContain('@click.stop="selectTask(subtask)">') expect(css).toContain('.task-detail-trigger:hover{color:var(--text-primary);background:#fff7eb}') }) @@ -503,8 +496,8 @@ describe('task and habit row decoration', () => { expect(mvpPanel).not.toContain('RefreshCw,') }) - it('supports vertical drag handles for reordering tasks and habits', () => { - expect(app.match(/class="drag-handle task-drag-handle"/g)?.length).toBe(2) + it('supports vertical drag handles for reordering visible parent tasks and habits', () => { + expect(app.match(/class="drag-handle task-drag-handle"/g)?.length).toBe(1) expect(app).toContain('@pointerdown.stop="startTaskReorder') expect(app).toContain('@pointerup.stop="finishTaskReorder') expect(app).toContain("api('/tasks/reorder'") @@ -520,11 +513,10 @@ describe('task and habit row decoration', () => { expect(mvpPanel.match(/'--reorder-y': `\$\{habitReorder\?\.id === h\.id \? habitReorder\.offsetY : 0\}px`/g)?.length).toBe(1) }) - it('exposes complete task titles on every ellipsized parent and child title node', () => { + it('exposes complete titles on every ellipsized visible task title node', () => { expect(app.match(/\{\{node\.task\.title\}\}<\/strong>/g)).toHaveLength(2) - expect(app.match(/\{\{subtask\.title\}\}<\/strong>/g)).toHaveLength(2) + expect(app).not.toContain('{{subtask.title}}') expect(app).not.toContain('{{node.task.title}}') - expect(app).not.toContain('{{subtask.title}}') }) it('keeps the Trash restore action at least 44px tall', () => { @@ -549,11 +541,11 @@ describe('task and habit row decoration', () => { expect(purgeBlock).toContain("await mutateTrashTask(task, () => api(`/trash/${task.id}`") }) - it('supports pointer dragging for desktop task and subtask completion', () => { - expect(app.match(/@pointerdown="startTaskPointer/g)?.length).toBe(3) - expect(app.match(/@pointermove="moveTaskPointer/g)?.length).toBe(3) - expect(app.match(/@pointerup="finishTaskPointer/g)?.length).toBe(3) - expect(app.match(/@pointercancel="cancelTaskPointer/g)?.length).toBe(3) + it('supports pointer dragging for desktop visible task completion', () => { + expect(app.match(/@pointerdown="startTaskPointer/g)?.length).toBe(1) + expect(app.match(/@pointermove="moveTaskPointer/g)?.length).toBe(1) + expect(app.match(/@pointerup="finishTaskPointer/g)?.length).toBe(1) + expect(app.match(/@pointercancel="cancelTaskPointer/g)?.length).toBe(1) }) it('supports pointer dragging for desktop habit rows in Today and Habits views', () => { @@ -579,27 +571,25 @@ describe('task and habit row decoration', () => { expect(mvpPanel).toContain('shouldToggleRowSwipe') }) - it('collapses and expands subtasks when the parent task is clicked', () => { - expect(app).toContain('const collapsedTaskIds = ref(new Set())') - expect(app).toContain('function toggleTaskChildren(task: Task)') - expect(app).toContain('selectTaskUnlessSwiped(node.task, true)') - expect(app).toContain('v-if="!collapsedTaskIds.has(node.task.id)" v-for="subtask in node.subtasks"') - expect(app).toContain(':aria-expanded="!collapsedTaskIds.has(node.task.id)"') + it('shows subtasks only in task details while keeping the parent progress summary', () => { + const overdue = app.slice(app.indexOf('
', app.indexOf('
/g)).toHaveLength(2) + expect(app).toContain('v-for="subtask in selectedTaskSubtasks"') + expect(app).toContain('class="subtask-detail"') }) - it('starts parent tasks collapsed when entering a task list or upcoming view', () => { - expect(app).toContain('function collapseLoadedTaskChildren()') - expect(app).toContain("else if (isTaskView(activeView.value)) {\n await loadAll()\n if (activeView.value === 'tasks' || activeView.value === 'upcoming') collapseLoadedTaskChildren()") - expect(app).toContain("else {\n await loadAll()\n if (view === 'tasks' || view === 'upcoming') collapseLoadedTaskChildren()") - expect(app).toContain('tasks.value.filter((task) => task.subtasks?.length).map((task) => task.id)') - }) - - it('shows visible completion buttons for tasks and subtasks while keeping swipe shortcuts', () => { + it('shows visible completion buttons for parent tasks while keeping subtask completion in details', () => { expect(app).not.toContain('class="sr-only" :aria-label="node.task.completed') expect(app).not.toContain('class="sr-only" :aria-label="subtask.completed') expect(app).toContain('class="task-check"') - expect(app.match(/class="task-check"/g)?.length).toBe(4) + expect(app.match(/class="task-check"/g)?.length).toBe(2) expect(app).toContain('class="task-check-mark"') + expect(app).toContain('class="subtask-detail" @click="toggle(subtask)"') expect(css).toContain('.task-check{') expect(css).toContain('.task-check-mark{') expect(css).toContain('.task-check:focus-visible{')