@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
|
@@ -5,12 +5,14 @@ import { useTranslation } from "@byline/i18n/react";
|
|
|
5
5
|
import { Button, CloseIcon, DraggableSortable, ErrorText, GripperVerticalIcon, HelpText, IconButton, Label, PlusIcon, moveItem, useSortable } from "@byline/ui/react";
|
|
6
6
|
import clsx from "clsx";
|
|
7
7
|
import { useFieldError, useFieldValue, useFormContext } 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
|
-
const RelationManyTile = ({ id, children, onRemove, removeAriaLabel })=>{
|
|
12
|
+
const RelationManyTile = ({ id, children, onRemove, removeAriaLabel, readOnly })=>{
|
|
12
13
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
13
14
|
id,
|
|
15
|
+
disabled: readOnly,
|
|
14
16
|
transition: {
|
|
15
17
|
duration: 250,
|
|
16
18
|
easing: 'cubic-bezier(0, 0.2, 0.2, 1)'
|
|
@@ -30,6 +32,7 @@ const RelationManyTile = ({ id, children, onRemove, removeAriaLabel })=>{
|
|
|
30
32
|
]),
|
|
31
33
|
children: [
|
|
32
34
|
/*#__PURE__*/ jsx("button", {
|
|
35
|
+
disabled: readOnly,
|
|
33
36
|
type: "button",
|
|
34
37
|
className: clsx('byline-field-relation-many-grip', relation_field_module["many-grip"]),
|
|
35
38
|
...attributes,
|
|
@@ -41,6 +44,7 @@ const RelationManyTile = ({ id, children, onRemove, removeAriaLabel })=>{
|
|
|
41
44
|
children: children
|
|
42
45
|
}),
|
|
43
46
|
/*#__PURE__*/ jsx(IconButton, {
|
|
47
|
+
disabled: readOnly,
|
|
44
48
|
type: "button",
|
|
45
49
|
size: "xs",
|
|
46
50
|
intent: "noeffect",
|
|
@@ -57,7 +61,7 @@ const RelationManyTile = ({ id, children, onRemove, removeAriaLabel })=>{
|
|
|
57
61
|
};
|
|
58
62
|
const RelationManyField = ({ field, defaultValue, id, path })=>{
|
|
59
63
|
const fieldPath = path ?? field.name;
|
|
60
|
-
const htmlId = id
|
|
64
|
+
const htmlId = useScopedDomId(fieldPath, id);
|
|
61
65
|
const { t } = useTranslation('byline-admin');
|
|
62
66
|
const fieldError = useFieldError(fieldPath);
|
|
63
67
|
const { getFieldValue, setFieldValue } = useFormContext();
|
|
@@ -76,6 +80,7 @@ const RelationManyField = ({ field, defaultValue, id, path })=>{
|
|
|
76
80
|
return Array.isArray(defaultValue) ? defaultValue : [];
|
|
77
81
|
};
|
|
78
82
|
const handleAddMany = (selections)=>{
|
|
83
|
+
if (field.readOnly) return;
|
|
79
84
|
setPickerOpen(false);
|
|
80
85
|
const current = currentArray();
|
|
81
86
|
const existing = new Set(current.map((v)=>v.targetDocumentId));
|
|
@@ -98,10 +103,11 @@ const RelationManyField = ({ field, defaultValue, id, path })=>{
|
|
|
98
103
|
]);
|
|
99
104
|
};
|
|
100
105
|
const handleRemove = (targetDocumentId)=>{
|
|
106
|
+
if (field.readOnly) return;
|
|
101
107
|
setFieldValue(fieldPath, currentArray().filter((v)=>v.targetDocumentId !== targetDocumentId));
|
|
102
108
|
};
|
|
103
109
|
const handleDragEnd = ({ moveFromIndex, moveToIndex })=>{
|
|
104
|
-
if (moveFromIndex === moveToIndex) return;
|
|
110
|
+
if (field.readOnly || moveFromIndex === moveToIndex) return;
|
|
105
111
|
setFieldValue(fieldPath, moveItem(currentArray(), moveFromIndex, moveToIndex));
|
|
106
112
|
};
|
|
107
113
|
return /*#__PURE__*/ jsxs("div", {
|
|
@@ -145,6 +151,7 @@ const RelationManyField = ({ field, defaultValue, id, path })=>{
|
|
|
145
151
|
onDragEnd: handleDragEnd,
|
|
146
152
|
className: clsx('byline-field-relation-many', relation_field_module.many),
|
|
147
153
|
children: items.map((value)=>/*#__PURE__*/ jsx(RelationManyTile, {
|
|
154
|
+
readOnly: field.readOnly,
|
|
148
155
|
id: value.targetDocumentId,
|
|
149
156
|
onRemove: ()=>handleRemove(value.targetDocumentId),
|
|
150
157
|
removeAriaLabel: t('fields.relation.removeAriaLabel', {
|
|
@@ -162,6 +169,7 @@ const RelationManyField = ({ field, defaultValue, id, path })=>{
|
|
|
162
169
|
/*#__PURE__*/ jsx("div", {
|
|
163
170
|
className: clsx('byline-field-relation-many-add', relation_field_module["many-add"]),
|
|
164
171
|
children: /*#__PURE__*/ jsxs(Button, {
|
|
172
|
+
disabled: field.readOnly,
|
|
165
173
|
id: htmlId,
|
|
166
174
|
size: "xs",
|
|
167
175
|
variant: "outlined",
|
|
@@ -185,7 +193,7 @@ const RelationManyField = ({ field, defaultValue, id, path })=>{
|
|
|
185
193
|
text: field.helpText
|
|
186
194
|
}),
|
|
187
195
|
fieldError && /*#__PURE__*/ jsx(ErrorText, {
|
|
188
|
-
id: `${
|
|
196
|
+
id: `${htmlId}-error`,
|
|
189
197
|
text: fieldError
|
|
190
198
|
}),
|
|
191
199
|
/*#__PURE__*/ jsx(RelationPicker, {
|
|
@@ -19,7 +19,9 @@ const RelationPicker = ({ targetCollectionPath, targetDefinition, displayField,
|
|
|
19
19
|
const [documents, setDocuments] = useState([]);
|
|
20
20
|
const [totalPages, setTotalPages] = useState(1);
|
|
21
21
|
const [collectionId, setCollectionId] = useState(null);
|
|
22
|
-
const
|
|
22
|
+
const [refreshNonce, setRefreshNonce] = useState(0);
|
|
23
|
+
const { getCollectionDocuments, canCreateInCollection, getCreateDocumentUrl } = useBylineFieldServices();
|
|
24
|
+
const createHref = null != getCreateDocumentUrl && canCreateInCollection?.(targetCollectionPath) === true ? getCreateDocumentUrl(targetCollectionPath) : null;
|
|
23
25
|
const targetAdminConfig = getCollectionAdminConfig(targetCollectionPath);
|
|
24
26
|
const pickerColumns = targetAdminConfig?.itemView;
|
|
25
27
|
useEffect(()=>{
|
|
@@ -77,7 +79,8 @@ const RelationPicker = ({ targetCollectionPath, targetDefinition, displayField,
|
|
|
77
79
|
pickerColumns,
|
|
78
80
|
getCollectionDocuments,
|
|
79
81
|
t,
|
|
80
|
-
targetAdminConfig?.itemViewSort
|
|
82
|
+
targetAdminConfig?.itemViewSort,
|
|
83
|
+
refreshNonce
|
|
81
84
|
]);
|
|
82
85
|
const resolvedDisplayField = displayField ?? targetDefinition?.useAsTitle ?? resolveFallbackDisplayField(targetDefinition) ?? null;
|
|
83
86
|
const handleSelect = useCallback(()=>{
|
|
@@ -127,12 +130,40 @@ const RelationPicker = ({ targetCollectionPath, targetDefinition, displayField,
|
|
|
127
130
|
width: '100%'
|
|
128
131
|
},
|
|
129
132
|
children: [
|
|
130
|
-
/*#__PURE__*/
|
|
133
|
+
/*#__PURE__*/ jsxs(Modal.Header, {
|
|
131
134
|
className: clsx('byline-field-relation-picker-header', relation_picker_module.header),
|
|
132
|
-
children:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
children: [
|
|
136
|
+
/*#__PURE__*/ jsx("h3", {
|
|
137
|
+
className: clsx('byline-field-relation-picker-title', relation_picker_module.title),
|
|
138
|
+
children: title
|
|
139
|
+
}),
|
|
140
|
+
/*#__PURE__*/ jsxs("div", {
|
|
141
|
+
className: clsx('byline-field-relation-picker-actions', relation_picker_module.actions),
|
|
142
|
+
children: [
|
|
143
|
+
/*#__PURE__*/ jsx(Button, {
|
|
144
|
+
type: "button",
|
|
145
|
+
size: "xs",
|
|
146
|
+
variant: "outlined",
|
|
147
|
+
className: clsx('byline-field-relation-picker-refresh', relation_picker_module.refresh),
|
|
148
|
+
onClick: ()=>setRefreshNonce((nonce)=>nonce + 1),
|
|
149
|
+
children: t('fields.relation.picker.refresh')
|
|
150
|
+
}),
|
|
151
|
+
null != createHref && /*#__PURE__*/ jsx(Button, {
|
|
152
|
+
size: "xs",
|
|
153
|
+
variant: "outlined",
|
|
154
|
+
className: clsx('byline-field-relation-picker-create', relation_picker_module.create),
|
|
155
|
+
render: /*#__PURE__*/ jsx("a", {
|
|
156
|
+
href: createHref,
|
|
157
|
+
target: "_blank",
|
|
158
|
+
rel: "noopener noreferrer",
|
|
159
|
+
children: t('fields.relation.picker.createNew', {
|
|
160
|
+
label: targetDefinition?.labels.singular ?? targetCollectionPath
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
]
|
|
165
|
+
})
|
|
166
|
+
]
|
|
136
167
|
}),
|
|
137
168
|
/*#__PURE__*/ jsx(Modal.Content, {
|
|
138
169
|
children: /*#__PURE__*/ jsxs("div", {
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
:is(.header-quQOWP, .byline-field-relation-picker-header) {
|
|
2
2
|
margin-bottom: var(--spacing-8);
|
|
3
|
+
justify-content: space-between;
|
|
4
|
+
align-items: baseline;
|
|
5
|
+
gap: var(--spacing-8);
|
|
6
|
+
flex-wrap: wrap;
|
|
3
7
|
padding-top: 1rem;
|
|
8
|
+
display: flex;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
:is(.actions-s42_zl, .byline-field-relation-picker-actions) {
|
|
12
|
+
align-items: center;
|
|
13
|
+
gap: var(--spacing-8);
|
|
14
|
+
display: flex;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:is(.refresh-CVQwEV, .byline-field-relation-picker-refresh) {
|
|
18
|
+
white-space: nowrap;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
:is(.create-Ol3jYM, .byline-field-relation-picker-create) {
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
text-decoration: none;
|
|
4
24
|
}
|
|
5
25
|
|
|
6
26
|
:is(.title-nX1CSv, .byline-field-relation-picker-title) {
|
|
@@ -2,6 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { ErrorText, Label, Select } 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 select_field_module from "./select-field.module.js";
|
|
6
7
|
const SelectField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
7
8
|
const fieldPath = path ?? field.name;
|
|
@@ -9,7 +10,7 @@ const SelectField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
9
10
|
const isDirty = useIsDirty(fieldPath);
|
|
10
11
|
const fieldValue = useFieldValue(fieldPath);
|
|
11
12
|
const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
|
|
12
|
-
const htmlId = id
|
|
13
|
+
const htmlId = useScopedDomId(fieldPath, id);
|
|
13
14
|
return /*#__PURE__*/ jsxs("div", {
|
|
14
15
|
className: `byline-field-select ${field.name}`,
|
|
15
16
|
children: [
|
|
@@ -21,6 +22,7 @@ const SelectField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
21
22
|
className: clsx('byline-field-select-label', select_field_module.label)
|
|
22
23
|
}),
|
|
23
24
|
/*#__PURE__*/ jsx(Select, {
|
|
25
|
+
disabled: field.readOnly,
|
|
24
26
|
size: "xs",
|
|
25
27
|
variant: "outlined",
|
|
26
28
|
id: htmlId,
|
|
@@ -43,7 +45,7 @@ const SelectField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
43
45
|
])
|
|
44
46
|
}),
|
|
45
47
|
fieldError && /*#__PURE__*/ jsx(ErrorText, {
|
|
46
|
-
id: `${
|
|
48
|
+
id: `${htmlId}-error`,
|
|
47
49
|
text: fieldError
|
|
48
50
|
})
|
|
49
51
|
]
|
|
@@ -3,6 +3,7 @@ import { useCallback } from "react";
|
|
|
3
3
|
import { Input, 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 text_field_module from "./text-field.module.js";
|
|
8
9
|
const TextField = ({ field, value, defaultValue, onChange, id, path, locale, components })=>{
|
|
@@ -10,11 +11,12 @@ const TextField = ({ field, value, defaultValue, onChange, id, path, locale, com
|
|
|
10
11
|
const fieldError = useFieldError(fieldPath);
|
|
11
12
|
const fieldValue = useFieldValue(fieldPath);
|
|
12
13
|
const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
|
|
13
|
-
const htmlId = id
|
|
14
|
+
const htmlId = useScopedDomId(fieldPath, id);
|
|
14
15
|
const handleChange = useCallback((value)=>{
|
|
15
|
-
if (onChange) onChange(value);
|
|
16
|
+
if (onChange && !field.readOnly) onChange(value);
|
|
16
17
|
}, [
|
|
17
|
-
onChange
|
|
18
|
+
onChange,
|
|
19
|
+
field.readOnly
|
|
18
20
|
]);
|
|
19
21
|
const slots = components;
|
|
20
22
|
const CustomLabel = slots?.Label;
|
|
@@ -72,6 +74,7 @@ const TextField = ({ field, value, defaultValue, onChange, id, path, locale, com
|
|
|
72
74
|
placeholder: field.placeholder
|
|
73
75
|
});
|
|
74
76
|
return /*#__PURE__*/ jsx(Input, {
|
|
77
|
+
readOnly: field.readOnly,
|
|
75
78
|
id: htmlId,
|
|
76
79
|
name: field.name,
|
|
77
80
|
label: suppressInputLabel ? void 0 : field.label,
|
|
@@ -3,6 +3,7 @@ import { useCallback } from "react";
|
|
|
3
3
|
import { Label, TextArea } 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 text_area_field_module from "./text-area-field.module.js";
|
|
8
9
|
const TextAreaField = ({ field, value, defaultValue, onChange, id, path, locale, components })=>{
|
|
@@ -10,11 +11,12 @@ const TextAreaField = ({ field, value, defaultValue, onChange, id, path, locale,
|
|
|
10
11
|
const fieldError = useFieldError(fieldPath);
|
|
11
12
|
const fieldValue = useFieldValue(fieldPath);
|
|
12
13
|
const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
|
|
13
|
-
const htmlId = id
|
|
14
|
+
const htmlId = useScopedDomId(fieldPath, id);
|
|
14
15
|
const handleChange = useCallback((value)=>{
|
|
15
|
-
if (onChange) onChange(value);
|
|
16
|
+
if (onChange && !field.readOnly) onChange(value);
|
|
16
17
|
}, [
|
|
17
|
-
onChange
|
|
18
|
+
onChange,
|
|
19
|
+
field.readOnly
|
|
18
20
|
]);
|
|
19
21
|
const slots = components;
|
|
20
22
|
const CustomLabel = slots?.Label;
|
|
@@ -72,6 +74,7 @@ const TextAreaField = ({ field, value, defaultValue, onChange, id, path, locale,
|
|
|
72
74
|
placeholder: field.placeholder
|
|
73
75
|
});
|
|
74
76
|
return /*#__PURE__*/ jsx(TextArea, {
|
|
77
|
+
readOnly: field.readOnly,
|
|
75
78
|
id: htmlId,
|
|
76
79
|
name: field.name,
|
|
77
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 {};
|