@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.
Files changed (229) hide show
  1. package/CHANGELOG.md +714 -0
  2. package/dist/FieldEditWidget.d.ts +16 -2
  3. package/dist/field-type-alias.d.ts +16 -0
  4. package/dist/index.cjs +4 -4
  5. package/dist/index.css +27 -9
  6. package/dist/index.d.ts +425 -72
  7. package/dist/index.js +2530 -2228
  8. package/dist/widgets/AddressField.d.ts +3 -3
  9. package/dist/widgets/AutoNumberField.d.ts +1 -1
  10. package/dist/widgets/AvatarField.d.ts +1 -1
  11. package/dist/widgets/BooleanField.d.ts +1 -1
  12. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  13. package/dist/widgets/CheckboxesField.d.ts +1 -1
  14. package/dist/widgets/CodeField.d.ts +1 -1
  15. package/dist/widgets/ColorField.d.ts +1 -1
  16. package/dist/widgets/CurrencyField.d.ts +1 -1
  17. package/dist/widgets/DateField.d.ts +1 -1
  18. package/dist/widgets/DateTimeField.d.ts +1 -1
  19. package/dist/widgets/EmailField.d.ts +1 -1
  20. package/dist/widgets/FileField.d.ts +18 -2
  21. package/dist/widgets/FilterConditionField.d.ts +28 -1
  22. package/dist/widgets/FormulaField.d.ts +1 -1
  23. package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
  24. package/dist/widgets/GeolocationField.d.ts +1 -1
  25. package/dist/widgets/GridField.d.ts +56 -4
  26. package/dist/widgets/ImageField.d.ts +1 -1
  27. package/dist/widgets/LocationField.d.ts +1 -1
  28. package/dist/widgets/LookupField.d.ts +1 -1
  29. package/dist/widgets/MasterDetailField.d.ts +1 -1
  30. package/dist/widgets/MultiSelectField.d.ts +1 -1
  31. package/dist/widgets/NumberField.d.ts +1 -1
  32. package/dist/widgets/ObjectField.d.ts +1 -1
  33. package/dist/widgets/ObjectRefField.d.ts +1 -1
  34. package/dist/widgets/OptionsEmptyState.d.ts +13 -7
  35. package/dist/widgets/PasswordField.d.ts +1 -1
  36. package/dist/widgets/PercentField.d.ts +1 -1
  37. package/dist/widgets/PhoneField.d.ts +1 -1
  38. package/dist/widgets/QRCodeField.d.ts +1 -1
  39. package/dist/widgets/RadioField.d.ts +1 -1
  40. package/dist/widgets/RatingField.d.ts +1 -1
  41. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  42. package/dist/widgets/RecordPickerDialog.d.ts +2 -4
  43. package/dist/widgets/RichTextField.d.ts +37 -1
  44. package/dist/widgets/SelectField.d.ts +1 -1
  45. package/dist/widgets/SignatureField.d.ts +32 -2
  46. package/dist/widgets/SliderField.d.ts +37 -2
  47. package/dist/widgets/SummaryField.d.ts +1 -1
  48. package/dist/widgets/TagsField.d.ts +1 -1
  49. package/dist/widgets/TextAreaField.d.ts +6 -2
  50. package/dist/widgets/TextField.d.ts +1 -1
  51. package/dist/widgets/TimeField.d.ts +1 -1
  52. package/dist/widgets/UrlField.d.ts +1 -1
  53. package/dist/widgets/UserField.d.ts +1 -1
  54. package/dist/widgets/VectorField.d.ts +1 -1
  55. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  56. package/dist/widgets/address-format.d.ts +27 -7
  57. package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
  58. package/dist/widgets/richTextDisplay.d.ts +80 -0
  59. package/dist/widgets/toHostGroupProps.d.ts +119 -21
  60. package/dist/widgets/types.d.ts +8 -3
  61. package/package.json +9 -10
  62. package/dist/widgets/CharacterCount.d.ts +0 -56
  63. package/src/FieldEditWidget.test.ts +0 -133
  64. package/src/FieldEditWidget.tsx +0 -207
  65. package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
  66. package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
  67. package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
  68. package/src/__tests__/DateCellRenderer.test.tsx +0 -82
  69. package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
  70. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  71. package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
  72. package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
  73. package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
  74. package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
  75. package/src/__tests__/cell-truncation.test.tsx +0 -96
  76. package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
  77. package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
  78. package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
  79. package/src/__tests__/date-locale-channel.test.tsx +0 -305
  80. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  81. package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
  82. package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
  83. package/src/__tests__/group-labelling-declaration.test.ts +0 -148
  84. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  85. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  86. package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
  87. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  88. package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
  89. package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
  90. package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
  91. package/src/__tests__/registry-collision.test.ts +0 -91
  92. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  93. package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
  94. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  95. package/src/__tests__/validation-feedback.test.tsx +0 -361
  96. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  97. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  98. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
  99. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  100. package/src/coerce-safe-value.test.ts +0 -34
  101. package/src/complex-widgets.test.tsx +0 -924
  102. package/src/currency.ts +0 -144
  103. package/src/datetime-widgets.test.tsx +0 -185
  104. package/src/field-type-alias.multiple.test.ts +0 -81
  105. package/src/field-type-alias.ts +0 -156
  106. package/src/field-type-coverage.test.ts +0 -67
  107. package/src/index.css +0 -44
  108. package/src/index.tsx +0 -2745
  109. package/src/standard-widgets.test.tsx +0 -364
  110. package/src/widgets/AddressField.postalCode.test.tsx +0 -257
  111. package/src/widgets/AddressField.tsx +0 -182
  112. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
  113. package/src/widgets/AutoNumberField.tsx +0 -18
  114. package/src/widgets/AvatarField.tsx +0 -117
  115. package/src/widgets/BooleanField.tsx +0 -86
  116. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  117. package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
  118. package/src/widgets/CharacterCount.tsx +0 -263
  119. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  120. package/src/widgets/CheckboxesField.tsx +0 -169
  121. package/src/widgets/CodeField.tsx +0 -41
  122. package/src/widgets/ColorField.tsx +0 -53
  123. package/src/widgets/CurrencyField.tsx +0 -135
  124. package/src/widgets/DateField.tsx +0 -40
  125. package/src/widgets/DateTimeField.tsx +0 -46
  126. package/src/widgets/EmailField.tsx +0 -37
  127. package/src/widgets/FileField.tsx +0 -512
  128. package/src/widgets/FilterConditionField.tsx +0 -454
  129. package/src/widgets/FormulaField.tsx +0 -38
  130. package/src/widgets/FullscreenFieldEditor.tsx +0 -60
  131. package/src/widgets/GeolocationField.tsx +0 -193
  132. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  133. package/src/widgets/GridField.test.tsx +0 -632
  134. package/src/widgets/GridField.tsx +0 -1070
  135. package/src/widgets/ImageCropperDialog.tsx +0 -245
  136. package/src/widgets/ImageField.maxSize.test.tsx +0 -154
  137. package/src/widgets/ImageField.tsx +0 -305
  138. package/src/widgets/ImageLightbox.test.tsx +0 -52
  139. package/src/widgets/ImageLightbox.tsx +0 -108
  140. package/src/widgets/LocationField.tsx +0 -57
  141. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  142. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  143. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  144. package/src/widgets/LookupField.idField.test.tsx +0 -61
  145. package/src/widgets/LookupField.tsx +0 -1295
  146. package/src/widgets/MarkdownContent.tsx +0 -19
  147. package/src/widgets/MasterDetailField.tsx +0 -131
  148. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  149. package/src/widgets/MultiSelectField.tsx +0 -196
  150. package/src/widgets/NumberField.tsx +0 -49
  151. package/src/widgets/ObjectField.tsx +0 -94
  152. package/src/widgets/ObjectRefField.tsx +0 -121
  153. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  154. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  155. package/src/widgets/OptionsEmptyState.tsx +0 -99
  156. package/src/widgets/PasswordField.tsx +0 -49
  157. package/src/widgets/PeoplePicker.test.tsx +0 -360
  158. package/src/widgets/PeoplePicker.tsx +0 -579
  159. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  160. package/src/widgets/PercentField.tsx +0 -95
  161. package/src/widgets/PersonRow.tsx +0 -117
  162. package/src/widgets/PhoneField.tsx +0 -37
  163. package/src/widgets/QRCodeField.tsx +0 -103
  164. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  165. package/src/widgets/RadioField.tsx +0 -143
  166. package/src/widgets/RatingField.tsx +0 -94
  167. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  168. package/src/widgets/RecipientPickerField.tsx +0 -188
  169. package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
  170. package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
  171. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  172. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  173. package/src/widgets/RecordPickerDialog.tsx +0 -1365
  174. package/src/widgets/RichTextField.tsx +0 -176
  175. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  176. package/src/widgets/SelectField.tsx +0 -193
  177. package/src/widgets/SelectionTray.tsx +0 -110
  178. package/src/widgets/SignatureField.tsx +0 -140
  179. package/src/widgets/SliderField.tsx +0 -39
  180. package/src/widgets/SummaryField.tsx +0 -31
  181. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  182. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  183. package/src/widgets/TagsField.tsx +0 -88
  184. package/src/widgets/TextAreaField.tsx +0 -213
  185. package/src/widgets/TextField.tsx +0 -45
  186. package/src/widgets/TimeField.tsx +0 -31
  187. package/src/widgets/UrlField.tsx +0 -46
  188. package/src/widgets/UserField.tsx +0 -59
  189. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  190. package/src/widgets/VectorField.tsx +0 -29
  191. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  192. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
  193. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  194. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  195. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  196. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  197. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  198. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  199. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  200. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  201. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  202. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  203. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  204. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  205. package/src/widgets/address-format.ts +0 -120
  206. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  207. package/src/widgets/deriveLookupColumns.ts +0 -108
  208. package/src/widgets/file-size-guard.ts +0 -55
  209. package/src/widgets/file-value.test.ts +0 -230
  210. package/src/widgets/file-value.ts +0 -246
  211. package/src/widgets/nativeDateValue.test.ts +0 -115
  212. package/src/widgets/nativeDateValue.ts +0 -112
  213. package/src/widgets/openNativePicker.ts +0 -18
  214. package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
  215. package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
  216. package/src/widgets/personDisplay.test.ts +0 -113
  217. package/src/widgets/personDisplay.ts +0 -92
  218. package/src/widgets/recentLookups.test.ts +0 -45
  219. package/src/widgets/recentLookups.ts +0 -47
  220. package/src/widgets/toDomProps.ts +0 -204
  221. package/src/widgets/toHostGroupProps.ts +0 -102
  222. package/src/widgets/types.ts +0 -242
  223. package/src/widgets/useCascadingOptions.ts +0 -46
  224. package/src/widgets/useFieldTranslation.ts +0 -132
  225. package/src/widgets/useRecordQuery.test.ts +0 -174
  226. package/src/widgets/useRecordQuery.ts +0 -291
  227. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  228. package/src/widgets/useUploadingSignal.ts +0 -20
  229. 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
- }