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-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-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 |
|
||||
|
||||
@@ -371,4 +371,37 @@ describe('TimelineView', () => {
|
||||
const titles = (document.body.textContent?.match(/Ein gewaltiger Stadtbrand/g) ?? []).length;
|
||||
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);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -25,14 +25,18 @@ export interface SplitLetters {
|
||||
* 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
|
||||
* 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
|
||||
* and letters do not, so they never enter the lookup.
|
||||
* letter (filter-then-cluster, REQ-008). Curated PERSONAL events carry an `eventId`; derived
|
||||
* 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> {
|
||||
const lookup = new Map<string, string>();
|
||||
const collect = (entries: TimelineEntryDTO[]) => {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user