Skip to content

A dataset measure over a date field renders its raw ISO string — formatMeasure short-circuits before format is read #7178

Description

@os-warren

Found by opening a dashboard in a real ObjectStack application (objectstack-ai/duly) against published @objectstack/* 17.2.0. Source-confirmed afterwards in this repo.

The claim

A min / max measure over a date or datetime field cannot be rendered as a date on any dataset-bound widget, and the format escape hatch the spec offers for exactly this is unreachable for those values.

The KPI tile renders 2026-07-04T07:00:00.000Z where a date belongs — a 24-character ISO string in text-2xl font-semibold, which wraps to two lines inside the card.

Where it comes from

packages/plugin-dashboard/src/DatasetWidget.tsx:768 renders the dataset-bound metric value:

<span className={cn('text-2xl font-semibold tabular-nums', accentClass)}>
  {formatMeasure(value, f?.format, f?.currency, f?.percentScale, displayLocale)}
</span>

and formatMeasure in packages/core/src/utils/dataset-format.ts opens with:

if (v == null) return '—';
if (typeof v !== 'number') return String(v);

That second line is before format is touched. So for any non-numeric measure value the function is String(v) and nothing else — the format argument the call site carefully threads through is dead on this path. tabular-nums on the span is the same assumption stated in CSS.

Everything downstream of that guard is a numeral-pattern formatter (decimals from format.split('.')[1], %, $, currency), which no date pattern can enter.

Why the escape hatch does not close it

DatasetMeasureSchema in @objectstack/spec carries format: z.ZodOptional<z.ZodString> — an open string, so authoring format: 'YYYY-MM-DD' (or anything else) passes validate and build cleanly. It then has no effect, because the value never reaches the branch that reads it. An author gets a green toolchain and an unchanged raw timestamp, with nothing anywhere saying why.

Reproduce

Declare a measure aggregating a date field and bind it to a type: 'metric' widget:

// dataset
{ name: 'oldest_touch', label: 'Oldest touch', aggregate: 'min', field: 'last_update_at' }

// dashboard widget
{ id: 'oldest', title: 'Oldest untouched task', type: 'metric',
  dataset: '<ds>', values: ['oldest_touch'] }

Open the dashboard. The tile shows the raw ISO string. Adding format: 'YYYY-MM-DD' to the measure changes nothing.

Scope beyond the metric tile

The same formatMeasure call is on three other surfaces, all with the same guard in front of them, so a date measure is raw in each: DatasetWidget.tsx:876 (chart value formatting), DatasetWidget.tsx:1083 (dataset table cells), and views/metadata-admin/previews/DatasetPreview.tsx:235. PivotTable.tsx:46 takes value: number outright.

Suggested direction

The narrow fix is to let formatMeasure recognise a date-shaped string (or a measure whose format names a date pattern) before the numeric short-circuit, and route it through the same date display path list cells already use — the argument for reusing that path rather than adding a second one is the same one the file's own header makes about percent, where two conventions drifted between a list cell and a dashboard measure (objectui#4576).

Whatever the resolution, the silence is the worse half: a format that the schema accepts and the renderer can never read is declared ≠ enforced. If dates are out of scope for measure formatting, the spec's format should say so and a date-typed aggregate should be refused at author time rather than rendered raw.

Application impact

Low severity — the number is correct and legible, just ugly. Reported because the silence is the reusable finding, not the wrapped timestamp.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatchedpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions