@byline/admin 6.2.1 → 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 +4 -1
- package/dist/fields/code/code-field.js +2 -1
- package/dist/fields/datetime/datetime-field.js +5 -2
- package/dist/fields/field-renderer.js +2 -1
- package/dist/fields/field-services-types.d.ts +23 -0
- package/dist/fields/file/file-field.js +6 -4
- package/dist/fields/group/group-field.js +8 -3
- package/dist/fields/image/image-field.js +6 -4
- package/dist/fields/numerical/numerical-field.js +2 -1
- package/dist/fields/relation/relation-field.js +8 -2
- package/dist/fields/relation/relation-many-field.js +12 -4
- package/dist/fields/relation/relation-picker.js +38 -7
- package/dist/fields/relation/relation-picker.module.js +3 -0
- package/dist/fields/relation/relation-picker_module.css +20 -0
- package/dist/fields/select/select-field.js +4 -2
- package/dist/fields/text/text-field.js +6 -3
- package/dist/fields/text-area/text-area-field.js +6 -3
- 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 +115 -130
- package/dist/forms/form-dom-scope.d.ts +34 -0
- package/dist/forms/form-dom-scope.js +20 -0
- package/dist/forms/form-layout.js +10 -3
- package/dist/forms/form-page-chrome.js +14 -24
- package/dist/forms/form-renderer.d.ts +2 -1
- package/dist/forms/form-renderer.js +13 -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/dist/presentation/tabs.js +2 -1
- package/dist/react.d.ts +1 -1
- 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 +4 -1
- package/src/fields/code/code-field.tsx +2 -1
- package/src/fields/datetime/datetime-field.tsx +5 -2
- package/src/fields/direct-widget-dom-ids.test.tsx +142 -0
- package/src/fields/field-dom-ids.test.tsx +293 -0
- package/src/fields/field-renderer.tsx +2 -1
- package/src/fields/field-services-types.ts +25 -0
- package/src/fields/file/file-field.tsx +6 -4
- package/src/fields/group/group-field.tsx +5 -3
- package/src/fields/image/image-field.tsx +6 -4
- package/src/fields/numerical/numerical-field.tsx +2 -1
- package/src/fields/relation/relation-field.tsx +8 -2
- package/src/fields/relation/relation-many-field.tsx +19 -4
- package/src/fields/relation/relation-picker-create.test.tsx +203 -0
- package/src/fields/relation/relation-picker-refresh.test.tsx +325 -0
- package/src/fields/relation/relation-picker.module.css +32 -0
- package/src/fields/relation/relation-picker.tsx +59 -1
- package/src/fields/select/select-field.tsx +4 -2
- package/src/fields/text/text-field.tsx +5 -3
- package/src/fields/text-area/text-area-field.tsx +5 -3
- 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 +180 -234
- package/src/forms/form-correctness.test.tsx +560 -0
- package/src/forms/form-dom-scope.test.tsx +213 -0
- package/src/forms/form-dom-scope.tsx +70 -0
- package/src/forms/form-layout.tsx +10 -3
- 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 +26 -64
- package/src/forms/form-submit-isolation.test.tsx +247 -0
- 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
- package/src/presentation/tab-dom-ids.test.tsx +153 -0
- package/src/presentation/tabs.test.tsx +49 -2
- package/src/presentation/tabs.tsx +11 -1
- package/src/react.ts +2 -0
|
@@ -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
|
});
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Checkbox } from "@byline/ui/react";
|
|
3
3
|
import { useFieldError, useFieldValue } from "../../forms/form-context.js";
|
|
4
|
+
import { useScopedDomId } from "../../forms/form-dom-scope.js";
|
|
4
5
|
import checkbox_field_module from "./checkbox-field.module.js";
|
|
5
6
|
const CheckboxField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
6
7
|
const fieldPath = path ?? field.name;
|
|
8
|
+
const htmlId = useScopedDomId(fieldPath, id);
|
|
7
9
|
const fieldError = useFieldError(fieldPath);
|
|
8
10
|
const fieldValue = useFieldValue(fieldPath);
|
|
9
11
|
const checked = value ?? fieldValue ?? defaultValue ?? false;
|
|
10
12
|
return /*#__PURE__*/ jsx("div", {
|
|
11
13
|
className: `byline-field-checkbox ${field.name} ${checkbox_field_module["field-checkbox"]}`,
|
|
12
14
|
children: /*#__PURE__*/ jsx(Checkbox, {
|
|
13
|
-
|
|
15
|
+
disabled: field.readOnly,
|
|
16
|
+
id: htmlId,
|
|
14
17
|
name: field.name,
|
|
15
18
|
label: field.label,
|
|
16
19
|
checked: checked,
|
|
@@ -3,6 +3,7 @@ import react, { Suspense, useCallback } from "react";
|
|
|
3
3
|
import { ErrorText, HelpText, Label } from "@byline/ui/react";
|
|
4
4
|
import clsx from "clsx";
|
|
5
5
|
import { useFieldError, useFieldValue } from "../../forms/form-context.js";
|
|
6
|
+
import { useScopedDomId } from "../../forms/form-dom-scope.js";
|
|
6
7
|
import { LocaleBadge } from "../locale-badge.js";
|
|
7
8
|
import code_field_module from "./code-field.module.js";
|
|
8
9
|
const CodeEditor = /*#__PURE__*/ react.lazy(()=>import("./code-editor.js"));
|
|
@@ -15,7 +16,7 @@ const CodeField = ({ field, value, defaultValue, onChange, id, path, locale, com
|
|
|
15
16
|
const fieldError = useFieldError(fieldPath);
|
|
16
17
|
const fieldValue = useFieldValue(fieldPath);
|
|
17
18
|
const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
|
|
18
|
-
const htmlId = id
|
|
19
|
+
const htmlId = useScopedDomId(fieldPath, id);
|
|
19
20
|
const siblingPath = field.languageField ? siblingFieldPath(fieldPath, field.languageField) : fieldPath;
|
|
20
21
|
const siblingLanguage = useFieldValue(siblingPath);
|
|
21
22
|
const effectiveLanguage = (field.languageField ? siblingLanguage : void 0) || field.language;
|
|
@@ -2,9 +2,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { DatePicker, ErrorText } from "@byline/ui/react";
|
|
3
3
|
import clsx from "clsx";
|
|
4
4
|
import { useFieldError, useFieldValue, useIsDirty } from "../../forms/form-context.js";
|
|
5
|
+
import { useScopedDomId } from "../../forms/form-dom-scope.js";
|
|
5
6
|
import datetime_field_module from "./datetime-field.module.js";
|
|
6
7
|
const DateTimeField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
7
8
|
const fieldPath = path ?? field.name;
|
|
9
|
+
const htmlId = useScopedDomId(fieldPath, id);
|
|
8
10
|
const fieldError = useFieldError(fieldPath);
|
|
9
11
|
const isDirty = useIsDirty(fieldPath);
|
|
10
12
|
const fieldValue = useFieldValue(fieldPath);
|
|
@@ -13,7 +15,8 @@ const DateTimeField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
13
15
|
className: `byline-field-datetime ${field.name}`,
|
|
14
16
|
children: [
|
|
15
17
|
/*#__PURE__*/ jsx(DatePicker, {
|
|
16
|
-
|
|
18
|
+
disabled: field.readOnly,
|
|
19
|
+
id: htmlId,
|
|
17
20
|
name: field.name,
|
|
18
21
|
label: field.label,
|
|
19
22
|
required: !field.optional,
|
|
@@ -28,7 +31,7 @@ const DateTimeField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
28
31
|
])
|
|
29
32
|
}),
|
|
30
33
|
fieldError && /*#__PURE__*/ jsx(ErrorText, {
|
|
31
|
-
id: `${
|
|
34
|
+
id: `${htmlId}-error`,
|
|
32
35
|
text: fieldError
|
|
33
36
|
})
|
|
34
37
|
]
|
|
@@ -2,6 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { getAdminConfig } from "@byline/core";
|
|
3
3
|
import clsx from "clsx";
|
|
4
4
|
import { useFormContext } from "../forms/form-context.js";
|
|
5
|
+
import { useScopedDomId } from "../forms/form-dom-scope.js";
|
|
5
6
|
import { ArrayField } from "./array/array-field.js";
|
|
6
7
|
import { BlocksField } from "./blocks/blocks-field.js";
|
|
7
8
|
import { CheckboxField } from "./checkbox/checkbox-field.js";
|
|
@@ -22,7 +23,7 @@ import { useFieldChangeHandler } from "./use-field-change-handler.js";
|
|
|
22
23
|
import { useFieldCondition } from "./use-field-condition.js";
|
|
23
24
|
const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, contentLocale, components, editor, fieldAdmin })=>{
|
|
24
25
|
const path = basePath ? `${basePath}.${field.name}` : field.name;
|
|
25
|
-
const htmlId = path.replace(/[[\].]/g, '-');
|
|
26
|
+
const htmlId = useScopedDomId(path.replace(/[[\].]/g, '-'));
|
|
26
27
|
const handleChange = useFieldChangeHandler(field, path);
|
|
27
28
|
const { getFieldValue } = useFormContext();
|
|
28
29
|
const visible = useFieldCondition(field, basePath);
|
|
@@ -101,6 +101,10 @@ export type GetTreeParentFn = (input: {
|
|
|
101
101
|
placed: boolean;
|
|
102
102
|
parentDocumentId: string | null;
|
|
103
103
|
}>;
|
|
104
|
+
/** See `BylineFieldServices.canCreateInCollection`. */
|
|
105
|
+
export type CanCreateInCollectionFn = (collectionPath: string) => boolean;
|
|
106
|
+
/** See `BylineFieldServices.getCreateDocumentUrl`. */
|
|
107
|
+
export type GetCreateDocumentUrlFn = (collectionPath: string) => string;
|
|
104
108
|
export interface BylineFieldServices {
|
|
105
109
|
getCollectionDocuments: GetCollectionDocumentsFn;
|
|
106
110
|
uploadField: UploadFieldFn;
|
|
@@ -113,4 +117,23 @@ export interface BylineFieldServices {
|
|
|
113
117
|
removeFromTree?: RemoveFromTreeFn;
|
|
114
118
|
getTreeAncestors?: GetTreeAncestorsFn;
|
|
115
119
|
getTreeParent?: GetTreeParentFn;
|
|
120
|
+
/**
|
|
121
|
+
* Whether the viewer may create documents in a collection, used to decide
|
|
122
|
+
* whether to offer a create affordance in the relation picker.
|
|
123
|
+
*
|
|
124
|
+
* Cosmetic only: the create view enforces the ability server-side regardless
|
|
125
|
+
* of what this returns. Optional, like the tree operations above — a host that
|
|
126
|
+
* wires neither this nor `getCreateDocumentUrl` simply gets no affordance, and
|
|
127
|
+
* the widget guards on their presence.
|
|
128
|
+
*/
|
|
129
|
+
canCreateInCollection?: CanCreateInCollectionFn;
|
|
130
|
+
/**
|
|
131
|
+
* Root-relative URL of a collection's create view, built from the host's
|
|
132
|
+
* configured admin path.
|
|
133
|
+
*
|
|
134
|
+
* Root-relative rather than absolute because no origin is available during the
|
|
135
|
+
* server render, and the admin base path is host configuration —
|
|
136
|
+
* `@byline/admin` must not assume `/admin`.
|
|
137
|
+
*/
|
|
138
|
+
getCreateDocumentUrl?: GetCreateDocumentUrlFn;
|
|
116
139
|
}
|
|
@@ -4,6 +4,7 @@ import { useTranslation } from "@byline/i18n/react";
|
|
|
4
4
|
import { CloseIcon, DocumentIcon, DownloadIcon, ErrorText, HelpText, IconButton, Label, LoaderRing, VideoIcon } from "@byline/ui/react";
|
|
5
5
|
import clsx from "clsx";
|
|
6
6
|
import { useFieldError, useFieldValue, useFormContext, useIsDirty, useIsFieldUploading } from "../../forms/form-context.js";
|
|
7
|
+
import { useScopedDomId } from "../../forms/form-dom-scope.js";
|
|
7
8
|
import { useFieldChangeHandler } from "../use-field-change-handler.js";
|
|
8
9
|
import file_field_module from "./file-field.module.js";
|
|
9
10
|
import { FileUploadField } from "./file-upload-field.js";
|
|
@@ -37,12 +38,13 @@ const FileField = ({ field, value, defaultValue, onChange: _onChange, path })=>{
|
|
|
37
38
|
const showUploadWidget = null == incomingValue || isOldPlaceholder(incomingValue);
|
|
38
39
|
const uploadGated = field.upload?.requireSavedDocument === true && null == documentId;
|
|
39
40
|
const handleRemove = ()=>{
|
|
41
|
+
if (field.readOnly) return;
|
|
40
42
|
if (isPending) removePendingUpload(fieldPath);
|
|
41
43
|
handleChange(null);
|
|
42
44
|
};
|
|
43
45
|
const isVideo = incomingValue?.mimeType?.startsWith('video/') === true;
|
|
44
46
|
const FileGlyph = isVideo ? VideoIcon : DocumentIcon;
|
|
45
|
-
const htmlId = fieldPath;
|
|
47
|
+
const htmlId = useScopedDomId(fieldPath);
|
|
46
48
|
return /*#__PURE__*/ jsxs("div", {
|
|
47
49
|
className: `byline-field-file ${field.name}`,
|
|
48
50
|
children: [
|
|
@@ -60,7 +62,7 @@ const FileField = ({ field, value, defaultValue, onChange: _onChange, path })=>{
|
|
|
60
62
|
role: "note",
|
|
61
63
|
"data-testid": "upload-require-saved-document",
|
|
62
64
|
children: t('fields.upload.requireSavedDocument')
|
|
63
|
-
}) : collectionPath ? /*#__PURE__*/ jsx(FileUploadField, {
|
|
65
|
+
}) : collectionPath && !field.readOnly ? /*#__PURE__*/ jsx(FileUploadField, {
|
|
64
66
|
field: field,
|
|
65
67
|
collectionPath: collectionPath,
|
|
66
68
|
fieldPath: fieldPath,
|
|
@@ -99,7 +101,7 @@ const FileField = ({ field, value, defaultValue, onChange: _onChange, path })=>{
|
|
|
99
101
|
intent: "noeffect",
|
|
100
102
|
onClick: handleRemove,
|
|
101
103
|
size: "xs",
|
|
102
|
-
disabled: isUploading,
|
|
104
|
+
disabled: isUploading || field.readOnly,
|
|
103
105
|
"aria-label": t('fields.file.removeAriaLabel'),
|
|
104
106
|
children: /*#__PURE__*/ jsx(CloseIcon, {
|
|
105
107
|
width: "15px",
|
|
@@ -222,7 +224,7 @@ const FileField = ({ field, value, defaultValue, onChange: _onChange, path })=>{
|
|
|
222
224
|
text: field.helpText
|
|
223
225
|
}),
|
|
224
226
|
fieldError && /*#__PURE__*/ jsx(ErrorText, {
|
|
225
|
-
id: `${
|
|
227
|
+
id: `${htmlId}-error`,
|
|
226
228
|
text: fieldError
|
|
227
229
|
})
|
|
228
230
|
]
|
|
@@ -6,9 +6,11 @@ import { sliceFieldAdmin } from "../field-admin.js";
|
|
|
6
6
|
import { placeholderForField } from "../field-helpers.js";
|
|
7
7
|
import { FieldRenderer } from "../field-renderer.js";
|
|
8
8
|
import { useFieldError } from "../../forms/form-context.js";
|
|
9
|
+
import { useScopedDomId } from "../../forms/form-dom-scope.js";
|
|
9
10
|
import group_field_module from "./group-field.module.js";
|
|
10
11
|
const GroupField = ({ field, defaultValue, path, disableSorting = true, contentLocale, fieldAdmin })=>{
|
|
11
|
-
const
|
|
12
|
+
const htmlId = useScopedDomId(path ?? field.name);
|
|
13
|
+
const fieldError = useFieldError(path ?? field.name);
|
|
12
14
|
const groupData = useMemo(()=>{
|
|
13
15
|
if (defaultValue && 'object' == typeof defaultValue && !Array.isArray(defaultValue)) return defaultValue;
|
|
14
16
|
const placeholder = {};
|
|
@@ -44,7 +46,10 @@ const GroupField = ({ field, defaultValue, path, disableSorting = true, contentL
|
|
|
44
46
|
/*#__PURE__*/ jsx("div", {
|
|
45
47
|
className: clsx('byline-field-group-body', group_field_module.body),
|
|
46
48
|
children: field.fields.map((innerField)=>/*#__PURE__*/ jsx(FieldRenderer, {
|
|
47
|
-
field:
|
|
49
|
+
field: field.readOnly ? {
|
|
50
|
+
...innerField,
|
|
51
|
+
readOnly: true
|
|
52
|
+
} : innerField,
|
|
48
53
|
defaultValue: groupData[innerField.name],
|
|
49
54
|
basePath: path,
|
|
50
55
|
disableSorting: disableSorting,
|
|
@@ -55,7 +60,7 @@ const GroupField = ({ field, defaultValue, path, disableSorting = true, contentL
|
|
|
55
60
|
}, innerField.name))
|
|
56
61
|
}),
|
|
57
62
|
fieldError && /*#__PURE__*/ jsx(ErrorText, {
|
|
58
|
-
id: `${
|
|
63
|
+
id: `${htmlId}-error`,
|
|
59
64
|
text: fieldError
|
|
60
65
|
})
|
|
61
66
|
]
|
|
@@ -5,6 +5,7 @@ import { useTranslation } from "@byline/i18n/react";
|
|
|
5
5
|
import { CloseIcon, ErrorText, HelpText, IconButton, ImageLightbox, Label, LoaderRing } from "@byline/ui/react";
|
|
6
6
|
import clsx from "clsx";
|
|
7
7
|
import { useFieldError, useFieldValue, useFormContext, useIsDirty, useIsFieldUploading } from "../../forms/form-context.js";
|
|
8
|
+
import { useScopedDomId } from "../../forms/form-dom-scope.js";
|
|
8
9
|
import { useFieldChangeHandler } from "../use-field-change-handler.js";
|
|
9
10
|
import image_field_module from "./image-field.module.js";
|
|
10
11
|
import { ImageUploadField } from "./image-upload-field.js";
|
|
@@ -29,12 +30,13 @@ const ImageField = ({ field, value, defaultValue, onChange: _onChange, path })=>
|
|
|
29
30
|
const thumbVariant = incomingValue && !isPendingStoredFileValue(incomingValue) ? incomingValue.variants?.find((v)=>'thumbnail' === v.name) : void 0;
|
|
30
31
|
const previewUrl = thumbVariant?.storageUrl ?? incomingValue?.storageUrl;
|
|
31
32
|
const handleRemove = ()=>{
|
|
33
|
+
if (field.readOnly) return;
|
|
32
34
|
if (isPending) removePendingUpload(fieldPath);
|
|
33
35
|
handleChange(null);
|
|
34
36
|
};
|
|
35
37
|
const [lightboxOpen, setLightboxOpen] = useState(false);
|
|
36
38
|
const canOpenLightbox = !isPending && !!incomingValue?.storageUrl;
|
|
37
|
-
const htmlId = fieldPath;
|
|
39
|
+
const htmlId = useScopedDomId(fieldPath);
|
|
38
40
|
return /*#__PURE__*/ jsxs("div", {
|
|
39
41
|
className: `byline-field-image ${field.name}`,
|
|
40
42
|
children: [
|
|
@@ -52,7 +54,7 @@ const ImageField = ({ field, value, defaultValue, onChange: _onChange, path })=>
|
|
|
52
54
|
role: "note",
|
|
53
55
|
"data-testid": "upload-require-saved-document",
|
|
54
56
|
children: t('fields.upload.requireSavedDocument')
|
|
55
|
-
}) : collectionPath ? /*#__PURE__*/ jsx(ImageUploadField, {
|
|
57
|
+
}) : collectionPath && !field.readOnly ? /*#__PURE__*/ jsx(ImageUploadField, {
|
|
56
58
|
field: field,
|
|
57
59
|
collectionPath: collectionPath,
|
|
58
60
|
fieldPath: fieldPath,
|
|
@@ -78,7 +80,7 @@ const ImageField = ({ field, value, defaultValue, onChange: _onChange, path })=>
|
|
|
78
80
|
intent: "noeffect",
|
|
79
81
|
onClick: handleRemove,
|
|
80
82
|
size: "xs",
|
|
81
|
-
disabled: isUploading,
|
|
83
|
+
disabled: isUploading || field.readOnly,
|
|
82
84
|
"aria-label": t('fields.image.removeAriaLabel'),
|
|
83
85
|
children: /*#__PURE__*/ jsx(CloseIcon, {
|
|
84
86
|
width: "15px",
|
|
@@ -224,7 +226,7 @@ const ImageField = ({ field, value, defaultValue, onChange: _onChange, path })=>
|
|
|
224
226
|
text: field.helpText
|
|
225
227
|
}),
|
|
226
228
|
fieldError && /*#__PURE__*/ jsx(ErrorText, {
|
|
227
|
-
id: `${
|
|
229
|
+
id: `${htmlId}-error`,
|
|
228
230
|
text: fieldError
|
|
229
231
|
}),
|
|
230
232
|
canOpenLightbox && incomingValue?.storageUrl && /*#__PURE__*/ jsx(ImageLightbox, {
|
|
@@ -3,13 +3,14 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { normalizeNumericValue } from "@byline/core";
|
|
4
4
|
import { Input } from "@byline/ui/react";
|
|
5
5
|
import { useFieldError, useFieldValue } from "../../forms/form-context.js";
|
|
6
|
+
import { useScopedDomId } from "../../forms/form-dom-scope.js";
|
|
6
7
|
const COMPLETE_NUMERIC_RE = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
7
8
|
const NumericalField = ({ field, value, defaultValue, onChange, id, path, components })=>{
|
|
8
9
|
const fieldPath = path ?? field.name;
|
|
9
10
|
const fieldError = useFieldError(fieldPath);
|
|
10
11
|
const fieldValue = useFieldValue(fieldPath);
|
|
11
12
|
const incomingValue = void 0 !== value ? value : void 0 !== fieldValue ? fieldValue : defaultValue ?? null;
|
|
12
|
-
const htmlId = id
|
|
13
|
+
const htmlId = useScopedDomId(fieldPath, id);
|
|
13
14
|
const canonicalDisplay = null == incomingValue ? '' : String(incomingValue);
|
|
14
15
|
const [displayValue, setDisplayValue] = useState(canonicalDisplay);
|
|
15
16
|
const [isEditing, setIsEditing] = useState(false);
|
|
@@ -5,12 +5,13 @@ import { useTranslation } from "@byline/i18n/react";
|
|
|
5
5
|
import { Button, CloseIcon, EditIcon, ErrorText, HelpText, IconButton, Label } from "@byline/ui/react";
|
|
6
6
|
import clsx from "clsx";
|
|
7
7
|
import { useFieldError, useFieldValue } from "../../forms/form-context.js";
|
|
8
|
+
import { useScopedDomId } from "../../forms/form-dom-scope.js";
|
|
8
9
|
import relation_field_module from "./relation-field.module.js";
|
|
9
10
|
import { RelationPicker } from "./relation-picker.js";
|
|
10
11
|
import { RelationSummary } from "./relation-summary.js";
|
|
11
12
|
const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
12
13
|
const fieldPath = path ?? field.name;
|
|
13
|
-
const htmlId = id
|
|
14
|
+
const htmlId = useScopedDomId(fieldPath, id);
|
|
14
15
|
const fieldError = useFieldError(fieldPath);
|
|
15
16
|
const fieldValue = useFieldValue(fieldPath);
|
|
16
17
|
const incomingValue = void 0 !== fieldValue ? fieldValue ?? null : value ?? defaultValue ?? null;
|
|
@@ -21,6 +22,7 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
21
22
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
22
23
|
const [pickedRecord, setPickedRecord] = useState(null);
|
|
23
24
|
const handleSelect = (selection)=>{
|
|
25
|
+
if (field.readOnly) return;
|
|
24
26
|
setPickerOpen(false);
|
|
25
27
|
selection.record ? setPickedRecord({
|
|
26
28
|
id: selection.targetDocumentId,
|
|
@@ -32,6 +34,7 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
32
34
|
});
|
|
33
35
|
};
|
|
34
36
|
const handleRemove = ()=>{
|
|
37
|
+
if (field.readOnly) return;
|
|
35
38
|
setPickedRecord(null);
|
|
36
39
|
onChange?.(null);
|
|
37
40
|
};
|
|
@@ -79,6 +82,7 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
79
82
|
className: clsx('byline-field-relation-actions', relation_field_module.actions),
|
|
80
83
|
children: [
|
|
81
84
|
/*#__PURE__*/ jsx(IconButton, {
|
|
85
|
+
disabled: field.readOnly,
|
|
82
86
|
id: htmlId,
|
|
83
87
|
type: "button",
|
|
84
88
|
intent: "noeffect",
|
|
@@ -93,6 +97,7 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
93
97
|
})
|
|
94
98
|
}),
|
|
95
99
|
/*#__PURE__*/ jsx(IconButton, {
|
|
100
|
+
disabled: field.readOnly,
|
|
96
101
|
type: "button",
|
|
97
102
|
intent: "noeffect",
|
|
98
103
|
size: "xs",
|
|
@@ -109,6 +114,7 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
109
114
|
})
|
|
110
115
|
]
|
|
111
116
|
}) : /*#__PURE__*/ jsx(Button, {
|
|
117
|
+
disabled: field.readOnly,
|
|
112
118
|
id: htmlId,
|
|
113
119
|
size: "xs",
|
|
114
120
|
variant: "outlined",
|
|
@@ -124,7 +130,7 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
124
130
|
text: field.helpText
|
|
125
131
|
}),
|
|
126
132
|
fieldError && /*#__PURE__*/ jsx(ErrorText, {
|
|
127
|
-
id: `${
|
|
133
|
+
id: `${htmlId}-error`,
|
|
128
134
|
text: fieldError
|
|
129
135
|
}),
|
|
130
136
|
!isUnknown && /*#__PURE__*/ jsx(RelationPicker, {
|