@object-ui/fields 17.5.0 → 17.7.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 +6150 -0
- package/README.md +108 -8
- package/dist/FieldEditWidget.d.ts +70 -8
- package/dist/coerceToSafeValue.d.ts +54 -0
- package/dist/currency.d.ts +1 -1
- package/dist/field-type-alias.d.ts +16 -0
- package/dist/index.cjs +5 -5
- package/dist/index.css +53 -11
- package/dist/index.d.ts +653 -128
- package/dist/index.js +4275 -3171
- package/dist/widgets/AddressField.d.ts +3 -3
- package/dist/widgets/AutoNumberField.d.ts +1 -1
- package/dist/widgets/AvatarField.d.ts +20 -4
- package/dist/widgets/BooleanField.d.ts +2 -2
- 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 +2 -2
- package/dist/widgets/DateTimeField.d.ts +2 -2
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +18 -2
- package/dist/widgets/FilterConditionField.d.ts +43 -2
- package/dist/widgets/FormulaField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
- package/dist/widgets/GeolocationField.d.ts +10 -4
- package/dist/widgets/GridField.d.ts +102 -86
- package/dist/widgets/ImageField.d.ts +1 -1
- package/dist/widgets/LocationField.d.ts +31 -4
- package/dist/widgets/LookupField.d.ts +9 -1
- package/dist/widgets/MasterDetailField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/NumberField.d.ts +2 -2
- package/dist/widgets/ObjectField.d.ts +10 -3
- package/dist/widgets/ObjectRefField.d.ts +1 -1
- package/dist/widgets/OptionsEmptyState.d.ts +38 -8
- package/dist/widgets/PasswordField.d.ts +15 -1
- package/dist/widgets/PeoplePicker.d.ts +17 -2
- package/dist/widgets/PercentField.d.ts +3 -2
- package/dist/widgets/PersonRow.d.ts +13 -2
- 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 +44 -1
- package/dist/widgets/RecordPickerDialog.d.ts +33 -16
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +1 -1
- package/dist/widgets/SelectionTray.d.ts +13 -2
- 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 +2 -2
- package/dist/widgets/TimeField.d.ts +2 -2
- 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/file-affordance.d.ts +33 -0
- package/dist/widgets/file-size-guard.d.ts +5 -23
- package/dist/widgets/file-value.d.ts +38 -9
- package/dist/widgets/lookupColumnDisplay.d.ts +110 -0
- package/dist/widgets/nativeDateValue.d.ts +9 -27
- package/dist/widgets/number-format.d.ts +137 -0
- package/dist/widgets/numberBadInput.d.ts +135 -0
- package/dist/widgets/personDisplay.d.ts +22 -4
- package/dist/widgets/richTextDisplay.d.ts +116 -0
- package/dist/widgets/toDomProps.d.ts +10 -1
- package/dist/widgets/toHostGroupProps.d.ts +172 -21
- package/dist/widgets/toHostProps.d.ts +105 -0
- package/dist/widgets/types.d.ts +19 -6
- package/dist/widgets/upload-error-message.d.ts +6 -0
- package/dist/widgets/uploadingScope.d.ts +142 -0
- package/dist/widgets/useCascadingOptions.d.ts +3 -2
- package/dist/widgets/useRecordQuery.d.ts +4 -2
- package/dist/widgets/useUploadingSignal.d.ts +8 -0
- package/package.json +11 -13
- 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
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Combobox, EmptyValue, cn } from '@object-ui/components';
|
|
3
|
-
import { SchemaRendererContext } from '@object-ui/react';
|
|
4
|
-
import type { FieldWidgetComponentProps } from './types';
|
|
5
|
-
import { toDomProps } from './toDomProps';
|
|
6
|
-
import { useFieldTranslation } from './useFieldTranslation';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* ObjectRefField — object-name picker for form fields that store the machine
|
|
10
|
-
* name of a registered object (e.g. `sys_sharing_rule.object_name`).
|
|
11
|
-
*
|
|
12
|
-
* Reached via the field `widget: 'object-ref'` hint (declared on the object
|
|
13
|
-
* metadata; resolves as `field:object-ref`). Renders a searchable dropdown of
|
|
14
|
-
* the platform's registered objects instead of a free-text input where the
|
|
15
|
-
* admin has to remember a machine name.
|
|
16
|
-
*
|
|
17
|
-
* Object catalog source, in order of preference:
|
|
18
|
-
* 1. `dataSource.getObjects()` — the metadata-registry list endpoint
|
|
19
|
-
* (`GET /api/v1/meta/object`), which merges code- and DB-defined objects.
|
|
20
|
-
* 2. Fallback: `dataSource.find('sys_metadata', { type: 'object' })` — the
|
|
21
|
-
* same source the Setup app's object grid browses.
|
|
22
|
-
* The stored value is always the object's `name` string. If the catalog can't
|
|
23
|
-
* load, the current value is still shown, so the control degrades safely.
|
|
24
|
-
*/
|
|
25
|
-
interface ObjectHeader {
|
|
26
|
-
name: string;
|
|
27
|
-
label?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function ObjectRefField({
|
|
31
|
-
value,
|
|
32
|
-
onChange,
|
|
33
|
-
readonly,
|
|
34
|
-
className,
|
|
35
|
-
error,
|
|
36
|
-
...props
|
|
37
|
-
}: FieldWidgetComponentProps<string>) {
|
|
38
|
-
const ctx = React.useContext(SchemaRendererContext);
|
|
39
|
-
const { t } = useFieldTranslation();
|
|
40
|
-
const dataSource: any = props.dataSource ?? (ctx as any)?.dataSource ?? null;
|
|
41
|
-
const disabled = props.disabled;
|
|
42
|
-
|
|
43
|
-
const [objects, setObjects] = React.useState<ObjectHeader[] | null>(null);
|
|
44
|
-
|
|
45
|
-
React.useEffect(() => {
|
|
46
|
-
if (!dataSource) return;
|
|
47
|
-
let cancelled = false;
|
|
48
|
-
(async () => {
|
|
49
|
-
try {
|
|
50
|
-
let rows: ObjectHeader[] = [];
|
|
51
|
-
if (typeof dataSource.getObjects === 'function') {
|
|
52
|
-
rows = (await dataSource.getObjects()) ?? [];
|
|
53
|
-
} else if (typeof dataSource.find === 'function') {
|
|
54
|
-
const res = await dataSource.find('sys_metadata', {
|
|
55
|
-
$filter: { type: 'object' },
|
|
56
|
-
$top: 1000,
|
|
57
|
-
});
|
|
58
|
-
const list: any[] =
|
|
59
|
-
res?.data ?? res?.records ?? (Array.isArray(res) ? res : []);
|
|
60
|
-
rows = list
|
|
61
|
-
.map((r) => ({
|
|
62
|
-
name: String(r?.name ?? ''),
|
|
63
|
-
label: r?.label != null ? String(r.label) : undefined,
|
|
64
|
-
}))
|
|
65
|
-
.filter((r) => r.name);
|
|
66
|
-
}
|
|
67
|
-
if (!cancelled) setObjects(rows);
|
|
68
|
-
} catch {
|
|
69
|
-
if (!cancelled) setObjects([]);
|
|
70
|
-
}
|
|
71
|
-
})();
|
|
72
|
-
return () => {
|
|
73
|
-
cancelled = true;
|
|
74
|
-
};
|
|
75
|
-
}, [dataSource]);
|
|
76
|
-
|
|
77
|
-
const options = React.useMemo(() => {
|
|
78
|
-
const opts = (objects ?? []).map((o) => ({
|
|
79
|
-
value: o.name,
|
|
80
|
-
label: o.label ? `${o.label} (${o.name})` : o.name,
|
|
81
|
-
}));
|
|
82
|
-
// Keep the current value selectable even if the catalog didn't include it
|
|
83
|
-
// (unknown / not-yet-published object), so editing never drops it.
|
|
84
|
-
if (value && !opts.some((o) => o.value === value)) {
|
|
85
|
-
opts.unshift({ value, label: value });
|
|
86
|
-
}
|
|
87
|
-
return opts.sort((a, b) => a.label.localeCompare(b.label));
|
|
88
|
-
}, [objects, value]);
|
|
89
|
-
|
|
90
|
-
if (readonly) {
|
|
91
|
-
if (!value) return <EmptyValue />;
|
|
92
|
-
const label = options.find((o) => o.value === value)?.label ?? value;
|
|
93
|
-
return <span className={className}>{label}</span>;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// DOM pass-through onto the combobox trigger — the widget's real focusable
|
|
97
|
-
// control (objectui#3318). `name` is withheld: the trigger is a button, not
|
|
98
|
-
// a submission control (same reasoning as #3306's SelectTrigger).
|
|
99
|
-
const { name: _domName, ...triggerDomProps } = toDomProps(props);
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<Combobox
|
|
103
|
-
{...triggerDomProps}
|
|
104
|
-
options={options}
|
|
105
|
-
value={value ?? ''}
|
|
106
|
-
onValueChange={(v) => onChange(v as any)}
|
|
107
|
-
placeholder={objects === null ? t('fields.objectRef.loading') : t('fields.objectRef.placeholder')}
|
|
108
|
-
searchPlaceholder={t('fields.objectRef.search')}
|
|
109
|
-
emptyText={objects === null ? t('common.loading') : t('fields.objectRef.empty')}
|
|
110
|
-
disabled={disabled}
|
|
111
|
-
// Fill the form column like every other input. Combobox's own
|
|
112
|
-
// `w-[200px]` is a component default that left this control stranded at
|
|
113
|
-
// a third of the row while `名称` beside it ran full width.
|
|
114
|
-
className={cn('w-full', className)}
|
|
115
|
-
// AFTER the spread so this widget's own computation wins (#3222).
|
|
116
|
-
aria-invalid={!!error}
|
|
117
|
-
/>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export default ObjectRefField;
|
|
@@ -1,88 +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
|
-
/**
|
|
10
|
-
* The option widgets' empty/gate copy with NO `I18nProvider` mounted
|
|
11
|
-
* (objectui#3231) — standalone/embedded usage, and every test in the repo that
|
|
12
|
-
* renders a field widget bare.
|
|
13
|
-
*
|
|
14
|
-
* Routing the copy through `useFieldTranslation()` must not turn it into a raw
|
|
15
|
-
* i18n key when nothing is configured: `createSafeTranslation` probes the
|
|
16
|
-
* instance and falls back to its English defaults map. That is the behaviour
|
|
17
|
-
* the removed literals used to provide unconditionally, so it gets its own pin.
|
|
18
|
-
*
|
|
19
|
-
* Why a separate FILE rather than another case in `OptionsEmptyState.test.tsx`:
|
|
20
|
-
* mounting `I18nProvider` calls `initReactI18next`, which installs that
|
|
21
|
-
* instance as react-i18next's GLOBAL default. Once any sibling test has done
|
|
22
|
-
* so there is no "no provider" state left to observe in that module graph, and
|
|
23
|
-
* the assertion would silently read the last locale mounted instead. Vitest's
|
|
24
|
-
* per-file isolation (`isolate: true` on the `dom` project) is what makes this
|
|
25
|
-
* file's observation honest.
|
|
26
|
-
*/
|
|
27
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
28
|
-
import React from 'react';
|
|
29
|
-
import { render, screen } from '@testing-library/react';
|
|
30
|
-
import '@testing-library/jest-dom';
|
|
31
|
-
import { SelectField } from './SelectField';
|
|
32
|
-
import { MultiSelectField } from './MultiSelectField';
|
|
33
|
-
import { RadioField } from './RadioField';
|
|
34
|
-
import { CheckboxesField } from './CheckboxesField';
|
|
35
|
-
|
|
36
|
-
const WIDGETS = [
|
|
37
|
-
{ label: 'SelectField', Widget: SelectField, type: 'select', testId: 'select-empty-province' },
|
|
38
|
-
{
|
|
39
|
-
label: 'MultiSelectField',
|
|
40
|
-
Widget: MultiSelectField,
|
|
41
|
-
type: 'multiselect',
|
|
42
|
-
testId: 'multiselect-empty-province',
|
|
43
|
-
},
|
|
44
|
-
{ label: 'RadioField', Widget: RadioField, type: 'radio', testId: 'radio-empty-province' },
|
|
45
|
-
{
|
|
46
|
-
label: 'CheckboxesField',
|
|
47
|
-
Widget: CheckboxesField,
|
|
48
|
-
type: 'checkboxes',
|
|
49
|
-
testId: 'checkboxes-empty-province',
|
|
50
|
-
},
|
|
51
|
-
] as const;
|
|
52
|
-
|
|
53
|
-
const gatedField = (type: string) =>
|
|
54
|
-
({
|
|
55
|
-
name: 'province',
|
|
56
|
-
type,
|
|
57
|
-
dependsOn: 'country',
|
|
58
|
-
options: [{ label: 'Zhejiang', value: 'zj', visibleWhen: "record.country == 'cn'" }],
|
|
59
|
-
}) as any;
|
|
60
|
-
|
|
61
|
-
const unconfiguredField = (type: string) => ({ name: 'province', type, options: [] }) as any;
|
|
62
|
-
|
|
63
|
-
function renderWidget(Widget: React.ComponentType<any>, field: any) {
|
|
64
|
-
return render(
|
|
65
|
-
<Widget
|
|
66
|
-
value={undefined}
|
|
67
|
-
onChange={vi.fn()}
|
|
68
|
-
field={field}
|
|
69
|
-
{...({ name: 'province', dependentValues: {} } as any)}
|
|
70
|
-
/>,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
describe('option widgets — English fallback survives with no i18n configured', () => {
|
|
75
|
-
it.each(WIDGETS)('$label renders the gate sentence, not a raw key', ({ Widget, type, testId }) => {
|
|
76
|
-
renderWidget(Widget, gatedField(type));
|
|
77
|
-
const box = screen.getByTestId(testId);
|
|
78
|
-
expect(box).toHaveTextContent('Select country first');
|
|
79
|
-
expect(box.textContent).not.toContain('fields.options');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it.each(WIDGETS)('$label renders the empty sentence, not a raw key', ({ Widget, type, testId }) => {
|
|
83
|
-
renderWidget(Widget, unconfiguredField(type));
|
|
84
|
-
const box = screen.getByTestId(testId);
|
|
85
|
-
expect(box).toHaveTextContent('No options available');
|
|
86
|
-
expect(box.textContent).not.toContain('fields.options');
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,169 +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
|
-
/**
|
|
10
|
-
* The empty / dependency-gated state of the fixed-option widgets (objectui#3231).
|
|
11
|
-
*
|
|
12
|
-
* Two invariants, pinned for ALL FOUR widgets — they are what regressed:
|
|
13
|
-
*
|
|
14
|
-
* 1. **declared ⇒ delivered.** `emptyHint` is declared on
|
|
15
|
-
* `FieldWidgetComponentProps` and computed by the form renderer. Every one
|
|
16
|
-
* of the four widgets used to destructure it into `_emptyHint` and drop it
|
|
17
|
-
* on the floor, so the host's hint could never reach a user once the field
|
|
18
|
-
* went through the `field:` registry (i.e. always, in the real console).
|
|
19
|
-
* 2. **the fallback is translated.** With no host hint the widget renders its
|
|
20
|
-
* own copy — which used to be a hardcoded English literal
|
|
21
|
-
* (`'No options available'`, `` `Select ${…} first` ``) even though
|
|
22
|
-
* `useFieldTranslation()` was already imported in one of the same files.
|
|
23
|
-
*
|
|
24
|
-
* All four are covered deliberately: they were four independent copies of the
|
|
25
|
-
* same block, which is exactly why they drifted together and why one widget's
|
|
26
|
-
* test would have proven nothing about the other three. They now share
|
|
27
|
-
* `OptionsEmptyState`; these tests keep that true from the outside, so the
|
|
28
|
-
* guarantee survives someone re-inlining the block.
|
|
29
|
-
*/
|
|
30
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
31
|
-
import React from 'react';
|
|
32
|
-
import { render, screen } from '@testing-library/react';
|
|
33
|
-
import '@testing-library/jest-dom';
|
|
34
|
-
import { I18nProvider } from '@object-ui/i18n';
|
|
35
|
-
import { SelectField } from './SelectField';
|
|
36
|
-
import { MultiSelectField } from './MultiSelectField';
|
|
37
|
-
import { RadioField } from './RadioField';
|
|
38
|
-
import { CheckboxesField } from './CheckboxesField';
|
|
39
|
-
|
|
40
|
-
interface WidgetCase {
|
|
41
|
-
label: string;
|
|
42
|
-
Widget: React.ComponentType<any>;
|
|
43
|
-
type: string;
|
|
44
|
-
testId: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const WIDGETS: WidgetCase[] = [
|
|
48
|
-
{ label: 'SelectField', Widget: SelectField, type: 'select', testId: 'select-empty-province' },
|
|
49
|
-
{
|
|
50
|
-
label: 'MultiSelectField',
|
|
51
|
-
Widget: MultiSelectField,
|
|
52
|
-
type: 'multiselect',
|
|
53
|
-
testId: 'multiselect-empty-province',
|
|
54
|
-
},
|
|
55
|
-
{ label: 'RadioField', Widget: RadioField, type: 'radio', testId: 'radio-empty-province' },
|
|
56
|
-
{
|
|
57
|
-
label: 'CheckboxesField',
|
|
58
|
-
Widget: CheckboxesField,
|
|
59
|
-
type: 'checkboxes',
|
|
60
|
-
testId: 'checkboxes-empty-province',
|
|
61
|
-
},
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
/** Gated: `country` is empty, so no option is offered yet (#2284). */
|
|
65
|
-
const gatedField = (type: string) =>
|
|
66
|
-
({
|
|
67
|
-
name: 'province',
|
|
68
|
-
type,
|
|
69
|
-
dependsOn: 'country',
|
|
70
|
-
options: [
|
|
71
|
-
{ label: 'Zhejiang', value: 'zj', visibleWhen: "record.country == 'cn'" },
|
|
72
|
-
{ label: 'California', value: 'ca', visibleWhen: "record.country == 'us'" },
|
|
73
|
-
],
|
|
74
|
-
}) as any;
|
|
75
|
-
|
|
76
|
-
/** Not gated, simply nothing to offer — the other way a list ends up empty. */
|
|
77
|
-
const unconfiguredField = (type: string) => ({ name: 'province', type, options: [] }) as any;
|
|
78
|
-
|
|
79
|
-
function renderWidget(
|
|
80
|
-
{ Widget }: WidgetCase,
|
|
81
|
-
field: any,
|
|
82
|
-
extra: Record<string, unknown> = {},
|
|
83
|
-
language?: string,
|
|
84
|
-
) {
|
|
85
|
-
const element = (
|
|
86
|
-
<Widget
|
|
87
|
-
value={undefined}
|
|
88
|
-
onChange={vi.fn()}
|
|
89
|
-
field={field}
|
|
90
|
-
{...({ name: 'province', dependentValues: {}, ...extra } as any)}
|
|
91
|
-
/>
|
|
92
|
-
);
|
|
93
|
-
return render(
|
|
94
|
-
language ? (
|
|
95
|
-
<I18nProvider config={{ defaultLanguage: language, detectBrowserLanguage: false }}>
|
|
96
|
-
{element}
|
|
97
|
-
</I18nProvider>
|
|
98
|
-
) : (
|
|
99
|
-
element
|
|
100
|
-
),
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
describe('option widgets — the host-supplied `emptyHint` reaches the user (objectui#3231)', () => {
|
|
105
|
-
it.each(WIDGETS)(
|
|
106
|
-
'$label renders the host hint instead of its own gate copy',
|
|
107
|
-
(widget) => {
|
|
108
|
-
// What the form renderer actually computes: a sentence built from the
|
|
109
|
-
// controlling field's human LABEL, which the widget cannot know (its
|
|
110
|
-
// metadata only carries the name `country`).
|
|
111
|
-
renderWidget(widget, gatedField(widget.type), {
|
|
112
|
-
emptyHint: 'Choose the Country field first',
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const box = screen.getByTestId(widget.testId);
|
|
116
|
-
expect(box).toHaveTextContent('Choose the Country field first');
|
|
117
|
-
// …and NOT the widget's own copy, which is what used to render.
|
|
118
|
-
expect(box.textContent).not.toMatch(/select country first/i);
|
|
119
|
-
},
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
it.each(WIDGETS)(
|
|
123
|
-
'$label renders the host hint for an unconfigured (ungated) list too',
|
|
124
|
-
(widget) => {
|
|
125
|
-
// `emptyHint` is a declared prop of the widget contract, not a private
|
|
126
|
-
// channel of one form-renderer branch: supplied ⇒ used, always.
|
|
127
|
-
renderWidget(widget, unconfiguredField(widget.type), {
|
|
128
|
-
emptyHint: 'Options come from the price book',
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
expect(screen.getByTestId(widget.testId)).toHaveTextContent(
|
|
132
|
-
'Options come from the price book',
|
|
133
|
-
);
|
|
134
|
-
},
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
it.each(WIDGETS)('$label prefers a host hint over its translated fallback', (widget) => {
|
|
138
|
-
// Even under a non-English locale the HOST wins — it is the single source.
|
|
139
|
-
renderWidget(widget, gatedField(widget.type), { emptyHint: 'Pick a Country first' }, 'zh');
|
|
140
|
-
|
|
141
|
-
const box = screen.getByTestId(widget.testId);
|
|
142
|
-
expect(box).toHaveTextContent('Pick a Country first');
|
|
143
|
-
expect(box.textContent).not.toContain('请先选择');
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe('option widgets — the fallback copy is translated, never a hardcoded literal', () => {
|
|
148
|
-
it.each(WIDGETS)('$label translates the gate copy when no host hint is given', (widget) => {
|
|
149
|
-
renderWidget(widget, gatedField(widget.type), {}, 'zh');
|
|
150
|
-
|
|
151
|
-
const box = screen.getByTestId(widget.testId);
|
|
152
|
-
expect(box).toHaveTextContent('请先选择country');
|
|
153
|
-
expect(box.textContent).not.toMatch(/select .* first/i);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it.each(WIDGETS)('$label translates the empty copy when no host hint is given', (widget) => {
|
|
157
|
-
renderWidget(widget, unconfiguredField(widget.type), {}, 'zh');
|
|
158
|
-
|
|
159
|
-
const box = screen.getByTestId(widget.testId);
|
|
160
|
-
expect(box).toHaveTextContent('暂无可选项');
|
|
161
|
-
expect(box.textContent).not.toMatch(/no options available/i);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// The no-provider fallback lives in `OptionsEmptyState.no-provider.test.tsx`:
|
|
166
|
-
// mounting `I18nProvider` here installs its instance as react-i18next's global
|
|
167
|
-
// default (`initReactI18next`), so after any test above there is no longer a
|
|
168
|
-
// "no provider" state to observe within this module graph.
|
|
169
|
-
|
|
@@ -1,99 +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 React from 'react';
|
|
10
|
-
import { cn } from '@object-ui/components';
|
|
11
|
-
import { useFieldTranslation } from './useFieldTranslation';
|
|
12
|
-
import type { HostGroupProps } from './toHostGroupProps';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The "this option list cannot be filled" state shared by every fixed-option
|
|
16
|
-
* widget (`SelectField` single, `MultiSelectField`, `RadioField`,
|
|
17
|
-
* `CheckboxesField`) — objectui#3231.
|
|
18
|
-
*
|
|
19
|
-
* ## One producer, one consumer
|
|
20
|
-
*
|
|
21
|
-
* `emptyHint` is a declared prop on `FieldWidgetComponentProps`, computed by
|
|
22
|
-
* the form renderer (`form.tsx`, the `#2284` dependency gate) and forwarded to
|
|
23
|
-
* these widgets. **When the host supplies it, it wins**: the host resolves the
|
|
24
|
-
* controlling fields to their human LABELS ("Select Country first"), where a
|
|
25
|
-
* widget on its own only knows the raw metadata names ("Select country first").
|
|
26
|
-
* Absent a host hint — a standalone widget, the inline grid editor, an action
|
|
27
|
-
* param dialog — the widget falls back to its own copy.
|
|
28
|
-
*
|
|
29
|
-
* Each of the four widgets used to inline this box, destructure `emptyHint`
|
|
30
|
-
* into `_emptyHint` and throw it away, then render a hardcoded English literal.
|
|
31
|
-
* Four independent copies is precisely why they drifted together, so the box
|
|
32
|
-
* AND the copy live here only: adding a fifth option widget cannot re-introduce
|
|
33
|
-
* the gap without deliberately re-implementing it.
|
|
34
|
-
*
|
|
35
|
-
* ## Fallback copy is translated
|
|
36
|
-
*
|
|
37
|
-
* The fallback goes through `useFieldTranslation()` (`fields.options.*`,
|
|
38
|
-
* present in all ten locale packs) rather than an English string literal — the
|
|
39
|
-
* gate sentence is the SAME i18n key the form renderer uses, so the two can
|
|
40
|
-
* never say different things in the same locale.
|
|
41
|
-
*/
|
|
42
|
-
export interface OptionsEmptyStateProps {
|
|
43
|
-
/**
|
|
44
|
-
* Host-computed hint. Wins over the widget's own copy whenever it is a
|
|
45
|
-
* non-empty string — this is the whole point of the prop.
|
|
46
|
-
*/
|
|
47
|
-
emptyHint?: string;
|
|
48
|
-
/** The list is waiting on a `dependsOn` controlling field (vs. unconfigured). */
|
|
49
|
-
gated: boolean;
|
|
50
|
-
/** Controlling field names, used by the fallback gate sentence. */
|
|
51
|
-
dependsOnFields: readonly string[];
|
|
52
|
-
/** Widget-specific stable locator, e.g. `select-empty-${fieldName}`. */
|
|
53
|
-
testId?: string;
|
|
54
|
-
/** Widget-specific sizing (the dropdown is a fixed `h-9`, lists grow). */
|
|
55
|
-
className?: string;
|
|
56
|
-
/**
|
|
57
|
-
* The host label's IDREF plumbing, when this box is the whole rendered surface
|
|
58
|
-
* of a group-labelled field (objectui#3990) — a `checkboxes` / `radio` /
|
|
59
|
-
* `multiselect` whose option list came back empty renders NOTHING but this
|
|
60
|
-
* box, so the visible label named zero elements until it landed here.
|
|
61
|
-
*
|
|
62
|
-
* A closed three-key type rather than an open props tail: only `id`,
|
|
63
|
-
* `aria-labelledby` and the `role` that makes them meaningful may reach this
|
|
64
|
-
* element, and reopening a spread is what objectui#3291 exists to prevent.
|
|
65
|
-
* Absent for the single `SelectField` — it is not group-labelled, its label
|
|
66
|
-
* still uses a plain `for`, and it must keep emitting no such attributes.
|
|
67
|
-
*/
|
|
68
|
-
hostGroupProps?: HostGroupProps;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function OptionsEmptyState({
|
|
72
|
-
emptyHint,
|
|
73
|
-
gated,
|
|
74
|
-
dependsOnFields,
|
|
75
|
-
testId,
|
|
76
|
-
className,
|
|
77
|
-
hostGroupProps,
|
|
78
|
-
}: OptionsEmptyStateProps) {
|
|
79
|
-
const { t } = useFieldTranslation();
|
|
80
|
-
// The host's hint when it computed one; otherwise this widget's own copy,
|
|
81
|
-
// translated. Never an English literal — that was the reported defect.
|
|
82
|
-
const hint =
|
|
83
|
-
emptyHint ||
|
|
84
|
-
(gated
|
|
85
|
-
? t('fields.options.selectFirst', { fields: dependsOnFields.join(' / ') })
|
|
86
|
-
: t('fields.options.empty'));
|
|
87
|
-
return (
|
|
88
|
-
<div
|
|
89
|
-
{...hostGroupProps}
|
|
90
|
-
data-testid={testId}
|
|
91
|
-
className={cn(
|
|
92
|
-
'flex w-full items-center rounded-md border border-input bg-muted/30 px-3 py-2 text-sm text-muted-foreground',
|
|
93
|
-
className,
|
|
94
|
-
)}
|
|
95
|
-
>
|
|
96
|
-
{hint}
|
|
97
|
-
</div>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { Input } from '@object-ui/components';
|
|
3
|
-
import { Button } from '@object-ui/components';
|
|
4
|
-
import { Eye, EyeOff } from 'lucide-react';
|
|
5
|
-
import { FieldWidgetComponentProps } from './types';
|
|
6
|
-
import { toDomProps } from './toDomProps';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* PasswordField - Secure password input with visibility toggle
|
|
10
|
-
* Includes show/hide toggle button and masks value in readonly mode
|
|
11
|
-
*/
|
|
12
|
-
export function PasswordField({ value, onChange, field, readonly, className, ...props }: FieldWidgetComponentProps<string>) {
|
|
13
|
-
const [showPassword, setShowPassword] = useState(false);
|
|
14
|
-
const config = field;
|
|
15
|
-
|
|
16
|
-
if (readonly) {
|
|
17
|
-
return <span className="text-sm">••••••••</span>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const domProps = toDomProps(props);
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<div className="relative">
|
|
24
|
-
<Input
|
|
25
|
-
{...domProps}
|
|
26
|
-
type={showPassword ? 'text' : 'password'}
|
|
27
|
-
value={value || ''}
|
|
28
|
-
onChange={(e) => onChange(e.target.value)}
|
|
29
|
-
placeholder={config?.placeholder}
|
|
30
|
-
disabled={readonly || domProps.disabled}
|
|
31
|
-
className={`pr-10 ${className || ''}`}
|
|
32
|
-
/>
|
|
33
|
-
<Button
|
|
34
|
-
type="button"
|
|
35
|
-
variant="ghost"
|
|
36
|
-
size="sm"
|
|
37
|
-
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
|
38
|
-
onClick={() => setShowPassword(!showPassword)}
|
|
39
|
-
tabIndex={-1}
|
|
40
|
-
>
|
|
41
|
-
{showPassword ? (
|
|
42
|
-
<EyeOff className="size-4 text-gray-500" />
|
|
43
|
-
) : (
|
|
44
|
-
<Eye className="size-4 text-gray-500" />
|
|
45
|
-
)}
|
|
46
|
-
</Button>
|
|
47
|
-
</div>
|
|
48
|
-
);
|
|
49
|
-
}
|