feat(relationship): date+precision edit UI, notes, and read-view display
Regenerate api.ts for the LocalDate+DatePrecision RelationshipDTO / RelationshipUpsertRequest and the new PUT, then migrate every caller: - RelationshipDateField (mirrors PersonLifeDateField: DAY/MONTH/YEAR, 44px targets, labelled, semantic dark-mode tokens, relation_* i18n keys). - AddRelationshipForm is now upsert-capable: an optional `relationship` prop pre-fills type, person, both dates+precision and notes; posts to ?/updateRelationship (else ?/addRelationship); the submit control disables and shows a progress spinner while a request is in flight (REQ-019); notes textarea (<=2000). - RelationshipChip gains an accessible Edit affordance (canWrite + onEdit); StammbaumCard wires it, formats the date range via formatRelationshipDateRange, and sorts by fromDate. PersonRelationshipsCard (read view) shows the date range and notes; no dates -> no date line. - persons/[id]/edit/+page.server.ts: updateRelationship action (PUT) + the addRelationship action reshaped to date+precision+notes (empty date omits precision for coherence). - Genealogy callers fixed for the dropped year fields: familyForest spouse-order and StammbaumConnectors ended-edge dashing now key off fromDate/toDate. - i18n relation_* form keys in de/en/es. REQ-004, REQ-014, REQ-015, REQ-016, REQ-019 Refs #837 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -97,3 +97,98 @@ describe('persons/[id]/edit update action — generation (#689)', () => {
|
||||
expect(body).toHaveProperty('generation', 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('persons/[id]/edit relationship actions (#837)', () => {
|
||||
function relForm(overrides: Record<string, string | null> = {}): Request {
|
||||
const fd = new FormData();
|
||||
fd.set('relatedPersonId', 'p2');
|
||||
fd.set('relationType', 'SPOUSE_OF');
|
||||
for (const [k, v] of Object.entries(overrides)) {
|
||||
if (v == null) fd.delete(k);
|
||||
else fd.set(k, v);
|
||||
}
|
||||
return new Request('http://localhost/persons/p1/edit', { method: 'POST', body: fd });
|
||||
}
|
||||
|
||||
it('addRelationship posts date + precision + notes', async () => {
|
||||
const post = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: {} });
|
||||
vi.mocked(createApiClient).mockReturnValue({ POST: post } as unknown as ReturnType<
|
||||
typeof createApiClient
|
||||
>);
|
||||
const request = relForm({
|
||||
fromDate: '1923-05-12',
|
||||
fromDatePrecision: 'DAY',
|
||||
notes: 'Hochzeit'
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await actions.addRelationship({ request, params: { id: 'p1' }, fetch: mockFetch } as any);
|
||||
|
||||
const [path, opts] = post.mock.calls[0];
|
||||
expect(path).toBe('/api/persons/{id}/relationships');
|
||||
expect(opts.body).toMatchObject({
|
||||
relatedPersonId: 'p2',
|
||||
relationType: 'SPOUSE_OF',
|
||||
fromDate: '1923-05-12',
|
||||
fromDatePrecision: 'DAY',
|
||||
notes: 'Hochzeit'
|
||||
});
|
||||
});
|
||||
|
||||
it('addRelationship omits precision when the date is empty (coherence)', async () => {
|
||||
const post = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: {} });
|
||||
vi.mocked(createApiClient).mockReturnValue({ POST: post } as unknown as ReturnType<
|
||||
typeof createApiClient
|
||||
>);
|
||||
const request = relForm({ fromDatePrecision: 'DAY' }); // precision but no date
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await actions.addRelationship({ request, params: { id: 'p1' }, fetch: mockFetch } as any);
|
||||
|
||||
const body = post.mock.calls[0][1].body;
|
||||
expect(body).not.toHaveProperty('fromDate');
|
||||
expect(body).not.toHaveProperty('fromDatePrecision');
|
||||
});
|
||||
|
||||
it('updateRelationship PUTs to the relId path with the new body', async () => {
|
||||
const put = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: {} });
|
||||
vi.mocked(createApiClient).mockReturnValue({ PUT: put } as unknown as ReturnType<
|
||||
typeof createApiClient
|
||||
>);
|
||||
const request = relForm({ relId: 'rel-9', fromDate: '1923-05-12', fromDatePrecision: 'DAY' });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await actions.updateRelationship({ request, params: { id: 'p1' }, fetch: mockFetch } as any);
|
||||
|
||||
const [path, opts] = put.mock.calls[0];
|
||||
expect(path).toBe('/api/persons/{id}/relationships/{relId}');
|
||||
expect(opts.params.path).toMatchObject({ id: 'p1', relId: 'rel-9' });
|
||||
expect(opts.body).toMatchObject({
|
||||
relatedPersonId: 'p2',
|
||||
relationType: 'SPOUSE_OF',
|
||||
fromDate: '1923-05-12',
|
||||
fromDatePrecision: 'DAY'
|
||||
});
|
||||
});
|
||||
|
||||
it('updateRelationship surfaces a backend error as a fail', async () => {
|
||||
const put = vi.fn().mockResolvedValue({
|
||||
response: { ok: false, status: 400 },
|
||||
error: { code: 'INVALID_RELATIONSHIP_DATES' }
|
||||
});
|
||||
vi.mocked(createApiClient).mockReturnValue({ PUT: put } as unknown as ReturnType<
|
||||
typeof createApiClient
|
||||
>);
|
||||
const request = relForm({ relId: 'rel-9' });
|
||||
|
||||
const result = (await actions.updateRelationship({
|
||||
request,
|
||||
params: { id: 'p1' },
|
||||
fetch: mockFetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any)) as { status: number; data: { relationshipError: string } };
|
||||
|
||||
expect(result.status).toBe(400);
|
||||
expect(result.data.relationshipError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user