@object-ui/fields 17.5.0 → 17.6.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 +714 -0
- package/dist/FieldEditWidget.d.ts +16 -2
- package/dist/field-type-alias.d.ts +16 -0
- package/dist/index.cjs +4 -4
- package/dist/index.css +27 -9
- package/dist/index.d.ts +425 -72
- package/dist/index.js +2530 -2228
- package/dist/widgets/AddressField.d.ts +3 -3
- package/dist/widgets/AutoNumberField.d.ts +1 -1
- package/dist/widgets/AvatarField.d.ts +1 -1
- package/dist/widgets/BooleanField.d.ts +1 -1
- package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
- 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/DateField.d.ts +1 -1
- package/dist/widgets/DateTimeField.d.ts +1 -1
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +18 -2
- package/dist/widgets/FilterConditionField.d.ts +28 -1
- package/dist/widgets/FormulaField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
- package/dist/widgets/GeolocationField.d.ts +1 -1
- package/dist/widgets/GridField.d.ts +56 -4
- 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/MasterDetailField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/NumberField.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 +13 -7
- package/dist/widgets/PasswordField.d.ts +1 -1
- 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 +2 -4
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +1 -1
- package/dist/widgets/SignatureField.d.ts +32 -2
- package/dist/widgets/SliderField.d.ts +37 -2
- package/dist/widgets/SummaryField.d.ts +1 -1
- package/dist/widgets/TagsField.d.ts +1 -1
- package/dist/widgets/TextAreaField.d.ts +6 -2
- package/dist/widgets/TextField.d.ts +1 -1
- package/dist/widgets/TimeField.d.ts +1 -1
- package/dist/widgets/UrlField.d.ts +1 -1
- package/dist/widgets/UserField.d.ts +1 -1
- package/dist/widgets/VectorField.d.ts +1 -1
- package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
- package/dist/widgets/address-format.d.ts +27 -7
- package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
- package/dist/widgets/richTextDisplay.d.ts +80 -0
- package/dist/widgets/toHostGroupProps.d.ts +119 -21
- package/dist/widgets/types.d.ts +8 -3
- package/package.json +9 -10
- package/dist/widgets/CharacterCount.d.ts +0 -56
- package/src/FieldEditWidget.test.ts +0 -133
- package/src/FieldEditWidget.tsx +0 -207
- package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
- package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
- package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
- package/src/__tests__/DateCellRenderer.test.tsx +0 -82
- package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
- package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
- package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
- package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
- package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
- package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
- package/src/__tests__/cell-truncation.test.tsx +0 -96
- package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
- package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
- package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
- package/src/__tests__/date-locale-channel.test.tsx +0 -305
- package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
- package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
- package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
- package/src/__tests__/group-labelling-declaration.test.ts +0 -148
- package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
- package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
- package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
- package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
- package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
- package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
- package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
- package/src/__tests__/registry-collision.test.ts +0 -91
- package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
- package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
- package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
- package/src/__tests__/validation-feedback.test.tsx +0 -361
- package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
- package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
- package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
- package/src/__tests__/widget-props-contract.test.tsx +0 -142
- package/src/coerce-safe-value.test.ts +0 -34
- package/src/complex-widgets.test.tsx +0 -924
- package/src/currency.ts +0 -144
- package/src/datetime-widgets.test.tsx +0 -185
- package/src/field-type-alias.multiple.test.ts +0 -81
- package/src/field-type-alias.ts +0 -156
- package/src/field-type-coverage.test.ts +0 -67
- package/src/index.css +0 -44
- package/src/index.tsx +0 -2745
- package/src/standard-widgets.test.tsx +0 -364
- package/src/widgets/AddressField.postalCode.test.tsx +0 -257
- package/src/widgets/AddressField.tsx +0 -182
- package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
- package/src/widgets/AutoNumberField.tsx +0 -18
- package/src/widgets/AvatarField.tsx +0 -117
- package/src/widgets/BooleanField.tsx +0 -86
- package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
- package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
- package/src/widgets/CharacterCount.tsx +0 -263
- package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
- package/src/widgets/CheckboxesField.tsx +0 -169
- package/src/widgets/CodeField.tsx +0 -41
- package/src/widgets/ColorField.tsx +0 -53
- package/src/widgets/CurrencyField.tsx +0 -135
- package/src/widgets/DateField.tsx +0 -40
- package/src/widgets/DateTimeField.tsx +0 -46
- package/src/widgets/EmailField.tsx +0 -37
- package/src/widgets/FileField.tsx +0 -512
- package/src/widgets/FilterConditionField.tsx +0 -454
- package/src/widgets/FormulaField.tsx +0 -38
- package/src/widgets/FullscreenFieldEditor.tsx +0 -60
- package/src/widgets/GeolocationField.tsx +0 -193
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
- package/src/widgets/GridField.test.tsx +0 -632
- package/src/widgets/GridField.tsx +0 -1070
- package/src/widgets/ImageCropperDialog.tsx +0 -245
- package/src/widgets/ImageField.maxSize.test.tsx +0 -154
- package/src/widgets/ImageField.tsx +0 -305
- package/src/widgets/ImageLightbox.test.tsx +0 -52
- package/src/widgets/ImageLightbox.tsx +0 -108
- package/src/widgets/LocationField.tsx +0 -57
- package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
- package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
- package/src/widgets/LookupField.hydration.test.tsx +0 -136
- package/src/widgets/LookupField.idField.test.tsx +0 -61
- package/src/widgets/LookupField.tsx +0 -1295
- package/src/widgets/MarkdownContent.tsx +0 -19
- package/src/widgets/MasterDetailField.tsx +0 -131
- package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
- package/src/widgets/MultiSelectField.tsx +0 -196
- package/src/widgets/NumberField.tsx +0 -49
- package/src/widgets/ObjectField.tsx +0 -94
- package/src/widgets/ObjectRefField.tsx +0 -121
- package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
- package/src/widgets/OptionsEmptyState.test.tsx +0 -169
- package/src/widgets/OptionsEmptyState.tsx +0 -99
- package/src/widgets/PasswordField.tsx +0 -49
- package/src/widgets/PeoplePicker.test.tsx +0 -360
- package/src/widgets/PeoplePicker.tsx +0 -579
- package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
- package/src/widgets/PercentField.tsx +0 -95
- package/src/widgets/PersonRow.tsx +0 -117
- package/src/widgets/PhoneField.tsx +0 -37
- package/src/widgets/QRCodeField.tsx +0 -103
- package/src/widgets/RadioField.cascade.test.tsx +0 -141
- package/src/widgets/RadioField.tsx +0 -143
- package/src/widgets/RatingField.tsx +0 -94
- package/src/widgets/RecipientPickerField.test.tsx +0 -157
- package/src/widgets/RecipientPickerField.tsx +0 -188
- package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
- package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
- package/src/widgets/RecordPickerDialog.tsx +0 -1365
- package/src/widgets/RichTextField.tsx +0 -176
- package/src/widgets/SelectField.cascade.test.tsx +0 -138
- package/src/widgets/SelectField.tsx +0 -193
- package/src/widgets/SelectionTray.tsx +0 -110
- package/src/widgets/SignatureField.tsx +0 -140
- package/src/widgets/SliderField.tsx +0 -39
- package/src/widgets/SummaryField.tsx +0 -31
- package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
- package/src/widgets/TagsField.placeholder.test.tsx +0 -95
- package/src/widgets/TagsField.tsx +0 -88
- package/src/widgets/TextAreaField.tsx +0 -213
- package/src/widgets/TextField.tsx +0 -45
- package/src/widgets/TimeField.tsx +0 -31
- package/src/widgets/UrlField.tsx +0 -46
- package/src/widgets/UserField.tsx +0 -59
- package/src/widgets/UserFieldPicker.test.tsx +0 -78
- package/src/widgets/VectorField.tsx +0 -29
- package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
- package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
- package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
- package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
- package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
- package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
- package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
- package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
- package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
- package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
- package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
- package/src/widgets/_cell-renderer-bridge.ts +0 -19
- package/src/widgets/address-format.ts +0 -120
- package/src/widgets/deriveLookupColumns.test.ts +0 -112
- package/src/widgets/deriveLookupColumns.ts +0 -108
- package/src/widgets/file-size-guard.ts +0 -55
- package/src/widgets/file-value.test.ts +0 -230
- package/src/widgets/file-value.ts +0 -246
- package/src/widgets/nativeDateValue.test.ts +0 -115
- package/src/widgets/nativeDateValue.ts +0 -112
- package/src/widgets/openNativePicker.ts +0 -18
- package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
- package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
- package/src/widgets/personDisplay.test.ts +0 -113
- package/src/widgets/personDisplay.ts +0 -92
- package/src/widgets/recentLookups.test.ts +0 -45
- package/src/widgets/recentLookups.ts +0 -47
- package/src/widgets/toDomProps.ts +0 -204
- package/src/widgets/toHostGroupProps.ts +0 -102
- package/src/widgets/types.ts +0 -242
- package/src/widgets/useCascadingOptions.ts +0 -46
- package/src/widgets/useFieldTranslation.ts +0 -132
- package/src/widgets/useRecordQuery.test.ts +0 -174
- package/src/widgets/useRecordQuery.ts +0 -291
- package/src/widgets/useUploadingSignal.test.tsx +0 -62
- package/src/widgets/useUploadingSignal.ts +0 -20
- package/src/withFieldCarrier.tsx +0 -75
package/src/widgets/types.ts
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import type { AriaAttributes, FocusEventHandler, MouseEventHandler } from 'react';
|
|
2
|
-
import type { DependsOnInput, FieldMetadata } from '@object-ui/types';
|
|
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
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Props every field widget in this package receives at RUNTIME.
|
|
49
|
-
*
|
|
50
|
-
* Named `FieldWidgetComponentProps`, not `FieldWidgetProps` (objectui#3161,
|
|
51
|
-
* objectstack#4115 ledger batch 7), adopting the name `@object-ui/app-shell`
|
|
52
|
-
* settled on for the same split in objectui#3169: `@objectstack/spec/ui` owns
|
|
53
|
-
* `FieldWidgetProps` for the DECLARED widget-plugin contract — a zod object
|
|
54
|
-
* (`FieldWidgetPropsSchema`) a plugin manifest is validated against, with
|
|
55
|
-
* `field` narrowed to `{ name?, label?, type: <the FieldType enum> }` and
|
|
56
|
-
* `readonly` / `required` carrying `.default()`s. This is the React interface
|
|
57
|
-
* the widgets in this directory actually implement, against
|
|
58
|
-
* `@object-ui/types`'s much richer `FieldMetadata`.
|
|
59
|
-
*
|
|
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.
|
|
81
|
-
*
|
|
82
|
-
* ## Why there is no `[key: string]: any` (objectui#3221)
|
|
83
|
-
*
|
|
84
|
-
* This type used to end in an index signature, "load-bearing" for the widgets'
|
|
85
|
-
* `...props` spreads. It was also why every drift above was invisible: **a type
|
|
86
|
-
* that claims to have every key can never be reported as missing one** (the
|
|
87
|
-
* objectstack#4075 mechanism). `props.required` and `props.error` were legal
|
|
88
|
-
* reads typed `any` and always `undefined` at runtime; a misspelled prop
|
|
89
|
-
* (`readOnly` for `readonly`, `onchange` for `onChange`) compiled; and any
|
|
90
|
-
* structural/parity comparison against the type was useless *in principle*,
|
|
91
|
-
* which is why the batch-7 symbol guard was the only detector that could see
|
|
92
|
-
* the collision at all.
|
|
93
|
-
*
|
|
94
|
-
* It is replaced by the CLOSED set below: the controlled-input contract, the
|
|
95
|
-
* host plumbing the form renderer genuinely forwards, and a DOM pass-through
|
|
96
|
-
* allowance. `data-*` is an open family by design (it is open in HTML too) and
|
|
97
|
-
* is expressed as a template-literal index signature, which — unlike
|
|
98
|
-
* `[key: string]` — leaves `keyof` finite, so an undeclared prop still fails.
|
|
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
|
-
*
|
|
116
|
-
* Adding a key here is a contract change: say who produces it and who reads it.
|
|
117
|
-
*/
|
|
118
|
-
export type FieldWidgetComponentProps<T = any> = {
|
|
119
|
-
/* ── The controlled-input contract every widget implements ─────────────── */
|
|
120
|
-
|
|
121
|
-
value: T;
|
|
122
|
-
onChange: (val: T) => void;
|
|
123
|
-
/**
|
|
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.
|
|
135
|
-
*/
|
|
136
|
-
field: FieldMetadata;
|
|
137
|
-
readonly?: boolean;
|
|
138
|
-
disabled?: boolean;
|
|
139
|
-
className?: string;
|
|
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;
|
|
156
|
-
/**
|
|
157
|
-
* Upload widgets (`file`/`image`) fire this when their in-progress state
|
|
158
|
-
* flips, so a host can block submit until a presigned upload settles. Other
|
|
159
|
-
* widgets ignore it.
|
|
160
|
-
*/
|
|
161
|
-
onUploadingChange?: (uploading: boolean) => void;
|
|
162
|
-
|
|
163
|
-
/* ── Host plumbing: what a rendering host actually forwards ─────────────── */
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* DataSource for widgets that query records (lookup / user / object-ref /
|
|
167
|
-
* recipient-picker / grid). Injected by the form renderer for the field
|
|
168
|
-
* types that need it, and passed directly by inline-edit hosts. Option
|
|
169
|
-
* widgets destructure it purely to keep it off their DOM spread.
|
|
170
|
-
*
|
|
171
|
-
* Left structural (`unknown`): `@object-ui/fields` must not depend on a
|
|
172
|
-
* concrete adapter — every consumer narrows it itself.
|
|
173
|
-
*/
|
|
174
|
-
dataSource?: unknown;
|
|
175
|
-
/**
|
|
176
|
-
* Live sibling-field values driving cascading / role-gated options and
|
|
177
|
-
* dependent lookups (ADR-0058, #2215/#2284). The form renderer passes the
|
|
178
|
-
* in-progress record; widgets fall back to `SchemaRendererContext`.
|
|
179
|
-
*/
|
|
180
|
-
dependentValues?: Record<string, unknown>;
|
|
181
|
-
/**
|
|
182
|
-
* Controlling field(s) that gate this field's option list. Normally declared
|
|
183
|
-
* on the field metadata; accepted as a prop so a host can drive the gate for
|
|
184
|
-
* a field it synthesised. Field metadata wins when both are present.
|
|
185
|
-
*/
|
|
186
|
-
dependsOn?: DependsOnInput;
|
|
187
|
-
/**
|
|
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.
|
|
209
|
-
*/
|
|
210
|
-
emptyHint?: string;
|
|
211
|
-
/**
|
|
212
|
-
* Render as a single-line, borderless control for a grid cell (the inline
|
|
213
|
-
* editor and the line-item grid set it) instead of the full form layout.
|
|
214
|
-
*/
|
|
215
|
-
compact?: boolean;
|
|
216
|
-
/**
|
|
217
|
-
* Receive the FULL selected record rather than just its id, so a host can
|
|
218
|
-
* auto-fill sibling fields from it (a line-item grid copying a product's
|
|
219
|
-
* price). When provided, the host owns the resulting value change.
|
|
220
|
-
*/
|
|
221
|
-
onSelectRecord?: (record: Record<string, any>) => void;
|
|
222
|
-
/**
|
|
223
|
-
* Offer a "create new" affordance in a record picker, carrying whatever the
|
|
224
|
-
* user had typed. Also declarable on the field metadata.
|
|
225
|
-
*/
|
|
226
|
-
onCreateNew?: (searchQuery: string) => void;
|
|
227
|
-
|
|
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 & {
|
|
235
|
-
/**
|
|
236
|
-
* Arbitrary `data-*` attributes (test ids, analytics hooks). Open by design,
|
|
237
|
-
* but a template-literal key — `keyof` stays finite, so this does NOT
|
|
238
|
-
* reintroduce the index signature objectui#3221 removed: `props.required`
|
|
239
|
-
* is still an error, `props['data-testid']` is still fine.
|
|
240
|
-
*/
|
|
241
|
-
[dataAttribute: `data-${string}`]: string | number | boolean | undefined;
|
|
242
|
-
};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { useContext, useMemo } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
resolveCascadingOptions,
|
|
4
|
-
type CascadingOptions,
|
|
5
|
-
type OptionLike,
|
|
6
|
-
type DependsOnInput,
|
|
7
|
-
} from '@object-ui/core';
|
|
8
|
-
import { SchemaRendererContext, usePredicateScope } from '@object-ui/react';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Shared per-option cascading / role-gating resolution for the option widgets
|
|
12
|
-
* (`SelectField` single, `MultiSelectField`, `RadioField`) — the client half of
|
|
13
|
-
* ADR-0058 (#2284). Each option may carry a `visibleWhen` CEL predicate; the
|
|
14
|
-
* offered set narrows against the live form record (`record.country == 'cn'`) +
|
|
15
|
-
* the global predicate scope (`'admin' in current_user.positions`). A field
|
|
16
|
-
* declares which sibling fields drive its list via `dependsOn`; while any is
|
|
17
|
-
* empty the list is *gated* — callers surface a "select the parent first" hint
|
|
18
|
-
* rather than an unfiltered set, mirroring the dependent-lookup UX.
|
|
19
|
-
*
|
|
20
|
-
* This is the React wrapper that sources `record` (the live form values) and the
|
|
21
|
-
* predicate scope from context; the actual resolution is the pure
|
|
22
|
-
* {@link resolveCascadingOptions} in `@object-ui/core`, shared with the form
|
|
23
|
-
* renderer so gating/filtering can never drift between them (#2715).
|
|
24
|
-
*/
|
|
25
|
-
export type CascadingOptionsResult<T extends OptionLike> = CascadingOptions<T>;
|
|
26
|
-
|
|
27
|
-
export function useCascadingOptions<T extends OptionLike>(
|
|
28
|
-
rawOptions: readonly T[],
|
|
29
|
-
dependsOn: DependsOnInput,
|
|
30
|
-
dependentValues: Record<string, unknown> | undefined,
|
|
31
|
-
): CascadingOptionsResult<T> {
|
|
32
|
-
// Live form values for cascading options — injected by the form renderer as
|
|
33
|
-
// `dependentValues` (same channel dependent lookups use), falling back to the
|
|
34
|
-
// record on SchemaRendererContext. `current_user` etc. come from the global
|
|
35
|
-
// predicate scope so role/context predicates resolve too.
|
|
36
|
-
const ctx = useContext(SchemaRendererContext) as any;
|
|
37
|
-
const record = useMemo<Record<string, unknown>>(() => {
|
|
38
|
-
return (dependentValues ?? ctx?.formValues ?? ctx?.data ?? {}) as Record<string, unknown>;
|
|
39
|
-
}, [dependentValues, ctx?.formValues, ctx?.data]);
|
|
40
|
-
const predicateScope = usePredicateScope();
|
|
41
|
-
|
|
42
|
-
return useMemo(
|
|
43
|
-
() => resolveCascadingOptions(rawOptions, record, dependsOn, predicateScope),
|
|
44
|
-
[rawOptions, record, dependsOn, predicateScope],
|
|
45
|
-
);
|
|
46
|
-
}
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Safe translation hook for field widgets.
|
|
3
|
-
* Falls back to English defaults when no I18nProvider is available.
|
|
4
|
-
*/
|
|
5
|
-
import { createSafeTranslation } from '@object-ui/i18n';
|
|
6
|
-
|
|
7
|
-
const FIELD_DEFAULTS: Record<string, string> = {
|
|
8
|
-
'common.selectOption': 'Select an option',
|
|
9
|
-
'common.select': 'Select…',
|
|
10
|
-
'common.loading': 'Loading…',
|
|
11
|
-
'common.noResults': 'No results found',
|
|
12
|
-
'common.cancel': 'Cancel',
|
|
13
|
-
'common.confirm': 'Confirm',
|
|
14
|
-
'table.selected': '{{count}} selected',
|
|
15
|
-
'table.search': 'Search…',
|
|
16
|
-
'lookup.loading': 'Loading…',
|
|
17
|
-
'lookup.noOptions': 'No options found',
|
|
18
|
-
'lookup.noRecords': 'No records found',
|
|
19
|
-
'lookup.recentlyUsed': 'Recently used',
|
|
20
|
-
'lookup.allResults': 'All results',
|
|
21
|
-
'lookup.createNew': 'Create new',
|
|
22
|
-
'lookup.createNamed': 'Create new "{{name}}"',
|
|
23
|
-
'lookup.showingResults': 'Showing {{shown}} of {{total}} results',
|
|
24
|
-
'lookup.showAllResults': 'Show all results ({{count}})',
|
|
25
|
-
'lookup.selectedBadge': 'Selected',
|
|
26
|
-
'lookup.browseAll': 'Browse all records',
|
|
27
|
-
'lookup.remove': 'Remove {{label}}',
|
|
28
|
-
'lookup.selectFirst': 'Select {{fields}} first',
|
|
29
|
-
'lookup.selectRecord': 'Select record',
|
|
30
|
-
'lookup.recordCount': '{{count}} records',
|
|
31
|
-
'lookup.recordCountOne': '1 record',
|
|
32
|
-
'lookup.pageOf': 'Page {{current}} of {{total}}',
|
|
33
|
-
'lookup.filters': 'Filters',
|
|
34
|
-
'lookup.clear': 'Clear',
|
|
35
|
-
'lookup.yes': 'Yes',
|
|
36
|
-
'lookup.filterPlaceholder': 'Filter {{label}}',
|
|
37
|
-
'lookup.prevPage': 'Previous page',
|
|
38
|
-
'lookup.nextPage': 'Next page',
|
|
39
|
-
'lookup.jumpToPage': 'Jump to page',
|
|
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',
|
|
47
|
-
// objectstack#3821 — sharing-rule authoring widgets (object-ref /
|
|
48
|
-
// recipient-picker / filter-condition). The recipient placeholder is keyed
|
|
49
|
-
// PER TYPE rather than interpolating the enum value into an English
|
|
50
|
-
// sentence, which no locale could translate.
|
|
51
|
-
'fields.objectRef.loading': 'Loading objects…',
|
|
52
|
-
'fields.objectRef.placeholder': 'Select an object',
|
|
53
|
-
'fields.objectRef.search': 'Search objects…',
|
|
54
|
-
'fields.objectRef.empty': 'No objects found',
|
|
55
|
-
'fields.recipient.selectTypeFirst': 'Select a recipient type first.',
|
|
56
|
-
'fields.recipient.loading': 'Loading…',
|
|
57
|
-
'fields.recipient.search': 'Search…',
|
|
58
|
-
'fields.recipient.empty': 'No matches',
|
|
59
|
-
'fields.recipient.select': 'Select a recipient',
|
|
60
|
-
'fields.recipient.selectUser': 'Select a user',
|
|
61
|
-
'fields.recipient.selectTeam': 'Select a team',
|
|
62
|
-
'fields.recipient.selectBusinessUnit': 'Select a business unit',
|
|
63
|
-
'fields.recipient.selectPosition': 'Select a position',
|
|
64
|
-
'fields.recipient.selectUnitAndSubordinates': 'Select a business unit',
|
|
65
|
-
'fields.filterCondition.selectObjectFirst': 'Select an object first.',
|
|
66
|
-
// objectstack#3896 — this used to be 'All records'. An empty criteria never
|
|
67
|
-
// meant "share everything"; it meant the predicate was missing, and the
|
|
68
|
-
// sharing evaluator failed open on it. Such a rule is now refused on save
|
|
69
|
-
// and shares nothing, so say that rather than advertise the old bug.
|
|
70
|
-
'fields.filterCondition.noCriteria': 'No criteria — this rule shares nothing',
|
|
71
|
-
'fields.filterCondition.criteriaRequired':
|
|
72
|
-
'Add at least one condition. A rule with no criteria would share every record, so it cannot be saved.',
|
|
73
|
-
'fields.filterCondition.invalidJson': 'Invalid JSON — the rule will match no records until fixed.',
|
|
74
|
-
'fields.filterCondition.jsonOnly': 'This criteria can only be edited as JSON',
|
|
75
|
-
'fields.filterCondition.editAsJson': 'Edit as JSON',
|
|
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.
|
|
114
|
-
// objectui#2600 B5 — capability picker scope group headers.
|
|
115
|
-
'capability.group.platform': 'Platform',
|
|
116
|
-
'capability.group.org': 'Organization',
|
|
117
|
-
'capability.group.other': 'Other',
|
|
118
|
-
// objectui#2600 B5 — curated platform capability labels (registry serves
|
|
119
|
-
// English; dots in the api-name become underscores in the key).
|
|
120
|
-
'capability.label.manage_users': 'Manage Users',
|
|
121
|
-
'capability.label.manage_org_users': 'Manage Organization Users',
|
|
122
|
-
'capability.label.manage_metadata': 'Manage Metadata',
|
|
123
|
-
'capability.label.manage_platform_settings': 'Manage Platform Settings',
|
|
124
|
-
'capability.label.setup_access': 'Setup Access',
|
|
125
|
-
'capability.label.setup_write': 'Write Settings',
|
|
126
|
-
'capability.label.studio_access': 'Studio Access',
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
export const useFieldTranslation = createSafeTranslation(
|
|
130
|
-
FIELD_DEFAULTS,
|
|
131
|
-
'common.selectOption',
|
|
132
|
-
);
|
|
@@ -1,174 +0,0 @@
|
|
|
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 { renderHook, act, waitFor } from '@testing-library/react';
|
|
10
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
11
|
-
import { useRecordQuery } from './useRecordQuery';
|
|
12
|
-
|
|
13
|
-
function makeDataSource(result: any = { data: [], total: 0 }) {
|
|
14
|
-
return { find: vi.fn().mockResolvedValue(result) } as any;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('useRecordQuery', () => {
|
|
18
|
-
it('fetches on mount with $top and exposes records/total', async () => {
|
|
19
|
-
const ds = makeDataSource({ data: [{ id: 1 }], total: 5 });
|
|
20
|
-
const { result } = renderHook(() =>
|
|
21
|
-
useRecordQuery({ dataSource: ds, objectName: 'sys_user' }),
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
25
|
-
expect(ds.find).toHaveBeenCalledWith('sys_user', { $top: 50 });
|
|
26
|
-
expect(result.current.records).toEqual([{ id: 1 }]);
|
|
27
|
-
expect(result.current.total).toBe(5);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('tolerates a bare-array response (no { data } envelope)', async () => {
|
|
31
|
-
const ds = { find: vi.fn().mockResolvedValue([{ id: 1 }, { id: 2 }]) } as any;
|
|
32
|
-
const { result } = renderHook(() =>
|
|
33
|
-
useRecordQuery({ dataSource: ds, objectName: 'o' }),
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
await waitFor(() => expect(result.current.records).toHaveLength(2));
|
|
37
|
-
expect(result.current.total).toBe(2);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('adds $skip only when paginate is set', async () => {
|
|
41
|
-
const ds = makeDataSource({ data: [], total: 100 });
|
|
42
|
-
const { result } = renderHook(() =>
|
|
43
|
-
useRecordQuery({ dataSource: ds, objectName: 'o', pageSize: 10, paginate: true }),
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
await waitFor(() => expect(ds.find).toHaveBeenLastCalledWith('o', { $top: 10, $skip: 0 }));
|
|
47
|
-
// waitFor: the call firing doesn't guarantee its promise resolved and
|
|
48
|
-
// setTotal flushed — assert on the settled state, not a bare read.
|
|
49
|
-
await waitFor(() => expect(result.current.totalPages).toBe(10));
|
|
50
|
-
|
|
51
|
-
act(() => result.current.setPage(3));
|
|
52
|
-
await waitFor(() =>
|
|
53
|
-
expect(ds.find).toHaveBeenLastCalledWith('o', { $top: 10, $skip: 20 }),
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('debounces setSearch, resets to page 1, and coalesces rapid input', async () => {
|
|
58
|
-
const ds = makeDataSource();
|
|
59
|
-
const { result } = renderHook(() =>
|
|
60
|
-
useRecordQuery({ dataSource: ds, objectName: 'o', debounceMs: 20 }),
|
|
61
|
-
);
|
|
62
|
-
await waitFor(() => expect(ds.find).toHaveBeenCalledTimes(1)); // mount fetch
|
|
63
|
-
|
|
64
|
-
act(() => {
|
|
65
|
-
result.current.setSearch('a');
|
|
66
|
-
result.current.setSearch('ab');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
await waitFor(() =>
|
|
70
|
-
expect(ds.find).toHaveBeenLastCalledWith('o', { $top: 50, $search: 'ab' }),
|
|
71
|
-
);
|
|
72
|
-
// The superseded 'a' term never reached the server.
|
|
73
|
-
expect(ds.find).not.toHaveBeenCalledWith('o', { $top: 50, $search: 'a' });
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('toggleSort sets $orderby asc then flips to desc', async () => {
|
|
77
|
-
const ds = makeDataSource();
|
|
78
|
-
const { result } = renderHook(() =>
|
|
79
|
-
useRecordQuery({ dataSource: ds, objectName: 'o' }),
|
|
80
|
-
);
|
|
81
|
-
await waitFor(() => expect(ds.find).toHaveBeenCalled());
|
|
82
|
-
|
|
83
|
-
act(() => result.current.toggleSort('name'));
|
|
84
|
-
await waitFor(() =>
|
|
85
|
-
expect(ds.find).toHaveBeenLastCalledWith('o', { $top: 50, $orderby: { name: 'asc' } }),
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
act(() => result.current.toggleSort('name'));
|
|
89
|
-
await waitFor(() =>
|
|
90
|
-
expect(ds.find).toHaveBeenLastCalledWith('o', { $top: 50, $orderby: { name: 'desc' } }),
|
|
91
|
-
);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('passes $filter and refetches when the filter value changes', async () => {
|
|
95
|
-
const ds = makeDataSource();
|
|
96
|
-
const { rerender } = renderHook(
|
|
97
|
-
({ filter }) => useRecordQuery({ dataSource: ds, objectName: 'o', filter }),
|
|
98
|
-
{ initialProps: { filter: { banned: { $ne: true } } as Record<string, any> } },
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
await waitFor(() =>
|
|
102
|
-
expect(ds.find).toHaveBeenLastCalledWith('o', {
|
|
103
|
-
$top: 50,
|
|
104
|
-
$filter: { banned: { $ne: true } },
|
|
105
|
-
}),
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
rerender({ filter: { banned: { $ne: true }, active: true } });
|
|
109
|
-
await waitFor(() =>
|
|
110
|
-
expect(ds.find).toHaveBeenLastCalledWith('o', {
|
|
111
|
-
$top: 50,
|
|
112
|
-
$filter: { banned: { $ne: true }, active: true },
|
|
113
|
-
}),
|
|
114
|
-
);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('passes $expand and $searchFields through', async () => {
|
|
118
|
-
const ds = makeDataSource();
|
|
119
|
-
renderHook(() =>
|
|
120
|
-
useRecordQuery({
|
|
121
|
-
dataSource: ds,
|
|
122
|
-
objectName: 'sys_user',
|
|
123
|
-
expand: ['primary_business_unit_id'],
|
|
124
|
-
searchFields: ['name', 'email'],
|
|
125
|
-
}),
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
await waitFor(() =>
|
|
129
|
-
expect(ds.find).toHaveBeenLastCalledWith('sys_user', {
|
|
130
|
-
$top: 50,
|
|
131
|
-
$expand: ['primary_business_unit_id'],
|
|
132
|
-
$searchFields: ['name', 'email'],
|
|
133
|
-
}),
|
|
134
|
-
);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('does not fetch while disabled and clears records when re-disabled', async () => {
|
|
138
|
-
const ds = makeDataSource({ data: [{ id: 1 }], total: 1 });
|
|
139
|
-
const { result, rerender } = renderHook(
|
|
140
|
-
({ enabled }) => useRecordQuery({ dataSource: ds, objectName: 'o', enabled }),
|
|
141
|
-
{ initialProps: { enabled: false } },
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
await new Promise(r => setTimeout(r, 20));
|
|
145
|
-
expect(ds.find).not.toHaveBeenCalled();
|
|
146
|
-
|
|
147
|
-
rerender({ enabled: true });
|
|
148
|
-
await waitFor(() => expect(result.current.records).toEqual([{ id: 1 }]));
|
|
149
|
-
|
|
150
|
-
rerender({ enabled: false });
|
|
151
|
-
await waitFor(() => expect(result.current.records).toEqual([]));
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('captures fetch errors and empties the result set', async () => {
|
|
155
|
-
const ds = { find: vi.fn().mockRejectedValue(new Error('boom')) } as any;
|
|
156
|
-
const { result } = renderHook(() =>
|
|
157
|
-
useRecordQuery({ dataSource: ds, objectName: 'o' }),
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
await waitFor(() => expect(result.current.error).toBe('boom'));
|
|
161
|
-
expect(result.current.records).toEqual([]);
|
|
162
|
-
expect(result.current.loading).toBe(false);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('stays idle when no dataSource is provided', async () => {
|
|
166
|
-
const { result } = renderHook(() =>
|
|
167
|
-
useRecordQuery({ dataSource: null, objectName: 'o' }),
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
await new Promise(r => setTimeout(r, 10));
|
|
171
|
-
expect(result.current.loading).toBe(false);
|
|
172
|
-
expect(result.current.records).toEqual([]);
|
|
173
|
-
});
|
|
174
|
-
});
|