mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 00:06:23 +00:00
feat: offer to remove a cancelled meeting from the calendar in the cancellation mail (bulwarkmail/webmail#1425)
This commit is contained in:
@@ -29,6 +29,7 @@ import type { Calendar, CalendarEvent, Participant } from '../types';
|
||||
import {
|
||||
createEvent,
|
||||
deleteEvents,
|
||||
findEventsByUid,
|
||||
getCalendars,
|
||||
getEvents,
|
||||
queryEvents,
|
||||
@@ -43,6 +44,7 @@ import { expandRecurringEvents } from '../../lib/recurrence-expansion';
|
||||
import { getEventStartDate } from '../../lib/calendar-utils';
|
||||
import { alertsToReminders, calendarDefaultAlerts, defaultAlertIdPrefix, remindersToDefaultAlerts } from '../../lib/calendar-alerts';
|
||||
import { getEffectiveAlerts } from '../../lib/calendar-alert-scheduler';
|
||||
import { removeCancelledMeeting } from '../../lib/calendar-cancellation';
|
||||
|
||||
const SERVER = process.env.BULWARK_LIVE_JMAP;
|
||||
const USER = process.env.BULWARK_LIVE_USER ?? 'usera@example.org';
|
||||
@@ -607,6 +609,89 @@ live('live Stalwart calendar', () => {
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
liveTwo('a cancelled meeting (#1425)', () => {
|
||||
const after = '2027-10-01T00:00:00.000Z';
|
||||
const before = '2027-10-31T00:00:00.000Z';
|
||||
const me = [USER];
|
||||
|
||||
/** Run `fn` as the second user, in the app's singleton. */
|
||||
async function asOther<T>(fn: () => Promise<T>): Promise<T> {
|
||||
(await singleton()).restoreSnapshot(other!.snapshot());
|
||||
useCalendarStore.getState().reset();
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
await connectSingleton();
|
||||
}
|
||||
}
|
||||
|
||||
function removeAsOther(cancel: Partial<CalendarEvent>) {
|
||||
return asOther(async () => {
|
||||
await useCalendarStore.getState().fetchCalendars();
|
||||
const s = useCalendarStore.getState();
|
||||
return removeCancelledMeeting(cancel, {
|
||||
findEventsByUid: (u) => findEventsByUid(u),
|
||||
deleteEvent: s.deleteEvent,
|
||||
updateEvent: s.updateEvent,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('takes one cancelled occurrence, then the cancelled series, out of the attendee\'s calendar', async () => {
|
||||
await connectSingleton();
|
||||
// Four Mondays from 4 October 2027, the second user invited.
|
||||
const invitation = editorSave(
|
||||
{ title: `${RUN} cancelled`, start: '2027-10-04T10:00:00', duration: 'PT1H', timeZone: 'Europe/Berlin' } as CalendarEvent,
|
||||
{ attendees: [{ name: 'Land', email: USER2! }] },
|
||||
me,
|
||||
);
|
||||
const created = await useCalendarStore.getState().createEvent({
|
||||
...seriesData('cancelled', '2027-10-04T10:00:00', 4),
|
||||
participants: invitation.updates.participants,
|
||||
organizerCalendarAddress: invitation.updates.organizerCalendarAddress,
|
||||
}, calendarId, { sendSchedulingMessages: true });
|
||||
const uid = created.uid!;
|
||||
const copies = await waitFor(() => seriesByUid(other!, uid), (list) => list.length > 0);
|
||||
expect(copies, 'the invitation reached the second user').toHaveLength(1);
|
||||
|
||||
// The organizer cancels the second Monday. The attendee's 1.0 keeps
|
||||
// it, marked cancelled; 0.16 takes it out itself.
|
||||
const occurrences = await loadOccurrences(uid, after, before);
|
||||
const second = occurrences.find((e) => (e.recurrenceId ?? '').startsWith('2027-10-11'))!;
|
||||
expect(await deleteOccurrence('this', { event: second, userEmails: me }, storeActions())).toBe(true);
|
||||
const key = '2027-10-11T10:00:00';
|
||||
const marked = await waitFor(() => seriesByUid(other!, uid), (list) =>
|
||||
/cancelled|excluded/.test(JSON.stringify(list[0]?.recurrenceOverrides?.[key] ?? null)));
|
||||
const entry = marked[0].recurrenceOverrides?.[key] as { sequence?: number; excluded?: boolean } | undefined;
|
||||
expect(JSON.stringify(entry ?? null)).toMatch(/cancelled|excluded/);
|
||||
|
||||
const one = await removeAsOther({
|
||||
uid,
|
||||
recurrenceId: key,
|
||||
recurrenceIdTimeZone: 'Europe/Berlin',
|
||||
sequence: entry?.sequence ?? 0,
|
||||
});
|
||||
expect(one.kind).toBe(entry?.excluded ? 'none' : 'exclude');
|
||||
const [excluded] = await seriesByUid(other!, uid);
|
||||
expect(excluded.recurrenceOverrides?.[key]).toEqual({ excluded: true });
|
||||
expect(excluded.status).not.toBe('cancelled');
|
||||
|
||||
// An older cancellation of the whole meeting is not applied.
|
||||
const outdated = await removeAsOther({ uid, sequence: -1 });
|
||||
expect(outdated.kind).toBe('outdated');
|
||||
|
||||
// The organizer cancels the series.
|
||||
const left = await loadOccurrences(uid, after, before);
|
||||
expect(await deleteOccurrence('all', { event: left[0], userEmails: me }, storeActions())).toBe(true);
|
||||
// Both servers keep it, marked cancelled.
|
||||
const marked2 = await waitFor(() => seriesByUid(other!, uid), (list) => list[0]?.status === 'cancelled');
|
||||
expect(marked2[0]?.status).toBe('cancelled');
|
||||
const all = await removeAsOther({ uid, sequence: marked2[0].sequence ?? 0 });
|
||||
expect(all.kind).toBe('delete');
|
||||
expect(await seriesByUid(other!, uid)).toEqual([]);
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
liveTwo('shares', () => {
|
||||
let mailboxId = '';
|
||||
let sharedCalendarId = '';
|
||||
|
||||
Reference in New Issue
Block a user