@object-ui/fields 17.6.0 → 18.0.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 (64) hide show
  1. package/CHANGELOG.md +6228 -0
  2. package/README.md +251 -9
  3. package/dist/FieldEditWidget.d.ts +54 -6
  4. package/dist/coerceToSafeValue.d.ts +54 -0
  5. package/dist/currency.d.ts +1 -1
  6. package/dist/fieldHelpText.d.ts +41 -0
  7. package/dist/index.cjs +5 -5
  8. package/dist/index.css +68 -5
  9. package/dist/index.d.ts +446 -69
  10. package/dist/index.js +5326 -3443
  11. package/dist/resolveActionParams.d.ts +259 -0
  12. package/dist/widgets/AvatarField.d.ts +19 -3
  13. package/dist/widgets/BooleanField.d.ts +1 -1
  14. package/dist/widgets/DateField.d.ts +1 -1
  15. package/dist/widgets/DateTimeField.d.ts +1 -1
  16. package/dist/widgets/FilterConditionField.d.ts +21 -7
  17. package/dist/widgets/FormulaField.d.ts +41 -0
  18. package/dist/widgets/GeolocationField.d.ts +9 -3
  19. package/dist/widgets/GridField.d.ts +73 -101
  20. package/dist/widgets/LocationField.d.ts +30 -3
  21. package/dist/widgets/LookupField.d.ts +8 -0
  22. package/dist/widgets/NumberField.d.ts +1 -1
  23. package/dist/widgets/ObjectField.d.ts +9 -2
  24. package/dist/widgets/OptionsEmptyState.d.ts +29 -5
  25. package/dist/widgets/PasswordField.d.ts +14 -0
  26. package/dist/widgets/PeoplePicker.d.ts +17 -2
  27. package/dist/widgets/PercentField.d.ts +2 -1
  28. package/dist/widgets/PersonRow.d.ts +13 -2
  29. package/dist/widgets/RecipientPickerField.d.ts +75 -0
  30. package/dist/widgets/RecordPickerDialog.d.ts +31 -12
  31. package/dist/widgets/RegistryRefField.d.ts +77 -0
  32. package/dist/widgets/RichTextField.d.ts +1 -1
  33. package/dist/widgets/SelectionTray.d.ts +13 -2
  34. package/dist/widgets/SummaryField.d.ts +30 -0
  35. package/dist/widgets/TextField.d.ts +1 -1
  36. package/dist/widgets/TimeField.d.ts +1 -1
  37. package/dist/widgets/booleanValueLabel.d.ts +18 -0
  38. package/dist/widgets/date-picker/DatePickerField.d.ts +40 -0
  39. package/dist/widgets/date-picker/localeDateText.d.ts +39 -0
  40. package/dist/widgets/deriveLookupColumns.d.ts +1 -1
  41. package/dist/widgets/file-affordance.d.ts +63 -0
  42. package/dist/widgets/file-refused.d.ts +17 -0
  43. package/dist/widgets/file-size-guard.d.ts +5 -23
  44. package/dist/widgets/file-value.d.ts +49 -9
  45. package/dist/widgets/longTextCarrierHandoff.d.ts +35 -0
  46. package/dist/widgets/lookupColumnDisplay.d.ts +26 -9
  47. package/dist/widgets/nativeDateValue.d.ts +9 -27
  48. package/dist/widgets/number-format.d.ts +137 -0
  49. package/dist/widgets/numberBadInput.d.ts +135 -0
  50. package/dist/widgets/objectRefLabel.d.ts +16 -0
  51. package/dist/widgets/personDisplay.d.ts +22 -4
  52. package/dist/widgets/recipientKinds.d.ts +85 -0
  53. package/dist/widgets/richTextDisplay.d.ts +32 -13
  54. package/dist/widgets/searchOffer.d.ts +8 -0
  55. package/dist/widgets/toDomProps.d.ts +26 -4
  56. package/dist/widgets/toHostGroupProps.d.ts +53 -0
  57. package/dist/widgets/toHostProps.d.ts +105 -0
  58. package/dist/widgets/types.d.ts +11 -3
  59. package/dist/widgets/upload-error-message.d.ts +6 -0
  60. package/dist/widgets/uploadingScope.d.ts +142 -0
  61. package/dist/widgets/useCascadingOptions.d.ts +3 -2
  62. package/dist/widgets/useRecordQuery.d.ts +17 -2
  63. package/dist/widgets/useUploadingSignal.d.ts +8 -0
  64. package/package.json +11 -12
package/README.md CHANGED
@@ -5,7 +5,7 @@ The standard field library and registry for Object UI.
5
5
  ## Features
6
6
 
7
7
  - 📚 **Standard Fields** - Implementation of all ObjectStack protocol fields (Text, Number, Date, Lookup, etc.)
8
- - 🔌 **Plugin System** - `FieldRegistry` allows registering custom renderers or overriding standard ones.
8
+ - 🔌 **Plugin System** - `registerFieldRenderer` registers custom renderers, or overrides standard ones.
9
9
  - 🛠 **Helpers** - Utilities for schema mapping, validation, and expression evaluation.
10
10
 
11
11
  ## Installation
@@ -23,10 +23,15 @@ The Field Registry is the core mechanism that allows decoupling view components
23
23
  You can override standard fields or add new ones:
24
24
 
25
25
  ```tsx
26
- import { registerFieldRenderer } from '@object-ui/fields';
27
- import { MyCustomColorPicker } from './MyCustomColorPicker';
26
+ import { registerFieldRenderer, type CellRendererProps } from '@object-ui/fields';
27
+ import type { FC } from 'react';
28
28
 
29
- // Register a new 'color' field type
29
+ // Your own renderer — anywhere in your app; it takes the standard props bag.
30
+ declare const MyCustomColorPicker: FC<CellRendererProps>;
31
+
32
+ // ⚠️ `color` is a SHIPPED type (`ColorSwatchCellRenderer`), and `getCellRenderer`
33
+ // reads the registry BEFORE the standard map — so this call OVERRIDES the
34
+ // built-in renderer. To ADD a type instead, register a name nothing ships.
30
35
  registerFieldRenderer('color', MyCustomColorPicker);
31
36
  ```
32
37
 
@@ -35,14 +40,33 @@ registerFieldRenderer('color', MyCustomColorPicker);
35
40
  View components use `getCellRenderer` to resolve the correct component for a field type.
36
41
 
37
42
  ```tsx
38
- import { getCellRenderer } from '@object-ui/fields';
39
-
40
- const MyGridCell = ({ field, value }) => {
41
- const Renderer = getCellRenderer(field.type);
42
- return <Renderer field={field} value={value} />;
43
+ import { getCellRenderer, resolveCellRendererType, type CellRendererProps } from '@object-ui/fields';
44
+
45
+ const MyGridCell = ({ field, value, row }: CellRendererProps & { row: Record<string, unknown> }) => {
46
+ // Resolve the renderer KEY first, then the renderer. A field's declared
47
+ // `type` is not always the renderer's key: a textual field carrying a
48
+ // format hint (`Field.text({ format: 'phone' })`) resolves to the richer
49
+ // renderer, and one declaring a widget with a read cell of its own
50
+ // (`widget: 'object-ref'`, `widget: 'recipient-picker'`) resolves to that
51
+ // cell. Passing `field.type` raw skips that mapping and draws such a
52
+ // column as bare text, silently. Resolving first is never worse: with no
53
+ // hint the resolver returns the declared `type` unchanged.
54
+ const Renderer = getCellRenderer(resolveCellRendererType(field));
55
+ // `dependentValues` is the row, for a cell whose value only means something
56
+ // beside a sibling column: a `recipient-picker` face reads the row's
57
+ // `recipient_type` from it. Without it, that face draws the stored text.
58
+ return <Renderer field={field} value={value} dependentValues={row} />;
43
59
  };
44
60
  ```
45
61
 
62
+ This package registers the `object-ref` and `recipient-picker` keys with the stored
63
+ text as their default face. A host that can read the referenced lists registers faces
64
+ that name the value under the same keys (`registerFieldRenderer`); the console's live in
65
+ `@object-ui/app-shell`. They name a value as the edit widgets do, through the kind table
66
+ and the spellings this package publishes for it: `RECIPIENT_RECORD_KINDS`,
67
+ `RECIPIENT_REGISTRY_KINDS`, `FIELD_RECIPIENT_TYPE`, `recipientCandidateLabel`,
68
+ `objectRefOptionLabel` and the shared `UnresolvedReferenceMark` drawing.
69
+
46
70
  ## Standard Field Types
47
71
 
48
72
  Supported types out of the box:
@@ -55,6 +79,33 @@ Supported types out of the box:
55
79
  - **Media**: `file`, `image`
56
80
  - **System**: `formula`, `summary`, `auto_number`
57
81
 
82
+ ### `type="number"` widgets: what is announced and what is not
83
+
84
+ `NumberField`, `CurrencyField`, `PercentField` and `GeolocationField` all render
85
+ a native `type="number"` input, so the **browser** decides what the box accepts.
86
+ They share one reading of that, in `widgets/numberBadInput.tsx`:
87
+
88
+ - **Announced.** When the browser reports `validity.badInput` — the box is
89
+ holding text it cannot convert, e.g. a typed `1e`, which Chromium keeps
90
+ DISPLAYING while `.value` reads `''` — the control is marked `aria-invalid`
91
+ and draws a `Not saved: …` message, reusing objectui#6716's refusal shape.
92
+ Both a change arm and a blur arm are wired, because pasting into an empty box
93
+ never moves `.value` and so fires no React change event at all. The sentence
94
+ resolves through the `fields.number.badInput` locale key (objectui#8148), so
95
+ it follows the reader's language; the example numeral each box quotes is the
96
+ widget's own and is interpolated rather than translated.
97
+ - ⚠️ **Not announced, and not announceable.** Entries the browser silently
98
+ **truncates**: `1.2.3` stores `1.23`, `0x10` stores `10`. The characters are
99
+ discarded as they arrive, before any handler here runs, so no widget-side
100
+ guard can refuse them. Recovering them would mean abandoning `type="number"`
101
+ and with it the mobile numeric keyboard and the `min`/`max`/`step` spinner
102
+ (objectui#2572).
103
+
104
+ ⛔ Silence therefore means "the browser read *something*", never "the value is
105
+ correct". User-facing wording lives in
106
+ [the fields guide](../../content/docs/guide/fields.md). The measured browser vs
107
+ happy-dom matrix is in `src/__tests__/numberInputBrowserReadings.ts`.
108
+
58
109
  ### Rendering form field widgets outside the form
59
110
 
60
111
  The full widget surface is exported for consumers that render field widgets
@@ -72,6 +123,178 @@ The app-shell `ActionParamDialog` uses these to render declared action params
72
123
  through the exact same widgets as the object form — with a drift test pinning
73
124
  param support ⊇ form support.
74
125
 
126
+ ### Resolving declared action params
127
+
128
+ An action param is declared either inline (`name` + `type`) or field-backed
129
+ (`field`, plus `objectOverride` when the field lives on another object). Before
130
+ a dialog can render one, it has to be resolved against the object's field
131
+ definitions. This package holds the one resolver for that, shared by the
132
+ record page's action dialog (`@object-ui/app-shell`) and the selection bar's
133
+ bulk dialog (`@object-ui/plugin-grid`):
134
+
135
+ ```ts
136
+ import {
137
+ resolveActionParams,
138
+ withKnownObjects,
139
+ type RawActionParam,
140
+ type ResolveActionParamsContext,
141
+ } from '@object-ui/fields';
142
+
143
+ type KnownObject = ResolveActionParamsContext['objects'][number];
144
+
145
+ // The object definitions the caller renders, and the ones the session's
146
+ // metadata store holds (`useMetadata().objects` from `@object-ui/react`).
147
+ declare const callerObjects: KnownObject[];
148
+ declare const storeObjects: KnownObject[];
149
+
150
+ const params: RawActionParam[] = [
151
+ { field: 'crm_campaign', objectOverride: 'crm_campaign_member' }, // field-backed
152
+ { name: 'note', type: 'textarea', label: 'Note' }, // inline
153
+ ];
154
+
155
+ const resolved = resolveActionParams(params, {
156
+ objectName: 'crm_lead', // the owner when a param names no objectOverride
157
+ objects: withKnownObjects(callerObjects, storeObjects), // the caller's defs win by name
158
+ fieldLabel: (_objectName, _fieldName, fallback) => fallback,
159
+ locale: 'en',
160
+ });
161
+ // resolved[0] takes its type, label and `referenceTo` from the
162
+ // `crm_campaign_member.crm_campaign` field definition; resolved[1] is as declared.
163
+ ```
164
+
165
+ - `resolveActionParam(param, ctx)` / `resolveActionParams(params, ctx)` — a
166
+ field-backed param inherits the field's type, label, options, placeholder,
167
+ help text, default and picker config. Inline keys on the param override the
168
+ field. A param whose field is not in `ctx.objects` comes back with
169
+ `unresolvedField: '<object>.<field>'`, and both dialogs refuse it rather than
170
+ drawing an input.
171
+ - `withKnownObjects(supplied, stored)` — unions two object lists by `name`,
172
+ the caller's first.
173
+ - `resolveParamWidgetType(type)` — the param-type fold (`checkbox`,
174
+ `reference`, `datetime-local` aliases) onto `resolveFormWidgetType`.
175
+ - `RESOLVED_ONLY_PARAM_KEYS` — keys of the resolved shape an author may not
176
+ write on a param (`referenceTo`, `displayField`, …), each with the fix the
177
+ resolver's dev warning prints.
178
+ - Types: `RawActionParam`, `RawActionParamOption`, `ResolveActionParamsContext`.
179
+
180
+ ### A field's help line
181
+
182
+ `fieldHelpText(field)` is the one read of the help line a field draws under its
183
+ control. The record form (`@object-ui/plugin-form`) and a field-backed action
184
+ param both use it, so the two cannot disagree:
185
+
186
+ ```ts
187
+ import { fieldHelpText } from '@object-ui/fields';
188
+
189
+ fieldHelpText({ inlineHelpText: 'Shown under the control', description: 'About the field' });
190
+ // → 'Shown under the control'
191
+ fieldHelpText({ inlineHelpText: '', description: 'About the field' });
192
+ // → 'About the field' (an empty string reads as absent)
193
+ ```
194
+
195
+ It reads `@objectstack/spec`'s `inlineHelpText`, then `description`. It does
196
+ not read `help`: the spec refuses that key on a field, and an objectstack
197
+ server on spec 17.7.0 or later serves a field's translated help text on
198
+ `description`.
199
+
200
+ ### Enumerating the CELL renderer registry
201
+
202
+ `listCellRendererTypes()` is the read-side twin of `FORM_FIELD_TYPES`: every
203
+ field type `getCellRenderer` resolves to a renderer **of its own**, as opposed
204
+ to the `TextCellRenderer` fallback every other spelling lands on.
205
+
206
+ ⚠️ It is a **function**, not a frozen constant, and the difference is
207
+ load-bearing. `registerFieldRenderer` is published, so the cell registry can
208
+ grow after this module is evaluated; a constant would be a snapshot taken at
209
+ import time. Consumers that reason about "all registered cell types" — the two
210
+ censuses in `@object-ui/fields` and `@object-ui/plugin-detail` that measure what
211
+ every type draws — reconcile their tables against this reading so a newly
212
+ registered type fails them by name instead of slipping past a frozen
213
+ population (objectui#8734).
214
+
215
+ ### Asking whether a cell is masked
216
+
217
+ `isMaskedFieldType(type)` answers whether a cell of that field type is drawn
218
+ as a mask (`••••••`) instead of its value. It is the read-side twin of
219
+ `isInlineExcludedFieldType()`, and a consumer that acts on a cell's value asks
220
+ it rather than keeping its own list. The detail page's copy affordance in
221
+ `@object-ui/plugin-detail` is one such consumer: it refuses to copy a masked
222
+ row (objectui#8686). `MASKED_FIELD_TYPES` is the declared set behind it,
223
+ `password` and `secret`. The standard cell of each member draws the mask, and
224
+ the predicate answers `true` for it. Like `listCellRendererTypes()`, the
225
+ answer is a live reading of the cell registry taken when you call it, and,
226
+ with nothing overridden at runtime, it agrees with what `getCellRenderer`
227
+ resolves for every type that function lists. To add a masked type, register
228
+ the package's own mask under it with
229
+ `registerFieldRenderer('api_token', getCellRenderer('password'))`, while
230
+ `password` still resolves to the shipped mask. The predicate then answers
231
+ `true` with no change to the declared set, and the detail page refuses to copy
232
+ that row. Overriding a declared type with one of this package's own renderers
233
+ makes the predicate answer `false`, since none of them is the mask, and the
234
+ cell draws what that renderer draws: after
235
+ `registerFieldRenderer('password', TextCellRenderer)` the cell shows the value
236
+ and the detail row copies again. Overriding a declared type with a component
237
+ of your own keeps the declared answer, `true`, because the package cannot
238
+ tell whether an opaque component hides the value, so it errs toward
239
+ withholding it. Your own component under an undeclared type answers `false`.
240
+ Spellings are matched raw, the way `getCellRenderer` looks them up:
241
+ `field:password` renders in the clear and is not masked.
242
+
243
+ The mask itself is exported as `MaskedCellRenderer` (objectui#10657). A table
244
+ that cannot type a column yet, because its object's field types are still
245
+ loading or their read failed, draws that column WITHHELD with it rather than as
246
+ text: `object-grid`, the related list and `object-data-table` do. It is the
247
+ shipped mask, not whatever is registered for `password` at the time, and its
248
+ `field` prop is optional because it reads only whether a value is present.
249
+ `registerFieldRenderer('api_token', MaskedCellRenderer)` masks a type the same
250
+ way the `getCellRenderer('password')` form above does.
251
+
252
+ ```tsx
253
+ import { MaskedCellRenderer } from '@object-ui/fields';
254
+
255
+ const withheldCell = (value: unknown) => <MaskedCellRenderer value={value} />;
256
+ ```
257
+
258
+ ### Polymorphic pointer pairs (`referenceVia`)
259
+
260
+ A `text` field that declares `@objectstack/spec`'s `referenceVia` is the id half
261
+ of a two-column pointer pair: its value is a record id of the object that the
262
+ sibling column it names holds, on the same row (`sys_approval_request.record_id`
263
+ with `referenceVia: 'object_name'`, or `sys_activity.source_id` with
264
+ `referenceVia: 'source_object'`). The target changes per row, and a cell
265
+ renderer gets one value and no row, so the pair is resolved by the caller that
266
+ holds the row (objectui#12045):
267
+
268
+ - `resolveRecordPointer(field, row, optionsOf?)` is the one place the rule
269
+ lives. It returns `{ objectName, recordId }` (a `RecordPointer`) for a `text`
270
+ field with a non-empty `referenceVia` whose row fills both halves, and `null`
271
+ otherwise.
272
+ - When the field also declares `referenceDeletedWhen: { field, value }` (the
273
+ platform's assertion that it deleted the target, objectui#12489) and the row's
274
+ sibling `field` holds `value`, the pair carries `deletedLabel`: that option's
275
+ label from `optionsOf(field)`, the options the caller draws `select` cells
276
+ with. A face draws it as a tombstone and reads and links nothing; the default
277
+ face draws it in place of the record id.
278
+ - `RECORD_POINTER_CARD_TYPE` is the registry key a resolved pair is drawn with,
279
+ the pair as the renderer's `value`. This package registers a default under it
280
+ that draws the record id as text, so nothing changes for a host that does not
281
+ register a face. `@object-ui/app-shell` registers its record preview card
282
+ there.
283
+
284
+ The record details grid in `@object-ui/plugin-detail` is the call site today;
285
+ list cells are not one yet.
286
+
287
+ ```tsx
288
+ import { getCellRenderer, resolveRecordPointer, RECORD_POINTER_CARD_TYPE } from '@object-ui/fields';
289
+
290
+ function PointerValue({ field, row }: { field: { name: string; type: string; referenceVia?: string }; row: Record<string, unknown> }) {
291
+ const pointer = resolveRecordPointer(field, row);
292
+ if (!pointer) return <>{String(row[field.name] ?? '')}</>;
293
+ const Face = getCellRenderer(RECORD_POINTER_CARD_TYPE);
294
+ return <Face value={pointer} field={{ name: field.name, type: 'text' }} />;
295
+ }
296
+ ```
297
+
75
298
  ### File uploads in line-item grids
76
299
 
77
300
  `GridField` (the master-detail line-items grid) supports `type: 'file'` columns:
@@ -83,6 +306,21 @@ or photo per row without opening the row form (objectui#2360). Columns accept
83
306
  exported as `FileCell`). Auto-derived subform columns map `file`/`image`/
84
307
  `avatar` fields to file columns instead of dropping them.
85
308
 
309
+ ### Registry-name pickers (`position-ref`, `permission-ref`)
310
+
311
+ A `text` field that stores the machine name of one registry item declares
312
+ `widget: 'position-ref'` or `widget: 'permission-ref'` (resolved as
313
+ `field:<widget>`). The widget lists that registry through the data source's
314
+ `listMetadataItems(type)`, shows each item's label beside its name, and writes
315
+ the name only. A stored name the loaded registry lacks stays, flagged *not
316
+ found*. `position-ref` leaves out the audience anchors (the spec's
317
+ `AUDIENCE_ANCHOR_POSITIONS`: `everyone`, `guest`), because the server refuses
318
+ an anchor as a grant target; a stored anchor still shows as its own option.
319
+ When the adapter cannot list a registry, the list is empty, or the
320
+ load fails, it is a text box that takes the typed name (a failure is shown with
321
+ its cause). See
322
+ [`content/docs/guide/fields.md`](../../content/docs/guide/fields.md).
323
+
86
324
  ### Multi-value selects
87
325
 
88
326
  A `select` field declared `multiple: true` selects zero-or-more values (spec
@@ -111,6 +349,10 @@ never drift). While a `dependsOn` parent is empty the control is gated; a parent
111
349
  change re-filters the list and clears a now-invalid value (scalar `select` /
112
350
  `radio` drop the value; multi-value `multiselect` / `checkboxes` prune just the
113
351
  offered-out entries).
352
+ An option whose `label` is blank (the empty string is a legal label; an absent one is
353
+ not) displays its `value` instead of an empty row, on the editable and the read-only
354
+ path alike — all eight read sites across the four widgets share the one
355
+ `optionDisplayLabel` helper in `@object-ui/core` (objectui#9230).
114
356
  Client-side hiding is UX only — gate authorization-sensitive values on the
115
357
  server too. See
116
358
  [`content/docs/fields/select.mdx`](../../content/docs/fields/select.mdx).
@@ -39,10 +39,58 @@ export declare function isInlineExcludedFieldType(type: string | undefined): boo
39
39
  * `null` for types without a registered widget so the caller can fall back to
40
40
  * a plain editor.
41
41
  *
42
- * `autoFocus` is forwarded because an inline-edit host enters edit mode ON a
43
- * field and expects the caret to land there (objectui#4220 — the detail page's
44
- * delegation): each widget's own `toDomProps` whitelist already carries it onto
45
- * the real focusable control, so nothing here needs to know which element that
46
- * is. A host that passes nothing is unaffected.
42
+ * The host's DOM pass-through set is forwarded WHOLE (objectui#6909). This
43
+ * component's props are `FieldWidgetComponentProps`, so a caller may already
44
+ * pass `id`, `name`, `autoFocus`, `tabIndex`, `onBlur`, `onFocus`, `onClick`,
45
+ * any `aria-*` and any `data-*` with no type error — but the body used to
46
+ * destructure five keys and render the widget with those, so `autoFocus` was
47
+ * the ONLY survivor of the whole block and everything else was silently
48
+ * dropped. That is this package's own first-class defect class, named in
49
+ * `widgets/toDomProps.ts`: a key that type-checks, reads as supported, and
50
+ * silently never reaches the element (objectui#3290's `aria-required`,
51
+ * objectui#3222's validation slot). Forwarding is not a widening: the keys were
52
+ * already declared, and each widget still re-filters through its own
53
+ * `toDomProps` before anything reaches a DOM element.
54
+ *
55
+ * `autoFocus`' original reason survives inside that set — an inline-edit host
56
+ * enters edit mode ON a field and expects the caret to land there
57
+ * (objectui#4220, the detail page's delegation) — and so does its property:
58
+ * each widget's own whitelist carries these onto the real focusable control, so
59
+ * nothing here needs to know which element that is. A host that passes nothing
60
+ * is unaffected.
61
+ *
62
+ * The host's NON-DOM set is forwarded WHOLE too (`f08bcd9af`), through the
63
+ * sibling executor `toHostProps`. The DOM fix left the other half of the
64
+ * contract undelivered: `error`, `onUploadingChange` and the "Host plumbing"
65
+ * block (`dataSource`, `dependentValues`, `dependsOn`, `dependsOnLabels`,
66
+ * `emptyHint`, `onSelectRecord`, `onCreateNew`) still type-checked, read as
67
+ * supported, and never reached the widget. `error` was the live one:
68
+ * `InlineFieldInput` has passed `error={error}` since PR #7109 and this factory
69
+ * dropped it, so a control that had failed validation never reported
70
+ * `aria-invalid`. Forwarding is not a widening — every one of those keys is
71
+ * already declared on `FieldWidgetComponentProps`, the same argument #7009
72
+ * landed on in this file.
73
+ *
74
+ * ⛔ They do NOT go through `toDomProps`. None of them is DOM-legal, and that
75
+ * whitelist is closed for exactly this reason — a `dataSource` adapter routed
76
+ * there becomes `dataSource="[object Object]"` on an `<input>`, the leak the
77
+ * helper exists to prevent. `toHostProps`' direction-3 assertion makes the two
78
+ * sets provably disjoint, so the order of the two spreads below is not a
79
+ * question anyone has to answer again.
80
+ *
81
+ * ## `dataSource` precedence: the explicit prop WINS
82
+ *
83
+ * Delivering `dataSource` can change behaviour where before it could not
84
+ * arrive, because the relational widgets fall back to `SchemaRendererContext`
85
+ * (which the grid already provides). The precedence is therefore STATED rather
86
+ * than left to emerge: **a host's explicit `dataSource` prop wins over the
87
+ * context**. That is not a new decision — `LookupField` already resolves
88
+ * `props.dataSource ?? lookupField?.dataSource ?? fieldMeta?.dataSource ??
89
+ * contextDataSource` and documents that order on the line that does it. This
90
+ * factory is a CONDUIT and resolves nothing: adding a resolution here would
91
+ * give `dataSource` a second author, the `field || schema` shape objectui#3233
92
+ * removed. A host that passes no `dataSource` keeps reading the context exactly
93
+ * as before, so no in-repo host changes behaviour. The full per-key precedence
94
+ * table lives on `toHostProps`, next to the list it governs.
47
95
  */
48
- export declare function FieldEditWidget({ field, value, onChange, readonly, autoFocus, }: FieldWidgetComponentProps<any>): React.ReactElement | null;
96
+ export declare function FieldEditWidget(props: FieldWidgetComponentProps<any>): React.ReactElement | null;
@@ -0,0 +1,54 @@
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
+ * `coerceToSafeValue` — the one answer this package has to "what text does a
10
+ * string-typed cell draw for a value that is not a string?".
11
+ *
12
+ * ## Why this is its own module (objectui#8580)
13
+ *
14
+ * It used to be defined in `./index.tsx`, the barrel, and every text-like cell
15
+ * renderer there reaches it. The rich-content renderers in
16
+ * `./widgets/richTextDisplay.tsx` need the SAME answer — `@objectstack/spec`
17
+ * places `markdown` / `html` / `richtext` in the same value class as `text` /
18
+ * `textarea` / `code` (`STRING_VALUE_TYPES`, "Value is a plain string"), and
19
+ * before objectui#8580 they had a private one: `String(value)`, which drew a
20
+ * childless container for `[]` and the literal `[object Object]` for `{}`
21
+ * where `text` draws the No-value affordance and `[Object]`. That module is
22
+ * imported BY the barrel (`4bb940b6e` extracted it so `RichTextField` could
23
+ * reach the display pipelines without importing the barrel back), so it
24
+ * cannot import the barrel to borrow this helper without re-creating exactly
25
+ * the cycle that extraction removed. The helper therefore lives below both
26
+ * consumers. The barrel re-exports it unchanged; it is part of this package's
27
+ * published surface and nothing about its contract moved.
28
+ */
29
+ /**
30
+ * Coerce a value to a safe primitive for rendering.
31
+ * Handles MongoDB wrapper types ($numberDecimal, $oid, $date), expanded
32
+ * reference objects, and arrays so that no raw object is ever passed as
33
+ * a React child — preventing React error #310.
34
+ *
35
+ * A STRING is returned verbatim, whatever its shape. This helper is reached by
36
+ * every text-like cell (`text`, `textarea`, `code`, `time`, `auto_number`,
37
+ * `qrcode` all register to `TextCellRenderer`), so it may not classify a value
38
+ * by looking at its characters. It used to: any string starting `{`/`[` and
39
+ * ending `}`/`]` was `JSON.parse`d and the result run through the
40
+ * reference-label extraction below, which answers `[Object]` for an object
41
+ * carrying no name/label/externalId/id. That turned the showcase Field Zoo's
42
+ * `code` value `{"ok": true}` into the literal `[Object]`, and `[1, 2, 3]` in a
43
+ * text field into `1, 2, 3` (objectui#7246).
44
+ *
45
+ * Shape is not a type. The reference case the parse was written for
46
+ * (objectui#1426 — an unresolved external-id ref arriving as
47
+ * '{"externalId":"Website Relaunch"}') belongs to reference-TYPED columns and
48
+ * is handled there: `LookupCellRenderer` carries its own JSON-string branch,
49
+ * which resolves the label through the referenced object's schema and links to
50
+ * the record — neither of which this type-blind helper could ever do. Scoped,
51
+ * not dropped; both halves are pinned in
52
+ * `__tests__/textCellJsonText-7246.test.tsx`.
53
+ */
54
+ export declare function coerceToSafeValue(value: unknown): string | number | boolean | null | undefined;
@@ -44,7 +44,7 @@ export declare function currencyFractionDigits(currency: string): number;
44
44
  * Falls back to `currency` itself — the code as given — in the three cases
45
45
  * where ICU cannot answer, because a bare code is a legitimate adornment (it is
46
46
  * what ICU itself returns for KWD/BHD/CHF/ISK, and what
47
- * `formatAmount`'s own bad-code branch renders beside it):
47
+ * `formatCurrency`'s own bad-code branch renders beside it):
48
48
  *
49
49
  * 1. an invalid currency code — `Intl` throws `RangeError: Invalid currency
50
50
  * code`;
@@ -0,0 +1,41 @@
1
+ import { BaseFieldMetadata } from '@object-ui/types';
2
+ /**
3
+ * The help line a field draws under its control: the ONE read of a field
4
+ * definition's help text (objectui#12213). The order lives here and nowhere
5
+ * else, and both of its readers call this function:
6
+ *
7
+ * - `@object-ui/plugin-form`, for the record form's help line. `ObjectForm`'s
8
+ * field builder and `fromObjectSchema` both call it, so the flat, sectioned
9
+ * and tabbed forms cannot drift apart.
10
+ * - `resolveActionParam` in this package, for a field-backed action param
11
+ * that declares no `helpText` of its own.
12
+ *
13
+ * First non-empty value wins:
14
+ *
15
+ * 1. `inlineHelpText`, `@objectstack/spec`'s `FieldSchema.inlineHelpText`
16
+ * ("Help text displayed below the field in forms"). Studio's object field
17
+ * inspector writes it, and flow screens draw it in the same place.
18
+ * 2. `description`, the fallback the form always had. An objectstack server
19
+ * on `@objectstack/spec` 17.7.0 or later serves a field's translated help
20
+ * text here.
21
+ *
22
+ * ## `||`, not `??`
23
+ *
24
+ * An empty string reads as absent, so `inlineHelpText: ''` falls back to the
25
+ * description instead of hiding it. The spec declares no setting that hides
26
+ * the help line, and Studio's inspector stores a cleared box as no key at all
27
+ * (`inlineHelpText: v || undefined`). With `??`, an empty string would become
28
+ * a second, undeclared way to suppress the description.
29
+ *
30
+ * ## `help` is not read
31
+ *
32
+ * `FieldSchema` refuses `help` by name and suggests `inlineHelpText`, so no
33
+ * document that passes the write door carries it. Its one producer was an
34
+ * objectstack server older than `@objectstack/spec` 17.7.0, whose translator
35
+ * put a field's translated description on the served field's `help` key;
36
+ * 17.7.0 serves that text on `description`. The read of that served leg is
37
+ * retired (the ruling on objectui#12213): translated field help needs an
38
+ * objectstack server on spec 17.7.0 or later. A definition that still carries
39
+ * `help` draws its `inlineHelpText`, else its `description`, else nothing.
40
+ */
41
+ export declare function fieldHelpText(field: Pick<BaseFieldMetadata, 'inlineHelpText' | 'description'>): string | undefined;