mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(workspace-cleanup): preserve explicit selections
This commit is contained in:
+63
@@ -7,6 +7,7 @@ import type {
|
||||
WorkspaceCleanupScanProgress,
|
||||
WorkspaceCleanupScanResult
|
||||
} from '../../../../shared/workspace-cleanup'
|
||||
import type { WorkspaceCleanupBrowseState } from '../../../../shared/workspace-cleanup-browse-state'
|
||||
import { createTestStore, seedStore } from '../../store/slices/store-test-helpers'
|
||||
import { resetWorkspaceCleanupBrowsePersistTimer } from '../../store/slices/workspace-cleanup-browse'
|
||||
import { NOW, makeCandidate } from '../../store/slices/workspace-cleanup-slice-test-harness'
|
||||
@@ -349,6 +350,35 @@ describe('WorkspaceCleanupDialog stale-while-revalidate', () => {
|
||||
expect(rowCheckbox('alpha')?.getAttribute('aria-checked')).toBe('true')
|
||||
})
|
||||
|
||||
it('reports selections removed by facet filters', async () => {
|
||||
installApi(cachedFleet())
|
||||
await renderDialog()
|
||||
await openDialog()
|
||||
|
||||
await act(async () => rowCheckbox('alpha')?.click())
|
||||
const browse = holders.store.getState().workspaceCleanupBrowse as WorkspaceCleanupBrowseState
|
||||
await act(async () => {
|
||||
holders.store.setState({
|
||||
workspaceCleanupBrowse: {
|
||||
...browse,
|
||||
filters: {
|
||||
...browse.filters,
|
||||
safety: { ...browse.filters.safety, tiers: ['review'] }
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
await flush()
|
||||
|
||||
expect(holders.infoToasts).toContain(
|
||||
'1 selected workspace is hidden by the current filters and was deselected.'
|
||||
)
|
||||
|
||||
await act(async () => holders.store.setState({ workspaceCleanupBrowse: browse }))
|
||||
await flush()
|
||||
expect(rowCheckbox('alpha')?.getAttribute('aria-checked')).toBe('false')
|
||||
})
|
||||
|
||||
it('adopts the in-flight scan on reopen instead of starting a duplicate', async () => {
|
||||
const rig = installApi(cachedFleet())
|
||||
await renderDialog()
|
||||
@@ -433,4 +463,37 @@ describe('WorkspaceCleanupDialog stale-while-revalidate', () => {
|
||||
expect(rowNames()).not.toContain('beta')
|
||||
expect(holders.infoToasts).toContain('1 selected workspace no longer exists.')
|
||||
})
|
||||
|
||||
it('keeps selected review rows outside the select-all scope', async () => {
|
||||
installApi({
|
||||
scannedAt: CACHED_AT,
|
||||
candidates: [
|
||||
makeCandidate({ worktreeId: 'repo1::/tmp/ready', displayName: 'ready' }),
|
||||
makeCandidate({
|
||||
worktreeId: 'repo1::/tmp/review',
|
||||
displayName: 'review',
|
||||
tier: 'review',
|
||||
selectedByDefault: false,
|
||||
git: { clean: null, upstreamAhead: 0, upstreamBehind: 0, checkedAt: null }
|
||||
})
|
||||
],
|
||||
errors: []
|
||||
})
|
||||
await renderDialog()
|
||||
await openDialog()
|
||||
|
||||
await act(async () => rowCheckbox('review')?.click())
|
||||
const selectAll = container?.querySelector<HTMLElement>(
|
||||
'[aria-label="Select 1 safety-checked workspace"]'
|
||||
)
|
||||
expect(selectAll?.getAttribute('aria-checked')).toBe('false')
|
||||
|
||||
await act(async () => selectAll?.click())
|
||||
expect(rowCheckbox('ready')?.getAttribute('aria-checked')).toBe('true')
|
||||
expect(rowCheckbox('review')?.getAttribute('aria-checked')).toBe('true')
|
||||
|
||||
await act(async () => selectAll?.click())
|
||||
expect(rowCheckbox('ready')?.getAttribute('aria-checked')).toBe('false')
|
||||
expect(rowCheckbox('review')?.getAttribute('aria-checked')).toBe('true')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -286,10 +286,9 @@ function WorkspaceCleanupDialogContent({
|
||||
)
|
||||
// Header state is scoped to the same rows the header action controls.
|
||||
const selectedSelectableCount = useMemo(() => {
|
||||
const selectable = new Set(selectableIdentities)
|
||||
let count = 0
|
||||
for (const id of selectedIds) {
|
||||
if (selectable.has(id)) {
|
||||
for (const identity of selectableIdentities) {
|
||||
if (selectedIds.has(identity)) {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,10 +64,7 @@ export function formatWorkspaceCleanupScanProgress(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Why no suggestion count: "suggestion" is Orca's verdict about the user's own
|
||||
* work. The dialog stopped acting on it; this toast must stop announcing it.
|
||||
*/
|
||||
/** Reports scan size without classifying the user's work. */
|
||||
export function formatWorkspaceCleanupReadyToast(workspaceCount: number): string {
|
||||
if (workspaceCount === 0) {
|
||||
return translate('components.workspace.cleanup.scan.noWorkspaces', 'No workspaces found.')
|
||||
|
||||
+7
-6
@@ -95,16 +95,17 @@ describe('WorkspaceCleanupSortHeader', () => {
|
||||
it('states how many workspaces select-all would take', () => {
|
||||
render({ field: 'name', direction: 'asc' }, { selectableCount: 12 })
|
||||
|
||||
// Why: select-all takes the deletable subset, not every matched row; an
|
||||
// unqualified label would overstate a destructive action's scope.
|
||||
expect(container?.textContent).toContain('Select all 12 deletable workspaces')
|
||||
expect(container?.textContent).toContain('Select all 12 safety-checked workspaces')
|
||||
})
|
||||
|
||||
it('uses singular copy for one safety-checked workspace', () => {
|
||||
render({ field: 'name', direction: 'asc' }, { selectableCount: 1 })
|
||||
|
||||
expect(container?.textContent).toContain('Select 1 safety-checked workspace')
|
||||
})
|
||||
|
||||
it('does not report all-selected when the selected rows are not the selectable ones', () => {
|
||||
const onToggleSelectAll = vi.fn()
|
||||
// Why: `selectedCount` used to be canQueue-scoped while `selectableCount`
|
||||
// is canSelect-scoped, so hand-picking review rows flipped the header to
|
||||
// fully-checked and the next click cleared the whole selection.
|
||||
render(
|
||||
{ field: 'name', direction: 'asc' },
|
||||
{ selectableCount: 3, selectedCount: 0, onToggleSelectAll }
|
||||
|
||||
@@ -31,6 +31,17 @@ export function WorkspaceCleanupSortHeader({
|
||||
}): React.JSX.Element {
|
||||
const allSelected = selectableCount > 0 && selectedCount >= selectableCount
|
||||
const someSelected = selectableCount > 0 && selectedCount > 0 && !allSelected
|
||||
const selectAllLabel =
|
||||
selectableCount === 1
|
||||
? translate(
|
||||
'components.workspace.cleanup.browse.selectAllCountOne',
|
||||
'Select 1 safety-checked workspace'
|
||||
)
|
||||
: translate(
|
||||
'components.workspace.cleanup.browse.selectAllCount',
|
||||
'Select all {{value0}} safety-checked workspaces',
|
||||
{ value0: selectableCount }
|
||||
)
|
||||
return (
|
||||
<div className="flex items-center gap-1 border-b border-border bg-muted/25 px-3 py-1.5">
|
||||
<button
|
||||
@@ -38,10 +49,7 @@ export function WorkspaceCleanupSortHeader({
|
||||
role="checkbox"
|
||||
aria-checked={someSelected ? 'mixed' : allSelected}
|
||||
disabled={selectableCount === 0}
|
||||
aria-label={translate(
|
||||
'components.workspace.cleanup.browse.selectAll',
|
||||
'Select all matching workspaces'
|
||||
)}
|
||||
aria-label={selectAllLabel}
|
||||
onClick={() => onToggleSelectAll(!allSelected)}
|
||||
className="flex size-4 shrink-0 items-center justify-center rounded border border-border bg-background text-primary hover:bg-accent disabled:opacity-40 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
@@ -51,16 +59,7 @@ export function WorkspaceCleanupSortHeader({
|
||||
<Minus className="size-3" strokeWidth={3} />
|
||||
) : null}
|
||||
</button>
|
||||
<span className="min-w-0 flex-1 text-xs text-muted-foreground">
|
||||
{/* Why the count: select-all takes the deletable subset of the matched
|
||||
rows, not every matched row — an unqualified label would overstate
|
||||
the scope of a destructive action. */}
|
||||
{translate(
|
||||
'components.workspace.cleanup.browse.selectAllCount',
|
||||
'Select all {{value0}} deletable workspaces',
|
||||
{ value0: selectableCount }
|
||||
)}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 text-xs text-muted-foreground">{selectAllLabel}</span>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -16229,7 +16229,8 @@
|
||||
"selectedCount": "{{value0}} selected",
|
||||
"selectionWithheldOne": "1 selected workspace is hidden by the current filters and was deselected.",
|
||||
"selectionWithheld": "{{value0}} selected workspaces are hidden by the current filters and were deselected.",
|
||||
"selectAllCount": "Select all {{value0}} deletable workspaces"
|
||||
"selectAllCountOne": "Select 1 safety-checked workspace",
|
||||
"selectAllCount": "Select all {{value0}} safety-checked workspaces"
|
||||
},
|
||||
"scan": {
|
||||
"progress": "Scanning workspaces ({{value0}}/{{value1}})",
|
||||
|
||||
@@ -14546,6 +14546,16 @@
|
||||
},
|
||||
"workspace": {
|
||||
"cleanup": {
|
||||
"browse": {
|
||||
"selectionWithheldOne": "선택한 워크스페이스 1개가 현재 필터에 의해 숨겨져 선택이 해제되었습니다.",
|
||||
"selectionWithheld": "선택한 워크스페이스 {{value0}}개가 현재 필터에 의해 숨겨져 선택이 해제되었습니다.",
|
||||
"selectAllCountOne": "안전 검사를 통과한 워크스페이스 1개 선택",
|
||||
"selectAllCount": "안전 검사를 통과한 워크스페이스 {{value0}}개 모두 선택"
|
||||
},
|
||||
"scan": {
|
||||
"readyOne": "워크스페이스 1개를 찾았습니다.",
|
||||
"readyMany": "워크스페이스 {{value0}}개를 찾았습니다."
|
||||
},
|
||||
"presentationFixtures": {
|
||||
"reviewAlphaCleanup": "알파 정리 검토"
|
||||
},
|
||||
|
||||
@@ -14537,6 +14537,16 @@
|
||||
},
|
||||
"workspace": {
|
||||
"cleanup": {
|
||||
"browse": {
|
||||
"selectionWithheldOne": "当前筛选条件隐藏了 1 个已选工作区,已取消选择。",
|
||||
"selectionWithheld": "当前筛选条件隐藏了 {{value0}} 个已选工作区,已取消选择。",
|
||||
"selectAllCountOne": "选择 1 个通过安全检查的工作区",
|
||||
"selectAllCount": "选择全部 {{value0}} 个通过安全检查的工作区"
|
||||
},
|
||||
"scan": {
|
||||
"readyOne": "找到 1 个工作区。",
|
||||
"readyMany": "找到 {{value0}} 个工作区。"
|
||||
},
|
||||
"presentationFixtures": {
|
||||
"reviewAlphaCleanup": "评审 Alpha 清理"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user