@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,68 @@
|
|
|
1
|
+
import { AddressValue } from '@objectstack/spec/data';
|
|
2
|
+
/**
|
|
3
|
+
* Address data structure — `z.input` of `@objectstack/spec`'s
|
|
4
|
+
* `AddressValueSchema`, which is what the platform stores and what
|
|
5
|
+
* `/api/v1/data/**` serves back.
|
|
6
|
+
*
|
|
7
|
+
* The postal code is spelled `postalCode` (objectstack#5143). `AddressField`
|
|
8
|
+
* used to read and write `zipCode`, a key that appears nowhere in the spec, so
|
|
9
|
+
* the stored postal code never reached the input — and whatever the user then
|
|
10
|
+
* typed into that apparently-empty box was written under a key
|
|
11
|
+
* `AddressValueSchema` strips: lost outright on a new record, and on an
|
|
12
|
+
* existing one silently discarded while the stale stored value survived. (A
|
|
13
|
+
* postal code nobody touched did survive an unrelated edit, because the write
|
|
14
|
+
* spread the whole stored object through; the issue's account of that half is
|
|
15
|
+
* corrected in `AddressField.postalCode.test.tsx`.) One name, on both sides:
|
|
16
|
+
* the contract's.
|
|
17
|
+
*
|
|
18
|
+
* IMPORTED rather than declared since objectui#4167. rc.6 publishes
|
|
19
|
+
* `AddressValue` under this exact name, and the local copy — whose own comment
|
|
20
|
+
* already claimed to be "the part names of `AddressSchema`" — declared five of
|
|
21
|
+
* the seven parts. `countryCode` and `formatted` were missing, which is
|
|
22
|
+
* objectstack#4115's failure class precisely: a canonical CLAIM over a narrower
|
|
23
|
+
* shape, believed by the next reader. The claim is now the code.
|
|
24
|
+
*
|
|
25
|
+
* `AddressField` still renders five inputs and {@link formatAddress} still
|
|
26
|
+
* composes five parts, and that is a deliberate split rather than an omission:
|
|
27
|
+
* `formatted` is a derived one-line rendering of the parts (what this module
|
|
28
|
+
* computes) and `countryCode` is the ISO pair for `country`, neither of which a
|
|
29
|
+
* human types into a part box. Binding the type does not publish them as
|
|
30
|
+
* inputs — it stops the type from asserting the platform cannot store them, and
|
|
31
|
+
* it means a write from the widget no longer looks type-clean while dropping a
|
|
32
|
+
* key the contract carries: the `{ ...address }` spread in `handleFieldChange`
|
|
33
|
+
* preserves both, and now says so.
|
|
34
|
+
*
|
|
35
|
+
* The declaration moved here (objectui#4037) so the display cell renderer and
|
|
36
|
+
* the widget share one definition; the SOURCE of that definition is the spec.
|
|
37
|
+
*/
|
|
38
|
+
export type { AddressValue };
|
|
39
|
+
/**
|
|
40
|
+
* The shape written by builds up to and including 17.0.0-rc.1, whose postal
|
|
41
|
+
* code landed under `zipCode` (objectstack#5143). Read-time compatibility ONLY,
|
|
42
|
+
* and deliberately not part of {@link AddressValue}: authoring code must not be
|
|
43
|
+
* able to spell `zipCode`, and nothing here ever writes it back — the first
|
|
44
|
+
* edit of any part normalizes the record onto `postalCode` (see
|
|
45
|
+
* `AddressField`'s `handleFieldChange`). This is not a producer alias to be
|
|
46
|
+
* honoured forever; it exists to carry data THIS widget mis-wrote, and can go
|
|
47
|
+
* once no such data remains.
|
|
48
|
+
*/
|
|
49
|
+
export type LegacyAddressValue = AddressValue & {
|
|
50
|
+
zipCode?: string;
|
|
51
|
+
};
|
|
52
|
+
/** Postal code of a stored address, preferring the canonical key. */
|
|
53
|
+
export declare function readPostalCode(addr: AddressValue): string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Formats a stored address as a single line: `Street, City, State ZIP, Country`.
|
|
56
|
+
*
|
|
57
|
+
* Ordering and separators are NOT invented here — they are the rule
|
|
58
|
+
* `AddressField` already applied in its readonly branch, and the sub-field set
|
|
59
|
+
* its inputs expose (street / city / state / postalCode / country). Missing
|
|
60
|
+
* parts are dropped rather than spaced over, so a street-only address is
|
|
61
|
+
* `"中策路 1 号"` and never `", , ,"`.
|
|
62
|
+
*
|
|
63
|
+
* Returns `''` when no part is usable — callers decide what an empty address
|
|
64
|
+
* looks like (an `EmptyValue` placeholder for the cell renderer), and a value
|
|
65
|
+
* carrying no recognized part at all is NOT silently blanked: see
|
|
66
|
+
* `AddressCellRenderer`, which falls back to JSON so unknown data stays visible.
|
|
67
|
+
*/
|
|
68
|
+
export declare function formatAddress(addr: AddressValue): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client-side `maxSize` guard shared by the file and image widgets
|
|
3
|
+
* (objectui#4141).
|
|
4
|
+
*
|
|
5
|
+
* Why this is shared rather than mirrored per widget: the guard is the only
|
|
6
|
+
* thing standing between a declared `maxSize` and a real `presigned → PUT →
|
|
7
|
+
* complete` round trip, and `paramToField` copies `maxSize` onto the field
|
|
8
|
+
* config for **every** param type regardless of which widget ends up rendering
|
|
9
|
+
* it. So a widget that re-derives the check drifts silently — the image param
|
|
10
|
+
* shipped receiving the constraint and never reading it, while the file param
|
|
11
|
+
* rejected the identical pick, and nothing failed until QA drove the two side by
|
|
12
|
+
* side. One exported function makes the difference visible: a widget either
|
|
13
|
+
* calls it or conspicuously does not.
|
|
14
|
+
*
|
|
15
|
+
* Both widgets deliberately share the `fields.file.exceedsMaxSize` key too. The
|
|
16
|
+
* sentence names a file and a limit and says nothing file-widget-specific, it is
|
|
17
|
+
* already translated in all 11 built-in locales, and a parallel `fields.image.*`
|
|
18
|
+
* twin would be the same sentence maintained twice — drifting the moment one
|
|
19
|
+
* translator rewords one of them.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Minimal shape of the i18next `t` this module needs, matching the established
|
|
23
|
+
* `TranslateFn` pattern in `app-shell/src/providers/writeWarningToast`.
|
|
24
|
+
*/
|
|
25
|
+
export type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
|
|
26
|
+
/**
|
|
27
|
+
* The rejection message for a pick that exceeds `maxSize`, or `null` when the
|
|
28
|
+
* pick is within the limit — so a caller reads it as `if (err) reject`.
|
|
29
|
+
*
|
|
30
|
+
* A falsy `maxSize` (undeclared, or an explicit `0`) means **unrestricted**,
|
|
31
|
+
* preserving the `if (maxSize && …)` semantics the file widget has always
|
|
32
|
+
* applied: an undeclared limit must never turn into a 0-byte one.
|
|
33
|
+
*
|
|
34
|
+
* Takes `name` and `size` separately instead of a `File` because the image
|
|
35
|
+
* widget's crop path validates a re-encoded `Blob`, which carries no name of its
|
|
36
|
+
* own — that name comes from the cropper's output.
|
|
37
|
+
*/
|
|
38
|
+
export declare function maxSizeError(t: TranslateFn, pick: {
|
|
39
|
+
name: string;
|
|
40
|
+
size: number;
|
|
41
|
+
}, maxSize: number | undefined): string | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
* Stored value → `<input type="date">` value (`YYYY-MM-DD`).
|
|
10
|
+
*
|
|
11
|
+
* A leading `YYYY-MM-DD` is passed through VERBATIM rather than re-parsed: a
|
|
12
|
+
* date-only string is parsed as UTC midnight by `new Date()`, so reading local
|
|
13
|
+
* calendar components back out of it shifts the day by one everywhere west of
|
|
14
|
+
* Greenwich. `2026-06-17` must stay June 17th in every zone.
|
|
15
|
+
*/
|
|
16
|
+
export declare function toDateInputValue(value: unknown): string;
|
|
17
|
+
/**
|
|
18
|
+
* Stored value → `<input type="datetime-local">` value (`YYYY-MM-DDTHH:mm`),
|
|
19
|
+
* expressed in the viewer's local wall clock — the same instant the detail
|
|
20
|
+
* page shows, so opening the editor never appears to move the time.
|
|
21
|
+
*
|
|
22
|
+
* A value that is ALREADY a zone-less local datetime is truncated to minutes
|
|
23
|
+
* in place instead of being round-tripped through `Date`, which would
|
|
24
|
+
* reinterpret it and then re-render it, twice risking an off-by-a-zone shift.
|
|
25
|
+
*/
|
|
26
|
+
export declare function toDateTimeInputValue(value: unknown): string;
|
|
27
|
+
/**
|
|
28
|
+
* `<input type="datetime-local">` value → stored value (ISO-8601, UTC).
|
|
29
|
+
*
|
|
30
|
+
* The control hands us a zone-less local wall clock; `new Date()` reads that
|
|
31
|
+
* form as local time, which is the inverse of what `toDateTimeInputValue`
|
|
32
|
+
* wrote — the pair is what keeps read and write on one basis.
|
|
33
|
+
*
|
|
34
|
+
* An unparseable string is returned untouched rather than blanked: dropping a
|
|
35
|
+
* value the user can see in the box would be a data loss dressed up as a fix.
|
|
36
|
+
*/
|
|
37
|
+
export declare function fromDateTimeInputValue(value: string): string;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { DomProps as CoreDomProps } from '@object-ui/core';
|
|
2
|
+
/**
|
|
3
|
+
* The RUNTIME EXECUTOR of the "DOM pass-through" section of
|
|
4
|
+
* {@link FieldWidgetComponentProps} (objectui#3291).
|
|
5
|
+
*
|
|
6
|
+
* ## The defect
|
|
7
|
+
*
|
|
8
|
+
* Field widgets forwarded their leftover props to a control with a bare
|
|
9
|
+
* spread — `const { inputType, ...domProps } = props as any; <Input
|
|
10
|
+
* {...domProps} />`. Whatever a host handed the widget therefore became a DOM
|
|
11
|
+
* attribute. Measured on `origin/main`, a real form + a real widget:
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* <input placeholder="PH-f" zzcanary="CANARY-STR" zzcanaryobj="[object Object]"
|
|
15
|
+
* zzcanarynum="42" id="…" type="text" value="" name="f">
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The `[object Object]` this issue is named for is an authored field-config
|
|
19
|
+
* key being `String()`-ed onto an attribute. React 19 does not warn: an
|
|
20
|
+
* all-lowercase unknown attribute is passed through in complete silence, which
|
|
21
|
+
* is why this survived so long.
|
|
22
|
+
*
|
|
23
|
+
* ## Why a whitelist, and not a list of keys to drop
|
|
24
|
+
*
|
|
25
|
+
* The biggest leak source is not any NAMED renderer prop — it is the open tail
|
|
26
|
+
* of author-supplied keys. The form renderer destructures a fixed set of known
|
|
27
|
+
* keys and forwards `...fieldProps` verbatim
|
|
28
|
+
* (`components/src/renderers/form/form.tsx`), and `SchemaRenderer` is wider
|
|
29
|
+
* still: it spreads the whole authored node as props and has no strip layer at
|
|
30
|
+
* all, so on the SDUI path a widget's own spread is the ONLY line of defence.
|
|
31
|
+
*
|
|
32
|
+
* A blacklist enumerating today's renderer-only keys (`error`, `emptyHint`,
|
|
33
|
+
* `dataSource`, `dependentValues`, `dependsOn`, `options`, `inputType`, …)
|
|
34
|
+
* would pass every one of the canaries above and would not stop the next
|
|
35
|
+
* authored key either. Only "keep the known-safe set, drop the rest" closes
|
|
36
|
+
* it, which is what this function does.
|
|
37
|
+
*
|
|
38
|
+
* ## Declared = enforced
|
|
39
|
+
*
|
|
40
|
+
* {@link FieldWidgetComponentProps} already DECLARES the closed set of keys a
|
|
41
|
+
* widget may receive, and {@link FieldWidgetDomProps} names the subset that may
|
|
42
|
+
* legitimately reach a DOM element (objectui#3221). Until now that was a
|
|
43
|
+
* type-level claim a widget could violate at runtime simply by spreading. This
|
|
44
|
+
* function is that declaration's executable form, and TWO compile-time
|
|
45
|
+
* assertions below bind them in both directions — forwarding an undeclared key
|
|
46
|
+
* and declaring an unforwarded DOM key are each a compile error. Neither
|
|
47
|
+
* direction can drift silently.
|
|
48
|
+
*
|
|
49
|
+
* ## Deliberately NOT forwarded
|
|
50
|
+
*
|
|
51
|
+
* `role` and other HTML global attributes are absent because the contract does
|
|
52
|
+
* not declare them. They reached the DOM before this change only through the
|
|
53
|
+
* open spread. If a field node should be able to author one, DECLARE it on
|
|
54
|
+
* `FieldWidgetComponentProps` first and add it here — do not reopen the spread
|
|
55
|
+
* (AGENTS.md #0.1: fix the contract, never widen the consumer).
|
|
56
|
+
*
|
|
57
|
+
* ## Where the mechanism lives now, and why the key list stayed here
|
|
58
|
+
*
|
|
59
|
+
* objectui#4425 phase 2 promoted this whitelist to the SDUI widget contract
|
|
60
|
+
* generally, so the MECHANISM — filter by a declared key list plus the `aria-*`
|
|
61
|
+
* / `data-*` open families — moved to `@object-ui/core`
|
|
62
|
+
* (`utils/dom-props.ts`, `pickDomProps`), where every plugin package can reach
|
|
63
|
+
* it without depending on `@object-ui/fields` (the objectui#4409
|
|
64
|
+
* dependency-direction method: `plugin-chatbot` declares `@object-ui/core` and
|
|
65
|
+
* does not declare this package).
|
|
66
|
+
*
|
|
67
|
+
* The KEY LIST did not move, because it is not the same list. Measured on
|
|
68
|
+
* objectui#4431:
|
|
69
|
+
*
|
|
70
|
+
* - `name` and `disabled` are here and NOT in the SDUI set: both are legal
|
|
71
|
+
* only on form controls, which is what every widget in this package renders
|
|
72
|
+
* and what `FieldWidgetComponentProps` declares. `name` on the `<div>` an
|
|
73
|
+
* SDUI container widget renders is a leak — one of the 14 attributes #4431
|
|
74
|
+
* closed — and two widgets here already hand-strip it when they spread onto
|
|
75
|
+
* something that is not a control (`ObjectRefField`'s trigger,
|
|
76
|
+
* `FileField`'s dropzone).
|
|
77
|
+
* - `role` is in the SDUI set and NOT here: `SchemaRenderer` resolves it for
|
|
78
|
+
* every node from `@objectstack/spec`'s `AriaPropsSchema`, while this
|
|
79
|
+
* contract deliberately does not declare it (see "Deliberately NOT
|
|
80
|
+
* forwarded" above).
|
|
81
|
+
*
|
|
82
|
+
* So: one mechanism, two declarations, each bound by the assertions below to
|
|
83
|
+
* the contract it executes. Not two judges.
|
|
84
|
+
*/
|
|
85
|
+
declare const DOM_PASS_THROUGH_KEYS: readonly ["id", "name", "autoFocus", "tabIndex", "onBlur", "onFocus", "onClick", "className", "disabled"];
|
|
86
|
+
type DomPassThroughKey = (typeof DOM_PASS_THROUGH_KEYS)[number];
|
|
87
|
+
/** The subset of `P` this helper forwards, with each key's declared type. */
|
|
88
|
+
export type DomProps<P> = CoreDomProps<P, DomPassThroughKey>;
|
|
89
|
+
/**
|
|
90
|
+
* Keep only what may legitimately become a DOM attribute, and drop everything
|
|
91
|
+
* else — renderer plumbing, and any extra key an author put on the field
|
|
92
|
+
* config or SDUI node.
|
|
93
|
+
*
|
|
94
|
+
* Replaces the bare `{...props}` spread in every field widget that renders a
|
|
95
|
+
* host element:
|
|
96
|
+
*
|
|
97
|
+
* ```tsx
|
|
98
|
+
* // before — forwards whatever it was handed
|
|
99
|
+
* const { inputType, ...domProps } = props as any;
|
|
100
|
+
* return <Input {...domProps} … />;
|
|
101
|
+
*
|
|
102
|
+
* // after
|
|
103
|
+
* return <Input {...toDomProps(props)} … />;
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* Semantic props a widget INTERPRETS (`value`, `onChange`, `field`,
|
|
107
|
+
* `readonly`, `error`, …) are read from `props` as before; this function only
|
|
108
|
+
* governs what gets spread.
|
|
109
|
+
*/
|
|
110
|
+
export declare function toDomProps<P extends object>(props: P): DomProps<P>;
|
|
111
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
* The props that make a group-labelled widget's rendered surface the thing its
|
|
10
|
+
* host label actually NAMES, on the surfaces that are not controls
|
|
11
|
+
* (objectui#3990).
|
|
12
|
+
*
|
|
13
|
+
* ## What it is for
|
|
14
|
+
*
|
|
15
|
+
* A widget declared `labelling: 'group'` (objectui#3961) is told by the form
|
|
16
|
+
* renderer: your label publishes an `id` and drops its `for`, so name yourself
|
|
17
|
+
* by IDREF. The renderer hands down exactly two facts that address the field as
|
|
18
|
+
* a WHOLE:
|
|
19
|
+
*
|
|
20
|
+
* - `id` — the host's control id (`…-form-item`), the id the label's `for`
|
|
21
|
+
* used to point at;
|
|
22
|
+
* - `aria-labelledby` — the IDREF of that visible label.
|
|
23
|
+
*
|
|
24
|
+
* Every editable branch already consumes them inside its wider `toDomProps`
|
|
25
|
+
* spread. The branches that returned EARLY did not — a field-level
|
|
26
|
+
* `readonly: true`, or an option list with zero offered options — so the label
|
|
27
|
+
* published an id that no element in the document referenced. Measured on
|
|
28
|
+
* `origin/main`, one field per row, reading the host label against the DOM:
|
|
29
|
+
*
|
|
30
|
+
* ```
|
|
31
|
+
* multiselect readonly+value consumers=0 byLabelText=0 namedByRole=[]
|
|
32
|
+
* multiselect readonly+empty consumers=0 byLabelText=0 namedByRole=[]
|
|
33
|
+
* multiselect zeroOptions consumers=0 byLabelText=0 namedByRole=[]
|
|
34
|
+
* multiselect editable consumers=1 byLabelText=1 namedByRole=[group:1]
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* All seven group-labelled types measured the same, in every readonly state.
|
|
38
|
+
* This is quieter than the dangling `for` #3961 replaced: a `for` that resolves
|
|
39
|
+
* to nothing can at least be swept for, while "a label published an id and
|
|
40
|
+
* nobody consumed it" looks like healthy markup.
|
|
41
|
+
*
|
|
42
|
+
* ## Why this narrow pair, and not the whole `toDomProps` result
|
|
43
|
+
*
|
|
44
|
+
* Everything else in the DOM pass-through addresses a CONTROL, and a readonly
|
|
45
|
+
* display has none: `aria-describedby` / `aria-required` are announced when
|
|
46
|
+
* focus lands on something focusable, `disabled` / `tabIndex` / the focus
|
|
47
|
+
* handlers only mean something on an interactive element, and `name` is
|
|
48
|
+
* DOM-legal on form controls only — on a `div` it is exactly the leak
|
|
49
|
+
* objectui#3291 sweeps for. Spreading the full result would also put the host's
|
|
50
|
+
* `className` on top of the widget's own, because the widgets whose readonly
|
|
51
|
+
* surface is a placeholder or a plain container (`EmptyValue`, `FileField`,
|
|
52
|
+
* `AddressField`) keep `className` inside `props`.
|
|
53
|
+
*
|
|
54
|
+
* ## Why `role` travels with the two keys
|
|
55
|
+
*
|
|
56
|
+
* `aria-labelledby` on a role-less `div` / `span` names NOTHING — `generic`
|
|
57
|
+
* prohibits an author name — so the pair is inert without a role that supports
|
|
58
|
+
* naming. `group` is the one that fits a readonly surface: it names a set of
|
|
59
|
+
* values (chips, checked labels, stars, file names, a formatted address)
|
|
60
|
+
* without promising the interactivity `textbox` or `radiogroup` would. It is
|
|
61
|
+
* deliberately NOT the role the same widget answers with while editable:
|
|
62
|
+
* `RadioField`'s readonly branch renders the chosen label as text with no radios
|
|
63
|
+
* left in it, and `FileField`'s renders file names with no dropzone button.
|
|
64
|
+
*
|
|
65
|
+
* The role is emitted ONLY when a host actually named the field, mirroring
|
|
66
|
+
* every editable branch's `isLabelledGroup` test: standalone rendering (the
|
|
67
|
+
* grid's inline cell editor, a bare SDUI node) hands down neither key, so every
|
|
68
|
+
* value here is `undefined`, React emits no attribute, and that markup stays
|
|
69
|
+
* byte-identical to what it was.
|
|
70
|
+
*/
|
|
71
|
+
export interface HostGroupProps {
|
|
72
|
+
/** The host's control id — the id its label would have pointed `for` at. */
|
|
73
|
+
id?: string;
|
|
74
|
+
/** IDREF of the host's visible label. */
|
|
75
|
+
'aria-labelledby'?: string;
|
|
76
|
+
/** `'group'` when — and only when — a host named this surface. */
|
|
77
|
+
role?: 'group';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Read the two whole-field keys out of a widget's leftover props, plus the role
|
|
81
|
+
* that makes them mean something. See {@link HostGroupProps}.
|
|
82
|
+
*
|
|
83
|
+
* Routed through {@link toDomProps} rather than reading `props` directly so the
|
|
84
|
+
* pair keeps ONE gate: those keys reach an element only if the DOM pass-through
|
|
85
|
+
* whitelist still forwards them, and its two compile-time assertions bind that
|
|
86
|
+
* whitelist to the props contract in both directions.
|
|
87
|
+
*/
|
|
88
|
+
export declare function toHostGroupProps(props: {
|
|
89
|
+
id?: string;
|
|
90
|
+
'aria-labelledby'?: string;
|
|
91
|
+
}): HostGroupProps;
|
package/dist/widgets/types.d.ts
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
import { AriaAttributes, FocusEventHandler, MouseEventHandler } from 'react';
|
|
2
2
|
import { DependsOnInput, FieldMetadata } from '@object-ui/types';
|
|
3
|
+
/**
|
|
4
|
+
* DOM pass-through: what a widget's `...props` spread may legitimately put on
|
|
5
|
+
* the element it renders.
|
|
6
|
+
*
|
|
7
|
+
* A NAMED type rather than an inline block of {@link FieldWidgetComponentProps}
|
|
8
|
+
* so the compiler can bind it to its runtime executor in BOTH directions
|
|
9
|
+
* (objectui#3291). `toDomProps` asserts:
|
|
10
|
+
*
|
|
11
|
+
* - every key it forwards is declared on the widget contract — deleting one
|
|
12
|
+
* here without deleting it there is a compile error;
|
|
13
|
+
* - every key of THIS type is one it forwards — adding one here without
|
|
14
|
+
* adding it to `DOM_PASS_THROUGH_KEYS` is a compile error too.
|
|
15
|
+
*
|
|
16
|
+
* The second direction is the one that matters for the failure mode this repo
|
|
17
|
+
* treats as first-class: DECLARED BUT NOT DELIVERED (objectui#3290's
|
|
18
|
+
* `aria-required` that never reached a control, objectui#3222's validation slot
|
|
19
|
+
* nobody produced). Without it, a key added here would type-check, read as
|
|
20
|
+
* supported, and silently never reach the DOM — and the leak test cannot see
|
|
21
|
+
* that class of bug, because it looks for attributes that ARRIVE, not for ones
|
|
22
|
+
* that go missing.
|
|
23
|
+
*
|
|
24
|
+
* `className` and `disabled` are deliberately NOT here. They are DOM-legal and
|
|
25
|
+
* `toDomProps` does forward them, but they are declared on the controlled-input
|
|
26
|
+
* block of {@link FieldWidgetComponentProps} because widgets also INTERPRET
|
|
27
|
+
* them (className is composed with the widget's own classes; disabled is OR-ed
|
|
28
|
+
* with readonly). They are therefore bound in the forward direction only.
|
|
29
|
+
*
|
|
30
|
+
* `AriaAttributes` and the `data-${string}` family are matched by prefix at
|
|
31
|
+
* runtime rather than key-by-key, so they are intersected in separately.
|
|
32
|
+
*
|
|
33
|
+
* Adding a key here is a contract change: say who produces it and who reads it.
|
|
34
|
+
*/
|
|
35
|
+
export type FieldWidgetDomProps = {
|
|
36
|
+
id?: string;
|
|
37
|
+
/** react-hook-form's field name, spread in by the form renderer. */
|
|
38
|
+
name?: string;
|
|
39
|
+
autoFocus?: boolean;
|
|
40
|
+
tabIndex?: number;
|
|
41
|
+
onBlur?: FocusEventHandler<HTMLElement>;
|
|
42
|
+
onFocus?: FocusEventHandler<HTMLElement>;
|
|
43
|
+
onClick?: MouseEventHandler<HTMLElement>;
|
|
44
|
+
};
|
|
3
45
|
/**
|
|
4
46
|
* Props every field widget in this package receives at RUNTIME.
|
|
5
47
|
*
|
|
@@ -13,13 +55,27 @@ import { DependsOnInput, FieldMetadata } from '@object-ui/types';
|
|
|
13
55
|
* the widgets in this directory actually implement, against
|
|
14
56
|
* `@object-ui/types`'s much richer `FieldMetadata`.
|
|
15
57
|
*
|
|
16
|
-
* The two are NOT interchangeable,
|
|
17
|
-
*
|
|
18
|
-
* `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
58
|
+
* The two are NOT interchangeable, but they no longer disagree on the
|
|
59
|
+
* validation slot: this type used to call it `errorMessage` while the spec
|
|
60
|
+
* calls it `error`, so a widget written to the published contract read
|
|
61
|
+
* `undefined` forever. objectui#3222 resolved that in the direction the
|
|
62
|
+
* contract points — the slot below IS the spec's `error`, and the form
|
|
63
|
+
* renderer now produces it (see `error`'s own doc comment). Reading
|
|
64
|
+
* `props.required` off this type is still a compile error rather than a
|
|
65
|
+
* silent `any`; that key is deliberately NOT lowered here (see below).
|
|
66
|
+
*
|
|
67
|
+
* ## Why `required` is still absent, though the spec declares it
|
|
68
|
+
*
|
|
69
|
+
* The required MARKER is drawn exactly once, by the form renderer's
|
|
70
|
+
* `<FormLabel>` (the `*` with `aria-label="required"`, which the label
|
|
71
|
+
* association folds into the control's accessible name). Handing `required`
|
|
72
|
+
* to widgets would give that marker a second possible author, and the very
|
|
73
|
+
* next AI-written widget draws its own asterisk — the same double-display
|
|
74
|
+
* failure that keeps the validation TEXT out of the widget below. The a11y
|
|
75
|
+
* state a widget genuinely could carry is `aria-required` on the input, and
|
|
76
|
+
* that needs no new key at all: `AriaAttributes` is already part of this type
|
|
77
|
+
* and every widget already forwards it to its control. Tracked separately;
|
|
78
|
+
* do not add `required` here to "align" without that decision.
|
|
23
79
|
*
|
|
24
80
|
* ## Why there is no `[key: string]: any` (objectui#3221)
|
|
25
81
|
*
|
|
@@ -39,37 +95,66 @@ import { DependsOnInput, FieldMetadata } from '@object-ui/types';
|
|
|
39
95
|
* is expressed as a template-literal index signature, which — unlike
|
|
40
96
|
* `[key: string]` — leaves `keyof` finite, so an undeclared prop still fails.
|
|
41
97
|
*
|
|
98
|
+
* ## Why there is no `schema` (objectui#3233)
|
|
99
|
+
*
|
|
100
|
+
* `schema` used to be declared here as a SECOND carrier for what `field`
|
|
101
|
+
* already means — `SchemaRenderer` handed the SDUI node down as `schema`, the
|
|
102
|
+
* form renderer forwarded `schema={props.field || props.schema || props}`
|
|
103
|
+
* *alongside* `field`, and ~30 widgets resolved their config as
|
|
104
|
+
* `field || schema`. One concept, two keys, two producers: exactly the
|
|
105
|
+
* consumer-side tolerance AGENTS.md #0.1 forbids, and exactly the shape that
|
|
106
|
+
* lets an AI-written widget pick the wrong spelling and still "work" under one
|
|
107
|
+
* host while reading `undefined` under another.
|
|
108
|
+
*
|
|
109
|
+
* v17 converged it at the PRODUCERS. `field` is the only carrier a widget ever
|
|
110
|
+
* sees; the SDUI node → `field` translation happens once, in this package's
|
|
111
|
+
* registry adapter (`withFieldCarrier`), not ~30 times in the widgets. Reading
|
|
112
|
+
* `props.schema` is now a compile error rather than a silent second contract.
|
|
113
|
+
*
|
|
42
114
|
* Adding a key here is a contract change: say who produces it and who reads it.
|
|
43
115
|
*/
|
|
44
116
|
export type FieldWidgetComponentProps<T = any> = {
|
|
45
117
|
value: T;
|
|
46
118
|
onChange: (val: T) => void;
|
|
47
119
|
/**
|
|
48
|
-
* The field's metadata
|
|
49
|
-
*
|
|
120
|
+
* The field's metadata, and the SINGLE carrier for it (objectui#3233).
|
|
121
|
+
*
|
|
122
|
+
* Deliberately the looser `@object-ui/types` shape rather than the spec's,
|
|
123
|
+
* to avoid a circular dependency for now.
|
|
124
|
+
*
|
|
125
|
+
* Every host produces it: the form renderer's `renderFieldComponent`, the
|
|
126
|
+
* inline-edit hosts (`FieldEditWidget`), `ActionParamDialog` /
|
|
127
|
+
* `BulkActionDialog`, and — since #3233 — the registry adapter that adapts
|
|
128
|
+
* `SchemaRenderer`'s SDUI node onto this contract (`withFieldCarrier` in
|
|
129
|
+
* `packages/fields/src/withFieldCarrier.tsx`). A widget reads `props.field`
|
|
130
|
+
* and nothing else; there is no second key to check.
|
|
50
131
|
*/
|
|
51
132
|
field: FieldMetadata;
|
|
52
133
|
readonly?: boolean;
|
|
53
134
|
disabled?: boolean;
|
|
54
135
|
className?: string;
|
|
55
|
-
|
|
136
|
+
/**
|
|
137
|
+
* The active validation message for this field, named as
|
|
138
|
+
* `@objectstack/spec/ui`'s `FieldWidgetPropsSchema` names it (objectui#3222).
|
|
139
|
+
*
|
|
140
|
+
* **Producer**: the form renderer, from react-hook-form's
|
|
141
|
+
* `fieldState.error?.message` (`packages/components/src/renderers/form/
|
|
142
|
+
* form.tsx`). Before #3222 nothing in the repo produced it under EITHER
|
|
143
|
+
* spelling, so the seven widgets computing `aria-invalid={!!errorMessage}`
|
|
144
|
+
* were computing it from a permanent `undefined` — `aria-invalid` was never
|
|
145
|
+
* once set and a screen reader was never told the field had failed.
|
|
146
|
+
*
|
|
147
|
+
* **Consumer**: a widget reads this ONLY to drive `aria-invalid` on the
|
|
148
|
+
* control it renders. The message TEXT stays with the form renderer's
|
|
149
|
+
* `<FormMessage/>`; a widget that also renders it double-displays it.
|
|
150
|
+
*/
|
|
151
|
+
error?: string;
|
|
56
152
|
/**
|
|
57
153
|
* Upload widgets (`file`/`image`) fire this when their in-progress state
|
|
58
154
|
* flips, so a host can block submit until a presigned upload settles. Other
|
|
59
155
|
* widgets ignore it.
|
|
60
156
|
*/
|
|
61
157
|
onUploadingChange?: (uploading: boolean) => void;
|
|
62
|
-
/**
|
|
63
|
-
* The raw authored node the widget was rendered from. Two hosts supply it:
|
|
64
|
-
* `SchemaRenderer` (`<Component schema={schema} {...schema} />`) and the
|
|
65
|
-
* form renderer's `renderFieldComponent` (`schema={props.field || ...}`),
|
|
66
|
-
* which is why ~25 widgets read `field || schema` for their config.
|
|
67
|
-
*
|
|
68
|
-
* This is a second carrier for what `field` already means, i.e. a de-facto
|
|
69
|
-
* second contract (AGENTS.md #0.1) — declared here so it is at least
|
|
70
|
-
* visible; converging the two is tracked separately.
|
|
71
|
-
*/
|
|
72
|
-
schema?: FieldMetadata | Record<string, unknown>;
|
|
73
158
|
/**
|
|
74
159
|
* DataSource for widgets that query records (lookup / user / object-ref /
|
|
75
160
|
* recipient-picker / grid). Injected by the form renderer for the field
|
|
@@ -93,9 +178,27 @@ export type FieldWidgetComponentProps<T = any> = {
|
|
|
93
178
|
*/
|
|
94
179
|
dependsOn?: DependsOnInput;
|
|
95
180
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
181
|
+
* Controlling-field name → its human LABEL, for widgets that have to NAME a
|
|
182
|
+
* sibling field in user-visible copy (the dependent-lookup gate hint).
|
|
183
|
+
*
|
|
184
|
+
* Only the form renderer knows a field's label — the widget sees its own
|
|
185
|
+
* metadata and a `depends_on` list of API names. Without this map the gate
|
|
186
|
+
* sentence interpolated the raw API name into every locale, `en` included
|
|
187
|
+
* ("Select crm_account first"), which is an internal identifier leaking into
|
|
188
|
+
* the UI, not merely an untranslated word (objectstack#5407). This is the
|
|
189
|
+
* lookup-side counterpart of `emptyHint`, which the form already resolves to
|
|
190
|
+
* labels for the fixed-option widgets. A name with no entry falls back to
|
|
191
|
+
* itself, so a host that passes nothing renders exactly what it did before.
|
|
192
|
+
*/
|
|
193
|
+
dependsOnLabels?: Record<string, string>;
|
|
194
|
+
/**
|
|
195
|
+
* Hint shown when an option list cannot be filled — typically a
|
|
196
|
+
* dependency-gated list still waiting on its controlling field (#2284).
|
|
197
|
+
* Forwarded by the form renderer, which resolves the controlling fields to
|
|
198
|
+
* their human LABELS. **When supplied it wins**; the option widgets fall back
|
|
199
|
+
* to their own translated copy only when a host computed none (objectui#3231
|
|
200
|
+
* — they used to discard this prop and always render their own hardcoded
|
|
201
|
+
* English). See `OptionsEmptyState`, the single consumer.
|
|
99
202
|
*/
|
|
100
203
|
emptyHint?: string;
|
|
101
204
|
/**
|
|
@@ -114,15 +217,7 @@ export type FieldWidgetComponentProps<T = any> = {
|
|
|
114
217
|
* user had typed. Also declarable on the field metadata.
|
|
115
218
|
*/
|
|
116
219
|
onCreateNew?: (searchQuery: string) => void;
|
|
117
|
-
|
|
118
|
-
/** react-hook-form's field name, spread in by the form renderer. */
|
|
119
|
-
name?: string;
|
|
120
|
-
autoFocus?: boolean;
|
|
121
|
-
tabIndex?: number;
|
|
122
|
-
onBlur?: FocusEventHandler<HTMLElement>;
|
|
123
|
-
onFocus?: FocusEventHandler<HTMLElement>;
|
|
124
|
-
onClick?: MouseEventHandler<HTMLElement>;
|
|
125
|
-
} & AriaAttributes & {
|
|
220
|
+
} & FieldWidgetDomProps & AriaAttributes & {
|
|
126
221
|
/**
|
|
127
222
|
* Arbitrary `data-*` attributes (test ids, analytics hooks). Open by design,
|
|
128
223
|
* but a template-literal key — `keyof` stays finite, so this does NOT
|
|
@@ -46,8 +46,14 @@ export interface UseRecordQueryOptions {
|
|
|
46
46
|
* `$filter` — already merged by the caller (base `lookup_filters`, dependent
|
|
47
47
|
* lookup chain, candidate hygiene like `banned != true`, …). Compared by
|
|
48
48
|
* value, so a referentially-new-but-equal object each render will not loop.
|
|
49
|
+
*
|
|
50
|
+
* Either the `QueryParams.$filter` record form or a lowered ObjectQL AST node
|
|
51
|
+
* (an array), since the picker's merge now produces both (#3831). Typed
|
|
52
|
+
* `unknown` rather than `Record< string, any >` so an array cannot slip
|
|
53
|
+
* through a type that silently accepts it; emptiness is decided by
|
|
54
|
+
* {@link hasFilter}, not by `Object.keys`.
|
|
49
55
|
*/
|
|
50
|
-
filter?:
|
|
56
|
+
filter?: unknown;
|
|
51
57
|
/** `$expand` — related entities to include (e.g. `['primary_business_unit_id']`). */
|
|
52
58
|
expand?: string[];
|
|
53
59
|
/** `$searchFields` — narrow the server searchable set (ADR-0061). */
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Adapt `SchemaRenderer`'s SDUI carrier onto the field-widget contract —
|
|
4
|
+
* objectui#3233, the producer-side half of retiring `schema` from
|
|
5
|
+
* {@link import('./widgets/types').FieldWidgetComponentProps}.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this exists
|
|
8
|
+
*
|
|
9
|
+
* Two kinds of host render a registered field widget, and they name the
|
|
10
|
+
* metadata differently *for good reason*:
|
|
11
|
+
*
|
|
12
|
+
* - the **form renderer** (`renderFieldComponent` in
|
|
13
|
+
* `@object-ui/components`), the inline-edit hosts (`FieldEditWidget`) and
|
|
14
|
+
* the action dialogs pass **`field`** — the field metadata they already
|
|
15
|
+
* hold;
|
|
16
|
+
* - **`SchemaRenderer`** (`@object-ui/react`) passes **`schema`** — the
|
|
17
|
+
* authored node. That key is the UNIVERSAL SDUI contract *every* registered
|
|
18
|
+
* component receives (`element:*`, `page:*`, `object-grid`,
|
|
19
|
+
* `ReportRenderer` …). It is not being retired and must not be: it is what
|
|
20
|
+
* the renderer means by "the node you were rendered from".
|
|
21
|
+
*
|
|
22
|
+
* What WAS wrong is that both keys reached the widget, so ~30 widgets resolved
|
|
23
|
+
* their config as `field || schema` — one concept under two spellings, a second
|
|
24
|
+
* de-facto contract on the widget side (AGENTS.md #0.1) and precisely the shape
|
|
25
|
+
* that lets an AI-written widget pick one spelling and appear to work under one
|
|
26
|
+
* host while reading `undefined` under the other.
|
|
27
|
+
*
|
|
28
|
+
* The translation belongs at the seam, exactly once: here. Downstream `field`
|
|
29
|
+
* is the only carrier, and a widget reading `props.schema` no longer compiles.
|
|
30
|
+
*
|
|
31
|
+
* ## Guarantee
|
|
32
|
+
*
|
|
33
|
+
* Payload-preserving by construction: the node is forwarded **by reference**,
|
|
34
|
+
* so a widget's `field` is the very object it used to read through `schema`.
|
|
35
|
+
* `field` wins when a host supplies both (the form path always does, and it
|
|
36
|
+
* holds the resolved metadata rather than the raw node).
|
|
37
|
+
*
|
|
38
|
+
* `schema` is *consumed* here rather than forwarded — with the key gone from
|
|
39
|
+
* the contract, passing it on would only reach the DOM as a stray attribute
|
|
40
|
+
* through a widget's `...props` spread.
|
|
41
|
+
*
|
|
42
|
+
* ## For widget authors outside this repo
|
|
43
|
+
*
|
|
44
|
+
* Registering a widget raw means `SchemaRenderer` hands it `schema` and no
|
|
45
|
+
* `field`. Wrap it once at registration and the widget only ever implements
|
|
46
|
+
* `FieldWidgetComponentProps`:
|
|
47
|
+
*
|
|
48
|
+
* ```tsx
|
|
49
|
+
* import { withFieldCarrier } from '@object-ui/fields';
|
|
50
|
+
*
|
|
51
|
+
* ComponentRegistry.register('color', withFieldCarrier(ColorPickerField), {
|
|
52
|
+
* namespace: 'field',
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function withFieldCarrier<P extends object>(FieldWidget: React.ComponentType<P>): React.FC<Record<string, any>>;
|