mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 00:07:01 +00:00
fix: move a task into or within a shared calendar (bulwarkmail/webmail#1204)
This commit is contained in:
@@ -783,6 +783,38 @@ live('live Stalwart calendar', () => {
|
||||
expect(list).toHaveLength(1);
|
||||
expect(list[0].recurrenceOverrides?.['2027-10-11T10:00:00']?.title).toBe(`${RUN} owner second`);
|
||||
}, 60_000);
|
||||
|
||||
it('moves a task into the shared calendar and back out of it (#1204)', async () => {
|
||||
expect(sharedCalendarId, 'the calendar shared above').toBeTruthy();
|
||||
await connectSingleton();
|
||||
const [[, made]] = await raw(client, [['CalendarEvent/set', {
|
||||
accountId: client.accountId,
|
||||
create: { t: { '@type': 'Task', title: `${RUN} moving task`, calendarIds: { [calendarId]: true }, due: '2027-11-03T10:00:00', timeZone: 'Europe/Berlin' } },
|
||||
}, '0']]);
|
||||
expect(made.created?.t?.id, JSON.stringify(made.notCreated)).toBeTruthy();
|
||||
const store = useCalendarStore.getState();
|
||||
await store.fetchCalendars();
|
||||
await useCalendarStore.getState().fetchTasks();
|
||||
const taskOf = () => useCalendarStore.getState().tasks.find((t) => t.title === `${RUN} moving task`);
|
||||
const shared = useCalendarStore.getState().calendars.find((c) => c.name === `${RUN} team`)!;
|
||||
const task = taskOf()!;
|
||||
expect(task.accountId).toBeFalsy();
|
||||
|
||||
await useCalendarStore.getState().updateTask(task.id, { calendarIds: { [shared.id]: true } });
|
||||
const inShared = (await raw(other!, [['CalendarEvent/query', { accountId: other!.accountId, filter: { inCalendar: sharedCalendarId } }, 'q'],
|
||||
['CalendarEvent/get', { accountId: other!.accountId, '#ids': { resultOf: 'q', name: 'CalendarEvent/query', path: '/ids' }, properties: ['id', 'uid', 'title', '@type'] }, 'g']]))
|
||||
.find((r) => r[2] === 'g')?.[1].list as Record<string, any>[];
|
||||
const moved = inShared.find((e) => e.title === `${RUN} moving task`);
|
||||
expect(moved?.uid).toBe(task.uid);
|
||||
expect((await raw(client, [['CalendarEvent/get', { accountId: client.accountId, ids: [task.id], properties: ['id'] }, '0']]))[0][1].notFound)
|
||||
.toEqual([task.id]);
|
||||
expect(taskOf()?.accountId).toBe(other!.accountId);
|
||||
|
||||
// And back into the user's own calendar.
|
||||
await useCalendarStore.getState().updateTask(taskOf()!.id, { calendarIds: { [calendarId]: true } });
|
||||
expect(taskOf()?.accountId).toBeFalsy();
|
||||
await removeRunEvents(client);
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
describe('a new calendar', () => {
|
||||
|
||||
@@ -656,6 +656,49 @@ describe('calendar-store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTask in shared calendars (#1204)', () => {
|
||||
const calendars = [
|
||||
{ id: 'own', name: 'Own' },
|
||||
{ id: 'acc-2:shared', originalId: 'shared', accountId: 'acc-2', isShared: true, name: 'Team' },
|
||||
{ id: 'acc-2:other', originalId: 'other', accountId: 'acc-2', isShared: true, name: 'Team 2' },
|
||||
] as any[];
|
||||
|
||||
it('moves a task within a shared account under the raw calendar id', async () => {
|
||||
useCalendarStore.setState({
|
||||
calendars,
|
||||
tasks: [{ id: 'acc-2:t1', originalId: 't1', accountId: 'acc-2', calendarIds: { 'acc-2:shared': true } } as any],
|
||||
});
|
||||
mockUpdateEvent.mockResolvedValue(undefined);
|
||||
await useCalendarStore.getState().updateTask('acc-2:t1', { calendarIds: { 'acc-2:other': true } });
|
||||
expect(mockUpdateEvent).toHaveBeenCalledWith('t1', { calendarIds: { other: true } }, undefined, 'acc-2');
|
||||
expect(mockCreateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('moves an own task into a shared calendar by making it there and removing the original', async () => {
|
||||
useCalendarStore.setState({
|
||||
calendars,
|
||||
tasks: [{ id: 't1', calendarIds: { own: true }, title: 'Pay' } as any],
|
||||
});
|
||||
mockGetEvents.mockResolvedValue([{
|
||||
id: 't1', '@type': 'Task', uid: 'u-1', title: 'Pay', calendarIds: { own: true }, sequence: 2, created: 'x',
|
||||
}]);
|
||||
mockCreateEvent.mockResolvedValue({ id: 'n1' });
|
||||
mockDeleteEvents.mockResolvedValue(undefined);
|
||||
await useCalendarStore.getState().updateTask('t1', { calendarIds: { 'acc-2:shared': true }, title: 'Pay rent' });
|
||||
|
||||
expect(mockUpdateEvent).not.toHaveBeenCalled();
|
||||
const [created, calendarId, , account] = mockCreateEvent.mock.calls[0];
|
||||
expect(calendarId).toBe('shared');
|
||||
expect(account).toBe('acc-2');
|
||||
expect(created).toMatchObject({ '@type': 'Task', uid: 'u-1', title: 'Pay rent' });
|
||||
expect(created).not.toHaveProperty('id');
|
||||
expect(created).not.toHaveProperty('sequence');
|
||||
expect(mockDeleteEvents).toHaveBeenCalledWith(['t1'], undefined, 'acc-1');
|
||||
// Created first: a failed create leaves the task where it was.
|
||||
expect(mockCreateEvent.mock.invocationCallOrder[0]).toBeLessThan(mockDeleteEvents.mock.invocationCallOrder[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteEvent', () => {
|
||||
it('should remove event from list', async () => {
|
||||
useCalendarStore.setState({
|
||||
|
||||
@@ -1540,7 +1540,46 @@ export const useCalendarStore = create<CalendarState>()(
|
||||
const scope = writeScope();
|
||||
const task = get().tasks.find((t) => t.id === id);
|
||||
const realId = task?.originalId || id;
|
||||
await apiUpdateEvent(realId, changes, undefined, writeAccount(scope, task?.accountId));
|
||||
const sourceAccount = task?.accountId ?? undefined;
|
||||
// Store calendar ids (namespaced for a shared account) back to the raw
|
||||
// ids of the account that owns the calendar, which may be another one
|
||||
// than the task's (webmail #1204).
|
||||
const patch: Partial<CalendarEvent> = { ...changes };
|
||||
let destAccount = sourceAccount;
|
||||
if (changes.calendarIds) {
|
||||
const remapped: Record<string, boolean> = {};
|
||||
for (const [calId, on] of Object.entries(changes.calendarIds)) {
|
||||
const cal = get().calendars.find((c) => c.id === calId);
|
||||
remapped[cal?.originalId || calId] = on;
|
||||
if (on && cal) destAccount = cal.accountId ?? undefined;
|
||||
}
|
||||
patch.calendarIds = remapped;
|
||||
}
|
||||
if (destAccount !== sourceAccount) {
|
||||
// JMAP has no move across accounts: create the task in the calendar's
|
||||
// account, then destroy the original. Creating first means a failed
|
||||
// create leaves the task where it was.
|
||||
const [original] = await fetchEvents([realId], writeAccount(scope, sourceAccount));
|
||||
if (!original) throw new Error('Task not found');
|
||||
assertInScope(scope);
|
||||
const {
|
||||
id: _id, created: _created, updated: _updated, sequence: _sequence, isOrigin: _isOrigin,
|
||||
utcStart: _utcStart, utcEnd: _utcEnd, calendarIds: _calendarIds, ...rest
|
||||
} = original as CalendarEvent & Record<string, unknown>;
|
||||
const destCalendarId = Object.keys(patch.calendarIds ?? {})[0];
|
||||
const { calendarIds: _patchCalendars, ...fields } = patch;
|
||||
await apiCreateEvent(
|
||||
{ ...rest, ...fields, '@type': 'Task' } as Partial<CalendarEvent>,
|
||||
destCalendarId,
|
||||
undefined,
|
||||
writeAccount(scope, destAccount),
|
||||
);
|
||||
await apiDeleteEvents([realId], undefined, writeAccount(scope, sourceAccount));
|
||||
// Under a new id in another account: reload rather than patch.
|
||||
if (inScope(scope)) await get().fetchTasks();
|
||||
return;
|
||||
}
|
||||
await apiUpdateEvent(realId, patch, undefined, writeAccount(scope, sourceAccount));
|
||||
// Saved; the next account's tasks can carry the same ids.
|
||||
if (!inScope(scope)) return;
|
||||
set({ tasks: get().tasks.map((t) => (t.id === id ? { ...t, ...changes } : t)) });
|
||||
|
||||
Reference in New Issue
Block a user