Update a re-imported event rather than skipping it

Contacts and calendars disagreed on a re-import: a vCard or LDIF entry
whose identity a book already held overwrote the card there (#242, #274),
while an event whose UID a calendar held was counted and thrown away
(#222). The asymmetry was never decided -- it was where each half stopped.

Decided on #279: calendars update too, with two properties held back.
`participants` carries every attendee's accepted/declined and
`recurrenceOverrides` holds every "just this Wednesday" edit made here.
Both are decisions taken after the file was written, and a file that
mentions them at all describes them as they were at export, so writing
either one over would destroy work silently and return no error. A
corrected export now fixes the time, the title and the location, and
leaves who said yes alone. `uid` is held back with them: it is what the
two were matched on, so it is already equal.

The scan returns uid -> id rather than a set of UIDs, since updating
needs something to address, and creates and updates now share one
`maxObjectsInSet` budget the way contacts' `writeCards` does -- 300 new
and 300 changed batched separately would be two calls of 300, neither
over a ceiling of 500 and both refused. Counts become created/updated,
reported as the contacts import reports them.

Still no scheduling messages, on an update as much as on a create. That
is a real cost -- an event a re-import moves is moved here and nowhere
else -- and it is the lesser one: an import is not the place to start
mailing a room full of people who never asked for it.

Driven against the mock end to end: a second file with the same UID
updated the event in place, took the file's title, start and location,
and left an accepted RSVP and a per-occurrence override untouched even
though the file carried participants of its own.
This commit is contained in:
2026-09-07 12:56:36 -07:00
parent 7825097333
commit 8173e22ccb
13 changed files with 220 additions and 72 deletions
+11 -8
View File
@@ -65,16 +65,19 @@ export function CalendarSidebar() {
const calendarId = importInto.current;
if (!calendarId) return;
try {
const { created, skipped } = await cal.importIcs(await file.text(), calendarId);
const { created, updated } = await cal.importIcs(await file.text(), calendarId);
/*
* The two counts are kept apart on purpose. "Imported 40 events" over a
* file of 240 reads as a failure when 200 of them were simply already
* here, and a re-import where everything is already here would otherwise
* report importing nothing at all.
* The two counts are kept apart on purpose, the way the contacts import
* keeps them. "Imported 40 events" over a file of 240 reads as a failure
* when the other 200 were updated, and a re-import of a corrected export
* -- the reason for doing this at all -- creates nothing and would
* otherwise report importing nothing at all.
*/
if (!created) toast.success(plural(skipped, { one: "Already here: {n} event, nothing imported", other: "Already here: {n} events, nothing imported" }));
else if (skipped) toast.success(`${plural(created, { one: "Imported {n} event", other: "Imported {n} events" })} · ${plural(skipped, { one: "{n} was already here", other: "{n} were already here" })}`);
else toast.success(plural(created, { one: "Imported {n} event", other: "Imported {n} events" }));
const imported = plural(created, { one: "Imported {n} event", other: "Imported {n} events" });
const refreshed = plural(updated, { one: "{n} updated", other: "{n} updated" });
if (!created) toast.success(plural(updated, { one: "Updated {n} event, nothing new", other: "Updated {n} events, nothing new" }));
else if (updated) toast.success(`${imported} · ${refreshed}`);
else toast.success(imported);
} catch (err) {
toast.error(t("Could not import this file: {error}", { error: (err as Error).message }));
}