Fix Kanban unread bell dismissal (#2163)

This commit is contained in:
Neil 2026-05-17 15:03:15 -07:00 committed by GitHub
parent c9b12d1618
commit 9e30fe31bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 124 additions and 0 deletions

View File

@ -0,0 +1,113 @@
import { renderToStaticMarkup } from 'react-dom/server'
import type { ReactNode } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Repo, Worktree, WorktreeCardProperty } from '../../../../shared/types'
const fetchHostedReviewForBranch = vi.fn()
const fetchIssue = vi.fn()
const openModal = vi.fn()
const updateWorktreeMeta = vi.fn()
let worktreeCardProperties: WorktreeCardProperty[] = ['status', 'unread']
vi.mock('@/store', () => ({
useAppStore: (selector: (state: unknown) => unknown) =>
selector({
deleteStateByWorktreeId: {},
fetchHostedReviewForBranch,
fetchIssue,
gitConflictOperationByWorktree: {},
hostedReviewCache: {},
issueCache: {},
openModal,
remoteBranchConflictByWorktreeId: {},
settings: null,
sshConnectionStates: new Map(),
sshTargetLabels: new Map(),
updateWorktreeMeta,
worktreeCardProperties
})
}))
vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealWorktree: vi.fn()
}))
vi.mock('@/components/ui/tooltip', () => ({
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipContent: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('./use-worktree-activity-status', () => ({
useWorktreeActivityStatus: () => 'idle'
}))
vi.mock('./CacheTimer', () => ({
default: () => null
}))
vi.mock('./WorktreeCardAgents', () => ({
default: () => null
}))
vi.mock('./SshDisconnectedDialog', () => ({
SshDisconnectedDialog: () => null
}))
vi.mock('./WorktreeContextMenu', () => ({
default: ({ children }: { children: ReactNode }) => <>{children}</>,
CLOSE_ALL_CONTEXT_MENUS_EVENT: 'orca:test-close-context-menus',
WORKTREE_CONTEXT_MENU_SCOPE_ATTR: 'data-orca-context-menu-scope'
}))
function makeRepo(): Repo {
return {
id: 'repo-1',
path: '/repo',
displayName: 'orca',
badgeColor: '#999999',
addedAt: 1
}
}
function makeWorktree(overrides: Partial<Worktree> = {}): Worktree {
return {
id: 'repo-1::/repo/worktrees/quick-action',
repoId: 'repo-1',
path: '/repo/worktrees/quick-action',
displayName: 'Quick action',
branch: 'quick-action',
head: 'abc123',
isBare: false,
isMainWorktree: false,
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: true,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1,
...overrides
}
}
describe('WorktreeCard quick actions', () => {
beforeEach(() => {
vi.clearAllMocks()
worktreeCardProperties = ['status', 'unread']
})
it('marks the unread toggle as a workspace-board-preserving action', async () => {
const { default: WorktreeCard } = await import('./WorktreeCard')
const markup = renderToStaticMarkup(
<WorktreeCard worktree={makeWorktree()} repo={makeRepo()} isActive={false} />
)
expect(markup).toContain('aria-label="Mark as read"')
expect(markup).toContain('data-workspace-board-preserve-open=""')
})
})

View File

@ -336,6 +336,15 @@ const WorktreeCard = React.memo(function WorktreeCard({
[isDeleting, isMultiSelected, selectedWorktrees, worktree.id]
)
const stopQuickActionPointerPropagation = useCallback(
(event: React.PointerEvent<HTMLButtonElement>) => {
// Why: the Kanban board is dismissed by document-level pointer handling.
// Quick card actions mutate metadata, but must not count as card activation.
event.stopPropagation()
},
[]
)
// Why: the 'unread' card property is the user's opt-out. When off, we render
// as if the workspace is read so bold emphasis never appears. The persisted
// `worktree.isUnread` flag is unchanged; only the rendering changes.
@ -386,6 +395,8 @@ const WorktreeCard = React.memo(function WorktreeCard({
<TooltipTrigger asChild>
<button
type="button"
data-workspace-board-preserve-open=""
onPointerDown={stopQuickActionPointerPropagation}
onClick={handleToggleUnreadQuick}
className={cn(
'group/unread flex size-4 cursor-pointer items-center justify-center rounded transition-all',