diff --git a/frontend/src/lib/components/TagParentPicker.svelte b/frontend/src/lib/components/TagParentPicker.svelte index a67faef9..1d37902f 100644 --- a/frontend/src/lib/components/TagParentPicker.svelte +++ b/frontend/src/lib/components/TagParentPicker.svelte @@ -7,14 +7,27 @@ import { createTypeahead } from '$lib/hooks/useTypeahead.svelte'; type Tag = components['schemas']['Tag']; +interface FlatTagRef { + id: string; + name: string; + parentId?: string; +} + interface Props { name: string; value?: string; excludeIds?: string[]; initialName?: string; + allTags?: FlatTagRef[]; } -let { name, value = $bindable(''), excludeIds = [], initialName = '' }: Props = $props(); +let { + name, + value = $bindable(''), + excludeIds = [], + initialName = '', + allTags = [] +}: Props = $props(); // displayName must be both prop-derived AND locally writable (user typing), so $state + // $effect is the correct pattern here — writable $derived is read-only and won't work. @@ -135,7 +148,8 @@ function handleKeydown(e: KeyboardEvent) { > {tag.name} {#if tag.parentId} - {tag.parentId} + {@const parentName = allTags.find((t) => t.id === tag.parentId)?.name ?? tag.parentId} + {parentName} {/if} {/each} diff --git a/frontend/src/lib/components/TagParentPicker.svelte.spec.ts b/frontend/src/lib/components/TagParentPicker.svelte.spec.ts index e5311983..03c905a5 100644 --- a/frontend/src/lib/components/TagParentPicker.svelte.spec.ts +++ b/frontend/src/lib/components/TagParentPicker.svelte.spec.ts @@ -162,3 +162,35 @@ describe('TagParentPicker – ARIA combobox', () => { expect(option.id).toBe('parentId-option-0'); }); }); + +// ─── Parent name resolution ─────────────────────────────────────────────────── + +describe('TagParentPicker – parent name subtitle', () => { + it('shows parent name instead of UUID when allTags is provided', async () => { + mockFetchWithTags([{ id: 't2', name: 'Keller', parentId: 't1' }]); + const allTags = [ + { id: 't1', name: 'Haus', documentCount: 5 }, + { id: 't2', name: 'Keller', parentId: 't1', documentCount: 2 } + ]; + render(TagParentPicker, { name: 'parentId', allTags }); + + const input = page.getByRole('combobox'); + await input.fill('K'); + await vi.advanceTimersByTimeAsync(300); + + await expect.element(page.getByText('Haus')).toBeInTheDocument(); + }); + + it('shows nothing as subtitle when tag has no parentId', async () => { + mockFetchWithTags([{ id: 't1', name: 'Haus' }]); + const allTags = [{ id: 't1', name: 'Haus', documentCount: 5 }]; + render(TagParentPicker, { name: 'parentId', allTags }); + + const input = page.getByRole('combobox'); + await input.fill('H'); + await vi.advanceTimersByTimeAsync(300); + + // Only the tag name should appear (no subtitle) + await expect.element(page.getByRole('option', { name: 'Haus' })).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/routes/admin/tags/[id]/+page.svelte b/frontend/src/routes/admin/tags/[id]/+page.svelte index 1e0e5174..daae6402 100644 --- a/frontend/src/routes/admin/tags/[id]/+page.svelte +++ b/frontend/src/routes/admin/tags/[id]/+page.svelte @@ -131,6 +131,7 @@ const colors = [ bind:value={parentId} excludeIds={[data.tag.id]} initialName={parentName} + allTags={data.tags} />