fix: keep calendar.deleteEvent working for callers whose client names no account

This commit is contained in:
Linus Rath
2026-11-03 00:00:17 +01:00
parent 0fb8f44cc4
commit 82c3c969be
5 changed files with 19 additions and 12 deletions
+3 -3
View File
@@ -13,7 +13,7 @@ import { recordUndo, runUndo } from '../../undo-history';
import { KEYWORD_PREFIX, KEYWORD_PREFIX_LEGACY } from '../../thread-utils';
import { readId, readObject, readString, sessionClient, withinRate } from '../host-api-input';
import { MAX_PLUGIN_MOVES_PER_MINUTE } from '../host-api-mail';
import { issueUndoToken, redeemUndoToken } from './c.undo';
import { issueUndoToken, redeemUndoToken, undoAccount } from './c.undo';
import type { MailBatchAction, MailBatchResult } from './c.types';
/** Most messages one email.batch call changes. */
@@ -134,7 +134,7 @@ export async function doEmailBatch(pluginId: string, input: unknown): Promise<Ma
const run = once(undo);
// One entry in the app's undo history: Ctrl+Z reaches it too.
const entry = recordUndo(`Plugin ${pluginId}: ${label} messages`, () => { void run().catch(() => { /* the plugin's undo reports it */ }); });
const token = issueUndoToken(pluginId, client.getAccountId(), 'batch', { entryId: entry.id, run } satisfies BatchUndo);
const token = issueUndoToken(pluginId, undoAccount(client), 'batch', { entryId: entry.id, run } satisfies BatchUndo);
return { done, failed: missing, undoToken: token };
}
@@ -142,7 +142,7 @@ export async function doEmailBatch(pluginId: string, input: unknown): Promise<Ma
export async function doEmailUndoBatch(pluginId: string, token: unknown): Promise<void> {
const method = 'email.undoBatch';
const client: IJMAPClient = sessionClient(method);
const { entryId, run } = redeemUndoToken<BatchUndo>(method, pluginId, client.getAccountId(), 'batch', token);
const { entryId, run } = redeemUndoToken<BatchUndo>(method, pluginId, undoAccount(client), 'batch', token);
// Off the history first, so Ctrl+Z can't run it again; an entry already
// taken by Ctrl+Z has run (or is running) the same undo.
runUndo(entryId);
+3 -3
View File
@@ -12,7 +12,7 @@ import { answerInvitation, attendeeOf, readOwnInvitation, type RsvpStatus } from
import { readId, sessionClient } from '../host-api-input';
import { readNamedEmail } from '../host-api-mail';
import { doCalendarDeleteEvent } from '../host-api-calendar';
import { issueUndoToken, redeemUndoToken } from './c.undo';
import { issueUndoToken, redeemUndoToken, undoAccount } from './c.undo';
import type { EventDeleteResult, InvitationResponseView } from './c.types';
const STATUSES: readonly RsvpStatus[] = ['accepted', 'tentative', 'declined'];
@@ -159,14 +159,14 @@ export async function doCalendarDeleteWithUndo(pluginId: string, idArg: unknown)
if (!deleted) return { undoToken: null };
const copy: Record<string, unknown> = { ...event };
for (const key of NOT_RECREATED) delete copy[key];
return { undoToken: issueUndoToken(pluginId, client.getAccountId(), 'event', copy) };
return { undoToken: issueUndoToken(pluginId, undoAccount(client), 'event', copy) };
}
/** Puts back an event calendar.deleteEvent deleted, from the host's copy. Sends no invitations. */
export async function doCalendarUndoDelete(pluginId: string, tokenArg: unknown): Promise<{ id: string } | null> {
const method = 'calendar.undoDelete';
const client = sessionClient(method);
const copy = redeemUndoToken<Partial<CalendarEvent>>(method, pluginId, client.getAccountId(), 'event', tokenArg);
const copy = redeemUndoToken<Partial<CalendarEvent>>(method, pluginId, undoAccount(client), 'event', tokenArg);
const created = await useCalendarStore.getState().createEvent(client, copy, false);
return created ? { id: created.id } : null;
}
+3 -3
View File
@@ -20,7 +20,7 @@ import { isValidSieveHeaderName } from '../../sieve/generator';
import { KEYWORD_PREFIX, KEYWORD_PREFIX_LEGACY } from '../../thread-utils';
import { generateUUID } from '../../utils';
import { isPlainRecord, readId, readObject, readString, sessionClient } from '../host-api-input';
import { issueUndoToken, redeemUndoToken } from './c.undo';
import { issueUndoToken, redeemUndoToken, undoAccount } from './c.undo';
import type {
FilterCreateResult, FilterRuleActionType, FilterRuleField, FilterRuleInfo, FilterRuleOp, FiltersListView,
} from './c.types';
@@ -214,14 +214,14 @@ export async function doFiltersCreate(pluginId: string, input: unknown): Promise
}
if (!change) throw new Error(`${method}: nothing was written`);
refreshFilterStore(client, accountId);
return { id: rule.id, undoToken: issueUndoToken(pluginId, client.getAccountId(), 'filters', change) };
return { id: rule.id, undoToken: issueUndoToken(pluginId, undoAccount(client), 'filters', change) };
}
/** Take back a rule filters.create added: the script gets its earlier bytes back. */
export async function doFiltersUndo(pluginId: string, token: unknown): Promise<void> {
const method = 'filters.undo';
const client = sieveClient(method);
const change = redeemUndoToken<FiltersChange>(method, pluginId, client.getAccountId(), 'filters', token);
const change = redeemUndoToken<FiltersChange>(method, pluginId, undoAccount(client), 'filters', token);
try {
await restoreAccountFilters(client, change);
} catch (err) {
+3 -3
View File
@@ -26,7 +26,7 @@ import {
} from '../../settings-schema';
import { isPlainRecord, sessionClient } from '../host-api-input';
import { messagesFor } from '../host-api-app';
import { issueUndoToken, redeemUndoToken } from './c.undo';
import { issueUndoToken, redeemUndoToken, undoAccount } from './c.undo';
import type {
SettingDescription, SettingRefusal, SettingValue, SettingsChangeResult, SettingsUndoResult,
} from './c.types';
@@ -239,7 +239,7 @@ export async function doSetSettings(pluginId: string, arg: unknown): Promise<Set
previous[key] = old;
}
const undoToken = Object.keys(previous).length > 0
? issueUndoToken(pluginId, client.getAccountId(), 'settings', previous)
? issueUndoToken(pluginId, undoAccount(client), 'settings', previous)
: null;
return { applied, refused, undoToken };
}
@@ -252,7 +252,7 @@ export async function doSetSettings(pluginId: string, arg: unknown): Promise<Set
export async function doUndoSettings(pluginId: string, token: unknown): Promise<SettingsUndoResult> {
const method = 'app.undoSettings';
const client = sessionClient(method);
const previous = redeemUndoToken<Record<string, SettingValue | null>>(method, pluginId, client.getAccountId(), 'settings', token);
const previous = redeemUndoToken<Record<string, SettingValue | null>>(method, pluginId, undoAccount(client), 'settings', token);
const restored: string[] = [];
const refused: SettingsUndoResult['refused'] = {};
const policy = usePolicyStore.getState();
+7
View File
@@ -5,6 +5,8 @@
// host a copy of something to re-create.
import { generateUUID } from '../../utils';
import type { IJMAPClient } from '../../jmap/client-interface';
import { useAuthStore } from '@/stores/auth-store';
/** How long an undo token can be used. */
export const UNDO_TOKEN_TTL_MS = 30 * 60_000;
@@ -51,6 +53,11 @@ export function redeemUndoToken<T>(method: string, pluginId: string, accountId:
return entry.data as T;
}
/** The account an undo token is bound to: the client's JMAP account, else the signed-in login. */
export function undoAccount(client: IJMAPClient): string {
return client.getAccountId?.() || useAuthStore.getState().activeAccountId || '';
}
/** For tests. */
export function clearUndoTokens(): void {
tokens.clear();