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
57 changes: 57 additions & 0 deletions .changeset/7963-alert-dialog-footer-keys-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
'@object-ui/types': minor
'@object-ui/components': patch
---

`AlertDialogSchema` retires `cancelLabel`, `confirmLabel` and `confirmVariant`
(objectui#7963, ADR-0049 enforce-or-remove; maintainer ruling 2026-09-10).
`cancelText` and `actionText` are the surviving spellings, and `confirmVariant`
has **no** survivor at all.

**Breaking, and graded `minor` by this repo's convention** (AGENTS.md — a
breaking change here is `minor`; a `major` would drag the whole 39-package fixed
group off `@objectstack`'s cadence). A document that authored any of the three
used to parse **green**; it now reds at that key.

**What was measured.** Tree-wide on `72bcd7783`, a point-access probe scores
`schema.cancelLabel` / `schema.confirmLabel` / `schema.confirmVariant` at
**0 / 0 / 0**, against firing controls on the very renderer under test
(`packages/components/src/renderers/overlay/alert-dialog.tsx`):
`schema.cancelText` = **15**, read at `:37`, and `schema.actionText` = **5**,
read at `:38`. The keys did reach the Radix root through the renderer's
rest-spread, so a grep alone was not a verdict — the DOM reading is, and it is
kept as a live pin: varying one key per fixture through the real renderer leaves
the normalised dialog HTML unmoved, against a `CHANNEL` control (`open`, unread
and live through that same spread) and a `WIRED` control (`cancelText` /
`actionText` drawing both footer buttons). The `AlertDialog` root renders a
context provider rather than an element, so an unknown prop is dropped before
reaching any node. An author who wrote the declared trio got an **empty footer**.

**A named refusal, not a deletion.** `BaseSchemaCore` ends `.passthrough()` and
the TypeScript `BaseSchema` closes with `[key: string]: any`, so a *dropped*
member key is kept, not refused — deleting the declarations would have left the
silent accept exactly as it was. Each key stays declared and unwritable:
`retirementTombstone()` on the Zod face, `?: never` on the TypeScript face. The
messages name the remedy.

| retired key | what to write instead |
| --- | --- |
| `cancelLabel` | `cancelText` |
| `confirmLabel` | `actionText` |
| `confirmVariant` | **nothing** — see below |

⚠️ `confirmVariant` has no replacement and its message says so plainly rather
than pointing at a key that does not do the same job: `cancelText` / `actionText`
are the footer's two *labels*, not a variant, and the node declares no variant
key at all (the confirm button is `AlertDialogAction`, which ships one fixed
`buttonVariants()` style). Whether that button should be styleable from metadata
is a separate question needing its own card.

**Nothing else moves.** These spellings are overloaded across the tree and every
other owner is a live key on a different declaration —
`FormSchema.cancelLabel`, `objectql.ts`'s `confirmLabel`, `plugin-designer`'s
`ConfirmDialog` React props, `plugin-grid`'s `def.confirmLabel`, and
`plugin-form`'s `ModalForm` / `DrawerForm`, which build a local `cancelLabel`
*from* `schema.cancelText`. None is an `AlertDialogSchema`; none is touched, and
a pin asserts it. No fixture, catalog schema, example app or doc fence authored
any of the three on an `alert-dialog` node, so no shipped document is stranded.
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,41 @@
* `confirmVariant` row demonstrably distinguishes one button variant from
* another on this very DOM.
*
* ⛔ This file measures. It does not retire anything, it does not teach the
* renderer a new key, and it does not animate `confirmVariant` — whether a
* footer button variant is a capability this project wants is the maintainer
* ruling objectui#7963 asks for, and this reading is that ruling's INPUT.
* ⛔ This file measures. It does not teach the renderer a new key and it does not
* animate `confirmVariant`.
*
* ## The ruling this reading fed, and why this file is KEPT
*
* ⭐ RE-POINTED, ⛔ not deleted. The maintainer ruled on 2026-09-10: retire all
* three from `AlertDialogSchema`, both faces, ADR-0049 enforce-or-remove, with
* `cancelText` / `actionText` as the surviving spellings and NO survivor for
* `confirmVariant`. This file is the measurement that ruling was taken on — a
* retirement does not retire its own evidence, so every reading below stays,
* and every one of them must go on reading the same after the change as before.
*
* ⚠️ Read what did and did not move, because it is easy to get backwards:
*
* - the RENDERER is untouched. It never read the three keys and still does not,
* so the DOM readings below are unchanged BY CONSTRUCTION. If one of them
* ever flips, something taught the renderer a retired key.
* - the SCHEMA changed, and this package does not validate. `SchemaRenderer`
* renders a node; it does not `safeParse` one. So the "renders an EMPTY
* footer" reading at the bottom is STILL TRUE of a raw node handed straight
* to the renderer — what the retirement moved is the gate one step earlier,
* where an author is now refused BY NAME instead of silently drawing nothing.
* That is asserted at the bottom of this file against the mirror, so the two
* halves of the card are tied together in one place; its full contract lives
* in `packages/types/src/__tests__/alert-dialog-footer-keys-refusal-7963.test.ts`.
*/

import { describe, expect, it } from 'vitest';
import { cleanup, render } from '@testing-library/react';
import React from 'react';
import { SchemaRenderer } from '@object-ui/react';
// The schema half of this card. Imported for the closure leg at the bottom
// only — nothing above validates, and that asymmetry is the point of the note
// in this file's header.
import { AlertDialogSchema as AlertDialogMirror } from '@object-ui/types/zod';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout` (objectui#3010/#3021).
import '../renderers';
Expand Down Expand Up @@ -283,28 +308,57 @@ describe('objectui#7963 — the three declared footer keys, one varied per fixtu
});

/* ────────────────────────────────────────────────────────────────────────────
* The user-visible consequence the card reported
* The user-visible consequence the card reported — and where it is caught now
* ───────────────────────────────────────────────────────────────────────── */

describe('objectui#7963 — a document written strictly against the declared keys', () => {
it('renders an EMPTY footer', () => {
/** The footer an author used to write against the three keys the type declared. */
const RETIRED_ONLY = {
type: 'alert-dialog',
title: 'Delete this account?',
trigger: { type: 'button', label: 'Delete account' },
cancelLabel: 'Keep it',
confirmLabel: 'Delete',
confirmVariant: 'destructive',
defaultOpen: true,
};

describe('objectui#7963 — a document written strictly against the three retired keys', () => {
it('STILL renders an EMPTY footer — the renderer does not validate, and it did not change', () => {
// The card's headline claim, measured rather than reasoned: an author who
// writes only what `AlertDialogSchema` declares for the footer gets no
// footer buttons at all. Paired with `WIRED` above, which is the same node
// in the read dialect drawing two.
const declaredOnly = {
type: 'alert-dialog',
title: 'Delete this account?',
trigger: { type: 'button', label: 'Delete account' },
cancelLabel: 'Keep it',
confirmLabel: 'Delete',
confirmVariant: 'destructive',
defaultOpen: true,
};

const reading = probe(declaredOnly);
// wrote only what `AlertDialogSchema` used to declare for the footer got no
// footer buttons at all. Paired with `WIRED` above, which is the same node in
// the surviving dialect drawing two.
//
// ⚠️ This reading is UNCHANGED by the retirement, and that is the point.
// `SchemaRenderer` renders a node, it never `safeParse`s one, so a raw node
// reaching this renderer still draws nothing. The retirement did not repair
// the render — it moved the failure one step earlier, to a place where the
// author is told why (the leg below).
const reading = probe(RETIRED_ONLY);

expect(reading.dialogHtml).not.toBeNull(); // the dialog itself DID mount
expect(reading.footerLabels).toEqual([]); // …with nothing in its footer
});

it('and is now REFUSED BY NAME at the schema, instead of being accepted in silence', () => {
// The closure objectui#7963 landed, asserted here beside the reading that
// justified it so the two halves cannot drift apart. Before the retirement
// this document parsed GREEN — `BaseSchemaCore` ends `.passthrough()`, so an
// authored value was KEPT, not refused, which is why a bare deletion of the
// declarations would have changed nothing an author could see.
const result = AlertDialogMirror.safeParse(RETIRED_ONLY);

expect(result.success).toBe(false);
const paths = (result.success ? [] : result.error.issues).map((issue) => issue.path.join('.'));
for (const key of ['cancelLabel', 'confirmLabel', 'confirmVariant']) {
expect(paths, key).toContain(key);
}
});

it('CONTROL — the surviving dialect, the node `WIRED` draws two buttons from, parses green', () => {
// Without this the leg above would pass just as well against a mirror that
// refused every alert-dialog document, and the refusal would read as working
// while it had in fact taken the whole node down.
expect(AlertDialogMirror.safeParse(baseNode()).success).toBe(true);
});
});
Loading
Loading