mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
fix: put back an event moved in place to another account's calendar #34
An app that moves an event by changing its calendar in place can move it into a calendar of another account on the device. A patch cannot move an event between accounts, so the move was dropped; with nothing else changed, the row was then marked clean where it stood. The phone showed the event in the other account's calendar while the server kept it in the old one, until the next server change moved it back. The row now goes back to its calendar at once, rewritten from the last server version. Changes made together with the move still upload, and their accepted write puts the row back as before. Checked with a run through the engine and the fake server with a shared account: the event is back in its calendar and clean, and the server and the other account are untouched.
This commit is contained in:
@@ -131,6 +131,41 @@ describe('calendar device edits through the engine', () => {
|
||||
expect(h.events().every((e) => Number(e[Events.DIRTY] ?? 0) === 0)).toBe(true);
|
||||
});
|
||||
|
||||
it('puts an event an app moved in place to a calendar of another account back into its calendar', async () => {
|
||||
const h = real({ accounts: 'withShared' });
|
||||
const team = String(h.server.all('Calendar', 'team')[0].id);
|
||||
h.prefs.calendarSelection[`team/${team}`] = true;
|
||||
const id = h.server.addEvent('a', {
|
||||
uid: 'lunch-uid',
|
||||
title: 'Lunch',
|
||||
start: '2026-10-06T12:00:00',
|
||||
duration: 'PT1H',
|
||||
timeZone: 'Europe/Berlin',
|
||||
calendarIds: { [h.calendar]: true },
|
||||
});
|
||||
await h.run(CALENDAR_AUTHORITY);
|
||||
const calendarRow = (key: string) => Number(h.device.rows('calendars').find((c) => c._sync_id === key)!._id);
|
||||
const row = () => h.events().find((e) => e[Events._SYNC_ID] === `a/${id}`)!;
|
||||
expect(row()[Events.CALENDAR_ID]).toBe(calendarRow(`a/${h.calendar}`));
|
||||
|
||||
// An app moves it by changing CALENDAR_ID in place (a move across JMAP accounts can't be a patch).
|
||||
h.device.user.updateEvent(Number(row()._id), { [Events.CALENDAR_ID]: calendarRow(`team/${team}`) });
|
||||
const report = await h.run(CALENDAR_AUTHORITY);
|
||||
|
||||
expect(row()).toMatchObject({ [Events.CALENDAR_ID]: calendarRow(`a/${h.calendar}`), [Events.DIRTY]: 0 });
|
||||
expect(h.server.get('CalendarEvent', 'a', id)!.calendarIds).toEqual({ [h.calendar]: true });
|
||||
expect(h.server.all('CalendarEvent', 'team')).toEqual([]);
|
||||
// Nothing was sent; the change counts as skipped (a report entry needs the engine to report reverts).
|
||||
expect(report).toMatchObject({ outcome: 'ok', stats: { uploaded: { created: 0, updated: 0, deleted: 0 }, skipped: 1 } });
|
||||
|
||||
// Together with another edit: that one uploads, and the row goes back all the same.
|
||||
h.device.user.updateEvent(Number(row()._id), { [Events.CALENDAR_ID]: calendarRow(`team/${team}`), [Events.TITLE]: 'Team lunch' });
|
||||
expect(await h.run(CALENDAR_AUTHORITY)).toMatchObject({ outcome: 'ok', stats: { uploaded: { updated: 1 } } });
|
||||
expect(h.server.get('CalendarEvent', 'a', id)).toMatchObject({ title: 'Team lunch', calendarIds: { [h.calendar]: true } });
|
||||
expect(row()).toMatchObject({ [Events.CALENDAR_ID]: calendarRow(`a/${h.calendar}`), [Events.TITLE]: 'Team lunch', [Events.DIRTY]: 0 });
|
||||
expect(h.server.all('CalendarEvent', 'team')).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps a description and location cleared on one occurrence cleared', async () => {
|
||||
const h = real();
|
||||
const id = h.server.addEvent('a', {
|
||||
|
||||
@@ -316,6 +316,13 @@ function planUpload(local: LocalEvent, ctx: CalendarContext): Plan {
|
||||
}
|
||||
if (!keys.length) {
|
||||
if (computation.dropped.some((u) => u === 'timing' || u === 'rule')) return { kind: 'skip', reason: SKIP.ruleNotRepresentable };
|
||||
// A move into a calendar of another JMAP account is no patch (only new rows are created across accounts):
|
||||
// the row goes back to its calendar instead of staying there unsynced. With other changes, their accepted
|
||||
// write puts it back.
|
||||
if (computation.dropped.includes('calendar')) {
|
||||
const ops = revertGroup(local, ctx);
|
||||
if (ops) return { kind: 'revert', ops };
|
||||
}
|
||||
return { kind: 'clean', ops: cleanGroup(local, computation) };
|
||||
}
|
||||
const action: UploadAction<CalendarEventWire> = { kind: 'update', id: ref.id, patch: computation.patch };
|
||||
|
||||
Reference in New Issue
Block a user