@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/README.md +8 -0
- package/dist/editor.css +1 -1
- package/dist/editor.js +166 -163
- package/package.json +1 -1
- package/src/api.ts +80 -2
- package/src/app-context.tsx +42 -3
- package/src/blockkit.tsx +410 -0
- package/src/buzola.gen.ts +117 -23
- package/src/chrome.ts +16 -0
- package/src/components.tsx +102 -10
- package/src/fields.tsx +257 -2
- package/src/furniture.tsx +924 -0
- package/src/nav.ts +134 -0
- package/src/routes/_layout.tsx +36 -54
- package/src/routes/admin-page.tsx +36 -0
- package/src/routes/block-type.tsx +29 -0
- package/src/routes/content-type.tsx +32 -0
- package/src/routes/menu.tsx +30 -0
- package/src/routes/menus.tsx +18 -0
- package/src/routes/page.tsx +1 -1
- package/src/routes/redirects.tsx +17 -0
- package/src/routes/schema.tsx +34 -0
- package/src/routes/taxonomies.tsx +18 -0
- package/src/routes/taxonomy.tsx +29 -0
- package/src/routes/widget-area.tsx +29 -0
- package/src/routes/widgets.tsx +18 -0
- package/src/schema-builder.tsx +944 -0
- package/src/types.ts +237 -1
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 = {
|
|
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
|
+
}
|