From 8d1a4325becbedb9e59979ff17d122d1026bd3e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 05:11:39 +0000 Subject: [PATCH 1/2] fix(types): type dueLike by reference and declare settledWhen on date and datetime field metadata (objectui#12297) DateFieldMetadata and DateTimeFieldMetadata now take dueLike from @objectstack/spec's FieldSchema instead of a local boolean, and declare settledWhen beside it by the same reference. settledWhen is declared, not yet rendered: the date cells' read belongs to objectui#11815. Claude-Session: https://claude.ai/code/session_01Qe458QJ3QyHSzgf9t1dDEH Co-authored-by: Claude --- ...tadata-deadline-by-reference-12297.test.ts | 109 ++++++++++++++++++ ...ield-metadata-family-retired-12242.test.ts | 8 +- packages/types/src/field-types.ts | 56 ++++++--- 3 files changed, 155 insertions(+), 18 deletions(-) create mode 100644 packages/types/src/__tests__/field-metadata-deadline-by-reference-12297.test.ts diff --git a/packages/types/src/__tests__/field-metadata-deadline-by-reference-12297.test.ts b/packages/types/src/__tests__/field-metadata-deadline-by-reference-12297.test.ts new file mode 100644 index 0000000000..e894b8ecb2 --- /dev/null +++ b/packages/types/src/__tests__/field-metadata-deadline-by-reference-12297.test.ts @@ -0,0 +1,109 @@ +/** + * 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. + */ + +/** + * objectui#12297 — the deadline semantic, `dueLike` and `settledWhen`, is typed + * on `DateFieldMetadata` and `DateTimeFieldMetadata` BY REFERENCE to + * `@objectstack/spec`'s `FieldSchema`, the convention `visibleWhen` and + * `currencyConfig` already follow in `field-types.ts`. + * + * - The type half is an exact `Equal` against `SpecField[KEY]`, read by this + * package's `type-check` through `tsconfig.test.json`. An exact equality, + * not an assignment: a local `boolean` happens to equal the spec's type + * today, and only the reference keeps it equal when the spec moves. + * - CONTROL: a non-deadline field type (`time`, `text`) carries neither key, + * and a fresh literal carrying one is refused. + * - The runtime half reads the installed spec, so "the spec declares both + * keys on a date field" and "the spec refuses them elsewhere" are + * re-derived on every run. + * + * Not pinned here: whether a renderer reads either key. `settledWhen` is + * declared and not yet read; the read is objectui#11815. + */ +import { describe, it, expect } from 'vitest'; +import { FieldSchema, type Field as SpecField } from '@objectstack/spec/data'; +import type { + DateFieldMetadata, + DateTimeFieldMetadata, + TextFieldMetadata, + TimeFieldMetadata, +} from '../field-types.js'; + +type Expect = T; +type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; + +// ── Declared by reference ──────────────────────────────────────────────────── + +export type assertionDeadlineByReference = [ + Expect>, + Expect>, + Expect>, + Expect>, +]; + +/** CONTROL: the keys live on the two deadline types only. */ +export type assertionNotOnOtherTypes = [ + Expect>, + Expect>, + Expect>, + Expect>, +]; + +export const _dateDeadline: DateFieldMetadata = { + type: 'date', + name: 'due_date', + dueLike: true, + settledWhen: "record.status == 'done'", +}; + +export const _datetimeDeadlineEnvelope: DateTimeFieldMetadata = { + type: 'datetime', + name: 'respond_by', + dueLike: true, + settledWhen: { dialect: 'cel', source: "record.status == 'closed'" }, +}; + +export const _timeRefusesDueLike: TimeFieldMetadata = { + type: 'time', + name: 'opens_at', + // @ts-expect-error — objectui#12297: `dueLike` is a `date` / `datetime` key only + dueLike: true, +}; + +export const _textRefusesSettledWhen: TextFieldMetadata = { + type: 'text', + name: 'status_note', + // @ts-expect-error — objectui#12297: `settledWhen` is a `date` / `datetime` key only + settledWhen: "record.status == 'done'", +}; + +// ── The installed spec ─────────────────────────────────────────────────────── + +/** Whether the parse failed with an issue at `path`. */ +function refusedAt(res: ReturnType, path: string): boolean { + if (res.success) return false; + return res.error.issues.some((i) => i.path.join('.') === path); +} + +describe('installed @objectstack/spec — the deadline keys (objectui#12297)', () => { + it('accepts both keys, in both wire shapes, on a date and a datetime field', () => { + expect(FieldSchema.safeParse(_dateDeadline).success).toBe(true); + expect(FieldSchema.safeParse(_datetimeDeadlineEnvelope).success).toBe(true); + }); + + it('refuses `dueLike` on a non-deadline type, with a minimal field of that type as the control', () => { + expect(FieldSchema.safeParse({ type: 'time', name: 'f' }).success).toBe(true); + expect(refusedAt(FieldSchema.safeParse({ type: 'time', name: 'f', dueLike: true }), 'dueLike')).toBe(true); + }); + + it('refuses `settledWhen` without `dueLike: true`, and accepts the same field with it', () => { + const settled = { type: 'date', name: 'f', settledWhen: "record.status == 'done'" }; + expect(refusedAt(FieldSchema.safeParse(settled), 'settledWhen')).toBe(true); + expect(FieldSchema.safeParse({ ...settled, dueLike: true }).success).toBe(true); + }); +}); diff --git a/packages/types/src/__tests__/field-metadata-family-retired-12242.test.ts b/packages/types/src/__tests__/field-metadata-family-retired-12242.test.ts index 8f18bbf030..7c8f2afa99 100644 --- a/packages/types/src/__tests__/field-metadata-family-retired-12242.test.ts +++ b/packages/types/src/__tests__/field-metadata-family-retired-12242.test.ts @@ -36,10 +36,10 @@ * - `GeolocationFieldMetadata` leaves the `FieldMetadata` union: `geolocation` * is not a spec `FieldType`. * - * `dueLike` is deliberately NOT moved. The installed spec refuses it, but - * `@objectstack/spec`'s own source has since declared it on `FieldSchema` - * (unpublished at the version installed here), so the authoring face is where - * it is headed, not away from; the card holds it for a decision. + * `dueLike` was deliberately NOT moved: `@objectstack/spec` declared it on + * `FieldSchema` before it reached the installed release. Now that it has, it + * is typed by reference beside `settledWhen` (objectui#12297), pinned in + * `field-metadata-deadline-by-reference-12297.test.ts`. */ import { describe, it, expect } from 'vitest'; import { FieldSchema, ObjectSchema, type Field as SpecField } from '@objectstack/spec/data'; diff --git a/packages/types/src/field-types.ts b/packages/types/src/field-types.ts index 05ea4b272a..81b45d441d 100644 --- a/packages/types/src/field-types.ts +++ b/packages/types/src/field-types.ts @@ -584,12 +584,35 @@ export interface DateFieldMetadata extends BaseFieldMetadata { */ max_date?: never; /** - * Marks this field as due/deadline-semantic (vs. a plain start/end/created - * date). Gates the relative-time "Overdue Nd" wording and red styling — - * without it, a past date renders as neutral "Nd ago" text. Auto-detected - * from common due/deadline field-name conventions when omitted. + * Declares this date a deadline — `@objectstack/spec`'s + * `FieldSchema.dueLike`, typed BY REFERENCE (objectui#12297). `true` lets a + * renderer show relative overdue wording ("Overdue Nd") and an overdue + * colour once the date has passed; a past date that is not a deadline reads + * as neutral "N days ago" text. The spec authors it on `date` and + * `datetime` fields only and refuses it on any other type. Display only: + * nothing on the write path reads it. + * + * ⚠️ The spec says absent or `false` means "not a deadline", with nothing + * inferred from the field's name. Today's date cells still fall back to the + * due/deadline field-NAME convention when the key is not `true`, and `false` + * does not suppress that fallback; retiring it is objectui#11815. + */ + dueLike?: SpecField['dueLike']; + /** + * When this field's deadline is SETTLED for a record — + * `@objectstack/spec`'s `FieldSchema.settledWhen`, typed BY REFERENCE + * (objectui#12297): a CEL predicate over the record's own columns, read as + * `record` (`record.status == 'done'`), as a bare string or the spec's + * expression envelope (`{ dialect, source }`). While it holds, no overdue + * wording or colour applies to that record. The spec requires + * `dueLike: true` beside it and refuses it on any type but `date` / + * `datetime`. Display only: nothing on the write path reads it. + * + * ⚠️ Declared, not yet rendered: no date cell evaluates it today, so a past + * deadline on a settled record still renders overdue. The read is + * objectui#11815. */ - dueLike?: boolean; + settledWhen?: SpecField['settledWhen']; } /** @@ -609,21 +632,26 @@ export interface DateTimeFieldMetadata extends BaseFieldMetadata { */ max_date?: never; /** - * Marks this field as due/deadline-semantic — the same key, with the same - * meaning, as {@link DateFieldMetadata.dueLike} one interface up. - * - * It is declared here because the runtime honours it here (objectui#8958). - * `DetailViewFieldSchema.dueLike` has always described itself as marking "a - * date/datetime field", and `DateTimeCellRenderer` now reads it; leaving it - * off this interface would keep the mismatch alive with the sign flipped — - * a key the renderer honours but the authoring type rejects. + * Declares this datetime a deadline — `@objectstack/spec`'s + * `FieldSchema.dueLike`, typed BY REFERENCE (objectui#12297): the same key, + * with the same meaning and the same name-convention fallback, as + * {@link DateFieldMetadata.dueLike} one interface up. `DateTimeCellRenderer` + * reads it (objectui#8958). * * ⚠️ Day granularity, inherited from the shared relative-time path: the * overdue wording gates on whole calendar days, so a datetime a few hours * past its deadline is not yet overdue. Sub-day precision is a separate * call and was not taken. */ - dueLike?: boolean; + dueLike?: SpecField['dueLike']; + /** + * When this datetime's deadline is SETTLED for a record — + * `@objectstack/spec`'s `FieldSchema.settledWhen`, typed BY REFERENCE + * (objectui#12297): the same key, with the same meaning and the same + * "declared, not yet rendered" status, as + * {@link DateFieldMetadata.settledWhen} one interface up. + */ + settledWhen?: SpecField['settledWhen']; } /** From 868d7b8e2e87843d7e2a88e8bec580339a35794d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 05:13:01 +0000 Subject: [PATCH 2/2] docs(fields): teach dueLike and settledWhen as the spec declares them (objectui#12297) The date and datetime field pages and the schema reference name both keys as FieldSchema members, say the spec reads an absent dueLike as "not a deadline" while today's cells still apply the field-name fallback, and say settledWhen is declared and not yet rendered. Claude-Session: https://claude.ai/code/session_01Qe458QJ3QyHSzgf9t1dDEH Co-authored-by: Claude --- .../12297-date-deadline-keys-by-reference.md | 8 ++++ content/docs/api/schema-reference.md | 8 ++++ content/docs/fields/date.mdx | 29 +++++++++++-- content/docs/fields/datetime.mdx | 41 +++++++++++++++---- 4 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 .changeset/12297-date-deadline-keys-by-reference.md diff --git a/.changeset/12297-date-deadline-keys-by-reference.md b/.changeset/12297-date-deadline-keys-by-reference.md new file mode 100644 index 0000000000..8d9533b030 --- /dev/null +++ b/.changeset/12297-date-deadline-keys-by-reference.md @@ -0,0 +1,8 @@ +--- +'@object-ui/types': minor +--- + +`DateFieldMetadata` and `DateTimeFieldMetadata` take the deadline keys from `@objectstack/spec`'s `FieldSchema` (objectui#12297). + +- **`dueLike`** is typed by reference to `FieldSchema.dueLike` instead of a local `boolean`, so it follows the spec when the spec moves. Its value type is unchanged. +- **`settledWhen`** is new on both interfaces, typed by reference to `FieldSchema.settledWhen`: a CEL predicate over the record (`record.status == 'done'`), as a bare string or as the spec's expression envelope, that settles the deadline for that record. The spec accepts it only beside `dueLike: true`. It is declared, not yet rendered: no date or datetime cell evaluates it yet, so a past deadline on a settled record still renders overdue. diff --git a/content/docs/api/schema-reference.md b/content/docs/api/schema-reference.md index 21ddfd7787..f858822e48 100644 --- a/content/docs/api/schema-reference.md +++ b/content/docs/api/schema-reference.md @@ -1303,6 +1303,14 @@ An enhanced detail view for a single record with sections, tabs and navigation. > ⚠️ The authored entry wins. Where the object's own field metadata also > declares `dueLike`, that value is consulted only when the entry leaves the > key out — it does not overwrite what the view author wrote. +> +> On the object's field metadata, `dueLike` and its partner `settledWhen` are +> `@objectstack/spec`'s `FieldSchema.dueLike` and `FieldSchema.settledWhen`, +> both for `date` / `datetime` fields only. `settledWhen` is a CEL predicate +> over the record (`record.status == 'done'`) that settles the deadline for +> that record, and the spec accepts it only beside `dueLike: true`. It is an +> object-metadata key: a `DetailViewField` entry does not carry it. ⚠️ It is +> declared, not yet rendered: no date cell evaluates it today. **Related:** [DetailSchema](#detailschema), [ObjectViewSchema](#objectviewschema) diff --git a/content/docs/fields/date.mdx b/content/docs/fields/date.mdx index a612bc17af..02fabb19b0 100644 --- a/content/docs/fields/date.mdx +++ b/content/docs/fields/date.mdx @@ -33,10 +33,31 @@ const closeDate: DateFieldMetadata = { }; ``` -`dueLike` marks the field as due/deadline-semantic, gating the relative "Overdue Nd" -wording and the red styling. `datetime` is its own type with its own metadata -(`DateTimeFieldMetadata`) and honours `dueLike` identically; see -[DateTime Field](/docs/fields/datetime). +`dueLike` declares the date a deadline, gating the relative "Overdue Nd" wording and +the red styling. It is `@objectstack/spec`'s `FieldSchema.dueLike`, and the spec +accepts it on `date` and `datetime` fields only. `datetime` is its own type with its +own metadata (`DateTimeFieldMetadata`) and honours `dueLike` identically; see +[DateTime Field](/docs/fields/datetime), which also covers the field-name fallback +today's cells still apply when the key is absent. + +`settledWhen` says when the deadline is settled for one record: a CEL predicate over +the record's own columns, read as `record`. While it holds, no overdue wording or +colour applies to that record. It is `FieldSchema.settledWhen`, and the spec accepts +it only beside `dueLike: true`: + +```ts +import type { DateFieldMetadata } from '@object-ui/types'; + +const dueDate: DateFieldMetadata = { + type: 'date', + name: 'due_date', + dueLike: true, + settledWhen: "record.status == 'done'", +}; +``` + +⚠️ `settledWhen` is declared, not yet rendered: no date cell evaluates it today, so a +past deadline on a settled record still renders overdue. There is no date-range key. `@objectstack/spec`'s `FieldSchema` declares no date bound (its `min` / `max` are numbers) and refuses `min_date` / `max_date` by name, and no diff --git a/content/docs/fields/datetime.mdx b/content/docs/fields/datetime.mdx index 3b2f4299a7..08354559d7 100644 --- a/content/docs/fields/datetime.mdx +++ b/content/docs/fields/datetime.mdx @@ -44,19 +44,42 @@ const startsAt: DateTimeFieldMetadata = { ## Overdue Affordance -`dueLike` marks the field as due/deadline-semantic. It is the same key, with the -same meaning, that the [Date Field](/docs/fields/date) uses — a past `starts_at` -or `created_at` is not a missed deadline, so only a field that says so gets the -affordance: +`dueLike` declares the datetime a deadline. It is `@objectstack/spec`'s +`FieldSchema.dueLike`, the same key, with the same meaning, that the +[Date Field](/docs/fields/date) uses, and the spec accepts it on `date` and +`datetime` fields only. A past `starts_at` or `created_at` is not a missed +deadline, so only a field that says so gets the affordance: - the relative face reads **"Overdue Nd"** instead of the neutral "N days ago"; - the cell is styled red, whichever display face it is painting. -The key is optional, and only a declared `true` short-circuits. With the key -absent, common due/deadline field-name conventions (`due_at`, `deadline`, -`expires_at`, `sla_at`, `renewal_at`, `next_action_at`, …) turn the affordance -on by themselves — and writing `dueLike: false` does **not** suppress that -fallback. Authoring a neutral field name is the opt-out. +The spec reads an absent key, or `false`, as "not a deadline", with nothing +inferred from the field's name. Today's cells do not follow that yet: only a +declared `true` short-circuits, and with the key absent, common due/deadline +field-name conventions (`due_at`, `deadline`, `expires_at`, `sla_at`, +`renewal_at`, `next_action_at`, …) turn the affordance on by themselves — +writing `dueLike: false` does **not** suppress that fallback. Until the fallback +is retired, authoring a neutral field name is the opt-out. + +`settledWhen` says when the deadline is settled for one record. It is +`FieldSchema.settledWhen`: a CEL predicate over the record's own columns, read as +`record`, written as a bare string or as the spec's expression envelope. While it +holds, no overdue wording or colour applies to that record, and the spec accepts it +only beside `dueLike: true`: + +```ts +import type { DateTimeFieldMetadata } from '@object-ui/types'; + +const respondBy: DateTimeFieldMetadata = { + type: 'datetime', + name: 'respond_by', + dueLike: true, + settledWhen: { dialect: 'cel', source: "record.status == 'closed'" }, +}; +``` + +⚠️ `settledWhen` is declared, not yet rendered: no datetime cell evaluates it +today, so a past deadline on a settled record still renders overdue. The affordance is **day-granular**. It compares calendar days, so a datetime a few hours past its deadline still reads "Today" and is not red — the same answer