diff --git a/frontend/src/lib/components/StammbaumSidePanel.svelte b/frontend/src/lib/components/StammbaumSidePanel.svelte index 3d3a834d..5ec163ac 100644 --- a/frontend/src/lib/components/StammbaumSidePanel.svelte +++ b/frontend/src/lib/components/StammbaumSidePanel.svelte @@ -3,13 +3,13 @@ import { onMount } from 'svelte'; import { invalidateAll } from '$app/navigation'; import { m } from '$lib/paraglide/messages.js'; import { chipLabel, otherName, inferredRelationshipLabel } from '$lib/relationshipLabels'; -import PersonTypeahead from '$lib/components/PersonTypeahead.svelte'; +import AddRelationshipForm from '$lib/components/AddRelationshipForm.svelte'; +import type { RelFormData } from '$lib/components/AddRelationshipForm.svelte'; import type { components } from '$lib/generated/api'; type PersonNodeDTO = components['schemas']['PersonNodeDTO']; type RelationshipDTO = components['schemas']['RelationshipDTO']; type InferredRelationshipWithPersonDTO = components['schemas']['InferredRelationshipWithPersonDTO']; -type RelationType = NonNullable; interface Props { node: PersonNodeDTO; @@ -24,32 +24,9 @@ let derivedRels = $state([]); let loading = $state(false); let error = $state(null); -let addFormOpen = $state(false); -let addType = $state('PARENT_OF'); -let addRelatedPersonId = $state(''); -let addRelatedPersonName = $state(''); -let addFromYear = $state(''); -let addToYear = $state(''); -let saving = $state(false); -let saveError = $state(null); - -const yearError = $derived.by(() => { - const from = addFromYear.trim(); - const to = addToYear.trim(); - if (!from || !to) return null; - const fromInt = parseInt(from, 10); - const toInt = parseInt(to, 10); - if (Number.isNaN(fromInt) || Number.isNaN(toInt)) return null; - return toInt < fromInt ? m.relation_year_error_bis_before_von() : null; -}); - -const submitDisabled = $derived(saving || addRelatedPersonId === '' || yearError !== null); - $effect(() => { const id = node.id; loadFor(id); - addFormOpen = false; - resetForm(); }); async function loadFor(id: string) { @@ -73,51 +50,21 @@ async function loadFor(id: string) { } } -function resetForm() { - addType = 'PARENT_OF'; - addRelatedPersonId = ''; - addRelatedPersonName = ''; - addFromYear = ''; - addToYear = ''; - saveError = null; -} - -async function submitAdd(event: Event) { - event.preventDefault(); - if (submitDisabled) return; - saving = true; - saveError = null; - try { - const body: Record = { - relatedPersonId: addRelatedPersonId, - relationType: addType - }; - if (addFromYear.trim()) { - const v = parseInt(addFromYear, 10); - if (!Number.isNaN(v)) body.fromYear = v; - } - if (addToYear.trim()) { - const v = parseInt(addToYear, 10); - if (!Number.isNaN(v)) body.toYear = v; - } - const res = await fetch(`/api/persons/${node.id}/relationships`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body) - }); - if (!res.ok) { - saveError = m.error_internal_error(); - return; - } - addFormOpen = false; - resetForm(); - await loadFor(node.id); - await invalidateAll(); - } catch { - saveError = m.error_internal_error(); - } finally { - saving = false; - } +async function handleAddRelationship(data: RelFormData) { + const body: Record = { + relatedPersonId: data.relatedPersonId, + relationType: data.relationType + }; + if (data.fromYear !== undefined) body.fromYear = data.fromYear; + if (data.toYear !== undefined) body.toYear = data.toYear; + const res = await fetch(`/api/persons/${node.id}/relationships`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + if (!res.ok) throw new Error('Failed to add relationship'); + await loadFor(node.id); + await invalidateAll(); } function handleEscape(event: KeyboardEvent) { @@ -196,89 +143,9 @@ const topDerived = $derived( {/if} {#if canWrite} - {#if !addFormOpen} - - {:else} -
- - -
- - -
- {#if yearError} - - {/if} - {#if saveError} - - {/if} -
- - -
- - {/if} + {#key node.id} + + {/key} {/if} diff --git a/frontend/src/lib/components/StammbaumSidePanel.svelte.spec.ts b/frontend/src/lib/components/StammbaumSidePanel.svelte.spec.ts index 4d426a02..1006367b 100644 --- a/frontend/src/lib/components/StammbaumSidePanel.svelte.spec.ts +++ b/frontend/src/lib/components/StammbaumSidePanel.svelte.spec.ts @@ -4,6 +4,7 @@ import { page } from 'vitest/browser'; import StammbaumSidePanel from './StammbaumSidePanel.svelte'; vi.mock('$app/navigation', () => ({ invalidateAll: vi.fn() })); +vi.mock('$app/forms', () => ({ enhance: () => () => {} })); vi.mock('$lib/components/PersonTypeahead.svelte', () => ({ default: () => null })); const makeNode = () => ({ @@ -54,6 +55,23 @@ describe('StammbaumSidePanel', () => { await expect.element(page.getByText('Noch keine Beziehungen bekannt.')).toBeInTheDocument(); }); + it('year inputs inside the add form have label elements (canWrite=true)', async () => { + render(StammbaumSidePanel, { node: makeNode(), onClose: vi.fn(), canWrite: true }); + await expect.element(page.getByText('Noch keine Beziehungen bekannt.')).toBeInTheDocument(); + const addBtn = [...document.querySelectorAll('button')].find((b) => + /Beziehung hinzufügen/i.test(b.textContent ?? '') + ); + addBtn!.click(); + await expect.element(page.getByRole('combobox')).toBeInTheDocument(); + const yearInputs = [...document.querySelectorAll('input')].filter( + (i) => i.inputMode === 'numeric' + ); + expect(yearInputs.length).toBeGreaterThan(0); + for (const input of yearInputs) { + expect(input.closest('label')).not.toBeNull(); + } + }); + it('shows loading indicator while fetching', async () => { let resolveFirst: (v: unknown) => void; vi.stubGlobal(