smoodly 0.0.28 → 0.0.30
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 +123 -17
- package/dist/admin/client-ops.js +1 -0
- package/dist/admin/drift-copy.d.ts +19 -0
- package/dist/admin/drift-copy.js +89 -0
- package/dist/admin/drift.d.ts +76 -0
- package/dist/admin/drift.js +264 -0
- package/dist/admin/editor/CanvasWarning.d.ts +5 -0
- package/dist/admin/editor/CanvasWarning.js +10 -0
- package/dist/admin/editor/EditorView.js +113 -26
- package/dist/admin/editor/InlineRichtext.js +3 -1
- package/dist/admin/editor/Outline.d.ts +7 -0
- package/dist/admin/editor/Outline.js +8 -5
- package/dist/admin/editor/PageInspector.d.ts +6 -1
- package/dist/admin/editor/PageInspector.js +4 -2
- package/dist/admin/editor/PageSettings.js +26 -16
- package/dist/admin/editor/UnknownNode.d.ts +7 -0
- package/dist/admin/editor/UnknownNode.js +8 -0
- package/dist/admin/editor/canvas-diagnosis.d.ts +49 -0
- package/dist/admin/editor/canvas-diagnosis.js +106 -0
- package/dist/admin/editor/crash-copy.d.ts +7 -0
- package/dist/admin/editor/crash-copy.js +13 -0
- package/dist/admin/editor/protocol.d.ts +2 -0
- package/dist/admin/editor/useCanvasBridge.d.ts +3 -0
- package/dist/admin/editor/useCanvasBridge.js +14 -7
- package/dist/admin/editor/useCanvasDiagnosis.d.ts +7 -0
- package/dist/admin/editor/useCanvasDiagnosis.js +24 -0
- package/dist/admin/field-paths.d.ts +3 -0
- package/dist/admin/field-paths.js +26 -0
- package/dist/admin/forms/OrphanFields.d.ts +8 -0
- package/dist/admin/forms/OrphanFields.js +11 -0
- package/dist/admin/forms/RefPicker.js +6 -2
- package/dist/admin/forms/RichtextElementCard.js +9 -3
- package/dist/admin/forms/RichtextWidget.js +3 -1
- package/dist/admin/forms/TagsField.js +1 -1
- package/dist/admin/forms/richtext-form-context.d.ts +8 -0
- package/dist/admin/forms/richtext-form-context.js +5 -0
- package/dist/admin/health.d.ts +23 -0
- package/dist/admin/health.js +42 -0
- package/dist/admin/index.d.ts +1 -0
- package/dist/admin/next/bridge.js +4 -2
- package/dist/admin/next/index.d.ts +0 -1
- package/dist/admin/next/index.js +6 -1
- package/dist/admin/ops-impl.js +40 -9
- package/dist/admin/ops.d.ts +13 -0
- package/dist/admin/shell/AddRedirectModal.js +1 -1
- package/dist/admin/shell/ContentHealthPage.d.ts +10 -0
- package/dist/admin/shell/ContentHealthPage.js +37 -0
- package/dist/admin/shell/EntriesList.js +4 -4
- package/dist/admin/shell/EntryForm.js +20 -3
- package/dist/admin/shell/ErrorPane.d.ts +4 -2
- package/dist/admin/shell/ErrorPane.js +5 -4
- package/dist/admin/shell/GlobalForm.js +22 -2
- package/dist/admin/shell/PagesList.js +3 -3
- package/dist/admin/shell/SettingsView.js +6 -2
- package/dist/admin/shell/health-list.d.ts +27 -0
- package/dist/admin/shell/health-list.js +47 -0
- package/dist/admin/shell/settings.d.ts +4 -0
- package/dist/admin/shell/settings.js +5 -2
- package/dist/admin/tree-ops.d.ts +4 -0
- package/dist/admin/tree-ops.js +15 -1
- package/dist/admin/ui/ActionMenu.d.ts +20 -0
- package/dist/admin/ui/ActionMenu.js +33 -7
- package/dist/admin/ui/Combobox.d.ts +8 -2
- package/dist/admin/ui/Combobox.js +48 -16
- package/dist/admin/ui/ConfirmDialog.js +3 -3
- package/dist/admin/ui/DriftNotice.d.ts +6 -0
- package/dist/admin/ui/DriftNotice.js +10 -0
- package/dist/admin/ui/combo-box.d.ts +13 -0
- package/dist/admin/ui/combo-box.js +8 -0
- package/dist/admin/ui/confirm.d.ts +8 -0
- package/dist/admin/ui/confirm.js +17 -4
- package/dist/admin/ui/theme.js +29 -3
- package/dist/admin/validate.js +15 -47
- package/dist/custom/CustomSection.d.ts +1 -0
- package/dist/custom/CustomSection.js +4 -1
- package/dist/entry-store.d.ts +13 -0
- package/dist/entry-store.js +26 -3
- package/dist/index.d.ts +2 -2
- package/dist/marks.d.ts +12 -0
- package/dist/marks.js +14 -0
- package/dist/next/canvas-boundary.d.ts +16 -0
- package/dist/next/canvas-boundary.js +25 -0
- package/dist/next/page-renderer.d.ts +3 -1
- package/dist/next/page-renderer.js +33 -4
- package/dist/pairing.js +10 -3
- package/dist/render.d.ts +12 -1
- package/dist/render.js +15 -4
- package/dist/section-crash.d.ts +11 -0
- package/dist/section-crash.js +16 -0
- package/dist/section-guard.d.ts +27 -0
- package/dist/section-guard.js +41 -0
- package/dist/store.d.ts +9 -0
- package/dist/store.js +8 -0
- package/dist/supabase-entry-store.d.ts +2 -1
- package/dist/supabase-entry-store.js +27 -5
- package/dist/supabase-store.d.ts +2 -1
- package/dist/supabase-store.js +27 -0
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { CrashPlaceholder } from "./section-crash.js";
|
|
3
|
+
/** The paired component's own view call, set by pair() and the custom section — only for a plain-function view. */
|
|
4
|
+
export const RENDER_VIEW = Symbol("smoodly.renderView");
|
|
5
|
+
/** A view the guard may call: a plain function — not a client reference or memo/forwardRef ($$typeof), not a class. */
|
|
6
|
+
export function isPlainView(View) {
|
|
7
|
+
if (typeof View !== "function")
|
|
8
|
+
return false;
|
|
9
|
+
const v = View;
|
|
10
|
+
return v.$$typeof === undefined && !v.prototype?.isReactComponent;
|
|
11
|
+
}
|
|
12
|
+
/** The placeholder shows in the canvas, and under `next dev` so a developer never misses their own crash. */
|
|
13
|
+
const placeholderShown = (editor) => editor || process.env.NODE_ENV === "development";
|
|
14
|
+
function failed(error, ctx) {
|
|
15
|
+
ctx.rethrow?.(error); // notFound(), redirects and bailouts are control flow, not crashes
|
|
16
|
+
console.error(`smoodly: section "${ctx.type}" (node ${ctx.id}) failed to render and is left out of the page.`, error);
|
|
17
|
+
return placeholderShown(ctx.editor) ? _jsx(CrashPlaceholder, { id: ctx.id, title: ctx.title, error: error, live: "section" }) : null;
|
|
18
|
+
}
|
|
19
|
+
const isThenable = (v) => typeof v === "object" && v !== null && typeof v.then === "function";
|
|
20
|
+
/** A section through the guard when its component offers the view call; any other component as an element, unguarded. */
|
|
21
|
+
export function renderGuarded(Component, props, ctx) {
|
|
22
|
+
const render = Component[RENDER_VIEW];
|
|
23
|
+
if (!render)
|
|
24
|
+
return _jsx(Component, { ...props });
|
|
25
|
+
try {
|
|
26
|
+
const out = render(props);
|
|
27
|
+
if (!isThenable(out))
|
|
28
|
+
return out;
|
|
29
|
+
// Settled up front, so a rejection is never unhandled while React gets to the guard.
|
|
30
|
+
const settled = Promise.resolve(out).then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
|
|
31
|
+
return _jsx(SectionGuard, { settled: settled, ctx: ctx });
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return failed(error, ctx);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** An async section, awaited in place — no <Suspense>, so it renders where it always did. */
|
|
38
|
+
export async function SectionGuard({ settled, ctx }) {
|
|
39
|
+
const result = await settled;
|
|
40
|
+
return result.ok ? result.value : failed(result.error, ctx);
|
|
41
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -52,10 +52,18 @@ export type PageVersion = {
|
|
|
52
52
|
tree: PageTree;
|
|
53
53
|
savedAt: number;
|
|
54
54
|
};
|
|
55
|
+
/** One page for the drift scan (spec 2026-09-27 editor safety §3): locale → draft and live tree; null where there is none. */
|
|
56
|
+
export type PageScan = {
|
|
57
|
+
record: PageRecord;
|
|
58
|
+
draft: Record<string, PageTree | null>;
|
|
59
|
+
live: Record<string, PageTree | null>;
|
|
60
|
+
};
|
|
55
61
|
export interface PageStore {
|
|
56
62
|
createPage(input: CreatePageInput, by?: string): Promise<PageRecord>;
|
|
57
63
|
getPage(id: string): Promise<PageRecord | null>;
|
|
58
64
|
listPages(): Promise<PageRecord[]>;
|
|
65
|
+
/** Every page with each locale's draft and live tree, in few reads (spec 2026-09-27 editor safety §3). */
|
|
66
|
+
scanTrees(): Promise<PageScan[]>;
|
|
59
67
|
resolvePath(locale: string, path: string): Promise<PathHit | null>;
|
|
60
68
|
/** The database's schema version from `smoodly_meta`; null when the table is missing (older than v1). Memory: always current (spec 2026-09-26 §5). */
|
|
61
69
|
schemaVersion(): Promise<number | null>;
|
|
@@ -212,6 +220,7 @@ export declare class MemoryPageStore implements PageStore {
|
|
|
212
220
|
private dropVersions;
|
|
213
221
|
createPage(input: CreatePageInput, by?: string): Promise<PageRecord>;
|
|
214
222
|
getPage(id: string): Promise<PageRecord | null>;
|
|
223
|
+
scanTrees(): Promise<PageScan[]>;
|
|
215
224
|
listPages(): Promise<PageRecord[]>;
|
|
216
225
|
resolvePath(locale: string, path: string): Promise<PathHit | null>;
|
|
217
226
|
schemaVersion(): Promise<number>;
|
package/dist/store.js
CHANGED
|
@@ -277,6 +277,14 @@ export class MemoryPageStore {
|
|
|
277
277
|
const page = this.pages.get(id);
|
|
278
278
|
return page ? this.snapshot(page) : null;
|
|
279
279
|
}
|
|
280
|
+
async scanTrees() {
|
|
281
|
+
const tree = (id) => (id ? structuredClone(this.versions.get(id)?.tree ?? null) : null);
|
|
282
|
+
return (await this.listPages()).map((record) => ({
|
|
283
|
+
record,
|
|
284
|
+
draft: Object.fromEntries(Object.entries(record.locales).map(([l, row]) => [l, tree(row.draftVersionId)])),
|
|
285
|
+
live: Object.fromEntries(Object.entries(record.locales).map(([l, row]) => [l, tree(row.publishedVersionId)])),
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
280
288
|
async listPages() {
|
|
281
289
|
return [...this.pages.values()]
|
|
282
290
|
// null sorts last; the SQL equivalent is ORDER BY sort ASC NULLS LAST, created_at ASC.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
2
|
import type { CollectionSchema } from "./collections.ts";
|
|
3
|
-
import type { AddEntryLocaleOptions, CreateEntryOptions, EntryRecord, EntryStore, EntryStoreOptions, EntryUsage, EntryVersion, ListOptions } from "./entry-store.ts";
|
|
3
|
+
import type { AddEntryLocaleOptions, CreateEntryOptions, EntryRecord, EntryStore, EntryStoreOptions, EntryScan, EntryUsage, EntryVersion, ListOptions } from "./entry-store.ts";
|
|
4
4
|
import { type DraftToken } from "./draft-token.ts";
|
|
5
5
|
export declare class SupabaseEntryStore implements EntryStore {
|
|
6
6
|
private db;
|
|
@@ -68,6 +68,7 @@ export declare class SupabaseEntryStore implements EntryStore {
|
|
|
68
68
|
discardDraft(collection: string, id: string, locale: string, by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
69
69
|
addLocale(collection: string, id: string, locale: string, options: AddEntryLocaleOptions, by?: string): Promise<EntryRecord>;
|
|
70
70
|
removeLocale(collection: string, id: string, locale: string): Promise<EntryRecord>;
|
|
71
|
+
scanEntries(): Promise<EntryScan[]>;
|
|
71
72
|
listVersions(collection: string, id: string, locale: string): Promise<EntryVersion[]>;
|
|
72
73
|
restore(collection: string, id: string, locale: string, versionId: string, by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
73
74
|
getVersion(collection: string, versionId: string): Promise<EntryVersion | null>;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// calls the smoodly_reorder SQL function: one statement, updated_at
|
|
8
8
|
// untouched.
|
|
9
9
|
import { collectionDepth, collectionOrder } from "./collections.js";
|
|
10
|
-
import { ARRAY_FIELD_TYPES, DEFAULT_ENTRY_LOCALES, entryErrors, entryFieldsIn, entryLocaleRow, entryRefEdges, entryTitleIn, isGlobalSchema, schemaOf, splitFields, } from "./entry-store.js";
|
|
10
|
+
import { ARRAY_FIELD_TYPES, DEFAULT_ENTRY_LOCALES, entryErrors, entryFieldsIn, entryLocaleRow, entryRefEdges, entryTitleIn, isGlobalSchema, keepLocalizedShared, schemaOf, splitFields, } from "./entry-store.js";
|
|
11
11
|
import { globalEntryId } from "./global-id.js";
|
|
12
12
|
import { SupabasePathIndex } from "./supabase-path-index.js";
|
|
13
13
|
import { groupRedirects } from "./store.js";
|
|
@@ -473,11 +473,12 @@ export class SupabaseEntryStore {
|
|
|
473
473
|
if (isGlobalSchema(schema) && slug !== null)
|
|
474
474
|
throw entryErrors.noSlug(collection);
|
|
475
475
|
await this.assertSiblingSlug(collection, before.parentId, locale, slug, id);
|
|
476
|
+
const nodeFields = keepLocalizedShared(before.fields, shared, schema);
|
|
476
477
|
// The snapshot of what the row is about to hold, written under the guard BEFORE the live
|
|
477
478
|
// columns change (spec 2026-09-26 concurrency §3): a stale save leaves nothing behind.
|
|
478
|
-
const next = { ...before, fields:
|
|
479
|
+
const next = { ...before, fields: nodeFields, locales: { ...before.locales, [locale]: { ...row, fields: localized, slug } } };
|
|
479
480
|
const { created } = await this.recordVersion(next, locale, expected !== undefined);
|
|
480
|
-
await this.updateNode(id, { fields:
|
|
481
|
+
await this.updateNode(id, { fields: nodeFields });
|
|
481
482
|
await this.updateLocale(id, locale, { slug, fields: localized }, { by });
|
|
482
483
|
const record = await this.must(collection, id);
|
|
483
484
|
try {
|
|
@@ -535,7 +536,7 @@ export class SupabaseEntryStore {
|
|
|
535
536
|
const matched = await this.guardedUpdateLocale(id, locale, { fields: localized, draft_version_id: row.publishedVersionId }, { by, guard: expected !== undefined ? row.draftVersionId : undefined });
|
|
536
537
|
if (!matched)
|
|
537
538
|
throw await this.stale(collection, id, locale);
|
|
538
|
-
await this.updateNode(id, { fields: shared });
|
|
539
|
+
await this.updateNode(id, { fields: keepLocalizedShared(record.fields, shared, schema) });
|
|
539
540
|
if (row.draftVersionId && row.draftVersionId !== row.publishedVersionId) {
|
|
540
541
|
// The pointer moved first, so a failure here leaves a stray row, never a dangling pointer.
|
|
541
542
|
const gone = await this.db.from("entry_versions").delete().eq("id", row.draftVersionId);
|
|
@@ -598,6 +599,26 @@ export class SupabaseEntryStore {
|
|
|
598
599
|
await this.writeRefs(final);
|
|
599
600
|
return final;
|
|
600
601
|
}
|
|
602
|
+
async scanEntries() {
|
|
603
|
+
const res = await this.db.from("entries").select(SELECT).order("created_at", { ascending: true }).order("id", { ascending: true });
|
|
604
|
+
if (res.error)
|
|
605
|
+
this.fail(res.error.message);
|
|
606
|
+
const records = res.data.map(toRecord);
|
|
607
|
+
const ids = records.flatMap((r) => Object.values(r.locales).flatMap((row) => (row.status === "published" && row.publishedVersionId ? [row.publishedVersionId] : [])));
|
|
608
|
+
// In chunks: one in() over hundreds of ids would overflow the request URL.
|
|
609
|
+
const snaps = new Map();
|
|
610
|
+
for (let i = 0; i < ids.length; i += 100) {
|
|
611
|
+
for (const [id, fields] of await this.versionFields(ids.slice(i, i + 100)))
|
|
612
|
+
snaps.set(id, fields);
|
|
613
|
+
}
|
|
614
|
+
return records.map((record) => ({
|
|
615
|
+
record,
|
|
616
|
+
live: Object.fromEntries(Object.entries(record.locales).map(([l, row]) => [
|
|
617
|
+
l,
|
|
618
|
+
row.status !== "published" ? null : (row.publishedVersionId ? snaps.get(row.publishedVersionId) : undefined) ?? entryFieldsIn(record, l),
|
|
619
|
+
])),
|
|
620
|
+
}));
|
|
621
|
+
}
|
|
601
622
|
async listVersions(collection, id, locale) {
|
|
602
623
|
await this.must(collection, id);
|
|
603
624
|
const res = await this.db
|
|
@@ -621,7 +642,8 @@ export class SupabaseEntryStore {
|
|
|
621
642
|
assertFresh(row, expected);
|
|
622
643
|
if (versionId === row.publishedVersionId)
|
|
623
644
|
return this.discardDraft(collection, id, locale, by, expected);
|
|
624
|
-
const { shared, localized } = splitFields(version.fields, schema);
|
|
645
|
+
const { shared: split, localized } = splitFields(version.fields, schema);
|
|
646
|
+
const shared = keepLocalizedShared(entry.fields, split, schema);
|
|
625
647
|
// The snapshot first, under the guard, as in update.
|
|
626
648
|
const next = { ...entry, fields: shared, locales: { ...entry.locales, [locale]: { ...row, fields: localized } } };
|
|
627
649
|
await this.recordVersion(next, locale, expected !== undefined);
|
package/dist/supabase-store.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
import { type AddLocaleOptions, type CreatePageInput, type PageRecord, type PageStore, type PageStoreOptions, type PageVersion } from "./store.ts";
|
|
2
|
+
import { type AddLocaleOptions, type CreatePageInput, type PageRecord, type PageStore, type PageStoreOptions, type PageVersion, type PageScan } from "./store.ts";
|
|
3
3
|
import type { PageTree } from "./render.tsx";
|
|
4
4
|
import type { PathRow } from "./path-index.ts";
|
|
5
5
|
import { type DraftToken } from "./draft-token.ts";
|
|
@@ -58,6 +58,7 @@ export declare class SupabasePageStore implements PageStore {
|
|
|
58
58
|
private hardDelete;
|
|
59
59
|
createPage(input: CreatePageInput, by?: string): Promise<PageRecord>;
|
|
60
60
|
getPage(id: string): Promise<PageRecord | null>;
|
|
61
|
+
scanTrees(): Promise<PageScan[]>;
|
|
61
62
|
listPages(): Promise<PageRecord[]>;
|
|
62
63
|
/** The site's one warning per process (spec 2026-09-26 §5): fired by the first locate, never blocking. */
|
|
63
64
|
private schemaChecked;
|
package/dist/supabase-store.js
CHANGED
|
@@ -322,6 +322,33 @@ export class SupabasePageStore {
|
|
|
322
322
|
getPage(id) {
|
|
323
323
|
return this.row(id);
|
|
324
324
|
}
|
|
325
|
+
async scanTrees() {
|
|
326
|
+
const records = await this.listPages();
|
|
327
|
+
const ids = new Set();
|
|
328
|
+
for (const r of records)
|
|
329
|
+
for (const row of Object.values(r.locales)) {
|
|
330
|
+
if (row.draftVersionId)
|
|
331
|
+
ids.add(row.draftVersionId);
|
|
332
|
+
if (row.publishedVersionId)
|
|
333
|
+
ids.add(row.publishedVersionId);
|
|
334
|
+
}
|
|
335
|
+
// In chunks: one in() over hundreds of ids would overflow the request URL.
|
|
336
|
+
const trees = new Map();
|
|
337
|
+
const all = [...ids];
|
|
338
|
+
for (let i = 0; i < all.length; i += 100) {
|
|
339
|
+
const res = await this.db.from("page_versions").select("id, tree").in("id", all.slice(i, i + 100));
|
|
340
|
+
if (res.error)
|
|
341
|
+
this.fail(res.error.message);
|
|
342
|
+
for (const v of res.data)
|
|
343
|
+
trees.set(v.id, v.tree);
|
|
344
|
+
}
|
|
345
|
+
const tree = (id) => (id ? trees.get(id) ?? null : null);
|
|
346
|
+
return records.map((record) => ({
|
|
347
|
+
record,
|
|
348
|
+
draft: Object.fromEntries(Object.entries(record.locales).map(([l, row]) => [l, tree(row.draftVersionId)])),
|
|
349
|
+
live: Object.fromEntries(Object.entries(record.locales).map(([l, row]) => [l, tree(row.publishedVersionId)])),
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
325
352
|
async listPages() {
|
|
326
353
|
const res = await this.db
|
|
327
354
|
.from("pages")
|
package/package.json
CHANGED