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
19 changes: 19 additions & 0 deletions .changeset/10657-fields-masked-cell-renderer-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@object-ui/fields': minor
---

`MaskedCellRenderer`, the mask a `password` / `secret` cell draws, is now exported, so a table that cannot type a column yet draws it WITHHELD instead of as text (objectui#10657, which folded objectui#10706).

**New export `MaskedCellRenderer`.** It is the component the standard `password` and
`secret` cells already resolve to: `••••••` for a value that is set, the empty-value mark
for one that is not. Its `field` prop is optional, because it reads only whether a value
is present. It is the shipped mask, not whatever is registered for `password` at the
time, so a host override of a masked type does not change what a withheld cell draws.

`object-grid`, the related list and `object-data-table` use it for a column whose type is
not known yet: while the object's field types are loading, or after their read failed,
they draw such a column as this mask rather than as its value.

`registerFieldRenderer('api_token', MaskedCellRenderer)` now masks a type the same way
the `getCellRenderer('password')` form does, and `isMaskedFieldType('api_token')` answers
`true` for it. Nothing that already resolves changes.
29 changes: 29 additions & 0 deletions .changeset/10657-plugin-dashboard-withheld-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@object-ui/plugin-dashboard': patch
---

`object-data-table` no longer draws an untyped column as text while the object definition is loading or after its read failed: it draws the mask instead (objectui#10657, the objectui#10706 class at this producer).

A cell here draws from the column's authored `type`, or the object's. Bound and inline
rows are drawn before the object definition arrives, and a failed read never delivers
one, so in that window a column that authors no `type` drew its value as text, a
`password` / `secret` field included. objectui#10657's first change already set
`TableColumn.masked` on every column in that window, but the flag withholds; it does not
draw.

Now, in that window, a column with no `type` of its own is drawn as the mask
(`MaskedCellRenderer` from `@object-ui/fields`), declared and derived columns alike, and
stays so when the read failed: fail closed, never back to text. A column that authors its
`type` draws from it, and a column with a `cell` of its own draws what that `cell`
returns. Once the definition lands, each column draws from its declared type as before.

The record drawer a row opens (record drill-down) drew the clicked record's fields from
the same unknown types, so after a failed read it printed a `password` / `secret` field
as text too. In that window it now draws every value as the mask.

The pending objectui#10657 `object-data-table` changeset says a `password` / `secret`
field is still drawn as text in that window; that no longer holds once this change ships
with it.

This is defence in depth. The real guard is still the server: ObjectStack's read mask
replaces `secret` and `password` values with its mask before they reach the browser.
26 changes: 26 additions & 0 deletions .changeset/10657-plugin-detail-withheld-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
'@object-ui/plugin-detail': patch
---

The related list no longer draws a cell as text while its object definition is loading or after its read failed: it draws the mask instead (objectui#10657, the objectui#10706 class at this producer).

The list draws a cell from the object's field type, never from a column's authored
`type`. While the definition was in flight, and for good after its read failed, no column
had a cell, so the table drew every value as text, a `password` / `secret` field
included. objectui#10657's first change already set `TableColumn.masked` on every column
in that window (no copy, tooltip or header sort), but the flag withholds; it does not
draw.

Now, in that window, every column without a `cell` of its own is drawn as the mask
(`MaskedCellRenderer` from `@object-ui/fields`), and stays so when the read failed: fail
closed, never back to text. A column whose `cell` the author supplied draws what that
`cell` returns. Once the definition lands, each column draws from its declared type as
before.

The cost: while the definition is loading, and for good after a failed read, the list
shows masks where it used to show values. The pending objectui#10657 related-list
changeset says a `password` / `secret` field is still drawn as text in that window; that
no longer holds once this change ships with it.

This is defence in depth. The real guard is still the server: ObjectStack's read mask
replaces `secret` and `password` values with its mask before they reach the browser.
39 changes: 39 additions & 0 deletions .changeset/10657-plugin-grid-withheld-first-paint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
'@object-ui/plugin-grid': minor
---

`object-grid` no longer draws an untyped column as text before its object schema has loaded: the column is WITHHELD until it has, and stays withheld when the read fails; a host can hand the grid the field types it holds (`objectFields`) so there is no such window (objectui#10657, which folded objectui#10706).

On the host-fetched path (rows handed down as `data`, as `ListView` does) the rows paint
before the grid's own `getObjectSchema` read lands. An untyped view column takes its type
from that schema, so until it arrived a column over a `password` / `secret` field drew
the raw value as text and handed it out (copy, tooltip, the grid's client export, the
mobile card), and grouping by it printed the value as the group label. A failed read was
swallowed as non-fatal, leaving the column in the clear for the life of the grid.

- **Withheld.** While an object-bound grid (an object name, and a data source that can
answer `getObjectSchema`) has no field types in hand, a column with no `type` of its
own is drawn as the mask (`MaskedCellRenderer` from `@object-ui/fields`), carries
`TableColumn.masked`, is left out of the grid's client export, is drawn through its cell
on the mobile card, and cannot be a grouping key (the entry is ignored without a
warning until the types are known; a masked field is still warned about). No type is
inferred for it from its name or values meanwhile. It stays withheld when the read
fails: fail closed, never back to text. A column that authors its own `type` draws
from it, as it did. A grid with no `getObjectSchema` to wait for is unchanged.
- **The record panel** a row opens under overlay navigation (drawer, modal, split,
popover) with no declared fields in hand printed every value of the record, inferred
from its value and name. While the types are unknown it now draws each value as the
mask as well.
- **New prop `objectFields` on `ObjectGrid` (`ObjectGridComponentProps`).** The object's
field catalogue, handed down by a host that has already read the definition. The grid
answers every field-type question from it until its own read lands, so the columns
draw from their declared types at first paint and nothing is withheld. It is a runtime
prop only: `SchemaRenderer` strips an `objectFields` key authored in the schema
(objectui#8818), so no view author can hand the grid a catalogue.

The pending objectui#10583 changeset lists the host-fetched window under "Not covered",
and the pending objectui#10657 grid changeset says that window stays not covered. Neither
holds once this change ships with them.

This is defence in depth. The real guard is still the server: ObjectStack's read mask
replaces `secret` and `password` values with its mask before they reach the browser.
14 changes: 14 additions & 0 deletions .changeset/10657-plugin-list-grid-object-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@object-ui/plugin-list': patch
---

`ListView` hands its grid the object's field types together with the rows, so the grid draws a `password` / `secret` column masked from its first paint (objectui#10657, which folded objectui#10706).

`ListView` fetches the rows and hands them to `object-grid` as `data`, and those rows
painted before the grid's own read of the object definition settled. In that window an
untyped column over a `password` / `secret` field had no type: it drew the raw value, and
since `object-grid` now withholds such a column instead, it would draw the mask for every
untyped column until the read lands. `ListView` reads the object definition before it
fetches the rows, so it now passes the definition's fields to the grid as `objectFields`
with them, and the grid has no window at all. Only the grid view is handed it. When the
definition could not be read, nothing is passed and the grid withholds on its own.
15 changes: 15 additions & 0 deletions .changeset/10657-types-withheld-doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@object-ui/types': patch
---

`TableColumn.masked`'s documentation describes withheld columns: a column whose type is not known yet is drawn as the mask and carries the flag (objectui#10657, which folded objectui#10706).

The JSDoc said that the related list and `object-data-table` still draw a `password` /
`secret` field as text while their object definition is loading or after its read
failed, and that `object-grid` draws an untyped column's raw value on its host-fetched
path until its object schema arrives. Neither holds any more: each of the three producers
now draws such a column as the mask and sets the flag on it, and `object-grid` takes the
field types from a host that has them (`objectFields`). The JSDoc is rewritten to say so.
The pending objectui#10657 `@object-ui/types` changeset says these docs state what that
window does not cover; once this change ships with it, they state what it withholds
instead. No type or schema shape changes.
37 changes: 20 additions & 17 deletions content/docs/components/complex/data-table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -170,23 +170,26 @@ so a column that shows a `secret` field as `text` keeps the flag once the object
types are known.

The related list and `object-data-table` fail closed: while the object definition is
still loading, or after its read failed, they set the flag on every column. What they
do not cover: in that same window, their cells draw a field the object declares as
`password` / `secret` as text, because the mask comes from that declaration
(objectui#10657).

Once the object schema has loaded, `object-grid` also leaves every masked field of the
grid's object out of its own client export (CSV and JSON), draws those fields through
their `cell` on its mobile card, and refuses them as grouping keys (the entry is
ignored with a console warning). What `object-grid` does not cover: on the
host-fetched path (rows handed down as `data`, as `ListView` and `ObjectView` do), the
grid's guards and the cell's own mask depend on the object schema, which the grid
fetches after first paint, so until it arrives, and for good if that read fails, an
untyped view column over a `password` / `secret` field draws and hands out the raw
value (objectui#10657, which folded objectui#10706). The server-streamed export
(`exportDownload`) sends the masked columns as before and relies on the server's
masking, and the client JSON export writes an expanded lookup record whole, so a
credential field of the related object is not pruned.
still loading, or after its read failed, they set the flag on every column.

A column whose type is not known yet is **withheld** (objectui#10657, which folded
objectui#10706). While the object's field types are loading, or after their read
failed, `object-grid`, the related list and `object-data-table` draw such a column as
the mask, never as text, and set the flag on it; it stays withheld when the read
failed. In `object-grid` and `object-data-table` that is a column with no `type` of its
own. The related list draws its cells from the object's types only, so there it is
every column without a `cell` of its own. A host that already holds the object's fields
hands them to `object-grid` as its `objectFields` prop, as the list view does, and then
there is nothing to withhold.

`object-grid` also leaves every masked or withheld field of the grid's object out of its
own client export (CSV and JSON), draws those fields through their `cell` on its mobile
card, and refuses them as grouping keys (a masked entry is ignored with a console
warning; a withheld one groups again once the types are known). What `object-grid` does
not cover: the server-streamed export (`exportDownload`) sends the masked columns as
before and relies on the server's masking, and the client JSON export writes an
expanded lookup record whole, so a credential field of the related object is not
pruned.

## Examples

Expand Down
15 changes: 15 additions & 0 deletions packages/fields/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,21 @@ withholding it. Your own component under an undeclared type answers `false`.
Spellings are matched raw, the way `getCellRenderer` looks them up:
`field:password` renders in the clear and is not masked.

The mask itself is exported as `MaskedCellRenderer` (objectui#10657). A table
that cannot type a column yet, because its object's field types are still
loading or their read failed, draws that column WITHHELD with it rather than as
text: `object-grid`, the related list and `object-data-table` do. It is the
shipped mask, not whatever is registered for `password` at the time, and its
`field` prop is optional because it reads only whether a value is present.
`registerFieldRenderer('api_token', MaskedCellRenderer)` masks a type the same
way the `getCellRenderer('password')` form above does.

```tsx
import { MaskedCellRenderer } from '@object-ui/fields';

const withheldCell = (value: unknown) => <MaskedCellRenderer value={value} />;
```

### File uploads in line-item grids

`GridField` (the master-detail line-items grid) supports `type: 'file'` columns:
Expand Down
16 changes: 15 additions & 1 deletion packages/fields/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3362,8 +3362,22 @@ function RepeaterCellRenderer({ value }: CellRendererProps): React.ReactElement
* states: `EmptyValue` holds a hook, and an inline arrow in the table below
* is a new component type on every resolution, so it would tear that hook down
* per render.
*
* Exported (objectui#10657) for the cell a producer cannot type yet. While an
* object-bound table is still waiting for its object's field types, or after
* that read failed, a column with no type of its own could be a credential,
* so `ObjectGrid`, `RelatedList` and `ObjectDataTable` draw it WITHHELD: as
* this mask, never as text. It is this component and not the live registry
* entry for `password`, so a host override of a masked type cannot change what
* a withheld cell draws. Registering it for a type
* (`registerFieldRenderer('api_token', MaskedCellRenderer)`) masks that type,
* as {@link isMaskedFieldType} describes. `field` is optional: the mask reads
* only whether the value is present, so a withheld cell has no field metadata
* to invent.
*/
function MaskedCellRenderer({ value }: CellRendererProps): React.ReactElement {
export function MaskedCellRenderer({
value,
}: Omit<CellRendererProps, 'field'> & { field?: CellRendererProps['field'] }): React.ReactElement {
if (isEmptyValue(coerceToSafeValue(value))) return <EmptyValue />;
return <span>••••••</span>;
}
Expand Down
5 changes: 4 additions & 1 deletion packages/plugin-dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,10 @@ an author override does not win: `type: 'text'` over a `secret` field keeps the
flag, though the cell then draws the value as text. Rows handed in as `data`
or through `bind` can be drawn before the object schema arrives, and a failed
schema read never delivers one; in that window the widget flags every column,
and a masked field is still drawn as text.
and a column with no `type` of its own draws as the mask, never as text, until
the schema arrives, or for good when the read failed (objectui#10657). A column
that authors its `type` draws from it meanwhile. The record drawer a row opens
(record drill-down) draws every value as the mask in that window too.

```jsonc
{
Expand Down
27 changes: 26 additions & 1 deletion packages/plugin-dashboard/src/ObjectDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import type { ObjectDataTableSchema, TableColumn } from '@object-ui/types';
import { normalizeTableColumnType } from '@object-ui/types';
import { Skeleton, RefreshIndicator, cn } from '@object-ui/components';
import { isMaskedFieldType } from '@object-ui/fields';
import { isMaskedFieldType, MaskedCellRenderer } from '@object-ui/fields';
import { useSafeFieldLabel, useObjectTranslation, useLocalization, useDisplayLocale } from '@object-ui/i18n';
import { usePermissions } from '@object-ui/permissions';
import { resolveFilterPlaceholders, humanizeFieldKey } from './utils';
Expand Down Expand Up @@ -413,6 +413,15 @@ export type AuthoredColumnOverrides =
*/
const EMPTY_ROWS = Object.freeze([]) as unknown as any[];

/**
* The cell a WITHHELD column draws (objectui#10657): the mask, never the
* value. See `enrich` for when a column is withheld. Module-level, so the
* column keeps one `cell` identity across renders.
*/
function withheldCell(value: unknown): React.ReactNode {
return React.createElement(MaskedCellRenderer, { value });
}

/**
* Normalize columns to support both string[] shorthand and object[] formats.
*
Expand Down Expand Up @@ -948,6 +957,21 @@ export const ObjectDataTable: React.FC<ObjectDataTableProps> = ({ schema, dataSo

if (typeof col.cell === 'function') return { ...col, name: fieldMeta.name, type: columnType, align: inferredAlign, ...maskedStamp };

// ⭐ WITHHELD (objectui#10657, the objectui#10706 class at this
// producer). The flag above withholds; it does not DRAW. `cell` draws
// from `fieldMeta.type`, which is the authored `type` or the object's,
// so while the object's types are unknown a column that authors none has
// no type, and the cell would draw its value as text: a `password` /
// `secret` field in the clear, and for good when the read failed. Such a
// column draws the mask instead until the definition lands, and keeps
// drawing it when the read failed (fail closed, never falling back to
// text). A column that authors its own `type` draws from it, as it does
// once the definition is in hand: `password` masks, `text` draws the
// text it was told to.
if (objectTypesPending && !authored.type) {
return { ...col, name: fieldMeta.name, type: columnType, align: inferredAlign, cell: withheldCell, ...maskedStamp };
}

// Tenant-default currency backstops a currency column with no explicit code.
const cell = (value: any): React.ReactNode => renderFieldValue(value, fieldMeta, tenantCurrency, displayLocale);
return { ...col, name: fieldMeta.name, type: columnType, align: inferredAlign, cell, ...maskedStamp };
Expand Down Expand Up @@ -1129,6 +1153,7 @@ export const ObjectDataTable: React.FC<ObjectDataTableProps> = ({ schema, dataSo
title={recordTitle}
target={drillDown?.target === 'dialog' ? 'dialog' : 'drawer'}
onClose={() => setDrillRecord(null)}
objectTypesPending={objectTypesPending}
/>
)}
</div>
Expand Down
Loading
Loading