mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 00:02:35 +00:00
Resolve workspace cleanup active view during render
This commit is contained in:
@@ -38,6 +38,11 @@ import {
|
||||
type WorkspaceCleanupScanError,
|
||||
type WorkspaceCleanupTier
|
||||
} from '../../../../shared/workspace-cleanup'
|
||||
import {
|
||||
resolveWorkspaceCleanupActiveView,
|
||||
type WorkspaceCleanupView,
|
||||
type WorkspaceCleanupViewCounts
|
||||
} from './workspace-cleanup-view-selection'
|
||||
|
||||
const TIER_LABELS: Record<WorkspaceCleanupTier, string> = {
|
||||
ready: 'Suggested cleanup',
|
||||
@@ -45,8 +50,6 @@ const TIER_LABELS: Record<WorkspaceCleanupTier, string> = {
|
||||
protected: 'Not suggested for cleanup'
|
||||
}
|
||||
|
||||
type CleanupView = WorkspaceCleanupTier | 'hidden'
|
||||
|
||||
const BLOCKER_LABELS: Record<WorkspaceCleanupBlocker, string> = {
|
||||
'main-worktree': 'Main workspace',
|
||||
'folder-repo': 'Folder project',
|
||||
@@ -185,7 +188,7 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
|
||||
|
||||
const open = activeModal === 'workspace-cleanup'
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set())
|
||||
const [activeView, setActiveView] = useState<CleanupView>('ready')
|
||||
const [activeView, setActiveView] = useState<WorkspaceCleanupView>('ready')
|
||||
const [confirming, setConfirming] = useState(false)
|
||||
const [removing, setRemoving] = useState(false)
|
||||
const [rowFailures, setRowFailures] = useState<Record<string, string>>({})
|
||||
@@ -278,6 +281,22 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
|
||||
const hiddenByKeepCount = filteredCandidates.filter((candidate) =>
|
||||
candidate.blockers.includes('dismissed')
|
||||
).length
|
||||
const cleanupViewCounts = useMemo<WorkspaceCleanupViewCounts>(
|
||||
() => ({
|
||||
ready: groups.ready.length,
|
||||
review: groups.review.length,
|
||||
protected: groups.protected.length,
|
||||
hidden: hiddenCandidates.length
|
||||
}),
|
||||
[groups.protected.length, groups.ready.length, groups.review.length, hiddenCandidates.length]
|
||||
)
|
||||
const resolvedActiveView = resolveWorkspaceCleanupActiveView({
|
||||
requestedView: activeView,
|
||||
counts: cleanupViewCounts,
|
||||
open,
|
||||
loading,
|
||||
hasScan: scan != null
|
||||
})
|
||||
const repoNameById = useMemo(
|
||||
() => new Map(repos.map((repo) => [repo.id, repo.displayName || repo.path])),
|
||||
[repos]
|
||||
@@ -295,7 +314,7 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
|
||||
const inactiveCount = filteredCandidates.length
|
||||
const hasAnyCandidates = candidates.length > 0
|
||||
const initialLoading = loading && !scan
|
||||
const activeRows = activeView === 'hidden' ? hiddenCandidates : groups[activeView]
|
||||
const activeRows = resolvedActiveView === 'hidden' ? hiddenCandidates : groups[resolvedActiveView]
|
||||
const activeQueueableRows = useMemo(
|
||||
() => activeRows.filter(canQueueWorkspaceCleanupCandidate),
|
||||
[activeRows]
|
||||
@@ -313,33 +332,6 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
|
||||
? 'mixed'
|
||||
: 'unchecked'
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || loading || !scan) {
|
||||
return
|
||||
}
|
||||
if (activeRows.length > 0) {
|
||||
return
|
||||
}
|
||||
if (readyCount > 0) {
|
||||
setActiveView('ready')
|
||||
} else if (groups.review.length > 0) {
|
||||
setActiveView('review')
|
||||
} else if (groups.protected.length > 0) {
|
||||
setActiveView('protected')
|
||||
} else if (hiddenCandidates.length > 0) {
|
||||
setActiveView('hidden')
|
||||
}
|
||||
}, [
|
||||
activeRows.length,
|
||||
groups.protected.length,
|
||||
groups.review.length,
|
||||
hiddenCandidates.length,
|
||||
loading,
|
||||
open,
|
||||
readyCount,
|
||||
scan
|
||||
])
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
if (!nextOpen && !removing) {
|
||||
@@ -567,19 +559,14 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
|
||||
|
||||
<div className="grid min-h-0 flex-1 grid-cols-1 overflow-hidden md:grid-cols-[185px_minmax(0,1fr)]">
|
||||
<CleanupViewNav
|
||||
activeView={activeView}
|
||||
counts={{
|
||||
ready: groups.ready.length,
|
||||
review: groups.review.length,
|
||||
protected: groups.protected.length,
|
||||
hidden: hiddenByKeepCount
|
||||
}}
|
||||
activeView={resolvedActiveView}
|
||||
counts={cleanupViewCounts}
|
||||
onViewChange={setActiveView}
|
||||
/>
|
||||
<div className="flex min-h-0 min-w-0 flex-col border-t border-border md:border-l md:border-t-0">
|
||||
<div className="flex min-h-10 items-center justify-between gap-3 border-b border-border px-3 py-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{activeView !== 'hidden' && activeQueueableRows.length > 0 ? (
|
||||
{resolvedActiveView !== 'hidden' && activeQueueableRows.length > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
role="checkbox"
|
||||
@@ -588,8 +575,8 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
|
||||
}
|
||||
aria-label={
|
||||
allActiveQueueableSelected
|
||||
? `Unselect all in ${TIER_LABELS[activeView]}`
|
||||
: `Select all in ${TIER_LABELS[activeView]}`
|
||||
? `Unselect all in ${TIER_LABELS[resolvedActiveView]}`
|
||||
: `Select all in ${TIER_LABELS[resolvedActiveView]}`
|
||||
}
|
||||
onClick={toggleActiveSelection}
|
||||
className="flex size-4 shrink-0 items-center justify-center rounded border border-border bg-background text-primary hover:bg-accent"
|
||||
@@ -602,12 +589,12 @@ export default function WorkspaceCleanupDialog(): React.JSX.Element {
|
||||
</button>
|
||||
) : null}
|
||||
<div className="min-w-0 truncate text-[11px] font-semibold uppercase tracking-[0.05em] text-muted-foreground">
|
||||
{activeView === 'hidden'
|
||||
{resolvedActiveView === 'hidden'
|
||||
? 'Ignored cleanup suggestions'
|
||||
: TIER_LABELS[activeView]}
|
||||
: TIER_LABELS[resolvedActiveView]}
|
||||
</div>
|
||||
</div>
|
||||
{activeView === 'hidden' && hiddenByKeepCount > 0 ? (
|
||||
{resolvedActiveView === 'hidden' && hiddenByKeepCount > 0 ? (
|
||||
<Button
|
||||
variant="link"
|
||||
size="xs"
|
||||
@@ -723,11 +710,11 @@ function CleanupViewNav({
|
||||
counts,
|
||||
onViewChange
|
||||
}: {
|
||||
activeView: CleanupView
|
||||
counts: Record<CleanupView, number>
|
||||
onViewChange: (view: CleanupView) => void
|
||||
activeView: WorkspaceCleanupView
|
||||
counts: WorkspaceCleanupViewCounts
|
||||
onViewChange: (view: WorkspaceCleanupView) => void
|
||||
}): React.JSX.Element {
|
||||
const items: { view: CleanupView; label: string }[] = [
|
||||
const items: { view: WorkspaceCleanupView; label: string }[] = [
|
||||
{ view: 'ready', label: 'Suggested' },
|
||||
{ view: 'review', label: 'Needs review' },
|
||||
{ view: 'protected', label: 'Not suggested' },
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
resolveWorkspaceCleanupActiveView,
|
||||
type WorkspaceCleanupViewCounts
|
||||
} from './workspace-cleanup-view-selection'
|
||||
|
||||
const emptyCounts: WorkspaceCleanupViewCounts = {
|
||||
ready: 0,
|
||||
review: 0,
|
||||
protected: 0,
|
||||
hidden: 0
|
||||
}
|
||||
|
||||
describe('resolveWorkspaceCleanupActiveView', () => {
|
||||
it('keeps the requested view while it still has rows', () => {
|
||||
expect(
|
||||
resolveWorkspaceCleanupActiveView({
|
||||
requestedView: 'review',
|
||||
counts: { ...emptyCounts, ready: 2, review: 1 },
|
||||
open: true,
|
||||
loading: false,
|
||||
hasScan: true
|
||||
})
|
||||
).toBe('review')
|
||||
})
|
||||
|
||||
it('falls back to the first populated cleanup view when the requested view is empty', () => {
|
||||
expect(
|
||||
resolveWorkspaceCleanupActiveView({
|
||||
requestedView: 'protected',
|
||||
counts: { ...emptyCounts, review: 3, hidden: 1 },
|
||||
open: true,
|
||||
loading: false,
|
||||
hasScan: true
|
||||
})
|
||||
).toBe('review')
|
||||
})
|
||||
|
||||
it('uses hidden suggestions when no visible cleanup views have rows', () => {
|
||||
expect(
|
||||
resolveWorkspaceCleanupActiveView({
|
||||
requestedView: 'ready',
|
||||
counts: { ...emptyCounts, hidden: 2 },
|
||||
open: true,
|
||||
loading: false,
|
||||
hasScan: true
|
||||
})
|
||||
).toBe('hidden')
|
||||
})
|
||||
|
||||
it('leaves the requested view alone before an open completed scan', () => {
|
||||
expect(
|
||||
resolveWorkspaceCleanupActiveView({
|
||||
requestedView: 'protected',
|
||||
counts: { ...emptyCounts, ready: 4 },
|
||||
open: false,
|
||||
loading: false,
|
||||
hasScan: true
|
||||
})
|
||||
).toBe('protected')
|
||||
|
||||
expect(
|
||||
resolveWorkspaceCleanupActiveView({
|
||||
requestedView: 'protected',
|
||||
counts: { ...emptyCounts, ready: 4 },
|
||||
open: true,
|
||||
loading: true,
|
||||
hasScan: true
|
||||
})
|
||||
).toBe('protected')
|
||||
|
||||
expect(
|
||||
resolveWorkspaceCleanupActiveView({
|
||||
requestedView: 'protected',
|
||||
counts: { ...emptyCounts, ready: 4 },
|
||||
open: true,
|
||||
loading: false,
|
||||
hasScan: false
|
||||
})
|
||||
).toBe('protected')
|
||||
})
|
||||
|
||||
it('keeps an empty requested view when every view is empty', () => {
|
||||
expect(
|
||||
resolveWorkspaceCleanupActiveView({
|
||||
requestedView: 'hidden',
|
||||
counts: emptyCounts,
|
||||
open: true,
|
||||
loading: false,
|
||||
hasScan: true
|
||||
})
|
||||
).toBe('hidden')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
export type WorkspaceCleanupView = 'ready' | 'review' | 'protected' | 'hidden'
|
||||
|
||||
export type WorkspaceCleanupViewCounts = Record<WorkspaceCleanupView, number>
|
||||
|
||||
export function resolveWorkspaceCleanupActiveView({
|
||||
requestedView,
|
||||
counts,
|
||||
open,
|
||||
loading,
|
||||
hasScan
|
||||
}: {
|
||||
requestedView: WorkspaceCleanupView
|
||||
counts: WorkspaceCleanupViewCounts
|
||||
open: boolean
|
||||
loading: boolean
|
||||
hasScan: boolean
|
||||
}): WorkspaceCleanupView {
|
||||
if (!open || loading || !hasScan || counts[requestedView] > 0) {
|
||||
return requestedView
|
||||
}
|
||||
if (counts.ready > 0) {
|
||||
return 'ready'
|
||||
}
|
||||
if (counts.review > 0) {
|
||||
return 'review'
|
||||
}
|
||||
if (counts.protected > 0) {
|
||||
return 'protected'
|
||||
}
|
||||
if (counts.hidden > 0) {
|
||||
return 'hidden'
|
||||
}
|
||||
return requestedView
|
||||
}
|
||||
Reference in New Issue
Block a user