fix(timeline): keep HISTORICAL events out of inline clustering
buildEventLookup keyed on `kind === 'EVENT' && eventId` with no type check, so a HISTORICAL curated event with ≥1 linked letter entered the lookup and rendered as a mint EventCluster card — silently downgrading from the full-width WorldBand that #779 REQ-009 mandates ("world-bands render exactly as before"). The lookup now excludes `type === 'HISTORICAL'`, so a world event always keeps its WorldBand and its letters stay loose chronological. Closes the spec gap pinned as REQ-014. Fixes review finding #2. Refs #850 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -207,5 +207,5 @@
|
|||||||
| REQ-011 | wrapping header keeps #842 add-event CTA + #780 filter trigger; meta-line drops the grouping segment | #850 | inline-event-clustering | `frontend/src/routes/zeitstrahl/+page.svelte` | `page.svelte.spec.ts#renders the meta sub-line with range and counts, no grouping segment`, `#drops the letters segment instead of showing "0 Briefe"` (no "Gruppierung") | Done |
|
| REQ-011 | wrapping header keeps #842 add-event CTA + #780 filter trigger; meta-line drops the grouping segment | #850 | inline-event-clustering | `frontend/src/routes/zeitstrahl/+page.svelte` | `page.svelte.spec.ts#renders the meta sub-line with range and counts, no grouping segment`, `#drops the letters segment instead of showing "0 Briefe"` (no "Gruppierung") | Done |
|
||||||
| REQ-012 | show-more/less labels are new Paraglide keys in de/en/es; unused #827 grouping/Thema keys removed | #850 | inline-event-clustering | `frontend/messages/{de,en,es}.json`, `frontend/src/lib/timeline/EventCluster.svelte` | `messages.spec.ts#zeitstrahl visual-fidelity keys are present in all locales` (now asserts `timeline_bucket_show_more`/`_less`; `timeline_grouping_date` removed) | Done |
|
| REQ-012 | show-more/less labels are new Paraglide keys in de/en/es; unused #827 grouping/Thema keys removed | #850 | inline-event-clustering | `frontend/messages/{de,en,es}.json`, `frontend/src/lib/timeline/EventCluster.svelte` | `messages.spec.ts#zeitstrahl visual-fidelity keys are present in all locales` (now asserts `timeline_bucket_show_more`/`_less`; `timeline_grouping_date` removed) | Done |
|
||||||
| REQ-013 | `GET /api/timeline` failure → existing localized error state via `getErrorMessage(code)` (unchanged #779) | #850 | inline-event-clustering | `frontend/src/routes/zeitstrahl/+page.svelte` (unchanged error path) | covered by #779 `zeitstrahl` error-state tests (regression — no change) | Done |
|
| REQ-013 | `GET /api/timeline` failure → existing localized error state via `getErrorMessage(code)` (unchanged #779) | #850 | inline-event-clustering | `frontend/src/routes/zeitstrahl/+page.svelte` (unchanged error path) | covered by #779 `zeitstrahl` error-state tests (regression — no change) | Done |
|
||||||
| REQ-014 | HISTORICAL curated event with ≥1 linked letter keeps its full-width WorldBand — never clusters into a card (preserves #779 REQ-009); its letters stay loose | #850 | inline-event-clustering | `frontend/src/lib/timeline/eventClustering.ts` (`buildEventLookup` excludes HISTORICAL) | `eventClustering.spec.ts#excludes a HISTORICAL event from the lookup`; `YearBand.svelte.spec.ts#renders a HISTORICAL event with a same-year linked letter as a WorldBand, letter loose` | Planned |
|
| REQ-014 | HISTORICAL curated event with ≥1 linked letter keeps its full-width WorldBand — never clusters into a card (preserves #779 REQ-009); its letters stay loose | #850 | inline-event-clustering | `frontend/src/lib/timeline/eventClustering.ts` (`buildEventLookup` excludes HISTORICAL) | `eventClustering.spec.ts#excludes a HISTORICAL event so its letters stay loose, keeping its WorldBand (REQ-014)`; `TimelineView.svelte.spec.ts#keeps a HISTORICAL event a WorldBand with a same-year linked letter — never clusters (REQ-014)` | Done |
|
||||||
| REQ-015 | a cross-year ✉ card is placed at its earliest linked letter's chronological position in the band — never appended after later-dated loose letters | #850 | inline-event-clustering | `frontend/src/lib/timeline/YearBand.svelte` | `YearBand.svelte.spec.ts#interleaves a cross-year card before a later-dated loose letter in the same band` | Planned |
|
| REQ-015 | a cross-year ✉ card is placed at its earliest linked letter's chronological position in the band — never appended after later-dated loose letters | #850 | inline-event-clustering | `frontend/src/lib/timeline/YearBand.svelte` | `YearBand.svelte.spec.ts#interleaves a cross-year card before a later-dated loose letter in the same band` | Planned |
|
||||||
|
|||||||
@@ -371,4 +371,37 @@ describe('TimelineView', () => {
|
|||||||
const titles = (document.body.textContent?.match(/Ein gewaltiger Stadtbrand/g) ?? []).length;
|
const titles = (document.body.textContent?.match(/Ein gewaltiger Stadtbrand/g) ?? []).length;
|
||||||
expect(titles).toBe(1);
|
expect(titles).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps a HISTORICAL event a WorldBand with a same-year linked letter — never clusters (REQ-014)', () => {
|
||||||
|
const evId = 'dddddddd-dddd-dddd-dddd-dddddddddddd';
|
||||||
|
const world = makeEntry({
|
||||||
|
kind: 'EVENT',
|
||||||
|
type: 'HISTORICAL',
|
||||||
|
derived: false,
|
||||||
|
eventId: evId,
|
||||||
|
eventDate: '1916-07-01',
|
||||||
|
precision: 'DAY',
|
||||||
|
title: 'Schlacht an der Somme',
|
||||||
|
senderName: '',
|
||||||
|
receiverName: '',
|
||||||
|
documentId: undefined
|
||||||
|
});
|
||||||
|
const letter = makeEntry({
|
||||||
|
eventDate: '1916-05-10',
|
||||||
|
documentId: 'doc-world-linked',
|
||||||
|
title: 'Brief von der Front',
|
||||||
|
linkedEventId: evId
|
||||||
|
});
|
||||||
|
render(TimelineView, {
|
||||||
|
timeline: makeTimelineDTO({ years: [makeYear(1916, [world, letter])] })
|
||||||
|
});
|
||||||
|
// the world event stays a full-width band — no contained event card
|
||||||
|
expect(document.querySelector('[data-testid="event-card"]')).toBeNull();
|
||||||
|
expect(document.querySelector('a.lcard.ev')).toBeNull();
|
||||||
|
// the linked letter renders loose on the spine, not inside a card
|
||||||
|
expect(document.querySelector('.letter-row')).not.toBeNull();
|
||||||
|
// and the band keeps its WorldBand "· historisch" register
|
||||||
|
expect(document.body.textContent).toContain(m.timeline_layer_historical_suffix());
|
||||||
|
expect(document.body.textContent).toContain('Schlacht an der Somme');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,6 +44,18 @@ describe('eventClustering — buildEventLookup', () => {
|
|||||||
};
|
};
|
||||||
expect(buildEventLookup(timeline).size).toBe(0);
|
expect(buildEventLookup(timeline).size).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('excludes a HISTORICAL event so its letters stay loose, keeping its WorldBand (REQ-014)', () => {
|
||||||
|
const timeline: TimelineDTO = {
|
||||||
|
years: [
|
||||||
|
{ year: 1916, entries: [makeEvent({ eventId: EV_A, type: 'HISTORICAL', title: 'Somme' })] }
|
||||||
|
],
|
||||||
|
undated: []
|
||||||
|
};
|
||||||
|
const lookup = buildEventLookup(timeline);
|
||||||
|
expect(lookup.has(EV_A)).toBe(false);
|
||||||
|
expect(lookup.size).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('eventClustering — splitYearLetters', () => {
|
describe('eventClustering — splitYearLetters', () => {
|
||||||
|
|||||||
@@ -25,14 +25,18 @@ export interface SplitLetters {
|
|||||||
* Maps each curated event present in the (already layer-filtered) timeline to its title. These
|
* Maps each curated event present in the (already layer-filtered) timeline to its title. These
|
||||||
* are the only events a letter may cluster under — a letter whose `linkedEventId` is absent here
|
* are the only events a letter may cluster under — a letter whose `linkedEventId` is absent here
|
||||||
* links to an event the #780 layer filter removed, so it falls back to a loose chronological
|
* links to an event the #780 layer filter removed, so it falls back to a loose chronological
|
||||||
* letter (filter-then-cluster, REQ-008). Curated events carry an `eventId`; derived life-events
|
* letter (filter-then-cluster, REQ-008). Curated PERSONAL events carry an `eventId`; derived
|
||||||
* and letters do not, so they never enter the lookup.
|
* life-events and letters do not, so they never enter the lookup. HISTORICAL events are excluded
|
||||||
|
* too: a world event always keeps its full-width WorldBand and never clusters, even with linked
|
||||||
|
* letters (REQ-014) — those letters stay loose.
|
||||||
*/
|
*/
|
||||||
export function buildEventLookup(timeline: TimelineDTO): Map<string, string> {
|
export function buildEventLookup(timeline: TimelineDTO): Map<string, string> {
|
||||||
const lookup = new Map<string, string>();
|
const lookup = new Map<string, string>();
|
||||||
const collect = (entries: TimelineEntryDTO[]) => {
|
const collect = (entries: TimelineEntryDTO[]) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.kind === 'EVENT' && entry.eventId) lookup.set(entry.eventId, entry.title ?? '');
|
if (entry.kind === 'EVENT' && entry.eventId && entry.type !== 'HISTORICAL') {
|
||||||
|
lookup.set(entry.eventId, entry.title ?? '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
for (const band of timeline.years) collect(band.entries);
|
for (const band of timeline.years) collect(band.entries);
|
||||||
|
|||||||
Reference in New Issue
Block a user