refactor: reveal task detail on selection
ci / gitleaks (push) Successful in 7s
ci / docker (push) Successful in 4m23s

This commit is contained in:
2026-09-08 19:30:44 +08:00
parent 5ec081223a
commit 4d6dc25749
3 changed files with 26 additions and 8 deletions
+8 -5
View File
@@ -703,6 +703,10 @@ async function addSubtask() {
if (!subtaskTitle) return if (!subtaskTitle) return
try { const child = await api('/tasks', { method: 'POST', body: JSON.stringify({ title: subtaskTitle, list_id: selectedTask.value.list_id, parent_id: selectedTask.value.id }) }); if (selectedTask.value) selectedTask.value.subtasks = [...(selectedTask.value.subtasks ?? []), child]; tasks.value.push(child); toast('子任务已添加') } catch (reason) { fail(reason) } try { const child = await api('/tasks', { method: 'POST', body: JSON.stringify({ title: subtaskTitle, list_id: selectedTask.value.list_id, parent_id: selectedTask.value.id }) }); if (selectedTask.value) selectedTask.value.subtasks = [...(selectedTask.value.subtasks ?? []), child]; tasks.value.push(child); toast('子任务已添加') } catch (reason) { fail(reason) }
} }
function closeTaskDetail() {
mobileDetail.value = false
selectedTask.value = null
}
function selectTask(task: Task) { function selectTask(task: Task) {
selectedTask.value = { ...task, subtasks: task.subtasks ? [...task.subtasks] : [] } selectedTask.value = { ...task, subtasks: task.subtasks ? [...task.subtasks] : [] }
markdownPreview.value = false; moreSettingsOpen.value = false; mobileDetail.value = true markdownPreview.value = false; moreSettingsOpen.value = false; mobileDetail.value = true
@@ -770,7 +774,7 @@ onMounted(bootstrap)
<small v-if="error" role="alert">{{ error }}</small> <small v-if="error" role="alert">{{ error }}</small>
</section> </section>
</div> </div>
<div v-else class="shell" :class="{ 'sidebar-collapsed': sidebarCollapsed, 'mobile-sidebar-open': mobileSidebar }"> <div v-else class="shell" :class="{ 'sidebar-collapsed': sidebarCollapsed, 'detail-open': Boolean(selectedTask), 'mobile-sidebar-open': mobileSidebar }">
<div v-if="mobileSidebar || mobileDetail" class="scrim" @click="mobileSidebar=false;mobileDetail=false" /> <div v-if="mobileSidebar || mobileDetail" class="scrim" @click="mobileSidebar=false;mobileDetail=false" />
<aside class="sidebar" :class="{ open: mobileSidebar }"> <aside class="sidebar" :class="{ open: mobileSidebar }">
<div class="brand-row"><div class="brand small">do<span>do</span></div><button class="icon mobile-only" aria-label="关闭菜单" @click="mobileSidebar=false"><X /></button></div> <div class="brand-row"><div class="brand small">do<span>do</span></div><button class="icon mobile-only" aria-label="关闭菜单" @click="mobileSidebar=false"><X /></button></div>
@@ -853,9 +857,9 @@ onMounted(bootstrap)
</template> </template>
</main> </main>
<aside class="detail" :class="{open:mobileDetail}"> <aside v-if="selectedTask" class="detail" :class="{open:mobileDetail}">
<div class="detail-head"><span>任务详情</span><button class="icon mobile-only" aria-label="关闭详情" @click="mobileDetail=false"><X/></button></div> <div class="detail-head"><span>任务详情</span><button class="icon" aria-label="关闭详情" @click="closeTaskDetail"><X/></button></div>
<div v-if="selectedTask" class="detail-form"> <div class="detail-form">
<div class="detail-title"><button class="check large" :class="`p${selectedTask.priority}`" @click="toggle(selectedTask)"><Check v-if="selectedTask.completed"/></button><textarea v-model="selectedTask.title" rows="2" aria-label="任务标题" @blur="saveTask"/></div> <div class="detail-title"><button class="check large" :class="`p${selectedTask.priority}`" @click="toggle(selectedTask)"><Check v-if="selectedTask.completed"/></button><textarea v-model="selectedTask.title" rows="2" aria-label="任务标题" @blur="saveTask"/></div>
<label>清单<select v-model="selectedTask.list_id" @change="saveTask"><option v-for="list in lists" :key="list.id" :value="list.id">{{list.name}}</option></select></label> <label>清单<select v-model="selectedTask.list_id" @change="saveTask"><option v-for="list in lists" :key="list.id" :value="list.id">{{list.name}}</option></select></label>
<label>截止时间<input :value="toDateTimeLocal(selectedTask.due_at)" type="datetime-local" @change="selectedTask!.due_at=($event.target as HTMLInputElement).value;saveTask()"></label> <label>截止时间<input :value="toDateTimeLocal(selectedTask.due_at)" type="datetime-local" @change="selectedTask!.due_at=($event.target as HTMLInputElement).value;saveTask()"></label>
@@ -868,7 +872,6 @@ onMounted(bootstrap)
</div></details> </div></details>
<div class="detail-actions"><button class="secondary" @click="saveTask">保存更改</button><button class="danger-text" @click="removeTask(selectedTask)"><Trash2/>移到回收站</button></div> <div class="detail-actions"><button class="secondary" @click="saveTask">保存更改</button><button class="danger-text" @click="removeTask(selectedTask)"><Trash2/>移到回收站</button></div>
</div> </div>
<div v-else class="paper"><ListChecks/><b>选中一个任务</b><p>日期优先级子任务和 Markdown 备注会出现在这里</p></div>
</aside> </aside>
<div v-if="mobileMore" class="more-mask" @click.self="mobileMore=false;switchView('settings')"><section id="mobile-more-menu" class="more-sheet" role="dialog" aria-modal="true" aria-label="更多导航" @click.stop><div class="more-sheet-head"><b>更多</b><button class="icon" aria-label="关闭更多菜单" @click="mobileMore=false"><X/></button></div><button @click="switchView('settings')"><Settings/>设置与数据</button></section></div> <div v-if="mobileMore" class="more-mask" @click.self="mobileMore=false;switchView('settings')"><section id="mobile-more-menu" class="more-sheet" role="dialog" aria-modal="true" aria-label="更多导航" @click.stop><div class="more-sheet-head"><b>更多</b><button class="icon" aria-label="关闭更多菜单" @click="mobileMore=false"><X/></button></div><button @click="switchView('settings')"><Settings/>设置与数据</button></section></div>
File diff suppressed because one or more lines are too long
+16 -1
View File
@@ -402,6 +402,20 @@ describe('unified floating add interaction', () => {
}) })
}) })
describe('desktop task detail disclosure', () => {
it('gives the task list the full remaining width until a task is selected', () => {
expect(app).toContain("'detail-open': Boolean(selectedTask)")
expect(app).toContain('<aside v-if="selectedTask" class="detail"')
expect(app).toContain('@click="closeTaskDetail"')
expect(app).toContain('function closeTaskDetail()')
expect(app).not.toContain('<div v-else class="paper">')
expect(css).toContain('.shell{height:100vh;display:grid;grid-template-columns:238px minmax(430px,1fr) 0;')
expect(css).toContain('.shell.detail-open{grid-template-columns:238px minmax(430px,1fr) 350px}')
expect(css).toContain('.shell.sidebar-collapsed.detail-open{grid-template-columns:0 minmax(430px,1fr) 350px}')
expect(css).toContain('transition:grid-template-columns .22s ease')
})
})
describe('desktop sidebar collapse & folder scrollbar', () => { describe('desktop sidebar collapse & folder scrollbar', () => {
it('keeps overlay scrims out of the desktop grid layout', () => { it('keeps overlay scrims out of the desktop grid layout', () => {
expect(css).toContain('.scrim{display:none}') expect(css).toContain('.scrim{display:none}')
@@ -411,7 +425,8 @@ describe('desktop sidebar collapse & folder scrollbar', () => {
it('lets the top menu collapse the desktop sidebar', () => { it('lets the top menu collapse the desktop sidebar', () => {
expect(app).toContain('sidebar-collapsed') expect(app).toContain('sidebar-collapsed')
expect(app).toContain('toggleSidebar') expect(app).toContain('toggleSidebar')
expect(css).toContain('.shell.sidebar-collapsed{grid-template-columns:0 minmax(430px,1fr) 350px}') expect(css).toContain('.shell.sidebar-collapsed{grid-template-columns:0 minmax(430px,1fr) 0}')
expect(css).toContain('.shell.sidebar-collapsed.detail-open{grid-template-columns:0 minmax(430px,1fr) 350px}')
}) })
it('hides the folders scrollbar while staying scrollable', () => { it('hides the folders scrollbar while staying scrollable', () => {