diff --git a/frontend/src/routes/search/InterpretationChipRow.svelte b/frontend/src/routes/search/InterpretationChipRow.svelte index 62a26c81..2356a774 100644 --- a/frontend/src/routes/search/InterpretationChipRow.svelte +++ b/frontend/src/routes/search/InterpretationChipRow.svelte @@ -6,6 +6,7 @@ import type { components } from '$lib/generated/api'; import type { ChipType } from './chip-types.js'; type NlQueryInterpretation = components['schemas']['NlQueryInterpretation']; +type TagHint = components['schemas']['TagHint']; let { interpretation, @@ -19,7 +20,8 @@ type Chip = | { key: string; type: 'sender'; label: string } | { key: string; type: 'directional'; from: string; to: string } | { key: string; type: 'date'; label: string } - | { key: string; type: 'keyword'; value: string; label: string }; + | { key: string; type: 'keyword'; value: string; label: string } + | { key: string; type: 'theme'; tag: TagHint; label: string }; // Locally removed chips. The parent remounts this component (via {#key}) on every // new NL search, so this set never needs an explicit reset. @@ -36,9 +38,22 @@ function dateRangeLabel(from: string | undefined, to: string | undefined): strin return fromYear ?? toYear ?? ''; } +function tagColorStyle(color: string | undefined): string | undefined { + if (!color) return undefined; + return `background-color: var(--c-tag-${color}); border-left-color: var(--c-tag-${color})`; +} + const chips = $derived.by(() => { const list: Chip[] = []; - const { resolvedPersons, dateFrom, dateTo, keywords, keywordsApplied } = interpretation; + const { + resolvedPersons, + dateFrom, + dateTo, + keywords, + keywordsApplied, + resolvedTags, + tagsApplied + } = interpretation; if (resolvedPersons.length >= 2) { list.push({ @@ -74,6 +89,17 @@ const chips = $derived.by(() => { } } + if (tagsApplied) { + for (const tag of resolvedTags) { + list.push({ + key: 'theme:' + tag.id, + type: 'theme', + tag, + label: `${m.search_chip_theme_prefix()}: ${tag.name}` + }); + } + } + return list.filter((chip) => !removed.has(chip.key)); }); @@ -83,7 +109,13 @@ const showKeywordsNotApplied = $derived( function remove(chip: Chip) { removed.add(chip.key); - onRemoveChip(chip.type, chip.type === 'keyword' ? chip.value : undefined); + if (chip.type === 'keyword') { + onRemoveChip(chip.type, chip.value); + } else if (chip.type === 'theme') { + onRemoveChip(chip.type, chip.tag.name); + } else { + onRemoveChip(chip.type, undefined); + } } const nameSpan = 'sm:max-w-[12rem] max-w-[8rem] truncate'; @@ -113,6 +145,21 @@ const removeButton = + {:else if chip.type === 'theme'} + + {m.search_chip_theme_prefix()}: + {chip.tag.name} + + {:else} {chip.label} diff --git a/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts b/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts index 59b82e0b..9cefc628 100644 --- a/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts +++ b/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts @@ -6,6 +6,7 @@ import type { components } from '$lib/generated/api'; type NlQueryInterpretation = components['schemas']['NlQueryInterpretation']; type PersonHint = components['schemas']['PersonHint']; +type TagHint = components['schemas']['TagHint']; afterEach(() => cleanup()); @@ -132,4 +133,79 @@ describe('InterpretationChipRow', () => { .element(page.getByRole('button', { name: new RegExp('Absender') })) .toBeInTheDocument(); }); + + // ── theme chips ───────────────────────────────────────────────────────────── + + const makeTag = (id: string, name: string, color?: string): TagHint => ({ id, name, color }); + + it('renders theme chips when tagsApplied is true', async () => { + const { container } = render(InterpretationChipRow, { + interpretation: makeInterpretation({ + resolvedTags: [makeTag('t1', 'Hochzeit')], + tagsApplied: true + }), + onRemoveChip: vi.fn() + }); + expect(container.querySelectorAll('[data-chip-type="theme"]')).toHaveLength(1); + await expect.element(page.getByText(/Thema: Hochzeit/)).toBeInTheDocument(); + }); + + it('renders no theme chips when tagsApplied is false', async () => { + const { container } = render(InterpretationChipRow, { + interpretation: makeInterpretation({ + resolvedTags: [makeTag('t1', 'Hochzeit')], + tagsApplied: false + }), + onRemoveChip: vi.fn() + }); + expect(container.querySelectorAll('[data-chip-type="theme"]')).toHaveLength(0); + }); + + it('renders exactly N theme chips for N resolved tags', async () => { + const { container } = render(InterpretationChipRow, { + interpretation: makeInterpretation({ + resolvedTags: [makeTag('t1', 'Krieg'), makeTag('t2', 'Hochzeit'), makeTag('t3', 'Familie')], + tagsApplied: true + }), + onRemoveChip: vi.fn() + }); + expect(container.querySelectorAll('[data-chip-type="theme"]')).toHaveLength(3); + }); + + it('calls onRemoveChip with "theme" and tag name when × is clicked', async () => { + const onRemoveChip = vi.fn(); + render(InterpretationChipRow, { + interpretation: makeInterpretation({ + resolvedTags: [makeTag('t1', 'Hochzeit')], + tagsApplied: true + }), + onRemoveChip + }); + await page.getByRole('button', { name: /Thema: Hochzeit/ }).click(); + expect(onRemoveChip).toHaveBeenCalledWith('theme', 'Hochzeit'); + }); + + it('applies inline color style for a tag with a color', async () => { + const { container } = render(InterpretationChipRow, { + interpretation: makeInterpretation({ + resolvedTags: [makeTag('t1', 'Hochzeit', 'sage')], + tagsApplied: true + }), + onRemoveChip: vi.fn() + }); + const chip = container.querySelector('[data-chip-type="theme"]') as HTMLElement; + expect(chip.style.backgroundColor).toBeTruthy(); + }); + + it('omits color style for a tag with no color', async () => { + const { container } = render(InterpretationChipRow, { + interpretation: makeInterpretation({ + resolvedTags: [makeTag('t1', 'Hochzeit')], + tagsApplied: true + }), + onRemoveChip: vi.fn() + }); + const chip = container.querySelector('[data-chip-type="theme"]') as HTMLElement; + expect(chip.getAttribute('style')).toBeFalsy(); + }); });