From a2ee8a267f166b5c3d4d7300566ba2631fd02fd0 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:53:48 +0000 Subject: [PATCH] chore(fields): narrow the datetime style cast, and make the #7443 changeset accurate for consumers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups recorded from the contract review of objectui#7621, neither of which gated its PASS (objectui#7747). The `DateTimeCellRenderer` style cast goes from `as any` to `(field as DateTimeFieldMetadata | undefined)`. Some cast is load-bearing — `FieldMetadata` is a 37-member union and `BaseFieldMetadata` carries no `format`, so the bare read is `TS2339` — but `as any` was wider than the job. `DateTimeFieldMetadata` is exported from `@object-ui/types` and this file already imports from there, so the narrow cast cost nothing. It is type-level only: the emitted expression is unchanged, and the file's `@typescript-eslint/no-explicit-any` warning count drops 46 to 45, the one removed being the line changed here. The `7443-datetime-compact-style.md` changeset claimed every existing cell renders byte-identically. That is true measured in this repository — no in-repo `datetime` field authors a `format` — but a changeset is a release-note input read by consumers who are not this repository, and objectui#7621 made `DateTimeCellRenderer` start reading `field.format`. The sentence now names its own precondition and the measured consequence: an authored `format` other than `'compact'` is neither rejected nor passed through, it silently selects the verbose `formatDateTime` default. The note also states the gap rather than papering over it — `format` has no declared value vocabulary on a datetime field in either `@object-ui/types` or `@objectstack/spec`. Only the prose changed; the frontmatter and its three package names are byte-identical. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 --- .changeset/7443-datetime-compact-style.md | 18 ++++++++++++++++-- .../7747-datetime-format-cast-narrowing.md | 19 +++++++++++++++++++ packages/fields/src/index.tsx | 8 ++++++-- 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 .changeset/7747-datetime-format-cast-narrowing.md diff --git a/.changeset/7443-datetime-compact-style.md b/.changeset/7443-datetime-compact-style.md index c7190f6f2..70900b838 100644 --- a/.changeset/7443-datetime-compact-style.md +++ b/.changeset/7443-datetime-compact-style.md @@ -13,8 +13,22 @@ The cell now reads `field.format` (it destructured `value` only, so a `datetime` field could not reach the style vocabulary a `date` field has) and renders through the shared function, and `data-table`'s `formatCellValue` calls `formatDateTime` instead of a third, independently authored option bag. -Every existing cell renders byte-identically; `'compact'` is today's face -named and rehoused, not a new one. +Every existing cell without an authored `format`, and every cell authoring +`'compact'`, renders byte-identically; `'compact'` is today's face named and +rehoused, not a new one. A `datetime` field that authors any OTHER non-empty +`format` does change: the cell previously ignored `field` altogether and always +painted the compact face, and now anything other than `'compact'` selects the +verbose `formatDateTime` default — measured as `Jul 4, 2024, 07:00 AM` in +`en-US` for the instant whose compact face is `7/4/2024 7:00 am`. An +unrecognised value is neither rejected nor passed through; it silently lands on +that verbose face. No `datetime` field in this repository authors a `format`, so +no cell here moves — a consumer that authored one is the case this sentence is +for. Note that `format` has no declared value vocabulary to check a value +against: `@object-ui/types` types it `format?: string`, and `@objectstack/spec` +carries one free-form `format?: string` on its shared field schema, described +"Format string (e.g. email, phone)" and accepting any string. `'compact'` is +therefore the only value with a defined `datetime` meaning, and every other +value means "the verbose face" by fallthrough rather than by design. Additive, no signature change: `formatDateTime(value, options?)` is unchanged and `formatDateTime(v, { locale })` keeps meaning what it meant. diff --git a/.changeset/7747-datetime-format-cast-narrowing.md b/.changeset/7747-datetime-format-cast-narrowing.md new file mode 100644 index 000000000..b9f09b92e --- /dev/null +++ b/.changeset/7747-datetime-format-cast-narrowing.md @@ -0,0 +1,19 @@ +--- +--- + +Narrow the `DateTimeCellRenderer` style cast from `as any` to the subtype that +actually declares the property (objectui#7747). + +`const style = (field as any)?.format || 'compact'` becomes +`(field as DateTimeFieldMetadata | undefined)?.format`. Some cast is +load-bearing — `FieldMetadata` is a 37-member union and `BaseFieldMetadata` +carries no `format`, so the bare read is `TS2339` — but `as any` was wider than +the job: `DateTimeFieldMetadata` is exported from `@object-ui/types`, which this +file already imports from, so the narrow cast was available at zero cost. The +difference that buys: `as any` would also have silenced a typo in the property +name, and the narrow cast will not. + +Type-level only; no package is released by this change. The emitted expression +is identical, so every cell renders exactly what it rendered before — +`'compact'` and an absent or empty `format` keep the compact face, and any +other value keeps selecting the verbose `formatDateTime` default. diff --git a/packages/fields/src/index.tsx b/packages/fields/src/index.tsx index bb0974998..c16ba226e 100644 --- a/packages/fields/src/index.tsx +++ b/packages/fields/src/index.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import type { FieldMetadata, SelectOptionMetadata } from '@object-ui/types'; +import type { DateTimeFieldMetadata, FieldMetadata, SelectOptionMetadata } from '@object-ui/types'; import { ComponentRegistry, percentDisplayValue, getRecordDisplayName, humanizeLabel, isMissingForRequired, formatDate, formatDateTime, formatDateTimeCompactParts, formatRelativeDate, type ComponentMeta, type DateDisplayOptions } from '@object-ui/core'; // The platform's own value-shape contract, asked rather than restated // (objectui#6744). See `locationStoredValueSchemaFor` below for why this is a @@ -882,7 +882,11 @@ export function DateTimeCellRenderer({ value, field }: CellRendererProps): React // (objectui#7443). `||`, not `??`, matches the `date` cell and keeps an // authored empty string on the compact face rather than dropping it into // the verbose default. - const style = (field as any)?.format || 'compact'; + // `FieldMetadata` is a 37-member union and `BaseFieldMetadata` carries no + // `format`, so the bare property read is `TS2339` — SOME cast is load-bearing. + // `DateTimeFieldMetadata` is the narrowest one that carries it (objectui#7747); + // `as any` would also silence a typo in the property name, this does not. + const style = (field as DateTimeFieldMetadata | undefined)?.format || 'compact'; // The compact face is painted in two halves — the time is muted and offset // — so this branch asks the shared module for the halves rather than the