@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,291 +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
- * Either the `QueryParams.$filter` record form or a lowered ObjectQL AST node
61
- * (an array), since the picker's merge now produces both (#3831). Typed
62
- * `unknown` rather than `Record< string, any >` so an array cannot slip
63
- * through a type that silently accepts it; emptiness is decided by
64
- * {@link hasFilter}, not by `Object.keys`.
65
- */
66
- filter?: unknown;
67
- /** `$expand` — related entities to include (e.g. `['primary_business_unit_id']`). */
68
- expand?: string[];
69
- /** `$searchFields` — narrow the server searchable set (ADR-0061). */
70
- searchFields?: string[];
71
- /** Debounce applied to {@link UseRecordQueryResult.setSearch}, in ms. Default 300. */
72
- debounceMs?: number;
73
- }
74
-
75
- export interface RecordQuerySort {
76
- field: string;
77
- direction: 'asc' | 'desc';
78
- }
79
-
80
- export interface UseRecordQueryResult {
81
- /** Records returned by the current query. */
82
- records: any[];
83
- loading: boolean;
84
- error: string | null;
85
- /** Total matching records (server-reported, else the current page length). */
86
- total: number;
87
- /** `ceil(total / pageSize)`, at least 1. */
88
- totalPages: number;
89
- page: number;
90
- search: string;
91
- sort: RecordQuerySort | null;
92
- /** Jump to a page (1-indexed). No-op unless `paginate` is set. */
93
- setPage: (page: number) => void;
94
- /** Set the search term; debounced, and resets to page 1. */
95
- setSearch: (query: string) => void;
96
- /** Toggle sort on a field (new field → asc, same field → flip); resets to page 1. */
97
- toggleSort: (field: string) => void;
98
- /** Set the sort directly (or clear with `null`); resets to page 1. */
99
- setSort: (sort: RecordQuerySort | null) => void;
100
- /** Clear query state (search/page/sort/records/error). */
101
- reset: () => void;
102
- /** Imperatively refetch with the current params. */
103
- refetch: () => void;
104
- }
105
-
106
- /**
107
- * Is this filter source worth sending as `$filter` at all?
108
- *
109
- * Array-aware on purpose. The picker's merge yields either the
110
- * `QueryParams.$filter` record form or a lowered ObjectQL AST node (#3831), and
111
- * `Object.keys` on an array returns its INDICES — so the record-only test read
112
- * `['0','1','2']` for an AST node and was right only by accident. An empty array
113
- * is "no filter" for the same reason an empty object is.
114
- */
115
- function hasFilter(filter: unknown): boolean {
116
- if (filter === null || filter === undefined) return false;
117
- if (Array.isArray(filter)) return filter.length > 0;
118
- if (typeof filter !== 'object') return false;
119
- return Object.keys(filter as object).length > 0;
120
- }
121
-
122
- export function useRecordQuery(options: UseRecordQueryOptions): UseRecordQueryResult {
123
- const {
124
- dataSource,
125
- objectName,
126
- enabled = true,
127
- pageSize = 50,
128
- paginate = false,
129
- filter,
130
- expand,
131
- searchFields,
132
- debounceMs = 300,
133
- } = options;
134
-
135
- const [records, setRecords] = useState<any[]>([]);
136
- const [loading, setLoading] = useState(false);
137
- const [error, setError] = useState<string | null>(null);
138
- const [total, setTotal] = useState(0);
139
- const [page, setPageState] = useState(1);
140
- const [search, setSearchState] = useState('');
141
- const [sort, setSortState] = useState<RecordQuerySort | null>(null);
142
-
143
- const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
144
-
145
- // Stable signatures for object/array inputs so the fetch effect keys on their
146
- // *value*, not a fresh reference every render.
147
- const filterSignature = useMemo(
148
- () => (hasFilter(filter) ? JSON.stringify(filter) : ''),
149
- [filter],
150
- );
151
- const expandSignature = useMemo(() => (expand?.length ? expand.join(',') : ''), [expand]);
152
- const searchFieldsSignature = useMemo(
153
- () => (searchFields?.length ? searchFields.join(',') : ''),
154
- [searchFields],
155
- );
156
-
157
- const canQuery =
158
- !!enabled && !!dataSource && typeof dataSource.find === 'function' && !!objectName;
159
-
160
- // Core fetch. Takes the controlled inputs as arguments so the debounced
161
- // search path and the effect path share one implementation.
162
- const runQuery = useCallback(
163
- async (searchTerm: string, pageArg: number, sortArg: RecordQuerySort | null) => {
164
- if (!canQuery || !dataSource || !objectName) return;
165
-
166
- setLoading(true);
167
- setError(null);
168
-
169
- try {
170
- const params: QueryParams = { $top: pageSize };
171
- if (paginate) params.$skip = (pageArg - 1) * pageSize;
172
- if (searchTerm && searchTerm.trim()) params.$search = searchTerm.trim();
173
- if (searchFields && searchFields.length > 0) params.$searchFields = searchFields;
174
- if (sortArg) params.$orderby = { [sortArg.field]: sortArg.direction };
175
- // `QueryParams.$filter` is declared `Record< string, any >`, which the
176
- // AST-array form does not describe — the cast is at this ONE assignment
177
- // rather than widening a shared type that several other producers
178
- // (plugin-list's `buildEffectiveFilter`, plugin-view's ObjectView)
179
- // already feed arrays through.
180
- if (hasFilter(filter)) params.$filter = filter as Record<string, any>;
181
- if (expand && expand.length > 0) params.$expand = expand;
182
-
183
- const result = await dataSource.find(objectName, params);
184
- const data: any[] = result?.data ?? (result as any) ?? [];
185
-
186
- setRecords(data);
187
- setTotal(result?.total ?? data.length);
188
- } catch (err) {
189
- setError(err instanceof Error ? err.message : String(err));
190
- setRecords([]);
191
- } finally {
192
- setLoading(false);
193
- }
194
- },
195
- // filter/expand/searchFields are referenced via their signatures below so
196
- // this callback stays stable across referentially-new-but-equal props.
197
- // eslint-disable-next-line react-hooks/exhaustive-deps
198
- [canQuery, dataSource, objectName, pageSize, paginate, filterSignature, expandSignature, searchFieldsSignature],
199
- );
200
-
201
- // Fetch on enable and whenever page/sort/filter/expand change. `search` is
202
- // intentionally excluded — it has its own debounced path in `setSearch`.
203
- useEffect(() => {
204
- if (canQuery) {
205
- runQuery(search, page, sort);
206
- }
207
- // eslint-disable-next-line react-hooks/exhaustive-deps
208
- }, [canQuery, page, sort, runQuery]);
209
-
210
- // Reset query state when disabled — separate from the fetch effect so resets
211
- // don't re-trigger a fetch (React #185).
212
- useEffect(() => {
213
- if (!enabled) {
214
- setRecords([]);
215
- setError(null);
216
- setSearchState('');
217
- setPageState(1);
218
- setSortState(null);
219
- }
220
- }, [enabled]);
221
-
222
- // Clean up the debounce timer on unmount.
223
- useEffect(
224
- () => () => {
225
- if (debounceTimer.current) clearTimeout(debounceTimer.current);
226
- },
227
- [],
228
- );
229
-
230
- const setSearch = useCallback(
231
- (query: string) => {
232
- setSearchState(query);
233
- setPageState(1);
234
- if (debounceTimer.current) clearTimeout(debounceTimer.current);
235
- debounceTimer.current = setTimeout(() => {
236
- runQuery(query, 1, sort);
237
- }, debounceMs);
238
- },
239
- [runQuery, sort, debounceMs],
240
- );
241
-
242
- const setPage = useCallback((next: number) => {
243
- setPageState(next);
244
- }, []);
245
-
246
- const toggleSort = useCallback((field: string) => {
247
- setSortState(prev =>
248
- prev && prev.field === field
249
- ? { field, direction: prev.direction === 'asc' ? 'desc' : 'asc' }
250
- : { field, direction: 'asc' },
251
- );
252
- setPageState(1);
253
- }, []);
254
-
255
- const setSort = useCallback((next: RecordQuerySort | null) => {
256
- setSortState(next);
257
- setPageState(1);
258
- }, []);
259
-
260
- const reset = useCallback(() => {
261
- if (debounceTimer.current) clearTimeout(debounceTimer.current);
262
- setRecords([]);
263
- setError(null);
264
- setSearchState('');
265
- setPageState(1);
266
- setSortState(null);
267
- }, []);
268
-
269
- const refetch = useCallback(() => {
270
- runQuery(search, page, sort);
271
- }, [runQuery, search, page, sort]);
272
-
273
- const totalPages = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
274
-
275
- return {
276
- records,
277
- loading,
278
- error,
279
- total,
280
- totalPages,
281
- page,
282
- search,
283
- sort,
284
- setPage,
285
- setSearch,
286
- toggleSort,
287
- setSort,
288
- reset,
289
- refetch,
290
- };
291
- }
@@ -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
- }