@object-ui/fields 17.7.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.
package/README.md CHANGED
@@ -42,18 +42,31 @@ View components use `getCellRenderer` to resolve the correct component for a fie
42
42
  ```tsx
43
43
  import { getCellRenderer, resolveCellRendererType, type CellRendererProps } from '@object-ui/fields';
44
44
 
45
- const MyGridCell = ({ field, value }: CellRendererProps) => {
45
+ const MyGridCell = ({ field, value, row }: CellRendererProps & { row: Record<string, unknown> }) => {
46
46
  // Resolve the renderer KEY first, then the renderer. A field's declared
47
47
  // `type` is not always the renderer's key: a textual field carrying a
48
48
  // format hint (`Field.text({ format: 'phone' })`) resolves to the richer
49
- // renderer. Passing `field.type` raw skips that mapping and draws such a
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
50
52
  // column as bare text, silently. Resolving first is never worse: with no
51
- // format hint the resolver returns the declared `type` unchanged.
53
+ // hint the resolver returns the declared `type` unchanged.
52
54
  const Renderer = getCellRenderer(resolveCellRendererType(field));
53
- return <Renderer field={field} value={value} />;
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} />;
54
59
  };
55
60
  ```
56
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
+
57
70
  ## Standard Field Types
58
71
 
59
72
  Supported types out of the box:
@@ -110,6 +123,80 @@ The app-shell `ActionParamDialog` uses these to render declared action params
110
123
  through the exact same widgets as the object form — with a drift test pinning
111
124
  param support ⊇ form support.
112
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
+
113
200
  ### Enumerating the CELL renderer registry
114
201
 
115
202
  `listCellRendererTypes()` is the read-side twin of `FORM_FIELD_TYPES`: every
@@ -168,6 +255,46 @@ import { MaskedCellRenderer } from '@object-ui/fields';
168
255
  const withheldCell = (value: unknown) => <MaskedCellRenderer value={value} />;
169
256
  ```
170
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
+
171
298
  ### File uploads in line-item grids
172
299
 
173
300
  `GridField` (the master-detail line-items grid) supports `type: 'file'` columns:
@@ -179,6 +306,21 @@ or photo per row without opening the row form (objectui#2360). Columns accept
179
306
  exported as `FileCell`). Auto-derived subform columns map `file`/`image`/
180
307
  `avatar` fields to file columns instead of dropping them.
181
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
+
182
324
  ### Multi-value selects
183
325
 
184
326
  A `select` field declared `multiple: true` selects zero-or-more values (spec
@@ -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;