feat(ui-workspace): archive session from the row menu
The visual-only Delete session placeholder becomes a wired Archive session action: no confirmation dialog (non-destructive), failures stay console diagnostics. tree.ts hides archived sessions in every derivation (workspace groups, Ungrouped, search, flat list) through the sessionVisible predicate. The workspace-management e2e pins the archive round trip across reload.
This commit is contained in:
@@ -88,7 +88,7 @@ describe('workspace browser rows', () => {
|
||||
const onOpen = vi.fn()
|
||||
render(
|
||||
<SessionNodeItem node={node} currentId={node.id} now={0} onOpen={onOpen}
|
||||
onRename={vi.fn()} onFork={vi.fn()} t={t} />,
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />,
|
||||
)
|
||||
|
||||
const row = screen.getByRole('treeitem')
|
||||
@@ -157,18 +157,20 @@ describe('workspace browser rows', () => {
|
||||
expect(screen.queryByRole('button', { name: /工作区/ })).toBeNull()
|
||||
})
|
||||
|
||||
it('session row menu opens without opening the session and dispatches rename and fork', () => {
|
||||
it('session row menu opens without opening the session and dispatches rename, fork, and archive', () => {
|
||||
const onOpen = vi.fn()
|
||||
const onRename = vi.fn()
|
||||
const onFork = vi.fn()
|
||||
const onArchive = vi.fn()
|
||||
const node: SessionNode = {
|
||||
id: sid('s1'), title: 'One', blank: false, running: false, updatedAt: 0,
|
||||
}
|
||||
render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={onOpen}
|
||||
onRename={onRename} onFork={onFork} t={t} />)
|
||||
onRename={onRename} onFork={onFork} onArchive={onArchive} t={t} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: '会话“One”的操作' }))
|
||||
expect(onOpen).not.toHaveBeenCalled()
|
||||
expect(screen.getByRole('menuitem', { name: '删除会话' }).className).toMatch(/danger/)
|
||||
// Archive is not destructive (log and accounting slot remain): no danger styling.
|
||||
expect(screen.getByRole('menuitem', { name: '归档会话' }).className).not.toMatch(/danger/)
|
||||
// Rename dispatches with the current display title (dialog prefill).
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '重命名' }))
|
||||
expect(screen.queryByRole('menu')).toBeNull()
|
||||
@@ -177,10 +179,12 @@ describe('workspace browser rows', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '会话“One”的操作' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '分叉会话' }))
|
||||
expect(onFork).toHaveBeenCalledWith(node.id)
|
||||
// Delete stays visual-only.
|
||||
// Archive dispatches without opening the session.
|
||||
fireEvent.click(screen.getByRole('button', { name: '会话“One”的操作' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '删除会话' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '归档会话' }))
|
||||
expect(onArchive).toHaveBeenCalledWith(node.id)
|
||||
expect(onRename).toHaveBeenCalledOnce()
|
||||
expect(onOpen).not.toHaveBeenCalled()
|
||||
// Escape closes without selecting (Menu onClose path).
|
||||
fireEvent.click(screen.getByRole('button', { name: '会话“One”的操作' }))
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
@@ -194,7 +198,7 @@ describe('workspace browser rows', () => {
|
||||
id: sid('s1'), title: 'Hovered', blank: false, running: true, updatedAt: 0,
|
||||
}
|
||||
render(<SessionNodeItem node={node} currentId={undefined} now={60_000} onOpen={vi.fn()}
|
||||
onRename={vi.fn()} onFork={vi.fn()} t={t} />)
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
|
||||
const wrapper = screen.getByRole('treeitem').parentElement as HTMLElement
|
||||
fireEvent.pointerEnter(wrapper)
|
||||
act(() => { vi.advanceTimersByTime(500) })
|
||||
@@ -220,7 +224,7 @@ describe('workspace browser rows', () => {
|
||||
id: sid('s1'), title: 'Quiet', blank: false, running: false, updatedAt: 0,
|
||||
}
|
||||
render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
|
||||
onRename={vi.fn()} onFork={vi.fn()} t={t} />)
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
|
||||
fireEvent.pointerEnter(screen.getByRole('treeitem').parentElement as HTMLElement)
|
||||
act(() => { vi.advanceTimersByTime(500) })
|
||||
expect(screen.getByText('空闲')).toBeTruthy()
|
||||
@@ -237,7 +241,7 @@ describe('workspace browser rows', () => {
|
||||
const inactive = dragProps()
|
||||
const { rerender } = render(
|
||||
<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
|
||||
onRename={vi.fn()} onFork={vi.fn()} drag={inactive} t={t} />,
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} drag={inactive} t={t} />,
|
||||
)
|
||||
const row = screen.getByRole('treeitem')
|
||||
stubRect(row)
|
||||
@@ -255,7 +259,7 @@ describe('workspace browser rows', () => {
|
||||
const active = dragProps({ active: true, marker: 'before' })
|
||||
rerender(
|
||||
<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
|
||||
onRename={vi.fn()} onFork={vi.fn()} drag={active} t={t} />,
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} drag={active} t={t} />,
|
||||
)
|
||||
stubRect(screen.getByRole('treeitem'))
|
||||
// Top half hovers/drops 'before'; bottom half 'after' (row mid = 117).
|
||||
@@ -269,7 +273,7 @@ describe('workspace browser rows', () => {
|
||||
const after = dragProps({ active: true, marker: 'after' })
|
||||
rerender(
|
||||
<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
|
||||
onRename={vi.fn()} onFork={vi.fn()} drag={after} t={t} />,
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} drag={after} t={t} />,
|
||||
)
|
||||
expect(screen.getByRole('treeitem').className).toMatch(/dropAfter/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user