@backmatter/studio 0.0.16 → 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/dist/chunks/{UiKitApp-_EDqWID3.js → UiKitApp-D7vllRQE.js} +2 -2
- package/dist/chunks/{WorkspaceRoutes-B2PrFgpz.js → WorkspaceRoutes-DH1LD-2Q.js} +26374 -25887
- package/dist/chunks/{dashboard-BnlnMnnr.js → dashboard-Dsaeud2x.js} +2 -2
- package/dist/chunks/{define-studio-BYtYDGLX.js → define-studio-DlYjgrwr.js} +12 -16
- package/dist/chunks/{index-D_BHF_8q.js → index-CGMqw2NH.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/define.js +1 -1
- package/dist/index.js +4 -4
- package/dist/site-widget.css +45 -1
- package/dist/site-widget.js +2799 -2791
- package/dist/studio.css +93 -1
- package/package.json +2 -2
- package/src/api.ts +19 -0
- package/src/chat/ChatProvider.tsx +6 -0
- package/src/chat/Composer.tsx +3 -1
- package/src/chrome/Toast.tsx +115 -0
- package/src/chrome/WorkspaceStatusBar.tsx +12 -3
- package/src/chrome/commit-error.ts +13 -0
- package/src/chrome/pull-failure.ts +61 -0
- package/src/fields/FieldControl.tsx +748 -0
- package/src/fields/choose-field-input.ts +64 -0
- package/src/fields/load-relation-documents.ts +35 -0
- package/src/fields/omit-field.ts +12 -0
- package/src/fields/relation-targets.ts +44 -0
- package/src/fields/resolve-object-fields.ts +21 -0
- package/src/pages/BlockSettingsDialog.tsx +19 -115
- package/src/pages/BlocksList.tsx +43 -35
- package/src/pages/BodyProse.tsx +16 -1
- package/src/pages/CodeSessionPage.tsx +180 -7
- package/src/pages/DocEditorPage.tsx +87 -92
- package/src/pages/RenderedBody.tsx +17 -2
- package/src/pages/branch-notice.ts +16 -0
- package/src/pages/code-session-context.tsx +2 -0
- package/src/shell-resets.css +15 -0
- package/src/ui/Input.tsx +178 -20
- package/src/ui/popup.ts +11 -2
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// packages/studio/src/fields/choose-field-input.ts
|
|
2
|
+
import type { SchemaField } from "../api";
|
|
3
|
+
import { resolveFieldInput, type StudioDefinition } from "../define-studio";
|
|
4
|
+
|
|
5
|
+
export type FieldInputKind =
|
|
6
|
+
| "custom"
|
|
7
|
+
| "select"
|
|
8
|
+
| "color"
|
|
9
|
+
| "slider"
|
|
10
|
+
| "relation"
|
|
11
|
+
| "object"
|
|
12
|
+
| "array"
|
|
13
|
+
| "media"
|
|
14
|
+
| "boolean"
|
|
15
|
+
| "number"
|
|
16
|
+
| "blocks"
|
|
17
|
+
| "text";
|
|
18
|
+
|
|
19
|
+
export function chooseFieldInput(
|
|
20
|
+
definition: StudioDefinition,
|
|
21
|
+
field: SchemaField,
|
|
22
|
+
models: Array<{ type: string; external?: boolean }>,
|
|
23
|
+
): FieldInputKind {
|
|
24
|
+
if (resolveFieldInput(definition, field)) {
|
|
25
|
+
return "custom";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (field.kind === "text") {
|
|
29
|
+
if (field.options && field.options.length > 0) {
|
|
30
|
+
return "select";
|
|
31
|
+
}
|
|
32
|
+
if (field.input === "color") {
|
|
33
|
+
return "color";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (field.kind === "number") {
|
|
38
|
+
if (typeof field.min === "number" && typeof field.max === "number") {
|
|
39
|
+
return "slider";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (field.kind === "relation") {
|
|
44
|
+
const target = models.find((m) => m.type === field.relationTo);
|
|
45
|
+
if (target && !target.external) {
|
|
46
|
+
return "relation";
|
|
47
|
+
}
|
|
48
|
+
return "text";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (field.kind === "object") {
|
|
52
|
+
return "object";
|
|
53
|
+
}
|
|
54
|
+
if (field.kind === "array") {
|
|
55
|
+
return "array";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (field.kind === "media") return "media";
|
|
59
|
+
if (field.kind === "boolean") return "boolean";
|
|
60
|
+
if (field.kind === "number") return "number";
|
|
61
|
+
if (field.kind === "blocks") return "blocks";
|
|
62
|
+
|
|
63
|
+
return "text";
|
|
64
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// packages/studio/src/fields/load-relation-documents.ts
|
|
2
|
+
type DocListRow = { type: string; slug: string; status: string };
|
|
3
|
+
type DocEntry = { slug: string; frontmatter: Record<string, unknown> };
|
|
4
|
+
|
|
5
|
+
export async function loadRelationDocuments(
|
|
6
|
+
types: string[],
|
|
7
|
+
list: () => Promise<DocListRow[]>,
|
|
8
|
+
read: (type: string, slug: string) => Promise<{ frontmatter: Record<string, unknown> }>,
|
|
9
|
+
): Promise<Record<string, DocEntry[]>> {
|
|
10
|
+
const wanted = new Set(types);
|
|
11
|
+
const grouped: Record<string, DocEntry[]> = Object.fromEntries(types.map((type) => [type, []]));
|
|
12
|
+
let rows: DocListRow[];
|
|
13
|
+
try {
|
|
14
|
+
rows = await list();
|
|
15
|
+
} catch {
|
|
16
|
+
return grouped;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const selected = rows.filter((row) => wanted.has(row.type) && row.status !== "deleted");
|
|
20
|
+
const loaded = await Promise.all(
|
|
21
|
+
selected.map(async (row) => {
|
|
22
|
+
try {
|
|
23
|
+
const { frontmatter } = await read(row.type, row.slug);
|
|
24
|
+
return { type: row.type, entry: { slug: row.slug, frontmatter } satisfies DocEntry };
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
for (const item of loaded) {
|
|
31
|
+
if (!item) continue;
|
|
32
|
+
grouped[item.type]!.push(item.entry);
|
|
33
|
+
}
|
|
34
|
+
return grouped;
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// packages/studio/src/fields/omit-field.ts
|
|
2
|
+
export function omitField(
|
|
3
|
+
record: Record<string, unknown>,
|
|
4
|
+
name: string,
|
|
5
|
+
next: unknown,
|
|
6
|
+
): Record<string, unknown> {
|
|
7
|
+
if (next === undefined) {
|
|
8
|
+
const { [name]: _removed, ...rest } = record;
|
|
9
|
+
return rest;
|
|
10
|
+
}
|
|
11
|
+
return { ...record, [name]: next };
|
|
12
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// packages/studio/src/fields/relation-targets.ts
|
|
2
|
+
import type { SchemaField } from "../api";
|
|
3
|
+
import { resolveObjectFields } from "./resolve-object-fields";
|
|
4
|
+
|
|
5
|
+
type ModelRef = { type: string; external?: boolean };
|
|
6
|
+
type ObjectDef = { name: string; fields: Record<string, SchemaField> };
|
|
7
|
+
|
|
8
|
+
function collectFromFields(
|
|
9
|
+
fields: Record<string, SchemaField>,
|
|
10
|
+
models: ModelRef[],
|
|
11
|
+
objects: ObjectDef[],
|
|
12
|
+
visiting: Set<string>,
|
|
13
|
+
targets: Set<string>,
|
|
14
|
+
): void {
|
|
15
|
+
for (const field of Object.values(fields)) {
|
|
16
|
+
if (field.kind === "relation" && field.relationTo) {
|
|
17
|
+
const target = models.find((model) => model.type === field.relationTo);
|
|
18
|
+
if (target && !target.external) {
|
|
19
|
+
targets.add(field.relationTo);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nested = resolveObjectFields(field, objects);
|
|
24
|
+
if (!nested) continue;
|
|
25
|
+
|
|
26
|
+
let nextVisiting = visiting;
|
|
27
|
+
if (field.object && (field.kind === "object" || field.kind === "array")) {
|
|
28
|
+
if (visiting.has(field.object)) continue;
|
|
29
|
+
nextVisiting = new Set(visiting).add(field.object);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
collectFromFields(nested, models, objects, nextVisiting, targets);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function relationTargets(
|
|
37
|
+
fields: Record<string, SchemaField>,
|
|
38
|
+
models: ModelRef[],
|
|
39
|
+
objects: ObjectDef[],
|
|
40
|
+
): string[] {
|
|
41
|
+
const targets = new Set<string>();
|
|
42
|
+
collectFromFields(fields, models, objects, new Set<string>(), targets);
|
|
43
|
+
return [...targets].sort();
|
|
44
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// packages/studio/src/fields/resolve-object-fields.ts
|
|
2
|
+
import type { SchemaField } from "../api";
|
|
3
|
+
|
|
4
|
+
export function resolveObjectFields(
|
|
5
|
+
field: SchemaField,
|
|
6
|
+
objects: Array<{ name: string; fields: Record<string, SchemaField> }>,
|
|
7
|
+
): Record<string, SchemaField> | null {
|
|
8
|
+
if (field.fields) {
|
|
9
|
+
return field.fields;
|
|
10
|
+
}
|
|
11
|
+
if (field.object) {
|
|
12
|
+
const entry = objects.find((object) => object.name === field.object);
|
|
13
|
+
return entry?.fields ?? null;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function nestedStartsCollapsed(ancestorObjectNames: string[], objectName: string | undefined): boolean {
|
|
19
|
+
if (!objectName) return false;
|
|
20
|
+
return ancestorObjectNames.includes(objectName);
|
|
21
|
+
}
|
|
@@ -3,30 +3,32 @@ import { useState } from "react";
|
|
|
3
3
|
import { Dialog } from "@base-ui/react/dialog";
|
|
4
4
|
import * as stylex from "@stylexjs/stylex";
|
|
5
5
|
import type { SchemaField } from "../api";
|
|
6
|
-
import {
|
|
6
|
+
import { FieldControl } from "../fields/FieldControl";
|
|
7
|
+
import { omitField } from "../fields/omit-field";
|
|
7
8
|
import { colors, radius, space, type, z } from "../styles/tokens.stylex";
|
|
8
9
|
import { Button } from "../ui/Button";
|
|
9
10
|
import { Field } from "../ui/Field";
|
|
10
|
-
import { Input, Textarea } from "../ui/Input";
|
|
11
11
|
import { cx, isTransitioning } from "../ui/popup";
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
type ModelRef = { type: string; external?: boolean; fields?: Record<string, SchemaField> };
|
|
14
|
+
type RelationDocumentLists = Record<
|
|
15
|
+
string,
|
|
16
|
+
Array<{ slug: string; frontmatter: Record<string, unknown> }>
|
|
17
|
+
>;
|
|
13
18
|
|
|
14
19
|
function fieldLabel(name: string, field: SchemaField) {
|
|
15
20
|
if ("label" in field && typeof field.label === "string" && field.label) return field.label;
|
|
16
21
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
function compositeText(value: unknown): string {
|
|
20
|
-
if (value == null || value === "") return "";
|
|
21
|
-
if (typeof value === "string") return value;
|
|
22
|
-
return JSON.stringify(value, null, 2);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
24
|
export function BlockSettingsDialog({
|
|
26
25
|
name,
|
|
27
26
|
title,
|
|
28
27
|
fields,
|
|
29
28
|
value,
|
|
29
|
+
models,
|
|
30
|
+
objects = [],
|
|
31
|
+
documentLists,
|
|
30
32
|
readOnly,
|
|
31
33
|
onOpenChange,
|
|
32
34
|
onSave,
|
|
@@ -36,32 +38,19 @@ export function BlockSettingsDialog({
|
|
|
36
38
|
title?: string;
|
|
37
39
|
fields: Record<string, SchemaField>;
|
|
38
40
|
value: Record<string, unknown>;
|
|
41
|
+
models: ModelRef[];
|
|
42
|
+
objects?: Array<{ name: string; fields: Record<string, SchemaField> }>;
|
|
43
|
+
documentLists?: RelationDocumentLists;
|
|
39
44
|
readOnly?: boolean;
|
|
40
45
|
onOpenChange: (open: boolean) => void;
|
|
41
46
|
onSave: (props: Record<string, unknown>) => void;
|
|
42
47
|
onRemove: () => void;
|
|
43
48
|
}) {
|
|
44
49
|
const [draft, setDraft] = useState(value);
|
|
45
|
-
const [error, setError] = useState<string | null>(null);
|
|
46
50
|
const names = Object.keys(fields);
|
|
47
51
|
|
|
48
52
|
function save() {
|
|
49
|
-
|
|
50
|
-
for (const [fieldName, field] of Object.entries(fields)) {
|
|
51
|
-
if (field.kind !== "object" && field.kind !== "array") continue;
|
|
52
|
-
const raw = next[fieldName];
|
|
53
|
-
if (typeof raw !== "string" || raw.trim() === "") {
|
|
54
|
-
next[fieldName] = raw === "" ? "" : raw;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
next[fieldName] = JSON.parse(raw);
|
|
59
|
-
} catch {
|
|
60
|
-
setError(`${fieldLabel(fieldName, field)} needs valid JSON.`);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
onSave(next);
|
|
53
|
+
onSave(draft);
|
|
65
54
|
}
|
|
66
55
|
|
|
67
56
|
return (
|
|
@@ -100,17 +89,18 @@ export function BlockSettingsDialog({
|
|
|
100
89
|
name={fieldName}
|
|
101
90
|
field={field}
|
|
102
91
|
value={draft[fieldName]}
|
|
92
|
+
models={models}
|
|
93
|
+
objects={objects}
|
|
94
|
+
documentLists={documentLists}
|
|
103
95
|
readOnly={readOnly}
|
|
104
96
|
onChange={(next) => {
|
|
105
|
-
|
|
106
|
-
setDraft((current) => ({ ...current, [fieldName]: next }));
|
|
97
|
+
setDraft((current) => omitField(current, fieldName, next));
|
|
107
98
|
}}
|
|
108
99
|
/>
|
|
109
100
|
</Field>
|
|
110
101
|
);
|
|
111
102
|
})
|
|
112
103
|
)}
|
|
113
|
-
{error ? <p {...stylex.props(styles.error)}>{error}</p> : null}
|
|
114
104
|
<div {...stylex.props(styles.actions)}>
|
|
115
105
|
{readOnly ? null : (
|
|
116
106
|
<Button variant="danger" onClick={onRemove}>
|
|
@@ -137,82 +127,6 @@ export function BlockSettingsDialog({
|
|
|
137
127
|
);
|
|
138
128
|
}
|
|
139
129
|
|
|
140
|
-
function FieldControl({
|
|
141
|
-
name,
|
|
142
|
-
field,
|
|
143
|
-
value,
|
|
144
|
-
readOnly,
|
|
145
|
-
onChange,
|
|
146
|
-
}: {
|
|
147
|
-
name: string;
|
|
148
|
-
field: SchemaField;
|
|
149
|
-
value: unknown;
|
|
150
|
-
readOnly?: boolean;
|
|
151
|
-
onChange: (value: unknown) => void;
|
|
152
|
-
}) {
|
|
153
|
-
const CustomInput = useFieldInput(field);
|
|
154
|
-
if (CustomInput) {
|
|
155
|
-
return (
|
|
156
|
-
<CustomInput name={name} field={field} value={value} readOnly={readOnly} onChange={onChange} />
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
if (field.kind === "media") {
|
|
160
|
-
return <MediaFieldInput name={name} value={value} readOnly={readOnly} onChange={onChange} />;
|
|
161
|
-
}
|
|
162
|
-
if (field.kind === "boolean") {
|
|
163
|
-
return (
|
|
164
|
-
<input
|
|
165
|
-
type="checkbox"
|
|
166
|
-
checked={value === true}
|
|
167
|
-
disabled={readOnly}
|
|
168
|
-
onChange={(event) => onChange(event.target.checked)}
|
|
169
|
-
{...stylex.props(styles.check)}
|
|
170
|
-
/>
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
if (field.kind === "number") {
|
|
174
|
-
return (
|
|
175
|
-
<Input
|
|
176
|
-
type="number"
|
|
177
|
-
value={value == null || value === "" ? "" : String(value)}
|
|
178
|
-
readOnly={readOnly}
|
|
179
|
-
onChange={(event) => {
|
|
180
|
-
const raw = event.target.value;
|
|
181
|
-
onChange(raw === "" ? "" : Number(raw));
|
|
182
|
-
}}
|
|
183
|
-
/>
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
if ("multiline" in field && field.multiline) {
|
|
187
|
-
return (
|
|
188
|
-
<Textarea
|
|
189
|
-
rows={4}
|
|
190
|
-
value={value == null ? "" : String(value)}
|
|
191
|
-
readOnly={readOnly}
|
|
192
|
-
onChange={(event) => onChange(event.target.value)}
|
|
193
|
-
/>
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
if (field.kind === "object" || field.kind === "array") {
|
|
197
|
-
return (
|
|
198
|
-
<Textarea
|
|
199
|
-
rows={4}
|
|
200
|
-
value={compositeText(value)}
|
|
201
|
-
readOnly={readOnly}
|
|
202
|
-
spellCheck={false}
|
|
203
|
-
onChange={(event) => onChange(event.target.value)}
|
|
204
|
-
/>
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
return (
|
|
208
|
-
<Input
|
|
209
|
-
value={value == null ? "" : String(value)}
|
|
210
|
-
readOnly={readOnly}
|
|
211
|
-
onChange={(event) => onChange(event.target.value)}
|
|
212
|
-
/>
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
130
|
const styles = stylex.create({
|
|
217
131
|
backdrop: {
|
|
218
132
|
position: "fixed",
|
|
@@ -282,16 +196,6 @@ const styles = stylex.create({
|
|
|
282
196
|
color: colors.textMuted,
|
|
283
197
|
fontSize: type.sm,
|
|
284
198
|
},
|
|
285
|
-
error: {
|
|
286
|
-
margin: 0,
|
|
287
|
-
color: colors.danger,
|
|
288
|
-
fontSize: type.sm,
|
|
289
|
-
},
|
|
290
|
-
check: {
|
|
291
|
-
width: 16,
|
|
292
|
-
height: 16,
|
|
293
|
-
margin: 0,
|
|
294
|
-
},
|
|
295
199
|
actions: {
|
|
296
200
|
display: "flex",
|
|
297
201
|
alignItems: "center",
|
package/src/pages/BlocksList.tsx
CHANGED
|
@@ -5,11 +5,12 @@ import { DragDropProvider } from "@dnd-kit/react";
|
|
|
5
5
|
import { isSortable, useSortable } from "@dnd-kit/react/sortable";
|
|
6
6
|
import { Menu } from "@base-ui/react/menu";
|
|
7
7
|
import * as stylex from "@stylexjs/stylex";
|
|
8
|
-
import type { BlockSnapshot } from "../api";
|
|
9
|
-
import {
|
|
8
|
+
import type { BlockSnapshot, SchemaField } from "../api";
|
|
9
|
+
import { useBlockConfig } from "../define-studio";
|
|
10
|
+
import { FieldControl } from "../fields/FieldControl";
|
|
11
|
+
import { omitField } from "../fields/omit-field";
|
|
10
12
|
import { Button } from "../ui/Button";
|
|
11
13
|
import { Field } from "../ui/Field";
|
|
12
|
-
import { Input } from "../ui/Input";
|
|
13
14
|
import { Text } from "../ui/Text";
|
|
14
15
|
import { cx, popup, popupClassName } from "../ui/popup";
|
|
15
16
|
import { colors, radius, space } from "../styles/tokens.stylex";
|
|
@@ -37,10 +38,17 @@ function sortTransition() {
|
|
|
37
38
|
: SORT_TRANSITION;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function fieldLabel(name: string) {
|
|
41
|
+
function fieldLabel(name: string, field: SchemaField) {
|
|
42
|
+
if ("label" in field && typeof field.label === "string" && field.label) return field.label;
|
|
41
43
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
type ModelRef = { type: string; external?: boolean; fields?: Record<string, SchemaField> };
|
|
47
|
+
type RelationDocumentLists = Record<
|
|
48
|
+
string,
|
|
49
|
+
Array<{ slug: string; frontmatter: Record<string, unknown> }>
|
|
50
|
+
>;
|
|
51
|
+
|
|
44
52
|
function GripIcon() {
|
|
45
53
|
return (
|
|
46
54
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden>
|
|
@@ -58,6 +66,9 @@ function BlockItem({
|
|
|
58
66
|
block,
|
|
59
67
|
index,
|
|
60
68
|
catalog,
|
|
69
|
+
models,
|
|
70
|
+
objects,
|
|
71
|
+
documentLists,
|
|
61
72
|
open,
|
|
62
73
|
readOnly,
|
|
63
74
|
onToggle,
|
|
@@ -67,6 +78,9 @@ function BlockItem({
|
|
|
67
78
|
block: BlockInstance;
|
|
68
79
|
index: number;
|
|
69
80
|
catalog: BlockSnapshot[];
|
|
81
|
+
models: ModelRef[];
|
|
82
|
+
objects: Array<{ name: string; fields: Record<string, SchemaField> }>;
|
|
83
|
+
documentLists?: RelationDocumentLists;
|
|
70
84
|
open: boolean;
|
|
71
85
|
readOnly?: boolean;
|
|
72
86
|
onToggle: () => void;
|
|
@@ -83,8 +97,7 @@ function BlockItem({
|
|
|
83
97
|
transition: sortTransition(),
|
|
84
98
|
});
|
|
85
99
|
const def = catalog.find((b) => b.name === block.type);
|
|
86
|
-
const
|
|
87
|
-
const studio = useStudioDefinition();
|
|
100
|
+
const fieldEntries = Object.entries(def?.fields ?? {});
|
|
88
101
|
const config = useBlockConfig(block.type);
|
|
89
102
|
const Preview = config?.preview;
|
|
90
103
|
|
|
@@ -140,41 +153,27 @@ function BlockItem({
|
|
|
140
153
|
<Preview type={block.type} props={block.props} />
|
|
141
154
|
</div>
|
|
142
155
|
) : null}
|
|
143
|
-
{
|
|
156
|
+
{fieldEntries.length === 0 ? (
|
|
144
157
|
<Text tone="muted" size="sm">
|
|
145
158
|
{def
|
|
146
159
|
? "This block has no fields."
|
|
147
160
|
: `Unknown block type "${block.type}".`}
|
|
148
161
|
</Text>
|
|
149
162
|
) : (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
{
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<Input
|
|
165
|
-
value={String(block.props[name] ?? "")}
|
|
166
|
-
readOnly={readOnly}
|
|
167
|
-
onChange={(e) =>
|
|
168
|
-
onUpdateProps({
|
|
169
|
-
...block.props,
|
|
170
|
-
[name]: e.target.value,
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
|
-
/>
|
|
174
|
-
)}
|
|
175
|
-
</Field>
|
|
176
|
-
);
|
|
177
|
-
})
|
|
163
|
+
fieldEntries.map(([name, field]) => (
|
|
164
|
+
<Field key={name} label={fieldLabel(name, field)}>
|
|
165
|
+
<FieldControl
|
|
166
|
+
name={name}
|
|
167
|
+
field={field}
|
|
168
|
+
value={block.props[name]}
|
|
169
|
+
models={models}
|
|
170
|
+
objects={objects}
|
|
171
|
+
documentLists={documentLists}
|
|
172
|
+
readOnly={readOnly}
|
|
173
|
+
onChange={(next) => onUpdateProps(omitField(block.props, name, next))}
|
|
174
|
+
/>
|
|
175
|
+
</Field>
|
|
176
|
+
))
|
|
178
177
|
)}
|
|
179
178
|
</div>
|
|
180
179
|
) : null}
|
|
@@ -185,12 +184,18 @@ function BlockItem({
|
|
|
185
184
|
export function BlocksList({
|
|
186
185
|
blocks,
|
|
187
186
|
catalog,
|
|
187
|
+
models,
|
|
188
|
+
objects = [],
|
|
189
|
+
documentLists,
|
|
188
190
|
readOnly,
|
|
189
191
|
focusBlock,
|
|
190
192
|
onChange,
|
|
191
193
|
}: {
|
|
192
194
|
blocks: BlockInstance[];
|
|
193
195
|
catalog: BlockSnapshot[];
|
|
196
|
+
models: ModelRef[];
|
|
197
|
+
objects?: Array<{ name: string; fields: Record<string, SchemaField> }>;
|
|
198
|
+
documentLists?: RelationDocumentLists;
|
|
194
199
|
readOnly?: boolean;
|
|
195
200
|
focusBlock?: { id: string; nonce: number } | null;
|
|
196
201
|
onChange: (next: BlockInstance[]) => void;
|
|
@@ -241,6 +246,9 @@ export function BlocksList({
|
|
|
241
246
|
block={block}
|
|
242
247
|
index={index}
|
|
243
248
|
catalog={catalog}
|
|
249
|
+
models={models}
|
|
250
|
+
objects={objects}
|
|
251
|
+
documentLists={documentLists}
|
|
244
252
|
open={openId === block.id}
|
|
245
253
|
readOnly={readOnly}
|
|
246
254
|
onToggle={() => setOpenId(openId === block.id ? null : block.id)}
|
package/src/pages/BodyProse.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useId, useRef, useState, type KeyboardEvent } from "react";
|
|
3
3
|
import { Menu } from "@base-ui/react/menu";
|
|
4
4
|
import * as stylex from "@stylexjs/stylex";
|
|
5
|
-
import type { BlockSnapshot } from "../api";
|
|
5
|
+
import type { BlockSnapshot, SchemaField } from "../api";
|
|
6
6
|
import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
7
7
|
import { Button } from "../ui/Button";
|
|
8
8
|
import { Textarea } from "../ui/Input";
|
|
@@ -15,15 +15,27 @@ type BodyView = "rendered" | "source";
|
|
|
15
15
|
|
|
16
16
|
const hover = "@media (hover: hover)";
|
|
17
17
|
|
|
18
|
+
type ModelRef = { type: string; external?: boolean; fields?: Record<string, SchemaField> };
|
|
19
|
+
type RelationDocumentLists = Record<
|
|
20
|
+
string,
|
|
21
|
+
Array<{ slug: string; frontmatter: Record<string, unknown> }>
|
|
22
|
+
>;
|
|
23
|
+
|
|
18
24
|
export function BodyField({
|
|
19
25
|
value,
|
|
20
26
|
catalog = [],
|
|
27
|
+
models,
|
|
28
|
+
objects = [],
|
|
29
|
+
documentLists,
|
|
21
30
|
readOnly,
|
|
22
31
|
onChange,
|
|
23
32
|
onSelect,
|
|
24
33
|
}: {
|
|
25
34
|
value: string;
|
|
26
35
|
catalog?: BlockSnapshot[];
|
|
36
|
+
models: ModelRef[];
|
|
37
|
+
objects?: Array<{ name: string; fields: Record<string, SchemaField> }>;
|
|
38
|
+
documentLists?: RelationDocumentLists;
|
|
27
39
|
readOnly?: boolean;
|
|
28
40
|
onChange: (value: string) => void;
|
|
29
41
|
onSelect: (selection: { start: number; end: number } | null) => void;
|
|
@@ -153,6 +165,9 @@ export function BodyField({
|
|
|
153
165
|
ref={rendered}
|
|
154
166
|
value={value}
|
|
155
167
|
catalog={catalog}
|
|
168
|
+
models={models}
|
|
169
|
+
objects={objects}
|
|
170
|
+
documentLists={documentLists}
|
|
156
171
|
readOnly={readOnly}
|
|
157
172
|
labelledBy={labelId}
|
|
158
173
|
onChange={onChange}
|