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:
parent
e4aadcceff
commit
e138d28fa6
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
Loading…
Reference in New Issue