@acms-subir/core 0.3.43 → 0.3.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @acms-subir/core
2
2
 
3
+ ## 0.3.44
4
+
5
+ ### Patch Changes
6
+
7
+ - b011cee: Moving one date of a repeating event on the admin Schedule now keeps what belongs to that date. Dragging a single date still splits it off into a one-off copy, but the copy now keeps the series' hosts, room hold, translations and SEO title and description. Before, all of these were left behind, so the room looked free at the new time, the date dropped off the hosts' My events, and the Spanish title was gone. That date's RSVPs, including places in line, move to the copy at the new time. Everyone with a seat gets the "new time" email with the updated calendar file, while places in line move quietly. **Undo** moves the RSVPs back to the series' date (with the email again) before the copy is deleted, so undoing never cancels anyone. Undo now refuses with "This event has unpublished changes" before anything changes, instead of deleting the copy first.
8
+
9
+ Dates moved before this release keep their copies as they are: staff can still add the hosts and room on those by hand. `repo.createEvent` now stores `translations` (it ignored them before), and the repository has a new `moveOccurrenceRsvps(eventId, start, { eventId?, start, end })`.
10
+
3
11
  ## 0.3.43
4
12
 
5
13
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acms-subir/core",
3
- "version": "0.3.43",
3
+ "version": "0.3.44",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -121,7 +121,9 @@ export async function moveSeries(repo: ContentRepository, id: string, days: numb
121
121
 
122
122
  // Move one occurrence of a repeating event: skip that date in the series and
123
123
  // add a one-off copy on the new date. No schema change, and the copy is a
124
- // normal event the editor can open.
124
+ // normal event the editor can open. The copy keeps the series' hosts, room
125
+ // hold and translations, and that date's RSVPs move to it (people get the
126
+ // "new time" email).
125
127
  export async function moveOccurrence(repo: ContentRepository, id: string, occurrence: string, days: number, hooks: SaveHooks): Promise<{ event: CalEvent; undo: MoveUndo }> {
126
128
  const e = await repo.getEvent(id);
127
129
  if (!e) throw new ApiError(404, 'Event not found.');
@@ -139,16 +141,22 @@ export async function moveOccurrence(repo: ContentRepository, id: string, occurr
139
141
  const endDay = `${t.getUTCFullYear()}-${pad(t.getUTCMonth() + 1)}-${pad(t.getUTCDate())}`;
140
142
  const endStr = e.allDay ? endDay : `${endDay}T${pad(t.getUTCHours())}:${pad(t.getUTCMinutes())}`;
141
143
 
144
+ const settings = await repo.getSettings();
145
+ const was = findOccurrence(e, occurrence, settings);
142
146
  await saveNow(repo, e, { exdates: [...e.exdates, occurrence] }, hooks);
143
147
  const copy = await repo.createEvent({
144
148
  title: e.title, slug: await uniqueSlug(repo, e.title), start: formatWall(newStart), end: endStr, allDay: e.allDay,
145
149
  timezone: e.timezone, status: 'draft', excerpt: e.excerpt, bodyJson: e.bodyJson, bodyHtml: e.bodyHtml, cover: e.cover, coverAlt: e.coverAlt,
146
- location: e.location, cta: e.cta, calendarIds: e.calendarIds, fields: e.fields, tags: e.tags,
150
+ seoTitle: e.seoTitle, seoDesc: e.seoDesc, location: e.location, cta: e.cta, calendarIds: e.calendarIds, fields: e.fields, tags: e.tags,
147
151
  // a cancelled date stays cancelled on its new day
148
152
  cancelled: e.cancelled || e.cancelledDates.includes(occurrence), rsvp: e.rsvp,
149
- template: e.template, featured: false,
153
+ template: e.template, featured: false, roomHold: e.roomHold, hostIds: e.hostIds, translations: e.translations,
150
154
  });
151
155
  const event = live(e.status) ? await saveEvent(repo, copy.id, { status: 'published' }, hooks) : copy;
156
+ const now = findOccurrence(event, event.start, settings);
157
+ if (was && now && await repo.moveOccurrenceRsvps(e.id, was.start, { eventId: event.id, start: now.start, end: now.end })) {
158
+ await kickEmail(repo, hooks.locals, waitUntilFrom(hooks.locals));
159
+ }
152
160
  return { event, undo: { kind: 'split', id: copy.id, seriesId: e.id, occurrence } };
153
161
  }
154
162
 
@@ -157,13 +165,20 @@ export async function undoMove(repo: ContentRepository, undo: MoveUndo, hooks: S
157
165
  await moveSeries(repo, undo.id, undo.days, hooks);
158
166
  return;
159
167
  }
160
- const series = await repo.getEvent(undo.seriesId);
168
+ let series = await repo.getEvent(undo.seriesId);
161
169
  const copy = await repo.getEvent(undo.id);
170
+ if (series) series = await saveNow(repo, series, { exdates: series.exdates.filter((x) => x !== undo.occurrence) }, hooks);
162
171
  if (copy) {
172
+ // the RSVPs go back to the series' date before the copy (and its RSVPs) goes
173
+ const settings = await repo.getSettings();
174
+ const back = series && findOccurrence(series, undo.occurrence, settings);
175
+ const from = findOccurrence(copy, copy.start, settings);
176
+ if (back && from && await repo.moveOccurrenceRsvps(copy.id, from.start, { eventId: series!.id, start: back.start, end: back.end })) {
177
+ await kickEmail(repo, hooks.locals, waitUntilFrom(hooks.locals));
178
+ }
163
179
  await repo.deleteEvent(copy.id);
164
180
  await eventDeleted(repo, copy, { waitUntil: waitUntilFrom(hooks.locals) });
165
181
  }
166
- if (series) await saveNow(repo, series, { exdates: series.exdates.filter((x) => x !== undo.occurrence) }, hooks);
167
182
  }
168
183
 
169
184
  // ---- cancel one date ----------------------------------------------------------
@@ -847,8 +847,8 @@ export class SqlRepository implements ContentRepository {
847
847
  await this.driver.run(
848
848
  `INSERT INTO events (id, slug, title, body_json, body_html, excerpt, cover, cover_alt, seo_title, seo_desc, status, featured, tags, template,
849
849
  starts, ends, all_day, timezone, rrule, exdates, cancelled, location, cta, calendar_ids, fields, source_uid, start_at, end_at,
850
- published_at, scheduled_at, created_at, updated_at, rsvp, room_hold, cancelled_dates, host_ids, request)
851
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
850
+ published_at, scheduled_at, created_at, updated_at, rsvp, room_hold, cancelled_dates, host_ids, request, translations)
851
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
852
852
  [
853
853
  id, data.slug, data.title, data.bodyJson ?? '', data.bodyHtml ?? '', data.excerpt ?? '', data.cover ?? '', data.coverAlt ?? '',
854
854
  data.seoTitle ?? '', data.seoDesc ?? '', data.status ?? 'draft', data.featured ? 1 : 0, JSON.stringify(data.tags ?? []), data.template ?? '',
@@ -857,7 +857,7 @@ export class SqlRepository implements ContentRepository {
857
857
  JSON.stringify(data.fields ?? {}), data.sourceUid ?? '', startAt, endAt,
858
858
  data.publishedAt ?? (data.status === 'published' ? now : null), data.scheduledAt ?? null, now, now,
859
859
  JSON.stringify(data.rsvp ?? {}), JSON.stringify(data.roomHold ?? {}), JSON.stringify(data.cancelledDates ?? []),
860
- JSON.stringify(data.hostIds ?? []), data.request ? JSON.stringify(data.request) : null,
860
+ JSON.stringify(data.hostIds ?? []), data.request ? JSON.stringify(data.request) : null, JSON.stringify(data.translations ?? {}),
861
861
  ]
862
862
  );
863
863
  await this.syncEventIndex(id);
@@ -965,6 +965,19 @@ export class SqlRepository implements ContentRepository {
965
965
  ]);
966
966
  return Number((await this.first("SELECT COUNT(*) AS n FROM bookings WHERE event_id=? AND kind='rsvp' AND email_mutation=?", [id, mutation]))?.n ?? 0);
967
967
  }
968
+ async moveOccurrenceRsvps(id: string, start: number, to: { eventId?: string; start: number; end: number }) {
969
+ await this.ready;
970
+ const target = to.eventId || id;
971
+ // one mutation id marks every moved row; only the seated ones are emailed
972
+ const mutation = crypto.randomUUID();
973
+ const notifications = bookingJobs(target,'rescheduled',undefined,false,mutation).map(s=>({...s,sql:s.sql.replace('b.id = ?',"b.event_id = ? AND b.kind='rsvp' AND b.status IN ('pending','confirmed')")}));
974
+ await this.driver.batch([
975
+ {sql:`UPDATE bookings SET event_id=?,start_at=?,end_at=?,hold_start=?,hold_end=?,notification_version=notification_version+1,email_mutation=?,reminder_sent_at=NULL,updated_at=?
976
+ WHERE event_id=? AND kind='rsvp' AND start_at=? AND status IN ${RSVP_ACTIVE}`,args:[target,to.start,to.end,to.start,to.end,mutation,Date.now(),id,start]},
977
+ ...notifications,
978
+ ]);
979
+ return Number((await this.first("SELECT COUNT(*) AS n FROM bookings WHERE event_id=? AND kind='rsvp' AND email_mutation=?", [target, mutation]))?.n ?? 0);
980
+ }
968
981
  async discardEventDraft(id: string) {
969
982
  await this.ready;
970
983
  await this.driver.run('UPDATE events SET draft = NULL WHERE id = ?', [id]);
@@ -155,6 +155,13 @@ export interface ContentRepository {
155
155
  setEventCancelledDates(id: string, dates: string[]): Promise<void>;
156
156
  /** Cancels the RSVPs for one date of an event (they get the "cancelled" email). Returns how many. */
157
157
  cancelOccurrenceRsvps(id: string, start: number): Promise<number>;
158
+ /**
159
+ * Moves the RSVPs for one date of an event (those starting at `start`) to a
160
+ * new time, and to another event when `to.eventId` is set. Pending and
161
+ * confirmed ones get the "new time" email; places in line move quietly.
162
+ * Returns how many moved.
163
+ */
164
+ moveOccurrenceRsvps(id: string, start: number, to: { eventId?: string; start: number; end: number }): Promise<number>;
158
165
  deleteEvent(id: string): Promise<void>;
159
166
  // saved-place sync (lib/maps/places.ts): rewrite just the location of the
160
167
  // live row and/or the staged draft, leaving every other field — and the