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