@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.
- package/dist/fields/array/array-field.js +20 -24
- package/dist/fields/blocks/blocks-field.js +19 -17
- package/dist/fields/file/file-upload-field.js +2 -2
- package/dist/fields/image/image-upload-field.js +12 -3
- package/dist/forms/__probe-nested.test.node.d.ts +1 -0
- package/dist/forms/__probe-two.test.node.d.ts +1 -0
- package/dist/forms/form-context.d.ts +2 -1
- package/dist/forms/form-context.js +17 -3
- package/dist/forms/form-renderer.js +2 -0
- package/dist/forms/nested-path.d.ts +5 -1
- package/dist/forms/nested-path.js +84 -15
- package/dist/forms/pending-uploads.d.ts +6 -0
- package/dist/forms/pending-uploads.js +11 -0
- package/dist/forms/pending-uploads.test.node.d.ts +1 -0
- package/dist/forms/repeating-items.d.ts +16 -0
- package/dist/forms/repeating-items.js +28 -0
- package/dist/forms/repeating-items.test.node.d.ts +1 -0
- package/package.json +5 -5
- package/src/fields/array/array-field.tsx +28 -38
- package/src/fields/blocks/blocks-field.tsx +24 -27
- package/src/fields/code/code-field.tsx +1 -1
- package/src/fields/file/file-upload-field.tsx +9 -5
- package/src/fields/image/image-upload-field.tsx +24 -6
- package/src/forms/form-context.tsx +31 -4
- package/src/forms/form-renderer.tsx +2 -0
- package/src/forms/nested-path.test.node.ts +50 -1
- package/src/forms/nested-path.ts +106 -18
- package/src/forms/pending-uploads.test.node.ts +23 -0
- package/src/forms/pending-uploads.ts +22 -0
- package/src/forms/repeating-items.test.node.ts +36 -0
- package/src/forms/repeating-items.ts +48 -0
- package/src/forms/upload-executor.test.node.ts +64 -3
- 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 && '
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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 =
|
|
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) =>
|
|
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,
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
|
17
|
+
for (const segment of parsed.segments){
|
|
9
18
|
if (null == current) return;
|
|
10
|
-
current = current[
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 <
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
172
|
-
"@byline/
|
|
173
|
-
"@byline/
|
|
174
|
-
"@byline/core": "4.
|
|
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",
|