From 4f4615806805a68cb3438e785703e3fcc70fcfb5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 18:42:39 +0000 Subject: [PATCH 1/6] test(plugin-grid): pin one absolute face per grid date column (red) Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude --- ...bjectGrid.dateColumnOneFace-25884.test.tsx | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 ui/packages/plugin-grid/src/__tests__/ObjectGrid.dateColumnOneFace-25884.test.tsx diff --git a/ui/packages/plugin-grid/src/__tests__/ObjectGrid.dateColumnOneFace-25884.test.tsx b/ui/packages/plugin-grid/src/__tests__/ObjectGrid.dateColumnOneFace-25884.test.tsx new file mode 100644 index 00000000000..817b699cf72 --- /dev/null +++ b/ui/packages/plugin-grid/src/__tests__/ObjectGrid.dateColumnOneFace-25884.test.tsx @@ -0,0 +1,194 @@ +/** + * 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. + */ + +/** + * objectstack#25884 — a grid's `date` column reads in ONE face, the absolute + * short date, when its field declares no `format`. + * + * Measured on a CRM's quote list sorted by its quote date: the column read + * "Yesterday · 3 days ago · 7 days ago · Sep 25 · Sep 10", because the cell's + * default face was `'relative'`, and `formatRelativeDate` is relative only + * inside a ±7-day window and absolute outside it. Three faces in one column: + * the eye cannot scan it and a sorted column does not look sorted. The ruling + * on the card: absolute in a sortable column, relative at most as a tooltip. + * + * Every column path of the real `ObjectGrid` is driven (A object `columns`, + * B string `columns`, C the inline-data projection, D the object-schema + * default columns), because each builds its own cell and each must say the + * cell sits in a column. + * + * CONTROLS: a field that DECLARES `format: 'relative'` keeps the relative face + * in the same grid, and a declared deadline keeps its red, its overdue + * wording moving to the cell's tooltip. + */ +import React from 'react'; +import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vitest'; +import { + assertNoOtherNetworkEscape, + installRecordSecurityExplainDouble, +} from '@object-ui/test-support'; +import { render, screen, cleanup, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { registerAllFields, formatDate } from '@object-ui/fields'; +import { ActionProvider } from '@object-ui/react'; +import { ObjectGrid } from '../ObjectGrid'; + +registerAllFields(); + +beforeAll(() => { + if (!Element.prototype.hasPointerCapture) { + Element.prototype.hasPointerCapture = vi.fn(() => false) as any; + } + if (!Element.prototype.scrollIntoView) { + Element.prototype.scrollIntoView = vi.fn() as any; + } +}); + +beforeEach(() => installRecordSecurityExplainDouble(vi)); +afterEach(() => { + assertNoOtherNetworkEscape(expect); + cleanup(); + vi.unstubAllGlobals(); +}); + +/** A calendar day `offset` days from today, as a `date` field stores it. */ +function storedDay(offset: number): string { + const d = new Date(); + d.setDate(d.getDate() + offset); + const pad = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; +} + +// Four days inside the relative window and two outside it, one of them in a +// past year, so every branch of the old default is read in one column. +const OFFSETS = [-1, -3, -6, 2, -40, -400]; + +const QUOTE_SCHEMA = { + name: 'quote', + label: 'Quote', + fields: { + title: { type: 'text', label: 'Title' }, + quoted_on: { type: 'date', label: 'Quoted on' }, + expires_on: { type: 'date', label: 'Expires on', dueLike: true }, + followed_up: { type: 'date', label: 'Followed up', format: 'relative' }, + }, +}; + +const ROWS = OFFSETS.map((offset, i) => ({ + id: `q${i}`, + title: `Quote ${i}`, + quoted_on: storedDay(offset), + expires_on: storedDay(offset), + followed_up: storedDay(offset), +})); + +const NAMES = ['title', 'quoted_on', 'expires_on', 'followed_up']; + +function makeDataSource() { + return { + find: vi.fn(async () => ({ data: ROWS, total: ROWS.length })), + getObjectSchema: vi.fn(async () => QUOTE_SCHEMA), + } as any; +} + +function renderGrid(schemaOverrides: Record, props: Record = {}) { + render( + + + , + ); +} + +/** The column's index among the header cells, by its label. */ +function columnIndex(label: string): number { + const headers = [...document.querySelectorAll('thead tr th')].map((th) => th.textContent ?? ''); + const index = headers.findIndex((text) => text.includes(label)); + if (index < 0) throw new Error(`no column headed ${label}; headers: ${JSON.stringify(headers)}`); + return index; +} + +/** The column's cell for each row, top to bottom, keyed by the row's title. */ +function columnCells(label: string): Map { + const index = columnIndex(label); + const cells = new Map(); + for (const tr of document.querySelectorAll('tbody tr')) { + const tds = tr.querySelectorAll('td'); + const title = [...tds].map((td) => td.textContent ?? '').find((text) => /^Quote \d$/.test(text)); + if (title && tds.length > index) cells.set(title, tds[index] as HTMLElement); + } + return cells; +} + +// Every phrase the relative face writes in `en`, inside its window. +const RELATIVE_WORDING = /ago|yesterday|today|tomorrow|^in \d|overdue/i; + +async function settled() { + await waitFor(() => expect(screen.getAllByText(/^Quote \d$/)).toHaveLength(ROWS.length)); + await waitFor(() => expect(columnCells('Quoted on').size).toBe(ROWS.length)); +} + +function expectOneAbsoluteFace(label: string, field: 'quoted_on' | 'expires_on') { + const cells = columnCells(label); + ROWS.forEach((row, i) => { + const text = cells.get(`Quote ${i}`)?.textContent ?? ''; + expect(text).not.toMatch(RELATIVE_WORDING); + expect(text).toBe(formatDate(row[field], undefined, { locale: 'en-US' })); + }); +} + +async function expectGridFaces() { + await settled(); + // The defect: one face per column, whatever the distance from today. + expectOneAbsoluteFace('Quoted on', 'quoted_on'); + expectOneAbsoluteFace('Expires on', 'expires_on'); + + // The deadline keeps its red on the past rows, and the overdue wording the + // cell no longer paints is its tooltip. + const expires = columnCells('Expires on'); + const threeDaysPast = expires.get('Quote 1')!.querySelector('span.tabular-nums')!; + expect(threeDaysPast.className).toContain('text-red-600'); + expect(threeDaysPast.getAttribute('title')).toBe('Overdue 3d'); + const inTwoDays = expires.get('Quote 3')!.querySelector('span.tabular-nums')!; + expect(inTwoDays.className).not.toContain('text-red-600'); + expect(inTwoDays.getAttribute('title')).toBe('In 2 days'); + + // An undeclared date's relative phrase is its tooltip too. + const yesterday = columnCells('Quoted on').get('Quote 0')!.querySelector('span.tabular-nums')!; + expect(yesterday.getAttribute('title')).toBe('Yesterday'); + + // CONTROL: a declared `format: 'relative'` keeps the relative face. + const followed = columnCells('Followed up'); + expect(followed.get('Quote 0')?.textContent).toBe('Yesterday'); + expect(followed.get('Quote 1')?.textContent).toBe('3 days ago'); +} + +describe('objectstack#25884 — a grid date column reads in one absolute face', () => { + it('A: object `columns`', async () => { + renderGrid({ columns: NAMES.map((field) => ({ field })), data: { provider: 'value', items: ROWS } }); + await expectGridFaces(); + }); + + it('B: string `columns`', async () => { + renderGrid({ columns: NAMES, data: { provider: 'value', items: ROWS } }); + await expectGridFaces(); + }); + + it('C: inline-data projection (`fields`, no `columns`)', async () => { + renderGrid({ fields: NAMES, data: { provider: 'value', items: ROWS } }); + await expectGridFaces(); + }); + + it('D: object-schema default columns (rows handed down)', async () => { + renderGrid({}, { data: ROWS }); + await expectGridFaces(); + }); +}); From 8f4a261c47cd26fabfff676326036b65e432eab9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 19:23:19 +0000 Subject: [PATCH 2/6] fix(fields,plugin-grid): a grid date column reads in one absolute face A date field with no declared format defaulted to the relative face, which is absolute outside a 7-day window, so one grid column mixed three faces. The date cell now takes inGridColumn; ObjectGrid passes it on every column cell, and the group heading restates the same selection. The relative phrase moves to the cell's tooltip; a declared format still wins. Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude --- .changeset/25884-grid-date-absolute.md | 18 +++++++++ ui/packages/fields/src/index.tsx | 38 +++++++++++++++++-- ui/packages/plugin-grid/src/ObjectGrid.tsx | 26 ++++++++----- .../ObjectGrid.deadlineSettled-11815.test.tsx | 38 +++++++++++++------ 4 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 .changeset/25884-grid-date-absolute.md diff --git a/.changeset/25884-grid-date-absolute.md b/.changeset/25884-grid-date-absolute.md new file mode 100644 index 00000000000..ad1e2ba1225 --- /dev/null +++ b/.changeset/25884-grid-date-absolute.md @@ -0,0 +1,18 @@ +--- +'@object-ui/fields': patch +'@object-ui/plugin-grid': patch +--- + +fix(fields,plugin-grid): a grid's `date` column reads in one absolute face when its field declares no `format` + +Clause-②: no +Metadata-protocol: none + +A list grid's `date` column used to mix three faces in one column, for example "Yesterday · 3 days ago · Sep 25 · Sep 20, 2025". The cell's default face was `'relative'`, which writes a relative phrase within ±7 days of today and an absolute date outside that window. A sorted column did not look sorted, and "Yesterday" in an expiry column read as a status. + +- **Default change for an undeclared `format` (visible from metadata).** In a grid column, a `date` field that declares no `format` now renders the locale's short absolute date: `Oct 25`, with the year added when the date is not in the current year. Every row of the column uses this one face. The group headings of a grid grouped by that field change with it, so a heading still reads like the cell under it. +- **The relative phrase moves to the tooltip.** It is shown on hover ("Yesterday", "In 2 days", or "Overdue 3d" on a declared `dueLike` field). A date outside the ±7-day window has no relative phrase, so it keeps its ISO `title` as before. A past declared deadline is still red. +- **A declared `format` still wins.** A field that declares `format: 'relative'` keeps the relative face in the grid. This is how an author who wants relative wording in a list keeps it. +- **Other surfaces are unchanged.** The record detail body, the header highlights strip, kanban and gallery cards, the tree, and the grid's mobile and record panels render `date` cells as before. `DateCellRenderer` paints the absolute default only when its host passes the new `inGridColumn` prop, and `ObjectGrid` passes it on every column it builds. `datetime` cells are not affected, because their default face was already absolute (`compact`). + +What to do: nothing. To keep the relative wording in a grid column, declare `format: 'relative'` on the field. diff --git a/ui/packages/fields/src/index.tsx b/ui/packages/fields/src/index.tsx index 2ac4abd3f41..d8c92f6c83b 100644 --- a/ui/packages/fields/src/index.tsx +++ b/ui/packages/fields/src/index.tsx @@ -425,6 +425,23 @@ export interface CellRendererProps { * the default face that drops the current year. */ alwaysShowYear?: boolean; + /** + * The cell is one of a grid COLUMN's cells, read against the cells above + * and below it (objectstack#25884). Read by the `date` cell only. + * + * A `date` field that declares no `format` then paints `formatDate`'s + * absolute default face instead of `'relative'`, and the relative phrase + * becomes the cell's tooltip. The relative face is absolute outside a ±7-day + * window, so as a column default it mixed faces in one column ("Yesterday", + * "Sep 25", "Sep 20, 2025"), and a sorted column did not look sorted. The + * ruling on the card: absolute in a sortable column, relative at most as a + * tooltip. A declared `format` still wins, `'relative'` included. + * + * `ObjectGrid` passes it on every column cell it builds. Absent or `false` + * changes nothing, so the record detail body, the highlights strip and the + * card surfaces keep the relative default. + */ + inGridColumn?: boolean; } // `coerceToSafeValue` lives in `./coerceToSafeValue.ts` (objectui#8580) so @@ -1205,19 +1222,34 @@ export function DateCellRenderer({ value, field, deadlineSettled, alwaysShowYear const date = new Date(safe as string | number); const dateField = field as any; - const style = dateField.format || 'relative'; + const declaredStyle: string | undefined = dateField.format || undefined; + // objectstack#25884 — in a grid column the undeclared default is the + // absolute face (`formatDate`'s default, `undefined`), one face per column; + // everywhere else it stays `'relative'`. ⚠️ `ObjectGrid`'s + // `temporalGroupHeading` restates this selection for its group headings, + // and `groupDateHeading-13234.test.tsx` holds the two equal. + const style = declaredStyle ?? (inGridColumn ? undefined : 'relative'); // Both halves of the affordance come from the shared reads above, so the // `datetime` sibling one function down answers this question identically // instead of carrying a second copy (objectui#8958). const dueLike = resolveDueLike(field, deadlineSettled); - const formatted = formatDate(safe as string | Date, style, { dueLike, locale, t, alwaysShowYear }); + const options = { dueLike, locale, t, alwaysShowYear }; + const formatted = formatDate(safe as string | Date, style, options); const isOverdue = isOverdueInstant(displayDate, dueLike); + // The relative phrase the grid default no longer paints ("Yesterday", + // "Overdue 3d") is the tooltip instead. Outside the ±7-day window the phrase + // IS the absolute face, which says nothing new, so the ISO `title` stays. + const relative = inGridColumn && declaredStyle === undefined + ? formatDate(safe as string | Date, 'relative', options) + : undefined; + const title = relative !== undefined && relative !== formatted ? relative : date.toISOString(); + return ( {formatted} diff --git a/ui/packages/plugin-grid/src/ObjectGrid.tsx b/ui/packages/plugin-grid/src/ObjectGrid.tsx index aafaa39eb4a..c60325b7b4a 100644 --- a/ui/packages/plugin-grid/src/ObjectGrid.tsx +++ b/ui/packages/plugin-grid/src/ObjectGrid.tsx @@ -1688,7 +1688,9 @@ interface TemporalGroupFace { * empty key, the stored text otherwise. * * The formatting CALLS are the cells' own, with the cells' arguments: - * `DateCellRenderer` paints `formatDate(value, format || 'relative', …)`; + * `DateCellRenderer` in a grid column (`inGridColumn`, objectstack#25884) + * paints `formatDate(value, format || undefined, …)`, the absolute default + * face when no `format` is declared; * `DateTimeCellRenderer` paints `formatRelativeDate` for `'relative'`, the * compact face for no format and for `'short'`, and `formatDateTime` with the * authored word otherwise (the compact face's string is the cell's two halves @@ -1713,7 +1715,7 @@ function temporalGroupHeading( if (!key) return undefined; // The cells' validity guard, the shared parse step. if (isNaN(toDisplayDate(key).getTime())) return undefined; - if (face.rendererType === 'date') return formatDate(key, face.format || 'relative', { locale }); + if (face.rendererType === 'date') return formatDate(key, face.format || undefined, { locale }); // A `datetime` cell renders an INSTANT, from the engine's parse. const instant = new Date(key); const style = !face.format || face.format === 'short' ? 'compact' : face.format; @@ -3759,6 +3761,12 @@ export const ObjectGrid: React.FC = ({ // one helper in `@object-ui/core`, with the scope and relation collapse // this grid's conditional formatting uses; a field that is not a declared // deadline answers `false` before any predicate runs. + // + // objectstack#25884 — every column cell built below also carries + // `inGridColumn`, so a `date` field with no declared `format` reads in + // one absolute face down the column (`CellRendererProps.inGridColumn`). + // The record panel and the mobile card's slots draw their cells without + // it, outside this function. const settledFor = (field: unknown, row: Record | undefined): boolean => isDeadlineSettled(field as Parameters[0], row, { scope: predicateScope, fields: objectFields }); // Map field type to column header icon (Airtable-style) @@ -4082,7 +4090,7 @@ export const ObjectGrid: React.FC = ({ // Both link and action: link takes priority for navigation, action executes on secondary interaction cellRenderer = (value: any, row: any) => { const displayContent = pointerFace?.(row) ?? (LinkContentRenderer - ? + ? : (value != null && value !== '' ? String(value) : )); return ( = ({ // Link column: clicking navigates to the record detail cellRenderer = (value: any, row: any) => { const displayContent = pointerFace?.(row) ?? (LinkContentRenderer - ? + ? : (value != null && value !== '' ? String(value) : )); return ( = ({ // `dependentValues` for a cell whose value only means something // beside a sibling column (objectui#12613). cellRenderer = (value: any, row: any) => ( - pointerFace?.(row) ?? + pointerFace?.(row) ?? ); } else { // Default renderer with empty value handling @@ -4321,7 +4329,7 @@ export const ObjectGrid: React.FC = ({ if (isPrimaryField && LinkContentRenderer) { cellRenderer = (value: any, row: any) => { - const displayContent = pointerFace?.(row) ?? ; + const displayContent = pointerFace?.(row) ?? ; return ( = ({ ); } else if (CellRenderer) { - cellRenderer = (value: any, row: any) => pointerFace?.(row) ?? ; + cellRenderer = (value: any, row: any) => pointerFace?.(row) ?? ; } return { @@ -4510,7 +4518,7 @@ export const ObjectGrid: React.FC = ({ ...(schema.showColumnTypeIcons && rendererType && { headerIcon: getTypeIcon(rendererType) }), ...(inferredAlign && { align: inferredAlign }), ...(CellRenderer && { - cell: (value: any, row: any) => pointerFace?.(row) ?? , + cell: (value: any, row: any) => pointerFace?.(row) ?? , }), sortable: fieldDef?.sortable !== false, }; @@ -4618,7 +4626,7 @@ export const ObjectGrid: React.FC = ({ // Aligned on the RENDERER key, like paths A-C: a `text` column with // `format: 'currency'` renders as currency, so it aligns as currency. ...(!!rendererType && numericTypes.includes(rendererType) && { align: 'right' as const }), - cell: (value: any, row: any) => pointerFace?.(row) ?? , + cell: (value: any, row: any) => pointerFace?.(row) ?? , sortable: field.sortable !== false, }); }); diff --git a/ui/packages/plugin-grid/src/__tests__/ObjectGrid.deadlineSettled-11815.test.tsx b/ui/packages/plugin-grid/src/__tests__/ObjectGrid.deadlineSettled-11815.test.tsx index d6d5c91ae86..21cd5e57132 100644 --- a/ui/packages/plugin-grid/src/__tests__/ObjectGrid.deadlineSettled-11815.test.tsx +++ b/ui/packages/plugin-grid/src/__tests__/ObjectGrid.deadlineSettled-11815.test.tsx @@ -13,6 +13,9 @@ * row's reads "Overdue Nd" in red (ruling D). The projection asks for the * `settledWhen` operand when no column shows it. * + * objectstack#25884 — a grid column paints the absolute date, so the wording + * half of that verdict is read from the cell's tooltip; the red is on the cell. + * * The deadline is named `deliver_by` on purpose: no retired name guess can * paint it, so only the declaration can. */ @@ -22,9 +25,9 @@ import { assertNoOtherNetworkEscape, installRecordSecurityExplainDouble, } from '@object-ui/test-support'; -import { render, screen, cleanup, waitFor } from '@testing-library/react'; +import { render, cleanup, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { registerAllFields } from '@object-ui/fields'; +import { registerAllFields, formatDate } from '@object-ui/fields'; import { ActionProvider } from '@object-ui/react'; import { ObjectGrid } from '../ObjectGrid'; @@ -93,17 +96,30 @@ function renderGrid(schemaOverrides: Record, props: Record screen.getByText(text).closest('span.tabular-nums')?.className.includes('text-red-600'); +/** + * The date cells whose tooltip reads `title`. objectstack#25884: a grid + * column paints the absolute date, and the relative phrase ("Overdue 5d", + * "5 days ago") the cell used to paint is its tooltip, so the per-row + * verdict is read there; the red stays on the cell. + */ +const cellsTitled = (title: string) => + [...document.querySelectorAll('span.tabular-nums')].filter((el) => el.getAttribute('title') === title); +const absolute = (day: string) => formatDate(day, undefined, { locale: 'en-US' }); /** One overdue (open row) and one neutral (done row) deadline, and the undeclared control. */ async function expectOpenOverdueDoneNeutral() { - await waitFor(() => expect(screen.getByText('Overdue 5d')).toBeInTheDocument()); - expect(screen.getAllByText('Overdue 5d')).toHaveLength(1); - expect(isRed('Overdue 5d')).toBe(true); - expect(screen.getAllByText('5 days ago')).toHaveLength(1); - expect(isRed('5 days ago')).toBe(false); + await waitFor(() => expect(cellsTitled('Overdue 5d')).toHaveLength(1)); + const [overdue] = cellsTitled('Overdue 5d'); + expect(overdue.className).toContain('text-red-600'); + expect(overdue.textContent).toBe(absolute(daysAgo(5))); + const neutral = cellsTitled('5 days ago'); + expect(neutral).toHaveLength(1); + expect(neutral[0].className).not.toContain('text-red-600'); + expect(neutral[0].textContent).toBe(absolute(daysAgo(5))); // CONTROL: an undeclared past date reads neutral on both rows. - expect(screen.getAllByText('4 days ago')).toHaveLength(2); + const undeclared = cellsTitled('4 days ago'); + expect(undeclared).toHaveLength(2); + for (const cell of undeclared) expect(cell.className).not.toContain('text-red-600'); } const NAMES = ['title', 'deliver_by', 'started_on']; @@ -150,8 +166,8 @@ describe('objectui#11815 — the projection carries what settledWhen reads', () const selects = ds.find.mock.calls.map(([, params]: any[]) => params?.$select); expect(selects.length).toBeGreaterThan(0); for (const sel of selects) expect(sel).toEqual(expect.arrayContaining(['id', 'title', 'deliver_by', 'status'])); - await waitFor(() => expect(screen.getByText('Overdue 5d')).toBeInTheDocument()); - expect(screen.getAllByText('5 days ago')).toHaveLength(1); + await waitFor(() => expect(cellsTitled('Overdue 5d')).toHaveLength(1)); + expect(cellsTitled('5 days ago')).toHaveLength(1); }); it('CONTROL — an undeclared shown date pulls nothing extra into the projection', async () => { From 089cca6b77d612c7c7b613c666c46ebe87b2155b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 19:26:46 +0000 Subject: [PATCH 3/6] fix(fields): destructure inGridColumn in DateCellRenderer Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude --- ui/packages/fields/src/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/fields/src/index.tsx b/ui/packages/fields/src/index.tsx index d8c92f6c83b..04cd12d255d 100644 --- a/ui/packages/fields/src/index.tsx +++ b/ui/packages/fields/src/index.tsx @@ -1161,7 +1161,7 @@ function isOverdueInstant(date: Date, dueLike: boolean): boolean { /** * Date field cell renderer */ -export function DateCellRenderer({ value, field, deadlineSettled, alwaysShowYear }: CellRendererProps): React.ReactElement { +export function DateCellRenderer({ value, field, deadlineSettled, alwaysShowYear, inGridColumn }: CellRendererProps): React.ReactElement { // `useDisplayLocale()`, NOT `useLocalization().locale` (objectui#4468). The // raw tenant locale is `undefined` on any workspace that never configured // one, and `Intl` reads the MACHINE's locale from `undefined` — so a `zh` From b5bc03282c69350178205300efadcac40a3c09d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 20:24:59 +0000 Subject: [PATCH 4/6] chore(sdui): re-record the fields and plugin-grid source digests Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude --- scripts/sdui-manifest.sources/fields.json | 2 +- scripts/sdui-manifest.sources/plugin-grid.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/sdui-manifest.sources/fields.json b/scripts/sdui-manifest.sources/fields.json index 112d644e064..76af46272d7 100644 --- a/scripts/sdui-manifest.sources/fields.json +++ b/scripts/sdui-manifest.sources/fields.json @@ -1 +1 @@ -{"package":"ui/packages/fields","sources":96,"sha256":"14f77ca68180dba0ba0226c934cc41bbb0c4d317a1472fb3512d75cf3b4ab7ac","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/fields","sources":96,"sha256":"21e2839c0b78f6377d302e01bd43869d0dbd848a398a68fdb1dc200f1868c61a","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-grid.json b/scripts/sdui-manifest.sources/plugin-grid.json index 8e53b237242..14be69f06bc 100644 --- a/scripts/sdui-manifest.sources/plugin-grid.json +++ b/scripts/sdui-manifest.sources/plugin-grid.json @@ -1 +1 @@ -{"package":"ui/packages/plugin-grid","sources":38,"sha256":"f6f7b1cbd89a1536a6e33febf4db131b8d526bd7281be460c401dccb8e97876a","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/plugin-grid","sources":38,"sha256":"8ac44768d18846f153f2b3b4ec942e19563fa4d75b6bdda23a4f8e4c27ca4716","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} From aa24a3a2c861172463cd0063682084267ccb5c83 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 21:28:15 +0000 Subject: [PATCH 5/6] docs(changeset): name the surfaces the grid date default reaches Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude --- .changeset/25884-grid-date-absolute.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/25884-grid-date-absolute.md b/.changeset/25884-grid-date-absolute.md index ad1e2ba1225..c3bc64d71db 100644 --- a/.changeset/25884-grid-date-absolute.md +++ b/.changeset/25884-grid-date-absolute.md @@ -13,6 +13,6 @@ A list grid's `date` column used to mix three faces in one column, for example " - **Default change for an undeclared `format` (visible from metadata).** In a grid column, a `date` field that declares no `format` now renders the locale's short absolute date: `Oct 25`, with the year added when the date is not in the current year. Every row of the column uses this one face. The group headings of a grid grouped by that field change with it, so a heading still reads like the cell under it. - **The relative phrase moves to the tooltip.** It is shown on hover ("Yesterday", "In 2 days", or "Overdue 3d" on a declared `dueLike` field). A date outside the ±7-day window has no relative phrase, so it keeps its ISO `title` as before. A past declared deadline is still red. - **A declared `format` still wins.** A field that declares `format: 'relative'` keeps the relative face in the grid. This is how an author who wants relative wording in a list keeps it. -- **Other surfaces are unchanged.** The record detail body, the header highlights strip, kanban and gallery cards, the tree, and the grid's mobile and record panels render `date` cells as before. `DateCellRenderer` paints the absolute default only when its host passes the new `inGridColumn` prop, and `ObjectGrid` passes it on every column it builds. `datetime` cells are not affected, because their default face was already absolute (`compact`). +- **Other surfaces are unchanged.** The record detail body, the header highlights strip, kanban and gallery cards, the tree, a record page's related list and the grid's record panel render `date` cells as before. `DateCellRenderer` paints the absolute default only when its host passes the new `inGridColumn` prop, and `ObjectGrid` passes it on every column it builds. On mobile, a grid card draws its title through the column's cell, so a `date` title reads absolute there too; the card's other date slots were already absolute. `datetime` cells are not affected, because their default face was already absolute (`compact`). What to do: nothing. To keep the relative wording in a grid column, declare `format: 'relative'` on the field. From c17032e06249382a74d15abf3fe8ba6fc7311e39 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 00:39:47 +0000 Subject: [PATCH 6/6] chore(sdui): re-record the plugin-grid source digest after merging main Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude --- scripts/sdui-manifest.sources/plugin-grid.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdui-manifest.sources/plugin-grid.json b/scripts/sdui-manifest.sources/plugin-grid.json index 0e5567fe367..6a1ed5187a7 100644 --- a/scripts/sdui-manifest.sources/plugin-grid.json +++ b/scripts/sdui-manifest.sources/plugin-grid.json @@ -1 +1 @@ -{"package":"ui/packages/plugin-grid","sources":46,"sha256":"c0a562a633d4901dcde8bad54819f468c8ed4d7ca4f002cf7defa764ebfcd7d8","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/plugin-grid","sources":46,"sha256":"39a2253866b588db404ead870382e69c7d4edd339d0a8b8ddb25b2ebe508841c","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}