@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
|
@@ -3,6 +3,7 @@ import { Input } from '@object-ui/components';
|
|
|
3
3
|
import { Button } from '@object-ui/components';
|
|
4
4
|
import { Eye, EyeOff } from 'lucide-react';
|
|
5
5
|
import { FieldWidgetComponentProps } from './types';
|
|
6
|
+
import { toDomProps } from './toDomProps';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* PasswordField - Secure password input with visibility toggle
|
|
@@ -10,14 +11,13 @@ import { FieldWidgetComponentProps } from './types';
|
|
|
10
11
|
*/
|
|
11
12
|
export function PasswordField({ value, onChange, field, readonly, className, ...props }: FieldWidgetComponentProps<string>) {
|
|
12
13
|
const [showPassword, setShowPassword] = useState(false);
|
|
13
|
-
const config = field
|
|
14
|
+
const config = field;
|
|
14
15
|
|
|
15
16
|
if (readonly) {
|
|
16
17
|
return <span className="text-sm">••••••••</span>;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
const { inputType, ...domProps } = props as any;
|
|
20
|
+
const domProps = toDomProps(props);
|
|
21
21
|
|
|
22
22
|
return (
|
|
23
23
|
<div className="relative">
|
|
@@ -316,8 +316,14 @@ export function PeoplePicker({
|
|
|
316
316
|
// returns the same records (StrictMode double-effect, refetch-on-focus)
|
|
317
317
|
// re-emits a new array; resetting on identity alone yanked the cursor to -1
|
|
318
318
|
// mid-navigation (flaky ArrowDown→Enter, and a real UX nit).
|
|
319
|
+
//
|
|
320
|
+
// The id separator is the escaped U+0000 spelling, not the byte itself
|
|
321
|
+
// (objectstack#5450): one raw NUL made this whole file binary to grep and
|
|
322
|
+
// ripgrep — a content search printed `binary file matches` and no line. The
|
|
323
|
+
// runtime value is unchanged, and it is the same separator the `cursorEpoch`
|
|
324
|
+
// key below already spelled this way.
|
|
319
325
|
const recordsSignature = useMemo(
|
|
320
|
-
() => query.records.map(r => String(getPersonId(r, idField))).join('
|
|
326
|
+
() => query.records.map(r => String(getPersonId(r, idField))).join('\u0000'),
|
|
321
327
|
[query.records, idField],
|
|
322
328
|
);
|
|
323
329
|
// The reset runs in the RENDER PHASE (the "adjusting state during render"
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Input, Slider, EmptyValue } from '@object-ui/components';
|
|
3
3
|
import { FieldWidgetComponentProps } from './types';
|
|
4
|
+
import { toDomProps } from './toDomProps';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* PercentField - Percentage input with configurable decimal precision
|
|
7
8
|
* Stores values as decimals (0-1) and displays as percentages (0-100%)
|
|
8
9
|
* Includes a slider for interactive control.
|
|
9
10
|
*/
|
|
10
|
-
export function PercentField({ value, onChange, field, readonly,
|
|
11
|
-
const percentField =
|
|
11
|
+
export function PercentField({ value, onChange, field, readonly, error, className, ...props }: FieldWidgetComponentProps<number>) {
|
|
12
|
+
const percentField = field as any;
|
|
12
13
|
const precision = percentField?.precision ?? 2;
|
|
13
14
|
|
|
14
15
|
// Convention detection. A field declaring `max > 1` (e.g. `max: 100`) stores
|
|
@@ -64,7 +65,7 @@ export function PercentField({ value, onChange, field, readonly, errorMessage, c
|
|
|
64
65
|
<div className="space-y-2">
|
|
65
66
|
<div className="relative">
|
|
66
67
|
<Input
|
|
67
|
-
{...props}
|
|
68
|
+
{...toDomProps(props)}
|
|
68
69
|
type="number"
|
|
69
70
|
value={displayValue}
|
|
70
71
|
onChange={handleChange}
|
|
@@ -72,7 +73,7 @@ export function PercentField({ value, onChange, field, readonly, errorMessage, c
|
|
|
72
73
|
disabled={readonly || props.disabled}
|
|
73
74
|
className={`pr-8 ${className || ''}`}
|
|
74
75
|
step={Math.pow(10, -precision).toFixed(precision)}
|
|
75
|
-
aria-invalid={!!
|
|
76
|
+
aria-invalid={!!error}
|
|
76
77
|
/>
|
|
77
78
|
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-gray-500">
|
|
78
79
|
%
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Input, EmptyValue } from '@object-ui/components';
|
|
3
3
|
import { FieldWidgetComponentProps } from './types';
|
|
4
|
+
import { toDomProps } from './toDomProps';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* PhoneField - Telephone number input with tel link in readonly mode
|
|
7
8
|
* Renders as a clickable tel link when readonly, standard phone input otherwise
|
|
8
9
|
*/
|
|
9
|
-
export function PhoneField({ value, onChange, field, readonly,
|
|
10
|
-
const config = field
|
|
10
|
+
export function PhoneField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
|
|
11
|
+
const config = field;
|
|
11
12
|
if (readonly) {
|
|
12
13
|
if (!value) return <EmptyValue />;
|
|
13
14
|
return (
|
|
@@ -20,8 +21,7 @@ export function PhoneField({ value, onChange, field, readonly, errorMessage, ...
|
|
|
20
21
|
);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
const { inputType, ...domProps } = props as any;
|
|
24
|
+
const domProps = toDomProps(props);
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
27
|
<Input
|
|
@@ -31,7 +31,7 @@ export function PhoneField({ value, onChange, field, readonly, errorMessage, ...
|
|
|
31
31
|
onChange={(e) => onChange(e.target.value)}
|
|
32
32
|
placeholder={config?.placeholder || '(555) 123-4567'}
|
|
33
33
|
disabled={readonly || domProps.disabled}
|
|
34
|
-
aria-invalid={!!
|
|
34
|
+
aria-invalid={!!error}
|
|
35
35
|
/>
|
|
36
36
|
);
|
|
37
37
|
}
|
|
@@ -2,14 +2,15 @@ import React from 'react';
|
|
|
2
2
|
import { Input, Button, EmptyValue } from '@object-ui/components';
|
|
3
3
|
import { QrCode, Copy } from 'lucide-react';
|
|
4
4
|
import { FieldWidgetComponentProps } from './types';
|
|
5
|
+
import { toDomProps } from './toDomProps';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* QR Code field widget - generates QR codes from text
|
|
8
9
|
* Uses a simple SVG-based QR code generator
|
|
9
10
|
*/
|
|
10
|
-
export function QRCodeField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps<string>) {
|
|
11
|
+
export function QRCodeField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
|
|
11
12
|
const [showQR, setShowQR] = React.useState(false);
|
|
12
|
-
const config = field
|
|
13
|
+
const config = field;
|
|
13
14
|
|
|
14
15
|
// Simple QR code generation using an external library would be ideal
|
|
15
16
|
// For now, we'll use a service API approach or placeholder
|
|
@@ -47,12 +48,18 @@ export function QRCodeField({ value, onChange, field, readonly, ...props }: Fiel
|
|
|
47
48
|
<div className="space-y-3">
|
|
48
49
|
<div className="flex items-center gap-2">
|
|
49
50
|
<Input
|
|
51
|
+
// DOM pass-through onto the real focusable control (objectui#3318).
|
|
52
|
+
{...toDomProps(props)}
|
|
50
53
|
type="text"
|
|
51
54
|
value={value || ''}
|
|
52
55
|
onChange={(e) => onChange(e.target.value)}
|
|
53
56
|
placeholder={config?.placeholder || 'Enter text for QR code'}
|
|
54
57
|
disabled={readonly || props.disabled}
|
|
55
58
|
className={props.className}
|
|
59
|
+
// AFTER the spread so this widget's own computation wins: `error`
|
|
60
|
+
// is the published validation slot (#3222), `!!undefined` →
|
|
61
|
+
// explicit "false".
|
|
62
|
+
aria-invalid={!!error}
|
|
56
63
|
/>
|
|
57
64
|
{value && (
|
|
58
65
|
<>
|
|
@@ -2,6 +2,9 @@ import React, { useId, useEffect } from 'react';
|
|
|
2
2
|
import { RadioGroup, RadioGroupItem, Label, EmptyValue } from '@object-ui/components';
|
|
3
3
|
import { isValueStillOffered, type OptionLike } from '@object-ui/core';
|
|
4
4
|
import { FieldWidgetComponentProps } from './types';
|
|
5
|
+
import { toDomProps } from './toDomProps';
|
|
6
|
+
import { toHostGroupProps } from './toHostGroupProps';
|
|
7
|
+
import { OptionsEmptyState } from './OptionsEmptyState';
|
|
5
8
|
import { useCascadingOptions } from './useCascadingOptions';
|
|
6
9
|
|
|
7
10
|
type Option = OptionLike;
|
|
@@ -24,14 +27,14 @@ export function RadioField({
|
|
|
24
27
|
field,
|
|
25
28
|
readonly,
|
|
26
29
|
className,
|
|
27
|
-
schema,
|
|
28
30
|
dependentValues,
|
|
29
31
|
dependsOn: dependsOnProp,
|
|
30
|
-
emptyHint
|
|
32
|
+
emptyHint,
|
|
31
33
|
dataSource: _dataSource,
|
|
34
|
+
error,
|
|
32
35
|
...props
|
|
33
36
|
}: FieldWidgetComponentProps<string>) {
|
|
34
|
-
const config =
|
|
37
|
+
const config = field as any;
|
|
35
38
|
const rawOptions: Option[] = config?.options || [];
|
|
36
39
|
const groupId = useId();
|
|
37
40
|
const fieldName = props.name || config?.name || props.id || '';
|
|
@@ -47,42 +50,81 @@ export function RadioField({
|
|
|
47
50
|
// (parent changed / predicate flipped), drop it so no stale pair persists.
|
|
48
51
|
useEffect(() => {
|
|
49
52
|
if (readonly) return;
|
|
53
|
+
// Never configured → nothing to prune against; see `MultiSelectField`'s
|
|
54
|
+
// copy of this guard for the measured failure (objectui#4220).
|
|
55
|
+
if (rawOptions.length === 0) return;
|
|
56
|
+
// Gated → the authored list is withheld until the `dependsOn` parent is
|
|
57
|
+
// chosen, so the empty offered set is missing information, not a verdict on
|
|
58
|
+
// the stored value; clearing here fired on mount (objectui#4247). Same
|
|
59
|
+
// reasoning, at length, in `MultiSelectField`.
|
|
60
|
+
if (gated) return;
|
|
50
61
|
if (value === undefined || value === null || (value as unknown) === '') return;
|
|
51
62
|
if (!isValueStillOffered(value, options)) onChange?.(undefined as unknown as string);
|
|
52
63
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
64
|
}, [options, gated]);
|
|
54
65
|
|
|
66
|
+
// The host's group label has to be consumed on EVERY surface this widget can
|
|
67
|
+
// render, not only the `RadioGroup` (objectui#3990): both branches below return
|
|
68
|
+
// before that spread, which is how a field-level `readonly: true` and a
|
|
69
|
+
// zero-option list ended up with a published label id and no consumer at all.
|
|
70
|
+
// See `toHostGroupProps` — note the role it emits is `group`, not the
|
|
71
|
+
// `radiogroup` the editable branch keeps: there is not one radio left in
|
|
72
|
+
// either of these surfaces.
|
|
73
|
+
const hostGroupProps = toHostGroupProps(props);
|
|
74
|
+
|
|
55
75
|
if (readonly) {
|
|
56
|
-
|
|
76
|
+
// `EmptyValue`'s own `aria-label` ("No value") is outranked by
|
|
77
|
+
// `aria-labelledby` per accname, and on the `generic` role that placeholder
|
|
78
|
+
// span carries it was never exposed as a name anyway.
|
|
79
|
+
if (value == null || value === '') return <EmptyValue {...hostGroupProps} />;
|
|
57
80
|
// Label from the raw set so a stored value hidden by `visibleWhen` still
|
|
58
81
|
// renders its label rather than a bare id.
|
|
59
82
|
const opt = rawOptions.find((o) => o.value === value);
|
|
60
|
-
return <span className="text-sm">{opt?.label || String(value)}</span>;
|
|
83
|
+
return <span {...hostGroupProps} className="text-sm">{opt?.label || String(value)}</span>;
|
|
61
84
|
}
|
|
62
85
|
|
|
63
86
|
// No offered options is unfillable — surface a legible state instead of an
|
|
64
|
-
// empty radio group:
|
|
65
|
-
//
|
|
87
|
+
// empty radio group: the host's `emptyHint` when it computed one, else this
|
|
88
|
+
// widget's own translated copy. Shared with the select / multiselect /
|
|
89
|
+
// checkboxes so the four cannot drift again (objectui#3231).
|
|
66
90
|
if (options.length === 0) {
|
|
67
|
-
const hint = gated
|
|
68
|
-
? `Select ${dependsOnFields.join(' / ')} first`
|
|
69
|
-
: 'No options available';
|
|
70
91
|
return (
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{
|
|
76
|
-
|
|
92
|
+
<OptionsEmptyState
|
|
93
|
+
emptyHint={emptyHint}
|
|
94
|
+
gated={gated}
|
|
95
|
+
dependsOnFields={dependsOnFields}
|
|
96
|
+
testId={fieldName ? `radio-empty-${fieldName}` : undefined}
|
|
97
|
+
className="min-h-9"
|
|
98
|
+
// This box IS the field in this state, so it is what the host label
|
|
99
|
+
// names (objectui#3990).
|
|
100
|
+
hostGroupProps={hostGroupProps}
|
|
101
|
+
/>
|
|
77
102
|
);
|
|
78
103
|
}
|
|
79
104
|
|
|
80
105
|
return (
|
|
106
|
+
// DOM pass-through onto the radiogroup (objectui#3318). It also carries the
|
|
107
|
+
// host's group label: when the form renderer associates its `<FormLabel>` by
|
|
108
|
+
// IDREF (`aria-labelledby`, objectui#3961) that key rides in this same
|
|
109
|
+
// whitelist spread — `toDomProps` forwards the whole `aria-` family — so this
|
|
110
|
+
// widget needs no branch of its own. `radiogroup` is already a member of the
|
|
111
|
+
// group role family, so nothing here overrides it with `role="group"`: it is
|
|
112
|
+
// the more specific role AND the correct carrier of `aria-invalid` below.
|
|
113
|
+
//
|
|
114
|
+
// Unlike Radix
|
|
115
|
+
// `Select.Root` (#3306) this Root IS a real DOM element — a
|
|
116
|
+
// `<div role="radiogroup">` — and `radiogroup` is exactly the role
|
|
117
|
+
// WAI-ARIA designates to carry `aria-invalid` for a set of radios
|
|
118
|
+
// (`radio` itself does not support it): the group's state is announced
|
|
119
|
+
// when focus lands on any radio inside it.
|
|
81
120
|
<RadioGroup
|
|
121
|
+
{...toDomProps(props)}
|
|
82
122
|
value={value ?? ''}
|
|
83
123
|
onValueChange={onChange}
|
|
84
124
|
disabled={props.disabled}
|
|
85
125
|
className={className}
|
|
126
|
+
// AFTER the spread so this widget's own computation wins (#3222).
|
|
127
|
+
aria-invalid={!!error}
|
|
86
128
|
>
|
|
87
129
|
{options.map((opt) => {
|
|
88
130
|
// Radix speaks strings — stringify the (possibly numeric,
|
|
@@ -2,14 +2,16 @@ import React from 'react';
|
|
|
2
2
|
import { Star } from 'lucide-react';
|
|
3
3
|
import { cn } from '@object-ui/components';
|
|
4
4
|
import { FieldWidgetComponentProps } from './types';
|
|
5
|
+
import { toDomProps } from './toDomProps';
|
|
6
|
+
import { toHostGroupProps } from './toHostGroupProps';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Rating field widget - provides a star rating input
|
|
8
10
|
* Supports numeric values from 0 to max (default 5)
|
|
9
11
|
*/
|
|
10
|
-
export function RatingField({ value, onChange, field, readonly, className, ...props }: FieldWidgetComponentProps<number>) {
|
|
12
|
+
export function RatingField({ value, onChange, field, readonly, className, error, ...props }: FieldWidgetComponentProps<number>) {
|
|
11
13
|
// Get rating-specific configuration from field metadata
|
|
12
|
-
const ratingField =
|
|
14
|
+
const ratingField = field as any;
|
|
13
15
|
const max = ratingField?.max ?? 5;
|
|
14
16
|
const currentValue = value ?? 0;
|
|
15
17
|
|
|
@@ -18,8 +20,11 @@ export function RatingField({ value, onChange, field, readonly, className, ...pr
|
|
|
18
20
|
const displayValue = hoverValue !== null ? hoverValue : currentValue;
|
|
19
21
|
|
|
20
22
|
if (readonly) {
|
|
23
|
+
// The readonly star row is the field's whole rendered surface, so it — not
|
|
24
|
+
// only the editable row below — consumes the host's group label
|
|
25
|
+
// (objectui#3990). See `toHostGroupProps`.
|
|
21
26
|
return (
|
|
22
|
-
<div className={cn("flex items-center gap-1", className)}>
|
|
27
|
+
<div {...toHostGroupProps(props)} className={cn("flex items-center gap-1", className)}>
|
|
23
28
|
{Array.from({ length: max }, (_, i) => (
|
|
24
29
|
<Star
|
|
25
30
|
key={i}
|
|
@@ -37,8 +42,30 @@ export function RatingField({ value, onChange, field, readonly, className, ...pr
|
|
|
37
42
|
);
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
// DOM pass-through (objectui#3318): the container carries the form
|
|
46
|
+
// renderer's id / aria-describedby, but NOT its `aria-invalid` — a plain
|
|
47
|
+
// wrapper div is not where assistive tech reads the invalid state. That
|
|
48
|
+
// state goes onto every focusable star button below, computed from the
|
|
49
|
+
// published `error` slot (#3222). `name` is withheld too: it is only
|
|
50
|
+
// DOM-legal on form controls, and on this div it is exactly the leak
|
|
51
|
+
// #3291 sweeps for.
|
|
52
|
+
const {
|
|
53
|
+
'aria-invalid': _hostAriaInvalid,
|
|
54
|
+
name: _domName,
|
|
55
|
+
...groupDomProps
|
|
56
|
+
} = toDomProps(props);
|
|
57
|
+
// When the host named this container by IDREF (`aria-labelledby`,
|
|
58
|
+
// objectui#3961) it IS the labelled group of stars, so it answers with the
|
|
59
|
+
// matching role; a `label for` pointing at this `div` was inert. Standalone
|
|
60
|
+
// (no host label) the markup is unchanged.
|
|
61
|
+
const isLabelledGroup = groupDomProps['aria-labelledby'] != null;
|
|
62
|
+
|
|
40
63
|
return (
|
|
41
|
-
<div
|
|
64
|
+
<div
|
|
65
|
+
{...groupDomProps}
|
|
66
|
+
role={isLabelledGroup ? 'group' : undefined}
|
|
67
|
+
className={cn("flex items-center gap-1", className)}
|
|
68
|
+
>
|
|
42
69
|
{Array.from({ length: max }, (_, i) => (
|
|
43
70
|
<button
|
|
44
71
|
key={i}
|
|
@@ -48,6 +75,7 @@ export function RatingField({ value, onChange, field, readonly, className, ...pr
|
|
|
48
75
|
onMouseLeave={() => setHoverValue(null)}
|
|
49
76
|
className="focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded"
|
|
50
77
|
disabled={readonly || props.disabled}
|
|
78
|
+
aria-invalid={!!error}
|
|
51
79
|
>
|
|
52
80
|
<Star
|
|
53
81
|
className={`w-5 h-5 transition-colors ${
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { Combobox, EmptyValue, cn } from '@object-ui/components';
|
|
3
3
|
import { SchemaRendererContext } from '@object-ui/react';
|
|
4
4
|
import type { FieldWidgetComponentProps } from './types';
|
|
5
|
+
import { toDomProps } from './toDomProps';
|
|
5
6
|
import { useFieldTranslation } from './useFieldTranslation';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -53,6 +54,7 @@ export function RecipientPickerField({
|
|
|
53
54
|
onChange,
|
|
54
55
|
readonly,
|
|
55
56
|
className,
|
|
57
|
+
error,
|
|
56
58
|
...props
|
|
57
59
|
}: FieldWidgetComponentProps<string>) {
|
|
58
60
|
const ctx = React.useContext(SchemaRendererContext);
|
|
@@ -133,6 +135,8 @@ export function RecipientPickerField({
|
|
|
133
135
|
// field is never un-editable.
|
|
134
136
|
return (
|
|
135
137
|
<input
|
|
138
|
+
// DOM pass-through onto the real focusable control (objectui#3318).
|
|
139
|
+
{...toDomProps(props)}
|
|
136
140
|
className={cn(
|
|
137
141
|
'w-full rounded border bg-background px-2 py-1 text-sm',
|
|
138
142
|
className,
|
|
@@ -140,6 +144,7 @@ export function RecipientPickerField({
|
|
|
140
144
|
value={value ?? ''}
|
|
141
145
|
disabled={disabled || readonly}
|
|
142
146
|
onChange={(e) => onChange(e.target.value as any)}
|
|
147
|
+
aria-invalid={!!error}
|
|
143
148
|
/>
|
|
144
149
|
);
|
|
145
150
|
}
|
|
@@ -149,8 +154,19 @@ export function RecipientPickerField({
|
|
|
149
154
|
return <span className={className}>{options.find((o) => o.value === value)?.label ?? value}</span>;
|
|
150
155
|
}
|
|
151
156
|
|
|
157
|
+
// DOM pass-through onto the combobox trigger — the widget's real focusable
|
|
158
|
+
// control (objectui#3318). `name` is withheld: the trigger is a button, not
|
|
159
|
+
// a submission control (same reasoning as #3306's SelectTrigger).
|
|
160
|
+
//
|
|
161
|
+
// NOTE this widget stays on the #3318 ledger regardless: its dependency-
|
|
162
|
+
// gated state (no `recipient_type` chosen yet — the state a fresh form and
|
|
163
|
+
// the registry sweep render) is a plain hint paragraph with no focusable
|
|
164
|
+
// control, so there is nothing there to carry the attribute.
|
|
165
|
+
const { name: _domName, ...triggerDomProps } = toDomProps(props);
|
|
166
|
+
|
|
152
167
|
return (
|
|
153
168
|
<Combobox
|
|
169
|
+
{...triggerDomProps}
|
|
154
170
|
options={options}
|
|
155
171
|
value={value ?? ''}
|
|
156
172
|
onValueChange={(v) => onChange(v as any)}
|
|
@@ -163,6 +179,8 @@ export function RecipientPickerField({
|
|
|
163
179
|
emptyText={records === null ? t('fields.recipient.loading') : t('fields.recipient.empty')}
|
|
164
180
|
disabled={disabled}
|
|
165
181
|
className={cn('w-full', className)}
|
|
182
|
+
// AFTER the spread so this widget's own computation wins (#3222).
|
|
183
|
+
aria-invalid={!!error}
|
|
166
184
|
/>
|
|
167
185
|
);
|
|
168
186
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* `RecordPickerDialog.baseFilter` accepts TWO shapes — objectui#3831.
|
|
11
|
+
*
|
|
12
|
+
* The slot was declared `Record< string, any >` and merged by object spread,
|
|
13
|
+
* which serves the dependent-lookup chain (#2215) exactly right and cannot
|
|
14
|
+
* carry a spec `ViewFilterRule[]` at all: TypeScript accepts an array there
|
|
15
|
+
* (an array satisfies a string index of `any`), the spread flattens it to
|
|
16
|
+
* `{"0": rule, "1": rule}`, and the query then filters on columns literally
|
|
17
|
+
* named `0`/`1` — green types, wrong query, no diagnostic. That is the slot
|
|
18
|
+
* `record:related_list.add.picker.filter` has to reach.
|
|
19
|
+
*
|
|
20
|
+
* So the merge now discriminates on shape, and both directions are pinned here:
|
|
21
|
+
*
|
|
22
|
+
* - **record form** keeps KEY-OVERWRITE precedence, byte for byte. A cascaded
|
|
23
|
+
* parent value must REPLACE a stale same-field `lookupFilters` entry, not
|
|
24
|
+
* intersect with it — `account = 'stale' AND account = 'a1'` returns nothing.
|
|
25
|
+
* (`LookupField.dependsOn.test.tsx` owns the #2215 behaviour end-to-end and
|
|
26
|
+
* is deliberately left untouched; this file pins the same precedence at the
|
|
27
|
+
* dialog's own boundary, so a future merge change cannot pass by only
|
|
28
|
+
* satisfying the array path.)
|
|
29
|
+
* - **rule array** lowers through `mergeFilterNodes`, the repo's single filter
|
|
30
|
+
* sink, so all 19 `VIEW_FILTER_OPERATORS` survive — including the four
|
|
31
|
+
* (`before`, `after`, `is_empty`, `is_not_empty`) the MongoDB-style record
|
|
32
|
+
* form has no `$op` for and which a hand-rolled second lowering would have
|
|
33
|
+
* had to drop or throw on.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { render, waitFor } from '@testing-library/react';
|
|
37
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
38
|
+
import { RecordPickerDialog } from './RecordPickerDialog';
|
|
39
|
+
|
|
40
|
+
const records = [{ id: 'r1', name: 'One' }];
|
|
41
|
+
|
|
42
|
+
function makeDataSource() {
|
|
43
|
+
const find = vi.fn(async () => ({ data: records, total: records.length }));
|
|
44
|
+
return { find } as any;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The `$filter` of the most recent query. */
|
|
48
|
+
function lastFilter(ds: any) {
|
|
49
|
+
const calls = ds.find.mock.calls;
|
|
50
|
+
return calls[calls.length - 1][1]?.$filter;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('RecordPickerDialog baseFilter — record form (#2215 precedence)', () => {
|
|
54
|
+
it('spreads a record baseFilter last, overwriting lookupFilters on the same field', async () => {
|
|
55
|
+
const ds = makeDataSource();
|
|
56
|
+
render(
|
|
57
|
+
<RecordPickerDialog
|
|
58
|
+
open
|
|
59
|
+
onOpenChange={vi.fn()}
|
|
60
|
+
dataSource={ds}
|
|
61
|
+
objectName="contacts"
|
|
62
|
+
onSelect={vi.fn()}
|
|
63
|
+
lookupFilters={[{ field: 'account', operator: 'eq', value: 'stale' }]}
|
|
64
|
+
baseFilter={{ account: 'a1' }}
|
|
65
|
+
/>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
await waitFor(() => expect(ds.find).toHaveBeenCalled());
|
|
69
|
+
// Still the plain record form — NOT lowered to an AST, and emphatically not
|
|
70
|
+
// an `and` of the stale value with the cascaded one.
|
|
71
|
+
expect(lastFilter(ds)).toEqual({ account: 'a1' });
|
|
72
|
+
expect(Array.isArray(lastFilter(ds))).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('sends no $filter at all when nothing is constrained', async () => {
|
|
76
|
+
const ds = makeDataSource();
|
|
77
|
+
render(
|
|
78
|
+
<RecordPickerDialog
|
|
79
|
+
open
|
|
80
|
+
onOpenChange={vi.fn()}
|
|
81
|
+
dataSource={ds}
|
|
82
|
+
objectName="contacts"
|
|
83
|
+
onSelect={vi.fn()}
|
|
84
|
+
/>,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
await waitFor(() => expect(ds.find).toHaveBeenCalled());
|
|
88
|
+
expect(lastFilter(ds)).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('RecordPickerDialog baseFilter — spec ViewFilterRule[] (#3831)', () => {
|
|
93
|
+
it('lowers a rule array to AST nodes instead of spreading it into index keys', async () => {
|
|
94
|
+
const ds = makeDataSource();
|
|
95
|
+
render(
|
|
96
|
+
<RecordPickerDialog
|
|
97
|
+
open
|
|
98
|
+
onOpenChange={vi.fn()}
|
|
99
|
+
dataSource={ds}
|
|
100
|
+
objectName="sys_position"
|
|
101
|
+
onSelect={vi.fn()}
|
|
102
|
+
baseFilter={[{ field: 'is_active', operator: 'equals', value: true }]}
|
|
103
|
+
/>,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
await waitFor(() => expect(ds.find).toHaveBeenCalled());
|
|
107
|
+
expect(lastFilter(ds)).toEqual([['is_active', 'equals', true]]);
|
|
108
|
+
// The old object-spread failure mode, pinned by structure rather than by
|
|
109
|
+
// key: the spread produced a PLAIN OBJECT whose values were still rule
|
|
110
|
+
// objects (`{"0": {field, operator, value}}`), so the query asked for a
|
|
111
|
+
// column named `0`. Asserting on `Object.keys` cannot tell the two apart —
|
|
112
|
+
// an array's keys are its indices too — so what is pinned is that each
|
|
113
|
+
// element is an AST TRIPLE and no longer carries a `field` member.
|
|
114
|
+
const filter = lastFilter(ds) as unknown[];
|
|
115
|
+
expect(Array.isArray(filter)).toBe(true);
|
|
116
|
+
expect(Array.isArray(filter[0])).toBe(true);
|
|
117
|
+
expect(filter[0]).not.toHaveProperty('field');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('preserves the four operators the record form cannot spell', async () => {
|
|
121
|
+
const ds = makeDataSource();
|
|
122
|
+
render(
|
|
123
|
+
<RecordPickerDialog
|
|
124
|
+
open
|
|
125
|
+
onOpenChange={vi.fn()}
|
|
126
|
+
dataSource={ds}
|
|
127
|
+
objectName="sys_license"
|
|
128
|
+
onSelect={vi.fn()}
|
|
129
|
+
baseFilter={[
|
|
130
|
+
{ field: 'starts_at', operator: 'before', value: '2026-01-01' },
|
|
131
|
+
{ field: 'expires_at', operator: 'after', value: '2026-01-01' },
|
|
132
|
+
{ field: 'revoked_at', operator: 'is_empty' },
|
|
133
|
+
{ field: 'assigned_to', operator: 'is_not_empty' },
|
|
134
|
+
]}
|
|
135
|
+
/>,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
await waitFor(() => expect(ds.find).toHaveBeenCalled());
|
|
139
|
+
expect(lastFilter(ds)).toEqual([
|
|
140
|
+
['starts_at', 'before', '2026-01-01'],
|
|
141
|
+
['expires_at', 'after', '2026-01-01'],
|
|
142
|
+
// A rule with no `value` stays two-element: inventing `null` here would be
|
|
143
|
+
// a real `{revoked_at: null}` predicate, i.e. a different question.
|
|
144
|
+
['revoked_at', 'is_empty'],
|
|
145
|
+
['assigned_to', 'is_not_empty'],
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('keeps a record baseFilter and a rule array as separate `and` children', async () => {
|
|
150
|
+
const ds = makeDataSource();
|
|
151
|
+
render(
|
|
152
|
+
<RecordPickerDialog
|
|
153
|
+
open
|
|
154
|
+
onOpenChange={vi.fn()}
|
|
155
|
+
dataSource={ds}
|
|
156
|
+
objectName="sys_position"
|
|
157
|
+
onSelect={vi.fn()}
|
|
158
|
+
// A rule array is the author's restriction; the record-form base still
|
|
159
|
+
// arrives via `lookupFilters` here, and the two must BOTH apply.
|
|
160
|
+
lookupFilters={[{ field: 'company', operator: 'eq', value: 'c1' }]}
|
|
161
|
+
baseFilter={[{ field: 'is_active', operator: 'equals', value: true }]}
|
|
162
|
+
/>,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
await waitFor(() => expect(ds.find).toHaveBeenCalled());
|
|
166
|
+
expect(lastFilter(ds)).toEqual([
|
|
167
|
+
'and',
|
|
168
|
+
['company', '=', 'c1'],
|
|
169
|
+
[['is_active', 'equals', true]],
|
|
170
|
+
]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('sends no $filter for an empty rule array', async () => {
|
|
174
|
+
const ds = makeDataSource();
|
|
175
|
+
render(
|
|
176
|
+
<RecordPickerDialog
|
|
177
|
+
open
|
|
178
|
+
onOpenChange={vi.fn()}
|
|
179
|
+
dataSource={ds}
|
|
180
|
+
objectName="sys_position"
|
|
181
|
+
onSelect={vi.fn()}
|
|
182
|
+
baseFilter={[]}
|
|
183
|
+
/>,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await waitFor(() => expect(ds.find).toHaveBeenCalled());
|
|
187
|
+
// An empty restriction is no restriction — not an empty `$filter` the wire
|
|
188
|
+
// has to interpret.
|
|
189
|
+
expect(lastFilter(ds)).toBeUndefined();
|
|
190
|
+
});
|
|
191
|
+
});
|