@nextbridgehq/payload-block-builder 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.cts CHANGED
@@ -4,6 +4,10 @@ declare function BlockDataField({ path }: {
4
4
  path: string;
5
5
  }): React.JSX.Element;
6
6
 
7
+ declare function BlockVersionSync({ path }: {
8
+ path: string;
9
+ }): any;
10
+
7
11
  type Props$1 = {
8
12
  path: string;
9
13
  readOnly?: boolean;
@@ -19,4 +23,4 @@ declare function BuilderShell({ loadSlug }: Props): React.JSX.Element;
19
23
 
20
24
  declare function BlockBuilderNavLink(): React.JSX.Element;
21
25
 
22
- export { BlockBuilderNavLink, BlockDataField, BuilderShell, EditInBuilderButton, SchemaBuilderField };
26
+ export { BlockBuilderNavLink, BlockDataField, BlockVersionSync, BuilderShell, EditInBuilderButton, SchemaBuilderField };
package/dist/client.d.ts CHANGED
@@ -4,6 +4,10 @@ declare function BlockDataField({ path }: {
4
4
  path: string;
5
5
  }): React.JSX.Element;
6
6
 
7
+ declare function BlockVersionSync({ path }: {
8
+ path: string;
9
+ }): any;
10
+
7
11
  type Props$1 = {
8
12
  path: string;
9
13
  readOnly?: boolean;
@@ -19,4 +23,4 @@ declare function BuilderShell({ loadSlug }: Props): React.JSX.Element;
19
23
 
20
24
  declare function BlockBuilderNavLink(): React.JSX.Element;
21
25
 
22
- export { BlockBuilderNavLink, BlockDataField, BuilderShell, EditInBuilderButton, SchemaBuilderField };
26
+ export { BlockBuilderNavLink, BlockDataField, BlockVersionSync, BuilderShell, EditInBuilderButton, SchemaBuilderField };
package/dist/client.js CHANGED
@@ -39,55 +39,132 @@ function MediaPicker({ label, required, value, onChange }) {
39
39
  "x"
40
40
  ))) : /* @__PURE__ */ React.createElement(ListDrawerToggler, { className: "bdf-upload-btn" }, "Choose from Media Library")), /* @__PURE__ */ React.createElement(ListDrawer, { onSelect: handleSelect }));
41
41
  }
42
- function RelationshipPicker({ label, required, collection, value, onChange }) {
42
+ function toRelDoc(v) {
43
+ if (!v) return null;
44
+ if (typeof v === "object") {
45
+ const o = v;
46
+ if (!o.id) return null;
47
+ return { id: o.id, title: o.title ? String(o.title) : null };
48
+ }
49
+ if (typeof v === "string" || typeof v === "number") return { id: v, title: null };
50
+ return null;
51
+ }
52
+ function RelationshipPicker({ label, required, collection, hasMany = false, value, onChange }) {
43
53
  const changeRef = useRef(onChange);
44
54
  const closeRef = useRef(() => {
45
55
  });
46
56
  useEffect(() => {
47
57
  changeRef.current = onChange;
48
58
  });
59
+ const items = hasMany ? Array.isArray(value) ? value.map(toRelDoc).filter(Boolean) : [] : (() => {
60
+ const d = toRelDoc(value);
61
+ return d ? [d] : [];
62
+ })();
63
+ const itemsRef = useRef(items);
64
+ useEffect(() => {
65
+ itemsRef.current = items;
66
+ });
49
67
  const handleSelect = useCallback(
50
68
  ({ docID, doc }) => {
51
- changeRef.current({
52
- id: docID,
53
- title: doc?.title ?? doc?.name ?? doc?.slug ?? null
54
- });
55
- closeRef.current();
69
+ const title = String(doc?.title ?? doc?.name ?? doc?.slug ?? "") || null;
70
+ const entry = { id: docID, title };
71
+ if (hasMany) {
72
+ const alreadyExists = itemsRef.current.some((i) => String(i.id) === String(docID));
73
+ if (!alreadyExists) changeRef.current([...itemsRef.current, entry]);
74
+ } else {
75
+ changeRef.current(entry);
76
+ closeRef.current();
77
+ }
56
78
  },
57
- []
79
+ [hasMany]
58
80
  );
59
- const [ListDrawer, ListDrawerToggler, { closeDrawer }] = useListDrawer({
81
+ const [ListDrawer, ListDrawerToggler, { closeDrawer, openDrawer }] = useListDrawer({
60
82
  collectionSlugs: [collection]
61
83
  });
62
84
  closeRef.current = closeDrawer;
63
- const relObj = value && typeof value === "object" ? value : null;
64
- const relId = relObj?.id ?? (typeof value === "string" || typeof value === "number" ? value : null);
65
- const relTitle = relObj?.title ? String(relObj.title) : null;
66
- const [fetchedTitle, setFetchedTitle] = useState(null);
85
+ const [fetchedTitles, setFetchedTitles] = useState({});
86
+ const fetchingRef = useRef(/* @__PURE__ */ new Set());
67
87
  useEffect(() => {
68
- if (!relId || relTitle) {
69
- setFetchedTitle(null);
70
- return;
71
- }
72
- fetch(`/api/${collection}/${String(relId)}?depth=0`, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : null).then((doc) => {
73
- if (doc) {
74
- const t = doc.title ?? doc.name ?? doc.slug ?? null;
75
- setFetchedTitle(t ? String(t) : null);
76
- }
77
- }).catch(() => {
88
+ const missing = items.filter(
89
+ (i) => !i.title && !fetchedTitles[String(i.id)] && !fetchingRef.current.has(String(i.id))
90
+ );
91
+ if (missing.length === 0) return;
92
+ missing.forEach((item) => {
93
+ const idStr = String(item.id);
94
+ fetchingRef.current.add(idStr);
95
+ fetch(`/api/${collection}/${idStr}?depth=0`, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : null).then((doc) => {
96
+ if (doc) {
97
+ const t = doc.title ?? doc.name ?? doc.slug ?? null;
98
+ if (t) setFetchedTitles((prev) => ({ ...prev, [idStr]: String(t) }));
99
+ }
100
+ }).catch(() => {
101
+ fetchingRef.current.delete(idStr);
102
+ });
78
103
  });
79
- }, [relId, relTitle, collection]);
80
- const displayTitle = relTitle ?? fetchedTitle;
81
- return /* @__PURE__ */ React.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ React.createElement("label", { className: "bdf-label" }, label, required && /* @__PURE__ */ React.createElement("span", { className: "bdf-required" }, "*"), /* @__PURE__ */ React.createElement("span", { style: { marginLeft: 6, fontSize: 11, color: "var(--theme-elevation-400)", fontWeight: 400 } }, "(", collection, ")")), /* @__PURE__ */ React.createElement("div", { className: "bdf-upload-area" }, relId ? /* @__PURE__ */ React.createElement("div", { className: "bdf-upload-selected" }, /* @__PURE__ */ React.createElement("span", { className: "bdf-upload-name" }, displayTitle ?? `ID: ${String(relId)}`), /* @__PURE__ */ React.createElement("div", { className: "bdf-upload-actions" }, /* @__PURE__ */ React.createElement(ListDrawerToggler, { className: "bdf-upload-btn" }, "Change"), /* @__PURE__ */ React.createElement(
82
- "button",
104
+ }, [items, collection]);
105
+ function getTitle(item) {
106
+ return item.title ?? fetchedTitles[String(item.id)] ?? `ID: ${String(item.id)}`;
107
+ }
108
+ function clearOne(e, id) {
109
+ e.stopPropagation();
110
+ if (hasMany) onChange(items.filter((i) => String(i.id) !== String(id)));
111
+ else onChange(null);
112
+ }
113
+ return /* @__PURE__ */ React.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ React.createElement("label", { className: "bdf-label" }, label, required && /* @__PURE__ */ React.createElement("span", { className: "bdf-required" }, "*")), /* @__PURE__ */ React.createElement("div", { className: `bdf-rel${hasMany ? " bdf-rel--multi" : ""}` }, /* @__PURE__ */ React.createElement(
114
+ "div",
83
115
  {
84
- type: "button",
85
- className: "bdf-icon-btn bdf-icon-btn--danger",
86
- title: "Remove",
87
- onClick: () => onChange(null)
116
+ role: "button",
117
+ tabIndex: 0,
118
+ className: "bdf-rel__control",
119
+ onClick: openDrawer,
120
+ onKeyDown: (e) => {
121
+ if (e.key === "Enter" || e.key === " ") openDrawer();
122
+ }
88
123
  },
89
- "\xD7"
90
- ))) : /* @__PURE__ */ React.createElement(ListDrawerToggler, { className: "bdf-upload-btn" }, "Choose from ", collection)), /* @__PURE__ */ React.createElement(ListDrawer, { onSelect: handleSelect }));
124
+ /* @__PURE__ */ React.createElement("div", { className: "bdf-rel__values" }, items.length === 0 && /* @__PURE__ */ React.createElement("span", { className: "bdf-rel__placeholder" }, "Select a value..."), !hasMany && items.length > 0 && /* @__PURE__ */ React.createElement("span", { className: "bdf-rel__single" }, getTitle(items[0])), hasMany && items.map((item) => /* @__PURE__ */ React.createElement("span", { key: String(item.id), className: "bdf-rel__chip" }, /* @__PURE__ */ React.createElement("span", { className: "bdf-rel__chip-label" }, getTitle(item)), /* @__PURE__ */ React.createElement(
125
+ "button",
126
+ {
127
+ type: "button",
128
+ className: "bdf-rel__chip-remove",
129
+ onClick: (e) => clearOne(e, item.id),
130
+ "aria-label": `Remove ${getTitle(item)}`
131
+ },
132
+ /* @__PURE__ */ React.createElement("svg", { height: "12", width: "12", viewBox: "0 0 20 20", "aria-hidden": "true", focusable: "false", fill: "currentColor" }, /* @__PURE__ */ React.createElement("path", { d: "M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z" }))
133
+ )))),
134
+ /* @__PURE__ */ React.createElement("div", { className: "bdf-rel__indicators" }, !hasMany && items.length > 0 && /* @__PURE__ */ React.createElement(
135
+ "button",
136
+ {
137
+ type: "button",
138
+ className: "bdf-rel__clear",
139
+ onClick: (e) => clearOne(e, items[0].id),
140
+ "aria-label": "Clear"
141
+ },
142
+ /* @__PURE__ */ React.createElement("svg", { height: "16", width: "16", viewBox: "0 0 20 20", "aria-hidden": "true", focusable: "false", fill: "currentColor" }, /* @__PURE__ */ React.createElement("path", { d: "M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z" }))
143
+ ), /* @__PURE__ */ React.createElement("span", { className: "bdf-rel__sep" }), /* @__PURE__ */ React.createElement("span", { className: "bdf-rel__chevron" }, /* @__PURE__ */ React.createElement("svg", { height: "16", width: "16", viewBox: "0 0 20 20", "aria-hidden": "true", focusable: "false", fill: "currentColor" }, /* @__PURE__ */ React.createElement("path", { d: "M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z" }))))
144
+ ), hasMany && /* @__PURE__ */ React.createElement(ListDrawerToggler, { className: "bdf-rel__add", onClick: (e) => e.stopPropagation() }, "+")), /* @__PURE__ */ React.createElement("span", { className: "bdf-rel__hint" }, collection, hasMany ? " \xB7 multiple" : ""), /* @__PURE__ */ React.createElement(ListDrawer, { onSelect: handleSelect }));
145
+ }
146
+ function JsonField({ label, required, value, onChange }) {
147
+ const [text, setText] = useState(
148
+ () => value !== void 0 ? JSON.stringify(value, null, 2) : ""
149
+ );
150
+ const [hasError, setHasError] = useState(false);
151
+ return /* @__PURE__ */ React.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ React.createElement("label", { className: "bdf-label" }, label, required && /* @__PURE__ */ React.createElement("span", { className: "bdf-required" }, "*"), /* @__PURE__ */ React.createElement("span", { style: { marginLeft: 6, fontSize: 11, color: "var(--theme-elevation-400)", fontWeight: 400 } }, "(JSON)")), /* @__PURE__ */ React.createElement(
152
+ "textarea",
153
+ {
154
+ className: "bdf-input bdf-textarea bdf-mono",
155
+ value: text,
156
+ rows: 4,
157
+ onChange: (e) => {
158
+ setText(e.target.value);
159
+ try {
160
+ onChange(JSON.parse(e.target.value));
161
+ setHasError(false);
162
+ } catch {
163
+ setHasError(true);
164
+ }
165
+ }
166
+ }
167
+ ), hasError && /* @__PURE__ */ React.createElement("div", { className: "bdf-error", style: { marginTop: 4 } }, "Invalid JSON \u2014 changes not saved until fixed."));
91
168
  }
92
169
  function SchemaForm({ schema, value, onChange }) {
93
170
  const set = useCallback(
@@ -114,7 +191,7 @@ function FieldInput({ field, value, onChange }) {
114
191
  "input",
115
192
  {
116
193
  className: "bdf-input",
117
- type: field.type === "email" ? "email" : "text",
194
+ type: field.type === "email" ? "email" : field.type === "url" ? "url" : "text",
118
195
  value: value ?? "",
119
196
  onChange: (e) => onChange(e.target.value)
120
197
  }
@@ -167,7 +244,7 @@ function FieldInput({ field, value, onChange }) {
167
244
  className: "bdf-input",
168
245
  type: "number",
169
246
  value: numVal,
170
- onChange: (e) => onChange(e.target.value === "" ? void 0 : e.target.valueAsNumber)
247
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.valueAsNumber)
171
248
  }
172
249
  ));
173
250
  }
@@ -227,33 +304,24 @@ function FieldInput({ field, value, onChange }) {
227
304
  }
228
305
  );
229
306
  case "relationship": {
230
- const collection = field.collection ?? "media";
307
+ const relField = field;
308
+ if (!relField.collection) {
309
+ return /* @__PURE__ */ React.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ React.createElement("label", { className: "bdf-label" }, label), /* @__PURE__ */ React.createElement("div", { className: "bdf-error" }, "Relationship field ", /* @__PURE__ */ React.createElement("strong", null, field.name), " has no ", /* @__PURE__ */ React.createElement("code", null, "collection"), " defined in its schema."));
310
+ }
231
311
  return /* @__PURE__ */ React.createElement(
232
312
  RelationshipPicker,
233
313
  {
234
314
  label,
235
315
  required: field.required,
236
- collection,
316
+ collection: relField.collection,
317
+ hasMany: relField.hasMany ?? false,
237
318
  value,
238
319
  onChange
239
320
  }
240
321
  );
241
322
  }
242
323
  case "json":
243
- return /* @__PURE__ */ React.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ React.createElement("label", { className: "bdf-label" }, label, field.required && /* @__PURE__ */ React.createElement("span", { className: "bdf-required" }, "*"), /* @__PURE__ */ React.createElement("span", { style: { marginLeft: 6, fontSize: 11, color: "var(--theme-elevation-400)", fontWeight: 400 } }, "(JSON)")), /* @__PURE__ */ React.createElement(
244
- "textarea",
245
- {
246
- className: "bdf-input bdf-textarea bdf-mono",
247
- value: value !== void 0 ? JSON.stringify(value, null, 2) : "",
248
- rows: 4,
249
- onChange: (e) => {
250
- try {
251
- onChange(JSON.parse(e.target.value));
252
- } catch {
253
- }
254
- }
255
- }
256
- ));
324
+ return /* @__PURE__ */ React.createElement(JsonField, { label, required: field.required, value, onChange });
257
325
  case "array": {
258
326
  const rows = Array.isArray(value) ? value : [];
259
327
  const subFields = field.fields ?? [];
@@ -349,9 +417,37 @@ function BlockDataField({ path }) {
349
417
  )));
350
418
  }
351
419
 
420
+ // src/components/BlockVersionSync/index.tsx
421
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
422
+ import { useField as useField2, useFormFields as useFormFields2 } from "@payloadcms/ui";
423
+ function BlockVersionSync({ path }) {
424
+ const blockVersionPath = path.replace(/\.blockDefinition$/, ".blockVersion");
425
+ const { setValue: setVersion } = useField2({ path: blockVersionPath });
426
+ const blockDefValue = useFormFields2(([fields]) => fields[path]?.value);
427
+ const prevDefIdRef = useRef2(null);
428
+ useEffect2(() => {
429
+ const defId = blockDefValue && typeof blockDefValue === "object" ? blockDefValue.id : typeof blockDefValue === "string" || typeof blockDefValue === "number" ? blockDefValue : null;
430
+ if (defId === prevDefIdRef.current) return;
431
+ prevDefIdRef.current = defId;
432
+ if (!defId) {
433
+ setVersion(null);
434
+ return;
435
+ }
436
+ fetch(`/api/block-definitions/${String(defId)}?depth=1`, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : null).then((doc) => {
437
+ if (!doc) return;
438
+ const currentVersion = doc.currentVersion;
439
+ if (!currentVersion) return;
440
+ const versionId = typeof currentVersion === "object" ? currentVersion.id : currentVersion;
441
+ if (versionId) setVersion(versionId);
442
+ }).catch(() => {
443
+ });
444
+ }, [blockDefValue, setVersion]);
445
+ return null;
446
+ }
447
+
352
448
  // src/components/SchemaBuilderField/index.tsx
353
- import React5, { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
354
- import { useField as useField2 } from "@payloadcms/ui";
449
+ import React5, { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
450
+ import { useField as useField3 } from "@payloadcms/ui";
355
451
 
356
452
  // src/components/SchemaBuilderField/FieldRow.tsx
357
453
  import React4, { useState as useState2 } from "react";
@@ -925,22 +1021,22 @@ function parseSchema(raw) {
925
1021
  return [];
926
1022
  }
927
1023
  function SchemaBuilderField({ path, readOnly }) {
928
- const { value, setValue } = useField2({ path });
929
- const setValueRef = useRef2(setValue);
930
- useEffect2(() => {
1024
+ const { value, setValue } = useField3({ path });
1025
+ const setValueRef = useRef3(setValue);
1026
+ useEffect3(() => {
931
1027
  setValueRef.current = setValue;
932
1028
  });
933
1029
  const [fields, setFields] = useState3(() => parseSchema(value));
934
1030
  const hasExistingValue = value !== void 0 && value !== null;
935
1031
  const [hydrated, setHydrated] = useState3(!hasExistingValue);
936
- useEffect2(() => {
1032
+ useEffect3(() => {
937
1033
  if (hydrated) return;
938
1034
  if (value !== void 0 && value !== null) {
939
1035
  setFields(parseSchema(value));
940
1036
  setHydrated(true);
941
1037
  }
942
1038
  }, [value, hydrated]);
943
- useEffect2(() => {
1039
+ useEffect3(() => {
944
1040
  if (!hydrated) return;
945
1041
  setValueRef.current({ fields });
946
1042
  }, [fields, hydrated]);
@@ -997,10 +1093,10 @@ function SchemaBuilderField({ path, readOnly }) {
997
1093
  // src/components/EditInBuilderButton/index.tsx
998
1094
  import React6 from "react";
999
1095
  import { useDocumentInfo } from "@payloadcms/ui";
1000
- import { useField as useField3 } from "@payloadcms/ui";
1096
+ import { useField as useField4 } from "@payloadcms/ui";
1001
1097
  function EditInBuilderButton() {
1002
1098
  const { id } = useDocumentInfo();
1003
- const { value: slug } = useField3({ path: "slug" });
1099
+ const { value: slug } = useField4({ path: "slug" });
1004
1100
  if (!id || !slug) return null;
1005
1101
  return /* @__PURE__ */ React6.createElement("div", { style: { marginTop: "1rem" } }, /* @__PURE__ */ React6.createElement(
1006
1102
  "a",
@@ -1028,7 +1124,7 @@ function EditInBuilderButton() {
1028
1124
  }
1029
1125
 
1030
1126
  // src/block-builder/components/canvas/BuilderShell.tsx
1031
- import React16, { useCallback as useCallback5, useEffect as useEffect5, useState as useState8 } from "react";
1127
+ import React16, { useCallback as useCallback5, useEffect as useEffect6, useState as useState8 } from "react";
1032
1128
 
1033
1129
  // src/block-builder/store/builder.store.ts
1034
1130
  import { create } from "zustand";
@@ -1185,7 +1281,7 @@ var useBuilderStore = create()(
1185
1281
  );
1186
1282
 
1187
1283
  // src/block-builder/components/canvas/TopBar.tsx
1188
- import React7, { useEffect as useEffect3, useRef as useRef3, useState as useState4 } from "react";
1284
+ import React7, { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
1189
1285
  import { Blocks, ChevronDown, X } from "lucide-react";
1190
1286
 
1191
1287
  // src/block-builder/lib/mapToSaveRequest.ts
@@ -1340,7 +1436,7 @@ function generateIndexFile(blocks) {
1340
1436
  }
1341
1437
 
1342
1438
  // src/block-builder/components/canvas/TopBar.tsx
1343
- function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersionId, onVersionSelect, onRestoreVersion, onAfterPublish }) {
1439
+ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersionId, onVersionSelect, onRestoreVersion, onAfterPublish, notification, onSetNotification }) {
1344
1440
  const blocks = useBuilderStore((s) => s.blocks);
1345
1441
  const activeBlockId = useBuilderStore((s) => s.activeBlockId);
1346
1442
  const activeBlock = blocks.find((b) => b.id === activeBlockId);
@@ -1348,21 +1444,21 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1348
1444
  const isDirty = useBuilderStore((s) => s.isDirty);
1349
1445
  const isReadOnly = useBuilderStore((s) => s.isReadOnly);
1350
1446
  const setVersionMeta = useBuilderStore((s) => s.setVersionMeta);
1351
- const [notification, setNotification] = useState4(null);
1447
+ const setNotification = onSetNotification;
1352
1448
  const [versionDropdownOpen, setVersionDropdownOpen] = useState4(false);
1353
1449
  const [blockPickerOpen, setBlockPickerOpen] = useState4(false);
1354
- const dropdownRef = useRef3(null);
1355
- const blockPickerRef = useRef3(null);
1450
+ const dropdownRef = useRef4(null);
1451
+ const blockPickerRef = useRef4(null);
1356
1452
  const selectedVersion = versions.find((v) => v.id === selectedVersionId);
1357
1453
  const currentVersion = versions.find((v) => v.isCurrent);
1358
1454
  const activeBlockDef = blockDefs.find((b) => b.slug === activeSlug);
1359
- useEffect3(() => {
1455
+ useEffect4(() => {
1360
1456
  if (notification?.status === "success") {
1361
1457
  const t = setTimeout(() => setNotification(null), 3e3);
1362
1458
  return () => clearTimeout(t);
1363
1459
  }
1364
1460
  }, [notification]);
1365
- useEffect3(() => {
1461
+ useEffect4(() => {
1366
1462
  function handleClick(e) {
1367
1463
  if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
1368
1464
  setVersionDropdownOpen(false);
@@ -1525,14 +1621,32 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1525
1621
  // src/block-builder/components/canvas/BlockList.tsx
1526
1622
  import React8 from "react";
1527
1623
  import { Copy, Trash2, Plus } from "lucide-react";
1528
- function BlockList() {
1624
+ function BlockList({ blockDefs = [], activeSlug, onBlockSelect }) {
1529
1625
  const blocks = useBuilderStore((s) => s.blocks);
1530
1626
  const activeBlockId = useBuilderStore((s) => s.activeBlockId);
1531
1627
  const addBlock = useBuilderStore((s) => s.addBlock);
1532
1628
  const removeBlock = useBuilderStore((s) => s.removeBlock);
1533
1629
  const duplicateBlock = useBuilderStore((s) => s.duplicateBlock);
1534
1630
  const setActiveBlock = useBuilderStore((s) => s.setActiveBlock);
1535
- return /* @__PURE__ */ React8.createElement("div", { className: "bb-sidebar bb-sidebar--200 bb-sidebar--blocks" }, /* @__PURE__ */ React8.createElement("div", { className: "bb-sidebar__header" }, /* @__PURE__ */ React8.createElement("span", { className: "bb-sidebar__title" }, "Blocks"), /* @__PURE__ */ React8.createElement("button", { type: "button", onClick: addBlock, title: "Add block", className: "bb-sidebar__add" }, /* @__PURE__ */ React8.createElement(Plus, { size: 14, strokeWidth: 2 }))), /* @__PURE__ */ React8.createElement("div", { className: "bb-sidebar__body" }, blocks.length === 0 && /* @__PURE__ */ React8.createElement("div", { className: "bb-block-empty" }, "No blocks yet.", /* @__PURE__ */ React8.createElement("br", null), "Click + to create one."), blocks.map((block) => {
1631
+ const useApiNav = blockDefs.length > 0;
1632
+ const loadedBlock = useApiNav ? blocks.find((b) => b.slug === activeSlug) : null;
1633
+ const localOnlyBlocks = blocks.filter(
1634
+ (b) => !blockDefs.some((d) => d.slug === b.slug)
1635
+ );
1636
+ return /* @__PURE__ */ React8.createElement("div", { className: "bb-sidebar bb-sidebar--200 bb-sidebar--blocks" }, /* @__PURE__ */ React8.createElement("div", { className: "bb-sidebar__header" }, /* @__PURE__ */ React8.createElement("span", { className: "bb-sidebar__title" }, "Blocks"), /* @__PURE__ */ React8.createElement("button", { type: "button", onClick: addBlock, title: "Add block", className: "bb-sidebar__add" }, /* @__PURE__ */ React8.createElement(Plus, { size: 14, strokeWidth: 2 }))), /* @__PURE__ */ React8.createElement("div", { className: "bb-sidebar__body" }, useApiNav && blockDefs.map((def) => {
1637
+ const isActive = def.slug === activeSlug;
1638
+ const fieldCount = isActive && loadedBlock ? loadedBlock.fields.length : null;
1639
+ return /* @__PURE__ */ React8.createElement(
1640
+ "div",
1641
+ {
1642
+ key: def.id,
1643
+ onClick: () => onBlockSelect?.(def.slug),
1644
+ className: `bb-block-item${isActive ? " bb-block-item--active" : ""}`
1645
+ },
1646
+ /* @__PURE__ */ React8.createElement("div", { className: "bb-block-item__slug" }, def.slug),
1647
+ /* @__PURE__ */ React8.createElement("div", { className: "bb-block-item__meta" }, fieldCount !== null ? `${fieldCount} field${fieldCount !== 1 ? "s" : ""}` : def.name)
1648
+ );
1649
+ }), localOnlyBlocks.map((block) => {
1536
1650
  const isActive = block.id === activeBlockId;
1537
1651
  return /* @__PURE__ */ React8.createElement(
1538
1652
  "div",
@@ -1571,7 +1685,7 @@ function BlockList() {
1571
1685
  )
1572
1686
  )
1573
1687
  );
1574
- })));
1688
+ }), !useApiNav && blocks.length === 0 && /* @__PURE__ */ React8.createElement("div", { className: "bb-block-empty" }, "No blocks yet.", /* @__PURE__ */ React8.createElement("br", null), "Click + to create one.")));
1575
1689
  }
1576
1690
 
1577
1691
  // src/block-builder/components/canvas/BuilderCanvas.tsx
@@ -2105,7 +2219,7 @@ function FieldButton({
2105
2219
  }
2106
2220
 
2107
2221
  // src/block-builder/components/canvas/CodePreview.tsx
2108
- import React15, { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState7 } from "react";
2222
+ import React15, { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef5, useState as useState7 } from "react";
2109
2223
  var KEYWORDS = /* @__PURE__ */ new Set([
2110
2224
  "import",
2111
2225
  "export",
@@ -2200,8 +2314,8 @@ function CodePreview() {
2200
2314
  const [fileMap, setFileMap] = useState7({});
2201
2315
  const [activeFile, setActiveFile] = useState7(null);
2202
2316
  const [copied, setCopied] = useState7(false);
2203
- const timerRef = useRef4(null);
2204
- const copyTimerRef = useRef4(null);
2317
+ const timerRef = useRef5(null);
2318
+ const copyTimerRef = useRef5(null);
2205
2319
  const regenerate = useCallback4(() => {
2206
2320
  if (blocks.length === 0) {
2207
2321
  setFileMap({});
@@ -2219,7 +2333,7 @@ function CodePreview() {
2219
2333
  return prev && keys.includes(prev) ? prev : keys[0] ?? null;
2220
2334
  });
2221
2335
  }, [blocks]);
2222
- useEffect4(() => {
2336
+ useEffect5(() => {
2223
2337
  if (timerRef.current) clearTimeout(timerRef.current);
2224
2338
  timerRef.current = setTimeout(regenerate, 300);
2225
2339
  return () => {
@@ -2374,20 +2488,20 @@ function BuilderShell({ loadSlug }) {
2374
2488
  const isReadOnly = useBuilderStore((s) => s.isReadOnly);
2375
2489
  const [activeSlug, setActiveSlug] = useState8(loadSlug ?? null);
2376
2490
  const [loading, setLoading] = useState8(!!loadSlug);
2377
- const [loadError, setLoadError] = useState8(null);
2491
+ const [notification, setNotification] = useState8(null);
2378
2492
  const [showCodePreview, setShowCodePreview] = useState8(false);
2379
2493
  const [versions, setVersions] = useState8([]);
2380
2494
  const [selectedVersionId, setSelectedVersionId] = useState8(null);
2381
2495
  const [blockDefs, setBlockDefs] = useState8([]);
2382
2496
  const [mobilePanelTab, setMobilePanelTab] = useState8("blocks");
2383
- useEffect5(() => {
2497
+ useEffect6(() => {
2384
2498
  fetch("/api/block-definitions?limit=200&depth=0").then((r) => r.json()).then((json) => {
2385
2499
  setBlockDefs(
2386
2500
  (json.docs ?? []).map((d) => ({ id: String(d.id), slug: d.slug, name: d.name }))
2387
2501
  );
2388
2502
  }).catch((err) => {
2389
2503
  console.error("[block-builder] Failed to load block definitions:", err);
2390
- setLoadError("Could not load block definitions. Please refresh the page.");
2504
+ setNotification({ status: "error", title: "Failed to load block definitions", errors: ["Could not load block definitions. Please refresh the page."] });
2391
2505
  });
2392
2506
  }, []);
2393
2507
  const loadVersionsForSlug = useCallback5(async (slug) => {
@@ -2401,7 +2515,7 @@ function BuilderShell({ loadSlug }) {
2401
2515
  }, []);
2402
2516
  const loadVersion = useCallback5(async (slug, versionId) => {
2403
2517
  setLoading(true);
2404
- setLoadError(null);
2518
+ setNotification(null);
2405
2519
  const url = versionId ? `/api/block-builder/load/${encodeURIComponent(slug)}?versionId=${encodeURIComponent(versionId)}` : `/api/block-builder/load/${encodeURIComponent(slug)}`;
2406
2520
  try {
2407
2521
  const res = await fetch(url);
@@ -2411,10 +2525,10 @@ function BuilderShell({ loadSlug }) {
2411
2525
  setVersionMeta(json.versionId ?? null, !(json.isCurrent ?? true));
2412
2526
  setSelectedVersionId(json.versionId ?? null);
2413
2527
  } else {
2414
- setLoadError(json.error ?? "Failed to load block");
2528
+ setNotification({ status: "error", title: "Failed to load block", errors: [json.error ?? "Unknown error"] });
2415
2529
  }
2416
2530
  } catch (err) {
2417
- setLoadError(err instanceof Error ? err.message : "Network error");
2531
+ setNotification({ status: "error", title: "Network error", errors: [err instanceof Error ? err.message : "Could not reach the server."] });
2418
2532
  } finally {
2419
2533
  setLoading(false);
2420
2534
  }
@@ -2424,7 +2538,7 @@ function BuilderShell({ loadSlug }) {
2424
2538
  setBlockSlug(slug);
2425
2539
  setVersions([]);
2426
2540
  setSelectedVersionId(null);
2427
- setLoadError(null);
2541
+ setNotification(null);
2428
2542
  setMobilePanelTab("canvas");
2429
2543
  await loadVersion(slug);
2430
2544
  const list = await loadVersionsForSlug(slug);
@@ -2432,7 +2546,7 @@ function BuilderShell({ loadSlug }) {
2432
2546
  const current = list.find((v) => v.isCurrent) ?? list[0];
2433
2547
  if (current) setSelectedVersionId(current.id);
2434
2548
  }, [loadVersion, loadVersionsForSlug, setBlockSlug]);
2435
- useEffect5(() => {
2549
+ useEffect6(() => {
2436
2550
  if (!loadSlug) return;
2437
2551
  loadBlockBySlug(loadSlug);
2438
2552
  }, [loadSlug]);
@@ -2465,9 +2579,11 @@ function BuilderShell({ loadSlug }) {
2465
2579
  selectedVersionId,
2466
2580
  onVersionSelect: handleVersionSelect,
2467
2581
  onRestoreVersion: handleRestoreVersion,
2468
- onAfterPublish: handleAfterPublish
2582
+ onAfterPublish: handleAfterPublish,
2583
+ notification,
2584
+ onSetNotification: setNotification
2469
2585
  }
2470
- ), loading && /* @__PURE__ */ React16.createElement("div", { className: "bb-loading-bar" }, "Loading..."), loadError && /* @__PURE__ */ React16.createElement("div", { className: "bb-error-bar" }, "Error: ", loadError), isReadOnly && !loading && /* @__PURE__ */ React16.createElement("div", { className: "bb-readonly-banner" }, /* @__PURE__ */ React16.createElement("span", { className: "bb-readonly-banner__icon" }, "[i]"), /* @__PURE__ */ React16.createElement("span", null, "You are viewing a previous version - read only.", /* @__PURE__ */ React16.createElement(
2586
+ ), loading && /* @__PURE__ */ React16.createElement("div", { className: "bb-loading-bar" }, "Loading..."), isReadOnly && !loading && /* @__PURE__ */ React16.createElement("div", { className: "bb-readonly-banner" }, /* @__PURE__ */ React16.createElement("span", { className: "bb-readonly-banner__icon" }, "[i]"), /* @__PURE__ */ React16.createElement("span", null, "You are viewing a previous version - read only.", /* @__PURE__ */ React16.createElement(
2471
2587
  "button",
2472
2588
  {
2473
2589
  type: "button",
@@ -2475,7 +2591,7 @@ function BuilderShell({ loadSlug }) {
2475
2591
  onClick: handleRestoreVersion
2476
2592
  },
2477
2593
  "Switch to latest"
2478
- ))), /* @__PURE__ */ React16.createElement("div", { className: "bb-main", "data-mobile-panel": mobilePanelTab }, /* @__PURE__ */ React16.createElement(BlockList, null), /* @__PURE__ */ React16.createElement("div", { className: "bb-main__center" }, /* @__PURE__ */ React16.createElement(FieldPalette, null), /* @__PURE__ */ React16.createElement(BuilderCanvas, null)), /* @__PURE__ */ React16.createElement(ConfigPanel, null)), /* @__PURE__ */ React16.createElement("div", { className: "bb-footer" }, /* @__PURE__ */ React16.createElement(
2594
+ ))), /* @__PURE__ */ React16.createElement("div", { className: "bb-main", "data-mobile-panel": mobilePanelTab }, /* @__PURE__ */ React16.createElement(BlockList, { blockDefs, activeSlug, onBlockSelect: loadBlockBySlug }), /* @__PURE__ */ React16.createElement("div", { className: "bb-main__center" }, /* @__PURE__ */ React16.createElement(FieldPalette, null), /* @__PURE__ */ React16.createElement(BuilderCanvas, null)), /* @__PURE__ */ React16.createElement(ConfigPanel, null)), /* @__PURE__ */ React16.createElement("div", { className: "bb-footer" }, /* @__PURE__ */ React16.createElement(
2479
2595
  "button",
2480
2596
  {
2481
2597
  type: "button",
@@ -2532,6 +2648,7 @@ function BlockBuilderNavLink() {
2532
2648
  export {
2533
2649
  BlockBuilderNavLink,
2534
2650
  BlockDataField,
2651
+ BlockVersionSync,
2535
2652
  BuilderShell,
2536
2653
  EditInBuilderButton,
2537
2654
  SchemaBuilderField
package/dist/index.cjs CHANGED
@@ -164,13 +164,26 @@ function dbLayoutField(fieldName = "dbLayout", tabLabel = "DB Layout") {
164
164
  type: "relationship",
165
165
  relationTo: "block-definitions",
166
166
  required: true,
167
- admin: { description: "Which block type to use.", width: "50%" }
167
+ admin: {
168
+ description: "Which block type to use.",
169
+ width: "50%",
170
+ components: {
171
+ afterInput: ["@nextbridgehq/payload-block-builder/client#BlockVersionSync"]
172
+ }
173
+ }
168
174
  },
169
175
  {
170
176
  name: "blockVersion",
171
177
  type: "relationship",
172
178
  relationTo: "block-definition-versions",
173
179
  required: true,
180
+ filterOptions: ({ siblingData }) => {
181
+ const def = siblingData?.blockDefinition;
182
+ if (!def) return false;
183
+ const defId = def && typeof def === "object" ? def.id : def;
184
+ if (!defId) return false;
185
+ return { blockDefinition: { equals: defId } };
186
+ },
174
187
  admin: { description: "Which schema version to use.", width: "50%" }
175
188
  }
176
189
  ]
package/dist/index.js CHANGED
@@ -136,13 +136,26 @@ function dbLayoutField(fieldName = "dbLayout", tabLabel = "DB Layout") {
136
136
  type: "relationship",
137
137
  relationTo: "block-definitions",
138
138
  required: true,
139
- admin: { description: "Which block type to use.", width: "50%" }
139
+ admin: {
140
+ description: "Which block type to use.",
141
+ width: "50%",
142
+ components: {
143
+ afterInput: ["@nextbridgehq/payload-block-builder/client#BlockVersionSync"]
144
+ }
145
+ }
140
146
  },
141
147
  {
142
148
  name: "blockVersion",
143
149
  type: "relationship",
144
150
  relationTo: "block-definition-versions",
145
151
  required: true,
152
+ filterOptions: ({ siblingData }) => {
153
+ const def = siblingData?.blockDefinition;
154
+ if (!def) return false;
155
+ const defId = def && typeof def === "object" ? def.id : def;
156
+ if (!defId) return false;
157
+ return { blockDefinition: { equals: defId } };
158
+ },
146
159
  admin: { description: "Which schema version to use.", width: "50%" }
147
160
  }
148
161
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextbridgehq/payload-block-builder",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Block Builder for Payload CMS",
5
5
  "keywords": ["payload", "payloadcms", "payload-plugin", "cms", "block-builder", "dynamic-blocks"],
6
6
  "homepage": "https://github.com/nextbridgehq/block-builder",