@pramen/cms-editor 0.0.59 → 0.0.60

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/src/types.ts CHANGED
@@ -58,6 +58,9 @@ export type FieldType =
58
58
  | "slug"
59
59
  | "media"
60
60
  | "select"
61
+ /** A pointer to a record that is not this row — a pramen row or an external record.
62
+ * Stored as an opaque id; resolved through `referenceFrom`. */
63
+ | "reference"
61
64
  | "repeater"
62
65
  | "group";
63
66
 
@@ -77,6 +80,27 @@ export interface FieldDefinition {
77
80
  optionsFrom?: string;
78
81
  /** For `slug`: the sibling field this one is derived from (e.g. `"title"`). */
79
82
  from?: string;
83
+ /** For `reference`: the query handler that resolves it. Called `{ search?, limit, offset }`
84
+ * to browse and `{ ids }` to resolve labels for stored values; returns
85
+ * {@link ReferenceResult} either way. */
86
+ referenceFrom?: string;
87
+ /** For `reference`: store a list of ids rather than one. */
88
+ multiple?: boolean;
89
+ }
90
+
91
+ /** One option a `reference` field's handler returns. */
92
+ export interface ReferenceOption {
93
+ value: string;
94
+ label: string;
95
+ /** Secondary text under the label — a date, an owner, a status. A picker over a thousand
96
+ * records usually needs more than a name to tell two rows apart. */
97
+ hint?: string;
98
+ }
99
+
100
+ /** What a `reference` field's handler returns, for both request shapes. */
101
+ export interface ReferenceResult {
102
+ items: ReferenceOption[];
103
+ hasMore?: boolean;
80
104
  }
81
105
 
82
106
  export interface RegionDefinition {
@@ -99,6 +123,13 @@ export interface BlockType {
99
123
  fieldsSchema?: FieldDefinition[] | null;
100
124
  icon?: string | null;
101
125
  category?: string | null;
126
+ /** The `cmsBootstrap` owner that declares this type in the app's code, or null/absent when
127
+ * an editor authored it. The builder renders an owned type read-only: a save would 409, and
128
+ * before it did, it would have been quietly reverted at the next cold start.
129
+ *
130
+ * Only meaningful when `CmsCapabilities.codeDefinedTypes` is true — an older server sends
131
+ * no owner for any row, which is indistinguishable from "nothing is code-defined". */
132
+ managedBy?: string | null;
102
133
  }
103
134
 
104
135
  export interface ContentType {
@@ -108,6 +139,8 @@ export interface ContentType {
108
139
  regions?: RegionDefinition[] | null;
109
140
  fieldsSchema?: FieldDefinition[] | null;
110
141
  defaultBlocks?: DefaultBlockDefinition[] | null;
142
+ /** See `BlockType.managedBy`. */
143
+ managedBy?: string | null;
111
144
  }
112
145
 
113
146
  /** A collection: one of the host app's own pramen entities, edited generically via a
@@ -119,6 +152,11 @@ export interface CollectionMeta {
119
152
  label: string;
120
153
  pluralLabel: string;
121
154
  icon?: string;
155
+ /** Where this collection sits in the primary nav — see {@link NAV_ORDER}. Filled in
156
+ * server-side, so the editor sorts one list of numbers. Optional here only because an
157
+ * OLDER server does not send it; `NAV_ORDER.collections` is the fallback, which is
158
+ * exactly where collections rendered before the key existed. */
159
+ navOrder?: number;
122
160
  fields: FieldDefinition[];
123
161
  list: string[];
124
162
  titleField: string;
@@ -152,12 +190,66 @@ export interface CmsCapabilities {
152
190
  * without this probe the editor renders N tabs that all show every type's pages under a
153
191
  * heading claiming otherwise, and "New page" from any of them stamps that tab's type. */
154
192
  pagesByType: boolean;
193
+ /** The server has the site-furniture handlers (menus, redirects, taxonomies, widget
194
+ * areas). Declared, not probed: an older server answers `listMenus` with a 404 like any
195
+ * unknown handler, and a nav section whose every screen fails is worse than no section. */
196
+ siteFurniture: boolean;
197
+ /** The server stamps `managedBy` on the types `cmsBootstrap` declares, and refuses to
198
+ * update one. Declared rather than inferred from the rows: this package has no dependency
199
+ * on `@pramen/cms`, so a NEWER editor can run against an older server — where every row
200
+ * reports no owner, which reads identically to "no type is code-defined". Trusting the row
201
+ * there means the builder offers a save the server accepts and the next cold start
202
+ * reverts, i.e. GitHub #48 in the deployment that upgraded the editor to fix it. */
203
+ codeDefinedTypes: boolean;
204
+ /** Whether THIS caller may author, i.e. holds one of the deployment's `editorRoles`.
205
+ *
206
+ * Per-caller, unlike the rest of this probe. The read handlers are open to
207
+ * `editorRoles ∪ reviewerRoles` but every write is editor-only, and the editor cannot
208
+ * derive that: it knows the caller's roles from `me` but not which roles the deployment
209
+ * configured. Without it a reviewer-only session gets the authoring nav and every screen
210
+ * 403s on its first save. */
211
+ canEdit: boolean;
155
212
  }
156
213
 
214
+ /**
215
+ * Where each built-in section sits in the primary nav — the editor's mirror of
216
+ * `NAV_ORDER` in @pramen/cms.
217
+ *
218
+ * A mirror, not an import: the editor is a standalone browser app that speaks to the CMS
219
+ * purely over HTTP and has no server-package dependency (see the note at the top of this
220
+ * file). The numbers are ordinals spaced 100 apart; only their relative order is contract.
221
+ */
222
+ export const NAV_ORDER = {
223
+ pages: 100,
224
+ collections: 200,
225
+ media: 300,
226
+ menus: 400,
227
+ taxonomies: 500,
228
+ widgets: 600,
229
+ redirects: 700,
230
+ adminPages: 800,
231
+ types: 900,
232
+ users: 1000,
233
+ settings: 1100,
234
+ extra: 1200,
235
+ } as const;
236
+
157
237
  /** Used until `listCmsCapabilities` answers, and when it cannot (an older server). Assumes
158
238
  * MONOLINGUAL: a hidden i18n surface on a multilingual site is recoverable by reloading,
159
239
  * where a half-rendered one on a single-locale site is what this replaced. */
160
- export const DEFAULT_CAPABILITIES: CmsCapabilities = { locales: ["en"], defaultLocale: "en", multilingual: false, pagesByType: false };
240
+ export const DEFAULT_CAPABILITIES: CmsCapabilities = {
241
+ locales: ["en"],
242
+ defaultLocale: "en",
243
+ multilingual: false,
244
+ pagesByType: false,
245
+ siteFurniture: false,
246
+ codeDefinedTypes: false,
247
+ // Fails OPEN, unlike its neighbours. An older server sends no `canEdit`, and hiding
248
+ // every authoring control from a real editor is unrecoverable from inside the editor;
249
+ // showing one that 403s is a legible error with a way forward. The server is the
250
+ // boundary either way — this only decides what is drawn.
251
+ canEdit: true,
252
+ };
161
253
 
162
254
  /** Mirror of @pramen/cms `CollectionFeature`. */
163
255
  export type CollectionFeature = "drafts" | "scheduling" | "revisions" | "preview";
@@ -223,3 +315,147 @@ export interface AuditEntry {
223
315
  note: string | null;
224
316
  createdAt: string;
225
317
  }
318
+
319
+ // --- site furniture (mirrors of the @pramen/cms shapes) ----------------------
320
+
321
+ /** What a menu item points at. `custom` is a literal href; the rest are references the
322
+ * server resolves at read time, so a link follows its page instead of freezing a slug. */
323
+ export type MenuItemKind = "custom" | "page" | "term" | "collection";
324
+
325
+ export interface MenuItem {
326
+ id: string;
327
+ label: string;
328
+ kind?: MenuItemKind;
329
+ /** `page`: a page id · `term`: a term id · `collection`: a collection slug. */
330
+ ref?: string | null;
331
+ /** `custom`: the href as authored. Filled in by `getMenu` for the resolved kinds. */
332
+ url?: string;
333
+ target?: string;
334
+ titleAttr?: string;
335
+ cssClasses?: string;
336
+ children?: MenuItem[];
337
+ }
338
+
339
+ export interface Menu {
340
+ id: string;
341
+ name: string;
342
+ label: string;
343
+ items?: MenuItem[] | null;
344
+ /** Optimistic-concurrency counter — send it back as `expectedVersion` so a second
345
+ * editor's whole-tree overwrite is a 409 rather than a silent replacement. */
346
+ version?: number;
347
+ }
348
+
349
+ /** Mirror of `MAX_MENU_DEPTH` in @pramen/cms — the editor refuses to nest deeper rather
350
+ * than letting a save fail on the server with the tree already reordered on screen. */
351
+ export const MAX_MENU_DEPTH = 5;
352
+
353
+ export interface Redirect {
354
+ id: string;
355
+ fromPath: string;
356
+ toPath: string;
357
+ status: number;
358
+ enabled: boolean;
359
+ note?: string | null;
360
+ createdAt?: string;
361
+ updatedAt?: string;
362
+ }
363
+
364
+ /** Mirror of `REDIRECT_STATUSES` in @pramen/cms. */
365
+ export const REDIRECT_STATUSES = [301, 302, 307, 308] as const;
366
+
367
+ export interface Taxonomy {
368
+ id: string;
369
+ slug: string;
370
+ label: string;
371
+ pluralLabel?: string | null;
372
+ description?: string | null;
373
+ hierarchical?: boolean;
374
+ }
375
+
376
+ export interface Term {
377
+ id: string;
378
+ taxonomyId: string;
379
+ slug: string;
380
+ label: string;
381
+ description?: string | null;
382
+ parentId?: string | null;
383
+ position?: number;
384
+ children?: Term[];
385
+ }
386
+
387
+ export type WidgetType = "content" | "menu" | "component";
388
+
389
+ export interface Widget {
390
+ id: string;
391
+ type: WidgetType;
392
+ title?: string | null;
393
+ content?: RichTextDoc;
394
+ menuName?: string;
395
+ componentId?: string;
396
+ componentProps?: FieldValues;
397
+ }
398
+
399
+ export interface WidgetArea {
400
+ id: string;
401
+ name: string;
402
+ label: string;
403
+ description?: string | null;
404
+ widgets?: Widget[] | null;
405
+ /** See {@link Menu.version}. */
406
+ version?: number;
407
+ }
408
+
409
+ // --- Block Kit: custom admin pages (mirrors @pramen/cms `./blockkit`) ----------------
410
+
411
+ /** An input a Block Kit form or actions row can carry. */
412
+ export type AdminInput =
413
+ | { type: "text_input"; action_id: string; label?: string; placeholder?: string; initial_value?: string; multiline?: boolean; required?: boolean }
414
+ | { type: "number_input"; action_id: string; label?: string; placeholder?: string; initial_value?: number; min?: number; max?: number; required?: boolean }
415
+ | { type: "select"; action_id: string; label?: string; options: { value: string; label: string }[]; initial_value?: string; required?: boolean }
416
+ | { type: "toggle"; action_id: string; label?: string; initial_value?: boolean }
417
+ /** Write-only: deliberately has NO `initial_value`, so a stored secret is never echoed
418
+ * back into the admin's DOM. */
419
+ | { type: "secret_input"; action_id: string; label?: string; placeholder?: string; required?: boolean };
420
+
421
+ export interface AdminButton {
422
+ type: "button";
423
+ action_id: string;
424
+ label: string;
425
+ style?: "primary" | "secondary" | "danger";
426
+ value?: string;
427
+ /** Ask before firing. A page has no code in the browser, so it cannot put up its own. */
428
+ confirm?: string;
429
+ }
430
+
431
+ export type AdminElement = AdminButton | AdminInput;
432
+
433
+ export type AdminBlock =
434
+ | { type: "header"; text: string; level?: 1 | 2 | 3 }
435
+ | { type: "section"; text: string }
436
+ | { type: "divider" }
437
+ | { type: "context"; text: string }
438
+ | { type: "fields"; fields: { label: string; value: string }[] }
439
+ | { type: "table"; columns: { key: string; label: string }[]; rows: Record<string, string | number | boolean | null>[]; empty?: string }
440
+ | { type: "stats"; stats: { label: string; value: string; hint?: string }[] }
441
+ | { type: "actions"; block_id?: string; elements: AdminElement[] }
442
+ | { type: "form"; block_id: string; fields: AdminInput[]; submit: { label: string; action_id: string } }
443
+ | { type: "image"; url: string; alt?: string; caption?: string }
444
+ | { type: "columns"; columns: AdminBlock[][] }
445
+ | { type: "empty"; text: string; hint?: string }
446
+ | { type: "accordion"; title: string; blocks: AdminBlock[]; open?: boolean };
447
+
448
+ /** What a Block Kit page answers with. The WHOLE page comes back on every interaction. */
449
+ export interface AdminPageResponse {
450
+ blocks: AdminBlock[];
451
+ toast?: { text: string; tone?: "info" | "success" | "error" };
452
+ }
453
+
454
+ /** A custom admin page, as the editor sees it (from `listAdminPages`) — never the render
455
+ * function, and never the role list. A page the caller may not open is simply absent. */
456
+ export interface AdminPageMeta {
457
+ slug: string;
458
+ label: string;
459
+ icon?: string;
460
+ navOrder?: number;
461
+ }