@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
|
@@ -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 {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseInstancePath } from "@byline/core";
|
|
1
2
|
import { get } from "./nested-path.js";
|
|
2
3
|
async function executeUploads(pendingUploads, uploadField, executionContext) {
|
|
3
4
|
const results = [];
|
|
@@ -36,16 +37,14 @@ function buildUploadFormData(fieldPath, upload, executionContext) {
|
|
|
36
37
|
formData.append('field', uploadFieldName(fieldPath));
|
|
37
38
|
formData.append('fieldPath', fieldPath);
|
|
38
39
|
if (executionContext?.documentId) formData.append('documentId', executionContext.documentId);
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (void 0 !== serialized) formData.append(leafName(contextPath), serialized);
|
|
48
|
-
}
|
|
40
|
+
const formValues = executionContext?.getFormValues?.();
|
|
41
|
+
const contextPaths = executionContext?.fields != null ? findUploadFieldByPath(executionContext.fields, fieldPath, formValues)?.context : void 0;
|
|
42
|
+
if (contextPaths && contextPaths.length > 0 && formValues) for (const contextPath of contextPaths){
|
|
43
|
+
const resolvedPath = resolveContextPath(fieldPath, contextPath);
|
|
44
|
+
if (void 0 === resolvedPath) continue;
|
|
45
|
+
const value = '' === resolvedPath ? formValues : get(formValues, resolvedPath);
|
|
46
|
+
const serialized = serializeContextValue(value);
|
|
47
|
+
if (void 0 !== serialized) formData.append(leafName(contextPath), serialized);
|
|
49
48
|
}
|
|
50
49
|
return formData;
|
|
51
50
|
}
|
|
@@ -89,27 +88,49 @@ function serializeContextValue(value) {
|
|
|
89
88
|
function isRelationEnvelope(value) {
|
|
90
89
|
return 'object' == typeof value && null !== value && 'string' == typeof value.targetDocumentId;
|
|
91
90
|
}
|
|
92
|
-
function findUploadFieldByPath(fields, fieldPath) {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
91
|
+
function findUploadFieldByPath(fields, fieldPath, formValues) {
|
|
92
|
+
const parsed = parseInstancePath(fieldPath);
|
|
93
|
+
if (!parsed.ok) return;
|
|
94
|
+
return resolveUploadConfig(fields, parsed.segments, formValues);
|
|
95
|
+
}
|
|
96
|
+
function selectItem(value, segment) {
|
|
97
|
+
if (!Array.isArray(value)) return;
|
|
98
|
+
if ('index' === segment.kind) return value[segment.index];
|
|
99
|
+
if ('id' === segment.kind) return value.find((item)=>item?._id === segment.id);
|
|
100
|
+
}
|
|
101
|
+
function resolveUploadConfig(fields, segments, value) {
|
|
102
|
+
const head = segments[0];
|
|
103
|
+
if (null == head || 'field' !== head.kind) return;
|
|
104
|
+
const field = fields.find((candidate)=>candidate.name === head.name);
|
|
105
|
+
if (null == field) return;
|
|
106
|
+
const fieldValue = value?.[head.name];
|
|
107
|
+
const rest = segments.slice(1);
|
|
108
|
+
if (0 === rest.length) return 'image' === field.type || 'file' === field.type ? field.upload : void 0;
|
|
109
|
+
if ('group' === field.type) return resolveUploadConfig(field.fields, rest, fieldValue);
|
|
110
|
+
if ('array' === field.type) {
|
|
111
|
+
const selector = rest[0];
|
|
112
|
+
if (selector?.kind === 'index' || selector?.kind === 'id') return resolveUploadConfig(field.fields, rest.slice(1), selectItem(fieldValue, selector));
|
|
113
|
+
return resolveUploadConfig(field.fields, rest, void 0);
|
|
114
|
+
}
|
|
115
|
+
if ('blocks' === field.type) {
|
|
116
|
+
const selector = rest[0];
|
|
117
|
+
const remainder = selector?.kind === 'index' || selector?.kind === 'id' ? rest.slice(1) : rest;
|
|
118
|
+
const item = selector?.kind === 'index' || selector?.kind === 'id' ? selectItem(fieldValue, selector) : void 0;
|
|
119
|
+
const blockType = item?._type;
|
|
120
|
+
if ('string' == typeof blockType) {
|
|
121
|
+
const block = field.blocks.find((candidate)=>candidate.blockType === blockType);
|
|
122
|
+
return null == block ? void 0 : resolveUploadConfig(block.fields, remainder, item);
|
|
104
123
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
124
|
+
let found;
|
|
125
|
+
let matches = 0;
|
|
126
|
+
for (const block of field.blocks){
|
|
127
|
+
const result = resolveUploadConfig(block.fields, remainder, void 0);
|
|
128
|
+
if (void 0 !== result) {
|
|
129
|
+
matches += 1;
|
|
130
|
+
found = result;
|
|
131
|
+
}
|
|
111
132
|
}
|
|
112
|
-
|
|
133
|
+
return 1 === matches ? found : void 0;
|
|
113
134
|
}
|
|
114
135
|
}
|
|
115
136
|
async function executeUploadsWithProgress(pendingUploads, uploadField, onProgress, executionContext) {
|
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.4.1",
|
|
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/
|
|
172
|
-
"@byline/
|
|
173
|
-
"@byline/i18n": "4.
|
|
174
|
-
"@byline/
|
|
171
|
+
"@byline/auth": "4.4.1",
|
|
172
|
+
"@byline/ui": "4.4.1",
|
|
173
|
+
"@byline/i18n": "4.4.1",
|
|
174
|
+
"@byline/core": "4.4.1"
|
|
175
175
|
},
|
|
176
176
|
"peerDependencies": {
|
|
177
177
|
"react": "^19.0.0",
|
|
@@ -18,6 +18,8 @@ import { defaultScalarForField } from '../../fields/field-helpers'
|
|
|
18
18
|
import { FieldRenderer } from '../../fields/field-renderer'
|
|
19
19
|
import { SortableItem, StaticItem } from '../../fields/sortable-item'
|
|
20
20
|
import { useFormContext } from '../../forms/form-context'
|
|
21
|
+
import { hasExistingIdTargets } from '../../forms/nested-path'
|
|
22
|
+
import { moveRepeatingItems, repeatingItemId, repeatingItemPath } from '../../forms/repeating-items'
|
|
21
23
|
import styles from './array-field.module.css'
|
|
22
24
|
|
|
23
25
|
// ---------------------------------------------------------------------------
|
|
@@ -31,7 +33,6 @@ export const ArrayField = ({
|
|
|
31
33
|
defaultValue,
|
|
32
34
|
path,
|
|
33
35
|
disableSorting = false,
|
|
34
|
-
collectionPath,
|
|
35
36
|
contentLocale,
|
|
36
37
|
fieldAdmin,
|
|
37
38
|
}: {
|
|
@@ -39,13 +40,6 @@ export const ArrayField = ({
|
|
|
39
40
|
defaultValue: any
|
|
40
41
|
path: string
|
|
41
42
|
disableSorting?: boolean
|
|
42
|
-
/**
|
|
43
|
-
* Collection path forwarded to upload-capable fields (`file` / `image`)
|
|
44
|
-
* nested inside an array item, which need it to reach the `/upload`
|
|
45
|
-
* endpoint. Without it those fields fall back to their empty placeholder
|
|
46
|
-
* and never render an upload widget.
|
|
47
|
-
*/
|
|
48
|
-
collectionPath?: string
|
|
49
43
|
/**
|
|
50
44
|
* Active content locale, forwarded to each array item's fields so
|
|
51
45
|
* localized widgets nested inside an array (e.g. a `localized` richText)
|
|
@@ -63,7 +57,8 @@ export const ArrayField = ({
|
|
|
63
57
|
*/
|
|
64
58
|
fieldAdmin?: Record<string, FieldAdminConfig>
|
|
65
59
|
}) => {
|
|
66
|
-
const { appendPatch, getFieldValue, getFieldValues, setFieldStore } =
|
|
60
|
+
const { appendPatch, getFieldValue, getFieldValues, removePendingUploadsUnder, setFieldStore } =
|
|
61
|
+
useFormContext()
|
|
67
62
|
const { t } = useTranslation('byline-admin')
|
|
68
63
|
const [items, setItems] = useState<{ id: string; data: any }[]>([])
|
|
69
64
|
|
|
@@ -80,10 +75,10 @@ export const ArrayField = ({
|
|
|
80
75
|
setItems(
|
|
81
76
|
source.map((item: any) => ({
|
|
82
77
|
id:
|
|
83
|
-
item && typeof item === 'object' && '
|
|
84
|
-
? String((item as {
|
|
85
|
-
: item && typeof item === 'object' && '
|
|
86
|
-
? String((item as {
|
|
78
|
+
item && typeof item === 'object' && '_id' in item
|
|
79
|
+
? String((item as { _id: string })._id)
|
|
80
|
+
: item && typeof item === 'object' && 'id' in item
|
|
81
|
+
? String((item as { id: string }).id)
|
|
87
82
|
: crypto.randomUUID(),
|
|
88
83
|
data: item,
|
|
89
84
|
}))
|
|
@@ -93,23 +88,6 @@ export const ArrayField = ({
|
|
|
93
88
|
}
|
|
94
89
|
}, [defaultValue, getFieldValue, path])
|
|
95
90
|
|
|
96
|
-
/**
|
|
97
|
-
* Stable patch identity for an array item. Persisted items carry `_id`
|
|
98
|
-
* (the array-item identity from `store_meta`) — that is what the server's
|
|
99
|
-
* patch engine matches on (`applyArrayPatch`: `item._id === patch.itemId`),
|
|
100
|
-
* so it MUST be preferred here. `id` is accepted as a legacy/seed-data
|
|
101
|
-
* alias. Items added this session have neither (the storage layer assigns
|
|
102
|
-
* `_id` at write time), so fall back to the item's current index — the
|
|
103
|
-
* patch engine resolves a pure-integer itemId as an index fallback.
|
|
104
|
-
*/
|
|
105
|
-
const patchItemId = (item: unknown, index: number): string => {
|
|
106
|
-
if (item && typeof item === 'object') {
|
|
107
|
-
if ('_id' in item) return String((item as { _id: string })._id)
|
|
108
|
-
if ('id' in item) return String((item as { id: string }).id)
|
|
109
|
-
}
|
|
110
|
-
return String(index)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
91
|
const handleDragEnd = ({
|
|
114
92
|
moveFromIndex,
|
|
115
93
|
moveToIndex,
|
|
@@ -117,23 +95,20 @@ export const ArrayField = ({
|
|
|
117
95
|
moveFromIndex: number
|
|
118
96
|
moveToIndex: number
|
|
119
97
|
}) => {
|
|
120
|
-
setItems((prev) => moveItem(prev, moveFromIndex, moveToIndex))
|
|
121
98
|
const currentArray = (getFieldValue(path) ?? defaultValue) as any[]
|
|
99
|
+
if (!Array.isArray(currentArray)) return
|
|
122
100
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const clampedTo = Math.max(0, Math.min(moveToIndex, currentArray.length - 1))
|
|
126
|
-
if (clampedFrom === clampedTo) return
|
|
127
|
-
|
|
128
|
-
const item = currentArray[clampedFrom]
|
|
101
|
+
const move = moveRepeatingItems(currentArray, moveFromIndex, moveToIndex)
|
|
102
|
+
if (move == null) return
|
|
129
103
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
104
|
+
setItems((prev) => moveItem(prev, move.fromIndex, move.toIndex))
|
|
105
|
+
setFieldStore(path, move.items)
|
|
106
|
+
appendPatch({
|
|
107
|
+
kind: 'array.move',
|
|
108
|
+
path,
|
|
109
|
+
itemId: move.itemId,
|
|
110
|
+
toIndex: move.toIndex,
|
|
111
|
+
})
|
|
137
112
|
}
|
|
138
113
|
|
|
139
114
|
const handleAddItem = async (atIndex?: number) => {
|
|
@@ -161,6 +136,11 @@ export const ArrayField = ({
|
|
|
161
136
|
}
|
|
162
137
|
}
|
|
163
138
|
|
|
139
|
+
// Defaults may resolve asynchronously. If an enclosing stable-id item was
|
|
140
|
+
// removed in the meantime, do not append a structural patch that would
|
|
141
|
+
// recreate that missing parent on the server.
|
|
142
|
+
if (!hasExistingIdTargets(getFieldValues(), path)) return
|
|
143
|
+
|
|
164
144
|
const currentArray = (getFieldValue(path) ?? defaultValue) as any[]
|
|
165
145
|
const insertAt = atIndex != null ? atIndex : currentArray ? currentArray.length : 0
|
|
166
146
|
|
|
@@ -188,13 +168,15 @@ export const ArrayField = ({
|
|
|
188
168
|
if (!Array.isArray(currentArray) || index < 0 || index >= currentArray.length) return
|
|
189
169
|
|
|
190
170
|
const item = currentArray[index]
|
|
171
|
+
const itemPath = repeatingItemPath(path, item, index)
|
|
191
172
|
|
|
192
173
|
setItems((prev) => prev.filter((_, i) => i !== index))
|
|
174
|
+
removePendingUploadsUnder(itemPath)
|
|
193
175
|
|
|
194
176
|
appendPatch({
|
|
195
177
|
kind: 'array.remove',
|
|
196
178
|
path: path,
|
|
197
|
-
itemId:
|
|
179
|
+
itemId: repeatingItemId(item) ?? String(index),
|
|
198
180
|
})
|
|
199
181
|
|
|
200
182
|
const newArrayValue = [...currentArray]
|
|
@@ -208,7 +190,7 @@ export const ArrayField = ({
|
|
|
208
190
|
|
|
209
191
|
const renderItem = (itemWrapper: { id: string; data: any }, index: number) => {
|
|
210
192
|
const item = itemWrapper.data
|
|
211
|
-
const arrayElementPath =
|
|
193
|
+
const arrayElementPath = repeatingItemPath(path, item, index)
|
|
212
194
|
|
|
213
195
|
if (!item || typeof item !== 'object') return null
|
|
214
196
|
|
|
@@ -242,7 +224,6 @@ export const ArrayField = ({
|
|
|
242
224
|
defaultValue={groupData[innerField.name]}
|
|
243
225
|
basePath={`${arrayElementPath}.${childField.name}`}
|
|
244
226
|
disableSorting={true}
|
|
245
|
-
collectionPath={collectionPath}
|
|
246
227
|
contentLocale={contentLocale}
|
|
247
228
|
components={groupAdmin?.[innerField.name]?.components}
|
|
248
229
|
editor={groupAdmin?.[innerField.name]?.editor}
|
|
@@ -260,7 +241,6 @@ export const ArrayField = ({
|
|
|
260
241
|
defaultValue={initial}
|
|
261
242
|
basePath={arrayElementPath}
|
|
262
243
|
disableSorting={true}
|
|
263
|
-
collectionPath={collectionPath}
|
|
264
244
|
contentLocale={contentLocale}
|
|
265
245
|
components={fieldAdmin?.[childField.name]?.components}
|
|
266
246
|
editor={fieldAdmin?.[childField.name]?.editor}
|
|
@@ -31,6 +31,8 @@ import { defaultScalarForField } from '../../fields/field-helpers'
|
|
|
31
31
|
import { GroupField } from '../../fields/group/group-field'
|
|
32
32
|
import { SortableItem } from '../../fields/sortable-item'
|
|
33
33
|
import { useFormContext } from '../../forms/form-context'
|
|
34
|
+
import { hasExistingIdTargets } from '../../forms/nested-path'
|
|
35
|
+
import { moveRepeatingItems, repeatingItemId, repeatingItemPath } from '../../forms/repeating-items'
|
|
34
36
|
import styles from './blocks-field.module.css'
|
|
35
37
|
|
|
36
38
|
// ---------------------------------------------------------------------------
|
|
@@ -54,7 +56,8 @@ export const BlocksField = ({
|
|
|
54
56
|
*/
|
|
55
57
|
contentLocale?: string
|
|
56
58
|
}) => {
|
|
57
|
-
const { appendPatch, getFieldValue, getFieldValues, setFieldStore } =
|
|
59
|
+
const { appendPatch, getFieldValue, getFieldValues, removePendingUploadsUnder, setFieldStore } =
|
|
60
|
+
useFormContext()
|
|
58
61
|
const { t } = useTranslation('byline-admin')
|
|
59
62
|
const [items, setItems] = useState<{ id: string; data: any }[]>([])
|
|
60
63
|
const [showAddBlockModal, setShowAddBlockModal] = useState(false)
|
|
@@ -118,27 +121,20 @@ export const BlocksField = ({
|
|
|
118
121
|
moveFromIndex: number
|
|
119
122
|
moveToIndex: number
|
|
120
123
|
}) => {
|
|
121
|
-
setItems((prev) => moveItem(prev, moveFromIndex, moveToIndex))
|
|
122
124
|
const currentArray = (getFieldValue(path) ?? defaultValue) as any[]
|
|
125
|
+
if (!Array.isArray(currentArray)) return
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
appendPatch({
|
|
136
|
-
kind: 'array.move',
|
|
137
|
-
path: path,
|
|
138
|
-
itemId,
|
|
139
|
-
toIndex: clampedTo,
|
|
140
|
-
})
|
|
141
|
-
}
|
|
127
|
+
const move = moveRepeatingItems(currentArray, moveFromIndex, moveToIndex)
|
|
128
|
+
if (move == null) return
|
|
129
|
+
|
|
130
|
+
setItems((prev) => moveItem(prev, move.fromIndex, move.toIndex))
|
|
131
|
+
setFieldStore(path, move.items)
|
|
132
|
+
appendPatch({
|
|
133
|
+
kind: 'array.move',
|
|
134
|
+
path,
|
|
135
|
+
itemId: move.itemId,
|
|
136
|
+
toIndex: move.toIndex,
|
|
137
|
+
})
|
|
142
138
|
}
|
|
143
139
|
|
|
144
140
|
const handleAddItem = async (forcedVariantName?: string, atIndex?: number) => {
|
|
@@ -163,6 +159,8 @@ export const BlocksField = ({
|
|
|
163
159
|
newItem[f.name] = await defaultScalarForField(f, getFieldValues)
|
|
164
160
|
}
|
|
165
161
|
|
|
162
|
+
if (!hasExistingIdTargets(getFieldValues(), path)) return
|
|
163
|
+
|
|
166
164
|
const currentArray = (getFieldValue(path) ?? defaultValue) as any[]
|
|
167
165
|
const insertAt = atIndex != null ? atIndex : currentArray ? currentArray.length : 0
|
|
168
166
|
|
|
@@ -190,12 +188,11 @@ export const BlocksField = ({
|
|
|
190
188
|
if (!Array.isArray(currentArray) || index < 0 || index >= currentArray.length) return
|
|
191
189
|
|
|
192
190
|
const item = currentArray[index]
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
? String((item as { _id: string })._id)
|
|
196
|
-
: String(index)
|
|
191
|
+
const itemPath = repeatingItemPath(path, item, index)
|
|
192
|
+
const itemId = repeatingItemId(item) ?? String(index)
|
|
197
193
|
|
|
198
194
|
setItems((prev) => prev.filter((_, i) => i !== index))
|
|
195
|
+
removePendingUploadsUnder(itemPath)
|
|
199
196
|
|
|
200
197
|
appendPatch({
|
|
201
198
|
kind: 'array.remove',
|
|
@@ -219,7 +216,7 @@ export const BlocksField = ({
|
|
|
219
216
|
|
|
220
217
|
const renderItem = (itemWrapper: { id: string; data: any }, index: number) => {
|
|
221
218
|
const item = itemWrapper.data
|
|
222
|
-
const arrayElementPath =
|
|
219
|
+
const arrayElementPath = repeatingItemPath(path, item, index)
|
|
223
220
|
|
|
224
221
|
if (!item || typeof item !== 'object' || typeof item._type !== 'string') return null
|
|
225
222
|
|
|
@@ -233,9 +230,9 @@ export const BlocksField = ({
|
|
|
233
230
|
// Render the block's children directly with arrayElementPath as the
|
|
234
231
|
// path (not basePath). FieldRenderer would append the group name
|
|
235
232
|
// (e.g. "richTextBlock") producing paths like
|
|
236
|
-
// "content[
|
|
233
|
+
// "content[id=...].richTextBlock.constrainedWidth", but the flat block
|
|
237
234
|
// shape stores fields directly on the item so the correct path is
|
|
238
|
-
// "content[
|
|
235
|
+
// "content[id=...].constrainedWidth".
|
|
239
236
|
const body = (
|
|
240
237
|
<GroupField
|
|
241
238
|
key={subField.blockType}
|
|
@@ -24,7 +24,7 @@ const CodeEditor = React.lazy(() => import('./code-editor'))
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Resolve the form-store path of a sibling field (same group/block/array
|
|
27
|
-
* item scope). `content[
|
|
27
|
+
* item scope). `content[id=x].code` + `language` → `content[id=x].language`;
|
|
28
28
|
* a top-level `code` + `language` → `language`.
|
|
29
29
|
*/
|
|
30
30
|
const siblingFieldPath = (fieldPath: string, siblingName: string): string => {
|
|
@@ -49,8 +49,6 @@ interface FieldRendererProps {
|
|
|
49
49
|
basePath?: string
|
|
50
50
|
disableSorting?: boolean
|
|
51
51
|
hideLabel?: boolean
|
|
52
|
-
/** Collection path (e.g. `'media'`) forwarded to upload-capable fields. */
|
|
53
|
-
collectionPath?: string
|
|
54
52
|
/**
|
|
55
53
|
* The active content locale (e.g. `'en'`, `'fr'`). When provided and
|
|
56
54
|
* `field.localized === true`, a small locale badge is shown so the editor
|
|
@@ -86,7 +84,6 @@ export const FieldRenderer = ({
|
|
|
86
84
|
basePath,
|
|
87
85
|
disableSorting,
|
|
88
86
|
hideLabel,
|
|
89
|
-
collectionPath,
|
|
90
87
|
contentLocale,
|
|
91
88
|
components,
|
|
92
89
|
editor,
|
|
@@ -256,7 +253,6 @@ export const FieldRenderer = ({
|
|
|
256
253
|
defaultValue={defaultValue}
|
|
257
254
|
onChange={handleChange}
|
|
258
255
|
path={path}
|
|
259
|
-
collectionPath={collectionPath}
|
|
260
256
|
/>
|
|
261
257
|
)
|
|
262
258
|
case 'image':
|
|
@@ -266,7 +262,6 @@ export const FieldRenderer = ({
|
|
|
266
262
|
defaultValue={defaultValue}
|
|
267
263
|
onChange={handleChange}
|
|
268
264
|
path={path}
|
|
269
|
-
collectionPath={collectionPath}
|
|
270
265
|
/>
|
|
271
266
|
)
|
|
272
267
|
case 'relation':
|
|
@@ -301,7 +296,6 @@ export const FieldRenderer = ({
|
|
|
301
296
|
defaultValue={defaultValue}
|
|
302
297
|
path={path}
|
|
303
298
|
disableSorting={disableSorting}
|
|
304
|
-
collectionPath={collectionPath}
|
|
305
299
|
contentLocale={contentLocale}
|
|
306
300
|
fieldAdmin={fieldAdmin}
|
|
307
301
|
/>
|
|
@@ -324,7 +318,6 @@ export const FieldRenderer = ({
|
|
|
324
318
|
defaultValue={defaultValue}
|
|
325
319
|
path={path}
|
|
326
320
|
disableSorting={disableSorting}
|
|
327
|
-
collectionPath={collectionPath}
|
|
328
321
|
contentLocale={contentLocale}
|
|
329
322
|
fieldAdmin={fieldAdmin}
|
|
330
323
|
/>
|