@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.
Files changed (82) hide show
  1. package/dist/fields/array/array-field.js +29 -12
  2. package/dist/fields/blocks/blocks-field.js +20 -8
  3. package/dist/fields/checkbox/checkbox-field.js +4 -1
  4. package/dist/fields/code/code-field.js +2 -1
  5. package/dist/fields/datetime/datetime-field.js +5 -2
  6. package/dist/fields/field-renderer.js +2 -1
  7. package/dist/fields/field-services-types.d.ts +23 -0
  8. package/dist/fields/file/file-field.js +6 -4
  9. package/dist/fields/group/group-field.js +8 -3
  10. package/dist/fields/image/image-field.js +6 -4
  11. package/dist/fields/numerical/numerical-field.js +2 -1
  12. package/dist/fields/relation/relation-field.js +8 -2
  13. package/dist/fields/relation/relation-many-field.js +12 -4
  14. package/dist/fields/relation/relation-picker.js +38 -7
  15. package/dist/fields/relation/relation-picker.module.js +3 -0
  16. package/dist/fields/relation/relation-picker_module.css +20 -0
  17. package/dist/fields/select/select-field.js +4 -2
  18. package/dist/fields/text/text-field.js +6 -3
  19. package/dist/fields/text-area/text-area-field.js +6 -3
  20. package/dist/fields/use-field-change-handler.js +24 -6
  21. package/dist/fields/use-field-condition.d.ts +0 -15
  22. package/dist/fields/use-field-condition.js +10 -1
  23. package/dist/forms/form-context.d.ts +3 -12
  24. package/dist/forms/form-context.js +115 -130
  25. package/dist/forms/form-dom-scope.d.ts +34 -0
  26. package/dist/forms/form-dom-scope.js +20 -0
  27. package/dist/forms/form-layout.js +10 -3
  28. package/dist/forms/form-page-chrome.js +14 -24
  29. package/dist/forms/form-renderer.d.ts +2 -1
  30. package/dist/forms/form-renderer.js +13 -56
  31. package/dist/forms/nested-path.d.ts +2 -0
  32. package/dist/forms/nested-path.js +26 -1
  33. package/dist/forms/use-document-reload-recovery.d.ts +22 -0
  34. package/dist/forms/use-document-reload-recovery.js +67 -0
  35. package/dist/forms/use-form-submission.d.ts +1 -1
  36. package/dist/forms/use-form-submission.js +45 -27
  37. package/dist/forms/use-form-tabs.d.ts +2 -2
  38. package/dist/forms/use-form-tabs.js +18 -11
  39. package/dist/presentation/tabs.js +2 -1
  40. package/dist/react.d.ts +1 -1
  41. package/package.json +14 -14
  42. package/src/fields/array/array-field.tsx +24 -12
  43. package/src/fields/blocks/blocks-field.tsx +18 -8
  44. package/src/fields/checkbox/checkbox-field.tsx +4 -1
  45. package/src/fields/code/code-field.tsx +2 -1
  46. package/src/fields/datetime/datetime-field.tsx +5 -2
  47. package/src/fields/direct-widget-dom-ids.test.tsx +142 -0
  48. package/src/fields/field-dom-ids.test.tsx +293 -0
  49. package/src/fields/field-renderer.tsx +2 -1
  50. package/src/fields/field-services-types.ts +25 -0
  51. package/src/fields/file/file-field.tsx +6 -4
  52. package/src/fields/group/group-field.tsx +5 -3
  53. package/src/fields/image/image-field.tsx +6 -4
  54. package/src/fields/numerical/numerical-field.tsx +2 -1
  55. package/src/fields/relation/relation-field.tsx +8 -2
  56. package/src/fields/relation/relation-many-field.tsx +19 -4
  57. package/src/fields/relation/relation-picker-create.test.tsx +203 -0
  58. package/src/fields/relation/relation-picker-refresh.test.tsx +325 -0
  59. package/src/fields/relation/relation-picker.module.css +32 -0
  60. package/src/fields/relation/relation-picker.tsx +59 -1
  61. package/src/fields/select/select-field.tsx +4 -2
  62. package/src/fields/text/text-field.tsx +5 -3
  63. package/src/fields/text-area/text-area-field.tsx +5 -3
  64. package/src/fields/use-field-change-handler.ts +43 -9
  65. package/src/fields/use-field-condition.ts +17 -1
  66. package/src/forms/form-context.tsx +180 -234
  67. package/src/forms/form-correctness.test.tsx +560 -0
  68. package/src/forms/form-dom-scope.test.tsx +213 -0
  69. package/src/forms/form-dom-scope.tsx +70 -0
  70. package/src/forms/form-layout.tsx +10 -3
  71. package/src/forms/form-page-chrome.tsx +17 -34
  72. package/src/forms/form-renderer-submit.test.tsx +36 -0
  73. package/src/forms/form-renderer.tsx +26 -64
  74. package/src/forms/form-submit-isolation.test.tsx +247 -0
  75. package/src/forms/nested-path.ts +35 -0
  76. package/src/forms/use-document-reload-recovery.ts +92 -0
  77. package/src/forms/use-form-submission.ts +56 -27
  78. package/src/forms/use-form-tabs.ts +25 -17
  79. package/src/presentation/tab-dom-ids.test.tsx +153 -0
  80. package/src/presentation/tabs.test.tsx +49 -2
  81. package/src/presentation/tabs.tsx +11 -1
  82. 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 ?? fieldPath;
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: `${field.name}-error`,
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 { getCollectionDocuments } = useBylineFieldServices();
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__*/ jsx(Modal.Header, {
133
+ /*#__PURE__*/ jsxs(Modal.Header, {
131
134
  className: clsx('byline-field-relation-picker-header', relation_picker_module.header),
132
- children: /*#__PURE__*/ jsx("h3", {
133
- className: clsx('byline-field-relation-picker-title', relation_picker_module.title),
134
- children: title
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,9 @@
1
1
  import "./relation-picker_module.css";
2
2
  const relation_picker_module = {
3
3
  header: "header-quQOWP",
4
+ actions: "actions-s42_zl",
5
+ refresh: "refresh-CVQwEV",
6
+ create: "create-Ol3jYM",
4
7
  title: "title-nX1CSv",
5
8
  body: "body-S2EcHm",
6
9
  list: "list-UFzd89",
@@ -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 ?? fieldPath;
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: `${field.name}-error`,
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 ?? fieldPath;
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 ?? fieldPath;
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
- setFieldValue(path, ctx.value);
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
- return Boolean(field.condition(data, siblingData));
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 {};