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
31 changes: 31 additions & 0 deletions .changeset/calendar-view-mode-agenda-retired-5740.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
---
'@object-ui/types': minor
---

`'agenda'` leaves `CalendarViewMode` and the zod `CalendarViewModeSchema`
(objectui#5740 — the value-level residue of objectui#5667's key-level
convergence of `CalendarViewSchema` on the registered `calendar-view`
renderer's measured read set; ADR-0049 enforce-or-remove).

The union declared a value nothing enforced: the registered renderer's `view`
input declares `enum: ['month','week','day']`, `resolveAuthoredView` resolves
any off-enum value — `'agenda'` included — to `undefined` (the component's
`'month'` default), and `CalendarView` renders no agenda view. An author
writing the type-legal, zod-valid `view: 'agenda'` got a month calendar with
no error or warning. No in-repo, example, or catalog app authors
`view: 'agenda'` (measured during objectui#5667's sweep and re-measured for
this change, including the objectstack tree).

**This narrows the accept set — unlike #5667's key retirements, which created
no new rejections.** `view` is a declared key, and declared keys are validated
even under `.passthrough()`, so `view: 'agenda'` is now a **validation error
that previously parsed green** (an `invalid_value` issue on the `view` path,
offering `month`/`week`/`day`). Undeclared keys still pass through unchanged.
Breaking on the published zod surface; ships as `minor` per this repo's
version-alignment policy (majors track `@objectstack`).

The runtime boundary is unchanged: an off-union `view` in raw metadata still
falls back to the component's `'month'` default at the renderer, and the
registry input already declared the three-value enum. Docblocks, the schema
reference table, and the zod `describe` no longer teach an `'agenda'`
fallback.
2 changes: 1 addition & 1 deletion content/docs/api/schema-reference.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -1076,7 +1076,7 @@ authored `events` is dropped by design, objectui#4433).
| `endDateField` | `string` | Record field for the event end date/time. Default `"end"`. |
| `allDayField` | `string` | Record field for the all-day flag. Default `"allDay"`. |
| `colorField` | `string` | Record field for the event color. Default `"color"`. |
| `view` | `CalendarViewMode` | View mode: `"month"`, `"week"`, `"day"`. Any other value falls back to `"month"`. |
| `view` | `CalendarViewMode` | View mode: `"month"`, `"week"`, `"day"` — the full union. `"agenda"` was retired in objectui#5740 and now fails validation. Default `"month"`. |
| `currentDate` | `string \| Date` | Initial calendar date — an ISO date string when authored as JSON. |
| `allowCreate` | `boolean` | Show the "New event" affordance; clicking it dispatches a `create` action. Default `false`. |
| `onEventClick` | `function` | Host-only: forwarded when a React host supplies a function; authored JSON cannot produce one. |
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
/**
* 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.
*/

/**
* Retirement pin — `'agenda'` leaves `CalendarViewMode` and
* `CalendarViewModeSchema` (objectui#5740, the value-level residue of
* objectui#5667's key-level convergence on the `calendar-view` renderer's
* measured read set).
*
* The declared union named `'agenda'`, but the registered renderer's `view`
* input declares `enum: ['month','week','day']`, `resolveAuthoredView`
* resolves any off-enum value to `undefined` (the component's `'month'`
* default), and `CalendarView` renders no agenda view — so an author writing
* the type-legal, zod-valid `view: 'agenda'` got a month calendar with no
* error. Declared ≠ enforced, on a published surface (ADR-0049).
*
* ⚠️ Unlike #5667's key retirements, this one NARROWS the accept set: `view`
* is a DECLARED key, and declared keys are validated even under
* `.passthrough()`, so `view: 'agenda'` — which parsed green before — is now
* a validation error. That is exactly why the zod half is pinned by the
* REFUSAL ENVELOPE (a `view`-path `invalid_value` issue naming the surviving
* vocabulary) rather than a bare `success === false`, and why the passthrough
* control below pins that the rejection is declared-key validation, not a
* strictness change. The TS half erases at runtime, so it is pinned with
* `@ts-expect-error`, which is real enforcement here because
* `packages/types/tsconfig.test.json` is chained from this package's
* `type-check` script (#3009).
*
* The runtime resolver is deliberately NOT changed: an off-union value in raw
* metadata still falls back to `'month'` at the renderer boundary
* (`calendar-view-renderer.propsContract.test.tsx` pins that branch).
*/

import { describe, it, expect } from 'vitest';
import { CalendarViewModeSchema, CalendarViewSchema } from '../zod/complex.zod.js';
import type { CalendarViewMode } from '../complex.js';

/** The retired value, and the survivors the renderer actually renders. */
const RETIRED = 'agenda';
const SURVIVORS = ['month', 'week', 'day'] as const;

describe('CalendarViewModeSchema — the shared enum no longer offers the retired value', () => {
it('is exactly the rendered set', () => {
expect(CalendarViewModeSchema.options).toEqual([...SURVIVORS]);
});

it('rejects the retired value with an invalid_value issue', () => {
const result = CalendarViewModeSchema.safeParse(RETIRED);
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.issues).toHaveLength(1);
expect(result.error.issues[0].code).toBe('invalid_value');
});
});

describe('CalendarViewSchema.view — the NEW rejection this retirement creates', () => {
it("rejects `view: 'agenda'` on the `view` path, where it previously parsed green", () => {
const result = CalendarViewSchema.safeParse({ type: 'calendar-view', view: RETIRED });

expect(result.success).toBe(false);
if (result.success) return;

// The envelope, not just the verdict: exactly one issue, on the `view`
// path, coded as a closed-vocabulary violation. A bare `success === false`
// would also pass if the node had been rejected for an unrelated reason.
//
// The offending value is deliberately NOT asserted to appear in the
// message: measured on zod 4, an `invalid_value` issue carries the ALLOWED
// `values` and no echo of the input.
const viewIssues = result.error.issues.filter((i) => i.path.join('.') === 'view');
expect(viewIssues).toHaveLength(1);
expect(viewIssues[0].code).toBe('invalid_value');

// The shrink, read off the refusal: the retired value is gone from the
// offered vocabulary and every survivor is still in it.
const offered = (viewIssues[0] as { values?: unknown[] }).values ?? [];
expect(offered).not.toContain(RETIRED);
for (const survivor of SURVIVORS) expect(offered).toContain(survivor);
});

it('still accepts every rendered view mode in full', () => {
// Full green parses, not merely "no `view` issue": a value-level shrink
// that quietly invalidated a survivor would otherwise go unseen.
for (const survivor of SURVIVORS) {
const result = CalendarViewSchema.safeParse({ type: 'calendar-view', view: survivor });
expect(result.success).toBe(true);
}
});

it('passthrough control: an UNDECLARED key still parses — the rejection above is declared-key validation, not a strictness change', () => {
// `BaseSchema` is `.passthrough()`. This pin keeps the two facts apart:
// unknown keys pass (unchanged since #5667), while a declared key's value
// is validated (the new rejection above). If this control ever reds, the
// schema's strictness changed — a different contract decision than #5740.
const result = CalendarViewSchema.safeParse({
type: 'calendar-view',
someUndeclaredKey: RETIRED,
});
expect(result.success).toBe(true);
});
});

describe('the published TS twin no longer offers the retired value', () => {
it('`CalendarViewMode` rejects it at compile time and keeps the survivors', () => {
// @ts-expect-error — 'agenda' left the union (objectui#5740).
const retired: CalendarViewMode = RETIRED;
expect(retired).toBe(RETIRED);

const survivors: CalendarViewMode[] = [...SURVIVORS];
expect(survivors).toEqual([...SURVIVORS]);
});
});
15 changes: 10 additions & 5 deletions packages/types/src/complex.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,9 +127,14 @@ export interface KanbanSchema extends BaseSchema {
}

/**
* Calendar view mode
* Calendar view mode — the registered `calendar-view` renderer's rendered set.
*
* `'agenda'` was retired from this union (objectui#5740): no view ever
* rendered it — the renderer resolved it to the `'month'` default — and no
* measured app authors it (ADR-0049 enforce-or-remove, the value-level
* residue of objectui#5667's key-level convergence).
*/
export type CalendarViewMode = 'month' | 'week' | 'day' | 'agenda';
export type CalendarViewMode = 'month' | 'week' | 'day';

/**
* Calendar event
Expand DownExpand Up@@ -225,9 +230,9 @@ export interface CalendarViewSchema extends BaseSchema {
/**
* Calendar view mode.
*
* The registered renderer renders `'month' | 'week' | 'day'` and falls back
* to `'month'` for any other value — including `'agenda'`, which
* {@link CalendarViewMode} still names.
* {@link CalendarViewMode} equals the renderer's rendered set since
* objectui#5740 retired `'agenda'`; at runtime the renderer still resolves
* any off-union value in raw metadata to the `'month'` default.
* @default 'month'
*/
view?: CalendarViewMode;
Expand Down
16 changes: 13 additions & 3 deletions packages/types/src/zod/complex.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,9 +66,13 @@ export const KanbanSchema = BaseSchema.extend({
});

/**
* Calendar View Mode
* Calendar View Mode — the registered renderer's rendered set.
*
* `'agenda'` was retired (objectui#5740): no view ever rendered it, and no
* measured app authors it. `view` is a DECLARED key, so this retirement is a
* new rejection — see the accept-set note on {@link CalendarViewSchema}.
*/
export const CalendarViewModeSchema = z.enum(['month', 'week', 'day', 'agenda']);
export const CalendarViewModeSchema = z.enum(['month', 'week', 'day']);

/**
* Calendar Event Schema
Expand DownExpand Up@@ -97,6 +101,12 @@ export const CalendarEventSchema = z.object({
* `BaseSchema` is `.passthrough()`, so the retired keys are not REJECTED here
* — they are simply no longer declared or type-checked. The material accept
* change is that `events` is no longer required.
*
* Value-level residue (objectui#5740): `'agenda'` left
* `CalendarViewModeSchema`. Unlike the key retirements above, this IS a new
* rejection — `view` is a declared key, and declared keys are validated even
* under `.passthrough()` — so `view: 'agenda'`, which parsed green before,
* now fails with an `invalid_value` issue on the `view` path.
*/
export const CalendarViewSchema = BaseSchema.extend({
type: z.literal('calendar-view'),
Expand All@@ -118,7 +128,7 @@ export const CalendarViewSchema = BaseSchema.extend({
allDayField: z.string().optional().describe("Record field for the all-day flag (default 'allDay')"),
colorField: z.string().optional().describe("Record field for the event color (default 'color')"),
view: CalendarViewModeSchema.optional().describe(
"View mode (the renderer renders 'month' | 'week' | 'day'; other values fall back to 'month')",
"View mode 'month' | 'week' | 'day', the renderer's rendered set ('agenda' was retired: objectui#5740)",
),
currentDate: z
.union([z.string(), z.date()])
Expand Down
Loading