feat(timeline): add a WRITE_ALL-gated edit pencil to WorldBand
A curated HISTORICAL band had no edit affordance at all. Mirror the EventPill pencil inline at the end of the band (data-testid="event-edit", /edit href, aria-hidden glyph + sr-only Bearbeiten), gated on canWrite && eventId != null. Thread canWrite to WorldBand through both the year-band path and the undated bucket. Refs #842 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -79,7 +79,7 @@ const isEmpty = $derived(timeline.years.length === 0 && timeline.undated.length
|
|||||||
<li>
|
<li>
|
||||||
{#if entry.kind === 'EVENT'}
|
{#if entry.kind === 'EVENT'}
|
||||||
{#if entry.type === 'HISTORICAL'}
|
{#if entry.type === 'HISTORICAL'}
|
||||||
<WorldBand entry={entry} />
|
<WorldBand entry={entry} canWrite={canWrite} />
|
||||||
{:else}
|
{:else}
|
||||||
<EventPill entry={entry} canWrite={canWrite} />
|
<EventPill entry={entry} canWrite={canWrite} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -312,4 +312,33 @@ describe('TimelineView', () => {
|
|||||||
render(TimelineView, { canWrite: false, timeline: bothPaths() });
|
render(TimelineView, { canWrite: false, timeline: bothPaths() });
|
||||||
expect(document.querySelectorAll('[data-testid="event-edit"]')).toHaveLength(0);
|
expect(document.querySelectorAll('[data-testid="event-edit"]')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('threads canWrite to a curated HISTORICAL world band in both paths (REQ-006/009)', () => {
|
||||||
|
const world = (eventId: string, title: string) =>
|
||||||
|
makeEntry({
|
||||||
|
kind: 'EVENT',
|
||||||
|
type: 'HISTORICAL',
|
||||||
|
derived: false,
|
||||||
|
eventId,
|
||||||
|
precision: 'YEAR',
|
||||||
|
eventDate: '1929-01-01',
|
||||||
|
eventDateEnd: undefined,
|
||||||
|
title,
|
||||||
|
senderName: '',
|
||||||
|
receiverName: '',
|
||||||
|
documentId: undefined
|
||||||
|
});
|
||||||
|
render(TimelineView, {
|
||||||
|
canWrite: true,
|
||||||
|
timeline: makeTimelineDTO({
|
||||||
|
years: [makeYear(1929, [world('wb', 'Weltwirtschaftskrise')])],
|
||||||
|
undated: [world('wu', 'Unbekanntes Weltereignis')]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const hrefs = Array.from(document.querySelectorAll('[data-testid="event-edit"]')).map((a) =>
|
||||||
|
a.getAttribute('href')
|
||||||
|
);
|
||||||
|
expect(hrefs).toContain('/zeitstrahl/events/wb/edit');
|
||||||
|
expect(hrefs).toContain('/zeitstrahl/events/wu/edit');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ type TimelineEntryDTO = components['schemas']['TimelineEntryDTO'];
|
|||||||
* (REQ-009). A RANGE carries a visible span pill ("1914–1918") with a Zeitraum
|
* (REQ-009). A RANGE carries a visible span pill ("1914–1918") with a Zeitraum
|
||||||
* aria-label; a RANGE with no end degrades to the start year, no pill (REQ-010).
|
* aria-label; a RANGE with no end degrades to the start year, no pill (REQ-010).
|
||||||
* The glyph is a redundant non-color cue with an sr-only label (REQ-018); text
|
* The glyph is a redundant non-color cue with an sr-only label (REQ-018); text
|
||||||
* uses text-ink-2 to stay AA in both themes (REQ-019).
|
* uses text-ink-2 to stay AA in both themes (REQ-019). A curator (`canWrite`,
|
||||||
|
* gate-closed by default) gets an inline edit pencil for a curated event with an
|
||||||
|
* eventId — #842 REQ-006/007/008; UX gate only, the #781 route guard is the boundary.
|
||||||
*/
|
*/
|
||||||
let { entry }: { entry: TimelineEntryDTO } = $props();
|
let { entry, canWrite = false }: { entry: TimelineEntryDTO; canWrite?: boolean } = $props();
|
||||||
|
|
||||||
const config = $derived(getAccentConfig(entry));
|
const config = $derived(getAccentConfig(entry));
|
||||||
const dateLabel = $derived(timelineDateLabel(entry.eventDate, entry.precision, entry.eventDateEnd));
|
const dateLabel = $derived(timelineDateLabel(entry.eventDate, entry.precision, entry.eventDateEnd));
|
||||||
@@ -24,6 +26,9 @@ const dateText = $derived(showSpan ? null : entry.precision === 'RANGE' ? fromYe
|
|||||||
// Every WorldBand is a HISTORICAL band, so the visible "historisch" register
|
// Every WorldBand is a HISTORICAL band, so the visible "historisch" register
|
||||||
// always trails the subtitle as plain text — never a second pill (REQ-009).
|
// always trails the subtitle as plain text — never a second pill (REQ-009).
|
||||||
const historical = $derived(m.timeline_layer_historical_suffix());
|
const historical = $derived(m.timeline_layer_historical_suffix());
|
||||||
|
// A HISTORICAL event is never derived, so the edit gate is just the curator
|
||||||
|
// flag plus a real eventId (#842 REQ-006/008).
|
||||||
|
const canEdit = $derived(canWrite && entry.eventId != null);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="my-3 border-y border-line bg-canvas px-4 py-2 text-center">
|
<div class="my-3 border-y border-line bg-canvas px-4 py-2 text-center">
|
||||||
@@ -46,4 +51,14 @@ const historical = $derived(m.timeline_layer_historical_suffix());
|
|||||||
<!-- Single trailing "· historisch" register, after the title and any
|
<!-- Single trailing "· historisch" register, after the title and any
|
||||||
span pill / date — one render site, consistent separator (REQ-009). -->
|
span pill / date — one render site, consistent separator (REQ-009). -->
|
||||||
<span class="ml-2 font-sans text-xs text-ink-3">· {historical}</span>
|
<span class="ml-2 font-sans text-xs text-ink-3">· {historical}</span>
|
||||||
|
{#if canEdit}
|
||||||
|
<a
|
||||||
|
data-testid="event-edit"
|
||||||
|
href="/zeitstrahl/events/{entry.eventId}/edit"
|
||||||
|
class="ml-2 rounded-sm px-1 font-sans text-xs text-ink-3 hover:text-brand-navy focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-navy"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">✎</span>
|
||||||
|
<span class="sr-only">{m.btn_edit()}</span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,4 +73,35 @@ describe('WorldBand', () => {
|
|||||||
expect(pill?.textContent).not.toContain(m.timeline_layer_historical_suffix());
|
expect(pill?.textContent).not.toContain(m.timeline_layer_historical_suffix());
|
||||||
expect(document.body.textContent).toContain(m.timeline_layer_historical_suffix());
|
expect(document.body.textContent).toContain(m.timeline_layer_historical_suffix());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const HIST_EVENT_ID = '44444444-4444-4444-4444-444444444444';
|
||||||
|
|
||||||
|
it('shows an edit affordance for a curated HISTORICAL event when canWrite is true (REQ-006)', () => {
|
||||||
|
render(WorldBand, { canWrite: true, entry: historical({ eventId: HIST_EVENT_ID }) });
|
||||||
|
const edit = document.querySelector('[data-testid="event-edit"]') as HTMLAnchorElement | null;
|
||||||
|
expect(edit).not.toBeNull();
|
||||||
|
expect(edit?.getAttribute('href')).toBe(`/zeitstrahl/events/${HIST_EVENT_ID}/edit`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mirrors the EventPill pencil: aria-hidden ✎ glyph + sr-only Bearbeiten label (REQ-006)', () => {
|
||||||
|
render(WorldBand, { canWrite: true, entry: historical({ eventId: HIST_EVENT_ID }) });
|
||||||
|
const edit = document.querySelector('[data-testid="event-edit"]');
|
||||||
|
expect(edit?.querySelector('[aria-hidden="true"]')?.textContent).toBe('✎');
|
||||||
|
expect(edit?.querySelector('.sr-only')?.textContent).toBe(m.btn_edit());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders no edit affordance for a curated HISTORICAL event when canWrite is false (REQ-007)', () => {
|
||||||
|
render(WorldBand, { canWrite: false, entry: historical({ eventId: HIST_EVENT_ID }) });
|
||||||
|
expect(document.querySelector('[data-testid="event-edit"]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders no edit affordance when the canWrite prop is omitted (gate-closed default) (REQ-007)', () => {
|
||||||
|
render(WorldBand, { entry: historical({ eventId: HIST_EVENT_ID }) });
|
||||||
|
expect(document.querySelector('[data-testid="event-edit"]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows no edit affordance when eventId is null even with canWrite (REQ-008)', () => {
|
||||||
|
render(WorldBand, { canWrite: true, entry: historical({ eventId: undefined }) });
|
||||||
|
expect(document.querySelector('[data-testid="event-edit"]')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const rows = $derived.by<Row[]>(() => {
|
|||||||
{#each rows as row (row.t === 'strip' ? `strip-${year.year}` : entryKey(row.entry))}
|
{#each rows as row (row.t === 'strip' ? `strip-${year.year}` : entryKey(row.entry))}
|
||||||
{#if row.t === 'event'}
|
{#if row.t === 'event'}
|
||||||
{#if row.entry.type === 'HISTORICAL'}
|
{#if row.entry.type === 'HISTORICAL'}
|
||||||
<WorldBand entry={row.entry} />
|
<WorldBand entry={row.entry} canWrite={canWrite} />
|
||||||
{:else}
|
{:else}
|
||||||
<EventPill entry={row.entry} canWrite={canWrite} />
|
<EventPill entry={row.entry} canWrite={canWrite} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user