feat(tsk_PbcHccuPmwxV): 任务取消/删除 UI 入口(带确认)
## 后端
- Store.deleteTask():BFS 收集子孙 id,清理 events,删根节点(子孙/runs/approvals 由 FK CASCADE 自动清除),广播 task.updated{kind:'delete'}
- API POST /api/tasks/:id/cancel:transition → cancelled(复用状态机守卫)
- API DELETE /api/tasks/:id:级联删除任务及子孙
## 前端
- 任务行(task-row):鼠标悬停后右侧出现「✕取消」「🗑删除」浮动按钮,点击弹 confirm() 二次确认(含子孙数提示),stopPropagation 防止触发行展开
- 任务详情(detail):底部行内确认区——「取消任务」显示确认条(确认取消 / 算了),「删除」显示含级联提示的高亮确认条,折叠/切换项目时自动清除确认状态
- 归档详情对话框:底部增加「🗑 删除归档」按钮,confirm() 确认后删除并关闭弹窗
- CSS:.task-row-cancel/.task-row-delete(悬停淡入)、.detail-actions / .detail-action-confirm / .detail-confirm-msg、.archive-modal-actions
## 测试
- 新增 4 个 store 测试:deleteTask 叶子、deleteTask 级联、deleteTask 不存在、transition→cancelled
- 全部 78 条 pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -189,6 +189,56 @@ test('createTask:deps 引用不存在/跨项目任务被拒绝', () => {
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('deleteTask:删除叶子任务,不影响同项目其他任务', () => {
|
||||
const s = freshStore();
|
||||
const p = s.createProject({ name: 'del1', repoPath: '/tmp/del1-' + Math.random() });
|
||||
const a = s.createTask({ projectId: p.id, title: 'A', complexity: 'easy' });
|
||||
const b = s.createTask({ projectId: p.id, title: 'B', complexity: 'easy' });
|
||||
const { deleted } = s.deleteTask(a.id);
|
||||
assert.equal(deleted, 1);
|
||||
assert.equal(s.getTask(a.id), null); // 已删
|
||||
assert.ok(s.getTask(b.id)); // 不受影响
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('deleteTask:级联删除带子任务的父任务', () => {
|
||||
const s = freshStore();
|
||||
const p = s.createProject({ name: 'del2', repoPath: '/tmp/del2-' + Math.random() });
|
||||
const root = s.createTask({ projectId: p.id, title: 'root', complexity: 'hard' });
|
||||
s.setPlan(root.id, '拆'); s.transition(root.id, 'plan_review'); s.decide(root.id, 'accept', 'user');
|
||||
const c1 = s.createTask({ projectId: p.id, parentId: root.id, title: 'c1', complexity: 'easy' });
|
||||
const c2 = s.createTask({ projectId: p.id, parentId: root.id, title: 'c2', complexity: 'easy' });
|
||||
// c1 下再建孙子
|
||||
const gc = s.createTask({ projectId: p.id, parentId: c1.id, title: 'gc', complexity: 'easy' });
|
||||
const { deleted } = s.deleteTask(root.id);
|
||||
assert.equal(deleted, 4); // root + c1 + c2 + gc
|
||||
assert.equal(s.getTask(root.id), null);
|
||||
assert.equal(s.getTask(c1.id), null);
|
||||
assert.equal(s.getTask(c2.id), null);
|
||||
assert.equal(s.getTask(gc.id), null);
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('deleteTask:不存在的任务抛 StoreError', () => {
|
||||
const s = freshStore();
|
||||
assert.throws(() => s.deleteTask('tsk_nonexistent'), (e: Error) => e.message.includes('任务不存在'));
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('transition → cancelled:合法的状态流转(init/ready/executing 等)', () => {
|
||||
const s = freshStore();
|
||||
const p = s.createProject({ name: 'can', repoPath: '/tmp/can-' + Math.random() });
|
||||
|
||||
const t1 = s.createTask({ projectId: p.id, title: 'init task', complexity: 'easy' });
|
||||
assert.equal(t1.status, 'ready');
|
||||
const cancelled = s.transition(t1.id, 'cancelled', { by: 'user' });
|
||||
assert.equal(cancelled.status, 'cancelled');
|
||||
|
||||
// 已取消的任务不可再次流转
|
||||
assert.throws(() => s.transition(t1.id, 'ready'), (e: Error) => e.message.includes('非法状态流转'));
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('容器收口:已拆解 Hard 的子任务全 done → 容器自动 done(逐级向上)', () => {
|
||||
const s = freshStore();
|
||||
const p = s.createProject({ name: 'cc', repoPath: '/tmp/cc-' + Math.random() });
|
||||
|
||||
Reference in New Issue
Block a user