@byline/admin 4.3.0 → 4.4.1
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.d.ts +1 -8
- package/dist/fields/array/array-field.js +21 -27
- package/dist/fields/blocks/blocks-field.js +19 -17
- package/dist/fields/field-renderer.d.ts +1 -3
- package/dist/fields/field-renderer.js +3 -7
- package/dist/fields/file/file-field.d.ts +1 -3
- package/dist/fields/file/file-field.js +2 -2
- package/dist/fields/file/file-upload-field.js +2 -2
- package/dist/fields/group/group-field.d.ts +1 -7
- package/dist/fields/group/group-field.js +1 -2
- package/dist/fields/image/image-field.d.ts +1 -3
- package/dist/fields/image/image-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 +22 -2
- package/dist/forms/form-context.js +19 -4
- package/dist/forms/form-renderer.js +3 -1
- 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/dist/forms/upload-executor.js +50 -29
- package/package.json +5 -5
- package/src/fields/array/array-field.tsx +28 -48
- package/src/fields/blocks/blocks-field.tsx +24 -27
- package/src/fields/code/code-field.tsx +1 -1
- package/src/fields/field-renderer.tsx +0 -7
- package/src/fields/file/file-field.tsx +4 -4
- package/src/fields/file/file-upload-field.tsx +9 -5
- package/src/fields/group/group-field.tsx +0 -8
- package/src/fields/image/image-field.tsx +4 -4
- package/src/fields/image/image-upload-field.tsx +24 -6
- package/src/forms/form-context.tsx +52 -4
- package/src/forms/form-renderer.tsx +3 -1
- 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 +248 -0
- package/src/forms/upload-executor.ts +94 -30
|
@@ -6,18 +6,11 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import type { ArrayField as ArrayFieldType, FieldAdminConfig } from '@byline/core';
|
|
9
|
-
export declare const ArrayField: ({ field, defaultValue, path, disableSorting,
|
|
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)
|
|
@@ -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
|
-
const ArrayField = ({ field, defaultValue, path, disableSorting = false,
|
|
13
|
-
const { appendPatch, getFieldValue, getFieldValues, setFieldStore } = useFormContext();
|
|
14
|
+
const ArrayField = ({ field, defaultValue, path, disableSorting = false, contentLocale, fieldAdmin })=>{
|
|
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, collect
|
|
|
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, collect
|
|
|
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, collect
|
|
|
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, collect
|
|
|
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;
|
|
@@ -127,7 +123,6 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, collect
|
|
|
127
123
|
defaultValue: groupData[innerField.name],
|
|
128
124
|
basePath: `${arrayElementPath}.${childField.name}`,
|
|
129
125
|
disableSorting: true,
|
|
130
|
-
collectionPath: collectionPath,
|
|
131
126
|
contentLocale: contentLocale,
|
|
132
127
|
components: groupAdmin?.[innerField.name]?.components,
|
|
133
128
|
editor: groupAdmin?.[innerField.name]?.editor,
|
|
@@ -141,7 +136,6 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, collect
|
|
|
141
136
|
defaultValue: initial,
|
|
142
137
|
basePath: arrayElementPath,
|
|
143
138
|
disableSorting: true,
|
|
144
|
-
collectionPath: collectionPath,
|
|
145
139
|
contentLocale: contentLocale,
|
|
146
140
|
components: fieldAdmin?.[childField.name]?.components,
|
|
147
141
|
editor: fieldAdmin?.[childField.name]?.editor,
|
|
@@ -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;
|
|
@@ -12,8 +12,6 @@ interface FieldRendererProps {
|
|
|
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
|
|
@@ -42,5 +40,5 @@ interface FieldRendererProps {
|
|
|
42
40
|
*/
|
|
43
41
|
fieldAdmin?: Record<string, FieldAdminConfig>;
|
|
44
42
|
}
|
|
45
|
-
export declare const FieldRenderer: ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel,
|
|
43
|
+
export declare const FieldRenderer: ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, contentLocale, components, editor, fieldAdmin, }: FieldRendererProps) => import("react").JSX.Element | null;
|
|
46
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,
|
|
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, {
|
|
@@ -204,7 +202,6 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
|
|
|
204
202
|
defaultValue: defaultValue,
|
|
205
203
|
path: path,
|
|
206
204
|
disableSorting: disableSorting,
|
|
207
|
-
collectionPath: collectionPath,
|
|
208
205
|
contentLocale: contentLocale,
|
|
209
206
|
fieldAdmin: fieldAdmin
|
|
210
207
|
});
|
|
@@ -223,7 +220,6 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
|
|
|
223
220
|
defaultValue: defaultValue,
|
|
224
221
|
path: path,
|
|
225
222
|
disableSorting: disableSorting,
|
|
226
|
-
collectionPath: collectionPath,
|
|
227
223
|
contentLocale: contentLocale,
|
|
228
224
|
fieldAdmin: fieldAdmin
|
|
229
225
|
});
|
|
@@ -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,
|
|
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,
|
|
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);
|
|
@@ -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
|
}, [
|
|
@@ -20,12 +20,6 @@ interface GroupFieldProps {
|
|
|
20
20
|
* listeners.
|
|
21
21
|
*/
|
|
22
22
|
disableSorting?: boolean;
|
|
23
|
-
/**
|
|
24
|
-
* Collection path forwarded to upload-capable child fields (`file` / `image`),
|
|
25
|
-
* which need it to reach the `/upload` endpoint. Without it those fields fall
|
|
26
|
-
* back to their empty placeholder and never render an upload widget.
|
|
27
|
-
*/
|
|
28
|
-
collectionPath?: string;
|
|
29
23
|
/**
|
|
30
24
|
* Active content locale, forwarded to child fields so localized widgets
|
|
31
25
|
* nested inside the group (e.g. a `localized` richText) can render their
|
|
@@ -44,5 +38,5 @@ interface GroupFieldProps {
|
|
|
44
38
|
*/
|
|
45
39
|
fieldAdmin?: Record<string, FieldAdminConfig>;
|
|
46
40
|
}
|
|
47
|
-
export declare const GroupField: ({ field, defaultValue, path, disableSorting,
|
|
41
|
+
export declare const GroupField: ({ field, defaultValue, path, disableSorting, contentLocale, fieldAdmin, }: GroupFieldProps) => import("react").JSX.Element;
|
|
48
42
|
export {};
|
|
@@ -7,7 +7,7 @@ import { placeholderForField } from "../field-helpers.js";
|
|
|
7
7
|
import { FieldRenderer } from "../field-renderer.js";
|
|
8
8
|
import { useFieldError } from "../../forms/form-context.js";
|
|
9
9
|
import group_field_module from "./group-field.module.js";
|
|
10
|
-
const GroupField = ({ field, defaultValue, path, disableSorting = true,
|
|
10
|
+
const GroupField = ({ field, defaultValue, path, disableSorting = true, contentLocale, fieldAdmin })=>{
|
|
11
11
|
const fieldError = useFieldError(field.name);
|
|
12
12
|
const groupData = useMemo(()=>{
|
|
13
13
|
if (defaultValue && 'object' == typeof defaultValue && !Array.isArray(defaultValue)) return defaultValue;
|
|
@@ -48,7 +48,6 @@ const GroupField = ({ field, defaultValue, path, disableSorting = true, collecti
|
|
|
48
48
|
defaultValue: groupData[innerField.name],
|
|
49
49
|
basePath: path,
|
|
50
50
|
disableSorting: disableSorting,
|
|
51
|
-
collectionPath: collectionPath,
|
|
52
51
|
contentLocale: contentLocale,
|
|
53
52
|
components: fieldAdmin?.[innerField.name]?.components,
|
|
54
53
|
editor: fieldAdmin?.[innerField.name]?.editor,
|
|
@@ -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,
|
|
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,
|
|
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;
|
|
@@ -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 {};
|
|
@@ -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;
|
|
@@ -81,8 +94,9 @@ interface FormContextType {
|
|
|
81
94
|
subscribeField: (name: string, listener: FieldListener) => () => void;
|
|
82
95
|
subscribeErrors: (listener: ErrorsListener) => () => void;
|
|
83
96
|
subscribeMeta: (listener: MetaListener) => () => void;
|
|
84
|
-
addPendingUpload: (fieldPath: string, upload: PendingUpload) =>
|
|
97
|
+
addPendingUpload: (fieldPath: string, upload: PendingUpload) => boolean;
|
|
85
98
|
removePendingUpload: (fieldPath: string) => void;
|
|
99
|
+
removePendingUploadsUnder: (itemPath: string) => void;
|
|
86
100
|
getPendingUploads: () => Map<string, PendingUpload>;
|
|
87
101
|
hasPendingUploads: () => boolean;
|
|
88
102
|
clearPendingUploads: () => void;
|
|
@@ -97,7 +111,7 @@ interface FormContextType {
|
|
|
97
111
|
subscribeSystemAvailableLocales: (listener: SystemAvailableLocalesListener) => () => void;
|
|
98
112
|
}
|
|
99
113
|
export declare const useFormContext: () => FormContextType;
|
|
100
|
-
export declare const FormProvider: ({ children, initialData, documentId, }: {
|
|
114
|
+
export declare const FormProvider: ({ children, initialData, documentId, collectionPath, }: {
|
|
101
115
|
children: React.ReactNode;
|
|
102
116
|
initialData?: Record<string, any>;
|
|
103
117
|
/**
|
|
@@ -105,6 +119,12 @@ export declare const FormProvider: ({ children, initialData, documentId, }: {
|
|
|
105
119
|
* the context for upload widgets honouring `upload.requireSavedDocument`.
|
|
106
120
|
*/
|
|
107
121
|
documentId?: string | null;
|
|
122
|
+
/**
|
|
123
|
+
* Path of the collection being edited. Exposed on the context so upload
|
|
124
|
+
* widgets can reach the upload endpoint from any nesting depth without
|
|
125
|
+
* every container forwarding it — see `FormContextType.collectionPath`.
|
|
126
|
+
*/
|
|
127
|
+
collectionPath?: string | null;
|
|
108
128
|
}) => React.JSX.Element;
|
|
109
129
|
/**
|
|
110
130
|
* Subscribe to the system `path` slot edited by the path widget.
|
|
@@ -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;
|
|
@@ -22,7 +23,7 @@ const useFormContext = ()=>{
|
|
|
22
23
|
if (null == context) throw new Error('useFormContext must be used within a FormProvider');
|
|
23
24
|
return context;
|
|
24
25
|
};
|
|
25
|
-
const FormProvider = ({ children, initialData = {}, documentId = null })=>{
|
|
26
|
+
const FormProvider = ({ children, initialData = {}, documentId = null, collectionPath = null })=>{
|
|
26
27
|
const fieldValues = useRef(JSON.parse(JSON.stringify(initialData?.fields ?? initialData)));
|
|
27
28
|
const initialValues = useRef(initialData?.fields ?? initialData);
|
|
28
29
|
const errorsRef = useRef([]);
|
|
@@ -95,11 +96,12 @@ const FormProvider = ({ children, initialData = {}, documentId = null })=>{
|
|
|
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 })=>{
|
|
|
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 })=>{
|
|
|
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 })=>{
|
|
|
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(()=>{
|
|
@@ -377,6 +390,7 @@ const FormProvider = ({ children, initialData = {}, documentId = null })=>{
|
|
|
377
390
|
return /*#__PURE__*/ jsx(FormContext.Provider, {
|
|
378
391
|
value: {
|
|
379
392
|
documentId,
|
|
393
|
+
collectionPath,
|
|
380
394
|
setFieldValue,
|
|
381
395
|
setFieldStore,
|
|
382
396
|
getFieldValue,
|
|
@@ -402,6 +416,7 @@ const FormProvider = ({ children, initialData = {}, documentId = null })=>{
|
|
|
402
416
|
subscribeMeta,
|
|
403
417
|
addPendingUpload,
|
|
404
418
|
removePendingUpload,
|
|
419
|
+
removePendingUploadsUnder,
|
|
405
420
|
getPendingUploads,
|
|
406
421
|
hasPendingUploads,
|
|
407
422
|
clearPendingUploads,
|
|
@@ -183,7 +183,6 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
183
183
|
return /*#__PURE__*/ jsx(FieldRenderer, {
|
|
184
184
|
field: field,
|
|
185
185
|
defaultValue: initialData?.fields?.[field.name],
|
|
186
|
-
collectionPath: collectionPath,
|
|
187
186
|
contentLocale: contentLocale,
|
|
188
187
|
components: adminConfig?.fields?.[field.name]?.components,
|
|
189
188
|
editor: adminConfig?.fields?.[field.name]?.editor,
|
|
@@ -233,6 +232,8 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
233
232
|
noValidate: true,
|
|
234
233
|
onSubmit: handleSubmit,
|
|
235
234
|
className: classnames('byline-form', form_renderer_module.form),
|
|
235
|
+
inert: isUploading ? true : void 0,
|
|
236
|
+
"aria-busy": isUploading,
|
|
236
237
|
children: [
|
|
237
238
|
/*#__PURE__*/ jsxs("div", {
|
|
238
239
|
className: classnames('byline-form-heading-row', form_renderer_module["heading-row"]),
|
|
@@ -405,6 +406,7 @@ const FormRenderer = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpu
|
|
|
405
406
|
return /*#__PURE__*/ jsx(FormProvider, {
|
|
406
407
|
initialData: initialData,
|
|
407
408
|
documentId: 'edit' === mode && 'string' == typeof initialData?.id ? initialData.id : null,
|
|
409
|
+
collectionPath: collectionPath ?? null,
|
|
408
410
|
children: /*#__PURE__*/ jsx(FormContent, {
|
|
409
411
|
mode: mode,
|
|
410
412
|
fields: fields,
|
|
@@ -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;
|