@byline/admin 3.16.0 → 3.17.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.d.ts +8 -1
- package/dist/fields/array/array-field.js +3 -1
- package/dist/fields/field-renderer.d.ts +1 -1
- package/dist/fields/field-renderer.js +10 -1
- package/dist/fields/group/group-field.d.ts +7 -1
- package/dist/fields/group/group-field.js +2 -1
- package/dist/fields/use-field-change-handler.js +2 -1
- package/dist/fields/use-field-condition.d.ts +24 -0
- package/dist/fields/use-field-condition.js +28 -0
- package/dist/forms/form-context.js +4 -5
- package/package.json +5 -5
- package/src/fields/array/array-field.tsx +10 -0
- package/src/fields/field-renderer.tsx +23 -1
- package/src/fields/group/group-field.tsx +14 -1
- package/src/fields/use-field-change-handler.ts +4 -0
- package/src/fields/use-field-condition.ts +52 -0
- package/src/forms/form-context.tsx +16 -4
|
@@ -6,11 +6,18 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import type { ArrayField as ArrayFieldType } from '@byline/core';
|
|
9
|
-
export declare const ArrayField: ({ field, defaultValue, path, disableSorting, contentLocale, }: {
|
|
9
|
+
export declare const ArrayField: ({ field, defaultValue, path, disableSorting, collectionPath, contentLocale, }: {
|
|
10
10
|
field: ArrayFieldType;
|
|
11
11
|
defaultValue: any;
|
|
12
12
|
path: string;
|
|
13
13
|
disableSorting?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Collection path forwarded to upload-capable fields (`file` / `image`)
|
|
16
|
+
* nested inside an array item, which need it to reach the `/upload`
|
|
17
|
+
* endpoint. Without it those fields fall back to their empty placeholder
|
|
18
|
+
* and never render an upload widget.
|
|
19
|
+
*/
|
|
20
|
+
collectionPath?: string;
|
|
14
21
|
/**
|
|
15
22
|
* Active content locale, forwarded to each array item's fields so
|
|
16
23
|
* localized widgets nested inside an array (e.g. a `localized` richText)
|
|
@@ -8,7 +8,7 @@ import { FieldRenderer } from "../field-renderer.js";
|
|
|
8
8
|
import { SortableItem } from "../sortable-item.js";
|
|
9
9
|
import { useFormContext } from "../../forms/form-context.js";
|
|
10
10
|
import array_field_module from "./array-field.module.js";
|
|
11
|
-
const ArrayField = ({ field, defaultValue, path, disableSorting = false, contentLocale })=>{
|
|
11
|
+
const ArrayField = ({ field, defaultValue, path, disableSorting = false, collectionPath, contentLocale })=>{
|
|
12
12
|
const { appendPatch, getFieldValue, getFieldValues, setFieldStore } = useFormContext();
|
|
13
13
|
const { t } = useTranslation('byline-admin');
|
|
14
14
|
const [items, setItems] = useState([]);
|
|
@@ -118,6 +118,7 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
|
|
|
118
118
|
defaultValue: groupData[innerField.name],
|
|
119
119
|
basePath: `${arrayElementPath}.${childField.name}`,
|
|
120
120
|
disableSorting: true,
|
|
121
|
+
collectionPath: collectionPath,
|
|
121
122
|
contentLocale: contentLocale
|
|
122
123
|
}, innerField.name))
|
|
123
124
|
]
|
|
@@ -128,6 +129,7 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
|
|
|
128
129
|
defaultValue: initial,
|
|
129
130
|
basePath: arrayElementPath,
|
|
130
131
|
disableSorting: true,
|
|
132
|
+
collectionPath: collectionPath,
|
|
131
133
|
contentLocale: contentLocale
|
|
132
134
|
}, childField.name);
|
|
133
135
|
});
|
|
@@ -33,5 +33,5 @@ interface FieldRendererProps {
|
|
|
33
33
|
*/
|
|
34
34
|
editor?: RichTextEditorComponent;
|
|
35
35
|
}
|
|
36
|
-
export declare const FieldRenderer: ({ field, defaultValue, basePath, disableSorting, hideLabel, collectionPath, contentLocale, components, editor, }: FieldRendererProps) => import("react").JSX.Element | null;
|
|
36
|
+
export declare const FieldRenderer: ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, collectionPath, contentLocale, components, editor, }: FieldRendererProps) => import("react").JSX.Element | null;
|
|
37
37
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { getClientConfig } from "@byline/core";
|
|
3
3
|
import classnames from "classnames";
|
|
4
|
+
import { useFormContext } from "../forms/form-context.js";
|
|
4
5
|
import { ArrayField } from "./array/array-field.js";
|
|
5
6
|
import { BlocksField } from "./blocks/blocks-field.js";
|
|
6
7
|
import { CheckboxField } from "./checkbox/checkbox-field.js";
|
|
@@ -17,14 +18,20 @@ import { SelectField } from "./select/select-field.js";
|
|
|
17
18
|
import { TextField } from "./text/text-field.js";
|
|
18
19
|
import { TextAreaField } from "./text-area/text-area-field.js";
|
|
19
20
|
import { useFieldChangeHandler } from "./use-field-change-handler.js";
|
|
20
|
-
|
|
21
|
+
import { useFieldCondition } from "./use-field-condition.js";
|
|
22
|
+
const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, collectionPath, contentLocale, components, editor })=>{
|
|
21
23
|
const path = basePath ? `${basePath}.${field.name}` : field.name;
|
|
22
24
|
const htmlId = path.replace(/[[\].]/g, '-');
|
|
23
25
|
const handleChange = useFieldChangeHandler(field, path);
|
|
26
|
+
const { getFieldValue } = useFormContext();
|
|
27
|
+
const visible = useFieldCondition(field, basePath);
|
|
28
|
+
const storedValue = field.condition ? getFieldValue(path) : void 0;
|
|
29
|
+
const defaultValue = void 0 !== storedValue ? storedValue : initialDefault;
|
|
24
30
|
const isLocalised = true === field.localized;
|
|
25
31
|
const badge = isLocalised && contentLocale && !hideLabel ? /*#__PURE__*/ jsx(LocaleBadge, {
|
|
26
32
|
locale: contentLocale
|
|
27
33
|
}) : null;
|
|
34
|
+
if (!visible) return null;
|
|
28
35
|
const renderField = ()=>{
|
|
29
36
|
switch(field.type){
|
|
30
37
|
case 'text':
|
|
@@ -180,6 +187,7 @@ const FieldRenderer = ({ field, defaultValue, basePath, disableSorting, hideLabe
|
|
|
180
187
|
} : field,
|
|
181
188
|
defaultValue: defaultValue,
|
|
182
189
|
path: path,
|
|
190
|
+
collectionPath: collectionPath,
|
|
183
191
|
contentLocale: contentLocale
|
|
184
192
|
});
|
|
185
193
|
case 'blocks':
|
|
@@ -197,6 +205,7 @@ const FieldRenderer = ({ field, defaultValue, basePath, disableSorting, hideLabe
|
|
|
197
205
|
defaultValue: defaultValue,
|
|
198
206
|
path: path,
|
|
199
207
|
disableSorting: disableSorting,
|
|
208
|
+
collectionPath: collectionPath,
|
|
200
209
|
contentLocale: contentLocale
|
|
201
210
|
});
|
|
202
211
|
default:
|
|
@@ -10,6 +10,12 @@ interface GroupFieldProps {
|
|
|
10
10
|
field: GroupFieldType;
|
|
11
11
|
defaultValue: any;
|
|
12
12
|
path: string;
|
|
13
|
+
/**
|
|
14
|
+
* Collection path forwarded to upload-capable child fields (`file` / `image`),
|
|
15
|
+
* which need it to reach the `/upload` endpoint. Without it those fields fall
|
|
16
|
+
* back to their empty placeholder and never render an upload widget.
|
|
17
|
+
*/
|
|
18
|
+
collectionPath?: string;
|
|
13
19
|
/**
|
|
14
20
|
* Active content locale, forwarded to child fields so localized widgets
|
|
15
21
|
* nested inside the group (e.g. a `localized` richText) can render their
|
|
@@ -17,5 +23,5 @@ interface GroupFieldProps {
|
|
|
17
23
|
*/
|
|
18
24
|
contentLocale?: string;
|
|
19
25
|
}
|
|
20
|
-
export declare const GroupField: ({ field, defaultValue, path, contentLocale }: GroupFieldProps) => import("react").JSX.Element;
|
|
26
|
+
export declare const GroupField: ({ field, defaultValue, path, collectionPath, contentLocale, }: GroupFieldProps) => import("react").JSX.Element;
|
|
21
27
|
export {};
|
|
@@ -6,7 +6,7 @@ import { placeholderForField } from "../field-helpers.js";
|
|
|
6
6
|
import { FieldRenderer } from "../field-renderer.js";
|
|
7
7
|
import { useFieldError } from "../../forms/form-context.js";
|
|
8
8
|
import group_field_module from "./group-field.module.js";
|
|
9
|
-
const GroupField = ({ field, defaultValue, path, contentLocale })=>{
|
|
9
|
+
const GroupField = ({ field, defaultValue, path, collectionPath, contentLocale })=>{
|
|
10
10
|
const fieldError = useFieldError(field.name);
|
|
11
11
|
const groupData = useMemo(()=>{
|
|
12
12
|
if (defaultValue && 'object' == typeof defaultValue && !Array.isArray(defaultValue)) return defaultValue;
|
|
@@ -47,6 +47,7 @@ const GroupField = ({ field, defaultValue, path, contentLocale })=>{
|
|
|
47
47
|
defaultValue: groupData[innerField.name],
|
|
48
48
|
basePath: path,
|
|
49
49
|
disableSorting: true,
|
|
50
|
+
collectionPath: collectionPath,
|
|
50
51
|
contentLocale: contentLocale
|
|
51
52
|
}, innerField.name))
|
|
52
53
|
}),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
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
|
+
export declare const useFieldCondition: (field: Field, basePath?: string) => boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { useFormContext } from "../forms/form-context.js";
|
|
3
|
+
import { get } from "../forms/nested-path.js";
|
|
4
|
+
const useFieldCondition = (field, basePath)=>{
|
|
5
|
+
const { getFieldValues, subscribeMeta } = useFormContext();
|
|
6
|
+
const evaluate = useCallback(()=>{
|
|
7
|
+
if (!field.condition) return true;
|
|
8
|
+
const data = getFieldValues();
|
|
9
|
+
const siblingData = basePath ? get(data, basePath) ?? {} : data;
|
|
10
|
+
return Boolean(field.condition(data, siblingData));
|
|
11
|
+
}, [
|
|
12
|
+
field,
|
|
13
|
+
basePath,
|
|
14
|
+
getFieldValues
|
|
15
|
+
]);
|
|
16
|
+
const [visible, setVisible] = useState(evaluate);
|
|
17
|
+
useEffect(()=>{
|
|
18
|
+
if (!field.condition) return;
|
|
19
|
+
setVisible(evaluate());
|
|
20
|
+
return subscribeMeta(()=>setVisible(evaluate()));
|
|
21
|
+
}, [
|
|
22
|
+
field.condition,
|
|
23
|
+
subscribeMeta,
|
|
24
|
+
evaluate
|
|
25
|
+
]);
|
|
26
|
+
return visible;
|
|
27
|
+
};
|
|
28
|
+
export { useFieldCondition };
|
|
@@ -144,10 +144,6 @@ const FormProvider = ({ children, initialData = {} })=>{
|
|
|
144
144
|
];
|
|
145
145
|
dirtyFields.current.add('__patch__');
|
|
146
146
|
notifyMetaListeners();
|
|
147
|
-
if ('production' !== process.env.NODE_ENV) console.debug('FormContext.appendPatch', {
|
|
148
|
-
patch,
|
|
149
|
-
dirtyCount: dirtyFields.current.size
|
|
150
|
-
});
|
|
151
147
|
}, [
|
|
152
148
|
notifyMetaListeners
|
|
153
149
|
]);
|
|
@@ -248,6 +244,7 @@ const FormProvider = ({ children, initialData = {} })=>{
|
|
|
248
244
|
const formErrors = [];
|
|
249
245
|
const data = getFieldValues();
|
|
250
246
|
for (const field of fields){
|
|
247
|
+
if (field.condition && !field.condition(data, data)) continue;
|
|
251
248
|
const value = getFieldValue(field.name);
|
|
252
249
|
if (!field.optional && (null == value || '' === value)) formErrors.push({
|
|
253
250
|
field: field.name,
|
|
@@ -331,6 +328,7 @@ const FormProvider = ({ children, initialData = {} })=>{
|
|
|
331
328
|
for (const field of fields){
|
|
332
329
|
const fns = normalizeHooks(field.hooks?.beforeValidate);
|
|
333
330
|
if (0 === fns.length) continue;
|
|
331
|
+
if (field.condition && !field.condition(data, data)) continue;
|
|
334
332
|
const path = field.name;
|
|
335
333
|
const value = getFieldValue(path);
|
|
336
334
|
const ctx = {
|
|
@@ -339,7 +337,8 @@ const FormProvider = ({ children, initialData = {} })=>{
|
|
|
339
337
|
data,
|
|
340
338
|
path,
|
|
341
339
|
field,
|
|
342
|
-
operation: 'submit'
|
|
340
|
+
operation: 'submit',
|
|
341
|
+
setFieldValue
|
|
343
342
|
};
|
|
344
343
|
try {
|
|
345
344
|
for (const fn of fns){
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/admin",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.17.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -155,10 +155,10 @@
|
|
|
155
155
|
"react-diff-viewer-continued": "^4.2.2",
|
|
156
156
|
"uuid": "^14.0.0",
|
|
157
157
|
"zod": "^4.4.3",
|
|
158
|
-
"@byline/auth": "3.
|
|
159
|
-
"@byline/
|
|
160
|
-
"@byline/core": "3.
|
|
161
|
-
"@byline/
|
|
158
|
+
"@byline/auth": "3.17.0",
|
|
159
|
+
"@byline/i18n": "3.17.0",
|
|
160
|
+
"@byline/core": "3.17.0",
|
|
161
|
+
"@byline/ui": "3.17.0"
|
|
162
162
|
},
|
|
163
163
|
"peerDependencies": {
|
|
164
164
|
"react": "^19.0.0",
|
|
@@ -29,12 +29,20 @@ export const ArrayField = ({
|
|
|
29
29
|
defaultValue,
|
|
30
30
|
path,
|
|
31
31
|
disableSorting = false,
|
|
32
|
+
collectionPath,
|
|
32
33
|
contentLocale,
|
|
33
34
|
}: {
|
|
34
35
|
field: ArrayFieldType
|
|
35
36
|
defaultValue: any
|
|
36
37
|
path: string
|
|
37
38
|
disableSorting?: boolean
|
|
39
|
+
/**
|
|
40
|
+
* Collection path forwarded to upload-capable fields (`file` / `image`)
|
|
41
|
+
* nested inside an array item, which need it to reach the `/upload`
|
|
42
|
+
* endpoint. Without it those fields fall back to their empty placeholder
|
|
43
|
+
* and never render an upload widget.
|
|
44
|
+
*/
|
|
45
|
+
collectionPath?: string
|
|
38
46
|
/**
|
|
39
47
|
* Active content locale, forwarded to each array item's fields so
|
|
40
48
|
* localized widgets nested inside an array (e.g. a `localized` richText)
|
|
@@ -205,6 +213,7 @@ export const ArrayField = ({
|
|
|
205
213
|
defaultValue={groupData[innerField.name]}
|
|
206
214
|
basePath={`${arrayElementPath}.${childField.name}`}
|
|
207
215
|
disableSorting={true}
|
|
216
|
+
collectionPath={collectionPath}
|
|
208
217
|
contentLocale={contentLocale}
|
|
209
218
|
/>
|
|
210
219
|
))}
|
|
@@ -219,6 +228,7 @@ export const ArrayField = ({
|
|
|
219
228
|
defaultValue={initial}
|
|
220
229
|
basePath={arrayElementPath}
|
|
221
230
|
disableSorting={true}
|
|
231
|
+
collectionPath={collectionPath}
|
|
222
232
|
contentLocale={contentLocale}
|
|
223
233
|
/>
|
|
224
234
|
)
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
import { getClientConfig } from '@byline/core'
|
|
18
18
|
import cx from 'classnames'
|
|
19
19
|
|
|
20
|
+
import { useFormContext } from '../forms/form-context'
|
|
20
21
|
import { ArrayField } from './array/array-field'
|
|
21
22
|
import { BlocksField } from './blocks/blocks-field'
|
|
22
23
|
import { CheckboxField } from './checkbox/checkbox-field'
|
|
@@ -33,6 +34,7 @@ import { SelectField } from './select/select-field'
|
|
|
33
34
|
import { TextField } from './text/text-field'
|
|
34
35
|
import { TextAreaField } from './text-area/text-area-field'
|
|
35
36
|
import { useFieldChangeHandler } from './use-field-change-handler'
|
|
37
|
+
import { useFieldCondition } from './use-field-condition'
|
|
36
38
|
|
|
37
39
|
// ---------------------------------------------------------------------------
|
|
38
40
|
// FieldRenderer — the main field type switch. Delegates to the appropriate
|
|
@@ -69,7 +71,7 @@ interface FieldRendererProps {
|
|
|
69
71
|
|
|
70
72
|
export const FieldRenderer = ({
|
|
71
73
|
field,
|
|
72
|
-
defaultValue,
|
|
74
|
+
defaultValue: initialDefault,
|
|
73
75
|
basePath,
|
|
74
76
|
disableSorting,
|
|
75
77
|
hideLabel,
|
|
@@ -82,6 +84,20 @@ export const FieldRenderer = ({
|
|
|
82
84
|
const htmlId = path.replace(/[[\].]/g, '-')
|
|
83
85
|
|
|
84
86
|
const handleChange = useFieldChangeHandler(field, path)
|
|
87
|
+
const { getFieldValue } = useFormContext()
|
|
88
|
+
|
|
89
|
+
// Conditional visibility (BaseField.condition) — re-evaluated on every form
|
|
90
|
+
// edit; the field unmounts while its condition is false.
|
|
91
|
+
const visible = useFieldCondition(field, basePath)
|
|
92
|
+
|
|
93
|
+
// Conditional fields unmount while hidden, so on re-show the uncontrolled
|
|
94
|
+
// widget must be re-seeded from the live form store (which survives the
|
|
95
|
+
// unmount) rather than the initial document data — otherwise an edit made
|
|
96
|
+
// before hiding would be visually reverted while the store (and the patch
|
|
97
|
+
// stream) still carried it. `undefined` means the path was never written,
|
|
98
|
+
// in which case the initial default stands.
|
|
99
|
+
const storedValue = field.condition ? getFieldValue(path) : undefined
|
|
100
|
+
const defaultValue = storedValue !== undefined ? storedValue : initialDefault
|
|
85
101
|
|
|
86
102
|
// When a locale is active and the field is localised, inject a badge into
|
|
87
103
|
// the field label so the editor knows they are editing locale-specific content.
|
|
@@ -90,6 +106,10 @@ export const FieldRenderer = ({
|
|
|
90
106
|
const badge =
|
|
91
107
|
isLocalised && contentLocale && !hideLabel ? <LocaleBadge locale={contentLocale} /> : null
|
|
92
108
|
|
|
109
|
+
// All hooks have run by this point, so a conditional bail-out is safe.
|
|
110
|
+
// Hiding retains the field's stored value — no clearing patch is emitted.
|
|
111
|
+
if (!visible) return null
|
|
112
|
+
|
|
93
113
|
/**
|
|
94
114
|
* Render the underlying field widget. If the field is localised, we wrap it
|
|
95
115
|
* so we can append the locale badge after the label.
|
|
@@ -254,6 +274,7 @@ export const FieldRenderer = ({
|
|
|
254
274
|
}
|
|
255
275
|
defaultValue={defaultValue}
|
|
256
276
|
path={path}
|
|
277
|
+
collectionPath={collectionPath}
|
|
257
278
|
contentLocale={contentLocale}
|
|
258
279
|
/>
|
|
259
280
|
)
|
|
@@ -275,6 +296,7 @@ export const FieldRenderer = ({
|
|
|
275
296
|
defaultValue={defaultValue}
|
|
276
297
|
path={path}
|
|
277
298
|
disableSorting={disableSorting}
|
|
299
|
+
collectionPath={collectionPath}
|
|
278
300
|
contentLocale={contentLocale}
|
|
279
301
|
/>
|
|
280
302
|
)
|
|
@@ -32,6 +32,12 @@ interface GroupFieldProps {
|
|
|
32
32
|
field: GroupFieldType
|
|
33
33
|
defaultValue: any
|
|
34
34
|
path: string
|
|
35
|
+
/**
|
|
36
|
+
* Collection path forwarded to upload-capable child fields (`file` / `image`),
|
|
37
|
+
* which need it to reach the `/upload` endpoint. Without it those fields fall
|
|
38
|
+
* back to their empty placeholder and never render an upload widget.
|
|
39
|
+
*/
|
|
40
|
+
collectionPath?: string
|
|
35
41
|
/**
|
|
36
42
|
* Active content locale, forwarded to child fields so localized widgets
|
|
37
43
|
* nested inside the group (e.g. a `localized` richText) can render their
|
|
@@ -40,7 +46,13 @@ interface GroupFieldProps {
|
|
|
40
46
|
contentLocale?: string
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
export const GroupField = ({
|
|
49
|
+
export const GroupField = ({
|
|
50
|
+
field,
|
|
51
|
+
defaultValue,
|
|
52
|
+
path,
|
|
53
|
+
collectionPath,
|
|
54
|
+
contentLocale,
|
|
55
|
+
}: GroupFieldProps) => {
|
|
44
56
|
const fieldError = useFieldError(field.name)
|
|
45
57
|
// Default value for a group field is a plain object: { rating: 5, comment: '...' }
|
|
46
58
|
// Normalize to a plain object if not already one.
|
|
@@ -80,6 +92,7 @@ export const GroupField = ({ field, defaultValue, path, contentLocale }: GroupFi
|
|
|
80
92
|
defaultValue={groupData[innerField.name]}
|
|
81
93
|
basePath={path}
|
|
82
94
|
disableSorting={true}
|
|
95
|
+
collectionPath={collectionPath}
|
|
83
96
|
contentLocale={contentLocale}
|
|
84
97
|
/>
|
|
85
98
|
)
|
|
@@ -53,6 +53,10 @@ export function useFieldChangeHandler(field: Field, path: string) {
|
|
|
53
53
|
path,
|
|
54
54
|
field,
|
|
55
55
|
operation: 'change',
|
|
56
|
+
// Raw store write for cross-field behaviour (e.g. mutual exclusivity
|
|
57
|
+
// across array items). Deliberately does not run the target field's
|
|
58
|
+
// own hooks — see FieldHookContext.setFieldValue.
|
|
59
|
+
setFieldValue,
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
clearFieldError(path)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
10
|
+
|
|
11
|
+
import type { Field } from '@byline/core'
|
|
12
|
+
|
|
13
|
+
import { useFormContext } from '../forms/form-context'
|
|
14
|
+
import { get as getNestedValue } from '../forms/nested-path'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Evaluate a field's visibility `condition` against live form data.
|
|
18
|
+
*
|
|
19
|
+
* Subscribes to the form's meta listeners — fired on every field commit, the
|
|
20
|
+
* same loop `TabDefinition.condition` rides — and recomputes the predicate.
|
|
21
|
+
* The consumer only re-renders when the boolean actually flips, so a stable
|
|
22
|
+
* condition costs one function call per form edit, no reconciliation.
|
|
23
|
+
*
|
|
24
|
+
* `basePath` is the field's sibling scope: the enclosing group / array item
|
|
25
|
+
* (e.g. `files[0].filesGroup` for `files[0].filesGroup.thumbnailPage`).
|
|
26
|
+
* Root-level fields pass no basePath and receive the full form data as their
|
|
27
|
+
* sibling scope, mirroring `FieldCondition`'s contract.
|
|
28
|
+
*
|
|
29
|
+
* Fields without a condition are always visible and no subscription is made.
|
|
30
|
+
*/
|
|
31
|
+
export const useFieldCondition = (field: Field, basePath?: string): boolean => {
|
|
32
|
+
const { getFieldValues, subscribeMeta } = useFormContext()
|
|
33
|
+
|
|
34
|
+
const evaluate = useCallback((): boolean => {
|
|
35
|
+
if (!field.condition) return true
|
|
36
|
+
const data = getFieldValues()
|
|
37
|
+
const siblingData = basePath ? (getNestedValue(data, basePath) ?? {}) : data
|
|
38
|
+
return Boolean(field.condition(data, siblingData))
|
|
39
|
+
}, [field, basePath, getFieldValues])
|
|
40
|
+
|
|
41
|
+
const [visible, setVisible] = useState<boolean>(evaluate)
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!field.condition) return undefined
|
|
45
|
+
// Re-evaluate on (re)subscribe as well as on every subsequent form edit —
|
|
46
|
+
// the store may have changed between the initial render and this effect.
|
|
47
|
+
setVisible(evaluate())
|
|
48
|
+
return subscribeMeta(() => setVisible(evaluate()))
|
|
49
|
+
}, [field.condition, subscribeMeta, evaluate])
|
|
50
|
+
|
|
51
|
+
return visible
|
|
52
|
+
}
|
|
@@ -308,10 +308,11 @@ export const FormProvider = ({
|
|
|
308
308
|
// for patches that don't correspond to a specific field.set.
|
|
309
309
|
dirtyFields.current.add('__patch__')
|
|
310
310
|
notifyMetaListeners()
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
311
|
+
// Dev-time patch tracing — uncomment when debugging the patch stream.
|
|
312
|
+
// if (process.env.NODE_ENV !== 'production') {
|
|
313
|
+
// // eslint-disable-next-line no-console
|
|
314
|
+
// console.debug('FormContext.appendPatch', { patch, dirtyCount: dirtyFields.current.size })
|
|
315
|
+
// }
|
|
315
316
|
},
|
|
316
317
|
[notifyMetaListeners]
|
|
317
318
|
)
|
|
@@ -476,6 +477,12 @@ export const FormProvider = ({
|
|
|
476
477
|
const data = getFieldValues()
|
|
477
478
|
|
|
478
479
|
for (const field of fields) {
|
|
480
|
+
// Condition-hidden fields are exempt from client-side validation — a
|
|
481
|
+
// field the editor cannot currently see must not block submit. Only
|
|
482
|
+
// top-level fields flow through this walk, and a root-level field's
|
|
483
|
+
// sibling scope is the form data itself (see FieldCondition).
|
|
484
|
+
if (field.condition && !field.condition(data, data)) continue
|
|
485
|
+
|
|
479
486
|
const value = getFieldValue(field.name)
|
|
480
487
|
|
|
481
488
|
// Required field validation
|
|
@@ -583,6 +590,10 @@ export const FormProvider = ({
|
|
|
583
590
|
const fns = normalizeHooks(field.hooks?.beforeValidate)
|
|
584
591
|
if (fns.length === 0) continue
|
|
585
592
|
|
|
593
|
+
// Condition-hidden fields skip submit-time hooks, mirroring their
|
|
594
|
+
// exemption from validateForm below.
|
|
595
|
+
if (field.condition && !field.condition(data, data)) continue
|
|
596
|
+
|
|
586
597
|
const path = field.name
|
|
587
598
|
const value = getFieldValue(path)
|
|
588
599
|
|
|
@@ -593,6 +604,7 @@ export const FormProvider = ({
|
|
|
593
604
|
path,
|
|
594
605
|
field,
|
|
595
606
|
operation: 'submit',
|
|
607
|
+
setFieldValue,
|
|
596
608
|
}
|
|
597
609
|
|
|
598
610
|
try {
|