fix: unify task detail saving
This commit is contained in:
+67
-45
@@ -137,6 +137,8 @@ const selectedTaskRepeat = ref<RepeatOption>('none')
|
|||||||
const selectedAfterCompletionDays = ref('1')
|
const selectedAfterCompletionDays = ref('1')
|
||||||
const selectedRepeatError = ref('')
|
const selectedRepeatError = ref('')
|
||||||
const selectedTaskRecurrence = ref<Recurrence | null>(null)
|
const selectedTaskRecurrence = ref<Recurrence | null>(null)
|
||||||
|
const recurrenceLoading = ref(false)
|
||||||
|
const savingSelectedTask = ref(false)
|
||||||
const defaultRepeatConfig = (): TaskRepeatConfig => ({ frequency: 'daily', interval: 1, weekdays: [], monthDays: [], endMode: 'never', count: 10, until: '' })
|
const defaultRepeatConfig = (): TaskRepeatConfig => ({ frequency: 'daily', interval: 1, weekdays: [], monthDays: [], endMode: 'never', count: 10, until: '' })
|
||||||
const composeRepeatConfig = ref<TaskRepeatConfig>(defaultRepeatConfig())
|
const composeRepeatConfig = ref<TaskRepeatConfig>(defaultRepeatConfig())
|
||||||
const selectedRepeatConfig = ref<TaskRepeatConfig>(defaultRepeatConfig())
|
const selectedRepeatConfig = ref<TaskRepeatConfig>(defaultRepeatConfig())
|
||||||
@@ -194,23 +196,21 @@ function activateFloatingAdd(origin: { x: number; y: number }) {
|
|||||||
else if (activeView.value === 'countdowns') countdownComposer.value?.openCountdownComposer(origin)
|
else if (activeView.value === 'countdowns') countdownComposer.value?.openCountdownComposer(origin)
|
||||||
else if (['tasks', 'today', 'upcoming'].includes(activeView.value)) openTaskCompose()
|
else if (['tasks', 'today', 'upcoming'].includes(activeView.value)) openTaskCompose()
|
||||||
}
|
}
|
||||||
async function saveRepeat(task: Task, value: RepeatOption, config = selectedRepeatConfig.value) {
|
async function saveRepeat(task: Task, value: RepeatOption, config: TaskRepeatConfig, afterCompletionDays: string, recurrence: Recurrence | null) {
|
||||||
if (value !== 'none' && !task.due_at) throw new Error('请先设置截止时间')
|
if (value !== 'none' && !task.due_at) throw new Error('请先设置截止时间')
|
||||||
if (value === 'none') {
|
if (value === 'none') {
|
||||||
if (selectedTaskRecurrence.value) await api(`/recurrences/${selectedTaskRecurrence.value.id}`, { method: 'DELETE' })
|
if (recurrence) await api(`/recurrences/${recurrence.id}`, { method: 'DELETE' })
|
||||||
selectedTaskRecurrence.value = null
|
return null
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const recurrencePayload = buildTaskRecurrencePayload(value, { afterCompletionDays: selectedAfterCompletionDays.value, repeatConfig: config })
|
const recurrencePayload = buildTaskRecurrencePayload(value, { afterCompletionDays, repeatConfig: config })
|
||||||
if (selectedTaskRecurrence.value) {
|
if (recurrence) {
|
||||||
const updated = await api(`/recurrences/${selectedTaskRecurrence.value.id}`, { method: 'PATCH', body: JSON.stringify(recurrencePayload) }) as Recurrence
|
return await api(`/recurrences/${recurrence.id}`, { method: 'PATCH', body: JSON.stringify(recurrencePayload) }) as Recurrence
|
||||||
selectedTaskRecurrence.value = updated
|
|
||||||
} else {
|
|
||||||
selectedTaskRecurrence.value = await api('/recurrences', { method: 'POST', body: JSON.stringify({ task_id: task.id, ...recurrencePayload }) }) as Recurrence
|
|
||||||
}
|
}
|
||||||
|
return await api('/recurrences', { method: 'POST', body: JSON.stringify({ task_id: task.id, ...recurrencePayload }) }) as Recurrence
|
||||||
}
|
}
|
||||||
async function loadTaskRecurrence(task: Task) {
|
async function loadTaskRecurrence(task: Task) {
|
||||||
const token = ++recurrenceLoadToken
|
const token = ++recurrenceLoadToken
|
||||||
|
recurrenceLoading.value = true
|
||||||
selectedTaskRecurrence.value = null
|
selectedTaskRecurrence.value = null
|
||||||
selectedTaskRepeat.value = 'none'
|
selectedTaskRepeat.value = 'none'
|
||||||
selectedAfterCompletionDays.value = '1'
|
selectedAfterCompletionDays.value = '1'
|
||||||
@@ -223,14 +223,12 @@ async function loadTaskRecurrence(task: Task) {
|
|||||||
selectedTaskRepeat.value = parsed.option
|
selectedTaskRepeat.value = parsed.option
|
||||||
selectedAfterCompletionDays.value = String(parsed.afterCompletionDays)
|
selectedAfterCompletionDays.value = String(parsed.afterCompletionDays)
|
||||||
selectedRepeatConfig.value = recurrence?.rrule ? parseTaskRrule(recurrence.rrule) : defaultRepeatConfig()
|
selectedRepeatConfig.value = recurrence?.rrule ? parseTaskRrule(recurrence.rrule) : defaultRepeatConfig()
|
||||||
} catch (reason) { if (token === recurrenceLoadToken) fail(reason) }
|
} catch (reason) {
|
||||||
|
if (token === recurrenceLoadToken) fail(reason)
|
||||||
|
} finally {
|
||||||
|
if (token === recurrenceLoadToken) recurrenceLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function updateSelectedTaskRepeat() {
|
|
||||||
if (!selectedTask.value) return
|
|
||||||
try { selectedRepeatError.value = ''; await saveRepeat(selectedTask.value, selectedTaskRepeat.value); toast('重复设置已保存') }
|
|
||||||
catch (reason) { selectedRepeatError.value = reason instanceof Error ? reason.message : '保存失败'; fail(reason) }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitTaskCompose() {
|
async function submitTaskCompose() {
|
||||||
const normalized = normalizeRequiredName(composeTitle.value)
|
const normalized = normalizeRequiredName(composeTitle.value)
|
||||||
if (normalized.error) {
|
if (normalized.error) {
|
||||||
@@ -619,7 +617,7 @@ async function switchView(view: View, listId?: string) {
|
|||||||
async function loadTodayView() {
|
async function loadTodayView() {
|
||||||
await loadAll()
|
await loadAll()
|
||||||
}
|
}
|
||||||
function applyTaskUpdate(task: Task, updated: Task) {
|
function applyTaskUpdate(task: Task, updated: Task, updateSelected = true) {
|
||||||
const index = tasks.value.findIndex((item) => item.id === task.id)
|
const index = tasks.value.findIndex((item) => item.id === task.id)
|
||||||
if (index >= 0) tasks.value[index] = { ...tasks.value[index], ...updated }
|
if (index >= 0) tasks.value[index] = { ...tasks.value[index], ...updated }
|
||||||
const overdueIndex = overdueTasks.value.findIndex((item) => item.id === task.id)
|
const overdueIndex = overdueTasks.value.findIndex((item) => item.id === task.id)
|
||||||
@@ -627,11 +625,11 @@ function applyTaskUpdate(task: Task, updated: Task) {
|
|||||||
if (updated.completed && !completionExitingTaskIds.value.has(task.id)) overdueTasks.value.splice(overdueIndex, 1)
|
if (updated.completed && !completionExitingTaskIds.value.has(task.id)) overdueTasks.value.splice(overdueIndex, 1)
|
||||||
else overdueTasks.value[overdueIndex] = { ...overdueTasks.value[overdueIndex], ...updated }
|
else overdueTasks.value[overdueIndex] = { ...overdueTasks.value[overdueIndex], ...updated }
|
||||||
}
|
}
|
||||||
if (selectedTask.value?.id === task.id) selectedTask.value = { ...selectedTask.value, ...updated }
|
if (updateSelected && selectedTask.value?.id === task.id) selectedTask.value = { ...selectedTask.value, ...updated }
|
||||||
}
|
}
|
||||||
async function patchTask(task: Task, patch: Partial<Task>) {
|
async function patchTask(task: Task, patch: Partial<Task>, updateSelected = true) {
|
||||||
const updated = await api(`/tasks/${task.id}`, { method: 'PATCH', body: JSON.stringify({ ...patch, version: task.version }) }) as Task
|
const updated = await api(`/tasks/${task.id}`, { method: 'PATCH', body: JSON.stringify({ ...patch, version: task.version }) }) as Task
|
||||||
applyTaskUpdate(task, updated)
|
applyTaskUpdate(task, updated, updateSelected)
|
||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
const taskMutationNavigation = ref(0)
|
const taskMutationNavigation = ref(0)
|
||||||
@@ -842,30 +840,54 @@ async function refreshTodayAfterTaskSave() {
|
|||||||
loadTodayTaskSummary(),
|
loadTodayTaskSummary(),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
async function saveTask() {
|
async function saveTask(options?: { showSuccess?: boolean, expectedTaskId?: string, expectedSelectionToken?: number }) {
|
||||||
if (!selectedTask.value) return
|
const task = selectedTask.value
|
||||||
const normalized = normalizeRequiredName(selectedTask.value.title)
|
if (!task || (options?.expectedTaskId && task.id !== options.expectedTaskId)) return false
|
||||||
|
const normalized = normalizeRequiredName(task.title)
|
||||||
if (normalized.error) {
|
if (normalized.error) {
|
||||||
error.value = normalized.error
|
error.value = normalized.error
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
selectedTask.value.title = normalized.value
|
task.title = normalized.value
|
||||||
try {
|
try {
|
||||||
const task = selectedTask.value
|
|
||||||
const dueAt = fromDateTimeLocal(toDateTimeLocal(task.due_at))
|
const dueAt = fromDateTimeLocal(toDateTimeLocal(task.due_at))
|
||||||
const updated = await patchTask(task, { title: task.title.trim(), description: task.description, priority: Number(task.priority), due_at: dueAt, list_id: task.list_id } as Partial<Task>)
|
const updated = await patchTask(task, { title: task.title.trim(), description: task.description, priority: Number(task.priority), due_at: dueAt, list_id: task.list_id } as Partial<Task>, false)
|
||||||
if (selectedTaskRecurrence.value) {
|
const selectionMatches = options?.expectedSelectionToken === undefined || recurrenceLoadToken === options.expectedSelectionToken
|
||||||
if (dueAt) {
|
if (selectedTask.value?.id === task.id && selectionMatches) selectedTask.value = { ...selectedTask.value, ...updated }
|
||||||
await api(`/recurrences/${selectedTaskRecurrence.value.id}`, { method: 'PATCH', body: JSON.stringify({ due_at: dueAt }) })
|
|
||||||
} else {
|
|
||||||
selectedTaskRecurrence.value = null
|
|
||||||
selectedTaskRepeat.value = 'none'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (selectedTask.value) selectedTask.value = { ...selectedTask.value, ...updated }
|
|
||||||
await refreshTodayAfterTaskSave()
|
await refreshTodayAfterTaskSave()
|
||||||
|
if (options?.showSuccess !== false) toast('已保存')
|
||||||
|
return updated
|
||||||
|
} catch (reason) {
|
||||||
|
fail(reason)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function saveSelectedTaskChanges() {
|
||||||
|
if (savingSelectedTask.value || recurrenceLoading.value) return
|
||||||
|
const taskId = selectedTask.value?.id
|
||||||
|
if (!taskId) return
|
||||||
|
savingSelectedTask.value = true
|
||||||
|
const selectionToken = recurrenceLoadToken
|
||||||
|
const repeatValue = selectedTask.value?.due_at ? selectedTaskRepeat.value : 'none'
|
||||||
|
const repeatConfig = structuredClone(selectedRepeatConfig.value)
|
||||||
|
const afterCompletionDays = selectedAfterCompletionDays.value
|
||||||
|
const recurrence = selectedTaskRecurrence.value
|
||||||
|
selectedRepeatError.value = ''
|
||||||
|
try {
|
||||||
|
const taskSaved = await saveTask({ showSuccess: false, expectedTaskId: taskId, expectedSelectionToken: selectionToken })
|
||||||
|
if (!taskSaved || recurrenceLoadToken !== selectionToken || selectedTask.value?.id !== taskId) return
|
||||||
|
const repeatRecurrence = taskSaved.due_at ? recurrence : null
|
||||||
|
const updatedRecurrence = await saveRepeat(taskSaved, repeatValue, repeatConfig, afterCompletionDays, repeatRecurrence)
|
||||||
|
if (recurrenceLoadToken !== selectionToken || selectedTask.value?.id !== taskId) return
|
||||||
|
selectedTaskRecurrence.value = updatedRecurrence
|
||||||
toast('已保存')
|
toast('已保存')
|
||||||
} catch (reason) { fail(reason) }
|
} catch (reason) {
|
||||||
|
if (recurrenceLoadToken !== selectionToken || selectedTask.value?.id !== taskId) return
|
||||||
|
selectedRepeatError.value = reason instanceof Error ? reason.message : '保存失败'
|
||||||
|
fail(reason)
|
||||||
|
} finally {
|
||||||
|
savingSelectedTask.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function removeTask(task: Task) {
|
async function removeTask(task: Task) {
|
||||||
if (!window.confirm(`把“${task.title}”移到回收站?`)) return
|
if (!window.confirm(`把“${task.title}”移到回收站?`)) return
|
||||||
@@ -1384,19 +1406,19 @@ onUnmounted(() => {
|
|||||||
<aside v-if="selectedTask" class="detail" :class="{open:mobileDetail}">
|
<aside v-if="selectedTask" class="detail" :class="{open:mobileDetail}">
|
||||||
<div class="detail-head"><span>任务详情</span><button class="icon" aria-label="关闭详情" @click="closeTaskDetail"><X/></button></div>
|
<div class="detail-head"><span>任务详情</span><button class="icon" aria-label="关闭详情" @click="closeTaskDetail"><X/></button></div>
|
||||||
<div 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="任务标题"/></div>
|
||||||
<label>清单<select v-model="selectedTask.list_id" class="task-detail-field-input" @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" class="task-detail-field-input"><option v-for="list in lists" :key="list.id" :value="list.id">{{list.name}}</option></select></label>
|
||||||
<label class="task-detail-due-row">截止时间<input class="task-detail-due-input task-detail-field-input" :value="toDateTimeLocal(selectedTask.due_at)" type="datetime-local" @change="selectedTask!.due_at=($event.target as HTMLInputElement).value;saveTask()"></label>
|
<label class="task-detail-due-row">截止时间<input class="task-detail-due-input task-detail-field-input" :value="toDateTimeLocal(selectedTask.due_at)" type="datetime-local" @change="selectedTask!.due_at=($event.target as HTMLInputElement).value"></label>
|
||||||
<label>重复<select v-model="selectedTaskRepeat" class="task-detail-field-input" :disabled="!selectedTask.due_at"><option value="none">不重复</option><option value="daily">每天</option><option value="weekly">每周</option><option value="monthly">每月</option><option value="yearly">每年</option><option value="after_completion">完成后重复</option><option value="custom">自定义…</option></select><small v-if="!selectedTask.due_at" class="field-hint">请先设置截止时间,才能开启重复</small></label>
|
<label>重复<select v-model="selectedTaskRepeat" class="task-detail-field-input" :disabled="!selectedTask.due_at"><option value="none">不重复</option><option value="daily">每天</option><option value="weekly">每周</option><option value="monthly">每月</option><option value="yearly">每年</option><option value="after_completion">完成后重复</option><option value="custom">自定义…</option></select><small v-if="!selectedTask.due_at" class="field-hint">请先设置截止时间,才能开启重复</small></label>
|
||||||
<section v-if="selectedTaskRepeat==='after_completion'" class="after-completion-fields"><div>完成后 <input v-model="selectedAfterCompletionDays" type="number" min="1" max="3650" step="1" inputmode="numeric" aria-label="完成后重复天数"> 天重复</div><small>每次完成后,将截止时间顺延对应天数;首版永不结束</small></section>
|
<section v-if="selectedTaskRepeat==='after_completion'" class="after-completion-fields"><div>完成后 <input v-model="selectedAfterCompletionDays" type="number" min="1" max="3650" step="1" inputmode="numeric" aria-label="完成后重复天数"> 天重复</div><small>每次完成后,将截止时间顺延对应天数;首版永不结束</small></section>
|
||||||
<section v-if="selectedTaskRepeat==='custom'" class="repeat-custom-fields"><div><span>每隔</span><input v-model.number="selectedRepeatConfig.interval" type="number" min="1"><select v-model="selectedRepeatConfig.frequency"><option value="daily">天</option><option value="weekly">周</option><option value="monthly">月</option><option value="yearly">年</option></select></div><label v-if="selectedRepeatConfig.frequency==='weekly'">重复日期<span class="weekday-picker"><label v-for="day in weekdayOptions" :key="day.value"><input v-model="selectedRepeatConfig.weekdays" type="checkbox" :value="day.value">{{day.label}}</label></span></label><label v-if="selectedRepeatConfig.frequency==='monthly'">每月日期<input v-model="selectedRepeatConfig.monthDays" placeholder="例如 1,15,31" @change="selectedRepeatConfig.monthDays=String(($event.target as HTMLInputElement).value).split(',').map(Number).filter(Boolean)"></label><label>结束方式<select v-model="selectedRepeatConfig.endMode"><option value="never">永不结束</option><option value="date">指定日期</option><option value="count">重复次数</option></select></label><label v-if="selectedRepeatConfig.endMode==='date'">结束日期<input v-model="selectedRepeatConfig.until" type="date"></label><label v-if="selectedRepeatConfig.endMode==='count'">重复次数<input v-model.number="selectedRepeatConfig.count" type="number" min="1"></label></section>
|
<section v-if="selectedTaskRepeat==='custom'" class="repeat-custom-fields"><div><span>每隔</span><input v-model.number="selectedRepeatConfig.interval" type="number" min="1"><select v-model="selectedRepeatConfig.frequency"><option value="daily">天</option><option value="weekly">周</option><option value="monthly">月</option><option value="yearly">年</option></select></div><label v-if="selectedRepeatConfig.frequency==='weekly'">重复日期<span class="weekday-picker"><label v-for="day in weekdayOptions" :key="day.value"><input v-model="selectedRepeatConfig.weekdays" type="checkbox" :value="day.value">{{day.label}}</label></span></label><label v-if="selectedRepeatConfig.frequency==='monthly'">每月日期<input v-model="selectedRepeatConfig.monthDays" placeholder="例如 1,15,31" @change="selectedRepeatConfig.monthDays=String(($event.target as HTMLInputElement).value).split(',').map(Number).filter(Boolean)"></label><label>结束方式<select v-model="selectedRepeatConfig.endMode"><option value="never">永不结束</option><option value="date">指定日期</option><option value="count">重复次数</option></select></label><label v-if="selectedRepeatConfig.endMode==='date'">结束日期<input v-model="selectedRepeatConfig.until" type="date"></label><label v-if="selectedRepeatConfig.endMode==='count'">重复次数<input v-model.number="selectedRepeatConfig.count" type="number" min="1"></label></section>
|
||||||
<small v-if="selectedRepeatError" role="alert" class="field-error">{{selectedRepeatError}}</small><button type="button" class="soft-button repeat-save" :disabled="!selectedTask.due_at" @click="updateSelectedTaskRepeat">保存重复设置</button>
|
<small v-if="selectedRepeatError" role="alert" class="field-error">{{selectedRepeatError}}</small>
|
||||||
<div class="field markdown"><div class="field-label"><span>备注</span><span><button :class="{active:!markdownPreview}" @click="markdownPreview=false">编辑</button><button :class="{active:markdownPreview}" @click="markdownPreview=true">预览</button></span></div><div v-if="markdownPreview" class="markdown-preview" v-html="renderMarkdown(selectedTask.description)"/><textarea v-else v-model="selectedTask.description" rows="9" placeholder="支持 Markdown…" @blur="saveTask"/></div>
|
<div class="field markdown"><div class="field-label"><span>备注</span><span><button :class="{active:!markdownPreview}" @click="markdownPreview=false">编辑</button><button :class="{active:markdownPreview}" @click="markdownPreview=true">预览</button></span></div><div v-if="markdownPreview" class="markdown-preview" v-html="renderMarkdown(selectedTask.description)"/><textarea v-else v-model="selectedTask.description" rows="9" placeholder="支持 Markdown…"/></div>
|
||||||
<div class="subtasks"><div class="field-label"><span>子任务</span><button class="link" @click="addSubtask"><Plus/>添加</button></div><button v-for="subtask in selectedTaskSubtasks" :key="subtask.id" class="subtask-detail" @click="toggle(subtask)"><span class="check"><Check v-if="subtask.completed"/></span><span :class="{strike:subtask.completed}">{{subtask.title}}</span></button><span v-if="!selectedTaskSubtasks.length" class="hint">把这件事拆成更小的步骤</span></div>
|
<div class="subtasks"><div class="field-label"><span>子任务</span><button class="link" @click="addSubtask"><Plus/>添加</button></div><button v-for="subtask in selectedTaskSubtasks" :key="subtask.id" class="subtask-detail" @click="toggle(subtask)"><span class="check"><Check v-if="subtask.completed"/></span><span :class="{strike:subtask.completed}">{{subtask.title}}</span></button><span v-if="!selectedTaskSubtasks.length" class="hint">把这件事拆成更小的步骤</span></div>
|
||||||
<details class="more-settings" :open="moreSettingsOpen" @toggle="moreSettingsOpen=($event.target as HTMLDetailsElement).open"><summary>更多设置</summary><div class="more-settings-body">
|
<details class="more-settings" :open="moreSettingsOpen" @toggle="moreSettingsOpen=($event.target as HTMLDetailsElement).open"><summary>更多设置</summary><div class="more-settings-body">
|
||||||
<label>优先级<select v-model.number="selectedTask.priority" @change="saveTask"><option :value="0">无</option><option :value="1">低</option><option :value="2">中</option><option :value="3">高</option></select></label>
|
<label>优先级<select v-model.number="selectedTask.priority"><option :value="0">无</option><option :value="1">低</option><option :value="2">中</option><option :value="3">高</option></select></label>
|
||||||
</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" :disabled="savingSelectedTask || recurrenceLoading" @click="saveSelectedTaskChanges">{{savingSelectedTask?'正在保存…':recurrenceLoading?'正在读取…':'保存更改'}}</button><button class="danger-text" @click="removeTask(selectedTask)"><Trash2/>移到回收站</button></div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
+31
-10
@@ -275,11 +275,12 @@ describe('completion feedback motion', () => {
|
|||||||
expect(authBlock.indexOf("await syncBrowserTimezone(data.user?.timezone)")).toBeLessThan(authBlock.indexOf('await loadRestoredView()'))
|
expect(authBlock.indexOf("await syncBrowserTimezone(data.user?.timezone)")).toBeLessThan(authBlock.indexOf('await loadRestoredView()'))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('saves due removal atomically without deleting recurrence a second time', () => {
|
it('relies on the task PATCH for atomic due and recurrence synchronization', () => {
|
||||||
const saveTaskBlock = app.slice(app.indexOf('async function saveTask()'), app.indexOf('async function removeTask('))
|
const saveTaskBlock = app.slice(app.indexOf('async function saveTask('), app.indexOf('async function saveSelectedTaskChanges('))
|
||||||
expect(saveTaskBlock).not.toContain("method: 'DELETE'")
|
expect(saveTaskBlock).not.toContain('/recurrences/')
|
||||||
expect(saveTaskBlock).toContain('selectedTaskRecurrence.value = null')
|
expect(saveTaskBlock).toContain('as Partial<Task>, false')
|
||||||
expect(saveTaskBlock).toContain("selectedTaskRepeat.value = 'none'")
|
const unifiedSaveBlock = app.slice(app.indexOf('async function saveSelectedTaskChanges('), app.indexOf('async function removeTask('))
|
||||||
|
expect(unifiedSaveBlock).toContain('const repeatRecurrence = taskSaved.due_at ? recurrence : null')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps all task composer touch controls at least 44px on mobile without horizontal overflow', () => {
|
it('keeps all task composer touch controls at least 44px on mobile without horizontal overflow', () => {
|
||||||
@@ -739,8 +740,8 @@ describe('task and habit row decoration', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('reclassifies Today tasks after due edits without toggling page loading', () => {
|
it('reclassifies Today tasks after due edits without toggling page loading', () => {
|
||||||
const saveBlock = app.slice(app.indexOf('async function saveTask()'), app.indexOf('async function removeTask'))
|
const saveBlock = app.slice(app.indexOf('async function saveTask('), app.indexOf('async function removeTask'))
|
||||||
const refreshBlock = app.slice(app.indexOf('async function refreshTodayAfterTaskSave()'), app.indexOf('async function saveTask()'))
|
const refreshBlock = app.slice(app.indexOf('async function refreshTodayAfterTaskSave()'), app.indexOf('async function saveTask('))
|
||||||
expect(saveBlock).toContain('await refreshTodayAfterTaskSave()')
|
expect(saveBlock).toContain('await refreshTodayAfterTaskSave()')
|
||||||
expect(refreshBlock).toContain('loadTasksPage(request)')
|
expect(refreshBlock).toContain('loadTasksPage(request)')
|
||||||
expect(refreshBlock).toContain('loadOverdueTasks(request)')
|
expect(refreshBlock).toContain('loadOverdueTasks(request)')
|
||||||
@@ -966,13 +967,33 @@ describe('unified floating add interaction', () => {
|
|||||||
expect(app).toContain('完成后 <input v-model="selectedAfterCompletionDays"')
|
expect(app).toContain('完成后 <input v-model="selectedAfterCompletionDays"')
|
||||||
expect(app).toContain('每次完成后,将截止时间顺延对应天数;首版永不结束')
|
expect(app).toContain('每次完成后,将截止时间顺延对应天数;首版永不结束')
|
||||||
expect(app).toContain('buildTaskRecurrencePayload(composeRepeat.value')
|
expect(app).toContain('buildTaskRecurrencePayload(composeRepeat.value')
|
||||||
expect(app).toContain('buildTaskRecurrencePayload(value, { afterCompletionDays: selectedAfterCompletionDays.value')
|
expect(app).toContain('buildTaskRecurrencePayload(value, { afterCompletionDays, repeatConfig: config })')
|
||||||
expect(app).toContain("api(`/tasks/${task.id}/recurrence`)")
|
expect(app).toContain("api(`/tasks/${task.id}/recurrence`)")
|
||||||
const createBlock = app.slice(app.indexOf('async function submitTaskCompose()'), app.indexOf('function toggleSidebar()'))
|
const createBlock = app.slice(app.indexOf('async function submitTaskCompose()'), app.indexOf('function toggleSidebar()'))
|
||||||
expect(createBlock).toContain("api('/tasks',")
|
expect(createBlock).toContain("api('/tasks',")
|
||||||
expect(createBlock).not.toContain("api('/recurrences'")
|
expect(createBlock).not.toContain("api('/recurrences'")
|
||||||
const saveBlock = app.slice(app.indexOf('async function updateSelectedTaskRepeat()'), app.indexOf('async function submitTaskCompose()'))
|
const saveBlock = app.slice(app.indexOf('async function saveSelectedTaskChanges()'), app.indexOf('async function removeTask'))
|
||||||
expect(saveBlock).not.toContain('selectedTaskRepeat.value = parseTaskRecurrence')
|
expect(saveBlock).toContain("const taskId = selectedTask.value?.id")
|
||||||
|
expect(saveBlock).toContain('const selectionToken = recurrenceLoadToken')
|
||||||
|
expect(saveBlock).toContain("const repeatValue = selectedTask.value?.due_at ? selectedTaskRepeat.value : 'none'")
|
||||||
|
expect(saveBlock).toContain('structuredClone(selectedRepeatConfig.value)')
|
||||||
|
expect(saveBlock).toContain("const taskSaved = await saveTask({ showSuccess: false, expectedTaskId: taskId, expectedSelectionToken: selectionToken })")
|
||||||
|
expect(saveBlock).toContain("recurrenceLoadToken !== selectionToken")
|
||||||
|
expect(saveBlock).toContain('const repeatRecurrence = taskSaved.due_at ? recurrence : null')
|
||||||
|
expect(saveBlock).toContain('await saveRepeat(taskSaved, repeatValue, repeatConfig, afterCompletionDays, repeatRecurrence)')
|
||||||
|
expect(saveBlock).toContain("selectedRepeatError.value = ''")
|
||||||
|
expect(saveBlock).toContain("selectedRepeatError.value = reason instanceof Error ? reason.message : '保存失败'")
|
||||||
|
expect(saveBlock.indexOf("toast('已保存')")).toBeGreaterThan(saveBlock.indexOf('await saveRepeat(taskSaved, repeatValue, repeatConfig, afterCompletionDays, repeatRecurrence)'))
|
||||||
|
expect(saveBlock).toContain('if (savingSelectedTask.value || recurrenceLoading.value) return')
|
||||||
|
expect(saveBlock).toContain('savingSelectedTask.value = true')
|
||||||
|
expect(saveBlock).toContain('savingSelectedTask.value = false')
|
||||||
|
expect(saveBlock).toContain("const repeatValue = selectedTask.value?.due_at ? selectedTaskRepeat.value : 'none'")
|
||||||
|
expect(app).toContain(':disabled="savingSelectedTask || recurrenceLoading" @click="saveSelectedTaskChanges"')
|
||||||
|
expect(app).not.toContain('@blur="saveTask()"')
|
||||||
|
expect(app).not.toContain('@change="saveTask()"')
|
||||||
|
expect(app).not.toContain('value;saveTask()')
|
||||||
|
expect(app).not.toContain('async function updateSelectedTaskRepeat()')
|
||||||
|
expect(app).not.toContain('保存重复设置')
|
||||||
expect(css).toContain('.after-completion-fields input{width:76px;min-height:44px')
|
expect(css).toContain('.after-completion-fields input{width:76px;min-height:44px')
|
||||||
expect(app).toContain('<option value="custom">自定义…</option>')
|
expect(app).toContain('<option value="custom">自定义…</option>')
|
||||||
expect(app).toContain('class="repeat-custom-fields"')
|
expect(app).toContain('class="repeat-custom-fields"')
|
||||||
|
|||||||
Reference in New Issue
Block a user