@nextbridgehq/payload-block-builder 0.1.9 → 0.2.0
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 +165 -98
- package/dist/bin/init.js +0 -0
- package/dist/client.cjs +1251 -485
- package/dist/client.d.cts +2 -2
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1263 -472
- package/dist/index.cjs +874 -520
- package/dist/index.d.cts +52 -8
- package/dist/index.d.ts +52 -8
- package/dist/index.js +871 -518
- package/package.json +29 -10
- package/src/block-builder/builder.css +391 -36
- package/src/components/BlockDataField/BlockDataField.css +482 -393
- package/src/components/SchemaBuilderField/SchemaBuilderField.css +361 -361
package/dist/client.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use client'
|
|
2
|
+
"use strict";
|
|
2
3
|
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
4
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -136,7 +137,8 @@ function RelationshipPicker({ label, required, collection, hasMany = false, valu
|
|
|
136
137
|
const t = doc.title ?? doc.name ?? doc.slug ?? null;
|
|
137
138
|
if (t) setFetchedTitles((prev) => ({ ...prev, [idStr]: String(t) }));
|
|
138
139
|
}
|
|
139
|
-
}).catch(() => {
|
|
140
|
+
}).catch((err) => {
|
|
141
|
+
console.error("[Block Builder] Failed to load relation title:", err);
|
|
140
142
|
fetchingRef.current.delete(idStr);
|
|
141
143
|
});
|
|
142
144
|
});
|
|
@@ -205,22 +207,79 @@ function JsonField({ label, required, value, onChange }) {
|
|
|
205
207
|
}
|
|
206
208
|
), hasError && /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-error", style: { marginTop: 4 } }, "Invalid JSON \u2014 changes not saved until fixed."));
|
|
207
209
|
}
|
|
208
|
-
function SchemaForm({ schema, value, onChange }) {
|
|
210
|
+
function SchemaForm({ schema, value, onChange, depth = 0 }) {
|
|
209
211
|
const set = (0, import_react.useCallback)(
|
|
210
212
|
(key, val) => onChange({ ...value, [key]: val }),
|
|
211
213
|
[value, onChange]
|
|
212
214
|
);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
onChange: (v) => set(field.name, v)
|
|
215
|
+
if (depth > 10) {
|
|
216
|
+
return /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-error" }, "Max nesting depth reached (10).");
|
|
217
|
+
}
|
|
218
|
+
return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, schema.map((field) => {
|
|
219
|
+
if (field.type === "row") {
|
|
220
|
+
return /* @__PURE__ */ import_react.default.createElement("div", { key: field.name, className: "bdf-row" }, /* @__PURE__ */ import_react.default.createElement(SchemaForm, { schema: field.fields, value, onChange, depth: depth + 1 }));
|
|
220
221
|
}
|
|
221
|
-
|
|
222
|
+
if (field.type === "collapsible") {
|
|
223
|
+
return /* @__PURE__ */ import_react.default.createElement(CollapsibleSection, { key: field.name, field, value, onChange, depth: depth + 1 });
|
|
224
|
+
}
|
|
225
|
+
if (field.type === "tabs") {
|
|
226
|
+
return /* @__PURE__ */ import_react.default.createElement(TabsSection, { key: field.name, field, value, onChange, depth: depth + 1 });
|
|
227
|
+
}
|
|
228
|
+
return /* @__PURE__ */ import_react.default.createElement(
|
|
229
|
+
FieldInput,
|
|
230
|
+
{
|
|
231
|
+
key: field.name,
|
|
232
|
+
field,
|
|
233
|
+
value: value[field.name],
|
|
234
|
+
onChange: (v) => set(field.name, v),
|
|
235
|
+
depth
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
function CollapsibleSection({
|
|
241
|
+
field,
|
|
242
|
+
value,
|
|
243
|
+
onChange,
|
|
244
|
+
depth
|
|
245
|
+
}) {
|
|
246
|
+
const [open, setOpen] = (0, import_react.useState)(true);
|
|
247
|
+
return /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-collapsible" }, /* @__PURE__ */ import_react.default.createElement(
|
|
248
|
+
"button",
|
|
249
|
+
{
|
|
250
|
+
type: "button",
|
|
251
|
+
className: "bdf-collapsible__header",
|
|
252
|
+
onClick: () => setOpen((o) => !o)
|
|
253
|
+
},
|
|
254
|
+
/* @__PURE__ */ import_react.default.createElement("span", { className: `bdf-collapsible__caret${open ? " bdf-collapsible__caret--open" : ""}` }, "\u25B8"),
|
|
255
|
+
field.label
|
|
256
|
+
), open && /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-collapsible__body" }, /* @__PURE__ */ import_react.default.createElement(SchemaForm, { schema: field.fields, value, onChange, depth })));
|
|
257
|
+
}
|
|
258
|
+
function TabsSection({
|
|
259
|
+
field,
|
|
260
|
+
value,
|
|
261
|
+
onChange,
|
|
262
|
+
depth
|
|
263
|
+
}) {
|
|
264
|
+
const [activeTab, setActiveTab] = (0, import_react.useState)(0);
|
|
265
|
+
const set = (0, import_react.useCallback)(
|
|
266
|
+
(key, val) => onChange({ ...value, [key]: val }),
|
|
267
|
+
[value, onChange]
|
|
268
|
+
);
|
|
269
|
+
const tabs = field.tabs ?? [];
|
|
270
|
+
const tab = tabs[activeTab];
|
|
271
|
+
return /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-tabs" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-tabs__list" }, tabs.map((t, i) => /* @__PURE__ */ import_react.default.createElement(
|
|
272
|
+
"button",
|
|
273
|
+
{
|
|
274
|
+
key: t.name ?? t.label ?? i,
|
|
275
|
+
type: "button",
|
|
276
|
+
className: `bdf-tabs__tab${i === activeTab ? " bdf-tabs__tab--active" : ""}`,
|
|
277
|
+
onClick: () => setActiveTab(i)
|
|
278
|
+
},
|
|
279
|
+
t.label
|
|
280
|
+
))), /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-tabs__panel" }, tab && (tab.name ? /* @__PURE__ */ import_react.default.createElement(SchemaForm, { schema: tab.fields, value: value[tab.name] ?? {}, onChange: (v) => set(tab.name, v), depth }) : /* @__PURE__ */ import_react.default.createElement(SchemaForm, { schema: tab.fields, value, onChange, depth }))));
|
|
222
281
|
}
|
|
223
|
-
function FieldInput({ field, value, onChange }) {
|
|
282
|
+
function FieldInput({ field, value, onChange, depth }) {
|
|
224
283
|
const label = field.label ?? field.name;
|
|
225
284
|
switch (field.type) {
|
|
226
285
|
case "text":
|
|
@@ -373,7 +432,8 @@ function FieldInput({ field, value, onChange }) {
|
|
|
373
432
|
const next = [...rows];
|
|
374
433
|
next[i] = updated;
|
|
375
434
|
onChange(next);
|
|
376
|
-
}
|
|
435
|
+
},
|
|
436
|
+
depth: depth + 1
|
|
377
437
|
}
|
|
378
438
|
), /* @__PURE__ */ import_react.default.createElement(
|
|
379
439
|
"button",
|
|
@@ -397,7 +457,7 @@ function FieldInput({ field, value, onChange }) {
|
|
|
397
457
|
case "group": {
|
|
398
458
|
const groupVal = value ?? {};
|
|
399
459
|
const subFields = field.fields ?? [];
|
|
400
|
-
return /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-fieldset" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-fieldset__header" }, label), /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-fieldset__body" }, /* @__PURE__ */ import_react.default.createElement(SchemaForm, { schema: subFields, value: groupVal, onChange }))));
|
|
460
|
+
return /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-fieldset" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-fieldset__header" }, label), /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-fieldset__body" }, /* @__PURE__ */ import_react.default.createElement(SchemaForm, { schema: subFields, value: groupVal, onChange, depth: depth + 1 }))));
|
|
401
461
|
}
|
|
402
462
|
default:
|
|
403
463
|
return /* @__PURE__ */ import_react.default.createElement("div", { style: { fontSize: 12, color: "var(--theme-elevation-400)", fontFamily: "var(--font-body)", padding: "4px 0" } }, "Unsupported field type: ", /* @__PURE__ */ import_react.default.createElement("strong", null, field.type), " (", label, ")");
|
|
@@ -418,6 +478,10 @@ function BlockDataField({ path }) {
|
|
|
418
478
|
setError(null);
|
|
419
479
|
return;
|
|
420
480
|
}
|
|
481
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(String(versionId))) {
|
|
482
|
+
setError("Invalid version ID format");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
421
485
|
setLoading(true);
|
|
422
486
|
setError(null);
|
|
423
487
|
fetch(`/api/block-definition-versions/${versionId}?depth=0`, { credentials: "same-origin" }).then((r) => {
|
|
@@ -472,20 +536,24 @@ function BlockVersionSync({ path }) {
|
|
|
472
536
|
setVersion(null);
|
|
473
537
|
return;
|
|
474
538
|
}
|
|
539
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(String(defId))) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
475
542
|
fetch(`/api/block-definitions/${String(defId)}?depth=1`, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : null).then((doc) => {
|
|
476
543
|
if (!doc) return;
|
|
477
544
|
const currentVersion = doc.currentVersion;
|
|
478
545
|
if (!currentVersion) return;
|
|
479
546
|
const versionId = typeof currentVersion === "object" ? currentVersion.id : currentVersion;
|
|
480
547
|
if (versionId) setVersion(versionId);
|
|
481
|
-
}).catch(() => {
|
|
548
|
+
}).catch((err) => {
|
|
549
|
+
console.error("[Block Builder] BlockVersionSync fetch error:", err);
|
|
482
550
|
});
|
|
483
551
|
}, [blockDefValue, setVersion]);
|
|
484
552
|
return null;
|
|
485
553
|
}
|
|
486
554
|
|
|
487
555
|
// src/components/SchemaBuilderField/index.tsx
|
|
488
|
-
var
|
|
556
|
+
var import_react7 = __toESM(require("react"), 1);
|
|
489
557
|
var import_ui3 = require("@payloadcms/ui");
|
|
490
558
|
|
|
491
559
|
// src/components/SchemaBuilderField/FieldRow.tsx
|
|
@@ -1048,131 +1116,75 @@ function FieldRow({
|
|
|
1048
1116
|
)))));
|
|
1049
1117
|
}
|
|
1050
1118
|
|
|
1051
|
-
// src/components/
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1119
|
+
// src/block-builder/components/ErrorBoundary.tsx
|
|
1120
|
+
var import_react6 = __toESM(require("react"), 1);
|
|
1121
|
+
|
|
1122
|
+
// src/block-builder/store/builder.store.ts
|
|
1123
|
+
var import_zustand = require("zustand");
|
|
1124
|
+
var import_middleware = require("zustand/middleware");
|
|
1125
|
+
var import_immer = require("zustand/middleware/immer");
|
|
1126
|
+
|
|
1127
|
+
// src/utils/uuid.ts
|
|
1128
|
+
function uuidv4() {
|
|
1129
|
+
const c = globalThis.crypto;
|
|
1130
|
+
if (typeof c?.randomUUID === "function") {
|
|
1131
|
+
return c.randomUUID();
|
|
1059
1132
|
}
|
|
1060
|
-
|
|
1133
|
+
if (typeof c?.getRandomValues === "function") {
|
|
1134
|
+
const bytes = c.getRandomValues(new Uint8Array(16));
|
|
1135
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
1136
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
1137
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1138
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1139
|
+
}
|
|
1140
|
+
throw new Error(
|
|
1141
|
+
"No cryptographic random source available. `crypto.randomUUID` requires a secure context (HTTPS or localhost)."
|
|
1142
|
+
);
|
|
1061
1143
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
(
|
|
1072
|
-
|
|
1073
|
-
if (
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
setFields((prev) => [
|
|
1084
|
-
...prev,
|
|
1085
|
-
{ name: "", type: "text", label: "", required: false }
|
|
1086
|
-
]);
|
|
1087
|
-
}, []);
|
|
1088
|
-
const updateField = (0, import_react6.useCallback)((index, updated) => {
|
|
1089
|
-
setFields((prev) => prev.map((f, i) => i === index ? updated : f));
|
|
1090
|
-
}, []);
|
|
1091
|
-
const removeField = (0, import_react6.useCallback)((index) => {
|
|
1092
|
-
setFields((prev) => prev.filter((_, i) => i !== index));
|
|
1093
|
-
}, []);
|
|
1094
|
-
const moveField = (0, import_react6.useCallback)((index, dir) => {
|
|
1095
|
-
setFields((prev) => {
|
|
1096
|
-
const next = [...prev];
|
|
1097
|
-
const target = index + dir;
|
|
1098
|
-
if (target < 0 || target >= next.length) return prev;
|
|
1099
|
-
[next[index], next[target]] = [next[target], next[index]];
|
|
1100
|
-
return next;
|
|
1101
|
-
});
|
|
1102
|
-
}, []);
|
|
1103
|
-
return /* @__PURE__ */ import_react6.default.createElement("div", null, /* @__PURE__ */ import_react6.default.createElement("div", { className: "sbf-section-heading" }, /* @__PURE__ */ import_react6.default.createElement("span", { className: "sbf-section-label" }, "Fields", fields.length > 0 && /* @__PURE__ */ import_react6.default.createElement(
|
|
1104
|
-
"span",
|
|
1105
|
-
{
|
|
1106
|
-
style: {
|
|
1107
|
-
marginLeft: 8,
|
|
1108
|
-
fontSize: 11,
|
|
1109
|
-
color: "var(--theme-elevation-400)",
|
|
1110
|
-
fontWeight: 400
|
|
1144
|
+
|
|
1145
|
+
// src/block-builder/store/builder.store.ts
|
|
1146
|
+
var TAB_PATH_SEP = "::";
|
|
1147
|
+
var BUILDER_PERSIST_KEY = "@nextbridgehq/payload-block-builder";
|
|
1148
|
+
function encodeTabPath(fieldId, tabIndex) {
|
|
1149
|
+
return `${fieldId}${TAB_PATH_SEP}${tabIndex}`;
|
|
1150
|
+
}
|
|
1151
|
+
function walkPath(block, parentPath, create2) {
|
|
1152
|
+
let currentFields = block.fields;
|
|
1153
|
+
for (const segment of parentPath) {
|
|
1154
|
+
const sepIndex = segment.indexOf(TAB_PATH_SEP);
|
|
1155
|
+
if (sepIndex !== -1) {
|
|
1156
|
+
const fieldId = segment.slice(0, sepIndex);
|
|
1157
|
+
const tabIndex = Number(segment.slice(sepIndex + TAB_PATH_SEP.length));
|
|
1158
|
+
const parentField2 = currentFields.find((f) => f.id === fieldId);
|
|
1159
|
+
if (!parentField2 || parentField2.type !== "tabs" || !Array.isArray(parentField2.tabs)) return null;
|
|
1160
|
+
const tab = parentField2.tabs[tabIndex];
|
|
1161
|
+
if (!tab) return null;
|
|
1162
|
+
if (!tab.fields) {
|
|
1163
|
+
if (!create2) return null;
|
|
1164
|
+
tab.fields = [];
|
|
1111
1165
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
fields.length,
|
|
1115
|
-
")"
|
|
1116
|
-
))), fields.length === 0 ? /* @__PURE__ */ import_react6.default.createElement("div", { className: "sbf-empty" }, "No fields defined. Click \u201CAdd Field\u201D below to add the first field to this block schema.") : /* @__PURE__ */ import_react6.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "calc(var(--base, 16px) / 2)" } }, fields.map((field, i) => /* @__PURE__ */ import_react6.default.createElement(
|
|
1117
|
-
FieldRow,
|
|
1118
|
-
{
|
|
1119
|
-
key: i,
|
|
1120
|
-
index: i,
|
|
1121
|
-
total: fields.length,
|
|
1122
|
-
field,
|
|
1123
|
-
onChange: (updated) => updateField(i, updated),
|
|
1124
|
-
onRemove: () => removeField(i),
|
|
1125
|
-
onMoveUp: i > 0 ? () => moveField(i, -1) : void 0,
|
|
1126
|
-
onMoveDown: i < fields.length - 1 ? () => moveField(i, 1) : void 0,
|
|
1127
|
-
readOnly
|
|
1166
|
+
currentFields = tab.fields;
|
|
1167
|
+
continue;
|
|
1128
1168
|
}
|
|
1129
|
-
|
|
1169
|
+
const parentField = currentFields.find((f) => f.id === segment);
|
|
1170
|
+
if (!parentField) return null;
|
|
1171
|
+
if (!parentField.fields) {
|
|
1172
|
+
if (!create2) return null;
|
|
1173
|
+
parentField.fields = [];
|
|
1174
|
+
}
|
|
1175
|
+
currentFields = parentField.fields;
|
|
1176
|
+
}
|
|
1177
|
+
return currentFields;
|
|
1130
1178
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
function EditInBuilderButton() {
|
|
1137
|
-
const { id } = (0, import_ui4.useDocumentInfo)();
|
|
1138
|
-
const { value: slug } = (0, import_ui5.useField)({ path: "slug" });
|
|
1139
|
-
if (!id || !slug) return null;
|
|
1140
|
-
return /* @__PURE__ */ import_react7.default.createElement("div", { style: { marginTop: "1rem" } }, /* @__PURE__ */ import_react7.default.createElement(
|
|
1141
|
-
"a",
|
|
1142
|
-
{
|
|
1143
|
-
href: `/block-builder?load=${slug}`,
|
|
1144
|
-
target: "_blank",
|
|
1145
|
-
rel: "noopener noreferrer",
|
|
1146
|
-
style: {
|
|
1147
|
-
display: "inline-flex",
|
|
1148
|
-
alignItems: "center",
|
|
1149
|
-
gap: "6px",
|
|
1150
|
-
padding: "8px 16px",
|
|
1151
|
-
borderRadius: "6px",
|
|
1152
|
-
border: "1px solid #16a34a",
|
|
1153
|
-
color: "#16a34a",
|
|
1154
|
-
textDecoration: "none",
|
|
1155
|
-
fontSize: "13px",
|
|
1156
|
-
fontWeight: 500,
|
|
1157
|
-
background: "transparent",
|
|
1158
|
-
cursor: "pointer"
|
|
1159
|
-
}
|
|
1160
|
-
},
|
|
1161
|
-
"Edit in Block Builder"
|
|
1162
|
-
));
|
|
1179
|
+
function getTargetFields(block, parentPath) {
|
|
1180
|
+
return walkPath(block, parentPath, false);
|
|
1181
|
+
}
|
|
1182
|
+
function ensureTargetFields(block, parentPath) {
|
|
1183
|
+
return walkPath(block, parentPath, true);
|
|
1163
1184
|
}
|
|
1164
|
-
|
|
1165
|
-
// src/block-builder/components/canvas/BuilderShell.tsx
|
|
1166
|
-
var import_react17 = __toESM(require("react"), 1);
|
|
1167
|
-
|
|
1168
|
-
// src/block-builder/store/builder.store.ts
|
|
1169
|
-
var import_zustand = require("zustand");
|
|
1170
|
-
var import_middleware = require("zustand/middleware");
|
|
1171
|
-
var import_immer = require("zustand/middleware/immer");
|
|
1172
|
-
var import_uuid = require("uuid");
|
|
1173
1185
|
function createDefaultField(type) {
|
|
1174
1186
|
const base = {
|
|
1175
|
-
id: (
|
|
1187
|
+
id: uuidv4(),
|
|
1176
1188
|
type,
|
|
1177
1189
|
name: `${type}Field`,
|
|
1178
1190
|
label: `${type.charAt(0).toUpperCase()}${type.slice(1)} Field`,
|
|
@@ -1180,7 +1192,6 @@ function createDefaultField(type) {
|
|
|
1180
1192
|
};
|
|
1181
1193
|
switch (type) {
|
|
1182
1194
|
case "select":
|
|
1183
|
-
case "radio":
|
|
1184
1195
|
return {
|
|
1185
1196
|
...base,
|
|
1186
1197
|
options: [
|
|
@@ -1189,18 +1200,34 @@ function createDefaultField(type) {
|
|
|
1189
1200
|
]
|
|
1190
1201
|
};
|
|
1191
1202
|
case "relationship":
|
|
1192
|
-
return { ...base,
|
|
1203
|
+
return { ...base, collection: "", hasMany: false };
|
|
1193
1204
|
case "array":
|
|
1194
1205
|
return { ...base, fields: [] };
|
|
1195
1206
|
case "group":
|
|
1196
1207
|
return { ...base, fields: [] };
|
|
1208
|
+
case "row":
|
|
1209
|
+
return { ...base, fields: [] };
|
|
1210
|
+
case "collapsible":
|
|
1211
|
+
return { ...base, label: base.label ?? "Collapsible Section", fields: [] };
|
|
1212
|
+
case "tabs":
|
|
1213
|
+
return { ...base, tabs: [{ id: uuidv4(), label: "Tab 1", fields: [] }] };
|
|
1197
1214
|
default:
|
|
1198
1215
|
return base;
|
|
1199
1216
|
}
|
|
1200
1217
|
}
|
|
1218
|
+
function regenerateFieldIds(fields) {
|
|
1219
|
+
return fields.map((f) => {
|
|
1220
|
+
const next = { ...f, id: uuidv4() };
|
|
1221
|
+
if (next.fields) next.fields = regenerateFieldIds(next.fields);
|
|
1222
|
+
if (next.tabs) {
|
|
1223
|
+
next.tabs = next.tabs.map((tab) => ({ ...tab, fields: regenerateFieldIds(tab.fields) }));
|
|
1224
|
+
}
|
|
1225
|
+
return next;
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1201
1228
|
function createDefaultBlock() {
|
|
1202
1229
|
return {
|
|
1203
|
-
id: (
|
|
1230
|
+
id: uuidv4(),
|
|
1204
1231
|
slug: "myBlock",
|
|
1205
1232
|
interfaceName: "MyBlock",
|
|
1206
1233
|
labels: { singular: "My Block", plural: "My Blocks" },
|
|
@@ -1221,11 +1248,13 @@ var useBuilderStore = (0, import_zustand.create)()(
|
|
|
1221
1248
|
isReadOnly: false,
|
|
1222
1249
|
loadedVersionId: null,
|
|
1223
1250
|
blockSlug: null,
|
|
1251
|
+
activeParentPath: [],
|
|
1224
1252
|
addBlock: () => set((state) => {
|
|
1225
1253
|
const block = createDefaultBlock();
|
|
1226
1254
|
state.blocks.push(block);
|
|
1227
1255
|
state.activeBlockId = block.id;
|
|
1228
1256
|
state.activeFieldId = null;
|
|
1257
|
+
state.activeParentPath = [];
|
|
1229
1258
|
state.isDirty = true;
|
|
1230
1259
|
}),
|
|
1231
1260
|
removeBlock: (blockId) => set((state) => {
|
|
@@ -1233,6 +1262,7 @@ var useBuilderStore = (0, import_zustand.create)()(
|
|
|
1233
1262
|
if (state.activeBlockId === blockId) {
|
|
1234
1263
|
state.activeBlockId = state.blocks[0]?.id ?? null;
|
|
1235
1264
|
state.activeFieldId = null;
|
|
1265
|
+
state.activeParentPath = [];
|
|
1236
1266
|
}
|
|
1237
1267
|
state.isDirty = true;
|
|
1238
1268
|
}),
|
|
@@ -1244,15 +1274,16 @@ var useBuilderStore = (0, import_zustand.create)()(
|
|
|
1244
1274
|
setActiveBlock: (blockId) => set((state) => {
|
|
1245
1275
|
state.activeBlockId = blockId;
|
|
1246
1276
|
state.activeFieldId = null;
|
|
1277
|
+
state.activeParentPath = [];
|
|
1247
1278
|
}),
|
|
1248
1279
|
duplicateBlock: (blockId) => set((state) => {
|
|
1249
1280
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1250
1281
|
if (!block) return;
|
|
1251
1282
|
const clone = JSON.parse(JSON.stringify(block));
|
|
1252
|
-
clone.id = (
|
|
1253
|
-
clone.slug = `${block.slug}
|
|
1283
|
+
clone.id = uuidv4();
|
|
1284
|
+
clone.slug = `${block.slug}-copy`;
|
|
1254
1285
|
clone.interfaceName = block.interfaceName ? `${block.interfaceName}Copy` : void 0;
|
|
1255
|
-
clone.fields = clone.fields
|
|
1286
|
+
clone.fields = regenerateFieldIds(clone.fields);
|
|
1256
1287
|
const idx = state.blocks.findIndex((b) => b.id === blockId);
|
|
1257
1288
|
state.blocks.splice(idx + 1, 0, clone);
|
|
1258
1289
|
state.activeBlockId = clone.id;
|
|
@@ -1261,36 +1292,47 @@ var useBuilderStore = (0, import_zustand.create)()(
|
|
|
1261
1292
|
addField: (blockId, type) => set((state) => {
|
|
1262
1293
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1263
1294
|
if (!block) return;
|
|
1295
|
+
const targetFields = ensureTargetFields(block, state.activeParentPath);
|
|
1296
|
+
if (!targetFields) return;
|
|
1264
1297
|
const field = createDefaultField(type);
|
|
1265
|
-
|
|
1298
|
+
targetFields.push(field);
|
|
1266
1299
|
state.activeFieldId = field.id;
|
|
1267
1300
|
state.isDirty = true;
|
|
1268
1301
|
}),
|
|
1269
1302
|
removeField: (blockId, fieldId) => set((state) => {
|
|
1270
1303
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1271
1304
|
if (!block) return;
|
|
1272
|
-
|
|
1305
|
+
const targetFields = ensureTargetFields(block, state.activeParentPath);
|
|
1306
|
+
if (!targetFields) return;
|
|
1307
|
+
const index = targetFields.findIndex((f) => f.id === fieldId);
|
|
1308
|
+
if (index !== -1) {
|
|
1309
|
+
targetFields.splice(index, 1);
|
|
1310
|
+
}
|
|
1273
1311
|
if (state.activeFieldId === fieldId) state.activeFieldId = null;
|
|
1274
1312
|
state.isDirty = true;
|
|
1275
1313
|
}),
|
|
1276
1314
|
updateField: (blockId, fieldId, updates) => set((state) => {
|
|
1277
1315
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1278
1316
|
if (!block) return;
|
|
1279
|
-
const
|
|
1317
|
+
const targetFields = ensureTargetFields(block, state.activeParentPath);
|
|
1318
|
+
if (!targetFields) return;
|
|
1319
|
+
const field = targetFields.find((f) => f.id === fieldId);
|
|
1280
1320
|
if (field) Object.assign(field, updates);
|
|
1281
1321
|
state.isDirty = true;
|
|
1282
1322
|
}),
|
|
1283
1323
|
reorderFields: (blockId, fromIndex, toIndex) => set((state) => {
|
|
1284
1324
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1285
1325
|
if (!block) return;
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1326
|
+
const targetFields = ensureTargetFields(block, state.activeParentPath);
|
|
1327
|
+
if (!targetFields) return;
|
|
1328
|
+
const [moved] = targetFields.splice(fromIndex, 1);
|
|
1329
|
+
targetFields.splice(toIndex, 0, moved);
|
|
1288
1330
|
state.isDirty = true;
|
|
1289
1331
|
}),
|
|
1290
1332
|
setActiveField: (fieldId) => set((state) => {
|
|
1291
1333
|
state.activeFieldId = fieldId;
|
|
1292
1334
|
}),
|
|
1293
|
-
reset: () => set(() => ({ ...initialState, isReadOnly: false, loadedVersionId: null, blockSlug: null })),
|
|
1335
|
+
reset: () => set(() => ({ ...initialState, isReadOnly: false, loadedVersionId: null, blockSlug: null, activeParentPath: [] })),
|
|
1294
1336
|
markClean: () => set((state) => {
|
|
1295
1337
|
state.isDirty = false;
|
|
1296
1338
|
}),
|
|
@@ -1298,6 +1340,7 @@ var useBuilderStore = (0, import_zustand.create)()(
|
|
|
1298
1340
|
state.blocks = [block];
|
|
1299
1341
|
state.activeBlockId = block.id;
|
|
1300
1342
|
state.activeFieldId = null;
|
|
1343
|
+
state.activeParentPath = [];
|
|
1301
1344
|
state.isDirty = false;
|
|
1302
1345
|
}),
|
|
1303
1346
|
setVersionMeta: (versionId, isReadOnly) => set((state) => {
|
|
@@ -1306,10 +1349,36 @@ var useBuilderStore = (0, import_zustand.create)()(
|
|
|
1306
1349
|
}),
|
|
1307
1350
|
setBlockSlug: (slug) => set((state) => {
|
|
1308
1351
|
state.blockSlug = slug;
|
|
1352
|
+
}),
|
|
1353
|
+
pushParentPath: (fieldId) => set((state) => {
|
|
1354
|
+
state.activeParentPath.push(fieldId);
|
|
1355
|
+
state.activeFieldId = null;
|
|
1356
|
+
}),
|
|
1357
|
+
popParentPath: () => set((state) => {
|
|
1358
|
+
state.activeParentPath.pop();
|
|
1359
|
+
state.activeFieldId = null;
|
|
1360
|
+
}),
|
|
1361
|
+
// Jump directly to an ancestor level -- `depth` is the number of
|
|
1362
|
+
// segments to keep, so breadcrumb index `i` maps to `i + 1`.
|
|
1363
|
+
truncateParentPath: (depth) => set((state) => {
|
|
1364
|
+
if (depth < 0 || depth >= state.activeParentPath.length) return;
|
|
1365
|
+
state.activeParentPath = state.activeParentPath.slice(0, depth);
|
|
1366
|
+
state.activeFieldId = null;
|
|
1367
|
+
}),
|
|
1368
|
+
resetParentPath: () => set((state) => {
|
|
1369
|
+
state.activeParentPath = [];
|
|
1370
|
+
state.activeFieldId = null;
|
|
1309
1371
|
})
|
|
1310
1372
|
})),
|
|
1311
1373
|
{
|
|
1312
|
-
name:
|
|
1374
|
+
name: BUILDER_PERSIST_KEY,
|
|
1375
|
+
version: 1,
|
|
1376
|
+
migrate: (persistedState, version) => {
|
|
1377
|
+
if (version === 0) {
|
|
1378
|
+
return { blocks: [], activeBlockId: null };
|
|
1379
|
+
}
|
|
1380
|
+
return persistedState;
|
|
1381
|
+
},
|
|
1313
1382
|
partialize: (state) => ({
|
|
1314
1383
|
blocks: state.blocks,
|
|
1315
1384
|
activeBlockId: state.activeBlockId
|
|
@@ -1319,163 +1388,191 @@ var useBuilderStore = (0, import_zustand.create)()(
|
|
|
1319
1388
|
)
|
|
1320
1389
|
);
|
|
1321
1390
|
|
|
1322
|
-
// src/block-builder/components/
|
|
1323
|
-
var
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
}).map(({ id: _id, relationTo, ...f }) => ({
|
|
1344
|
-
...f,
|
|
1345
|
-
type: TYPE_MAP[f.type] ?? f.type,
|
|
1346
|
-
// normalizer reads `collection`, block-builder stores `relationTo`
|
|
1347
|
-
...relationTo ? { collection: relationTo } : {}
|
|
1348
|
-
}));
|
|
1349
|
-
return {
|
|
1350
|
-
blockSlug: normalizeSlug(block.slug),
|
|
1351
|
-
name: block.labels?.singular ?? block.slug,
|
|
1352
|
-
schema: { fields },
|
|
1353
|
-
changelog: "Created via block builder"
|
|
1354
|
-
};
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
// src/block-builder/lib/codegen.ts
|
|
1358
|
-
function indent(n) {
|
|
1359
|
-
return " ".repeat(n);
|
|
1360
|
-
}
|
|
1361
|
-
function escStr(s) {
|
|
1362
|
-
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
1363
|
-
}
|
|
1364
|
-
function fieldToCode(field, depth = 1) {
|
|
1365
|
-
const pad = indent(depth);
|
|
1366
|
-
const innerPad = indent(depth + 1);
|
|
1367
|
-
const lines = [];
|
|
1368
|
-
lines.push(`${pad}name: '${escStr(field.name)}'`);
|
|
1369
|
-
lines.push(`${pad}type: '${field.type}'`);
|
|
1370
|
-
if (field.label) lines.push(`${pad}label: '${escStr(field.label)}'`);
|
|
1371
|
-
if (field.required) lines.push(`${pad}required: true`);
|
|
1372
|
-
if (field.unique) lines.push(`${pad}unique: true`);
|
|
1373
|
-
if (field.localized) lines.push(`${pad}localized: true`);
|
|
1374
|
-
if (field.defaultValue !== void 0) {
|
|
1375
|
-
const val = typeof field.defaultValue === "string" ? `'${escStr(String(field.defaultValue))}'` : field.defaultValue;
|
|
1376
|
-
lines.push(`${pad}defaultValue: ${val}`);
|
|
1377
|
-
}
|
|
1378
|
-
if (field.type === "richText") {
|
|
1379
|
-
lines.push(`${pad}editor: lexicalEditor({})`);
|
|
1380
|
-
}
|
|
1381
|
-
if (field.options && field.options.length > 0) {
|
|
1382
|
-
const opts = field.options.map((o) => `{ label: '${escStr(o.label)}', value: '${escStr(o.value)}' }`).join(`, `);
|
|
1383
|
-
lines.push(`${pad}options: [${opts}]`);
|
|
1384
|
-
}
|
|
1385
|
-
if (field.relationTo) {
|
|
1386
|
-
lines.push(`${pad}relationTo: '${escStr(field.relationTo)}'`);
|
|
1391
|
+
// src/block-builder/components/ErrorBoundary.tsx
|
|
1392
|
+
var ErrorBoundary = class extends import_react6.Component {
|
|
1393
|
+
constructor() {
|
|
1394
|
+
super(...arguments);
|
|
1395
|
+
this.state = {
|
|
1396
|
+
hasError: false,
|
|
1397
|
+
error: null
|
|
1398
|
+
};
|
|
1399
|
+
/**
|
|
1400
|
+
* Clears persisted builder state before reloading. "Try again" alone only
|
|
1401
|
+
* resets the boundary, so if the cause is corrupt persisted state the next
|
|
1402
|
+
* render throws immediately -- this gives the user a way out of that loop.
|
|
1403
|
+
*/
|
|
1404
|
+
this.handleResetState = () => {
|
|
1405
|
+
try {
|
|
1406
|
+
window.localStorage.removeItem(BUILDER_PERSIST_KEY);
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
console.error("[Block Builder] Could not clear persisted state:", err);
|
|
1409
|
+
}
|
|
1410
|
+
window.location.reload();
|
|
1411
|
+
};
|
|
1387
1412
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1413
|
+
static getDerivedStateFromError(error) {
|
|
1414
|
+
return { hasError: true, error };
|
|
1390
1415
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
if (field.fields && field.fields.length > 0) {
|
|
1394
|
-
const nested = field.fields.map((f) => `${innerPad}{
|
|
1395
|
-
${fieldToCode(f, depth + 2)}
|
|
1396
|
-
${innerPad}}`).join(",\n");
|
|
1397
|
-
lines.push(`${pad}fields: [
|
|
1398
|
-
${nested}
|
|
1399
|
-
${innerPad}]`);
|
|
1416
|
+
componentDidCatch(error, errorInfo) {
|
|
1417
|
+
console.error("[Block Builder] Uncaught error:", error, errorInfo);
|
|
1400
1418
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1419
|
+
render() {
|
|
1420
|
+
if (this.state.hasError) {
|
|
1421
|
+
return /* @__PURE__ */ import_react6.default.createElement("div", { className: "bb-error-boundary", role: "alert" }, /* @__PURE__ */ import_react6.default.createElement("h2", { className: "bb-error-boundary__title" }, "Something went wrong in the Block Builder."), /* @__PURE__ */ import_react6.default.createElement("details", { className: "bb-error-boundary__details" }, /* @__PURE__ */ import_react6.default.createElement("summary", null, "Error details"), /* @__PURE__ */ import_react6.default.createElement("pre", { className: "bb-error-boundary__trace" }, this.state.error?.toString())), /* @__PURE__ */ import_react6.default.createElement("div", { className: "bb-error-boundary__actions" }, /* @__PURE__ */ import_react6.default.createElement(
|
|
1422
|
+
"button",
|
|
1423
|
+
{
|
|
1424
|
+
type: "button",
|
|
1425
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
1426
|
+
className: "bb-error-boundary__btn bb-error-boundary__btn--primary"
|
|
1427
|
+
},
|
|
1428
|
+
"Try again"
|
|
1429
|
+
), /* @__PURE__ */ import_react6.default.createElement(
|
|
1430
|
+
"button",
|
|
1431
|
+
{
|
|
1432
|
+
type: "button",
|
|
1433
|
+
onClick: this.handleResetState,
|
|
1434
|
+
className: "bb-error-boundary__btn"
|
|
1435
|
+
},
|
|
1436
|
+
"Reset builder state"
|
|
1437
|
+
)), /* @__PURE__ */ import_react6.default.createElement("p", { className: "bb-error-boundary__hint" }, "\u201CReset builder state\u201D discards unsaved local edits and reloads. Published versions are stored in the database and are not affected."));
|
|
1438
|
+
}
|
|
1439
|
+
return this.props.children;
|
|
1410
1440
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
// src/components/SchemaBuilderField/index.tsx
|
|
1444
|
+
function parseSchema(raw) {
|
|
1445
|
+
try {
|
|
1446
|
+
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
1447
|
+
if (!parsed) return [];
|
|
1448
|
+
if (Array.isArray(parsed)) return parsed;
|
|
1449
|
+
if (parsed?.fields && Array.isArray(parsed.fields)) return parsed.fields;
|
|
1450
|
+
} catch {
|
|
1418
1451
|
}
|
|
1419
|
-
|
|
1420
|
-
${fieldToCode(f, 2)}
|
|
1421
|
-
}`).join(",\n");
|
|
1422
|
-
const labelsCode = block.labels ? `
|
|
1423
|
-
labels: {
|
|
1424
|
-
singular: '${escStr(block.labels.singular ?? block.slug)}',
|
|
1425
|
-
plural: '${escStr(block.labels.plural ?? block.slug + "s")}',
|
|
1426
|
-
},` : "";
|
|
1427
|
-
const interfaceLine = block.interfaceName ? `
|
|
1428
|
-
interfaceName: '${escStr(block.interfaceName)}',` : "";
|
|
1429
|
-
const exportName = block.interfaceName ?? toCamelCase(block.slug);
|
|
1430
|
-
return [
|
|
1431
|
-
imports.join("\n"),
|
|
1432
|
-
"",
|
|
1433
|
-
`export const ${exportName}: Block = {`,
|
|
1434
|
-
` slug: '${escStr(block.slug)}',${interfaceLine}${labelsCode}`,
|
|
1435
|
-
` fields: [`,
|
|
1436
|
-
fieldsCode,
|
|
1437
|
-
` ],`,
|
|
1438
|
-
`}`,
|
|
1439
|
-
""
|
|
1440
|
-
].join("\n");
|
|
1452
|
+
return [];
|
|
1441
1453
|
}
|
|
1442
|
-
function
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
)
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
)
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
};
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1454
|
+
function SchemaBuilderField({ path, readOnly }) {
|
|
1455
|
+
const { value, setValue } = (0, import_ui3.useField)({ path });
|
|
1456
|
+
const setValueRef = (0, import_react7.useRef)(setValue);
|
|
1457
|
+
(0, import_react7.useEffect)(() => {
|
|
1458
|
+
setValueRef.current = setValue;
|
|
1459
|
+
});
|
|
1460
|
+
const [fields, setFields] = (0, import_react7.useState)(() => parseSchema(value));
|
|
1461
|
+
const hasExistingValue = value !== void 0 && value !== null;
|
|
1462
|
+
const [hydrated, setHydrated] = (0, import_react7.useState)(!hasExistingValue);
|
|
1463
|
+
(0, import_react7.useEffect)(() => {
|
|
1464
|
+
if (hydrated) return;
|
|
1465
|
+
if (value !== void 0 && value !== null) {
|
|
1466
|
+
setFields(parseSchema(value));
|
|
1467
|
+
setHydrated(true);
|
|
1468
|
+
}
|
|
1469
|
+
}, [value, hydrated]);
|
|
1470
|
+
(0, import_react7.useEffect)(() => {
|
|
1471
|
+
if (!hydrated) return;
|
|
1472
|
+
setValueRef.current({ fields });
|
|
1473
|
+
}, [fields, hydrated]);
|
|
1474
|
+
const addField = (0, import_react7.useCallback)(() => {
|
|
1475
|
+
setFields((prev) => [
|
|
1476
|
+
...prev,
|
|
1477
|
+
{ name: "", type: "text", label: "", required: false }
|
|
1478
|
+
]);
|
|
1479
|
+
}, []);
|
|
1480
|
+
const updateField = (0, import_react7.useCallback)((index, updated) => {
|
|
1481
|
+
setFields((prev) => prev.map((f, i) => i === index ? updated : f));
|
|
1482
|
+
}, []);
|
|
1483
|
+
const removeField = (0, import_react7.useCallback)((index) => {
|
|
1484
|
+
setFields((prev) => prev.filter((_, i) => i !== index));
|
|
1485
|
+
}, []);
|
|
1486
|
+
const moveField = (0, import_react7.useCallback)((index, dir) => {
|
|
1487
|
+
setFields((prev) => {
|
|
1488
|
+
const next = [...prev];
|
|
1489
|
+
const target = index + dir;
|
|
1490
|
+
if (target < 0 || target >= next.length) return prev;
|
|
1491
|
+
[next[index], next[target]] = [next[target], next[index]];
|
|
1492
|
+
return next;
|
|
1493
|
+
});
|
|
1494
|
+
}, []);
|
|
1495
|
+
return /* @__PURE__ */ import_react7.default.createElement(ErrorBoundary, null, /* @__PURE__ */ import_react7.default.createElement("div", null, /* @__PURE__ */ import_react7.default.createElement("div", { className: "sbf-section-heading" }, /* @__PURE__ */ import_react7.default.createElement("span", { className: "sbf-section-label" }, "Fields", fields.length > 0 && /* @__PURE__ */ import_react7.default.createElement(
|
|
1496
|
+
"span",
|
|
1497
|
+
{
|
|
1498
|
+
style: {
|
|
1499
|
+
marginLeft: 8,
|
|
1500
|
+
fontSize: 11,
|
|
1501
|
+
color: "var(--theme-elevation-400)",
|
|
1502
|
+
fontWeight: 400
|
|
1503
|
+
}
|
|
1504
|
+
},
|
|
1505
|
+
"(",
|
|
1506
|
+
fields.length,
|
|
1507
|
+
")"
|
|
1508
|
+
))), fields.length === 0 ? /* @__PURE__ */ import_react7.default.createElement("div", { className: "sbf-empty" }, "No fields defined. Click \u201CAdd Field\u201D below to add the first field to this block schema.") : /* @__PURE__ */ import_react7.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "calc(var(--base, 16px) / 2)" } }, fields.map((field, i) => /* @__PURE__ */ import_react7.default.createElement(
|
|
1509
|
+
FieldRow,
|
|
1510
|
+
{
|
|
1511
|
+
key: i,
|
|
1512
|
+
index: i,
|
|
1513
|
+
total: fields.length,
|
|
1514
|
+
field,
|
|
1515
|
+
onChange: (updated) => updateField(i, updated),
|
|
1516
|
+
onRemove: () => removeField(i),
|
|
1517
|
+
onMoveUp: i > 0 ? () => moveField(i, -1) : void 0,
|
|
1518
|
+
onMoveDown: i < fields.length - 1 ? () => moveField(i, 1) : void 0,
|
|
1519
|
+
readOnly
|
|
1520
|
+
}
|
|
1521
|
+
))), !readOnly && /* @__PURE__ */ import_react7.default.createElement("div", { style: { marginTop: "calc(var(--base, 16px) / 2)", marginBottom: "calc(var(--base, 16px) / 2)" } }, /* @__PURE__ */ import_react7.default.createElement("button", { type: "button", className: "sbf-add-btn", onClick: addField }, /* @__PURE__ */ import_react7.default.createElement("span", { className: "sbf-add-btn__icon" }, "+"), "Add Field"))));
|
|
1461
1522
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
""
|
|
1473
|
-
|
|
1474
|
-
|
|
1523
|
+
|
|
1524
|
+
// src/components/EditInBuilderButton/index.tsx
|
|
1525
|
+
var import_react8 = __toESM(require("react"), 1);
|
|
1526
|
+
var import_ui4 = require("@payloadcms/ui");
|
|
1527
|
+
var import_ui5 = require("@payloadcms/ui");
|
|
1528
|
+
function EditInBuilderButton() {
|
|
1529
|
+
const { id } = (0, import_ui4.useDocumentInfo)();
|
|
1530
|
+
const { value: slug } = (0, import_ui5.useField)({ path: "slug" });
|
|
1531
|
+
if (!id || !slug) return null;
|
|
1532
|
+
return /* @__PURE__ */ import_react8.default.createElement("div", { style: { marginTop: "1rem" } }, /* @__PURE__ */ import_react8.default.createElement(
|
|
1533
|
+
"a",
|
|
1534
|
+
{
|
|
1535
|
+
href: `/block-builder?load=${slug}`,
|
|
1536
|
+
target: "_blank",
|
|
1537
|
+
rel: "noopener noreferrer",
|
|
1538
|
+
style: {
|
|
1539
|
+
display: "inline-flex",
|
|
1540
|
+
alignItems: "center",
|
|
1541
|
+
gap: "6px",
|
|
1542
|
+
padding: "8px 16px",
|
|
1543
|
+
borderRadius: "6px",
|
|
1544
|
+
border: "1px solid #16a34a",
|
|
1545
|
+
color: "#16a34a",
|
|
1546
|
+
textDecoration: "none",
|
|
1547
|
+
fontSize: "13px",
|
|
1548
|
+
fontWeight: 500,
|
|
1549
|
+
background: "transparent",
|
|
1550
|
+
cursor: "pointer"
|
|
1551
|
+
}
|
|
1552
|
+
},
|
|
1553
|
+
"Edit in Block Builder"
|
|
1554
|
+
));
|
|
1475
1555
|
}
|
|
1476
1556
|
|
|
1557
|
+
// src/block-builder/components/canvas/BuilderShell.tsx
|
|
1558
|
+
var import_react19 = __toESM(require("react"), 1);
|
|
1559
|
+
|
|
1477
1560
|
// src/block-builder/components/canvas/TopBar.tsx
|
|
1478
|
-
|
|
1561
|
+
var import_react9 = __toESM(require("react"), 1);
|
|
1562
|
+
var import_lucide_react = require("lucide-react");
|
|
1563
|
+
function ensureFieldIds(fields) {
|
|
1564
|
+
if (!Array.isArray(fields)) return [];
|
|
1565
|
+
return fields.map((field) => {
|
|
1566
|
+
const f = { ...field };
|
|
1567
|
+
if (!f.id) f.id = uuidv4();
|
|
1568
|
+
if (Array.isArray(f.fields)) f.fields = ensureFieldIds(f.fields);
|
|
1569
|
+
if (Array.isArray(f.tabs)) {
|
|
1570
|
+
f.tabs = f.tabs.map((tab) => ({ ...tab, fields: ensureFieldIds(tab.fields) }));
|
|
1571
|
+
}
|
|
1572
|
+
return f;
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersionId, onVersionSelect, onRestoreVersion, onAfterPublish, notification, onSetNotification, previewOpen, onTogglePreview }) {
|
|
1479
1576
|
const blocks = useBuilderStore((s) => s.blocks);
|
|
1480
1577
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
1481
1578
|
const activeBlock = blocks.find((b) => b.id === activeBlockId);
|
|
@@ -1483,21 +1580,23 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1483
1580
|
const isDirty = useBuilderStore((s) => s.isDirty);
|
|
1484
1581
|
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
1485
1582
|
const setVersionMeta = useBuilderStore((s) => s.setVersionMeta);
|
|
1583
|
+
const loadBlock = useBuilderStore((s) => s.loadBlock);
|
|
1486
1584
|
const setNotification = onSetNotification;
|
|
1487
|
-
const [versionDropdownOpen, setVersionDropdownOpen] = (0,
|
|
1488
|
-
const [blockPickerOpen, setBlockPickerOpen] = (0,
|
|
1489
|
-
const dropdownRef = (0,
|
|
1490
|
-
const blockPickerRef = (0,
|
|
1585
|
+
const [versionDropdownOpen, setVersionDropdownOpen] = (0, import_react9.useState)(false);
|
|
1586
|
+
const [blockPickerOpen, setBlockPickerOpen] = (0, import_react9.useState)(false);
|
|
1587
|
+
const dropdownRef = (0, import_react9.useRef)(null);
|
|
1588
|
+
const blockPickerRef = (0, import_react9.useRef)(null);
|
|
1589
|
+
const fileInputRef = (0, import_react9.useRef)(null);
|
|
1491
1590
|
const selectedVersion = versions.find((v) => v.id === selectedVersionId);
|
|
1492
1591
|
const currentVersion = versions.find((v) => v.isCurrent);
|
|
1493
1592
|
const activeBlockDef = blockDefs.find((b) => b.slug === activeSlug);
|
|
1494
|
-
(0,
|
|
1593
|
+
(0, import_react9.useEffect)(() => {
|
|
1495
1594
|
if (notification?.status === "success") {
|
|
1496
1595
|
const t = setTimeout(() => setNotification(null), 3e3);
|
|
1497
1596
|
return () => clearTimeout(t);
|
|
1498
1597
|
}
|
|
1499
1598
|
}, [notification]);
|
|
1500
|
-
(0,
|
|
1599
|
+
(0, import_react9.useEffect)(() => {
|
|
1501
1600
|
function handleClick(e) {
|
|
1502
1601
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
|
1503
1602
|
setVersionDropdownOpen(false);
|
|
@@ -1513,10 +1612,17 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1513
1612
|
if (!activeBlock || isReadOnly) return false;
|
|
1514
1613
|
setNotification({ status: "publishing" });
|
|
1515
1614
|
try {
|
|
1516
|
-
const
|
|
1615
|
+
const normalizeSlug = (s) => s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1616
|
+
const publishedSlug = normalizeSlug(activeBlock.slug);
|
|
1617
|
+
const req = {
|
|
1618
|
+
blockSlug: publishedSlug,
|
|
1619
|
+
name: activeBlock.labels?.singular ?? activeBlock.slug,
|
|
1620
|
+
schema: { fields: activeBlock.fields },
|
|
1621
|
+
changelog: "Created via block builder"
|
|
1622
|
+
};
|
|
1517
1623
|
const res = await fetch("/api/blocks/save", {
|
|
1518
1624
|
method: "POST",
|
|
1519
|
-
headers: { "Content-Type": "application/json" },
|
|
1625
|
+
headers: { "Content-Type": "application/json", "X-Block-Builder": "1" },
|
|
1520
1626
|
body: JSON.stringify(req)
|
|
1521
1627
|
});
|
|
1522
1628
|
const json = await res.json();
|
|
@@ -1527,7 +1633,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1527
1633
|
status: "success",
|
|
1528
1634
|
msg: `v${json.versionNumber ?? "?"} published successfully!`
|
|
1529
1635
|
});
|
|
1530
|
-
onAfterPublish();
|
|
1636
|
+
onAfterPublish(publishedSlug);
|
|
1531
1637
|
return true;
|
|
1532
1638
|
} else {
|
|
1533
1639
|
setNotification({
|
|
@@ -1546,35 +1652,54 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1546
1652
|
return false;
|
|
1547
1653
|
}
|
|
1548
1654
|
}
|
|
1549
|
-
function
|
|
1550
|
-
if (
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1655
|
+
function handleExportJson() {
|
|
1656
|
+
if (!activeBlock) return;
|
|
1657
|
+
const blob = new Blob([JSON.stringify(activeBlock, null, 2)], { type: "application/json" });
|
|
1658
|
+
const url = URL.createObjectURL(blob);
|
|
1659
|
+
const a = document.createElement("a");
|
|
1660
|
+
a.href = url;
|
|
1661
|
+
a.download = `${activeBlock.slug}-schema.json`;
|
|
1662
|
+
document.body.appendChild(a);
|
|
1663
|
+
a.click();
|
|
1664
|
+
document.body.removeChild(a);
|
|
1665
|
+
URL.revokeObjectURL(url);
|
|
1666
|
+
}
|
|
1667
|
+
function handleImportJson(e) {
|
|
1668
|
+
const file = e.target.files?.[0];
|
|
1669
|
+
if (!file) return;
|
|
1670
|
+
const reader = new FileReader();
|
|
1671
|
+
reader.onload = (event) => {
|
|
1672
|
+
try {
|
|
1673
|
+
const json = JSON.parse(event.target?.result);
|
|
1674
|
+
if (json && json.slug && Array.isArray(json.fields)) {
|
|
1675
|
+
if (!json.id) json.id = uuidv4();
|
|
1676
|
+
json.fields = ensureFieldIds(json.fields);
|
|
1677
|
+
loadBlock(json);
|
|
1678
|
+
setNotification({ status: "success", msg: "Block imported successfully!" });
|
|
1679
|
+
} else {
|
|
1680
|
+
setNotification({ status: "error", title: "Invalid JSON", errors: ["The file does not contain a valid block schema."] });
|
|
1681
|
+
}
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
setNotification({ status: "error", title: "Parse Error", errors: ["Could not parse JSON file."] });
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
reader.readAsText(file);
|
|
1687
|
+
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
1563
1688
|
}
|
|
1564
1689
|
function formatDate(iso) {
|
|
1565
1690
|
return new Date(iso).toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
1566
1691
|
}
|
|
1567
|
-
return /* @__PURE__ */
|
|
1692
|
+
return /* @__PURE__ */ import_react9.default.createElement(import_react9.default.Fragment, null, /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-topbar" }, /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-topbar__brand" }, /* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-topbar__title" }, "Block Builder"), isDirty && !isReadOnly && /* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-topbar__dirty" }, "* unsaved")), /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-topbar__selectors" }, blockDefs.length > 0 && /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-block-picker", ref: blockPickerRef }, /* @__PURE__ */ import_react9.default.createElement(
|
|
1568
1693
|
"button",
|
|
1569
1694
|
{
|
|
1570
1695
|
type: "button",
|
|
1571
1696
|
className: "bb-block-picker__trigger",
|
|
1572
1697
|
onClick: () => setBlockPickerOpen((o) => !o)
|
|
1573
1698
|
},
|
|
1574
|
-
/* @__PURE__ */
|
|
1575
|
-
/* @__PURE__ */
|
|
1576
|
-
/* @__PURE__ */
|
|
1577
|
-
), blockPickerOpen && /* @__PURE__ */
|
|
1699
|
+
/* @__PURE__ */ import_react9.default.createElement(import_lucide_react.Blocks, { size: 14, strokeWidth: 1.75, className: "bb-block-picker__icon" }),
|
|
1700
|
+
/* @__PURE__ */ import_react9.default.createElement("span", null, activeBlockDef?.name ?? activeSlug ?? "Select a block"),
|
|
1701
|
+
/* @__PURE__ */ import_react9.default.createElement(import_lucide_react.ChevronDown, { size: 14, strokeWidth: 1.75, className: "bb-version-selector__chevron" })
|
|
1702
|
+
), blockPickerOpen && /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-block-picker__dropdown" }, /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-version-dropdown__header" }, "Block Definitions"), blockDefs.map((b) => /* @__PURE__ */ import_react9.default.createElement(
|
|
1578
1703
|
"button",
|
|
1579
1704
|
{
|
|
1580
1705
|
key: b.id,
|
|
@@ -1585,20 +1710,20 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1585
1710
|
onBlockSelect(b.slug);
|
|
1586
1711
|
}
|
|
1587
1712
|
},
|
|
1588
|
-
/* @__PURE__ */
|
|
1589
|
-
/* @__PURE__ */
|
|
1590
|
-
)))), versions.length > 0 && /* @__PURE__ */
|
|
1713
|
+
/* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-block-picker__item-name" }, b.name),
|
|
1714
|
+
/* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-version-dropdown__meta" }, b.slug)
|
|
1715
|
+
)))), versions.length > 0 && /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-version-selector", ref: dropdownRef }, /* @__PURE__ */ import_react9.default.createElement(
|
|
1591
1716
|
"button",
|
|
1592
1717
|
{
|
|
1593
1718
|
type: "button",
|
|
1594
1719
|
className: `bb-version-selector__trigger${isReadOnly ? " bb-version-selector__trigger--readonly" : ""}`,
|
|
1595
1720
|
onClick: () => setVersionDropdownOpen((o) => !o)
|
|
1596
1721
|
},
|
|
1597
|
-
/* @__PURE__ */
|
|
1598
|
-
/* @__PURE__ */
|
|
1599
|
-
selectedVersion?.isCurrent && /* @__PURE__ */
|
|
1600
|
-
/* @__PURE__ */
|
|
1601
|
-
), versionDropdownOpen && /* @__PURE__ */
|
|
1722
|
+
/* @__PURE__ */ import_react9.default.createElement("span", { className: `bb-version-selector__dot${selectedVersion?.isCurrent ? " bb-version-selector__dot--current" : " bb-version-selector__dot--old"}` }),
|
|
1723
|
+
/* @__PURE__ */ import_react9.default.createElement("span", null, selectedVersion?.label ?? `v${selectedVersion?.versionNumber ?? "?"}`),
|
|
1724
|
+
selectedVersion?.isCurrent && /* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-version-selector__badge" }, "current"),
|
|
1725
|
+
/* @__PURE__ */ import_react9.default.createElement(import_lucide_react.ChevronDown, { size: 14, strokeWidth: 1.75, className: "bb-version-selector__chevron" })
|
|
1726
|
+
), versionDropdownOpen && /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-version-dropdown" }, /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-version-dropdown__header" }, "Version History"), versions.map((v) => /* @__PURE__ */ import_react9.default.createElement(
|
|
1602
1727
|
"button",
|
|
1603
1728
|
{
|
|
1604
1729
|
key: v.id,
|
|
@@ -1609,20 +1734,45 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1609
1734
|
onVersionSelect(v.id);
|
|
1610
1735
|
}
|
|
1611
1736
|
},
|
|
1612
|
-
/* @__PURE__ */
|
|
1613
|
-
/* @__PURE__ */
|
|
1614
|
-
/* @__PURE__ */
|
|
1615
|
-
))))), /* @__PURE__ */
|
|
1737
|
+
/* @__PURE__ */ import_react9.default.createElement("span", { className: `bb-version-selector__dot${v.isCurrent ? " bb-version-selector__dot--current" : " bb-version-selector__dot--old"}` }),
|
|
1738
|
+
/* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-version-dropdown__label" }, v.label, v.isCurrent && /* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-version-selector__badge" }, "current")),
|
|
1739
|
+
/* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-version-dropdown__meta" }, v.changelog ? `${v.changelog.slice(0, 32)}${v.changelog.length > 32 ? "..." : ""}` : formatDate(v.createdAt))
|
|
1740
|
+
))))), /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-topbar__actions" }, /* @__PURE__ */ import_react9.default.createElement(
|
|
1741
|
+
"input",
|
|
1742
|
+
{
|
|
1743
|
+
type: "file",
|
|
1744
|
+
accept: ".json",
|
|
1745
|
+
ref: fileInputRef,
|
|
1746
|
+
style: { display: "none" },
|
|
1747
|
+
onChange: handleImportJson
|
|
1748
|
+
}
|
|
1749
|
+
), /* @__PURE__ */ import_react9.default.createElement(
|
|
1616
1750
|
"button",
|
|
1617
1751
|
{
|
|
1618
1752
|
type: "button",
|
|
1619
|
-
onClick:
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1753
|
+
onClick: () => fileInputRef.current?.click(),
|
|
1754
|
+
className: "bb-btn bb-btn--secondary"
|
|
1755
|
+
},
|
|
1756
|
+
"Import JSON"
|
|
1757
|
+
), /* @__PURE__ */ import_react9.default.createElement(
|
|
1758
|
+
"button",
|
|
1759
|
+
{
|
|
1760
|
+
type: "button",
|
|
1761
|
+
onClick: handleExportJson,
|
|
1762
|
+
disabled: !activeBlock,
|
|
1763
|
+
className: "bb-btn bb-btn--secondary"
|
|
1623
1764
|
},
|
|
1624
|
-
"Export
|
|
1625
|
-
),
|
|
1765
|
+
"Export JSON"
|
|
1766
|
+
), /* @__PURE__ */ import_react9.default.createElement("div", { style: { width: 1, height: 24, background: "var(--bb-border)", margin: "0 4px" } }), /* @__PURE__ */ import_react9.default.createElement(
|
|
1767
|
+
"button",
|
|
1768
|
+
{
|
|
1769
|
+
type: "button",
|
|
1770
|
+
onClick: onTogglePreview,
|
|
1771
|
+
disabled: !activeBlock,
|
|
1772
|
+
className: `bb-btn ${previewOpen ? "bb-btn--primary" : "bb-btn--secondary"}`
|
|
1773
|
+
},
|
|
1774
|
+
previewOpen ? "Close Preview" : "Live Preview"
|
|
1775
|
+
), isReadOnly ? /* @__PURE__ */ import_react9.default.createElement(import_react9.default.Fragment, null, /* @__PURE__ */ import_react9.default.createElement(
|
|
1626
1776
|
"button",
|
|
1627
1777
|
{
|
|
1628
1778
|
type: "button",
|
|
@@ -1633,7 +1783,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1633
1783
|
disabled: !currentVersion
|
|
1634
1784
|
},
|
|
1635
1785
|
"Back to current"
|
|
1636
|
-
), /* @__PURE__ */
|
|
1786
|
+
), /* @__PURE__ */ import_react9.default.createElement(
|
|
1637
1787
|
"button",
|
|
1638
1788
|
{
|
|
1639
1789
|
type: "button",
|
|
@@ -1645,7 +1795,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1645
1795
|
className: "bb-btn bb-btn--warning"
|
|
1646
1796
|
},
|
|
1647
1797
|
notification?.status === "publishing" ? "Restoring..." : "Restore as new version"
|
|
1648
|
-
)) : /* @__PURE__ */
|
|
1798
|
+
)) : /* @__PURE__ */ import_react9.default.createElement(
|
|
1649
1799
|
"button",
|
|
1650
1800
|
{
|
|
1651
1801
|
type: "button",
|
|
@@ -1654,11 +1804,11 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1654
1804
|
className: "bb-btn bb-btn--primary"
|
|
1655
1805
|
},
|
|
1656
1806
|
notification?.status === "publishing" ? "Publishing..." : "Publish to Payload"
|
|
1657
|
-
))), notification?.status === "publishing" && /* @__PURE__ */
|
|
1807
|
+
))), notification?.status === "publishing" && /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-notify bb-notify--publishing" }, /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-notify__spinner" }), /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ import_react9.default.createElement("p", { className: "bb-notify__title" }, isReadOnly ? "Restoring version..." : "Publishing to Payload..."), /* @__PURE__ */ import_react9.default.createElement("p", { className: "bb-notify__sub" }, "Validating schema and saving block definition.")))), notification?.status === "success" && /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-notify bb-notify--success" }, /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-notify__icon" }, "OK"), /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ import_react9.default.createElement("p", { className: "bb-notify__title" }, notification.msg), /* @__PURE__ */ import_react9.default.createElement("p", { className: "bb-notify__sub" }, "The block definition and version have been saved.")), /* @__PURE__ */ import_react9.default.createElement("button", { className: "bb-notify__close", onClick: () => setNotification(null) }, /* @__PURE__ */ import_react9.default.createElement(import_lucide_react.X, { size: 12, strokeWidth: 2 })))), notification?.status === "error" && /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-notify bb-notify--error" }, /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-notify__icon" }, "!"), /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ import_react9.default.createElement("p", { className: "bb-notify__title" }, notification.title), /* @__PURE__ */ import_react9.default.createElement("p", { className: "bb-notify__sub" }, "Fix the following errors before publishing:"), /* @__PURE__ */ import_react9.default.createElement("ul", { className: "bb-notify__error-list" }, notification.errors.map((e, i) => /* @__PURE__ */ import_react9.default.createElement("li", { key: i }, e)))), /* @__PURE__ */ import_react9.default.createElement("button", { className: "bb-notify__close", onClick: () => setNotification(null) }, /* @__PURE__ */ import_react9.default.createElement(import_lucide_react.X, { size: 12, strokeWidth: 2 })))));
|
|
1658
1808
|
}
|
|
1659
1809
|
|
|
1660
1810
|
// src/block-builder/components/canvas/BlockList.tsx
|
|
1661
|
-
var
|
|
1811
|
+
var import_react10 = __toESM(require("react"), 1);
|
|
1662
1812
|
var import_lucide_react2 = require("lucide-react");
|
|
1663
1813
|
function BlockList({ blockDefs = [], activeSlug, onBlockSelect }) {
|
|
1664
1814
|
const blocks = useBuilderStore((s) => s.blocks);
|
|
@@ -1672,37 +1822,37 @@ function BlockList({ blockDefs = [], activeSlug, onBlockSelect }) {
|
|
|
1672
1822
|
const localOnlyBlocks = blocks.filter(
|
|
1673
1823
|
(b) => !blockDefs.some((d) => d.slug === b.slug)
|
|
1674
1824
|
);
|
|
1675
|
-
return /* @__PURE__ */
|
|
1825
|
+
return /* @__PURE__ */ import_react10.default.createElement("div", { className: "bb-sidebar bb-sidebar--200 bb-sidebar--blocks" }, /* @__PURE__ */ import_react10.default.createElement("div", { className: "bb-sidebar__header" }, /* @__PURE__ */ import_react10.default.createElement("span", { className: "bb-sidebar__title" }, "Blocks"), /* @__PURE__ */ import_react10.default.createElement("button", { type: "button", onClick: addBlock, title: "Add block", className: "bb-sidebar__add" }, /* @__PURE__ */ import_react10.default.createElement(import_lucide_react2.Plus, { size: 14, strokeWidth: 2 }))), /* @__PURE__ */ import_react10.default.createElement("div", { className: "bb-sidebar__body" }, useApiNav && blockDefs.map((def) => {
|
|
1676
1826
|
const isActive = def.slug === activeSlug;
|
|
1677
1827
|
const fieldCount = isActive && loadedBlock ? loadedBlock.fields.length : null;
|
|
1678
|
-
return /* @__PURE__ */
|
|
1828
|
+
return /* @__PURE__ */ import_react10.default.createElement(
|
|
1679
1829
|
"div",
|
|
1680
1830
|
{
|
|
1681
1831
|
key: def.id,
|
|
1682
1832
|
onClick: () => onBlockSelect?.(def.slug),
|
|
1683
1833
|
className: `bb-block-item${isActive ? " bb-block-item--active" : ""}`
|
|
1684
1834
|
},
|
|
1685
|
-
/* @__PURE__ */
|
|
1686
|
-
/* @__PURE__ */
|
|
1835
|
+
/* @__PURE__ */ import_react10.default.createElement("div", { className: "bb-block-item__slug" }, def.slug),
|
|
1836
|
+
/* @__PURE__ */ import_react10.default.createElement("div", { className: "bb-block-item__meta" }, fieldCount !== null ? `${fieldCount} field${fieldCount !== 1 ? "s" : ""}` : def.name)
|
|
1687
1837
|
);
|
|
1688
1838
|
}), localOnlyBlocks.map((block) => {
|
|
1689
1839
|
const isActive = block.id === activeBlockId;
|
|
1690
|
-
return /* @__PURE__ */
|
|
1840
|
+
return /* @__PURE__ */ import_react10.default.createElement(
|
|
1691
1841
|
"div",
|
|
1692
1842
|
{
|
|
1693
1843
|
key: block.id,
|
|
1694
1844
|
onClick: () => setActiveBlock(block.id),
|
|
1695
1845
|
className: `bb-block-item${isActive ? " bb-block-item--active" : ""}`
|
|
1696
1846
|
},
|
|
1697
|
-
/* @__PURE__ */
|
|
1698
|
-
/* @__PURE__ */
|
|
1699
|
-
/* @__PURE__ */
|
|
1847
|
+
/* @__PURE__ */ import_react10.default.createElement("div", { className: "bb-block-item__slug" }, block.slug),
|
|
1848
|
+
/* @__PURE__ */ import_react10.default.createElement("div", { className: "bb-block-item__meta" }, block.fields.length, " field", block.fields.length !== 1 ? "s" : ""),
|
|
1849
|
+
/* @__PURE__ */ import_react10.default.createElement(
|
|
1700
1850
|
"div",
|
|
1701
1851
|
{
|
|
1702
1852
|
className: "bb-block-item__actions",
|
|
1703
1853
|
onClick: (e) => e.stopPropagation()
|
|
1704
1854
|
},
|
|
1705
|
-
/* @__PURE__ */
|
|
1855
|
+
/* @__PURE__ */ import_react10.default.createElement(
|
|
1706
1856
|
"button",
|
|
1707
1857
|
{
|
|
1708
1858
|
type: "button",
|
|
@@ -1710,9 +1860,9 @@ function BlockList({ blockDefs = [], activeSlug, onBlockSelect }) {
|
|
|
1710
1860
|
className: "bb-block-action",
|
|
1711
1861
|
title: "Duplicate"
|
|
1712
1862
|
},
|
|
1713
|
-
/* @__PURE__ */
|
|
1863
|
+
/* @__PURE__ */ import_react10.default.createElement(import_lucide_react2.Copy, { size: 12, strokeWidth: 1.75 })
|
|
1714
1864
|
),
|
|
1715
|
-
/* @__PURE__ */
|
|
1865
|
+
/* @__PURE__ */ import_react10.default.createElement(
|
|
1716
1866
|
"button",
|
|
1717
1867
|
{
|
|
1718
1868
|
type: "button",
|
|
@@ -1720,69 +1870,103 @@ function BlockList({ blockDefs = [], activeSlug, onBlockSelect }) {
|
|
|
1720
1870
|
className: "bb-block-action bb-block-action--danger",
|
|
1721
1871
|
title: "Delete"
|
|
1722
1872
|
},
|
|
1723
|
-
/* @__PURE__ */
|
|
1873
|
+
/* @__PURE__ */ import_react10.default.createElement(import_lucide_react2.Trash2, { size: 12, strokeWidth: 1.75 })
|
|
1724
1874
|
)
|
|
1725
1875
|
)
|
|
1726
1876
|
);
|
|
1727
|
-
}), !useApiNav && blocks.length === 0 && /* @__PURE__ */
|
|
1877
|
+
}), !useApiNav && blocks.length === 0 && /* @__PURE__ */ import_react10.default.createElement("div", { className: "bb-block-empty" }, "No blocks yet.", /* @__PURE__ */ import_react10.default.createElement("br", null), "Click + to create one.")));
|
|
1728
1878
|
}
|
|
1729
1879
|
|
|
1730
1880
|
// src/block-builder/components/canvas/BuilderCanvas.tsx
|
|
1731
|
-
var
|
|
1881
|
+
var import_react12 = __toESM(require("react"), 1);
|
|
1732
1882
|
var import_core = require("@dnd-kit/core");
|
|
1733
1883
|
var import_sortable2 = require("@dnd-kit/sortable");
|
|
1734
1884
|
var import_modifiers = require("@dnd-kit/modifiers");
|
|
1735
1885
|
|
|
1736
1886
|
// src/block-builder/components/canvas/SortableFieldCard.tsx
|
|
1737
|
-
var
|
|
1887
|
+
var import_react11 = __toESM(require("react"), 1);
|
|
1738
1888
|
var import_sortable = require("@dnd-kit/sortable");
|
|
1739
1889
|
var import_utilities = require("@dnd-kit/utilities");
|
|
1740
1890
|
var import_lucide_react3 = require("lucide-react");
|
|
1741
1891
|
var ICON_MAP = {
|
|
1742
1892
|
text: import_lucide_react3.Type,
|
|
1743
1893
|
textarea: import_lucide_react3.AlignLeft,
|
|
1894
|
+
richtext: import_lucide_react3.AlignJustify,
|
|
1744
1895
|
number: import_lucide_react3.Hash,
|
|
1745
1896
|
email: import_lucide_react3.Mail,
|
|
1746
1897
|
date: import_lucide_react3.Calendar,
|
|
1747
1898
|
checkbox: import_lucide_react3.CheckSquare,
|
|
1748
1899
|
select: import_lucide_react3.ChevronDown,
|
|
1749
|
-
|
|
1750
|
-
|
|
1900
|
+
image: import_lucide_react3.Image,
|
|
1901
|
+
file: import_lucide_react3.Upload,
|
|
1751
1902
|
relationship: import_lucide_react3.Link,
|
|
1752
|
-
json: import_lucide_react3.Braces
|
|
1903
|
+
json: import_lucide_react3.Braces,
|
|
1904
|
+
array: import_lucide_react3.List,
|
|
1905
|
+
group: import_lucide_react3.Box,
|
|
1906
|
+
row: import_lucide_react3.Columns,
|
|
1907
|
+
tabs: import_lucide_react3.Folder,
|
|
1908
|
+
collapsible: import_lucide_react3.ChevronDown
|
|
1753
1909
|
};
|
|
1754
|
-
|
|
1910
|
+
var SortableFieldCard = import_react11.default.memo(function SortableFieldCard2({
|
|
1911
|
+
field,
|
|
1912
|
+
blockId,
|
|
1913
|
+
index,
|
|
1914
|
+
isActive,
|
|
1915
|
+
isReadOnly,
|
|
1916
|
+
onDrillDown
|
|
1917
|
+
}) {
|
|
1755
1918
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = (0, import_sortable.useSortable)({
|
|
1756
1919
|
id: field.id
|
|
1757
1920
|
});
|
|
1758
|
-
const activeFieldId = useBuilderStore((s) => s.activeFieldId);
|
|
1759
1921
|
const setActiveField = useBuilderStore((s) => s.setActiveField);
|
|
1760
1922
|
const removeField = useBuilderStore((s) => s.removeField);
|
|
1761
|
-
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
1762
|
-
const isActive = activeFieldId === field.id;
|
|
1763
1923
|
const wrapStyle = {
|
|
1764
1924
|
transform: import_utilities.CSS.Transform.toString(transform),
|
|
1765
1925
|
transition
|
|
1766
1926
|
};
|
|
1767
|
-
return /* @__PURE__ */
|
|
1927
|
+
return /* @__PURE__ */ import_react11.default.createElement(
|
|
1768
1928
|
"div",
|
|
1769
1929
|
{
|
|
1770
1930
|
ref: setNodeRef,
|
|
1771
1931
|
style: wrapStyle,
|
|
1772
1932
|
className: `bb-field-card-wrap${isDragging ? " bb-field-card-wrap--dragging" : ""}`,
|
|
1773
1933
|
...attributes,
|
|
1774
|
-
...listeners
|
|
1934
|
+
...listeners,
|
|
1935
|
+
"aria-label": `Draggable field card for ${field.name || "unnamed"}`
|
|
1775
1936
|
},
|
|
1776
|
-
/* @__PURE__ */
|
|
1937
|
+
/* @__PURE__ */ import_react11.default.createElement(
|
|
1777
1938
|
"div",
|
|
1778
1939
|
{
|
|
1779
1940
|
className: `bb-field-card${isActive ? " bb-field-card--active" : ""}`,
|
|
1780
1941
|
onClick: () => setActiveField(isActive ? null : field.id)
|
|
1781
1942
|
},
|
|
1782
|
-
/* @__PURE__ */
|
|
1783
|
-
/* @__PURE__ */
|
|
1784
|
-
/* @__PURE__ */
|
|
1785
|
-
|
|
1943
|
+
/* @__PURE__ */ import_react11.default.createElement("span", { className: "bb-field-card__icon" }, /* @__PURE__ */ import_react11.default.createElement(FieldIcon, { type: field.type })),
|
|
1944
|
+
/* @__PURE__ */ import_react11.default.createElement("div", { className: "bb-field-card__body" }, /* @__PURE__ */ import_react11.default.createElement("div", { className: "bb-field-card__name" }, field.name || /* @__PURE__ */ import_react11.default.createElement("span", { className: "bb-field-card__name--empty" }, "unnamed")), /* @__PURE__ */ import_react11.default.createElement("div", { className: "bb-field-card__type" }, field.type, field.required && /* @__PURE__ */ import_react11.default.createElement("span", { className: "bb-field-card__required" }, "*"))),
|
|
1945
|
+
/* @__PURE__ */ import_react11.default.createElement("span", { className: "bb-field-card__index" }, "#", index + 1),
|
|
1946
|
+
onDrillDown && /* @__PURE__ */ import_react11.default.createElement(
|
|
1947
|
+
"button",
|
|
1948
|
+
{
|
|
1949
|
+
type: "button",
|
|
1950
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
1951
|
+
onClick: (e) => {
|
|
1952
|
+
e.stopPropagation();
|
|
1953
|
+
onDrillDown();
|
|
1954
|
+
},
|
|
1955
|
+
className: "bb-field-card__drill",
|
|
1956
|
+
title: "Edit Inner Fields",
|
|
1957
|
+
style: {
|
|
1958
|
+
marginLeft: "auto",
|
|
1959
|
+
fontSize: 12,
|
|
1960
|
+
padding: "2px 8px",
|
|
1961
|
+
borderRadius: 4,
|
|
1962
|
+
border: "1px solid var(--theme-border)",
|
|
1963
|
+
background: "var(--theme-elevation-100)",
|
|
1964
|
+
cursor: "pointer"
|
|
1965
|
+
}
|
|
1966
|
+
},
|
|
1967
|
+
"Edit Fields"
|
|
1968
|
+
),
|
|
1969
|
+
!isReadOnly && /* @__PURE__ */ import_react11.default.createElement(
|
|
1786
1970
|
"button",
|
|
1787
1971
|
{
|
|
1788
1972
|
type: "button",
|
|
@@ -1794,21 +1978,46 @@ function SortableFieldCard({ field, blockId, index }) {
|
|
|
1794
1978
|
className: "bb-field-card__delete",
|
|
1795
1979
|
title: "Remove field"
|
|
1796
1980
|
},
|
|
1797
|
-
/* @__PURE__ */
|
|
1981
|
+
/* @__PURE__ */ import_react11.default.createElement(import_lucide_react3.X, { size: 12, strokeWidth: 2 })
|
|
1798
1982
|
)
|
|
1799
1983
|
)
|
|
1800
1984
|
);
|
|
1801
|
-
}
|
|
1985
|
+
});
|
|
1802
1986
|
function FieldIcon({ type }) {
|
|
1803
1987
|
const Icon = ICON_MAP[type];
|
|
1804
|
-
return Icon ? /* @__PURE__ */
|
|
1988
|
+
return Icon ? /* @__PURE__ */ import_react11.default.createElement(Icon, { size: 13, strokeWidth: 1.75 }) : null;
|
|
1805
1989
|
}
|
|
1806
1990
|
|
|
1807
1991
|
// src/block-builder/components/canvas/BuilderCanvas.tsx
|
|
1992
|
+
function resolvePathLabels(block, parentPath) {
|
|
1993
|
+
const labels = [];
|
|
1994
|
+
let currentFields = block.fields;
|
|
1995
|
+
for (const segment of parentPath) {
|
|
1996
|
+
const sepIndex = segment.indexOf("::");
|
|
1997
|
+
if (sepIndex !== -1) {
|
|
1998
|
+
const fieldId = segment.slice(0, sepIndex);
|
|
1999
|
+
const tabIndex = Number(segment.slice(sepIndex + 2));
|
|
2000
|
+
const tabsField = currentFields?.find((f) => f.id === fieldId);
|
|
2001
|
+
const tab = tabsField?.tabs?.[tabIndex];
|
|
2002
|
+
labels.push(tab?.label || tabsField?.name || "Tab");
|
|
2003
|
+
currentFields = tab?.fields;
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
const field = currentFields?.find((f) => f.id === segment);
|
|
2007
|
+
labels.push(field?.name || field?.label || "Nested Field");
|
|
2008
|
+
currentFields = field?.fields;
|
|
2009
|
+
}
|
|
2010
|
+
return labels;
|
|
2011
|
+
}
|
|
1808
2012
|
function BuilderCanvas() {
|
|
1809
2013
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
1810
2014
|
const block = useBuilderStore((s) => s.blocks.find((b) => b.id === activeBlockId));
|
|
1811
2015
|
const reorderFields = useBuilderStore((s) => s.reorderFields);
|
|
2016
|
+
const activeFieldId = useBuilderStore((s) => s.activeFieldId);
|
|
2017
|
+
const activeParentPath = useBuilderStore((s) => s.activeParentPath);
|
|
2018
|
+
const truncateParentPath = useBuilderStore((s) => s.truncateParentPath);
|
|
2019
|
+
const resetParentPath = useBuilderStore((s) => s.resetParentPath);
|
|
2020
|
+
const pushParentPath = useBuilderStore((s) => s.pushParentPath);
|
|
1812
2021
|
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
1813
2022
|
const sensors = (0, import_core.useSensors)(
|
|
1814
2023
|
(0, import_core.useSensor)(import_core.PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
@@ -1818,16 +2027,41 @@ function BuilderCanvas() {
|
|
|
1818
2027
|
if (isReadOnly) return;
|
|
1819
2028
|
const { active, over } = event;
|
|
1820
2029
|
if (!over || active.id === over.id || !block) return;
|
|
1821
|
-
const
|
|
1822
|
-
|
|
2030
|
+
const targetFields2 = getTargetFields(block, activeParentPath);
|
|
2031
|
+
if (!targetFields2) return;
|
|
2032
|
+
const fromIndex = targetFields2.findIndex((f) => f.id === active.id);
|
|
2033
|
+
const toIndex = targetFields2.findIndex((f) => f.id === over.id);
|
|
1823
2034
|
if (fromIndex !== -1 && toIndex !== -1) {
|
|
1824
2035
|
reorderFields(block.id, fromIndex, toIndex);
|
|
1825
2036
|
}
|
|
1826
2037
|
}
|
|
1827
2038
|
if (!block) {
|
|
1828
|
-
return /* @__PURE__ */
|
|
2039
|
+
return /* @__PURE__ */ import_react12.default.createElement("div", { className: "bb-canvas", style: { display: "flex", alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ import_react12.default.createElement("span", { className: "bb-canvas__no-block" }, "Select or create a block from the left panel."));
|
|
1829
2040
|
}
|
|
1830
|
-
|
|
2041
|
+
const targetFields = getTargetFields(block, activeParentPath) || [];
|
|
2042
|
+
const pathLabels = resolvePathLabels(block, activeParentPath);
|
|
2043
|
+
return /* @__PURE__ */ import_react12.default.createElement("div", { className: `bb-canvas${isReadOnly ? " bb-canvas--readonly" : ""}` }, /* @__PURE__ */ import_react12.default.createElement("div", { className: "bb-canvas__inner" }, /* @__PURE__ */ import_react12.default.createElement("div", { className: "bb-canvas__header" }, activeParentPath.length === 0 ? /* @__PURE__ */ import_react12.default.createElement(import_react12.default.Fragment, null, block.slug, " - ", targetFields.length, " field", targetFields.length !== 1 ? "s" : "") : /* @__PURE__ */ import_react12.default.createElement("nav", { className: "bb-breadcrumb", "aria-label": "Field path" }, /* @__PURE__ */ import_react12.default.createElement(
|
|
2044
|
+
"button",
|
|
2045
|
+
{
|
|
2046
|
+
type: "button",
|
|
2047
|
+
onClick: () => resetParentPath(),
|
|
2048
|
+
className: "bb-breadcrumb__item"
|
|
2049
|
+
},
|
|
2050
|
+
block.slug
|
|
2051
|
+
), /* @__PURE__ */ import_react12.default.createElement("span", { className: "bb-breadcrumb__sep" }, "/"), activeParentPath.map((segment, index) => {
|
|
2052
|
+
const isLast = index === activeParentPath.length - 1;
|
|
2053
|
+
return /* @__PURE__ */ import_react12.default.createElement(import_react12.default.Fragment, { key: segment }, /* @__PURE__ */ import_react12.default.createElement(
|
|
2054
|
+
"button",
|
|
2055
|
+
{
|
|
2056
|
+
type: "button",
|
|
2057
|
+
onClick: isLast ? void 0 : () => truncateParentPath(index + 1),
|
|
2058
|
+
className: "bb-breadcrumb__item",
|
|
2059
|
+
"aria-current": isLast ? "location" : void 0,
|
|
2060
|
+
"data-current": isLast ? "true" : void 0
|
|
2061
|
+
},
|
|
2062
|
+
pathLabels[index]
|
|
2063
|
+
), !isLast && /* @__PURE__ */ import_react12.default.createElement("span", { className: "bb-breadcrumb__sep" }, "/"));
|
|
2064
|
+
}))), targetFields.length === 0 ? /* @__PURE__ */ import_react12.default.createElement("div", { className: "bb-canvas__empty" }, "Add fields from the palette on the left") : /* @__PURE__ */ import_react12.default.createElement(
|
|
1831
2065
|
import_core.DndContext,
|
|
1832
2066
|
{
|
|
1833
2067
|
sensors,
|
|
@@ -1835,19 +2069,22 @@ function BuilderCanvas() {
|
|
|
1835
2069
|
modifiers: [import_modifiers.restrictToVerticalAxis, import_modifiers.restrictToParentElement],
|
|
1836
2070
|
onDragEnd: handleDragEnd
|
|
1837
2071
|
},
|
|
1838
|
-
/* @__PURE__ */
|
|
2072
|
+
/* @__PURE__ */ import_react12.default.createElement(
|
|
1839
2073
|
import_sortable2.SortableContext,
|
|
1840
2074
|
{
|
|
1841
|
-
items:
|
|
2075
|
+
items: targetFields.map((f) => f.id),
|
|
1842
2076
|
strategy: import_sortable2.verticalListSortingStrategy
|
|
1843
2077
|
},
|
|
1844
|
-
/* @__PURE__ */
|
|
2078
|
+
/* @__PURE__ */ import_react12.default.createElement("div", { className: "bb-canvas__field-list" }, targetFields.map((field, i) => /* @__PURE__ */ import_react12.default.createElement(
|
|
1845
2079
|
SortableFieldCard,
|
|
1846
2080
|
{
|
|
1847
2081
|
key: field.id,
|
|
1848
2082
|
field,
|
|
1849
2083
|
blockId: block.id,
|
|
1850
|
-
index: i
|
|
2084
|
+
index: i,
|
|
2085
|
+
isActive: activeFieldId === field.id,
|
|
2086
|
+
isReadOnly,
|
|
2087
|
+
onDrillDown: ["group", "row", "array", "collapsible"].includes(field.type) ? () => pushParentPath(field.id) : void 0
|
|
1851
2088
|
}
|
|
1852
2089
|
)))
|
|
1853
2090
|
)
|
|
@@ -1855,18 +2092,18 @@ function BuilderCanvas() {
|
|
|
1855
2092
|
}
|
|
1856
2093
|
|
|
1857
2094
|
// src/block-builder/components/canvas/ConfigPanel.tsx
|
|
1858
|
-
var
|
|
2095
|
+
var import_react15 = __toESM(require("react"), 1);
|
|
1859
2096
|
|
|
1860
2097
|
// src/block-builder/components/config/BlockConfig.tsx
|
|
1861
|
-
var
|
|
2098
|
+
var import_react13 = __toESM(require("react"), 1);
|
|
1862
2099
|
function BlockConfig() {
|
|
1863
2100
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
1864
2101
|
const block = useBuilderStore((s) => s.blocks.find((b) => b.id === activeBlockId));
|
|
1865
2102
|
const updateBlock = useBuilderStore((s) => s.updateBlock);
|
|
1866
2103
|
if (!block) {
|
|
1867
|
-
return /* @__PURE__ */
|
|
2104
|
+
return /* @__PURE__ */ import_react13.default.createElement("div", { className: "bb-form__empty" }, "No block selected.");
|
|
1868
2105
|
}
|
|
1869
|
-
return /* @__PURE__ */
|
|
2106
|
+
return /* @__PURE__ */ import_react13.default.createElement("div", { className: "bb-form" }, /* @__PURE__ */ import_react13.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react13.default.createElement("label", { className: "bb-form__label" }, "Slug *"), /* @__PURE__ */ import_react13.default.createElement(
|
|
1870
2107
|
"input",
|
|
1871
2108
|
{
|
|
1872
2109
|
type: "text",
|
|
@@ -1875,7 +2112,7 @@ function BlockConfig() {
|
|
|
1875
2112
|
placeholder: "myBlock",
|
|
1876
2113
|
className: "bb-input"
|
|
1877
2114
|
}
|
|
1878
|
-
), /* @__PURE__ */
|
|
2115
|
+
), /* @__PURE__ */ import_react13.default.createElement("span", { className: "bb-form__hint" }, "Unique identifier used in code and database")), /* @__PURE__ */ import_react13.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react13.default.createElement("label", { className: "bb-form__label" }, "Interface Name"), /* @__PURE__ */ import_react13.default.createElement(
|
|
1879
2116
|
"input",
|
|
1880
2117
|
{
|
|
1881
2118
|
type: "text",
|
|
@@ -1884,7 +2121,7 @@ function BlockConfig() {
|
|
|
1884
2121
|
placeholder: "MyBlock",
|
|
1885
2122
|
className: "bb-input"
|
|
1886
2123
|
}
|
|
1887
|
-
)), /* @__PURE__ */
|
|
2124
|
+
)), /* @__PURE__ */ import_react13.default.createElement("div", { className: "bb-grid-2" }, /* @__PURE__ */ import_react13.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react13.default.createElement("label", { className: "bb-form__label" }, "Singular Label"), /* @__PURE__ */ import_react13.default.createElement(
|
|
1888
2125
|
"input",
|
|
1889
2126
|
{
|
|
1890
2127
|
type: "text",
|
|
@@ -1893,7 +2130,7 @@ function BlockConfig() {
|
|
|
1893
2130
|
placeholder: "My Block",
|
|
1894
2131
|
className: "bb-input"
|
|
1895
2132
|
}
|
|
1896
|
-
)), /* @__PURE__ */
|
|
2133
|
+
)), /* @__PURE__ */ import_react13.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react13.default.createElement("label", { className: "bb-form__label" }, "Plural Label"), /* @__PURE__ */ import_react13.default.createElement(
|
|
1897
2134
|
"input",
|
|
1898
2135
|
{
|
|
1899
2136
|
type: "text",
|
|
@@ -1902,7 +2139,7 @@ function BlockConfig() {
|
|
|
1902
2139
|
placeholder: "My Blocks",
|
|
1903
2140
|
className: "bb-input"
|
|
1904
2141
|
}
|
|
1905
|
-
))), /* @__PURE__ */
|
|
2142
|
+
))), /* @__PURE__ */ import_react13.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react13.default.createElement("label", { className: "bb-form__label" }, "Image URL"), /* @__PURE__ */ import_react13.default.createElement(
|
|
1906
2143
|
"input",
|
|
1907
2144
|
{
|
|
1908
2145
|
type: "text",
|
|
@@ -1911,38 +2148,62 @@ function BlockConfig() {
|
|
|
1911
2148
|
placeholder: "https://...",
|
|
1912
2149
|
className: "bb-input"
|
|
1913
2150
|
}
|
|
1914
|
-
)), /* @__PURE__ */
|
|
2151
|
+
)), /* @__PURE__ */ import_react13.default.createElement("div", { className: "bb-stat-box" }, /* @__PURE__ */ import_react13.default.createElement("strong", null, block.fields.length), " field", block.fields.length !== 1 ? "s" : "", " defined"));
|
|
1915
2152
|
}
|
|
1916
2153
|
|
|
1917
2154
|
// src/block-builder/components/config/FieldConfig.tsx
|
|
1918
|
-
var
|
|
2155
|
+
var import_react14 = __toESM(require("react"), 1);
|
|
1919
2156
|
var ALL_TYPES = [
|
|
1920
2157
|
"text",
|
|
1921
2158
|
"textarea",
|
|
2159
|
+
"richtext",
|
|
1922
2160
|
"number",
|
|
1923
2161
|
"email",
|
|
2162
|
+
"url",
|
|
2163
|
+
"color",
|
|
1924
2164
|
"date",
|
|
1925
2165
|
"checkbox",
|
|
1926
2166
|
"select",
|
|
1927
|
-
"
|
|
1928
|
-
"
|
|
2167
|
+
"multiselect",
|
|
2168
|
+
"image",
|
|
2169
|
+
"file",
|
|
1929
2170
|
"relationship",
|
|
1930
|
-
"json"
|
|
2171
|
+
"json",
|
|
2172
|
+
"array",
|
|
2173
|
+
"group",
|
|
2174
|
+
"blocks",
|
|
2175
|
+
"row",
|
|
2176
|
+
"tabs",
|
|
2177
|
+
"collapsible"
|
|
1931
2178
|
];
|
|
2179
|
+
var NO_DEFAULT_VALUE_TYPES = /* @__PURE__ */ new Set([
|
|
2180
|
+
"array",
|
|
2181
|
+
"group",
|
|
2182
|
+
"blocks",
|
|
2183
|
+
"row",
|
|
2184
|
+
"tabs",
|
|
2185
|
+
"collapsible",
|
|
2186
|
+
"image",
|
|
2187
|
+
"file",
|
|
2188
|
+
"relationship",
|
|
2189
|
+
"json"
|
|
2190
|
+
]);
|
|
1932
2191
|
function FieldConfig() {
|
|
1933
2192
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
1934
2193
|
const activeFieldId = useBuilderStore((s) => s.activeFieldId);
|
|
2194
|
+
const activeParentPath = useBuilderStore((s) => s.activeParentPath);
|
|
1935
2195
|
const block = useBuilderStore((s) => s.blocks.find((b) => b.id === activeBlockId));
|
|
1936
|
-
const field = block?.
|
|
2196
|
+
const field = block ? getTargetFields(block, activeParentPath)?.find((f) => f.id === activeFieldId) : void 0;
|
|
1937
2197
|
const updateField = useBuilderStore((s) => s.updateField);
|
|
2198
|
+
const pushParentPath = useBuilderStore((s) => s.pushParentPath);
|
|
1938
2199
|
if (!activeBlockId || !activeFieldId || !field) {
|
|
1939
|
-
return /* @__PURE__ */
|
|
2200
|
+
return /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-form__empty" }, "Select a field to configure it.");
|
|
1940
2201
|
}
|
|
1941
2202
|
function upd(updates) {
|
|
1942
2203
|
updateField(activeBlockId, activeFieldId, updates);
|
|
1943
2204
|
}
|
|
1944
|
-
const needsOptions = field.type === "select" || field.type === "
|
|
1945
|
-
return /* @__PURE__ */
|
|
2205
|
+
const needsOptions = field.type === "select" || field.type === "multiselect";
|
|
2206
|
+
return /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-form" }, /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-grid-2" }, /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react14.default.createElement("label", { className: "bb-form__label" }, "Field Name *"), /* @__PURE__ */ import_react14.default.createElement(
|
|
1946
2207
|
"input",
|
|
1947
2208
|
{
|
|
1948
2209
|
type: "text",
|
|
@@ -1951,15 +2212,15 @@ function FieldConfig() {
|
|
|
1951
2212
|
placeholder: "fieldName",
|
|
1952
2213
|
className: "bb-input"
|
|
1953
2214
|
}
|
|
1954
|
-
)), /* @__PURE__ */
|
|
2215
|
+
)), /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react14.default.createElement("label", { className: "bb-form__label" }, "Type *"), /* @__PURE__ */ import_react14.default.createElement(
|
|
1955
2216
|
"select",
|
|
1956
2217
|
{
|
|
1957
2218
|
value: field.type,
|
|
1958
2219
|
onChange: (e) => upd({ type: e.target.value }),
|
|
1959
2220
|
className: "bb-input bb-select"
|
|
1960
2221
|
},
|
|
1961
|
-
ALL_TYPES.map((t) => /* @__PURE__ */
|
|
1962
|
-
))), /* @__PURE__ */
|
|
2222
|
+
ALL_TYPES.map((t) => /* @__PURE__ */ import_react14.default.createElement("option", { key: t, value: t }, t))
|
|
2223
|
+
))), /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react14.default.createElement("label", { className: "bb-form__label" }, "Label"), /* @__PURE__ */ import_react14.default.createElement(
|
|
1963
2224
|
"input",
|
|
1964
2225
|
{
|
|
1965
2226
|
type: "text",
|
|
@@ -1968,28 +2229,44 @@ function FieldConfig() {
|
|
|
1968
2229
|
placeholder: "Human-readable label",
|
|
1969
2230
|
className: "bb-input"
|
|
1970
2231
|
}
|
|
1971
|
-
)), /* @__PURE__ */
|
|
2232
|
+
)), /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-flags-row" }, /* @__PURE__ */ import_react14.default.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ import_react14.default.createElement(
|
|
1972
2233
|
"input",
|
|
1973
2234
|
{
|
|
1974
2235
|
type: "checkbox",
|
|
1975
2236
|
checked: field.required ?? false,
|
|
1976
2237
|
onChange: (e) => upd({ required: e.target.checked })
|
|
1977
2238
|
}
|
|
1978
|
-
), "Required"), /* @__PURE__ */
|
|
2239
|
+
), "Required"), /* @__PURE__ */ import_react14.default.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ import_react14.default.createElement(
|
|
1979
2240
|
"input",
|
|
1980
2241
|
{
|
|
1981
2242
|
type: "checkbox",
|
|
1982
2243
|
checked: field.unique ?? false,
|
|
1983
2244
|
onChange: (e) => upd({ unique: e.target.checked })
|
|
1984
2245
|
}
|
|
1985
|
-
), "Unique"), /* @__PURE__ */
|
|
2246
|
+
), "Unique"), /* @__PURE__ */ import_react14.default.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ import_react14.default.createElement(
|
|
1986
2247
|
"input",
|
|
1987
2248
|
{
|
|
1988
2249
|
type: "checkbox",
|
|
1989
2250
|
checked: field.localized ?? false,
|
|
1990
2251
|
onChange: (e) => upd({ localized: e.target.checked })
|
|
1991
2252
|
}
|
|
1992
|
-
), "Localized")), /* @__PURE__ */
|
|
2253
|
+
), "Localized")), !NO_DEFAULT_VALUE_TYPES.has(field.type) && /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react14.default.createElement("label", { className: "bb-form__label" }, "Default Value"), field.type === "checkbox" ? /* @__PURE__ */ import_react14.default.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ import_react14.default.createElement(
|
|
2254
|
+
"input",
|
|
2255
|
+
{
|
|
2256
|
+
type: "checkbox",
|
|
2257
|
+
checked: field.defaultValue === true,
|
|
2258
|
+
onChange: (e) => upd({ defaultValue: e.target.checked })
|
|
2259
|
+
}
|
|
2260
|
+
), "Checked by default") : field.type === "number" ? /* @__PURE__ */ import_react14.default.createElement(
|
|
2261
|
+
"input",
|
|
2262
|
+
{
|
|
2263
|
+
type: "number",
|
|
2264
|
+
value: typeof field.defaultValue === "number" ? field.defaultValue : "",
|
|
2265
|
+
onChange: (e) => upd({ defaultValue: e.target.value === "" ? void 0 : e.target.valueAsNumber }),
|
|
2266
|
+
placeholder: "0",
|
|
2267
|
+
className: "bb-input"
|
|
2268
|
+
}
|
|
2269
|
+
) : /* @__PURE__ */ import_react14.default.createElement(
|
|
1993
2270
|
"input",
|
|
1994
2271
|
{
|
|
1995
2272
|
type: "text",
|
|
@@ -1998,23 +2275,23 @@ function FieldConfig() {
|
|
|
1998
2275
|
placeholder: "Default value",
|
|
1999
2276
|
className: "bb-input"
|
|
2000
2277
|
}
|
|
2001
|
-
)), field.type === "relationship" && /* @__PURE__ */
|
|
2278
|
+
)), field.type === "relationship" && /* @__PURE__ */ import_react14.default.createElement(import_react14.default.Fragment, null, /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react14.default.createElement("label", { className: "bb-form__label" }, "Relation To (collection slug)"), /* @__PURE__ */ import_react14.default.createElement(
|
|
2002
2279
|
"input",
|
|
2003
2280
|
{
|
|
2004
2281
|
type: "text",
|
|
2005
|
-
value: field.
|
|
2006
|
-
onChange: (e) => upd({
|
|
2282
|
+
value: field.collection ?? "",
|
|
2283
|
+
onChange: (e) => upd({ collection: e.target.value }),
|
|
2007
2284
|
placeholder: "pages",
|
|
2008
2285
|
className: "bb-input"
|
|
2009
2286
|
}
|
|
2010
|
-
)), /* @__PURE__ */
|
|
2287
|
+
)), /* @__PURE__ */ import_react14.default.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ import_react14.default.createElement(
|
|
2011
2288
|
"input",
|
|
2012
2289
|
{
|
|
2013
2290
|
type: "checkbox",
|
|
2014
2291
|
checked: field.hasMany ?? false,
|
|
2015
2292
|
onChange: (e) => upd({ hasMany: e.target.checked })
|
|
2016
2293
|
}
|
|
2017
|
-
), "Has Many")), field.type === "array" && /* @__PURE__ */
|
|
2294
|
+
), "Has Many")), field.type === "array" && /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-grid-2" }, /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react14.default.createElement("label", { className: "bb-form__label" }, "Min Rows"), /* @__PURE__ */ import_react14.default.createElement(
|
|
2018
2295
|
"input",
|
|
2019
2296
|
{
|
|
2020
2297
|
type: "number",
|
|
@@ -2022,7 +2299,7 @@ function FieldConfig() {
|
|
|
2022
2299
|
onChange: (e) => upd({ minRows: e.target.value === "" ? void 0 : e.target.valueAsNumber }),
|
|
2023
2300
|
className: "bb-input"
|
|
2024
2301
|
}
|
|
2025
|
-
)), /* @__PURE__ */
|
|
2302
|
+
)), /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react14.default.createElement("label", { className: "bb-form__label" }, "Max Rows"), /* @__PURE__ */ import_react14.default.createElement(
|
|
2026
2303
|
"input",
|
|
2027
2304
|
{
|
|
2028
2305
|
type: "number",
|
|
@@ -2030,7 +2307,54 @@ function FieldConfig() {
|
|
|
2030
2307
|
onChange: (e) => upd({ maxRows: e.target.value === "" ? void 0 : e.target.valueAsNumber }),
|
|
2031
2308
|
className: "bb-input"
|
|
2032
2309
|
}
|
|
2033
|
-
))),
|
|
2310
|
+
))), field.type === "tabs" && /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-options-label" }, "Tabs"), (field.tabs ?? []).map((tab, i) => /* @__PURE__ */ import_react14.default.createElement("div", { key: tab.id ?? i, className: "bb-option-row" }, /* @__PURE__ */ import_react14.default.createElement(
|
|
2311
|
+
"input",
|
|
2312
|
+
{
|
|
2313
|
+
type: "text",
|
|
2314
|
+
value: tab.label,
|
|
2315
|
+
placeholder: "Tab label",
|
|
2316
|
+
onChange: (e) => {
|
|
2317
|
+
const next = [...field.tabs ?? []];
|
|
2318
|
+
next[i] = { ...next[i], label: e.target.value };
|
|
2319
|
+
upd({ tabs: next });
|
|
2320
|
+
},
|
|
2321
|
+
className: "bb-input"
|
|
2322
|
+
}
|
|
2323
|
+
), /* @__PURE__ */ import_react14.default.createElement(
|
|
2324
|
+
"button",
|
|
2325
|
+
{
|
|
2326
|
+
type: "button",
|
|
2327
|
+
onClick: () => pushParentPath(encodeTabPath(field.id, i)),
|
|
2328
|
+
className: "bb-add-option",
|
|
2329
|
+
title: "Edit this tab's fields"
|
|
2330
|
+
},
|
|
2331
|
+
"Edit Fields"
|
|
2332
|
+
), /* @__PURE__ */ import_react14.default.createElement(
|
|
2333
|
+
"button",
|
|
2334
|
+
{
|
|
2335
|
+
type: "button",
|
|
2336
|
+
onClick: () => {
|
|
2337
|
+
const next = (field.tabs ?? []).filter((_, k) => k !== i);
|
|
2338
|
+
upd({ tabs: next });
|
|
2339
|
+
},
|
|
2340
|
+
className: "bb-option-delete",
|
|
2341
|
+
title: "Remove tab"
|
|
2342
|
+
},
|
|
2343
|
+
"x"
|
|
2344
|
+
))), /* @__PURE__ */ import_react14.default.createElement(
|
|
2345
|
+
"button",
|
|
2346
|
+
{
|
|
2347
|
+
type: "button",
|
|
2348
|
+
onClick: () => upd({
|
|
2349
|
+
tabs: [
|
|
2350
|
+
...field.tabs ?? [],
|
|
2351
|
+
{ id: uuidv4(), label: `Tab ${(field.tabs?.length ?? 0) + 1}`, fields: [] }
|
|
2352
|
+
]
|
|
2353
|
+
}),
|
|
2354
|
+
className: "bb-add-option"
|
|
2355
|
+
},
|
|
2356
|
+
"+ Add Tab"
|
|
2357
|
+
)), needsOptions && /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ import_react14.default.createElement("div", { className: "bb-options-label" }, "Options"), (field.options ?? []).map((opt, i) => /* @__PURE__ */ import_react14.default.createElement("div", { key: i, className: "bb-option-row" }, /* @__PURE__ */ import_react14.default.createElement(
|
|
2034
2358
|
"input",
|
|
2035
2359
|
{
|
|
2036
2360
|
type: "text",
|
|
@@ -2043,7 +2367,7 @@ function FieldConfig() {
|
|
|
2043
2367
|
},
|
|
2044
2368
|
className: "bb-input"
|
|
2045
2369
|
}
|
|
2046
|
-
), /* @__PURE__ */
|
|
2370
|
+
), /* @__PURE__ */ import_react14.default.createElement(
|
|
2047
2371
|
"input",
|
|
2048
2372
|
{
|
|
2049
2373
|
type: "text",
|
|
@@ -2056,7 +2380,7 @@ function FieldConfig() {
|
|
|
2056
2380
|
},
|
|
2057
2381
|
className: "bb-input"
|
|
2058
2382
|
}
|
|
2059
|
-
), /* @__PURE__ */
|
|
2383
|
+
), /* @__PURE__ */ import_react14.default.createElement(
|
|
2060
2384
|
"button",
|
|
2061
2385
|
{
|
|
2062
2386
|
type: "button",
|
|
@@ -2068,7 +2392,7 @@ function FieldConfig() {
|
|
|
2068
2392
|
title: "Remove option"
|
|
2069
2393
|
},
|
|
2070
2394
|
"x"
|
|
2071
|
-
))), /* @__PURE__ */
|
|
2395
|
+
))), /* @__PURE__ */ import_react14.default.createElement(
|
|
2072
2396
|
"button",
|
|
2073
2397
|
{
|
|
2074
2398
|
type: "button",
|
|
@@ -2081,11 +2405,11 @@ function FieldConfig() {
|
|
|
2081
2405
|
|
|
2082
2406
|
// src/block-builder/components/canvas/ConfigPanel.tsx
|
|
2083
2407
|
function ConfigPanel() {
|
|
2084
|
-
const [tab, setTab] = (0,
|
|
2408
|
+
const [tab, setTab] = (0, import_react15.useState)("block");
|
|
2085
2409
|
const activeFieldId = useBuilderStore((s) => s.activeFieldId);
|
|
2086
2410
|
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
2087
2411
|
const activeTab = activeFieldId ? "field" : tab;
|
|
2088
|
-
return /* @__PURE__ */
|
|
2412
|
+
return /* @__PURE__ */ import_react15.default.createElement("div", { className: `bb-config${isReadOnly ? " bb-config--readonly" : ""}` }, /* @__PURE__ */ import_react15.default.createElement("div", { className: "bb-config__tabs" }, ["block", "field"].map((t) => /* @__PURE__ */ import_react15.default.createElement(
|
|
2089
2413
|
"button",
|
|
2090
2414
|
{
|
|
2091
2415
|
key: t,
|
|
@@ -2094,11 +2418,11 @@ function ConfigPanel() {
|
|
|
2094
2418
|
className: `bb-config__tab${activeTab === t ? " bb-config__tab--active" : ""}`
|
|
2095
2419
|
},
|
|
2096
2420
|
t
|
|
2097
|
-
))), /* @__PURE__ */
|
|
2421
|
+
))), /* @__PURE__ */ import_react15.default.createElement("div", { className: "bb-config__body" }, activeTab === "block" ? /* @__PURE__ */ import_react15.default.createElement(BlockConfig, null) : /* @__PURE__ */ import_react15.default.createElement(FieldConfig, null)), isReadOnly && /* @__PURE__ */ import_react15.default.createElement("div", { className: "bb-config__readonly-overlay" }, /* @__PURE__ */ import_react15.default.createElement("span", { className: "bb-config__readonly-label" }, "Read only")));
|
|
2098
2422
|
}
|
|
2099
2423
|
|
|
2100
2424
|
// src/block-builder/components/sidebar/FieldPalette.tsx
|
|
2101
|
-
var
|
|
2425
|
+
var import_react16 = __toESM(require("react"), 1);
|
|
2102
2426
|
var import_lucide_react4 = require("lucide-react");
|
|
2103
2427
|
|
|
2104
2428
|
// src/block-builder/lib/field-palette.ts
|
|
@@ -2112,11 +2436,17 @@ var FIELD_PALETTE = [
|
|
|
2112
2436
|
{ type: "checkbox", label: "Checkbox", description: "Boolean toggle", icon: "CheckSquare", category: "basic", color: "bg-blue-500/20 text-blue-400 border-blue-500/30" },
|
|
2113
2437
|
// Choice
|
|
2114
2438
|
{ type: "select", label: "Select", description: "Dropdown selection", icon: "ChevronDown", category: "choice", color: "bg-purple-500/20 text-purple-400 border-purple-500/30" },
|
|
2115
|
-
{ type: "radio", label: "Radio", description: "Radio button group", icon: "Circle", category: "choice", color: "bg-purple-500/20 text-purple-400 border-purple-500/30" },
|
|
2116
2439
|
// Media
|
|
2117
|
-
{ type: "
|
|
2440
|
+
{ type: "image", label: "Image", description: "Image upload", icon: "Image", category: "media", color: "bg-green-500/20 text-green-400 border-green-500/30" },
|
|
2441
|
+
{ type: "file", label: "File", description: "File upload", icon: "Upload", category: "media", color: "bg-green-500/20 text-green-400 border-green-500/30" },
|
|
2118
2442
|
// Relational
|
|
2119
2443
|
{ type: "relationship", label: "Relationship", description: "Link to another collection", icon: "Link", category: "relational", color: "bg-orange-500/20 text-orange-400 border-orange-500/30" },
|
|
2444
|
+
// Layout
|
|
2445
|
+
{ type: "array", label: "Array", description: "Repeating list of fields", icon: "List", category: "layout", color: "bg-pink-500/20 text-pink-400 border-pink-500/30" },
|
|
2446
|
+
{ type: "group", label: "Group", description: "Grouped fields object", icon: "Box", category: "layout", color: "bg-pink-500/20 text-pink-400 border-pink-500/30" },
|
|
2447
|
+
{ type: "row", label: "Row", description: "Horizontal layout row", icon: "Columns", category: "layout", color: "bg-pink-500/20 text-pink-400 border-pink-500/30" },
|
|
2448
|
+
{ type: "tabs", label: "Tabs", description: "Tabbed layout container", icon: "Folder", category: "layout", color: "bg-pink-500/20 text-pink-400 border-pink-500/30" },
|
|
2449
|
+
{ type: "collapsible", label: "Collapsible", description: "Expandable field group", icon: "ChevronDown", category: "layout", color: "bg-pink-500/20 text-pink-400 border-pink-500/30" },
|
|
2120
2450
|
// Advanced
|
|
2121
2451
|
{ type: "json", label: "JSON", description: "Raw JSON data field", icon: "Braces", category: "advanced", color: "bg-red-500/20 text-red-400 border-red-500/30" }
|
|
2122
2452
|
];
|
|
@@ -2125,6 +2455,7 @@ var FIELD_CATEGORIES = [
|
|
|
2125
2455
|
{ id: "choice", label: "Choice" },
|
|
2126
2456
|
{ id: "media", label: "Media" },
|
|
2127
2457
|
{ id: "relational", label: "Relational" },
|
|
2458
|
+
{ id: "layout", label: "Layout" },
|
|
2128
2459
|
{ id: "advanced", label: "Advanced" }
|
|
2129
2460
|
];
|
|
2130
2461
|
function getFieldMeta(type) {
|
|
@@ -2146,10 +2477,13 @@ var ICON_MAP2 = {
|
|
|
2146
2477
|
Link: import_lucide_react4.Link,
|
|
2147
2478
|
List: import_lucide_react4.List,
|
|
2148
2479
|
Folder: import_lucide_react4.Folder,
|
|
2149
|
-
Braces: import_lucide_react4.Braces
|
|
2480
|
+
Braces: import_lucide_react4.Braces,
|
|
2481
|
+
Image: import_lucide_react4.Image,
|
|
2482
|
+
Box: import_lucide_react4.Box,
|
|
2483
|
+
Columns: import_lucide_react4.Columns
|
|
2150
2484
|
};
|
|
2151
2485
|
function FieldPalette() {
|
|
2152
|
-
const [search, setSearch] = (0,
|
|
2486
|
+
const [search, setSearch] = (0, import_react16.useState)("");
|
|
2153
2487
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
2154
2488
|
const addField = useBuilderStore((s) => s.addField);
|
|
2155
2489
|
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
@@ -2160,7 +2494,7 @@ function FieldPalette() {
|
|
|
2160
2494
|
if (!activeBlockId || isReadOnly) return;
|
|
2161
2495
|
addField(activeBlockId, type);
|
|
2162
2496
|
}
|
|
2163
|
-
return /* @__PURE__ */
|
|
2497
|
+
return /* @__PURE__ */ import_react16.default.createElement("div", { className: `bb-sidebar bb-sidebar--200 bb-sidebar--palette${isReadOnly ? " bb-sidebar--readonly" : ""}` }, /* @__PURE__ */ import_react16.default.createElement("div", { className: "bb-sidebar__header" }, /* @__PURE__ */ import_react16.default.createElement("span", { className: "bb-sidebar__title" }, "Fields")), /* @__PURE__ */ import_react16.default.createElement("div", { className: "bb-palette__search-wrap" }, /* @__PURE__ */ import_react16.default.createElement(
|
|
2164
2498
|
"input",
|
|
2165
2499
|
{
|
|
2166
2500
|
type: "text",
|
|
@@ -2169,7 +2503,7 @@ function FieldPalette() {
|
|
|
2169
2503
|
placeholder: "Search...",
|
|
2170
2504
|
className: "bb-palette__search"
|
|
2171
2505
|
}
|
|
2172
|
-
)), /* @__PURE__ */
|
|
2506
|
+
)), /* @__PURE__ */ import_react16.default.createElement("div", { className: "bb-sidebar__body" }, search.trim() ? /* @__PURE__ */ import_react16.default.createElement("div", { className: "bb-palette__items" }, filtered.map((item) => /* @__PURE__ */ import_react16.default.createElement(
|
|
2173
2507
|
FieldButton,
|
|
2174
2508
|
{
|
|
2175
2509
|
key: item.type,
|
|
@@ -2181,7 +2515,7 @@ function FieldPalette() {
|
|
|
2181
2515
|
}
|
|
2182
2516
|
))) : FIELD_CATEGORIES.map((cat) => {
|
|
2183
2517
|
const items = FIELD_PALETTE.filter((f) => f.category === cat.id);
|
|
2184
|
-
return /* @__PURE__ */
|
|
2518
|
+
return /* @__PURE__ */ import_react16.default.createElement("div", { key: cat.id, style: { marginTop: 6 } }, /* @__PURE__ */ import_react16.default.createElement("div", { className: "bb-palette__cat-label" }, cat.label), /* @__PURE__ */ import_react16.default.createElement("div", { className: "bb-palette__items" }, items.map((item) => /* @__PURE__ */ import_react16.default.createElement(
|
|
2185
2519
|
FieldButton,
|
|
2186
2520
|
{
|
|
2187
2521
|
key: item.type,
|
|
@@ -2192,7 +2526,7 @@ function FieldPalette() {
|
|
|
2192
2526
|
disabled: !activeBlockId || isReadOnly
|
|
2193
2527
|
}
|
|
2194
2528
|
))));
|
|
2195
|
-
})), !activeBlockId && /* @__PURE__ */
|
|
2529
|
+
})), !activeBlockId && /* @__PURE__ */ import_react16.default.createElement("div", { className: "bb-sidebar__footer" }, "Select a block first"));
|
|
2196
2530
|
}
|
|
2197
2531
|
function FieldButton({
|
|
2198
2532
|
type,
|
|
@@ -2203,7 +2537,7 @@ function FieldButton({
|
|
|
2203
2537
|
}) {
|
|
2204
2538
|
const meta = getFieldMeta(type);
|
|
2205
2539
|
const Icon = ICON_MAP2[icon];
|
|
2206
|
-
return /* @__PURE__ */
|
|
2540
|
+
return /* @__PURE__ */ import_react16.default.createElement(
|
|
2207
2541
|
"button",
|
|
2208
2542
|
{
|
|
2209
2543
|
type: "button",
|
|
@@ -2212,14 +2546,291 @@ function FieldButton({
|
|
|
2212
2546
|
title: meta?.description,
|
|
2213
2547
|
className: "bb-palette__item"
|
|
2214
2548
|
},
|
|
2215
|
-
/* @__PURE__ */
|
|
2216
|
-
/* @__PURE__ */
|
|
2217
|
-
/* @__PURE__ */
|
|
2549
|
+
/* @__PURE__ */ import_react16.default.createElement("span", { className: "bb-palette__item__icon" }, Icon ? /* @__PURE__ */ import_react16.default.createElement(Icon, { size: 13, strokeWidth: 1.75 }) : null),
|
|
2550
|
+
/* @__PURE__ */ import_react16.default.createElement("span", { className: "bb-palette__item__label" }, label),
|
|
2551
|
+
/* @__PURE__ */ import_react16.default.createElement("span", { className: "bb-palette__item__type" }, type)
|
|
2218
2552
|
);
|
|
2219
2553
|
}
|
|
2220
2554
|
|
|
2221
2555
|
// src/block-builder/components/canvas/CodePreview.tsx
|
|
2222
|
-
var
|
|
2556
|
+
var import_react17 = __toESM(require("react"), 1);
|
|
2557
|
+
|
|
2558
|
+
// src/block-builder/lib/codegen.ts
|
|
2559
|
+
function indent(n) {
|
|
2560
|
+
return " ".repeat(n);
|
|
2561
|
+
}
|
|
2562
|
+
function safeStr(s) {
|
|
2563
|
+
return JSON.stringify(s);
|
|
2564
|
+
}
|
|
2565
|
+
function listToCode(items, body, depth) {
|
|
2566
|
+
if (items.length === 0) return "[]";
|
|
2567
|
+
const pad = indent(depth);
|
|
2568
|
+
const innerPad = indent(depth + 1);
|
|
2569
|
+
const entries = items.map((item) => `${innerPad}{
|
|
2570
|
+
${body(item)}
|
|
2571
|
+
${innerPad}}`).join(",\n");
|
|
2572
|
+
return `[
|
|
2573
|
+
${entries}
|
|
2574
|
+
${pad}]`;
|
|
2575
|
+
}
|
|
2576
|
+
var UNNAMED_TYPES = /* @__PURE__ */ new Set(["row", "tabs", "collapsible"]);
|
|
2577
|
+
var NO_LABEL_TYPES = /* @__PURE__ */ new Set(["row"]);
|
|
2578
|
+
var NO_LOCALIZED_TYPES = /* @__PURE__ */ new Set(["row", "tabs", "collapsible"]);
|
|
2579
|
+
var NO_ADMIN_DESCRIPTION_TYPES = /* @__PURE__ */ new Set(["row", "tabs"]);
|
|
2580
|
+
var FIELDS_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
2581
|
+
"array",
|
|
2582
|
+
"group",
|
|
2583
|
+
"row",
|
|
2584
|
+
"collapsible"
|
|
2585
|
+
]);
|
|
2586
|
+
var PAYLOAD_TYPE = {
|
|
2587
|
+
richtext: "richText",
|
|
2588
|
+
image: "upload",
|
|
2589
|
+
file: "upload",
|
|
2590
|
+
multiselect: "select",
|
|
2591
|
+
url: "text",
|
|
2592
|
+
color: "text"
|
|
2593
|
+
};
|
|
2594
|
+
var UPLOAD_TYPES = /* @__PURE__ */ new Set(["image", "file"]);
|
|
2595
|
+
var DEFAULT_UPLOAD_COLLECTION = "media";
|
|
2596
|
+
function payloadType(type) {
|
|
2597
|
+
return PAYLOAD_TYPE[type] ?? type;
|
|
2598
|
+
}
|
|
2599
|
+
function fieldToCode(field, depth = 1) {
|
|
2600
|
+
const pad = indent(depth);
|
|
2601
|
+
const innerPad = indent(depth + 1);
|
|
2602
|
+
const lines = [];
|
|
2603
|
+
if (!UNNAMED_TYPES.has(field.type)) {
|
|
2604
|
+
lines.push(`${pad}name: ${safeStr(field.name)}`);
|
|
2605
|
+
}
|
|
2606
|
+
lines.push(`${pad}type: '${payloadType(field.type)}'`);
|
|
2607
|
+
if (field.label && !NO_LABEL_TYPES.has(field.type)) {
|
|
2608
|
+
lines.push(`${pad}label: ${safeStr(field.label)}`);
|
|
2609
|
+
}
|
|
2610
|
+
if (field.required) lines.push(`${pad}required: true`);
|
|
2611
|
+
if (field.unique) lines.push(`${pad}unique: true`);
|
|
2612
|
+
if (field.localized && !NO_LOCALIZED_TYPES.has(field.type)) {
|
|
2613
|
+
lines.push(`${pad}localized: true`);
|
|
2614
|
+
}
|
|
2615
|
+
if (field.defaultValue !== void 0) {
|
|
2616
|
+
const val = typeof field.defaultValue === "string" ? safeStr(String(field.defaultValue)) : field.defaultValue;
|
|
2617
|
+
lines.push(`${pad}defaultValue: ${val}`);
|
|
2618
|
+
}
|
|
2619
|
+
if (field.type === "richtext") {
|
|
2620
|
+
lines.push(`${pad}editor: lexicalEditor({})`);
|
|
2621
|
+
}
|
|
2622
|
+
if (field.options && field.options.length > 0) {
|
|
2623
|
+
const opts = field.options.map((o) => `{ label: ${safeStr(o.label)}, value: ${safeStr(o.value)} }`).join(`, `);
|
|
2624
|
+
lines.push(`${pad}options: [${opts}]`);
|
|
2625
|
+
}
|
|
2626
|
+
if (UPLOAD_TYPES.has(field.type)) {
|
|
2627
|
+
lines.push(
|
|
2628
|
+
`${pad}relationTo: ${safeStr(field.collection || DEFAULT_UPLOAD_COLLECTION)}`
|
|
2629
|
+
);
|
|
2630
|
+
} else if (field.collection) {
|
|
2631
|
+
lines.push(`${pad}relationTo: ${safeStr(field.collection)}`);
|
|
2632
|
+
}
|
|
2633
|
+
if (field.type === "multiselect") {
|
|
2634
|
+
lines.push(`${pad}hasMany: true`);
|
|
2635
|
+
} else if (field.hasMany !== void 0) {
|
|
2636
|
+
lines.push(`${pad}hasMany: ${field.hasMany}`);
|
|
2637
|
+
}
|
|
2638
|
+
if (field.minRows !== void 0) lines.push(`${pad}minRows: ${field.minRows}`);
|
|
2639
|
+
if (field.maxRows !== void 0) lines.push(`${pad}maxRows: ${field.maxRows}`);
|
|
2640
|
+
const children = field.fields ?? [];
|
|
2641
|
+
if (children.length > 0 || FIELDS_CONTAINER_TYPES.has(field.type)) {
|
|
2642
|
+
lines.push(
|
|
2643
|
+
`${pad}fields: ${listToCode(children, (f) => fieldToCode(f, depth + 2), depth)}`
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
if (field.type === "blocks") {
|
|
2647
|
+
lines.push(`${pad}blocks: []`);
|
|
2648
|
+
}
|
|
2649
|
+
if (field.type === "tabs") {
|
|
2650
|
+
const tabsCode = (field.tabs ?? []).map((tab) => {
|
|
2651
|
+
const tabPad = indent(depth + 2);
|
|
2652
|
+
const tabLines = [];
|
|
2653
|
+
if (tab.name) tabLines.push(`${tabPad}name: ${safeStr(tab.name)}`);
|
|
2654
|
+
tabLines.push(`${tabPad}label: ${safeStr(tab.label)}`);
|
|
2655
|
+
tabLines.push(
|
|
2656
|
+
`${tabPad}fields: ${listToCode(tab.fields ?? [], (f) => fieldToCode(f, depth + 4), depth + 2)}`
|
|
2657
|
+
);
|
|
2658
|
+
return `${innerPad}{
|
|
2659
|
+
${tabLines.join(",\n")}
|
|
2660
|
+
${innerPad}}`;
|
|
2661
|
+
}).join(",\n");
|
|
2662
|
+
lines.push(
|
|
2663
|
+
`${pad}tabs: ${(field.tabs ?? []).length > 0 ? `[
|
|
2664
|
+
${tabsCode}
|
|
2665
|
+
${pad}]` : "[]"}`
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
const adminParts = [];
|
|
2669
|
+
if (field.admin?.description && !NO_ADMIN_DESCRIPTION_TYPES.has(field.type))
|
|
2670
|
+
adminParts.push(`description: ${safeStr(field.admin.description)}`);
|
|
2671
|
+
if (field.admin?.placeholder)
|
|
2672
|
+
adminParts.push(`placeholder: ${safeStr(field.admin.placeholder)}`);
|
|
2673
|
+
if (field.admin?.readOnly) adminParts.push(`readOnly: true`);
|
|
2674
|
+
if (field.admin?.hidden) adminParts.push(`hidden: true`);
|
|
2675
|
+
if (adminParts.length > 0) {
|
|
2676
|
+
lines.push(`${pad}admin: { ${adminParts.join(", ")} }`);
|
|
2677
|
+
}
|
|
2678
|
+
return lines.join(",\n");
|
|
2679
|
+
}
|
|
2680
|
+
function generateBlockCode(block) {
|
|
2681
|
+
const hasRichText = containsRichText(block.fields);
|
|
2682
|
+
const imports = [`import type { Block } from 'payload'`];
|
|
2683
|
+
if (hasRichText) {
|
|
2684
|
+
imports.push(`import { lexicalEditor } from '@payloadcms/richtext-lexical'`);
|
|
2685
|
+
}
|
|
2686
|
+
const fieldsCode = block.fields.map((f) => ` {
|
|
2687
|
+
${fieldToCode(f, 3)}
|
|
2688
|
+
}`).join(",\n");
|
|
2689
|
+
const labelsCode = block.labels ? `
|
|
2690
|
+
labels: {
|
|
2691
|
+
singular: ${safeStr(block.labels.singular ?? block.slug)},
|
|
2692
|
+
plural: ${safeStr(block.labels.plural ?? block.slug + "s")},
|
|
2693
|
+
},` : "";
|
|
2694
|
+
const interfaceLine = block.interfaceName ? `
|
|
2695
|
+
interfaceName: ${safeStr(block.interfaceName)},` : "";
|
|
2696
|
+
const exportName = block.interfaceName ?? toCamelCase(block.slug);
|
|
2697
|
+
return [
|
|
2698
|
+
imports.join("\n"),
|
|
2699
|
+
"",
|
|
2700
|
+
`export const ${exportName}: Block = {`,
|
|
2701
|
+
` slug: ${safeStr(block.slug)},${interfaceLine}${labelsCode}`,
|
|
2702
|
+
` fields: [`,
|
|
2703
|
+
fieldsCode,
|
|
2704
|
+
` ],`,
|
|
2705
|
+
`}`,
|
|
2706
|
+
""
|
|
2707
|
+
].join("\n");
|
|
2708
|
+
}
|
|
2709
|
+
function containsRichText(fields) {
|
|
2710
|
+
return fields.some((f) => {
|
|
2711
|
+
if (f.type === "richtext") return true;
|
|
2712
|
+
if (f.fields && containsRichText(f.fields)) return true;
|
|
2713
|
+
if (f.tabs?.some((tab) => containsRichText(tab.fields ?? []))) return true;
|
|
2714
|
+
return false;
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
function toCamelCase(slug) {
|
|
2718
|
+
return slug.split(/[-_]/).map(
|
|
2719
|
+
(part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
|
|
2720
|
+
).join("");
|
|
2721
|
+
}
|
|
2722
|
+
function toPascalCase(slug) {
|
|
2723
|
+
return slug.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
2724
|
+
}
|
|
2725
|
+
function getTsType(field) {
|
|
2726
|
+
switch (field.type) {
|
|
2727
|
+
case "number":
|
|
2728
|
+
return "number";
|
|
2729
|
+
case "checkbox":
|
|
2730
|
+
return "boolean";
|
|
2731
|
+
case "text":
|
|
2732
|
+
case "textarea":
|
|
2733
|
+
case "email":
|
|
2734
|
+
case "url":
|
|
2735
|
+
case "color":
|
|
2736
|
+
case "select":
|
|
2737
|
+
case "date":
|
|
2738
|
+
return "string";
|
|
2739
|
+
case "multiselect":
|
|
2740
|
+
return "string[]";
|
|
2741
|
+
case "group":
|
|
2742
|
+
return field.fields ? objectType(flattenProps(field.fields)) : "any";
|
|
2743
|
+
case "array":
|
|
2744
|
+
if (field.fields) {
|
|
2745
|
+
const inner = flattenProps(field.fields).map((p) => `${p.name}: ${p.type}`).join("; ");
|
|
2746
|
+
return `Array<{ id: string; ${inner} }>`;
|
|
2747
|
+
}
|
|
2748
|
+
return "any[]";
|
|
2749
|
+
default:
|
|
2750
|
+
return "any";
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
function objectType(props) {
|
|
2754
|
+
return props.length > 0 ? `{ ${props.map((p) => `${p.name}: ${p.type}`).join("; ")} }` : "Record<string, unknown>";
|
|
2755
|
+
}
|
|
2756
|
+
function flattenProps(fields) {
|
|
2757
|
+
const out = [];
|
|
2758
|
+
for (const f of fields) {
|
|
2759
|
+
if (f.type === "row" || f.type === "collapsible") {
|
|
2760
|
+
out.push(...flattenProps(f.fields ?? []));
|
|
2761
|
+
} else if (f.type === "tabs") {
|
|
2762
|
+
for (const tab of f.tabs ?? []) {
|
|
2763
|
+
if (tab.name) {
|
|
2764
|
+
out.push({ name: tab.name, type: objectType(flattenProps(tab.fields ?? [])) });
|
|
2765
|
+
} else {
|
|
2766
|
+
out.push(...flattenProps(tab.fields ?? []));
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
} else {
|
|
2770
|
+
out.push({ name: f.name, type: getTsType(f) });
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
return out;
|
|
2774
|
+
}
|
|
2775
|
+
function generateReactComponent(block) {
|
|
2776
|
+
const componentName = block.interfaceName ?? toPascalCase(block.slug);
|
|
2777
|
+
const propsName = `${componentName}Props`;
|
|
2778
|
+
const propList = flattenProps(block.fields);
|
|
2779
|
+
const propsCode = propList.map((p) => ` ${p.name}: ${p.type}`).join("\n");
|
|
2780
|
+
const fieldsJsx = propList.map((p) => ` <div className="field-${p.name}">
|
|
2781
|
+
{/* ${p.name} */}
|
|
2782
|
+
{String(props.${p.name})}
|
|
2783
|
+
</div>`).join("\n");
|
|
2784
|
+
const code = [
|
|
2785
|
+
`import React from 'react'`,
|
|
2786
|
+
``,
|
|
2787
|
+
`export type ${propsName} = {`,
|
|
2788
|
+
propsCode,
|
|
2789
|
+
`}`,
|
|
2790
|
+
``,
|
|
2791
|
+
`export function ${componentName}(props: ${propsName}) {`,
|
|
2792
|
+
` return (`,
|
|
2793
|
+
` <div className="${block.slug}">`,
|
|
2794
|
+
fieldsJsx,
|
|
2795
|
+
` </div>`,
|
|
2796
|
+
` )`,
|
|
2797
|
+
`}`,
|
|
2798
|
+
``
|
|
2799
|
+
].join("\n");
|
|
2800
|
+
return {
|
|
2801
|
+
filename: `${componentName}.tsx`,
|
|
2802
|
+
code,
|
|
2803
|
+
language: "typescript"
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
function generateBlockOutput(block) {
|
|
2807
|
+
return {
|
|
2808
|
+
filename: `${block.slug}.ts`,
|
|
2809
|
+
code: generateBlockCode(block),
|
|
2810
|
+
language: "typescript"
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
function generateAllBlocks(blocks, options = {}) {
|
|
2814
|
+
return blocks.flatMap(
|
|
2815
|
+
(block) => options.react ? [generateBlockOutput(block), generateReactComponent(block)] : [generateBlockOutput(block)]
|
|
2816
|
+
);
|
|
2817
|
+
}
|
|
2818
|
+
function generateIndexFile(blocks) {
|
|
2819
|
+
const exportName = (b) => b.interfaceName ?? toCamelCase(b.slug);
|
|
2820
|
+
const imports = blocks.map((b) => `import { ${exportName(b)} } from './${b.slug}'`).join("\n");
|
|
2821
|
+
const exportList = blocks.map((b) => ` ${exportName(b)}`).join(",\n");
|
|
2822
|
+
const code = [
|
|
2823
|
+
imports,
|
|
2824
|
+
"",
|
|
2825
|
+
`export const blocks = [`,
|
|
2826
|
+
exportList,
|
|
2827
|
+
`] as const`,
|
|
2828
|
+
""
|
|
2829
|
+
].join("\n");
|
|
2830
|
+
return { filename: "index.ts", code, language: "typescript" };
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
// src/block-builder/components/canvas/CodePreview.tsx
|
|
2223
2834
|
var KEYWORDS = /* @__PURE__ */ new Set([
|
|
2224
2835
|
"import",
|
|
2225
2836
|
"export",
|
|
@@ -2307,22 +2918,22 @@ var TOKEN_COLORS = {
|
|
|
2307
2918
|
};
|
|
2308
2919
|
function HighlightedCode({ code }) {
|
|
2309
2920
|
const tokens = tokenise(code);
|
|
2310
|
-
return /* @__PURE__ */
|
|
2921
|
+
return /* @__PURE__ */ import_react17.default.createElement("code", { style: { fontFamily: "inherit" } }, tokens.map((tok, i) => /* @__PURE__ */ import_react17.default.createElement("span", { key: i, style: { color: TOKEN_COLORS[tok.kind] } }, tok.text)));
|
|
2311
2922
|
}
|
|
2312
2923
|
function CodePreview() {
|
|
2313
2924
|
const blocks = useBuilderStore((s) => s.blocks);
|
|
2314
|
-
const [fileMap, setFileMap] = (0,
|
|
2315
|
-
const [activeFile, setActiveFile] = (0,
|
|
2316
|
-
const [copied, setCopied] = (0,
|
|
2317
|
-
const timerRef = (0,
|
|
2318
|
-
const copyTimerRef = (0,
|
|
2319
|
-
const regenerate = (0,
|
|
2925
|
+
const [fileMap, setFileMap] = (0, import_react17.useState)({});
|
|
2926
|
+
const [activeFile, setActiveFile] = (0, import_react17.useState)(null);
|
|
2927
|
+
const [copied, setCopied] = (0, import_react17.useState)(false);
|
|
2928
|
+
const timerRef = (0, import_react17.useRef)(null);
|
|
2929
|
+
const copyTimerRef = (0, import_react17.useRef)(null);
|
|
2930
|
+
const regenerate = (0, import_react17.useCallback)(() => {
|
|
2320
2931
|
if (blocks.length === 0) {
|
|
2321
2932
|
setFileMap({});
|
|
2322
2933
|
setActiveFile(null);
|
|
2323
2934
|
return;
|
|
2324
2935
|
}
|
|
2325
|
-
const blockOutputs = generateAllBlocks(blocks);
|
|
2936
|
+
const blockOutputs = generateAllBlocks(blocks, { react: true });
|
|
2326
2937
|
const indexOutput = generateIndexFile(blocks);
|
|
2327
2938
|
const next = {};
|
|
2328
2939
|
for (const out of blockOutputs) next[out.filename] = out.code;
|
|
@@ -2333,7 +2944,7 @@ function CodePreview() {
|
|
|
2333
2944
|
return prev && keys.includes(prev) ? prev : keys[0] ?? null;
|
|
2334
2945
|
});
|
|
2335
2946
|
}, [blocks]);
|
|
2336
|
-
(0,
|
|
2947
|
+
(0, import_react17.useEffect)(() => {
|
|
2337
2948
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
2338
2949
|
timerRef.current = setTimeout(regenerate, 300);
|
|
2339
2950
|
return () => {
|
|
@@ -2352,7 +2963,7 @@ function CodePreview() {
|
|
|
2352
2963
|
const activeCode = activeFile ? fileMap[activeFile] ?? "" : "";
|
|
2353
2964
|
const lineCount = activeCode ? activeCode.split("\n").length : 0;
|
|
2354
2965
|
if (fileNames.length === 0) {
|
|
2355
|
-
return /* @__PURE__ */
|
|
2966
|
+
return /* @__PURE__ */ import_react17.default.createElement(
|
|
2356
2967
|
"div",
|
|
2357
2968
|
{
|
|
2358
2969
|
className: "code-preview",
|
|
@@ -2369,7 +2980,7 @@ function CodePreview() {
|
|
|
2369
2980
|
"Add a block to see generated TypeScript code"
|
|
2370
2981
|
);
|
|
2371
2982
|
}
|
|
2372
|
-
return /* @__PURE__ */
|
|
2983
|
+
return /* @__PURE__ */ import_react17.default.createElement("div", { className: "code-preview", style: { height: "100%", display: "flex", flexDirection: "column", overflow: "hidden" } }, /* @__PURE__ */ import_react17.default.createElement(
|
|
2373
2984
|
"div",
|
|
2374
2985
|
{
|
|
2375
2986
|
style: {
|
|
@@ -2383,7 +2994,7 @@ function CodePreview() {
|
|
|
2383
2994
|
},
|
|
2384
2995
|
fileNames.map((name) => {
|
|
2385
2996
|
const isActive = activeFile === name;
|
|
2386
|
-
return /* @__PURE__ */
|
|
2997
|
+
return /* @__PURE__ */ import_react17.default.createElement(
|
|
2387
2998
|
"button",
|
|
2388
2999
|
{
|
|
2389
3000
|
key: name,
|
|
@@ -2406,8 +3017,8 @@ function CodePreview() {
|
|
|
2406
3017
|
name
|
|
2407
3018
|
);
|
|
2408
3019
|
}),
|
|
2409
|
-
/* @__PURE__ */
|
|
2410
|
-
lineCount > 0 && /* @__PURE__ */
|
|
3020
|
+
/* @__PURE__ */ import_react17.default.createElement("div", { style: { flex: 1 } }),
|
|
3021
|
+
lineCount > 0 && /* @__PURE__ */ import_react17.default.createElement(
|
|
2411
3022
|
"span",
|
|
2412
3023
|
{
|
|
2413
3024
|
style: {
|
|
@@ -2420,7 +3031,7 @@ function CodePreview() {
|
|
|
2420
3031
|
lineCount,
|
|
2421
3032
|
" lines"
|
|
2422
3033
|
),
|
|
2423
|
-
/* @__PURE__ */
|
|
3034
|
+
/* @__PURE__ */ import_react17.default.createElement(
|
|
2424
3035
|
"button",
|
|
2425
3036
|
{
|
|
2426
3037
|
type: "button",
|
|
@@ -2443,7 +3054,7 @@ function CodePreview() {
|
|
|
2443
3054
|
},
|
|
2444
3055
|
copied ? "Copied" : "Copy"
|
|
2445
3056
|
)
|
|
2446
|
-
), /* @__PURE__ */
|
|
3057
|
+
), /* @__PURE__ */ import_react17.default.createElement("div", { style: { flex: 1, overflow: "auto", display: "flex" } }, /* @__PURE__ */ import_react17.default.createElement(
|
|
2447
3058
|
"div",
|
|
2448
3059
|
{
|
|
2449
3060
|
style: {
|
|
@@ -2460,8 +3071,8 @@ function CodePreview() {
|
|
|
2460
3071
|
},
|
|
2461
3072
|
"aria-hidden": "true"
|
|
2462
3073
|
},
|
|
2463
|
-
activeCode.split("\n").map((_, i) => /* @__PURE__ */
|
|
2464
|
-
), /* @__PURE__ */
|
|
3074
|
+
activeCode.split("\n").map((_, i) => /* @__PURE__ */ import_react17.default.createElement("div", { key: i }, i + 1))
|
|
3075
|
+
), /* @__PURE__ */ import_react17.default.createElement(
|
|
2465
3076
|
"pre",
|
|
2466
3077
|
{
|
|
2467
3078
|
style: {
|
|
@@ -2476,49 +3087,194 @@ function CodePreview() {
|
|
|
2476
3087
|
whiteSpace: "pre"
|
|
2477
3088
|
}
|
|
2478
3089
|
},
|
|
2479
|
-
/* @__PURE__ */
|
|
3090
|
+
/* @__PURE__ */ import_react17.default.createElement(HighlightedCode, { code: activeCode })
|
|
2480
3091
|
)));
|
|
2481
3092
|
}
|
|
2482
3093
|
|
|
3094
|
+
// src/block-builder/components/canvas/LivePreview.tsx
|
|
3095
|
+
var import_react18 = __toESM(require("react"), 1);
|
|
3096
|
+
var import_lucide_react5 = require("lucide-react");
|
|
3097
|
+
var TYPE_ICON = {
|
|
3098
|
+
text: import_lucide_react5.Type,
|
|
3099
|
+
textarea: import_lucide_react5.AlignLeft,
|
|
3100
|
+
richtext: import_lucide_react5.AlignJustify,
|
|
3101
|
+
number: import_lucide_react5.Hash,
|
|
3102
|
+
email: import_lucide_react5.Mail,
|
|
3103
|
+
url: import_lucide_react5.Link,
|
|
3104
|
+
date: import_lucide_react5.Calendar,
|
|
3105
|
+
checkbox: import_lucide_react5.CheckSquare,
|
|
3106
|
+
select: import_lucide_react5.ChevronDown,
|
|
3107
|
+
image: import_lucide_react5.Image,
|
|
3108
|
+
file: import_lucide_react5.Upload,
|
|
3109
|
+
relationship: import_lucide_react5.Link,
|
|
3110
|
+
json: import_lucide_react5.Braces,
|
|
3111
|
+
array: import_lucide_react5.List,
|
|
3112
|
+
group: import_lucide_react5.Box,
|
|
3113
|
+
row: import_lucide_react5.Columns,
|
|
3114
|
+
tabs: import_lucide_react5.Folder,
|
|
3115
|
+
collapsible: import_lucide_react5.ChevronDown
|
|
3116
|
+
};
|
|
3117
|
+
function TypeIcon({ type }) {
|
|
3118
|
+
const Icon = TYPE_ICON[type] ?? import_lucide_react5.Type;
|
|
3119
|
+
return /* @__PURE__ */ import_react18.default.createElement(Icon, { size: 13, strokeWidth: 1.75 });
|
|
3120
|
+
}
|
|
3121
|
+
function fieldDisplayLabel(field) {
|
|
3122
|
+
return field.label || field.name || "Untitled field";
|
|
3123
|
+
}
|
|
3124
|
+
function fullFieldPath(field, prefix) {
|
|
3125
|
+
const name = field.name || "\u2014";
|
|
3126
|
+
return prefix ? `${prefix}.${name}` : name;
|
|
3127
|
+
}
|
|
3128
|
+
function fieldPlaceholder(field) {
|
|
3129
|
+
if (field.admin?.placeholder) return field.admin.placeholder;
|
|
3130
|
+
switch (field.type) {
|
|
3131
|
+
case "email":
|
|
3132
|
+
return "name@example.com";
|
|
3133
|
+
case "url":
|
|
3134
|
+
return "https://example.com";
|
|
3135
|
+
case "textarea":
|
|
3136
|
+
case "richtext":
|
|
3137
|
+
return `Write ${fieldDisplayLabel(field).toLowerCase()} here\u2026`;
|
|
3138
|
+
case "text":
|
|
3139
|
+
default:
|
|
3140
|
+
return `Enter ${fieldDisplayLabel(field).toLowerCase()}`;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
function pluralize(count, word) {
|
|
3144
|
+
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
3145
|
+
}
|
|
3146
|
+
function FieldMeta({ field, path }) {
|
|
3147
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field__meta" }, path !== null && /* @__PURE__ */ import_react18.default.createElement("code", { className: "bb-preview-field__slug" }, path), /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-preview-field__badge" }, field.type));
|
|
3148
|
+
}
|
|
3149
|
+
function FieldHead({ field, prefix }) {
|
|
3150
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field__head" }, /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-preview-field__icon" }, /* @__PURE__ */ import_react18.default.createElement(TypeIcon, { type: field.type })), /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field__headtext" }, /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-preview-field__label" }, fieldDisplayLabel(field), field.required && /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-preview-field__required" }, "*")), /* @__PURE__ */ import_react18.default.createElement(FieldMeta, { field, path: fullFieldPath(field, prefix) })));
|
|
3151
|
+
}
|
|
3152
|
+
function PreviewField({ field, prefix = "" }) {
|
|
3153
|
+
switch (field.type) {
|
|
3154
|
+
case "text":
|
|
3155
|
+
case "url":
|
|
3156
|
+
case "email":
|
|
3157
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ import_react18.default.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ import_react18.default.createElement("input", { className: "bb-preview-input", disabled: true, placeholder: fieldPlaceholder(field) }));
|
|
3158
|
+
case "textarea":
|
|
3159
|
+
case "richtext":
|
|
3160
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ import_react18.default.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ import_react18.default.createElement("textarea", { className: "bb-preview-textarea", disabled: true, rows: 3, placeholder: fieldPlaceholder(field) }));
|
|
3161
|
+
case "number":
|
|
3162
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ import_react18.default.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ import_react18.default.createElement("input", { className: "bb-preview-input", type: "number", disabled: true, placeholder: field.admin?.placeholder ?? "0" }));
|
|
3163
|
+
case "date":
|
|
3164
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ import_react18.default.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ import_react18.default.createElement("input", { className: "bb-preview-input", type: "date", disabled: true }));
|
|
3165
|
+
case "checkbox":
|
|
3166
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ import_react18.default.createElement("label", { className: "bb-preview-checkbox" }, /* @__PURE__ */ import_react18.default.createElement("input", { type: "checkbox", className: "bb-preview-checkbox__input", disabled: true }), /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-preview-field__icon" }, /* @__PURE__ */ import_react18.default.createElement(TypeIcon, { type: field.type })), /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field__headtext" }, /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-preview-field__label" }, fieldDisplayLabel(field), field.required && /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-preview-field__required" }, "*")), /* @__PURE__ */ import_react18.default.createElement(FieldMeta, { field, path: fullFieldPath(field, prefix) }))));
|
|
3167
|
+
case "select":
|
|
3168
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ import_react18.default.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ import_react18.default.createElement("select", { className: "bb-preview-select", disabled: true }, /* @__PURE__ */ import_react18.default.createElement("option", null, field.options?.[0]?.label ?? "Select an option")));
|
|
3169
|
+
case "json":
|
|
3170
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ import_react18.default.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-placeholder" }, "Raw JSON data"));
|
|
3171
|
+
case "image":
|
|
3172
|
+
case "file":
|
|
3173
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ import_react18.default.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-placeholder bb-preview-placeholder--upload" }, /* @__PURE__ */ import_react18.default.createElement(TypeIcon, { type: field.type }), field.type === "image" ? "Image upload" : "File upload"));
|
|
3174
|
+
case "relationship":
|
|
3175
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ import_react18.default.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-placeholder bb-preview-placeholder--upload" }, /* @__PURE__ */ import_react18.default.createElement(TypeIcon, { type: field.type }), field.collection ? `Linked to \u201C${field.collection}\u201D` : "No collection set", field.hasMany ? " \xB7 multiple" : ""));
|
|
3176
|
+
case "array": {
|
|
3177
|
+
const ownPath = fullFieldPath(field, prefix);
|
|
3178
|
+
return /* @__PURE__ */ import_react18.default.createElement(PreviewGroup, { field, ownPath, sublabel: `repeating \xB7 ${pluralize(field.fields?.length ?? 0, "field")} per row` }, (field.fields ?? []).map((f) => /* @__PURE__ */ import_react18.default.createElement(PreviewField, { key: f.id, field: f, prefix: `${ownPath}[]` })));
|
|
3179
|
+
}
|
|
3180
|
+
case "group": {
|
|
3181
|
+
const ownPath = fullFieldPath(field, prefix);
|
|
3182
|
+
return /* @__PURE__ */ import_react18.default.createElement(PreviewGroup, { field, ownPath }, (field.fields ?? []).map((f) => /* @__PURE__ */ import_react18.default.createElement(PreviewField, { key: f.id, field: f, prefix: ownPath })));
|
|
3183
|
+
}
|
|
3184
|
+
case "collapsible":
|
|
3185
|
+
return /* @__PURE__ */ import_react18.default.createElement(PreviewGroup, { field, ownPath: null }, (field.fields ?? []).map((f) => /* @__PURE__ */ import_react18.default.createElement(PreviewField, { key: f.id, field: f, prefix })));
|
|
3186
|
+
case "row":
|
|
3187
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-row" }, (field.fields ?? []).map((f) => /* @__PURE__ */ import_react18.default.createElement(PreviewField, { key: f.id, field: f, prefix })));
|
|
3188
|
+
case "tabs":
|
|
3189
|
+
return /* @__PURE__ */ import_react18.default.createElement(PreviewTabs, { field, prefix });
|
|
3190
|
+
default:
|
|
3191
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ import_react18.default.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-placeholder" }, "Unsupported preview for \u201C", field.type, "\u201D"));
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
function PreviewGroup({ field, ownPath, sublabel, children }) {
|
|
3195
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-group" }, /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-group__header" }, /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-preview-field__icon" }, /* @__PURE__ */ import_react18.default.createElement(TypeIcon, { type: field.type })), /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field__headtext" }, /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-preview-field__label" }, fieldDisplayLabel(field), sublabel && /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-preview-group__sublabel" }, sublabel)), /* @__PURE__ */ import_react18.default.createElement(FieldMeta, { field, path: ownPath }))), /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-group__body" }, import_react18.default.Children.count(children) > 0 ? children : /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-placeholder" }, "No fields inside yet")));
|
|
3196
|
+
}
|
|
3197
|
+
function PreviewTabs({ field, prefix }) {
|
|
3198
|
+
const [active, setActive] = (0, import_react18.useState)(0);
|
|
3199
|
+
const tabs = field.tabs ?? [];
|
|
3200
|
+
const tab = tabs[active];
|
|
3201
|
+
const tabPrefix = tab?.name ? prefix ? `${prefix}.${tab.name}` : tab.name : prefix;
|
|
3202
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-group" }, /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-group__header" }, /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-preview-field__icon" }, /* @__PURE__ */ import_react18.default.createElement(TypeIcon, { type: field.type })), /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-field__headtext" }, /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-preview-field__label" }, fieldDisplayLabel(field)), /* @__PURE__ */ import_react18.default.createElement(FieldMeta, { field, path: null }))), /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-tabs__list" }, tabs.length === 0 && /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-preview-placeholder", style: { margin: 8 } }, "No tabs yet"), tabs.map((t, i) => /* @__PURE__ */ import_react18.default.createElement(
|
|
3203
|
+
"button",
|
|
3204
|
+
{
|
|
3205
|
+
key: t.id ?? i,
|
|
3206
|
+
type: "button",
|
|
3207
|
+
className: `bb-preview-tabs__tab${i === active ? " bb-preview-tabs__tab--active" : ""}`,
|
|
3208
|
+
onClick: () => setActive(i)
|
|
3209
|
+
},
|
|
3210
|
+
t.label
|
|
3211
|
+
))), tab && /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-group__body" }, tab.name && /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-tabs__path" }, "Nested under ", /* @__PURE__ */ import_react18.default.createElement("code", { className: "bb-preview-field__slug" }, tabPrefix)), (tab.fields ?? []).length > 0 ? (tab.fields ?? []).map((f) => /* @__PURE__ */ import_react18.default.createElement(PreviewField, { key: f.id, field: f, prefix: tabPrefix })) : /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-preview-placeholder" }, "No fields inside yet")));
|
|
3212
|
+
}
|
|
3213
|
+
function LivePreview() {
|
|
3214
|
+
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
3215
|
+
const blocks = useBuilderStore((s) => s.blocks);
|
|
3216
|
+
const block = blocks.find((b) => b.id === activeBlockId);
|
|
3217
|
+
if (!block || block.fields.length === 0) {
|
|
3218
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-live-preview bb-live-preview--empty" }, /* @__PURE__ */ import_react18.default.createElement(import_lucide_react5.LayoutTemplate, { size: 32, strokeWidth: 1.5, color: "var(--bb-text-subtle)", style: { marginBottom: 16 } }), /* @__PURE__ */ import_react18.default.createElement("p", { style: { fontWeight: 600, marginBottom: 8, color: "var(--bb-text)" } }, "Nothing to preview yet"), /* @__PURE__ */ import_react18.default.createElement("p", { style: { fontSize: 12, color: "var(--bb-text-subtle)", textAlign: "center", maxWidth: 260 } }, "Add fields from the palette on the left \u2014 this panel updates live as you build."));
|
|
3219
|
+
}
|
|
3220
|
+
return /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-live-preview" }, /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-live-preview__header" }, /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-live-preview__title" }, "Live Preview"), /* @__PURE__ */ import_react18.default.createElement("span", { className: "bb-live-preview__meta" }, block.labels?.singular ?? block.slug, " \xB7 ", pluralize(block.fields.length, "field"))), /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-live-preview__body" }, /* @__PURE__ */ import_react18.default.createElement("div", { className: "bb-live-preview__page" }, block.fields.map((field) => /* @__PURE__ */ import_react18.default.createElement(PreviewField, { key: field.id, field })))));
|
|
3221
|
+
}
|
|
3222
|
+
|
|
2483
3223
|
// src/block-builder/components/canvas/BuilderShell.tsx
|
|
2484
3224
|
function BuilderShell({ loadSlug }) {
|
|
2485
3225
|
const loadBlock = useBuilderStore((s) => s.loadBlock);
|
|
2486
3226
|
const setVersionMeta = useBuilderStore((s) => s.setVersionMeta);
|
|
2487
3227
|
const setBlockSlug = useBuilderStore((s) => s.setBlockSlug);
|
|
2488
3228
|
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
2489
|
-
const [activeSlug, setActiveSlug] = (0,
|
|
2490
|
-
const [loading, setLoading] = (0,
|
|
2491
|
-
const [notification, setNotification] = (0,
|
|
2492
|
-
const [showCodePreview, setShowCodePreview] = (0,
|
|
2493
|
-
const [versions, setVersions] = (0,
|
|
2494
|
-
const [selectedVersionId, setSelectedVersionId] = (0,
|
|
2495
|
-
const [blockDefs, setBlockDefs] = (0,
|
|
2496
|
-
const [mobilePanelTab, setMobilePanelTab] = (0,
|
|
2497
|
-
(0,
|
|
2498
|
-
|
|
3229
|
+
const [activeSlug, setActiveSlug] = (0, import_react19.useState)(loadSlug ?? null);
|
|
3230
|
+
const [loading, setLoading] = (0, import_react19.useState)(!!loadSlug);
|
|
3231
|
+
const [notification, setNotification] = (0, import_react19.useState)(null);
|
|
3232
|
+
const [showCodePreview, setShowCodePreview] = (0, import_react19.useState)(false);
|
|
3233
|
+
const [versions, setVersions] = (0, import_react19.useState)([]);
|
|
3234
|
+
const [selectedVersionId, setSelectedVersionId] = (0, import_react19.useState)(null);
|
|
3235
|
+
const [blockDefs, setBlockDefs] = (0, import_react19.useState)([]);
|
|
3236
|
+
const [mobilePanelTab, setMobilePanelTab] = (0, import_react19.useState)("blocks");
|
|
3237
|
+
const [isMounted, setIsMounted] = (0, import_react19.useState)(false);
|
|
3238
|
+
const [previewOpen, setPreviewOpen] = (0, import_react19.useState)(false);
|
|
3239
|
+
(0, import_react19.useEffect)(() => {
|
|
3240
|
+
setIsMounted(true);
|
|
3241
|
+
}, []);
|
|
3242
|
+
const refreshBlockDefs = (0, import_react19.useCallback)(async (reportErrors = false) => {
|
|
3243
|
+
try {
|
|
3244
|
+
const res = await fetch("/api/block-definitions?limit=200&depth=0");
|
|
3245
|
+
const json = await res.json();
|
|
2499
3246
|
setBlockDefs(
|
|
2500
3247
|
(json.docs ?? []).map((d) => ({ id: String(d.id), slug: d.slug, name: d.name }))
|
|
2501
3248
|
);
|
|
2502
|
-
}
|
|
3249
|
+
} catch (err) {
|
|
2503
3250
|
console.error("[block-builder] Failed to load block definitions:", err);
|
|
2504
|
-
|
|
2505
|
-
|
|
3251
|
+
if (reportErrors) {
|
|
3252
|
+
setNotification({ status: "error", title: "Failed to load block definitions", errors: ["Could not load block definitions. Please refresh the page."] });
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
2506
3255
|
}, []);
|
|
2507
|
-
|
|
3256
|
+
(0, import_react19.useEffect)(() => {
|
|
3257
|
+
refreshBlockDefs(true);
|
|
3258
|
+
}, [refreshBlockDefs]);
|
|
3259
|
+
const loadVersionsForSlug = (0, import_react19.useCallback)(async (slug) => {
|
|
2508
3260
|
try {
|
|
2509
|
-
const res = await fetch(`/api/block-builder/versions/${encodeURIComponent(slug)}
|
|
3261
|
+
const res = await fetch(`/api/block-builder/versions/${encodeURIComponent(slug)}`, {
|
|
3262
|
+
headers: { "X-Block-Builder": "1" }
|
|
3263
|
+
});
|
|
2510
3264
|
const json = await res.json();
|
|
2511
3265
|
return json.versions ?? [];
|
|
2512
3266
|
} catch {
|
|
2513
3267
|
return [];
|
|
2514
3268
|
}
|
|
2515
3269
|
}, []);
|
|
2516
|
-
const loadVersion = (0,
|
|
3270
|
+
const loadVersion = (0, import_react19.useCallback)(async (slug, versionId) => {
|
|
2517
3271
|
setLoading(true);
|
|
2518
3272
|
setNotification(null);
|
|
2519
3273
|
const url = versionId ? `/api/block-builder/load/${encodeURIComponent(slug)}?versionId=${encodeURIComponent(versionId)}` : `/api/block-builder/load/${encodeURIComponent(slug)}`;
|
|
2520
3274
|
try {
|
|
2521
|
-
const res = await fetch(url
|
|
3275
|
+
const res = await fetch(url, {
|
|
3276
|
+
headers: { "X-Block-Builder": "1" }
|
|
3277
|
+
});
|
|
2522
3278
|
const json = await res.json();
|
|
2523
3279
|
if (json.block) {
|
|
2524
3280
|
loadBlock(json.block);
|
|
@@ -2533,7 +3289,7 @@ function BuilderShell({ loadSlug }) {
|
|
|
2533
3289
|
setLoading(false);
|
|
2534
3290
|
}
|
|
2535
3291
|
}, [loadBlock, setVersionMeta]);
|
|
2536
|
-
const loadBlockBySlug = (0,
|
|
3292
|
+
const loadBlockBySlug = (0, import_react19.useCallback)(async (slug) => {
|
|
2537
3293
|
setActiveSlug(slug);
|
|
2538
3294
|
setBlockSlug(slug);
|
|
2539
3295
|
setVersions([]);
|
|
@@ -2546,7 +3302,7 @@ function BuilderShell({ loadSlug }) {
|
|
|
2546
3302
|
const current = list.find((v) => v.isCurrent) ?? list[0];
|
|
2547
3303
|
if (current) setSelectedVersionId(current.id);
|
|
2548
3304
|
}, [loadVersion, loadVersionsForSlug, setBlockSlug]);
|
|
2549
|
-
(0,
|
|
3305
|
+
(0, import_react19.useEffect)(() => {
|
|
2550
3306
|
if (!loadSlug) return;
|
|
2551
3307
|
loadBlockBySlug(loadSlug);
|
|
2552
3308
|
}, [loadSlug]);
|
|
@@ -2562,14 +3318,22 @@ function BuilderShell({ loadSlug }) {
|
|
|
2562
3318
|
const current = list.find((v) => v.isCurrent) ?? list[0];
|
|
2563
3319
|
if (current) setSelectedVersionId(current.id);
|
|
2564
3320
|
}
|
|
2565
|
-
async function handleAfterPublish() {
|
|
2566
|
-
|
|
2567
|
-
|
|
3321
|
+
async function handleAfterPublish(publishedSlug) {
|
|
3322
|
+
setActiveSlug(publishedSlug);
|
|
3323
|
+
setBlockSlug(publishedSlug);
|
|
3324
|
+
const [list] = await Promise.all([
|
|
3325
|
+
loadVersionsForSlug(publishedSlug),
|
|
3326
|
+
// A first publish creates a definition the picker has never seen.
|
|
3327
|
+
refreshBlockDefs()
|
|
3328
|
+
]);
|
|
2568
3329
|
setVersions(list);
|
|
2569
3330
|
const current = list.find((v) => v.isCurrent) ?? list[0];
|
|
2570
|
-
|
|
3331
|
+
setSelectedVersionId(current ? current.id : null);
|
|
2571
3332
|
}
|
|
2572
|
-
|
|
3333
|
+
if (!isMounted) {
|
|
3334
|
+
return /* @__PURE__ */ import_react19.default.createElement("div", { className: "bb-shell" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "bb-loading-bar" }, "Initializing Builder..."));
|
|
3335
|
+
}
|
|
3336
|
+
return /* @__PURE__ */ import_react19.default.createElement(ErrorBoundary, null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "bb-shell" }, /* @__PURE__ */ import_react19.default.createElement(
|
|
2573
3337
|
TopBar,
|
|
2574
3338
|
{
|
|
2575
3339
|
blockDefs,
|
|
@@ -2581,9 +3345,11 @@ function BuilderShell({ loadSlug }) {
|
|
|
2581
3345
|
onRestoreVersion: handleRestoreVersion,
|
|
2582
3346
|
onAfterPublish: handleAfterPublish,
|
|
2583
3347
|
notification,
|
|
2584
|
-
onSetNotification: setNotification
|
|
3348
|
+
onSetNotification: setNotification,
|
|
3349
|
+
previewOpen,
|
|
3350
|
+
onTogglePreview: () => setPreviewOpen((p) => !p)
|
|
2585
3351
|
}
|
|
2586
|
-
), loading && /* @__PURE__ */
|
|
3352
|
+
), loading && /* @__PURE__ */ import_react19.default.createElement("div", { className: "bb-loading-bar" }, "Loading..."), isReadOnly && !loading && /* @__PURE__ */ import_react19.default.createElement("div", { className: "bb-readonly-banner" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "bb-readonly-banner__icon" }, "[i]"), /* @__PURE__ */ import_react19.default.createElement("span", null, "You are viewing a previous version - read only.", /* @__PURE__ */ import_react19.default.createElement(
|
|
2587
3353
|
"button",
|
|
2588
3354
|
{
|
|
2589
3355
|
type: "button",
|
|
@@ -2591,7 +3357,7 @@ function BuilderShell({ loadSlug }) {
|
|
|
2591
3357
|
onClick: handleRestoreVersion
|
|
2592
3358
|
},
|
|
2593
3359
|
"Switch to latest"
|
|
2594
|
-
))), /* @__PURE__ */
|
|
3360
|
+
))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "bb-main", "data-mobile-panel": mobilePanelTab }, /* @__PURE__ */ import_react19.default.createElement(BlockList, { blockDefs, activeSlug, onBlockSelect: loadBlockBySlug }), /* @__PURE__ */ import_react19.default.createElement("div", { className: "bb-main__center", style: { display: "flex", flex: 1 } }, /* @__PURE__ */ import_react19.default.createElement(FieldPalette, null), /* @__PURE__ */ import_react19.default.createElement(BuilderCanvas, null), previewOpen && /* @__PURE__ */ import_react19.default.createElement(LivePreview, null)), /* @__PURE__ */ import_react19.default.createElement(ConfigPanel, null)), /* @__PURE__ */ import_react19.default.createElement("div", { className: "bb-footer" }, /* @__PURE__ */ import_react19.default.createElement(
|
|
2595
3361
|
"button",
|
|
2596
3362
|
{
|
|
2597
3363
|
type: "button",
|
|
@@ -2600,12 +3366,12 @@ function BuilderShell({ loadSlug }) {
|
|
|
2600
3366
|
},
|
|
2601
3367
|
showCodePreview ? "v" : ">",
|
|
2602
3368
|
" Code Preview"
|
|
2603
|
-
), showCodePreview && /* @__PURE__ */
|
|
3369
|
+
), showCodePreview && /* @__PURE__ */ import_react19.default.createElement("div", { className: "bb-footer__content" }, /* @__PURE__ */ import_react19.default.createElement(CodePreview, null))), /* @__PURE__ */ import_react19.default.createElement("nav", { className: "bb-mobile-nav", "aria-label": "Panel navigation" }, [
|
|
2604
3370
|
{ id: "blocks", icon: "B", label: "Blocks" },
|
|
2605
3371
|
{ id: "canvas", icon: "[]", label: "Canvas" },
|
|
2606
3372
|
{ id: "palette", icon: "+", label: "Fields" },
|
|
2607
3373
|
{ id: "config", icon: "*", label: "Config" }
|
|
2608
|
-
].map(({ id, icon, label }) => /* @__PURE__ */
|
|
3374
|
+
].map(({ id, icon, label }) => /* @__PURE__ */ import_react19.default.createElement(
|
|
2609
3375
|
"button",
|
|
2610
3376
|
{
|
|
2611
3377
|
key: id,
|
|
@@ -2613,16 +3379,16 @@ function BuilderShell({ loadSlug }) {
|
|
|
2613
3379
|
className: `bb-mobile-nav__tab${mobilePanelTab === id ? " bb-mobile-nav__tab--active" : ""}`,
|
|
2614
3380
|
onClick: () => setMobilePanelTab(id)
|
|
2615
3381
|
},
|
|
2616
|
-
/* @__PURE__ */
|
|
3382
|
+
/* @__PURE__ */ import_react19.default.createElement("span", { className: "bb-mobile-nav__icon" }, icon),
|
|
2617
3383
|
label
|
|
2618
|
-
))));
|
|
3384
|
+
)))));
|
|
2619
3385
|
}
|
|
2620
3386
|
|
|
2621
3387
|
// src/components/BlockBuilderNavLink/index.tsx
|
|
2622
|
-
var
|
|
3388
|
+
var import_react20 = __toESM(require("react"), 1);
|
|
2623
3389
|
var import_link = __toESM(require("next/link"), 1);
|
|
2624
3390
|
function BlockBuilderNavLink() {
|
|
2625
|
-
return /* @__PURE__ */
|
|
3391
|
+
return /* @__PURE__ */ import_react20.default.createElement("div", { style: { padding: "0 16px", marginTop: "8px" } }, /* @__PURE__ */ import_react20.default.createElement(
|
|
2626
3392
|
import_link.default,
|
|
2627
3393
|
{
|
|
2628
3394
|
href: "/block-builder",
|