Skip to content
1 change: 1 addition & 0 deletions src/router/worker-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export function extractWorkItemId(data: CascadeJob): string | undefined {
if (jobData.type === 'trello' && jobData.workItemId) return jobData.workItemId;
if (jobData.type === 'jira' && jobData.issueKey) return jobData.issueKey;
if (jobData.type === 'github') return jobData.triggerResult?.workItemId;
if (jobData.type === 'linear') return jobData.triggerResult?.workItemId ?? jobData.workItemId;
// Dashboard jobs (manual-run, retry-run, debug-analysis)
if (jobData.workItemId) return jobData.workItemId;
return undefined;
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/router/worker-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,20 @@ describe('extractWorkItemId', () => {
expect(extractWorkItemId(job)).toBe('gh-wi-1');
});

it('returns triggerResult.workItemId for linear jobs when present', () => {
const job = {
type: 'linear',
workItemId: 'linear-issue-uuid',
triggerResult: { workItemId: 'TEAM-123' },
} as unknown as CascadeJob;
expect(extractWorkItemId(job)).toBe('TEAM-123');
});

it('falls back to top-level workItemId for linear jobs without triggerResult work item', () => {
const job = { type: 'linear', workItemId: 'linear-issue-uuid' } as unknown as CascadeJob;
expect(extractWorkItemId(job)).toBe('linear-issue-uuid');
});

it('returns workItemId from dashboard jobs', () => {
const job = { type: 'manual-run', workItemId: 'wi-dash' } as unknown as CascadeJob;
expect(extractWorkItemId(job)).toBe('wi-dash');
Expand Down
52 changes: 52 additions & 0 deletions tests/unit/web/combobox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Structural regression guard for the shared Combobox component.
*
* cmdk v1+ always sets `data-disabled="false"` on every non-disabled
* CommandItem. Tailwind's bare `data-[disabled]:` variant generates the
* CSS selector `[data-disabled]`, which matches any element that *has* the
* attribute — including `data-disabled="false"`. This caused every option
* in every cascade dashboard combobox to receive `pointer-events-none` and
* `opacity-50`, making them visually greyed-out and impossible to click.
*
* The fix is `data-[disabled=true]:` which generates `[data-disabled="true"]`
* and only activates when the item is explicitly disabled.
*
* This test reads the source directly (Combobox uses React hooks so it
* cannot be called as a plain function outside a React rendering context,
* and the unit environment has no jsdom). The source-read pattern follows
* the precedent in `pm-wizard-styling-guard.test.ts`.
*/

import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const REPO_ROOT = resolve(__dirname, '..', '..', '..');
const COMBOBOX_PATH = resolve(REPO_ROOT, 'web/src/components/ui/combobox.tsx');

describe('Combobox — disabled-item CSS regression guard', () => {
it('uses data-[disabled=true] not bare data-[disabled] for pointer-events', () => {
const source = readFileSync(COMBOBOX_PATH, 'utf8');
// Must NOT have the bare attribute selector — it matches data-disabled="false".
expect(source, 'bare data-[disabled]: matches data-disabled="false" set by cmdk').not.toMatch(
/data-\[disabled\]:pointer-events-none/,
);
// Must use the value-qualified selector — only fires when actually disabled.
expect(source, 'data-[disabled=true]: must be present').toContain(
'data-[disabled=true]:pointer-events-none',
);
});

it('uses data-[disabled=true] not bare data-[disabled] for opacity', () => {
const source = readFileSync(COMBOBOX_PATH, 'utf8');
expect(source, 'bare data-[disabled]: matches data-disabled="false" set by cmdk').not.toMatch(
/data-\[disabled\]:opacity-50/,
);
expect(source, 'data-[disabled=true]: must be present').toContain(
'data-[disabled=true]:opacity-50',
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,7 @@ export function LabelMappingStep({
*/
function renderBulkBanner(
missingSlots: ReadonlyArray<{ slot: string; name: string; color?: string }>,
onCreate:
| ((slots: ReadonlyArray<{ slot: string; name: string; color?: string }>) => void)
| undefined,
onCreate: (slots: ReadonlyArray<{ slot: string; name: string; color?: string }>) => void,
busy: boolean,
): ReactNode {
const count = missingSlots.length;
Expand Down Expand Up @@ -279,7 +277,7 @@ function renderBulkBanner(
size: 'sm',
'data-action': 'create-missing-labels',
disabled: busy,
onClick: () => onCreate?.(missingSlots),
onClick: () => onCreate(missingSlots),
} as React.ComponentProps<typeof Button> & DataProps,
busy ? 'Creating…' : 'Create all',
),
Expand All @@ -299,6 +297,8 @@ interface CreateLabelFormProps {
* directly and traverse the returned tree without a React renderer.
*/
function CreateLabelForm({ slotKey, defaultName, defaultColor, onCreate }: CreateLabelFormProps) {
// Intentionally seeded once from defaultName — the user may edit the field
// freely, and resetting it mid-edit when team details load would be jarring.
const [newLabelName, setNewLabelName] = useState(defaultName ?? '');
return createElement(
'div',
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/ui/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export function Combobox({
key={option.value}
value={option.value}
onSelect={() => handleSelect(option.value)}
className="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground"
className="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground"
>
<Check
className={cn(
Expand Down
Loading