@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,174 +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 { renderHook, act, waitFor } from '@testing-library/react';
10
- import { describe, it, expect, vi } from 'vitest';
11
- import { useRecordQuery } from './useRecordQuery';
12
-
13
- function makeDataSource(result: any = { data: [], total: 0 }) {
14
- return { find: vi.fn().mockResolvedValue(result) } as any;
15
- }
16
-
17
- describe('useRecordQuery', () => {
18
- it('fetches on mount with $top and exposes records/total', async () => {
19
- const ds = makeDataSource({ data: [{ id: 1 }], total: 5 });
20
- const { result } = renderHook(() =>
21
- useRecordQuery({ dataSource: ds, objectName: 'sys_user' }),
22
- );
23
-
24
- await waitFor(() => expect(result.current.loading).toBe(false));
25
- expect(ds.find).toHaveBeenCalledWith('sys_user', { $top: 50 });
26
- expect(result.current.records).toEqual([{ id: 1 }]);
27
- expect(result.current.total).toBe(5);
28
- });
29
-
30
- it('tolerates a bare-array response (no { data } envelope)', async () => {
31
- const ds = { find: vi.fn().mockResolvedValue([{ id: 1 }, { id: 2 }]) } as any;
32
- const { result } = renderHook(() =>
33
- useRecordQuery({ dataSource: ds, objectName: 'o' }),
34
- );
35
-
36
- await waitFor(() => expect(result.current.records).toHaveLength(2));
37
- expect(result.current.total).toBe(2);
38
- });
39
-
40
- it('adds $skip only when paginate is set', async () => {
41
- const ds = makeDataSource({ data: [], total: 100 });
42
- const { result } = renderHook(() =>
43
- useRecordQuery({ dataSource: ds, objectName: 'o', pageSize: 10, paginate: true }),
44
- );
45
-
46
- await waitFor(() => expect(ds.find).toHaveBeenLastCalledWith('o', { $top: 10, $skip: 0 }));
47
- // waitFor: the call firing doesn't guarantee its promise resolved and
48
- // setTotal flushed — assert on the settled state, not a bare read.
49
- await waitFor(() => expect(result.current.totalPages).toBe(10));
50
-
51
- act(() => result.current.setPage(3));
52
- await waitFor(() =>
53
- expect(ds.find).toHaveBeenLastCalledWith('o', { $top: 10, $skip: 20 }),
54
- );
55
- });
56
-
57
- it('debounces setSearch, resets to page 1, and coalesces rapid input', async () => {
58
- const ds = makeDataSource();
59
- const { result } = renderHook(() =>
60
- useRecordQuery({ dataSource: ds, objectName: 'o', debounceMs: 20 }),
61
- );
62
- await waitFor(() => expect(ds.find).toHaveBeenCalledTimes(1)); // mount fetch
63
-
64
- act(() => {
65
- result.current.setSearch('a');
66
- result.current.setSearch('ab');
67
- });
68
-
69
- await waitFor(() =>
70
- expect(ds.find).toHaveBeenLastCalledWith('o', { $top: 50, $search: 'ab' }),
71
- );
72
- // The superseded 'a' term never reached the server.
73
- expect(ds.find).not.toHaveBeenCalledWith('o', { $top: 50, $search: 'a' });
74
- });
75
-
76
- it('toggleSort sets $orderby asc then flips to desc', async () => {
77
- const ds = makeDataSource();
78
- const { result } = renderHook(() =>
79
- useRecordQuery({ dataSource: ds, objectName: 'o' }),
80
- );
81
- await waitFor(() => expect(ds.find).toHaveBeenCalled());
82
-
83
- act(() => result.current.toggleSort('name'));
84
- await waitFor(() =>
85
- expect(ds.find).toHaveBeenLastCalledWith('o', { $top: 50, $orderby: { name: 'asc' } }),
86
- );
87
-
88
- act(() => result.current.toggleSort('name'));
89
- await waitFor(() =>
90
- expect(ds.find).toHaveBeenLastCalledWith('o', { $top: 50, $orderby: { name: 'desc' } }),
91
- );
92
- });
93
-
94
- it('passes $filter and refetches when the filter value changes', async () => {
95
- const ds = makeDataSource();
96
- const { rerender } = renderHook(
97
- ({ filter }) => useRecordQuery({ dataSource: ds, objectName: 'o', filter }),
98
- { initialProps: { filter: { banned: { $ne: true } } as Record<string, any> } },
99
- );
100
-
101
- await waitFor(() =>
102
- expect(ds.find).toHaveBeenLastCalledWith('o', {
103
- $top: 50,
104
- $filter: { banned: { $ne: true } },
105
- }),
106
- );
107
-
108
- rerender({ filter: { banned: { $ne: true }, active: true } });
109
- await waitFor(() =>
110
- expect(ds.find).toHaveBeenLastCalledWith('o', {
111
- $top: 50,
112
- $filter: { banned: { $ne: true }, active: true },
113
- }),
114
- );
115
- });
116
-
117
- it('passes $expand and $searchFields through', async () => {
118
- const ds = makeDataSource();
119
- renderHook(() =>
120
- useRecordQuery({
121
- dataSource: ds,
122
- objectName: 'sys_user',
123
- expand: ['primary_business_unit_id'],
124
- searchFields: ['name', 'email'],
125
- }),
126
- );
127
-
128
- await waitFor(() =>
129
- expect(ds.find).toHaveBeenLastCalledWith('sys_user', {
130
- $top: 50,
131
- $expand: ['primary_business_unit_id'],
132
- $searchFields: ['name', 'email'],
133
- }),
134
- );
135
- });
136
-
137
- it('does not fetch while disabled and clears records when re-disabled', async () => {
138
- const ds = makeDataSource({ data: [{ id: 1 }], total: 1 });
139
- const { result, rerender } = renderHook(
140
- ({ enabled }) => useRecordQuery({ dataSource: ds, objectName: 'o', enabled }),
141
- { initialProps: { enabled: false } },
142
- );
143
-
144
- await new Promise(r => setTimeout(r, 20));
145
- expect(ds.find).not.toHaveBeenCalled();
146
-
147
- rerender({ enabled: true });
148
- await waitFor(() => expect(result.current.records).toEqual([{ id: 1 }]));
149
-
150
- rerender({ enabled: false });
151
- await waitFor(() => expect(result.current.records).toEqual([]));
152
- });
153
-
154
- it('captures fetch errors and empties the result set', async () => {
155
- const ds = { find: vi.fn().mockRejectedValue(new Error('boom')) } as any;
156
- const { result } = renderHook(() =>
157
- useRecordQuery({ dataSource: ds, objectName: 'o' }),
158
- );
159
-
160
- await waitFor(() => expect(result.current.error).toBe('boom'));
161
- expect(result.current.records).toEqual([]);
162
- expect(result.current.loading).toBe(false);
163
- });
164
-
165
- it('stays idle when no dataSource is provided', async () => {
166
- const { result } = renderHook(() =>
167
- useRecordQuery({ dataSource: null, objectName: 'o' }),
168
- );
169
-
170
- await new Promise(r => setTimeout(r, 10));
171
- expect(result.current.loading).toBe(false);
172
- expect(result.current.records).toEqual([]);
173
- });
174
- });
@@ -1,264 +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 { useCallback, useEffect, useMemo, useRef, useState } from 'react';
10
- import type { DataSource, QueryParams } from '@object-ui/types';
11
-
12
- /**
13
- * useRecordQuery — the shared record-query kernel behind record pickers.
14
- *
15
- * Encapsulates the query/pagination/search/sort loop that was previously
16
- * duplicated (and independently tuned) inside `RecordPickerDialog.fetchRecords`
17
- * and `LookupField.fetchLookupData`:
18
- *
19
- * - builds the `QueryParams` (`$top`/`$skip`/`$search`/`$searchFields`/
20
- * `$orderby`/`$filter`/`$expand`),
21
- * - issues `dataSource.find(objectName, params)` and normalises the result
22
- * (`{ data, total }`, tolerating a bare array),
23
- * - owns `records`/`loading`/`error`/`total` plus the `page`/`search`/`sort`
24
- * controls, with a debounced search path,
25
- * - clears itself when `enabled` goes false (e.g. a dialog closing).
26
- *
27
- * The caller keeps ownership of *selection* state and of record→option mapping;
28
- * this hook only answers "what records match the current query". It is the
29
- * reusable core the search-first PeoplePicker (and a future org-tree tier)
30
- * compose on top of.
31
- *
32
- * Effect shape intentionally mirrors the original components to preserve
33
- * behaviour: the fetch effect keys on page/sort/filter/expand (NOT `search`,
34
- * which drives its own debounced fetch), and state resets live in a separate
35
- * `enabled`-keyed effect so they never cascade into a fetch (React #185).
36
- */
37
- export interface UseRecordQueryOptions {
38
- /** Backing data source. When absent/invalid the hook stays idle. */
39
- dataSource?: DataSource | null;
40
- /** Object/resource name to query (e.g. `sys_user`). */
41
- objectName?: string | null;
42
- /**
43
- * Gate fetching. When false no request is issued and query state is cleared.
44
- * Typically wired to a dialog's `open`, plus any "dependencies satisfied"
45
- * guard. Default `true`.
46
- */
47
- enabled?: boolean;
48
- /** `$top` — page size. Default 50. */
49
- pageSize?: number;
50
- /**
51
- * When true, use page-based pagination (`$skip = (page - 1) * pageSize`).
52
- * When false (default) a single page of `pageSize` records is fetched.
53
- */
54
- paginate?: boolean;
55
- /**
56
- * `$filter` — already merged by the caller (base `lookup_filters`, dependent
57
- * lookup chain, candidate hygiene like `banned != true`, …). Compared by
58
- * value, so a referentially-new-but-equal object each render will not loop.
59
- */
60
- filter?: Record<string, any>;
61
- /** `$expand` — related entities to include (e.g. `['primary_business_unit_id']`). */
62
- expand?: string[];
63
- /** `$searchFields` — narrow the server searchable set (ADR-0061). */
64
- searchFields?: string[];
65
- /** Debounce applied to {@link UseRecordQueryResult.setSearch}, in ms. Default 300. */
66
- debounceMs?: number;
67
- }
68
-
69
- export interface RecordQuerySort {
70
- field: string;
71
- direction: 'asc' | 'desc';
72
- }
73
-
74
- export interface UseRecordQueryResult {
75
- /** Records returned by the current query. */
76
- records: any[];
77
- loading: boolean;
78
- error: string | null;
79
- /** Total matching records (server-reported, else the current page length). */
80
- total: number;
81
- /** `ceil(total / pageSize)`, at least 1. */
82
- totalPages: number;
83
- page: number;
84
- search: string;
85
- sort: RecordQuerySort | null;
86
- /** Jump to a page (1-indexed). No-op unless `paginate` is set. */
87
- setPage: (page: number) => void;
88
- /** Set the search term; debounced, and resets to page 1. */
89
- setSearch: (query: string) => void;
90
- /** Toggle sort on a field (new field → asc, same field → flip); resets to page 1. */
91
- toggleSort: (field: string) => void;
92
- /** Set the sort directly (or clear with `null`); resets to page 1. */
93
- setSort: (sort: RecordQuerySort | null) => void;
94
- /** Clear query state (search/page/sort/records/error). */
95
- reset: () => void;
96
- /** Imperatively refetch with the current params. */
97
- refetch: () => void;
98
- }
99
-
100
- export function useRecordQuery(options: UseRecordQueryOptions): UseRecordQueryResult {
101
- const {
102
- dataSource,
103
- objectName,
104
- enabled = true,
105
- pageSize = 50,
106
- paginate = false,
107
- filter,
108
- expand,
109
- searchFields,
110
- debounceMs = 300,
111
- } = options;
112
-
113
- const [records, setRecords] = useState<any[]>([]);
114
- const [loading, setLoading] = useState(false);
115
- const [error, setError] = useState<string | null>(null);
116
- const [total, setTotal] = useState(0);
117
- const [page, setPageState] = useState(1);
118
- const [search, setSearchState] = useState('');
119
- const [sort, setSortState] = useState<RecordQuerySort | null>(null);
120
-
121
- const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
122
-
123
- // Stable signatures for object/array inputs so the fetch effect keys on their
124
- // *value*, not a fresh reference every render.
125
- const filterSignature = useMemo(
126
- () => (filter && Object.keys(filter).length ? JSON.stringify(filter) : ''),
127
- [filter],
128
- );
129
- const expandSignature = useMemo(() => (expand?.length ? expand.join(',') : ''), [expand]);
130
- const searchFieldsSignature = useMemo(
131
- () => (searchFields?.length ? searchFields.join(',') : ''),
132
- [searchFields],
133
- );
134
-
135
- const canQuery =
136
- !!enabled && !!dataSource && typeof dataSource.find === 'function' && !!objectName;
137
-
138
- // Core fetch. Takes the controlled inputs as arguments so the debounced
139
- // search path and the effect path share one implementation.
140
- const runQuery = useCallback(
141
- async (searchTerm: string, pageArg: number, sortArg: RecordQuerySort | null) => {
142
- if (!canQuery || !dataSource || !objectName) return;
143
-
144
- setLoading(true);
145
- setError(null);
146
-
147
- try {
148
- const params: QueryParams = { $top: pageSize };
149
- if (paginate) params.$skip = (pageArg - 1) * pageSize;
150
- if (searchTerm && searchTerm.trim()) params.$search = searchTerm.trim();
151
- if (searchFields && searchFields.length > 0) params.$searchFields = searchFields;
152
- if (sortArg) params.$orderby = { [sortArg.field]: sortArg.direction };
153
- if (filter && Object.keys(filter).length > 0) params.$filter = filter;
154
- if (expand && expand.length > 0) params.$expand = expand;
155
-
156
- const result = await dataSource.find(objectName, params);
157
- const data: any[] = result?.data ?? (result as any) ?? [];
158
-
159
- setRecords(data);
160
- setTotal(result?.total ?? data.length);
161
- } catch (err) {
162
- setError(err instanceof Error ? err.message : String(err));
163
- setRecords([]);
164
- } finally {
165
- setLoading(false);
166
- }
167
- },
168
- // filter/expand/searchFields are referenced via their signatures below so
169
- // this callback stays stable across referentially-new-but-equal props.
170
- // eslint-disable-next-line react-hooks/exhaustive-deps
171
- [canQuery, dataSource, objectName, pageSize, paginate, filterSignature, expandSignature, searchFieldsSignature],
172
- );
173
-
174
- // Fetch on enable and whenever page/sort/filter/expand change. `search` is
175
- // intentionally excluded — it has its own debounced path in `setSearch`.
176
- useEffect(() => {
177
- if (canQuery) {
178
- runQuery(search, page, sort);
179
- }
180
- // eslint-disable-next-line react-hooks/exhaustive-deps
181
- }, [canQuery, page, sort, runQuery]);
182
-
183
- // Reset query state when disabled — separate from the fetch effect so resets
184
- // don't re-trigger a fetch (React #185).
185
- useEffect(() => {
186
- if (!enabled) {
187
- setRecords([]);
188
- setError(null);
189
- setSearchState('');
190
- setPageState(1);
191
- setSortState(null);
192
- }
193
- }, [enabled]);
194
-
195
- // Clean up the debounce timer on unmount.
196
- useEffect(
197
- () => () => {
198
- if (debounceTimer.current) clearTimeout(debounceTimer.current);
199
- },
200
- [],
201
- );
202
-
203
- const setSearch = useCallback(
204
- (query: string) => {
205
- setSearchState(query);
206
- setPageState(1);
207
- if (debounceTimer.current) clearTimeout(debounceTimer.current);
208
- debounceTimer.current = setTimeout(() => {
209
- runQuery(query, 1, sort);
210
- }, debounceMs);
211
- },
212
- [runQuery, sort, debounceMs],
213
- );
214
-
215
- const setPage = useCallback((next: number) => {
216
- setPageState(next);
217
- }, []);
218
-
219
- const toggleSort = useCallback((field: string) => {
220
- setSortState(prev =>
221
- prev && prev.field === field
222
- ? { field, direction: prev.direction === 'asc' ? 'desc' : 'asc' }
223
- : { field, direction: 'asc' },
224
- );
225
- setPageState(1);
226
- }, []);
227
-
228
- const setSort = useCallback((next: RecordQuerySort | null) => {
229
- setSortState(next);
230
- setPageState(1);
231
- }, []);
232
-
233
- const reset = useCallback(() => {
234
- if (debounceTimer.current) clearTimeout(debounceTimer.current);
235
- setRecords([]);
236
- setError(null);
237
- setSearchState('');
238
- setPageState(1);
239
- setSortState(null);
240
- }, []);
241
-
242
- const refetch = useCallback(() => {
243
- runQuery(search, page, sort);
244
- }, [runQuery, search, page, sort]);
245
-
246
- const totalPages = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
247
-
248
- return {
249
- records,
250
- loading,
251
- error,
252
- total,
253
- totalPages,
254
- page,
255
- search,
256
- sort,
257
- setPage,
258
- setSearch,
259
- toggleSort,
260
- setSort,
261
- reset,
262
- refetch,
263
- };
264
- }
@@ -1,62 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { renderHook } from '@testing-library/react';
3
- import { useUploadingSignal } from './useUploadingSignal';
4
-
5
- /**
6
- * useUploadingSignal — surfaces an upload widget's in-progress flips to a host
7
- * (ActionParamDialog blocks Confirm while a file/image param uploads). It must
8
- * fire only when `uploading` changes and always call the latest callback.
9
- */
10
- describe('useUploadingSignal', () => {
11
- it('fires with the initial value on mount', () => {
12
- const cb = vi.fn();
13
- renderHook(() => useUploadingSignal(false, cb));
14
- expect(cb).toHaveBeenCalledTimes(1);
15
- expect(cb).toHaveBeenLastCalledWith(false);
16
- });
17
-
18
- it('fires again only when `uploading` changes', () => {
19
- const cb = vi.fn();
20
- const { rerender } = renderHook(({ u }) => useUploadingSignal(u, cb), {
21
- initialProps: { u: false },
22
- });
23
- cb.mockClear();
24
-
25
- rerender({ u: true });
26
- expect(cb).toHaveBeenCalledTimes(1);
27
- expect(cb).toHaveBeenLastCalledWith(true);
28
-
29
- // Same value → no extra fire.
30
- rerender({ u: true });
31
- expect(cb).toHaveBeenCalledTimes(1);
32
-
33
- rerender({ u: false });
34
- expect(cb).toHaveBeenCalledTimes(2);
35
- expect(cb).toHaveBeenLastCalledWith(false);
36
- });
37
-
38
- it('always invokes the latest callback (no stale closure)', () => {
39
- const first = vi.fn();
40
- const second = vi.fn();
41
- const { rerender } = renderHook(
42
- ({ cb, u }) => useUploadingSignal(u, cb),
43
- { initialProps: { cb: first, u: false } },
44
- );
45
- // Swap the callback without changing `uploading` → no fire yet.
46
- rerender({ cb: second, u: false });
47
- expect(second).not.toHaveBeenCalled();
48
- // Now flip `uploading` → the NEW callback fires, not the old one.
49
- rerender({ cb: second, u: true });
50
- expect(second).toHaveBeenCalledWith(true);
51
- expect(first).not.toHaveBeenCalledWith(true);
52
- });
53
-
54
- it('is a no-op when no callback is provided', () => {
55
- expect(() => {
56
- const { rerender } = renderHook(({ u }) => useUploadingSignal(u), {
57
- initialProps: { u: false },
58
- });
59
- rerender({ u: true });
60
- }).not.toThrow();
61
- });
62
- });
@@ -1,20 +0,0 @@
1
- import { useEffect, useRef } from 'react';
2
-
3
- /**
4
- * Notify a parent whenever an upload widget's in-progress state flips. Used by
5
- * hosts that must block a submit until the presigned upload resolves — e.g.
6
- * `ActionParamDialog` disables Confirm so a `file`/`image` param can't be
7
- * submitted mid-upload (the value only becomes the fileId once the upload
8
- * settles). Fires with the latest callback and only when `uploading` actually
9
- * changes, so an inline arrow prop doesn't thrash the parent every render.
10
- */
11
- export function useUploadingSignal(
12
- uploading: boolean,
13
- onUploadingChange?: (uploading: boolean) => void,
14
- ): void {
15
- const ref = useRef(onUploadingChange);
16
- ref.current = onUploadingChange;
17
- useEffect(() => {
18
- ref.current?.(uploading);
19
- }, [uploading]);
20
- }
@@ -1,75 +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
-
11
- /**
12
- * Adapt `SchemaRenderer`'s SDUI carrier onto the field-widget contract —
13
- * objectui#3233, the producer-side half of retiring `schema` from
14
- * {@link import('./widgets/types').FieldWidgetComponentProps}.
15
- *
16
- * ## Why this exists
17
- *
18
- * Two kinds of host render a registered field widget, and they name the
19
- * metadata differently *for good reason*:
20
- *
21
- * - the **form renderer** (`renderFieldComponent` in
22
- * `@object-ui/components`), the inline-edit hosts (`FieldEditWidget`) and
23
- * the action dialogs pass **`field`** — the field metadata they already
24
- * hold;
25
- * - **`SchemaRenderer`** (`@object-ui/react`) passes **`schema`** — the
26
- * authored node. That key is the UNIVERSAL SDUI contract *every* registered
27
- * component receives (`element:*`, `page:*`, `object-grid`,
28
- * `ReportRenderer` …). It is not being retired and must not be: it is what
29
- * the renderer means by "the node you were rendered from".
30
- *
31
- * What WAS wrong is that both keys reached the widget, so ~30 widgets resolved
32
- * their config as `field || schema` — one concept under two spellings, a second
33
- * de-facto contract on the widget side (AGENTS.md #0.1) and precisely the shape
34
- * that lets an AI-written widget pick one spelling and appear to work under one
35
- * host while reading `undefined` under the other.
36
- *
37
- * The translation belongs at the seam, exactly once: here. Downstream `field`
38
- * is the only carrier, and a widget reading `props.schema` no longer compiles.
39
- *
40
- * ## Guarantee
41
- *
42
- * Payload-preserving by construction: the node is forwarded **by reference**,
43
- * so a widget's `field` is the very object it used to read through `schema`.
44
- * `field` wins when a host supplies both (the form path always does, and it
45
- * holds the resolved metadata rather than the raw node).
46
- *
47
- * `schema` is *consumed* here rather than forwarded — with the key gone from
48
- * the contract, passing it on would only reach the DOM as a stray attribute
49
- * through a widget's `...props` spread.
50
- *
51
- * ## For widget authors outside this repo
52
- *
53
- * Registering a widget raw means `SchemaRenderer` hands it `schema` and no
54
- * `field`. Wrap it once at registration and the widget only ever implements
55
- * `FieldWidgetComponentProps`:
56
- *
57
- * ```tsx
58
- * import { withFieldCarrier } from '@object-ui/fields';
59
- *
60
- * ComponentRegistry.register('color', withFieldCarrier(ColorPickerField), {
61
- * namespace: 'field',
62
- * });
63
- * ```
64
- */
65
- export function withFieldCarrier<P extends object>(
66
- FieldWidget: React.ComponentType<P>,
67
- ): React.FC<Record<string, any>> {
68
- const FieldCarrierAdapter: React.FC<Record<string, any>> = ({ schema, field, ...props }) => (
69
- <FieldWidget {...(props as unknown as P)} field={field ?? schema} />
70
- );
71
- FieldCarrierAdapter.displayName = `FieldCarrier(${
72
- (FieldWidget as any).displayName || (FieldWidget as any).name || 'Component'
73
- })`;
74
- return FieldCarrierAdapter;
75
- }