feat(relationship): editable relationships with LocalDate+DatePrecision dates and notes (#841)
All checks were successful
CI / Unit & Component Tests (push) Successful in 5m10s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 5m14s
CI / fail2ban Regex (push) Successful in 50s
CI / Semgrep Security Scan (push) Successful in 27s
CI / Compose Bucket Idempotency (push) Successful in 1m5s
All checks were successful
CI / Unit & Component Tests (push) Successful in 5m10s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 5m14s
CI / fail2ban Regex (push) Successful in 50s
CI / Semgrep Security Scan (push) Successful in 27s
CI / Compose Bucket Idempotency (push) Successful in 1m5s
Closes #837 Makes `PersonRelationship` fully editable (type, related person, dates, notes), migrates its dates from `Integer fromYear/toYear` to `LocalDate + DatePrecision` (mirroring the #773 person pattern, ADR-039 / V76), activates the previously-dead `notes` column, and gives the Zeitstrahl's derived **Heirat** events full date precision for free. Both Open Decisions confirmed as adopted: **no `@Version`** (last-write-wins, single-writer archive) and **`DELETE` ownership-mismatch aligned 403 → 404** (anti-enumeration, matching the new `PUT`). ## What's in it - **V78** migrates `person_relationships.from_year/to_year` → `from_date`/`to_date` + NOT-NULL `*_date_precision` (default `UNKNOWN`); pre-check abort on corrupt years, `YYYY-01-01`/`YEAR` backfill, 5 named CHECK constraints, year columns dropped. - **`PUT /api/persons/{id}/relationships/{relId}`** (`@RequirePermission(WRITE_ALL)`) re-runs every create invariant (self / coherence / order / reverse-PARENT_OF / duplicate) and re-flags family membership; orientation preserved per viewpoint. - New `ErrorCode.INVALID_RELATIONSHIP_DATES` registered in all four sites (§3.6). - `TimelineEventService` sources the derived marriage date from `SPOUSE_OF.fromDate` + precision. - Frontend: `RelationshipDateField` (DAY/MONTH/YEAR), upsert-capable `AddRelationshipForm` (pre-fill + notes + in-flight submit lock), `RelationshipChip` Edit affordance, `updateRelationship` server action, read-view date range + notes, `formatRelationshipDateRange` helper. `api.ts` regenerated. - Docs: ADR-044, db-orm/db-relationships diagrams, DEPLOYMENT §5 deploy note, RTM REQ-001…REQ-019. ## Requirements All 19 EARS requirements implemented red/green and marked `Done` in `.specify/rtm.md`. ## Test plan - **Backend** (targeted, green): `RelationshipMigrationTest` (Testcontainers pg16, 8), `RelationshipServiceTest` (22), `RelationshipControllerTest` (15), `RelationshipServiceIntegrationTest` (real DB, 10), `DerivedEventsAssemblyTest` (17), `ArchitectureTest` (14); `clean package` builds. - **Frontend** (green): `relationshipDates.spec.ts`, `AddRelationshipForm.svelte.spec.ts`, `RelationshipChip.svelte.spec.ts`, `PersonRelationshipsCard.svelte.test.ts`, `page.server.spec.ts`, `messages.spec.ts`. `npm run check` = 798 (below the ~834 baseline); `npm run lint` clean. ## Notes for reviewers - **Spec deviation:** the edit form was built by making `AddRelationshipForm` upsert-capable rather than a duplicate `EditRelationshipForm` (DRY); RTM rows reference `AddRelationshipForm.svelte.spec.ts`. - `api.ts` regenerated from the live spec; only relationship-relevant hunks remain (one springdoc `PageableObject` field-reorder pruned). - **Deploy:** V78 is one-way and not rolling-deploy-safe — stop old JAR → start new JAR (Flyway runs first); targeted `pg_restore -t person_relationships` for rollback. No maintenance window. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Marcel <marcel@familienarchiv> Reviewed-on: #841
This commit was merged in pull request #841.
This commit is contained in:
@@ -4,6 +4,7 @@ import { m } from '$lib/paraglide/messages.js';
|
||||
import RelationshipChip from '$lib/person/relationship/RelationshipChip.svelte';
|
||||
import AddRelationshipForm from '$lib/person/relationship/AddRelationshipForm.svelte';
|
||||
import { chipLabel, otherName, inferredRelationshipLabel } from '$lib/person/relationshipLabels';
|
||||
import { formatRelationshipDateRange } from '$lib/person/relationshipDates';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
type RelationshipDTO = components['schemas']['RelationshipDTO'];
|
||||
@@ -29,13 +30,15 @@ let {
|
||||
|
||||
type RelationType = NonNullable<RelationshipDTO['relationType']>;
|
||||
|
||||
const sortedDirect = $derived([...relationships].sort(byTypeThenYear));
|
||||
const sortedDirect = $derived([...relationships].sort(byTypeThenDate));
|
||||
const topDerived = $derived(inferredRelationships.slice(0, 5));
|
||||
let editingRelId = $state<string | null>(null);
|
||||
|
||||
function byTypeThenYear(a: RelationshipDTO, b: RelationshipDTO): number {
|
||||
function byTypeThenDate(a: RelationshipDTO, b: RelationshipDTO): number {
|
||||
const order = relationTypeOrder(a.relationType) - relationTypeOrder(b.relationType);
|
||||
if (order !== 0) return order;
|
||||
return (a.fromYear ?? 0) - (b.fromYear ?? 0);
|
||||
// ISO dates sort lexicographically == chronologically; a missing date sorts first.
|
||||
return (a.fromDate ?? '').localeCompare(b.fromDate ?? '');
|
||||
}
|
||||
|
||||
function relationTypeOrder(t: RelationType | undefined): number {
|
||||
@@ -53,13 +56,13 @@ function relationTypeOrder(t: RelationType | undefined): number {
|
||||
return order[t ?? 'OTHER'] ?? 99;
|
||||
}
|
||||
|
||||
function yearRange(rel: RelationshipDTO): string {
|
||||
const from = rel.fromYear;
|
||||
const to = rel.toYear;
|
||||
if (from && to) return `${from}–${to}`;
|
||||
if (from) return m.relation_year_from({ year: from });
|
||||
if (to) return m.relation_year_to({ year: to });
|
||||
return '';
|
||||
function dateRangeOf(rel: RelationshipDTO): string {
|
||||
return formatRelationshipDateRange(
|
||||
rel.fromDate,
|
||||
rel.fromDatePrecision,
|
||||
rel.toDate,
|
||||
rel.toDatePrecision
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -132,10 +135,20 @@ function yearRange(rel: RelationshipDTO): string {
|
||||
<RelationshipChip
|
||||
chipLabel={chipLabel(rel, personId)}
|
||||
otherName={otherName(rel, personId)}
|
||||
yearRange={yearRange(rel)}
|
||||
dateRange={dateRangeOf(rel)}
|
||||
canWrite={canWrite}
|
||||
relId={rel.id}
|
||||
onEdit={canWrite ? () => (editingRelId = rel.id) : undefined}
|
||||
/>
|
||||
{#if editingRelId === rel.id}
|
||||
<li>
|
||||
<AddRelationshipForm
|
||||
personId={personId}
|
||||
relationship={rel}
|
||||
onClose={() => (editingRelId = null)}
|
||||
/>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
@@ -111,17 +111,21 @@ describe('StammbaumCard', () => {
|
||||
expect(items.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('renders the year range "from–to" for a relationship with both years', async () => {
|
||||
it('renders the date range "from – to" for a relationship with both dates', async () => {
|
||||
render(StammbaumCard, {
|
||||
props: baseProps({
|
||||
relationships: [
|
||||
{
|
||||
id: 'r-1',
|
||||
personId: 'p-1',
|
||||
relatedPersonId: 'p-x',
|
||||
personDisplayName: 'Anna',
|
||||
relatedPersonDisplayName: 'Xavier',
|
||||
relationType: 'COLLEAGUE',
|
||||
fromYear: 1940,
|
||||
toYear: 1945,
|
||||
personA: { id: 'p-1', displayName: 'Anna' },
|
||||
personB: { id: 'p-x', displayName: 'Xavier' }
|
||||
fromDate: '1940-01-01',
|
||||
fromDatePrecision: 'YEAR',
|
||||
toDate: '1945-01-01',
|
||||
toDatePrecision: 'YEAR'
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -131,23 +135,27 @@ describe('StammbaumCard', () => {
|
||||
expect(document.body.textContent).toContain('1945');
|
||||
});
|
||||
|
||||
it('renders only "fromYear" for a relationship with no end year', async () => {
|
||||
it('renders only the start date for a relationship with no end date', async () => {
|
||||
render(StammbaumCard, {
|
||||
props: baseProps({
|
||||
relationships: [
|
||||
{
|
||||
id: 'r-2',
|
||||
personId: 'p-1',
|
||||
relatedPersonId: 'p-y',
|
||||
personDisplayName: 'Anna',
|
||||
relatedPersonDisplayName: 'Yvonne',
|
||||
relationType: 'NEIGHBOR',
|
||||
fromYear: 1935,
|
||||
personA: { id: 'p-1', displayName: 'Anna' },
|
||||
personB: { id: 'p-y', displayName: 'Yvonne' }
|
||||
fromDate: '1935-01-01',
|
||||
fromDatePrecision: 'YEAR',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
expect(document.body.textContent).toContain('1935');
|
||||
expect(document.body.textContent).not.toContain('1935–');
|
||||
expect(document.body.textContent).not.toContain('1935 –');
|
||||
});
|
||||
|
||||
it('renders the inferred-relationships disclosure when topDerived has items', async () => {
|
||||
|
||||
@@ -250,7 +250,7 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
y2={bCenter.y}
|
||||
stroke="var(--c-primary)"
|
||||
stroke-width="1.5"
|
||||
stroke-dasharray={e.toYear ? '4 4' : undefined}
|
||||
stroke-dasharray={e.toDate ? '4 4' : undefined}
|
||||
/>
|
||||
<circle
|
||||
cx={(aCenter.x + bCenter.x) / 2}
|
||||
|
||||
@@ -18,7 +18,9 @@ function parentEdge(parentId: string, childId: string): RelationshipDTO {
|
||||
relatedPersonId: childId,
|
||||
personDisplayName: '',
|
||||
relatedPersonDisplayName: '',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,7 +32,9 @@ function endedSpouseEdge(a: string, b: string): RelationshipDTO {
|
||||
personDisplayName: '',
|
||||
relatedPersonDisplayName: '',
|
||||
relationType: 'SPOUSE_OF',
|
||||
toYear: 1950
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDate: '1950-01-01',
|
||||
toDatePrecision: 'YEAR'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -54,12 +54,19 @@ async function loadFor(id: string) {
|
||||
}
|
||||
|
||||
async function handleAddRelationship(data: RelFormData) {
|
||||
const body: Record<string, string | number> = {
|
||||
const body: Record<string, string> = {
|
||||
relatedPersonId: data.relatedPersonId,
|
||||
relationType: data.relationType
|
||||
};
|
||||
if (data.fromYear !== undefined) body.fromYear = data.fromYear;
|
||||
if (data.toYear !== undefined) body.toYear = data.toYear;
|
||||
if (data.fromDate) {
|
||||
body.fromDate = data.fromDate;
|
||||
if (data.fromDatePrecision) body.fromDatePrecision = data.fromDatePrecision;
|
||||
}
|
||||
if (data.toDate) {
|
||||
body.toDate = data.toDate;
|
||||
if (data.toDatePrecision) body.toDatePrecision = data.toDatePrecision;
|
||||
}
|
||||
if (data.notes) body.notes = data.notes;
|
||||
const res = await csrfFetch(`/api/persons/${node.id}/relationships`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -72,20 +72,20 @@ 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 () => {
|
||||
it('date inputs inside the add form have accessible labels (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<HTMLButtonElement>('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(
|
||||
await expect.element(page.getByRole('combobox', { name: 'Typ' })).toBeInTheDocument();
|
||||
const dateInputs = [...document.querySelectorAll('input')].filter(
|
||||
(i) => i.inputMode === 'numeric'
|
||||
);
|
||||
expect(yearInputs.length).toBeGreaterThan(0);
|
||||
for (const input of yearInputs) {
|
||||
expect(input.closest('label')).not.toBeNull();
|
||||
expect(dateInputs.length).toBeGreaterThan(0);
|
||||
for (const input of dateInputs) {
|
||||
expect(input.getAttribute('aria-label')).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import { render } from 'vitest-browser-svelte';
|
||||
import StammbaumTree from './StammbaumTree.svelte';
|
||||
import type { PanZoomState } from './panZoom';
|
||||
import { DIMMED_OPACITY } from './layout/highlightLineage';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
type RelationshipDTO = components['schemas']['RelationshipDTO'];
|
||||
|
||||
const ID_A = '00000000-0000-0000-0000-000000000001';
|
||||
const ID_B = '00000000-0000-0000-0000-000000000002';
|
||||
@@ -105,7 +108,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: PARENT_B,
|
||||
personDisplayName: 'Walter',
|
||||
relatedPersonDisplayName: 'Eugenie',
|
||||
relationType: 'SPOUSE_OF'
|
||||
relationType: 'SPOUSE_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p1a',
|
||||
@@ -113,7 +118,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: CHILD_1,
|
||||
personDisplayName: 'Walter',
|
||||
relatedPersonDisplayName: 'Clara',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p1b',
|
||||
@@ -121,7 +128,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: CHILD_1,
|
||||
personDisplayName: 'Eugenie',
|
||||
relatedPersonDisplayName: 'Clara',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p2a',
|
||||
@@ -129,7 +138,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: CHILD_2,
|
||||
personDisplayName: 'Walter',
|
||||
relatedPersonDisplayName: 'Hans',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p2b',
|
||||
@@ -137,7 +148,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: CHILD_2,
|
||||
personDisplayName: 'Eugenie',
|
||||
relatedPersonDisplayName: 'Hans',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
}
|
||||
],
|
||||
selectedId: null,
|
||||
@@ -181,7 +194,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: PARENT_B,
|
||||
personDisplayName: 'Walter',
|
||||
relatedPersonDisplayName: 'Eugenie',
|
||||
relationType: 'SPOUSE_OF'
|
||||
relationType: 'SPOUSE_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p1',
|
||||
@@ -189,7 +204,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: CHILD,
|
||||
personDisplayName: 'Walter',
|
||||
relatedPersonDisplayName: 'Hans',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
@@ -197,7 +214,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: CHILD,
|
||||
personDisplayName: 'Eugenie',
|
||||
relatedPersonDisplayName: 'Hans',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
}
|
||||
],
|
||||
selectedId: null,
|
||||
@@ -244,7 +263,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: EUGENIE,
|
||||
personDisplayName: 'Walter',
|
||||
relatedPersonDisplayName: 'Eugenie',
|
||||
relationType: 'SPOUSE_OF'
|
||||
relationType: 'SPOUSE_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p1',
|
||||
@@ -252,7 +273,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: HANS,
|
||||
personDisplayName: 'Walter',
|
||||
relatedPersonDisplayName: 'Hans',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
@@ -260,7 +283,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: HANS,
|
||||
personDisplayName: 'Eugenie',
|
||||
relatedPersonDisplayName: 'Hans',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
@@ -268,7 +293,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: CLARA,
|
||||
personDisplayName: 'Walter',
|
||||
relatedPersonDisplayName: 'Clara',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
@@ -276,7 +303,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: CLARA,
|
||||
personDisplayName: 'Eugenie',
|
||||
relatedPersonDisplayName: 'Clara',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
@@ -284,7 +313,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: HILDE,
|
||||
personDisplayName: 'Hans',
|
||||
relatedPersonDisplayName: 'Hilde',
|
||||
relationType: 'SPOUSE_OF'
|
||||
relationType: 'SPOUSE_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
@@ -292,7 +323,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: LILI,
|
||||
personDisplayName: 'Hans',
|
||||
relatedPersonDisplayName: 'Lili',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
@@ -300,7 +333,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: LILI,
|
||||
personDisplayName: 'Hilde',
|
||||
relatedPersonDisplayName: 'Lili',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
}
|
||||
],
|
||||
selectedId: null,
|
||||
@@ -358,7 +393,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: ID_B,
|
||||
personDisplayName: 'Anna',
|
||||
relatedPersonDisplayName: 'Bertha',
|
||||
relationType: 'SPOUSE_OF'
|
||||
relationType: 'SPOUSE_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
}
|
||||
],
|
||||
selectedId: null,
|
||||
@@ -391,7 +428,9 @@ describe('StammbaumTree viewBox', () => {
|
||||
relatedPersonId: ID_B,
|
||||
personDisplayName: 'Anna',
|
||||
relatedPersonDisplayName: 'Bertha',
|
||||
relationType: 'SPOUSE_OF'
|
||||
relationType: 'SPOUSE_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
}
|
||||
],
|
||||
selectedId: null,
|
||||
@@ -599,7 +638,9 @@ describe('StammbaumTree keyboard pan/zoom (#692)', () => {
|
||||
relatedPersonId: ID_B,
|
||||
personDisplayName: 'Anna',
|
||||
relatedPersonDisplayName: 'Bertha',
|
||||
relationType: 'SPOUSE_OF'
|
||||
relationType: 'SPOUSE_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
}
|
||||
],
|
||||
selectedId: null,
|
||||
@@ -668,7 +709,9 @@ describe('StammbaumTree keyboard pan/zoom (#692)', () => {
|
||||
personDisplayName: 'Anna',
|
||||
relatedPersonDisplayName: 'Bertha',
|
||||
relationType: 'SPOUSE_OF',
|
||||
toYear: 1925
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDate: '1925-01-01',
|
||||
toDatePrecision: 'YEAR'
|
||||
}
|
||||
],
|
||||
selectedId: null,
|
||||
@@ -695,7 +738,9 @@ describe('StammbaumTree keyboard pan/zoom (#692)', () => {
|
||||
relatedPersonId: ID_B,
|
||||
personDisplayName: 'Anna',
|
||||
relatedPersonDisplayName: 'Bertha',
|
||||
relationType: 'SPOUSE_OF'
|
||||
relationType: 'SPOUSE_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
}
|
||||
],
|
||||
selectedId: null,
|
||||
@@ -723,7 +768,9 @@ describe('StammbaumTree keyboard pan/zoom (#692)', () => {
|
||||
relatedPersonId: CHILD,
|
||||
personDisplayName: 'Parent',
|
||||
relatedPersonDisplayName: 'Child',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
}
|
||||
],
|
||||
selectedId: null,
|
||||
@@ -908,6 +955,8 @@ describe('StammbaumTree lineage highlight (#703)', () => {
|
||||
personDisplayName: string;
|
||||
relatedPersonDisplayName: string;
|
||||
relationType: 'PARENT_OF' | 'SPOUSE_OF';
|
||||
fromDatePrecision: 'UNKNOWN';
|
||||
toDatePrecision: 'UNKNOWN';
|
||||
};
|
||||
const edge = (
|
||||
personId: string,
|
||||
@@ -919,7 +968,9 @@ describe('StammbaumTree lineage highlight (#703)', () => {
|
||||
relatedPersonId,
|
||||
personDisplayName: '',
|
||||
relatedPersonDisplayName: '',
|
||||
relationType
|
||||
relationType,
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
});
|
||||
|
||||
const NODES = [
|
||||
@@ -1104,14 +1155,16 @@ describe('StammbaumTree keyboard tab order (#718)', () => {
|
||||
// year, then a deterministic id tie-break), not alphabetically — with no birth
|
||||
// years here Walter (id …a1) owns the run and Eugenie sits to his right. So the
|
||||
// deterministic visual order is Walter, Eugenie (top row) then Clara, Hans.
|
||||
const FAMILY_EDGES = [
|
||||
const FAMILY_EDGES: RelationshipDTO[] = [
|
||||
{
|
||||
id: 'sp',
|
||||
personId: WALTER,
|
||||
relatedPersonId: EUGENIE,
|
||||
personDisplayName: 'Walter',
|
||||
relatedPersonDisplayName: 'Eugenie',
|
||||
relationType: 'SPOUSE_OF'
|
||||
relationType: 'SPOUSE_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p1',
|
||||
@@ -1119,7 +1172,9 @@ describe('StammbaumTree keyboard tab order (#718)', () => {
|
||||
relatedPersonId: CLARA,
|
||||
personDisplayName: 'Walter',
|
||||
relatedPersonDisplayName: 'Clara',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
@@ -1127,7 +1182,9 @@ describe('StammbaumTree keyboard tab order (#718)', () => {
|
||||
relatedPersonId: CLARA,
|
||||
personDisplayName: 'Eugenie',
|
||||
relatedPersonDisplayName: 'Clara',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
@@ -1135,7 +1192,9 @@ describe('StammbaumTree keyboard tab order (#718)', () => {
|
||||
relatedPersonId: HANS,
|
||||
personDisplayName: 'Walter',
|
||||
relatedPersonDisplayName: 'Hans',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
@@ -1143,7 +1202,9 @@ describe('StammbaumTree keyboard tab order (#718)', () => {
|
||||
relatedPersonId: HANS,
|
||||
personDisplayName: 'Eugenie',
|
||||
relatedPersonDisplayName: 'Hans',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ function parentEdge(parentId: string, childId: string, id = parentId + childId):
|
||||
relatedPersonId: childId,
|
||||
personDisplayName: '',
|
||||
relatedPersonDisplayName: '',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,7 +55,9 @@ function spouseEdge(a: string, b: string, id = a + b): RelationshipDTO {
|
||||
relatedPersonId: b,
|
||||
personDisplayName: '',
|
||||
relatedPersonDisplayName: '',
|
||||
relationType: 'SPOUSE_OF'
|
||||
relationType: 'SPOUSE_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -220,7 +224,10 @@ describe('buildLayout — multi-spouse ordering (#361)', () => {
|
||||
fromYear: number | undefined,
|
||||
id = a + b
|
||||
): RelationshipDTO {
|
||||
return { ...spouseEdge(a, b, id), fromYear };
|
||||
return {
|
||||
...spouseEdge(a, b, id),
|
||||
...(fromYear != null ? { fromDate: `${fromYear}-01-01`, fromDatePrecision: 'YEAR' } : {})
|
||||
};
|
||||
}
|
||||
|
||||
it('multi_spouses_ordered_by_fromYear_then_displayName', () => {
|
||||
@@ -329,7 +336,7 @@ describe('buildLayout — multi-spouse ordering (#361)', () => {
|
||||
// fail fast instead so the maintainer either updates the test or
|
||||
// splits into a year-branch / name-branch pair.
|
||||
const spouseEdgesWithYear = fixtureEdges.filter(
|
||||
(e) => e.relationType === 'SPOUSE_OF' && e.fromYear != null
|
||||
(e) => e.relationType === 'SPOUSE_OF' && e.fromDate != null
|
||||
);
|
||||
expect(
|
||||
spouseEdgesWithYear,
|
||||
|
||||
@@ -21,7 +21,9 @@ function parent(p: string, c: string): RelationshipDTO {
|
||||
relatedPersonId: c,
|
||||
personDisplayName: '',
|
||||
relatedPersonDisplayName: '',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,7 +35,9 @@ function spouse(a: string, b: string, fromYear?: number): RelationshipDTO {
|
||||
personDisplayName: '',
|
||||
relatedPersonDisplayName: '',
|
||||
relationType: 'SPOUSE_OF',
|
||||
...(fromYear != null ? { fromYear } : {})
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN',
|
||||
...(fromYear != null ? { fromDate: `${fromYear}-01-01`, fromDatePrecision: 'YEAR' } : {})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,10 @@ export function buildFamilyForest(nodes: PersonNodeDTO[], edges: RelationshipDTO
|
||||
} else if (e.relationType === 'SPOUSE_OF') {
|
||||
addToSet(spouses, e.personId, e.relatedPersonId);
|
||||
addToSet(spouses, e.relatedPersonId, e.personId);
|
||||
spouseYear.set(pairKey(e.personId, e.relatedPersonId), e.fromYear ?? undefined);
|
||||
spouseYear.set(
|
||||
pairKey(e.personId, e.relatedPersonId),
|
||||
e.fromDate ? Number(e.fromDate.slice(0, 4)) : undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ function parentEdge(parentId: string, childId: string, id = parentId + childId):
|
||||
relatedPersonId: childId,
|
||||
personDisplayName: '',
|
||||
relatedPersonDisplayName: '',
|
||||
relationType: 'PARENT_OF'
|
||||
relationType: 'PARENT_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +26,9 @@ function spouseEdge(a: string, b: string, id = a + b): RelationshipDTO {
|
||||
relatedPersonId: b,
|
||||
personDisplayName: '',
|
||||
relatedPersonDisplayName: '',
|
||||
relationType: 'SPOUSE_OF'
|
||||
relationType: 'SPOUSE_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,7 +39,9 @@ function siblingEdge(a: string, b: string, id = a + b): RelationshipDTO {
|
||||
relatedPersonId: b,
|
||||
personDisplayName: '',
|
||||
relatedPersonDisplayName: '',
|
||||
relationType: 'SIBLING_OF'
|
||||
relationType: 'SIBLING_OF',
|
||||
fromDatePrecision: 'UNKNOWN',
|
||||
toDatePrecision: 'UNKNOWN'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user