@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,175 +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
- * `TextAreaField`'s fullscreen affordance has ONE source (objectui#3232).
11
- *
12
- * It used to be read from four places with a `??` chain — a `mobileFullscreen`
13
- * (camelCase) prop, `field.mobile_fullscreen`, a `mobile_fullscreen` prop, and
14
- * `schema.mobile_fullscreen`. Only the field-metadata flag was ever produced:
15
- * `ObjectForm` stamps it on long-text fields from
16
- * `ObjectFormSchema.mobile.fullscreenLongText`. The camelCase prop had no
17
- * producer anywhere in the repo, and the snake_case prop could not arrive
18
- * because `stripRegisteredFieldProps` (components/src/renderers/form/form.tsx)
19
- * removes it from what registered widgets are forwarded.
20
- *
21
- * So this file pins both halves of that convergence:
22
- *
23
- * 1. **The live path still works.** The metadata flag drives the affordance
24
- * end to end — button, dialog, and the committed edit. Deleting the dead
25
- * reads must not have cost the working behaviour, and this is the test
26
- * that fails if a future refactor drops the real read too.
27
- * 2. **The retired prop spellings are gone**, at compile time (they are not
28
- * on the closed `FieldWidgetComponentProps`, objectui#3221) and at runtime
29
- * (a host that passes one anyway gets no affordance). Convergence means a
30
- * misspelled flag is now inert and loud, not silently absorbed.
31
- *
32
- * Note the built-in (unregistered) `textarea` branch in `form.tsx` reads
33
- * `mobile_fullscreen || fullscreen` off the form-field props and renders its
34
- * own `FullscreenTextarea`. That is a separate, live path and is untouched
35
- * here — this file is about the registered `field:textarea` widget only.
36
- */
37
-
38
- import { describe, it, expect, vi } from 'vitest';
39
- import { render, screen, fireEvent } from '@testing-library/react';
40
- import '@testing-library/jest-dom';
41
-
42
- import { TextAreaField } from '../TextAreaField';
43
- import type { FieldWidgetComponentProps } from '../types';
44
- import type { FieldMetadata } from '@object-ui/types';
45
-
46
- const fieldMeta = (extra: Record<string, unknown> = {}) =>
47
- ({
48
- name: 'description',
49
- label: 'Description',
50
- type: 'textarea',
51
- ...extra,
52
- }) as unknown as FieldMetadata;
53
-
54
- describe('TextAreaField mobile fullscreen — the metadata flag is the only source', () => {
55
- it('renders the expand affordance when the field metadata sets mobile_fullscreen', () => {
56
- render(
57
- <TextAreaField
58
- value="hello"
59
- onChange={() => {}}
60
- field={fieldMeta({ mobile_fullscreen: true })}
61
- />,
62
- );
63
-
64
- expect(screen.getByTestId('textarea-fullscreen-toggle')).toBeInTheDocument();
65
- });
66
-
67
- it('opens the fullscreen dialog and commits the edited draft', () => {
68
- const onChange = vi.fn();
69
- render(
70
- <TextAreaField
71
- value="hello"
72
- onChange={onChange}
73
- field={fieldMeta({ mobile_fullscreen: true })}
74
- />,
75
- );
76
-
77
- // Closed until the affordance is used — the dialog is not just mounted.
78
- expect(screen.queryByTestId('textarea-fullscreen-dialog')).not.toBeInTheDocument();
79
-
80
- fireEvent.click(screen.getByTestId('textarea-fullscreen-toggle'));
81
- expect(screen.getByTestId('textarea-fullscreen-dialog')).toBeInTheDocument();
82
-
83
- const dialogInput = screen.getByTestId('textarea-fullscreen-input');
84
- expect(dialogInput).toHaveValue('hello');
85
-
86
- fireEvent.change(dialogInput, { target: { value: 'hello from fullscreen' } });
87
- // The draft is local until "Done" — the host is not notified per keystroke.
88
- expect(onChange).not.toHaveBeenCalled();
89
-
90
- fireEvent.click(screen.getByTestId('textarea-fullscreen-save'));
91
- expect(onChange).toHaveBeenCalledWith('hello from fullscreen');
92
- });
93
-
94
- it('no longer reads the flag off a `schema` prop (objectui#3233)', () => {
95
- // This used to be the second carrier: `SchemaRenderer` passed the authored
96
- // node as `schema` and the widget resolved `field || schema`. v17 converged
97
- // that at the producer — the registry adapter (`withFieldCarrier`) maps the
98
- // SDUI node onto `field` before any widget sees it, so the widget has one
99
- // read. A host that still passes `schema` is now inert AND loud: a compile
100
- // error on the closed props type, and no affordance at runtime.
101
- const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
102
- try {
103
- const legacyHostProps = {
104
- schema: fieldMeta({ mobile_fullscreen: true }),
105
- } as unknown as Partial<FieldWidgetComponentProps<string>>;
106
-
107
- render(
108
- <TextAreaField
109
- value=""
110
- onChange={() => {}}
111
- field={fieldMeta()}
112
- {...legacyHostProps}
113
- />,
114
- );
115
-
116
- expect(screen.queryByTestId('textarea-fullscreen-toggle')).not.toBeInTheDocument();
117
- } finally {
118
- consoleError.mockRestore();
119
- }
120
- });
121
-
122
- it('renders no affordance when the metadata does not opt in', () => {
123
- render(<TextAreaField value="hello" onChange={() => {}} field={fieldMeta()} />);
124
-
125
- expect(screen.queryByTestId('textarea-fullscreen-toggle')).not.toBeInTheDocument();
126
- expect(screen.queryByTestId('textarea-fullscreen-dialog')).not.toBeInTheDocument();
127
- });
128
- });
129
-
130
- describe('TextAreaField mobile fullscreen — the retired prop spellings', () => {
131
- it('does not declare them on the widget contract', () => {
132
- const props = {} as FieldWidgetComponentProps<string>;
133
-
134
- // Both were read by the widget and written by no host. With the closed
135
- // props type (objectui#3221) they are now compile errors, so a future
136
- // author reaches for the metadata flag instead of a silently ignored prop.
137
- // @ts-expect-error `mobileFullscreen` is not part of this contract
138
- void props.mobileFullscreen;
139
- // @ts-expect-error `mobile_fullscreen` is a metadata key, not a widget prop
140
- void props.mobile_fullscreen;
141
- // …and the retired second METADATA carrier (objectui#3233). The flag has
142
- // one source and that source now has one carrier.
143
- // @ts-expect-error `schema` was retired from the widget contract in v17
144
- void props.schema;
145
-
146
- expect(true).toBe(true);
147
- });
148
-
149
- it('ignores them at runtime when a host passes them anyway', () => {
150
- // Untyped hosts (plain JS, `as any` spreads) can still get these onto the
151
- // element. They must not resurrect the affordance: the metadata flag is
152
- // the contract. Unknown props land on the DOM spread, so React's unknown
153
- // -attribute warnings are expected noise here and are suppressed.
154
- const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
155
- try {
156
- const hostProps = {
157
- mobileFullscreen: true,
158
- mobile_fullscreen: true,
159
- } as unknown as Partial<FieldWidgetComponentProps<string>>;
160
-
161
- render(
162
- <TextAreaField
163
- value="hello"
164
- onChange={() => {}}
165
- field={fieldMeta()}
166
- {...hostProps}
167
- />,
168
- );
169
-
170
- expect(screen.queryByTestId('textarea-fullscreen-toggle')).not.toBeInTheDocument();
171
- } finally {
172
- consoleError.mockRestore();
173
- }
174
- });
175
- });
@@ -1,19 +0,0 @@
1
- /**
2
- * Internal bridge module to break the circular dependency between
3
- * index.tsx (defines getCellRenderer) and RecordPickerDialog.tsx
4
- * (consumes it).
5
- *
6
- * index.tsx registers the resolver via setCellRendererResolver().
7
- * LookupField reads it via getCellRendererResolver() to pass as a prop.
8
- */
9
- import type { CellRendererResolver } from './RecordPickerDialog';
10
-
11
- let _resolver: CellRendererResolver | undefined;
12
-
13
- export function setCellRendererResolver(resolver: CellRendererResolver): void {
14
- _resolver = resolver;
15
- }
16
-
17
- export function getCellRendererResolver(): CellRendererResolver | undefined {
18
- return _resolver;
19
- }
@@ -1,112 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { deriveLookupColumns } from './deriveLookupColumns';
3
-
4
- const accountSchema = {
5
- name: 'showcase_account',
6
- fields: {
7
- created_at: { type: 'datetime', label: 'Created At' },
8
- updated_at: { type: 'datetime', label: 'Last Modified At' },
9
- name: { type: 'text', label: 'Account Name' },
10
- industry: { type: 'select', label: 'Industry' },
11
- annual_revenue: { type: 'currency', label: 'Annual Revenue' },
12
- website: { type: 'url', label: 'Website' },
13
- hq: { type: 'location', label: 'Headquarters' },
14
- status: { type: 'select', label: 'Lifecycle' },
15
- support_config: { type: 'json', label: 'Support Config' },
16
- owner_id: { type: 'lookup', label: 'Owner' },
17
- organization_id: { type: 'lookup', label: 'Organization' },
18
- },
19
- };
20
-
21
- describe('deriveLookupColumns', () => {
22
- it('returns [] when no schema/fields are available', () => {
23
- expect(deriveLookupColumns(undefined, { displayField: 'name' })).toEqual([]);
24
- expect(deriveLookupColumns({ fields: {} }, { displayField: 'name' })).toEqual([]);
25
- });
26
-
27
- it('leads with the display field and adds business fields in declaration order', () => {
28
- const cols = deriveLookupColumns(accountSchema, { displayField: 'name', max: 4 });
29
- expect(cols.map((c) => c.field)).toEqual(['name', 'industry', 'annual_revenue', 'website']);
30
- });
31
-
32
- it('carries field label and type onto each column for type-aware rendering', () => {
33
- const cols = deriveLookupColumns(accountSchema, { displayField: 'name', max: 3 });
34
- expect(cols[0]).toEqual({ field: 'name', label: 'Account Name', type: 'text' });
35
- expect(cols[2]).toEqual({ field: 'annual_revenue', label: 'Annual Revenue', type: 'currency' });
36
- });
37
-
38
- it('skips system/audit fields and heavy non-tabular types', () => {
39
- const cols = deriveLookupColumns(accountSchema, { displayField: 'name', max: 10 });
40
- const names = cols.map((c) => c.field);
41
- expect(names).not.toContain('created_at');
42
- expect(names).not.toContain('updated_at');
43
- expect(names).not.toContain('owner_id');
44
- expect(names).not.toContain('organization_id');
45
- expect(names).not.toContain('support_config'); // json
46
- expect(names).not.toContain('hq'); // location
47
- });
48
-
49
- it('respects the max column cap', () => {
50
- expect(deriveLookupColumns(accountSchema, { displayField: 'name', max: 2 })).toHaveLength(2);
51
- });
52
-
53
- it('skips fields flagged `system: true` even when the name is not a known audit column', () => {
54
- const schema = {
55
- fields: {
56
- name: { type: 'text', label: 'Name' },
57
- // Non-canonical name, but stamped system by the framework — must be skipped.
58
- custom_ref: { type: 'lookup', label: 'Custom Ref', system: true } as any,
59
- code: { type: 'text', label: 'Code' },
60
- },
61
- };
62
- const cols = deriveLookupColumns(schema, { displayField: 'name', max: 10 });
63
- expect(cols.map((c) => c.field)).toEqual(['name', 'code']);
64
- });
65
-
66
- it('honours an object-level displayFields list, with the display field first', () => {
67
- const schema = {
68
- fields: {
69
- name: { type: 'text', label: 'Name' },
70
- code: { type: 'text', label: 'Code' },
71
- region: { type: 'select', label: 'Region' },
72
- },
73
- displayFields: ['code', 'region'],
74
- };
75
- const cols = deriveLookupColumns(schema, { displayField: 'name', max: 4 });
76
- expect(cols.map((c) => c.field)).toEqual(['name', 'code', 'region']);
77
- });
78
-
79
- it('does not duplicate the display field when it also appears in displayFields', () => {
80
- const schema = {
81
- fields: { name: { type: 'text' }, region: { type: 'select' } },
82
- displayFields: ['name', 'region'],
83
- };
84
- const cols = deriveLookupColumns(schema, { displayField: 'name' });
85
- expect(cols.map((c) => c.field)).toEqual(['name', 'region']);
86
- });
87
-
88
- it('prefers ADR-0085 highlightFields over the declaration-order walk', () => {
89
- const schema = {
90
- fields: {
91
- name: { type: 'text', label: 'Name' },
92
- code: { type: 'text', label: 'Code' },
93
- region: { type: 'select', label: 'Region' },
94
- notes: { type: 'text', label: 'Notes' },
95
- },
96
- highlightFields: ['region', 'code'],
97
- };
98
- const cols = deriveLookupColumns(schema, { displayField: 'name', max: 4 });
99
- // Display field leads, then highlightFields in order — not the raw field walk.
100
- expect(cols.map((c) => c.field)).toEqual(['name', 'region', 'code']);
101
- });
102
-
103
- it('lets highlightFields win over legacy displayFields when both are present', () => {
104
- const schema = {
105
- fields: { name: { type: 'text' }, code: { type: 'text' }, region: { type: 'select' } },
106
- highlightFields: ['region'],
107
- displayFields: ['code'],
108
- };
109
- const cols = deriveLookupColumns(schema, { displayField: 'name' });
110
- expect(cols.map((c) => c.field)).toEqual(['name', 'region']);
111
- });
112
- });
@@ -1,108 +0,0 @@
1
- import type { LookupColumnDef } from '@object-ui/types';
2
- import { isSystemManagedField } from '@object-ui/types';
3
-
4
- /**
5
- * Field types that don't render usefully as a compact picker column (large
6
- * blobs, media, geometry, free-form text). Kept out of auto-derived columns.
7
- */
8
- const NON_TABULAR_TYPES = new Set<string>([
9
- 'json', 'object', 'grid', 'html', 'richtext', 'markdown', 'code',
10
- 'image', 'file', 'avatar', 'vector', 'location', 'geometry',
11
- 'textarea', 'secret', 'password', 'encrypted',
12
- ]);
13
-
14
- export interface DeriveColumnsOptions {
15
- /** Primary display field — always the leading column. */
16
- displayField: string;
17
- /** Maximum number of columns to derive (including the display field). */
18
- max?: number;
19
- }
20
-
21
- interface ObjectSchemaLike {
22
- fields?: Record<string, { type?: string; label?: string; hidden?: boolean; system?: boolean }>;
23
- /**
24
- * ADR-0085 semantic role: the object's most important fields. The single
25
- * source for "how to list this object" — shared with the detail-page related
26
- * list — so a lookup picker and a related list of the same object agree on
27
- * columns with zero per-surface config.
28
- */
29
- highlightFields?: unknown;
30
- /** Object-level "fields to display in search result cards" (legacy spec metadata). */
31
- displayFields?: unknown;
32
- }
33
-
34
- function dedupe(names: string[]): string[] {
35
- const seen = new Set<string>();
36
- const out: string[] = [];
37
- for (const n of names) {
38
- if (!seen.has(n)) {
39
- seen.add(n);
40
- out.push(n);
41
- }
42
- }
43
- return out;
44
- }
45
-
46
- /**
47
- * Derive sensible Record Picker columns from a referenced object's schema when
48
- * a lookup field declares no explicit `lookup_columns`.
49
- *
50
- * Priority:
51
- * 1. The referenced object's ADR-0085 `highlightFields` — the canonical
52
- * "how to list this object" set, shared with the detail-page related list.
53
- * 2. Else the object's legacy `displayFields` search-card shape.
54
- * 3. Otherwise the display field plus the next few business fields in
55
- * declaration order, skipping system/audit fields and heavy non-tabular
56
- * types.
57
- *
58
- * The result gives every lookup a multi-column, disambiguating picker with zero
59
- * field-level configuration. Column `type` is carried through so the picker can
60
- * render currency / select / date cells via the shared cell renderer.
61
- *
62
- * Returns an empty array when no usable schema is available, in which case the
63
- * caller should fall back to the single display-field column.
64
- */
65
- export function deriveLookupColumns(
66
- objectSchema: ObjectSchemaLike | null | undefined,
67
- { displayField, max = 4 }: DeriveColumnsOptions,
68
- ): LookupColumnDef[] {
69
- const fields = objectSchema?.fields ?? {};
70
- const fieldDef = (name: string) => fields[name] ?? {};
71
- const toCol = (name: string): LookupColumnDef => {
72
- const f = fieldDef(name);
73
- return { field: name, ...(f.label ? { label: f.label } : {}), ...(f.type ? { type: f.type } : {}) };
74
- };
75
-
76
- if (Object.keys(fields).length === 0) return [];
77
-
78
- // 1 & 2. Honour a declared column list — `highlightFields` (ADR-0085
79
- // canonical, shared with the related list) first, then the legacy
80
- // `displayFields` search-card list. The display field always leads.
81
- const fromDeclaredList = (list: unknown): LookupColumnDef[] | null => {
82
- if (!Array.isArray(list) || list.length === 0) return null;
83
- const names = (list as unknown[]).filter(
84
- (n): n is string => typeof n === 'string' && n.length > 0,
85
- );
86
- const ordered = dedupe([displayField, ...names.filter((n) => n !== displayField)]);
87
- return ordered.slice(0, max).map(toCol);
88
- };
89
- const fromHighlights = fromDeclaredList(objectSchema?.highlightFields);
90
- if (fromHighlights) return fromHighlights;
91
- const fromDisplay = fromDeclaredList(objectSchema?.displayFields);
92
- if (fromDisplay) return fromDisplay;
93
-
94
- // 3. Derive from the field set in declaration order.
95
- const candidates = Object.keys(fields).filter((name) => {
96
- if (name === displayField) return false;
97
- const f = fieldDef(name);
98
- // Skip framework-managed system/audit/ownership columns (incl. owner_id) —
99
- // they carry no disambiguating value in a record picker.
100
- if (isSystemManagedField(name, f)) return false;
101
- if (f.hidden) return false;
102
- if (f.type && NON_TABULAR_TYPES.has(f.type)) return false;
103
- return true;
104
- });
105
-
106
- const ordered = dedupe([displayField, ...candidates]);
107
- return ordered.slice(0, max).map(toCol);
108
- }
@@ -1,230 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import {
3
- isFileIdToken,
4
- fileIdOf,
5
- readFileValue,
6
- readFileValues,
7
- isImageValue,
8
- fileValueForSubmit,
9
- uploadResultView,
10
- withRecentUploads,
11
- } from './file-value';
12
-
13
- const uploadResult = (over: Record<string, unknown> = {}) => ({
14
- url: 'https://app.example.com/api/v1/storage/files/file_a',
15
- name: 'a.png',
16
- size: 1024,
17
- mimeType: 'image/png',
18
- ...over,
19
- });
20
-
21
- describe('isFileIdToken', () => {
22
- it.each(['file_a', 'abc123', '0e2f4c1a-9b7d-4e3f-8a1b-2c3d4e5f6a7b', 'A-_9'])(
23
- 'accepts the minted id %s',
24
- (v) => expect(isFileIdToken(v)).toBe(true),
25
- );
26
-
27
- it.each([
28
- 'https://cdn.example.com/a.png',
29
- '/api/v1/storage/files/file_a',
30
- 'data:image/png;base64,aGk=',
31
- 'blob:http://localhost/abc',
32
- 'a.png',
33
- '',
34
- ])('rejects the URL-shaped or dotted value %s', (v) => expect(isFileIdToken(v)).toBe(false));
35
-
36
- it('rejects non-strings and over-long values', () => {
37
- expect(isFileIdToken(null)).toBe(false);
38
- expect(isFileIdToken(42)).toBe(false);
39
- expect(isFileIdToken('a'.repeat(65))).toBe(false);
40
- });
41
- });
42
-
43
- describe('fileIdOf', () => {
44
- it('reads a bare reference', () => {
45
- expect(fileIdOf('file_a')).toBe('file_a');
46
- });
47
-
48
- it('prefers file_id, then id', () => {
49
- expect(fileIdOf({ file_id: 'legacy', id: 'expanded' })).toBe('legacy');
50
- expect(fileIdOf({ id: 'expanded' })).toBe('expanded');
51
- });
52
-
53
- it('returns undefined when there is no id to read', () => {
54
- expect(fileIdOf({ url: 'https://cdn.example.com/a.png', name: 'a.png' })).toBeUndefined();
55
- expect(fileIdOf('https://cdn.example.com/a.png')).toBeUndefined();
56
- expect(fileIdOf(null)).toBeUndefined();
57
- });
58
- });
59
-
60
- describe('readFileValue', () => {
61
- /**
62
- * The casing split is the reason this module exists: the platform's expanded
63
- * form is camelCase `mimeType`, the legacy inline blob is snake_case
64
- * `mime_type`. A widget reading only one silently stops recognising images
65
- * the moment the backend switches form.
66
- */
67
- it('reads the expanded form (camelCase mimeType)', () => {
68
- const view = readFileValue({
69
- id: 'file_a',
70
- name: 'a.png',
71
- size: 1024,
72
- mimeType: 'image/png',
73
- url: '/api/v1/storage/files/file_a',
74
- });
75
-
76
- expect(view).toMatchObject({
77
- id: 'file_a',
78
- name: 'a.png',
79
- size: 1024,
80
- mimeType: 'image/png',
81
- url: '/api/v1/storage/files/file_a',
82
- });
83
- expect(isImageValue(view)).toBe(true);
84
- });
85
-
86
- it('reads the legacy inline blob (snake_case mime_type)', () => {
87
- const view = readFileValue({
88
- file_id: 'file_a',
89
- name: 'a.png',
90
- original_name: 'original.png',
91
- size: 1024,
92
- mime_type: 'image/png',
93
- url: 'https://cdn.example.com/a.png',
94
- });
95
-
96
- expect(view).toMatchObject({ id: 'file_a', name: 'a.png', mimeType: 'image/png' });
97
- expect(isImageValue(view)).toBe(true);
98
- });
99
-
100
- it('falls back to original_name when name is absent', () => {
101
- expect(readFileValue({ original_name: 'orig.pdf' }).name).toBe('orig.pdf');
102
- });
103
-
104
- it('derives a name from the URL when the value carries none', () => {
105
- expect(readFileValue({ url: 'https://cdn.example.com/docs/report%20v2.pdf' }).name).toBe(
106
- 'report v2.pdf',
107
- );
108
- expect(readFileValue('https://cdn.example.com/a.png').name).toBe('a.png');
109
- });
110
-
111
- it('resolves a bare reference to the stable download URL', () => {
112
- const view = readFileValue('file_a', 'File');
113
-
114
- // The read path may leave a value in its bare-reference form (seen on the
115
- // edit-form data path); the stable endpoint is derivable from the id, so a
116
- // thumbnail still renders instead of a broken `<img src="">`.
117
- expect(view).toMatchObject({ id: 'file_a', name: 'File' });
118
- expect(view.url).toBe('/api/v1/storage/files/file_a');
119
- });
120
-
121
- it('derives the URL of an object that carries an id but no url', () => {
122
- const view = readFileValue({ id: 'file_b', name: 'b.png' });
123
- expect(view.url).toBe('/api/v1/storage/files/file_b');
124
- });
125
-
126
- it('treats a URL string as a URL, never as a reference', () => {
127
- const view = readFileValue('/api/v1/storage/files/file_a');
128
- expect(view.id).toBeUndefined();
129
- expect(view.url).toBe('/api/v1/storage/files/file_a');
130
- });
131
-
132
- it('uses the supplied fallback name so widgets stay localised', () => {
133
- expect(readFileValue({}, '文件').name).toBe('文件');
134
- expect(readFileValue(null, '文件').name).toBe('文件');
135
- });
136
-
137
- it('is not an image when the MIME type says otherwise or is missing', () => {
138
- expect(isImageValue(readFileValue({ mimeType: 'application/pdf' }))).toBe(false);
139
- expect(isImageValue(readFileValue({ name: 'a.png' }))).toBe(false);
140
- });
141
- });
142
-
143
- describe('readFileValues', () => {
144
- it('normalises single, array and empty values', () => {
145
- expect(readFileValues(null)).toEqual([]);
146
- expect(readFileValues([])).toEqual([]);
147
- expect(readFileValues('file_a')).toHaveLength(1);
148
- expect(readFileValues(['file_a', 'file_b'])).toHaveLength(2);
149
- });
150
-
151
- it('drops null entries inside an array', () => {
152
- expect(readFileValues(['file_a', null, 'file_b'])).toHaveLength(2);
153
- });
154
-
155
- it('handles a mixed-form array during the transition', () => {
156
- const views = readFileValues([
157
- 'file_a',
158
- { id: 'file_b', name: 'b.png', mimeType: 'image/png' },
159
- { name: 'c.png', mime_type: 'image/png', url: 'https://cdn.example.com/c.png' },
160
- ]);
161
-
162
- expect(views.map((v) => v.id)).toEqual(['file_a', 'file_b', undefined]);
163
- expect(views.map(isImageValue)).toEqual([false, true, true]);
164
- });
165
- });
166
-
167
- describe('fileValueForSubmit', () => {
168
- it('submits the bare reference when the adapter surfaced a fileId', () => {
169
- expect(fileValueForSubmit(uploadResult({ meta: { fileId: 'file_a' } }), 'orig.png')).toBe(
170
- 'file_a',
171
- );
172
- });
173
-
174
- /**
175
- * Back-compat: the object-URL fallback adapter, or a backend predating
176
- * file-as-reference, surfaces no fileId. The same build must keep working
177
- * there, so it submits the legacy blob unchanged.
178
- */
179
- it('falls back to the legacy inline blob when there is no fileId', () => {
180
- expect(fileValueForSubmit(uploadResult(), 'orig.png')).toEqual({
181
- name: 'a.png',
182
- original_name: 'orig.png',
183
- size: 1024,
184
- mime_type: 'image/png',
185
- url: 'https://app.example.com/api/v1/storage/files/file_a',
186
- });
187
- });
188
-
189
- it('ignores a meta.fileId that is not id-shaped', () => {
190
- const v = fileValueForSubmit(uploadResult({ meta: { fileId: 'https://evil.example/x' } }));
191
- expect(typeof v).toBe('object');
192
- });
193
-
194
- it('round-trips through readFileValue in both modes', () => {
195
- const asRef = fileValueForSubmit(uploadResult({ meta: { fileId: 'file_a' } }));
196
- const asBlob = fileValueForSubmit(uploadResult(), 'orig.png');
197
-
198
- expect(readFileValue(asRef).id).toBe('file_a');
199
- expect(readFileValue(asBlob).mimeType).toBe('image/png');
200
- });
201
- });
202
-
203
- describe('withRecentUploads', () => {
204
- it('fills in display details for a bare reference just uploaded', () => {
205
- const view = uploadResultView(uploadResult({ meta: { fileId: 'file_a' } }), 'orig.png');
206
- const merged = withRecentUploads(readFileValues('file_a'), { file_a: view });
207
-
208
- expect(merged[0]).toMatchObject({
209
- id: 'file_a',
210
- name: 'a.png',
211
- mimeType: 'image/png',
212
- url: 'https://app.example.com/api/v1/storage/files/file_a',
213
- });
214
- // The raw value stays the reference — display enrichment must not change
215
- // what the field submits.
216
- expect(merged[0].raw).toBe('file_a');
217
- });
218
-
219
- it('leaves a value the backend already expanded alone', () => {
220
- const stale = uploadResultView(uploadResult({ name: 'stale.png', meta: { fileId: 'file_a' } }));
221
- const expanded = readFileValues([{ id: 'file_a', name: 'fresh.png', url: '/u', mimeType: 'image/png' }]);
222
-
223
- expect(withRecentUploads(expanded, { file_a: stale })[0].name).toBe('fresh.png');
224
- });
225
-
226
- it('is a no-op with nothing remembered', () => {
227
- const views = readFileValues('file_a');
228
- expect(withRecentUploads(views, {})).toBe(views);
229
- });
230
- });