Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/12613-widget-ref-read-cells.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@object-ui/fields': minor
'@object-ui/plugin-grid': minor
'@object-ui/app-shell': minor
---

A list names what an `object-ref` or `recipient-picker` field references, instead of printing its api name (objectui#12613). The Sharing Rules list read `crm_account` and `sales_manager` where the edit form reads "Account" and "Sales Manager".

- **The declared `widget` picks the read cell (`@object-ui/fields`).** `resolveCellRendererType` now promotes a textual field declaring `widget: 'object-ref'` or `widget: 'recipient-picker'` to a cell key of that name, the same second step a `format` hint takes. Both keys are registered with the stored text as their default face, as `RECORD_POINTER_CARD_TYPE` is. A widget with no read cell of its own (`filter-condition`, `position-ref`, …) still draws as its `type`, and nothing is keyed on an object or field name.
- **`CellRendererProps` gains one optional member, `dependentValues`**: the row's values, the cell-side counterpart of the edit widget's `dependentValues`. `ObjectGrid` (`@object-ui/plugin-grid`) passes the row, and the widget hint, on every column path. A call site that passes nothing draws the stored text exactly as before.
- **The edit widgets' kind table and spellings are published** so a host face names a value as the widgets do: `RECIPIENT_RECORD_KINDS`, `RECIPIENT_REGISTRY_KINDS`, `FIELD_RECIPIENT_TYPE`, `recipientCandidateLabel` and `objectRefOptionLabel`, plus the shared `UnresolvedReferenceMark` drawing. `ObjectRefField` and `RecipientPickerField` read through them and list and store exactly what they did.
- **The console's faces (`@object-ui/app-shell`)** register under the two keys and load lazily, on the first cell that draws one, so a page without such a column pays nothing on load. `object-ref` draws the object's icon and label from the console's metadata store (the same object list the picker offers), the label translated through the app bundle like every other object label, with the picker's `Label (name)` spelling as its tooltip. `recipient-picker` draws the label of the user, team, business unit, position or user column the stored value names: record recipients by their exact id, one `$in` query per kind for the whole page, with the fields the viewer may not read left out of the label; positions from the metadata store; the `field` kind from the shared object's schema.
- **A value nothing names** (a deleted record, a renamed object, a read the viewer is refused) draws as the raw stored value, muted, with the shared unresolved-reference mark. It is never blank.

Stored values do not change.
2 changes: 2 additions & 0 deletions packages/app-shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"./dist/views/record-approvals-renderer.js",
"./dist/views/record-attachments-renderer.js",
"./dist/views/record-preview/record-pointer-card-renderer.js",
"./dist/views/widget-ref-cells/widget-ref-cell-renderers.js",
"./dist/views/studio-design/studio-canvas-preview.js",
"./src/index.ts",
"./src/views/app-launcher-renderer.tsx",
Expand All @@ -40,6 +41,7 @@
"./src/views/record-approvals-renderer.tsx",
"./src/views/record-attachments-renderer.tsx",
"./src/views/record-preview/record-pointer-card-renderer.tsx",
"./src/views/widget-ref-cells/widget-ref-cell-renderers.tsx",
"./src/views/studio-design/studio-canvas-preview.tsx",
"./src/styles.css"
],
Expand Down
4 changes: 4 additions & 0 deletions packages/app-shell/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ import './views/record-approval-decision-renderer.js';
// pair, registered under `@object-ui/fields`' `RECORD_POINTER_CARD_TYPE`
// (objectui#12045).
import './views/record-preview/record-pointer-card-renderer.js';
// The faces of `@object-ui/fields`' widget-hinted reference keys,
// `object-ref` and `recipient-picker`, loaded lazily on the first cell that
// draws one (objectui#12613).
import './views/widget-ref-cells/widget-ref-cell-renderers.js';
// `global:search` / `global:notifications` — the two spec `PageComponentType`
// members the 2026-08-26 ruling on objectstack#12183 kept declared because both
// data sources shipped (`f99932a42`). Registered here, not in
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* The console's faces for the two widget-hinted reference keys of
* `@object-ui/fields` (objectui#12613). A textual field declaring
* `widget: 'object-ref'` or `widget: 'recipient-picker'` resolves to that key
* (`resolveCellRendererType`), and these name what the stored value references
* instead of printing the api name — the Sharing Rules list read `crm_account`
* and `sales_manager` where the edit form reads "Account" and "Sales Manager".
*
* Loaded lazily by `./widget-ref-cell-renderers.tsx`; see `./refLabelDirectory.ts`
* for where the labels come from and why the faces live in this package.
*/

import * as React from 'react';
import {
TextCellRenderer,
UnresolvedReferenceMark,
coerceToSafeValue,
objectRefOptionLabel,
type CellRendererProps,
} from '@object-ui/fields';
import { EmptyValue, LazyIcon } from '@object-ui/components';
import { isEmptyValue } from '@object-ui/core';
import { useObjectLabel } from '@object-ui/i18n';
import { useObjectRefLabel, useRecipientLabel } from './refLabelDirectory.js';

/**
* The stored value the directory could NOT name: raw and muted, never a blank,
* in the shipped unresolved-reference presentation, so this state looks the
* same here as on a lookup cell. Its tooltip is the stored value itself: the
* lookup family's sentence names a RECORD, which an object or a position is not.
*/
function Unresolved({ text }: { text: string }) {
return <UnresolvedReferenceMark raw={text} hint={text} />;
}

/**
* `object-ref`: the object's icon and label, the label translated through the
* app bundle like every other object label, with the picker's own
* `Label (name)` spelling as its tooltip so the api name stays one hover away.
* The icon is drawn the way the console's navigation draws an object's —
* `LazyIcon`, which falls back to the database glyph for an object that names
* no icon (or one Lucide does not have).
*/
export function ObjectRefCellFace({ value, field }: CellRendererProps) {
const ref = useObjectRefLabel(value);
const { objectLabel } = useObjectLabel();
const safe = coerceToSafeValue(value);
if (isEmptyValue(safe)) return <EmptyValue />;
if (ref.state === 'resolved' && typeof value === 'string') {
const label = objectLabel({ name: value, label: ref.label || value });
return (
<span className="inline-flex max-w-full items-center gap-1.5" title={objectRefOptionLabel(value, label)}>
<LazyIcon name={ref.icon} className="size-3.5 shrink-0 text-muted-foreground" aria-hidden="true" data-slot="object-ref-icon" />
<span className="truncate">{label}</span>
</span>
);
}
if (ref.state === 'unresolved') return <Unresolved text={String(safe)} />;
return <TextCellRenderer value={value} field={field} />;
}

/**
* `recipient-picker`: the label of the user, team, business unit, position or
* user column the stored value names — the label the picker shows for it.
* Which of those it names is the row's `recipient_type`, so the face reads
* `dependentValues`; without them it draws the stored text as the text cell
* does.
*/
export function RecipientCellFace({ value, field, dependentValues }: CellRendererProps) {
const ref = useRecipientLabel(value, dependentValues);
const safe = coerceToSafeValue(value);
if (isEmptyValue(safe)) return <EmptyValue />;
if (ref.state === 'resolved') {
return (
<span className="block max-w-full truncate" title={ref.label}>
{ref.label}
</span>
);
}
if (ref.state === 'unresolved') return <Unresolved text={String(safe)} />;
return <TextCellRenderer value={value} field={field} />;
}
Loading
Loading