Fix Linear filter chips showing UUIDs after dropdown closes (#12564)

Fetch metadata when filters are selected, not only while the popover
is open, so chip labels remain readable after closing the dropdown.
This commit is contained in:
Jinjing 2026-08-04 12:37:59 -07:00 committed by GitHub
parent e4aadcceff
commit e138d28fa6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 222 additions and 5 deletions

View File

@ -1,10 +1,48 @@
import { describe, expect, it } from 'vitest'
// @vitest-environment happy-dom
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { LinearTeam } from '../../../shared/types'
import {
clearLinearIssueAttributeFacet,
countLinearIssueAttributeFilters,
linearIssueAttributeFilterPillLabels
} from './linear-issue-attribute-filter-sections'
import type { LinearIssueAttributeFilter } from '../../../shared/linear-issue-attribute-filter'
import LinearIssueAttributeFilterDropdowns from './linear-issue-attribute-filter-dropdowns'
const metadataMocks = vi.hoisted(() => ({
useTeamsStates: vi.fn((teamIds: readonly string[]) => ({
data: teamIds.length > 0 ? [{ id: 'state-1', name: 'Todo' }] : [],
loading: false,
error: null
})),
useTeamsLabels: vi.fn((teamIds: readonly string[]) => ({
data: teamIds.length > 0 ? [{ id: 'label-1', name: 'Bug' }] : [],
loading: false,
error: null
})),
useTeamsMembers: vi.fn((teamIds: readonly string[]) => ({
data: teamIds.length > 0 ? [{ id: 'member-1', displayName: 'Ada Lovelace' }] : [],
loading: false,
error: null
}))
}))
vi.mock('@/hooks/useIssueMetadata', () => metadataMocks)
const roots: Root[] = []
afterEach(() => {
roots.splice(0).forEach((root) => {
act(() => root.unmount())
})
document.body.replaceChildren()
metadataMocks.useTeamsStates.mockClear()
metadataMocks.useTeamsLabels.mockClear()
metadataMocks.useTeamsMembers.mockClear()
})
const sample: LinearIssueAttributeFilter = {
stateIds: ['s1', 's2'],
@ -38,3 +76,72 @@ describe('linear-issue-attribute-filter helpers', () => {
expect(pills[3]?.value).toBe('Bug')
})
})
describe('LinearIssueAttributeFilterDropdowns', () => {
it('keeps metadata lazy while closed with only static filter labels', () => {
const team: LinearTeam = { id: 'team-1', name: 'Engineering', key: 'ENG' }
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
roots.push(root)
act(() => {
root.render(
<LinearIssueAttributeFilterDropdowns
value={{
stateIds: [],
priorities: [1],
assignee: { kind: 'unassigned' },
labelIds: []
}}
onChange={() => undefined}
workspaceId="workspace-1"
isAllWorkspaces={false}
primaryTeam={team}
selectedTeamIds={[]}
availableTeams={[team]}
/>
)
})
expect(metadataMocks.useTeamsStates).toHaveBeenCalledWith([], undefined, null)
expect(metadataMocks.useTeamsLabels).toHaveBeenCalledWith([], undefined, null)
expect(metadataMocks.useTeamsMembers).toHaveBeenCalledWith([], undefined, null)
})
it('keeps readable metadata names available after the popover closes', () => {
const value: LinearIssueAttributeFilter = {
stateIds: ['state-1'],
priorities: [],
assignee: { kind: 'user', id: 'member-1' },
labelIds: ['label-1']
}
const team: LinearTeam = { id: 'team-1', name: 'Engineering', key: 'ENG' }
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
roots.push(root)
act(() => {
root.render(
<LinearIssueAttributeFilterDropdowns
value={value}
onChange={() => undefined}
workspaceId="workspace-1"
isAllWorkspaces={false}
primaryTeam={team}
selectedTeamIds={[]}
availableTeams={[team]}
/>
)
})
expect(container.textContent).toContain('Todo')
expect(container.textContent).toContain('Ada Lovelace')
expect(container.textContent).toContain('Bug')
expect(container.textContent).not.toContain('state-1')
expect(metadataMocks.useTeamsStates).toHaveBeenCalledWith(['team-1'], undefined, 'workspace-1')
expect(metadataMocks.useTeamsLabels).toHaveBeenCalledWith(['team-1'], undefined, 'workspace-1')
expect(metadataMocks.useTeamsMembers).toHaveBeenCalledWith(['team-1'], undefined, 'workspace-1')
})
})

View File

@ -76,9 +76,15 @@ export default function LinearIssueAttributeFilterDropdowns({
}: Props): React.JSX.Element {
const [popoverOpen, setPopoverOpen] = useState(false)
const [openSection, setOpenSection] = useState<LinearIssueFilterSectionKey | null>(null)
const activeCount = countLinearIssueAttributeFilters(value)
const metadataNeeded =
popoverOpen ||
value.stateIds.length > 0 ||
value.labelIds.length > 0 ||
value.assignee?.kind === 'user'
const activeTeamIds = useMemo(() => {
if (!popoverOpen || isAllWorkspaces) {
if (!metadataNeeded || isAllWorkspaces) {
return [] as string[]
}
return resolveLinearIssueAttributeFilterTeamIds({
@ -86,10 +92,10 @@ export default function LinearIssueAttributeFilterDropdowns({
availableTeams,
primaryTeamId: primaryTeam?.id ?? null
})
}, [popoverOpen, isAllWorkspaces, selectedTeamIds, availableTeams, primaryTeam?.id])
}, [metadataNeeded, isAllWorkspaces, selectedTeamIds, availableTeams, primaryTeam?.id])
const concreteWorkspaceId =
popoverOpen && !isAllWorkspaces && workspaceId && workspaceId !== 'all' ? workspaceId : null
metadataNeeded && !isAllWorkspaces && workspaceId && workspaceId !== 'all' ? workspaceId : null
// Why: multi-team / All teams must union filter options across every selected team (#8739).
const states = useTeamsStates(activeTeamIds, settings, concreteWorkspaceId)
@ -179,7 +185,6 @@ export default function LinearIssueAttributeFilterDropdowns({
[members.data]
)
const activeCount = countLinearIssueAttributeFilters(value)
const pills = linearIssueAttributeFilterPillLabels({
value,
stateNamesById,

View File

@ -0,0 +1,105 @@
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import { getStoreState, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
const FIXTURE = {
workspace: {
id: 'linear-workspace-3393',
displayName: 'Linear E2E User',
email: 'linear-e2e@example.test',
organizationId: 'linear-org-3393',
organizationName: 'Linear E2E Workspace'
},
team: {
id: 'linear-team-3393',
name: 'Engineering',
key: 'ENG'
},
state: {
id: 'linear-state-uuid-3393',
name: 'In Review',
type: 'started',
color: '#888888',
position: 1
},
issue: {
id: 'linear-issue-3393',
workspaceId: 'linear-workspace-3393',
identifier: 'ENG-3393',
title: 'Keep filter chip labels readable',
url: 'https://linear.example.test/ENG-3393',
state: { name: 'In Review', type: 'started', color: '#888888' },
team: { id: 'linear-team-3393', name: 'Engineering', key: 'ENG' },
labels: [],
labelIds: [],
priority: 0,
updatedAt: '2026-08-04T18:00:00.000Z'
}
} as const
async function installLinearFilterBackend(electronApp: ElectronApplication): Promise<void> {
await electronApp.evaluate(({ ipcMain }, fixture) => {
ipcMain.removeHandler('linear:status')
ipcMain.handle('linear:status', async () => ({
connected: true,
viewer: fixture.workspace,
workspaces: [fixture.workspace],
activeWorkspaceId: fixture.workspace.id,
selectedWorkspaceId: fixture.workspace.id
}))
ipcMain.removeHandler('linear:listTeams')
ipcMain.handle('linear:listTeams', async () => [fixture.team])
ipcMain.removeHandler('linear:listIssues')
ipcMain.handle('linear:listIssues', async () => ({ items: [fixture.issue], hasMore: false }))
ipcMain.removeHandler('linear:teamStates')
ipcMain.handle('linear:teamStates', async () => [fixture.state])
ipcMain.removeHandler('linear:teamLabels')
ipcMain.handle('linear:teamLabels', async () => [])
ipcMain.removeHandler('linear:teamMembers')
ipcMain.handle('linear:teamMembers', async () => [])
}, FIXTURE)
}
async function openLinearTasks(page: Page): Promise<void> {
await page.evaluate(async () => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
await store.getState().checkLinearConnection(true)
store.getState().openTaskPage({ taskSource: 'linear' })
})
}
test('Linear filter chips keep readable names after the dropdown closes', async ({
electronApp,
orcaPage
}) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await installLinearFilterBackend(electronApp)
await openLinearTasks(orcaPage)
await expect
.poll(() => getStoreState<string>(orcaPage, 'activeView'), { timeout: 5_000 })
.toBe('tasks')
const filtersButton = orcaPage.getByRole('button', { name: 'Filters', exact: true })
await expect(filtersButton).toBeVisible()
await expect(orcaPage.getByText(FIXTURE.issue.title, { exact: true })).toBeVisible()
await filtersButton.click()
const popover = orcaPage.locator('[data-slot="popover-content"]')
await popover.getByRole('button', { name: 'Status', exact: true }).click()
await popover.getByText(FIXTURE.state.name, { exact: true }).click()
await filtersButton.click()
await expect(popover).toHaveCount(0)
const statusChip = orcaPage.getByRole('button', { name: 'Remove Status filter' }).locator('..')
await expect(statusChip).toContainText(FIXTURE.state.name)
await expect(statusChip).not.toContainText(FIXTURE.state.id)
})