feat: 添加起始日期功能并更新相关组件与测试用例
- 在任务创建和编辑界面中新增起始日期字段,默认值为当天。 - 更新任务卡片展示逻辑,仅在起始日期不是当天时显示相关信息。 - 修改相关测试用例以覆盖起始日期的功能,包括批量创建和编辑任务的场景。 - 数据库模式版本更新至4,以支持起始日期字段的存储与迁移。
This commit is contained in:
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest'
|
|||||||
import { createMemoryTaskRepository } from '@/data/taskRepository'
|
import { createMemoryTaskRepository } from '@/data/taskRepository'
|
||||||
import { useTaskStore } from '@/stores/tasks'
|
import { useTaskStore } from '@/stores/tasks'
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
|
import { todayIso } from '@/utils/date'
|
||||||
|
|
||||||
import BatchAddDrawer from './BatchAddDrawer.vue'
|
import BatchAddDrawer from './BatchAddDrawer.vue'
|
||||||
|
|
||||||
@@ -68,13 +69,16 @@ describe('BatchAddDrawer 提交', () => {
|
|||||||
expect(store.byColumn.todo.map((task) => task.title)).toEqual(['第一条', '第二条', '第三条'])
|
expect(store.byColumn.todo.map((task) => task.title)).toEqual(['第一条', '第二条', '第三条'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('目标列、优先级、截止日期与标签整批共用', async () => {
|
it('目标列、优先级、起始日期、截止日期与标签整批共用', async () => {
|
||||||
const { store, wrapper } = setup()
|
const { store, wrapper } = setup()
|
||||||
await wrapper.find('textarea').setValue('第一条\n第二条')
|
await wrapper.find('textarea').setValue('第一条\n第二条')
|
||||||
|
|
||||||
await buttonByText(wrapper, '进行中')?.trigger('click')
|
await buttonByText(wrapper, '进行中')?.trigger('click')
|
||||||
await buttonByText(wrapper, '高')?.trigger('click')
|
await buttonByText(wrapper, '高')?.trigger('click')
|
||||||
await wrapper.find('input[type="date"]').setValue('2026-10-20')
|
// 两个日期输入按出现顺序为:起始日期、截止日期;
|
||||||
|
// 每次赋值后 DOM 会被重渲染重建,必须重新查询再操作下一个
|
||||||
|
await wrapper.findAll('input[type="date"]')[0]?.setValue('2026-10-12')
|
||||||
|
await wrapper.findAll('input[type="date"]')[1]?.setValue('2026-10-20')
|
||||||
|
|
||||||
const tagInput = wrapper.find('input[aria-label="添加标签"]')
|
const tagInput = wrapper.find('input[aria-label="添加标签"]')
|
||||||
await tagInput.setValue('前端')
|
await tagInput.setValue('前端')
|
||||||
@@ -85,11 +89,21 @@ describe('BatchAddDrawer 提交', () => {
|
|||||||
const created = store.byColumn.doing
|
const created = store.byColumn.doing
|
||||||
expect(created.map((task) => task.title)).toEqual(['第一条', '第二条'])
|
expect(created.map((task) => task.title)).toEqual(['第一条', '第二条'])
|
||||||
expect(created[0]?.priority).toBe(3)
|
expect(created[0]?.priority).toBe(3)
|
||||||
|
expect(created[0]?.startDate).toBe('2026-10-12')
|
||||||
expect(created[0]?.dueDate).toBe('2026-10-20')
|
expect(created[0]?.dueDate).toBe('2026-10-20')
|
||||||
expect(created[0]?.tags).toEqual(['前端'])
|
expect(created[0]?.tags).toEqual(['前端'])
|
||||||
expect(created[1]?.tags).toEqual(['前端'])
|
expect(created[1]?.tags).toEqual(['前端'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('起始日期默认预填当天', async () => {
|
||||||
|
const { store, wrapper } = setup()
|
||||||
|
await wrapper.find('textarea').setValue('A')
|
||||||
|
|
||||||
|
await buttonByText(wrapper, '添加 1 个任务')?.trigger('click')
|
||||||
|
|
||||||
|
expect(store.tasks[0]?.startDate).toBe(todayIso())
|
||||||
|
})
|
||||||
|
|
||||||
it('提交后请求关闭并提示数量', async () => {
|
it('提交后请求关闭并提示数量', async () => {
|
||||||
const { wrapper } = setup()
|
const { wrapper } = setup()
|
||||||
await wrapper.find('textarea').setValue('A\nB')
|
await wrapper.find('textarea').setValue('A\nB')
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 一行一条,整段粘贴即可录入一批 —— 从笔记、Markdown 清单、聊天记录里
|
* 一行一条,整段粘贴即可录入一批 —— 从笔记、Markdown 清单、聊天记录里
|
||||||
* 直接复制过来就能用,行首的「-」「1.」这类列表符号会被自动去掉。
|
* 直接复制过来就能用,行首的「-」「1.」这类列表符号会被自动去掉。
|
||||||
*
|
*
|
||||||
* 目标列、优先级、标签、截止日期是这一批**共用**的属性:批量录入时逐个
|
* 目标列、优先级、标签、起始日期、截止日期是这一批**共用**的属性:批量录入时逐个
|
||||||
* 任务调字段不现实,而「先统一建出来、再单独改个别任务」正是看板的用法。
|
* 任务调字段不现实,而「先统一建出来、再单独改个别任务」正是看板的用法。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ import { useTaskStore } from '@/stores/tasks'
|
|||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
import { COLUMNS, PRIORITY_LABELS, type ColumnId, type Priority } from '@/types/task'
|
import { COLUMNS, PRIORITY_LABELS, type ColumnId, type Priority } from '@/types/task'
|
||||||
import { parseTaskLines } from '@/utils/batch'
|
import { parseTaskLines } from '@/utils/batch'
|
||||||
|
import { todayIso } from '@/utils/date'
|
||||||
|
|
||||||
import DatePicker from './DatePicker.vue'
|
import DatePicker from './DatePicker.vue'
|
||||||
import SideDrawer from './SideDrawer.vue'
|
import SideDrawer from './SideDrawer.vue'
|
||||||
@@ -33,6 +34,7 @@ const text = ref('')
|
|||||||
const status = ref<ColumnId>('todo')
|
const status = ref<ColumnId>('todo')
|
||||||
const priority = ref<Priority>(2)
|
const priority = ref<Priority>(2)
|
||||||
const dueDate = ref<string | null>(null)
|
const dueDate = ref<string | null>(null)
|
||||||
|
const startDate = ref<string | null>(todayIso())
|
||||||
const tags = ref<string[]>([])
|
const tags = ref<string[]>([])
|
||||||
|
|
||||||
const titles = computed(() => parseTaskLines(text.value))
|
const titles = computed(() => parseTaskLines(text.value))
|
||||||
@@ -47,6 +49,8 @@ watch(
|
|||||||
status.value = 'todo'
|
status.value = 'todo'
|
||||||
priority.value = 2
|
priority.value = 2
|
||||||
dueDate.value = null
|
dueDate.value = null
|
||||||
|
// 每批都从「今天开工」开始,与 store 的默认值保持一致
|
||||||
|
startDate.value = todayIso()
|
||||||
tags.value = []
|
tags.value = []
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -60,6 +64,7 @@ function submit(): void {
|
|||||||
status: status.value,
|
status: status.value,
|
||||||
priority: priority.value,
|
priority: priority.value,
|
||||||
dueDate: dueDate.value,
|
dueDate: dueDate.value,
|
||||||
|
startDate: startDate.value,
|
||||||
tags: [...tags.value],
|
tags: [...tags.value],
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
@@ -138,6 +143,11 @@ function submit(): void {
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<span class="text-sm font-medium text-slate-600 dark:text-zinc-400">起始日期</span>
|
||||||
|
<DatePicker v-model="startDate" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5">
|
||||||
<span class="text-sm font-medium text-slate-600 dark:text-zinc-400">截止日期</span>
|
<span class="text-sm font-medium text-slate-600 dark:text-zinc-400">截止日期</span>
|
||||||
<DatePicker v-model="dueDate" />
|
<DatePicker v-model="dueDate" />
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-18T00:00:00.000Z',
|
createdAt: '2026-10-18T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-18T00:00:00.000Z',
|
updatedAt: '2026-10-18T00:00:00.000Z',
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-18T00:00:00.000Z',
|
createdAt: '2026-10-18T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-18T00:00:00.000Z',
|
updatedAt: '2026-10-18T00:00:00.000Z',
|
||||||
@@ -131,6 +132,24 @@ describe('截止日期', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('起始日期', () => {
|
||||||
|
it('起始日期为当天时不显示「开始」', () => {
|
||||||
|
const wrapper = mountCard({ startDate: dayjs().format('YYYY-MM-DD') })
|
||||||
|
expect(wrapper.text()).not.toContain('开始')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('起始日期不是当天时显示「M月D日 开始」', () => {
|
||||||
|
const startDate = dayjs().add(3, 'day').format('YYYY-MM-DD')
|
||||||
|
const wrapper = mountCard({ startDate })
|
||||||
|
expect(wrapper.text()).toContain('开始')
|
||||||
|
expect(wrapper.text()).toContain(dayjs(startDate).format('M月D日'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('未设置起始日期时不显示「开始」', () => {
|
||||||
|
expect(mountCard({ startDate: null }).text()).not.toContain('开始')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('已完成样式', () => {
|
describe('已完成样式', () => {
|
||||||
it('标题加删除线,但不再整卡降透明度', () => {
|
it('标题加删除线,但不再整卡降透明度', () => {
|
||||||
const wrapper = mountCard({ status: 'done' })
|
const wrapper = mountCard({ status: 'done' })
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import { computed } from 'vue'
|
|||||||
|
|
||||||
import { useTagColorStore } from '@/stores/tagColors'
|
import { useTagColorStore } from '@/stores/tagColors'
|
||||||
import { PRIORITY_LABELS, type MoveDirection, type Task } from '@/types/task'
|
import { PRIORITY_LABELS, type MoveDirection, type Task } from '@/types/task'
|
||||||
import { formatCompactDate, formatDateTime, formatDueDateForTask, isOverdue } from '@/utils/date'
|
import {
|
||||||
|
formatCompactDate,
|
||||||
|
formatDateTime,
|
||||||
|
formatDueDateForTask,
|
||||||
|
isOverdue,
|
||||||
|
todayIso,
|
||||||
|
} from '@/utils/date'
|
||||||
|
|
||||||
const MAX_VISIBLE_TAGS = 3
|
const MAX_VISIBLE_TAGS = 3
|
||||||
|
|
||||||
@@ -73,6 +79,19 @@ const dueLabel = computed(() =>
|
|||||||
? formatDueDateForTask(props.task.dueDate, props.task.status, props.task.completedAt)
|
? formatDueDateForTask(props.task.dueDate, props.task.status, props.task.completedAt)
|
||||||
: null,
|
: null,
|
||||||
)
|
)
|
||||||
|
/**
|
||||||
|
* 起始日期:只在「不是今天」时显示。
|
||||||
|
*
|
||||||
|
* 新建任务的起始日期默认就是当天,若照常展示,几乎每张卡片都会挂一条
|
||||||
|
* 相同的「M月D日 开始」,把标签和截止日期挤到第二行却提供不了新信息。
|
||||||
|
* 保留下来的是真正有排期差异的那些:提前排的、拖到未来开工的。
|
||||||
|
*/
|
||||||
|
const startLabel = computed(() => {
|
||||||
|
const startDate = props.task.startDate
|
||||||
|
if (!startDate || startDate === todayIso()) return null
|
||||||
|
const compact = formatCompactDate(startDate)
|
||||||
|
return compact ? `${compact} 开始` : null
|
||||||
|
})
|
||||||
const visibleTags = computed(() =>
|
const visibleTags = computed(() =>
|
||||||
props.task.tags.slice(0, MAX_VISIBLE_TAGS).map((tag) => ({
|
props.task.tags.slice(0, MAX_VISIBLE_TAGS).map((tag) => ({
|
||||||
name: tag,
|
name: tag,
|
||||||
@@ -244,6 +263,14 @@ const priorityTextClass = computed(() => {
|
|||||||
{{ PRIORITY_LABELS[task.priority] }}
|
{{ PRIORITY_LABELS[task.priority] }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="startLabel"
|
||||||
|
class="text-slate-500 dark:text-zinc-400"
|
||||||
|
:title="`开始于 ${props.task.startDate}`"
|
||||||
|
>
|
||||||
|
{{ startLabel }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="dueLabel"
|
v-if="dueLabel"
|
||||||
:class="
|
:class="
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import { createMemoryTaskRepository } from '@/data/taskRepository'
|
import { createMemoryTaskRepository } from '@/data/taskRepository'
|
||||||
import { useTaskStore } from '@/stores/tasks'
|
import { useTaskStore } from '@/stores/tasks'
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
|
import { todayIso } from '@/utils/date'
|
||||||
|
|
||||||
import TaskDetailDrawer from './TaskDetailDrawer.vue'
|
import TaskDetailDrawer from './TaskDetailDrawer.vue'
|
||||||
|
|
||||||
@@ -50,9 +51,10 @@ describe('TaskDetailDrawer 回填', () => {
|
|||||||
'原描述',
|
'原描述',
|
||||||
)
|
)
|
||||||
expect(wrapper.text()).toContain('#设计')
|
expect(wrapper.text()).toContain('#设计')
|
||||||
expect((wrapper.find('input[type="date"]').element as HTMLInputElement).value).toBe(
|
// 两个日期输入按出现顺序为:起始日期、截止日期;起始日期由 store 默认为当天
|
||||||
'2026-12-01',
|
const dateInputs = wrapper.findAll('input[type="date"]')
|
||||||
)
|
expect((dateInputs[0]?.element as HTMLInputElement).value).toBe(todayIso())
|
||||||
|
expect((dateInputs[1]?.element as HTMLInputElement).value).toBe('2026-12-01')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('回填当前优先级(低)', () => {
|
it('回填当前优先级(低)', () => {
|
||||||
@@ -81,7 +83,10 @@ describe('TaskDetailDrawer 保存', () => {
|
|||||||
await wrapper.find('input[aria-label="标题"]').setValue('新标题')
|
await wrapper.find('input[aria-label="标题"]').setValue('新标题')
|
||||||
await wrapper.find('textarea[aria-label="描述"]').setValue('新描述')
|
await wrapper.find('textarea[aria-label="描述"]').setValue('新描述')
|
||||||
await buttonByText(wrapper, '高')?.trigger('click')
|
await buttonByText(wrapper, '高')?.trigger('click')
|
||||||
await wrapper.find('input[type="date"]').setValue('2026-11-11')
|
// 两个日期输入按出现顺序为:起始日期、截止日期;
|
||||||
|
// 每次赋值后 DOM 会被重渲染重建,必须重新查询再操作下一个
|
||||||
|
await wrapper.findAll('input[type="date"]')[0]?.setValue('2026-11-05')
|
||||||
|
await wrapper.findAll('input[type="date"]')[1]?.setValue('2026-11-11')
|
||||||
await wrapper.find('input[aria-label="添加标签"]').setValue('前端')
|
await wrapper.find('input[aria-label="添加标签"]').setValue('前端')
|
||||||
await wrapper.find('input[aria-label="添加标签"]').trigger('keydown', { key: 'Enter' })
|
await wrapper.find('input[aria-label="添加标签"]').trigger('keydown', { key: 'Enter' })
|
||||||
await buttonByText(wrapper, '保存')?.trigger('click')
|
await buttonByText(wrapper, '保存')?.trigger('click')
|
||||||
@@ -90,6 +95,7 @@ describe('TaskDetailDrawer 保存', () => {
|
|||||||
expect(task?.title).toBe('新标题')
|
expect(task?.title).toBe('新标题')
|
||||||
expect(task?.description).toBe('新描述')
|
expect(task?.description).toBe('新描述')
|
||||||
expect(task?.priority).toBe(3)
|
expect(task?.priority).toBe(3)
|
||||||
|
expect(task?.startDate).toBe('2026-11-05')
|
||||||
expect(task?.dueDate).toBe('2026-11-11')
|
expect(task?.dueDate).toBe('2026-11-11')
|
||||||
expect(task?.tags).toEqual(['设计', '前端'])
|
expect(task?.tags).toEqual(['设计', '前端'])
|
||||||
expect(wrapper.emitted('close')).toHaveLength(1)
|
expect(wrapper.emitted('close')).toHaveLength(1)
|
||||||
@@ -118,7 +124,9 @@ describe('TaskDetailDrawer 保存', () => {
|
|||||||
|
|
||||||
it('清空截止日期会写入 null', async () => {
|
it('清空截止日期会写入 null', async () => {
|
||||||
const { store, wrapper, taskId } = setup()
|
const { store, wrapper, taskId } = setup()
|
||||||
await buttonByText(wrapper, '清除')?.trigger('click')
|
// 起始日期与截止日期各有一个「清除」,按出现顺序取后者
|
||||||
|
const clearButtons = wrapper.findAll('button').filter((node) => node.text() === '清除')
|
||||||
|
await clearButtons[1]?.trigger('click')
|
||||||
await buttonByText(wrapper, '保存')?.trigger('click')
|
await buttonByText(wrapper, '保存')?.trigger('click')
|
||||||
expect(store.tasks.find((item) => item.id === taskId)?.dueDate).toBeNull()
|
expect(store.tasks.find((item) => item.id === taskId)?.dueDate).toBeNull()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const description = ref('')
|
|||||||
const status = ref<ColumnId>('todo')
|
const status = ref<ColumnId>('todo')
|
||||||
const priority = ref<Priority>(2)
|
const priority = ref<Priority>(2)
|
||||||
const dueDate = ref<string | null>(null)
|
const dueDate = ref<string | null>(null)
|
||||||
|
const startDate = ref<string | null>(null)
|
||||||
const tags = ref<string[]>([])
|
const tags = ref<string[]>([])
|
||||||
const titleError = ref<string | null>(null)
|
const titleError = ref<string | null>(null)
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ watch(
|
|||||||
status.value = task?.status ?? 'todo'
|
status.value = task?.status ?? 'todo'
|
||||||
priority.value = task?.priority ?? 2
|
priority.value = task?.priority ?? 2
|
||||||
dueDate.value = task?.dueDate ?? null
|
dueDate.value = task?.dueDate ?? null
|
||||||
|
startDate.value = task?.startDate ?? null
|
||||||
tags.value = task ? [...task.tags] : []
|
tags.value = task ? [...task.tags] : []
|
||||||
titleError.value = null
|
titleError.value = null
|
||||||
confirmingClose.value = false
|
confirmingClose.value = false
|
||||||
@@ -92,6 +94,7 @@ const dirty = computed(() => {
|
|||||||
status.value !== task.status ||
|
status.value !== task.status ||
|
||||||
priority.value !== task.priority ||
|
priority.value !== task.priority ||
|
||||||
dueDate.value !== task.dueDate ||
|
dueDate.value !== task.dueDate ||
|
||||||
|
startDate.value !== task.startDate ||
|
||||||
!sameTags(tags.value, task.tags)
|
!sameTags(tags.value, task.tags)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -126,6 +129,7 @@ function save(): void {
|
|||||||
status: status.value,
|
status: status.value,
|
||||||
priority: priority.value,
|
priority: priority.value,
|
||||||
dueDate: dueDate.value,
|
dueDate: dueDate.value,
|
||||||
|
startDate: startDate.value,
|
||||||
tags: [...tags.value],
|
tags: [...tags.value],
|
||||||
})
|
})
|
||||||
emit('close')
|
emit('close')
|
||||||
@@ -239,6 +243,11 @@ function remove(): void {
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<span class="text-sm font-medium text-slate-600 dark:text-zinc-400">起始日期</span>
|
||||||
|
<DatePicker v-model="startDate" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5">
|
||||||
<span class="text-sm font-medium text-slate-600 dark:text-zinc-400">截止日期</span>
|
<span class="text-sm font-medium text-slate-600 dark:text-zinc-400">截止日期</span>
|
||||||
<DatePicker v-model="dueDate" />
|
<DatePicker v-model="dueDate" />
|
||||||
|
|||||||
+38
-2
@@ -21,6 +21,7 @@ function validTask(overrides: Record<string, unknown> = {}): Record<string, unkn
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: ['设计'],
|
tags: ['设计'],
|
||||||
dueDate: '2026-10-20',
|
dueDate: '2026-10-20',
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-18T00:00:00.000Z',
|
createdAt: '2026-10-18T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-18T00:00:00.000Z',
|
updatedAt: '2026-10-18T00:00:00.000Z',
|
||||||
@@ -97,6 +98,12 @@ describe('coerceTask', () => {
|
|||||||
expect(result?.sortOrder).toBe(0)
|
expect(result?.sortOrder).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('合法 startDate 保留,非法 startDate 收敛为 null', () => {
|
||||||
|
expect(coerceTask(validTask({ startDate: '2026-10-01' }))?.startDate).toBe('2026-10-01')
|
||||||
|
expect(coerceTask(validTask({ startDate: '2026-02-31' }))?.startDate).toBeNull()
|
||||||
|
expect(coerceTask(validTask({ startDate: undefined }))?.startDate).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
it('tags 过滤掉非字符串与空白项', () => {
|
it('tags 过滤掉非字符串与空白项', () => {
|
||||||
const result = coerceTask(validTask({ tags: ['ok', 1, null, ' ', 'ok2'] }))
|
const result = coerceTask(validTask({ tags: ['ok', 1, null, ' ', 'ok2'] }))
|
||||||
expect(result?.tags).toEqual(['ok', 'ok2'])
|
expect(result?.tags).toEqual(['ok', 'ok2'])
|
||||||
@@ -265,17 +272,46 @@ describe('版本迁移', () => {
|
|||||||
expect(state.tasks[0]?.archivedAt).toBe('2026-01-03T00:00:00.000Z')
|
expect(state.tasks[0]?.archivedAt).toBe('2026-01-03T00:00:00.000Z')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('v0/v1/v2 都能一路迁移到 v3', () => {
|
it('v3 数据迁移到 v4 时补上 startDate,其余字段不变', () => {
|
||||||
|
const v3 = JSON.stringify({
|
||||||
|
schemaVersion: 3,
|
||||||
|
tasks: [
|
||||||
|
{
|
||||||
|
id: 'legacy',
|
||||||
|
title: '旧任务',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 2,
|
||||||
|
sortOrder: 1024,
|
||||||
|
createdAt: '2026-01-01T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||||
|
completedAt: null,
|
||||||
|
archivedAt: null,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const state = parsePersistedState(v3)
|
||||||
|
|
||||||
|
expect(state.schemaVersion).toBe(SCHEMA_VERSION)
|
||||||
|
expect(state.tasks[0]?.startDate).toBeNull()
|
||||||
|
expect(state.tasks[0]?.deletedAt).toBeNull()
|
||||||
|
expect(state.tasks[0]?.title).toBe('旧任务')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('v0/v1/v2/v3 都能一路迁移到当前版本', () => {
|
||||||
const builds = [
|
const builds = [
|
||||||
() => ({ tasks: [{ id: 'a', title: 'A', status: 'todo', priority: 2 }] }),
|
() => ({ tasks: [{ id: 'a', title: 'A', status: 'todo', priority: 2 }] }),
|
||||||
() => ({ schemaVersion: 1, tasks: [{ id: 'a', title: 'A', status: 'todo', priority: 2 }] }),
|
() => ({ schemaVersion: 1, tasks: [{ id: 'a', title: 'A', status: 'todo', priority: 2 }] }),
|
||||||
() => ({ schemaVersion: 2, tasks: [{ id: 'a', title: 'A', status: 'todo', priority: 2 }] }),
|
() => ({ schemaVersion: 2, tasks: [{ id: 'a', title: 'A', status: 'todo', priority: 2 }] }),
|
||||||
|
() => ({ schemaVersion: 3, tasks: [{ id: 'a', title: 'A', status: 'todo', priority: 2 }] }),
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const build of builds) {
|
for (const build of builds) {
|
||||||
const state = parsePersistedState(JSON.stringify(build()))
|
const state = parsePersistedState(JSON.stringify(build()))
|
||||||
expect(state.schemaVersion).toBe(3)
|
expect(state.schemaVersion).toBe(SCHEMA_VERSION)
|
||||||
expect(state.tasks[0]?.deletedAt).toBeNull()
|
expect(state.tasks[0]?.deletedAt).toBeNull()
|
||||||
|
expect(state.tasks[0]?.startDate).toBeNull()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { isColumnId, isPriority, type Task } from '@/types/task'
|
import { isColumnId, isPriority, type Task } from '@/types/task'
|
||||||
|
|
||||||
export const SCHEMA_VERSION = 3
|
export const SCHEMA_VERSION = 4
|
||||||
|
|
||||||
/** 主数据在 localStorage 中的键名 */
|
/** 主数据在 localStorage 中的键名 */
|
||||||
export const STORAGE_KEY = 'kanban.tasks'
|
export const STORAGE_KEY = 'kanban.tasks'
|
||||||
@@ -69,6 +69,7 @@ export function coerceTask(value: unknown): Task | null {
|
|||||||
const tags = rawTags.filter((tag): tag is string => typeof tag === 'string' && tag.trim() !== '')
|
const tags = rawTags.filter((tag): tag is string => typeof tag === 'string' && tag.trim() !== '')
|
||||||
|
|
||||||
const dueDate = value.dueDate
|
const dueDate = value.dueDate
|
||||||
|
const startDate = value.startDate
|
||||||
const sortOrder = value.sortOrder
|
const sortOrder = value.sortOrder
|
||||||
const createdAt = readString(value, 'createdAt') ?? new Date().toISOString()
|
const createdAt = readString(value, 'createdAt') ?? new Date().toISOString()
|
||||||
const updatedAt = readString(value, 'updatedAt') ?? createdAt
|
const updatedAt = readString(value, 'updatedAt') ?? createdAt
|
||||||
@@ -85,6 +86,7 @@ export function coerceTask(value: unknown): Task | null {
|
|||||||
priority,
|
priority,
|
||||||
tags,
|
tags,
|
||||||
dueDate: isValidDateString(dueDate) ? dueDate : null,
|
dueDate: isValidDateString(dueDate) ? dueDate : null,
|
||||||
|
startDate: isValidDateString(startDate) ? startDate : null,
|
||||||
sortOrder: typeof sortOrder === 'number' && Number.isFinite(sortOrder) ? sortOrder : 0,
|
sortOrder: typeof sortOrder === 'number' && Number.isFinite(sortOrder) ? sortOrder : 0,
|
||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
@@ -117,6 +119,7 @@ type Migration = (state: PersistedState) => PersistedState
|
|||||||
* - v0:早期数据没有 schemaVersion 字段,任务结构与 v1 一致
|
* - v0:早期数据没有 schemaVersion 字段,任务结构与 v1 一致
|
||||||
* - v1 -> v2:新增 archivedAt 字段(归档功能)
|
* - v1 -> v2:新增 archivedAt 字段(归档功能)
|
||||||
* - v2 -> v3:新增 deletedAt 字段(回收站 / 软删除)
|
* - v2 -> v3:新增 deletedAt 字段(回收站 / 软删除)
|
||||||
|
* - v3 -> v4:新增 startDate 字段(起始日期)
|
||||||
* 未来新增字段时,在此登记 n -> n+1 的转换,并同步提升 SCHEMA_VERSION。
|
* 未来新增字段时,在此登记 n -> n+1 的转换,并同步提升 SCHEMA_VERSION。
|
||||||
*/
|
*/
|
||||||
const MIGRATIONS: Record<number, Migration> = {
|
const MIGRATIONS: Record<number, Migration> = {
|
||||||
@@ -129,6 +132,10 @@ const MIGRATIONS: Record<number, Migration> = {
|
|||||||
schemaVersion: 3,
|
schemaVersion: 3,
|
||||||
tasks: state.tasks.map((task) => ({ ...task, deletedAt: null })),
|
tasks: state.tasks.map((task) => ({ ...task, deletedAt: null })),
|
||||||
}),
|
}),
|
||||||
|
3: (state) => ({
|
||||||
|
schemaVersion: 4,
|
||||||
|
tasks: state.tasks.map((task) => ({ ...task, startDate: null })),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ function makeTask(id: string): Task {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-18T00:00:00.000Z',
|
createdAt: '2026-10-18T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-18T00:00:00.000Z',
|
updatedAt: '2026-10-18T00:00:00.000Z',
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-18T00:00:00.000Z',
|
createdAt: '2026-10-18T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-18T00:00:00.000Z',
|
updatedAt: '2026-10-18T00:00:00.000Z',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
|
|||||||
|
|
||||||
import { createMemoryTaskRepository, type TaskRepository } from '@/data/taskRepository'
|
import { createMemoryTaskRepository, type TaskRepository } from '@/data/taskRepository'
|
||||||
import type { Task } from '@/types/task'
|
import type { Task } from '@/types/task'
|
||||||
|
import { todayIso } from '@/utils/date'
|
||||||
|
|
||||||
import { useTagColorStore } from './tagColors'
|
import { useTagColorStore } from './tagColors'
|
||||||
import { UNDO_LIMIT, useTaskStore } from './tasks'
|
import { UNDO_LIMIT, useTaskStore } from './tasks'
|
||||||
@@ -33,6 +34,7 @@ describe('初始化', () => {
|
|||||||
priority: 3,
|
priority: 3,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-18T00:00:00.000Z',
|
createdAt: '2026-10-18T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-18T00:00:00.000Z',
|
updatedAt: '2026-10-18T00:00:00.000Z',
|
||||||
@@ -162,7 +164,14 @@ describe('批量创建', () => {
|
|||||||
it('每条任务可以带各自的列、优先级、标签与截止日期', () => {
|
it('每条任务可以带各自的列、优先级、标签与截止日期', () => {
|
||||||
const store = freshStore()
|
const store = freshStore()
|
||||||
const created = store.createTasks([
|
const created = store.createTasks([
|
||||||
{ title: 'A', status: 'doing', priority: 3, tags: ['前端'], dueDate: '2026-10-20' },
|
{
|
||||||
|
title: 'A',
|
||||||
|
status: 'doing',
|
||||||
|
priority: 3,
|
||||||
|
tags: ['前端'],
|
||||||
|
dueDate: '2026-10-20',
|
||||||
|
startDate: '2026-10-12',
|
||||||
|
},
|
||||||
{ title: 'B', status: 'done' },
|
{ title: 'B', status: 'done' },
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -170,9 +179,21 @@ describe('批量创建', () => {
|
|||||||
expect(created[0]?.priority).toBe(3)
|
expect(created[0]?.priority).toBe(3)
|
||||||
expect(created[0]?.tags).toEqual(['前端'])
|
expect(created[0]?.tags).toEqual(['前端'])
|
||||||
expect(created[0]?.dueDate).toBe('2026-10-20')
|
expect(created[0]?.dueDate).toBe('2026-10-20')
|
||||||
|
expect(created[0]?.startDate).toBe('2026-10-12')
|
||||||
expect(created[1]?.completedAt).not.toBeNull()
|
expect(created[1]?.completedAt).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('起始日期默认为当天,显式传 null 时保持为空', () => {
|
||||||
|
const store = freshStore()
|
||||||
|
const created = store.createTasks([
|
||||||
|
{ title: '未指定' },
|
||||||
|
{ title: '显式清空', startDate: null },
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(created[0]?.startDate).toBe(todayIso())
|
||||||
|
expect(created[1]?.startDate).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
it('跨列批量时各列内部都保持输入顺序', () => {
|
it('跨列批量时各列内部都保持输入顺序', () => {
|
||||||
const store = freshStore()
|
const store = freshStore()
|
||||||
store.createTasks([
|
store.createTasks([
|
||||||
@@ -533,6 +554,7 @@ describe('导出与导入', () => {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-18T00:00:00.000Z',
|
createdAt: '2026-10-18T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-18T00:00:00.000Z',
|
updatedAt: '2026-10-18T00:00:00.000Z',
|
||||||
@@ -691,6 +713,7 @@ function makePersistedTask(overrides: Partial<Task> = {}): Task {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-09-01T00:00:00.000Z',
|
createdAt: '2026-09-01T00:00:00.000Z',
|
||||||
updatedAt: '2026-09-01T00:00:00.000Z',
|
updatedAt: '2026-09-01T00:00:00.000Z',
|
||||||
@@ -938,6 +961,19 @@ describe('批量编辑', () => {
|
|||||||
expect(store.activeTasks.every((task) => task.dueDate === null)).toBe(true)
|
expect(store.activeTasks.every((task) => task.dueDate === null)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('批量设置与清除起始日期', () => {
|
||||||
|
const store = freshStore()
|
||||||
|
const a = store.createTask({ title: 'A' })
|
||||||
|
const b = store.createTask({ title: 'B' })
|
||||||
|
const ids = [a?.id ?? '', b?.id ?? '']
|
||||||
|
|
||||||
|
store.updateTasks(ids, { startDate: '2026-11-03' })
|
||||||
|
expect(store.activeTasks.every((task) => task.startDate === '2026-11-03')).toBe(true)
|
||||||
|
|
||||||
|
store.updateTasks(ids, { startDate: null })
|
||||||
|
expect(store.activeTasks.every((task) => task.startDate === null)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it('批量归档只处理未删除任务', () => {
|
it('批量归档只处理未删除任务', () => {
|
||||||
const store = freshStore()
|
const store = freshStore()
|
||||||
const a = store.createTask({ title: 'A', status: 'done' })
|
const a = store.createTask({ title: 'A', status: 'done' })
|
||||||
@@ -1279,6 +1315,7 @@ describe('撤销与重做', () => {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: longAgo,
|
createdAt: longAgo,
|
||||||
updatedAt: longAgo,
|
updatedAt: longAgo,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
type TaskRepository,
|
type TaskRepository,
|
||||||
} from '@/data/taskRepository'
|
} from '@/data/taskRepository'
|
||||||
import { COLUMN_IDS, DEFAULT_PRIORITY, type ColumnId, type Priority, type Task } from '@/types/task'
|
import { COLUMN_IDS, DEFAULT_PRIORITY, type ColumnId, type Priority, type Task } from '@/types/task'
|
||||||
|
import { todayIso } from '@/utils/date'
|
||||||
import { createId } from '@/utils/id'
|
import { createId } from '@/utils/id'
|
||||||
import { mergeTasks as mergeTaskLists } from '@/utils/merge'
|
import { mergeTasks as mergeTaskLists } from '@/utils/merge'
|
||||||
import {
|
import {
|
||||||
@@ -61,6 +62,8 @@ export interface CreateTaskInput {
|
|||||||
priority?: Priority
|
priority?: Priority
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
dueDate?: string | null
|
dueDate?: string | null
|
||||||
|
/** 不传则默认当天;显式传 null 表示不填起始日期 */
|
||||||
|
startDate?: string | null
|
||||||
status?: ColumnId
|
status?: ColumnId
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +361,8 @@ export const useTaskStore = defineStore('tasks', () => {
|
|||||||
priority: input.priority ?? DEFAULT_PRIORITY,
|
priority: input.priority ?? DEFAULT_PRIORITY,
|
||||||
tags: normalizeTags(input.tags ?? []),
|
tags: normalizeTags(input.tags ?? []),
|
||||||
dueDate: input.dueDate ?? null,
|
dueDate: input.dueDate ?? null,
|
||||||
|
// 起始日期默认当天:新建任务天然「今天开工」,用户显式清空时仍保持 null
|
||||||
|
startDate: input.startDate === undefined ? todayIso() : input.startDate,
|
||||||
sortOrder: sortOrderForPosition(working[status], 0),
|
sortOrder: sortOrderForPosition(working[status], 0),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
@@ -431,6 +436,7 @@ export const useTaskStore = defineStore('tasks', () => {
|
|||||||
if (patch.status !== undefined) taskPatch.status = patch.status
|
if (patch.status !== undefined) taskPatch.status = patch.status
|
||||||
if (patch.priority !== undefined) taskPatch.priority = patch.priority
|
if (patch.priority !== undefined) taskPatch.priority = patch.priority
|
||||||
if (patch.dueDate !== undefined) taskPatch.dueDate = patch.dueDate
|
if (patch.dueDate !== undefined) taskPatch.dueDate = patch.dueDate
|
||||||
|
if (patch.startDate !== undefined) taskPatch.startDate = patch.startDate
|
||||||
|
|
||||||
if (patch.addTags !== undefined || patch.removeTags !== undefined) {
|
if (patch.addTags !== undefined || patch.removeTags !== undefined) {
|
||||||
const tags = tagsAfterBatchPatch(task.tags, patch)
|
const tags = tagsAfterBatchPatch(task.tags, patch)
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export interface Task {
|
|||||||
tags: string[]
|
tags: string[]
|
||||||
/** 仅日期,格式 YYYY-MM-DD;无截止日期时为 null */
|
/** 仅日期,格式 YYYY-MM-DD;无截止日期时为 null */
|
||||||
dueDate: string | null
|
dueDate: string | null
|
||||||
|
/** 仅日期,格式 YYYY-MM-DD;任务尚未排期时为 null */
|
||||||
|
startDate: string | null
|
||||||
/** 同列内排序用的浮点数,采用相邻中点插入 */
|
/** 同列内排序用的浮点数,采用相邻中点插入 */
|
||||||
sortOrder: number
|
sortOrder: number
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ function makeTask(completedAt: string | null, overrides: Partial<Task> = {}): Ta
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-01T00:00:00.000Z',
|
createdAt: '2026-10-01T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-01T00:00:00.000Z',
|
updatedAt: '2026-10-01T00:00:00.000Z',
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-01T00:00:00.000Z',
|
createdAt: '2026-10-01T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-01T00:00:00.000Z',
|
updatedAt: '2026-10-01T00:00:00.000Z',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-18T00:00:00.000Z',
|
createdAt: '2026-10-18T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-18T00:00:00.000Z',
|
updatedAt: '2026-10-18T00:00:00.000Z',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-01T00:00:00.000Z',
|
createdAt: '2026-10-01T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-01T00:00:00.000Z',
|
updatedAt: '2026-10-01T00:00:00.000Z',
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: sequence * SORT_ORDER_STEP,
|
sortOrder: sequence * SORT_ORDER_STEP,
|
||||||
createdAt: '2026-10-18T00:00:00.000Z',
|
createdAt: '2026-10-18T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-18T00:00:00.000Z',
|
updatedAt: '2026-10-18T00:00:00.000Z',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-01T00:00:00.000Z',
|
createdAt: '2026-10-01T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-01T00:00:00.000Z',
|
updatedAt: '2026-10-01T00:00:00.000Z',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ function makeTask(id: string, tags: string[]): Task {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags,
|
tags,
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-01T00:00:00.000Z',
|
createdAt: '2026-10-01T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-01T00:00:00.000Z',
|
updatedAt: '2026-10-01T00:00:00.000Z',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: ['设计'],
|
tags: ['设计'],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-01T00:00:00.000Z',
|
createdAt: '2026-10-01T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-01T00:00:00.000Z',
|
updatedAt: '2026-10-01T00:00:00.000Z',
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export interface BatchPatch {
|
|||||||
priority?: Priority
|
priority?: Priority
|
||||||
/** null 表示清空截止日期 */
|
/** null 表示清空截止日期 */
|
||||||
dueDate?: string | null
|
dueDate?: string | null
|
||||||
|
/** null 表示清空起始日期 */
|
||||||
|
startDate?: string | null
|
||||||
addTags?: readonly string[]
|
addTags?: readonly string[]
|
||||||
removeTags?: readonly string[]
|
removeTags?: readonly string[]
|
||||||
/** true 归档,false 取消归档 */
|
/** true 归档,false 取消归档 */
|
||||||
@@ -78,6 +80,7 @@ export function isBatchPatchEmpty(patch: BatchPatch): boolean {
|
|||||||
patch.status === undefined &&
|
patch.status === undefined &&
|
||||||
patch.priority === undefined &&
|
patch.priority === undefined &&
|
||||||
patch.dueDate === undefined &&
|
patch.dueDate === undefined &&
|
||||||
|
patch.startDate === undefined &&
|
||||||
!patch.addTags?.length &&
|
!patch.addTags?.length &&
|
||||||
!patch.removeTags?.length &&
|
!patch.removeTags?.length &&
|
||||||
patch.archived === undefined
|
patch.archived === undefined
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-01T00:00:00.000Z',
|
createdAt: '2026-10-01T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-01T00:00:00.000Z',
|
updatedAt: '2026-10-01T00:00:00.000Z',
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ function makeTask(completedAt: string | null, overrides: Partial<Task> = {}): Ta
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
tags: [],
|
tags: [],
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
|
startDate: null,
|
||||||
sortOrder: 1024,
|
sortOrder: 1024,
|
||||||
createdAt: '2026-10-01T00:00:00.000Z',
|
createdAt: '2026-10-01T00:00:00.000Z',
|
||||||
updatedAt: '2026-10-01T00:00:00.000Z',
|
updatedAt: '2026-10-01T00:00:00.000Z',
|
||||||
|
|||||||
Reference in New Issue
Block a user