@object-ui/fields 17.3.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 (265) hide show
  1. package/CHANGELOG.md +1655 -0
  2. package/README.md +1 -12
  3. package/dist/FieldEditWidget.d.ts +23 -3
  4. package/dist/currency.d.ts +46 -0
  5. package/dist/field-type-alias.d.ts +35 -1
  6. package/dist/index.cjs +4 -4
  7. package/dist/index.css +651 -0
  8. package/dist/index.d.ts +474 -85
  9. package/dist/index.js +5418 -5023
  10. package/dist/widgets/AddressField.d.ts +4 -12
  11. package/dist/widgets/AutoNumberField.d.ts +1 -1
  12. package/dist/widgets/AvatarField.d.ts +1 -1
  13. package/dist/widgets/BooleanField.d.ts +1 -1
  14. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  15. package/dist/widgets/CheckboxesField.d.ts +1 -1
  16. package/dist/widgets/CodeField.d.ts +1 -1
  17. package/dist/widgets/ColorField.d.ts +1 -1
  18. package/dist/widgets/CurrencyField.d.ts +1 -1
  19. package/dist/widgets/DateField.d.ts +1 -1
  20. package/dist/widgets/DateTimeField.d.ts +1 -1
  21. package/dist/widgets/EmailField.d.ts +1 -1
  22. package/dist/widgets/FileField.d.ts +18 -2
  23. package/dist/widgets/FilterConditionField.d.ts +28 -1
  24. package/dist/widgets/FormulaField.d.ts +1 -1
  25. package/dist/widgets/FullscreenFieldEditor.d.ts +49 -152
  26. package/dist/widgets/GeolocationField.d.ts +1 -1
  27. package/dist/widgets/GridField.d.ts +68 -6
  28. package/dist/widgets/ImageField.d.ts +1 -1
  29. package/dist/widgets/LocationField.d.ts +1 -1
  30. package/dist/widgets/LookupField.d.ts +1 -1
  31. package/dist/widgets/MasterDetailField.d.ts +1 -1
  32. package/dist/widgets/MultiSelectField.d.ts +1 -1
  33. package/dist/widgets/NumberField.d.ts +1 -1
  34. package/dist/widgets/ObjectField.d.ts +1 -1
  35. package/dist/widgets/ObjectRefField.d.ts +1 -1
  36. package/dist/widgets/OptionsEmptyState.d.ts +21 -1
  37. package/dist/widgets/PasswordField.d.ts +1 -1
  38. package/dist/widgets/PercentField.d.ts +1 -1
  39. package/dist/widgets/PhoneField.d.ts +1 -1
  40. package/dist/widgets/QRCodeField.d.ts +1 -1
  41. package/dist/widgets/RadioField.d.ts +1 -1
  42. package/dist/widgets/RatingField.d.ts +1 -1
  43. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  44. package/dist/widgets/RecordPickerDialog.d.ts +34 -10
  45. package/dist/widgets/RichTextField.d.ts +37 -1
  46. package/dist/widgets/SelectField.d.ts +26 -6
  47. package/dist/widgets/SignatureField.d.ts +32 -2
  48. package/dist/widgets/SliderField.d.ts +37 -2
  49. package/dist/widgets/SummaryField.d.ts +1 -1
  50. package/dist/widgets/TagsField.d.ts +1 -1
  51. package/dist/widgets/TextAreaField.d.ts +6 -2
  52. package/dist/widgets/TextField.d.ts +1 -1
  53. package/dist/widgets/TimeField.d.ts +1 -1
  54. package/dist/widgets/UrlField.d.ts +1 -1
  55. package/dist/widgets/UserField.d.ts +1 -1
  56. package/dist/widgets/VectorField.d.ts +1 -1
  57. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  58. package/dist/widgets/address-format.d.ts +88 -0
  59. package/dist/widgets/file-size-guard.d.ts +41 -0
  60. package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
  61. package/dist/widgets/nativeDateValue.d.ts +37 -0
  62. package/dist/widgets/richTextDisplay.d.ts +80 -0
  63. package/dist/widgets/toDomProps.d.ts +30 -10
  64. package/dist/widgets/toHostGroupProps.d.ts +189 -0
  65. package/dist/widgets/types.d.ts +8 -3
  66. package/dist/widgets/useRecordQuery.d.ts +7 -1
  67. package/package.json +17 -14
  68. package/dist/FieldEditWidget.test.d.ts +0 -8
  69. package/dist/__tests__/DateCellRenderer.test.d.ts +0 -0
  70. package/dist/__tests__/NumberCellRenderer.test.d.ts +0 -0
  71. package/dist/__tests__/cell-truncation.test.d.ts +0 -0
  72. package/dist/__tests__/field-carrier-sdui.test.d.ts +0 -0
  73. package/dist/__tests__/lookupCellDisplayField.test.d.ts +0 -0
  74. package/dist/__tests__/lookupCellDisplayName.test.d.ts +0 -0
  75. package/dist/__tests__/lookupCellReferenceKey.test.d.ts +0 -0
  76. package/dist/__tests__/registry-collision.test.d.ts +0 -0
  77. package/dist/__tests__/resolveFieldCurrency.test.d.ts +0 -1
  78. package/dist/__tests__/spec-symbol-batch7.test.d.ts +0 -8
  79. package/dist/__tests__/validation-feedback.test.d.ts +0 -0
  80. package/dist/__tests__/widget-aria-invalid-e2e.test.d.ts +0 -0
  81. package/dist/__tests__/widget-aria-invalid-registry-e2e.test.d.ts +0 -0
  82. package/dist/__tests__/widget-dom-leak-e2e.test.d.ts +0 -0
  83. package/dist/__tests__/widget-props-contract.test.d.ts +0 -8
  84. package/dist/coerce-safe-value.test.d.ts +0 -1
  85. package/dist/complex-widgets.test.d.ts +0 -1
  86. package/dist/datetime-widgets.test.d.ts +0 -1
  87. package/dist/field-type-coverage.test.d.ts +0 -1
  88. package/dist/standard-widgets.test.d.ts +0 -0
  89. package/dist/widgets/AddressField.uniqueIds.test.d.ts +0 -0
  90. package/dist/widgets/CapabilityMultiSelectField.test.d.ts +0 -0
  91. package/dist/widgets/CharacterCount.d.ts +0 -56
  92. package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
  93. package/dist/widgets/GeolocationField.uniqueIds.test.d.ts +0 -0
  94. package/dist/widgets/GridField.test.d.ts +0 -1
  95. package/dist/widgets/ImageLightbox.test.d.ts +0 -1
  96. package/dist/widgets/LookupField.dependsOn.test.d.ts +0 -8
  97. package/dist/widgets/LookupField.gateHintLabel.test.d.ts +0 -8
  98. package/dist/widgets/LookupField.hydration.test.d.ts +0 -1
  99. package/dist/widgets/LookupField.idField.test.d.ts +0 -1
  100. package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
  101. package/dist/widgets/OptionsEmptyState.no-provider.test.d.ts +0 -0
  102. package/dist/widgets/OptionsEmptyState.test.d.ts +0 -0
  103. package/dist/widgets/PeoplePicker.test.d.ts +0 -8
  104. package/dist/widgets/PeoplePickerComponents.test.d.ts +0 -8
  105. package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
  106. package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
  107. package/dist/widgets/RecordPickerDialog.filterOptions.test.d.ts +0 -8
  108. package/dist/widgets/RecordPickerDialog.selectLabel.test.d.ts +0 -8
  109. package/dist/widgets/SelectField.cascade.test.d.ts +0 -0
  110. package/dist/widgets/TagsField.placeholder.no-provider.test.d.ts +0 -0
  111. package/dist/widgets/TagsField.placeholder.test.d.ts +0 -0
  112. package/dist/widgets/UserFieldPicker.test.d.ts +0 -8
  113. package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +0 -8
  114. package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +0 -8
  115. package/dist/widgets/__tests__/FullscreenFieldEditor.i18n.test.d.ts +0 -0
  116. package/dist/widgets/__tests__/FullscreenFieldEditor.no-provider.test.d.ts +0 -0
  117. package/dist/widgets/__tests__/RichTextField.fullscreenDisabled.test.d.ts +0 -0
  118. package/dist/widgets/__tests__/RichTextField.mobileFullscreen.test.d.ts +0 -0
  119. package/dist/widgets/__tests__/TextAreaField.characterCount.announcements.test.d.ts +0 -0
  120. package/dist/widgets/__tests__/TextAreaField.characterCount.i18n.test.d.ts +0 -0
  121. package/dist/widgets/__tests__/TextAreaField.characterCount.no-provider.test.d.ts +0 -0
  122. package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.d.ts +0 -0
  123. package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.d.ts +0 -0
  124. package/dist/widgets/__tests__/TextAreaField.fullscreenDisabled.test.d.ts +0 -0
  125. package/dist/widgets/__tests__/TextAreaField.mobileFullscreen.test.d.ts +0 -0
  126. package/dist/widgets/deriveLookupColumns.test.d.ts +0 -1
  127. package/dist/widgets/file-value.test.d.ts +0 -1
  128. package/dist/widgets/personDisplay.test.d.ts +0 -8
  129. package/dist/widgets/recentLookups.test.d.ts +0 -1
  130. package/dist/widgets/useRecordQuery.test.d.ts +0 -8
  131. package/dist/widgets/useUploadingSignal.test.d.ts +0 -1
  132. package/src/FieldEditWidget.test.ts +0 -133
  133. package/src/FieldEditWidget.tsx +0 -191
  134. package/src/__tests__/DateCellRenderer.test.tsx +0 -73
  135. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  136. package/src/__tests__/cell-truncation.test.tsx +0 -96
  137. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  138. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  139. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  140. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  141. package/src/__tests__/registry-collision.test.ts +0 -91
  142. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  143. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  144. package/src/__tests__/validation-feedback.test.tsx +0 -361
  145. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  146. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  147. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -598
  148. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  149. package/src/coerce-safe-value.test.ts +0 -34
  150. package/src/complex-widgets.test.tsx +0 -908
  151. package/src/currency.ts +0 -14
  152. package/src/datetime-widgets.test.tsx +0 -93
  153. package/src/field-type-alias.ts +0 -100
  154. package/src/field-type-coverage.test.ts +0 -67
  155. package/src/index.tsx +0 -2503
  156. package/src/standard-widgets.test.tsx +0 -357
  157. package/src/widgets/AddressField.tsx +0 -132
  158. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -92
  159. package/src/widgets/AutoNumberField.tsx +0 -18
  160. package/src/widgets/AvatarField.tsx +0 -117
  161. package/src/widgets/BooleanField.tsx +0 -52
  162. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  163. package/src/widgets/CapabilityMultiSelectField.tsx +0 -229
  164. package/src/widgets/CharacterCount.tsx +0 -263
  165. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  166. package/src/widgets/CheckboxesField.tsx +0 -136
  167. package/src/widgets/CodeField.tsx +0 -41
  168. package/src/widgets/ColorField.tsx +0 -53
  169. package/src/widgets/CurrencyField.tsx +0 -95
  170. package/src/widgets/DateField.tsx +0 -31
  171. package/src/widgets/DateTimeField.tsx +0 -37
  172. package/src/widgets/EmailField.tsx +0 -37
  173. package/src/widgets/FileField.tsx +0 -494
  174. package/src/widgets/FilterConditionField.tsx +0 -454
  175. package/src/widgets/FormulaField.tsx +0 -33
  176. package/src/widgets/FullscreenFieldEditor.tsx +0 -271
  177. package/src/widgets/GeolocationField.tsx +0 -167
  178. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  179. package/src/widgets/GridField.test.tsx +0 -406
  180. package/src/widgets/GridField.tsx +0 -966
  181. package/src/widgets/ImageCropperDialog.tsx +0 -245
  182. package/src/widgets/ImageField.tsx +0 -255
  183. package/src/widgets/ImageLightbox.test.tsx +0 -52
  184. package/src/widgets/ImageLightbox.tsx +0 -108
  185. package/src/widgets/LocationField.tsx +0 -57
  186. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  187. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  188. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  189. package/src/widgets/LookupField.idField.test.tsx +0 -61
  190. package/src/widgets/LookupField.tsx +0 -1291
  191. package/src/widgets/MarkdownContent.tsx +0 -19
  192. package/src/widgets/MasterDetailField.tsx +0 -131
  193. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  194. package/src/widgets/MultiSelectField.tsx +0 -142
  195. package/src/widgets/NumberField.tsx +0 -49
  196. package/src/widgets/ObjectField.tsx +0 -94
  197. package/src/widgets/ObjectRefField.tsx +0 -121
  198. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  199. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  200. package/src/widgets/OptionsEmptyState.tsx +0 -83
  201. package/src/widgets/PasswordField.tsx +0 -49
  202. package/src/widgets/PeoplePicker.test.tsx +0 -360
  203. package/src/widgets/PeoplePicker.tsx +0 -579
  204. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  205. package/src/widgets/PercentField.tsx +0 -95
  206. package/src/widgets/PersonRow.tsx +0 -117
  207. package/src/widgets/PhoneField.tsx +0 -37
  208. package/src/widgets/QRCodeField.tsx +0 -103
  209. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  210. package/src/widgets/RadioField.tsx +0 -111
  211. package/src/widgets/RatingField.tsx +0 -81
  212. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  213. package/src/widgets/RecipientPickerField.tsx +0 -188
  214. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  215. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  216. package/src/widgets/RecordPickerDialog.tsx +0 -1316
  217. package/src/widgets/RichTextField.tsx +0 -176
  218. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  219. package/src/widgets/SelectField.tsx +0 -162
  220. package/src/widgets/SelectionTray.tsx +0 -110
  221. package/src/widgets/SignatureField.tsx +0 -140
  222. package/src/widgets/SliderField.tsx +0 -39
  223. package/src/widgets/SummaryField.tsx +0 -31
  224. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  225. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  226. package/src/widgets/TagsField.tsx +0 -88
  227. package/src/widgets/TextAreaField.tsx +0 -213
  228. package/src/widgets/TextField.tsx +0 -45
  229. package/src/widgets/TimeField.tsx +0 -31
  230. package/src/widgets/UrlField.tsx +0 -46
  231. package/src/widgets/UserField.tsx +0 -58
  232. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  233. package/src/widgets/VectorField.tsx +0 -29
  234. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  235. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -142
  236. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  237. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  238. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  239. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  240. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  241. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  242. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  243. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  244. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  245. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  246. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  247. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  248. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  249. package/src/widgets/deriveLookupColumns.ts +0 -108
  250. package/src/widgets/file-value.test.ts +0 -230
  251. package/src/widgets/file-value.ts +0 -246
  252. package/src/widgets/openNativePicker.ts +0 -18
  253. package/src/widgets/personDisplay.test.ts +0 -113
  254. package/src/widgets/personDisplay.ts +0 -92
  255. package/src/widgets/recentLookups.test.ts +0 -45
  256. package/src/widgets/recentLookups.ts +0 -47
  257. package/src/widgets/toDomProps.ts +0 -175
  258. package/src/widgets/types.ts +0 -242
  259. package/src/widgets/useCascadingOptions.ts +0 -46
  260. package/src/widgets/useFieldTranslation.ts +0 -150
  261. package/src/widgets/useRecordQuery.test.ts +0 -174
  262. package/src/widgets/useRecordQuery.ts +0 -264
  263. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  264. package/src/widgets/useUploadingSignal.ts +0 -20
  265. package/src/withFieldCarrier.tsx +0 -75
@@ -1,157 +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
- * RecipientPickerField — the sharing-rule recipient picker (objectstack#3821).
11
- *
12
- * Two regressions are pinned here:
13
- * 1. the candidate query must use the STRUCTURED `$orderby` form. It used to
14
- * pass the clause string `'name asc'`, which ApiDataSource walked
15
- * character by character into `0 n,1 a,2 m,…`; the server then sorted by
16
- * columns that don't exist and every recipient list came back empty, so
17
- * the control read "No matches" forever and no rule could be saved.
18
- * 2. the placeholder must come from a per-type i18n key, not from
19
- * interpolating the enum value into an English sentence.
20
- */
21
- import { describe, it, expect, vi } from 'vitest';
22
- import React from 'react';
23
- import { render, screen, waitFor, fireEvent } from '@testing-library/react';
24
- import '@testing-library/jest-dom';
25
- import { RecipientPickerField } from './RecipientPickerField';
26
-
27
- const USERS = [
28
- { id: 'usr_1', name: 'Ada Auditor' },
29
- { id: 'usr_2', name: 'Bob Builder' },
30
- ];
31
-
32
- function mockDataSource(rows: any[] = USERS) {
33
- return { find: vi.fn().mockResolvedValue({ data: rows }) } as any;
34
- }
35
-
36
- function renderPicker(recipientType: string, dataSource: any, value = '') {
37
- return render(
38
- <RecipientPickerField
39
- value={value}
40
- onChange={vi.fn()}
41
- field={{ name: 'recipient_id' } as any}
42
- dataSource={dataSource}
43
- dependentValues={{ recipient_type: recipientType }}
44
- />,
45
- );
46
- }
47
-
48
- describe('RecipientPickerField — candidate query', () => {
49
- it('orders candidates with the structured $orderby form, never a clause string', async () => {
50
- const ds = mockDataSource();
51
- renderPicker('user', ds);
52
-
53
- await waitFor(() => expect(ds.find).toHaveBeenCalled());
54
- const [object, params] = ds.find.mock.calls[0];
55
- expect(object).toBe('sys_user');
56
- expect(params.$orderby).toEqual({ name: 'asc' });
57
- // A bare string is what regressed; assert the shape explicitly.
58
- expect(typeof params.$orderby).not.toBe('string');
59
- });
60
-
61
- it('queries the object mapped to each recipient type', async () => {
62
- for (const [type, object] of [
63
- ['user', 'sys_user'],
64
- ['team', 'sys_team'],
65
- ['business_unit', 'sys_business_unit'],
66
- ['unit_and_subordinates', 'sys_business_unit'],
67
- ['position', 'sys_position'],
68
- ] as const) {
69
- const ds = mockDataSource([]);
70
- const { unmount } = renderPicker(type, ds);
71
- await waitFor(() => expect(ds.find).toHaveBeenCalled());
72
- expect(ds.find.mock.calls[0][0]).toBe(object);
73
- unmount();
74
- }
75
- });
76
-
77
- it('shows the loaded records as options once the list is opened', async () => {
78
- const ds = mockDataSource();
79
- renderPicker('user', ds);
80
- await waitFor(() => expect(ds.find).toHaveBeenCalled());
81
-
82
- fireEvent.click(screen.getByRole('combobox'));
83
- expect(await screen.findByText('Ada Auditor')).toBeInTheDocument();
84
- expect(screen.getByText('Bob Builder')).toBeInTheDocument();
85
- });
86
- });
87
-
88
- describe('RecipientPickerField — keeping the stored recipient', () => {
89
- function renderControlled(initialType: string, value: string, onChange: any) {
90
- const ds = mockDataSource();
91
- const view = render(
92
- <RecipientPickerField
93
- value={value}
94
- onChange={onChange}
95
- field={{ name: 'recipient_id' } as any}
96
- dataSource={ds}
97
- dependentValues={{ recipient_type: initialType }}
98
- />,
99
- );
100
- const rerenderWith = (type: string) =>
101
- view.rerender(
102
- <RecipientPickerField
103
- value={value}
104
- onChange={onChange}
105
- field={{ name: 'recipient_id' } as any}
106
- dataSource={ds}
107
- dependentValues={{ recipient_type: type }}
108
- />,
109
- );
110
- return { ...view, rerenderWith };
111
- }
112
-
113
- it('keeps the saved recipient when the type arrives late (edit form hydrating)', async () => {
114
- // The edit dialog mounts the widget before the record lands, so
115
- // recipient_type is '' on the first render and 'user' on the next. That is
116
- // hydration, not the admin changing the type — clearing there wiped the
117
- // saved recipient and the blank got persisted on save.
118
- const onChange = vi.fn();
119
- const { rerenderWith } = renderControlled('', 'usr_1', onChange);
120
- rerenderWith('user');
121
- await waitFor(() => expect(onChange).not.toHaveBeenCalled());
122
- });
123
-
124
- it('clears the recipient when the admin actually switches type', async () => {
125
- const onChange = vi.fn();
126
- const { rerenderWith } = renderControlled('user', 'usr_1', onChange);
127
- rerenderWith('team');
128
- await waitFor(() => expect(onChange).toHaveBeenCalledWith(''));
129
- });
130
-
131
- it('does not clear when the type is unset back to empty', async () => {
132
- const onChange = vi.fn();
133
- const { rerenderWith } = renderControlled('user', 'usr_1', onChange);
134
- rerenderWith('');
135
- await waitFor(() => expect(onChange).not.toHaveBeenCalled());
136
- });
137
- });
138
-
139
- describe('RecipientPickerField — localizable copy', () => {
140
- it('takes the placeholder from a per-type key rather than the raw enum value', async () => {
141
- // With no I18nProvider the widget falls back to the English defaults, so
142
- // the assertion is on the *wording*: 'Select a business unit' can only come
143
- // from the keyed string — interpolating the enum produced
144
- // 'Select a business unit' only by accident and 'Select a unit and
145
- // subordinates' for the next type over.
146
- const ds = mockDataSource([]);
147
- renderPicker('unit_and_subordinates', ds);
148
- await waitFor(() => expect(ds.find).toHaveBeenCalled());
149
- expect(await screen.findByText('Select a business unit')).toBeInTheDocument();
150
- expect(screen.queryByText(/unit and subordinates/i)).not.toBeInTheDocument();
151
- });
152
-
153
- it('prompts for a recipient type before one is chosen', () => {
154
- renderPicker('', mockDataSource([]));
155
- expect(screen.getByText('Select a recipient type first.')).toBeInTheDocument();
156
- });
157
- });
@@ -1,188 +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
- * RecipientPickerField — dependent record picker for a polymorphic recipient
10
- * reference (e.g. `sys_sharing_rule.recipient_id`), whose target object is
11
- * decided by a sibling `recipient_type` select.
12
- *
13
- * Reached via the field `widget: 'recipient-picker'` hint (resolves as
14
- * `field:recipient-picker`). Reads the live `recipient_type` from
15
- * `dependentValues` (the form record), loads candidate records of the mapped
16
- * object via `dataSource.find(...)`, and stores the value the sharing evaluator
17
- * expects for that type:
18
- *
19
- * user → sys_user, store `id`
20
- * team → sys_team, store `id`
21
- * business_unit → sys_business_unit, store `id`
22
- * unit_and_subordinates → sys_business_unit, store `id`
23
- * position → sys_position, store `name` (matched against
24
- * sys_user_position.position at evaluation time)
25
- *
26
- * When `recipient_type` changes after mount the stored id is reset (an id valid
27
- * for one type is meaningless for another). Unknown types degrade to a plain
28
- * text input so nothing breaks.
29
- */
30
- interface RecipientMapping {
31
- object: string;
32
- /** Which record field to persist into recipient_id. */
33
- storeField: 'id' | 'name';
34
- /** Candidate display-label fields, in preference order. */
35
- labelFields: string[];
36
- /**
37
- * i18n key for the "choose one" placeholder. Keyed per type rather than
38
- * interpolating the enum value into an English sentence — "Select a
39
- * business unit" and "选择业务单元" share no structure (objectstack#3821).
40
- */
41
- placeholderKey: string;
42
- }
43
-
44
- const TYPE_TO_OBJECT: Record<string, RecipientMapping> = {
45
- user: { object: 'sys_user', storeField: 'id', labelFields: ['name', 'full_name', 'email'], placeholderKey: 'fields.recipient.selectUser' },
46
- team: { object: 'sys_team', storeField: 'id', labelFields: ['name', 'label'], placeholderKey: 'fields.recipient.selectTeam' },
47
- business_unit: { object: 'sys_business_unit', storeField: 'id', labelFields: ['name', 'label'], placeholderKey: 'fields.recipient.selectBusinessUnit' },
48
- unit_and_subordinates: { object: 'sys_business_unit', storeField: 'id', labelFields: ['name', 'label'], placeholderKey: 'fields.recipient.selectUnitAndSubordinates' },
49
- position: { object: 'sys_position', storeField: 'name', labelFields: ['label', 'name'], placeholderKey: 'fields.recipient.selectPosition' },
50
- };
51
-
52
- export function RecipientPickerField({
53
- value,
54
- onChange,
55
- readonly,
56
- className,
57
- error,
58
- ...props
59
- }: FieldWidgetComponentProps<string>) {
60
- const ctx = React.useContext(SchemaRendererContext);
61
- const { t } = useFieldTranslation();
62
- const dataSource: any = props.dataSource ?? (ctx as any)?.dataSource ?? null;
63
- const disabled = props.disabled;
64
- const dependentValues: Record<string, any> = (props as any).dependentValues ?? {};
65
- const recipientType = String(dependentValues.recipient_type ?? '');
66
- const mapping = TYPE_TO_OBJECT[recipientType];
67
-
68
- const [records, setRecords] = React.useState<any[] | null>(null);
69
-
70
- // Reset the stored recipient when the admin PICKS a different type (an id for
71
- // a user is not a valid team/business-unit id).
72
- //
73
- // "Different type" means one non-empty type replacing another. The empty
74
- // string is not a type — it is the edit form before the record has hydrated,
75
- // and treating `'' → 'user'` as a change wiped the saved recipient the moment
76
- // an existing rule was opened for editing (objectstack#3821): the picker went
77
- // blank, and saving persisted the blank. Only a non-empty predecessor can
78
- // invalidate the stored id.
79
- const prevType = React.useRef<string>('');
80
- React.useEffect(() => {
81
- if (prevType.current && recipientType && prevType.current !== recipientType && value) {
82
- onChange('' as any);
83
- }
84
- prevType.current = recipientType;
85
- // eslint-disable-next-line react-hooks/exhaustive-deps
86
- }, [recipientType]);
87
-
88
- React.useEffect(() => {
89
- setRecords(null);
90
- if (!dataSource || !mapping || typeof dataSource.find !== 'function') return;
91
- let cancelled = false;
92
- (async () => {
93
- try {
94
- // Object form, not the 'name asc' clause string: the clause string is
95
- // supported again (objectstack#3821 fixed ApiDataSource walking it
96
- // character by character), but the structured form can't regress that
97
- // way for any data source.
98
- const res = await dataSource.find(mapping.object, { $top: 500, $orderby: { name: 'asc' } });
99
- const list: any[] = res?.data ?? res?.records ?? (Array.isArray(res) ? res : []);
100
- if (!cancelled) setRecords(Array.isArray(list) ? list : []);
101
- } catch {
102
- if (!cancelled) setRecords([]);
103
- }
104
- })();
105
- return () => {
106
- cancelled = true;
107
- };
108
- }, [dataSource, mapping?.object]);
109
-
110
- const labelOf = (r: any): string => {
111
- for (const f of mapping?.labelFields ?? ['name']) if (r?.[f]) return String(r[f]);
112
- return String(r?.id ?? '');
113
- };
114
- const valueOf = (r: any): string => String(r?.[mapping?.storeField ?? 'id'] ?? '');
115
-
116
- const options = React.useMemo(() => {
117
- const opts = (records ?? [])
118
- .map((r) => ({ value: valueOf(r), label: labelOf(r) }))
119
- .filter((o) => o.value);
120
- if (value && !opts.some((o) => o.value === value)) opts.unshift({ value, label: value });
121
- return opts;
122
- // eslint-disable-next-line react-hooks/exhaustive-deps
123
- }, [records, value, mapping]);
124
-
125
- if (!recipientType) {
126
- return (
127
- <p className={cn('text-sm text-muted-foreground', className)}>
128
- {t('fields.recipient.selectTypeFirst')}
129
- </p>
130
- );
131
- }
132
-
133
- if (!mapping) {
134
- // Unknown / unsupported recipient type — keep a plain text input so the
135
- // field is never un-editable.
136
- return (
137
- <input
138
- // DOM pass-through onto the real focusable control (objectui#3318).
139
- {...toDomProps(props)}
140
- className={cn(
141
- 'w-full rounded border bg-background px-2 py-1 text-sm',
142
- className,
143
- )}
144
- value={value ?? ''}
145
- disabled={disabled || readonly}
146
- onChange={(e) => onChange(e.target.value as any)}
147
- aria-invalid={!!error}
148
- />
149
- );
150
- }
151
-
152
- if (readonly) {
153
- if (!value) return <EmptyValue />;
154
- return <span className={className}>{options.find((o) => o.value === value)?.label ?? value}</span>;
155
- }
156
-
157
- // DOM pass-through onto the combobox trigger — the widget's real focusable
158
- // control (objectui#3318). `name` is withheld: the trigger is a button, not
159
- // a submission control (same reasoning as #3306's SelectTrigger).
160
- //
161
- // NOTE this widget stays on the #3318 ledger regardless: its dependency-
162
- // gated state (no `recipient_type` chosen yet — the state a fresh form and
163
- // the registry sweep render) is a plain hint paragraph with no focusable
164
- // control, so there is nothing there to carry the attribute.
165
- const { name: _domName, ...triggerDomProps } = toDomProps(props);
166
-
167
- return (
168
- <Combobox
169
- {...triggerDomProps}
170
- options={options}
171
- value={value ?? ''}
172
- onValueChange={(v) => onChange(v as any)}
173
- placeholder={
174
- records === null
175
- ? t('fields.recipient.loading')
176
- : t(mapping.placeholderKey ?? 'fields.recipient.select')
177
- }
178
- searchPlaceholder={t('fields.recipient.search')}
179
- emptyText={records === null ? t('fields.recipient.loading') : t('fields.recipient.empty')}
180
- disabled={disabled}
181
- className={cn('w-full', className)}
182
- // AFTER the spread so this widget's own computation wins (#3222).
183
- aria-invalid={!!error}
184
- />
185
- );
186
- }
187
-
188
- export default RecipientPickerField;