✨ feat(vscode): 支持重置回待办并修好待办依赖拖拽
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import type { Issue, IssueColumn } from '../types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isColumnId, resolveKanbanDrag } from './kanbanDrag'
|
||||
|
||||
function issue(
|
||||
number: number,
|
||||
column: IssueColumn,
|
||||
extra: Partial<Issue> = {},
|
||||
): Issue {
|
||||
return {
|
||||
id: `repo#${number}`,
|
||||
number,
|
||||
title: `issue ${number}`,
|
||||
column,
|
||||
htmlUrl: '',
|
||||
...extra,
|
||||
}
|
||||
}
|
||||
|
||||
describe('isColumnId', () => {
|
||||
it('识别四列 id', () => {
|
||||
expect(isColumnId('todo')).toBe(true)
|
||||
expect(isColumnId('in-progress')).toBe(true)
|
||||
expect(isColumnId('review')).toBe(true)
|
||||
expect(isColumnId('done')).toBe(true)
|
||||
})
|
||||
|
||||
it('拒绝卡片 id', () => {
|
||||
expect(isColumnId('repo#1')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveKanbanDrag', () => {
|
||||
it('active===over → none', () => {
|
||||
const issues = [issue(1, 'todo')]
|
||||
expect(resolveKanbanDrag({ issues, activeId: 'repo#1', overId: 'repo#1' }))
|
||||
.toEqual({ type: 'none' })
|
||||
})
|
||||
|
||||
it('丢到另一张待办卡片 = set(over 为前置)', () => {
|
||||
const issues = [issue(1, 'todo'), issue(2, 'todo')]
|
||||
expect(resolveKanbanDrag({ issues, activeId: 'repo#1', overId: 'repo#2' }))
|
||||
.toEqual({ type: 'dependency-set', issueNumber: 1, prerequisiteNumber: 2 })
|
||||
})
|
||||
|
||||
it('丢到待办列空白且已有依赖 = clear', () => {
|
||||
const issues = [issue(1, 'todo', { prerequisite: 2 }), issue(2, 'todo')]
|
||||
expect(resolveKanbanDrag({ issues, activeId: 'repo#1', overId: 'todo' }))
|
||||
.toEqual({ type: 'dependency-clear', issueNumber: 1, prerequisiteNumber: 2 })
|
||||
})
|
||||
|
||||
it('over 是列且已在 todo 且无依赖 = none', () => {
|
||||
const issues = [issue(1, 'todo')]
|
||||
expect(resolveKanbanDrag({ issues, activeId: 'repo#1', overId: 'todo' }))
|
||||
.toEqual({ type: 'none' })
|
||||
})
|
||||
|
||||
it('跨列仍是 column', () => {
|
||||
const issues = [issue(1, 'in-progress'), issue(2, 'todo')]
|
||||
expect(resolveKanbanDrag({ issues, activeId: 'repo#1', overId: 'todo' }))
|
||||
.toEqual({ type: 'column', issueNumber: 1, toColumn: 'todo' })
|
||||
expect(resolveKanbanDrag({ issues, activeId: 'repo#1', overId: 'repo#2' }))
|
||||
.toEqual({ type: 'column', issueNumber: 1, toColumn: 'todo' })
|
||||
})
|
||||
|
||||
it('跨列带上 source / externalId', () => {
|
||||
const issues = [issue(1, 'review', { source: 'youtrack', externalId: 'LXF-1' })]
|
||||
expect(resolveKanbanDrag({ issues, activeId: 'repo#1', overId: 'todo' }))
|
||||
.toEqual({
|
||||
type: 'column',
|
||||
issueNumber: 1,
|
||||
toColumn: 'todo',
|
||||
source: 'youtrack',
|
||||
externalId: 'LXF-1',
|
||||
})
|
||||
})
|
||||
|
||||
it('环拒绝:不能把祖先挂到自己的后代下', () => {
|
||||
const issues = [
|
||||
issue(1, 'todo'),
|
||||
issue(2, 'todo', { prerequisite: 1 }),
|
||||
issue(3, 'todo', { prerequisite: 2 }),
|
||||
]
|
||||
expect(resolveKanbanDrag({ issues, activeId: 'repo#1', overId: 'repo#3' }))
|
||||
.toEqual({ type: 'none' })
|
||||
})
|
||||
|
||||
it('已经是这个 parent → none', () => {
|
||||
const issues = [issue(1, 'todo', { prerequisite: 2 }), issue(2, 'todo')]
|
||||
expect(resolveKanbanDrag({ issues, activeId: 'repo#1', overId: 'repo#2' }))
|
||||
.toEqual({ type: 'none' })
|
||||
})
|
||||
|
||||
it('换 parent 也是 dependency-set,不先 clear', () => {
|
||||
const issues = [
|
||||
issue(1, 'todo', { prerequisite: 2 }),
|
||||
issue(2, 'todo'),
|
||||
issue(3, 'todo'),
|
||||
]
|
||||
expect(resolveKanbanDrag({ issues, activeId: 'repo#1', overId: 'repo#3' }))
|
||||
.toEqual({ type: 'dependency-set', issueNumber: 1, prerequisiteNumber: 3 })
|
||||
})
|
||||
|
||||
it('同列非 todo → reorder', () => {
|
||||
const issues = [issue(1, 'in-progress'), issue(2, 'in-progress')]
|
||||
expect(resolveKanbanDrag({ issues, activeId: 'repo#1', overId: 'repo#2' }))
|
||||
.toEqual({ type: 'reorder', fromIndex: 0, toIndex: 1 })
|
||||
})
|
||||
|
||||
it('锁定:todo 且前置未完成,目标列不是 todo → none', () => {
|
||||
const issues = [
|
||||
issue(1, 'todo', { prerequisite: 2 }),
|
||||
issue(2, 'in-progress'),
|
||||
]
|
||||
expect(resolveKanbanDrag({ issues, activeId: 'repo#1', overId: 'in-progress' }))
|
||||
.toEqual({ type: 'none' })
|
||||
expect(resolveKanbanDrag({ issues, activeId: 'repo#1', overId: 'repo#2' }))
|
||||
.toEqual({ type: 'none' })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { Issue, IssueColumn } from '../types'
|
||||
import { COLUMN_ORDER } from '../types'
|
||||
import { isIssueLocked } from './dependencies'
|
||||
|
||||
export function isColumnId(id: string): id is IssueColumn {
|
||||
return (COLUMN_ORDER as string[]).includes(id)
|
||||
}
|
||||
|
||||
export type KanbanDragResult
|
||||
= | { type: 'none' }
|
||||
| { type: 'column', issueNumber: number, toColumn: IssueColumn, source?: 'gitea' | 'youtrack', externalId?: string }
|
||||
| { type: 'dependency-set', issueNumber: number, prerequisiteNumber: number }
|
||||
| { type: 'dependency-clear', issueNumber: number, prerequisiteNumber: number }
|
||||
| { type: 'reorder', fromIndex: number, toIndex: number }
|
||||
|
||||
function isDescendant(maybeAncestor: number, target: number, all: Issue[]): boolean {
|
||||
const visited = new Set<number>()
|
||||
let current: Issue | undefined = all.find(i => i.number === target)
|
||||
while (current && current.prerequisite != null) {
|
||||
if (visited.has(current.number))
|
||||
return false
|
||||
visited.add(current.number)
|
||||
if (current.prerequisite === maybeAncestor)
|
||||
return true
|
||||
current = all.find(i => i.number === current!.prerequisite)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function resolveKanbanDrag(opts: {
|
||||
issues: Issue[]
|
||||
activeId: string
|
||||
overId: string
|
||||
}): KanbanDragResult {
|
||||
const { issues, activeId, overId } = opts
|
||||
if (activeId === overId)
|
||||
return { type: 'none' }
|
||||
|
||||
const activeItem = issues.find(i => i.id === activeId)
|
||||
if (!activeItem)
|
||||
return { type: 'none' }
|
||||
|
||||
// ① 锁:待办且前置未完成,只能在 todo 列内操作
|
||||
const locked = isIssueLocked(activeItem, issues).locked && activeItem.column === 'todo'
|
||||
const overIsColumn = isColumnId(overId)
|
||||
const overItem = overIsColumn ? undefined : issues.find(i => i.id === overId)
|
||||
const targetColumn: IssueColumn | undefined = overIsColumn ? overId : overItem?.column
|
||||
if (locked && targetColumn && targetColumn !== 'todo')
|
||||
return { type: 'none' }
|
||||
|
||||
// ② over 是列:已在 todo 且有依赖 → 清依赖;已在该列 → none;否则换列
|
||||
if (overIsColumn) {
|
||||
if (overId === 'todo' && activeItem.column === 'todo' && activeItem.prerequisite != null) {
|
||||
return {
|
||||
type: 'dependency-clear',
|
||||
issueNumber: activeItem.number,
|
||||
prerequisiteNumber: activeItem.prerequisite,
|
||||
}
|
||||
}
|
||||
if (activeItem.column === overId)
|
||||
return { type: 'none' }
|
||||
return {
|
||||
type: 'column',
|
||||
issueNumber: activeItem.number,
|
||||
toColumn: overId,
|
||||
...(activeItem.source ? { source: activeItem.source } : {}),
|
||||
...(activeItem.externalId ? { externalId: activeItem.externalId } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
if (!overItem)
|
||||
return { type: 'none' }
|
||||
|
||||
// ③ over 是卡片:双方 todo → 设依赖;同列非 todo → 重排;跨列 → 换列
|
||||
if (activeItem.column === 'todo' && overItem.column === 'todo') {
|
||||
if (isDescendant(activeItem.number, overItem.number, issues))
|
||||
return { type: 'none' }
|
||||
if (activeItem.prerequisite === overItem.number)
|
||||
return { type: 'none' }
|
||||
return {
|
||||
type: 'dependency-set',
|
||||
issueNumber: activeItem.number,
|
||||
prerequisiteNumber: overItem.number,
|
||||
}
|
||||
}
|
||||
|
||||
if (activeItem.column === overItem.column) {
|
||||
return {
|
||||
type: 'reorder',
|
||||
fromIndex: issues.findIndex(i => i.id === activeId),
|
||||
toIndex: issues.findIndex(i => i.id === overId),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'column',
|
||||
issueNumber: activeItem.number,
|
||||
toColumn: overItem.column,
|
||||
...(activeItem.source ? { source: activeItem.source } : {}),
|
||||
...(activeItem.externalId ? { externalId: activeItem.externalId } : {}),
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export type ExtensionToWebview
|
||||
= | { type: 'issues/loading' }
|
||||
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean }
|
||||
| { type: 'issues/error', message: string }
|
||||
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string, sessionId?: string, implementSessionId?: string, reviewSessionId?: string, reviewSessionFileExists?: boolean, testSessionId?: string, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed', column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string, brainstormProfilePath?: string, testProfilePath?: string } }
|
||||
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string, sessionId?: string, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string, brainstormProfilePath?: string, testProfilePath?: string } }
|
||||
| { type: 'issue/pr-diff-summary-done', issueNumber: number }
|
||||
| { type: 'issue/append', issue: Issue, select?: boolean }
|
||||
| { type: 'issue/select-by-number', issueNumber: number }
|
||||
|
||||
Reference in New Issue
Block a user