diff --git a/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.test.tsx b/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.test.tsx index ffdedf9ede6..38554945f12 100644 --- a/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.test.tsx +++ b/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.test.tsx @@ -16,7 +16,15 @@ vi.mock('@/components/ui/tooltip', () => ({ const roots: Root[] = [] -async function renderLine(): Promise { +type RenderOverrides = { + inboxCount?: number + pending?: boolean + error?: string | null + onReview?: () => void + onSuppress?: () => void +} + +async function renderLine(overrides: RenderOverrides = {}): Promise { const container = document.createElement('div') document.body.appendChild(container) const root = createRoot(container) @@ -26,20 +34,11 @@ async function renderLine(): Promise { root.render( ) }) @@ -47,6 +46,10 @@ async function renderLine(): Promise { return container } +function getReviewButton(container: HTMLDivElement): HTMLButtonElement | null { + return container.querySelector('button[aria-label^="Review "]') +} + describe('NewExternalWorktreesInboxLine', () => { beforeEach(() => { globalThis.IS_REACT_ACT_ENVIRONMENT = true @@ -60,27 +63,83 @@ describe('NewExternalWorktreesInboxLine', () => { vi.clearAllMocks() }) - it('keeps suppress as a hover-revealed header icon instead of expanded text action', async () => { + it('states the count without naming any worktree', async () => { const container = await renderLine() + expect(container.textContent).toContain('24') + expect(container.textContent).toContain('hidden worktrees') + // The modal owns the list; the sidebar must not enumerate paths or names. + expect(container.querySelectorAll('li')).toHaveLength(0) + expect(container.textContent).not.toContain('Import') + }) + + it('opens review from a single card-wide button', async () => { + const onReview = vi.fn() + const container = await renderLine({ onReview }) + + const review = getReviewButton(container) + expect(review).not.toBeNull() + expect(review?.getAttribute('aria-label')).toBe('Review 24 hidden worktrees in orca') + + await act(async () => { + review?.click() + }) + expect(onReview).toHaveBeenCalledTimes(1) + }) + + it('uses the singular noun for one worktree', async () => { + const container = await renderLine({ inboxCount: 1 }) + + expect(container.textContent).toContain('hidden worktree') + expect(container.textContent).not.toContain('hidden worktrees') + expect(getReviewButton(container)?.getAttribute('aria-label')).toBe( + 'Review 1 hidden worktree in orca' + ) + }) + + it('keeps suppress as a hover-revealed control that does not trigger review', async () => { + const onReview = vi.fn() + const onSuppress = vi.fn() + const container = await renderLine({ onReview, onSuppress }) + const suppressButton = container.querySelector( 'button[aria-label="Hide external worktrees permanently for orca"]' ) expect(suppressButton).not.toBeNull() expect(suppressButton?.className).toContain('can-hover:group-hover:opacity-100') expect(container.textContent).toContain("Don't show again") + // Nested buttons would make the suppress click ambiguous. + expect(getReviewButton(container)?.contains(suppressButton)).toBe(false) - const expandButton = container.querySelector( - 'button[aria-label="Expand new externally-created worktrees for orca"]' - ) await act(async () => { - expandButton?.click() + suppressButton?.click() }) + expect(onSuppress).toHaveBeenCalledTimes(1) + expect(onReview).not.toHaveBeenCalled() + }) - expect(container.textContent).toContain('payments-refactor') - const textButtons = [...container.querySelectorAll('button')].filter( - (button) => button.textContent === "Don't show again" - ) - expect(textButtons).toHaveLength(0) + it('disables both actions while a mutation is pending', async () => { + const container = await renderLine({ pending: true }) + + expect(getReviewButton(container)?.disabled).toBe(true) + expect( + container.querySelector( + 'button[aria-label="Hide external worktrees permanently for orca"]' + )?.disabled + ).toBe(true) + expect(container.querySelector('section')?.getAttribute('aria-busy')).toBe('true') + }) + + it('renders nothing when the inbox is empty', async () => { + const container = await renderLine({ inboxCount: 0 }) + + expect(container.querySelector('section')).toBeNull() + }) + + it('surfaces the action error as an alert', async () => { + const container = await renderLine({ error: 'Could not import external worktrees. Try again.' }) + + const alert = container.querySelector('[role="alert"]') + expect(alert?.textContent).toBe('Could not import external worktrees. Try again.') }) }) diff --git a/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.tsx b/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.tsx index 4b83d7cb8f5..657b3b254e0 100644 --- a/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.tsx +++ b/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.tsx @@ -1,37 +1,30 @@ -import React, { useState } from 'react' +import React from 'react' import { ChevronRight, X } from 'lucide-react' import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' -import type { NewExternalWorktreeInboxPreview } from './new-external-worktrees-inbox-candidates' type NewExternalWorktreesInboxLineProps = { repoDisplayName: string - inboxWorktrees: readonly NewExternalWorktreeInboxPreview[] + inboxCount: number pending: boolean error: string | null - onImportWorktree?: (worktreeId: string) => void - onKeepHidden?: () => void - onImportAll?: () => void + onReview?: () => void onSuppress?: () => void className?: string } export default function NewExternalWorktreesInboxLine({ repoDisplayName, - inboxWorktrees, + inboxCount, pending, error, - onImportWorktree, - onKeepHidden, - onImportAll, + onReview, onSuppress, className }: NewExternalWorktreesInboxLineProps): React.JSX.Element | null { - const [isExpanded, setIsExpanded] = useState(false) - const inboxCount = inboxWorktrees.length const suppressLabel = translate( 'auto.components.sidebar.NewExternalWorktreesInboxLine.c3e8a1f4b2', "Don't show again" @@ -41,6 +34,27 @@ export default function NewExternalWorktreesInboxLine({ 'Hide external worktrees permanently for {{value0}}', { value0: repoDisplayName } ) + const isSingular = inboxCount === 1 + const countLabel = isSingular + ? translate( + 'auto.components.sidebar.NewExternalWorktreesInboxLine.2a6f31d8c7', + 'hidden worktree' + ) + : translate( + 'auto.components.sidebar.NewExternalWorktreesInboxLine.5b90e4a2f6', + 'hidden worktrees' + ) + const reviewAriaLabel = isSingular + ? translate( + 'auto.components.sidebar.NewExternalWorktreesInboxLine.7f18c5b0d3', + 'Review {{value0}} hidden worktree in {{value1}}', + { value0: inboxCount, value1: repoDisplayName } + ) + : translate( + 'auto.components.sidebar.NewExternalWorktreesInboxLine.4e2b7a9c05', + 'Review {{value0}} hidden worktrees in {{value1}}', + { value0: inboxCount, value1: repoDisplayName } + ) if (inboxCount === 0) { return null @@ -51,151 +65,55 @@ export default function NewExternalWorktreesInboxLine({ aria-busy={pending} className={cn('mx-1 my-0.5 ml-3 text-worktree-sidebar-foreground', className)} > -
- - - {translate( - 'auto.components.sidebar.NewExternalWorktreesInboxLine.7c4e9b2a81', - 'New externally-created worktrees' + disabled={pending || !onReview} + aria-label={reviewAriaLabel} + onClick={onReview} + className={cn( + 'flex min-h-8 w-full min-w-0 items-center gap-2 rounded-md border border-worktree-sidebar-border px-2 py-1.5', + 'text-[11px] leading-none text-muted-foreground transition-colors', + 'hover:bg-worktree-sidebar-accent hover:text-worktree-sidebar-accent-foreground', + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-worktree-sidebar-ring', + 'disabled:pointer-events-none disabled:opacity-60' )} - - - + + {inboxCount} + + {countLabel} + - {onSuppress ? ( - - - - - - {suppressLabel} - - - ) : null} - -
- - {isExpanded ? ( -
-

- {translate( - 'auto.components.sidebar.NewExternalWorktreesInboxLine.4d7a1c9e53', - 'These worktrees were created outside of Orca.' - )} -

-
    - {inboxWorktrees.map((worktree) => ( -
  • + + {onSuppress ? ( + + + - ) : null} -
  • - ))} -
-
-
- {onKeepHidden ? ( - - ) : null} - {onImportAll ? ( - - ) : null} -
-
-
- ) : null} +