@byline/admin 6.3.0 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fields/array/array-field.js +29 -12
- package/dist/fields/blocks/blocks-field.js +20 -8
- package/dist/fields/checkbox/checkbox-field.js +1 -0
- package/dist/fields/datetime/datetime-field.js +1 -0
- package/dist/fields/file/file-field.js +3 -2
- package/dist/fields/group/group-field.js +5 -2
- package/dist/fields/image/image-field.js +3 -2
- package/dist/fields/relation/relation-field.js +5 -0
- package/dist/fields/relation/relation-many-field.js +9 -2
- package/dist/fields/select/select-field.js +1 -0
- package/dist/fields/text/text-field.js +4 -2
- package/dist/fields/text-area/text-area-field.js +4 -2
- package/dist/fields/use-field-change-handler.js +24 -6
- package/dist/fields/use-field-condition.d.ts +0 -15
- package/dist/fields/use-field-condition.js +10 -1
- package/dist/forms/form-context.d.ts +3 -12
- package/dist/forms/form-context.js +111 -129
- package/dist/forms/form-layout.js +7 -2
- package/dist/forms/form-page-chrome.js +14 -24
- package/dist/forms/form-renderer.d.ts +2 -1
- package/dist/forms/form-renderer.js +12 -56
- package/dist/forms/nested-path.d.ts +2 -0
- package/dist/forms/nested-path.js +26 -1
- package/dist/forms/use-document-reload-recovery.d.ts +22 -0
- package/dist/forms/use-document-reload-recovery.js +67 -0
- package/dist/forms/use-form-submission.d.ts +1 -1
- package/dist/forms/use-form-submission.js +45 -27
- package/dist/forms/use-form-tabs.d.ts +2 -2
- package/dist/forms/use-form-tabs.js +18 -11
- package/package.json +14 -14
- package/src/fields/array/array-field.tsx +24 -12
- package/src/fields/blocks/blocks-field.tsx +18 -8
- package/src/fields/checkbox/checkbox-field.tsx +1 -0
- package/src/fields/datetime/datetime-field.tsx +1 -0
- package/src/fields/file/file-field.tsx +3 -2
- package/src/fields/group/group-field.tsx +2 -2
- package/src/fields/image/image-field.tsx +3 -2
- package/src/fields/relation/relation-field.tsx +5 -0
- package/src/fields/relation/relation-many-field.tsx +16 -2
- package/src/fields/select/select-field.tsx +1 -0
- package/src/fields/text/text-field.tsx +3 -2
- package/src/fields/text-area/text-area-field.tsx +3 -2
- package/src/fields/use-field-change-handler.ts +43 -9
- package/src/fields/use-field-condition.ts +17 -1
- package/src/forms/form-context.tsx +178 -233
- package/src/forms/form-correctness.test.tsx +560 -0
- package/src/forms/form-layout.tsx +3 -2
- package/src/forms/form-page-chrome.tsx +17 -34
- package/src/forms/form-renderer-submit.test.tsx +36 -0
- package/src/forms/form-renderer.tsx +15 -64
- package/src/forms/nested-path.ts +35 -0
- package/src/forms/use-document-reload-recovery.ts +92 -0
- package/src/forms/use-form-submission.ts +56 -27
- package/src/forms/use-form-tabs.ts +25 -17
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { useTranslation } from "@byline/i18n/react";
|
|
4
|
-
import { DraggableSortable, IconButton, PlusIcon, moveItem } from "@byline/ui/react";
|
|
4
|
+
import { DraggableSortable, ErrorText, IconButton, PlusIcon, moveItem } from "@byline/ui/react";
|
|
5
5
|
import clsx from "clsx";
|
|
6
6
|
import { sliceFieldAdmin } from "../field-admin.js";
|
|
7
7
|
import { defaultScalarForField } from "../field-helpers.js";
|
|
8
8
|
import { FieldRenderer } from "../field-renderer.js";
|
|
9
9
|
import { SortableItem, StaticItem } from "../sortable-item.js";
|
|
10
|
-
import { useFormContext } from "../../forms/form-context.js";
|
|
10
|
+
import { useFieldError, useFormContext } from "../../forms/form-context.js";
|
|
11
|
+
import { useScopedDomId } from "../../forms/form-dom-scope.js";
|
|
11
12
|
import { hasExistingIdTargets } from "../../forms/nested-path.js";
|
|
12
13
|
import { moveRepeatingItems, repeatingItemId, repeatingItemPath } from "../../forms/repeating-items.js";
|
|
13
14
|
import array_field_module from "./array-field.module.js";
|
|
14
15
|
const ArrayField = ({ field, defaultValue, path, disableSorting = false, contentLocale, fieldAdmin })=>{
|
|
15
16
|
const { appendPatch, getFieldValue, getFieldValues, removePendingUploadsUnder, setFieldStore } = useFormContext();
|
|
16
17
|
const { t } = useTranslation('byline-admin');
|
|
18
|
+
const fieldError = useFieldError(path);
|
|
19
|
+
const htmlId = useScopedDomId(path);
|
|
17
20
|
const [items, setItems] = useState([]);
|
|
18
21
|
useEffect(()=>{
|
|
19
22
|
const storeValue = getFieldValue(path);
|
|
@@ -28,6 +31,7 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
|
|
|
28
31
|
path
|
|
29
32
|
]);
|
|
30
33
|
const handleDragEnd = ({ moveFromIndex, moveToIndex })=>{
|
|
34
|
+
if (field.readOnly) return;
|
|
31
35
|
const currentArray = getFieldValue(path) ?? defaultValue;
|
|
32
36
|
if (!Array.isArray(currentArray)) return;
|
|
33
37
|
const move = moveRepeatingItems(currentArray, moveFromIndex, moveToIndex);
|
|
@@ -42,6 +46,7 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
|
|
|
42
46
|
});
|
|
43
47
|
};
|
|
44
48
|
const handleAddItem = async (atIndex)=>{
|
|
49
|
+
if (field.readOnly) return;
|
|
45
50
|
const childFields = field.fields ?? [];
|
|
46
51
|
if (0 === childFields.length) return;
|
|
47
52
|
const newId = crypto.randomUUID();
|
|
@@ -80,6 +85,7 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
|
|
|
80
85
|
setFieldStore(path, newArrayValue);
|
|
81
86
|
};
|
|
82
87
|
const handleRemoveItem = (index)=>{
|
|
88
|
+
if (field.readOnly) return;
|
|
83
89
|
const currentArray = getFieldValue(path) ?? defaultValue;
|
|
84
90
|
if (!Array.isArray(currentArray) || index < 0 || index >= currentArray.length) return;
|
|
85
91
|
const item = currentArray[index];
|
|
@@ -98,6 +104,7 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
|
|
|
98
104
|
setFieldStore(path, newArrayValue);
|
|
99
105
|
};
|
|
100
106
|
const handleInsertBelow = (index)=>{
|
|
107
|
+
if (field.readOnly) return;
|
|
101
108
|
handleAddItem(index + 1);
|
|
102
109
|
};
|
|
103
110
|
const renderItem = (itemWrapper, index)=>{
|
|
@@ -119,7 +126,10 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
|
|
|
119
126
|
children: childField.label
|
|
120
127
|
}),
|
|
121
128
|
childField.fields.map((innerField)=>/*#__PURE__*/ jsx(FieldRenderer, {
|
|
122
|
-
field:
|
|
129
|
+
field: field.readOnly || childField.readOnly ? {
|
|
130
|
+
...innerField,
|
|
131
|
+
readOnly: true
|
|
132
|
+
} : innerField,
|
|
123
133
|
defaultValue: groupData[innerField.name],
|
|
124
134
|
basePath: `${arrayElementPath}.${childField.name}`,
|
|
125
135
|
disableSorting: true,
|
|
@@ -132,7 +142,10 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
|
|
|
132
142
|
}, childField.name);
|
|
133
143
|
}
|
|
134
144
|
return /*#__PURE__*/ jsx(FieldRenderer, {
|
|
135
|
-
field:
|
|
145
|
+
field: field.readOnly ? {
|
|
146
|
+
...childField,
|
|
147
|
+
readOnly: true
|
|
148
|
+
} : childField,
|
|
136
149
|
defaultValue: initial,
|
|
137
150
|
basePath: arrayElementPath,
|
|
138
151
|
disableSorting: true,
|
|
@@ -143,10 +156,10 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
|
|
|
143
156
|
}, childField.name);
|
|
144
157
|
});
|
|
145
158
|
const itemLabel = `${field.label ?? field.name} ${index + 1}`;
|
|
146
|
-
if (disableSorting) return /*#__PURE__*/ jsx(StaticItem, {
|
|
159
|
+
if (disableSorting || field.readOnly) return /*#__PURE__*/ jsx(StaticItem, {
|
|
147
160
|
label: itemLabel,
|
|
148
|
-
onAddBelow: ()=>handleInsertBelow(index),
|
|
149
|
-
onRemove: ()=>handleRemoveItem(index),
|
|
161
|
+
onAddBelow: field.readOnly ? void 0 : ()=>handleInsertBelow(index),
|
|
162
|
+
onRemove: field.readOnly ? void 0 : ()=>handleRemoveItem(index),
|
|
150
163
|
children: /*#__PURE__*/ jsx("div", {
|
|
151
164
|
className: clsx('byline-field-array-group-fields', array_field_module["group-fields"]),
|
|
152
165
|
children: innerBody
|
|
@@ -155,8 +168,8 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
|
|
|
155
168
|
return /*#__PURE__*/ jsx(SortableItem, {
|
|
156
169
|
id: itemWrapper.id,
|
|
157
170
|
label: itemLabel,
|
|
158
|
-
onAddBelow: ()=>handleInsertBelow(index),
|
|
159
|
-
onRemove: ()=>handleRemoveItem(index),
|
|
171
|
+
onAddBelow: field.readOnly ? void 0 : ()=>handleInsertBelow(index),
|
|
172
|
+
onRemove: field.readOnly ? void 0 : ()=>handleRemoveItem(index),
|
|
160
173
|
children: /*#__PURE__*/ jsx("div", {
|
|
161
174
|
className: clsx('byline-field-array-group-fields', array_field_module["group-fields"]),
|
|
162
175
|
children: innerBody
|
|
@@ -191,11 +204,11 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
|
|
|
191
204
|
className: clsx('byline-field-array-title', array_field_module.title),
|
|
192
205
|
children: field.label
|
|
193
206
|
}),
|
|
194
|
-
disableSorting ? /*#__PURE__*/ jsxs("div", {
|
|
207
|
+
disableSorting || field.readOnly ? /*#__PURE__*/ jsxs("div", {
|
|
195
208
|
className: clsx('byline-field-array-stack', array_field_module.stack),
|
|
196
209
|
children: [
|
|
197
210
|
items.map((item, index)=>renderItem(item, index)),
|
|
198
|
-
addRow
|
|
211
|
+
!field.readOnly && addRow
|
|
199
212
|
]
|
|
200
213
|
}) : /*#__PURE__*/ jsxs(DraggableSortable, {
|
|
201
214
|
ids: items.map((i)=>i.id),
|
|
@@ -203,8 +216,12 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
|
|
|
203
216
|
className: clsx('byline-field-array-stack', array_field_module.stack),
|
|
204
217
|
children: [
|
|
205
218
|
items.map((item, index)=>renderItem(item, index)),
|
|
206
|
-
addRow
|
|
219
|
+
!field.readOnly && addRow
|
|
207
220
|
]
|
|
221
|
+
}),
|
|
222
|
+
fieldError && /*#__PURE__*/ jsx(ErrorText, {
|
|
223
|
+
id: `${htmlId}-error`,
|
|
224
|
+
text: fieldError
|
|
208
225
|
})
|
|
209
226
|
]
|
|
210
227
|
});
|
|
@@ -2,18 +2,21 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { getAdminConfig } from "@byline/core";
|
|
4
4
|
import { useTranslation } from "@byline/i18n/react";
|
|
5
|
-
import { Card, CloseIcon, DraggableSortable, IconButton, Modal, PlusIcon, moveItem } from "@byline/ui/react";
|
|
5
|
+
import { Card, CloseIcon, DraggableSortable, ErrorText, IconButton, Modal, PlusIcon, moveItem } from "@byline/ui/react";
|
|
6
6
|
import clsx from "clsx";
|
|
7
7
|
import { defaultScalarForField } from "../field-helpers.js";
|
|
8
8
|
import { GroupField } from "../group/group-field.js";
|
|
9
|
-
import { SortableItem } from "../sortable-item.js";
|
|
10
|
-
import { useFormContext } from "../../forms/form-context.js";
|
|
9
|
+
import { SortableItem, StaticItem } from "../sortable-item.js";
|
|
10
|
+
import { useFieldError, useFormContext } from "../../forms/form-context.js";
|
|
11
|
+
import { useScopedDomId } from "../../forms/form-dom-scope.js";
|
|
11
12
|
import { hasExistingIdTargets } from "../../forms/nested-path.js";
|
|
12
13
|
import { moveRepeatingItems, repeatingItemId, repeatingItemPath } from "../../forms/repeating-items.js";
|
|
13
14
|
import blocks_field_module from "./blocks-field.module.js";
|
|
14
15
|
const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
|
|
15
16
|
const { appendPatch, getFieldValue, getFieldValues, removePendingUploadsUnder, setFieldStore } = useFormContext();
|
|
16
17
|
const { t } = useTranslation('byline-admin');
|
|
18
|
+
const fieldError = useFieldError(path);
|
|
19
|
+
const htmlId = useScopedDomId(path);
|
|
17
20
|
const [items, setItems] = useState([]);
|
|
18
21
|
const [showAddBlockModal, setShowAddBlockModal] = useState(false);
|
|
19
22
|
const [pendingInsertIndex, setPendingInsertIndex] = useState(null);
|
|
@@ -45,6 +48,7 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
|
|
|
45
48
|
path
|
|
46
49
|
]);
|
|
47
50
|
const handleDragEnd = ({ moveFromIndex, moveToIndex })=>{
|
|
51
|
+
if (field.readOnly) return;
|
|
48
52
|
const currentArray = getFieldValue(path) ?? defaultValue;
|
|
49
53
|
if (!Array.isArray(currentArray)) return;
|
|
50
54
|
const move = moveRepeatingItems(currentArray, moveFromIndex, moveToIndex);
|
|
@@ -59,6 +63,7 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
|
|
|
59
63
|
});
|
|
60
64
|
};
|
|
61
65
|
const handleAddItem = async (forcedVariantName, atIndex)=>{
|
|
66
|
+
if (field.readOnly) return;
|
|
62
67
|
setShowAddBlockModal(false);
|
|
63
68
|
setPendingInsertIndex(null);
|
|
64
69
|
const variant = (null != forcedVariantName ? availableBlocks.find((v)=>v.blockType === forcedVariantName) : void 0) ?? availableBlocks[0];
|
|
@@ -97,6 +102,7 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
|
|
|
97
102
|
setFieldStore(path, newArrayValue);
|
|
98
103
|
};
|
|
99
104
|
const handleRemoveItem = (index)=>{
|
|
105
|
+
if (field.readOnly) return;
|
|
100
106
|
const currentArray = getFieldValue(path) ?? defaultValue;
|
|
101
107
|
if (!Array.isArray(currentArray) || index < 0 || index >= currentArray.length) return;
|
|
102
108
|
const item = currentArray[index];
|
|
@@ -134,6 +140,7 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
|
|
|
134
140
|
type: 'group',
|
|
135
141
|
name: subField.blockType,
|
|
136
142
|
fields: subField.fields,
|
|
143
|
+
readOnly: field.readOnly,
|
|
137
144
|
label: void 0
|
|
138
145
|
},
|
|
139
146
|
defaultValue: fieldData,
|
|
@@ -142,11 +149,12 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
|
|
|
142
149
|
contentLocale: contentLocale,
|
|
143
150
|
fieldAdmin: blockAdminByType.get(item._type)?.fields
|
|
144
151
|
}, subField.blockType);
|
|
145
|
-
|
|
152
|
+
const Item = field.readOnly ? StaticItem : SortableItem;
|
|
153
|
+
return /*#__PURE__*/ jsx(Item, {
|
|
146
154
|
id: itemWrapper.id,
|
|
147
155
|
label: label ?? subField.blockType,
|
|
148
|
-
onAddBelow: ()=>handleInsertBelow(index),
|
|
149
|
-
onRemove: ()=>handleRemoveItem(index),
|
|
156
|
+
onAddBelow: field.readOnly ? void 0 : ()=>handleInsertBelow(index),
|
|
157
|
+
onRemove: field.readOnly ? void 0 : ()=>handleRemoveItem(index),
|
|
150
158
|
children: body
|
|
151
159
|
}, itemWrapper.id);
|
|
152
160
|
};
|
|
@@ -171,7 +179,7 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
|
|
|
171
179
|
setPendingInsertIndex(null);
|
|
172
180
|
setShowAddBlockModal(true);
|
|
173
181
|
},
|
|
174
|
-
disabled: !selectedBlockName,
|
|
182
|
+
disabled: field.readOnly || !selectedBlockName,
|
|
175
183
|
"aria-label": t('fields.blocks.addBlockAriaLabel'),
|
|
176
184
|
variant: "outlined",
|
|
177
185
|
children: /*#__PURE__*/ jsx(PlusIcon, {})
|
|
@@ -179,7 +187,7 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
|
|
|
179
187
|
/*#__PURE__*/ jsx("button", {
|
|
180
188
|
type: "button",
|
|
181
189
|
tabIndex: -1,
|
|
182
|
-
disabled: !selectedBlockName,
|
|
190
|
+
disabled: field.readOnly || !selectedBlockName,
|
|
183
191
|
onClick: ()=>{
|
|
184
192
|
setPendingInsertIndex(null);
|
|
185
193
|
setShowAddBlockModal(true);
|
|
@@ -267,6 +275,10 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
|
|
|
267
275
|
})
|
|
268
276
|
]
|
|
269
277
|
})
|
|
278
|
+
}),
|
|
279
|
+
fieldError && /*#__PURE__*/ jsx(ErrorText, {
|
|
280
|
+
id: `${htmlId}-error`,
|
|
281
|
+
text: fieldError
|
|
270
282
|
})
|
|
271
283
|
]
|
|
272
284
|
});
|
|
@@ -12,6 +12,7 @@ const CheckboxField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
12
12
|
return /*#__PURE__*/ jsx("div", {
|
|
13
13
|
className: `byline-field-checkbox ${field.name} ${checkbox_field_module["field-checkbox"]}`,
|
|
14
14
|
children: /*#__PURE__*/ jsx(Checkbox, {
|
|
15
|
+
disabled: field.readOnly,
|
|
15
16
|
id: htmlId,
|
|
16
17
|
name: field.name,
|
|
17
18
|
label: field.label,
|
|
@@ -15,6 +15,7 @@ const DateTimeField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
15
15
|
className: `byline-field-datetime ${field.name}`,
|
|
16
16
|
children: [
|
|
17
17
|
/*#__PURE__*/ jsx(DatePicker, {
|
|
18
|
+
disabled: field.readOnly,
|
|
18
19
|
id: htmlId,
|
|
19
20
|
name: field.name,
|
|
20
21
|
label: field.label,
|
|
@@ -38,6 +38,7 @@ const FileField = ({ field, value, defaultValue, onChange: _onChange, path })=>{
|
|
|
38
38
|
const showUploadWidget = null == incomingValue || isOldPlaceholder(incomingValue);
|
|
39
39
|
const uploadGated = field.upload?.requireSavedDocument === true && null == documentId;
|
|
40
40
|
const handleRemove = ()=>{
|
|
41
|
+
if (field.readOnly) return;
|
|
41
42
|
if (isPending) removePendingUpload(fieldPath);
|
|
42
43
|
handleChange(null);
|
|
43
44
|
};
|
|
@@ -61,7 +62,7 @@ const FileField = ({ field, value, defaultValue, onChange: _onChange, path })=>{
|
|
|
61
62
|
role: "note",
|
|
62
63
|
"data-testid": "upload-require-saved-document",
|
|
63
64
|
children: t('fields.upload.requireSavedDocument')
|
|
64
|
-
}) : collectionPath ? /*#__PURE__*/ jsx(FileUploadField, {
|
|
65
|
+
}) : collectionPath && !field.readOnly ? /*#__PURE__*/ jsx(FileUploadField, {
|
|
65
66
|
field: field,
|
|
66
67
|
collectionPath: collectionPath,
|
|
67
68
|
fieldPath: fieldPath,
|
|
@@ -100,7 +101,7 @@ const FileField = ({ field, value, defaultValue, onChange: _onChange, path })=>{
|
|
|
100
101
|
intent: "noeffect",
|
|
101
102
|
onClick: handleRemove,
|
|
102
103
|
size: "xs",
|
|
103
|
-
disabled: isUploading,
|
|
104
|
+
disabled: isUploading || field.readOnly,
|
|
104
105
|
"aria-label": t('fields.file.removeAriaLabel'),
|
|
105
106
|
children: /*#__PURE__*/ jsx(CloseIcon, {
|
|
106
107
|
width: "15px",
|
|
@@ -10,7 +10,7 @@ import { useScopedDomId } from "../../forms/form-dom-scope.js";
|
|
|
10
10
|
import group_field_module from "./group-field.module.js";
|
|
11
11
|
const GroupField = ({ field, defaultValue, path, disableSorting = true, contentLocale, fieldAdmin })=>{
|
|
12
12
|
const htmlId = useScopedDomId(path ?? field.name);
|
|
13
|
-
const fieldError = useFieldError(field.name);
|
|
13
|
+
const fieldError = useFieldError(path ?? field.name);
|
|
14
14
|
const groupData = useMemo(()=>{
|
|
15
15
|
if (defaultValue && 'object' == typeof defaultValue && !Array.isArray(defaultValue)) return defaultValue;
|
|
16
16
|
const placeholder = {};
|
|
@@ -46,7 +46,10 @@ const GroupField = ({ field, defaultValue, path, disableSorting = true, contentL
|
|
|
46
46
|
/*#__PURE__*/ jsx("div", {
|
|
47
47
|
className: clsx('byline-field-group-body', group_field_module.body),
|
|
48
48
|
children: field.fields.map((innerField)=>/*#__PURE__*/ jsx(FieldRenderer, {
|
|
49
|
-
field:
|
|
49
|
+
field: field.readOnly ? {
|
|
50
|
+
...innerField,
|
|
51
|
+
readOnly: true
|
|
52
|
+
} : innerField,
|
|
50
53
|
defaultValue: groupData[innerField.name],
|
|
51
54
|
basePath: path,
|
|
52
55
|
disableSorting: disableSorting,
|
|
@@ -30,6 +30,7 @@ const ImageField = ({ field, value, defaultValue, onChange: _onChange, path })=>
|
|
|
30
30
|
const thumbVariant = incomingValue && !isPendingStoredFileValue(incomingValue) ? incomingValue.variants?.find((v)=>'thumbnail' === v.name) : void 0;
|
|
31
31
|
const previewUrl = thumbVariant?.storageUrl ?? incomingValue?.storageUrl;
|
|
32
32
|
const handleRemove = ()=>{
|
|
33
|
+
if (field.readOnly) return;
|
|
33
34
|
if (isPending) removePendingUpload(fieldPath);
|
|
34
35
|
handleChange(null);
|
|
35
36
|
};
|
|
@@ -53,7 +54,7 @@ const ImageField = ({ field, value, defaultValue, onChange: _onChange, path })=>
|
|
|
53
54
|
role: "note",
|
|
54
55
|
"data-testid": "upload-require-saved-document",
|
|
55
56
|
children: t('fields.upload.requireSavedDocument')
|
|
56
|
-
}) : collectionPath ? /*#__PURE__*/ jsx(ImageUploadField, {
|
|
57
|
+
}) : collectionPath && !field.readOnly ? /*#__PURE__*/ jsx(ImageUploadField, {
|
|
57
58
|
field: field,
|
|
58
59
|
collectionPath: collectionPath,
|
|
59
60
|
fieldPath: fieldPath,
|
|
@@ -79,7 +80,7 @@ const ImageField = ({ field, value, defaultValue, onChange: _onChange, path })=>
|
|
|
79
80
|
intent: "noeffect",
|
|
80
81
|
onClick: handleRemove,
|
|
81
82
|
size: "xs",
|
|
82
|
-
disabled: isUploading,
|
|
83
|
+
disabled: isUploading || field.readOnly,
|
|
83
84
|
"aria-label": t('fields.image.removeAriaLabel'),
|
|
84
85
|
children: /*#__PURE__*/ jsx(CloseIcon, {
|
|
85
86
|
width: "15px",
|
|
@@ -22,6 +22,7 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
22
22
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
23
23
|
const [pickedRecord, setPickedRecord] = useState(null);
|
|
24
24
|
const handleSelect = (selection)=>{
|
|
25
|
+
if (field.readOnly) return;
|
|
25
26
|
setPickerOpen(false);
|
|
26
27
|
selection.record ? setPickedRecord({
|
|
27
28
|
id: selection.targetDocumentId,
|
|
@@ -33,6 +34,7 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
33
34
|
});
|
|
34
35
|
};
|
|
35
36
|
const handleRemove = ()=>{
|
|
37
|
+
if (field.readOnly) return;
|
|
36
38
|
setPickedRecord(null);
|
|
37
39
|
onChange?.(null);
|
|
38
40
|
};
|
|
@@ -80,6 +82,7 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
80
82
|
className: clsx('byline-field-relation-actions', relation_field_module.actions),
|
|
81
83
|
children: [
|
|
82
84
|
/*#__PURE__*/ jsx(IconButton, {
|
|
85
|
+
disabled: field.readOnly,
|
|
83
86
|
id: htmlId,
|
|
84
87
|
type: "button",
|
|
85
88
|
intent: "noeffect",
|
|
@@ -94,6 +97,7 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
94
97
|
})
|
|
95
98
|
}),
|
|
96
99
|
/*#__PURE__*/ jsx(IconButton, {
|
|
100
|
+
disabled: field.readOnly,
|
|
97
101
|
type: "button",
|
|
98
102
|
intent: "noeffect",
|
|
99
103
|
size: "xs",
|
|
@@ -110,6 +114,7 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
110
114
|
})
|
|
111
115
|
]
|
|
112
116
|
}) : /*#__PURE__*/ jsx(Button, {
|
|
117
|
+
disabled: field.readOnly,
|
|
113
118
|
id: htmlId,
|
|
114
119
|
size: "xs",
|
|
115
120
|
variant: "outlined",
|
|
@@ -9,9 +9,10 @@ import { useScopedDomId } from "../../forms/form-dom-scope.js";
|
|
|
9
9
|
import relation_field_module from "./relation-field.module.js";
|
|
10
10
|
import { RelationPicker } from "./relation-picker.js";
|
|
11
11
|
import { RelationSummary } from "./relation-summary.js";
|
|
12
|
-
const RelationManyTile = ({ id, children, onRemove, removeAriaLabel })=>{
|
|
12
|
+
const RelationManyTile = ({ id, children, onRemove, removeAriaLabel, readOnly })=>{
|
|
13
13
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
14
14
|
id,
|
|
15
|
+
disabled: readOnly,
|
|
15
16
|
transition: {
|
|
16
17
|
duration: 250,
|
|
17
18
|
easing: 'cubic-bezier(0, 0.2, 0.2, 1)'
|
|
@@ -31,6 +32,7 @@ const RelationManyTile = ({ id, children, onRemove, removeAriaLabel })=>{
|
|
|
31
32
|
]),
|
|
32
33
|
children: [
|
|
33
34
|
/*#__PURE__*/ jsx("button", {
|
|
35
|
+
disabled: readOnly,
|
|
34
36
|
type: "button",
|
|
35
37
|
className: clsx('byline-field-relation-many-grip', relation_field_module["many-grip"]),
|
|
36
38
|
...attributes,
|
|
@@ -42,6 +44,7 @@ const RelationManyTile = ({ id, children, onRemove, removeAriaLabel })=>{
|
|
|
42
44
|
children: children
|
|
43
45
|
}),
|
|
44
46
|
/*#__PURE__*/ jsx(IconButton, {
|
|
47
|
+
disabled: readOnly,
|
|
45
48
|
type: "button",
|
|
46
49
|
size: "xs",
|
|
47
50
|
intent: "noeffect",
|
|
@@ -77,6 +80,7 @@ const RelationManyField = ({ field, defaultValue, id, path })=>{
|
|
|
77
80
|
return Array.isArray(defaultValue) ? defaultValue : [];
|
|
78
81
|
};
|
|
79
82
|
const handleAddMany = (selections)=>{
|
|
83
|
+
if (field.readOnly) return;
|
|
80
84
|
setPickerOpen(false);
|
|
81
85
|
const current = currentArray();
|
|
82
86
|
const existing = new Set(current.map((v)=>v.targetDocumentId));
|
|
@@ -99,10 +103,11 @@ const RelationManyField = ({ field, defaultValue, id, path })=>{
|
|
|
99
103
|
]);
|
|
100
104
|
};
|
|
101
105
|
const handleRemove = (targetDocumentId)=>{
|
|
106
|
+
if (field.readOnly) return;
|
|
102
107
|
setFieldValue(fieldPath, currentArray().filter((v)=>v.targetDocumentId !== targetDocumentId));
|
|
103
108
|
};
|
|
104
109
|
const handleDragEnd = ({ moveFromIndex, moveToIndex })=>{
|
|
105
|
-
if (moveFromIndex === moveToIndex) return;
|
|
110
|
+
if (field.readOnly || moveFromIndex === moveToIndex) return;
|
|
106
111
|
setFieldValue(fieldPath, moveItem(currentArray(), moveFromIndex, moveToIndex));
|
|
107
112
|
};
|
|
108
113
|
return /*#__PURE__*/ jsxs("div", {
|
|
@@ -146,6 +151,7 @@ const RelationManyField = ({ field, defaultValue, id, path })=>{
|
|
|
146
151
|
onDragEnd: handleDragEnd,
|
|
147
152
|
className: clsx('byline-field-relation-many', relation_field_module.many),
|
|
148
153
|
children: items.map((value)=>/*#__PURE__*/ jsx(RelationManyTile, {
|
|
154
|
+
readOnly: field.readOnly,
|
|
149
155
|
id: value.targetDocumentId,
|
|
150
156
|
onRemove: ()=>handleRemove(value.targetDocumentId),
|
|
151
157
|
removeAriaLabel: t('fields.relation.removeAriaLabel', {
|
|
@@ -163,6 +169,7 @@ const RelationManyField = ({ field, defaultValue, id, path })=>{
|
|
|
163
169
|
/*#__PURE__*/ jsx("div", {
|
|
164
170
|
className: clsx('byline-field-relation-many-add', relation_field_module["many-add"]),
|
|
165
171
|
children: /*#__PURE__*/ jsxs(Button, {
|
|
172
|
+
disabled: field.readOnly,
|
|
166
173
|
id: htmlId,
|
|
167
174
|
size: "xs",
|
|
168
175
|
variant: "outlined",
|
|
@@ -22,6 +22,7 @@ const SelectField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
22
22
|
className: clsx('byline-field-select-label', select_field_module.label)
|
|
23
23
|
}),
|
|
24
24
|
/*#__PURE__*/ jsx(Select, {
|
|
25
|
+
disabled: field.readOnly,
|
|
25
26
|
size: "xs",
|
|
26
27
|
variant: "outlined",
|
|
27
28
|
id: htmlId,
|
|
@@ -13,9 +13,10 @@ const TextField = ({ field, value, defaultValue, onChange, id, path, locale, com
|
|
|
13
13
|
const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
|
|
14
14
|
const htmlId = useScopedDomId(fieldPath, id);
|
|
15
15
|
const handleChange = useCallback((value)=>{
|
|
16
|
-
if (onChange) onChange(value);
|
|
16
|
+
if (onChange && !field.readOnly) onChange(value);
|
|
17
17
|
}, [
|
|
18
|
-
onChange
|
|
18
|
+
onChange,
|
|
19
|
+
field.readOnly
|
|
19
20
|
]);
|
|
20
21
|
const slots = components;
|
|
21
22
|
const CustomLabel = slots?.Label;
|
|
@@ -73,6 +74,7 @@ const TextField = ({ field, value, defaultValue, onChange, id, path, locale, com
|
|
|
73
74
|
placeholder: field.placeholder
|
|
74
75
|
});
|
|
75
76
|
return /*#__PURE__*/ jsx(Input, {
|
|
77
|
+
readOnly: field.readOnly,
|
|
76
78
|
id: htmlId,
|
|
77
79
|
name: field.name,
|
|
78
80
|
label: suppressInputLabel ? void 0 : field.label,
|
|
@@ -13,9 +13,10 @@ const TextAreaField = ({ field, value, defaultValue, onChange, id, path, locale,
|
|
|
13
13
|
const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
|
|
14
14
|
const htmlId = useScopedDomId(fieldPath, id);
|
|
15
15
|
const handleChange = useCallback((value)=>{
|
|
16
|
-
if (onChange) onChange(value);
|
|
16
|
+
if (onChange && !field.readOnly) onChange(value);
|
|
17
17
|
}, [
|
|
18
|
-
onChange
|
|
18
|
+
onChange,
|
|
19
|
+
field.readOnly
|
|
19
20
|
]);
|
|
20
21
|
const slots = components;
|
|
21
22
|
const CustomLabel = slots?.Label;
|
|
@@ -73,6 +74,7 @@ const TextAreaField = ({ field, value, defaultValue, onChange, id, path, locale,
|
|
|
73
74
|
placeholder: field.placeholder
|
|
74
75
|
});
|
|
75
76
|
return /*#__PURE__*/ jsx(TextArea, {
|
|
77
|
+
readOnly: field.readOnly,
|
|
76
78
|
id: htmlId,
|
|
77
79
|
name: field.name,
|
|
78
80
|
label: suppressInputLabel ? void 0 : field.label,
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { normalizeHooks } from "@byline/core";
|
|
3
3
|
import { useFormContext } from "../forms/form-context.js";
|
|
4
4
|
function useFieldChangeHandler(field, path) {
|
|
5
|
-
const { setFieldValue, getFieldValue, getFieldValues, setFieldError, clearFieldError } = useFormContext();
|
|
5
|
+
const { setFieldValue, getFieldValue, getFieldValues, setFieldError, clearFieldError, trackFieldChange } = useFormContext();
|
|
6
|
+
const generation = useRef(0);
|
|
7
|
+
const releasePending = useRef(void 0);
|
|
8
|
+
useEffect(()=>()=>{
|
|
9
|
+
generation.current++;
|
|
10
|
+
releasePending.current?.();
|
|
11
|
+
}, []);
|
|
6
12
|
return useCallback((value)=>{
|
|
13
|
+
releasePending.current?.();
|
|
14
|
+
const invocation = ++generation.current;
|
|
15
|
+
if (field.readOnly) return;
|
|
16
|
+
const current = ()=>invocation === generation.current;
|
|
7
17
|
const hooks = field.hooks;
|
|
8
18
|
const validateFns = normalizeHooks(hooks?.beforeValidate);
|
|
9
19
|
const changeFns = normalizeHooks(hooks?.beforeChange);
|
|
@@ -16,30 +26,37 @@ function useFieldChangeHandler(field, path) {
|
|
|
16
26
|
path,
|
|
17
27
|
field,
|
|
18
28
|
operation: 'change',
|
|
19
|
-
setFieldValue
|
|
29
|
+
setFieldValue: (target, next)=>{
|
|
30
|
+
if (current()) setFieldValue(target, next);
|
|
31
|
+
}
|
|
20
32
|
};
|
|
21
33
|
clearFieldError(path);
|
|
22
34
|
if (0 === changeFns.length) setFieldValue(path, value);
|
|
23
|
-
(async ()=>{
|
|
35
|
+
const pending = (async ()=>{
|
|
24
36
|
try {
|
|
25
37
|
let advisoryError;
|
|
26
38
|
for (const fn of validateFns){
|
|
27
39
|
const result = await fn(ctx);
|
|
40
|
+
if (!current()) return;
|
|
28
41
|
if (result?.error) advisoryError = result.error;
|
|
29
42
|
if (result?.value !== void 0) ctx.value = result.value;
|
|
30
43
|
}
|
|
31
44
|
for (const fn of changeFns){
|
|
32
45
|
const result = await fn(ctx);
|
|
46
|
+
if (!current()) return;
|
|
33
47
|
if (result?.error) return void setFieldError(path, result.error);
|
|
34
48
|
if (result?.value !== void 0) ctx.value = result.value;
|
|
35
49
|
}
|
|
36
|
-
|
|
50
|
+
if (!current()) return;
|
|
51
|
+
if (changeFns.length > 0 || !Object.is(ctx.value, value)) setFieldValue(path, ctx.value);
|
|
37
52
|
if (advisoryError) setFieldError(path, advisoryError);
|
|
38
53
|
} catch (err) {
|
|
54
|
+
if (!current()) return;
|
|
39
55
|
const message = err instanceof Error ? err.message : 'Unexpected hook error';
|
|
40
56
|
setFieldError(path, message);
|
|
41
57
|
}
|
|
42
58
|
})();
|
|
59
|
+
releasePending.current = trackFieldChange(pending);
|
|
43
60
|
}, [
|
|
44
61
|
field,
|
|
45
62
|
path,
|
|
@@ -47,7 +64,8 @@ function useFieldChangeHandler(field, path) {
|
|
|
47
64
|
getFieldValue,
|
|
48
65
|
getFieldValues,
|
|
49
66
|
setFieldError,
|
|
50
|
-
clearFieldError
|
|
67
|
+
clearFieldError,
|
|
68
|
+
trackFieldChange
|
|
51
69
|
]);
|
|
52
70
|
}
|
|
53
71
|
export { useFieldChangeHandler };
|
|
@@ -6,19 +6,4 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import type { Field } from '@byline/core';
|
|
9
|
-
/**
|
|
10
|
-
* Evaluate a field's visibility `condition` against live form data.
|
|
11
|
-
*
|
|
12
|
-
* Subscribes to the form's meta listeners — fired on every field commit, the
|
|
13
|
-
* same loop `TabDefinition.condition` rides — and recomputes the predicate.
|
|
14
|
-
* The consumer only re-renders when the boolean actually flips, so a stable
|
|
15
|
-
* condition costs one function call per form edit, no reconciliation.
|
|
16
|
-
*
|
|
17
|
-
* `basePath` is the field's sibling scope: the enclosing group / array item
|
|
18
|
-
* (e.g. `files[0].filesGroup` for `files[0].filesGroup.thumbnailPage`).
|
|
19
|
-
* Root-level fields pass no basePath and receive the full form data as their
|
|
20
|
-
* sibling scope, mirroring `FieldCondition`'s contract.
|
|
21
|
-
*
|
|
22
|
-
* Fields without a condition are always visible and no subscription is made.
|
|
23
|
-
*/
|
|
24
9
|
export declare const useFieldCondition: (field: Field, basePath?: string) => boolean;
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
2
|
import { useFormContext } from "../forms/form-context.js";
|
|
3
3
|
import { get } from "../forms/nested-path.js";
|
|
4
|
+
const reportedConditionFailures = new WeakSet();
|
|
4
5
|
const useFieldCondition = (field, basePath)=>{
|
|
5
6
|
const { getFieldValues, subscribeMeta } = useFormContext();
|
|
6
7
|
const evaluate = useCallback(()=>{
|
|
7
8
|
if (!field.condition) return true;
|
|
8
9
|
const data = getFieldValues();
|
|
9
10
|
const siblingData = basePath ? get(data, basePath) ?? {} : data;
|
|
10
|
-
|
|
11
|
+
try {
|
|
12
|
+
return Boolean(field.condition(data, siblingData));
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (!reportedConditionFailures.has(field)) {
|
|
15
|
+
reportedConditionFailures.add(field);
|
|
16
|
+
console.error(`Field condition threw for '${field.name}'; keeping the field visible`, error);
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
11
20
|
}, [
|
|
12
21
|
field,
|
|
13
22
|
basePath,
|
|
@@ -78,7 +78,9 @@ interface FormContextType {
|
|
|
78
78
|
hasChanges: () => boolean;
|
|
79
79
|
resetHasChanges: () => void;
|
|
80
80
|
runFieldHooks: (fields: Field[]) => Promise<FormError[]>;
|
|
81
|
-
validateForm: (fields: Field[]) => FormError[];
|
|
81
|
+
validateForm: (fields: Field[], additionalErrors?: FormError[]) => FormError[];
|
|
82
|
+
trackFieldChange: (pending: Promise<void>) => () => void;
|
|
83
|
+
waitForFieldChanges: () => Promise<void>;
|
|
82
84
|
errors: FormError[];
|
|
83
85
|
getErrors: () => FormError[];
|
|
84
86
|
clearErrors: () => void;
|
|
@@ -131,11 +133,6 @@ export declare const FormProvider: ({ children, initialData, documentId, collect
|
|
|
131
133
|
* Returns the current value (or `null` when no override is set).
|
|
132
134
|
*/
|
|
133
135
|
export declare const useSystemPath: () => string | null;
|
|
134
|
-
/**
|
|
135
|
-
* Subscribe to the system `availableLocales` slot edited by the
|
|
136
|
-
* available-locales widget. Returns the current advertised set (or `[]` when
|
|
137
|
-
* nothing is advertised / not yet surfaced).
|
|
138
|
-
*/
|
|
139
136
|
export declare const useSystemAvailableLocales: () => string[];
|
|
140
137
|
export declare const useFormStore: () => FormContextType;
|
|
141
138
|
export declare const useFieldError: (name: string) => string | undefined;
|
|
@@ -144,11 +141,5 @@ export declare const useFormMeta: () => {
|
|
|
144
141
|
};
|
|
145
142
|
export declare const useIsDirty: (name: string) => boolean;
|
|
146
143
|
export declare const useFieldValue: <T = any>(name: string) => T | undefined;
|
|
147
|
-
/**
|
|
148
|
-
* Subscribe to a single field's upload-in-flight state. Returns `true` while
|
|
149
|
-
* the form orchestrator is actively transporting this field's pending upload
|
|
150
|
-
* (between the `setFieldUploading(path, true)` and the matching `false`
|
|
151
|
-
* emitted by the upload executor's progress callback).
|
|
152
|
-
*/
|
|
153
144
|
export declare const useIsFieldUploading: (fieldPath: string) => boolean;
|
|
154
145
|
export {};
|