@object-ui/fields 17.2.0 → 17.5.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/CHANGELOG.md +1533 -0
- package/README.md +1 -12
- package/dist/FieldEditWidget.d.ts +7 -1
- package/dist/currency.d.ts +46 -0
- package/dist/field-type-alias.d.ts +19 -1
- package/dist/index.cjs +5 -5
- package/dist/index.css +633 -0
- package/dist/index.d.ts +52 -12
- package/dist/index.js +5167 -4759
- package/dist/widgets/AddressField.d.ts +4 -12
- package/dist/widgets/AvatarField.d.ts +1 -1
- package/dist/widgets/CharacterCount.d.ts +56 -0
- package/dist/widgets/CheckboxesField.d.ts +1 -1
- package/dist/widgets/CodeField.d.ts +1 -1
- package/dist/widgets/ColorField.d.ts +1 -1
- package/dist/widgets/CurrencyField.d.ts +1 -1
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +1 -1
- package/dist/widgets/FilterConditionField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +45 -0
- package/dist/widgets/GeolocationField.d.ts +1 -1
- package/dist/widgets/GridField.d.ts +12 -2
- package/dist/widgets/ImageField.d.ts +1 -1
- package/dist/widgets/LocationField.d.ts +1 -1
- package/dist/widgets/LookupField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/ObjectField.d.ts +1 -1
- package/dist/widgets/ObjectRefField.d.ts +1 -1
- package/dist/widgets/OptionsEmptyState.d.ts +59 -0
- package/dist/widgets/PercentField.d.ts +1 -1
- package/dist/widgets/PhoneField.d.ts +1 -1
- package/dist/widgets/QRCodeField.d.ts +1 -1
- package/dist/widgets/RadioField.d.ts +1 -1
- package/dist/widgets/RatingField.d.ts +1 -1
- package/dist/widgets/RecipientPickerField.d.ts +1 -1
- package/dist/widgets/RecordPickerDialog.d.ts +49 -7
- package/dist/widgets/RichTextField.d.ts +19 -1
- package/dist/widgets/SelectField.d.ts +25 -5
- package/dist/widgets/TagsField.d.ts +1 -1
- package/dist/widgets/TextAreaField.d.ts +44 -5
- package/dist/widgets/UrlField.d.ts +1 -1
- package/dist/widgets/address-format.d.ts +68 -0
- package/dist/widgets/file-size-guard.d.ts +41 -0
- package/dist/widgets/nativeDateValue.d.ts +37 -0
- package/dist/widgets/toDomProps.d.ts +111 -0
- package/dist/widgets/toHostGroupProps.d.ts +91 -0
- package/dist/widgets/types.d.ts +128 -33
- package/dist/widgets/useRecordQuery.d.ts +7 -1
- package/dist/withFieldCarrier.d.ts +56 -0
- package/package.json +20 -16
- package/src/FieldEditWidget.tsx +17 -1
- package/src/__tests__/AddressCellRenderer.test.tsx +166 -0
- package/src/__tests__/CurrencyField.minorUnits.test.tsx +191 -0
- package/src/__tests__/CurrencyField.symbol.test.tsx +198 -0
- package/src/__tests__/DateCellRenderer.test.tsx +9 -0
- package/src/__tests__/NumberCellRenderer.grouping.test.tsx +146 -0
- package/src/__tests__/PercentCellRenderer.locale.test.tsx +152 -0
- package/src/__tests__/PercentCellRenderer.test.tsx +115 -0
- package/src/__tests__/boolean-label-association-e2e.test.tsx +298 -0
- package/src/__tests__/capability-multiselect-retired.test.ts +109 -0
- package/src/__tests__/cell-truncation.test.tsx +96 -0
- package/src/__tests__/composite-group-label-e2e.test.tsx +436 -0
- package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +403 -0
- package/src/__tests__/date-formatter-residue-4272.test.ts +120 -0
- package/src/__tests__/date-locale-channel.test.tsx +305 -0
- package/src/__tests__/field-carrier-sdui.test.tsx +169 -0
- package/src/__tests__/formatCurrency.centsDigit.test.tsx +158 -0
- package/src/__tests__/formatCurrency.minorUnits.test.tsx +221 -0
- package/src/__tests__/group-labelling-declaration.test.ts +148 -0
- package/src/__tests__/lookupCellRecordLink.test.tsx +171 -0
- package/src/__tests__/percent-cell-vs-measure-4576.test.ts +115 -0
- package/src/__tests__/percent-formatter-locale-4553.test.ts +147 -0
- package/src/__tests__/percent-tie-halfup-4590.test.ts +224 -0
- package/src/__tests__/select-label-association-e2e.test.tsx +199 -0
- package/src/__tests__/spec-symbol-batch7.test.ts +29 -15
- package/src/__tests__/validation-feedback.test.tsx +22 -13
- package/src/__tests__/widget-aria-invalid-e2e.test.tsx +265 -0
- package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +338 -0
- package/src/__tests__/widget-dom-leak-e2e.test.tsx +357 -0
- package/src/__tests__/widget-props-contract.test.tsx +22 -9
- package/src/complex-widgets.test.tsx +22 -6
- package/src/currency.ts +130 -0
- package/src/datetime-widgets.test.tsx +94 -2
- package/src/field-type-alias.multiple.test.ts +81 -0
- package/src/field-type-alias.ts +58 -2
- package/src/index.css +44 -0
- package/src/index.tsx +558 -268
- package/src/standard-widgets.test.tsx +8 -1
- package/src/widgets/AddressField.postalCode.test.tsx +257 -0
- package/src/widgets/AddressField.tsx +107 -38
- package/src/widgets/AddressField.uniqueIds.test.tsx +93 -0
- package/src/widgets/AvatarField.tsx +9 -2
- package/src/widgets/BooleanField.tsx +44 -10
- package/src/widgets/CapabilityMultiSelectField.tsx +17 -5
- package/src/widgets/CharacterCount.tsx +263 -0
- package/src/widgets/CheckboxesField.tsx +66 -16
- package/src/widgets/CodeField.tsx +9 -2
- package/src/widgets/ColorField.tsx +11 -2
- package/src/widgets/CurrencyField.tsx +60 -19
- package/src/widgets/DateField.tsx +13 -4
- package/src/widgets/DateTimeField.tsx +14 -5
- package/src/widgets/EmailField.tsx +5 -5
- package/src/widgets/FileField.tsx +39 -11
- package/src/widgets/FilterConditionField.tsx +11 -0
- package/src/widgets/FormulaField.tsx +7 -2
- package/src/widgets/FullscreenFieldEditor.tsx +60 -0
- package/src/widgets/GeolocationField.tsx +49 -8
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +88 -0
- package/src/widgets/GridField.test.tsx +226 -0
- package/src/widgets/GridField.tsx +115 -11
- package/src/widgets/ImageField.maxSize.test.tsx +154 -0
- package/src/widgets/ImageField.tsx +63 -6
- package/src/widgets/LocationField.tsx +9 -2
- package/src/widgets/LookupField.gateHintLabel.test.tsx +135 -0
- package/src/widgets/LookupField.hydration.test.tsx +1 -1
- package/src/widgets/LookupField.tsx +49 -10
- package/src/widgets/MasterDetailField.tsx +1 -1
- package/src/widgets/MultiSelectField.tsx +86 -16
- package/src/widgets/NumberField.tsx +3 -3
- package/src/widgets/ObjectField.tsx +17 -8
- package/src/widgets/ObjectRefField.tsx +10 -0
- package/src/widgets/OptionsEmptyState.no-provider.test.tsx +88 -0
- package/src/widgets/OptionsEmptyState.test.tsx +169 -0
- package/src/widgets/OptionsEmptyState.tsx +99 -0
- package/src/widgets/PasswordField.tsx +3 -3
- package/src/widgets/PeoplePicker.tsx +7 -1
- package/src/widgets/PercentField.tsx +5 -4
- package/src/widgets/PhoneField.tsx +5 -5
- package/src/widgets/QRCodeField.tsx +9 -2
- package/src/widgets/RadioField.tsx +58 -16
- package/src/widgets/RatingField.tsx +32 -4
- package/src/widgets/RecipientPickerField.tsx +18 -0
- package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +191 -0
- package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +124 -0
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +611 -0
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +112 -0
- package/src/widgets/RecordPickerDialog.tsx +235 -21
- package/src/widgets/RichTextField.tsx +151 -24
- package/src/widgets/SelectField.tsx +84 -22
- package/src/widgets/SliderField.tsx +1 -1
- package/src/widgets/SummaryField.tsx +1 -1
- package/src/widgets/TagsField.placeholder.no-provider.test.tsx +68 -0
- package/src/widgets/TagsField.placeholder.test.tsx +95 -0
- package/src/widgets/TagsField.tsx +23 -2
- package/src/widgets/TextAreaField.tsx +171 -93
- package/src/widgets/TextField.tsx +3 -3
- package/src/widgets/TimeField.tsx +2 -2
- package/src/widgets/UrlField.tsx +5 -5
- package/src/widgets/UserField.tsx +4 -3
- package/src/widgets/VectorField.tsx +1 -1
- package/src/widgets/__tests__/FilterConditionField.operators.test.ts +25 -1
- package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +248 -0
- package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +116 -0
- package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +244 -0
- package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +195 -0
- package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +417 -0
- package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +287 -0
- package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +160 -0
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +161 -0
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +346 -0
- package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +246 -0
- package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +175 -0
- package/src/widgets/address-format.ts +120 -0
- package/src/widgets/file-size-guard.ts +55 -0
- package/src/widgets/nativeDateValue.test.ts +115 -0
- package/src/widgets/nativeDateValue.ts +112 -0
- package/src/widgets/optionWidgets.gatedOptions.test.tsx +267 -0
- package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +150 -0
- package/src/widgets/toDomProps.ts +204 -0
- package/src/widgets/toHostGroupProps.ts +102 -0
- package/src/widgets/types.ts +135 -35
- package/src/widgets/useFieldTranslation.ts +46 -4
- package/src/widgets/useRecordQuery.ts +30 -3
- package/src/withFieldCarrier.tsx +75 -0
- package/dist/FieldEditWidget.test.d.ts +0 -8
- package/dist/__tests__/DateCellRenderer.test.d.ts +0 -0
- package/dist/__tests__/NumberCellRenderer.test.d.ts +0 -0
- package/dist/__tests__/lookupCellDisplayField.test.d.ts +0 -0
- package/dist/__tests__/lookupCellDisplayName.test.d.ts +0 -0
- package/dist/__tests__/lookupCellReferenceKey.test.d.ts +0 -0
- package/dist/__tests__/registry-collision.test.d.ts +0 -0
- package/dist/__tests__/resolveFieldCurrency.test.d.ts +0 -1
- package/dist/__tests__/spec-symbol-batch7.test.d.ts +0 -8
- package/dist/__tests__/validation-feedback.test.d.ts +0 -0
- package/dist/__tests__/widget-props-contract.test.d.ts +0 -8
- package/dist/coerce-safe-value.test.d.ts +0 -1
- package/dist/complex-widgets.test.d.ts +0 -1
- package/dist/datetime-widgets.test.d.ts +0 -1
- package/dist/field-type-coverage.test.d.ts +0 -1
- package/dist/standard-widgets.test.d.ts +0 -0
- package/dist/widgets/CapabilityMultiSelectField.test.d.ts +0 -0
- package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
- package/dist/widgets/GridField.test.d.ts +0 -1
- package/dist/widgets/ImageLightbox.test.d.ts +0 -1
- package/dist/widgets/LookupField.dependsOn.test.d.ts +0 -8
- package/dist/widgets/LookupField.hydration.test.d.ts +0 -1
- package/dist/widgets/LookupField.idField.test.d.ts +0 -1
- package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
- package/dist/widgets/PeoplePicker.test.d.ts +0 -8
- package/dist/widgets/PeoplePickerComponents.test.d.ts +0 -8
- package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
- package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
- package/dist/widgets/SelectField.cascade.test.d.ts +0 -0
- package/dist/widgets/UserFieldPicker.test.d.ts +0 -8
- package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +0 -8
- package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +0 -8
- package/dist/widgets/deriveLookupColumns.test.d.ts +0 -1
- package/dist/widgets/file-value.test.d.ts +0 -1
- package/dist/widgets/personDisplay.test.d.ts +0 -8
- package/dist/widgets/recentLookups.test.d.ts +0 -1
- package/dist/widgets/useRecordQuery.test.d.ts +0 -8
- package/dist/widgets/useUploadingSignal.test.d.ts +0 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { toDomProps } from './toDomProps';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The props that make a group-labelled widget's rendered surface the thing its
|
|
13
|
+
* host label actually NAMES, on the surfaces that are not controls
|
|
14
|
+
* (objectui#3990).
|
|
15
|
+
*
|
|
16
|
+
* ## What it is for
|
|
17
|
+
*
|
|
18
|
+
* A widget declared `labelling: 'group'` (objectui#3961) is told by the form
|
|
19
|
+
* renderer: your label publishes an `id` and drops its `for`, so name yourself
|
|
20
|
+
* by IDREF. The renderer hands down exactly two facts that address the field as
|
|
21
|
+
* a WHOLE:
|
|
22
|
+
*
|
|
23
|
+
* - `id` — the host's control id (`…-form-item`), the id the label's `for`
|
|
24
|
+
* used to point at;
|
|
25
|
+
* - `aria-labelledby` — the IDREF of that visible label.
|
|
26
|
+
*
|
|
27
|
+
* Every editable branch already consumes them inside its wider `toDomProps`
|
|
28
|
+
* spread. The branches that returned EARLY did not — a field-level
|
|
29
|
+
* `readonly: true`, or an option list with zero offered options — so the label
|
|
30
|
+
* published an id that no element in the document referenced. Measured on
|
|
31
|
+
* `origin/main`, one field per row, reading the host label against the DOM:
|
|
32
|
+
*
|
|
33
|
+
* ```
|
|
34
|
+
* multiselect readonly+value consumers=0 byLabelText=0 namedByRole=[]
|
|
35
|
+
* multiselect readonly+empty consumers=0 byLabelText=0 namedByRole=[]
|
|
36
|
+
* multiselect zeroOptions consumers=0 byLabelText=0 namedByRole=[]
|
|
37
|
+
* multiselect editable consumers=1 byLabelText=1 namedByRole=[group:1]
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* All seven group-labelled types measured the same, in every readonly state.
|
|
41
|
+
* This is quieter than the dangling `for` #3961 replaced: a `for` that resolves
|
|
42
|
+
* to nothing can at least be swept for, while "a label published an id and
|
|
43
|
+
* nobody consumed it" looks like healthy markup.
|
|
44
|
+
*
|
|
45
|
+
* ## Why this narrow pair, and not the whole `toDomProps` result
|
|
46
|
+
*
|
|
47
|
+
* Everything else in the DOM pass-through addresses a CONTROL, and a readonly
|
|
48
|
+
* display has none: `aria-describedby` / `aria-required` are announced when
|
|
49
|
+
* focus lands on something focusable, `disabled` / `tabIndex` / the focus
|
|
50
|
+
* handlers only mean something on an interactive element, and `name` is
|
|
51
|
+
* DOM-legal on form controls only — on a `div` it is exactly the leak
|
|
52
|
+
* objectui#3291 sweeps for. Spreading the full result would also put the host's
|
|
53
|
+
* `className` on top of the widget's own, because the widgets whose readonly
|
|
54
|
+
* surface is a placeholder or a plain container (`EmptyValue`, `FileField`,
|
|
55
|
+
* `AddressField`) keep `className` inside `props`.
|
|
56
|
+
*
|
|
57
|
+
* ## Why `role` travels with the two keys
|
|
58
|
+
*
|
|
59
|
+
* `aria-labelledby` on a role-less `div` / `span` names NOTHING — `generic`
|
|
60
|
+
* prohibits an author name — so the pair is inert without a role that supports
|
|
61
|
+
* naming. `group` is the one that fits a readonly surface: it names a set of
|
|
62
|
+
* values (chips, checked labels, stars, file names, a formatted address)
|
|
63
|
+
* without promising the interactivity `textbox` or `radiogroup` would. It is
|
|
64
|
+
* deliberately NOT the role the same widget answers with while editable:
|
|
65
|
+
* `RadioField`'s readonly branch renders the chosen label as text with no radios
|
|
66
|
+
* left in it, and `FileField`'s renders file names with no dropzone button.
|
|
67
|
+
*
|
|
68
|
+
* The role is emitted ONLY when a host actually named the field, mirroring
|
|
69
|
+
* every editable branch's `isLabelledGroup` test: standalone rendering (the
|
|
70
|
+
* grid's inline cell editor, a bare SDUI node) hands down neither key, so every
|
|
71
|
+
* value here is `undefined`, React emits no attribute, and that markup stays
|
|
72
|
+
* byte-identical to what it was.
|
|
73
|
+
*/
|
|
74
|
+
export interface HostGroupProps {
|
|
75
|
+
/** The host's control id — the id its label would have pointed `for` at. */
|
|
76
|
+
id?: string;
|
|
77
|
+
/** IDREF of the host's visible label. */
|
|
78
|
+
'aria-labelledby'?: string;
|
|
79
|
+
/** `'group'` when — and only when — a host named this surface. */
|
|
80
|
+
role?: 'group';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read the two whole-field keys out of a widget's leftover props, plus the role
|
|
85
|
+
* that makes them mean something. See {@link HostGroupProps}.
|
|
86
|
+
*
|
|
87
|
+
* Routed through {@link toDomProps} rather than reading `props` directly so the
|
|
88
|
+
* pair keeps ONE gate: those keys reach an element only if the DOM pass-through
|
|
89
|
+
* whitelist still forwards them, and its two compile-time assertions bind that
|
|
90
|
+
* whitelist to the props contract in both directions.
|
|
91
|
+
*/
|
|
92
|
+
export function toHostGroupProps(props: {
|
|
93
|
+
id?: string;
|
|
94
|
+
'aria-labelledby'?: string;
|
|
95
|
+
}): HostGroupProps {
|
|
96
|
+
const { id, 'aria-labelledby': labelledBy } = toDomProps(props);
|
|
97
|
+
return {
|
|
98
|
+
id,
|
|
99
|
+
'aria-labelledby': labelledBy,
|
|
100
|
+
...(labelledBy != null ? { role: 'group' as const } : null),
|
|
101
|
+
};
|
|
102
|
+
}
|
package/src/widgets/types.ts
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
import type { AriaAttributes, FocusEventHandler, MouseEventHandler } from 'react';
|
|
2
2
|
import type { DependsOnInput, FieldMetadata } from '@object-ui/types';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* DOM pass-through: what a widget's `...props` spread may legitimately put on
|
|
6
|
+
* the element it renders.
|
|
7
|
+
*
|
|
8
|
+
* A NAMED type rather than an inline block of {@link FieldWidgetComponentProps}
|
|
9
|
+
* so the compiler can bind it to its runtime executor in BOTH directions
|
|
10
|
+
* (objectui#3291). `toDomProps` asserts:
|
|
11
|
+
*
|
|
12
|
+
* - every key it forwards is declared on the widget contract — deleting one
|
|
13
|
+
* here without deleting it there is a compile error;
|
|
14
|
+
* - every key of THIS type is one it forwards — adding one here without
|
|
15
|
+
* adding it to `DOM_PASS_THROUGH_KEYS` is a compile error too.
|
|
16
|
+
*
|
|
17
|
+
* The second direction is the one that matters for the failure mode this repo
|
|
18
|
+
* treats as first-class: DECLARED BUT NOT DELIVERED (objectui#3290's
|
|
19
|
+
* `aria-required` that never reached a control, objectui#3222's validation slot
|
|
20
|
+
* nobody produced). Without it, a key added here would type-check, read as
|
|
21
|
+
* supported, and silently never reach the DOM — and the leak test cannot see
|
|
22
|
+
* that class of bug, because it looks for attributes that ARRIVE, not for ones
|
|
23
|
+
* that go missing.
|
|
24
|
+
*
|
|
25
|
+
* `className` and `disabled` are deliberately NOT here. They are DOM-legal and
|
|
26
|
+
* `toDomProps` does forward them, but they are declared on the controlled-input
|
|
27
|
+
* block of {@link FieldWidgetComponentProps} because widgets also INTERPRET
|
|
28
|
+
* them (className is composed with the widget's own classes; disabled is OR-ed
|
|
29
|
+
* with readonly). They are therefore bound in the forward direction only.
|
|
30
|
+
*
|
|
31
|
+
* `AriaAttributes` and the `data-${string}` family are matched by prefix at
|
|
32
|
+
* runtime rather than key-by-key, so they are intersected in separately.
|
|
33
|
+
*
|
|
34
|
+
* Adding a key here is a contract change: say who produces it and who reads it.
|
|
35
|
+
*/
|
|
36
|
+
export type FieldWidgetDomProps = {
|
|
37
|
+
id?: string;
|
|
38
|
+
/** react-hook-form's field name, spread in by the form renderer. */
|
|
39
|
+
name?: string;
|
|
40
|
+
autoFocus?: boolean;
|
|
41
|
+
tabIndex?: number;
|
|
42
|
+
onBlur?: FocusEventHandler<HTMLElement>;
|
|
43
|
+
onFocus?: FocusEventHandler<HTMLElement>;
|
|
44
|
+
onClick?: MouseEventHandler<HTMLElement>;
|
|
45
|
+
};
|
|
46
|
+
|
|
4
47
|
/**
|
|
5
48
|
* Props every field widget in this package receives at RUNTIME.
|
|
6
49
|
*
|
|
@@ -14,13 +57,27 @@ import type { DependsOnInput, FieldMetadata } from '@object-ui/types';
|
|
|
14
57
|
* the widgets in this directory actually implement, against
|
|
15
58
|
* `@object-ui/types`'s much richer `FieldMetadata`.
|
|
16
59
|
*
|
|
17
|
-
* The two are NOT interchangeable,
|
|
18
|
-
*
|
|
19
|
-
* `
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
60
|
+
* The two are NOT interchangeable, but they no longer disagree on the
|
|
61
|
+
* validation slot: this type used to call it `errorMessage` while the spec
|
|
62
|
+
* calls it `error`, so a widget written to the published contract read
|
|
63
|
+
* `undefined` forever. objectui#3222 resolved that in the direction the
|
|
64
|
+
* contract points — the slot below IS the spec's `error`, and the form
|
|
65
|
+
* renderer now produces it (see `error`'s own doc comment). Reading
|
|
66
|
+
* `props.required` off this type is still a compile error rather than a
|
|
67
|
+
* silent `any`; that key is deliberately NOT lowered here (see below).
|
|
68
|
+
*
|
|
69
|
+
* ## Why `required` is still absent, though the spec declares it
|
|
70
|
+
*
|
|
71
|
+
* The required MARKER is drawn exactly once, by the form renderer's
|
|
72
|
+
* `<FormLabel>` (the `*` with `aria-label="required"`, which the label
|
|
73
|
+
* association folds into the control's accessible name). Handing `required`
|
|
74
|
+
* to widgets would give that marker a second possible author, and the very
|
|
75
|
+
* next AI-written widget draws its own asterisk — the same double-display
|
|
76
|
+
* failure that keeps the validation TEXT out of the widget below. The a11y
|
|
77
|
+
* state a widget genuinely could carry is `aria-required` on the input, and
|
|
78
|
+
* that needs no new key at all: `AriaAttributes` is already part of this type
|
|
79
|
+
* and every widget already forwards it to its control. Tracked separately;
|
|
80
|
+
* do not add `required` here to "align" without that decision.
|
|
24
81
|
*
|
|
25
82
|
* ## Why there is no `[key: string]: any` (objectui#3221)
|
|
26
83
|
*
|
|
@@ -40,6 +97,22 @@ import type { DependsOnInput, FieldMetadata } from '@object-ui/types';
|
|
|
40
97
|
* is expressed as a template-literal index signature, which — unlike
|
|
41
98
|
* `[key: string]` — leaves `keyof` finite, so an undeclared prop still fails.
|
|
42
99
|
*
|
|
100
|
+
* ## Why there is no `schema` (objectui#3233)
|
|
101
|
+
*
|
|
102
|
+
* `schema` used to be declared here as a SECOND carrier for what `field`
|
|
103
|
+
* already means — `SchemaRenderer` handed the SDUI node down as `schema`, the
|
|
104
|
+
* form renderer forwarded `schema={props.field || props.schema || props}`
|
|
105
|
+
* *alongside* `field`, and ~30 widgets resolved their config as
|
|
106
|
+
* `field || schema`. One concept, two keys, two producers: exactly the
|
|
107
|
+
* consumer-side tolerance AGENTS.md #0.1 forbids, and exactly the shape that
|
|
108
|
+
* lets an AI-written widget pick the wrong spelling and still "work" under one
|
|
109
|
+
* host while reading `undefined` under another.
|
|
110
|
+
*
|
|
111
|
+
* v17 converged it at the PRODUCERS. `field` is the only carrier a widget ever
|
|
112
|
+
* sees; the SDUI node → `field` translation happens once, in this package's
|
|
113
|
+
* registry adapter (`withFieldCarrier`), not ~30 times in the widgets. Reading
|
|
114
|
+
* `props.schema` is now a compile error rather than a silent second contract.
|
|
115
|
+
*
|
|
43
116
|
* Adding a key here is a contract change: say who produces it and who reads it.
|
|
44
117
|
*/
|
|
45
118
|
export type FieldWidgetComponentProps<T = any> = {
|
|
@@ -48,14 +121,38 @@ export type FieldWidgetComponentProps<T = any> = {
|
|
|
48
121
|
value: T;
|
|
49
122
|
onChange: (val: T) => void;
|
|
50
123
|
/**
|
|
51
|
-
* The field's metadata
|
|
52
|
-
*
|
|
124
|
+
* The field's metadata, and the SINGLE carrier for it (objectui#3233).
|
|
125
|
+
*
|
|
126
|
+
* Deliberately the looser `@object-ui/types` shape rather than the spec's,
|
|
127
|
+
* to avoid a circular dependency for now.
|
|
128
|
+
*
|
|
129
|
+
* Every host produces it: the form renderer's `renderFieldComponent`, the
|
|
130
|
+
* inline-edit hosts (`FieldEditWidget`), `ActionParamDialog` /
|
|
131
|
+
* `BulkActionDialog`, and — since #3233 — the registry adapter that adapts
|
|
132
|
+
* `SchemaRenderer`'s SDUI node onto this contract (`withFieldCarrier` in
|
|
133
|
+
* `packages/fields/src/withFieldCarrier.tsx`). A widget reads `props.field`
|
|
134
|
+
* and nothing else; there is no second key to check.
|
|
53
135
|
*/
|
|
54
136
|
field: FieldMetadata;
|
|
55
137
|
readonly?: boolean;
|
|
56
138
|
disabled?: boolean;
|
|
57
139
|
className?: string;
|
|
58
|
-
|
|
140
|
+
/**
|
|
141
|
+
* The active validation message for this field, named as
|
|
142
|
+
* `@objectstack/spec/ui`'s `FieldWidgetPropsSchema` names it (objectui#3222).
|
|
143
|
+
*
|
|
144
|
+
* **Producer**: the form renderer, from react-hook-form's
|
|
145
|
+
* `fieldState.error?.message` (`packages/components/src/renderers/form/
|
|
146
|
+
* form.tsx`). Before #3222 nothing in the repo produced it under EITHER
|
|
147
|
+
* spelling, so the seven widgets computing `aria-invalid={!!errorMessage}`
|
|
148
|
+
* were computing it from a permanent `undefined` — `aria-invalid` was never
|
|
149
|
+
* once set and a screen reader was never told the field had failed.
|
|
150
|
+
*
|
|
151
|
+
* **Consumer**: a widget reads this ONLY to drive `aria-invalid` on the
|
|
152
|
+
* control it renders. The message TEXT stays with the form renderer's
|
|
153
|
+
* `<FormMessage/>`; a widget that also renders it double-displays it.
|
|
154
|
+
*/
|
|
155
|
+
error?: string;
|
|
59
156
|
/**
|
|
60
157
|
* Upload widgets (`file`/`image`) fire this when their in-progress state
|
|
61
158
|
* flips, so a host can block submit until a presigned upload settles. Other
|
|
@@ -65,17 +162,6 @@ export type FieldWidgetComponentProps<T = any> = {
|
|
|
65
162
|
|
|
66
163
|
/* ── Host plumbing: what a rendering host actually forwards ─────────────── */
|
|
67
164
|
|
|
68
|
-
/**
|
|
69
|
-
* The raw authored node the widget was rendered from. Two hosts supply it:
|
|
70
|
-
* `SchemaRenderer` (`<Component schema={schema} {...schema} />`) and the
|
|
71
|
-
* form renderer's `renderFieldComponent` (`schema={props.field || ...}`),
|
|
72
|
-
* which is why ~25 widgets read `field || schema` for their config.
|
|
73
|
-
*
|
|
74
|
-
* This is a second carrier for what `field` already means, i.e. a de-facto
|
|
75
|
-
* second contract (AGENTS.md #0.1) — declared here so it is at least
|
|
76
|
-
* visible; converging the two is tracked separately.
|
|
77
|
-
*/
|
|
78
|
-
schema?: FieldMetadata | Record<string, unknown>;
|
|
79
165
|
/**
|
|
80
166
|
* DataSource for widgets that query records (lookup / user / object-ref /
|
|
81
167
|
* recipient-picker / grid). Injected by the form renderer for the field
|
|
@@ -99,9 +185,27 @@ export type FieldWidgetComponentProps<T = any> = {
|
|
|
99
185
|
*/
|
|
100
186
|
dependsOn?: DependsOnInput;
|
|
101
187
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
188
|
+
* Controlling-field name → its human LABEL, for widgets that have to NAME a
|
|
189
|
+
* sibling field in user-visible copy (the dependent-lookup gate hint).
|
|
190
|
+
*
|
|
191
|
+
* Only the form renderer knows a field's label — the widget sees its own
|
|
192
|
+
* metadata and a `depends_on` list of API names. Without this map the gate
|
|
193
|
+
* sentence interpolated the raw API name into every locale, `en` included
|
|
194
|
+
* ("Select crm_account first"), which is an internal identifier leaking into
|
|
195
|
+
* the UI, not merely an untranslated word (objectstack#5407). This is the
|
|
196
|
+
* lookup-side counterpart of `emptyHint`, which the form already resolves to
|
|
197
|
+
* labels for the fixed-option widgets. A name with no entry falls back to
|
|
198
|
+
* itself, so a host that passes nothing renders exactly what it did before.
|
|
199
|
+
*/
|
|
200
|
+
dependsOnLabels?: Record<string, string>;
|
|
201
|
+
/**
|
|
202
|
+
* Hint shown when an option list cannot be filled — typically a
|
|
203
|
+
* dependency-gated list still waiting on its controlling field (#2284).
|
|
204
|
+
* Forwarded by the form renderer, which resolves the controlling fields to
|
|
205
|
+
* their human LABELS. **When supplied it wins**; the option widgets fall back
|
|
206
|
+
* to their own translated copy only when a host computed none (objectui#3231
|
|
207
|
+
* — they used to discard this prop and always render their own hardcoded
|
|
208
|
+
* English). See `OptionsEmptyState`, the single consumer.
|
|
105
209
|
*/
|
|
106
210
|
emptyHint?: string;
|
|
107
211
|
/**
|
|
@@ -121,17 +225,13 @@ export type FieldWidgetComponentProps<T = any> = {
|
|
|
121
225
|
*/
|
|
122
226
|
onCreateNew?: (searchQuery: string) => void;
|
|
123
227
|
|
|
124
|
-
/* ── DOM pass-through
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
onBlur?: FocusEventHandler<HTMLElement>;
|
|
132
|
-
onFocus?: FocusEventHandler<HTMLElement>;
|
|
133
|
-
onClick?: MouseEventHandler<HTMLElement>;
|
|
134
|
-
} & AriaAttributes & {
|
|
228
|
+
/* ── DOM pass-through ───────────────────────────────────────────────────── */
|
|
229
|
+
//
|
|
230
|
+
// Lives in the named {@link FieldWidgetDomProps} above, because that is what
|
|
231
|
+
// lets the compiler bind the declaration to `toDomProps` — its runtime
|
|
232
|
+
// executor — in BOTH directions (objectui#3291). Add a DOM key THERE, not
|
|
233
|
+
// here, and the compiler will make you add it to the whitelist too.
|
|
234
|
+
} & FieldWidgetDomProps & AriaAttributes & {
|
|
135
235
|
/**
|
|
136
236
|
* Arbitrary `data-*` attributes (test ids, analytics hooks). Open by design,
|
|
137
237
|
* but a template-literal key — `keyof` stays finite, so this does NOT
|
|
@@ -6,14 +6,13 @@ import { createSafeTranslation } from '@object-ui/i18n';
|
|
|
6
6
|
|
|
7
7
|
const FIELD_DEFAULTS: Record<string, string> = {
|
|
8
8
|
'common.selectOption': 'Select an option',
|
|
9
|
-
'common.select': 'Select
|
|
10
|
-
'common.
|
|
11
|
-
'common.loading': 'Loading...',
|
|
9
|
+
'common.select': 'Select…',
|
|
10
|
+
'common.loading': 'Loading…',
|
|
12
11
|
'common.noResults': 'No results found',
|
|
13
12
|
'common.cancel': 'Cancel',
|
|
14
13
|
'common.confirm': 'Confirm',
|
|
15
14
|
'table.selected': '{{count}} selected',
|
|
16
|
-
'table.search': 'Search
|
|
15
|
+
'table.search': 'Search…',
|
|
17
16
|
'lookup.loading': 'Loading…',
|
|
18
17
|
'lookup.noOptions': 'No options found',
|
|
19
18
|
'lookup.noRecords': 'No records found',
|
|
@@ -39,6 +38,12 @@ const FIELD_DEFAULTS: Record<string, string> = {
|
|
|
39
38
|
'lookup.nextPage': 'Next page',
|
|
40
39
|
'lookup.jumpToPage': 'Jump to page',
|
|
41
40
|
'lookup.retry': 'Retry',
|
|
41
|
+
// objectui#3231 — the empty / dependency-gated state of the fixed-option
|
|
42
|
+
// widgets (select, multiselect, radio, checkboxes). Only used when the host
|
|
43
|
+
// supplies no `emptyHint`; the gate sentence shares its key with the form
|
|
44
|
+
// renderer so both cannot drift apart in a locale.
|
|
45
|
+
'fields.options.empty': 'No options available',
|
|
46
|
+
'fields.options.selectFirst': 'Select {{fields}} first',
|
|
42
47
|
// objectstack#3821 — sharing-rule authoring widgets (object-ref /
|
|
43
48
|
// recipient-picker / filter-condition). The recipient placeholder is keyed
|
|
44
49
|
// PER TYPE rather than interpolating the enum value into an English
|
|
@@ -69,6 +74,43 @@ const FIELD_DEFAULTS: Record<string, string> = {
|
|
|
69
74
|
'fields.filterCondition.jsonOnly': 'This criteria can only be edited as JSON',
|
|
70
75
|
'fields.filterCondition.editAsJson': 'Edit as JSON',
|
|
71
76
|
'fields.filterCondition.useVisualBuilder': 'Use visual builder',
|
|
77
|
+
// objectui#3342 — the tags widget's input hint. Used only when the field
|
|
78
|
+
// author declared no `placeholder` of their own (author declaration wins).
|
|
79
|
+
'fields.tags.placeholder': 'Type and press Enter to add…',
|
|
80
|
+
// objectui#3406 — the accessible name of `TextAreaField`'s character
|
|
81
|
+
// counter, rendered only when the field declares `maxLength`. The visible
|
|
82
|
+
// `{n}/{max}` is digits; this sentence is what a screen reader speaks, and
|
|
83
|
+
// the element is `aria-live`, so before the key existed a non-English
|
|
84
|
+
// session heard English on every keystroke. Byte-identical to the literal
|
|
85
|
+
// it replaces, so a no-provider render is unchanged.
|
|
86
|
+
'fields.textarea.characterCount': 'Character count: {{count}} of {{max}}',
|
|
87
|
+
// objectui#3408 — the NEAR-LIMIT warning, spoken by the counter's status
|
|
88
|
+
// region after the typist pauses and only once they are inside the last 10%
|
|
89
|
+
// (or last 20 characters) of the cap. The sentence above became the field's
|
|
90
|
+
// `aria-describedby` description in the same change; this one is the only
|
|
91
|
+
// thing that is ever announced mid-typing, which is why it says what is
|
|
92
|
+
// LEFT rather than repeating the whole count.
|
|
93
|
+
//
|
|
94
|
+
// Colon form, not "You have {{count}} characters remaining": `count` makes
|
|
95
|
+
// i18next attempt plural lookup (`_one` / `_few` / …) before the base key,
|
|
96
|
+
// and these packs declare the base key only — the same base-key fallback
|
|
97
|
+
// `characterCount` already relies on. A sentence whose grammar does not
|
|
98
|
+
// depend on the number stays correct at 1 without ten plural entries per
|
|
99
|
+
// pack, and matches the sibling key's shape.
|
|
100
|
+
'fields.textarea.charactersRemaining': 'Characters remaining: {{count}}',
|
|
101
|
+
// The fullscreen long-text dialog's copy (objectui#3404) is NOT declared
|
|
102
|
+
// here any more. It moved with the implementation: `FullscreenFieldEditor` is
|
|
103
|
+
// a thin wrapper over `FullscreenEditor` in `@object-ui/components`
|
|
104
|
+
// (objectui#3398), and that primitive carries its own `createSafeTranslation`
|
|
105
|
+
// defaults for the same `form.fullscreen.*` / `common.cancel` keys — still
|
|
106
|
+
// the built-in branch's keys, still byte-identical English, so provider-less
|
|
107
|
+
// rendering is unchanged and the ten locale packs need no edit. Leaving the
|
|
108
|
+
// entries here would have re-created in the defaults map exactly the
|
|
109
|
+
// duplication #3398 removed from the components, and this map's own
|
|
110
|
+
// discipline is that a default is declared where it is read.
|
|
111
|
+
//
|
|
112
|
+
// `common.cancel` stays above: `RecordPickerDialog` and `PeoplePicker` read
|
|
113
|
+
// it independently of anything fullscreen.
|
|
72
114
|
// objectui#2600 B5 — capability picker scope group headers.
|
|
73
115
|
'capability.group.platform': 'Platform',
|
|
74
116
|
'capability.group.org': 'Organization',
|
|
@@ -56,8 +56,14 @@ export interface UseRecordQueryOptions {
|
|
|
56
56
|
* `$filter` — already merged by the caller (base `lookup_filters`, dependent
|
|
57
57
|
* lookup chain, candidate hygiene like `banned != true`, …). Compared by
|
|
58
58
|
* value, so a referentially-new-but-equal object each render will not loop.
|
|
59
|
+
*
|
|
60
|
+
* Either the `QueryParams.$filter` record form or a lowered ObjectQL AST node
|
|
61
|
+
* (an array), since the picker's merge now produces both (#3831). Typed
|
|
62
|
+
* `unknown` rather than `Record< string, any >` so an array cannot slip
|
|
63
|
+
* through a type that silently accepts it; emptiness is decided by
|
|
64
|
+
* {@link hasFilter}, not by `Object.keys`.
|
|
59
65
|
*/
|
|
60
|
-
filter?:
|
|
66
|
+
filter?: unknown;
|
|
61
67
|
/** `$expand` — related entities to include (e.g. `['primary_business_unit_id']`). */
|
|
62
68
|
expand?: string[];
|
|
63
69
|
/** `$searchFields` — narrow the server searchable set (ADR-0061). */
|
|
@@ -97,6 +103,22 @@ export interface UseRecordQueryResult {
|
|
|
97
103
|
refetch: () => void;
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Is this filter source worth sending as `$filter` at all?
|
|
108
|
+
*
|
|
109
|
+
* Array-aware on purpose. The picker's merge yields either the
|
|
110
|
+
* `QueryParams.$filter` record form or a lowered ObjectQL AST node (#3831), and
|
|
111
|
+
* `Object.keys` on an array returns its INDICES — so the record-only test read
|
|
112
|
+
* `['0','1','2']` for an AST node and was right only by accident. An empty array
|
|
113
|
+
* is "no filter" for the same reason an empty object is.
|
|
114
|
+
*/
|
|
115
|
+
function hasFilter(filter: unknown): boolean {
|
|
116
|
+
if (filter === null || filter === undefined) return false;
|
|
117
|
+
if (Array.isArray(filter)) return filter.length > 0;
|
|
118
|
+
if (typeof filter !== 'object') return false;
|
|
119
|
+
return Object.keys(filter as object).length > 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
100
122
|
export function useRecordQuery(options: UseRecordQueryOptions): UseRecordQueryResult {
|
|
101
123
|
const {
|
|
102
124
|
dataSource,
|
|
@@ -123,7 +145,7 @@ export function useRecordQuery(options: UseRecordQueryOptions): UseRecordQueryRe
|
|
|
123
145
|
// Stable signatures for object/array inputs so the fetch effect keys on their
|
|
124
146
|
// *value*, not a fresh reference every render.
|
|
125
147
|
const filterSignature = useMemo(
|
|
126
|
-
() => (
|
|
148
|
+
() => (hasFilter(filter) ? JSON.stringify(filter) : ''),
|
|
127
149
|
[filter],
|
|
128
150
|
);
|
|
129
151
|
const expandSignature = useMemo(() => (expand?.length ? expand.join(',') : ''), [expand]);
|
|
@@ -150,7 +172,12 @@ export function useRecordQuery(options: UseRecordQueryOptions): UseRecordQueryRe
|
|
|
150
172
|
if (searchTerm && searchTerm.trim()) params.$search = searchTerm.trim();
|
|
151
173
|
if (searchFields && searchFields.length > 0) params.$searchFields = searchFields;
|
|
152
174
|
if (sortArg) params.$orderby = { [sortArg.field]: sortArg.direction };
|
|
153
|
-
|
|
175
|
+
// `QueryParams.$filter` is declared `Record< string, any >`, which the
|
|
176
|
+
// AST-array form does not describe — the cast is at this ONE assignment
|
|
177
|
+
// rather than widening a shared type that several other producers
|
|
178
|
+
// (plugin-list's `buildEffectiveFilter`, plugin-view's ObjectView)
|
|
179
|
+
// already feed arrays through.
|
|
180
|
+
if (hasFilter(filter)) params.$filter = filter as Record<string, any>;
|
|
154
181
|
if (expand && expand.length > 0) params.$expand = expand;
|
|
155
182
|
|
|
156
183
|
const result = await dataSource.find(objectName, params);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Adapt `SchemaRenderer`'s SDUI carrier onto the field-widget contract —
|
|
13
|
+
* objectui#3233, the producer-side half of retiring `schema` from
|
|
14
|
+
* {@link import('./widgets/types').FieldWidgetComponentProps}.
|
|
15
|
+
*
|
|
16
|
+
* ## Why this exists
|
|
17
|
+
*
|
|
18
|
+
* Two kinds of host render a registered field widget, and they name the
|
|
19
|
+
* metadata differently *for good reason*:
|
|
20
|
+
*
|
|
21
|
+
* - the **form renderer** (`renderFieldComponent` in
|
|
22
|
+
* `@object-ui/components`), the inline-edit hosts (`FieldEditWidget`) and
|
|
23
|
+
* the action dialogs pass **`field`** — the field metadata they already
|
|
24
|
+
* hold;
|
|
25
|
+
* - **`SchemaRenderer`** (`@object-ui/react`) passes **`schema`** — the
|
|
26
|
+
* authored node. That key is the UNIVERSAL SDUI contract *every* registered
|
|
27
|
+
* component receives (`element:*`, `page:*`, `object-grid`,
|
|
28
|
+
* `ReportRenderer` …). It is not being retired and must not be: it is what
|
|
29
|
+
* the renderer means by "the node you were rendered from".
|
|
30
|
+
*
|
|
31
|
+
* What WAS wrong is that both keys reached the widget, so ~30 widgets resolved
|
|
32
|
+
* their config as `field || schema` — one concept under two spellings, a second
|
|
33
|
+
* de-facto contract on the widget side (AGENTS.md #0.1) and precisely the shape
|
|
34
|
+
* that lets an AI-written widget pick one spelling and appear to work under one
|
|
35
|
+
* host while reading `undefined` under the other.
|
|
36
|
+
*
|
|
37
|
+
* The translation belongs at the seam, exactly once: here. Downstream `field`
|
|
38
|
+
* is the only carrier, and a widget reading `props.schema` no longer compiles.
|
|
39
|
+
*
|
|
40
|
+
* ## Guarantee
|
|
41
|
+
*
|
|
42
|
+
* Payload-preserving by construction: the node is forwarded **by reference**,
|
|
43
|
+
* so a widget's `field` is the very object it used to read through `schema`.
|
|
44
|
+
* `field` wins when a host supplies both (the form path always does, and it
|
|
45
|
+
* holds the resolved metadata rather than the raw node).
|
|
46
|
+
*
|
|
47
|
+
* `schema` is *consumed* here rather than forwarded — with the key gone from
|
|
48
|
+
* the contract, passing it on would only reach the DOM as a stray attribute
|
|
49
|
+
* through a widget's `...props` spread.
|
|
50
|
+
*
|
|
51
|
+
* ## For widget authors outside this repo
|
|
52
|
+
*
|
|
53
|
+
* Registering a widget raw means `SchemaRenderer` hands it `schema` and no
|
|
54
|
+
* `field`. Wrap it once at registration and the widget only ever implements
|
|
55
|
+
* `FieldWidgetComponentProps`:
|
|
56
|
+
*
|
|
57
|
+
* ```tsx
|
|
58
|
+
* import { withFieldCarrier } from '@object-ui/fields';
|
|
59
|
+
*
|
|
60
|
+
* ComponentRegistry.register('color', withFieldCarrier(ColorPickerField), {
|
|
61
|
+
* namespace: 'field',
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function withFieldCarrier<P extends object>(
|
|
66
|
+
FieldWidget: React.ComponentType<P>,
|
|
67
|
+
): React.FC<Record<string, any>> {
|
|
68
|
+
const FieldCarrierAdapter: React.FC<Record<string, any>> = ({ schema, field, ...props }) => (
|
|
69
|
+
<FieldWidget {...(props as unknown as P)} field={field ?? schema} />
|
|
70
|
+
);
|
|
71
|
+
FieldCarrierAdapter.displayName = `FieldCarrier(${
|
|
72
|
+
(FieldWidget as any).displayName || (FieldWidget as any).name || 'Component'
|
|
73
|
+
})`;
|
|
74
|
+
return FieldCarrierAdapter;
|
|
75
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
File without changes
|