@aglyn/plugins-events-calendar 1.0.0-beta.246 → 1.0.0-beta.249

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/README.md CHANGED
@@ -35,7 +35,7 @@ This package is a first-party Aglyn plugin. It is loaded through Aglyn's plugin
35
35
  - `events/list`: published events for the Event List component, sorted by start, with a `mode=past` option. Drafts never leave the server.
36
36
  - `events/dispatch`: receives a site event whose client-side trigger fired on the page, so that the action's server steps run.
37
37
 
38
- `registerEventsCalendarConsoleServerDeclarations()` (the `consoleServerDeclarations` surface) registers the server half of the events import and export, and the `event` draft writer with the `event` build operation an Aglyn AI build uses to draft dated events and place the Event List on a page. A drafted event is listed on the site only once a person publishes it.
38
+ `registerEventsCalendarConsoleServerDeclarations()` (the `consoleServerDeclarations` surface) registers the server half of the events import and export, and the `event` draft writer with the `event` build operation an Aglyn AI build or guided site start uses to make dated events and place the Event List on a page. An event is published for a member with the publishing role (admin or editor) and kept as a draft, with a notice, for an author; a new event with no cover gets one from the stock photo picker (`core.stock-photo-picker`) when a plugin registers one.
39
39
 
40
40
  Routes are served by the host app's API dispatcher under `/api/`, for example `/api/events/list`. There is no `consoleApi` registrar.
41
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-events-calendar",
3
- "version": "1.0.0-beta.246",
3
+ "version": "1.0.0-beta.249",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,14 +25,14 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.246",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.246",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.246",
31
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.246",
32
- "@aglyn/shared-util-timestamp": "1.0.0-beta.246",
33
- "@aglyn/tenant-data-admin": "1.0.0-beta.246",
34
- "@aglyn/tenant-feature-instance": "1.0.0-beta.246",
35
- "@aglyn/tenant-runtime": "1.0.0-beta.246",
28
+ "@aglyn/aglyn": "1.0.0-beta.249",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.249",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.249",
31
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.249",
32
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.249",
33
+ "@aglyn/tenant-data-admin": "1.0.0-beta.249",
34
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.249",
35
+ "@aglyn/tenant-runtime": "1.0.0-beta.249",
36
36
  "@swc/helpers": "0.5.23"
37
37
  },
38
38
  "peerDependencies": {
@@ -37,6 +37,8 @@ import { type EventWriteInput } from './event-write';
37
37
  export declare const EVENT_DRAFT_RESOURCE = "event";
38
38
  /** A wall time as the content states it: `YYYY-MM-DDTHH:MM`. */
39
39
  export declare const EVENT_DRAFT_WALL_TIME: RegExp;
40
+ /** A cover address the content may carry: https, or a path on the site. */
41
+ export declare const EVENT_DRAFT_COVER_ADDRESS: RegExp;
40
42
  /** The longest cover image address the content may carry. */
41
43
  export declare const EVENT_DRAFT_COVER_MAX_LENGTH = 2048;
42
44
  /** What a caller sends as `content`. */
@@ -33,6 +33,7 @@ import { EVENT_COVER_ALT_MAX_LENGTH, EVENT_DESCRIPTION_MAX_LENGTH, EVENT_LOCATIO
33
33
  * caller cannot ask for one: the content's keys are the event's own.
34
34
  */ /** The resource name the writer is registered under. */ export const EVENT_DRAFT_RESOURCE = 'event';
35
35
  /** A wall time as the content states it: `YYYY-MM-DDTHH:MM`. */ export const EVENT_DRAFT_WALL_TIME = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/;
36
+ /** A cover address the content may carry: https, or a path on the site. */ export const EVENT_DRAFT_COVER_ADDRESS = /^(https:\/\/|\/)\S+$/;
36
37
  /** The longest cover image address the content may carry. */ export const EVENT_DRAFT_COVER_MAX_LENGTH = 2048;
37
38
  /** Text on one line: controls and runs of space folded to one space. */ function line(value) {
38
39
  return typeof value === 'string' ? value.replace(RegExp("[\\u0000-\\u001F\\u007F-\\u009F\\s]+", "gu"), ' ').trim() : '';
@@ -91,7 +92,7 @@ function optional(content, key, label, max, problems, multiline = false) {
91
92
  const organizer = optional(content, 'organizer', 'organizer', EVENT_ORGANIZER_MAX_LENGTH, problems);
92
93
  const description = optional(content, 'description', 'description', EVENT_DESCRIPTION_MAX_LENGTH, problems, true);
93
94
  const coverImage = optional(content, 'coverImage', 'cover image address', EVENT_DRAFT_COVER_MAX_LENGTH, problems);
94
- if (coverImage && !/^(https:\/\/|\/)\S+$/.test(coverImage)) {
95
+ if (coverImage && !EVENT_DRAFT_COVER_ADDRESS.test(coverImage)) {
95
96
  problems.push('The cover image is an https address or a path on the site');
96
97
  }
97
98
  const coverImageAlt = optional(content, 'coverImageAlt', 'cover description', EVENT_COVER_ALT_MAX_LENGTH, problems);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/model/event-draft-content.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginDraftCheck } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n isValidTimeZone,\n zonedDayKey,\n zonedMinuteOfDay,\n zonedWallTimeToInstant,\n} from '@aglyn/shared-util-timestamp/zoned-time'\nimport {\n EVENT_COVER_ALT_MAX_LENGTH,\n EVENT_DESCRIPTION_MAX_LENGTH,\n EVENT_LOCATION_MAX_LENGTH,\n EVENT_ORGANIZER_MAX_LENGTH,\n EVENT_TITLE_MAX_LENGTH,\n type EventWriteInput,\n} from './event-write'\n\n/**\n * AN EVENT ANOTHER PLUGIN ASKS FOR, AS CONTENT (AGL-3743).\n *\n * What a caller of the `event` draft writer sends, and how it is read into\n * the input `eventWrite` stores — the same rule the Events page's editor and\n * the events import write through. Pure: no Firestore, no clock, so the\n * writer's `check` answers before anything is spent.\n *\n * Times are WALL TIMES in a named zone, the way a person types them into the\n * editor's date and time boxes: `startsAt` `\"2026-11-14T19:00\"` in\n * `\"America/Chicago\"` is the instant the event starts there. The event\n * stores only that instant (`startsAtMs`), exactly as an event the editor\n * saved; the zone is how the caller's words are read, not a stored field.\n *\n * An event has no recurrence, tickets, RSVP or online-meeting field, so a\n * caller cannot ask for one: the content's keys are the event's own.\n */\n\n/** The resource name the writer is registered under. */\nexport const EVENT_DRAFT_RESOURCE = 'event'\n\n/** A wall time as the content states it: `YYYY-MM-DDTHH:MM`. */\nexport const EVENT_DRAFT_WALL_TIME = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})$/\n\n/** The longest cover image address the content may carry. */\nexport const EVENT_DRAFT_COVER_MAX_LENGTH = 2048\n\n/** What a caller sends as `content`. */\nexport interface EventDraftContent {\n title: string\n /** `YYYY-MM-DDTHH:MM`, read in `timezone`. */\n startsAt: string\n /** `YYYY-MM-DDTHH:MM`, read in `timezone`, after the start; absent gives the editor's default length. */\n endsAt?: string\n /** The IANA zone the times are in, e.g. `America/Chicago`. */\n timezone: string\n location?: string\n organizer?: string\n description?: string\n /** An image address, as the editor's \"Cover image URL\" takes one. */\n coverImage?: string\n coverImageAlt?: string\n}\n\nexport type EventDraftContentRead =\n | { ok: true; value: EventWriteInput & { endsAtMs: number | null }; timezone: string }\n | { ok: false; problems: string[] }\n\n/** Text on one line: controls and runs of space folded to one space. */\nfunction line(value: unknown): string {\n return typeof value === 'string' ? value.replace(/[\\p{Cc}\\s]+/gu, ' ').trim() : ''\n}\n\n/**\n * The instant a wall time names in `timezone`, or `null` for one that does\n * not name a real calendar minute (month 13, February 30th, 24:30).\n */\nexport function eventDraftInstant(value: unknown, timezone: string): number | null {\n const match = typeof value === 'string' ? value.trim().match(EVENT_DRAFT_WALL_TIME) : null\n if (!match || !isValidTimeZone(timezone)) return null\n const [year, month, day, hour, minute] = match.slice(1).map(Number)\n if (month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59) return null\n // A day past the month's end would roll into the next: refused instead.\n if (day > new Date(Date.UTC(year, month, 0)).getUTCDate()) return null\n return zonedWallTimeToInstant({ year, month, day, hour, minute }, timezone)\n}\n\nfunction optional(\n content: Readonly<Record<string, unknown>>,\n key: keyof EventDraftContent,\n label: string,\n max: number,\n problems: string[],\n multiline = false,\n): string {\n const raw = content[key]\n if (raw === undefined || raw === null) return ''\n if (typeof raw !== 'string') {\n problems.push(`The ${label} is text`)\n return ''\n }\n const text = multiline ? raw.trim() : line(raw)\n if (text.length > max) problems.push(`The ${label} is longer than ${max} characters`)\n return text\n}\n\n/** The content as the writer stores it — always a draft — or every problem that stops it. */\nexport function readEventDraftContent(content: Readonly<Record<string, unknown>>): EventDraftContentRead {\n const problems: string[] = []\n const title = line(content['title'])\n if (!title) problems.push('The event needs a title')\n else if (title.length > EVENT_TITLE_MAX_LENGTH) {\n problems.push(`The title is longer than ${EVENT_TITLE_MAX_LENGTH} characters`)\n }\n const timezone = line(content['timezone'])\n const zoneOk = isValidTimeZone(timezone)\n if (!zoneOk) problems.push('The time zone is an IANA zone, such as America/Chicago')\n const startsAtMs = zoneOk ? eventDraftInstant(content['startsAt'], timezone) : null\n if (zoneOk && startsAtMs === null) {\n problems.push('The start is a date and time, written YYYY-MM-DDTHH:MM')\n }\n let endsAtMs: number | null = null\n const rawEnd = content['endsAt']\n if (rawEnd !== undefined && rawEnd !== null && rawEnd !== '' && zoneOk) {\n endsAtMs = eventDraftInstant(rawEnd, timezone)\n if (endsAtMs === null) problems.push('The end is a date and time, written YYYY-MM-DDTHH:MM')\n else if (startsAtMs !== null && endsAtMs <= startsAtMs) problems.push('The end is after the start')\n }\n const location = optional(content, 'location', 'location', EVENT_LOCATION_MAX_LENGTH, problems)\n const organizer = optional(content, 'organizer', 'organizer', EVENT_ORGANIZER_MAX_LENGTH, problems)\n const description = optional(content, 'description', 'description', EVENT_DESCRIPTION_MAX_LENGTH, problems, true)\n const coverImage = optional(content, 'coverImage', 'cover image address', EVENT_DRAFT_COVER_MAX_LENGTH, problems)\n if (coverImage && !/^(https:\\/\\/|\\/)\\S+$/.test(coverImage)) {\n problems.push('The cover image is an https address or a path on the site')\n }\n const coverImageAlt = optional(content, 'coverImageAlt', 'cover description', EVENT_COVER_ALT_MAX_LENGTH, problems)\n if (problems.length) return { ok: false, problems: [...new Set(problems)] }\n return {\n ok: true,\n timezone,\n value: {\n title,\n startsAtMs: startsAtMs as number,\n endsAtMs,\n location: location || null,\n organizer: organizer || null,\n description: description || null,\n coverImage: coverImage || null,\n coverImageAlt: coverImageAlt || null,\n status: 'draft',\n },\n }\n}\n\n/** `2026-11-14 19:00`: an instant as a wall clock in `timezone` reads it. */\nexport function eventDraftWallText(atMs: number, timezone: string): string {\n const minute = zonedMinuteOfDay(atMs, timezone)\n const pad = (value: number) => String(value).padStart(2, '0')\n return `${zonedDayKey(atMs, timezone)} ${pad(Math.floor(minute / 60))}:${pad(minute % 60)}`\n}\n\n/** Whether content is an event this plugin would store, with what it says about it. Pure. */\nexport function checkEventDraftContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readEventDraftContent(content)\n if (read.ok === false) return read\n return {\n ok: true,\n facts: {\n status: 'draft',\n startsAtMs: read.value.startsAtMs,\n timezone: read.timezone,\n startsAt: eventDraftWallText(read.value.startsAtMs, read.timezone),\n },\n }\n}\n"],"names":["isValidTimeZone","zonedDayKey","zonedMinuteOfDay","zonedWallTimeToInstant","EVENT_COVER_ALT_MAX_LENGTH","EVENT_DESCRIPTION_MAX_LENGTH","EVENT_LOCATION_MAX_LENGTH","EVENT_ORGANIZER_MAX_LENGTH","EVENT_TITLE_MAX_LENGTH","EVENT_DRAFT_RESOURCE","EVENT_DRAFT_WALL_TIME","EVENT_DRAFT_COVER_MAX_LENGTH","line","value","replace","trim","eventDraftInstant","timezone","match","year","month","day","hour","minute","slice","map","Number","Date","UTC","getUTCDate","optional","content","key","label","max","problems","multiline","raw","undefined","push","text","length","readEventDraftContent","title","zoneOk","startsAtMs","endsAtMs","rawEnd","location","organizer","description","coverImage","test","coverImageAlt","ok","Set","status","eventDraftWallText","atMs","pad","String","padStart","Math","floor","checkEventDraftContent","read","facts","startsAt"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/model/event-draft-content.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginDraftCheck } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n isValidTimeZone,\n zonedDayKey,\n zonedMinuteOfDay,\n zonedWallTimeToInstant,\n} from '@aglyn/shared-util-timestamp/zoned-time'\nimport {\n EVENT_COVER_ALT_MAX_LENGTH,\n EVENT_DESCRIPTION_MAX_LENGTH,\n EVENT_LOCATION_MAX_LENGTH,\n EVENT_ORGANIZER_MAX_LENGTH,\n EVENT_TITLE_MAX_LENGTH,\n type EventWriteInput,\n} from './event-write'\n\n/**\n * AN EVENT ANOTHER PLUGIN ASKS FOR, AS CONTENT (AGL-3743).\n *\n * What a caller of the `event` draft writer sends, and how it is read into\n * the input `eventWrite` stores — the same rule the Events page's editor and\n * the events import write through. Pure: no Firestore, no clock, so the\n * writer's `check` answers before anything is spent.\n *\n * Times are WALL TIMES in a named zone, the way a person types them into the\n * editor's date and time boxes: `startsAt` `\"2026-11-14T19:00\"` in\n * `\"America/Chicago\"` is the instant the event starts there. The event\n * stores only that instant (`startsAtMs`), exactly as an event the editor\n * saved; the zone is how the caller's words are read, not a stored field.\n *\n * An event has no recurrence, tickets, RSVP or online-meeting field, so a\n * caller cannot ask for one: the content's keys are the event's own.\n */\n\n/** The resource name the writer is registered under. */\nexport const EVENT_DRAFT_RESOURCE = 'event'\n\n/** A wall time as the content states it: `YYYY-MM-DDTHH:MM`. */\nexport const EVENT_DRAFT_WALL_TIME = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})$/\n\n/** A cover address the content may carry: https, or a path on the site. */\nexport const EVENT_DRAFT_COVER_ADDRESS = /^(https:\\/\\/|\\/)\\S+$/\n\n/** The longest cover image address the content may carry. */\nexport const EVENT_DRAFT_COVER_MAX_LENGTH = 2048\n\n/** What a caller sends as `content`. */\nexport interface EventDraftContent {\n title: string\n /** `YYYY-MM-DDTHH:MM`, read in `timezone`. */\n startsAt: string\n /** `YYYY-MM-DDTHH:MM`, read in `timezone`, after the start; absent gives the editor's default length. */\n endsAt?: string\n /** The IANA zone the times are in, e.g. `America/Chicago`. */\n timezone: string\n location?: string\n organizer?: string\n description?: string\n /** An image address, as the editor's \"Cover image URL\" takes one. */\n coverImage?: string\n coverImageAlt?: string\n}\n\nexport type EventDraftContentRead =\n | { ok: true; value: EventWriteInput & { endsAtMs: number | null }; timezone: string }\n | { ok: false; problems: string[] }\n\n/** Text on one line: controls and runs of space folded to one space. */\nfunction line(value: unknown): string {\n return typeof value === 'string' ? value.replace(/[\\p{Cc}\\s]+/gu, ' ').trim() : ''\n}\n\n/**\n * The instant a wall time names in `timezone`, or `null` for one that does\n * not name a real calendar minute (month 13, February 30th, 24:30).\n */\nexport function eventDraftInstant(value: unknown, timezone: string): number | null {\n const match = typeof value === 'string' ? value.trim().match(EVENT_DRAFT_WALL_TIME) : null\n if (!match || !isValidTimeZone(timezone)) return null\n const [year, month, day, hour, minute] = match.slice(1).map(Number)\n if (month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59) return null\n // A day past the month's end would roll into the next: refused instead.\n if (day > new Date(Date.UTC(year, month, 0)).getUTCDate()) return null\n return zonedWallTimeToInstant({ year, month, day, hour, minute }, timezone)\n}\n\nfunction optional(\n content: Readonly<Record<string, unknown>>,\n key: keyof EventDraftContent,\n label: string,\n max: number,\n problems: string[],\n multiline = false,\n): string {\n const raw = content[key]\n if (raw === undefined || raw === null) return ''\n if (typeof raw !== 'string') {\n problems.push(`The ${label} is text`)\n return ''\n }\n const text = multiline ? raw.trim() : line(raw)\n if (text.length > max) problems.push(`The ${label} is longer than ${max} characters`)\n return text\n}\n\n/** The content as the writer stores it — always a draft — or every problem that stops it. */\nexport function readEventDraftContent(content: Readonly<Record<string, unknown>>): EventDraftContentRead {\n const problems: string[] = []\n const title = line(content['title'])\n if (!title) problems.push('The event needs a title')\n else if (title.length > EVENT_TITLE_MAX_LENGTH) {\n problems.push(`The title is longer than ${EVENT_TITLE_MAX_LENGTH} characters`)\n }\n const timezone = line(content['timezone'])\n const zoneOk = isValidTimeZone(timezone)\n if (!zoneOk) problems.push('The time zone is an IANA zone, such as America/Chicago')\n const startsAtMs = zoneOk ? eventDraftInstant(content['startsAt'], timezone) : null\n if (zoneOk && startsAtMs === null) {\n problems.push('The start is a date and time, written YYYY-MM-DDTHH:MM')\n }\n let endsAtMs: number | null = null\n const rawEnd = content['endsAt']\n if (rawEnd !== undefined && rawEnd !== null && rawEnd !== '' && zoneOk) {\n endsAtMs = eventDraftInstant(rawEnd, timezone)\n if (endsAtMs === null) problems.push('The end is a date and time, written YYYY-MM-DDTHH:MM')\n else if (startsAtMs !== null && endsAtMs <= startsAtMs) problems.push('The end is after the start')\n }\n const location = optional(content, 'location', 'location', EVENT_LOCATION_MAX_LENGTH, problems)\n const organizer = optional(content, 'organizer', 'organizer', EVENT_ORGANIZER_MAX_LENGTH, problems)\n const description = optional(content, 'description', 'description', EVENT_DESCRIPTION_MAX_LENGTH, problems, true)\n const coverImage = optional(content, 'coverImage', 'cover image address', EVENT_DRAFT_COVER_MAX_LENGTH, problems)\n if (coverImage && !EVENT_DRAFT_COVER_ADDRESS.test(coverImage)) {\n problems.push('The cover image is an https address or a path on the site')\n }\n const coverImageAlt = optional(content, 'coverImageAlt', 'cover description', EVENT_COVER_ALT_MAX_LENGTH, problems)\n if (problems.length) return { ok: false, problems: [...new Set(problems)] }\n return {\n ok: true,\n timezone,\n value: {\n title,\n startsAtMs: startsAtMs as number,\n endsAtMs,\n location: location || null,\n organizer: organizer || null,\n description: description || null,\n coverImage: coverImage || null,\n coverImageAlt: coverImageAlt || null,\n status: 'draft',\n },\n }\n}\n\n/** `2026-11-14 19:00`: an instant as a wall clock in `timezone` reads it. */\nexport function eventDraftWallText(atMs: number, timezone: string): string {\n const minute = zonedMinuteOfDay(atMs, timezone)\n const pad = (value: number) => String(value).padStart(2, '0')\n return `${zonedDayKey(atMs, timezone)} ${pad(Math.floor(minute / 60))}:${pad(minute % 60)}`\n}\n\n/** Whether content is an event this plugin would store, with what it says about it. Pure. */\nexport function checkEventDraftContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readEventDraftContent(content)\n if (read.ok === false) return read\n return {\n ok: true,\n facts: {\n status: 'draft',\n startsAtMs: read.value.startsAtMs,\n timezone: read.timezone,\n startsAt: eventDraftWallText(read.value.startsAtMs, read.timezone),\n },\n }\n}\n"],"names":["isValidTimeZone","zonedDayKey","zonedMinuteOfDay","zonedWallTimeToInstant","EVENT_COVER_ALT_MAX_LENGTH","EVENT_DESCRIPTION_MAX_LENGTH","EVENT_LOCATION_MAX_LENGTH","EVENT_ORGANIZER_MAX_LENGTH","EVENT_TITLE_MAX_LENGTH","EVENT_DRAFT_RESOURCE","EVENT_DRAFT_WALL_TIME","EVENT_DRAFT_COVER_ADDRESS","EVENT_DRAFT_COVER_MAX_LENGTH","line","value","replace","trim","eventDraftInstant","timezone","match","year","month","day","hour","minute","slice","map","Number","Date","UTC","getUTCDate","optional","content","key","label","max","problems","multiline","raw","undefined","push","text","length","readEventDraftContent","title","zoneOk","startsAtMs","endsAtMs","rawEnd","location","organizer","description","coverImage","test","coverImageAlt","ok","Set","status","eventDraftWallText","atMs","pad","String","padStart","Math","floor","checkEventDraftContent","read","facts","startsAt"],"mappings":"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"}
@@ -16,8 +16,9 @@
16
16
  */
17
17
  import { type PluginAiCapability, type PluginAiCapabilityArgs } from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities';
18
18
  /**
19
- * WHAT AN AI BUILD CAN MAKE IN EVENTS (AGL-3743): a dated event, as a draft,
20
- * and the Event List block that shows the site's events on a page.
19
+ * WHAT AN AI BUILD CAN MAKE IN EVENTS (AGL-3743): a dated event, published
20
+ * when the member may publish (AGL-3755), and the Event List block that
21
+ * shows the site's events on a page.
21
22
  *
22
23
  * The capability the AI plugin's build planner offers for "add our fall
23
24
  * workshops" or "an events page". Its item is written by this plugin's
@@ -25,7 +26,9 @@ import { type PluginAiCapability, type PluginAiCapabilityArgs } from '@aglyn/agl
25
26
  * add-on, the member's role, a start after the write, and the shape
26
27
  * `eventWrite` stores. One item is one event; a page section that uses any
27
28
  * of them places the Event List block, which lists the site's published
28
- * events, so the drafts appear there once a person publishes them.
29
+ * events, so the events show there at once. A member who may write but not
30
+ * publish gets drafts, and the job row says so (`live` with the writer's
31
+ * `live: false`).
29
32
  *
30
33
  * The arguments are flat, as the contract requires, and are the event's own
31
34
  * fields: an event has no recurrence, tickets, RSVP or meeting link, so none
@@ -18,8 +18,9 @@ import { BUNDLE_ID } from "../constants/bundle-common.js";
18
18
  import { EVENT_DRAFT_RESOURCE } from "../model/event-draft-content.js";
19
19
  import { EVENT_DESCRIPTION_MAX_LENGTH, EVENT_LOCATION_MAX_LENGTH, EVENT_ORGANIZER_MAX_LENGTH, EVENT_TITLE_MAX_LENGTH } from "../model/event-write.js";
20
20
  /**
21
- * WHAT AN AI BUILD CAN MAKE IN EVENTS (AGL-3743): a dated event, as a draft,
22
- * and the Event List block that shows the site's events on a page.
21
+ * WHAT AN AI BUILD CAN MAKE IN EVENTS (AGL-3743): a dated event, published
22
+ * when the member may publish (AGL-3755), and the Event List block that
23
+ * shows the site's events on a page.
23
24
  *
24
25
  * The capability the AI plugin's build planner offers for "add our fall
25
26
  * workshops" or "an events page". Its item is written by this plugin's
@@ -27,7 +28,9 @@ import { EVENT_DESCRIPTION_MAX_LENGTH, EVENT_LOCATION_MAX_LENGTH, EVENT_ORGANIZE
27
28
  * add-on, the member's role, a start after the write, and the shape
28
29
  * `eventWrite` stores. One item is one event; a page section that uses any
29
30
  * of them places the Event List block, which lists the site's published
30
- * events, so the drafts appear there once a person publishes them.
31
+ * events, so the events show there at once. A member who may write but not
32
+ * publish gets drafts, and the job row says so (`live` with the writer's
33
+ * `live: false`).
31
34
  *
32
35
  * The arguments are flat, as the contract requires, and are the event's own
33
36
  * fields: an event has no recurrence, tickets, RSVP or meeting link, so none
@@ -59,9 +62,9 @@ const WALL_TIME = 'local date and time "YYYY-MM-DDTHH:MM", in timezone';
59
62
  export const eventAiCapability = {
60
63
  op: EVENT_AI_OP,
61
64
  noun: 'event',
62
- where: 'Events, as a draft to publish',
65
+ where: 'Events',
63
66
  intents: [
64
- 'dated events on the site’s events calendar — classes, workshops, tastings, shows, open houses, meetups — each with its time, place and description, drafted for the owner to publish; use this rather than a dataset for anything held on a date',
67
+ 'dated events on the site’s events calendar — classes, workshops, tastings, shows, open houses, meetups — each with its time, place and description, published to the site’s Event List; use this rather than a dataset for anything held on a date',
65
68
  'an events page or section that lists the site’s upcoming events'
66
69
  ],
67
70
  argsSchema: {
@@ -116,6 +119,8 @@ export const eventAiCapability = {
116
119
  freeAllowed: true,
117
120
  feature: 'eventCalendar',
118
121
  draftResource: EVENT_DRAFT_RESOURCE,
122
+ // Published as the Events page publishes, for the publishing role (AGL-3755).
123
+ live: true,
119
124
  // Written without a model: the planner already filled the arguments.
120
125
  estimateCredits: ()=>0,
121
126
  degrade: 'omit',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/server/event-ai-capability.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n registerPluginAiCapability,\n type PluginAiCapability,\n type PluginAiCapabilityArgs,\n} from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { EVENT_DRAFT_RESOURCE } from '../model/event-draft-content'\nimport {\n EVENT_DESCRIPTION_MAX_LENGTH,\n EVENT_LOCATION_MAX_LENGTH,\n EVENT_ORGANIZER_MAX_LENGTH,\n EVENT_TITLE_MAX_LENGTH,\n} from '../model/event-write'\n\n/**\n * WHAT AN AI BUILD CAN MAKE IN EVENTS (AGL-3743): a dated event, as a draft,\n * and the Event List block that shows the site's events on a page.\n *\n * The capability the AI plugin's build planner offers for \"add our fall\n * workshops\" or \"an events page\". Its item is written by this plugin's\n * `event` draft writer, so every rule is the writer's: the `eventCalendar`\n * add-on, the member's role, a start after the write, and the shape\n * `eventWrite` stores. One item is one event; a page section that uses any\n * of them places the Event List block, which lists the site's published\n * events, so the drafts appear there once a person publishes them.\n *\n * The arguments are flat, as the contract requires, and are the event's own\n * fields: an event has no recurrence, tickets, RSVP or meeting link, so none\n * is offered. The planner fills them in, so no model runs\n * (`estimateCredits` 0).\n */\n\n/** The operation's name in a build plan. */\nexport const EVENT_AI_OP = 'event'\n\n/**\n * The Event List's component id, as the page palette names the block. Spelled\n * here rather than imported: the component module brings React and MUI, and\n * the console server's boot registers this capability.\n */\nexport const EVENT_AI_PAGE_BLOCK = 'eventList'\n\n/** How many events one build may draft. */\nexport const EVENT_AI_MAX_PER_PLAN = 6\n\nconst WALL_TIME = 'local date and time \"YYYY-MM-DDTHH:MM\", in timezone'\n\n/** The writer's content for an item: the arguments, which are the content's own keys. Pure. */\nexport function eventDraftContentFromArgs(args: PluginAiCapabilityArgs): Readonly<Record<string, unknown>> {\n const keys = ['title', 'startsAt', 'endsAt', 'timezone', 'location', 'organizer', 'description']\n return Object.fromEntries(\n keys\n .filter((key) => typeof args[key] === 'string' && (args[key] as string).trim())\n .map((key) => [key, (args[key] as string).trim()]),\n )\n}\n\nexport const eventAiCapability: PluginAiCapability = {\n op: EVENT_AI_OP,\n noun: 'event',\n where: 'Events, as a draft to publish',\n intents: [\n 'dated events on the site’s events calendar — classes, workshops, tastings, shows, open houses, meetups — each with its time, place and description, drafted for the owner to publish; use this rather than a dataset for anything held on a date',\n 'an events page or section that lists the site’s upcoming events',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n title: {\n type: 'string',\n description:\n 'What the event is called, specific to this business, e.g. \"Autumn sourdough workshop\" — never a placeholder such as \"Event 1\" or \"Sample event\".',\n maxLength: EVENT_TITLE_MAX_LENGTH,\n },\n startsAt: {\n type: 'string',\n description:\n `When it starts, as ${WALL_TIME}. After today's date, within the next few months, on a day and at an hour this business would hold it; a date the request gives, as given.`,\n maxLength: 16,\n },\n endsAt: {\n type: 'string',\n description: `When it ends, as ${WALL_TIME}, after the start; leave it out for a one-hour event.`,\n maxLength: 16,\n },\n timezone: {\n type: 'string',\n description: 'The IANA time zone the business is in, e.g. \"America/Chicago\".',\n maxLength: 64,\n },\n location: {\n type: 'string',\n description:\n 'Where it happens, as a visitor reads it: the venue or address the request gives, or the business’s own place; \"Online\" for an online event. Leave it out rather than invent an address.',\n maxLength: EVENT_LOCATION_MAX_LENGTH,\n },\n organizer: {\n type: 'string',\n description: 'Who hosts it: the business’s name, or a host the request names. Never a person’s name the request does not give.',\n maxLength: EVENT_ORGANIZER_MAX_LENGTH,\n },\n description: {\n type: 'string',\n description:\n 'Two to four sentences a visitor reads: what happens, who it is for and what to bring or expect, in the business’s voice. No price, ticket, sign-up or link the request does not state, and no lorem ipsum.',\n maxLength: EVENT_DESCRIPTION_MAX_LENGTH,\n },\n },\n required: ['title', 'startsAt', 'timezone', 'description'],\n additionalProperties: false,\n },\n maxPerPlan: EVENT_AI_MAX_PER_PLAN,\n // The add-on is the gate, on every plan alike; a draft costs no credits.\n freeAllowed: true,\n feature: 'eventCalendar',\n draftResource: EVENT_DRAFT_RESOURCE,\n // Written without a model: the planner already filled the arguments.\n estimateCredits: () => 0,\n degrade: 'omit',\n // A page that shows the events places the Event List block.\n pageBlocks: [EVENT_AI_PAGE_BLOCK],\n draftContent: (item) => eventDraftContentFromArgs(item.args),\n}\n\n/**\n * Registers the capability; the console-server declarations call it beside\n * the writer, since only the console runs AI jobs (AGL-3026). Idempotent.\n */\nexport function registerEventAiCapability(): void {\n registerPluginAiCapability(eventAiCapability, { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginAiCapability","BUNDLE_ID","EVENT_DRAFT_RESOURCE","EVENT_DESCRIPTION_MAX_LENGTH","EVENT_LOCATION_MAX_LENGTH","EVENT_ORGANIZER_MAX_LENGTH","EVENT_TITLE_MAX_LENGTH","EVENT_AI_OP","EVENT_AI_PAGE_BLOCK","EVENT_AI_MAX_PER_PLAN","WALL_TIME","eventDraftContentFromArgs","args","keys","Object","fromEntries","filter","key","trim","map","eventAiCapability","op","noun","where","intents","argsSchema","type","properties","title","description","maxLength","startsAt","endsAt","timezone","location","organizer","required","additionalProperties","maxPerPlan","freeAllowed","feature","draftResource","estimateCredits","degrade","pageBlocks","draftContent","item","registerEventAiCapability","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,0BAA0B,QAGrB,qDAAoD;AAC3D,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,oBAAoB,QAAQ,kCAA8B;AACnE,SACEC,4BAA4B,EAC5BC,yBAAyB,EACzBC,0BAA0B,EAC1BC,sBAAsB,QACjB,0BAAsB;AAE7B;;;;;;;;;;;;;;;;CAgBC,GAED,0CAA0C,GAC1C,OAAO,MAAMC,cAAc,QAAO;AAElC;;;;CAIC,GACD,OAAO,MAAMC,sBAAsB,YAAW;AAE9C,yCAAyC,GACzC,OAAO,MAAMC,wBAAwB,EAAC;AAEtC,MAAMC,YAAY;AAElB,6FAA6F,GAC7F,OAAO,SAASC,0BAA0BC,IAA4B;IACpE,MAAMC,OAAO;QAAC;QAAS;QAAY;QAAU;QAAY;QAAY;QAAa;KAAc;IAChG,OAAOC,OAAOC,WAAW,CACvBF,KACGG,MAAM,CAAC,CAACC,MAAQ,OAAOL,IAAI,CAACK,IAAI,KAAK,YAAY,AAACL,IAAI,CAACK,IAAI,CAAYC,IAAI,IAC3EC,GAAG,CAAC,CAACF,MAAQ;YAACA;YAAML,IAAI,CAACK,IAAI,CAAYC,IAAI;SAAG;AAEvD;AAEA,OAAO,MAAME,oBAAwC;IACnDC,IAAId;IACJe,MAAM;IACNC,OAAO;IACPC,SAAS;QACP;QACA;KACD;IACDC,YAAY;QACVC,MAAM;QACNC,YAAY;YACVC,OAAO;gBACLF,MAAM;gBACNG,aACE;gBACFC,WAAWxB;YACb;YACAyB,UAAU;gBACRL,MAAM;gBACNG,aACE,CAAC,mBAAmB,EAAEnB,UAAU,0IAA0I,CAAC;gBAC7KoB,WAAW;YACb;YACAE,QAAQ;gBACNN,MAAM;gBACNG,aAAa,CAAC,iBAAiB,EAAEnB,UAAU,qDAAqD,CAAC;gBACjGoB,WAAW;YACb;YACAG,UAAU;gBACRP,MAAM;gBACNG,aAAa;gBACbC,WAAW;YACb;YACAI,UAAU;gBACRR,MAAM;gBACNG,aACE;gBACFC,WAAW1B;YACb;YACA+B,WAAW;gBACTT,MAAM;gBACNG,aAAa;gBACbC,WAAWzB;YACb;YACAwB,aAAa;gBACXH,MAAM;gBACNG,aACE;gBACFC,WAAW3B;YACb;QACF;QACAiC,UAAU;YAAC;YAAS;YAAY;YAAY;SAAc;QAC1DC,sBAAsB;IACxB;IACAC,YAAY7B;IACZ,yEAAyE;IACzE8B,aAAa;IACbC,SAAS;IACTC,eAAevC;IACf,qEAAqE;IACrEwC,iBAAiB,IAAM;IACvBC,SAAS;IACT,4DAA4D;IAC5DC,YAAY;QAACpC;KAAoB;IACjCqC,cAAc,CAACC,OAASnC,0BAA0BmC,KAAKlC,IAAI;AAC7D,EAAC;AAED;;;CAGC,GACD,OAAO,SAASmC;IACd/C,2BAA2BoB,mBAAmB;QAAE4B,UAAU/C;IAAU;AACtE"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/server/event-ai-capability.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n registerPluginAiCapability,\n type PluginAiCapability,\n type PluginAiCapabilityArgs,\n} from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { EVENT_DRAFT_RESOURCE } from '../model/event-draft-content'\nimport {\n EVENT_DESCRIPTION_MAX_LENGTH,\n EVENT_LOCATION_MAX_LENGTH,\n EVENT_ORGANIZER_MAX_LENGTH,\n EVENT_TITLE_MAX_LENGTH,\n} from '../model/event-write'\n\n/**\n * WHAT AN AI BUILD CAN MAKE IN EVENTS (AGL-3743): a dated event, published\n * when the member may publish (AGL-3755), and the Event List block that\n * shows the site's events on a page.\n *\n * The capability the AI plugin's build planner offers for \"add our fall\n * workshops\" or \"an events page\". Its item is written by this plugin's\n * `event` draft writer, so every rule is the writer's: the `eventCalendar`\n * add-on, the member's role, a start after the write, and the shape\n * `eventWrite` stores. One item is one event; a page section that uses any\n * of them places the Event List block, which lists the site's published\n * events, so the events show there at once. A member who may write but not\n * publish gets drafts, and the job row says so (`live` with the writer's\n * `live: false`).\n *\n * The arguments are flat, as the contract requires, and are the event's own\n * fields: an event has no recurrence, tickets, RSVP or meeting link, so none\n * is offered. The planner fills them in, so no model runs\n * (`estimateCredits` 0).\n */\n\n/** The operation's name in a build plan. */\nexport const EVENT_AI_OP = 'event'\n\n/**\n * The Event List's component id, as the page palette names the block. Spelled\n * here rather than imported: the component module brings React and MUI, and\n * the console server's boot registers this capability.\n */\nexport const EVENT_AI_PAGE_BLOCK = 'eventList'\n\n/** How many events one build may draft. */\nexport const EVENT_AI_MAX_PER_PLAN = 6\n\nconst WALL_TIME = 'local date and time \"YYYY-MM-DDTHH:MM\", in timezone'\n\n/** The writer's content for an item: the arguments, which are the content's own keys. Pure. */\nexport function eventDraftContentFromArgs(args: PluginAiCapabilityArgs): Readonly<Record<string, unknown>> {\n const keys = ['title', 'startsAt', 'endsAt', 'timezone', 'location', 'organizer', 'description']\n return Object.fromEntries(\n keys\n .filter((key) => typeof args[key] === 'string' && (args[key] as string).trim())\n .map((key) => [key, (args[key] as string).trim()]),\n )\n}\n\nexport const eventAiCapability: PluginAiCapability = {\n op: EVENT_AI_OP,\n noun: 'event',\n where: 'Events',\n intents: [\n 'dated events on the site’s events calendar — classes, workshops, tastings, shows, open houses, meetups — each with its time, place and description, published to the site’s Event List; use this rather than a dataset for anything held on a date',\n 'an events page or section that lists the site’s upcoming events',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n title: {\n type: 'string',\n description:\n 'What the event is called, specific to this business, e.g. \"Autumn sourdough workshop\" — never a placeholder such as \"Event 1\" or \"Sample event\".',\n maxLength: EVENT_TITLE_MAX_LENGTH,\n },\n startsAt: {\n type: 'string',\n description:\n `When it starts, as ${WALL_TIME}. After today's date, within the next few months, on a day and at an hour this business would hold it; a date the request gives, as given.`,\n maxLength: 16,\n },\n endsAt: {\n type: 'string',\n description: `When it ends, as ${WALL_TIME}, after the start; leave it out for a one-hour event.`,\n maxLength: 16,\n },\n timezone: {\n type: 'string',\n description: 'The IANA time zone the business is in, e.g. \"America/Chicago\".',\n maxLength: 64,\n },\n location: {\n type: 'string',\n description:\n 'Where it happens, as a visitor reads it: the venue or address the request gives, or the business’s own place; \"Online\" for an online event. Leave it out rather than invent an address.',\n maxLength: EVENT_LOCATION_MAX_LENGTH,\n },\n organizer: {\n type: 'string',\n description: 'Who hosts it: the business’s name, or a host the request names. Never a person’s name the request does not give.',\n maxLength: EVENT_ORGANIZER_MAX_LENGTH,\n },\n description: {\n type: 'string',\n description:\n 'Two to four sentences a visitor reads: what happens, who it is for and what to bring or expect, in the business’s voice. No price, ticket, sign-up or link the request does not state, and no lorem ipsum.',\n maxLength: EVENT_DESCRIPTION_MAX_LENGTH,\n },\n },\n required: ['title', 'startsAt', 'timezone', 'description'],\n additionalProperties: false,\n },\n maxPerPlan: EVENT_AI_MAX_PER_PLAN,\n // The add-on is the gate, on every plan alike; a draft costs no credits.\n freeAllowed: true,\n feature: 'eventCalendar',\n draftResource: EVENT_DRAFT_RESOURCE,\n // Published as the Events page publishes, for the publishing role (AGL-3755).\n live: true,\n // Written without a model: the planner already filled the arguments.\n estimateCredits: () => 0,\n degrade: 'omit',\n // A page that shows the events places the Event List block.\n pageBlocks: [EVENT_AI_PAGE_BLOCK],\n draftContent: (item) => eventDraftContentFromArgs(item.args),\n}\n\n/**\n * Registers the capability; the console-server declarations call it beside\n * the writer, since only the console runs AI jobs (AGL-3026). Idempotent.\n */\nexport function registerEventAiCapability(): void {\n registerPluginAiCapability(eventAiCapability, { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginAiCapability","BUNDLE_ID","EVENT_DRAFT_RESOURCE","EVENT_DESCRIPTION_MAX_LENGTH","EVENT_LOCATION_MAX_LENGTH","EVENT_ORGANIZER_MAX_LENGTH","EVENT_TITLE_MAX_LENGTH","EVENT_AI_OP","EVENT_AI_PAGE_BLOCK","EVENT_AI_MAX_PER_PLAN","WALL_TIME","eventDraftContentFromArgs","args","keys","Object","fromEntries","filter","key","trim","map","eventAiCapability","op","noun","where","intents","argsSchema","type","properties","title","description","maxLength","startsAt","endsAt","timezone","location","organizer","required","additionalProperties","maxPerPlan","freeAllowed","feature","draftResource","live","estimateCredits","degrade","pageBlocks","draftContent","item","registerEventAiCapability","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,0BAA0B,QAGrB,qDAAoD;AAC3D,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,oBAAoB,QAAQ,kCAA8B;AACnE,SACEC,4BAA4B,EAC5BC,yBAAyB,EACzBC,0BAA0B,EAC1BC,sBAAsB,QACjB,0BAAsB;AAE7B;;;;;;;;;;;;;;;;;;;CAmBC,GAED,0CAA0C,GAC1C,OAAO,MAAMC,cAAc,QAAO;AAElC;;;;CAIC,GACD,OAAO,MAAMC,sBAAsB,YAAW;AAE9C,yCAAyC,GACzC,OAAO,MAAMC,wBAAwB,EAAC;AAEtC,MAAMC,YAAY;AAElB,6FAA6F,GAC7F,OAAO,SAASC,0BAA0BC,IAA4B;IACpE,MAAMC,OAAO;QAAC;QAAS;QAAY;QAAU;QAAY;QAAY;QAAa;KAAc;IAChG,OAAOC,OAAOC,WAAW,CACvBF,KACGG,MAAM,CAAC,CAACC,MAAQ,OAAOL,IAAI,CAACK,IAAI,KAAK,YAAY,AAACL,IAAI,CAACK,IAAI,CAAYC,IAAI,IAC3EC,GAAG,CAAC,CAACF,MAAQ;YAACA;YAAML,IAAI,CAACK,IAAI,CAAYC,IAAI;SAAG;AAEvD;AAEA,OAAO,MAAME,oBAAwC;IACnDC,IAAId;IACJe,MAAM;IACNC,OAAO;IACPC,SAAS;QACP;QACA;KACD;IACDC,YAAY;QACVC,MAAM;QACNC,YAAY;YACVC,OAAO;gBACLF,MAAM;gBACNG,aACE;gBACFC,WAAWxB;YACb;YACAyB,UAAU;gBACRL,MAAM;gBACNG,aACE,CAAC,mBAAmB,EAAEnB,UAAU,0IAA0I,CAAC;gBAC7KoB,WAAW;YACb;YACAE,QAAQ;gBACNN,MAAM;gBACNG,aAAa,CAAC,iBAAiB,EAAEnB,UAAU,qDAAqD,CAAC;gBACjGoB,WAAW;YACb;YACAG,UAAU;gBACRP,MAAM;gBACNG,aAAa;gBACbC,WAAW;YACb;YACAI,UAAU;gBACRR,MAAM;gBACNG,aACE;gBACFC,WAAW1B;YACb;YACA+B,WAAW;gBACTT,MAAM;gBACNG,aAAa;gBACbC,WAAWzB;YACb;YACAwB,aAAa;gBACXH,MAAM;gBACNG,aACE;gBACFC,WAAW3B;YACb;QACF;QACAiC,UAAU;YAAC;YAAS;YAAY;YAAY;SAAc;QAC1DC,sBAAsB;IACxB;IACAC,YAAY7B;IACZ,yEAAyE;IACzE8B,aAAa;IACbC,SAAS;IACTC,eAAevC;IACf,8EAA8E;IAC9EwC,MAAM;IACN,qEAAqE;IACrEC,iBAAiB,IAAM;IACvBC,SAAS;IACT,4DAA4D;IAC5DC,YAAY;QAACrC;KAAoB;IACjCsC,cAAc,CAACC,OAASpC,0BAA0BoC,KAAKnC,IAAI;AAC7D,EAAC;AAED;;;CAGC,GACD,OAAO,SAASoC;IACdhD,2BAA2BoB,mBAAmB;QAAE6B,UAAUhD;IAAU;AACtE"}
@@ -15,6 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
18
+ import { type StockPhotoPicker } from '@aglyn/aglyn/plugin-manager/stock-photo-provider';
18
19
  /**
19
20
  * AN EVENT ANOTHER PLUGIN ASKS FOR (AGL-3743).
20
21
  *
@@ -24,9 +25,14 @@ import type { PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plug
24
25
  *
25
26
  * - THE DOCUMENT is a `hosts/{hostId}/events/{id}` event stored through
26
27
  * `eventWrite`, the one rule the Events page's editor and the events
27
- * import write through, born `status: 'draft'`. A draft is listed nowhere
28
- * on the site — the public listing asks for `status == 'published'` —
29
- * until a person sets it published on the Events page.
28
+ * import write through.
29
+ * - PUBLISHED when the member may publish (AGL-3755): the Events page's
30
+ * "Set published" is the event's `status` set to `published` and nothing
31
+ * else, so that is all this writer changes. The member must hold the
32
+ * publishing role (`hostRoleCanPublish`, admin or editor), the axis every
33
+ * other publish asks. A member who may write but not publish (`author`)
34
+ * gets the event as a draft, `live: false`, with a notice saying why, and
35
+ * it waits on the Events page for someone who may.
30
36
  * - THE PLAN is the `eventCalendar` add-on the Events page is gated on, read
31
37
  * from the workspace's own document the way the listing and the import
32
38
  * read it. There is no allowance to count.
@@ -34,19 +40,30 @@ import type { PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plug
34
40
  * - THE START is after the write. A draft that would open on the Events page
35
41
  * already over is a mistake in what was asked, so it is refused rather
36
42
  * than stored.
43
+ * - THE COVER (AGL-3755), when the content names none: a photo of the
44
+ * event's title from the deployment's stock photo picker
45
+ * (`core.stock-photo-picker`), kept in the site's own media library once
46
+ * and reused after, or a starter photo when the library is down. It is
47
+ * the editor's own "Cover image URL" field, `coverImage`, holding the
48
+ * photo's site-relative address. Photos of one act (the AI build) never
49
+ * repeat. A process with no picker writes the event without a cover.
37
50
  *
38
- * Nothing is published. The writer touches the one new document.
51
+ * The writer touches the one new document.
39
52
  */
40
53
  type Firestore = FirebaseFirestore.Firestore;
41
54
  /** The refusal for a member who may not write the site. */
42
55
  export declare const EVENT_DRAFT_ROLE_REFUSAL = "Editing requires the editor role";
43
56
  /** The refusal for a workspace without the add-on, in the Events page's words. */
44
57
  export declare const EVENT_DRAFT_PLAN_REFUSAL = "The Event Calendar is a paid add-on: enable it from Billing \u2192 Add-ons";
58
+ /** Why an event a member wrote was kept as a draft: they may write the site, not publish it. */
59
+ export declare const EVENT_DRAFT_UNPUBLISHED_NOTICE = "Kept as a draft: publishing an event takes the editor role, so someone who has it publishes it on the Events page.";
45
60
  /** The refusal for a start that is not after the write. */
46
61
  export declare function eventDraftPastRefusal(wallText: string): string;
47
62
  export interface EventDraftWriterDeps {
48
63
  /** The Admin SDK handle; specs hand in a double. */
49
64
  firestore?: () => Firestore;
65
+ /** The stock photo picker a cover comes from; specs hand in a double. */
66
+ stockPhotos?: () => StockPhotoPicker | null;
50
67
  }
51
68
  export declare function createEventDraftWriter(deps?: EventDraftWriterDeps): PluginResourceDraftWriter;
52
69
  export declare const eventDraftWriter: PluginResourceDraftWriter;
@@ -14,32 +14,45 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
17
+ */ import { hostRoleCanPublish, hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
18
18
  import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
19
+ import { stockPhotoPicker } from "@aglyn/aglyn/plugin-manager/stock-photo-provider";
19
20
  import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
20
- import { checkEventDraftContent, eventDraftWallText, readEventDraftContent } from "../model/event-draft-content.js";
21
+ import { checkEventDraftContent, EVENT_DRAFT_COVER_ADDRESS, eventDraftWallText, readEventDraftContent } from "../model/event-draft-content.js";
21
22
  import { eventWrite } from "../model/event-write.js";
22
23
  /** The refusal for a member who may not write the site. */ export const EVENT_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role';
23
24
  /** The refusal for a workspace without the add-on, in the Events page's words. */ export const EVENT_DRAFT_PLAN_REFUSAL = 'The Event Calendar is a paid add-on: enable it from Billing → Add-ons';
25
+ /** Why an event a member wrote was kept as a draft: they may write the site, not publish it. */ export const EVENT_DRAFT_UNPUBLISHED_NOTICE = 'Kept as a draft: publishing an event takes the editor role, so someone who has it publishes it on the Events page.';
24
26
  /** The refusal for a start that is not after the write. */ export function eventDraftPastRefusal(wallText) {
25
27
  return `A drafted event starts in the future: ${wallText} has already passed`;
26
28
  }
27
29
  function isDeleted(data) {
28
30
  return Boolean(data == null ? void 0 : data['deletedAt']) || (data == null ? void 0 : data['status']) === 'deleted';
29
31
  }
32
+ /** What a written event reports: published or a draft, and why a draft when it is one. */ function liveOf(published) {
33
+ return published ? {
34
+ live: true
35
+ } : {
36
+ live: false,
37
+ notice: EVENT_DRAFT_UNPUBLISHED_NOTICE
38
+ };
39
+ }
30
40
  function recordOf(event) {
31
41
  var _event_data, _data_title, _data_startsAtMs, _data_endsAtMs;
32
42
  const data = (_event_data = event.data()) != null ? _event_data : {};
33
- return {
43
+ const published = data['status'] === 'published';
44
+ return _extends({
34
45
  id: event.id,
35
46
  name: String((_data_title = data['title']) != null ? _data_title : ''),
36
47
  versionId: null,
37
- facts: {
38
- status: data['status'] === 'published' ? 'published' : 'draft',
48
+ facts: _extends({
49
+ status: published ? 'published' : 'draft',
39
50
  startsAtMs: Number((_data_startsAtMs = data['startsAtMs']) != null ? _data_startsAtMs : 0),
40
51
  endsAtMs: Number((_data_endsAtMs = data['endsAtMs']) != null ? _data_endsAtMs : 0)
41
- }
42
- };
52
+ }, typeof data['coverImage'] === 'string' ? {
53
+ coverImage: data['coverImage']
54
+ } : {})
55
+ }, liveOf(published));
43
56
  }
44
57
  function roleRefusal(host, uid) {
45
58
  var _host_get;
@@ -55,9 +68,36 @@ function planRefusal(org) {
55
68
  error: EVENT_DRAFT_PLAN_REFUSAL
56
69
  };
57
70
  }
71
+ /** A cover for an event the content gave none: the picker's, held to what the editor's field takes. */ async function coverFor(picker, request, title) {
72
+ if (!picker) return null;
73
+ try {
74
+ var _request_act, _request_act1;
75
+ const pick = await picker.pick(_extends({
76
+ hostId: request.hostId,
77
+ uid: request.uid,
78
+ subject: title
79
+ }, ((_request_act = request.act) == null ? void 0 : _request_act.business) ? {
80
+ business: request.act.business
81
+ } : {}, {
82
+ orientation: 'horizontal'
83
+ }, ((_request_act1 = request.act) == null ? void 0 : _request_act1.id) ? {
84
+ scope: request.act.id
85
+ } : {}, {
86
+ // The draft's id: asked again, the same photo.
87
+ seed: request.id
88
+ }));
89
+ return pick && EVENT_DRAFT_COVER_ADDRESS.test(pick.url) ? pick.url : null;
90
+ } catch (error) {
91
+ console.warn('event draft: no cover photo', {
92
+ error: String(error)
93
+ });
94
+ return null;
95
+ }
96
+ }
58
97
  export function createEventDraftWriter(deps = {}) {
59
- var _deps_firestore;
98
+ var _deps_firestore, _deps_stockPhotos;
60
99
  const firestore = (_deps_firestore = deps.firestore) != null ? _deps_firestore : ()=>firebaseAdmin.app().firestore();
100
+ const stockPhotos = (_deps_stockPhotos = deps.stockPhotos) != null ? _deps_stockPhotos : stockPhotoPicker;
61
101
  return {
62
102
  refusal: async (context)=>{
63
103
  var _roleRefusal;
@@ -91,12 +131,23 @@ export function createEventDraftWriter(deps = {}) {
91
131
  error: eventDraftPastRefusal(eventDraftWallText(read.value.startsAtMs, read.timezone))
92
132
  };
93
133
  }
94
- const write = eventWrite(read.value);
95
134
  const db = firestore();
96
135
  const hostRef = db.collection('hosts').doc(request.hostId);
97
136
  const eventRef = hostRef.collection('events').doc(request.id);
137
+ // A cover is fetched only for a new event a member may write, before
138
+ // the transaction: a photo's copy is too slow to hold one open.
139
+ if (!read.value.coverImage) {
140
+ const [host, existing] = await Promise.all([
141
+ hostRef.get(),
142
+ eventRef.get()
143
+ ]);
144
+ if (host.exists && !existing.exists && !roleRefusal(host, request.uid) && !planRefusal(request.org)) {
145
+ const cover = await coverFor(stockPhotos(), request, read.value.title);
146
+ if (cover) read.value.coverImage = cover;
147
+ }
148
+ }
98
149
  return db.runTransaction(async (tx)=>{
99
- var _roleRefusal;
150
+ var _roleRefusal, _host_get;
100
151
  const [host, existing] = await Promise.all([
101
152
  tx.get(hostRef),
102
153
  tx.get(eventRef)
@@ -122,23 +173,30 @@ export function createEventDraftWriter(deps = {}) {
122
173
  if (early) return _extends({
123
174
  ok: false
124
175
  }, early);
176
+ // The Events page's publish: `status`, and only for the publishing role.
177
+ const published = hostRoleCanPublish(((_host_get = host.get('memberRoles')) != null ? _host_get : {})[request.uid]);
178
+ const write = eventWrite(_extends({}, read.value, {
179
+ status: published ? 'published' : 'draft'
180
+ }));
125
181
  tx.create(eventRef, _extends({}, write.fields, {
126
182
  createdAt: request.now,
127
183
  updatedAt: request.now,
128
184
  createdBy: request.uid
129
185
  }));
130
- return {
186
+ return _extends({
131
187
  ok: true,
132
188
  replayed: false,
133
189
  id: request.id,
134
190
  name: write.fields.title,
135
191
  versionId: null,
136
- facts: {
137
- status: 'draft',
192
+ facts: _extends({
193
+ status: write.fields.status,
138
194
  startsAtMs: write.fields.startsAtMs,
139
195
  endsAtMs: write.fields.endsAtMs
140
- }
141
- };
196
+ }, write.fields.coverImage ? {
197
+ coverImage: write.fields.coverImage
198
+ } : {})
199
+ }, liveOf(published));
142
200
  });
143
201
  }
144
202
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/server/event-drafts.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type {\n PluginDraftContext,\n PluginDraftRecord,\n PluginDraftRefusal,\n PluginDraftWrite,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport {\n checkEventDraftContent,\n eventDraftWallText,\n readEventDraftContent,\n} from '../model/event-draft-content'\nimport { eventWrite } from '../model/event-write'\n\n/**\n * AN EVENT ANOTHER PLUGIN ASKS FOR (AGL-3743).\n *\n * The draft writer this plugin registers for the `event` resource on the\n * core's resource-drafts seam. A plugin that sets a site up from a brief —\n * an AI build — asks for the writer by name and gets this plugin's rules:\n *\n * - THE DOCUMENT is a `hosts/{hostId}/events/{id}` event stored through\n * `eventWrite`, the one rule the Events page's editor and the events\n * import write through, born `status: 'draft'`. A draft is listed nowhere\n * on the site — the public listing asks for `status == 'published'` —\n * until a person sets it published on the Events page.\n * - THE PLAN is the `eventCalendar` add-on the Events page is gated on, read\n * from the workspace's own document the way the listing and the import\n * read it. There is no allowance to count.\n * - THE ROLE is a member who may write the site's content.\n * - THE START is after the write. A draft that would open on the Events page\n * already over is a mistake in what was asked, so it is refused rather\n * than stored.\n *\n * Nothing is published. The writer touches the one new document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The refusal for a member who may not write the site. */\nexport const EVENT_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role'\n\n/** The refusal for a workspace without the add-on, in the Events page's words. */\nexport const EVENT_DRAFT_PLAN_REFUSAL =\n 'The Event Calendar is a paid add-on: enable it from Billing → Add-ons'\n\n/** The refusal for a start that is not after the write. */\nexport function eventDraftPastRefusal(wallText: string): string {\n return `A drafted event starts in the future: ${wallText} has already passed`\n}\n\nfunction isDeleted(data: Readonly<Record<string, unknown>> | undefined): boolean {\n return Boolean(data?.['deletedAt']) || data?.['status'] === 'deleted'\n}\n\nfunction recordOf(event: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (event.data() ?? {}) as Record<string, unknown>\n return {\n id: event.id,\n name: String(data['title'] ?? ''),\n versionId: null,\n facts: {\n status: data['status'] === 'published' ? 'published' : 'draft',\n startsAtMs: Number(data['startsAtMs'] ?? 0),\n endsAtMs: Number(data['endsAtMs'] ?? 0),\n },\n }\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: EVENT_DRAFT_ROLE_REFUSAL }\n}\n\nfunction planRefusal(org: PluginDraftContext['org']): PluginDraftRefusal | null {\n return checkEntitlement(org as never, 'eventCalendar')\n ? null\n : { status: 403, error: EVENT_DRAFT_PLAN_REFUSAL }\n}\n\nexport interface EventDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n}\n\nexport function createEventDraftWriter(deps: EventDraftWriterDeps = {}): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n return {\n refusal: async (context) => {\n const host = await firestore().collection('hosts').doc(context.hostId).get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return roleRefusal(host, context.uid) ?? planRefusal(context.org)\n },\n\n check: (content) => checkEventDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const event = await firestore().collection('hosts').doc(hostId).collection('events').doc(id).get()\n return event.exists && !isDeleted(event.data()) ? recordOf(event) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n // The request's name is the one asked for; the content's stands in.\n const asked = request.name.replace(/[\\p{Cc}\\s]+/gu, ' ').trim()\n const read = readEventDraftContent(asked ? { ...request.content, title: asked } : request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n if (read.value.startsAtMs <= request.now.getTime()) {\n return {\n ok: false,\n status: 400,\n error: eventDraftPastRefusal(eventDraftWallText(read.value.startsAtMs, read.timezone)),\n }\n }\n const write = eventWrite(read.value)\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const eventRef = hostRef.collection('events').doc(request.id)\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n const [host, existing] = await Promise.all([tx.get(hostRef), tx.get(eventRef)])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) {\n // Asked again under the same id: the draft it already wrote — unless\n // a person deleted it since, which a replay never brings back.\n return isDeleted(existing.data())\n ? { ok: false, status: 409, error: 'That event was deleted on the Events page' }\n : { ok: true, replayed: true, ...recordOf(existing) }\n }\n const early = roleRefusal(host, request.uid) ?? planRefusal(request.org)\n if (early) return { ok: false, ...early }\n tx.create(eventRef, {\n ...write.fields,\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name: write.fields.title,\n versionId: null,\n facts: {\n status: 'draft',\n startsAtMs: write.fields.startsAtMs,\n endsAtMs: write.fields.endsAtMs,\n },\n }\n })\n },\n }\n}\n\nexport const eventDraftWriter = createEventDraftWriter()\n"],"names":["hostRoleCanWrite","checkEntitlement","firebaseAdmin","checkEventDraftContent","eventDraftWallText","readEventDraftContent","eventWrite","EVENT_DRAFT_ROLE_REFUSAL","EVENT_DRAFT_PLAN_REFUSAL","eventDraftPastRefusal","wallText","isDeleted","data","Boolean","recordOf","event","id","name","String","versionId","facts","status","startsAtMs","Number","endsAtMs","roleRefusal","host","uid","role","get","error","planRefusal","org","createEventDraftWriter","deps","firestore","app","refusal","context","collection","doc","hostId","exists","check","content","read","write","request","asked","replace","trim","title","ok","problems","value","now","getTime","timezone","db","hostRef","eventRef","runTransaction","tx","existing","Promise","all","replayed","early","create","fields","createdAt","updatedAt","createdBy","eventDraftWriter"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/server/event-drafts.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanPublish, hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type {\n PluginDraftContext,\n PluginDraftRecord,\n PluginDraftRefusal,\n PluginDraftRequest,\n PluginDraftWrite,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n stockPhotoPicker,\n type StockPhotoPicker,\n} from '@aglyn/aglyn/plugin-manager/stock-photo-provider'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport {\n checkEventDraftContent,\n EVENT_DRAFT_COVER_ADDRESS,\n eventDraftWallText,\n readEventDraftContent,\n} from '../model/event-draft-content'\nimport { eventWrite } from '../model/event-write'\n\n/**\n * AN EVENT ANOTHER PLUGIN ASKS FOR (AGL-3743).\n *\n * The draft writer this plugin registers for the `event` resource on the\n * core's resource-drafts seam. A plugin that sets a site up from a brief —\n * an AI build — asks for the writer by name and gets this plugin's rules:\n *\n * - THE DOCUMENT is a `hosts/{hostId}/events/{id}` event stored through\n * `eventWrite`, the one rule the Events page's editor and the events\n * import write through.\n * - PUBLISHED when the member may publish (AGL-3755): the Events page's\n * \"Set published\" is the event's `status` set to `published` and nothing\n * else, so that is all this writer changes. The member must hold the\n * publishing role (`hostRoleCanPublish`, admin or editor), the axis every\n * other publish asks. A member who may write but not publish (`author`)\n * gets the event as a draft, `live: false`, with a notice saying why, and\n * it waits on the Events page for someone who may.\n * - THE PLAN is the `eventCalendar` add-on the Events page is gated on, read\n * from the workspace's own document the way the listing and the import\n * read it. There is no allowance to count.\n * - THE ROLE is a member who may write the site's content.\n * - THE START is after the write. A draft that would open on the Events page\n * already over is a mistake in what was asked, so it is refused rather\n * than stored.\n * - THE COVER (AGL-3755), when the content names none: a photo of the\n * event's title from the deployment's stock photo picker\n * (`core.stock-photo-picker`), kept in the site's own media library once\n * and reused after, or a starter photo when the library is down. It is\n * the editor's own \"Cover image URL\" field, `coverImage`, holding the\n * photo's site-relative address. Photos of one act (the AI build) never\n * repeat. A process with no picker writes the event without a cover.\n *\n * The writer touches the one new document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The refusal for a member who may not write the site. */\nexport const EVENT_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role'\n\n/** The refusal for a workspace without the add-on, in the Events page's words. */\nexport const EVENT_DRAFT_PLAN_REFUSAL =\n 'The Event Calendar is a paid add-on: enable it from Billing → Add-ons'\n\n/** Why an event a member wrote was kept as a draft: they may write the site, not publish it. */\nexport const EVENT_DRAFT_UNPUBLISHED_NOTICE =\n 'Kept as a draft: publishing an event takes the editor role, so someone who has it publishes it on the Events page.'\n\n/** The refusal for a start that is not after the write. */\nexport function eventDraftPastRefusal(wallText: string): string {\n return `A drafted event starts in the future: ${wallText} has already passed`\n}\n\nfunction isDeleted(data: Readonly<Record<string, unknown>> | undefined): boolean {\n return Boolean(data?.['deletedAt']) || data?.['status'] === 'deleted'\n}\n\n/** What a written event reports: published or a draft, and why a draft when it is one. */\nfunction liveOf(published: boolean): Pick<PluginDraftRecord, 'live' | 'notice'> {\n return published ? { live: true } : { live: false, notice: EVENT_DRAFT_UNPUBLISHED_NOTICE }\n}\n\nfunction recordOf(event: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (event.data() ?? {}) as Record<string, unknown>\n const published = data['status'] === 'published'\n return {\n id: event.id,\n name: String(data['title'] ?? ''),\n versionId: null,\n facts: {\n status: published ? 'published' : 'draft',\n startsAtMs: Number(data['startsAtMs'] ?? 0),\n endsAtMs: Number(data['endsAtMs'] ?? 0),\n ...(typeof data['coverImage'] === 'string' ? { coverImage: data['coverImage'] } : {}),\n },\n ...liveOf(published),\n }\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: EVENT_DRAFT_ROLE_REFUSAL }\n}\n\nfunction planRefusal(org: PluginDraftContext['org']): PluginDraftRefusal | null {\n return checkEntitlement(org as never, 'eventCalendar')\n ? null\n : { status: 403, error: EVENT_DRAFT_PLAN_REFUSAL }\n}\n\nexport interface EventDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n /** The stock photo picker a cover comes from; specs hand in a double. */\n stockPhotos?: () => StockPhotoPicker | null\n}\n\n/** A cover for an event the content gave none: the picker's, held to what the editor's field takes. */\nasync function coverFor(\n picker: StockPhotoPicker | null,\n request: PluginDraftRequest,\n title: string,\n): Promise<string | null> {\n if (!picker) return null\n try {\n const pick = await picker.pick({\n hostId: request.hostId,\n uid: request.uid,\n subject: title,\n ...(request.act?.business ? { business: request.act.business } : {}),\n orientation: 'horizontal',\n ...(request.act?.id ? { scope: request.act.id } : {}),\n // The draft's id: asked again, the same photo.\n seed: request.id,\n })\n return pick && EVENT_DRAFT_COVER_ADDRESS.test(pick.url) ? pick.url : null\n } catch (error) {\n console.warn('event draft: no cover photo', { error: String(error) })\n return null\n }\n}\n\nexport function createEventDraftWriter(deps: EventDraftWriterDeps = {}): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n const stockPhotos = deps.stockPhotos ?? stockPhotoPicker\n return {\n refusal: async (context) => {\n const host = await firestore().collection('hosts').doc(context.hostId).get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return roleRefusal(host, context.uid) ?? planRefusal(context.org)\n },\n\n check: (content) => checkEventDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const event = await firestore().collection('hosts').doc(hostId).collection('events').doc(id).get()\n return event.exists && !isDeleted(event.data()) ? recordOf(event) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n // The request's name is the one asked for; the content's stands in.\n const asked = request.name.replace(/[\\p{Cc}\\s]+/gu, ' ').trim()\n const read = readEventDraftContent(asked ? { ...request.content, title: asked } : request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n if (read.value.startsAtMs <= request.now.getTime()) {\n return {\n ok: false,\n status: 400,\n error: eventDraftPastRefusal(eventDraftWallText(read.value.startsAtMs, read.timezone)),\n }\n }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const eventRef = hostRef.collection('events').doc(request.id)\n // A cover is fetched only for a new event a member may write, before\n // the transaction: a photo's copy is too slow to hold one open.\n if (!read.value.coverImage) {\n const [host, existing] = await Promise.all([hostRef.get(), eventRef.get()])\n if (host.exists && !existing.exists && !roleRefusal(host, request.uid) && !planRefusal(request.org)) {\n const cover = await coverFor(stockPhotos(), request, read.value.title)\n if (cover) read.value.coverImage = cover\n }\n }\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n const [host, existing] = await Promise.all([tx.get(hostRef), tx.get(eventRef)])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) {\n // Asked again under the same id: the draft it already wrote — unless\n // a person deleted it since, which a replay never brings back.\n return isDeleted(existing.data())\n ? { ok: false, status: 409, error: 'That event was deleted on the Events page' }\n : { ok: true, replayed: true, ...recordOf(existing) }\n }\n const early = roleRefusal(host, request.uid) ?? planRefusal(request.org)\n if (early) return { ok: false, ...early }\n // The Events page's publish: `status`, and only for the publishing role.\n const published = hostRoleCanPublish((host.get('memberRoles') ?? {})[request.uid])\n const write = eventWrite({ ...read.value, status: published ? 'published' : 'draft' })\n tx.create(eventRef, {\n ...write.fields,\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name: write.fields.title,\n versionId: null,\n facts: {\n status: write.fields.status,\n startsAtMs: write.fields.startsAtMs,\n endsAtMs: write.fields.endsAtMs,\n ...(write.fields.coverImage ? { coverImage: write.fields.coverImage } : {}),\n },\n ...liveOf(published),\n }\n })\n },\n }\n}\n\nexport const eventDraftWriter = createEventDraftWriter()\n"],"names":["hostRoleCanPublish","hostRoleCanWrite","checkEntitlement","stockPhotoPicker","firebaseAdmin","checkEventDraftContent","EVENT_DRAFT_COVER_ADDRESS","eventDraftWallText","readEventDraftContent","eventWrite","EVENT_DRAFT_ROLE_REFUSAL","EVENT_DRAFT_PLAN_REFUSAL","EVENT_DRAFT_UNPUBLISHED_NOTICE","eventDraftPastRefusal","wallText","isDeleted","data","Boolean","liveOf","published","live","notice","recordOf","event","id","name","String","versionId","facts","status","startsAtMs","Number","endsAtMs","coverImage","roleRefusal","host","uid","role","get","error","planRefusal","org","coverFor","picker","request","title","pick","hostId","subject","act","business","orientation","scope","seed","test","url","console","warn","createEventDraftWriter","deps","firestore","app","stockPhotos","refusal","context","collection","doc","exists","check","content","read","write","asked","replace","trim","ok","problems","value","now","getTime","timezone","db","hostRef","eventRef","existing","Promise","all","cover","runTransaction","tx","replayed","early","create","fields","createdAt","updatedAt","createdBy","eventDraftWriter"],"mappings":"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"}