feat: redesign task detail paper flow
This commit is contained in:
+75
-11
@@ -322,10 +322,70 @@ describe('approved UI detail direction', () => {
|
||||
expect(css).not.toContain('.task-detail-trigger')
|
||||
})
|
||||
|
||||
it('keeps task details on a 12px rhythm with fixed labels and right-aligned controls', () => {
|
||||
expect(css).toContain('.detail-form{min-width:0;grid-template-columns:minmax(0,1fr);padding:19px;display:grid;gap:12px}')
|
||||
expect(css).toContain('.detail-form>label{grid-template-columns:80px 1fr;align-items:center}')
|
||||
expect(css).toContain('.task-detail-field-input{width:190px!important;max-width:100%;justify-self:end}')
|
||||
it('uses the approved paper-flow task detail structure and order', () => {
|
||||
const detail = app.slice(app.indexOf('<AppSheet v-if="selectedTask"'), app.indexOf('</AppSheet>', app.indexOf('<AppSheet v-if="selectedTask"')))
|
||||
const orderedMarkers = [
|
||||
'class="detail-head"',
|
||||
'class="detail-form"',
|
||||
'class="detail-title"',
|
||||
'class="task-detail-arrangement"',
|
||||
'class="field markdown task-detail-notes"',
|
||||
'class="subtasks task-detail-subtasks"',
|
||||
'class="more-settings"',
|
||||
'class="detail-actions"',
|
||||
]
|
||||
let cursor = -1
|
||||
for (const marker of orderedMarkers) {
|
||||
const next = detail.indexOf(marker)
|
||||
expect(next).toBeGreaterThan(cursor)
|
||||
cursor = next
|
||||
}
|
||||
expect(detail).toContain('<span class="task-detail-field-label">清单</span>')
|
||||
expect(detail).toContain('<span class="task-detail-field-label">截止日期</span>')
|
||||
expect(detail).toContain('<span class="task-detail-field-label">时间</span>')
|
||||
expect(detail).toContain('<span class="task-detail-field-label">重复</span>')
|
||||
expect(detail).toContain('<button class="danger-text detail-trash"')
|
||||
expect(detail).toContain('<button class="primary detail-save"')
|
||||
})
|
||||
|
||||
it('locks the task detail flow and exposes safe subtask removal semantics', () => {
|
||||
const detail = app.slice(app.indexOf('<AppSheet v-if="selectedTask"'), app.indexOf('</AppSheet>', app.indexOf('<AppSheet v-if="selectedTask"')))
|
||||
expect(detail).toContain(':busy="taskDetailBusy"')
|
||||
expect(detail).toContain('@submit.prevent="saveSelectedTaskChanges"')
|
||||
expect(detail).toContain('<button class="icon" type="button" :disabled="taskDetailBusy"')
|
||||
expect(detail).toContain('<fieldset class="detail-form" :disabled="taskDetailBusy">')
|
||||
expect(detail).toContain('class="subtask-remove" type="button"')
|
||||
expect(detail).toContain('@click="removeSubtask(subtask)"')
|
||||
expect(detail).toContain('<button class="danger-text detail-trash" type="button" :disabled="taskDetailBusy"')
|
||||
expect(detail).toContain('<button class="primary detail-save" type="submit"')
|
||||
const buttons = [...detail.matchAll(/<button\b([^>]*)>/g)]
|
||||
expect(buttons.length).toBeGreaterThan(0)
|
||||
for (const [, attributes] of buttons) expect(attributes).toMatch(/\btype="(?:button|submit)"/)
|
||||
const removeBlock = app.slice(app.indexOf('async function removeSubtask('), app.indexOf('function closeTaskDetail('))
|
||||
expect(removeBlock).toContain('if (taskDetailBusy.value) return')
|
||||
expect(removeBlock).toContain("await api(`/tasks/${subtask.id}`, { method: 'DELETE' })")
|
||||
expect(removeBlock).toContain('parent.subtasks = (parent.subtasks ?? []).filter')
|
||||
expect(removeBlock).toContain('tasks.value = tasks.value.filter')
|
||||
expect(removeBlock).not.toContain('selectedTask.value = null')
|
||||
})
|
||||
|
||||
it('matches the approved paper-flow geometry and material', () => {
|
||||
expect(css).toContain('.shell.detail-open{grid-template-columns:236px minmax(430px,1fr) 350px}')
|
||||
expect(css).toContain('@media(max-width:1050px) and (min-width:931px){.shell.detail-open{grid-template-columns:236px minmax(0,1fr) 350px}')
|
||||
expect(css).toContain('.detail{min-width:0;border-left:1px solid var(--line);background:#fffdf8;overflow:hidden;display:flex;flex-direction:column}')
|
||||
expect(css).toContain('.detail-head{height:58px;flex:0 0 58px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;')
|
||||
expect(css).toContain('.detail-form{min-width:0;border:0;margin:0;grid-template-columns:minmax(0,1fr);padding:18px;display:grid;gap:18px;overflow-y:auto;')
|
||||
expect(css).toContain('.detail-title textarea{min-width:0;min-height:62px;padding:0;border:0;background:transparent;box-shadow:none;resize:none;outline:none;font-size:20px;line-height:27px;')
|
||||
expect(css).toContain('.task-detail-arrangement{display:grid;gap:13px;padding-bottom:18px;border-bottom:1px solid #e4dbcf}')
|
||||
expect(css).toContain('.task-detail-date-time{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px}')
|
||||
expect(css).toContain('.task-detail-field-input{width:100%!important;min-height:44px;border-radius:10px}')
|
||||
expect(css).toContain('.detail-actions{position:sticky;bottom:0;z-index:3;min-height:69px;')
|
||||
expect(css).toContain('.detail-save{min-width:88px}')
|
||||
expect(css).toContain('.subtask-detail,.after-completion-fields,.repeat-custom-fields{width:100%;max-width:100%;min-width:0;border:0;border-radius:0;box-shadow:none;background:transparent;overflow:visible}')
|
||||
expect(css).toContain('.subtask-remove{width:44px;height:44px;flex:0 0 44px;')
|
||||
expect(css).toContain('@media(max-width:930px){.detail{position:relative;z-index:auto;left:auto;right:auto;top:auto;bottom:auto;width:100%;max-height:min(88dvh,760px);')
|
||||
expect(css).toContain('.task-detail-date-time{grid-template-columns:minmax(0,1fr)}')
|
||||
expect(css).toContain('.task-detail-date-control .task-compose-date-clear,.task-detail-time-control .task-compose-time-remove{min-width:44px;min-height:44px;width:44px;height:44px}')
|
||||
})
|
||||
|
||||
it('opens Today habit bodies with keyboard parity while check clicks stay isolated', () => {
|
||||
@@ -1177,11 +1237,12 @@ describe('task detail layout', () => {
|
||||
expect(css).toContain('.subtask-detail{width:100%;min-width:0;max-width:100%;overflow-wrap:anywhere;word-break:break-word;white-space:normal;display:flex;')
|
||||
})
|
||||
|
||||
it('keeps the list, due datetime and repeat controls equally compact', () => {
|
||||
expect(app).toContain('<div class="task-detail-due-row">')
|
||||
it('keeps the list, date, time, and repeat controls in the paper arrangement group', () => {
|
||||
expect(app).toContain('<section class="task-detail-arrangement" aria-label="安排">')
|
||||
expect(app).toContain('<div class="task-detail-date-time">')
|
||||
expect(app).toContain('v-model="selectedDueDate" class="task-detail-due-input task-detail-field-input"')
|
||||
expect(app.match(/class="task-detail-field-input"/g)).toHaveLength(2)
|
||||
expect(css).toContain('.task-detail-field-input{width:190px!important;max-width:100%;justify-self:end}')
|
||||
expect(css).toContain('.task-detail-field-input{width:100%!important;min-height:44px;border-radius:10px}')
|
||||
expect(css).toContain('.task-detail-due-input{padding-inline:9px!important}')
|
||||
})
|
||||
})
|
||||
@@ -1218,7 +1279,7 @@ describe('unified floating add interaction', () => {
|
||||
expect(app).toContain('const composeRepeat = ref')
|
||||
expect(app).toContain('const selectedTaskRepeat = ref')
|
||||
expect(app).toContain('重复<select v-model="composeRepeat"')
|
||||
expect(app).toContain('重复<select v-model="selectedTaskRepeat"')
|
||||
expect(app).toContain('<span class="task-detail-field-label">重复</span><select v-model="selectedTaskRepeat"')
|
||||
expect(app).toContain('<option value="yearly">每年</option><option value="after_completion">完成后重复</option><option value="custom">自定义…</option>')
|
||||
expect(app).toContain('完成后 <input v-model="composeAfterCompletionDays"')
|
||||
expect(app).toContain('完成后 <input v-model="selectedAfterCompletionDays"')
|
||||
@@ -1247,9 +1308,12 @@ describe('unified floating add interaction', () => {
|
||||
expect(saveBlock.indexOf("toast('已保存')")).toBeGreaterThan(saveBlock.indexOf('await saveRepeat(taskSaved, repeatValue, repeatConfig, afterCompletionDays, recurrence)'))
|
||||
expect(saveBlock).toContain('if (savingSelectedTask.value || recurrenceLoading.value) return')
|
||||
expect(saveBlock).toContain('savingSelectedTask.value = true')
|
||||
expect(saveBlock).toContain('savingSelectedTask.value = false')
|
||||
expect(saveBlock).toContain('} finally {\n savingSelectedTask.value = false\n }')
|
||||
expect(saveBlock).not.toContain("if (recurrenceLoadToken === selectionToken && selectedTask.value?.id === taskId) savingSelectedTask.value = false")
|
||||
const saveTaskBlock = app.slice(app.indexOf('async function saveTask('), app.indexOf('async function saveSelectedTaskChanges('))
|
||||
expect(saveTaskBlock).toContain('if (selectionMatches && selectedTask.value?.id === task.id) fail(reason)')
|
||||
expect(saveBlock).toContain("const repeatValue = selectedDueDate.value ? selectedTaskRepeat.value : 'none'")
|
||||
expect(app).toContain(':disabled="savingSelectedTask || recurrenceLoading" @click="saveSelectedTaskChanges"')
|
||||
expect(app).toContain('type="submit" :disabled="taskDetailBusy"')
|
||||
expect(app).not.toContain('@blur="saveTask()"')
|
||||
expect(app).not.toContain('@change="saveTask()"')
|
||||
expect(app).not.toContain('value;saveTask()')
|
||||
@@ -1422,7 +1486,7 @@ describe('quiet index sidebar parity', () => {
|
||||
expect(css).toContain('.brand-row{height:68px;')
|
||||
expect(css).toContain('border-bottom:1px solid rgba(222,205,185,.75)')
|
||||
expect(css).toContain('.primary-nav{display:grid;padding:10px 11px 8px;gap:2px}')
|
||||
expect(css).toContain('@media(max-width:1050px) and (min-width:931px){.shell.detail-open{grid-template-columns:236px minmax(400px,1fr) minmax(280px,30vw)}')
|
||||
expect(css).toContain('@media(max-width:1050px) and (min-width:931px){.shell.detail-open{grid-template-columns:236px minmax(0,1fr) 350px}')
|
||||
expect(css).not.toContain('grid-template-columns:220px')
|
||||
expect(css).toContain('@media(max-width:930px){.shell')
|
||||
expect(css).toContain('left:0;width:236px;max-width:86vw;')
|
||||
|
||||
Reference in New Issue
Block a user