@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,133 +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 { describe, it, expect } from 'vitest';
10
- import { FieldType } from '@objectstack/spec/data';
11
- import {
12
- FORM_FIELD_TYPES,
13
- INLINE_EXCLUDED_FIELD_TYPES,
14
- hasFieldEditWidget,
15
- isInlineExcludedFieldType,
16
- mapFieldTypeToFormType,
17
- } from './index';
18
-
19
- /**
20
- * A field type the form can render — either a direct widget-map key, or a spec
21
- * spelling that resolves onto one through the alias table (`secret` →
22
- * `field:password`, `autonumber` → `field:auto_number`, …).
23
- *
24
- * `FORM_FIELD_TYPES` alone is `Object.keys(fieldWidgetMap)`, which does NOT
25
- * include the alias-only spellings — see the note at index.tsx's
26
- * `resolveFormWidgetType`. Using it as the definition of "a real type" made the
27
- * staleness check below reject `secret`, a genuine spec field type the form
28
- * renders, purely because it arrives via an alias.
29
- */
30
- function isRenderableFormType(t: string): boolean {
31
- // `text` is the alias table's fallback, so a non-`field:text` result means an
32
- // explicit entry exists. `text` itself is a direct widget key, so it is
33
- // already covered by the first clause and no real type is missed here.
34
- return FORM_FIELD_TYPES.includes(t) || mapFieldTypeToFormType(t) !== 'field:text';
35
- }
36
-
37
- /**
38
- * Drift-guard: the inline cell editor reuses the form's field widgets, but the
39
- * two lists were hand-maintained separately and drifted — `lookup` (and other
40
- * relational types) had a perfectly good form widget yet fell back to a plain
41
- * text box inline, because nobody wired it up. This pins the contract: every
42
- * type the FORM can render must have an explicit inline decision — an editor or
43
- * a documented exclusion — so a new form widget can never again silently become
44
- * an editable text box (or a missing one) in the grid.
45
- */
46
- describe('inline editor ↔ form widget parity', () => {
47
- it('every form field type either has an inline editor or is explicitly excluded', () => {
48
- const undecided = FORM_FIELD_TYPES.filter(
49
- (t) => !hasFieldEditWidget(t) && !INLINE_EXCLUDED_FIELD_TYPES.has(t),
50
- );
51
- // If this fails: a form widget type has no inline decision. Either add it to
52
- // EDIT_WIDGETS (inline-editable) or to INLINE_EXCLUDED_FIELD_TYPES (with a
53
- // reason) in FieldEditWidget.tsx.
54
- expect(undecided).toEqual([]);
55
- });
56
-
57
- it('the exclusion set lists only real form types (no stale entries)', () => {
58
- const stale = [...INLINE_EXCLUDED_FIELD_TYPES].filter((t) => !isRenderableFormType(t));
59
- expect(stale).toEqual([]);
60
- });
61
-
62
- it('credential types are never inline-editable', () => {
63
- // The grid's fallback for a type with no inline editor is a PLAIN TEXT input.
64
- // Both of these are masked on read, so that input would show the mask as the
65
- // value and write it straight back; `secret` also round-trips through an
66
- // encrypted store (ADR-0100), so the cell holds an opaque ref, not the value.
67
- for (const t of ['password', 'secret']) {
68
- expect(hasFieldEditWidget(t), `${t} must not have an inline editor`).toBe(false);
69
- expect(INLINE_EXCLUDED_FIELD_TYPES.has(t), `${t} must be explicitly excluded`).toBe(true);
70
- }
71
- });
72
-
73
- it('relational fields use the standard picker inline (regression: lookup was a text box)', () => {
74
- for (const t of ['lookup', 'master_detail', 'user', 'owner']) {
75
- expect(hasFieldEditWidget(t)).toBe(true);
76
- }
77
- });
78
-
79
- it('computed / binary form types are NOT inline-editable (excluded)', () => {
80
- for (const t of ['formula', 'summary', 'auto_number', 'file', 'image']) {
81
- expect(hasFieldEditWidget(t)).toBe(false);
82
- expect(INLINE_EXCLUDED_FIELD_TYPES.has(t)).toBe(true);
83
- }
84
- });
85
-
86
- it('structured-value types edit inline with their form widget (color/address/location/code/…)', () => {
87
- for (const t of ['color', 'address', 'location', 'geolocation', 'code', 'qrcode']) {
88
- expect(hasFieldEditWidget(t)).toBe(true);
89
- expect(INLINE_EXCLUDED_FIELD_TYPES.has(t)).toBe(false);
90
- }
91
- });
92
- });
93
-
94
- /**
95
- * #2942 — the guard above iterates FORM_FIELD_TYPES (the form widget-map
96
- * keys), so a SPEC field type that reaches the form only through the alias
97
- * table could sit in neither set and silently fall back to the grid's plain
98
- * text input: `json`, `composite`, `record`, `repeater`, `tree`, `video`,
99
- * `audio`, `autonumber` all did. This pins the contract at the spec boundary:
100
- * every `FieldType` member must resolve (alias-aware) to an inline editor or
101
- * a documented exclusion.
102
- */
103
- describe('inline editor ↔ SPEC FieldType parity (#2942)', () => {
104
- const specTypes: string[] = Array.isArray((FieldType as { options?: readonly string[] }).options)
105
- ? [...(FieldType as { options: readonly string[] }).options]
106
- : [];
107
-
108
- it('reads a non-empty enum from the spec', () => {
109
- expect(specTypes, 'could not read FieldType.options from the spec').not.toEqual([]);
110
- });
111
-
112
- it('every spec field type resolves to an inline decision (editor or exclusion)', () => {
113
- const undecided = specTypes.filter(
114
- (t) => !hasFieldEditWidget(t) && !isInlineExcludedFieldType(t),
115
- );
116
- // If this fails: a spec field type would fall back to a plain text input
117
- // inline. Give it a widget (EDIT_WIDGETS / the alias table) or a
118
- // documented exclusion (INLINE_EXCLUDED_FIELD_TYPES).
119
- expect(undecided).toEqual([]);
120
- });
121
-
122
- it('the structured/computed spec spellings are closed corruption paths', () => {
123
- // Excluded (alias-aware): editing these through a text box corrupts the value.
124
- for (const t of ['composite', 'record', 'repeater', 'video', 'audio', 'autonumber']) {
125
- expect(isInlineExcludedFieldType(t), `${t} must be excluded from inline editing`).toBe(true);
126
- expect(hasFieldEditWidget(t), `${t} must not resolve to an editor`).toBe(false);
127
- }
128
- // Editable through their form widgets: json → code editor, tree → lookup picker.
129
- for (const t of ['json', 'tree']) {
130
- expect(hasFieldEditWidget(t), `${t} must resolve to its form widget`).toBe(true);
131
- }
132
- });
133
- });
@@ -1,207 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import React from 'react';
10
- import type { FieldWidgetComponentProps } from './widgets/types';
11
-
12
- // The SAME dedicated widgets the form renders — reused for in-place editing
13
- // (e.g. the data grid's inline cell editor) so a select edits as a dropdown, a
14
- // boolean as a checkbox, a date as a date picker, etc. — never a bare text box.
15
- import { TextField } from './widgets/TextField';
16
- import { TextAreaField } from './widgets/TextAreaField';
17
- import { NumberField } from './widgets/NumberField';
18
- import { CurrencyField } from './widgets/CurrencyField';
19
- import { PercentField } from './widgets/PercentField';
20
- import { SliderField } from './widgets/SliderField';
21
- import { RatingField } from './widgets/RatingField';
22
- import { BooleanField } from './widgets/BooleanField';
23
- import { SelectField } from './widgets/SelectField';
24
- import { MultiSelectField } from './widgets/MultiSelectField';
25
- import { RadioField } from './widgets/RadioField';
26
- import { CheckboxesField } from './widgets/CheckboxesField';
27
- import { TagsField } from './widgets/TagsField';
28
- import { DateField } from './widgets/DateField';
29
- import { DateTimeField } from './widgets/DateTimeField';
30
- import { TimeField } from './widgets/TimeField';
31
- import { EmailField } from './widgets/EmailField';
32
- import { PhoneField } from './widgets/PhoneField';
33
- import { UrlField } from './widgets/UrlField';
34
- // Relational pickers — the SAME standard widgets the form uses. They read the
35
- // related-object dataSource from SchemaRendererContext (which the grid already
36
- // provides), so they drop straight into an inline cell.
37
- import { LookupField } from './widgets/LookupField';
38
- import { UserField } from './widgets/UserField';
39
- // Structured-value editors — lightweight (no map/code-editor deps), same
40
- // widgets the form uses. Drop into a cell like the rest.
41
- import { ColorField } from './widgets/ColorField';
42
- import { AddressField } from './widgets/AddressField';
43
- import { LocationField } from './widgets/LocationField';
44
- import { GeolocationField } from './widgets/GeolocationField';
45
- import { CodeField } from './widgets/CodeField';
46
- import { QRCodeField } from './widgets/QRCodeField';
47
- // The FORM's spec-alias table (`json` → `field:code`, `tree` → `field:lookup`,
48
- // …) — inline resolution reuses it so spec spellings get the form's decision.
49
- import { mapFieldTypeToFormType } from './field-type-alias';
50
-
51
- /**
52
- * Field types that edit in place with a dedicated widget. Keyed by the raw
53
- * field `type` (the widget map mirrors the form's `mapFieldTypeToFormType`).
54
- * Rich/heavy types (file, image, lookup, richtext, …) are intentionally absent
55
- * so callers fall back to their own simpler editor.
56
- */
57
- const EDIT_WIDGETS: Record<string, React.ComponentType<FieldWidgetComponentProps<any>>> = {
58
- text: TextField,
59
- textarea: TextAreaField,
60
- number: NumberField,
61
- currency: CurrencyField,
62
- percent: PercentField,
63
- slider: SliderField,
64
- progress: SliderField,
65
- rating: RatingField,
66
- boolean: BooleanField,
67
- toggle: BooleanField,
68
- select: SelectField,
69
- status: SelectField,
70
- multiselect: MultiSelectField,
71
- radio: RadioField,
72
- checkboxes: CheckboxesField,
73
- tags: TagsField,
74
- date: DateField,
75
- datetime: DateTimeField,
76
- time: TimeField,
77
- email: EmailField,
78
- phone: PhoneField,
79
- url: UrlField,
80
- // Relational — the record/user pickers, same as the form (the form maps
81
- // master_detail to the single-value LookupField too, see fieldWidgetMap).
82
- lookup: LookupField,
83
- master_detail: LookupField,
84
- user: UserField,
85
- owner: UserField,
86
- // Structured-value editors — same widgets the form uses.
87
- color: ColorField,
88
- address: AddressField,
89
- location: LocationField,
90
- geolocation: GeolocationField,
91
- code: CodeField,
92
- qrcode: QRCodeField,
93
- };
94
-
95
- /**
96
- * Form field types that are deliberately NOT given an inline editor — every
97
- * other form widget type must be in {@link EDIT_WIDGETS}. This pairing is
98
- * enforced by a test against the form's widget map (`FORM_FIELD_TYPES`) so the
99
- * two can't silently drift again (which is how `lookup` was missed). To add a
100
- * type to inline editing, move it from here into EDIT_WIDGETS.
101
- */
102
- export const INLINE_EXCLUDED_FIELD_TYPES = new Set<string>([
103
- // Computed / read-only — the grid keeps these non-editable (no value to author).
104
- 'formula', 'summary', 'auto_number',
105
- // Binary / attachment — edited from the record form, shown read-only in the grid.
106
- 'file', 'image', 'avatar', 'signature',
107
- // Heavy / full editors — better in the record form than a cell.
108
- 'markdown', 'html', 'richtext',
109
- // Credentials — never inline-editable. Both are masked on read, so the cell has
110
- // no real value to seed an editor with, and the grid's fallback is a PLAIN TEXT
111
- // input: it would render the mask as if it were the value and write it straight
112
- // back. `secret` additionally round-trips through an encrypted store (ADR-0100,
113
- // data/field.zod.ts) — the cell holds an opaque ref, not the secret.
114
- 'password', 'secret',
115
- // Containers / non-authorable — a sub-form / sub-grid / embedding vector
116
- // doesn't belong in a single cell.
117
- 'object', 'grid', 'vector',
118
- // Widget-hint-only pickers — authored in the record form (they depend on
119
- // sibling fields / a loaded object catalog), not inline in a grid cell.
120
- 'object-ref', 'filter-condition', 'recipient-picker',
121
- ]);
122
-
123
- /** Field types whose value is chosen in one discrete gesture (no free typing). */
124
- export const DISCRETE_EDIT_TYPES = new Set<string>([
125
- 'boolean', 'toggle', 'select', 'status', 'radio', 'rating',
126
- ]);
127
-
128
- /**
129
- * Resolve a field type to its inline-editing key: the raw type when the
130
- * widget/exclusion tables name it directly, otherwise its form-alias target
131
- * (`mapFieldTypeToFormType`, `field:` prefix stripped).
132
- *
133
- * The tables are keyed by the FORM's spellings, but spec field types reach
134
- * this layer in the SPEC's spellings — `json`, `tree`, `composite`, `record`,
135
- * `repeater`, `video`, `audio`, `autonumber` are aliases in the form map and
136
- * were keys in NEITHER table here, so the grid silently fell back to a plain
137
- * text input for all of them: typing over structured JSON or a hierarchy ref
138
- * is a value-corruption path (#2942). Resolving through the same alias table
139
- * the form uses gives each spec type the form's own decision: `json` → the
140
- * code editor, `tree` → the lookup picker, and the container/computed/binary
141
- * families → their documented exclusions.
142
- */
143
- function resolveInlineEditType(type: string): string {
144
- if (type in EDIT_WIDGETS || INLINE_EXCLUDED_FIELD_TYPES.has(type)) return type;
145
- return mapFieldTypeToFormType(type).replace(/^field:/, '');
146
- }
147
-
148
- /** True when a field type has a dedicated in-place edit widget. */
149
- export function hasFieldEditWidget(type: string | undefined): boolean {
150
- return !!type && resolveInlineEditType(type) in EDIT_WIDGETS;
151
- }
152
-
153
- /**
154
- * True when a field type is deliberately NOT inline-editable (alias-aware:
155
- * `composite` resolves to the excluded `object`, `video` to `file`, …).
156
- * Grid hosts must treat these as read-only cells — falling back to a plain
157
- * text editor is exactly the corruption path the exclusion exists to close.
158
- */
159
- export function isInlineExcludedFieldType(type: string | undefined): boolean {
160
- return !!type && INLINE_EXCLUDED_FIELD_TYPES.has(resolveInlineEditType(type));
161
- }
162
-
163
- /**
164
- * Relational picker widgets (lookup / master_detail / user / owner) render best
165
- * in a grid cell as a single-line, borderless trigger — so the selected
166
- * record's NAME shows inside the trigger instead of a chip stacked above a
167
- * separate "Select…" button (which double-stacks and wastes the row height).
168
- * This mirrors how the line-item grid (`GridField`) renders its lookup cells.
169
- */
170
- const COMPACT_EDIT_TYPES = new Set<string>(['lookup', 'master_detail', 'user', 'owner']);
171
-
172
- /**
173
- * Render the dedicated edit widget for a field's type — the SAME control the
174
- * form uses — as a controlled `{ value, onChange, field }` input. Returns
175
- * `null` for types without a registered widget so the caller can fall back to
176
- * a plain editor.
177
- *
178
- * `autoFocus` is forwarded because an inline-edit host enters edit mode ON a
179
- * field and expects the caret to land there (objectui#4220 — the detail page's
180
- * delegation): each widget's own `toDomProps` whitelist already carries it onto
181
- * the real focusable control, so nothing here needs to know which element that
182
- * is. A host that passes nothing is unaffected.
183
- */
184
- export function FieldEditWidget({
185
- field,
186
- value,
187
- onChange,
188
- readonly,
189
- autoFocus,
190
- }: FieldWidgetComponentProps<any>): React.ReactElement | null {
191
- const resolved = field?.type ? resolveInlineEditType(field.type) : undefined;
192
- const Widget = resolved ? EDIT_WIDGETS[resolved] : undefined;
193
- if (!Widget) return null;
194
- // `compact` is a declared widget prop (objectui#3221 closed this type), so
195
- // the spread no longer needs an `any` escape hatch to get past it.
196
- const compactProps = resolved && COMPACT_EDIT_TYPES.has(resolved) ? { compact: true } : {};
197
- return (
198
- <Widget
199
- field={field}
200
- value={value}
201
- onChange={onChange}
202
- readonly={readonly}
203
- autoFocus={autoFocus}
204
- {...compactProps}
205
- />
206
- );
207
- }
@@ -1,166 +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
- * objectui#4037 (migrated from objectstack#5019): a `Field.address` value read
11
- * as stringified JSON on the record detail page — `{"street":"中策路 1 号",…}` —
12
- * because the DISPLAY registry mapped `address` to `JsonCellRenderer`, while
13
- * `location` next to it in the same field group had a real formatter. The
14
- * create/edit dialog rendered the same field correctly, so the gap was
15
- * display-side only.
16
- *
17
- * Every case below resolves its renderer through `getCellRenderer` — the
18
- * display resolver the detail page itself calls — rather than importing
19
- * `AddressCellRenderer` directly, so these tests fail on the JSON signature if
20
- * the registry entry ever regresses, not merely if the formatter changes.
21
- */
22
-
23
- import { describe, it, expect } from 'vitest';
24
- import React from 'react';
25
- import { render, screen } from '@testing-library/react';
26
- import '@testing-library/jest-dom';
27
- import { getCellRenderer, resolveCellRendererType } from '../index';
28
-
29
- /** Resolve + render exactly the way `DetailSection` builds a read-mode value. */
30
- function renderThroughDisplayRegistry(type: string, value: unknown) {
31
- const Renderer = getCellRenderer(resolveCellRendererType({ type }) || type);
32
- return render(<Renderer value={value as any} field={{ type } as any} />);
33
- }
34
-
35
- /** The stringified-object signature this issue is about. */
36
- function expectNoJsonSignature(text: string | null) {
37
- expect(text).not.toMatch(/[{}]/);
38
- expect(text).not.toMatch(/"(street|city|state|postalCode|country)"/);
39
- expect(text).not.toMatch(/undefined/);
40
- }
41
-
42
- /** The record from the issue's evidence table. */
43
- const ISSUE_ADDRESS = {
44
- street: '中策路 1 号',
45
- city: '杭州',
46
- state: '浙江',
47
- postalCode: '310000',
48
- country: '中国',
49
- };
50
-
51
- describe('address display renderer (objectui#4037)', () => {
52
- it('renders a populated address as a formatted line, not stringified JSON', () => {
53
- const { container } = renderThroughDisplayRegistry('address', ISSUE_ADDRESS);
54
- expectNoJsonSignature(container.textContent);
55
- expect(
56
- screen.getByText('中策路 1 号, 杭州, 浙江 310000, 中国'),
57
- ).toBeInTheDocument();
58
- });
59
-
60
- it('resolves `address` through the display registry the way it resolves `location`', () => {
61
- // The issue's decisive comparison: one address field beside one location
62
- // field, same record, same page. Both must be formatted; neither may fall
63
- // through to object stringification.
64
- const address = renderThroughDisplayRegistry('address', ISSUE_ADDRESS);
65
- const location = renderThroughDisplayRegistry('location', { lat: 30.2741, lng: 120.1551 });
66
- expectNoJsonSignature(address.container.textContent);
67
- expectNoJsonSignature(location.container.textContent);
68
- expect(location.container.textContent).toContain('30.2741');
69
- });
70
-
71
- it('carries the full value: every authored sub-field appears', () => {
72
- const { container } = renderThroughDisplayRegistry('address', ISSUE_ADDRESS);
73
- for (const part of Object.values(ISSUE_ADDRESS)) {
74
- expect(container.textContent).toContain(part);
75
- }
76
- });
77
- });
78
-
79
- describe('address display renderer — partial values render gracefully', () => {
80
- it('drops missing parts instead of leaving dangling separators', () => {
81
- const { container } = renderThroughDisplayRegistry('address', {
82
- street: '中策路 1 号',
83
- city: '杭州',
84
- });
85
- expect(screen.getByText('中策路 1 号, 杭州')).toBeInTheDocument();
86
- expectNoJsonSignature(container.textContent);
87
- expect(container.textContent).not.toMatch(/,\s*$/);
88
- expect(container.textContent).not.toMatch(/,\s*,/);
89
- });
90
-
91
- it('renders a street-only address as just the street', () => {
92
- renderThroughDisplayRegistry('address', { street: '中策路 1 号' });
93
- expect(screen.getByText('中策路 1 号')).toBeInTheDocument();
94
- });
95
-
96
- it('keeps state and postal code in one group, either alone', () => {
97
- const stateOnly = renderThroughDisplayRegistry('address', { city: 'SF', state: 'CA' });
98
- expect(stateOnly.container.textContent).toBe('SF, CA');
99
- const postalOnly = renderThroughDisplayRegistry('address', { city: 'SF', postalCode: '94102' });
100
- expect(postalOnly.container.textContent).toBe('SF, 94102');
101
- const both = renderThroughDisplayRegistry('address', {
102
- city: 'SF',
103
- state: 'CA',
104
- postalCode: '94102',
105
- });
106
- expect(both.container.textContent).toBe('SF, CA 94102');
107
- });
108
-
109
- it('ignores whitespace-only parts rather than spacing over them', () => {
110
- const { container } = renderThroughDisplayRegistry('address', {
111
- street: ' ',
112
- city: '杭州',
113
- country: '',
114
- });
115
- expect(container.textContent).toBe('杭州');
116
- });
117
-
118
- it('reads a legacy `zipCode` postal code (objectstack#5143 data)', () => {
119
- // Records written by builds up to 17.0.0-rc.1 stored the postal code under
120
- // `zipCode`. The input widget reads it; the detail page must not drop it.
121
- const { container } = renderThroughDisplayRegistry('address', {
122
- city: 'San Francisco',
123
- state: 'CA',
124
- zipCode: '94102',
125
- });
126
- expect(container.textContent).toBe('San Francisco, CA 94102');
127
- });
128
- });
129
-
130
- describe('address display renderer — nothing is silently swallowed', () => {
131
- it('shows the empty placeholder for a null value and for `{}`', () => {
132
- const nullValue = renderThroughDisplayRegistry('address', null);
133
- expect(nullValue.container.querySelector('[data-slot="empty-value"]')).not.toBeNull();
134
- const emptyObject = renderThroughDisplayRegistry('address', {});
135
- expect(emptyObject.container.querySelector('[data-slot="empty-value"]')).not.toBeNull();
136
- });
137
-
138
- it('keeps an unrecognized shape visible as JSON rather than blanking it', () => {
139
- // Strictly better than today, never worse: a value the formatter cannot
140
- // read retains the current compact-JSON rendering instead of disappearing.
141
- const { container } = renderThroughDisplayRegistry('address', { unexpected: 'data' });
142
- expect(container.textContent).toContain('unexpected');
143
- });
144
-
145
- it('passes a plain string address straight through', () => {
146
- renderThroughDisplayRegistry('address', '中策路 1 号, 杭州');
147
- expect(screen.getByText('中策路 1 号, 杭州')).toBeInTheDocument();
148
- });
149
- });
150
-
151
- describe('address display renderer — controls (unchanged surfaces)', () => {
152
- it('leaves `location` and `geolocation` formatting untouched', () => {
153
- const location = renderThroughDisplayRegistry('location', { lat: 30.2741, lng: 120.1551 });
154
- expect(location.container.textContent).toBe('30.2741, 120.1551');
155
- const geo = renderThroughDisplayRegistry('geolocation', { latitude: 30.2741, longitude: 120.1551 });
156
- expect(geo.container.textContent).toBe('30.2741, 120.1551');
157
- });
158
-
159
- it('leaves genuinely structural types stringifying as JSON', () => {
160
- // `address` moved out of the JSON bucket; `json` / `object` stay in it.
161
- for (const type of ['json', 'object', 'composite', 'record']) {
162
- const { container } = renderThroughDisplayRegistry(type, { a: 1 });
163
- expect(container.textContent).toBe('{"a":1}');
164
- }
165
- });
166
- });