feat(timeline): gate the EventPill edit pencil behind canWrite

Thread a gate-closed canWrite prop through TimelineView -> YearBand ->
EventPill and the undated bucket so a Reader never sees a dead-end edit
link. canEdit now also requires canWrite; the default false keeps an
un-threaded caller closed. The real boundary stays the #781 route guard
plus the backend permission -- this only hides the link.

Refs #842
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-15 08:03:53 +02:00
parent ec0e4dfa45
commit cd238285ae
5 changed files with 92 additions and 15 deletions

View File

@@ -51,8 +51,9 @@ describe('EventPill', () => {
expect(srOnly?.textContent).toBe('Geburt');
});
it('shows an edit affordance for a curated PERSONAL event with an eventId (REQ-008)', () => {
it('shows an edit affordance for a curated PERSONAL event when canWrite is true (REQ-005)', () => {
render(EventPill, {
canWrite: true,
entry: makeEntry({
kind: 'EVENT',
derived: false,
@@ -66,11 +67,45 @@ describe('EventPill', () => {
});
const edit = document.querySelector('[data-testid="event-edit"]') as HTMLAnchorElement | null;
expect(edit).not.toBeNull();
expect(edit?.getAttribute('href')).toContain(EVENT_ID);
expect(edit?.getAttribute('href')).toBe(`/zeitstrahl/events/${EVENT_ID}/edit`);
});
it('shows no edit affordance when eventId is null (REQ-008)', () => {
it('renders no edit affordance for a curated PERSONAL event when canWrite is false (REQ-007)', () => {
render(EventPill, {
canWrite: false,
entry: makeEntry({
kind: 'EVENT',
derived: false,
type: 'PERSONAL',
eventId: EVENT_ID,
title: 'Auswanderung',
senderName: '',
receiverName: '',
documentId: undefined
})
});
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(EventPill, {
entry: makeEntry({
kind: 'EVENT',
derived: false,
type: 'PERSONAL',
eventId: EVENT_ID,
title: 'Auswanderung',
senderName: '',
receiverName: '',
documentId: undefined
})
});
expect(document.querySelector('[data-testid="event-edit"]')).toBeNull();
});
it('shows no edit affordance when eventId is null even with canWrite (REQ-008)', () => {
render(EventPill, {
canWrite: true,
entry: makeEntry({
kind: 'EVENT',
derived: false,
@@ -85,8 +120,8 @@ describe('EventPill', () => {
expect(document.querySelector('[data-testid="event-edit"]')).toBeNull();
});
it('shows no edit affordance for a derived event (REQ-008)', () => {
render(EventPill, { entry: derived('MARRIAGE', 'Heirat') });
it('shows no edit affordance for a derived event even with canWrite (REQ-008)', () => {
render(EventPill, { canWrite: true, entry: derived('MARRIAGE', 'Heirat') });
expect(document.querySelector('[data-testid="event-edit"]')).toBeNull();
});