@object-ui/fields 17.5.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +6150 -0
  2. package/README.md +108 -8
  3. package/dist/FieldEditWidget.d.ts +70 -8
  4. package/dist/coerceToSafeValue.d.ts +54 -0
  5. package/dist/currency.d.ts +1 -1
  6. package/dist/field-type-alias.d.ts +16 -0
  7. package/dist/index.cjs +5 -5
  8. package/dist/index.css +53 -11
  9. package/dist/index.d.ts +653 -128
  10. package/dist/index.js +4275 -3171
  11. package/dist/widgets/AddressField.d.ts +3 -3
  12. package/dist/widgets/AutoNumberField.d.ts +1 -1
  13. package/dist/widgets/AvatarField.d.ts +20 -4
  14. package/dist/widgets/BooleanField.d.ts +2 -2
  15. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  16. package/dist/widgets/CheckboxesField.d.ts +1 -1
  17. package/dist/widgets/CodeField.d.ts +1 -1
  18. package/dist/widgets/ColorField.d.ts +1 -1
  19. package/dist/widgets/CurrencyField.d.ts +1 -1
  20. package/dist/widgets/DateField.d.ts +2 -2
  21. package/dist/widgets/DateTimeField.d.ts +2 -2
  22. package/dist/widgets/EmailField.d.ts +1 -1
  23. package/dist/widgets/FileField.d.ts +18 -2
  24. package/dist/widgets/FilterConditionField.d.ts +43 -2
  25. package/dist/widgets/FormulaField.d.ts +1 -1
  26. package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
  27. package/dist/widgets/GeolocationField.d.ts +10 -4
  28. package/dist/widgets/GridField.d.ts +102 -86
  29. package/dist/widgets/ImageField.d.ts +1 -1
  30. package/dist/widgets/LocationField.d.ts +31 -4
  31. package/dist/widgets/LookupField.d.ts +9 -1
  32. package/dist/widgets/MasterDetailField.d.ts +1 -1
  33. package/dist/widgets/MultiSelectField.d.ts +1 -1
  34. package/dist/widgets/NumberField.d.ts +2 -2
  35. package/dist/widgets/ObjectField.d.ts +10 -3
  36. package/dist/widgets/ObjectRefField.d.ts +1 -1
  37. package/dist/widgets/OptionsEmptyState.d.ts +38 -8
  38. package/dist/widgets/PasswordField.d.ts +15 -1
  39. package/dist/widgets/PeoplePicker.d.ts +17 -2
  40. package/dist/widgets/PercentField.d.ts +3 -2
  41. package/dist/widgets/PersonRow.d.ts +13 -2
  42. package/dist/widgets/PhoneField.d.ts +1 -1
  43. package/dist/widgets/QRCodeField.d.ts +1 -1
  44. package/dist/widgets/RadioField.d.ts +1 -1
  45. package/dist/widgets/RatingField.d.ts +1 -1
  46. package/dist/widgets/RecipientPickerField.d.ts +44 -1
  47. package/dist/widgets/RecordPickerDialog.d.ts +33 -16
  48. package/dist/widgets/RichTextField.d.ts +37 -1
  49. package/dist/widgets/SelectField.d.ts +1 -1
  50. package/dist/widgets/SelectionTray.d.ts +13 -2
  51. package/dist/widgets/SignatureField.d.ts +32 -2
  52. package/dist/widgets/SliderField.d.ts +37 -2
  53. package/dist/widgets/SummaryField.d.ts +1 -1
  54. package/dist/widgets/TagsField.d.ts +1 -1
  55. package/dist/widgets/TextAreaField.d.ts +6 -2
  56. package/dist/widgets/TextField.d.ts +2 -2
  57. package/dist/widgets/TimeField.d.ts +2 -2
  58. package/dist/widgets/UrlField.d.ts +1 -1
  59. package/dist/widgets/UserField.d.ts +1 -1
  60. package/dist/widgets/VectorField.d.ts +1 -1
  61. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  62. package/dist/widgets/address-format.d.ts +27 -7
  63. package/dist/widgets/file-affordance.d.ts +33 -0
  64. package/dist/widgets/file-size-guard.d.ts +5 -23
  65. package/dist/widgets/file-value.d.ts +38 -9
  66. package/dist/widgets/lookupColumnDisplay.d.ts +110 -0
  67. package/dist/widgets/nativeDateValue.d.ts +9 -27
  68. package/dist/widgets/number-format.d.ts +137 -0
  69. package/dist/widgets/numberBadInput.d.ts +135 -0
  70. package/dist/widgets/personDisplay.d.ts +22 -4
  71. package/dist/widgets/richTextDisplay.d.ts +116 -0
  72. package/dist/widgets/toDomProps.d.ts +10 -1
  73. package/dist/widgets/toHostGroupProps.d.ts +172 -21
  74. package/dist/widgets/toHostProps.d.ts +105 -0
  75. package/dist/widgets/types.d.ts +19 -6
  76. package/dist/widgets/upload-error-message.d.ts +6 -0
  77. package/dist/widgets/uploadingScope.d.ts +142 -0
  78. package/dist/widgets/useCascadingOptions.d.ts +3 -2
  79. package/dist/widgets/useRecordQuery.d.ts +4 -2
  80. package/dist/widgets/useUploadingSignal.d.ts +8 -0
  81. package/package.json +11 -13
  82. package/dist/widgets/CharacterCount.d.ts +0 -56
  83. package/src/FieldEditWidget.test.ts +0 -133
  84. package/src/FieldEditWidget.tsx +0 -207
  85. package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
  86. package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
  87. package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
  88. package/src/__tests__/DateCellRenderer.test.tsx +0 -82
  89. package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
  90. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  91. package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
  92. package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
  93. package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
  94. package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
  95. package/src/__tests__/cell-truncation.test.tsx +0 -96
  96. package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
  97. package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
  98. package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
  99. package/src/__tests__/date-locale-channel.test.tsx +0 -305
  100. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  101. package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
  102. package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
  103. package/src/__tests__/group-labelling-declaration.test.ts +0 -148
  104. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  105. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  106. package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
  107. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  108. package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
  109. package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
  110. package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
  111. package/src/__tests__/registry-collision.test.ts +0 -91
  112. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  113. package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
  114. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  115. package/src/__tests__/validation-feedback.test.tsx +0 -361
  116. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  117. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  118. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
  119. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  120. package/src/coerce-safe-value.test.ts +0 -34
  121. package/src/complex-widgets.test.tsx +0 -924
  122. package/src/currency.ts +0 -144
  123. package/src/datetime-widgets.test.tsx +0 -185
  124. package/src/field-type-alias.multiple.test.ts +0 -81
  125. package/src/field-type-alias.ts +0 -156
  126. package/src/field-type-coverage.test.ts +0 -67
  127. package/src/index.css +0 -44
  128. package/src/index.tsx +0 -2745
  129. package/src/standard-widgets.test.tsx +0 -364
  130. package/src/widgets/AddressField.postalCode.test.tsx +0 -257
  131. package/src/widgets/AddressField.tsx +0 -182
  132. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
  133. package/src/widgets/AutoNumberField.tsx +0 -18
  134. package/src/widgets/AvatarField.tsx +0 -117
  135. package/src/widgets/BooleanField.tsx +0 -86
  136. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  137. package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
  138. package/src/widgets/CharacterCount.tsx +0 -263
  139. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  140. package/src/widgets/CheckboxesField.tsx +0 -169
  141. package/src/widgets/CodeField.tsx +0 -41
  142. package/src/widgets/ColorField.tsx +0 -53
  143. package/src/widgets/CurrencyField.tsx +0 -135
  144. package/src/widgets/DateField.tsx +0 -40
  145. package/src/widgets/DateTimeField.tsx +0 -46
  146. package/src/widgets/EmailField.tsx +0 -37
  147. package/src/widgets/FileField.tsx +0 -512
  148. package/src/widgets/FilterConditionField.tsx +0 -454
  149. package/src/widgets/FormulaField.tsx +0 -38
  150. package/src/widgets/FullscreenFieldEditor.tsx +0 -60
  151. package/src/widgets/GeolocationField.tsx +0 -193
  152. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  153. package/src/widgets/GridField.test.tsx +0 -632
  154. package/src/widgets/GridField.tsx +0 -1070
  155. package/src/widgets/ImageCropperDialog.tsx +0 -245
  156. package/src/widgets/ImageField.maxSize.test.tsx +0 -154
  157. package/src/widgets/ImageField.tsx +0 -305
  158. package/src/widgets/ImageLightbox.test.tsx +0 -52
  159. package/src/widgets/ImageLightbox.tsx +0 -108
  160. package/src/widgets/LocationField.tsx +0 -57
  161. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  162. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  163. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  164. package/src/widgets/LookupField.idField.test.tsx +0 -61
  165. package/src/widgets/LookupField.tsx +0 -1295
  166. package/src/widgets/MarkdownContent.tsx +0 -19
  167. package/src/widgets/MasterDetailField.tsx +0 -131
  168. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  169. package/src/widgets/MultiSelectField.tsx +0 -196
  170. package/src/widgets/NumberField.tsx +0 -49
  171. package/src/widgets/ObjectField.tsx +0 -94
  172. package/src/widgets/ObjectRefField.tsx +0 -121
  173. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  174. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  175. package/src/widgets/OptionsEmptyState.tsx +0 -99
  176. package/src/widgets/PasswordField.tsx +0 -49
  177. package/src/widgets/PeoplePicker.test.tsx +0 -360
  178. package/src/widgets/PeoplePicker.tsx +0 -579
  179. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  180. package/src/widgets/PercentField.tsx +0 -95
  181. package/src/widgets/PersonRow.tsx +0 -117
  182. package/src/widgets/PhoneField.tsx +0 -37
  183. package/src/widgets/QRCodeField.tsx +0 -103
  184. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  185. package/src/widgets/RadioField.tsx +0 -143
  186. package/src/widgets/RatingField.tsx +0 -94
  187. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  188. package/src/widgets/RecipientPickerField.tsx +0 -188
  189. package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
  190. package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
  191. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  192. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  193. package/src/widgets/RecordPickerDialog.tsx +0 -1365
  194. package/src/widgets/RichTextField.tsx +0 -176
  195. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  196. package/src/widgets/SelectField.tsx +0 -193
  197. package/src/widgets/SelectionTray.tsx +0 -110
  198. package/src/widgets/SignatureField.tsx +0 -140
  199. package/src/widgets/SliderField.tsx +0 -39
  200. package/src/widgets/SummaryField.tsx +0 -31
  201. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  202. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  203. package/src/widgets/TagsField.tsx +0 -88
  204. package/src/widgets/TextAreaField.tsx +0 -213
  205. package/src/widgets/TextField.tsx +0 -45
  206. package/src/widgets/TimeField.tsx +0 -31
  207. package/src/widgets/UrlField.tsx +0 -46
  208. package/src/widgets/UserField.tsx +0 -59
  209. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  210. package/src/widgets/VectorField.tsx +0 -29
  211. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  212. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
  213. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  214. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  215. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  216. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  217. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  218. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  219. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  220. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  221. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  222. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  223. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  224. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  225. package/src/widgets/address-format.ts +0 -120
  226. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  227. package/src/widgets/deriveLookupColumns.ts +0 -108
  228. package/src/widgets/file-size-guard.ts +0 -55
  229. package/src/widgets/file-value.test.ts +0 -230
  230. package/src/widgets/file-value.ts +0 -246
  231. package/src/widgets/nativeDateValue.test.ts +0 -115
  232. package/src/widgets/nativeDateValue.ts +0 -112
  233. package/src/widgets/openNativePicker.ts +0 -18
  234. package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
  235. package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
  236. package/src/widgets/personDisplay.test.ts +0 -113
  237. package/src/widgets/personDisplay.ts +0 -92
  238. package/src/widgets/recentLookups.test.ts +0 -45
  239. package/src/widgets/recentLookups.ts +0 -47
  240. package/src/widgets/toDomProps.ts +0 -204
  241. package/src/widgets/toHostGroupProps.ts +0 -102
  242. package/src/widgets/types.ts +0 -242
  243. package/src/widgets/useCascadingOptions.ts +0 -46
  244. package/src/widgets/useFieldTranslation.ts +0 -132
  245. package/src/widgets/useRecordQuery.test.ts +0 -174
  246. package/src/widgets/useRecordQuery.ts +0 -291
  247. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  248. package/src/widgets/useUploadingSignal.ts +0 -20
  249. package/src/withFieldCarrier.tsx +0 -75
@@ -0,0 +1,142 @@
1
+ import * as React from 'react';
2
+ /**
3
+ * An ambient "is any upload still in flight?" scope for a whole form subtree.
4
+ *
5
+ * ## Why this exists beside `onUploadingChange`, and why it is not a second contract
6
+ *
7
+ * `onUploadingChange` (ADR-0059) is a PER-WIDGET prop: a host that renders one
8
+ * upload control and knows its name can pass a callback and keep the answer
9
+ * itself. That is `ActionParamDialog`, which holds a `Record<paramName, boolean>`
10
+ * because it lays out its own params one by one.
11
+ *
12
+ * A record form cannot do that. Its fields are rendered by the `form` node
13
+ * renderer from a `fields` array — the host hands over a list of field configs
14
+ * and never touches the widget — and its upload widgets may sit an arbitrary
15
+ * number of levels down (a section, a tab, a line-items subform). There is no
16
+ * place in that chain where the form host can attach a per-widget callback, so
17
+ * a record form was left with no notion of upload state at all and could submit
18
+ * mid-upload: the record was written WITHOUT the attachment and reported
19
+ * success (objectui#10166).
20
+ *
21
+ * The scope is the aggregation, not a second producer. `useUploadingSignal` is
22
+ * still the ONE place a widget's in-flight state is published; it drives the
23
+ * host prop and this scope from that same call, so the two can never disagree.
24
+ * A widget reports nothing extra and knows nothing about the scope, and a host
25
+ * that mounts no provider (every host on `main` today) is unaffected — the
26
+ * context read answers `null` and the reporting hook is inert.
27
+ *
28
+ * ## The one thing a mount-bound report cannot see (objectui#10180)
29
+ *
30
+ * A widget's in-flight state is React state, so it ends when the widget
31
+ * unmounts — and an upload does not. `FileField` does not abort its upload
32
+ * when it unmounts; the promise settles later and hands the value to the
33
+ * `onChange` it captured, which the form still accepts. So "the widget
34
+ * unmounted" is not "the upload ended". A wizard step the user leaves is the
35
+ * everyday case: leaving the step drops the widget's report, the wizard's final
36
+ * gate reads `false` while the upload is still running, and a Create pressed in
37
+ * that window writes the record WITHOUT the attachment and reports success.
38
+ *
39
+ * The in-repo upload widgets therefore also hold the scope for the lifetime of
40
+ * the upload ITSELF, through {@link useUploadingScopeHold}: taken when the
41
+ * upload starts, released when its promise settles, whether or not the widget
42
+ * is still mounted by then. While the widget is mounted the two entries start
43
+ * and stop together, so they cannot disagree; after it unmounts the hold is the
44
+ * only entry left, and it is the true one. The hold is internal to this package
45
+ * — it is not exported from the package entry.
46
+ */
47
+ /**
48
+ * The write half of a scope. Kept separate from the boolean so a widget that
49
+ * only REPORTS does not re-render when some other widget's upload flips —
50
+ * reporters read this context, the host owns the boolean.
51
+ */
52
+ interface UploadingScopeSink {
53
+ /**
54
+ * Record whether the widget registered under `id` is currently uploading.
55
+ * Reporting `false` for an unknown id is a no-op, which is what makes the
56
+ * unmount release below safe to call unconditionally.
57
+ */
58
+ report: (id: string, uploading: boolean) => void;
59
+ }
60
+ /** What {@link useUploadingScope} hands a host. */
61
+ export interface UploadingScope {
62
+ /** True while at least one upload widget inside the scope is in flight. */
63
+ anyUploading: boolean;
64
+ /**
65
+ * Passed to {@link UploadingScopeProvider}. Opaque to the host: it is the
66
+ * channel the widgets report on, not state the host is meant to read.
67
+ */
68
+ sink: UploadingScopeSink;
69
+ }
70
+ /**
71
+ * Own an uploading scope. The host reads `anyUploading` to gate its own submit
72
+ * and to say WHY that control is disabled, and renders its form subtree inside
73
+ * {@link UploadingScopeProvider}.
74
+ *
75
+ * The host owns the state rather than reading it back out of the context on
76
+ * purpose: a provider cannot consume its own value, and splitting it would give
77
+ * "is anything uploading" two authors.
78
+ */
79
+ export declare function useUploadingScope(): UploadingScope;
80
+ /**
81
+ * Publish a host's {@link useUploadingScope} to every upload widget rendered
82
+ * below it. Nesting is legal and CHAINS: an inner scope gates its own Save on
83
+ * its own uploads AND reports itself to the scope above, so an outer Save whose
84
+ * write encompasses the inner content is never blind to it. See
85
+ * {@link useUploadingScope} for why the direction is not a preference.
86
+ */
87
+ export declare function UploadingScopeProvider({ scope, children, }: {
88
+ scope: UploadingScope;
89
+ children: React.ReactNode;
90
+ }): React.ReactElement;
91
+ /**
92
+ * Report this widget's in-flight state to the enclosing scope, if there is one.
93
+ * Called by `useUploadingSignal` so every widget that already publishes to a
94
+ * host prop publishes here too, from the same state.
95
+ *
96
+ * ⚠️ The unmount release is load-bearing, not tidiness. A widget removed while
97
+ * its upload is in flight — a section collapsing, a tab switching, a subform
98
+ * row deleted — would otherwise leave its id in the set with nothing left to
99
+ * ever clear it, and the form's Save would stay disabled for the rest of the
100
+ * session. That failure mode is worse than the defect this scope fixes,
101
+ * because it has no user-visible cause at all.
102
+ *
103
+ * This report covers the widget's MOUNTED lifetime only. The upload's own
104
+ * lifetime, which can outlast the widget, is covered by
105
+ * {@link useUploadingScopeHold} — that is what keeps the release above from
106
+ * also releasing an upload that is still running (objectui#10180).
107
+ */
108
+ export declare function useUploadingScopeReport(uploading: boolean): void;
109
+ /**
110
+ * Hold the enclosing scope "uploading" for the lifetime of ONE upload
111
+ * (objectui#10180). The returned `hold()` is called when an upload starts and
112
+ * answers that upload's `release()`, to be called once its promise settles —
113
+ * in a `finally`, so a failed upload releases too.
114
+ *
115
+ * ## Why this is separate from the mount-bound report
116
+ *
117
+ * {@link useUploadingScopeReport} has to release on unmount (see there), and
118
+ * an upload keeps running after the widget that started it has unmounted —
119
+ * `FileField` does not abort it, and its settle still reaches the form's
120
+ * `onChange`. Leaving a wizard step mid-upload is exactly that: the step's
121
+ * widgets unmount, their reports are released, and without this hold the
122
+ * wizard's final gate read `false` while the file was still on its way, so a
123
+ * Create pressed in that window wrote the record without it.
124
+ *
125
+ * ## Why this cannot wedge the form
126
+ *
127
+ * The entry is released by the upload's own settle, not by a render, so it is
128
+ * cleared exactly when the upload ends, mounted widget or not. It can stay
129
+ * held only as long as the upload itself stays unsettled — the same bound a
130
+ * mounted widget's report already has — and while it is held the host shows
131
+ * the reason it gives for any upload in flight.
132
+ *
133
+ * The sink is captured when the upload STARTS: the scope the user picked the
134
+ * file under is the one whose save the file belongs to. A release that reaches
135
+ * a scope whose host has since unmounted is a no-op, and that host already
136
+ * released itself from any scope above it on the way out.
137
+ *
138
+ * Inert without a provider, like the report. Not exported from the package
139
+ * entry: it is how this package's own upload widgets report, not a contract.
140
+ */
141
+ export declare function useUploadingScopeHold(): () => () => void;
142
+ export {};
@@ -9,8 +9,9 @@ import { CascadingOptions, OptionLike, DependsOnInput } from '@object-ui/core';
9
9
  * empty the list is *gated* — callers surface a "select the parent first" hint
10
10
  * rather than an unfiltered set, mirroring the dependent-lookup UX.
11
11
  *
12
- * This is the React wrapper that sources `record` (the live form values) and the
13
- * predicate scope from context; the actual resolution is the pure
12
+ * This is the React wrapper: `record` (the live form values) arrives on the
13
+ * `dependentValues` argument its HOST passes, the predicate scope comes from
14
+ * context, and the actual resolution is the pure
14
15
  * {@link resolveCascadingOptions} in `@object-ui/core`, shared with the form
15
16
  * renderer so gating/filtering can never drift between them (#2715).
16
17
  */
@@ -12,7 +12,9 @@ import { DataSource } from '@object-ui/types';
12
12
  * (`{ data, total }`, tolerating a bare array),
13
13
  * - owns `records`/`loading`/`error`/`total` plus the `page`/`search`/`sort`
14
14
  * controls, with a debounced search path,
15
- * - clears itself when `enabled` goes false (e.g. a dialog closing).
15
+ * - clears itself when it can no longer query — `enabled` false (e.g. a dialog
16
+ * closing), or no data source or object name — superseding any read still in
17
+ * flight, so that read's answer commits nothing (objectui#10712).
16
18
  *
17
19
  * The caller keeps ownership of *selection* state and of record→option mapping;
18
20
  * this hook only answers "what records match the current query". It is the
@@ -43,7 +45,7 @@ export interface UseRecordQueryOptions {
43
45
  */
44
46
  paginate?: boolean;
45
47
  /**
46
- * `$filter` — already merged by the caller (base `lookup_filters`, dependent
48
+ * `$filter` — already merged by the caller (base `lookupFilters`, dependent
47
49
  * lookup chain, candidate hygiene like `banned != true`, …). Compared by
48
50
  * value, so a referentially-new-but-equal object each render will not loop.
49
51
  *
@@ -5,5 +5,13 @@
5
5
  * submitted mid-upload (the value only becomes the fileId once the upload
6
6
  * settles). Fires with the latest callback and only when `uploading` actually
7
7
  * changes, so an inline arrow prop doesn't thrash the parent every render.
8
+ *
9
+ * It ALSO reports into the ambient `UploadingScope` when one is mounted above
10
+ * the widget (objectui#10166). The prop suits a host that renders one named
11
+ * control; a record form hands its fields to the `form` node renderer and never
12
+ * touches the widget, so it has no place to attach a callback — the scope is how
13
+ * that host learns an upload is in flight. Both sinks are driven from this one
14
+ * call so they cannot disagree, and a host that mounts no provider is
15
+ * unaffected: `useUploadingScopeReport` is inert without one.
8
16
  */
9
17
  export declare function useUploadingSignal(uploading: boolean, onUploadingChange?: (uploading: boolean) => void): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/fields",
3
- "version": "17.5.0",
3
+ "version": "17.7.0",
4
4
  "description": "Field renderers and registry for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,25 +18,23 @@
18
18
  },
19
19
  "files": [
20
20
  "dist",
21
- "src",
22
21
  "README.md",
23
22
  "CHANGELOG.md",
24
23
  "LICENSE"
25
24
  ],
26
25
  "dependencies": {
27
- "@objectstack/spec": "^17.0.0-rc.6",
28
- "clsx": "^2.1.1",
29
- "lucide-react": "^1.29.0",
26
+ "@objectstack/spec": "^17.6.0",
27
+ "lucide-react": "^1.43.0",
30
28
  "react-markdown": "^10.1.0",
31
29
  "rehype-sanitize": "^6.0.0",
32
30
  "remark-gfm": "^4.0.1",
33
- "tailwind-merge": "^3.6.0",
34
- "@object-ui/components": "17.5.0",
35
- "@object-ui/core": "17.5.0",
36
- "@object-ui/i18n": "17.5.0",
37
- "@object-ui/providers": "17.5.0",
38
- "@object-ui/react": "17.5.0",
39
- "@object-ui/types": "17.5.0"
31
+ "@object-ui/components": "17.7.0",
32
+ "@object-ui/core": "17.7.0",
33
+ "@object-ui/permissions": "17.7.0",
34
+ "@object-ui/i18n": "17.7.0",
35
+ "@object-ui/providers": "17.7.0",
36
+ "@object-ui/react": "17.7.0",
37
+ "@object-ui/types": "17.7.0"
40
38
  },
41
39
  "peerDependencies": {
42
40
  "react": "^18.0.0 || ^19.0.0",
@@ -84,7 +82,7 @@
84
82
  "build": "tsc && vite build && node scripts/build-css.mjs",
85
83
  "clean": "rm -rf dist",
86
84
  "type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
87
- "test": "vitest run",
85
+ "test": "vitest run --root ../.. packages/fields/",
88
86
  "lint": "eslint ."
89
87
  }
90
88
  }
@@ -1,56 +0,0 @@
1
- import { default as React } from 'react';
2
- export interface CharacterCountProps {
3
- /**
4
- * Length of the text this surface is counting. The inline surface passes the
5
- * committed value's length; the fullscreen dialog passes the DRAFT's, which
6
- * is the number that is true of what the user is looking at.
7
- */
8
- length: number;
9
- /**
10
- * The declared cap. Callers render this component only when the field
11
- * declares one, so it is non-optional here — a counter with no cap to count
12
- * against has nothing to say and must not be mounted at all (an unconditional
13
- * live region in every long-text field is the objectui#3408 defect back
14
- * again).
15
- */
16
- maxLength: number;
17
- /**
18
- * The description node's `id`, minted by the host from `useId()`.
19
- *
20
- * The host MUST also name it in the edited control's `aria-describedby` —
21
- * this component renders the sentence, only the host knows which element is
22
- * being described, and a description nothing references is a node no screen
23
- * reader will ever reach. That association is asserted from the OUTSIDE (the
24
- * tests resolve `aria-describedby` and concatenate the referenced nodes' text,
25
- * exactly as an assistive technology does) rather than trusted here.
26
- *
27
- * Two surfaces of one widget therefore need two ids: the dialog's draft and
28
- * the inline value diverge the moment the user types in the dialog, so one
29
- * shared id would point both controls at whichever sentence rendered last.
30
- */
31
- descriptionId: string;
32
- /**
33
- * Render node 3, the threshold-gated debounced `aria-live` status region.
34
- *
35
- * Required on purpose — see the component doc above. `false` removes the
36
- * region from the DOM entirely (not merely empties it), which is the only
37
- * form of "no live region" a test can tell apart from "a live region that
38
- * happens to be silent right now".
39
- */
40
- announceNearLimit: boolean;
41
- /**
42
- * Classes for the visible digits. Required because placement is entirely the
43
- * surface's business — inline pins them to the textarea's bottom-right
44
- * corner, the dialog lays them out as a footer item — and a default borrowed
45
- * from either one would position the other wrongly while looking deliberate.
46
- */
47
- className: string;
48
- /**
49
- * `data-testid` for the visible digits, namespaced per surface
50
- * (`textarea-character-count`, `textarea-fullscreen-character-count`) so a
51
- * test that finds digits can say WHICH surface it found them on. Same
52
- * convention, same reason, as `FullscreenFieldEditor`'s `testIdPrefix`.
53
- */
54
- testId: string;
55
- }
56
- export declare function CharacterCount({ length, maxLength, descriptionId, announceNearLimit, className, testId, }: CharacterCountProps): React.JSX.Element;
@@ -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
- }