@byline/admin 4.4.0 → 4.5.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 (33) hide show
  1. package/dist/fields/array/array-field.js +20 -24
  2. package/dist/fields/blocks/blocks-field.js +19 -17
  3. package/dist/fields/file/file-upload-field.js +2 -2
  4. package/dist/fields/image/image-upload-field.js +12 -3
  5. package/dist/forms/__probe-nested.test.node.d.ts +1 -0
  6. package/dist/forms/__probe-two.test.node.d.ts +1 -0
  7. package/dist/forms/form-context.d.ts +2 -1
  8. package/dist/forms/form-context.js +17 -3
  9. package/dist/forms/form-renderer.js +2 -0
  10. package/dist/forms/nested-path.d.ts +5 -1
  11. package/dist/forms/nested-path.js +84 -15
  12. package/dist/forms/pending-uploads.d.ts +6 -0
  13. package/dist/forms/pending-uploads.js +11 -0
  14. package/dist/forms/pending-uploads.test.node.d.ts +1 -0
  15. package/dist/forms/repeating-items.d.ts +16 -0
  16. package/dist/forms/repeating-items.js +28 -0
  17. package/dist/forms/repeating-items.test.node.d.ts +1 -0
  18. package/package.json +5 -5
  19. package/src/fields/array/array-field.tsx +28 -38
  20. package/src/fields/blocks/blocks-field.tsx +24 -27
  21. package/src/fields/code/code-field.tsx +1 -1
  22. package/src/fields/file/file-upload-field.tsx +9 -5
  23. package/src/fields/image/image-upload-field.tsx +24 -6
  24. package/src/forms/form-context.tsx +31 -4
  25. package/src/forms/form-renderer.tsx +2 -0
  26. package/src/forms/nested-path.test.node.ts +50 -1
  27. package/src/forms/nested-path.ts +106 -18
  28. package/src/forms/pending-uploads.test.node.ts +23 -0
  29. package/src/forms/pending-uploads.ts +22 -0
  30. package/src/forms/repeating-items.test.node.ts +36 -0
  31. package/src/forms/repeating-items.ts +48 -0
  32. package/src/forms/upload-executor.test.node.ts +64 -3
  33. package/src/forms/upload-executor.ts +4 -1
@@ -8,16 +8,18 @@ import { defaultScalarForField } from "../field-helpers.js";
8
8
  import { FieldRenderer } from "../field-renderer.js";
9
9
  import { SortableItem, StaticItem } from "../sortable-item.js";
10
10
  import { useFormContext } from "../../forms/form-context.js";
11
+ import { hasExistingIdTargets } from "../../forms/nested-path.js";
12
+ import { moveRepeatingItems, repeatingItemId, repeatingItemPath } from "../../forms/repeating-items.js";
11
13
  import array_field_module from "./array-field.module.js";
12
14
  const ArrayField = ({ field, defaultValue, path, disableSorting = false, contentLocale, fieldAdmin })=>{
13
- const { appendPatch, getFieldValue, getFieldValues, setFieldStore } = useFormContext();
15
+ const { appendPatch, getFieldValue, getFieldValues, removePendingUploadsUnder, setFieldStore } = useFormContext();
14
16
  const { t } = useTranslation('byline-admin');
15
17
  const [items, setItems] = useState([]);
16
18
  useEffect(()=>{
17
19
  const storeValue = getFieldValue(path);
18
20
  const source = Array.isArray(storeValue) ? storeValue : defaultValue;
19
21
  Array.isArray(source) ? setItems(source.map((item)=>({
20
- id: item && 'object' == typeof item && 'id' in item ? String(item.id) : item && 'object' == typeof item && '_id' in item ? String(item._id) : crypto.randomUUID(),
22
+ id: item && 'object' == typeof item && '_id' in item ? String(item._id) : item && 'object' == typeof item && 'id' in item ? String(item.id) : crypto.randomUUID(),
21
23
  data: item
22
24
  }))) : setItems([]);
23
25
  }, [
@@ -25,28 +27,19 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
25
27
  getFieldValue,
26
28
  path
27
29
  ]);
28
- const patchItemId = (item, index)=>{
29
- if (item && 'object' == typeof item) {
30
- if ('_id' in item) return String(item._id);
31
- if ('id' in item) return String(item.id);
32
- }
33
- return String(index);
34
- };
35
30
  const handleDragEnd = ({ moveFromIndex, moveToIndex })=>{
36
- setItems((prev)=>moveItem(prev, moveFromIndex, moveToIndex));
37
31
  const currentArray = getFieldValue(path) ?? defaultValue;
38
- if (Array.isArray(currentArray)) {
39
- const clampedFrom = Math.max(0, Math.min(moveFromIndex, currentArray.length - 1));
40
- const clampedTo = Math.max(0, Math.min(moveToIndex, currentArray.length - 1));
41
- if (clampedFrom === clampedTo) return;
42
- const item = currentArray[clampedFrom];
43
- appendPatch({
44
- kind: 'array.move',
45
- path: path,
46
- itemId: patchItemId(item, clampedFrom),
47
- toIndex: clampedTo
48
- });
49
- }
32
+ if (!Array.isArray(currentArray)) return;
33
+ const move = moveRepeatingItems(currentArray, moveFromIndex, moveToIndex);
34
+ if (null == move) return;
35
+ setItems((prev)=>moveItem(prev, move.fromIndex, move.toIndex));
36
+ setFieldStore(path, move.items);
37
+ appendPatch({
38
+ kind: 'array.move',
39
+ path,
40
+ itemId: move.itemId,
41
+ toIndex: move.toIndex
42
+ });
50
43
  };
51
44
  const handleAddItem = async (atIndex)=>{
52
45
  const childFields = field.fields ?? [];
@@ -60,6 +53,7 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
60
53
  for (const innerField of childField.fields)groupObj[innerField.name] = await defaultScalarForField(innerField, getFieldValues);
61
54
  newItem[childField.name] = groupObj;
62
55
  } else newItem[childField.name] = await defaultScalarForField(childField, getFieldValues);
56
+ if (!hasExistingIdTargets(getFieldValues(), path)) return;
63
57
  const currentArray = getFieldValue(path) ?? defaultValue;
64
58
  const insertAt = null != atIndex ? atIndex : currentArray ? currentArray.length : 0;
65
59
  const newItemWrapper = {
@@ -89,11 +83,13 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
89
83
  const currentArray = getFieldValue(path) ?? defaultValue;
90
84
  if (!Array.isArray(currentArray) || index < 0 || index >= currentArray.length) return;
91
85
  const item = currentArray[index];
86
+ const itemPath = repeatingItemPath(path, item, index);
92
87
  setItems((prev)=>prev.filter((_, i)=>i !== index));
88
+ removePendingUploadsUnder(itemPath);
93
89
  appendPatch({
94
90
  kind: 'array.remove',
95
91
  path: path,
96
- itemId: patchItemId(item, index)
92
+ itemId: repeatingItemId(item) ?? String(index)
97
93
  });
98
94
  const newArrayValue = [
99
95
  ...currentArray
@@ -106,7 +102,7 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
106
102
  };
107
103
  const renderItem = (itemWrapper, index)=>{
108
104
  const item = itemWrapper.data;
109
- const arrayElementPath = `${path}[${index}]`;
105
+ const arrayElementPath = repeatingItemPath(path, item, index);
110
106
  if (!item || 'object' != typeof item) return null;
111
107
  const childFields = field.fields ?? [];
112
108
  if (0 === childFields.length) return null;
@@ -8,9 +8,11 @@ import { defaultScalarForField } from "../field-helpers.js";
8
8
  import { GroupField } from "../group/group-field.js";
9
9
  import { SortableItem } from "../sortable-item.js";
10
10
  import { useFormContext } from "../../forms/form-context.js";
11
+ import { hasExistingIdTargets } from "../../forms/nested-path.js";
12
+ import { moveRepeatingItems, repeatingItemId, repeatingItemPath } from "../../forms/repeating-items.js";
11
13
  import blocks_field_module from "./blocks-field.module.js";
12
14
  const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
13
- const { appendPatch, getFieldValue, getFieldValues, setFieldStore } = useFormContext();
15
+ const { appendPatch, getFieldValue, getFieldValues, removePendingUploadsUnder, setFieldStore } = useFormContext();
14
16
  const { t } = useTranslation('byline-admin');
15
17
  const [items, setItems] = useState([]);
16
18
  const [showAddBlockModal, setShowAddBlockModal] = useState(false);
@@ -43,21 +45,18 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
43
45
  path
44
46
  ]);
45
47
  const handleDragEnd = ({ moveFromIndex, moveToIndex })=>{
46
- setItems((prev)=>moveItem(prev, moveFromIndex, moveToIndex));
47
48
  const currentArray = getFieldValue(path) ?? defaultValue;
48
- if (Array.isArray(currentArray)) {
49
- const clampedFrom = Math.max(0, Math.min(moveFromIndex, currentArray.length - 1));
50
- const clampedTo = Math.max(0, Math.min(moveToIndex, currentArray.length - 1));
51
- if (clampedFrom === clampedTo) return;
52
- const item = currentArray[clampedFrom];
53
- const itemId = item && 'object' == typeof item && '_id' in item ? String(item._id) : String(clampedFrom);
54
- appendPatch({
55
- kind: 'array.move',
56
- path: path,
57
- itemId,
58
- toIndex: clampedTo
59
- });
60
- }
49
+ if (!Array.isArray(currentArray)) return;
50
+ const move = moveRepeatingItems(currentArray, moveFromIndex, moveToIndex);
51
+ if (null == move) return;
52
+ setItems((prev)=>moveItem(prev, move.fromIndex, move.toIndex));
53
+ setFieldStore(path, move.items);
54
+ appendPatch({
55
+ kind: 'array.move',
56
+ path,
57
+ itemId: move.itemId,
58
+ toIndex: move.toIndex
59
+ });
61
60
  };
62
61
  const handleAddItem = async (forcedVariantName, atIndex)=>{
63
62
  setShowAddBlockModal(false);
@@ -71,6 +70,7 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
71
70
  _type: variant.blockType
72
71
  };
73
72
  for (const f of compositeFields)newItem[f.name] = await defaultScalarForField(f, getFieldValues);
73
+ if (!hasExistingIdTargets(getFieldValues(), path)) return;
74
74
  const currentArray = getFieldValue(path) ?? defaultValue;
75
75
  const insertAt = null != atIndex ? atIndex : currentArray ? currentArray.length : 0;
76
76
  const newItemWrapper = {
@@ -100,8 +100,10 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
100
100
  const currentArray = getFieldValue(path) ?? defaultValue;
101
101
  if (!Array.isArray(currentArray) || index < 0 || index >= currentArray.length) return;
102
102
  const item = currentArray[index];
103
- const itemId = item && 'object' == typeof item && '_id' in item ? String(item._id) : String(index);
103
+ const itemPath = repeatingItemPath(path, item, index);
104
+ const itemId = repeatingItemId(item) ?? String(index);
104
105
  setItems((prev)=>prev.filter((_, i)=>i !== index));
106
+ removePendingUploadsUnder(itemPath);
105
107
  appendPatch({
106
108
  kind: 'array.remove',
107
109
  path: path,
@@ -121,7 +123,7 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
121
123
  };
122
124
  const renderItem = (itemWrapper, index)=>{
123
125
  const item = itemWrapper.data;
124
- const arrayElementPath = `${path}[${index}]`;
126
+ const arrayElementPath = repeatingItemPath(path, item, index);
125
127
  if (!item || 'object' != typeof item || 'string' != typeof item._type) return null;
126
128
  const subField = field.blocks?.find((b)=>b.blockType === item._type);
127
129
  if (null == subField) return null;
@@ -17,11 +17,11 @@ const FileUploadField = ({ field: _field, collectionPath, fieldPath, onUploaded,
17
17
  setErrorMessage(null);
18
18
  const previewUrl = URL.createObjectURL(file);
19
19
  const pendingValue = createPendingStoredFileValue(file, previewUrl);
20
- addPendingUpload(fieldPath, {
20
+ if (!addPendingUpload(fieldPath, {
21
21
  file,
22
22
  previewUrl,
23
23
  collectionPath
24
- });
24
+ })) return;
25
25
  setStatus('idle');
26
26
  onUploaded(pendingValue);
27
27
  }, [
@@ -1,5 +1,5 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { useCallback, useRef, useState } from "react";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { createPendingStoredFileValue } from "@byline/core";
4
4
  import { useTranslation } from "@byline/i18n/react";
5
5
  import classnames from "classnames";
@@ -7,11 +7,18 @@ import { useFormContext } from "../../forms/form-context.js";
7
7
  import image_upload_field_module from "./image-upload-field.module.js";
8
8
  const ImageUploadField = ({ field: _field, collectionPath, fieldPath, onUploaded, accept = 'image/*' })=>{
9
9
  const inputRef = useRef(null);
10
+ const mountedRef = useRef(true);
10
11
  const [status, setStatus] = useState('idle');
11
12
  const [errorMessage, setErrorMessage] = useState(null);
12
13
  const [isDragOver, setIsDragOver] = useState(false);
13
14
  const { addPendingUpload } = useFormContext();
14
15
  const { t } = useTranslation('byline-admin');
16
+ useEffect(()=>{
17
+ mountedRef.current = true;
18
+ return ()=>{
19
+ mountedRef.current = false;
20
+ };
21
+ }, []);
15
22
  const handleFileSelected = useCallback((file)=>{
16
23
  setStatus('processing');
17
24
  setErrorMessage(null);
@@ -23,6 +30,7 @@ const ImageUploadField = ({ field: _field, collectionPath, fieldPath, onUploaded
23
30
  const previewUrl = URL.createObjectURL(file);
24
31
  const img = new Image();
25
32
  img.onload = ()=>{
33
+ if (!mountedRef.current) return void URL.revokeObjectURL(previewUrl);
26
34
  const w = img.naturalWidth;
27
35
  const h = img.naturalHeight;
28
36
  const dimensions = w > 0 && h > 0 ? {
@@ -30,16 +38,17 @@ const ImageUploadField = ({ field: _field, collectionPath, fieldPath, onUploaded
30
38
  height: h
31
39
  } : void 0;
32
40
  const pendingValue = createPendingStoredFileValue(file, previewUrl, dimensions);
33
- addPendingUpload(fieldPath, {
41
+ if (!addPendingUpload(fieldPath, {
34
42
  file,
35
43
  previewUrl,
36
44
  collectionPath
37
- });
45
+ })) return;
38
46
  setStatus('idle');
39
47
  onUploaded(pendingValue);
40
48
  };
41
49
  img.onerror = ()=>{
42
50
  URL.revokeObjectURL(previewUrl);
51
+ if (!mountedRef.current) return;
43
52
  setStatus('error');
44
53
  setErrorMessage(t('fields.image.upload.errors.cannotRead'));
45
54
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -94,8 +94,9 @@ interface FormContextType {
94
94
  subscribeField: (name: string, listener: FieldListener) => () => void;
95
95
  subscribeErrors: (listener: ErrorsListener) => () => void;
96
96
  subscribeMeta: (listener: MetaListener) => () => void;
97
- addPendingUpload: (fieldPath: string, upload: PendingUpload) => void;
97
+ addPendingUpload: (fieldPath: string, upload: PendingUpload) => boolean;
98
98
  removePendingUpload: (fieldPath: string) => void;
99
+ removePendingUploadsUnder: (itemPath: string) => void;
99
100
  getPendingUploads: () => Map<string, PendingUpload>;
100
101
  hasPendingUploads: () => boolean;
101
102
  clearPendingUploads: () => void;
@@ -2,7 +2,8 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
4
4
  import { normalizeHooks } from "@byline/core";
5
- import { get, set as external_nested_path_js_set } from "./nested-path.js";
5
+ import { get, hasExistingIdTargets, setWithResult } from "./nested-path.js";
6
+ import { deletePendingUploadsUnderPath } from "./pending-uploads.js";
6
7
  import { useTrackedSlot } from "./use-tracked-slot.js";
7
8
  const sameLocaleSet = (a, b)=>{
8
9
  if (a.length !== b.length) return false;
@@ -95,11 +96,12 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
95
96
  const newFieldValues = {
96
97
  ...fieldValues.current
97
98
  };
98
- external_nested_path_js_set(newFieldValues, name, value);
99
+ if (!setWithResult(newFieldValues, name, value)) return false;
99
100
  fieldValues.current = newFieldValues;
100
101
  dirtyFields.current.add(name);
101
102
  notifyFieldListeners(name, value);
102
103
  notifyMetaListeners();
104
+ return true;
103
105
  }, [
104
106
  notifyFieldListeners,
105
107
  notifyMetaListeners
@@ -110,7 +112,7 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
110
112
  updateFieldStoreInternal
111
113
  ]);
112
114
  const setFieldValue = useCallback((name, value)=>{
113
- updateFieldStoreInternal(name, value);
115
+ if (!updateFieldStoreInternal(name, value)) return;
114
116
  const patch = {
115
117
  kind: 'field.set',
116
118
  path: name,
@@ -185,11 +187,16 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
185
187
  };
186
188
  }, []);
187
189
  const addPendingUpload = useCallback((fieldPath, upload)=>{
190
+ if (!hasExistingIdTargets(fieldValues.current, fieldPath)) {
191
+ URL.revokeObjectURL(upload.previewUrl);
192
+ return false;
193
+ }
188
194
  const existing = pendingUploadsRef.current.get(fieldPath);
189
195
  if (existing) URL.revokeObjectURL(existing.previewUrl);
190
196
  pendingUploadsRef.current.set(fieldPath, upload);
191
197
  dirtyFields.current.add(fieldPath);
192
198
  notifyMetaListeners();
199
+ return true;
193
200
  }, [
194
201
  notifyMetaListeners
195
202
  ]);
@@ -203,6 +210,12 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
203
210
  }, [
204
211
  notifyMetaListeners
205
212
  ]);
213
+ const removePendingUploadsUnder = useCallback((itemPath)=>{
214
+ const deleted = deletePendingUploadsUnderPath(pendingUploadsRef.current, itemPath, (url)=>URL.revokeObjectURL(url));
215
+ if (deleted) notifyMetaListeners();
216
+ }, [
217
+ notifyMetaListeners
218
+ ]);
206
219
  const getPendingUploads = useCallback(()=>new Map(pendingUploadsRef.current), []);
207
220
  const hasPendingUploads = useCallback(()=>pendingUploadsRef.current.size > 0, []);
208
221
  const clearPendingUploads = useCallback(()=>{
@@ -403,6 +416,7 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
403
416
  subscribeMeta,
404
417
  addPendingUpload,
405
418
  removePendingUpload,
419
+ removePendingUploadsUnder,
406
420
  getPendingUploads,
407
421
  hasPendingUploads,
408
422
  clearPendingUploads,
@@ -232,6 +232,8 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
232
232
  noValidate: true,
233
233
  onSubmit: handleSubmit,
234
234
  className: classnames('byline-form', form_renderer_module.form),
235
+ inert: isUploading ? true : void 0,
236
+ "aria-busy": isUploading,
235
237
  children: [
236
238
  /*#__PURE__*/ jsxs("div", {
237
239
  className: classnames('byline-form-heading-row', form_renderer_module["heading-row"]),
@@ -8,7 +8,7 @@
8
8
  * Minimal nested `get`/`set` over string field paths, replacing lodash-es
9
9
  * (which pulled a large shared chunk onto unrelated bundles). Supports the
10
10
  * dot + bracket notation produced by the form field-path builders, e.g.
11
- * `title`, `a.b.c`, `items[0].title`, `blocks[2].nested[1].field`.
11
+ * `title`, `a.b.c`, `items[0].title`, `blocks[id=abc].nested[1].field`.
12
12
  *
13
13
  * `set` mirrors lodash semantics: it creates intermediate **arrays** when the
14
14
  * next path segment is a numeric index and plain **objects** otherwise, and it
@@ -21,4 +21,8 @@
21
21
  /** Split a field path into segments: `items[0].title` -> ['items','0','title']. */
22
22
  export declare function toPath(path: string): string[];
23
23
  export declare function get<T = any>(object: unknown, path: string): T;
24
+ /** Whether every stable-id selector in a path still identifies a live item. */
25
+ export declare function hasExistingIdTargets(object: unknown, path: string): boolean;
26
+ /** Set a path and report whether all stable-id selectors resolved. */
27
+ export declare function setWithResult<T extends object>(object: T, path: string, value: unknown): boolean;
24
28
  export declare function set<T extends object>(object: T, path: string, value: unknown): T;
@@ -1,29 +1,98 @@
1
- const isIndexKey = (key)=>/^(?:0|[1-9]\d*)$/.test(key);
1
+ import { parseInstancePath } from "@byline/core";
2
2
  function toPath(path) {
3
3
  return path.match(/[^.[\]]+/g) ?? [];
4
4
  }
5
+ function selectId(value, id) {
6
+ if (!Array.isArray(value)) return -1;
7
+ return value.findIndex((item)=>null != item && 'object' == typeof item && item._id === id);
8
+ }
9
+ function newContainer(next) {
10
+ return next?.kind === 'index' || next?.kind === 'id' ? [] : {};
11
+ }
5
12
  function get(object, path) {
6
13
  if (null == object) return;
14
+ const parsed = parseInstancePath(path);
15
+ if (!parsed.ok) return;
7
16
  let current = object;
8
- for (const key of toPath(path)){
17
+ for (const segment of parsed.segments){
9
18
  if (null == current) return;
10
- current = current[key];
19
+ if ('field' === segment.kind) current = current[segment.name];
20
+ else if ('index' === segment.kind) current = current[segment.index];
21
+ else {
22
+ if ('id' !== segment.kind) return;
23
+ const index = selectId(current, segment.id);
24
+ if (-1 === index) return;
25
+ current = current[index];
26
+ }
11
27
  }
12
28
  return current;
13
29
  }
14
- function set(object, path, value) {
15
- if (null == object) return object;
16
- const keys = toPath(path);
17
- if (0 === keys.length) return object;
30
+ function hasExistingIdTargets(object, path) {
31
+ const parsed = parseInstancePath(path);
32
+ if (!parsed.ok) return false;
33
+ let current = object;
34
+ for (const segment of parsed.segments)if ('field' === segment.kind) current = current?.[segment.name];
35
+ else if ('index' === segment.kind) current = current?.[segment.index];
36
+ else {
37
+ if ('id' !== segment.kind) return false;
38
+ const index = selectId(current, segment.id);
39
+ if (-1 === index) return false;
40
+ current = current[index];
41
+ }
42
+ return true;
43
+ }
44
+ function setWithResult(object, path, value) {
45
+ if (null == object) return false;
46
+ const parsed = parseInstancePath(path);
47
+ if (!parsed.ok || 0 === parsed.segments.length) return false;
18
48
  let current = object;
19
- for(let i = 0; i < keys.length - 1; i++){
20
- const key = keys[i];
21
- const nextKey = keys[i + 1];
22
- const existing = current[key];
23
- if (null == existing || 'object' != typeof existing) current[key] = isIndexKey(nextKey) ? [] : {};
24
- current = current[key];
49
+ for(let i = 0; i < parsed.segments.length; i++){
50
+ const segment = parsed.segments[i];
51
+ const next = parsed.segments[i + 1];
52
+ const last = i === parsed.segments.length - 1;
53
+ if ('field' === segment.kind) {
54
+ if (last) {
55
+ current[segment.name] = value;
56
+ return true;
57
+ }
58
+ const existing = current[segment.name];
59
+ if (null == existing || 'object' != typeof existing) {
60
+ if (parsed.segments.slice(i + 1).some((candidate)=>'id' === candidate.kind)) return false;
61
+ current[segment.name] = newContainer(next);
62
+ }
63
+ current = current[segment.name];
64
+ continue;
65
+ }
66
+ if ('index' === segment.kind) {
67
+ if (!Array.isArray(current)) return false;
68
+ if (last) {
69
+ current[segment.index] = value;
70
+ return true;
71
+ }
72
+ const existing = current[segment.index];
73
+ if (null == existing || 'object' != typeof existing) {
74
+ if (parsed.segments.slice(i + 1).some((candidate)=>'id' === candidate.kind)) return false;
75
+ current[segment.index] = newContainer(next);
76
+ }
77
+ current = current[segment.index];
78
+ continue;
79
+ }
80
+ if ('id' === segment.kind) {
81
+ const index = selectId(current, segment.id);
82
+ if (-1 === index) return false;
83
+ if (last) {
84
+ current[index] = value;
85
+ return true;
86
+ }
87
+ current = current[index];
88
+ continue;
89
+ }
90
+ break;
25
91
  }
26
- current[keys[keys.length - 1]] = value;
92
+ return false;
93
+ }
94
+ function set(object, path, value) {
95
+ setWithResult(object, path, value);
27
96
  return object;
28
97
  }
29
- export { get, set, toPath };
98
+ export { get, hasExistingIdTargets, set, setWithResult, toPath };
@@ -0,0 +1,6 @@
1
+ interface PendingUploadLike {
2
+ previewUrl: string;
3
+ }
4
+ /** Remove deferred uploads belonging to one repeating item and its descendants. */
5
+ export declare function deletePendingUploadsUnderPath<T extends PendingUploadLike>(uploads: Map<string, T>, itemPath: string, revokeObjectURL: (url: string) => void): boolean;
6
+ export {};
@@ -0,0 +1,11 @@
1
+ function deletePendingUploadsUnderPath(uploads, itemPath, revokeObjectURL) {
2
+ const descendantPrefix = `${itemPath}.`;
3
+ let deleted = false;
4
+ for (const [fieldPath, upload] of uploads)if (fieldPath === itemPath || fieldPath.startsWith(descendantPrefix)) {
5
+ revokeObjectURL(upload.previewUrl);
6
+ uploads.delete(fieldPath);
7
+ deleted = true;
8
+ }
9
+ return deleted;
10
+ }
11
+ export { deletePendingUploadsUnderPath };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ /** Return a canonical identity that is safe in an instance-path selector. */
2
+ export declare function repeatingItemId(item: unknown): string | undefined;
3
+ /**
4
+ * Address an array/block item by stable identity when its id is path-safe.
5
+ * Positional fallback is retained for noncanonical create defaults and legacy
6
+ * adapter data that does not carry storage identity yet.
7
+ */
8
+ export declare function repeatingItemPath(parentPath: string, item: unknown, index: number): string;
9
+ export interface RepeatingItemMove<T> {
10
+ items: T[];
11
+ itemId: string;
12
+ fromIndex: number;
13
+ toIndex: number;
14
+ }
15
+ /** Build one synchronized form-store move and its matching patch identity. */
16
+ export declare function moveRepeatingItems<T>(items: readonly T[], moveFromIndex: number, moveToIndex: number): RepeatingItemMove<T> | null;
@@ -0,0 +1,28 @@
1
+ function repeatingItemId(item) {
2
+ if (null == item || 'object' != typeof item || !('_id' in item)) return;
3
+ const id = item._id;
4
+ return 'string' != typeof id || '' === id || /[.[\]]/.test(id) ? void 0 : id;
5
+ }
6
+ function repeatingItemPath(parentPath, item, index) {
7
+ const id = repeatingItemId(item);
8
+ return null != id ? `${parentPath}[id=${id}]` : `${parentPath}[${index}]`;
9
+ }
10
+ function moveRepeatingItems(items, moveFromIndex, moveToIndex) {
11
+ if (0 === items.length) return null;
12
+ const fromIndex = Math.max(0, Math.min(moveFromIndex, items.length - 1));
13
+ const toIndex = Math.max(0, Math.min(moveToIndex, items.length - 1));
14
+ if (fromIndex === toIndex) return null;
15
+ const source = items[fromIndex];
16
+ const moved = [
17
+ ...items
18
+ ];
19
+ const [item] = moved.splice(fromIndex, 1);
20
+ moved.splice(toIndex, 0, item);
21
+ return {
22
+ items: moved,
23
+ itemId: repeatingItemId(source) ?? String(fromIndex),
24
+ fromIndex,
25
+ toIndex
26
+ };
27
+ }
28
+ export { moveRepeatingItems, repeatingItemId, repeatingItemPath };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/admin",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "4.4.0",
5
+ "version": "4.5.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -168,10 +168,10 @@
168
168
  "react-diff-viewer-continued": "^4.4.0",
169
169
  "uuid": "^14.0.1",
170
170
  "zod": "^4.4.3",
171
- "@byline/auth": "4.4.0",
172
- "@byline/i18n": "4.4.0",
173
- "@byline/ui": "4.4.0",
174
- "@byline/core": "4.4.0"
171
+ "@byline/auth": "4.5.0",
172
+ "@byline/ui": "4.5.0",
173
+ "@byline/i18n": "4.5.0",
174
+ "@byline/core": "4.5.0"
175
175
  },
176
176
  "peerDependencies": {
177
177
  "react": "^19.0.0",