@acms-subir/core 0.3.56 → 0.3.58
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 +29 -0
- package/cli/migrations.generated.mjs +24 -5
- package/docs/calendar.md +130 -2
- package/docs/server.md +33 -1
- package/package.json +1 -1
- package/src/actions/index.ts +52 -9
- package/src/components/EventPage.astro +6 -5
- package/src/components/calendar/CalendarBoard.tsx +6 -3
- package/src/components/editor/event/SingleDates.tsx +75 -15
- package/src/lib/booking/service.ts +4 -2
- package/src/lib/calendar/api.ts +29 -8
- package/src/lib/calendar/board.ts +3 -2
- package/src/lib/calendar/cards.ts +4 -4
- package/src/lib/calendar/change-migration.ts +17 -3
- package/src/lib/calendar/data.ts +16 -7
- package/src/lib/calendar/event-words.ts +8 -0
- package/src/lib/calendar/events.ts +128 -15
- package/src/lib/calendar/google/map.ts +1 -1
- package/src/lib/calendar/host-requests.ts +65 -10
- package/src/lib/calendar/ics-write.ts +65 -15
- package/src/lib/calendar/member-words.ts +14 -1
- package/src/lib/calendar/moves.ts +15 -9
- package/src/lib/calendar/request-admin.ts +2 -1
- package/src/lib/calendar/request-mail.ts +2 -1
- package/src/lib/calendar/request-page.ts +2 -1
- package/src/lib/calendar/request-service.ts +7 -7
- package/src/lib/calendar/schedule.ts +42 -8
- package/src/lib/calendar/segments.ts +522 -0
- package/src/lib/display/board.ts +6 -5
- package/src/lib/editor/schedule.ts +4 -2
- package/src/lib/mobile/calendar.ts +6 -5
- package/src/lib/mobile/schemas.ts +10 -3
- package/src/lib/mobile/search.ts +2 -1
- package/src/lib/push/service.ts +42 -10
- package/src/lib/push/types.ts +2 -0
- package/src/lib/repo/migrations.ts +13 -3
- package/src/lib/repo/sql.ts +34 -13
- package/src/lib/repo/types.ts +3 -1
- package/src/lib/schema.ts +34 -0
- package/src/lib/sections.ts +3 -2
- package/src/lib/sync/catalog.ts +1 -0
- package/src/pages/admin/calendar/events/index.astro +9 -8
- package/src/pages/admin/calendar/requests.astro +5 -5
- package/src/pages/admin/calendar/resources/[id].astro +11 -0
- package/src/pages/api/calendar/events/index.ts +2 -1
- package/src/pages/members/events/index.astro +87 -7
- package/src/server/index.ts +46 -3
- package/src/styles/editor.css +2 -0
|
@@ -20,7 +20,7 @@ import type { ContentRepository } from '../repo/types';
|
|
|
20
20
|
import { EventLocation, type CalEvent, type MovedDate } from '../schema';
|
|
21
21
|
import { ApiError, coded } from '../api';
|
|
22
22
|
import { eventSideEffects, type SaveHooks } from '../content-service';
|
|
23
|
-
import { addWallDays, addWallMinutes, eventRoomIds, eventTimeZone, expandEvents, findOccurrence, formatWall, movedInstants, normalizeEventTimes, parseWall, wallMinutesBetween, type Occurrence } from './events';
|
|
23
|
+
import { addWallDays, addWallMinutes, eventRoomIds, eventTimeZone, expandEvents, findOccurrence, formatWall, movedInstants, normalizeEventTimes, parseWall, wallMinutesBetween, type Occurrence, partOf, partMinutes, type SeriesPart } from './events';
|
|
24
24
|
import { holdsFromEvents, roomBusy, type BusySlot } from '../booking/holds';
|
|
25
25
|
import { kickEmail } from '../email/service';
|
|
26
26
|
import { waitUntilFrom } from '../webhooks';
|
|
@@ -103,14 +103,14 @@ const started = () => refuse(400, 'started', 'That date has already started or p
|
|
|
103
103
|
|
|
104
104
|
// The new wall times: `start` in the event's form, `end` defaulting to the
|
|
105
105
|
// date's own length (all-day: the same number of days)
|
|
106
|
-
function newTimes(e: CalEvent, input: MoveInput): { start: string; end: string } {
|
|
106
|
+
function newTimes(e: CalEvent, input: MoveInput, part: SeriesPart): { start: string; end: string } {
|
|
107
107
|
const s = parseWall(input.start);
|
|
108
108
|
if (!s) throw refuse(400, 'bad-time', e.allDay ? 'Pick a date (YYYY-MM-DD).' : 'Pick a date and time (YYYY-MM-DDTHH:mm).');
|
|
109
109
|
if (s.dateOnly !== e.allDay) throw refuse(400, 'bad-time', e.allDay ? 'This event lasts all day: give a date only (YYYY-MM-DD).' : 'Give a date and a time (YYYY-MM-DDTHH:mm).');
|
|
110
110
|
let end = input.end;
|
|
111
111
|
if (!end) {
|
|
112
|
-
|
|
113
|
-
const length =
|
|
112
|
+
// the length of its part's dates (a change from a date on may have its own)
|
|
113
|
+
const length = partMinutes(part);
|
|
114
114
|
end = formatWall(e.allDay ? addWallDays(s, Math.round(length / 1440)) : addWallMinutes(s, Math.max(1, length)));
|
|
115
115
|
}
|
|
116
116
|
try {
|
|
@@ -162,7 +162,9 @@ async function planMove(repo: ContentRepository, actor: MoveActor | null, id: st
|
|
|
162
162
|
const settings = await repo.getSettings();
|
|
163
163
|
const current = dateToChange(e, key, settings);
|
|
164
164
|
const prior = e.movedDates[key];
|
|
165
|
-
|
|
165
|
+
// its part of the series (CalEvent.segments): the length, rooms and place it has without a move
|
|
166
|
+
const part = partOf(e, current);
|
|
167
|
+
const times = newTimes(e, input, part);
|
|
166
168
|
|
|
167
169
|
let roomIds = input.roomIds === undefined ? prior?.roomIds : [...new Set(input.roomIds)];
|
|
168
170
|
if (roomIds !== undefined) {
|
|
@@ -175,11 +177,11 @@ async function planMove(repo: ContentRepository, actor: MoveActor | null, id: st
|
|
|
175
177
|
if (roomIds.some((r) => !allowed.has(r))) throw refuse(403, 'room-not-allowed', 'Pick from the rooms this event uses, or ask the team for another room.');
|
|
176
178
|
}
|
|
177
179
|
// the event's own rooms aren't an override
|
|
178
|
-
if (sameRooms(roomIds,
|
|
180
|
+
if (sameRooms(roomIds, part.roomIds)) roomIds = undefined;
|
|
179
181
|
}
|
|
180
182
|
let location = input.location === undefined ? prior?.location : input.location === null ? undefined : EventLocation.parse({ ...e.location, ...input.location });
|
|
181
183
|
if (location && !location.name.trim() && !location.address.trim()) throw new ApiError(400, 'Give the place a name or an address.');
|
|
182
|
-
if (location && JSON.stringify(location) === JSON.stringify(e.location)) location = undefined;
|
|
184
|
+
if (location && JSON.stringify(location) === JSON.stringify(part.location ?? e.location)) location = undefined;
|
|
183
185
|
const note = input.note === undefined ? prior?.note ?? '' : String(input.note).replace(/\s+/g, ' ').trim().slice(0, NOTE_MAX);
|
|
184
186
|
|
|
185
187
|
const entry: MovedDate = { start: times.start, end: times.end, ...(roomIds ? { roomIds } : {}), ...(location ? { location } : {}), note, by: by || actorId(actor!), at: now };
|
|
@@ -306,6 +308,8 @@ export interface DateRow {
|
|
|
306
308
|
/** "Thu, Jan 10 · 6:00 – 7:00 PM" */
|
|
307
309
|
when: string;
|
|
308
310
|
cancelled: boolean;
|
|
311
|
+
/** cancelled because every date from some date on is (a cancelled part, CalEvent.segments) */
|
|
312
|
+
partCancelled: boolean;
|
|
309
313
|
/** moved: "Thu, Jan 10 · 6:00 PM" (where it was) and the note */
|
|
310
314
|
moved: { from: string; note: string } | null;
|
|
311
315
|
roomIds: string[];
|
|
@@ -319,19 +323,21 @@ export function upcomingDates(e: CalEvent, settings: Awaited<ReturnType<ContentR
|
|
|
319
323
|
const day = new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric', timeZone: tz });
|
|
320
324
|
const time = new Intl.DateTimeFormat(locale, { hour: 'numeric', minute: '2-digit', timeZone: tz });
|
|
321
325
|
const range = (a: number, b: number) => { try { return time.formatRange(a, b); } catch { return `${time.format(a)} – ${time.format(b)}`; } };
|
|
322
|
-
const es = parseWall(e.start), ee = parseWall(e.end) ?? es;
|
|
323
|
-
const length = es && ee ? wallMinutesBetween(es, ee) : 60;
|
|
324
326
|
return expandEvents([e], settings, { displayTz: settings.timezone || 'UTC', now, windowStartMs: now, windowEndMs: now + 400 * DAY })
|
|
325
327
|
.slice(0, limit)
|
|
326
328
|
.map((o) => {
|
|
327
329
|
const m = o.moved ? e.movedDates[o.key] : undefined;
|
|
328
330
|
const k = parseWall(o.key)!;
|
|
331
|
+
const part = partOf(e, o);
|
|
332
|
+
const length = partMinutes(part);
|
|
329
333
|
return {
|
|
330
334
|
key: o.key,
|
|
331
335
|
start: m?.start ?? o.key,
|
|
332
336
|
end: m?.end ?? formatWall(e.allDay ? addWallDays(k, Math.round(length / 1440)) : addWallMinutes(k, length)),
|
|
333
337
|
when: e.allDay ? day.format(o.start) : `${day.format(o.start)} · ${range(o.start, o.end)}`,
|
|
334
338
|
cancelled: o.cancelled,
|
|
339
|
+
// cancelled with the rest of its part (cancelFromDate): restored from the change, not one by one
|
|
340
|
+
partCancelled: part.cancelled,
|
|
335
341
|
moved: o.moved ? { from: e.allDay ? day.format(o.moved.from.start) : `${day.format(o.moved.from.start)} · ${time.format(o.moved.from.start)}`, note: o.moved.note } : null,
|
|
336
342
|
roomIds: o.roomIds,
|
|
337
343
|
};
|
|
@@ -110,7 +110,8 @@ export async function requestRows(repo: ContentRepository, settings: Settings, t
|
|
|
110
110
|
? { taken: true, text: `${found[0].room} on ${f.slot(found[0].start, found[0].end)}${found[0].title ? ` (${found[0].title})` : ''}${found.length > 1 ? ` and ${found.length - 1} more` : ''}` }
|
|
111
111
|
: { taken: false, text: `${names} ${d.roomIds.length > 1 ? 'are' : 'is'} free then` };
|
|
112
112
|
}
|
|
113
|
-
} else if (q.kind === 'move') {
|
|
113
|
+
} else if (q.kind === 'move' || q.kind === 'from-date') {
|
|
114
|
+
// a host's date move, or change from a date on ("Thu, Jan 17, 6:00 PM → Wed, Jan 16, 5:00 PM · Weekly on Wednesday")
|
|
114
115
|
title = e?.title ?? 'A deleted event';
|
|
115
116
|
line = await dateRequestWhen(repo, q);
|
|
116
117
|
if (tab === 'pending' && e) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ContentRepository } from '../repo/types';
|
|
2
2
|
import type { RequestRecord, Settings } from '../schema';
|
|
3
|
+
import { roomName } from '../schema';
|
|
3
4
|
import type { Mail } from '../email/types';
|
|
4
5
|
import { siteLang, siteLocale } from '../dates';
|
|
5
6
|
import { I18N, localeName } from '../i18n';
|
|
@@ -66,7 +67,7 @@ async function personSummary(repo: ContentRepository, q: RequestRecord, settings
|
|
|
66
67
|
const parts = [
|
|
67
68
|
requestWhen(d.start, d.end, settings, locale),
|
|
68
69
|
...(d.repeat === 'weekly' ? [w.weekly] : []),
|
|
69
|
-
d.roomIds.map((id) => rooms.find((r) => r.id === id)
|
|
70
|
+
d.roomIds.map((id) => rooms.find((r) => r.id === id)).filter((r) => r !== undefined).map((r) => roomName(r, lang)).join(', '),
|
|
70
71
|
d.people ? fill(w.aboutPeople, { count: String(d.people) }) : '',
|
|
71
72
|
type ? (lang && type.translations[lang]) || type.label : '',
|
|
72
73
|
].filter(Boolean);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ContentRepository } from '../repo/types';
|
|
2
2
|
import type { RequestRecord, Settings } from '../schema';
|
|
3
|
+
import { roomName } from '../schema';
|
|
3
4
|
import { requestWhen } from './request-mail';
|
|
4
5
|
import { requestSettings, type NewEventDetails, type RequestInput } from './request-service';
|
|
5
6
|
import { requestStrings } from './request-words';
|
|
@@ -44,7 +45,7 @@ export async function sentSummary(repo: ContentRepository, q: RequestRecord, set
|
|
|
44
45
|
const line = [
|
|
45
46
|
requestWhen(d.start, d.end, settings, locale),
|
|
46
47
|
d.repeat === 'weekly' ? w.weekly : d.repeat === 'other' ? d.repeatText : '',
|
|
47
|
-
d.roomIds.map((id) => rooms.find((r) => r.id === id)
|
|
48
|
+
d.roomIds.map((id) => rooms.find((r) => r.id === id)).filter((r) => r !== undefined).map((r) => roomName(r, lang)).join(', '),
|
|
48
49
|
d.people ? fill(w.aboutPeople, { count: String(d.people) }) : '',
|
|
49
50
|
type ? (lang && type.translations[lang]) || type.label : '',
|
|
50
51
|
].filter(Boolean).join(' · ');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { ContentRepository } from '../repo/types';
|
|
3
|
-
import { RequestRecord, RoomHold, type CalEvent, type RequestSettings, type Resource, type Settings } from '../schema';
|
|
3
|
+
import { RequestRecord, RoomHold, roomName, type CalEvent, type RequestSettings, type Resource, type Settings } from '../schema';
|
|
4
4
|
import { ApiError, coded } from '../api';
|
|
5
5
|
import { sha256Hex } from '../api-keys';
|
|
6
6
|
import { validAddress } from '../email/config';
|
|
@@ -139,7 +139,7 @@ export async function requestOptions(repo: ContentRepository, locale = '', now =
|
|
|
139
139
|
return {
|
|
140
140
|
open: rq.open,
|
|
141
141
|
types: rq.types.map((t) => ({ id: t.id, label: typeLabel(t, lang) })),
|
|
142
|
-
rooms: (await requestableRooms(repo)).map((r) => ({ slug: r.slug, name: r
|
|
142
|
+
rooms: (await requestableRooms(repo)).map((r) => ({ slug: r.slug, name: roomName(r, lang), color: /^#[0-9a-f]{6}$/i.test(r.color) ? r.color.toLowerCase() : '' })),
|
|
143
143
|
replyTime: words?.replyTime || rq.replyTime,
|
|
144
144
|
notice: words?.notice || rq.notice,
|
|
145
145
|
phone: rq.phone,
|
|
@@ -429,11 +429,11 @@ export async function approveRequest(repo: ContentRepository, who: Actor | null,
|
|
|
429
429
|
if (!event) throw new ApiError(409, 'That event was deleted. Decline the request, or reply to the person.');
|
|
430
430
|
done = await decide(repo, q, answer);
|
|
431
431
|
next = `/admin/calendar/events/${event.id}${q.key ? `?move=${encodeURIComponent(q.key)}` : ''}`;
|
|
432
|
-
} else if (q.kind === 'move') {
|
|
433
|
-
// a host's date move (lib/calendar/host-requests.ts): it runs, and the host hears about it there
|
|
432
|
+
} else if (q.kind === 'move' || q.kind === 'from-date') {
|
|
433
|
+
// a host's date move or change from a date on (lib/calendar/host-requests.ts): it runs, and the host hears about it there
|
|
434
434
|
const { approveDateRequest } = await import('./host-requests');
|
|
435
|
-
const { request, moved } = await approveDateRequest(repo, who, id, hooks, seen);
|
|
436
|
-
return { request, event: moved
|
|
435
|
+
const { request, moved, changed } = await approveDateRequest(repo, who, id, hooks, seen);
|
|
436
|
+
return { request, event: (moved ?? changed)!.event, next: `/admin/calendar/events/${request.eventId}` };
|
|
437
437
|
} else {
|
|
438
438
|
throw new ApiError(400, 'Open the event to answer this request.');
|
|
439
439
|
}
|
|
@@ -464,7 +464,7 @@ async function draftFrom(repo: ContentRepository, q: RequestRecord, hooks: SaveH
|
|
|
464
464
|
export async function declineRequest(repo: ContentRepository, who: Actor | null, id: string, reply: string, hooks: SaveHooks, seen?: number, opts: { email?: boolean } = {}): Promise<RequestRecord> {
|
|
465
465
|
requireAnswerer(who);
|
|
466
466
|
const q = await pendingRequest(repo, id, seen);
|
|
467
|
-
if (q.kind === 'move') return (await import('./host-requests')).declineDateRequest(repo, who, id, reply, hooks, seen);
|
|
467
|
+
if (q.kind === 'move' || q.kind === 'from-date') return (await import('./host-requests')).declineDateRequest(repo, who, id, reply, hooks, seen);
|
|
468
468
|
if (q.kind !== 'new' && q.kind !== 'change') throw new ApiError(400, 'Open the event to answer this request.');
|
|
469
469
|
const reason = text(reply, REQUEST_LIMITS.message);
|
|
470
470
|
const done = await decide(repo, q, { state: 'declined', reviewedBy: who.id, reviewedAt: Date.now(), reply: reason });
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
// Cancelling one date of a series applies at once too, without touching any
|
|
7
7
|
// staged edits.
|
|
8
8
|
import type { ContentRepository } from '../repo/types';
|
|
9
|
-
import type { CalEvent, MovedDate } from '../schema';
|
|
9
|
+
import type { CalEvent, MovedDate, SeriesSegment } from '../schema';
|
|
10
10
|
import { EventLocation, slugify } from '../schema';
|
|
11
11
|
import { defaultPlaceLocation } from '../maps/places';
|
|
12
12
|
import { ApiError } from '../api';
|
|
13
13
|
import { applyEventUpdate, eventSideEffects, flushEventHooks, saveEvent, type SaveHooks } from '../content-service';
|
|
14
|
-
import { addWallDays, addWallMinutes, findOccurrence, formatWall, parseWall, wallMinutesBetween } from './events';
|
|
14
|
+
import { addWallDays, addWallMinutes, findOccurrence, formatWall, parseWall, wallMinutesBetween, partOf, partMinutes } from './events';
|
|
15
15
|
import { moveOccurrence, moveOccurrenceBack, type MoveActor } from './moves';
|
|
16
16
|
import type { BusySlot } from '../booking/holds';
|
|
17
17
|
import { rruleMap, WEEKDAY_CODES, weekdayOf } from './rrule';
|
|
@@ -105,6 +105,8 @@ const dayOf = (wall: string) => {
|
|
|
105
105
|
|
|
106
106
|
export type MoveUndo =
|
|
107
107
|
| { kind: 'series'; id: string; days: number; minutes?: number }
|
|
108
|
+
// this and following dates (lib/calendar/segments.ts): the segments as they were, and where the change took over
|
|
109
|
+
| { kind: 'following'; id: string; from: string; previous: SeriesSegment[] }
|
|
108
110
|
// one date moved (previous: where it was moved before, null = not moved)
|
|
109
111
|
| { kind: 'date'; id: string; key: string; previous: Pick<MovedDate, 'start' | 'end' | 'roomIds' | 'location' | 'note'> | null }
|
|
110
112
|
// undo tokens from before moved dates: the date was split off into a copy
|
|
@@ -126,10 +128,16 @@ export async function moveSeries(repo: ContentRepository, id: string, days: numb
|
|
|
126
128
|
...(e.rrule ? { rrule: shiftRrule(e.rrule, dayShift, start), exdates: e.exdates.map((x) => shiftBy(x, total)) } : {}),
|
|
127
129
|
}, hooks);
|
|
128
130
|
// cancelled dates move with the series, like skipped ones; a moved date
|
|
129
|
-
// keeps its new time and follows under its new key
|
|
130
|
-
|
|
131
|
+
// keeps its new time and follows under its new key; a change from a date on
|
|
132
|
+
// (segments) moves as a whole: its change point, first date and rule
|
|
133
|
+
if (e.rrule && (e.cancelledDates.length || Object.keys(e.movedDates).length || e.segments.length)) {
|
|
131
134
|
const moved = Object.fromEntries(Object.entries(e.movedDates).map(([k, m]) => [shiftBy(k, total), m]));
|
|
132
|
-
|
|
135
|
+
const segments = e.segments.map((g) => {
|
|
136
|
+
const gs = shiftBy(g.start, total);
|
|
137
|
+
const shift = ((a, b) => (a && b ? Math.round(wallMinutesBetween(a, b) / 1440) : days))(dayOf(g.start), dayOf(gs));
|
|
138
|
+
return { ...g, from: shiftBy(g.from, total), start: gs, end: shiftBy(g.end, total), rrule: shiftRrule(g.rrule, shift, gs) };
|
|
139
|
+
});
|
|
140
|
+
await repo.setEventDates(id, { movedDates: moved, cancelledDates: e.cancelledDates.map((x) => shiftBy(x, total)), segments });
|
|
133
141
|
await flushEventHooks(repo, hooks);
|
|
134
142
|
event = (await repo.getEvent(id))!;
|
|
135
143
|
}
|
|
@@ -149,21 +157,47 @@ export async function moveDateBy(repo: ContentRepository, actor: MoveActor, id:
|
|
|
149
157
|
}
|
|
150
158
|
if (!Number.isInteger(minutes) || Math.abs(minutes) > 1440 * 3660) throw new ApiError(400, 'Pick a day to move it to.');
|
|
151
159
|
const prior = e.movedDates[key];
|
|
152
|
-
const es = parseWall(e.start), ee = parseWall(e.end) ?? es;
|
|
153
160
|
const k = parseWall(key);
|
|
154
|
-
|
|
161
|
+
const at = k && findOccurrence({ ...e, movedDates: {} }, key, await repo.getSettings());
|
|
162
|
+
if (!k || !at) throw new ApiError(400, 'That date isn’t part of this event.');
|
|
163
|
+
// its length is its part's (a change from a date on may have its own)
|
|
155
164
|
const startWall = prior?.start ?? key;
|
|
156
|
-
const endWall = prior?.end ?? formatWall(addWallMinutes(k,
|
|
165
|
+
const endWall = prior?.end ?? formatWall(addWallMinutes(k, partMinutes(partOf(e, at))));
|
|
157
166
|
const r = await moveOccurrence(repo, actor, id, key, { start: shiftBy(startWall, minutes), end: shiftBy(endWall, minutes) }, hooks);
|
|
158
167
|
const previous = prior ? { start: prior.start, end: prior.end, roomIds: prior.roomIds, location: prior.location, note: prior.note } : null;
|
|
159
168
|
return { event: r.event, clashes: r.clashes, undo: { kind: 'date', id, key, previous } };
|
|
160
169
|
}
|
|
161
170
|
|
|
171
|
+
// Move one date of a repeating event and every date after it by `minutes`
|
|
172
|
+
// (a drag on the Schedule, "This and following dates"): a change from that
|
|
173
|
+
// date on (lib/calendar/segments.ts) whose rule follows the new weekday or
|
|
174
|
+
// day, keeping each date's length. The dates before it stay where they are.
|
|
175
|
+
export async function moveFollowingBy(repo: ContentRepository, actor: MoveActor, id: string, key: string, minutes: number, hooks: SaveHooks): Promise<{ event: CalEvent; undo: MoveUndo; clashes: BusySlot[] }> {
|
|
176
|
+
const e = await repo.getEvent(id);
|
|
177
|
+
if (!e) throw new ApiError(404, 'Event not found.');
|
|
178
|
+
if (!Number.isInteger(minutes) || Math.abs(minutes) > 1440 * 3660) throw new ApiError(400, 'Pick a day to move it to.');
|
|
179
|
+
const { changeFromDate, changeFromDefaults } = await import('./segments');
|
|
180
|
+
const from = await changeFromDefaults(repo, id, key);
|
|
181
|
+
if (!from) throw new ApiError(400, 'That date isn’t part of this event.');
|
|
182
|
+
// a moved date was dragged from where it is now
|
|
183
|
+
const moved = e.movedDates[key];
|
|
184
|
+
const r = await changeFromDate(repo, actor, id, key, { start: shiftBy(moved?.start ?? from.start, minutes), end: shiftBy(moved?.end ?? from.end, minutes) }, hooks);
|
|
185
|
+
// undo puts the segments back, from the earliest one that changed
|
|
186
|
+
const same = (a: SeriesSegment, list: SeriesSegment[]) => list.some((x) => JSON.stringify(x) === JSON.stringify(a));
|
|
187
|
+
const changed = [...r.event.segments.filter((g) => !same(g, e.segments)), ...e.segments.filter((g) => !same(g, r.event.segments))].map((g) => g.from).sort();
|
|
188
|
+
return { event: r.event, clashes: r.clashes, undo: { kind: 'following', id, from: changed[0] ?? key, previous: e.segments } };
|
|
189
|
+
}
|
|
190
|
+
|
|
162
191
|
export async function undoMove(repo: ContentRepository, actor: MoveActor, undo: MoveUndo, hooks: SaveHooks): Promise<void> {
|
|
163
192
|
if (undo.kind === 'series') {
|
|
164
193
|
await moveSeries(repo, undo.id, undo.days, hooks, undo.minutes ?? 0);
|
|
165
194
|
return;
|
|
166
195
|
}
|
|
196
|
+
if (undo.kind === 'following') {
|
|
197
|
+
const { restoreSegments } = await import('./segments');
|
|
198
|
+
await restoreSegments(repo, actor, undo.id, undo.from, undo.previous, hooks);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
167
201
|
if (undo.kind === 'date') {
|
|
168
202
|
if (undo.previous) await moveOccurrence(repo, actor, undo.id, undo.key, { ...undo.previous, roomIds: undo.previous.roomIds ?? [], location: undo.previous.location ?? null }, hooks);
|
|
169
203
|
else await moveOccurrenceBack(repo, actor, undo.id, undo.key, hooks);
|