@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,92 +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
- * Pure display helpers for person (sys_user) records, shared by the
11
- * search-first PeoplePicker's rich rows, the SelectionTray chips, and the
12
- * read-only user cell renderer. Kept framework-free so they're trivially
13
- * unit-testable and reusable by a future org-tree tier.
14
- */
15
-
16
- /** Resolve a possibly-dotted path (e.g. `primary_business_unit_id.name`) against a record. */
17
- export function resolvePath(record: any, path: string): any {
18
- if (!record || !path) return undefined;
19
- if (!path.includes('.')) return record[path];
20
- return path.split('.').reduce((acc, key) => (acc == null ? acc : acc[key]), record);
21
- }
22
-
23
- /** Best display name for a person record, tolerating expanded / raw shapes. */
24
- export function getPersonName(record: any, nameField = 'name'): string {
25
- if (record == null) return '';
26
- if (typeof record !== 'object') return String(record);
27
- return String(record[nameField] ?? record.name ?? record.username ?? record.label ?? '');
28
- }
29
-
30
- /**
31
- * Up-to-2-char initials for the avatar fallback.
32
- * - "John Doe" → "JD"; "John" → "JO"
33
- * - CJK single token → trailing 1–2 chars (given name): "张三" → "张三", "王小明" → "小明"
34
- */
35
- export function getPersonInitials(name: string): string {
36
- const trimmed = (name || '').trim();
37
- if (!trimmed) return '?';
38
- const parts = trimmed.split(/\s+/).filter(Boolean);
39
- if (parts.length >= 2) {
40
- return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
41
- }
42
- const token = parts[0];
43
- if (/[㐀-鿿]/.test(token)) {
44
- return token.length <= 2 ? token : token.slice(-2);
45
- }
46
- return token.slice(0, 2).toUpperCase();
47
- }
48
-
49
- /** Secondary line: resolve each field path, drop empties, join with " · ". */
50
- export function getPersonSubtitle(record: any, fields?: string[]): string {
51
- if (!record || !fields || fields.length === 0) return '';
52
- return fields
53
- .map(f => resolvePath(record, f))
54
- .filter(v => v != null && v !== '')
55
- .map(v => String(v))
56
- .join(' · ');
57
- }
58
-
59
- /** Avatar image URL (defaults to `sys_user.image`); undefined when absent. */
60
- export function getPersonAvatarUrl(record: any, avatarField = 'image'): string | undefined {
61
- const v = record ? resolvePath(record, avatarField) : undefined;
62
- return v ? String(v) : undefined;
63
- }
64
-
65
- /** Record id, tolerating `id` / `_id` / a custom id field. */
66
- export function getPersonId(record: any, idField = 'id'): any {
67
- if (!record || typeof record !== 'object') return record;
68
- return record[idField] ?? record.id ?? record._id;
69
- }
70
-
71
- /**
72
- * Case-insensitive literal-substring match ranges of `query` in `text`, as
73
- * `[start, end)` index pairs. Used to highlight the typed term in candidate
74
- * rows — a strong disambiguation aid for same-named people. Pinyin matches are
75
- * resolved server-side and won't have client ranges; that's fine, the row still
76
- * shows, just without a highlight.
77
- */
78
- export function matchRanges(text: string, query: string): Array<[number, number]> {
79
- const t = text ?? '';
80
- const q = (query ?? '').trim().toLowerCase();
81
- if (!q || !t) return [];
82
- const lower = t.toLowerCase();
83
- const ranges: Array<[number, number]> = [];
84
- let from = 0;
85
- for (;;) {
86
- const idx = lower.indexOf(q, from);
87
- if (idx === -1) break;
88
- ranges.push([idx, idx + q.length]);
89
- from = idx + q.length;
90
- }
91
- return ranges;
92
- }
@@ -1,45 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { getRecentLookupIds, pushRecentLookupId } from './recentLookups';
3
-
4
- describe('recentLookups', () => {
5
- beforeEach(() => localStorage.clear());
6
-
7
- it('returns [] when nothing stored', () => {
8
- expect(getRecentLookupIds('acc')).toEqual([]);
9
- });
10
-
11
- it('stores and returns ids most-recent-first', () => {
12
- pushRecentLookupId('acc', 'a');
13
- pushRecentLookupId('acc', 'b');
14
- expect(getRecentLookupIds('acc')).toEqual(['b', 'a']);
15
- });
16
-
17
- it('de-dupes and moves a re-picked id to the front', () => {
18
- pushRecentLookupId('acc', 'a');
19
- pushRecentLookupId('acc', 'b');
20
- pushRecentLookupId('acc', 'a');
21
- expect(getRecentLookupIds('acc')).toEqual(['a', 'b']);
22
- });
23
-
24
- it('caps at 5 entries', () => {
25
- for (const id of ['a', 'b', 'c', 'd', 'e', 'f', 'g']) pushRecentLookupId('acc', id);
26
- const ids = getRecentLookupIds('acc');
27
- expect(ids).toHaveLength(5);
28
- expect(ids[0]).toBe('g');
29
- expect(ids).not.toContain('a');
30
- });
31
-
32
- it('scopes per object name', () => {
33
- pushRecentLookupId('acc', 'a');
34
- pushRecentLookupId('prod', 'x');
35
- expect(getRecentLookupIds('acc')).toEqual(['a']);
36
- expect(getRecentLookupIds('prod')).toEqual(['x']);
37
- });
38
-
39
- it('ignores empty/nullish ids and object names', () => {
40
- pushRecentLookupId('acc', '');
41
- pushRecentLookupId('acc', null);
42
- pushRecentLookupId('', 'a');
43
- expect(getRecentLookupIds('acc')).toEqual([]);
44
- });
45
- });
@@ -1,47 +0,0 @@
1
- /**
2
- * Recently-used lookup memory.
3
- *
4
- * Mainstream record pickers (Salesforce, Dataverse, SAP, ServiceNow) surface
5
- * the user's recently-picked records the moment a lookup is focused — before
6
- * any typing. We approximate that with a small per-object ring of the last few
7
- * selected record ids, kept in localStorage (no backend dependency). When a
8
- * recent-items API later exists, this module is the single seam to swap.
9
- */
10
-
11
- const KEY_PREFIX = 'objectui:lookup:recent:';
12
- const MAX_RECENT = 5;
13
-
14
- function storage(): Storage | null {
15
- try {
16
- return typeof localStorage !== 'undefined' ? localStorage : null;
17
- } catch {
18
- // Accessing localStorage can throw in sandboxed iframes / SSR.
19
- return null;
20
- }
21
- }
22
-
23
- /** Most-recent-first list of record ids previously chosen for `objectName`. */
24
- export function getRecentLookupIds(objectName: string): Array<string | number> {
25
- const s = storage();
26
- if (!objectName || !s) return [];
27
- try {
28
- const raw = s.getItem(KEY_PREFIX + objectName);
29
- if (!raw) return [];
30
- const arr = JSON.parse(raw);
31
- return Array.isArray(arr) ? arr.slice(0, MAX_RECENT) : [];
32
- } catch {
33
- return [];
34
- }
35
- }
36
-
37
- /** Record that `id` was just chosen for `objectName` (moves it to the front). */
38
- export function pushRecentLookupId(objectName: string, id: string | number | null | undefined): void {
39
- const s = storage();
40
- if (!objectName || id === null || id === undefined || id === '' || !s) return;
41
- try {
42
- const next = [id, ...getRecentLookupIds(objectName).filter((x) => x !== id)].slice(0, MAX_RECENT);
43
- s.setItem(KEY_PREFIX + objectName, JSON.stringify(next));
44
- } catch {
45
- // Ignore quota / serialization failures — recents are best-effort.
46
- }
47
- }
@@ -1,204 +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 {
10
- pickDomProps,
11
- type SduiDomPassThroughKey,
12
- type DomProps as CoreDomProps,
13
- } from '@object-ui/core';
14
- import type { FieldWidgetComponentProps, FieldWidgetDomProps } from './types';
15
-
16
- /**
17
- * The RUNTIME EXECUTOR of the "DOM pass-through" section of
18
- * {@link FieldWidgetComponentProps} (objectui#3291).
19
- *
20
- * ## The defect
21
- *
22
- * Field widgets forwarded their leftover props to a control with a bare
23
- * spread — `const { inputType, ...domProps } = props as any; <Input
24
- * {...domProps} />`. Whatever a host handed the widget therefore became a DOM
25
- * attribute. Measured on `origin/main`, a real form + a real widget:
26
- *
27
- * ```
28
- * <input placeholder="PH-f" zzcanary="CANARY-STR" zzcanaryobj="[object Object]"
29
- * zzcanarynum="42" id="…" type="text" value="" name="f">
30
- * ```
31
- *
32
- * The `[object Object]` this issue is named for is an authored field-config
33
- * key being `String()`-ed onto an attribute. React 19 does not warn: an
34
- * all-lowercase unknown attribute is passed through in complete silence, which
35
- * is why this survived so long.
36
- *
37
- * ## Why a whitelist, and not a list of keys to drop
38
- *
39
- * The biggest leak source is not any NAMED renderer prop — it is the open tail
40
- * of author-supplied keys. The form renderer destructures a fixed set of known
41
- * keys and forwards `...fieldProps` verbatim
42
- * (`components/src/renderers/form/form.tsx`), and `SchemaRenderer` is wider
43
- * still: it spreads the whole authored node as props and has no strip layer at
44
- * all, so on the SDUI path a widget's own spread is the ONLY line of defence.
45
- *
46
- * A blacklist enumerating today's renderer-only keys (`error`, `emptyHint`,
47
- * `dataSource`, `dependentValues`, `dependsOn`, `options`, `inputType`, …)
48
- * would pass every one of the canaries above and would not stop the next
49
- * authored key either. Only "keep the known-safe set, drop the rest" closes
50
- * it, which is what this function does.
51
- *
52
- * ## Declared = enforced
53
- *
54
- * {@link FieldWidgetComponentProps} already DECLARES the closed set of keys a
55
- * widget may receive, and {@link FieldWidgetDomProps} names the subset that may
56
- * legitimately reach a DOM element (objectui#3221). Until now that was a
57
- * type-level claim a widget could violate at runtime simply by spreading. This
58
- * function is that declaration's executable form, and TWO compile-time
59
- * assertions below bind them in both directions — forwarding an undeclared key
60
- * and declaring an unforwarded DOM key are each a compile error. Neither
61
- * direction can drift silently.
62
- *
63
- * ## Deliberately NOT forwarded
64
- *
65
- * `role` and other HTML global attributes are absent because the contract does
66
- * not declare them. They reached the DOM before this change only through the
67
- * open spread. If a field node should be able to author one, DECLARE it on
68
- * `FieldWidgetComponentProps` first and add it here — do not reopen the spread
69
- * (AGENTS.md #0.1: fix the contract, never widen the consumer).
70
- *
71
- * ## Where the mechanism lives now, and why the key list stayed here
72
- *
73
- * objectui#4425 phase 2 promoted this whitelist to the SDUI widget contract
74
- * generally, so the MECHANISM — filter by a declared key list plus the `aria-*`
75
- * / `data-*` open families — moved to `@object-ui/core`
76
- * (`utils/dom-props.ts`, `pickDomProps`), where every plugin package can reach
77
- * it without depending on `@object-ui/fields` (the objectui#4409
78
- * dependency-direction method: `plugin-chatbot` declares `@object-ui/core` and
79
- * does not declare this package).
80
- *
81
- * The KEY LIST did not move, because it is not the same list. Measured on
82
- * objectui#4431:
83
- *
84
- * - `name` and `disabled` are here and NOT in the SDUI set: both are legal
85
- * only on form controls, which is what every widget in this package renders
86
- * and what `FieldWidgetComponentProps` declares. `name` on the `<div>` an
87
- * SDUI container widget renders is a leak — one of the 14 attributes #4431
88
- * closed — and two widgets here already hand-strip it when they spread onto
89
- * something that is not a control (`ObjectRefField`'s trigger,
90
- * `FileField`'s dropzone).
91
- * - `role` is in the SDUI set and NOT here: `SchemaRenderer` resolves it for
92
- * every node from `@objectstack/spec`'s `AriaPropsSchema`, while this
93
- * contract deliberately does not declare it (see "Deliberately NOT
94
- * forwarded" above).
95
- *
96
- * So: one mechanism, two declarations, each bound by the assertions below to
97
- * the contract it executes. Not two judges.
98
- */
99
- const DOM_PASS_THROUGH_KEYS = [
100
- /* ── The contract's own "DOM pass-through" block, verbatim ─────────────── */
101
- 'id',
102
- 'name',
103
- 'autoFocus',
104
- 'tabIndex',
105
- 'onBlur',
106
- 'onFocus',
107
- 'onClick',
108
-
109
- /* ── Two more declared keys that ARE valid DOM attributes ──────────────── */
110
- //
111
- // Both are declared on `FieldWidgetComponentProps` under the
112
- // controlled-input contract rather than the DOM block, because widgets also
113
- // INTERPRET them (`className` gets composed with the widget's own classes;
114
- // `disabled` is OR-ed with `readonly`). They are still `class` and
115
- // `disabled` on the element, and every widget here already forwards them.
116
- //
117
- // Withholding them would make this helper a silent styling- and
118
- // interactivity-dropper: `<Input {...toDomProps(props)} />` — the shape this
119
- // change teaches every future widget author to write — would quietly lose
120
- // the host's className and disabled state. That is a NEW silent failure of
121
- // exactly the kind the whitelist exists to prevent, so they are forwarded.
122
- 'className',
123
- 'disabled',
124
- ] as const;
125
-
126
- type DomPassThroughKey = (typeof DOM_PASS_THROUGH_KEYS)[number];
127
-
128
- /**
129
- * Compile-time link to the declaration, direction 1 of 2: every key forwarded
130
- * at runtime must exist on {@link FieldWidgetComponentProps}. Deleting a key
131
- * from the contract without deleting it here is a compile error.
132
- *
133
- * Catches: helper forwards something the contract no longer declares.
134
- */
135
- type EveryForwardedKeyIsDeclared =
136
- DomPassThroughKey extends keyof FieldWidgetComponentProps ? true : never;
137
- const _everyForwardedKeyIsDeclared: EveryForwardedKeyIsDeclared = true;
138
- void _everyForwardedKeyIsDeclared;
139
-
140
- /**
141
- * Direction 2 of 2: every key of {@link FieldWidgetDomProps} — the contract's
142
- * DOM pass-through block — must be one this helper actually forwards. Adding a
143
- * DOM key to the contract without adding it to `DOM_PASS_THROUGH_KEYS` is a
144
- * compile error.
145
- *
146
- * Catches: DECLARED BUT NOT DELIVERED — a key that type-checks, reads as
147
- * supported, and silently never reaches the element. That is the failure this
148
- * repo treats as first-class (objectui#3290's `aria-required` that never
149
- * reached a control; objectui#3222's validation slot nobody produced), and it
150
- * is the one direction the leak test structurally CANNOT see: that test looks
151
- * for attributes that arrive, not for ones that go missing.
152
- *
153
- * `className` / `disabled` are not part of `FieldWidgetDomProps` (they are
154
- * controlled-input keys this helper also forwards — see above), so they are
155
- * bound by direction 1 only. That is deliberate, not an oversight.
156
- */
157
- type EveryDeclaredDomKeyIsForwarded =
158
- keyof FieldWidgetDomProps extends DomPassThroughKey ? true : never;
159
- const _everyDeclaredDomKeyIsForwarded: EveryDeclaredDomKeyIsForwarded = true;
160
- void _everyDeclaredDomKeyIsForwarded;
161
-
162
- /**
163
- * Direction 3, added with the core lift (objectui#4431): every key of the SDUI
164
- * contract's own whitelist that this contract also declares must be forwarded
165
- * here too, so the two lists cannot silently drift apart. `role` is the ONE
166
- * measured exception — the SDUI contract declares it, this one does not — and
167
- * naming it in the `Exclude` is what makes the divergence a deliberate,
168
- * reviewable act rather than an omission.
169
- *
170
- * Catches: a key added to the shared SDUI set that field widgets quietly stop
171
- * delivering.
172
- */
173
- type SharedSduiKey = Exclude<SduiDomPassThroughKey, 'role'>;
174
- type EverySharedSduiKeyIsForwarded = SharedSduiKey extends DomPassThroughKey ? true : never;
175
- const _everySharedSduiKeyIsForwarded: EverySharedSduiKeyIsForwarded = true;
176
- void _everySharedSduiKeyIsForwarded;
177
-
178
- /** The subset of `P` this helper forwards, with each key's declared type. */
179
- export type DomProps<P> = CoreDomProps<P, DomPassThroughKey>;
180
-
181
- /**
182
- * Keep only what may legitimately become a DOM attribute, and drop everything
183
- * else — renderer plumbing, and any extra key an author put on the field
184
- * config or SDUI node.
185
- *
186
- * Replaces the bare `{...props}` spread in every field widget that renders a
187
- * host element:
188
- *
189
- * ```tsx
190
- * // before — forwards whatever it was handed
191
- * const { inputType, ...domProps } = props as any;
192
- * return <Input {...domProps} … />;
193
- *
194
- * // after
195
- * return <Input {...toDomProps(props)} … />;
196
- * ```
197
- *
198
- * Semantic props a widget INTERPRETS (`value`, `onChange`, `field`,
199
- * `readonly`, `error`, …) are read from `props` as before; this function only
200
- * governs what gets spread.
201
- */
202
- export function toDomProps<P extends object>(props: P): DomProps<P> {
203
- return pickDomProps(props, DOM_PASS_THROUGH_KEYS);
204
- }
@@ -1,102 +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 { toDomProps } from './toDomProps';
10
-
11
- /**
12
- * The props that make a group-labelled widget's rendered surface the thing its
13
- * host label actually NAMES, on the surfaces that are not controls
14
- * (objectui#3990).
15
- *
16
- * ## What it is for
17
- *
18
- * A widget declared `labelling: 'group'` (objectui#3961) is told by the form
19
- * renderer: your label publishes an `id` and drops its `for`, so name yourself
20
- * by IDREF. The renderer hands down exactly two facts that address the field as
21
- * a WHOLE:
22
- *
23
- * - `id` — the host's control id (`…-form-item`), the id the label's `for`
24
- * used to point at;
25
- * - `aria-labelledby` — the IDREF of that visible label.
26
- *
27
- * Every editable branch already consumes them inside its wider `toDomProps`
28
- * spread. The branches that returned EARLY did not — a field-level
29
- * `readonly: true`, or an option list with zero offered options — so the label
30
- * published an id that no element in the document referenced. Measured on
31
- * `origin/main`, one field per row, reading the host label against the DOM:
32
- *
33
- * ```
34
- * multiselect readonly+value consumers=0 byLabelText=0 namedByRole=[]
35
- * multiselect readonly+empty consumers=0 byLabelText=0 namedByRole=[]
36
- * multiselect zeroOptions consumers=0 byLabelText=0 namedByRole=[]
37
- * multiselect editable consumers=1 byLabelText=1 namedByRole=[group:1]
38
- * ```
39
- *
40
- * All seven group-labelled types measured the same, in every readonly state.
41
- * This is quieter than the dangling `for` #3961 replaced: a `for` that resolves
42
- * to nothing can at least be swept for, while "a label published an id and
43
- * nobody consumed it" looks like healthy markup.
44
- *
45
- * ## Why this narrow pair, and not the whole `toDomProps` result
46
- *
47
- * Everything else in the DOM pass-through addresses a CONTROL, and a readonly
48
- * display has none: `aria-describedby` / `aria-required` are announced when
49
- * focus lands on something focusable, `disabled` / `tabIndex` / the focus
50
- * handlers only mean something on an interactive element, and `name` is
51
- * DOM-legal on form controls only — on a `div` it is exactly the leak
52
- * objectui#3291 sweeps for. Spreading the full result would also put the host's
53
- * `className` on top of the widget's own, because the widgets whose readonly
54
- * surface is a placeholder or a plain container (`EmptyValue`, `FileField`,
55
- * `AddressField`) keep `className` inside `props`.
56
- *
57
- * ## Why `role` travels with the two keys
58
- *
59
- * `aria-labelledby` on a role-less `div` / `span` names NOTHING — `generic`
60
- * prohibits an author name — so the pair is inert without a role that supports
61
- * naming. `group` is the one that fits a readonly surface: it names a set of
62
- * values (chips, checked labels, stars, file names, a formatted address)
63
- * without promising the interactivity `textbox` or `radiogroup` would. It is
64
- * deliberately NOT the role the same widget answers with while editable:
65
- * `RadioField`'s readonly branch renders the chosen label as text with no radios
66
- * left in it, and `FileField`'s renders file names with no dropzone button.
67
- *
68
- * The role is emitted ONLY when a host actually named the field, mirroring
69
- * every editable branch's `isLabelledGroup` test: standalone rendering (the
70
- * grid's inline cell editor, a bare SDUI node) hands down neither key, so every
71
- * value here is `undefined`, React emits no attribute, and that markup stays
72
- * byte-identical to what it was.
73
- */
74
- export interface HostGroupProps {
75
- /** The host's control id — the id its label would have pointed `for` at. */
76
- id?: string;
77
- /** IDREF of the host's visible label. */
78
- 'aria-labelledby'?: string;
79
- /** `'group'` when — and only when — a host named this surface. */
80
- role?: 'group';
81
- }
82
-
83
- /**
84
- * Read the two whole-field keys out of a widget's leftover props, plus the role
85
- * that makes them mean something. See {@link HostGroupProps}.
86
- *
87
- * Routed through {@link toDomProps} rather than reading `props` directly so the
88
- * pair keeps ONE gate: those keys reach an element only if the DOM pass-through
89
- * whitelist still forwards them, and its two compile-time assertions bind that
90
- * whitelist to the props contract in both directions.
91
- */
92
- export function toHostGroupProps(props: {
93
- id?: string;
94
- 'aria-labelledby'?: string;
95
- }): HostGroupProps {
96
- const { id, 'aria-labelledby': labelledBy } = toDomProps(props);
97
- return {
98
- id,
99
- 'aria-labelledby': labelledBy,
100
- ...(labelledBy != null ? { role: 'group' as const } : null),
101
- };
102
- }