diff --git a/frontend/src/routes/admin/tags/[id]/+page.server.ts b/frontend/src/routes/admin/tags/[id]/+page.server.ts index 45085f86..3288b743 100644 --- a/frontend/src/routes/admin/tags/[id]/+page.server.ts +++ b/frontend/src/routes/admin/tags/[id]/+page.server.ts @@ -32,10 +32,14 @@ export const actions: Actions = { return { success: true }; }, - delete: async ({ params, fetch }) => { + merge: async ({ params, request, fetch }) => { + const data = await request.formData(); + const targetId = data.get('targetId') as string; const api = createApiClient(fetch); - const result = await api.DELETE('/api/tags/{id}', { - params: { path: { id: params.id } } + + const result = await api.POST('/api/tags/{id}/merge', { + params: { path: { id: params.id } }, + body: { targetId } }); if (!result.response.ok) { @@ -43,6 +47,28 @@ export const actions: Actions = { return fail(result.response.status, { error: getErrorMessage(code) }); } + throw redirect(303, `/admin/tags/${result.data!.id}`); + }, + + delete: async ({ params, request, fetch }) => { + const data = await request.formData(); + const deleteMode = (data.get('deleteMode') as string) || 'single'; + const api = createApiClient(fetch); + + const result = + deleteMode === 'subtree' + ? await api.DELETE('/api/tags/{id}/subtree', { + params: { path: { id: params.id } } + }) + : await api.DELETE('/api/tags/{id}', { + params: { path: { id: params.id } } + }); + + if (!result.response.ok) { + const code = (result.error as unknown as { code?: string })?.code; + return fail(result.response.status, { error: getErrorMessage(code) }); + } + throw redirect(303, '/admin/tags'); } }; diff --git a/frontend/src/routes/admin/tags/[id]/+page.svelte b/frontend/src/routes/admin/tags/[id]/+page.svelte index ad14951c..1e0e5174 100644 --- a/frontend/src/routes/admin/tags/[id]/+page.svelte +++ b/frontend/src/routes/admin/tags/[id]/+page.svelte @@ -3,12 +3,14 @@ import { enhance } from '$app/forms'; import { m } from '$lib/paraglide/messages.js'; import { createUnsavedWarning } from '$lib/hooks/useUnsavedWarning.svelte'; import UnsavedWarningBanner from '$lib/components/UnsavedWarningBanner.svelte'; +import TagParentPicker from '$lib/components/TagParentPicker.svelte'; +import TagAncestry from './TagAncestry.svelte'; +import TagChildrenPreview from './TagChildrenPreview.svelte'; +import TagMergeZone from './TagMergeZone.svelte'; +import TagDeleteGuard from './TagDeleteGuard.svelte'; let { data, form } = $props(); -let deleteConfirmName = $state(''); -const deleteEnabled = $derived(deleteConfirmName === data.tag.name); - const unsaved = createUnsavedWarning(); function getInitialParentId() { @@ -17,18 +19,25 @@ function getInitialParentId() { function getInitialColor() { return data.tag.color ?? ''; } +function getInitialParentName() { + if (!data.tag.parentId) return ''; + return data.tags.find((t: { id: string }) => t.id === data.tag.parentId)?.name ?? ''; +} let parentId = $state(getInitialParentId()); let selectedColor = $state(getInitialColor()); +let parentName = $state(getInitialParentName()); -// SvelteKit reuses the same component instance when navigating between tags client-side. -// $state() only initialises on mount, so we need an effect to reset local form state -// whenever the server switches to a different tag. +// Reset state when navigating between tags client-side $effect(() => { - void data.tag.id; // declare dependency + void data.tag.id; parentId = data.tag.parentId ?? ''; selectedColor = data.tag.color ?? ''; - deleteConfirmName = ''; + parentName = getInitialParentName(); +}); + +$effect(() => { + if (form?.success) unsaved.clearOnSuccess(); }); const colors = [ @@ -43,14 +52,10 @@ const colors = [ 'sand', 'coral' ]; - -$effect(() => { - if (form?.success) unsaved.clearOnSuccess(); -});