feat: 回收站、批量编辑、合并导入与标签治理
- 数据:Task 增加 deletedAt,schema v2→v3;删除改为软删除,保留 30 天后自动清理 - 回收站:支持恢复、永久删除、二次确认清空;导航栏入口带数量角标 - 批量编辑:多选后统一移动列、改优先级、标签增删、设置截止日、归档 - 合并导入:按 id 匹配、updatedAt 新者胜;覆盖/合并二选一,执行前自动快照 - 标签治理:全局重命名、合并、删除;回收站任务不参与改写 - 看板语义读取统一改用 activeTasks,避免已删除任务泄漏到统计、今日与标签下拉 - UI:回收站图标与备份统一尺寸;批量编辑条统一控件高度并分组 - 测试:705 个单测通过,补充迁移、回收站、批量编辑、合并与标签用例
This commit is contained in:
@@ -157,6 +157,110 @@ describe('BackupPanel 导入', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('BackupPanel 合并导入', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.mocked(downloadText).mockClear()
|
||||
})
|
||||
|
||||
it('预览显示新增 / 更新 / 保持数量', async () => {
|
||||
const { store, wrapper } = await setup()
|
||||
const local = store.tasks[0]
|
||||
const text = JSON.stringify({
|
||||
schemaVersion: 3,
|
||||
tasks: [
|
||||
{
|
||||
id: local?.id,
|
||||
title: '更新后的任务',
|
||||
status: 'todo',
|
||||
priority: 2,
|
||||
sortOrder: 1024,
|
||||
createdAt: local?.createdAt,
|
||||
updatedAt: '2099-01-01T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'new-1',
|
||||
title: '新增任务',
|
||||
status: 'todo',
|
||||
priority: 2,
|
||||
sortOrder: 2048,
|
||||
createdAt: '2026-10-01T00:00:00.000Z',
|
||||
updatedAt: '2026-10-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await chooseFile(wrapper, text)
|
||||
await wrapper.find('input[value="merge"]').setValue(true)
|
||||
|
||||
expect(wrapper.text()).toContain('新增')
|
||||
expect(wrapper.text()).toContain('更新')
|
||||
expect(wrapper.text()).toContain('保持')
|
||||
// 默认仍是覆盖模式,不会静默改成合并
|
||||
expect(buttonByText(wrapper, '确认合并导入')).toBeDefined()
|
||||
})
|
||||
|
||||
it('确认合并后更新同 id 并新增其余任务', async () => {
|
||||
const { store, wrapper } = await setup()
|
||||
const local = store.tasks[0]
|
||||
const text = JSON.stringify({
|
||||
schemaVersion: 3,
|
||||
tasks: [
|
||||
{
|
||||
id: local?.id,
|
||||
title: '更新后的任务',
|
||||
status: 'todo',
|
||||
priority: 2,
|
||||
sortOrder: 1024,
|
||||
createdAt: local?.createdAt,
|
||||
updatedAt: '2099-01-01T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'new-1',
|
||||
title: '新增任务',
|
||||
status: 'todo',
|
||||
priority: 2,
|
||||
sortOrder: 2048,
|
||||
createdAt: '2026-10-01T00:00:00.000Z',
|
||||
updatedAt: '2026-10-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await chooseFile(wrapper, text)
|
||||
await wrapper.find('input[value="merge"]').setValue(true)
|
||||
await buttonByText(wrapper, '确认合并导入')?.trigger('click')
|
||||
|
||||
expect(store.activeTasks.map((task) => task.title).sort()).toEqual(
|
||||
['更新后的任务', '新增任务'].sort(),
|
||||
)
|
||||
expect(wrapper.text()).toContain('已合并')
|
||||
expect(wrapper.text()).toContain('新增 1 个,更新 1 个')
|
||||
})
|
||||
|
||||
it('合并不删除本地独有的任务', async () => {
|
||||
const { store, wrapper } = await setup()
|
||||
await chooseFile(wrapper, makePayload(['文件任务']))
|
||||
await wrapper.find('input[value="merge"]').setValue(true)
|
||||
await buttonByText(wrapper, '确认合并导入')?.trigger('click')
|
||||
|
||||
expect(store.activeTasks.map((task) => task.title).sort()).toEqual(
|
||||
['原有任务', '文件任务'].sort(),
|
||||
)
|
||||
})
|
||||
|
||||
it('合并前自动写快照', async () => {
|
||||
const { store, wrapper } = await setup()
|
||||
expect(store.snapshots).toHaveLength(0)
|
||||
|
||||
await chooseFile(wrapper, makePayload(['文件任务']))
|
||||
await wrapper.find('input[value="merge"]').setValue(true)
|
||||
await buttonByText(wrapper, '确认合并导入')?.trigger('click')
|
||||
|
||||
expect(store.snapshots).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('BackupPanel 自动备份', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
|
||||
Reference in New Issue
Block a user