@pramen/cms-editor 0.0.48 → 0.0.49
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/dist/index.html +1 -1
- package/dist/{main.qy6fhg00.js → main.ykztb0px.js} +55 -55
- package/package.json +1 -1
- package/src/api.ts +8 -6
- package/src/app-context.tsx +2 -2
- package/src/components.tsx +16 -16
- package/src/fields.tsx +11 -11
- package/src/types.ts +23 -5
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// same transport shape as @pramen/admin's api.ts. Config is persisted in localStorage.
|
|
3
3
|
|
|
4
4
|
import type { AssembledPage, AuditEntry, BlockType, ContentType, Media, Page } from "./types";
|
|
5
|
+
import type { RpcInput } from "./types";
|
|
5
6
|
|
|
6
7
|
export interface Config {
|
|
7
8
|
baseUrl: string;
|
|
@@ -72,20 +73,21 @@ export class Api {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
/** Call a CMS RPC handler. Throws ApiError on a non-`ok` envelope. */
|
|
75
|
-
async call<T = unknown>(name: string, input?:
|
|
76
|
+
async call<T = unknown>(name: string, input?: RpcInput): Promise<T> {
|
|
76
77
|
// Expired token: hand off to sign-in instead of firing a request that will 403 into an
|
|
77
78
|
// error banner. The returned promise never settles — navigation is already underway.
|
|
78
79
|
if (this.onExpired && this.cfg.token && isTokenExpired(this.cfg.token)) {
|
|
79
80
|
this.onExpired();
|
|
80
81
|
return new Promise<T>(() => {});
|
|
81
82
|
}
|
|
83
|
+
const headers = new Headers({
|
|
84
|
+
"content-type": "application/json",
|
|
85
|
+
"x-pramen-tenant": this.cfg.tenant || "main",
|
|
86
|
+
});
|
|
87
|
+
if (this.cfg.token) headers.set("authorization", `Bearer ${this.cfg.token}`);
|
|
82
88
|
const res = await fetch(`${this.base()}/rpc/${name}`, {
|
|
83
89
|
method: "POST",
|
|
84
|
-
headers
|
|
85
|
-
"content-type": "application/json",
|
|
86
|
-
"x-pramen-tenant": this.cfg.tenant || "main",
|
|
87
|
-
...(this.cfg.token ? { authorization: `Bearer ${this.cfg.token}` } : {}),
|
|
88
|
-
},
|
|
90
|
+
headers,
|
|
89
91
|
body: JSON.stringify(input ?? {}),
|
|
90
92
|
});
|
|
91
93
|
let body: { ok?: boolean; result?: unknown; error?: string; code?: string };
|
package/src/app-context.tsx
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { Button, Input } from "@podoba/react";
|
|
7
7
|
import { createContext, use, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { Api, clearConfig, isTokenExpired, loadConfig, saveConfig, type Config } from "./api";
|
|
9
|
-
import type { CollectionMeta } from "./types";
|
|
9
|
+
import type { CollectionMeta, JsonValue } from "./types";
|
|
10
10
|
|
|
11
11
|
declare global {
|
|
12
12
|
interface Window {
|
|
@@ -42,7 +42,7 @@ function redirectToSignIn(): void {
|
|
|
42
42
|
export interface Me {
|
|
43
43
|
userId?: string;
|
|
44
44
|
roles?: string[];
|
|
45
|
-
[k: string]:
|
|
45
|
+
[k: string]: JsonValue | undefined;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
interface AppContextValue {
|
package/src/components.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import { Api, ApiError } from "./api";
|
|
|
8
8
|
import { FieldForm, slugify } from "./fields";
|
|
9
9
|
import type { Config } from "./api";
|
|
10
10
|
import type { Me } from "./app-context";
|
|
11
|
-
import type { AssembledPage, AuditEntry, BlockType, CollectionMeta, ContentType, FieldDefinition, Media, Page, RegionDefinition, RenderedBlock } from "./types";
|
|
11
|
+
import type { AssembledPage, AuditEntry, BlockType, CollectionMeta, ContentType, FieldDefinition, FieldValues, Media, Page, RegionDefinition, RenderedBlock } from "./types";
|
|
12
12
|
|
|
13
13
|
export type InspectorTab = "settings" | "seo" | "workflow" | "i18n" | "audit";
|
|
14
14
|
export const INSPECTOR_TABS: InspectorTab[] = ["settings", "seo", "workflow", "i18n", "audit"];
|
|
@@ -201,7 +201,7 @@ function cellText(v: unknown): string {
|
|
|
201
201
|
const COLLECTION_PAGE_SIZE = 50;
|
|
202
202
|
|
|
203
203
|
export function CollectionList({ api, def, onOpen, onNew, onError }: { api: Api; def: CollectionMeta; onOpen: (id: string) => void; onNew: () => void; onError: (s: string) => void }) {
|
|
204
|
-
const [rows, setRows] = useState<
|
|
204
|
+
const [rows, setRows] = useState<FieldValues[]>([]);
|
|
205
205
|
const [offset, setOffset] = useState(0);
|
|
206
206
|
const [hasMore, setHasMore] = useState(false);
|
|
207
207
|
const [loading, setLoading] = useState(true);
|
|
@@ -210,7 +210,7 @@ export function CollectionList({ api, def, onOpen, onNew, onError }: { api: Api;
|
|
|
210
210
|
(off: number) => {
|
|
211
211
|
setLoading(true);
|
|
212
212
|
return api
|
|
213
|
-
.call<
|
|
213
|
+
.call<FieldValues[]>("collectionList", { collection: def.slug, limit: COLLECTION_PAGE_SIZE, offset: off })
|
|
214
214
|
.then((r) => {
|
|
215
215
|
setRows((prev) => (off === 0 ? r : [...prev, ...r]));
|
|
216
216
|
// A full page means there is probably more; a short one is definitely the end.
|
|
@@ -267,7 +267,7 @@ export function CollectionList({ api, def, onOpen, onNew, onError }: { api: Api;
|
|
|
267
267
|
|
|
268
268
|
export function CollectionEditor({ api, def, id, onSaved, onDeleted, onBack, onError }: { api: Api; def: CollectionMeta; id: string | null; onSaved: () => void; onDeleted: () => void; onBack: () => void; onError: (s: string) => void }) {
|
|
269
269
|
const isNew = id === null;
|
|
270
|
-
const [values, setValues] = useState<
|
|
270
|
+
const [values, setValues] = useState<FieldValues>({});
|
|
271
271
|
const [loading, setLoading] = useState(!isNew);
|
|
272
272
|
const [missing, setMissing] = useState(false);
|
|
273
273
|
const [busy, setBusy] = useState(false);
|
|
@@ -278,7 +278,7 @@ export function CollectionEditor({ api, def, id, onSaved, onDeleted, onBack, onE
|
|
|
278
278
|
let live = true;
|
|
279
279
|
setLoading(true);
|
|
280
280
|
setMissing(false);
|
|
281
|
-
api.call<
|
|
281
|
+
api.call<FieldValues | null>("collectionGet", { collection: def.slug, id })
|
|
282
282
|
.then((row) => { if (!live) return; if (row) setValues(row); else setMissing(true); })
|
|
283
283
|
.catch((e) => onError(errMsg(e)))
|
|
284
284
|
.finally(() => { if (live) setLoading(false); });
|
|
@@ -296,7 +296,7 @@ export function CollectionEditor({ api, def, id, onSaved, onDeleted, onBack, onE
|
|
|
296
296
|
} else {
|
|
297
297
|
// Editing: stay on the form. Reflect the persisted row the update echoes back (server
|
|
298
298
|
// defaults / normalization applied) and flash a confirmation instead of navigating.
|
|
299
|
-
const updated = await api.call<
|
|
299
|
+
const updated = await api.call<FieldValues>("collectionUpdate", { collection: def.slug, id, values });
|
|
300
300
|
if (updated) setValues(updated);
|
|
301
301
|
setOk(true);
|
|
302
302
|
setTimeout(() => setOk(false), 1200);
|
|
@@ -427,7 +427,7 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
|
|
|
427
427
|
});
|
|
428
428
|
try {
|
|
429
429
|
const { block, placement } = await api.call<{
|
|
430
|
-
block: { id: string; title?: string | null; fields?:
|
|
430
|
+
block: { id: string; title?: string | null; fields?: FieldValues | null };
|
|
431
431
|
placement: { id: string; isShared?: boolean | number };
|
|
432
432
|
}>("addBlock", { pageId: page.id, blockTypeSlug: slug, region, fields: {} });
|
|
433
433
|
const rb: RenderedBlock = {
|
|
@@ -470,7 +470,7 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
|
|
|
470
470
|
|
|
471
471
|
// Patch a block's raw fields into local state after an inline save — keeps the collapsed
|
|
472
472
|
// preview fresh without a full reload (which would remount every editor + lose caret/focus).
|
|
473
|
-
const patchBlockFields = useCallback((placementId: string, fields:
|
|
473
|
+
const patchBlockFields = useCallback((placementId: string, fields: FieldValues) => {
|
|
474
474
|
setAssembled((prev) => {
|
|
475
475
|
if (!prev) return prev;
|
|
476
476
|
const next: Record<string, RenderedBlock[]> = {};
|
|
@@ -539,7 +539,7 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
|
|
|
539
539
|
not in the inspector. For a content type with no regions (a fixed layout, all
|
|
540
540
|
of it page fields) this is the entire editor; the canvas is never empty. */}
|
|
541
541
|
{pageSchema.length ? (
|
|
542
|
-
<PageFields api={api} page={page} schema={pageSchema} initialFields={(assembled?.page.fields as
|
|
542
|
+
<PageFields api={api} page={page} schema={pageSchema} initialFields={(assembled?.page.fields as FieldValues) ?? {}} onDirtyChange={reportDirty} onError={setErr} />
|
|
543
543
|
) : null}
|
|
544
544
|
{regions.map((r) => {
|
|
545
545
|
const blocks = assembled?.regions[r.name] ?? [];
|
|
@@ -611,7 +611,7 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
|
|
|
611
611
|
isLast: boolean;
|
|
612
612
|
onMove: (dir: number) => void;
|
|
613
613
|
onRemove: () => void;
|
|
614
|
-
onPatch: (placementId: string, fields:
|
|
614
|
+
onPatch: (placementId: string, fields: FieldValues) => void;
|
|
615
615
|
onDirtyChange: (placementId: string, dirty: boolean) => void;
|
|
616
616
|
onError: (s: string) => void;
|
|
617
617
|
dragging: boolean;
|
|
@@ -621,7 +621,7 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
|
|
|
621
621
|
onDropBlock: () => void;
|
|
622
622
|
onDragEndBlock: () => void;
|
|
623
623
|
}) {
|
|
624
|
-
const [fields, setFields] = useState<
|
|
624
|
+
const [fields, setFields] = useState<FieldValues | null>(null);
|
|
625
625
|
const [collapsed, setCollapsed] = useState(false);
|
|
626
626
|
const [saveState, setSaveState] = useState<"idle" | "saving" | "saved">("idle");
|
|
627
627
|
const saved = useRef<string>(""); // JSON of the last-persisted fields — the dirty baseline
|
|
@@ -635,7 +635,7 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
|
|
|
635
635
|
useEffect(() => {
|
|
636
636
|
if (block.pending) { setFields({}); saved.current = "{}"; return; }
|
|
637
637
|
let alive = true;
|
|
638
|
-
api.call<{ fields?:
|
|
638
|
+
api.call<{ fields?: FieldValues }>("getBlock", { blockId }).then((b) => {
|
|
639
639
|
if (!alive) return;
|
|
640
640
|
const f = b?.fields ?? {};
|
|
641
641
|
setFields(f);
|
|
@@ -680,7 +680,7 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
|
|
|
680
680
|
return () => clearTimeout(t);
|
|
681
681
|
}, [fields, block.pending]);
|
|
682
682
|
|
|
683
|
-
const change = (next:
|
|
683
|
+
const change = (next: FieldValues) => setFields(next);
|
|
684
684
|
|
|
685
685
|
// Report dirty state up (for the editor's leave/unload guard); clear it on unmount so a
|
|
686
686
|
// removed block never leaves a stale "unsaved" flag behind.
|
|
@@ -891,8 +891,8 @@ function PageMeta({ api, page, onSaved, onError }: { api: Api; page: Page; onSav
|
|
|
891
891
|
}
|
|
892
892
|
|
|
893
893
|
/** The page's own FIELDS — its content. Rendered in the canvas, at full width. */
|
|
894
|
-
function PageFields({ api, page, schema, initialFields, onDirtyChange, onError }: { api: Api; page: Page; schema: FieldDefinition[]; initialFields:
|
|
895
|
-
const [fields, setFields] = useState<
|
|
894
|
+
function PageFields({ api, page, schema, initialFields, onDirtyChange, onError }: { api: Api; page: Page; schema: FieldDefinition[]; initialFields: FieldValues; onDirtyChange: (id: string, dirty: boolean) => void; onError: (s: string) => void }) {
|
|
895
|
+
const [fields, setFields] = useState<FieldValues>(initialFields);
|
|
896
896
|
const [ok, setOk] = useState(false);
|
|
897
897
|
const [busy, setBusy] = useState(false);
|
|
898
898
|
|
|
@@ -1480,7 +1480,7 @@ function plainText(html: string): string {
|
|
|
1480
1480
|
.trim();
|
|
1481
1481
|
}
|
|
1482
1482
|
/** One-line preview for a collapsed block: the first non-empty string field, tags stripped. */
|
|
1483
|
-
function blockPreview(fields:
|
|
1483
|
+
function blockPreview(fields: FieldValues): string {
|
|
1484
1484
|
const first = Object.values(fields).find((v) => typeof v === "string" && v.trim());
|
|
1485
1485
|
if (typeof first !== "string") return "";
|
|
1486
1486
|
const text = plainText(first);
|
package/src/fields.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { Button, Heading, Input, ModalDialog, ModalOverlay, ModalSurface, Text,
|
|
|
5
5
|
import { BlockEditor } from "@podoba/react/editor";
|
|
6
6
|
import { useEffect, useRef, useState, type DragEvent, type ReactNode } from "react";
|
|
7
7
|
import type { Api } from "./api";
|
|
8
|
-
import type { FieldDefinition, Media } from "./types";
|
|
8
|
+
import type { FieldDefinition, FieldValue, FieldValues, Media } from "./types";
|
|
9
9
|
|
|
10
10
|
// Tokenized bare control (podoba's filled-field skin) for the native inputs that
|
|
11
11
|
// don't map cleanly onto a podoba primitive (number/date/select/file).
|
|
@@ -147,8 +147,8 @@ function PublishControl({ value, onChange }: { value: string; onChange: (v: stri
|
|
|
147
147
|
);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
export function FieldForm({ schema, value, onChange, api }: { schema: FieldDefinition[]; value:
|
|
151
|
-
const set = (name: string, v:
|
|
150
|
+
export function FieldForm({ schema, value, onChange, api }: { schema: FieldDefinition[]; value: FieldValues; onChange: (v: FieldValues) => void; api: Api }) {
|
|
151
|
+
const set = (name: string, v: FieldValue) => onChange({ ...value, [name]: v });
|
|
152
152
|
return (
|
|
153
153
|
<div className="flex flex-col gap-4">
|
|
154
154
|
{schema.map((def) => (
|
|
@@ -171,13 +171,13 @@ export function FieldForm({ schema, value, onChange, api }: { schema: FieldDefin
|
|
|
171
171
|
* accessible-name computation via `aria-labelledby`. Those three go through the bare
|
|
172
172
|
* `CONTROL` skin instead, the same way `number`/`date` already do.
|
|
173
173
|
*/
|
|
174
|
-
function FieldInput({ def, value, onChange, api, hideLabelAs, siblings }: { def: FieldDefinition; value:
|
|
174
|
+
function FieldInput({ def, value, onChange, api, hideLabelAs, siblings }: { def: FieldDefinition; value: FieldValue; onChange: (v: FieldValue) => void; api: Api; hideLabelAs?: string; siblings?: FieldValues }) {
|
|
175
175
|
const label: ReactNode = hideLabelAs !== undefined ? undefined : (
|
|
176
176
|
<>
|
|
177
177
|
{def.label ?? def.name} {def.required ? <span className="text-danger">*</span> : null}
|
|
178
178
|
</>
|
|
179
179
|
);
|
|
180
|
-
const asText = (v:
|
|
180
|
+
const asText = (v: FieldValue) => (typeof v === "string" ? v : v == null ? "" : JSON.stringify(v));
|
|
181
181
|
switch (def.type) {
|
|
182
182
|
case "text":
|
|
183
183
|
case "url": {
|
|
@@ -258,12 +258,12 @@ function FieldInput({ def, value, onChange, api, hideLabelAs, siblings }: { def:
|
|
|
258
258
|
return (
|
|
259
259
|
<FieldShell label={label}>
|
|
260
260
|
<div className="rounded-lg border border-border bg-surface-muted p-3.5">
|
|
261
|
-
<FieldForm schema={def.fields ?? []} value={(value as
|
|
261
|
+
<FieldForm schema={def.fields ?? []} value={(value as FieldValues) ?? {}} onChange={onChange as (v: FieldValues) => void} api={api} />
|
|
262
262
|
</div>
|
|
263
263
|
</FieldShell>
|
|
264
264
|
);
|
|
265
265
|
case "repeater":
|
|
266
|
-
return <Repeater def={def} value={(value as
|
|
266
|
+
return <Repeater def={def} value={(value as FieldValues[]) ?? []} onChange={onChange as (v: FieldValues[]) => void} api={api} label={label} />;
|
|
267
267
|
default:
|
|
268
268
|
return null;
|
|
269
269
|
}
|
|
@@ -292,7 +292,7 @@ export function RichText({ value, onChange }: { value: string; onChange: (v: str
|
|
|
292
292
|
* Reordering is drag-and-drop from the ⠿ handle, mirroring the block canvas. ↑/↓ stay,
|
|
293
293
|
* because dragging is unavailable to keyboard users and awkward on touch.
|
|
294
294
|
*/
|
|
295
|
-
function Repeater({ def, value, onChange, api, label }: { def: FieldDefinition; value:
|
|
295
|
+
function Repeater({ def, value, onChange, api, label }: { def: FieldDefinition; value: FieldValues[]; onChange: (v: FieldValues[]) => void; api: Api; label: ReactNode }) {
|
|
296
296
|
const items = Array.isArray(value) ? value : [];
|
|
297
297
|
const fields = def.fields ?? [];
|
|
298
298
|
// One field, and not itself a tall control — the case where the card is pure overhead.
|
|
@@ -300,7 +300,7 @@ function Repeater({ def, value, onChange, api, label }: { def: FieldDefinition;
|
|
|
300
300
|
|
|
301
301
|
const [drag, setDrag] = useState<{ from: number; over: number } | null>(null);
|
|
302
302
|
|
|
303
|
-
const upd = (i: number, v:
|
|
303
|
+
const upd = (i: number, v: FieldValues) => onChange(items.map((it, j) => (j === i ? v : it)));
|
|
304
304
|
const add = () => onChange([...items, {}]);
|
|
305
305
|
const del = (i: number) => onChange(items.filter((_, j) => j !== i));
|
|
306
306
|
const moveTo = (from: number, to: number) => {
|
|
@@ -322,7 +322,7 @@ function Repeater({ def, value, onChange, api, label }: { def: FieldDefinition;
|
|
|
322
322
|
* instead titled every slide with the uuid of its image, which is worse than no summary
|
|
323
323
|
* at all — the point is to tell two rows apart at a glance.
|
|
324
324
|
*/
|
|
325
|
-
const summarise = (it:
|
|
325
|
+
const summarise = (it: FieldValues): string => {
|
|
326
326
|
const readable = ["text", "textarea", "richtext", "select", "url"];
|
|
327
327
|
for (const f of fields) {
|
|
328
328
|
if (!readable.includes(f.type)) continue;
|
|
@@ -545,7 +545,7 @@ function SlugField({ def, label, value, source, onChange }: { def: FieldDefiniti
|
|
|
545
545
|
// A `select` field. Static `options` render as-is; when `optionsFrom` is set, the options are
|
|
546
546
|
// fetched once from that query handler (returns `{ value, label }[]`) — e.g. a live list of
|
|
547
547
|
// campaigns — so the editor never has to hardcode or copy identifiers by hand.
|
|
548
|
-
function SelectField({ def, value, onChange, api, ariaLabel }: { def: FieldDefinition; value: string | null; onChange: (v:
|
|
548
|
+
function SelectField({ def, value, onChange, api, ariaLabel }: { def: FieldDefinition; value: string | null; onChange: (v: FieldValue) => void; api: Api; ariaLabel?: string }) {
|
|
549
549
|
const [dyn, setDyn] = useState<{ value: string; label: string }[] | null>(null);
|
|
550
550
|
const from = def.optionsFrom;
|
|
551
551
|
useEffect(() => {
|
package/src/types.ts
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
// @pramen/cms) so the editor stays a self-contained browser app with no server-package
|
|
3
3
|
// dependency — it speaks to the CMS purely over HTTP.
|
|
4
4
|
|
|
5
|
+
/** Any JSON value — the wire form of everything the CMS stores. */
|
|
6
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
7
|
+
|
|
8
|
+
/** One authored field value. Mirrors `FieldValue` in @pramen/cms: a `"media"` field
|
|
9
|
+
* arrives resolved to a `Media`, and `group`/`repeater` fields nest further bags. */
|
|
10
|
+
export type FieldValue = JsonValue | Media | FieldValues | FieldValue[];
|
|
11
|
+
|
|
12
|
+
/** A block / collection / page `fields` bag — field name -> authored value. */
|
|
13
|
+
export interface FieldValues {
|
|
14
|
+
[field: string]: FieldValue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The JSON body of an editor RPC call: an object of values, any of which may be
|
|
18
|
+
* omitted (an absent key is simply not sent). */
|
|
19
|
+
export interface RpcInput {
|
|
20
|
+
[key: string]: FieldValue | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
export type FieldType =
|
|
6
24
|
| "text"
|
|
7
25
|
| "textarea"
|
|
@@ -25,7 +43,7 @@ export interface FieldDefinition {
|
|
|
25
43
|
label?: string;
|
|
26
44
|
type: FieldType;
|
|
27
45
|
required?: boolean;
|
|
28
|
-
default?:
|
|
46
|
+
default?: FieldValue;
|
|
29
47
|
fields?: FieldDefinition[];
|
|
30
48
|
min?: number;
|
|
31
49
|
max?: number;
|
|
@@ -47,7 +65,7 @@ export interface RegionDefinition {
|
|
|
47
65
|
export interface DefaultBlockDefinition {
|
|
48
66
|
region: string;
|
|
49
67
|
blockTypeSlug: string;
|
|
50
|
-
fields?:
|
|
68
|
+
fields?: FieldValues;
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
export interface BlockType {
|
|
@@ -94,7 +112,7 @@ export interface Page {
|
|
|
94
112
|
slug: string;
|
|
95
113
|
status: string;
|
|
96
114
|
locale: string;
|
|
97
|
-
fields?:
|
|
115
|
+
fields?: FieldValues | null; // content-type-level structured data (fieldsSchema)
|
|
98
116
|
translationGroupId?: string | null;
|
|
99
117
|
metaTitle?: string | null;
|
|
100
118
|
metaDescription?: string | null;
|
|
@@ -122,13 +140,13 @@ export interface RenderedBlock {
|
|
|
122
140
|
block_id: string; // block instance id (edit)
|
|
123
141
|
block_type: string;
|
|
124
142
|
title: string | null;
|
|
125
|
-
fields:
|
|
143
|
+
fields: FieldValues;
|
|
126
144
|
is_shared: boolean;
|
|
127
145
|
pending?: boolean; // optimistic placeholder — not yet persisted (temp ids, no getBlock)
|
|
128
146
|
}
|
|
129
147
|
|
|
130
148
|
export interface AssembledPage {
|
|
131
|
-
page: Page & { translations?: { locale: string; slug: string }[]; seo?:
|
|
149
|
+
page: Page & { translations?: { locale: string; slug: string }[]; seo?: FieldValues };
|
|
132
150
|
regions: Record<string, RenderedBlock[]>;
|
|
133
151
|
}
|
|
134
152
|
|