From 17b95d4a731677aa026199bf6e4d3e599af68b58 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:12:11 +0100 Subject: [PATCH] fix: add an invitation only to a calendar that shows events in the account on screen --- .../calendar-invitation-banner.test.tsx | 27 +++++++++++++++++++ .../email/calendar-invitation-banner.tsx | 15 +++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/components/email/__tests__/calendar-invitation-banner.test.tsx b/components/email/__tests__/calendar-invitation-banner.test.tsx index f048e956a..71317f4c6 100644 --- a/components/email/__tests__/calendar-invitation-banner.test.tsx +++ b/components/email/__tests__/calendar-invitation-banner.test.tsx @@ -465,6 +465,33 @@ describe('CalendarInvitationBanner', () => { expect(screen.getByRole('heading', { name: 'Planning Session' })).toBeInTheDocument(); }); + // pim-p2-2: "Add to calendar" offered the account's task list, where the + // event would be stored but never shown, and other logins' calendars. + it('adds to the one calendar that shows events, never a task list or another login', async () => { + const before = mocks.calendarState.calendars; + mocks.calendarState.calendars = [ + { id: 'cal-1', name: 'Primary', color: '#2563eb', isDefault: true }, + { id: 'todo', name: 'To-do', color: '#16a34a', isDefault: false, isTasksOnly: true }, + { id: 'other', name: 'Other login', color: '#dc2626', isDefault: true, localAccountId: 'login-b' }, + ] as never; + try { + mocks.calendarState.importEvents.mockResolvedValue(1); + mocks.clientMock.parseCalendarEvents.mockResolvedValue([ + { uid: 'uid-tl', title: 'Target check', start: '2026-03-22T13:00:00Z', participants: { + attendee: { '@type': 'Participant', email: 'user@example.com', roles: { attendee: true }, participationStatus: 'needs-action' }, + } }, + ]); + render(); + await screen.findByRole('heading', { name: 'Target check' }); + fireEvent.click(screen.getByText('Add to calendar')); + await waitFor(() => expect(mocks.calendarState.importEvents).toHaveBeenCalledTimes(1)); + expect(JSON.stringify(mocks.calendarState.importEvents.mock.calls[0])).toContain('cal-1'); + expect(screen.queryByText('To-do')).toBeNull(); + } finally { + mocks.calendarState.calendars = before; + } + }); + it('does not show RSVP actions when the current user is the organizer', async () => { mocks.clientMock.parseCalendarEvents.mockResolvedValue([ { diff --git a/components/email/calendar-invitation-banner.tsx b/components/email/calendar-invitation-banner.tsx index c794eac84..a921ca733 100644 --- a/components/email/calendar-invitation-banner.tsx +++ b/components/email/calendar-invitation-banner.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { answerInvitation } from '@/lib/calendar-rsvp'; import { createPortal } from 'react-dom'; import { @@ -27,6 +27,7 @@ import { isDocumentRTL } from '@/i18n/direction'; import { useAuthStore } from '@/stores/auth-store'; import { useShallow } from "zustand/react/shallow"; import { useEmailStore } from '@/stores/email-store'; +import { eventTargets } from '@/lib/calendar-targets'; import { useCalendarStore } from '@/stores/calendar-store'; import { useSettingsStore } from '@/stores/settings-store'; import type { Email, CalendarEvent } from '@/lib/jmap/types'; @@ -455,7 +456,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp const timeFormat = useSettingsStore((s) => s.timeFormat); // The user's zone, as getEffectiveTimeZone resolves it. const userTimeZone = resolveTimeZone(useSettingsStore((s) => s.timeZone)); - const { calendars, supportsCalendar, importEvents, rsvpEvent, updateEvent, deleteEvent, events: storeEvents, setSelectedDate } = useCalendarStore(useShallow((state) => ({ + const { calendars: allCalendars, supportsCalendar, importEvents, rsvpEvent, updateEvent, deleteEvent, events: storeEvents, setSelectedDate } = useCalendarStore(useShallow((state) => ({ calendars: state.calendars, supportsCalendar: state.supportsCalendar, importEvents: state.importEvents, @@ -466,6 +467,16 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp setSelectedDate: state.setSelectedDate, }))); + // Where an invitation can go: the calendars of the account on screen that show + // in the grid. A task list would hide the event, and another login's calendar + // would put the meeting in the wrong account (pim-p2-2). + const activeLocalAccountId = useAuthStore((s) => s.activeAccountId); + const calendars = useMemo(() => { + const mine = allCalendars.filter((c) => !c.localAccountId || c.localAccountId === activeLocalAccountId); + const targets = eventTargets(mine); + return targets.length > 0 ? targets : mine; + }, [allCalendars, activeLocalAccountId]); + const [state, setState] = useState('loading'); const [parsedEvent, setParsedEvent] = useState | null>(null); const [rsvpStatus, setRsvpStatus] = useState(null);