@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,611 +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
- * Record Picker FILTER PANEL — select filter inputs carry the schema's options
11
- * (#3336).
12
- *
13
- * #3333 fixed the picker's table CELLS: they now format through the referenced
14
- * object's schema `fields` map (`fieldsMeta`) plus the shared i18n option
15
- * translation, so a `select` column renders the authored option label instead
16
- * of title-casing the raw stored value.
17
- *
18
- * The filter panel is the other face of the same widget and was still broken.
19
- * `LookupField` turns each typed picker column into a `RecordPickerFilterColumn`
20
- * (`{ field, label, type: 'select' }`) with no `options`, and the panel's select
21
- * input renders `col.options?.map(...)` — so the dropdown opened EMPTY and the
22
- * column could not be filtered at all.
23
- *
24
- * ## What these cases assert, and in which direction
25
- *
26
- * - **The two reproducers (`renderFilterBar` slot + built-in panel DOM) were
27
- * RED before the fix**: `options` was `undefined` and the dropdown rendered
28
- * zero `option` roles.
29
- * - **The parity case is the load-bearing one.** It asserts the filter option
30
- * label and the table cell for the SAME field are the same translated string,
31
- * under an `I18nProvider` that translates `fieldOptions.<obj>.<field>.<value>`.
32
- * That is what pins "one source" rather than "two derivations that happen to
33
- * agree today" — a second, filter-only derivation reading `meta.options`
34
- * directly would satisfy the label assertions but not this one.
35
- * - **Two cases are deliberate NO-OP pins, green before and after** (called out
36
- * individually below): a schema field with no `options` still yields an empty
37
- * dropdown (the fix adds no consumer-side leniency — no options synthesised
38
- * from the loaded page's raw values), and explicitly authored filter options
39
- * keep winning over the schema (including the `in`-operator options
40
- * auto-derived from `lookupFilters`). They guard the precedence and the
41
- * non-leniency the fix chose, not a defect it repaired.
42
- *
43
- * The second suite in this file covers #3422 — what the panel DOWNLOADS once an
44
- * option is picked. Having the right options to choose from (#3336) and sending
45
- * the right value for the chosen one are the two halves of the same control, so
46
- * they are pinned side by side; see that suite's own header for its directions.
47
- */
48
-
49
- import React from 'react';
50
- import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
51
- import { describe, it, expect, vi, beforeAll } from 'vitest';
52
- import { I18nProvider } from '@object-ui/i18n';
53
- import { RecordPickerDialog } from './RecordPickerDialog';
54
- import type { RecordPickerFilterBarProps } from './RecordPickerDialog';
55
- import { LookupField } from './LookupField';
56
- import { getCellRenderer } from '../index';
57
-
58
- // Radix Select opens on pointer events happy-dom/jsdom do not implement.
59
- beforeAll(() => {
60
- class MockPointerEvent extends Event {
61
- button: number;
62
- ctrlKey: boolean;
63
- pointerType: string;
64
- constructor(type: string, props: any = {}) {
65
- super(type, props);
66
- this.button = props.button ?? 0;
67
- this.ctrlKey = props.ctrlKey ?? false;
68
- this.pointerType = props.pointerType ?? 'mouse';
69
- }
70
- }
71
- (window as any).PointerEvent = MockPointerEvent;
72
- (HTMLElement.prototype as any).hasPointerCapture = vi.fn();
73
- (HTMLElement.prototype as any).releasePointerCapture = vi.fn();
74
- (HTMLElement.prototype as any).scrollIntoView = vi.fn();
75
- });
76
-
77
- const projects = [
78
- { id: 'p1', name: 'Line A retooling', project_phase: 'manufacturing' },
79
- ];
80
-
81
- /** The referenced object's schema `fields` map, as `getObjectSchema` returns it. */
82
- const projectFields = {
83
- name: { type: 'text', label: 'Name' },
84
- project_phase: {
85
- type: 'select',
86
- label: 'Project Phase',
87
- options: [
88
- { label: '01 Initiation', value: 'initiation' },
89
- { label: '03 Manufacturing', value: 'manufacturing' },
90
- ],
91
- },
92
- // A select field the metadata author never gave options to.
93
- risk_level: { type: 'select', label: 'Risk Level' },
94
- };
95
-
96
- function makeDataSource() {
97
- const find = vi.fn(async () => ({ data: projects, total: projects.length }));
98
- const getObjectSchema = vi.fn(async () => ({
99
- name: 'projects',
100
- fields: projectFields,
101
- highlightFields: ['name', 'project_phase'],
102
- }));
103
- return { find, getObjectSchema } as any;
104
- }
105
-
106
- const phaseFilterColumn = { field: 'project_phase', label: 'Project Phase', type: 'select' as const };
107
-
108
- /**
109
- * Open the built-in filter panel and return the first select trigger in it.
110
- *
111
- * The toggle is reached structurally (first button in the filter bar) rather
112
- * than by its accessible name: one case below mounts a `zh` I18nProvider, and
113
- * `initReactI18next` makes that instance the default for provider-less renders
114
- * afterwards — so a `/filters/i` name match would pass or fail depending on
115
- * test order, not on the behaviour under test.
116
- *
117
- * `findBy…`: the filter bar only exists once the filter columns are resolved,
118
- * which in the LookupField path waits on the referenced object's schema fetch.
119
- */
120
- async function openFilterPanel(): Promise<HTMLElement> {
121
- const bar = await screen.findByTestId('record-picker-filter-bar');
122
- fireEvent.click(bar.querySelector('button')!);
123
- return (await screen.findByTestId('record-picker-filter-panel')) as HTMLElement;
124
- }
125
-
126
- async function openFilterSelect(): Promise<Element> {
127
- const panel = await openFilterPanel();
128
- const trigger = panel.querySelector('[role="combobox"]');
129
- expect(trigger).toBeTruthy();
130
- await act(async () => {
131
- fireEvent.pointerDown(trigger!, { button: 0 });
132
- });
133
- return trigger!;
134
- }
135
-
136
- describe('RecordPickerDialog filter panel — select options come from the schema (#3336)', () => {
137
- it('hands the filter bar a select column carrying the schema field options', async () => {
138
- let captured: RecordPickerFilterBarProps | null = null;
139
- render(
140
- <RecordPickerDialog
141
- open
142
- onOpenChange={() => {}}
143
- dataSource={makeDataSource()}
144
- objectName="projects"
145
- columns={[{ field: 'project_phase', label: 'Project Phase', type: 'select' }]}
146
- onSelect={() => {}}
147
- cellRenderer={getCellRenderer}
148
- fieldsMeta={projectFields}
149
- filterColumns={[phaseFilterColumn]}
150
- renderFilterBar={(p) => {
151
- captured = p;
152
- return <div data-testid="external-filter-bar" />;
153
- }}
154
- />,
155
- );
156
-
157
- await waitFor(() => expect(captured).not.toBeNull());
158
- // Pre-fix this was `undefined` — the panel had nothing to render.
159
- expect(captured!.filterColumns[0].options).toEqual([
160
- { label: '01 Initiation', value: 'initiation' },
161
- { label: '03 Manufacturing', value: 'manufacturing' },
162
- ]);
163
- });
164
-
165
- it('renders the schema options in the built-in filter panel dropdown', async () => {
166
- render(
167
- <RecordPickerDialog
168
- open
169
- onOpenChange={() => {}}
170
- dataSource={makeDataSource()}
171
- objectName="projects"
172
- columns={[
173
- { field: 'name', label: 'Name', type: 'text' },
174
- { field: 'project_phase', label: 'Project Phase', type: 'select' },
175
- ]}
176
- onSelect={() => {}}
177
- cellRenderer={getCellRenderer}
178
- fieldsMeta={projectFields}
179
- filterColumns={[phaseFilterColumn]}
180
- />,
181
- );
182
-
183
- await waitFor(() => expect(screen.getByText('Line A retooling')).toBeInTheDocument());
184
- await openFilterSelect();
185
-
186
- // Pre-fix: SelectContent was empty, so neither option role existed.
187
- expect(await screen.findByRole('option', { name: '03 Manufacturing' })).toBeTruthy();
188
- expect(screen.getByRole('option', { name: '01 Initiation' })).toBeTruthy();
189
- });
190
-
191
- it('filters on the picked option value', async () => {
192
- const ds = makeDataSource();
193
- render(
194
- <RecordPickerDialog
195
- open
196
- onOpenChange={() => {}}
197
- dataSource={ds}
198
- objectName="projects"
199
- columns={[{ field: 'project_phase', label: 'Project Phase', type: 'select' }]}
200
- onSelect={() => {}}
201
- cellRenderer={getCellRenderer}
202
- fieldsMeta={projectFields}
203
- filterColumns={[phaseFilterColumn]}
204
- />,
205
- );
206
-
207
- await waitFor(() => expect(ds.find).toHaveBeenCalled());
208
- await openFilterSelect();
209
-
210
- const option = await screen.findByRole('option', { name: '03 Manufacturing' });
211
- await act(async () => {
212
- fireEvent.click(option);
213
- });
214
-
215
- await waitFor(() => {
216
- const lastParams = ds.find.mock.calls[ds.find.mock.calls.length - 1][1];
217
- expect(lastParams.$filter).toMatchObject({ project_phase: 'manufacturing' });
218
- });
219
- });
220
-
221
- /**
222
- * The load-bearing case: ONE source for both faces of the widget. The filter
223
- * option label and the table cell for the same field must be the same
224
- * translated string — a filter-only re-derivation that skipped the shared
225
- * i18n option path would show the authored English here.
226
- */
227
- it('translates filter option labels through the same i18n path as the cells', async () => {
228
- let captured: RecordPickerFilterBarProps | null = null;
229
- render(
230
- <I18nProvider
231
- config={{
232
- defaultLanguage: 'zh',
233
- detectBrowserLanguage: false,
234
- resources: {
235
- zh: {
236
- demo: {
237
- fields: { projects_zh: { project_phase: '项目阶段' } },
238
- fieldOptions: {
239
- projects_zh: {
240
- project_phase: {
241
- initiation: '01 立项',
242
- manufacturing: '03 制造',
243
- },
244
- },
245
- },
246
- },
247
- },
248
- },
249
- }}
250
- >
251
- {/* A dedicated object name: the bundle above stays registered on the
252
- default i18next instance for later renders, and keying it to
253
- `projects_zh` keeps it from translating the other cases' options. */}
254
- <RecordPickerDialog
255
- open
256
- onOpenChange={() => {}}
257
- dataSource={makeDataSource()}
258
- objectName="projects_zh"
259
- columns={[{ field: 'project_phase', label: 'Project Phase', type: 'select' }]}
260
- onSelect={() => {}}
261
- cellRenderer={getCellRenderer}
262
- fieldsMeta={projectFields}
263
- filterColumns={[phaseFilterColumn]}
264
- renderFilterBar={(p) => {
265
- captured = p;
266
- return <div data-testid="external-filter-bar" />;
267
- }}
268
- />
269
- </I18nProvider>,
270
- );
271
-
272
- // The cell already resolved the translated label after #3333 …
273
- await waitFor(() => expect(screen.getByText('03 制造')).toBeInTheDocument());
274
- // … and the filter option for the same field now says exactly the same thing.
275
- await waitFor(() => expect(captured).not.toBeNull());
276
- const labels = captured!.filterColumns[0].options?.map(o => o.label);
277
- expect(labels).toEqual(['01 立项', '03 制造']);
278
- expect(labels).not.toContain('03 Manufacturing');
279
- });
280
-
281
- /**
282
- * NO-OP PIN (green before and after). A select field the author gave no
283
- * options keeps an empty dropdown: the fix derives options from the schema
284
- * and adds no consumer-side leniency — it never invents entries from the
285
- * loaded page's raw stored values, which would hide the metadata gap behind
286
- * a list that changes per page.
287
- */
288
- it('leaves a select filter optionless when the schema field declares none', async () => {
289
- let captured: RecordPickerFilterBarProps | null = null;
290
- render(
291
- <RecordPickerDialog
292
- open
293
- onOpenChange={() => {}}
294
- dataSource={makeDataSource()}
295
- objectName="projects"
296
- columns={[{ field: 'name', label: 'Name', type: 'text' }]}
297
- onSelect={() => {}}
298
- cellRenderer={getCellRenderer}
299
- fieldsMeta={projectFields}
300
- filterColumns={[{ field: 'risk_level', label: 'Risk Level', type: 'select' }]}
301
- renderFilterBar={(p) => {
302
- captured = p;
303
- return <div data-testid="external-filter-bar" />;
304
- }}
305
- />,
306
- );
307
-
308
- await waitFor(() => expect(captured).not.toBeNull());
309
- expect(captured!.filterColumns[0].options).toBeUndefined();
310
- });
311
-
312
- /**
313
- * NO-OP PIN (green before and after). Authored filter options are the more
314
- * specific statement and keep winning — including the `in`-operator options
315
- * `lookupFilters` auto-derivation builds, which must not be replaced by the
316
- * schema's full option set (that would widen a deliberately narrowed list).
317
- */
318
- it('keeps explicitly authored filter options over the schema ones', async () => {
319
- let captured: RecordPickerFilterBarProps | null = null;
320
- render(
321
- <RecordPickerDialog
322
- open
323
- onOpenChange={() => {}}
324
- dataSource={makeDataSource()}
325
- objectName="projects"
326
- onSelect={() => {}}
327
- cellRenderer={getCellRenderer}
328
- fieldsMeta={projectFields}
329
- lookupFilters={[{ field: 'project_phase', operator: 'in', value: ['manufacturing'] }]}
330
- renderFilterBar={(p) => {
331
- captured = p;
332
- return <div data-testid="external-filter-bar" />;
333
- }}
334
- />,
335
- );
336
-
337
- await waitFor(() => expect(captured).not.toBeNull());
338
- expect(captured!.filterColumns[0].options).toEqual([
339
- { label: 'manufacturing', value: 'manufacturing' },
340
- ]);
341
- });
342
- });
343
-
344
- /**
345
- * Record Picker FILTER PANEL — the picked option keeps its AUTHORED type
346
- * (#3422).
347
- *
348
- * Radix `Select` speaks strings: options render as `String(opt.value)` and
349
- * `onValueChange` returns that string. The panel used to store the string
350
- * as-is, and `filterValuesToRecord`'s `select` branch passes its value through
351
- * untouched — so a `lookup_filters`-derived option whose authored value is a
352
- * number or a boolean queried `{ level: "1" }` against records storing
353
- * `level: 1`. The user picked an option that plainly has records and got an
354
- * empty list.
355
- *
356
- * The fix maps the control's string back through `col.options` at the control
357
- * boundary (`matchOptionValue`, the #3090 semantics from
358
- * `packages/components/src/renderers/form/option-value.ts`) — NOT a coercion
359
- * in `filterValuesToRecord`, which would have to guess whether `"1"` meant `1`
360
- * or `"1"`.
361
- *
362
- * ## Directions, decided before the run
363
- *
364
- * - **The two reproducers are RED before / GREEN after**: `$filter.level` was
365
- * the string `"1"`, `$filter.active` the string `"false"`. They assert
366
- * `toBe(1)` / `toBe(false)` plus an explicit `typeof`, because `"1" == 1` and
367
- * a loose assertion would have passed on the broken build.
368
- * - **Three cases are NO-OP pins, green before and after** (marked
369
- * individually): schema-derived options are already strings and must
370
- * round-trip byte-identical (the fix must not start morphing the path
371
- * #3336 just made work); and the `number` / `boolean` filter inputs, which
372
- * already converted explicitly, must keep doing exactly that — the
373
- * three-branch inconsistency the issue flagged is resolved by lifting
374
- * `select` up to them, not by touching them.
375
- * - **The collision case documents a tie-break, not a repair.** Two options
376
- * whose `String()` forms collide (`1` and `'1'`) cannot both be addressed by
377
- * a string-speaking control — Radix sees two `SelectItem`s with the same
378
- * `value`. The first match wins; the case pins that so the resolution is a
379
- * stated rule rather than an accident of `Array.prototype.find`.
380
- */
381
- describe('RecordPickerDialog filter panel — picked option keeps its authored type (#3422)', () => {
382
- const tasks = [{ id: 't1', name: 'Recalibrate press', level: 1, active: false }];
383
-
384
- function makeTaskDataSource() {
385
- const find = vi.fn(async () => ({ data: tasks, total: tasks.length }));
386
- const getObjectSchema = vi.fn(async () => ({
387
- name: 'tasks',
388
- fields: {
389
- name: { type: 'text', label: 'Name' },
390
- level: { type: 'number', label: 'Level' },
391
- active: { type: 'boolean', label: 'Active' },
392
- },
393
- highlightFields: ['name'],
394
- }));
395
- return { find, getObjectSchema } as any;
396
- }
397
-
398
- /** The `$filter` of the most recent `find` call. */
399
- function lastFilter(ds: any): Record<string, any> {
400
- return ds.find.mock.calls[ds.find.mock.calls.length - 1][1].$filter;
401
- }
402
-
403
- /**
404
- * REPRODUCER (red before the fix). `lookup_filters` with an `in` array of
405
- * numbers derives a select whose option values are numbers
406
- * (`LookupFilterDef.value` is `unknown`, so the author's type survives into
407
- * `options`). Picking one must query the number.
408
- */
409
- it('sends the authored NUMBER for a lookupFilters-derived select option', async () => {
410
- const ds = makeTaskDataSource();
411
- render(
412
- <RecordPickerDialog
413
- open
414
- onOpenChange={() => {}}
415
- dataSource={ds}
416
- objectName="tasks"
417
- onSelect={() => {}}
418
- cellRenderer={getCellRenderer}
419
- lookupFilters={[{ field: 'level', operator: 'in', value: [1, 2, 3] }]}
420
- />,
421
- );
422
-
423
- await waitFor(() => expect(ds.find).toHaveBeenCalled());
424
- await openFilterSelect();
425
-
426
- await act(async () => {
427
- fireEvent.click(await screen.findByRole('option', { name: '1' }));
428
- });
429
-
430
- // Pre-fix this was the string "1"; `toBe` distinguishes them, `==` would not.
431
- await waitFor(() => expect(lastFilter(ds).level).toBe(1));
432
- expect(typeof lastFilter(ds).level).toBe('number');
433
- });
434
-
435
- /** REPRODUCER (red before the fix) — the boolean half of the same morph. */
436
- it('sends the authored BOOLEAN for a lookupFilters-derived select option', async () => {
437
- const ds = makeTaskDataSource();
438
- render(
439
- <RecordPickerDialog
440
- open
441
- onOpenChange={() => {}}
442
- dataSource={ds}
443
- objectName="tasks"
444
- onSelect={() => {}}
445
- cellRenderer={getCellRenderer}
446
- lookupFilters={[{ field: 'active', operator: 'in', value: [true, false] }]}
447
- />,
448
- );
449
-
450
- await waitFor(() => expect(ds.find).toHaveBeenCalled());
451
- await openFilterSelect();
452
-
453
- await act(async () => {
454
- fireEvent.click(await screen.findByRole('option', { name: 'false' }));
455
- });
456
-
457
- // Pre-fix this was the string "false" — which is TRUTHY, so the query asked
458
- // for the opposite of what the user picked wherever the backend coerces.
459
- await waitFor(() => expect(lastFilter(ds).active).toBe(false));
460
- expect(typeof lastFilter(ds).active).toBe('boolean');
461
- });
462
-
463
- /**
464
- * NO-OP PIN (green before and after). Schema-derived options are strings by
465
- * spec (`SelectOptionSchema.value` is `string`), so the #3336 path must come
466
- * out of the round-trip byte-identical — the fix restores the authored type,
467
- * it does not invent one.
468
- */
469
- it('round-trips a schema-derived string option unchanged', async () => {
470
- const ds = makeDataSource();
471
- render(
472
- <RecordPickerDialog
473
- open
474
- onOpenChange={() => {}}
475
- dataSource={ds}
476
- objectName="projects"
477
- columns={[{ field: 'project_phase', label: 'Project Phase', type: 'select' }]}
478
- onSelect={() => {}}
479
- cellRenderer={getCellRenderer}
480
- fieldsMeta={projectFields}
481
- filterColumns={[phaseFilterColumn]}
482
- />,
483
- );
484
-
485
- await waitFor(() => expect(ds.find).toHaveBeenCalled());
486
- await openFilterSelect();
487
-
488
- await act(async () => {
489
- fireEvent.click(await screen.findByRole('option', { name: '03 Manufacturing' }));
490
- });
491
-
492
- await waitFor(() => expect(lastFilter(ds).project_phase).toBe('manufacturing'));
493
- expect(typeof lastFilter(ds).project_phase).toBe('string');
494
- });
495
-
496
- /**
497
- * TIE-BREAK PIN. `1` and `'1'` both render as the control string `"1"`, so
498
- * the control cannot tell them apart — Radix receives two `SelectItem`s with
499
- * the same `value` and React two children with the same key. The lookup
500
- * resolves to the FIRST declared match, whichever entry was clicked. This
501
- * documents the rule; it does not claim such an option list is usable.
502
- */
503
- it('resolves a String() collision to the first declared option', async () => {
504
- const ds = makeTaskDataSource();
505
- render(
506
- <RecordPickerDialog
507
- open
508
- onOpenChange={() => {}}
509
- dataSource={ds}
510
- objectName="tasks"
511
- onSelect={() => {}}
512
- cellRenderer={getCellRenderer}
513
- filterColumns={[
514
- {
515
- field: 'level',
516
- label: 'Level',
517
- type: 'select',
518
- options: [
519
- { label: 'Number one', value: 1 },
520
- { label: 'String one', value: '1' },
521
- ],
522
- },
523
- ]}
524
- />,
525
- );
526
-
527
- await waitFor(() => expect(ds.find).toHaveBeenCalled());
528
- await openFilterSelect();
529
-
530
- // Clicking the SECOND entry still yields the first match — that is the
531
- // documented consequence of a collision, not a bug in this assertion.
532
- await act(async () => {
533
- fireEvent.click(await screen.findByRole('option', { name: 'String one' }));
534
- });
535
-
536
- await waitFor(() => expect(lastFilter(ds).level).toBe(1));
537
- expect(typeof lastFilter(ds).level).toBe('number');
538
- });
539
-
540
- /**
541
- * NO-OP PIN (green before and after). The `number` and `boolean` filter
542
- * inputs already converted explicitly (`Number(raw)` / `Boolean(checked)`);
543
- * the issue flagged the three branches disagreeing, and the fix resolves that
544
- * by lifting `select` to their standard — these two must be untouched.
545
- */
546
- it('leaves the number and boolean filter inputs converting as before', async () => {
547
- const ds = makeTaskDataSource();
548
- render(
549
- <RecordPickerDialog
550
- open
551
- onOpenChange={() => {}}
552
- dataSource={ds}
553
- objectName="tasks"
554
- onSelect={() => {}}
555
- cellRenderer={getCellRenderer}
556
- filterColumns={[
557
- { field: 'level', label: 'Level', type: 'number' },
558
- { field: 'active', label: 'Active', type: 'boolean' },
559
- ]}
560
- />,
561
- );
562
-
563
- await waitFor(() => expect(ds.find).toHaveBeenCalled());
564
- const panel = await openFilterPanel();
565
-
566
- await act(async () => {
567
- fireEvent.change(panel.querySelector('input[type="number"]')!, { target: { value: '2' } });
568
- });
569
- await waitFor(() => expect(lastFilter(ds).level).toBe(2));
570
- expect(typeof lastFilter(ds).level).toBe('number');
571
-
572
- await act(async () => {
573
- fireEvent.click(panel.querySelector('[role="checkbox"]')!);
574
- });
575
- await waitFor(() => expect(lastFilter(ds).active).toBe(true));
576
- expect(typeof lastFilter(ds).active).toBe('boolean');
577
- });
578
- });
579
-
580
- describe('LookupField → picker filter panel wiring (#3336)', () => {
581
- const lookup = {
582
- name: 'project',
583
- label: 'Project',
584
- type: 'lookup',
585
- reference_to: 'projects',
586
- reference_field: 'name',
587
- } as any;
588
-
589
- it('offers the referenced object schema options in the derived select filter', async () => {
590
- render(
591
- <LookupField
592
- field={lookup}
593
- value={undefined}
594
- onChange={vi.fn()}
595
- readonly={false}
596
- dataSource={makeDataSource()}
597
- />,
598
- );
599
-
600
- // Open the "Browse all records" picker.
601
- await act(async () => {
602
- fireEvent.click(await screen.findByTestId('browse-all-records'));
603
- });
604
- await waitFor(() => expect(screen.getByTestId('record-picker-dialog')).toBeInTheDocument());
605
-
606
- // `project_phase` reaches the filter bar because it is a typed picker column
607
- // derived from `highlightFields`; its options come from the same schema.
608
- await openFilterSelect();
609
- expect(await screen.findByRole('option', { name: '03 Manufacturing' })).toBeTruthy();
610
- });
611
- });