@acms-subir/core 0.3.57 → 0.3.59
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 +30 -0
- package/cli/migrations.generated.mjs +9 -2
- package/docs/calendar.md +19 -2
- package/docs/google-calendar.md +22 -9
- package/package.json +1 -1
- package/src/lib/booking/service.ts +4 -2
- package/src/lib/calendar/api.ts +7 -7
- package/src/lib/calendar/data.ts +8 -6
- package/src/lib/calendar/google/inbound.ts +245 -45
- package/src/lib/calendar/google/map.ts +54 -14
- package/src/lib/calendar/google/match.ts +3 -2
- package/src/lib/calendar/google/outbound.ts +71 -25
- package/src/lib/calendar/ics-write.ts +3 -2
- 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 +2 -2
- package/src/lib/display/board.ts +6 -5
- package/src/lib/editor/schedule.ts +2 -0
- package/src/lib/mobile/calendar.ts +5 -5
- package/src/lib/mobile/search.ts +2 -1
- package/src/lib/repo/migrations.ts +8 -1
- package/src/lib/repo/sql.ts +10 -5
- package/src/lib/schema.ts +10 -0
- package/src/lib/sections.ts +2 -1
- package/src/pages/admin/calendar/resources/[id].astro +11 -0
- package/src/pages/members/events/index.astro +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @acms-subir/core
|
|
2
2
|
|
|
3
|
+
## 0.3.59
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fa476b6: The Google Calendar sync now maps Google's "this and following" to "from this date on" (C4), both ways.
|
|
8
|
+
|
|
9
|
+
- A split in Google changes the site event from that date on (`changeFromDate` as `GOOGLE_SYNC`). It stays one event, with the same page, hosts and RSVPs, and sign-ups move to the nearest new date with the "new time" email. The new Google series becomes that later part's pair. Its later time, rule or place edits change the part again, and its single dates are cancelled and moved like any date.
|
|
10
|
+
- A change from a date on, made on the site, gives the later part its own Google series. The first series gets an UNTIL before that day, as in the site's own feeds. Undo removes the later series again.
|
|
11
|
+
- Deleting the later series in Google, or ending a series sooner, cancels those dates on the site from the first one on (`cancelFromDate`). They're struck through, their rooms are freed, and they can be restored. Restoring them on either side brings them back on the other.
|
|
12
|
+
- At the first match, a split Google already has is recorded as it is (Q18). Matching now pairs a Google series that was split with its site twin.
|
|
13
|
+
- Before this, splits were only listed for staff, and both Google series stayed linked to the one site event. Those pairs carry on: the next change on either side applies the new way.
|
|
14
|
+
|
|
15
|
+
No migration. Nothing to do after updating.
|
|
16
|
+
|
|
17
|
+
## 0.3.58
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- c26c7c1: Rooms can have a name in each of the site's other languages. Admin → Calendar → Rooms → a room shows **Name in Español** (one field per extra language) under its name, and the editor API takes `translations: { es: { name } }`. Each place that names a room uses the name in its language, and falls back to the main name:
|
|
22
|
+
|
|
23
|
+
- public event data: `rooms` in `/api/v1/events`, `/api/v1/events/:slug` and search, following `?locale=` (else the site's main language);
|
|
24
|
+
- calendar views and cards, including "Reserved" blocks, and section event items on Spanish pages;
|
|
25
|
+
- the `/request` form and `GET /api/v1/requests/options?locale=`;
|
|
26
|
+
- the TV display (its room columns and legend);
|
|
27
|
+
- the hosts' My events;
|
|
28
|
+
- request emails and the request status page, in the requester's language;
|
|
29
|
+
- booking emails, in the booker's language.
|
|
30
|
+
|
|
31
|
+
The slug stays the same in every language. Staff screens, staff emails and `GET /api/v1/rooms/free` keep the main name. `roomName(room, locale)` in `lib/schema.ts` picks the name, and `es-MX` uses the `es` name. Migration 71 adds `resources.translations` (`'{}'` for existing rooms); changing it refreshes cached pages and API answers. Backups and environment sync carry it.
|
|
32
|
+
|
|
3
33
|
## 0.3.57
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
|
@@ -1363,13 +1363,20 @@ var MIGRATIONS = [
|
|
|
1363
1363
|
GCAL_MIGRATION,
|
|
1364
1364
|
// a series changed from one date on (CalEvent.segments): a column on the
|
|
1365
1365
|
// row, so the triggers on events see it, like moved_dates
|
|
1366
|
-
SEGMENTS_MIGRATION(contentVersionTriggers("events", [...CONTENT_VERSION_COLUMNS.events, "cancelled_dates", "moved_dates", "segments"])[2])
|
|
1366
|
+
SEGMENTS_MIGRATION(contentVersionTriggers("events", [...CONTENT_VERSION_COLUMNS.events, "cancelled_dates", "moved_dates", "segments"])[2]),
|
|
1367
|
+
// a room's name in other languages (Resource.translations): public, so a
|
|
1368
|
+
// change refreshes cached pages and API answers
|
|
1369
|
+
{ version: 71, statements: [
|
|
1370
|
+
`ALTER TABLE resources ADD COLUMN translations TEXT NOT NULL DEFAULT '{}'`,
|
|
1371
|
+
`DROP TRIGGER content_version_resources_update`,
|
|
1372
|
+
contentVersionTriggers("resources", [...CONTENT_VERSION_COLUMNS.resources, "color", "show_on_events", "translations"])[2]
|
|
1373
|
+
] }
|
|
1367
1374
|
];
|
|
1368
1375
|
var CURRENT_CONTENT_VERSION_COLUMNS = {
|
|
1369
1376
|
...CONTENT_VERSION_COLUMNS,
|
|
1370
1377
|
settings: [...CONTENT_VERSION_COLUMNS.settings, "links_verified", "links_avatar_shape", "links_avatar_fit", "links_avatar_scale", "legal"],
|
|
1371
1378
|
events: [...CONTENT_VERSION_COLUMNS.events, "cancelled_dates", "moved_dates", "segments"],
|
|
1372
|
-
resources: [...CONTENT_VERSION_COLUMNS.resources, "color", "show_on_events"],
|
|
1379
|
+
resources: [...CONTENT_VERSION_COLUMNS.resources, "color", "show_on_events", "translations"],
|
|
1373
1380
|
announcements: ANNOUNCEMENT_VISIBLE_COLUMNS
|
|
1374
1381
|
};
|
|
1375
1382
|
var LATEST_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
package/docs/calendar.md
CHANGED
|
@@ -283,6 +283,20 @@ Each room also has:
|
|
|
283
283
|
names the room. When it's off, only staff see that an event uses the room.
|
|
284
284
|
- **Anyone can check if it’s free** (`requestable`, off by default): the room answers
|
|
285
285
|
the public free/busy check below.
|
|
286
|
+
- **Name in Español** (and each of the site's other languages; `translations: { es: {
|
|
287
|
+
name } }`): the room's name on pages, app answers, TV screens and emails in that
|
|
288
|
+
language. Without one, the main name shows. The slug stays the same in every
|
|
289
|
+
language. Site code reads it with `roomName(room, locale)` from the schema.
|
|
290
|
+
|
|
291
|
+
Where a room's name follows the language:
|
|
292
|
+
- public event data (`rooms`, with `?locale=` in the app API);
|
|
293
|
+
- calendar views, cards and section items;
|
|
294
|
+
- the request form and `GET /api/v1/requests/options`;
|
|
295
|
+
- the TV display;
|
|
296
|
+
- My events;
|
|
297
|
+
- request emails and booking emails, in the person's language.
|
|
298
|
+
|
|
299
|
+
The admin and staff emails use the main name.
|
|
286
300
|
|
|
287
301
|
### Rooms in event data
|
|
288
302
|
|
|
@@ -426,8 +440,11 @@ calendars in step, both ways, within a few minutes. The full guide is
|
|
|
426
440
|
- **Booked appointments** from Google booking pages come in as "Booked" with the
|
|
427
441
|
internal tag `#google-booking` (`GOOGLE_BOOKING_TAG`), holding the link's room. Treat
|
|
428
442
|
them as bookings: the tag never shows publicly.
|
|
429
|
-
- **"This and following"**
|
|
430
|
-
|
|
443
|
+
- **"This and following"** maps to "from this date on", both ways:
|
|
444
|
+
- A Google split changes the event from that date on (`changeFromDate` as `GOOGLE_SYNC`).
|
|
445
|
+
- A change from a date on gives the later part its own Google series.
|
|
446
|
+
- Deleting the later series in Google, or ending a series sooner, cancels those dates
|
|
447
|
+
(`cancelFromDate`), so they stay restorable.
|
|
431
448
|
- Site code can read an event's Google copies with `googleCopies(repo, eventId)`, and
|
|
432
449
|
Recent changes with `googleSyncHistory(repo)`.
|
|
433
450
|
|
package/docs/google-calendar.md
CHANGED
|
@@ -185,12 +185,28 @@ A series is one event with a rule on both sides. Single dates:
|
|
|
185
185
|
taken room only warns. A date moved on the site moves that Google instance.
|
|
186
186
|
- **Per-date titles or descriptions** from Google aren't applied, because the
|
|
187
187
|
site keeps one title for every date. They're listed under Recent changes.
|
|
188
|
-
- **"This and following"**
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
- **"This and following"** maps to "from this date on", both ways.
|
|
189
|
+
- **In Google:** a split makes a second Google series. The site changes the
|
|
190
|
+
same event from that date on (`changeFromDate`, as `GOOGLE_SYNC`), so its
|
|
191
|
+
page, hosts and RSVPs stay. Sign-ups move to the nearest new date (with the
|
|
192
|
+
"new time" email). The date is the one Google split at: its id ends in
|
|
193
|
+
`_R` and that date's original start.
|
|
194
|
+
- **Pairs:** the new Google series becomes the later part's pair. Its later
|
|
195
|
+
time, rule or place edits change that part again. Its single dates are
|
|
196
|
+
cancelled and moved like any date. Its own title or description isn't kept,
|
|
197
|
+
because the site keeps one for the whole series.
|
|
198
|
+
- **On the site:** a change from a date on gives the later part its own
|
|
199
|
+
Google series (its hidden link names the part), and the first series gets
|
|
200
|
+
an UNTIL before that day, as in the site's feeds. **Undo** removes the
|
|
201
|
+
later series again.
|
|
202
|
+
- **Deleting:** deleting the later series in Google, or ending a series
|
|
203
|
+
sooner, cancels those dates on the site from the first one on
|
|
204
|
+
(`cancelFromDate`): struck through, rooms freed, restorable (Q6).
|
|
205
|
+
Restoring them, in Google or on the site, brings them back on the other
|
|
206
|
+
side. Dates cancelled from a date on, on the site, are removed from
|
|
207
|
+
Google, like a cancelled date's instance.
|
|
208
|
+
- **First match:** a split Google already has is recorded as it is. Neither
|
|
209
|
+
side changes until one of them does (Q18).
|
|
194
210
|
|
|
195
211
|
## Clashes
|
|
196
212
|
|
|
@@ -284,9 +300,6 @@ insert, update and delete of `events`) and `gcal_log`.
|
|
|
284
300
|
|
|
285
301
|
- **Google push notifications** (`events.watch`) for near-instant updates.
|
|
286
302
|
Reading every 2 minutes is the mechanism.
|
|
287
|
-
- **"This and following"** as a real change on the site, once "from this date
|
|
288
|
-
on" exists. Until then, splits are listed and both Google series stay linked
|
|
289
|
-
to the one site event.
|
|
290
303
|
|
|
291
304
|
## Troubleshooting
|
|
292
305
|
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import { collectResponse, displayAnswers } from '../forms/service';
|
|
|
5
5
|
// calendar immediately); the rest wait as "pending" for staff.
|
|
6
6
|
import type { ContentRepository } from '../repo/types';
|
|
7
7
|
import type { Booking, BookingLink, BookingStatus, CalEvent, Resource, Settings } from '../schema';
|
|
8
|
+
import { roomName } from '../schema';
|
|
8
9
|
import { loadFeed, expandVEvents } from '../calendar/ics';
|
|
9
10
|
import { emitWebhook } from '../webhooks';
|
|
10
11
|
import { parseFrom } from '../email/config';
|
|
@@ -108,8 +109,9 @@ async function describe(repo: ContentRepository, b: Booking, settings: Settings,
|
|
|
108
109
|
const tz = resource ? resourceZone(resource, settings.timezone) : settings.timezone || 'UTC';
|
|
109
110
|
return {
|
|
110
111
|
resource, link, event: null as CalEvent | null, tz,
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
// the room in the booking's language (its main name without one)
|
|
113
|
+
title: [resource && roomName(resource, locale), link?.name].filter(Boolean).join(' · ') || 'Booking',
|
|
114
|
+
location: resource ? roomName(resource, locale) : '',
|
|
113
115
|
when: whenText(b, tz, settings, link?.mode === 'day'),
|
|
114
116
|
};
|
|
115
117
|
}
|
package/src/lib/calendar/api.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// everywhere. Drafts, internal ids of private calendars, staging data and
|
|
4
4
|
// private custom fields (unless a staff caller asks for them) never appear
|
|
5
5
|
// here. Rooms are named only when set to show on public events (or for staff).
|
|
6
|
-
import { roomColor, type CalEvent, type Calendar, type MovedDate, type Resource } from '../schema';
|
|
6
|
+
import { roomColor, roomName, type CalEvent, type Calendar, type MovedDate, type Resource } from '../schema';
|
|
7
7
|
import { calendarFeedUrl, eventIcsUrl, eventUrl, publicEventFields } from './config';
|
|
8
8
|
import { describeRepeat } from './rrule';
|
|
9
9
|
import { localizeForm } from '../forms/localize';
|
|
@@ -67,11 +67,11 @@ export function movedDatesToJson(e: Pick<CalEvent, 'movedDates'>, includePrivate
|
|
|
67
67
|
return Object.fromEntries(Object.entries(e.movedDates ?? {}).map(([k, m]) => [k, includePrivate ? m : { start: m.start, end: m.end, note: m.note }]));
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/** Rooms as public data: only those set to show on public events, unless `includePrivate` (staff). Unknown ids are skipped. */
|
|
71
|
-
export function roomsToJson(ids: string[], resources: Map<string, Resource> | undefined, includePrivate = false): RoomJson[] {
|
|
70
|
+
/** Rooms as public data: only those set to show on public events, unless `includePrivate` (staff); named in `locale` when they have a name in it. Unknown ids are skipped. */
|
|
71
|
+
export function roomsToJson(ids: string[], resources: Map<string, Resource> | undefined, includePrivate = false, locale?: string | null): RoomJson[] {
|
|
72
72
|
if (!resources) return [];
|
|
73
73
|
return ids.map((id) => resources.get(id)).filter((r): r is Resource => Boolean(r) && (includePrivate || r!.showOnEvents))
|
|
74
|
-
.map((r) => ({ slug: r.slug, name: r
|
|
74
|
+
.map((r) => ({ slug: r.slug, name: roomName(r, locale), color: roomColor(r) || null }));
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
const abs = (origin: string, path: string) => (/^https?:\/\//.test(path) ? path : origin + path);
|
|
@@ -84,7 +84,7 @@ const untilDate = (locale: string) => (ymd: string) => {
|
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
// resources: every room by id (rooms stay [] without it); locale: the
|
|
87
|
-
// language of the repeat line ("Cada semana el sábado"; English without it)
|
|
87
|
+
// language of the repeat line and the room names ("Cada semana el sábado"; English without it)
|
|
88
88
|
export function eventToJson(e: CalEvent, origin: string, calendars: Map<string, Calendar>, siteTz = '', opts: { includePrivate?: boolean; resources?: Map<string, Resource>; locale?: string } = {}): EventJson {
|
|
89
89
|
return {
|
|
90
90
|
id: e.id,
|
|
@@ -106,7 +106,7 @@ export function eventToJson(e: CalEvent, origin: string, calendars: Map<string,
|
|
|
106
106
|
segments: seriesParts(e).slice(1).map((p) => ({
|
|
107
107
|
from: p.from, start: p.start, end: p.end, rrule: p.rrule, repeats: describeRepeat(p.rrule, p.start, opts.locale ? untilDate(opts.locale) : undefined, opts.locale), cancelled: p.cancelled, note: p.note,
|
|
108
108
|
location: p.location ? { name: p.location.name, address: p.location.address, url: p.location.url } : null,
|
|
109
|
-
rooms: roomsToJson(p.roomIds, opts.resources, opts.includePrivate),
|
|
109
|
+
rooms: roomsToJson(p.roomIds, opts.resources, opts.includePrivate, opts.locale),
|
|
110
110
|
})), exdates: e.exdates,
|
|
111
111
|
cancelledDates: e.cancelledDates ?? [],
|
|
112
112
|
movedDates: movedDatesToJson(e, opts.includePrivate),
|
|
@@ -121,7 +121,7 @@ export function eventToJson(e: CalEvent, origin: string, calendars: Map<string,
|
|
|
121
121
|
.map((id) => calendars.get(id))
|
|
122
122
|
.filter((c): c is Calendar => Boolean(c) && c!.visibility !== 'private')
|
|
123
123
|
.map((c) => ({ id: c.id, slug: c.slug, name: c.name, color: c.color })),
|
|
124
|
-
rooms: roomsToJson(e.roomHold.resourceIds, opts.resources, opts.includePrivate),
|
|
124
|
+
rooms: roomsToJson(e.roomHold.resourceIds, opts.resources, opts.includePrivate, opts.locale),
|
|
125
125
|
fields: opts.includePrivate ? e.fields : publicEventFields(e.fields),
|
|
126
126
|
updatedAt: new Date(e.updatedAt).toISOString(),
|
|
127
127
|
};
|
package/src/lib/calendar/data.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// details, lib/display: published events only, private calendars when it
|
|
8
8
|
// names them, bookings with the booker's name, never emails, phones or notes).
|
|
9
9
|
import type { ContentRepository } from '../repo/types';
|
|
10
|
-
import { roomColor, type BookingLink, type CalEvent, type Calendar, type EventLocation, type Resource, type Settings } from '../schema';
|
|
10
|
+
import { roomColor, roomName, type BookingLink, type CalEvent, type Calendar, type EventLocation, type Resource, type Settings } from '../schema';
|
|
11
11
|
import { eventIsListed, eventIsPublic, eventRoomIds, expandEvents, locationLine, safeColor, type OccurrenceMove } from './events';
|
|
12
12
|
import { expandVEvents, loadFeed, tzParts, type CalendarEvent, type CalendarWindow } from './ics';
|
|
13
13
|
import { CALENDAR, eventUrl, resourceUrl, withoutPrivateFields } from './config';
|
|
@@ -107,7 +107,7 @@ export async function collectViewEvents(
|
|
|
107
107
|
const rooms = (ids: string[]) => ids.map((id) => resources.get(id))
|
|
108
108
|
// staff views and screens with private details name every room
|
|
109
109
|
.filter((r): r is Resource => Boolean(r) && (scope.audience !== 'public' || r!.showOnEvents))
|
|
110
|
-
.map((r) => ({ id: r.id, slug: r.slug, name: r.
|
|
110
|
+
.map((r) => ({ id: r.id, slug: r.slug, name: roomName(r, scope.locale), color: roomColor(r) }));
|
|
111
111
|
const words = eventStrings(scope.locale).calendar;
|
|
112
112
|
for (const o of expandEvents(natives, settings, win)) {
|
|
113
113
|
// rooms are per date (a moved date can change them)
|
|
@@ -176,14 +176,16 @@ export async function collectViewEvents(
|
|
|
176
176
|
const r = byId.get(b.resourceId);
|
|
177
177
|
if (!r) continue;
|
|
178
178
|
const allDay = b.end - b.start >= 86_400_000 - 3_600_000;
|
|
179
|
+
// the room in the view's language (its main name without one)
|
|
180
|
+
const room = roomName(r, scope.locale);
|
|
179
181
|
const title = scope.audience === 'admin' ? `${r.name} · ${b.name}`
|
|
180
|
-
: scope.audience === 'screen' ? [linkName.get(b.linkId) ||
|
|
181
|
-
: `${words.reserved} · ${
|
|
182
|
+
: scope.audience === 'screen' ? [linkName.get(b.linkId) || room, b.name].filter(Boolean).join(' · ')
|
|
183
|
+
: `${words.reserved} · ${room}`;
|
|
182
184
|
events.push({
|
|
183
185
|
start: b.start, end: b.end, allDay, days: allDay ? Math.max(1, Math.round((b.end - b.start) / 86_400_000)) : 1,
|
|
184
|
-
title, location:
|
|
186
|
+
title, location: room, description: '', cal: inScope.length, color: '#8e919a', calendarName: room,
|
|
185
187
|
url: scope.audience === 'public' ? resourceUrl(r.slug) : undefined,
|
|
186
|
-
rooms: [{ id: r.id, slug: r.slug, name:
|
|
188
|
+
rooms: [{ id: r.id, slug: r.slug, name: room, color: roomColor(r) }],
|
|
187
189
|
});
|
|
188
190
|
}
|
|
189
191
|
}
|