@object-ui/fields 17.5.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 (229) hide show
  1. package/CHANGELOG.md +714 -0
  2. package/dist/FieldEditWidget.d.ts +16 -2
  3. package/dist/field-type-alias.d.ts +16 -0
  4. package/dist/index.cjs +4 -4
  5. package/dist/index.css +27 -9
  6. package/dist/index.d.ts +425 -72
  7. package/dist/index.js +2530 -2228
  8. package/dist/widgets/AddressField.d.ts +3 -3
  9. package/dist/widgets/AutoNumberField.d.ts +1 -1
  10. package/dist/widgets/AvatarField.d.ts +1 -1
  11. package/dist/widgets/BooleanField.d.ts +1 -1
  12. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  13. package/dist/widgets/CheckboxesField.d.ts +1 -1
  14. package/dist/widgets/CodeField.d.ts +1 -1
  15. package/dist/widgets/ColorField.d.ts +1 -1
  16. package/dist/widgets/CurrencyField.d.ts +1 -1
  17. package/dist/widgets/DateField.d.ts +1 -1
  18. package/dist/widgets/DateTimeField.d.ts +1 -1
  19. package/dist/widgets/EmailField.d.ts +1 -1
  20. package/dist/widgets/FileField.d.ts +18 -2
  21. package/dist/widgets/FilterConditionField.d.ts +28 -1
  22. package/dist/widgets/FormulaField.d.ts +1 -1
  23. package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
  24. package/dist/widgets/GeolocationField.d.ts +1 -1
  25. package/dist/widgets/GridField.d.ts +56 -4
  26. package/dist/widgets/ImageField.d.ts +1 -1
  27. package/dist/widgets/LocationField.d.ts +1 -1
  28. package/dist/widgets/LookupField.d.ts +1 -1
  29. package/dist/widgets/MasterDetailField.d.ts +1 -1
  30. package/dist/widgets/MultiSelectField.d.ts +1 -1
  31. package/dist/widgets/NumberField.d.ts +1 -1
  32. package/dist/widgets/ObjectField.d.ts +1 -1
  33. package/dist/widgets/ObjectRefField.d.ts +1 -1
  34. package/dist/widgets/OptionsEmptyState.d.ts +13 -7
  35. package/dist/widgets/PasswordField.d.ts +1 -1
  36. package/dist/widgets/PercentField.d.ts +1 -1
  37. package/dist/widgets/PhoneField.d.ts +1 -1
  38. package/dist/widgets/QRCodeField.d.ts +1 -1
  39. package/dist/widgets/RadioField.d.ts +1 -1
  40. package/dist/widgets/RatingField.d.ts +1 -1
  41. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  42. package/dist/widgets/RecordPickerDialog.d.ts +2 -4
  43. package/dist/widgets/RichTextField.d.ts +37 -1
  44. package/dist/widgets/SelectField.d.ts +1 -1
  45. package/dist/widgets/SignatureField.d.ts +32 -2
  46. package/dist/widgets/SliderField.d.ts +37 -2
  47. package/dist/widgets/SummaryField.d.ts +1 -1
  48. package/dist/widgets/TagsField.d.ts +1 -1
  49. package/dist/widgets/TextAreaField.d.ts +6 -2
  50. package/dist/widgets/TextField.d.ts +1 -1
  51. package/dist/widgets/TimeField.d.ts +1 -1
  52. package/dist/widgets/UrlField.d.ts +1 -1
  53. package/dist/widgets/UserField.d.ts +1 -1
  54. package/dist/widgets/VectorField.d.ts +1 -1
  55. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  56. package/dist/widgets/address-format.d.ts +27 -7
  57. package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
  58. package/dist/widgets/richTextDisplay.d.ts +80 -0
  59. package/dist/widgets/toHostGroupProps.d.ts +119 -21
  60. package/dist/widgets/types.d.ts +8 -3
  61. package/package.json +9 -10
  62. package/dist/widgets/CharacterCount.d.ts +0 -56
  63. package/src/FieldEditWidget.test.ts +0 -133
  64. package/src/FieldEditWidget.tsx +0 -207
  65. package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
  66. package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
  67. package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
  68. package/src/__tests__/DateCellRenderer.test.tsx +0 -82
  69. package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
  70. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  71. package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
  72. package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
  73. package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
  74. package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
  75. package/src/__tests__/cell-truncation.test.tsx +0 -96
  76. package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
  77. package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
  78. package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
  79. package/src/__tests__/date-locale-channel.test.tsx +0 -305
  80. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  81. package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
  82. package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
  83. package/src/__tests__/group-labelling-declaration.test.ts +0 -148
  84. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  85. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  86. package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
  87. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  88. package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
  89. package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
  90. package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
  91. package/src/__tests__/registry-collision.test.ts +0 -91
  92. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  93. package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
  94. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  95. package/src/__tests__/validation-feedback.test.tsx +0 -361
  96. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  97. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  98. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
  99. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  100. package/src/coerce-safe-value.test.ts +0 -34
  101. package/src/complex-widgets.test.tsx +0 -924
  102. package/src/currency.ts +0 -144
  103. package/src/datetime-widgets.test.tsx +0 -185
  104. package/src/field-type-alias.multiple.test.ts +0 -81
  105. package/src/field-type-alias.ts +0 -156
  106. package/src/field-type-coverage.test.ts +0 -67
  107. package/src/index.css +0 -44
  108. package/src/index.tsx +0 -2745
  109. package/src/standard-widgets.test.tsx +0 -364
  110. package/src/widgets/AddressField.postalCode.test.tsx +0 -257
  111. package/src/widgets/AddressField.tsx +0 -182
  112. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
  113. package/src/widgets/AutoNumberField.tsx +0 -18
  114. package/src/widgets/AvatarField.tsx +0 -117
  115. package/src/widgets/BooleanField.tsx +0 -86
  116. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  117. package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
  118. package/src/widgets/CharacterCount.tsx +0 -263
  119. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  120. package/src/widgets/CheckboxesField.tsx +0 -169
  121. package/src/widgets/CodeField.tsx +0 -41
  122. package/src/widgets/ColorField.tsx +0 -53
  123. package/src/widgets/CurrencyField.tsx +0 -135
  124. package/src/widgets/DateField.tsx +0 -40
  125. package/src/widgets/DateTimeField.tsx +0 -46
  126. package/src/widgets/EmailField.tsx +0 -37
  127. package/src/widgets/FileField.tsx +0 -512
  128. package/src/widgets/FilterConditionField.tsx +0 -454
  129. package/src/widgets/FormulaField.tsx +0 -38
  130. package/src/widgets/FullscreenFieldEditor.tsx +0 -60
  131. package/src/widgets/GeolocationField.tsx +0 -193
  132. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  133. package/src/widgets/GridField.test.tsx +0 -632
  134. package/src/widgets/GridField.tsx +0 -1070
  135. package/src/widgets/ImageCropperDialog.tsx +0 -245
  136. package/src/widgets/ImageField.maxSize.test.tsx +0 -154
  137. package/src/widgets/ImageField.tsx +0 -305
  138. package/src/widgets/ImageLightbox.test.tsx +0 -52
  139. package/src/widgets/ImageLightbox.tsx +0 -108
  140. package/src/widgets/LocationField.tsx +0 -57
  141. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  142. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  143. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  144. package/src/widgets/LookupField.idField.test.tsx +0 -61
  145. package/src/widgets/LookupField.tsx +0 -1295
  146. package/src/widgets/MarkdownContent.tsx +0 -19
  147. package/src/widgets/MasterDetailField.tsx +0 -131
  148. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  149. package/src/widgets/MultiSelectField.tsx +0 -196
  150. package/src/widgets/NumberField.tsx +0 -49
  151. package/src/widgets/ObjectField.tsx +0 -94
  152. package/src/widgets/ObjectRefField.tsx +0 -121
  153. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  154. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  155. package/src/widgets/OptionsEmptyState.tsx +0 -99
  156. package/src/widgets/PasswordField.tsx +0 -49
  157. package/src/widgets/PeoplePicker.test.tsx +0 -360
  158. package/src/widgets/PeoplePicker.tsx +0 -579
  159. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  160. package/src/widgets/PercentField.tsx +0 -95
  161. package/src/widgets/PersonRow.tsx +0 -117
  162. package/src/widgets/PhoneField.tsx +0 -37
  163. package/src/widgets/QRCodeField.tsx +0 -103
  164. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  165. package/src/widgets/RadioField.tsx +0 -143
  166. package/src/widgets/RatingField.tsx +0 -94
  167. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  168. package/src/widgets/RecipientPickerField.tsx +0 -188
  169. package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
  170. package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
  171. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  172. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  173. package/src/widgets/RecordPickerDialog.tsx +0 -1365
  174. package/src/widgets/RichTextField.tsx +0 -176
  175. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  176. package/src/widgets/SelectField.tsx +0 -193
  177. package/src/widgets/SelectionTray.tsx +0 -110
  178. package/src/widgets/SignatureField.tsx +0 -140
  179. package/src/widgets/SliderField.tsx +0 -39
  180. package/src/widgets/SummaryField.tsx +0 -31
  181. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  182. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  183. package/src/widgets/TagsField.tsx +0 -88
  184. package/src/widgets/TextAreaField.tsx +0 -213
  185. package/src/widgets/TextField.tsx +0 -45
  186. package/src/widgets/TimeField.tsx +0 -31
  187. package/src/widgets/UrlField.tsx +0 -46
  188. package/src/widgets/UserField.tsx +0 -59
  189. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  190. package/src/widgets/VectorField.tsx +0 -29
  191. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  192. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
  193. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  194. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  195. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  196. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  197. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  198. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  199. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  200. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  201. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  202. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  203. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  204. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  205. package/src/widgets/address-format.ts +0 -120
  206. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  207. package/src/widgets/deriveLookupColumns.ts +0 -108
  208. package/src/widgets/file-size-guard.ts +0 -55
  209. package/src/widgets/file-value.test.ts +0 -230
  210. package/src/widgets/file-value.ts +0 -246
  211. package/src/widgets/nativeDateValue.test.ts +0 -115
  212. package/src/widgets/nativeDateValue.ts +0 -112
  213. package/src/widgets/openNativePicker.ts +0 -18
  214. package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
  215. package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
  216. package/src/widgets/personDisplay.test.ts +0 -113
  217. package/src/widgets/personDisplay.ts +0 -92
  218. package/src/widgets/recentLookups.test.ts +0 -45
  219. package/src/widgets/recentLookups.ts +0 -47
  220. package/src/widgets/toDomProps.ts +0 -204
  221. package/src/widgets/toHostGroupProps.ts +0 -102
  222. package/src/widgets/types.ts +0 -242
  223. package/src/widgets/useCascadingOptions.ts +0 -46
  224. package/src/widgets/useFieldTranslation.ts +0 -132
  225. package/src/widgets/useRecordQuery.test.ts +0 -174
  226. package/src/widgets/useRecordQuery.ts +0 -291
  227. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  228. package/src/widgets/useUploadingSignal.ts +0 -20
  229. 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,120 +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
- * The address value shape, and the ONE formatting rule every address surface
11
- * renders it with.
12
- *
13
- * This module exists because the rule had exactly one implementation and only
14
- * one consumer could reach it (objectui#4037). `AddressField`'s readonly branch
15
- * collapsed a stored address to a formatted line, while the display (read)
16
- * registry the detail page uses mapped `address` straight to `JsonCellRenderer`
17
- * — so the same stored value rendered as `Street, City, State ZIP, Country` in
18
- * a readonly form and as `{"street":"…","city":"…"}` on the detail page.
19
- *
20
- * The fix keeps ONE definition rather than a second copy next to the renderer:
21
- * a duplicated rule is a rule that drifts, and two spellings of the same
22
- * address on two surfaces of one app is the very failure the issue reports.
23
- *
24
- * Deliberately pure — no React, no component imports — so the eager
25
- * `@object-ui/fields` barrel can use it for cell rendering without pulling
26
- * `AddressField` (and its inputs) out of the lazily-loaded widget chunk.
27
- */
28
-
29
- import type { AddressValue } from '@objectstack/spec/data';
30
-
31
- /**
32
- * Address data structure — `z.input` of `@objectstack/spec`'s
33
- * `AddressValueSchema`, which is what the platform stores and what
34
- * `/api/v1/data/**` serves back.
35
- *
36
- * The postal code is spelled `postalCode` (objectstack#5143). `AddressField`
37
- * used to read and write `zipCode`, a key that appears nowhere in the spec, so
38
- * the stored postal code never reached the input — and whatever the user then
39
- * typed into that apparently-empty box was written under a key
40
- * `AddressValueSchema` strips: lost outright on a new record, and on an
41
- * existing one silently discarded while the stale stored value survived. (A
42
- * postal code nobody touched did survive an unrelated edit, because the write
43
- * spread the whole stored object through; the issue's account of that half is
44
- * corrected in `AddressField.postalCode.test.tsx`.) One name, on both sides:
45
- * the contract's.
46
- *
47
- * IMPORTED rather than declared since objectui#4167. rc.6 publishes
48
- * `AddressValue` under this exact name, and the local copy — whose own comment
49
- * already claimed to be "the part names of `AddressSchema`" — declared five of
50
- * the seven parts. `countryCode` and `formatted` were missing, which is
51
- * objectstack#4115's failure class precisely: a canonical CLAIM over a narrower
52
- * shape, believed by the next reader. The claim is now the code.
53
- *
54
- * `AddressField` still renders five inputs and {@link formatAddress} still
55
- * composes five parts, and that is a deliberate split rather than an omission:
56
- * `formatted` is a derived one-line rendering of the parts (what this module
57
- * computes) and `countryCode` is the ISO pair for `country`, neither of which a
58
- * human types into a part box. Binding the type does not publish them as
59
- * inputs — it stops the type from asserting the platform cannot store them, and
60
- * it means a write from the widget no longer looks type-clean while dropping a
61
- * key the contract carries: the `{ ...address }` spread in `handleFieldChange`
62
- * preserves both, and now says so.
63
- *
64
- * The declaration moved here (objectui#4037) so the display cell renderer and
65
- * the widget share one definition; the SOURCE of that definition is the spec.
66
- */
67
- export type { AddressValue };
68
-
69
- /**
70
- * The shape written by builds up to and including 17.0.0-rc.1, whose postal
71
- * code landed under `zipCode` (objectstack#5143). Read-time compatibility ONLY,
72
- * and deliberately not part of {@link AddressValue}: authoring code must not be
73
- * able to spell `zipCode`, and nothing here ever writes it back — the first
74
- * edit of any part normalizes the record onto `postalCode` (see
75
- * `AddressField`'s `handleFieldChange`). This is not a producer alias to be
76
- * honoured forever; it exists to carry data THIS widget mis-wrote, and can go
77
- * once no such data remains.
78
- */
79
- export type LegacyAddressValue = AddressValue & { zipCode?: string };
80
-
81
- /** Postal code of a stored address, preferring the canonical key. */
82
- export function readPostalCode(addr: AddressValue): string | undefined {
83
- return addr.postalCode ?? (addr as LegacyAddressValue).zipCode;
84
- }
85
-
86
- /**
87
- * One address part, normalized for joining: absent, non-string and
88
- * whitespace-only parts all collapse to `''` so the caller's `filter(Boolean)`
89
- * drops them. This is what keeps a partial address free of dangling separators
90
- * and of the string `undefined`.
91
- */
92
- function part(value: unknown): string {
93
- if (value == null) return '';
94
- return String(value).trim();
95
- }
96
-
97
- /**
98
- * Formats a stored address as a single line: `Street, City, State ZIP, Country`.
99
- *
100
- * Ordering and separators are NOT invented here — they are the rule
101
- * `AddressField` already applied in its readonly branch, and the sub-field set
102
- * its inputs expose (street / city / state / postalCode / country). Missing
103
- * parts are dropped rather than spaced over, so a street-only address is
104
- * `"中策路 1 号"` and never `", , ,"`.
105
- *
106
- * Returns `''` when no part is usable — callers decide what an empty address
107
- * looks like (an `EmptyValue` placeholder for the cell renderer), and a value
108
- * carrying no recognized part at all is NOT silently blanked: see
109
- * `AddressCellRenderer`, which falls back to JSON so unknown data stays visible.
110
- */
111
- export function formatAddress(addr: AddressValue): string {
112
- // State and postal code share one comma-delimited group, space-separated
113
- // inside it ("CA 94102"); either one alone occupies the group by itself.
114
- const stateAndPostal = [part(addr.state), part(readPostalCode(addr))]
115
- .filter(Boolean)
116
- .join(' ');
117
- return [part(addr.street), part(addr.city), stateAndPostal, part(addr.country)]
118
- .filter(Boolean)
119
- .join(', ');
120
- }
@@ -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,55 +0,0 @@
1
- /**
2
- * The client-side `maxSize` guard shared by the file and image widgets
3
- * (objectui#4141).
4
- *
5
- * Why this is shared rather than mirrored per widget: the guard is the only
6
- * thing standing between a declared `maxSize` and a real `presigned → PUT →
7
- * complete` round trip, and `paramToField` copies `maxSize` onto the field
8
- * config for **every** param type regardless of which widget ends up rendering
9
- * it. So a widget that re-derives the check drifts silently — the image param
10
- * shipped receiving the constraint and never reading it, while the file param
11
- * rejected the identical pick, and nothing failed until QA drove the two side by
12
- * side. One exported function makes the difference visible: a widget either
13
- * calls it or conspicuously does not.
14
- *
15
- * Both widgets deliberately share the `fields.file.exceedsMaxSize` key too. The
16
- * sentence names a file and a limit and says nothing file-widget-specific, it is
17
- * already translated in all 11 built-in locales, and a parallel `fields.image.*`
18
- * twin would be the same sentence maintained twice — drifting the moment one
19
- * translator rewords one of them.
20
- */
21
-
22
- /**
23
- * Minimal shape of the i18next `t` this module needs, matching the established
24
- * `TranslateFn` pattern in `app-shell/src/providers/writeWarningToast`.
25
- */
26
- export type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
27
-
28
- /** Divisor for rendering a byte limit as MB, matching the widgets' size display. */
29
- const BYTES_PER_MB = 1024 * 1024;
30
-
31
- /**
32
- * The rejection message for a pick that exceeds `maxSize`, or `null` when the
33
- * pick is within the limit — so a caller reads it as `if (err) reject`.
34
- *
35
- * A falsy `maxSize` (undeclared, or an explicit `0`) means **unrestricted**,
36
- * preserving the `if (maxSize && …)` semantics the file widget has always
37
- * applied: an undeclared limit must never turn into a 0-byte one.
38
- *
39
- * Takes `name` and `size` separately instead of a `File` because the image
40
- * widget's crop path validates a re-encoded `Blob`, which carries no name of its
41
- * own — that name comes from the cropper's output.
42
- */
43
- export function maxSizeError(
44
- t: TranslateFn,
45
- pick: { name: string; size: number },
46
- maxSize: number | undefined,
47
- ): string | null {
48
- if (!maxSize || pick.size <= maxSize) return null;
49
- const maxMB = (maxSize / BYTES_PER_MB).toFixed(1);
50
- return t('fields.file.exceedsMaxSize', {
51
- defaultValue: `"${pick.name}" exceeds max size (${maxMB} MB)`,
52
- name: pick.name,
53
- max: maxMB,
54
- });
55
- }