diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index de284fa3e..2d92a3e0b 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -45,6 +45,7 @@ import { import { WorktreeCardPortsDetails, WorktreeCardPortsTrigger } from './WorktreeCardPorts' import { writeWorkspaceDragData } from './workspace-status' import { getWorktreeCardPrDisplay } from './worktree-card-pr-display' +import { useWorktreeCardDetailsHoverControl } from './worktree-card-details-hover-state' import { getWorkspacePortsByWorktreeId } from '@/lib/workspace-port-groups' import { RepoBadgeMark } from '@/components/repo/RepoBadgeLabel' import { RepoIconGlyph } from '@/components/repo/repo-icon' @@ -648,13 +649,17 @@ const WorktreeCard = React.memo(function WorktreeCard({ }, [metaReview, openTaskPage, repo] ) - const handleUnlinkReview = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation() - void updateWorktreeMeta(worktree.id, { linkedPR: null }) - }, - [updateWorktreeMeta, worktree.id] - ) + const detailsHoverControl = useWorktreeCardDetailsHoverControl() + const hasExplicitLinkedReview = + (metaReview?.provider === 'github' && worktree.linkedPR !== null) || + (metaReview?.provider === 'gitlab' && linkedGitLabMR !== null) + const handleUnlinkReview = useCallback(() => { + if (metaReview?.provider === 'gitlab') { + void updateWorktreeMeta(worktree.id, { linkedGitLabMR: null }) + return + } + void updateWorktreeMeta(worktree.id, { linkedPR: null }) + }, [metaReview?.provider, updateWorktreeMeta, worktree.id]) const handleOpenLinearIssueInOrca = useCallback( (e: React.MouseEvent) => { e.stopPropagation() @@ -737,6 +742,7 @@ const WorktreeCard = React.memo(function WorktreeCard({ workspaceTitle={worktree.displayName} detailsAfter={hasPorts ? : null} openDelay={100} + hoverControl={detailsHoverControl} onEditIssue={handleEditIssue} onEditComment={handleEditComment} onOpenGitHubIssueInOrca={ @@ -752,11 +758,7 @@ const WorktreeCard = React.memo(function WorktreeCard({ } // Why: compact mode hides the metadata badge row, so title hover // carries the same explicit-link affordance without adding chrome. - onUnlinkReview={ - metaReview?.provider === 'github' && worktree.linkedPR !== null - ? handleUnlinkReview - : undefined - } + onUnlinkReview={hasExplicitLinkedReview ? handleUnlinkReview : undefined} > {title} @@ -771,6 +773,7 @@ const WorktreeCard = React.memo(function WorktreeCard({ review={metaReview} comment={metaComment} detailsAfter={hasPorts ? : null} + hoverControl={detailsHoverControl} onEditIssue={handleEditIssue} onEditComment={handleEditComment} onOpenGitHubIssueInOrca={ @@ -780,13 +783,9 @@ const WorktreeCard = React.memo(function WorktreeCard({ onOpenReviewInOrca={ metaReview?.url && metaReview.provider === 'github' ? handleOpenReviewInOrca : undefined } - // Why: branch lookup can show a PR without persisted metadata. Only - // expose unlink when this workspace has an explicit GitHub linkedPR. - onUnlinkReview={ - metaReview?.provider === 'github' && worktree.linkedPR !== null - ? handleUnlinkReview - : undefined - } + // Why: branch lookup can show a review without persisted metadata. Only + // expose unlink when this workspace has an explicit linked PR/MR. + onUnlinkReview={hasExplicitLinkedReview ? handleUnlinkReview : undefined} >
{hasPorts && } diff --git a/src/renderer/src/components/sidebar/WorktreeCardMeta.interaction.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardMeta.interaction.test.tsx new file mode 100644 index 000000000..07c73ec56 --- /dev/null +++ b/src/renderer/src/components/sidebar/WorktreeCardMeta.interaction.test.tsx @@ -0,0 +1,181 @@ +// @vitest-environment happy-dom + +import { act, type ReactNode } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { WorktreeCardDetailsHover } from './WorktreeCardMeta' + +const interactionMocks = vi.hoisted(() => ({ + hoverOpen: false, + onHoverOpenChange: undefined as ((open: boolean) => void) | undefined, + reviewMenuOpen: false, + onReviewMenuOpenChange: undefined as ((open: boolean) => void) | undefined, + onUnlinkSelect: undefined as (() => void) | undefined +})) + +vi.mock('@/components/ui/hover-card', () => ({ + HoverCard: ({ + children, + open, + onOpenChange + }: { + children: ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + interactionMocks.hoverOpen = open ?? false + interactionMocks.onHoverOpenChange = onOpenChange + return
{children}
+ }, + HoverCardContent: ({ children }: { children: ReactNode }) =>
{children}
, + HoverCardTrigger: ({ children }: { children: ReactNode }) => <>{children} +})) + +vi.mock('@/components/ui/tooltip', () => ({ + Tooltip: ({ children, open }: { children: ReactNode; open?: boolean }) => ( +
{children}
+ ), + TooltipContent: ({ children }: { children: ReactNode }) => <>{children}, + TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children} +})) + +vi.mock('@/components/ui/dropdown-menu', () => ({ + DropdownMenu: ({ + children, + open, + onOpenChange + }: { + children: ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + interactionMocks.reviewMenuOpen = open ?? false + interactionMocks.onReviewMenuOpenChange = onOpenChange + return
{children}
+ }, + DropdownMenuTrigger: ({ children }: { children: ReactNode; asChild?: boolean }) => ( + <>{children} + ), + DropdownMenuContent: ({ children }: { children: ReactNode }) =>
{children}
, + DropdownMenuItem: ({ children, onSelect }: { children: ReactNode; onSelect?: () => void }) => ( + + ) +})) + +const reviewFixture = { + provider: 'github' as const, + number: 456, + title: 'Fix stale GH PR', + state: 'open' as const, + url: 'https://github.com/acme/orca/pull/456', + status: 'success' as const, + updatedAt: '2026-05-17T00:00:00.000Z', + mergeable: 'MERGEABLE' as const +} + +describe('WorktreeCardDetailsHover interactions', () => { + let container: HTMLDivElement + let root: Root + + afterEach(() => { + act(() => { + root.unmount() + }) + container.remove() + interactionMocks.hoverOpen = false + interactionMocks.reviewMenuOpen = false + interactionMocks.onHoverOpenChange = undefined + interactionMocks.onReviewMenuOpenChange = undefined + interactionMocks.onUnlinkSelect = undefined + }) + + function renderHover(onUnlinkReview = vi.fn()): ReturnType { + container = document.createElement('div') + root = createRoot(container) + act(() => { + root.render( + + Linked PR + + ) + }) + return onUnlinkReview + } + + it('defers hover close while the review menu is open', () => { + renderHover() + + act(() => { + interactionMocks.onHoverOpenChange?.(true) + interactionMocks.onReviewMenuOpenChange?.(true) + interactionMocks.onHoverOpenChange?.(false) + }) + + expect(container.querySelector('[data-hover-open]')?.getAttribute('data-hover-open')).toBe( + 'true' + ) + }) + + it('closes the hover after the review menu dismisses a deferred close', () => { + renderHover() + + act(() => { + interactionMocks.onHoverOpenChange?.(true) + interactionMocks.onReviewMenuOpenChange?.(true) + interactionMocks.onHoverOpenChange?.(false) + interactionMocks.onReviewMenuOpenChange?.(false) + }) + + expect(container.querySelector('[data-hover-open]')?.getAttribute('data-hover-open')).toBe( + 'false' + ) + }) + + it('suppresses the tooltip while the review menu is open', () => { + renderHover() + + act(() => { + interactionMocks.onReviewMenuOpenChange?.(true) + }) + + expect(container.querySelector('[data-tooltip-open]')?.getAttribute('data-tooltip-open')).toBe( + 'false' + ) + }) + + it('invokes unlink and closes the hover from the menu item', () => { + const onUnlinkReview = renderHover() + + act(() => { + interactionMocks.onHoverOpenChange?.(true) + interactionMocks.onReviewMenuOpenChange?.(true) + }) + + const unlinkButton = Array.from(container.querySelectorAll('button')).find((button) => + button.textContent?.includes('Unlink PR') + ) + + act(() => { + unlinkButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + }) + + expect(onUnlinkReview).toHaveBeenCalledTimes(1) + expect(container.querySelector('[data-hover-open]')?.getAttribute('data-hover-open')).toBe( + 'false' + ) + expect( + container.querySelector('[data-review-menu-open]')?.getAttribute('data-review-menu-open') + ).toBe('false') + }) +}) diff --git a/src/renderer/src/components/sidebar/WorktreeCardMeta.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardMeta.test.tsx index 49f66e86b..02851b42c 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardMeta.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardMeta.test.tsx @@ -15,6 +15,17 @@ vi.mock('@/components/ui/tooltip', () => ({ TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children} })) +vi.mock('@/components/ui/dropdown-menu', () => ({ + DropdownMenu: ({ children }: { children: ReactNode }) =>
{children}
, + DropdownMenuTrigger: ({ children }: { children: ReactNode; asChild?: boolean }) => ( + <>{children} + ), + DropdownMenuContent: ({ children }: { children: ReactNode }) =>
{children}
, + DropdownMenuItem: ({ children }: { children: ReactNode; onSelect?: () => void }) => ( +
{children}
+ ) +})) + describe('WorktreeCardDetailsHover', () => { it('includes branch identity before metadata details', () => { const markup = renderToStaticMarkup( @@ -46,7 +57,7 @@ describe('WorktreeCardDetailsHover', () => { expect(markup).toContain('Fix stale GH PR') }) - it('shows an unlink action for linked PR details when provided', () => { + it('puts unlink behind the first PR actions menu and keeps GitHub last', () => { const markup = renderToStaticMarkup( { comment={null} onEditIssue={vi.fn()} onEditComment={vi.fn()} + onOpenReviewInOrca={vi.fn()} onUnlinkReview={vi.fn()} > Linked PR ) - expect(markup).toContain('aria-label="Unlink PR"') + const moreActionsIndex = markup.indexOf('aria-label="More PR actions"') + const openInOrcaIndex = markup.indexOf('aria-label="Open in Orca"') + const viewOnGitHubIndex = markup.indexOf('aria-label="View on GitHub"') + + expect(moreActionsIndex).toBeGreaterThan(-1) + expect(markup).toContain('More PR actions') expect(markup).toContain('Unlink PR') + expect(moreActionsIndex).toBeLessThan(openInOrcaIndex) + expect(openInOrcaIndex).toBeLessThan(viewOnGitHubIndex) + expect(markup).not.toContain('aria-label="Unlink PR"') + }) + + it('labels GitLab unlink actions with MR terminology', () => { + const markup = renderToStaticMarkup( + + Linked MR + + ) + + expect(markup).toContain('aria-label="More MR actions"') + expect(markup).toContain('Unlink MR') + expect(markup).toContain('View on GitLab') }) it('displays Linear issue details with link', () => { diff --git a/src/renderer/src/components/sidebar/WorktreeCardMeta.tsx b/src/renderer/src/components/sidebar/WorktreeCardMeta.tsx index 37b8e5fba..6ceb28f24 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardMeta.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardMeta.tsx @@ -3,18 +3,25 @@ import { Badge } from '@/components/ui/badge' import { HoverCard, HoverCardTrigger, HoverCardContent } from '@/components/ui/hover-card' import { CircleDot, + Ellipsis, ExternalLink, - GitMerge, MonitorUp, Pencil, StickyNote, Unlink } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' import { LinearIcon } from '@/components/icons/LinearIcon' import { SelectedTextCopyMenu } from '@/components/SelectedTextCopyMenu' import CommentMarkdown from './CommentMarkdown' -import { PullRequestIcon } from './WorktreeCardHelpers' import { WORKTREE_NATIVE_CONTEXT_MENU_ATTR } from './WorktreeContextMenu' import { WorktreeCardDetailSection, @@ -27,50 +34,22 @@ import { ReviewChecksBadge, ReviewStateBadge } from './WorktreeCardMetadataStatusBadges' -import type { WorktreeCardPrDisplay } from './worktree-card-pr-display' -import type { IssueInfo } from '../../../../shared/types' +import { useWorktreeCardDetailsHoverControl } from './worktree-card-details-hover-state' +import { getReviewLabel, getProviderName, ReviewIcon } from './worktree-review-helpers' +import type { + WorktreeCardIssueDisplay, + WorktreeCardLinearIssueDisplay, + WorktreeCardMetaBadgesProps, + WorktreeCardMetaBadgesRootProps, + WorktreeCardDetailsHoverProps +} from './worktree-card-meta-types' -export type WorktreeCardIssueDisplay = - | IssueInfo - | { - number: number - title: string - state?: IssueInfo['state'] - url?: string - labels?: string[] - } - -export type WorktreeCardLinearIssueDisplay = { - identifier: string - title: string - url?: string - stateName?: string - labels?: string[] -} - -type WorktreeCardMetaBadgesProps = { - issue: WorktreeCardIssueDisplay | null - linearIssue: WorktreeCardLinearIssueDisplay | null - review: WorktreeCardPrDisplay | null - comment: string | null -} - -type WorktreeCardMetaBadgesRootProps = WorktreeCardMetaBadgesProps & - React.HTMLAttributes - -type WorktreeCardDetailsHoverProps = WorktreeCardMetaBadgesProps & { - children: React.ReactElement - branchName?: string - workspaceTitle?: string - detailsAfter?: React.ReactNode - openDelay?: number - closeDelay?: number - onEditIssue: (event: React.MouseEvent) => void - onEditComment: (event: React.MouseEvent) => void - onOpenGitHubIssueInOrca?: (event: React.MouseEvent) => void - onOpenLinearIssueInOrca?: (event: React.MouseEvent) => void - onOpenReviewInOrca?: (event: React.MouseEvent) => void - onUnlinkReview?: (event: React.MouseEvent) => void +export type { + WorktreeCardIssueDisplay, + WorktreeCardLinearIssueDisplay, + WorktreeCardMetaBadgesProps, + WorktreeCardMetaBadgesRootProps, + WorktreeCardDetailsHoverProps } function hasComment(comment: string | null): boolean { @@ -86,61 +65,6 @@ export function hasWorktreeCardDetails({ return Boolean(issue || linearIssue || review || hasComment(comment)) } -function getReviewLabel(review: WorktreeCardPrDisplay): 'MR' | 'PR' { - return review.provider === 'gitlab' ? 'MR' : 'PR' -} - -function getProviderName(review: WorktreeCardPrDisplay): string { - if (review.provider === 'gitlab') { - return 'GitLab' - } - if (review.provider === 'bitbucket') { - return 'Bitbucket' - } - if (review.provider === 'azure-devops') { - return 'Azure DevOps' - } - if (review.provider === 'gitea') { - return 'Gitea' - } - return 'GitHub' -} - -function ReviewIcon({ - review, - className -}: { - review: WorktreeCardPrDisplay - className?: string -}): React.JSX.Element { - const Icon = review.provider === 'gitlab' ? GitMerge : PullRequestIcon - // Why: the standalone CI glyph was removed from the card header, so linked - // PR metadata carries check health unless the review is already merged. - const checkTone = - review.state !== 'merged' && review.status === 'failure' - ? 'text-rose-500/85' - : review.state !== 'merged' && review.status === 'pending' - ? 'text-amber-500/85' - : review.state === 'open' && review.status === 'success' - ? 'text-emerald-500/80' - : null - return ( - - ) -} - export const WorktreeCardMetaBadges = React.forwardRef< HTMLDivElement, WorktreeCardMetaBadgesRootProps @@ -201,15 +125,23 @@ export function WorktreeCardDetailsHover({ onOpenGitHubIssueInOrca, onOpenLinearIssueInOrca, onOpenReviewInOrca, - onUnlinkReview + onUnlinkReview, + hoverControl }: WorktreeCardDetailsHoverProps): React.JSX.Element { - const [open, setOpen] = React.useState(false) + const internalHoverControl = useWorktreeCardDetailsHoverControl() + const { + hoverOpen, + reviewMenuOpen, + handleHoverOpenChange, + handleReviewMenuOpenChange, + closeHover + } = hoverControl ?? internalHoverControl const dismissAndRun = React.useCallback( (handler: ((event: React.MouseEvent) => void) | undefined) => (event: React.MouseEvent) => { - setOpen(false) + closeHover() handler?.(event) }, - [] + [closeHover] ) const showIdentityHeader = Boolean(branchName || workspaceTitle) @@ -227,7 +159,12 @@ export function WorktreeCardDetailsHover({ const issueLabels = issue?.labels ?? [] return ( - + {children} + {onUnlinkReview && ( + + + + + + + + + More {reviewLabel} actions + + + + { + closeHover() + onUnlinkReview?.() + }} + > + + Unlink {reviewLabel} + + + + )} {review.url && onOpenReviewInOrca && ( )} - {onUnlinkReview && ( - - - - )} } /> diff --git a/src/renderer/src/components/sidebar/worktree-card-details-hover-state.test.tsx b/src/renderer/src/components/sidebar/worktree-card-details-hover-state.test.tsx new file mode 100644 index 000000000..d985c2d78 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-card-details-hover-state.test.tsx @@ -0,0 +1,108 @@ +// @vitest-environment happy-dom + +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it } from 'vitest' +import { useWorktreeCardDetailsHoverControl } from './worktree-card-details-hover-state' + +type HoverControlSnapshot = ReturnType + +function HoverControlProbe({ + onChange +}: { + onChange: (control: HoverControlSnapshot) => void +}): null { + const control = useWorktreeCardDetailsHoverControl() + onChange(control) + return null +} + +describe('useWorktreeCardDetailsHoverControl', () => { + let container: HTMLDivElement + let root: Root + let control: HoverControlSnapshot | null = null + + afterEach(() => { + act(() => { + root.unmount() + }) + container.remove() + control = null + }) + + function mountProbe(): void { + container = document.createElement('div') + root = createRoot(container) + act(() => { + root.render( + { + control = next + }} + /> + ) + }) + } + + it('keeps the hover open while the review menu is open', () => { + mountProbe() + expect(control).not.toBeNull() + + act(() => { + control?.handleHoverOpenChange(true) + control?.handleReviewMenuOpenChange(true) + }) + expect(control?.hoverOpen).toBe(true) + + act(() => { + control?.handleHoverOpenChange(false) + }) + expect(control?.hoverOpen).toBe(true) + }) + + it('closes the hover after the review menu dismisses a deferred close', () => { + mountProbe() + expect(control).not.toBeNull() + + act(() => { + control?.handleHoverOpenChange(true) + control?.handleReviewMenuOpenChange(true) + control?.handleHoverOpenChange(false) + }) + expect(control?.hoverOpen).toBe(true) + + act(() => { + control?.handleReviewMenuOpenChange(false) + }) + expect(control?.hoverOpen).toBe(false) + }) + + it('clears a deferred close when the pointer returns before the menu closes', () => { + mountProbe() + expect(control).not.toBeNull() + + act(() => { + control?.handleHoverOpenChange(true) + control?.handleReviewMenuOpenChange(true) + control?.handleHoverOpenChange(false) + control?.handleHoverOpenChange(true) + control?.handleReviewMenuOpenChange(false) + }) + + expect(control?.hoverOpen).toBe(true) + }) + + it('closes both layers from closeHover', () => { + mountProbe() + expect(control).not.toBeNull() + + act(() => { + control?.handleHoverOpenChange(true) + control?.handleReviewMenuOpenChange(true) + control?.closeHover() + }) + + expect(control?.hoverOpen).toBe(false) + expect(control?.reviewMenuOpen).toBe(false) + }) +}) diff --git a/src/renderer/src/components/sidebar/worktree-card-details-hover-state.ts b/src/renderer/src/components/sidebar/worktree-card-details-hover-state.ts new file mode 100644 index 000000000..b90b20d55 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-card-details-hover-state.ts @@ -0,0 +1,45 @@ +import { useCallback, useRef, useState } from 'react' + +export function useWorktreeCardDetailsHoverControl() { + const [open, setOpen] = useState(false) + const [reviewMenuOpen, setReviewMenuOpen] = useState(false) + const pendingHoverCloseRef = useRef(false) + + const closeHover = useCallback(() => { + pendingHoverCloseRef.current = false + setReviewMenuOpen(false) + setOpen(false) + }, []) + + const handleHoverOpenChange = useCallback( + (next: boolean) => { + // Why: the portaled PR menu sits outside HoverCardContent — keep the card + // mounted until the menu closes so the unlink item stays clickable. + if (reviewMenuOpen) { + pendingHoverCloseRef.current = !next + return + } + pendingHoverCloseRef.current = false + setOpen(next) + }, + [reviewMenuOpen] + ) + + const handleReviewMenuOpenChange = useCallback((next: boolean) => { + setReviewMenuOpen(next) + if (!next && pendingHoverCloseRef.current) { + pendingHoverCloseRef.current = false + setOpen(false) + } + }, []) + + return { + hoverOpen: open || reviewMenuOpen, + reviewMenuOpen, + handleHoverOpenChange, + handleReviewMenuOpenChange, + closeHover + } +} + +export type WorktreeCardDetailsHoverControl = ReturnType diff --git a/src/renderer/src/components/sidebar/worktree-card-meta-types.ts b/src/renderer/src/components/sidebar/worktree-card-meta-types.ts new file mode 100644 index 000000000..2e5975f21 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-card-meta-types.ts @@ -0,0 +1,47 @@ +import type { IssueInfo } from '../../../../shared/types' +import type { WorktreeCardPrDisplay } from './worktree-card-pr-display' +import type { WorktreeCardDetailsHoverControl } from './worktree-card-details-hover-state' + +export type WorktreeCardIssueDisplay = + | IssueInfo + | { + number: number + title: string + state?: IssueInfo['state'] + url?: string + labels?: string[] + } + +export type WorktreeCardLinearIssueDisplay = { + identifier: string + title: string + url?: string + stateName?: string + labels?: string[] +} + +export type WorktreeCardMetaBadgesProps = { + issue: WorktreeCardIssueDisplay | null + linearIssue: WorktreeCardLinearIssueDisplay | null + review: WorktreeCardPrDisplay | null + comment: string | null +} + +export type WorktreeCardMetaBadgesRootProps = WorktreeCardMetaBadgesProps & + React.HTMLAttributes + +export type WorktreeCardDetailsHoverProps = WorktreeCardMetaBadgesProps & { + children: React.ReactElement + branchName?: string + workspaceTitle?: string + detailsAfter?: React.ReactNode + openDelay?: number + closeDelay?: number + onEditIssue: (event: React.MouseEvent) => void + onEditComment: (event: React.MouseEvent) => void + onOpenGitHubIssueInOrca?: (event: React.MouseEvent) => void + onOpenLinearIssueInOrca?: (event: React.MouseEvent) => void + onOpenReviewInOrca?: (event: React.MouseEvent) => void + onUnlinkReview?: () => void + hoverControl?: WorktreeCardDetailsHoverControl +} diff --git a/src/renderer/src/components/sidebar/worktree-review-helpers.tsx b/src/renderer/src/components/sidebar/worktree-review-helpers.tsx new file mode 100644 index 000000000..7251281f4 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-review-helpers.tsx @@ -0,0 +1,57 @@ +import { GitMerge } from 'lucide-react' +import { cn } from '@/lib/utils' +import { PullRequestIcon } from './WorktreeCardHelpers' +import type { WorktreeCardPrDisplay } from './worktree-card-pr-display' + +export function getReviewLabel(review: WorktreeCardPrDisplay): 'MR' | 'PR' { + return review.provider === 'gitlab' ? 'MR' : 'PR' +} + +export function getProviderName(review: WorktreeCardPrDisplay): string { + if (review.provider === 'gitlab') { + return 'GitLab' + } + if (review.provider === 'bitbucket') { + return 'Bitbucket' + } + if (review.provider === 'azure-devops') { + return 'Azure DevOps' + } + if (review.provider === 'gitea') { + return 'Gitea' + } + return 'GitHub' +} + +export function ReviewIcon({ + review, + className +}: { + review: WorktreeCardPrDisplay + className?: string +}): React.JSX.Element { + const Icon = review.provider === 'gitlab' ? GitMerge : PullRequestIcon + const checkTone = + review.state !== 'merged' && review.status === 'failure' + ? 'text-rose-500/85' + : review.state !== 'merged' && review.status === 'pending' + ? 'text-amber-500/85' + : review.state === 'open' && review.status === 'success' + ? 'text-emerald-500/80' + : null + return ( + + ) +}