@mosetta/editor 0.0.33 → 0.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @mosetta/editor
2
2
 
3
+ ## 0.0.35
4
+
5
+ - Built with the framework 0.0.13: a block's server action is told the block it is called from —
6
+ its settings as the page stores them, read by the host — so a form's server side can trust what
7
+ the site's author set on it (the hours a booking form offers).
8
+
9
+ ## 0.0.34
10
+
11
+ - A new page starts from the site's page preset: the prefab `pagePreset` — the blocks every page of
12
+ the site begins with, and an `outlet` block where the page's own go. The preset is copied into the
13
+ page; a site without one starts its pages in the frame of its home page. "Create a page" and the
14
+ content API's `createPage` (with `preset: true`) both do it. The page preset may hold prefabs.
15
+
3
16
  ## 0.0.33
4
17
 
5
18
  - The assistant's chat lets the person choose the model it works with, among the ones the platform
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mosetta/editor",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "license": "MIT",
5
5
  "description": "mosetta visual editor tile palette: block picker, page tree, inspector, isolated draft preview and the content API behind them.",
6
6
  "repository": {
@@ -14,15 +14,15 @@
14
14
  "dependencies": {
15
15
  "react": "*",
16
16
  "react-dom": "*",
17
- "@mosetta/content": "0.0.12",
18
- "@mosetta/core": "0.0.12",
19
- "@mosetta/di": "0.0.12",
20
- "@mosetta/runtime": "0.0.12",
21
- "@mosetta/stores": "0.0.12",
22
- "@mosetta/server-action": "0.0.12",
23
- "@mosetta/settings": "0.0.12",
24
- "@mosetta/ui": "0.0.12",
25
- "@mosetta/react": "0.0.12"
17
+ "@mosetta/content": "0.0.13",
18
+ "@mosetta/di": "0.0.13",
19
+ "@mosetta/react": "0.0.13",
20
+ "@mosetta/settings": "0.0.13",
21
+ "@mosetta/runtime": "0.0.13",
22
+ "@mosetta/core": "0.0.13",
23
+ "@mosetta/stores": "0.0.13",
24
+ "@mosetta/server-action": "0.0.13",
25
+ "@mosetta/ui": "0.0.13"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/react": "*",
@@ -42,6 +42,8 @@ import type { ZodType } from 'zod';
42
42
  import { projectNamespaceOf, unitOf } from '@mosetta/content';
43
43
  import type { IContentTransport } from '@mosetta/content';
44
44
  import { CONFIG_PATH } from '@mosetta/content/paths';
45
+ import { homeFrameOf, PAGE_PRESET, startedPage } from './pagePreset';
46
+ import type { TPageStart } from './pagePreset';
45
47
  import { blockSchemas, settingsIssueLine, treeSettingsIssues } from '@mosetta/settings/check';
46
48
  import type { ICatalogSettingsEntry } from '@mosetta/settings/check';
47
49
  import {
@@ -194,11 +196,14 @@ function pageReferences(page: IPageDoc): string[] {
194
196
  /**
195
197
  * A tree's references checked the way each kind exists: a module against the catalog; a live
196
198
  * PREFAB is content (the project's) or the distribution's and never in the catalog — a missing one
197
- * renders the node's children without it. Inside a prefab a prefab is refused: one level only.
199
+ * renders the node's children without it. Inside a prefab a prefab is refused: one level only —
200
+ * except in the site's page preset, which is never placed but copied into a new page, its prefabs
201
+ * becoming the page's own (pagePreset.ts).
198
202
  */
199
- function referenceIssues(refs: readonly string[], known: ReadonlySet<string>, kind: ContentKind): string[] {
203
+ function referenceIssues(refs: readonly string[], known: ReadonlySet<string>, kind: ContentKind, name: string): string[] {
204
+ const holdsPrefabs = kind === 'pages' || name === PAGE_PRESET;
200
205
  return refs.flatMap((ref) => {
201
- if (unitOf(ref) !== null) return kind === 'prefabs' ? [`a prefab cannot hold a prefab: ${ref}`] : [];
206
+ if (unitOf(ref) !== null) return holdsPrefabs ? [] : [`a prefab cannot hold a prefab: ${ref}`];
202
207
  return known.has(ref) ? [] : [`unknown tile reference: ${ref}`];
203
208
  });
204
209
  }
@@ -325,20 +330,43 @@ class LoadPageDraftAction extends ContentAction<TDraftInput, { draft: ILoadedDra
325
330
  }
326
331
  }
327
332
 
328
- type TPageInput = { name: string; page: IPageDoc } & IKindInput;
333
+ type TPageInput = { name: string; page: IPageDoc; preset?: boolean } & IKindInput;
329
334
 
330
335
  @DiServerAction(createPageToken)
331
- class CreatePageAction extends ContentAction<TPageInput, { draftId: string }> {
336
+ class CreatePageAction extends ContentAction<TPageInput, { draftId: string; started?: TPageStart }> {
332
337
  constructor(@DiInject(CONTENT_REPOSITORY_TOKEN) private readonly content: ContentRepository) {
333
338
  super();
334
339
  }
335
340
 
336
341
  protected inputSchema(builder: ICreateSchema['builder'], s: ContentSchemas) {
337
- return builder.object({ name: s.entityName, page: s.pageDoc, kind: s.contentKind.optional() }) as unknown as ZodType<TPageInput>;
342
+ return builder.object({ name: s.entityName, page: s.pageDoc, preset: builder.boolean().optional(), kind: s.contentKind.optional() }) as unknown as ZodType<TPageInput>;
338
343
  }
339
344
 
340
- protected async run({ name, page, kind = 'pages' }: TPageInput) {
341
- return { draftId: await this.content.create(kind, name, storedFormOf(kind, page), kind === 'prefabs' ? 'unit' : undefined) };
345
+ // with `preset`, a page starts as the site's pages do (pagePreset.ts): its blocks go into the
346
+ // site's page preset, or into its home page's frame
347
+ protected async run({ name, page, preset = false, kind = 'pages' }: TPageInput) {
348
+ const start = preset && kind === 'pages' ? await this.startOf() : null;
349
+ const placed = start ? startedPage(start.blocks, page.children ?? []) : null;
350
+ const made = placed?.started ? { ...page, children: placed.children } : page;
351
+ const draftId = await this.content.create(kind, name, storedFormOf(kind, made), kind === 'prefabs' ? 'unit' : undefined);
352
+ return { draftId, ...(placed?.started ? { started: start!.from } : {}) };
353
+ }
354
+
355
+ /** what a new page of this site starts with: its preset, else its home page's frame; null — neither */
356
+ private async startOf(): Promise<{ from: TPageStart; blocks: IBlueprintNodeDoc[] } | null> {
357
+ const published = async (kind: ContentKind, name: string): Promise<IPageDoc | null> => {
358
+ try {
359
+ return editedFormOf(kind, (await this.content.load<unknown>(kind, name)).doc);
360
+ } catch (e) {
361
+ if (e instanceof ContentError) return null;
362
+ throw e;
363
+ }
364
+ };
365
+ const preset = await published('prefabs', PAGE_PRESET);
366
+ if (preset) return { from: 'pagePreset', blocks: preset.children ?? [] };
367
+ const home = await published('pages', 'home');
368
+ const frame = home ? homeFrameOf(home.children ?? []) : [];
369
+ return frame.length ? { from: 'home', blocks: frame } : null;
342
370
  }
343
371
  }
344
372
 
@@ -367,7 +395,7 @@ class SavePageDraftAction extends ContentAction<TSaveInput, { warnings: string[]
367
395
  // NOT soft: a page whose seq does not follow the stored draft's comes back as CONFLICT.
368
396
  protected async run({ name, draftId, page, kind = 'pages' }: TSaveInput) {
369
397
  const known = await this.catalog.knownReferences();
370
- const warnings = referenceIssues(pageReferences(page), known, kind);
398
+ const warnings = referenceIssues(pageReferences(page), known, kind, name);
371
399
  await this.content.saveDraft(kind, name, draftId, storedFormOf(kind, page));
372
400
  return { warnings };
373
401
  }
@@ -450,7 +478,7 @@ class PublishPageAction extends ContentAction<TPublishInput, { live: number }> {
450
478
  const known = new Set(descriptors.map((d) => d.id));
451
479
  const edited = editedFormOf(kind, doc);
452
480
  const refs = pageReferences(edited);
453
- issues.push(...referenceIssues(refs, known, kind));
481
+ issues.push(...referenceIssues(refs, known, kind, name));
454
482
  issues.push(...treeSettingsIssues(edited, blockSchemas(descriptors as readonly ICatalogSettingsEntry[])).map(settingsIssueLine));
455
483
 
456
484
  if (kind === 'pages') {
@@ -63,9 +63,13 @@ export const loadPageDraftToken = new ServerActionToken<
63
63
  ContentActionResult<{ draft: ILoadedDraft<IPageDoc> }>
64
64
  >({ actionName: 'loadPageDraft', serverModuleName: 'content', tags: ['pages'] });
65
65
 
66
+ /**
67
+ * A new page or prefab. With `preset`, a page starts as the site's pages do: its blocks inside the
68
+ * site's page preset (or its home page's frame) — `started` says which, when either was used.
69
+ */
66
70
  export const createPageToken = new ServerActionToken<
67
- { name: string; page: IPageDoc } & IKindInput,
68
- ContentActionResult<{ draftId: string }>
71
+ { name: string; page: IPageDoc; preset?: boolean } & IKindInput,
72
+ ContentActionResult<{ draftId: string; started?: 'pagePreset' | 'home' }>
69
73
  >({ actionName: 'createPage', serverModuleName: 'content', invalidates: ['pages'] });
70
74
 
71
75
  export const savePageDraftToken = new ServerActionToken<
@@ -0,0 +1,81 @@
1
+ // A NEW PAGE STARTS FROM THE SITE'S PAGE PRESET: the prefab `pagePreset` — the blocks every new page
2
+ // of the site begins with (its theme, its translations, a sign-in wrapper, whatever the site wants
3
+ // on all of its pages), and an OUTLET block where the page's own blocks go. A site without one starts
4
+ // its pages in the frame of its home page: the prefabs that page is built in, one inside the other.
5
+ // The preset is COPIED into the page, not referenced: changing it changes the pages made after,
6
+ // never the ones made before. One rule for every way a page is made — the editor's Create, the
7
+ // assistant's create_page — so no author has to remember what a page of this site needs.
8
+ import type { IBlueprintNodeDoc } from '@mosetta/content';
9
+ import { isOutletReference, unitOf } from '@mosetta/content/units';
10
+
11
+ /** the prefab a site names its page preset by */
12
+ export const PAGE_PRESET = 'pagePreset';
13
+
14
+ /** where a new page's start came from: the site's preset, or its home page's frame */
15
+ export type TPageStart = 'pagePreset' | 'home';
16
+
17
+ type TNode = IBlueprintNodeDoc;
18
+
19
+ const idsIn = (nodes: readonly TNode[]): string[] => nodes.flatMap((node) => [node.id, ...idsIn(node.children ?? [])]);
20
+
21
+ /**
22
+ * The frame of a home page: its blocks while they are one prefab inside another, outermost first,
23
+ * each without the page's own blocks — none when the page's blocks are not one prefab.
24
+ */
25
+ export function homeFrameOf(home: readonly TNode[]): TNode[] {
26
+ const chain: TNode[] = [];
27
+ for (let level = home; level.length === 1 && unitOf(level[0].reference); level = level[0].children ?? []) chain.push(level[0]);
28
+ return chain.reduceRight<TNode[]>((inner, { children: _children, ...node }) => [{ ...node, children: inner }], []);
29
+ }
30
+
31
+ /** the first outlet of a tree in document order, as the path of child indexes leading to it */
32
+ function outletPath(nodes: readonly TNode[]): number[] | null {
33
+ for (let i = 0; i < nodes.length; i++) {
34
+ if (isOutletReference(nodes[i].reference)) return [i];
35
+ const below = outletPath(nodes[i].children ?? []);
36
+ if (below) return [i, ...below];
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /** the tree with the node at `path` replaced by `content` */
42
+ function replacedAt(nodes: readonly TNode[], path: readonly number[], content: readonly TNode[]): TNode[] {
43
+ const [at, ...rest] = path;
44
+ if (!rest.length) return [...nodes.slice(0, at), ...content, ...nodes.slice(at + 1)];
45
+ return nodes.map((node, i) => (i === at ? { ...node, children: replacedAt(node.children ?? [], rest, content) } : node));
46
+ }
47
+
48
+ /** the tree with `content` added to its innermost block while the blocks are one inside another */
49
+ function appendedInside(nodes: readonly TNode[], content: readonly TNode[]): TNode[] {
50
+ if (nodes.length !== 1) return [...nodes, ...content];
51
+ const [only] = nodes;
52
+ const children = only.children ?? [];
53
+ return [{ ...only, children: children.length === 1 ? appendedInside(children, content) : [...children, ...content] }];
54
+ }
55
+
56
+ /** the start's blocks renamed where the page's own blocks already have an id: the page's ids stay */
57
+ function withFreeIds(nodes: readonly TNode[], taken: Set<string>): TNode[] {
58
+ return nodes.map((node) => {
59
+ let id = node.id;
60
+ if (taken.has(id)) {
61
+ let n = 1;
62
+ while (taken.has(`${node.id}-preset${n > 1 ? n : ''}`)) n++;
63
+ id = `${node.id}-preset${n > 1 ? n : ''}`;
64
+ }
65
+ taken.add(id);
66
+ return { ...node, id, children: withFreeIds(node.children ?? [], taken) };
67
+ });
68
+ }
69
+
70
+ /**
71
+ * A new page's blocks: the start's (a copy) with the page's own `content` in its outlet — or, with
72
+ * no outlet, inside its innermost block. Content that already begins with a prefab of the start's
73
+ * (its author built the frame) is taken as it is; so is everything when the start is empty.
74
+ */
75
+ export function startedPage(start: readonly TNode[], content: readonly TNode[]): { children: TNode[]; started: boolean } {
76
+ const startPrefabs = new Set(start.map((node) => node.reference).filter((reference) => unitOf(reference)));
77
+ if (!start.length || content.some((node) => startPrefabs.has(node.reference))) return { children: [...content], started: false };
78
+ const copy = withFreeIds(structuredClone(start), new Set(idsIn(content)));
79
+ const path = outletPath(copy);
80
+ return { children: path ? replacedAt(copy, path, content) : appendedInside(copy, content), started: true };
81
+ }
@@ -1216,7 +1216,8 @@ function CreateModal({ routes, own, onClose, onJump, onCreated }: ICreateModalPr
1216
1216
  const submit = (): void => {
1217
1217
  if (!name || create.isLoading()) return;
1218
1218
  createdRef.current = name;
1219
- create.start!({ name, page: { id: name, reference: rootId, pattern: byName ? PREFAB_CANVAS_PATTERN : trimmed, children: [] }, kind });
1219
+ // a page starts as the site's pages do — in its page preset (or its home page's frame)
1220
+ create.start!({ name, page: { id: name, reference: rootId, pattern: byName ? PREFAB_CANVAS_PATTERN : trimmed, children: [] }, kind, ...(byName ? {} : { preset: true }) });
1220
1221
  };
1221
1222
 
1222
1223
  return (