From e204ed89b6d3203acf4a52d94c5b278c990e7c30 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 7 Apr 2026 16:05:56 +0200 Subject: [PATCH] fix(ui): switch alias operations from client fetch to form actions Replaces raw client-side fetch with SvelteKit form actions (addAlias, removeAlias) using the server-side API client for proper auth handling. 10 new component tests for NameHistoryEditCard. Co-Authored-By: Claude Sonnet 4.6 --- .../routes/persons/[id]/edit/+page.server.ts | 48 +++++ .../src/routes/persons/[id]/edit/+page.svelte | 7 +- .../[id]/edit/NameHistoryEditCard.svelte | 188 +++++++----------- .../edit/NameHistoryEditCard.svelte.test.ts | 86 ++++++++ 4 files changed, 206 insertions(+), 123 deletions(-) create mode 100644 frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte.test.ts diff --git a/frontend/src/routes/persons/[id]/edit/+page.server.ts b/frontend/src/routes/persons/[id]/edit/+page.server.ts index 13e39509..e814737a 100644 --- a/frontend/src/routes/persons/[id]/edit/+page.server.ts +++ b/frontend/src/routes/persons/[id]/edit/+page.server.ts @@ -86,5 +86,53 @@ export const actions = { } throw redirect(303, `/persons/${targetPersonId}`); + }, + + addAlias: async ({ request, params, fetch }) => { + const formData = await request.formData(); + const lastName = formData.get('lastName')?.toString().trim(); + const firstName = formData.get('firstName')?.toString().trim() || undefined; + const type = formData.get('type')?.toString(); + + if (!lastName) { + return fail(400, { aliasError: 'Nachname ist ein Pflichtfeld.' }); + } + if (!type) { + return fail(400, { aliasError: 'Art ist ein Pflichtfeld.' }); + } + + const api = createApiClient(fetch); + const result = await api.POST('/api/persons/{id}/aliases', { + params: { path: { id: params.id } }, + body: { lastName, firstName, type: type as 'BIRTH' | 'WIDOWED' | 'DIVORCED' | 'OTHER' } + }); + + if (!result.response.ok) { + const code = (result.error as unknown as { code?: string })?.code; + return fail(result.response.status, { aliasError: getErrorMessage(code) }); + } + + return { aliasSuccess: true }; + }, + + removeAlias: async ({ request, params, fetch }) => { + const formData = await request.formData(); + const aliasId = formData.get('aliasId')?.toString(); + + if (!aliasId) { + return fail(400, { aliasError: 'Alias ID fehlt.' }); + } + + const api = createApiClient(fetch); + const result = await api.DELETE('/api/persons/{id}/aliases/{aliasId}', { + params: { path: { id: params.id, aliasId } } + }); + + if (!result.response.ok) { + const code = (result.error as unknown as { code?: string })?.code; + return fail(result.response.status, { aliasError: getErrorMessage(code) }); + } + + return { aliasSuccess: true }; } }; diff --git a/frontend/src/routes/persons/[id]/edit/+page.svelte b/frontend/src/routes/persons/[id]/edit/+page.svelte index 4e641553..dd0f539e 100644 --- a/frontend/src/routes/persons/[id]/edit/+page.svelte +++ b/frontend/src/routes/persons/[id]/edit/+page.svelte @@ -51,12 +51,7 @@ const person = $derived(data.person); - + diff --git a/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte b/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte index f602c87a..a7e6c185 100644 --- a/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte +++ b/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte @@ -1,10 +1,8 @@
@@ -108,6 +45,10 @@ async function addAlias() { {m.person_alias_heading()} + {#if aliasError} +

{aliasError}

+ {/if} + {#if sorted.length === 0}

{m.person_alias_empty()}

{:else} @@ -117,7 +58,7 @@ async function addAlias() { {typeLabel(alias.type)} - {alias.firstName ?? personFirstName} + {#if alias.firstName}{alias.firstName}{/if} {alias.lastName} @@ -155,52 +96,50 @@ async function addAlias() { {m.person_alias_add_heading()} - {#if addError} -

{addError}

- {/if} +
+
+ -
- + - + - - -
- +
+ +
-
+
{/if}
@@ -213,18 +152,33 @@ async function addAlias() {
- + + +
diff --git a/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte.test.ts b/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte.test.ts new file mode 100644 index 00000000..ce5510cd --- /dev/null +++ b/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect } from 'vitest'; +import { render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import NameHistoryEditCard from './NameHistoryEditCard.svelte'; + +const aliases = [ + { id: 'a1', lastName: 'de Gruyter', firstName: null, type: 'BIRTH', sortOrder: 0 }, + { id: 'a2', lastName: 'Schmidt', firstName: 'Maria', type: 'WIDOWED', sortOrder: 1 } +]; + +describe('NameHistoryEditCard', () => { + it('should render alias rows when aliases exist', async () => { + render(NameHistoryEditCard, { aliases, canWrite: true }); + + await expect.element(page.getByText('de Gruyter')).toBeInTheDocument(); + await expect.element(page.getByText('Schmidt')).toBeInTheDocument(); + }); + + it('should show empty state when no aliases', async () => { + render(NameHistoryEditCard, { aliases: [], canWrite: true }); + + const emptyText = document.querySelector('.italic'); + expect(emptyText).not.toBeNull(); + }); + + it('should show add form when canWrite is true', async () => { + render(NameHistoryEditCard, { aliases: [], canWrite: true }); + + const form = document.querySelector('form[action="?/addAlias"]'); + expect(form).not.toBeNull(); + }); + + it('should hide add form when canWrite is false', async () => { + render(NameHistoryEditCard, { aliases: [], canWrite: false }); + + const form = document.querySelector('form[action="?/addAlias"]'); + expect(form).toBeNull(); + }); + + it('should hide delete buttons when canWrite is false', async () => { + render(NameHistoryEditCard, { aliases, canWrite: false }); + + const deleteButtons = document.querySelectorAll('button[aria-label*="Entfernen"]'); + expect(deleteButtons.length).toBe(0); + }); + + it('should show delete buttons when canWrite is true', async () => { + render(NameHistoryEditCard, { aliases, canWrite: true }); + + const deleteButtons = document.querySelectorAll('button[aria-label*="Entfernen"]'); + expect(deleteButtons.length).toBe(2); + }); + + it('should include alias name in delete button aria-label', async () => { + render(NameHistoryEditCard, { aliases: [aliases[0]], canWrite: true }); + + const btn = document.querySelector('button[aria-label*="de Gruyter"]'); + expect(btn).not.toBeNull(); + }); + + it('should show delete modal when delete button is clicked', async () => { + render(NameHistoryEditCard, { aliases: [aliases[0]], canWrite: true }); + + const deleteBtn = document.querySelector('button[aria-label*="de Gruyter"]')!; + deleteBtn.dispatchEvent(new MouseEvent('click', { bubbles: true })); + + await expect.element(page.getByText('Alias entfernen?')).toBeInTheDocument(); + }); + + it('should show alias error when provided', async () => { + render(NameHistoryEditCard, { + aliases: [], + canWrite: true, + aliasError: 'Something went wrong' + }); + + await expect.element(page.getByText('Something went wrong')).toBeInTheDocument(); + }); + + it('should have required attribute on lastName input', async () => { + render(NameHistoryEditCard, { aliases: [], canWrite: true }); + + const input = document.querySelector('input[name="lastName"]') as HTMLInputElement; + expect(input.required).toBe(true); + }); +});