@object-ui/fields 17.6.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 (50) hide show
  1. package/CHANGELOG.md +5436 -0
  2. package/README.md +108 -8
  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/index.cjs +5 -5
  7. package/dist/index.css +26 -2
  8. package/dist/index.d.ts +229 -57
  9. package/dist/index.js +3811 -3009
  10. package/dist/widgets/AvatarField.d.ts +19 -3
  11. package/dist/widgets/BooleanField.d.ts +1 -1
  12. package/dist/widgets/DateField.d.ts +1 -1
  13. package/dist/widgets/DateTimeField.d.ts +1 -1
  14. package/dist/widgets/FilterConditionField.d.ts +21 -7
  15. package/dist/widgets/GeolocationField.d.ts +9 -3
  16. package/dist/widgets/GridField.d.ts +63 -99
  17. package/dist/widgets/LocationField.d.ts +30 -3
  18. package/dist/widgets/LookupField.d.ts +8 -0
  19. package/dist/widgets/NumberField.d.ts +1 -1
  20. package/dist/widgets/ObjectField.d.ts +9 -2
  21. package/dist/widgets/OptionsEmptyState.d.ts +29 -5
  22. package/dist/widgets/PasswordField.d.ts +14 -0
  23. package/dist/widgets/PeoplePicker.d.ts +17 -2
  24. package/dist/widgets/PercentField.d.ts +2 -1
  25. package/dist/widgets/PersonRow.d.ts +13 -2
  26. package/dist/widgets/RecipientPickerField.d.ts +43 -0
  27. package/dist/widgets/RecordPickerDialog.d.ts +31 -12
  28. package/dist/widgets/RichTextField.d.ts +1 -1
  29. package/dist/widgets/SelectionTray.d.ts +13 -2
  30. package/dist/widgets/TextField.d.ts +1 -1
  31. package/dist/widgets/TimeField.d.ts +1 -1
  32. package/dist/widgets/file-affordance.d.ts +33 -0
  33. package/dist/widgets/file-size-guard.d.ts +5 -23
  34. package/dist/widgets/file-value.d.ts +38 -9
  35. package/dist/widgets/lookupColumnDisplay.d.ts +24 -7
  36. package/dist/widgets/nativeDateValue.d.ts +9 -27
  37. package/dist/widgets/number-format.d.ts +137 -0
  38. package/dist/widgets/numberBadInput.d.ts +135 -0
  39. package/dist/widgets/personDisplay.d.ts +22 -4
  40. package/dist/widgets/richTextDisplay.d.ts +36 -0
  41. package/dist/widgets/toDomProps.d.ts +10 -1
  42. package/dist/widgets/toHostGroupProps.d.ts +53 -0
  43. package/dist/widgets/toHostProps.d.ts +105 -0
  44. package/dist/widgets/types.d.ts +11 -3
  45. package/dist/widgets/upload-error-message.d.ts +6 -0
  46. package/dist/widgets/uploadingScope.d.ts +142 -0
  47. package/dist/widgets/useCascadingOptions.d.ts +3 -2
  48. package/dist/widgets/useRecordQuery.d.ts +4 -2
  49. package/dist/widgets/useUploadingSignal.d.ts +8 -0
  50. 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,10 +40,16 @@ 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);
43
+ import { getCellRenderer, resolveCellRendererType, type CellRendererProps } from '@object-ui/fields';
44
+
45
+ const MyGridCell = ({ field, value }: CellRendererProps) => {
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. Passing `field.type` raw skips that mapping and draws such a
50
+ // column as bare text, silently. Resolving first is never worse: with no
51
+ // format hint the resolver returns the declared `type` unchanged.
52
+ const Renderer = getCellRenderer(resolveCellRendererType(field));
42
53
  return <Renderer field={field} value={value} />;
43
54
  };
44
55
  ```
@@ -55,6 +66,33 @@ Supported types out of the box:
55
66
  - **Media**: `file`, `image`
56
67
  - **System**: `formula`, `summary`, `auto_number`
57
68
 
69
+ ### `type="number"` widgets: what is announced and what is not
70
+
71
+ `NumberField`, `CurrencyField`, `PercentField` and `GeolocationField` all render
72
+ a native `type="number"` input, so the **browser** decides what the box accepts.
73
+ They share one reading of that, in `widgets/numberBadInput.tsx`:
74
+
75
+ - **Announced.** When the browser reports `validity.badInput` — the box is
76
+ holding text it cannot convert, e.g. a typed `1e`, which Chromium keeps
77
+ DISPLAYING while `.value` reads `''` — the control is marked `aria-invalid`
78
+ and draws a `Not saved: …` message, reusing objectui#6716's refusal shape.
79
+ Both a change arm and a blur arm are wired, because pasting into an empty box
80
+ never moves `.value` and so fires no React change event at all. The sentence
81
+ resolves through the `fields.number.badInput` locale key (objectui#8148), so
82
+ it follows the reader's language; the example numeral each box quotes is the
83
+ widget's own and is interpolated rather than translated.
84
+ - ⚠️ **Not announced, and not announceable.** Entries the browser silently
85
+ **truncates**: `1.2.3` stores `1.23`, `0x10` stores `10`. The characters are
86
+ discarded as they arrive, before any handler here runs, so no widget-side
87
+ guard can refuse them. Recovering them would mean abandoning `type="number"`
88
+ and with it the mobile numeric keyboard and the `min`/`max`/`step` spinner
89
+ (objectui#2572).
90
+
91
+ ⛔ Silence therefore means "the browser read *something*", never "the value is
92
+ correct". User-facing wording lives in
93
+ [the fields guide](../../content/docs/guide/fields.md). The measured browser vs
94
+ happy-dom matrix is in `src/__tests__/numberInputBrowserReadings.ts`.
95
+
58
96
  ### Rendering form field widgets outside the form
59
97
 
60
98
  The full widget surface is exported for consumers that render field widgets
@@ -72,6 +110,64 @@ The app-shell `ActionParamDialog` uses these to render declared action params
72
110
  through the exact same widgets as the object form — with a drift test pinning
73
111
  param support ⊇ form support.
74
112
 
113
+ ### Enumerating the CELL renderer registry
114
+
115
+ `listCellRendererTypes()` is the read-side twin of `FORM_FIELD_TYPES`: every
116
+ field type `getCellRenderer` resolves to a renderer **of its own**, as opposed
117
+ to the `TextCellRenderer` fallback every other spelling lands on.
118
+
119
+ ⚠️ It is a **function**, not a frozen constant, and the difference is
120
+ load-bearing. `registerFieldRenderer` is published, so the cell registry can
121
+ grow after this module is evaluated; a constant would be a snapshot taken at
122
+ import time. Consumers that reason about "all registered cell types" — the two
123
+ censuses in `@object-ui/fields` and `@object-ui/plugin-detail` that measure what
124
+ every type draws — reconcile their tables against this reading so a newly
125
+ registered type fails them by name instead of slipping past a frozen
126
+ population (objectui#8734).
127
+
128
+ ### Asking whether a cell is masked
129
+
130
+ `isMaskedFieldType(type)` answers whether a cell of that field type is drawn
131
+ as a mask (`••••••`) instead of its value. It is the read-side twin of
132
+ `isInlineExcludedFieldType()`, and a consumer that acts on a cell's value asks
133
+ it rather than keeping its own list. The detail page's copy affordance in
134
+ `@object-ui/plugin-detail` is one such consumer: it refuses to copy a masked
135
+ row (objectui#8686). `MASKED_FIELD_TYPES` is the declared set behind it,
136
+ `password` and `secret`. The standard cell of each member draws the mask, and
137
+ the predicate answers `true` for it. Like `listCellRendererTypes()`, the
138
+ answer is a live reading of the cell registry taken when you call it, and,
139
+ with nothing overridden at runtime, it agrees with what `getCellRenderer`
140
+ resolves for every type that function lists. To add a masked type, register
141
+ the package's own mask under it with
142
+ `registerFieldRenderer('api_token', getCellRenderer('password'))`, while
143
+ `password` still resolves to the shipped mask. The predicate then answers
144
+ `true` with no change to the declared set, and the detail page refuses to copy
145
+ that row. Overriding a declared type with one of this package's own renderers
146
+ makes the predicate answer `false`, since none of them is the mask, and the
147
+ cell draws what that renderer draws: after
148
+ `registerFieldRenderer('password', TextCellRenderer)` the cell shows the value
149
+ and the detail row copies again. Overriding a declared type with a component
150
+ of your own keeps the declared answer, `true`, because the package cannot
151
+ tell whether an opaque component hides the value, so it errs toward
152
+ withholding it. Your own component under an undeclared type answers `false`.
153
+ Spellings are matched raw, the way `getCellRenderer` looks them up:
154
+ `field:password` renders in the clear and is not masked.
155
+
156
+ The mask itself is exported as `MaskedCellRenderer` (objectui#10657). A table
157
+ that cannot type a column yet, because its object's field types are still
158
+ loading or their read failed, draws that column WITHHELD with it rather than as
159
+ text: `object-grid`, the related list and `object-data-table` do. It is the
160
+ shipped mask, not whatever is registered for `password` at the time, and its
161
+ `field` prop is optional because it reads only whether a value is present.
162
+ `registerFieldRenderer('api_token', MaskedCellRenderer)` masks a type the same
163
+ way the `getCellRenderer('password')` form above does.
164
+
165
+ ```tsx
166
+ import { MaskedCellRenderer } from '@object-ui/fields';
167
+
168
+ const withheldCell = (value: unknown) => <MaskedCellRenderer value={value} />;
169
+ ```
170
+
75
171
  ### File uploads in line-item grids
76
172
 
77
173
  `GridField` (the master-detail line-items grid) supports `type: 'file'` columns:
@@ -111,6 +207,10 @@ never drift). While a `dependsOn` parent is empty the control is gated; a parent
111
207
  change re-filters the list and clears a now-invalid value (scalar `select` /
112
208
  `radio` drop the value; multi-value `multiselect` / `checkboxes` prune just the
113
209
  offered-out entries).
210
+ An option whose `label` is blank (the empty string is a legal label; an absent one is
211
+ not) displays its `value` instead of an empty row, on the editable and the read-only
212
+ path alike — all eight read sites across the four widgets share the one
213
+ `optionDisplayLabel` helper in `@object-ui/core` (objectui#9230).
114
214
  Client-side hiding is UX only — gate authorization-sensitive values on the
115
215
  server too. See
116
216
  [`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`;