diff --git a/frontend/src/routes/admin/tags/[id]/+page.server.ts b/frontend/src/routes/admin/tags/[id]/+page.server.ts index 0dbace2c..45085f86 100644 --- a/frontend/src/routes/admin/tags/[id]/+page.server.ts +++ b/frontend/src/routes/admin/tags/[id]/+page.server.ts @@ -15,9 +15,13 @@ export const actions: Actions = { const data = await request.formData(); const api = createApiClient(fetch); + const name = data.get('name') as string; + const parentId = (data.get('parentId') as string) || null; + const color = (data.get('color') as string) || null; + const result = await api.PUT('/api/tags/{id}', { params: { path: { id: params.id } }, - body: { name: data.get('name') as string } + body: { name, parentId: parentId ?? undefined, color: color ?? undefined } }); if (!result.response.ok) { diff --git a/frontend/src/routes/admin/tags/[id]/+page.svelte b/frontend/src/routes/admin/tags/[id]/+page.svelte index bf39a7d0..0bed5324 100644 --- a/frontend/src/routes/admin/tags/[id]/+page.svelte +++ b/frontend/src/routes/admin/tags/[id]/+page.svelte @@ -11,6 +11,29 @@ const deleteEnabled = $derived(deleteConfirmName === data.tag.name); const unsaved = createUnsavedWarning(); +function getInitialParentId() { + return data.tag.parentId ?? ''; +} +function getInitialColor() { + return data.tag.color ?? ''; +} + +let parentId = $state(getInitialParentId()); +let selectedColor = $state(getInitialColor()); + +const colors = [ + 'sage', + 'sienna', + 'amber', + 'slate', + 'violet', + 'rose', + 'cobalt', + 'moss', + 'sand', + 'coral' +]; + $effect(() => { if (form?.success) unsaved.clearOnSuccess(); }); @@ -21,6 +44,7 @@ $effect(() => {
{ oninput={unsaved.markDirty} class="mb-5" > -
+

{m.admin_col_name()}

@@ -77,6 +101,61 @@ $effect(() => { class="w-full rounded-sm border border-line bg-surface px-3 py-2 text-sm text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" />
+ + +
+

+ Übergeordnetes Schlagwort +

+ + +
+ + + {#if parentId === ''} +
+

Farbe

+
+ {#each colors as colorName (colorName)} + + {/each} + +
+ +
+ {:else} + + {/if} diff --git a/frontend/src/routes/admin/tags/[id]/page.svelte.spec.ts b/frontend/src/routes/admin/tags/[id]/page.svelte.spec.ts index e327f6b8..e6909200 100644 --- a/frontend/src/routes/admin/tags/[id]/page.svelte.spec.ts +++ b/frontend/src/routes/admin/tags/[id]/page.svelte.spec.ts @@ -9,7 +9,10 @@ vi.mock('$app/navigation', () => ({ beforeNavigate: vi.fn(), goto: vi.fn() })); import { beforeNavigate, goto } from '$app/navigation'; const baseTag = { id: 't1', name: 'Familie' }; -const baseData = { tag: baseTag }; +const baseData = { + tag: baseTag, + tags: [] as { id: string; name: string; parentId?: string; color?: string }[] +}; afterEach(cleanup); @@ -91,3 +94,74 @@ describe('Admin edit tag page – unsaved-changes guard', () => { expect(vi.mocked(goto)).toHaveBeenCalledWith('http://localhost/admin/tags/t2'); }); }); + +// ─── Parent selector ────────────────────────────────────────────────────────── + +describe('Admin edit tag page – parent selector', () => { + it('renders a parent selector', async () => { + render(Page, { data: baseData, form: null }); + await expect.element(page.getByRole('combobox', { name: /übergeordnet/i })).toBeInTheDocument(); + }); + + it('shows other tags in the parent selector', async () => { + render(Page, { + data: { + tag: { id: 't1', name: 'Familie' }, + tags: [ + { id: 't1', name: 'Familie' }, + { id: 't2', name: 'Reise' } + ] + }, + form: null + }); + await expect.element(page.getByRole('option', { name: 'Reise' })).toBeInTheDocument(); + }); + + it('does not show self in the parent selector', async () => { + render(Page, { + data: { + tag: { id: 't1', name: 'Familie' }, + tags: [ + { id: 't1', name: 'Familie' }, + { id: 't2', name: 'Reise' } + ] + }, + form: null + }); + const options = document.querySelectorAll('select[name="parentId"] option'); + const values = Array.from(options).map((o) => o.value); + expect(values).not.toContain('t1'); + }); +}); + +// ─── Color picker ───────────────────────────────────────────────────────────── + +describe('Admin edit tag page – color picker', () => { + it('renders color picker when tag has no parent', async () => { + render(Page, { + data: { tag: { id: 't1', name: 'Familie', parentId: undefined }, tags: [] }, + form: null + }); + await expect.element(page.getByTestId('color-picker')).toBeInTheDocument(); + }); + + it('hides color picker when tag already has a parent', async () => { + render(Page, { + data: { + tag: { id: 't1', name: 'Familie', parentId: 't2' }, + tags: [{ id: 't2', name: 'Reise' }] + }, + form: null + }); + await expect.element(page.getByTestId('color-picker')).not.toBeInTheDocument(); + }); + + it('pre-selects the current tag color in the color picker', async () => { + render(Page, { + data: { tag: { id: 't1', name: 'Familie', color: 'sage' }, tags: [] }, + form: null + }); + const selected = page.getByTestId('color-swatch-sage'); + await expect.element(selected).toHaveAttribute('aria-pressed', 'true'); + }); +}); diff --git a/frontend/src/routes/page.svelte.spec.ts b/frontend/src/routes/page.svelte.spec.ts index a31a7e4b..9c1f8890 100644 --- a/frontend/src/routes/page.svelte.spec.ts +++ b/frontend/src/routes/page.svelte.spec.ts @@ -31,7 +31,8 @@ const emptyData = { tags: [], sort: 'DATE' as const, dir: 'desc' as const, - tagQ: '' + tagQ: '', + tagOp: 'AND' }, documents: [], total: 0,