+
{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,