smoodly 0.0.15 → 0.0.17
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 +238 -36
- package/dist/admin/auth.d.ts +7 -4
- package/dist/admin/auth.js +1 -1
- package/dist/admin/client-ops.js +3 -3
- package/dist/admin/custom-section.d.ts +10 -0
- package/dist/admin/custom-section.js +10 -0
- package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
- package/dist/admin/editor/CanvasOverlay.js +6 -6
- package/dist/admin/editor/EditorView.d.ts +2 -0
- package/dist/admin/editor/EditorView.js +322 -62
- package/dist/admin/editor/GlobalInspector.d.ts +24 -0
- package/dist/admin/editor/GlobalInspector.js +13 -0
- package/dist/admin/editor/InsertPicker.d.ts +6 -5
- package/dist/admin/editor/InsertPicker.js +6 -4
- package/dist/admin/editor/InspectorFields.d.ts +21 -0
- package/dist/admin/editor/InspectorFields.js +22 -0
- package/dist/admin/editor/Outline.d.ts +0 -1
- package/dist/admin/editor/Outline.js +3 -3
- package/dist/admin/editor/PageSettings.js +3 -2
- package/dist/admin/editor/global-edit.d.ts +39 -0
- package/dist/admin/editor/global-edit.js +124 -0
- package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
- package/dist/admin/editor/useGlobalEdit.js +18 -0
- package/dist/admin/forms/FieldWidget.d.ts +14 -1
- package/dist/admin/forms/FieldWidget.js +38 -11
- package/dist/admin/forms/ListField.d.ts +9 -1
- package/dist/admin/forms/ListField.js +16 -8
- package/dist/admin/forms/RichtextElementCard.js +11 -5
- package/dist/admin/forms/RowsWidget.d.ts +13 -0
- package/dist/admin/forms/RowsWidget.js +26 -0
- package/dist/admin/forms/SpanBar.d.ts +5 -0
- package/dist/admin/forms/SpanBar.js +37 -0
- package/dist/admin/forms/richtext-insert.d.ts +2 -2
- package/dist/admin/forms/richtext-insert.js +3 -3
- package/dist/admin/forms/richtext-nodes.js +1 -1
- package/dist/admin/forms/tabs.d.ts +20 -8
- package/dist/admin/forms/tabs.js +19 -23
- package/dist/admin/forms/useRichtextEditor.js +8 -1
- package/dist/admin/global-items.d.ts +9 -0
- package/dist/admin/{shared-items.js → global-items.js} +29 -14
- package/dist/admin/index.d.ts +3 -3
- package/dist/admin/index.js +1 -1
- package/dist/admin/next/op-handler.js +4 -3
- package/dist/admin/ops-impl.d.ts +3 -0
- package/dist/admin/ops-impl.js +242 -56
- package/dist/admin/ops.d.ts +38 -6
- package/dist/admin/serialize.d.ts +18 -7
- package/dist/admin/serialize.js +27 -5
- package/dist/admin/shell/AdminApp.js +7 -7
- package/dist/admin/shell/EntriesList.js +19 -3
- package/dist/admin/shell/EntryForm.js +183 -77
- package/dist/admin/shell/GlobalFields.d.ts +24 -0
- package/dist/admin/shell/GlobalFields.js +19 -0
- package/dist/admin/shell/{SharedForm.d.ts → GlobalForm.d.ts} +1 -1
- package/dist/admin/shell/GlobalForm.js +258 -0
- package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
- package/dist/admin/shell/{SharedList.js → GlobalList.js} +12 -11
- package/dist/admin/shell/IconRail.js +14 -7
- package/dist/admin/shell/PagesList.js +4 -8
- package/dist/admin/shell/entries-list.d.ts +2 -2
- package/dist/admin/shell/entries-list.js +3 -3
- package/dist/admin/shell/{shared-list.d.ts → global-list.d.ts} +3 -3
- package/dist/admin/shell/{shared-list.js → global-list.js} +2 -2
- package/dist/admin/supabase-auth.js +1 -1
- package/dist/admin/tree-ops.d.ts +10 -8
- package/dist/admin/tree-ops.js +28 -16
- package/dist/admin/ui/ActionMenu.d.ts +11 -0
- package/dist/admin/ui/ActionMenu.js +25 -0
- package/dist/admin/ui/HistoryModal.d.ts +23 -0
- package/dist/admin/ui/HistoryModal.js +40 -0
- package/dist/admin/ui/Modal.d.ts +5 -1
- package/dist/admin/ui/Modal.js +3 -3
- package/dist/admin/ui/PublishControl.d.ts +31 -0
- package/dist/admin/ui/PublishControl.js +24 -0
- package/dist/admin/ui/format.d.ts +4 -0
- package/dist/admin/ui/format.js +9 -0
- package/dist/admin/ui/primitives.d.ts +16 -1
- package/dist/admin/ui/primitives.js +20 -4
- package/dist/admin/ui/status-label.d.ts +10 -0
- package/dist/admin/ui/status-label.js +14 -0
- package/dist/admin/ui/theme.js +93 -13
- package/dist/admin/validate.d.ts +27 -2
- package/dist/admin/validate.js +47 -10
- package/dist/collections.d.ts +3 -9
- package/dist/collections.js +1 -2
- package/dist/conditions.d.ts +33 -0
- package/dist/conditions.js +79 -0
- package/dist/config.d.ts +26 -19
- package/dist/config.js +26 -25
- package/dist/css.d.ts +7 -0
- package/dist/css.js +57 -0
- package/dist/custom/CustomSection.d.ts +26 -0
- package/dist/custom/CustomSection.js +20 -0
- package/dist/custom/css.d.ts +3 -0
- package/dist/custom/css.js +28 -0
- package/dist/custom/index.d.ts +26 -0
- package/dist/custom/index.js +16 -0
- package/dist/custom/rows.d.ts +28 -0
- package/dist/custom/rows.js +129 -0
- package/dist/custom/schema.d.ts +32 -0
- package/dist/custom/schema.js +39 -0
- package/dist/entry-store.d.ts +42 -17
- package/dist/entry-store.js +107 -40
- package/dist/fields.d.ts +5 -2
- package/dist/fields.js +4 -2
- package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
- package/dist/{shared-id.js → global-id.js} +4 -4
- package/dist/global.d.ts +57 -0
- package/dist/global.js +51 -0
- package/dist/index.d.ts +28 -18
- package/dist/index.js +10 -5
- package/dist/media.js +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/page-renderer.d.ts +4 -2
- package/dist/next/page-renderer.js +17 -13
- package/dist/next/queries.d.ts +6 -6
- package/dist/next/queries.js +7 -7
- package/dist/page.js +2 -2
- package/dist/pairing.d.ts +13 -7
- package/dist/pairing.js +4 -4
- package/dist/refs.d.ts +2 -2
- package/dist/refs.js +4 -4
- package/dist/render.d.ts +13 -1
- package/dist/render.js +10 -5
- package/dist/resolve.d.ts +11 -6
- package/dist/resolve.js +22 -12
- package/dist/revalidate.d.ts +4 -4
- package/dist/revalidate.js +4 -4
- package/dist/richtext-render.js +2 -2
- package/dist/richtext-walk.d.ts +5 -5
- package/dist/richtext-walk.js +4 -4
- package/dist/schema.d.ts +15 -10
- package/dist/schema.js +19 -7
- package/dist/section-wrapper.d.ts +18 -6
- package/dist/section-wrapper.js +24 -17
- package/dist/site.d.ts +17 -5
- package/dist/site.js +36 -12
- package/dist/sql-space.js +1 -1
- package/dist/sql.js +19 -16
- package/dist/store.d.ts +45 -13
- package/dist/store.js +88 -12
- package/dist/styles.d.ts +27 -0
- package/dist/styles.js +60 -0
- package/dist/supabase-entry-store.d.ts +11 -6
- package/dist/supabase-entry-store.js +115 -39
- package/dist/supabase-store.d.ts +13 -6
- package/dist/supabase-store.js +90 -20
- package/dist/zones.d.ts +9 -7
- package/dist/zones.js +5 -5
- package/package.json +1 -1
- package/dist/admin/shared-items.d.ts +0 -9
- package/dist/admin/shell/SharedForm.js +0 -134
- package/dist/shared.d.ts +0 -46
- package/dist/shared.js +0 -36
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResolvedConfig } from "../config.ts";
|
|
2
|
+
import type { Descriptor } from "../fields.ts";
|
|
2
3
|
import type { ZonePolicy } from "../zones.ts";
|
|
3
4
|
export type AdminField = Record<string, unknown> & {
|
|
4
5
|
type: string;
|
|
@@ -14,8 +15,11 @@ export type AdminSection = {
|
|
|
14
15
|
icon?: string;
|
|
15
16
|
description?: string;
|
|
16
17
|
fields: Record<string, AdminField>;
|
|
18
|
+
settings: Record<string, AdminField>;
|
|
19
|
+
/** Smoodly's styles set after the schema's toggle (spec 2026-09-14 §2); `{}` for `styles: false`. */
|
|
17
20
|
styles: Record<string, AdminField>;
|
|
18
|
-
|
|
21
|
+
/** The custom-CSS box exists for this section: the project flag, unless the schema says `css: false` (spec 2026-09-14 §4). */
|
|
22
|
+
css: boolean;
|
|
19
23
|
sample?: Record<string, unknown>;
|
|
20
24
|
};
|
|
21
25
|
export type AdminCollection = {
|
|
@@ -43,18 +47,19 @@ export type AdminPageTemplate = {
|
|
|
43
47
|
/** `meta: { seo: true }` — the SEO & Social tab exists for this template. */
|
|
44
48
|
seo: boolean;
|
|
45
49
|
};
|
|
46
|
-
export type
|
|
50
|
+
export type AdminGlobal = {
|
|
47
51
|
name: string;
|
|
48
52
|
title: string;
|
|
49
53
|
/** The section a visual item renders through; absent = an object item. */
|
|
50
54
|
section?: string;
|
|
51
|
-
versions: boolean;
|
|
52
55
|
fields: Record<string, AdminField>;
|
|
53
|
-
/**
|
|
56
|
+
/** The section's settings, styles set and css flag for a visual item; `{}` / `{}` / `false` for an object item. */
|
|
57
|
+
settings: Record<string, AdminField>;
|
|
54
58
|
styles: Record<string, AdminField>;
|
|
59
|
+
css: boolean;
|
|
55
60
|
};
|
|
56
|
-
/** A registered Element (spec 2026-09-08 block nodes §1): a section shape plus its contexts. */
|
|
57
|
-
export type AdminElement = AdminSection & {
|
|
61
|
+
/** A registered Element (spec 2026-09-08 block nodes §1): a section shape plus its contexts — no styles, no css. */
|
|
62
|
+
export type AdminElement = Omit<AdminSection, "styles" | "css"> & {
|
|
58
63
|
contexts: string[];
|
|
59
64
|
};
|
|
60
65
|
export type AdminRegistry = {
|
|
@@ -62,11 +67,15 @@ export type AdminRegistry = {
|
|
|
62
67
|
elements: AdminElement[];
|
|
63
68
|
collections: AdminCollection[];
|
|
64
69
|
pages: AdminPageTemplate[];
|
|
65
|
-
|
|
70
|
+
globals: AdminGlobal[];
|
|
71
|
+
/** The custom section (spec 2026-09-14 §6) as one synthetic section: its rows descriptor, the full styles set, the project css flag. */
|
|
72
|
+
custom: AdminSection;
|
|
66
73
|
locales: {
|
|
67
74
|
default: string;
|
|
68
75
|
supported: string[];
|
|
69
76
|
};
|
|
77
|
+
/** The project's custom-CSS flag (spec 2026-09-14 §4). */
|
|
78
|
+
css: boolean;
|
|
70
79
|
/** Which slug the site root serves. */
|
|
71
80
|
homePageSlug: string;
|
|
72
81
|
/** How deep editors may nest pages; 1 = flat. */
|
|
@@ -77,4 +86,6 @@ export type AdminRegistry = {
|
|
|
77
86
|
accept: string[];
|
|
78
87
|
};
|
|
79
88
|
};
|
|
89
|
+
/** A registered section's settings descriptors — what a visual global's form shows (spec 2026-09-14 §3). */
|
|
90
|
+
export declare function sectionSettings(config: ResolvedConfig, name: string): Record<string, Descriptor>;
|
|
80
91
|
export declare function serializeAdminConfig(config: ResolvedConfig): AdminRegistry;
|
package/dist/admin/serialize.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { fixedSlug } from "../page.js";
|
|
2
2
|
import { collectionDepth, collectionOrder } from "../collections.js";
|
|
3
3
|
import { perLocaleSegments } from "../paths.js";
|
|
4
|
+
import { styleDescriptors } from "../styles.js";
|
|
5
|
+
import { CUSTOM_NODE_TYPE, customFields, defaultRow } from "../custom/index.js";
|
|
4
6
|
function serializeDescriptor(d) {
|
|
5
7
|
const out = { type: d.type };
|
|
6
8
|
for (const [key, value] of Object.entries(d)) {
|
|
@@ -22,6 +24,14 @@ function serializeDescriptor(d) {
|
|
|
22
24
|
function serializeMap(map) {
|
|
23
25
|
return Object.fromEntries(Object.entries(map ?? {}).map(([k, d]) => [k, serializeDescriptor(d)]));
|
|
24
26
|
}
|
|
27
|
+
/** A registered section's settings descriptors — what a visual global's form shows (spec 2026-09-14 §3). */
|
|
28
|
+
export function sectionSettings(config, name) {
|
|
29
|
+
return (config.registry.sections ?? []).find((s) => s.schema.name === name)?.schema.settings ?? {};
|
|
30
|
+
}
|
|
31
|
+
/** Whether a section gets the custom-CSS box: the project flag, unless the schema opts out (spec 2026-09-14 §4). */
|
|
32
|
+
function sectionCss(config, name) {
|
|
33
|
+
return config.css && (config.registry.sections ?? []).find((s) => s.schema.name === name)?.schema.css !== false;
|
|
34
|
+
}
|
|
25
35
|
export function serializeAdminConfig(config) {
|
|
26
36
|
const perLocale = (v) => perLocaleSegments(v, config.locales.supported);
|
|
27
37
|
return {
|
|
@@ -33,8 +43,9 @@ export function serializeAdminConfig(config) {
|
|
|
33
43
|
icon: s.icon,
|
|
34
44
|
description: s.description,
|
|
35
45
|
fields: serializeMap(s.fields),
|
|
46
|
+
settings: serializeMap(s.settings),
|
|
36
47
|
styles: serializeMap(config.resolvedStyles(s.name)),
|
|
37
|
-
|
|
48
|
+
css: sectionCss(config, s.name),
|
|
38
49
|
sample: s.sample,
|
|
39
50
|
};
|
|
40
51
|
}),
|
|
@@ -47,8 +58,7 @@ export function serializeAdminConfig(config) {
|
|
|
47
58
|
description: e.description,
|
|
48
59
|
contexts: [...(e.contexts ?? [])],
|
|
49
60
|
fields: serializeMap(e.fields),
|
|
50
|
-
|
|
51
|
-
meta: serializeMap(e.meta),
|
|
61
|
+
settings: serializeMap(e.settings),
|
|
52
62
|
sample: e.sample,
|
|
53
63
|
};
|
|
54
64
|
}),
|
|
@@ -73,14 +83,26 @@ export function serializeAdminConfig(config) {
|
|
|
73
83
|
seo: p.meta?.seo === true,
|
|
74
84
|
};
|
|
75
85
|
}),
|
|
76
|
-
|
|
86
|
+
globals: (config.registry.globals ?? []).map((s) => ({
|
|
77
87
|
name: s.name,
|
|
78
88
|
title: s.title,
|
|
79
89
|
...(s.section !== undefined ? { section: s.section } : {}),
|
|
80
|
-
versions: s.versions === true,
|
|
81
90
|
fields: serializeMap(s.fields),
|
|
91
|
+
settings: serializeMap(s.section !== undefined ? sectionSettings(config, s.section) : {}),
|
|
82
92
|
styles: serializeMap(s.section !== undefined ? config.resolvedStyles(s.section) : {}),
|
|
93
|
+
css: s.section !== undefined ? sectionCss(config, s.section) : false,
|
|
83
94
|
})),
|
|
95
|
+
custom: {
|
|
96
|
+
name: CUSTOM_NODE_TYPE,
|
|
97
|
+
title: "Custom section",
|
|
98
|
+
description: "Your own rows of columns",
|
|
99
|
+
fields: serializeMap(customFields(config.custom.toolset)),
|
|
100
|
+
settings: {},
|
|
101
|
+
styles: serializeMap(styleDescriptors(config.styles)),
|
|
102
|
+
css: config.css,
|
|
103
|
+
sample: { rows: [defaultRow()] },
|
|
104
|
+
},
|
|
105
|
+
css: config.css,
|
|
84
106
|
locales: config.locales,
|
|
85
107
|
homePageSlug: config.homePageSlug,
|
|
86
108
|
pageDepth: config.pageDepth,
|
|
@@ -17,8 +17,8 @@ import { makeClientOps } from "../client-ops.js";
|
|
|
17
17
|
import { PagesList } from "./PagesList.js";
|
|
18
18
|
import { EntriesList } from "./EntriesList.js";
|
|
19
19
|
import { EntryForm } from "./EntryForm.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { GlobalList } from "./GlobalList.js";
|
|
21
|
+
import { GlobalForm } from "./GlobalForm.js";
|
|
22
22
|
import { EditorView } from "../editor/EditorView.js";
|
|
23
23
|
import { LoginView } from "./LoginView.js";
|
|
24
24
|
import { useAdminSession } from "./session.js";
|
|
@@ -48,11 +48,11 @@ export function AdminApp({ registry, op, auth }) {
|
|
|
48
48
|
else if (isCollection && !tail)
|
|
49
49
|
main = _jsx(EntriesList, { ops: ops, registry: registry, collection: head });
|
|
50
50
|
else if (isCollection && tail)
|
|
51
|
-
main = _jsx(EntryForm, { ops: ops, registry: registry, collection: head, id:
|
|
52
|
-
else if (head === "
|
|
53
|
-
main = _jsx(
|
|
54
|
-
else if (head === "
|
|
55
|
-
main = _jsx(
|
|
51
|
+
main = _jsx(EntryForm, { ops: ops, registry: registry, collection: head, id: decodeURIComponent(tail) });
|
|
52
|
+
else if (head === "globals" && !tail)
|
|
53
|
+
main = _jsx(GlobalList, { ops: ops, registry: registry });
|
|
54
|
+
else if (head === "globals" && tail)
|
|
55
|
+
main = _jsx(GlobalForm, { ops: ops, registry: registry, name: decodeURIComponent(tail) });
|
|
56
56
|
else if (head === "media")
|
|
57
57
|
main = _jsx(MediaScreen, {});
|
|
58
58
|
else
|
|
@@ -8,6 +8,7 @@ import { useCallback, useEffect, useState } from "react";
|
|
|
8
8
|
import { ListView } from "./ListView.js";
|
|
9
9
|
import { canReorder, entryRows, entrySourceLocale } from "./entries-list.js";
|
|
10
10
|
import { Button, StatusBadge } from "../ui/primitives.js";
|
|
11
|
+
import { statusLabel } from "../ui/status-label.js";
|
|
11
12
|
import { LocalePicker } from "../ui/LocaleSwitcher.js";
|
|
12
13
|
import { relativeTime } from "../ui/format.js";
|
|
13
14
|
import { useSortable } from "../ui/useSortable.js";
|
|
@@ -75,6 +76,21 @@ export function EntriesList({ ops, registry, collection }) {
|
|
|
75
76
|
const open = (id) => {
|
|
76
77
|
navigate(`${BASE}/${collection}/${encodeURIComponent(id)}?locale=${encodeURIComponent(locale)}`);
|
|
77
78
|
};
|
|
79
|
+
/** "+ New entry" creates an empty draft in the locale on screen and opens it (spec 2026-09-14 §4). */
|
|
80
|
+
const createEntry = async () => {
|
|
81
|
+
let result;
|
|
82
|
+
try {
|
|
83
|
+
result = await ops.entries.create(collection, { locale, fields: {} });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
result = { ok: false, code: "internal", message: "Couldn't reach the server." };
|
|
87
|
+
}
|
|
88
|
+
if (!result.ok) {
|
|
89
|
+
window.alert(result.message);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
open(result.data.id);
|
|
93
|
+
};
|
|
78
94
|
const addLocale = async (row) => {
|
|
79
95
|
const record = records?.find((r) => r.id === row.id);
|
|
80
96
|
if (!record)
|
|
@@ -95,10 +111,10 @@ export function EntriesList({ ops, registry, collection }) {
|
|
|
95
111
|
}
|
|
96
112
|
open(row.id);
|
|
97
113
|
};
|
|
98
|
-
return (_jsx(ListView, { title: meta.title, titleTrailing: multilingual && _jsx(LocalePicker, { locales: registry.locales.supported, value: locale, onChange: pickLocale }), actions: _jsx(Button, { onClick: () => {
|
|
114
|
+
return (_jsx(ListView, { title: meta.title, titleTrailing: multilingual && _jsx(LocalePicker, { locales: registry.locales.supported, value: locale, onChange: pickLocale }), actions: _jsx(Button, { onClick: () => { void createEntry(); }, children: "+ New entry" }), columns: [
|
|
99
115
|
{ label: "Title", width: "minmax(160px,1fr)" },
|
|
100
116
|
{ label: "Updated", width: "minmax(110px,160px)" },
|
|
101
|
-
{ label: "Status", width: "
|
|
117
|
+
{ label: "Status", width: "170px" },
|
|
102
118
|
], empty: records ? "Nothing here yet." : "Loading…", sortable: canReorder(meta) ? { hook: sortable, canDrag: () => true } : undefined, rows: rows.map((row) => {
|
|
103
119
|
const dim = row.present ? undefined : { opacity: 0.45 };
|
|
104
120
|
return {
|
|
@@ -110,7 +126,7 @@ export function EntriesList({ ops, registry, collection }) {
|
|
|
110
126
|
? _jsx("span", { className: "sm-cell sm-cell--muted", children: relativeTime(row.updatedAt) }, "u")
|
|
111
127
|
: _jsxs("span", { className: "sm-cell sm-cell--muted", children: ["not in ", locale, " \u2014 click to add"] }, "u"),
|
|
112
128
|
row.status
|
|
113
|
-
? _jsx(StatusBadge, { status: row.status }, "c")
|
|
129
|
+
? _jsx(StatusBadge, { status: statusLabel(row.status) }, "c")
|
|
114
130
|
: _jsx("span", { className: "sm-cell sm-cell--muted", children: "\u2014" }, "c"),
|
|
115
131
|
],
|
|
116
132
|
};
|
|
@@ -1,44 +1,73 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
// The classic field view for ONE locale of an entry (spec 2026-09-06
|
|
4
4
|
// §7): the fields in schema order, the same FieldWidget renderer as the
|
|
5
5
|
// editor sidebar, no tabs. In a multilingual project a field that is
|
|
6
6
|
// NOT .localized() (nor the slug) carries a globe-off mark after its
|
|
7
|
-
// name — every locale sees it
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
7
|
+
// name — every locale sees it. The locale switcher loads that locale's
|
|
8
|
+
// merged fields; switching to a locale the entry lacks shows the
|
|
9
|
+
// "Add <locale>" pane, which copies the locale on screen; "Remove from
|
|
10
|
+
// <locale>" sits in the sidebar behind a confirm.
|
|
11
|
+
//
|
|
12
|
+
// Since 2026-09-14 (spec collections draft/publish) the form autosaves
|
|
13
|
+
// like the page editor: no Save button, every edit persists after a
|
|
14
|
+
// debounce, the top bar carries the indicator, View live and the one
|
|
15
|
+
// publish control. A draft may be incomplete; publish validates it and
|
|
16
|
+
// lists the fields in an amber banner. The safe-delete guard's message
|
|
13
17
|
// renders verbatim — it is product surface.
|
|
14
|
-
import { useEffect, useMemo, useState } from "react";
|
|
18
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
15
19
|
import { FieldWidget } from "../forms/FieldWidget.js";
|
|
16
|
-
import {
|
|
20
|
+
import { revealField } from "../forms/reveal.js";
|
|
21
|
+
import { useDraftAutosave } from "../editor/autosave.js";
|
|
22
|
+
import { relativeTime, updatedLine } from "../ui/format.js";
|
|
17
23
|
import { LocaleSwitcher } from "../ui/LocaleSwitcher.js";
|
|
18
|
-
import { Banner, Breadcrumb, Button,
|
|
24
|
+
import { Banner, Breadcrumb, Button, FieldErrorList, InfoRow, LinkButton, SaveIndicator, SectionLabel, TopBar, UnlocalizedMark, } from "../ui/primitives.js";
|
|
25
|
+
import { PublishControl } from "../ui/PublishControl.js";
|
|
26
|
+
import { HistoryModal, historyRows } from "../ui/HistoryModal.js";
|
|
27
|
+
import { localeStatusOf } from "./pages-tree.js";
|
|
19
28
|
import { ResizeHandle } from "../ui/ResizeHandle.js";
|
|
20
29
|
import { entrySourceLocale, entryTitle, localizedFieldKeys } from "./entries-list.js";
|
|
21
30
|
import { BASE } from "./base.js";
|
|
22
31
|
import { localeFromUrl, writeLocaleToUrl } from "../editor/locale-url.js";
|
|
23
32
|
import { navigate, useLeaveGuard } from "./router.js";
|
|
24
|
-
const STATUSES = ["Draft", "Published"];
|
|
25
33
|
export function EntryForm({ ops, registry, collection, id }) {
|
|
26
34
|
const meta = registry.collections.find((c) => c.name === collection);
|
|
27
|
-
const isNew = id === "new";
|
|
28
35
|
const multilingual = registry.locales.supported.length > 1;
|
|
29
36
|
const [locale, setLocale] = useState(() => localeFromUrl(registry));
|
|
30
37
|
const [record, setRecord] = useState(null);
|
|
31
|
-
const [fields, setFields] = useState(
|
|
38
|
+
const [fields, setFields] = useState(null);
|
|
39
|
+
const [urls, setUrls] = useState({});
|
|
32
40
|
const [parentLocales, setParentLocales] = useState(registry.locales.supported);
|
|
33
41
|
const [refOptions, setRefOptions] = useState({});
|
|
34
|
-
const [
|
|
42
|
+
const [notice, setNotice] = useState(null);
|
|
35
43
|
const [fieldErrors, setFieldErrors] = useState({});
|
|
36
44
|
const [adding, setAdding] = useState(false);
|
|
37
45
|
const [reloadKey, setReloadKey] = useState(0);
|
|
46
|
+
/** An edit since the last save ack — Publish wakes on it, before the autosave runs (spec 2026-09-13 publish flow §2). */
|
|
38
47
|
const [dirty, setDirty] = useState(false);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
/** Any edit on this screen yet: the indicator stays hidden until the first one (§6). */
|
|
49
|
+
const [touched, setTouched] = useState(false);
|
|
50
|
+
const [busy, setBusy] = useState(null);
|
|
51
|
+
/** The moment after a publish: the button reads "Published ✓". */
|
|
52
|
+
const [published, setPublished] = useState(false);
|
|
53
|
+
const [history, setHistory] = useState(null);
|
|
54
|
+
const [snapshot, setSnapshot] = useState(null);
|
|
55
|
+
const formRef = useRef(null);
|
|
56
|
+
const fieldsRef = useRef(fields);
|
|
57
|
+
fieldsRef.current = fields;
|
|
58
|
+
// The page editor's controller: debounced, retrying, flushable (spec 2026-09-14 §3).
|
|
59
|
+
const { saveState, mark, flush } = useDraftAutosave({
|
|
60
|
+
read: () => fieldsRef.current,
|
|
61
|
+
persist: async (value) => {
|
|
62
|
+
const r = await ops.entries.save(collection, id, locale, value);
|
|
63
|
+
if (r.ok)
|
|
64
|
+
setRecord(r.data);
|
|
65
|
+
return r;
|
|
66
|
+
},
|
|
67
|
+
onSaved: () => setDirty(false),
|
|
68
|
+
});
|
|
69
|
+
// Reload and close ask only while a save is pending or in flight.
|
|
70
|
+
useLeaveGuard(dirty || saveState !== "saved");
|
|
42
71
|
const refTargets = useMemo(() => {
|
|
43
72
|
const out = {};
|
|
44
73
|
for (const [key, d] of Object.entries(meta?.fields ?? {})) {
|
|
@@ -50,26 +79,26 @@ export function EntryForm({ ops, registry, collection, id }) {
|
|
|
50
79
|
const localeKeys = useMemo(() => (meta ? localizedFieldKeys(meta) : []), [meta]);
|
|
51
80
|
// Load the locale's merged fields; an absent locale comes back with fields: null.
|
|
52
81
|
useEffect(() => {
|
|
53
|
-
if (isNew)
|
|
54
|
-
return;
|
|
55
82
|
let cancelled = false;
|
|
56
83
|
setFields(null);
|
|
57
84
|
setFieldErrors({});
|
|
85
|
+
setDirty(false);
|
|
86
|
+
setTouched(false);
|
|
58
87
|
ops.entries.get(collection, id, locale).then((r) => {
|
|
59
88
|
if (cancelled)
|
|
60
89
|
return;
|
|
61
90
|
if (r.ok) {
|
|
62
91
|
setRecord(r.data.record);
|
|
63
92
|
setFields(r.data.fields);
|
|
93
|
+
setUrls(r.data.urls);
|
|
64
94
|
setParentLocales(r.data.parentLocales);
|
|
65
|
-
setDirty(false);
|
|
66
95
|
}
|
|
67
96
|
else
|
|
68
|
-
|
|
97
|
+
setNotice({ tone: "error", text: r.message });
|
|
69
98
|
}).catch(() => { if (!cancelled)
|
|
70
|
-
|
|
99
|
+
setNotice({ tone: "error", text: "Couldn't reach the server." }); });
|
|
71
100
|
return () => { cancelled = true; };
|
|
72
|
-
}, [ops, collection, id,
|
|
101
|
+
}, [ops, collection, id, locale, reloadKey]);
|
|
73
102
|
// Ref options in the locale being edited: a target absent there is not offered.
|
|
74
103
|
useEffect(() => {
|
|
75
104
|
let cancelled = false;
|
|
@@ -93,64 +122,145 @@ export function EntryForm({ ops, registry, collection, id }) {
|
|
|
93
122
|
}, [ops, refTargets, registry, locale]);
|
|
94
123
|
if (!meta)
|
|
95
124
|
return _jsx("p", { style: { padding: 32 }, children: "No such collection." });
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
125
|
+
const row = record?.locales[locale];
|
|
126
|
+
const status = row ? localeStatusOf(row) : null;
|
|
127
|
+
/** An op's failure onto the screen: amber with the fields for a validation result, red otherwise. */
|
|
128
|
+
const fail = (r) => {
|
|
129
|
+
if (r.code === "validation" && r.fields && r.fields.length > 0) {
|
|
130
|
+
setFieldErrors(Object.fromEntries(r.fields.map((f) => [f.field, f.message])));
|
|
131
|
+
setNotice({ tone: "warn", text: r.message, fields: r.fields });
|
|
132
|
+
}
|
|
133
|
+
else
|
|
134
|
+
setNotice({ tone: "error", text: r.message });
|
|
105
135
|
};
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
navigate(`${BASE}/${collection}/${encodeURIComponent(r.data.id)}?locale=${encodeURIComponent(locale)}`, { force: true });
|
|
115
|
-
}
|
|
136
|
+
/** Publish what is on screen: the pending edit is flushed first, and a failed flush cancels the publish. */
|
|
137
|
+
const publish = async () => {
|
|
138
|
+
setNotice(null);
|
|
139
|
+
setBusy("publish");
|
|
140
|
+
if (!(await flush())) {
|
|
141
|
+
setBusy(null);
|
|
142
|
+
setNotice({ tone: "error", text: "Couldn't save your latest changes — publish cancelled." });
|
|
143
|
+
return;
|
|
116
144
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
setRecord(r.data);
|
|
121
|
-
setDirty(false);
|
|
122
|
-
}
|
|
145
|
+
let r;
|
|
146
|
+
try {
|
|
147
|
+
r = await ops.entries.setStatus(collection, id, locale, "published");
|
|
123
148
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (
|
|
149
|
+
catch {
|
|
150
|
+
r = { ok: false, code: "internal", message: "Couldn't reach the server." };
|
|
151
|
+
}
|
|
152
|
+
setBusy(null);
|
|
153
|
+
if (!r.ok) {
|
|
154
|
+
fail(r);
|
|
129
155
|
return;
|
|
130
|
-
|
|
131
|
-
|
|
156
|
+
}
|
|
157
|
+
setRecord(r.data);
|
|
158
|
+
setFieldErrors({});
|
|
159
|
+
setPublished(true);
|
|
160
|
+
setTimeout(() => setPublished(false), 1600);
|
|
161
|
+
};
|
|
162
|
+
const unpublish = async () => {
|
|
163
|
+
setNotice(null);
|
|
164
|
+
setBusy("unpublish");
|
|
165
|
+
let r;
|
|
166
|
+
try {
|
|
167
|
+
r = await ops.entries.setStatus(collection, id, locale, "draft");
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
r = { ok: false, code: "internal", message: "Couldn't reach the server." };
|
|
171
|
+
}
|
|
172
|
+
setBusy(null);
|
|
173
|
+
if (r.ok)
|
|
132
174
|
setRecord(r.data);
|
|
175
|
+
else
|
|
176
|
+
fail(r);
|
|
177
|
+
};
|
|
178
|
+
/** Discard (spec 2026-09-13 publish flow §4): confirm, flush so a late save cannot re-dirty the row, restore, reload. */
|
|
179
|
+
const discard = async () => {
|
|
180
|
+
if (!window.confirm("Discard your changes and restore the published version?"))
|
|
181
|
+
return;
|
|
182
|
+
setNotice(null);
|
|
183
|
+
setBusy("discard");
|
|
184
|
+
if (!(await flush())) {
|
|
185
|
+
setBusy(null);
|
|
186
|
+
setNotice({ tone: "error", text: "Couldn't save your latest changes — discard cancelled." });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
let r;
|
|
190
|
+
try {
|
|
191
|
+
r = await ops.entries.discardDraft(collection, id, locale);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
r = { ok: false, code: "internal", message: "Couldn't reach the server." };
|
|
195
|
+
}
|
|
196
|
+
setBusy(null);
|
|
197
|
+
if (!r.ok) {
|
|
198
|
+
fail(r);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
setRecord(r.data);
|
|
202
|
+
setReloadKey((k) => k + 1);
|
|
203
|
+
setNotice({ tone: "ok", text: "Changes discarded." });
|
|
204
|
+
};
|
|
205
|
+
/** History (spec 2026-09-13 history §5): flush first so the list's draft row is current. */
|
|
206
|
+
const openHistory = async () => {
|
|
207
|
+
if (!row)
|
|
208
|
+
return;
|
|
209
|
+
if (!(await flush())) {
|
|
210
|
+
setNotice({ tone: "error", text: "Couldn't save your latest changes — history not opened." });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const r = await ops.entries.listVersions(collection, id, locale);
|
|
214
|
+
if (!r.ok) {
|
|
215
|
+
fail(r);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
setSnapshot(null);
|
|
219
|
+
setHistory(historyRows(r.data, row));
|
|
220
|
+
};
|
|
221
|
+
const loadSnapshot = async (versionId) => {
|
|
222
|
+
const r = await ops.entries.getVersion(collection, id, versionId);
|
|
223
|
+
if (r.ok)
|
|
224
|
+
setSnapshot({ id: r.data.id, fields: r.data.fields });
|
|
225
|
+
else
|
|
226
|
+
fail(r);
|
|
227
|
+
};
|
|
228
|
+
const restore = async (versionId) => {
|
|
229
|
+
const r = await ops.entries.restore(collection, id, locale, versionId);
|
|
230
|
+
if (!r.ok) {
|
|
231
|
+
fail(r);
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
setRecord(r.data);
|
|
235
|
+
setReloadKey((k) => k + 1);
|
|
236
|
+
setNotice({ tone: "ok", text: "Version restored." });
|
|
237
|
+
return true;
|
|
133
238
|
};
|
|
134
239
|
const goToLocale = (next) => {
|
|
135
240
|
if (next === locale)
|
|
136
241
|
return;
|
|
137
242
|
writeLocaleToUrl(next);
|
|
138
|
-
|
|
243
|
+
setNotice(null);
|
|
139
244
|
setLocale(next);
|
|
140
245
|
};
|
|
141
|
-
|
|
246
|
+
/** A switch flushes the pending edit first: nothing is ever unsaved past the debounce. */
|
|
247
|
+
const switchLocale = async (next) => {
|
|
142
248
|
if (next === locale)
|
|
143
249
|
return;
|
|
144
|
-
if (
|
|
250
|
+
if (!(await flush())) {
|
|
251
|
+
setNotice({ tone: "error", text: "Couldn't save your latest changes — stayed on this locale." });
|
|
145
252
|
return;
|
|
253
|
+
}
|
|
146
254
|
goToLocale(next);
|
|
147
255
|
};
|
|
148
256
|
/** Add a locale, copying the locale on screen (or the record's source locale when this one is absent). */
|
|
149
257
|
const doAddLocale = async (target) => {
|
|
150
258
|
if (!record || adding)
|
|
151
259
|
return;
|
|
152
|
-
if (
|
|
260
|
+
if (!(await flush())) {
|
|
261
|
+
setNotice({ tone: "error", text: "Couldn't save your latest changes — locale not added." });
|
|
153
262
|
return;
|
|
263
|
+
}
|
|
154
264
|
const from = record.locales[locale] ? locale : entrySourceLocale(record, registry);
|
|
155
265
|
setAdding(true);
|
|
156
266
|
let result;
|
|
@@ -162,7 +272,7 @@ export function EntryForm({ ops, registry, collection, id }) {
|
|
|
162
272
|
}
|
|
163
273
|
setAdding(false);
|
|
164
274
|
if (!result.ok) {
|
|
165
|
-
|
|
275
|
+
fail(result);
|
|
166
276
|
return;
|
|
167
277
|
}
|
|
168
278
|
setRecord(result.data);
|
|
@@ -177,8 +287,6 @@ export function EntryForm({ ops, registry, collection, id }) {
|
|
|
177
287
|
const remaining = Object.keys(record.locales).filter((l) => l !== locale);
|
|
178
288
|
if (remaining.length === 0)
|
|
179
289
|
return;
|
|
180
|
-
if (dirty && !window.confirm("Discard unsaved changes?"))
|
|
181
|
-
return;
|
|
182
290
|
if (!window.confirm(`Remove this entry from ${locale}? Its ${locale} fields and history are deleted.`))
|
|
183
291
|
return;
|
|
184
292
|
let result;
|
|
@@ -189,7 +297,7 @@ export function EntryForm({ ops, registry, collection, id }) {
|
|
|
189
297
|
result = { ok: false, code: "internal", message: "Couldn't reach the server." };
|
|
190
298
|
}
|
|
191
299
|
if (!result.ok) {
|
|
192
|
-
|
|
300
|
+
fail(result);
|
|
193
301
|
return;
|
|
194
302
|
}
|
|
195
303
|
setRecord(result.data);
|
|
@@ -202,28 +310,26 @@ export function EntryForm({ ops, registry, collection, id }) {
|
|
|
202
310
|
if (r.ok)
|
|
203
311
|
navigate(`${BASE}/${collection}`, { force: true });
|
|
204
312
|
else
|
|
205
|
-
|
|
313
|
+
fail(r);
|
|
206
314
|
};
|
|
207
|
-
const heading =
|
|
315
|
+
const heading = record ? entryTitle(record, meta, registry, locale) : "…";
|
|
208
316
|
// The mount's path in the ACTIVE locale prefixes the slug; no path, no prefix.
|
|
209
317
|
const base = meta.path?.[locale];
|
|
210
318
|
const slugPrefix = base ? `/${base}/` : undefined;
|
|
211
319
|
const present = record ? registry.locales.supported.filter((l) => record.locales[l]) : [locale];
|
|
212
|
-
const widget = ([key, d]) => (_jsx(FieldWidget, { fieldKey: key, descriptor: d, value: fields?.[key], errors: fieldErrors, refOptions: refTargets[key] ? refOptions[refTargets[key]] ?? [] : undefined, prefix: d.type === "slug" ? slugPrefix : undefined, mark: multilingual && !localeKeys.includes(key) ? _jsx(UnlocalizedMark, {}) : undefined, onChange: (value) => {
|
|
213
|
-
|
|
320
|
+
const widget = ([key, d]) => (_jsx(FieldWidget, { fieldKey: key, descriptor: d, value: fields?.[key], errors: fieldErrors, refOptions: refTargets[key] ? refOptions[refTargets[key]] ?? [] : undefined, prefix: d.type === "slug" ? slugPrefix : undefined, mark: multilingual && !localeKeys.includes(key) ? _jsx(UnlocalizedMark, {}) : undefined, onChange: (value) => {
|
|
321
|
+
setFields((f) => ({ ...(f ?? {}), [key]: value }));
|
|
322
|
+
setDirty(true);
|
|
323
|
+
setTouched(true);
|
|
324
|
+
mark();
|
|
325
|
+
} }, key));
|
|
326
|
+
const absent = record !== null && fields === null && !record.locales[locale];
|
|
214
327
|
return (_jsxs(_Fragment, { children: [_jsx(TopBar, { left: _jsx(_Fragment, { children: _jsx(Breadcrumb, { backHref: `${BASE}/${collection}`, items: [
|
|
215
328
|
{
|
|
216
329
|
label: meta.title, href: `${BASE}/${collection}`,
|
|
217
|
-
trailing: multilingual && (
|
|
218
|
-
? _jsxs("span", { className: "sm-hint", children: ["\u00B7 ", locale] })
|
|
219
|
-
: _jsx(LocaleSwitcher, { registry: registry, present: present, parentLocales: parentLocales, value: locale, adding: adding, onSwitch: switchLocale, onAdd: (l) => void doAddLocale(l) })),
|
|
330
|
+
trailing: multilingual && (_jsx(LocaleSwitcher, { registry: registry, present: present, parentLocales: parentLocales, value: locale, adding: adding, onSwitch: (l) => { void switchLocale(l); }, onAdd: (l) => void doAddLocale(l) })),
|
|
220
331
|
},
|
|
221
332
|
{ label: heading },
|
|
222
|
-
] }) }), right:
|
|
223
|
-
}
|
|
224
|
-
function InfoRow({ label, value, mono }) {
|
|
225
|
-
return (_jsxs("div", { style: { display: "flex", justifyContent: "space-between", gap: 10, fontSize: "var(--text-sm)", color: "var(--text-muted)" }, children: [_jsx("span", { children: label }), _jsx("span", { style: {
|
|
226
|
-
color: "var(--text)", fontFamily: mono ? "var(--font-mono)" : undefined,
|
|
227
|
-
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
|
|
228
|
-
}, children: value })] }));
|
|
333
|
+
] }) }), right: _jsxs(_Fragment, { children: [(touched || saveState === "error") && _jsx(SaveIndicator, { state: saveState }), urls[locale] && (_jsx(LinkButton, { href: urls[locale], target: "_blank", rel: "noreferrer", title: "Opens the site route. While the admin's draft-mode cookie is on, this shows the DRAFT \u2014 open it in a private window to see what visitors see.", children: "View live" })), _jsx(PublishControl, { status: status, pending: dirty || saveState !== "saved", busy: busy, flash: published, disabled: !row, onPublish: () => { void publish(); }, onUnpublish: () => { void unpublish(); }, onDiscard: () => { void discard(); }, onHistory: () => { void openHistory(); } })] }) }), history && row && (_jsx(HistoryModal, { rows: history, draftId: row.draftVersionId, onSelect: (v) => { void loadSnapshot(v); }, onClose: () => setHistory(null), onRestore: restore, render: (versionId) => snapshot?.id === versionId ? (_jsx("fieldset", { disabled: true, className: "sm-history__form", children: Object.entries(meta.fields).map(([key, d]) => (_jsx(FieldWidget, { fieldKey: key, descriptor: d, value: snapshot.fields[key], refOptions: refTargets[key] ? refOptions[refTargets[key]] ?? [] : undefined, prefix: d.type === "slug" ? slugPrefix : undefined, onChange: () => { } }, key))) })) : _jsx("p", { className: "sm-hint", style: { padding: 16 }, children: "Loading\u2026" }) })), _jsxs("div", { className: "sm-editor", children: [_jsxs("div", { className: "sm-form", ref: formRef, children: [notice && (_jsxs(Banner, { tone: notice.tone, onDismiss: () => setNotice(null), children: [_jsx("p", { style: { margin: 0 }, children: notice.text }), _jsx(FieldErrorList, { fields: notice.fields ?? [], onReveal: (path) => { if (formRef.current)
|
|
334
|
+
revealField(formRef.current, path); } })] })), absent ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [_jsxs("p", { children: ["This entry doesn't exist in ", locale, " yet. Adding it copies the ", entrySourceLocale(record, registry), " fields as the first ", locale, " draft."] }), _jsxs("div", { style: { display: "flex", gap: 8 }, children: [_jsxs(Button, { onClick: () => void doAddLocale(locale), disabled: adding || !parentLocales.includes(locale), title: parentLocales.includes(locale) ? undefined : `add ${locale} to the parent first`, children: ["Add ", locale] }), _jsxs(Button, { variant: "secondary", onClick: () => { void switchLocale(entrySourceLocale(record, registry)); }, children: ["Back to ", entrySourceLocale(record, registry)] })] })] })) : fields !== null ? (Object.entries(meta.fields).map(widget)) : !notice && _jsx("p", { style: { color: "var(--text-muted)" }, children: "Loading\u2026" })] }), _jsx(ResizeHandle, {}), _jsxs("aside", { className: "sm-inspector", children: [record && row && (_jsxs("div", { className: "sm-card", children: [_jsx(SectionLabel, { children: "Document info" }), _jsx(InfoRow, { label: "Created", value: relativeTime(record.createdAt) }), _jsx(InfoRow, { label: "Updated", value: updatedLine(row.updatedAt, row.updatedBy) }), multilingual && _jsx(InfoRow, { label: "Languages", value: Object.keys(record.locales).join(", ") }), _jsx(InfoRow, { label: "ID", value: id, mono: true })] })), record && (_jsxs("div", { className: "sm-card", children: [multilingual && row && Object.keys(record.locales).length > 1 && (_jsxs(Button, { variant: "secondary", onClick: () => void doRemoveLocale(), children: ["Remove from ", locale] })), _jsx(Button, { variant: "danger", onClick: destroy, children: "Delete" })] }))] })] })] }));
|
|
229
335
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AdminGlobal } from "../serialize.ts";
|
|
2
|
+
import type { Group } from "../forms/tabs.ts";
|
|
3
|
+
import { type NodeValues } from "../editor/InspectorFields.tsx";
|
|
4
|
+
export type RefOption = {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
status: string;
|
|
8
|
+
};
|
|
9
|
+
/** The item's value map as the inspector reads it: the reserved keys pulled out into their namespaces. */
|
|
10
|
+
export declare function globalValues(fields: Record<string, unknown>): NodeValues;
|
|
11
|
+
export declare function GlobalFields({ item, groups, fields, errors, refOptions, multilingual, inlinePath, onEditHere, onChange }: {
|
|
12
|
+
item: AdminGlobal;
|
|
13
|
+
groups: Group[];
|
|
14
|
+
/** The merged draft fields; a visual item's settings, styles and css under their reserved keys. */
|
|
15
|
+
fields: Record<string, unknown>;
|
|
16
|
+
errors?: Record<string, string>;
|
|
17
|
+
/** Loaded ref options by target collection. */
|
|
18
|
+
refOptions: Record<string, RefOption[]>;
|
|
19
|
+
multilingual: boolean;
|
|
20
|
+
/** The content field with an active inline session: its richtext widget yields to a note. */
|
|
21
|
+
inlinePath?: string;
|
|
22
|
+
onEditHere?(): void;
|
|
23
|
+
onChange(ns: "fields" | "settings" | "styles" | "css", key: string, value: unknown): void;
|
|
24
|
+
}): import("react").JSX.Element;
|