From e138d28fa65916e7086449357e577a70e808bef7 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:37:59 -0700 Subject: [PATCH] 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. --- ...-issue-attribute-filter-dropdowns.test.tsx | 109 +++++++++++++++++- ...inear-issue-attribute-filter-dropdowns.tsx | 13 ++- tests/e2e/linear-filter-chip-labels.spec.ts | 105 +++++++++++++++++ 3 files changed, 222 insertions(+), 5 deletions(-) create mode 100644 tests/e2e/linear-filter-chip-labels.spec.ts diff --git a/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx b/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx index 753e24872..845ed09b0 100644 --- a/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx +++ b/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx @@ -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( + 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( + 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') + }) +}) diff --git a/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx b/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx index fa47d6def..9cf550e2f 100644 --- a/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx +++ b/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx @@ -76,9 +76,15 @@ export default function LinearIssueAttributeFilterDropdowns({ }: Props): React.JSX.Element { const [popoverOpen, setPopoverOpen] = useState(false) const [openSection, setOpenSection] = useState(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, diff --git a/tests/e2e/linear-filter-chip-labels.spec.ts b/tests/e2e/linear-filter-chip-labels.spec.ts new file mode 100644 index 000000000..1ccb3d069 --- /dev/null +++ b/tests/e2e/linear-filter-chip-labels.spec.ts @@ -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 { + 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 { + 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(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) +})