@byline/admin 4.2.0 → 4.4.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.
Files changed (36) hide show
  1. package/dist/fields/array/array-field.d.ts +12 -9
  2. package/dist/fields/array/array-field.js +46 -34
  3. package/dist/fields/blocks/blocks-field.js +1 -0
  4. package/dist/fields/draggable-context-menu.js +2 -1
  5. package/dist/fields/field-admin.d.ts +15 -0
  6. package/dist/fields/field-admin.js +11 -0
  7. package/dist/fields/field-admin.test.node.d.ts +8 -0
  8. package/dist/fields/field-renderer.d.ts +11 -4
  9. package/dist/fields/field-renderer.js +8 -9
  10. package/dist/fields/file/file-field.d.ts +1 -3
  11. package/dist/fields/file/file-field.js +2 -2
  12. package/dist/fields/group/group-field.d.ts +16 -9
  13. package/dist/fields/group/group-field.js +5 -4
  14. package/dist/fields/image/image-field.d.ts +1 -3
  15. package/dist/fields/image/image-field.js +2 -2
  16. package/dist/fields/sortable-item.d.ts +6 -0
  17. package/dist/fields/sortable-item.js +44 -25
  18. package/dist/forms/form-context.d.ts +20 -1
  19. package/dist/forms/form-context.js +3 -2
  20. package/dist/forms/form-renderer.js +4 -2
  21. package/dist/forms/upload-executor.js +50 -29
  22. package/package.json +10 -10
  23. package/src/fields/array/array-field.tsx +74 -43
  24. package/src/fields/blocks/blocks-field.tsx +5 -0
  25. package/src/fields/draggable-context-menu.tsx +9 -1
  26. package/src/fields/field-admin.test.node.ts +49 -0
  27. package/src/fields/field-admin.ts +39 -0
  28. package/src/fields/field-renderer.tsx +14 -7
  29. package/src/fields/file/file-field.tsx +4 -4
  30. package/src/fields/group/group-field.tsx +19 -11
  31. package/src/fields/image/image-field.tsx +4 -4
  32. package/src/fields/sortable-item.tsx +115 -34
  33. package/src/forms/form-context.tsx +30 -1
  34. package/src/forms/form-renderer.tsx +3 -1
  35. package/src/forms/upload-executor.test.node.ts +187 -0
  36. package/src/forms/upload-executor.ts +90 -29
@@ -5,23 +5,26 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
- import type { ArrayField as ArrayFieldType } from '@byline/core';
9
- export declare const ArrayField: ({ field, defaultValue, path, disableSorting, collectionPath, contentLocale, }: {
8
+ import type { ArrayField as ArrayFieldType, FieldAdminConfig } from '@byline/core';
9
+ export declare const ArrayField: ({ field, defaultValue, path, disableSorting, contentLocale, fieldAdmin, }: {
10
10
  field: ArrayFieldType;
11
11
  defaultValue: any;
12
12
  path: string;
13
13
  disableSorting?: boolean;
14
- /**
15
- * Collection path forwarded to upload-capable fields (`file` / `image`)
16
- * nested inside an array item, which need it to reach the `/upload`
17
- * endpoint. Without it those fields fall back to their empty placeholder
18
- * and never render an upload widget.
19
- */
20
- collectionPath?: string;
21
14
  /**
22
15
  * Active content locale, forwarded to each array item's fields so
23
16
  * localized widgets nested inside an array (e.g. a `localized` richText)
24
17
  * can render their locale badge.
25
18
  */
26
19
  contentLocale?: string;
20
+ /**
21
+ * Admin overrides for the array's child fields, keyed by dotted,
22
+ * index-free schema paths relative to this array ('answer',
23
+ * 'filesGroup.publicationFile'). Schema paths address declarations, so
24
+ * one entry applies to that field in every item. Arrives pre-sliced from
25
+ * the enclosing widget (`FieldRenderer` / `GroupField`); exact-name
26
+ * entries apply to the child, deeper entries are re-sliced and threaded
27
+ * on (see `sliceFieldAdmin`).
28
+ */
29
+ fieldAdmin?: Record<string, FieldAdminConfig>;
27
30
  }) => import("react").JSX.Element;
@@ -3,12 +3,13 @@ import { useEffect, useState } from "react";
3
3
  import { useTranslation } from "@byline/i18n/react";
4
4
  import { DraggableSortable, IconButton, PlusIcon, moveItem } from "@byline/ui/react";
5
5
  import classnames from "classnames";
6
+ import { sliceFieldAdmin } from "../field-admin.js";
6
7
  import { defaultScalarForField } from "../field-helpers.js";
7
8
  import { FieldRenderer } from "../field-renderer.js";
8
- import { SortableItem } from "../sortable-item.js";
9
+ import { SortableItem, StaticItem } from "../sortable-item.js";
9
10
  import { useFormContext } from "../../forms/form-context.js";
10
11
  import array_field_module from "./array-field.module.js";
11
- const ArrayField = ({ field, defaultValue, path, disableSorting = false, collectionPath, contentLocale })=>{
12
+ const ArrayField = ({ field, defaultValue, path, disableSorting = false, contentLocale, fieldAdmin })=>{
12
13
  const { appendPatch, getFieldValue, getFieldValues, setFieldStore } = useFormContext();
13
14
  const { t } = useTranslation('byline-admin');
14
15
  const [items, setItems] = useState([]);
@@ -113,6 +114,7 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, collect
113
114
  const initial = item[childField.name];
114
115
  if ('group' === childField.type && childField.fields && childField.fields.length > 0) {
115
116
  const groupData = initial && 'object' == typeof initial ? initial : {};
117
+ const groupAdmin = sliceFieldAdmin(fieldAdmin, childField.name);
116
118
  return /*#__PURE__*/ jsxs("div", {
117
119
  className: classnames('byline-field-array-group-fields', array_field_module["group-fields"]),
118
120
  children: [
@@ -125,8 +127,10 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, collect
125
127
  defaultValue: groupData[innerField.name],
126
128
  basePath: `${arrayElementPath}.${childField.name}`,
127
129
  disableSorting: true,
128
- collectionPath: collectionPath,
129
- contentLocale: contentLocale
130
+ contentLocale: contentLocale,
131
+ components: groupAdmin?.[innerField.name]?.components,
132
+ editor: groupAdmin?.[innerField.name]?.editor,
133
+ fieldAdmin: sliceFieldAdmin(groupAdmin, innerField.name)
130
134
  }, innerField.name))
131
135
  ]
132
136
  }, childField.name);
@@ -136,13 +140,17 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, collect
136
140
  defaultValue: initial,
137
141
  basePath: arrayElementPath,
138
142
  disableSorting: true,
139
- collectionPath: collectionPath,
140
- contentLocale: contentLocale
143
+ contentLocale: contentLocale,
144
+ components: fieldAdmin?.[childField.name]?.components,
145
+ editor: fieldAdmin?.[childField.name]?.editor,
146
+ fieldAdmin: sliceFieldAdmin(fieldAdmin, childField.name)
141
147
  }, childField.name);
142
148
  });
143
- const label = field.label ?? field.name;
144
- if (disableSorting) return /*#__PURE__*/ jsx("div", {
145
- className: classnames('byline-field-array-card', array_field_module.card),
149
+ const itemLabel = `${field.label ?? field.name} ${index + 1}`;
150
+ if (disableSorting) return /*#__PURE__*/ jsx(StaticItem, {
151
+ label: itemLabel,
152
+ onAddBelow: ()=>handleInsertBelow(index),
153
+ onRemove: ()=>handleRemoveItem(index),
146
154
  children: /*#__PURE__*/ jsx("div", {
147
155
  className: classnames('byline-field-array-group-fields', array_field_module["group-fields"]),
148
156
  children: innerBody
@@ -150,7 +158,7 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, collect
150
158
  }, itemWrapper.id);
151
159
  return /*#__PURE__*/ jsx(SortableItem, {
152
160
  id: itemWrapper.id,
153
- label: label,
161
+ label: itemLabel,
154
162
  onAddBelow: ()=>handleInsertBelow(index),
155
163
  onRemove: ()=>handleRemoveItem(index),
156
164
  children: /*#__PURE__*/ jsx("div", {
@@ -159,43 +167,47 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, collect
159
167
  })
160
168
  }, itemWrapper.id);
161
169
  };
170
+ const addRow = /*#__PURE__*/ jsxs("div", {
171
+ className: classnames('byline-field-array-add-row', array_field_module["add-row"]),
172
+ children: [
173
+ /*#__PURE__*/ jsx(IconButton, {
174
+ onClick: ()=>{
175
+ handleAddItem();
176
+ },
177
+ "aria-label": t('fields.array.addItemAriaLabel'),
178
+ children: /*#__PURE__*/ jsx(PlusIcon, {})
179
+ }),
180
+ /*#__PURE__*/ jsx("button", {
181
+ type: "button",
182
+ tabIndex: -1,
183
+ onClick: ()=>{
184
+ handleAddItem();
185
+ },
186
+ className: classnames('byline-field-array-add-label', array_field_module["add-label"]),
187
+ children: t('fields.array.addItem')
188
+ })
189
+ ]
190
+ });
162
191
  return /*#__PURE__*/ jsxs("div", {
163
192
  className: `byline-field-array ${field.name}`,
164
193
  children: [
165
- !disableSorting && field.label && /*#__PURE__*/ jsx("h3", {
194
+ field.label && /*#__PURE__*/ jsx("h3", {
166
195
  className: classnames('byline-field-array-title', array_field_module.title),
167
196
  children: field.label
168
197
  }),
169
- disableSorting ? /*#__PURE__*/ jsx("div", {
198
+ disableSorting ? /*#__PURE__*/ jsxs("div", {
170
199
  className: classnames('byline-field-array-stack', array_field_module.stack),
171
- children: items.map((item, index)=>renderItem(item, index))
200
+ children: [
201
+ items.map((item, index)=>renderItem(item, index)),
202
+ addRow
203
+ ]
172
204
  }) : /*#__PURE__*/ jsxs(DraggableSortable, {
173
205
  ids: items.map((i)=>i.id),
174
206
  onDragEnd: handleDragEnd,
175
207
  className: classnames('byline-field-array-stack', array_field_module.stack),
176
208
  children: [
177
209
  items.map((item, index)=>renderItem(item, index)),
178
- /*#__PURE__*/ jsxs("div", {
179
- className: classnames('byline-field-array-add-row', array_field_module["add-row"]),
180
- children: [
181
- /*#__PURE__*/ jsx(IconButton, {
182
- onClick: ()=>{
183
- handleAddItem();
184
- },
185
- "aria-label": t('fields.array.addItemAriaLabel'),
186
- children: /*#__PURE__*/ jsx(PlusIcon, {})
187
- }),
188
- /*#__PURE__*/ jsx("button", {
189
- type: "button",
190
- tabIndex: -1,
191
- onClick: ()=>{
192
- handleAddItem();
193
- },
194
- className: classnames('byline-field-array-add-label', array_field_module["add-label"]),
195
- children: t('fields.array.addItem')
196
- })
197
- ]
198
- })
210
+ addRow
199
211
  ]
200
212
  })
201
213
  ]
@@ -136,6 +136,7 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
136
136
  },
137
137
  defaultValue: fieldData,
138
138
  path: arrayElementPath,
139
+ disableSorting: false,
139
140
  contentLocale: contentLocale,
140
141
  fieldAdmin: blockAdminByType.get(item._type)?.fields
141
142
  }, subField.blockType);
@@ -16,7 +16,8 @@ function DraggableContextMenu({ onAddBelow, onRemove }) {
16
16
  /*#__PURE__*/ jsx(Dropdown.Trigger, {
17
17
  render: /*#__PURE__*/ jsx(IconButton, {
18
18
  variant: "text",
19
- size: "sm"
19
+ size: "sm",
20
+ "aria-label": t('fields.draggableMenu.triggerAriaLabel')
20
21
  }),
21
22
  children: /*#__PURE__*/ jsx(EllipsisIcon, {
22
23
  width: "16px",
@@ -0,0 +1,15 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { FieldAdminConfig } from '@byline/core';
9
+ /**
10
+ * Entries of `map` addressing descendants of `childName`, re-keyed with the
11
+ * `childName.` prefix stripped — the sub-map a structural child (group /
12
+ * array) threads to its own children. Returns `undefined` when `map` has no
13
+ * descendant entries for the child, so leaf widgets aren't handed empty maps.
14
+ */
15
+ export declare function sliceFieldAdmin(map: Record<string, FieldAdminConfig> | undefined, childName: string): Record<string, FieldAdminConfig> | undefined;
@@ -0,0 +1,11 @@
1
+ function sliceFieldAdmin(map, childName) {
2
+ if (null == map) return;
3
+ const prefix = `${childName}.`;
4
+ let sliced;
5
+ for (const [key, value] of Object.entries(map))if (key.startsWith(prefix)) {
6
+ sliced ??= {};
7
+ sliced[key.slice(prefix.length)] = value;
8
+ }
9
+ return sliced;
10
+ }
11
+ export { sliceFieldAdmin };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};
@@ -5,15 +5,13 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
- import type { Field, FieldComponentSlots, RichTextEditorComponent } from '@byline/core';
8
+ import type { Field, FieldAdminConfig, FieldComponentSlots, RichTextEditorComponent } from '@byline/core';
9
9
  interface FieldRendererProps {
10
10
  field: Field;
11
11
  defaultValue?: any;
12
12
  basePath?: string;
13
13
  disableSorting?: boolean;
14
14
  hideLabel?: boolean;
15
- /** Collection path (e.g. `'media'`) forwarded to upload-capable fields. */
16
- collectionPath?: string;
17
15
  /**
18
16
  * The active content locale (e.g. `'en'`, `'fr'`). When provided and
19
17
  * `field.localized === true`, a small locale badge is shown so the editor
@@ -32,6 +30,15 @@ interface FieldRendererProps {
32
30
  * Ignored when `field.type !== 'richText'`.
33
31
  */
34
32
  editor?: RichTextEditorComponent;
33
+ /**
34
+ * Admin overrides for this field's *descendants*, keyed by dotted,
35
+ * index-free schema paths relative to this field ('answer',
36
+ * 'filesGroup.publicationFile'). Only meaningful when `field` is a
37
+ * structural `group` / `array` — the widget slices the map per child
38
+ * (see `sliceFieldAdmin`). `components` / `editor` above stay the
39
+ * overrides for this field itself.
40
+ */
41
+ fieldAdmin?: Record<string, FieldAdminConfig>;
35
42
  }
36
- export declare const FieldRenderer: ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, collectionPath, contentLocale, components, editor, }: FieldRendererProps) => import("react").JSX.Element | null;
43
+ export declare const FieldRenderer: ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, contentLocale, components, editor, fieldAdmin, }: FieldRendererProps) => import("react").JSX.Element | null;
37
44
  export {};
@@ -20,7 +20,7 @@ import { TextField } from "./text/text-field.js";
20
20
  import { TextAreaField } from "./text-area/text-area-field.js";
21
21
  import { useFieldChangeHandler } from "./use-field-change-handler.js";
22
22
  import { useFieldCondition } from "./use-field-condition.js";
23
- const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, collectionPath, contentLocale, components, editor })=>{
23
+ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, contentLocale, components, editor, fieldAdmin })=>{
24
24
  const path = basePath ? `${basePath}.${field.name}` : field.name;
25
25
  const htmlId = path.replace(/[[\].]/g, '-');
26
26
  const handleChange = useFieldChangeHandler(field, path);
@@ -161,8 +161,7 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
161
161
  } : field,
162
162
  defaultValue: defaultValue,
163
163
  onChange: handleChange,
164
- path: path,
165
- collectionPath: collectionPath
164
+ path: path
166
165
  });
167
166
  case 'image':
168
167
  return /*#__PURE__*/ jsx(ImageField, {
@@ -172,8 +171,7 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
172
171
  } : field,
173
172
  defaultValue: defaultValue,
174
173
  onChange: handleChange,
175
- path: path,
176
- collectionPath: collectionPath
174
+ path: path
177
175
  });
178
176
  case 'relation':
179
177
  if (field.hasMany) return /*#__PURE__*/ jsx(RelationManyField, {
@@ -203,8 +201,9 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
203
201
  } : field,
204
202
  defaultValue: defaultValue,
205
203
  path: path,
206
- collectionPath: collectionPath,
207
- contentLocale: contentLocale
204
+ disableSorting: disableSorting,
205
+ contentLocale: contentLocale,
206
+ fieldAdmin: fieldAdmin
208
207
  });
209
208
  case 'blocks':
210
209
  if (!field.blocks) return null;
@@ -221,8 +220,8 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
221
220
  defaultValue: defaultValue,
222
221
  path: path,
223
222
  disableSorting: disableSorting,
224
- collectionPath: collectionPath,
225
- contentLocale: contentLocale
223
+ contentLocale: contentLocale,
224
+ fieldAdmin: fieldAdmin
226
225
  });
227
226
  default:
228
227
  return null;
@@ -8,12 +8,10 @@
8
8
  import { type FileField as FieldType, type StoredFileValue } from '@byline/core';
9
9
  interface FileFieldProps {
10
10
  field: FieldType;
11
- /** Collection path required to call the /upload endpoint. */
12
- collectionPath?: string;
13
11
  value?: StoredFileValue | null;
14
12
  defaultValue?: StoredFileValue | null;
15
13
  onChange?: (value: StoredFileValue | null) => void;
16
14
  path?: string;
17
15
  }
18
- export declare const FileField: ({ field, collectionPath, value, defaultValue, onChange: _onChange, path, }: FileFieldProps) => import("react").JSX.Element;
16
+ export declare const FileField: ({ field, value, defaultValue, onChange: _onChange, path, }: FileFieldProps) => import("react").JSX.Element;
19
17
  export {};
@@ -18,14 +18,14 @@ function triggerDownload(url, filename) {
18
18
  a.click();
19
19
  document.body.removeChild(a);
20
20
  }
21
- const FileField = ({ field, collectionPath, value, defaultValue, onChange: _onChange, path })=>{
21
+ const FileField = ({ field, value, defaultValue, onChange: _onChange, path })=>{
22
22
  const fieldPath = path ?? field.name;
23
23
  const { t } = useTranslation('byline-admin');
24
24
  const fieldError = useFieldError(fieldPath);
25
25
  const isDirty = useIsDirty(fieldPath);
26
26
  const fieldValue = useFieldValue(fieldPath);
27
27
  const isUploading = useIsFieldUploading(fieldPath);
28
- const { removePendingUpload, documentId } = useFormContext();
28
+ const { removePendingUpload, documentId, collectionPath } = useFormContext();
29
29
  const handleChange = useFieldChangeHandler(field, fieldPath);
30
30
  const incomingValue = isDirty ? fieldValue ?? null : value ?? fieldValue ?? defaultValue ?? null;
31
31
  const isPending = isPendingStoredFileValue(incomingValue);
@@ -11,11 +11,15 @@ interface GroupFieldProps {
11
11
  defaultValue: any;
12
12
  path: string;
13
13
  /**
14
- * Collection path forwarded to upload-capable child fields (`file` / `image`),
15
- * which need it to reach the `/upload` endpoint. Without it those fields fall
16
- * back to their empty placeholder and never render an upload widget.
14
+ * Threaded to child fields governs only the *drag* affordance of any
15
+ * `array` children (structural add/remove always renders; see ArrayField).
16
+ * Defaults to `true` (conservative): arrays inside plain schema groups
17
+ * stay drag-free. `BlocksField` passes `false` on its synthesized group so
18
+ * arrays directly inside blocks are fully sortable — safe because each
19
+ * `DraggableSortable` is an independent DndContext with grip-scoped
20
+ * listeners.
17
21
  */
18
- collectionPath?: string;
22
+ disableSorting?: boolean;
19
23
  /**
20
24
  * Active content locale, forwarded to child fields so localized widgets
21
25
  * nested inside the group (e.g. a `localized` richText) can render their
@@ -24,12 +28,15 @@ interface GroupFieldProps {
24
28
  contentLocale?: string;
25
29
  /**
26
30
  * Per-child-field admin overrides (`components` slots, richtext `editor`),
27
- * keyed by child field name. Threaded by `BlocksField` from the site-wide
28
- * `ClientConfig.blockAdmin` registry so block children can take per-field
29
- * admin config; plain groups receive none today (their children inherit
30
- * site-wide defaults).
31
+ * keyed by dotted, index-free schema paths relative to this group
32
+ * ('caption', 'faq.answer'). Threaded by `BlocksField` from the site-wide
33
+ * `ClientConfig.blockAdmin` registry (block children render through a
34
+ * synthesized group) and by `FieldRenderer` for plain schema groups, whose
35
+ * map arrives pre-sliced from the collection admin config. Exact-name
36
+ * entries apply to the child itself; deeper entries are re-sliced and
37
+ * threaded on (see `sliceFieldAdmin`).
31
38
  */
32
39
  fieldAdmin?: Record<string, FieldAdminConfig>;
33
40
  }
34
- export declare const GroupField: ({ field, defaultValue, path, collectionPath, contentLocale, fieldAdmin, }: GroupFieldProps) => import("react").JSX.Element;
41
+ export declare const GroupField: ({ field, defaultValue, path, disableSorting, contentLocale, fieldAdmin, }: GroupFieldProps) => import("react").JSX.Element;
35
42
  export {};
@@ -2,11 +2,12 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { ErrorText } from "@byline/ui/react";
4
4
  import classnames from "classnames";
5
+ import { sliceFieldAdmin } from "../field-admin.js";
5
6
  import { placeholderForField } from "../field-helpers.js";
6
7
  import { FieldRenderer } from "../field-renderer.js";
7
8
  import { useFieldError } from "../../forms/form-context.js";
8
9
  import group_field_module from "./group-field.module.js";
9
- const GroupField = ({ field, defaultValue, path, collectionPath, contentLocale, fieldAdmin })=>{
10
+ const GroupField = ({ field, defaultValue, path, disableSorting = true, contentLocale, fieldAdmin })=>{
10
11
  const fieldError = useFieldError(field.name);
11
12
  const groupData = useMemo(()=>{
12
13
  if (defaultValue && 'object' == typeof defaultValue && !Array.isArray(defaultValue)) return defaultValue;
@@ -46,11 +47,11 @@ const GroupField = ({ field, defaultValue, path, collectionPath, contentLocale,
46
47
  field: innerField,
47
48
  defaultValue: groupData[innerField.name],
48
49
  basePath: path,
49
- disableSorting: true,
50
- collectionPath: collectionPath,
50
+ disableSorting: disableSorting,
51
51
  contentLocale: contentLocale,
52
52
  components: fieldAdmin?.[innerField.name]?.components,
53
- editor: fieldAdmin?.[innerField.name]?.editor
53
+ editor: fieldAdmin?.[innerField.name]?.editor,
54
+ fieldAdmin: sliceFieldAdmin(fieldAdmin, innerField.name)
54
55
  }, innerField.name))
55
56
  }),
56
57
  fieldError && /*#__PURE__*/ jsx(ErrorText, {
@@ -8,12 +8,10 @@
8
8
  import { type ImageField as FieldType, type StoredFileValue } from '@byline/core';
9
9
  interface ImageFieldProps {
10
10
  field: FieldType;
11
- /** Collection path required to call the /upload endpoint. */
12
- collectionPath?: string;
13
11
  value?: StoredFileValue | null;
14
12
  defaultValue?: StoredFileValue | null;
15
13
  onChange?: (value: StoredFileValue | null) => void;
16
14
  path?: string;
17
15
  }
18
- export declare const ImageField: ({ field, collectionPath, value, defaultValue, onChange: _onChange, path, }: ImageFieldProps) => import("react").JSX.Element;
16
+ export declare const ImageField: ({ field, value, defaultValue, onChange: _onChange, path, }: ImageFieldProps) => import("react").JSX.Element;
19
17
  export {};
@@ -8,13 +8,13 @@ import { useFieldError, useFieldValue, useFormContext, useIsDirty, useIsFieldUpl
8
8
  import { useFieldChangeHandler } from "../use-field-change-handler.js";
9
9
  import image_field_module from "./image-field.module.js";
10
10
  import { ImageUploadField } from "./image-upload-field.js";
11
- const ImageField = ({ field, collectionPath, value, defaultValue, onChange: _onChange, path })=>{
11
+ const ImageField = ({ field, value, defaultValue, onChange: _onChange, path })=>{
12
12
  const fieldPath = path ?? field.name;
13
13
  const fieldError = useFieldError(fieldPath);
14
14
  const isDirty = useIsDirty(fieldPath);
15
15
  const fieldValue = useFieldValue(fieldPath);
16
16
  const isUploading = useIsFieldUploading(fieldPath);
17
- const { removePendingUpload, documentId } = useFormContext();
17
+ const { removePendingUpload, documentId, collectionPath } = useFormContext();
18
18
  const { t } = useTranslation('byline-admin');
19
19
  const handleChange = useFieldChangeHandler(field, fieldPath);
20
20
  const incomingValue = isDirty ? fieldValue ?? null : value ?? fieldValue ?? defaultValue ?? null;
@@ -13,3 +13,9 @@ export declare const SortableItem: ({ id, label, children, onAddBelow, onRemove,
13
13
  onAddBelow?: () => void;
14
14
  onRemove?: () => void;
15
15
  }) => import("react").JSX.Element;
16
+ export declare const StaticItem: ({ label, children, onAddBelow, onRemove, }: {
17
+ label: ReactNode;
18
+ children: ReactNode;
19
+ onAddBelow?: () => void;
20
+ onRemove?: () => void;
21
+ }) => import("react").JSX.Element;
@@ -5,25 +5,13 @@ import { ChevronDownIcon, GripperVerticalIcon, useSortable } from "@byline/ui/re
5
5
  import classnames from "classnames";
6
6
  import { DraggableContextMenu } from "./draggable-context-menu.js";
7
7
  import sortable_item_module from "./sortable-item.module.js";
8
- const SortableItem = ({ id, label, children, onAddBelow, onRemove })=>{
8
+ const ItemFrame = ({ label, children, onAddBelow, onRemove, grip, rootRef, style, dragging = false, rootClassName })=>{
9
9
  const { t } = useTranslation('byline-admin');
10
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
11
- id,
12
- transition: {
13
- duration: 250,
14
- easing: 'cubic-bezier(0, 0.2, 0.2, 1)'
15
- }
16
- });
17
10
  const [collapsed, setCollapsed] = useState(false);
18
- const style = {
19
- transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : void 0,
20
- transition,
21
- zIndex: isDragging ? 10 : 'auto'
22
- };
23
11
  return /*#__PURE__*/ jsxs("div", {
24
- ref: setNodeRef,
12
+ ref: rootRef,
25
13
  style: style,
26
- className: classnames('byline-sortable', sortable_item_module.root, isDragging && [
14
+ className: classnames('byline-sortable', sortable_item_module.root, rootClassName, dragging && [
27
15
  'byline-sortable-dragging',
28
16
  sortable_item_module.dragging
29
17
  ], collapsed && [
@@ -37,15 +25,7 @@ const SortableItem = ({ id, label, children, onAddBelow, onRemove })=>{
37
25
  sortable_item_module["header-expanded"]
38
26
  ]),
39
27
  children: [
40
- /*#__PURE__*/ jsx("button", {
41
- type: "button",
42
- className: classnames('byline-sortable-grip', sortable_item_module.grip),
43
- ...attributes,
44
- ...listeners,
45
- children: /*#__PURE__*/ jsx(GripperVerticalIcon, {
46
- className: classnames('byline-sortable-grip-icon', sortable_item_module["grip-icon"])
47
- })
48
- }),
28
+ grip,
49
29
  /*#__PURE__*/ jsx("div", {
50
30
  className: classnames('byline-sortable-label', sortable_item_module.label),
51
31
  children: label
@@ -78,4 +58,43 @@ const SortableItem = ({ id, label, children, onAddBelow, onRemove })=>{
78
58
  ]
79
59
  });
80
60
  };
81
- export { SortableItem };
61
+ const SortableItem = ({ id, label, children, onAddBelow, onRemove })=>{
62
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
63
+ id,
64
+ transition: {
65
+ duration: 250,
66
+ easing: 'cubic-bezier(0, 0.2, 0.2, 1)'
67
+ }
68
+ });
69
+ const style = {
70
+ transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : void 0,
71
+ transition,
72
+ zIndex: isDragging ? 10 : 'auto'
73
+ };
74
+ return /*#__PURE__*/ jsx(ItemFrame, {
75
+ label: label,
76
+ onAddBelow: onAddBelow,
77
+ onRemove: onRemove,
78
+ rootRef: setNodeRef,
79
+ style: style,
80
+ dragging: isDragging,
81
+ grip: /*#__PURE__*/ jsx("button", {
82
+ type: "button",
83
+ className: classnames('byline-sortable-grip', sortable_item_module.grip),
84
+ ...attributes,
85
+ ...listeners,
86
+ children: /*#__PURE__*/ jsx(GripperVerticalIcon, {
87
+ className: classnames('byline-sortable-grip-icon', sortable_item_module["grip-icon"])
88
+ })
89
+ }),
90
+ children: children
91
+ });
92
+ };
93
+ const StaticItem = ({ label, children, onAddBelow, onRemove })=>/*#__PURE__*/ jsx(ItemFrame, {
94
+ label: label,
95
+ onAddBelow: onAddBelow,
96
+ onRemove: onRemove,
97
+ rootClassName: "byline-sortable-static",
98
+ children: children
99
+ });
100
+ export { SortableItem, StaticItem };
@@ -55,6 +55,19 @@ interface FormContextType {
55
55
  * `@byline/core`).
56
56
  */
57
57
  documentId: string | null;
58
+ /**
59
+ * Path of the collection this form edits, `null` when the form is rendered
60
+ * without one. Upload widgets need it to address the upload endpoint.
61
+ *
62
+ * It lives here rather than being passed down because it is constant for
63
+ * the whole form: threading it as a prop meant every nesting-capable
64
+ * container (`array`, `group`, `blocks`) had to remember to forward it,
65
+ * and a container that forgot silently rendered upload fields read-only —
66
+ * no error, just a missing drop zone. That happened twice, in `array` /
67
+ * `group` and then in `blocks`. A value read from context cannot be
68
+ * dropped by a container that never carries it.
69
+ */
70
+ collectionPath: string | null;
58
71
  setFieldValue: (name: string, value: any) => void;
59
72
  setFieldStore: (name: string, value: any) => void;
60
73
  getFieldValue: (name: string) => any;
@@ -97,7 +110,7 @@ interface FormContextType {
97
110
  subscribeSystemAvailableLocales: (listener: SystemAvailableLocalesListener) => () => void;
98
111
  }
99
112
  export declare const useFormContext: () => FormContextType;
100
- export declare const FormProvider: ({ children, initialData, documentId, }: {
113
+ export declare const FormProvider: ({ children, initialData, documentId, collectionPath, }: {
101
114
  children: React.ReactNode;
102
115
  initialData?: Record<string, any>;
103
116
  /**
@@ -105,6 +118,12 @@ export declare const FormProvider: ({ children, initialData, documentId, }: {
105
118
  * the context for upload widgets honouring `upload.requireSavedDocument`.
106
119
  */
107
120
  documentId?: string | null;
121
+ /**
122
+ * Path of the collection being edited. Exposed on the context so upload
123
+ * widgets can reach the upload endpoint from any nesting depth without
124
+ * every container forwarding it — see `FormContextType.collectionPath`.
125
+ */
126
+ collectionPath?: string | null;
108
127
  }) => React.JSX.Element;
109
128
  /**
110
129
  * Subscribe to the system `path` slot edited by the path widget.
@@ -22,7 +22,7 @@ const useFormContext = ()=>{
22
22
  if (null == context) throw new Error('useFormContext must be used within a FormProvider');
23
23
  return context;
24
24
  };
25
- const FormProvider = ({ children, initialData = {}, documentId = null })=>{
25
+ const FormProvider = ({ children, initialData = {}, documentId = null, collectionPath = null })=>{
26
26
  const fieldValues = useRef(JSON.parse(JSON.stringify(initialData?.fields ?? initialData)));
27
27
  const initialValues = useRef(initialData?.fields ?? initialData);
28
28
  const errorsRef = useRef([]);
@@ -140,7 +140,7 @@ const FormProvider = ({ children, initialData = {}, documentId = null })=>{
140
140
  const appendPatch = useCallback((patch)=>{
141
141
  patchesRef.current = [
142
142
  ...patchesRef.current,
143
- patch
143
+ structuredClone(patch)
144
144
  ];
145
145
  dirtyFields.current.add('__patch__');
146
146
  notifyMetaListeners();
@@ -377,6 +377,7 @@ const FormProvider = ({ children, initialData = {}, documentId = null })=>{
377
377
  return /*#__PURE__*/ jsx(FormContext.Provider, {
378
378
  value: {
379
379
  documentId,
380
+ collectionPath,
380
381
  setFieldValue,
381
382
  setFieldStore,
382
383
  getFieldValue,