@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.
|
|
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.
|
|
18
|
-
"@mosetta/
|
|
19
|
-
"@mosetta/
|
|
20
|
-
"@mosetta/
|
|
21
|
-
"@mosetta/
|
|
22
|
-
"@mosetta/
|
|
23
|
-
"@mosetta/
|
|
24
|
-
"@mosetta/
|
|
25
|
-
"@mosetta/
|
|
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
|
|
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
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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 (
|