@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 +8 -0
- package/package.json +1 -1
- package/src/lib/calendar/schedule.ts +20 -5
- package/src/lib/repo/sql.ts +16 -3
- package/src/lib/repo/types.ts +7 -0
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
|
@@ -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
|
-
|
|
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 ----------------------------------------------------------
|
package/src/lib/repo/sql.ts
CHANGED
|
@@ -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]);
|
package/src/lib/repo/types.ts
CHANGED
|
@@ -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
|