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
36 changes: 36 additions & 0 deletions .changeset/6940-rowactions-boolean-mirror.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@object-ui/types': patch
---

`DataTableSchema.rowActions` validates as the boolean it has always been declared to be
(objectui#6940, maintainer ruling 2026-09-02, director seat summon #8, option A).

The hand-written zod mirror in `zod/data-display.zod.ts` declared
`rowActions: z.array(z.any()).optional()`. Every other face of the same key says
**boolean**: the TS declaration it mirrors (`rowActions?: boolean`), the renderer's
destructuring default (`rowActions = false`), its two truthiness gates and two
`colSpan` arithmetic sites, the registered authoring input
(`{ type: 'boolean', label: 'Show Row Actions' }`), `defaultProps: { rowActions: true }`,
and the renderer's own docblock example, which authors `"rowActions": true`. The mirror
was the single outlier — and the published one, so `safeValidateSchema` refused the
exact spelling the component's documentation, defaults and authoring UI all teach. Two
shipped `examples/schema-catalog` entries (`user-table.json`, `full-featured-table.json`)
failed validation for this and no other reason; both now validate **unchanged**.

**Patch, not minor or major, and the reasoning is the ruling's own:** no author can have
relied on an array value. The renderer never reads the array — it only truthiness-tests
the key — so the smallest zod-valid array, `[]`, rendered the actions column identically
to `true` (objectui#6318 measured both at 42 elements with the `Actions` header present,
against 39 with the key absent). An array authored here could therefore never have
carried meaning to any consumer: it either behaved exactly like `true` or, if empty,
still behaved exactly like `true`. Narrowing it takes away a spelling that was accepted
but inert, not one anything could have depended on.

A `boolean | array` union was considered and **not** taken: it would permanently accept
a shape the renderer cannot act on, which is the same second de-facto contract that the
array spelling already was.

The list view's same-named `rowActions` in `zod/objectql.zod.ts` — `z.array(z.string())`,
the legacy bare-name action list on `ObjectGridSchema` — is a **different key** that is
correct as it stands, is in parity with its own TS twin (`rowActions?: string[]`), and is
not touched.
124 changes: 124 additions & 0 deletions packages/types/src/__tests__/data-table-declared-keys-6882.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,12 @@
*/
import { describe, it, expect } from 'vitest';
import type { DataTableSchema } from '../data-display.js';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { DataTableSchema as DataTableMirror } from '../zod/data-display.zod.js';
import { ObjectGridSchema as ObjectGridMirror } from '../zod/objectql.zod.js';
import { safeValidateSchema } from '../zod/index.zod.js';

/**
* `T` with its string/number index signatures removed — the same shape
Expand DownExpand Up@@ -168,3 +174,121 @@ describe('objectui#6882 — DataTableSchema declares the two keys data-table rea
expect(typeof authored.renderCellEditor).toBe('function');
});
});

/* ══════════════════════════════════════════════════════════════════════════
* objectui#6940 — `DataTableSchema.rowActions` is a BOOLEAN on the mirror too
* ══════════════════════════════════════════════════════════════════════════
*
* Maintainer ruling 2026-09-02 (director seat, summon #8, verbatim
* 「7189 A 其他同意」), option A: the zod mirror in `../zod/data-display.zod.ts`
* becomes `z.boolean().optional()`, aligned with the TS declaration
* (`rowActions?: boolean`), the renderer's destructuring default
* (`rowActions = false`), the registered input (`type: 'boolean'`),
* `defaultProps` and the docblock example. Option B (a `boolean | array` union)
* was NOT taken: it would permanently accept a shape the renderer only
* truthiness-tests.
*
* ## Why the REFUSAL is the load-bearing half
*
* This is a NARROWING. A mirror that accepted both `true` and `[]` would
* satisfy a "`true` validates" assertion on its own — that assertion was green
* BEFORE this change for the array spelling and would stay green after a
* union. So the pin that carries the ruling's meaning is
* `_rowActionsArrayIsRefused` below, and it asserts not merely that the parse
* fails but that EVERY issue it raises is ON `rowActions` — a document refused
* for some unrelated reason would otherwise read as a passing narrowing pin.
*
* ## ⚠️ Two different keys are named `rowActions`
*
* `ObjectGridSchema.rowActions` (`../zod/objectql.zod.ts`, TS twin
* `../objectql.ts` `interface ObjectGridSchema`) is `z.array(z.string())` — the
* legacy bare-NAME action list, a genuinely different key that is correct as it
* stands and is NOT touched by this ruling. The last test below pins that
* separation, so a later sweep that "harmonises the two `rowActions`" turns red
* here instead of silently retyping a key no ruling covers.
*/

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');

/** The two entries the ruling names — they must validate UNCHANGED. */
const CATALOG_FIXTURES = [
'examples/schema-catalog/src/schemas/components-complex-data-table/user-table.json',
'examples/schema-catalog/src/schemas/components-complex-data-table/full-featured-table.json',
].map((rel) => ({ rel, abs: path.join(REPO_ROOT, rel) }));

/** A minimal document that is valid except for whatever `rowActions` is set to. */
const baseDoc = {
type: 'data-table',
columns: [{ header: 'Name', accessorKey: 'name' }],
data: [] as unknown[],
};

describe('objectui#6940 — the `rowActions` mirror is the declared boolean', () => {
it('`rowActions: true` validates — the spelling the renderer, inputs and docs all teach', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: true });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('`rowActions: false` validates too — the key is a boolean, not a truthy-only flag', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: false });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('⭐ `rowActions: []` is REFUSED, and refused ON `rowActions`', () => {
// `[]` was the SMALLEST value the pre-#6940 mirror accepted, and #6318
// measured that it renders the actions column identically to `true`
// (because `[]` is truthy) — so it made documents say something the
// renderer cannot act on. Narrowing is the whole point of the ruling;
// this is where that is proved.
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: [] });
expect(parsed.success, '`rowActions: []` still validates — the mirror did not narrow').toBe(false);

if (!parsed.success) {
const paths = parsed.error.issues.map((issue) => issue.path.join('.'));
// Every issue must be about `rowActions`. Without this, a document
// rejected for an unrelated reason would satisfy the assertion above.
expect(paths, `refused, but not on rowActions: ${JSON.stringify(paths)}`).toEqual(['rowActions']);
}
});

it('the published `safeValidateSchema` surface moves with it, in both directions', () => {
// The ruling is stated about THIS entry point ("changes what
// `safeValidateSchema` accepts on a published package"), and it is a
// `z.union` — so the refusal has to be measured here too rather than
// inferred from the member mirror: a sibling union member accepting the
// document would leave the published surface unchanged.
expect(safeValidateSchema({ ...baseDoc, rowActions: true }).success).toBe(true);
expect(safeValidateSchema({ ...baseDoc, rowActions: [] }).success).toBe(false);
});

it('the two schema-catalog entries this card was filed over are on disk', () => {
// Asserted before anything reads them: a path that silently resolved to
// nothing would make the next test a vacuous pass.
for (const { rel, abs } of CATALOG_FIXTURES) {
expect(fs.existsSync(abs), `fixture not found at ${rel}`).toBe(true);
}
});

it('…and both validate UNCHANGED — they author `rowActions: true` and always did', () => {
for (const { rel, abs } of CATALOG_FIXTURES) {
const doc = JSON.parse(fs.readFileSync(abs, 'utf8')) as Record<string, unknown>;
expect(doc.rowActions, `${rel} no longer authors the boolean this pin was written for`).toBe(true);

const parsed = safeValidateSchema(doc);
expect(parsed.success ? null : parsed.error.issues, `${rel} does not validate`).toBe(null);
}
});

it('the list view’s same-named `rowActions` is a DIFFERENT key and still takes `string[]`', () => {
// `ObjectGridSchema.rowActions` is the legacy bare-NAME action list. The
// ruling leaves it alone, and the mirror-parity ratchet agrees it is in
// parity with its TS twin (`rowActions?: string[]`) — it appears in
// NEITHER of that file's drift ledgers. Pinned here so the two keys are not
// later "harmonised" on the strength of sharing a name.
const grid = { type: 'object-grid', objectName: 'accounts', rowActions: ['edit', 'delete'] };
expect(ObjectGridMirror.safeParse(grid).success).toBe(true);

// …and the boolean this card installs on the OTHER key is not valid here.
expect(ObjectGridMirror.safeParse({ ...grid, rowActions: true }).success).toBe(false);
});
});
22 changes: 16 additions & 6 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@
* already pins equal to `keyof Declared`. Nothing asserts it against a written
* number, so this line is prose and can rot; the pin that cannot is the one
* comparing the two halves to each other.
* - **39 entries** in `KnownDrift`, **56 keys** across them. It was 12 / 17 until
* - **39 entries** in `KnownDrift`, **55 keys** across them — 56 until objectui#6940
* REPAIRED `DataTableSchema.rowActions` (the entry kept its other four keys, so
* the entry count did not move). It was 12 / 17 until
* objectui#6124 added the RUNTIME-SLOT class (28 pairs touched, 35 keys) — see
* the class note inside the ledger, above `ButtonSchema` — 36 / 52 until
* objectui#6576 minted `ObjectDataTableSchema` with one such arm (`onRowClick`),
Expand DownExpand Up@@ -726,10 +728,18 @@ interface KnownDrift {
*/
'crud.zod.ts#DetailSchema': 'onBack';
/**
* `rowActions` — DISJOINT: TS declares `rowActions?: boolean` (show the column or
* not), the mirror declares `any[]` (the actions themselves). One of the two is
* dead; which is a ruling. (`selectable` was a second drifted key here until
* objectui#5927 widened the mirror to `boolean | 'single' | 'multiple'` —
* `rowActions` was the FIFTH key here until objectui#6940 settled the ruling
* this entry was explicitly waiting on. It read: DISJOINT — TS declares
* `rowActions?: boolean` (show the column or not), the mirror declared
* `any[]` (the actions themselves); one of the two is dead, which is a
* ruling. The maintainer ruled the TS side live (2026-09-02, director seat
* summon #8, option A): the renderer only truthiness-tests the key, so the
* `any[]` face was the dead one, and the mirror became
* `z.boolean().optional()`. The pair is now IN PARITY on that key, so it left
* this entry — this ledger fails on a repair exactly as it fails on new
* drift, which is why correcting this line was part of that change and not
* optional. (`selectable` was likewise a drifted key here until objectui#5927
* widened the mirror to `boolean | 'single' | 'multiple'` —
* `resolveSelectionMode` in `renderers/complex/data-table.tsx` implements
* `'single'` as a real mode.)
*
Expand All@@ -738,7 +748,7 @@ interface KnownDrift {
* `schema.onSelectionChange(selectedData)`, …), so the TS side keeps them callable
* and the mirror refuses them by name.
*/
'data-display.zod.ts#DataTableSchema': 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
'data-display.zod.ts#DataTableSchema': 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
/** RUNTIME SLOT (objectui#6124): the `accordion` renderer spreads leftover props onto the Radix `Accordion` root, where `onValueChange` is a real prop. */
'disclosure.zod.ts#AccordionSchema': 'onValueChange';
/** RUNTIME SLOT (objectui#6124): the `collapsible` renderer spreads leftover props onto the Radix `Collapsible` root. */
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -261,7 +261,7 @@ export const DataTableSchema = BaseSchema.extend({
selectable: z.union([z.boolean(), z.enum(['single', 'multiple'])]).optional().describe('Enable row selection — `true`/`multiple` = multi-select, `single` = replace-on-select with no select-all'),
sortable: z.boolean().optional().describe('Enable sorting'),
exportable: z.boolean().optional().describe('Enable data export'),
rowActions: z.array(z.any()).optional().describe('Row action buttons'),
rowActions: z.boolean().optional().describe('Show the row actions column (edit/delete) — mirrors the boolean the renderer truthiness-tests (objectui#6940)'),
resizableColumns: z.boolean().optional().describe('Allow column resizing'),
reorderableColumns: z.boolean().optional().describe('Allow column reordering'),
onRowEdit: handlerKeyRefusal('onRowEdit', 'runtime-slot', 'Row edit handler'),
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
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
36 changes: 36 additions & 0 deletions .changeset/6940-rowactions-boolean-mirror.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@object-ui/types': patch
---

`DataTableSchema.rowActions` validates as the boolean it has always been declared to be
(objectui#6940, maintainer ruling 2026-09-02, director seat summon #8, option A).

The hand-written zod mirror in `zod/data-display.zod.ts` declared
`rowActions: z.array(z.any()).optional()`. Every other face of the same key says
**boolean**: the TS declaration it mirrors (`rowActions?: boolean`), the renderer's
destructuring default (`rowActions = false`), its two truthiness gates and two
`colSpan` arithmetic sites, the registered authoring input
(`{ type: 'boolean', label: 'Show Row Actions' }`), `defaultProps: { rowActions: true }`,
and the renderer's own docblock example, which authors `"rowActions": true`. The mirror
was the single outlier — and the published one, so `safeValidateSchema` refused the
exact spelling the component's documentation, defaults and authoring UI all teach. Two
shipped `examples/schema-catalog` entries (`user-table.json`, `full-featured-table.json`)
failed validation for this and no other reason; both now validate **unchanged**.

**Patch, not minor or major, and the reasoning is the ruling's own:** no author can have
relied on an array value. The renderer never reads the array — it only truthiness-tests
the key — so the smallest zod-valid array, `[]`, rendered the actions column identically
to `true` (objectui#6318 measured both at 42 elements with the `Actions` header present,
against 39 with the key absent). An array authored here could therefore never have
carried meaning to any consumer: it either behaved exactly like `true` or, if empty,
still behaved exactly like `true`. Narrowing it takes away a spelling that was accepted
but inert, not one anything could have depended on.

A `boolean | array` union was considered and **not** taken: it would permanently accept
a shape the renderer cannot act on, which is the same second de-facto contract that the
array spelling already was.

The list view's same-named `rowActions` in `zod/objectql.zod.ts` — `z.array(z.string())`,
the legacy bare-name action list on `ObjectGridSchema` — is a **different key** that is
correct as it stands, is in parity with its own TS twin (`rowActions?: string[]`), and is
not touched.
124 changes: 124 additions & 0 deletions packages/types/src/__tests__/data-table-declared-keys-6882.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,12 @@
*/
import { describe, it, expect } from 'vitest';
import type { DataTableSchema } from '../data-display.js';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { DataTableSchema as DataTableMirror } from '../zod/data-display.zod.js';
import { ObjectGridSchema as ObjectGridMirror } from '../zod/objectql.zod.js';
import { safeValidateSchema } from '../zod/index.zod.js';

/**
* `T` with its string/number index signatures removed — the same shape
Expand DownExpand Up@@ -168,3 +174,121 @@ describe('objectui#6882 — DataTableSchema declares the two keys data-table rea
expect(typeof authored.renderCellEditor).toBe('function');
});
});

/* ══════════════════════════════════════════════════════════════════════════
* objectui#6940 — `DataTableSchema.rowActions` is a BOOLEAN on the mirror too
* ══════════════════════════════════════════════════════════════════════════
*
* Maintainer ruling 2026-09-02 (director seat, summon #8, verbatim
* 「7189 A 其他同意」), option A: the zod mirror in `../zod/data-display.zod.ts`
* becomes `z.boolean().optional()`, aligned with the TS declaration
* (`rowActions?: boolean`), the renderer's destructuring default
* (`rowActions = false`), the registered input (`type: 'boolean'`),
* `defaultProps` and the docblock example. Option B (a `boolean | array` union)
* was NOT taken: it would permanently accept a shape the renderer only
* truthiness-tests.
*
* ## Why the REFUSAL is the load-bearing half
*
* This is a NARROWING. A mirror that accepted both `true` and `[]` would
* satisfy a "`true` validates" assertion on its own — that assertion was green
* BEFORE this change for the array spelling and would stay green after a
* union. So the pin that carries the ruling's meaning is
* `_rowActionsArrayIsRefused` below, and it asserts not merely that the parse
* fails but that EVERY issue it raises is ON `rowActions` — a document refused
* for some unrelated reason would otherwise read as a passing narrowing pin.
*
* ## ⚠️ Two different keys are named `rowActions`
*
* `ObjectGridSchema.rowActions` (`../zod/objectql.zod.ts`, TS twin
* `../objectql.ts` `interface ObjectGridSchema`) is `z.array(z.string())` — the
* legacy bare-NAME action list, a genuinely different key that is correct as it
* stands and is NOT touched by this ruling. The last test below pins that
* separation, so a later sweep that "harmonises the two `rowActions`" turns red
* here instead of silently retyping a key no ruling covers.
*/

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');

/** The two entries the ruling names — they must validate UNCHANGED. */
const CATALOG_FIXTURES = [
'examples/schema-catalog/src/schemas/components-complex-data-table/user-table.json',
'examples/schema-catalog/src/schemas/components-complex-data-table/full-featured-table.json',
].map((rel) => ({ rel, abs: path.join(REPO_ROOT, rel) }));

/** A minimal document that is valid except for whatever `rowActions` is set to. */
const baseDoc = {
type: 'data-table',
columns: [{ header: 'Name', accessorKey: 'name' }],
data: [] as unknown[],
};

describe('objectui#6940 — the `rowActions` mirror is the declared boolean', () => {
it('`rowActions: true` validates — the spelling the renderer, inputs and docs all teach', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: true });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('`rowActions: false` validates too — the key is a boolean, not a truthy-only flag', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: false });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('⭐ `rowActions: []` is REFUSED, and refused ON `rowActions`', () => {
// `[]` was the SMALLEST value the pre-#6940 mirror accepted, and #6318
// measured that it renders the actions column identically to `true`
// (because `[]` is truthy) — so it made documents say something the
// renderer cannot act on. Narrowing is the whole point of the ruling;
// this is where that is proved.
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: [] });
expect(parsed.success, '`rowActions: []` still validates — the mirror did not narrow').toBe(false);

if (!parsed.success) {
const paths = parsed.error.issues.map((issue) => issue.path.join('.'));
// Every issue must be about `rowActions`. Without this, a document
// rejected for an unrelated reason would satisfy the assertion above.
expect(paths, `refused, but not on rowActions: ${JSON.stringify(paths)}`).toEqual(['rowActions']);
}
});

it('the published `safeValidateSchema` surface moves with it, in both directions', () => {
// The ruling is stated about THIS entry point ("changes what
// `safeValidateSchema` accepts on a published package"), and it is a
// `z.union` — so the refusal has to be measured here too rather than
// inferred from the member mirror: a sibling union member accepting the
// document would leave the published surface unchanged.
expect(safeValidateSchema({ ...baseDoc, rowActions: true }).success).toBe(true);
expect(safeValidateSchema({ ...baseDoc, rowActions: [] }).success).toBe(false);
});

it('the two schema-catalog entries this card was filed over are on disk', () => {
// Asserted before anything reads them: a path that silently resolved to
// nothing would make the next test a vacuous pass.
for (const { rel, abs } of CATALOG_FIXTURES) {
expect(fs.existsSync(abs), `fixture not found at ${rel}`).toBe(true);
}
});

it('…and both validate UNCHANGED — they author `rowActions: true` and always did', () => {
for (const { rel, abs } of CATALOG_FIXTURES) {
const doc = JSON.parse(fs.readFileSync(abs, 'utf8')) as Record<string, unknown>;
expect(doc.rowActions, `${rel} no longer authors the boolean this pin was written for`).toBe(true);

const parsed = safeValidateSchema(doc);
expect(parsed.success ? null : parsed.error.issues, `${rel} does not validate`).toBe(null);
}
});

it('the list view’s same-named `rowActions` is a DIFFERENT key and still takes `string[]`', () => {
// `ObjectGridSchema.rowActions` is the legacy bare-NAME action list. The
// ruling leaves it alone, and the mirror-parity ratchet agrees it is in
// parity with its TS twin (`rowActions?: string[]`) — it appears in
// NEITHER of that file's drift ledgers. Pinned here so the two keys are not
// later "harmonised" on the strength of sharing a name.
const grid = { type: 'object-grid', objectName: 'accounts', rowActions: ['edit', 'delete'] };
expect(ObjectGridMirror.safeParse(grid).success).toBe(true);

// …and the boolean this card installs on the OTHER key is not valid here.
expect(ObjectGridMirror.safeParse({ ...grid, rowActions: true }).success).toBe(false);
});
});
22 changes: 16 additions & 6 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@
* already pins equal to `keyof Declared`. Nothing asserts it against a written
* number, so this line is prose and can rot; the pin that cannot is the one
* comparing the two halves to each other.
* - **39 entries** in `KnownDrift`, **56 keys** across them. It was 12 / 17 until
* - **39 entries** in `KnownDrift`, **55 keys** across them — 56 until objectui#6940
* REPAIRED `DataTableSchema.rowActions` (the entry kept its other four keys, so
* the entry count did not move). It was 12 / 17 until
* objectui#6124 added the RUNTIME-SLOT class (28 pairs touched, 35 keys) — see
* the class note inside the ledger, above `ButtonSchema` — 36 / 52 until
* objectui#6576 minted `ObjectDataTableSchema` with one such arm (`onRowClick`),
Expand DownExpand Up@@ -726,10 +728,18 @@ interface KnownDrift {
*/
'crud.zod.ts#DetailSchema': 'onBack';
/**
* `rowActions` — DISJOINT: TS declares `rowActions?: boolean` (show the column or
* not), the mirror declares `any[]` (the actions themselves). One of the two is
* dead; which is a ruling. (`selectable` was a second drifted key here until
* objectui#5927 widened the mirror to `boolean | 'single' | 'multiple'` —
* `rowActions` was the FIFTH key here until objectui#6940 settled the ruling
* this entry was explicitly waiting on. It read: DISJOINT — TS declares
* `rowActions?: boolean` (show the column or not), the mirror declared
* `any[]` (the actions themselves); one of the two is dead, which is a
* ruling. The maintainer ruled the TS side live (2026-09-02, director seat
* summon #8, option A): the renderer only truthiness-tests the key, so the
* `any[]` face was the dead one, and the mirror became
* `z.boolean().optional()`. The pair is now IN PARITY on that key, so it left
* this entry — this ledger fails on a repair exactly as it fails on new
* drift, which is why correcting this line was part of that change and not
* optional. (`selectable` was likewise a drifted key here until objectui#5927
* widened the mirror to `boolean | 'single' | 'multiple'` —
* `resolveSelectionMode` in `renderers/complex/data-table.tsx` implements
* `'single'` as a real mode.)
*
Expand All@@ -738,7 +748,7 @@ interface KnownDrift {
* `schema.onSelectionChange(selectedData)`, …), so the TS side keeps them callable
* and the mirror refuses them by name.
*/
'data-display.zod.ts#DataTableSchema': 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
'data-display.zod.ts#DataTableSchema': 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
/** RUNTIME SLOT (objectui#6124): the `accordion` renderer spreads leftover props onto the Radix `Accordion` root, where `onValueChange` is a real prop. */
'disclosure.zod.ts#AccordionSchema': 'onValueChange';
/** RUNTIME SLOT (objectui#6124): the `collapsible` renderer spreads leftover props onto the Radix `Collapsible` root. */
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -261,7 +261,7 @@ export const DataTableSchema = BaseSchema.extend({
selectable: z.union([z.boolean(), z.enum(['single', 'multiple'])]).optional().describe('Enable row selection — `true`/`multiple` = multi-select, `single` = replace-on-select with no select-all'),
sortable: z.boolean().optional().describe('Enable sorting'),
exportable: z.boolean().optional().describe('Enable data export'),
rowActions: z.array(z.any()).optional().describe('Row action buttons'),
rowActions: z.boolean().optional().describe('Show the row actions column (edit/delete) — mirrors the boolean the renderer truthiness-tests (objectui#6940)'),
resizableColumns: z.boolean().optional().describe('Allow column resizing'),
reorderableColumns: z.boolean().optional().describe('Allow column reordering'),
onRowEdit: handlerKeyRefusal('onRowEdit', 'runtime-slot', 'Row edit handler'),
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
36 changes: 36 additions & 0 deletions .changeset/6940-rowactions-boolean-mirror.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@object-ui/types': patch
---

`DataTableSchema.rowActions` validates as the boolean it has always been declared to be
(objectui#6940, maintainer ruling 2026-09-02, director seat summon #8, option A).

The hand-written zod mirror in `zod/data-display.zod.ts` declared
`rowActions: z.array(z.any()).optional()`. Every other face of the same key says
**boolean**: the TS declaration it mirrors (`rowActions?: boolean`), the renderer's
destructuring default (`rowActions = false`), its two truthiness gates and two
`colSpan` arithmetic sites, the registered authoring input
(`{ type: 'boolean', label: 'Show Row Actions' }`), `defaultProps: { rowActions: true }`,
and the renderer's own docblock example, which authors `"rowActions": true`. The mirror
was the single outlier — and the published one, so `safeValidateSchema` refused the
exact spelling the component's documentation, defaults and authoring UI all teach. Two
shipped `examples/schema-catalog` entries (`user-table.json`, `full-featured-table.json`)
failed validation for this and no other reason; both now validate **unchanged**.

**Patch, not minor or major, and the reasoning is the ruling's own:** no author can have
relied on an array value. The renderer never reads the array — it only truthiness-tests
the key — so the smallest zod-valid array, `[]`, rendered the actions column identically
to `true` (objectui#6318 measured both at 42 elements with the `Actions` header present,
against 39 with the key absent). An array authored here could therefore never have
carried meaning to any consumer: it either behaved exactly like `true` or, if empty,
still behaved exactly like `true`. Narrowing it takes away a spelling that was accepted
but inert, not one anything could have depended on.

A `boolean | array` union was considered and **not** taken: it would permanently accept
a shape the renderer cannot act on, which is the same second de-facto contract that the
array spelling already was.

The list view's same-named `rowActions` in `zod/objectql.zod.ts` — `z.array(z.string())`,
the legacy bare-name action list on `ObjectGridSchema` — is a **different key** that is
correct as it stands, is in parity with its own TS twin (`rowActions?: string[]`), and is
not touched.
124 changes: 124 additions & 0 deletions packages/types/src/__tests__/data-table-declared-keys-6882.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,12 @@
*/
import { describe, it, expect } from 'vitest';
import type { DataTableSchema } from '../data-display.js';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { DataTableSchema as DataTableMirror } from '../zod/data-display.zod.js';
import { ObjectGridSchema as ObjectGridMirror } from '../zod/objectql.zod.js';
import { safeValidateSchema } from '../zod/index.zod.js';

/**
* `T` with its string/number index signatures removed — the same shape
Expand DownExpand Up@@ -168,3 +174,121 @@ describe('objectui#6882 — DataTableSchema declares the two keys data-table rea
expect(typeof authored.renderCellEditor).toBe('function');
});
});

/* ══════════════════════════════════════════════════════════════════════════
* objectui#6940 — `DataTableSchema.rowActions` is a BOOLEAN on the mirror too
* ══════════════════════════════════════════════════════════════════════════
*
* Maintainer ruling 2026-09-02 (director seat, summon #8, verbatim
* 「7189 A 其他同意」), option A: the zod mirror in `../zod/data-display.zod.ts`
* becomes `z.boolean().optional()`, aligned with the TS declaration
* (`rowActions?: boolean`), the renderer's destructuring default
* (`rowActions = false`), the registered input (`type: 'boolean'`),
* `defaultProps` and the docblock example. Option B (a `boolean | array` union)
* was NOT taken: it would permanently accept a shape the renderer only
* truthiness-tests.
*
* ## Why the REFUSAL is the load-bearing half
*
* This is a NARROWING. A mirror that accepted both `true` and `[]` would
* satisfy a "`true` validates" assertion on its own — that assertion was green
* BEFORE this change for the array spelling and would stay green after a
* union. So the pin that carries the ruling's meaning is
* `_rowActionsArrayIsRefused` below, and it asserts not merely that the parse
* fails but that EVERY issue it raises is ON `rowActions` — a document refused
* for some unrelated reason would otherwise read as a passing narrowing pin.
*
* ## ⚠️ Two different keys are named `rowActions`
*
* `ObjectGridSchema.rowActions` (`../zod/objectql.zod.ts`, TS twin
* `../objectql.ts` `interface ObjectGridSchema`) is `z.array(z.string())` — the
* legacy bare-NAME action list, a genuinely different key that is correct as it
* stands and is NOT touched by this ruling. The last test below pins that
* separation, so a later sweep that "harmonises the two `rowActions`" turns red
* here instead of silently retyping a key no ruling covers.
*/

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');

/** The two entries the ruling names — they must validate UNCHANGED. */
const CATALOG_FIXTURES = [
'examples/schema-catalog/src/schemas/components-complex-data-table/user-table.json',
'examples/schema-catalog/src/schemas/components-complex-data-table/full-featured-table.json',
].map((rel) => ({ rel, abs: path.join(REPO_ROOT, rel) }));

/** A minimal document that is valid except for whatever `rowActions` is set to. */
const baseDoc = {
type: 'data-table',
columns: [{ header: 'Name', accessorKey: 'name' }],
data: [] as unknown[],
};

describe('objectui#6940 — the `rowActions` mirror is the declared boolean', () => {
it('`rowActions: true` validates — the spelling the renderer, inputs and docs all teach', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: true });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('`rowActions: false` validates too — the key is a boolean, not a truthy-only flag', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: false });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('⭐ `rowActions: []` is REFUSED, and refused ON `rowActions`', () => {
// `[]` was the SMALLEST value the pre-#6940 mirror accepted, and #6318
// measured that it renders the actions column identically to `true`
// (because `[]` is truthy) — so it made documents say something the
// renderer cannot act on. Narrowing is the whole point of the ruling;
// this is where that is proved.
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: [] });
expect(parsed.success, '`rowActions: []` still validates — the mirror did not narrow').toBe(false);

if (!parsed.success) {
const paths = parsed.error.issues.map((issue) => issue.path.join('.'));
// Every issue must be about `rowActions`. Without this, a document
// rejected for an unrelated reason would satisfy the assertion above.
expect(paths, `refused, but not on rowActions: ${JSON.stringify(paths)}`).toEqual(['rowActions']);
}
});

it('the published `safeValidateSchema` surface moves with it, in both directions', () => {
// The ruling is stated about THIS entry point ("changes what
// `safeValidateSchema` accepts on a published package"), and it is a
// `z.union` — so the refusal has to be measured here too rather than
// inferred from the member mirror: a sibling union member accepting the
// document would leave the published surface unchanged.
expect(safeValidateSchema({ ...baseDoc, rowActions: true }).success).toBe(true);
expect(safeValidateSchema({ ...baseDoc, rowActions: [] }).success).toBe(false);
});

it('the two schema-catalog entries this card was filed over are on disk', () => {
// Asserted before anything reads them: a path that silently resolved to
// nothing would make the next test a vacuous pass.
for (const { rel, abs } of CATALOG_FIXTURES) {
expect(fs.existsSync(abs), `fixture not found at ${rel}`).toBe(true);
}
});

it('…and both validate UNCHANGED — they author `rowActions: true` and always did', () => {
for (const { rel, abs } of CATALOG_FIXTURES) {
const doc = JSON.parse(fs.readFileSync(abs, 'utf8')) as Record<string, unknown>;
expect(doc.rowActions, `${rel} no longer authors the boolean this pin was written for`).toBe(true);

const parsed = safeValidateSchema(doc);
expect(parsed.success ? null : parsed.error.issues, `${rel} does not validate`).toBe(null);
}
});

it('the list view’s same-named `rowActions` is a DIFFERENT key and still takes `string[]`', () => {
// `ObjectGridSchema.rowActions` is the legacy bare-NAME action list. The
// ruling leaves it alone, and the mirror-parity ratchet agrees it is in
// parity with its TS twin (`rowActions?: string[]`) — it appears in
// NEITHER of that file's drift ledgers. Pinned here so the two keys are not
// later "harmonised" on the strength of sharing a name.
const grid = { type: 'object-grid', objectName: 'accounts', rowActions: ['edit', 'delete'] };
expect(ObjectGridMirror.safeParse(grid).success).toBe(true);

// …and the boolean this card installs on the OTHER key is not valid here.
expect(ObjectGridMirror.safeParse({ ...grid, rowActions: true }).success).toBe(false);
});
});
22 changes: 16 additions & 6 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@
* already pins equal to `keyof Declared`. Nothing asserts it against a written
* number, so this line is prose and can rot; the pin that cannot is the one
* comparing the two halves to each other.
* - **39 entries** in `KnownDrift`, **56 keys** across them. It was 12 / 17 until
* - **39 entries** in `KnownDrift`, **55 keys** across them — 56 until objectui#6940
* REPAIRED `DataTableSchema.rowActions` (the entry kept its other four keys, so
* the entry count did not move). It was 12 / 17 until
* objectui#6124 added the RUNTIME-SLOT class (28 pairs touched, 35 keys) — see
* the class note inside the ledger, above `ButtonSchema` — 36 / 52 until
* objectui#6576 minted `ObjectDataTableSchema` with one such arm (`onRowClick`),
Expand DownExpand Up@@ -726,10 +728,18 @@ interface KnownDrift {
*/
'crud.zod.ts#DetailSchema': 'onBack';
/**
* `rowActions` — DISJOINT: TS declares `rowActions?: boolean` (show the column or
* not), the mirror declares `any[]` (the actions themselves). One of the two is
* dead; which is a ruling. (`selectable` was a second drifted key here until
* objectui#5927 widened the mirror to `boolean | 'single' | 'multiple'` —
* `rowActions` was the FIFTH key here until objectui#6940 settled the ruling
* this entry was explicitly waiting on. It read: DISJOINT — TS declares
* `rowActions?: boolean` (show the column or not), the mirror declared
* `any[]` (the actions themselves); one of the two is dead, which is a
* ruling. The maintainer ruled the TS side live (2026-09-02, director seat
* summon #8, option A): the renderer only truthiness-tests the key, so the
* `any[]` face was the dead one, and the mirror became
* `z.boolean().optional()`. The pair is now IN PARITY on that key, so it left
* this entry — this ledger fails on a repair exactly as it fails on new
* drift, which is why correcting this line was part of that change and not
* optional. (`selectable` was likewise a drifted key here until objectui#5927
* widened the mirror to `boolean | 'single' | 'multiple'` —
* `resolveSelectionMode` in `renderers/complex/data-table.tsx` implements
* `'single'` as a real mode.)
*
Expand All@@ -738,7 +748,7 @@ interface KnownDrift {
* `schema.onSelectionChange(selectedData)`, …), so the TS side keeps them callable
* and the mirror refuses them by name.
*/
'data-display.zod.ts#DataTableSchema': 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
'data-display.zod.ts#DataTableSchema': 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
/** RUNTIME SLOT (objectui#6124): the `accordion` renderer spreads leftover props onto the Radix `Accordion` root, where `onValueChange` is a real prop. */
'disclosure.zod.ts#AccordionSchema': 'onValueChange';
/** RUNTIME SLOT (objectui#6124): the `collapsible` renderer spreads leftover props onto the Radix `Collapsible` root. */
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -261,7 +261,7 @@ export const DataTableSchema = BaseSchema.extend({
selectable: z.union([z.boolean(), z.enum(['single', 'multiple'])]).optional().describe('Enable row selection — `true`/`multiple` = multi-select, `single` = replace-on-select with no select-all'),
sortable: z.boolean().optional().describe('Enable sorting'),
exportable: z.boolean().optional().describe('Enable data export'),
rowActions: z.array(z.any()).optional().describe('Row action buttons'),
rowActions: z.boolean().optional().describe('Show the row actions column (edit/delete) — mirrors the boolean the renderer truthiness-tests (objectui#6940)'),
resizableColumns: z.boolean().optional().describe('Allow column resizing'),
reorderableColumns: z.boolean().optional().describe('Allow column reordering'),
onRowEdit: handlerKeyRefusal('onRowEdit', 'runtime-slot', 'Row edit handler'),
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
36 changes: 36 additions & 0 deletions .changeset/6940-rowactions-boolean-mirror.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@object-ui/types': patch
---

`DataTableSchema.rowActions` validates as the boolean it has always been declared to be
(objectui#6940, maintainer ruling 2026-09-02, director seat summon #8, option A).

The hand-written zod mirror in `zod/data-display.zod.ts` declared
`rowActions: z.array(z.any()).optional()`. Every other face of the same key says
**boolean**: the TS declaration it mirrors (`rowActions?: boolean`), the renderer's
destructuring default (`rowActions = false`), its two truthiness gates and two
`colSpan` arithmetic sites, the registered authoring input
(`{ type: 'boolean', label: 'Show Row Actions' }`), `defaultProps: { rowActions: true }`,
and the renderer's own docblock example, which authors `"rowActions": true`. The mirror
was the single outlier — and the published one, so `safeValidateSchema` refused the
exact spelling the component's documentation, defaults and authoring UI all teach. Two
shipped `examples/schema-catalog` entries (`user-table.json`, `full-featured-table.json`)
failed validation for this and no other reason; both now validate **unchanged**.

**Patch, not minor or major, and the reasoning is the ruling's own:** no author can have
relied on an array value. The renderer never reads the array — it only truthiness-tests
the key — so the smallest zod-valid array, `[]`, rendered the actions column identically
to `true` (objectui#6318 measured both at 42 elements with the `Actions` header present,
against 39 with the key absent). An array authored here could therefore never have
carried meaning to any consumer: it either behaved exactly like `true` or, if empty,
still behaved exactly like `true`. Narrowing it takes away a spelling that was accepted
but inert, not one anything could have depended on.

A `boolean | array` union was considered and **not** taken: it would permanently accept
a shape the renderer cannot act on, which is the same second de-facto contract that the
array spelling already was.

The list view's same-named `rowActions` in `zod/objectql.zod.ts` — `z.array(z.string())`,
the legacy bare-name action list on `ObjectGridSchema` — is a **different key** that is
correct as it stands, is in parity with its own TS twin (`rowActions?: string[]`), and is
not touched.
124 changes: 124 additions & 0 deletions packages/types/src/__tests__/data-table-declared-keys-6882.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,12 @@
*/
import { describe, it, expect } from 'vitest';
import type { DataTableSchema } from '../data-display.js';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { DataTableSchema as DataTableMirror } from '../zod/data-display.zod.js';
import { ObjectGridSchema as ObjectGridMirror } from '../zod/objectql.zod.js';
import { safeValidateSchema } from '../zod/index.zod.js';

/**
* `T` with its string/number index signatures removed — the same shape
Expand DownExpand Up@@ -168,3 +174,121 @@ describe('objectui#6882 — DataTableSchema declares the two keys data-table rea
expect(typeof authored.renderCellEditor).toBe('function');
});
});

/* ══════════════════════════════════════════════════════════════════════════
* objectui#6940 — `DataTableSchema.rowActions` is a BOOLEAN on the mirror too
* ══════════════════════════════════════════════════════════════════════════
*
* Maintainer ruling 2026-09-02 (director seat, summon #8, verbatim
* 「7189 A 其他同意」), option A: the zod mirror in `../zod/data-display.zod.ts`
* becomes `z.boolean().optional()`, aligned with the TS declaration
* (`rowActions?: boolean`), the renderer's destructuring default
* (`rowActions = false`), the registered input (`type: 'boolean'`),
* `defaultProps` and the docblock example. Option B (a `boolean | array` union)
* was NOT taken: it would permanently accept a shape the renderer only
* truthiness-tests.
*
* ## Why the REFUSAL is the load-bearing half
*
* This is a NARROWING. A mirror that accepted both `true` and `[]` would
* satisfy a "`true` validates" assertion on its own — that assertion was green
* BEFORE this change for the array spelling and would stay green after a
* union. So the pin that carries the ruling's meaning is
* `_rowActionsArrayIsRefused` below, and it asserts not merely that the parse
* fails but that EVERY issue it raises is ON `rowActions` — a document refused
* for some unrelated reason would otherwise read as a passing narrowing pin.
*
* ## ⚠️ Two different keys are named `rowActions`
*
* `ObjectGridSchema.rowActions` (`../zod/objectql.zod.ts`, TS twin
* `../objectql.ts` `interface ObjectGridSchema`) is `z.array(z.string())` — the
* legacy bare-NAME action list, a genuinely different key that is correct as it
* stands and is NOT touched by this ruling. The last test below pins that
* separation, so a later sweep that "harmonises the two `rowActions`" turns red
* here instead of silently retyping a key no ruling covers.
*/

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');

/** The two entries the ruling names — they must validate UNCHANGED. */
const CATALOG_FIXTURES = [
'examples/schema-catalog/src/schemas/components-complex-data-table/user-table.json',
'examples/schema-catalog/src/schemas/components-complex-data-table/full-featured-table.json',
].map((rel) => ({ rel, abs: path.join(REPO_ROOT, rel) }));

/** A minimal document that is valid except for whatever `rowActions` is set to. */
const baseDoc = {
type: 'data-table',
columns: [{ header: 'Name', accessorKey: 'name' }],
data: [] as unknown[],
};

describe('objectui#6940 — the `rowActions` mirror is the declared boolean', () => {
it('`rowActions: true` validates — the spelling the renderer, inputs and docs all teach', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: true });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('`rowActions: false` validates too — the key is a boolean, not a truthy-only flag', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: false });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('⭐ `rowActions: []` is REFUSED, and refused ON `rowActions`', () => {
// `[]` was the SMALLEST value the pre-#6940 mirror accepted, and #6318
// measured that it renders the actions column identically to `true`
// (because `[]` is truthy) — so it made documents say something the
// renderer cannot act on. Narrowing is the whole point of the ruling;
// this is where that is proved.
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: [] });
expect(parsed.success, '`rowActions: []` still validates — the mirror did not narrow').toBe(false);

if (!parsed.success) {
const paths = parsed.error.issues.map((issue) => issue.path.join('.'));
// Every issue must be about `rowActions`. Without this, a document
// rejected for an unrelated reason would satisfy the assertion above.
expect(paths, `refused, but not on rowActions: ${JSON.stringify(paths)}`).toEqual(['rowActions']);
}
});

it('the published `safeValidateSchema` surface moves with it, in both directions', () => {
// The ruling is stated about THIS entry point ("changes what
// `safeValidateSchema` accepts on a published package"), and it is a
// `z.union` — so the refusal has to be measured here too rather than
// inferred from the member mirror: a sibling union member accepting the
// document would leave the published surface unchanged.
expect(safeValidateSchema({ ...baseDoc, rowActions: true }).success).toBe(true);
expect(safeValidateSchema({ ...baseDoc, rowActions: [] }).success).toBe(false);
});

it('the two schema-catalog entries this card was filed over are on disk', () => {
// Asserted before anything reads them: a path that silently resolved to
// nothing would make the next test a vacuous pass.
for (const { rel, abs } of CATALOG_FIXTURES) {
expect(fs.existsSync(abs), `fixture not found at ${rel}`).toBe(true);
}
});

it('…and both validate UNCHANGED — they author `rowActions: true` and always did', () => {
for (const { rel, abs } of CATALOG_FIXTURES) {
const doc = JSON.parse(fs.readFileSync(abs, 'utf8')) as Record<string, unknown>;
expect(doc.rowActions, `${rel} no longer authors the boolean this pin was written for`).toBe(true);

const parsed = safeValidateSchema(doc);
expect(parsed.success ? null : parsed.error.issues, `${rel} does not validate`).toBe(null);
}
});

it('the list view’s same-named `rowActions` is a DIFFERENT key and still takes `string[]`', () => {
// `ObjectGridSchema.rowActions` is the legacy bare-NAME action list. The
// ruling leaves it alone, and the mirror-parity ratchet agrees it is in
// parity with its TS twin (`rowActions?: string[]`) — it appears in
// NEITHER of that file's drift ledgers. Pinned here so the two keys are not
// later "harmonised" on the strength of sharing a name.
const grid = { type: 'object-grid', objectName: 'accounts', rowActions: ['edit', 'delete'] };
expect(ObjectGridMirror.safeParse(grid).success).toBe(true);

// …and the boolean this card installs on the OTHER key is not valid here.
expect(ObjectGridMirror.safeParse({ ...grid, rowActions: true }).success).toBe(false);
});
});
22 changes: 16 additions & 6 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@
* already pins equal to `keyof Declared`. Nothing asserts it against a written
* number, so this line is prose and can rot; the pin that cannot is the one
* comparing the two halves to each other.
* - **39 entries** in `KnownDrift`, **56 keys** across them. It was 12 / 17 until
* - **39 entries** in `KnownDrift`, **55 keys** across them — 56 until objectui#6940
* REPAIRED `DataTableSchema.rowActions` (the entry kept its other four keys, so
* the entry count did not move). It was 12 / 17 until
* objectui#6124 added the RUNTIME-SLOT class (28 pairs touched, 35 keys) — see
* the class note inside the ledger, above `ButtonSchema` — 36 / 52 until
* objectui#6576 minted `ObjectDataTableSchema` with one such arm (`onRowClick`),
Expand DownExpand Up@@ -726,10 +728,18 @@ interface KnownDrift {
*/
'crud.zod.ts#DetailSchema': 'onBack';
/**
* `rowActions` — DISJOINT: TS declares `rowActions?: boolean` (show the column or
* not), the mirror declares `any[]` (the actions themselves). One of the two is
* dead; which is a ruling. (`selectable` was a second drifted key here until
* objectui#5927 widened the mirror to `boolean | 'single' | 'multiple'` —
* `rowActions` was the FIFTH key here until objectui#6940 settled the ruling
* this entry was explicitly waiting on. It read: DISJOINT — TS declares
* `rowActions?: boolean` (show the column or not), the mirror declared
* `any[]` (the actions themselves); one of the two is dead, which is a
* ruling. The maintainer ruled the TS side live (2026-09-02, director seat
* summon #8, option A): the renderer only truthiness-tests the key, so the
* `any[]` face was the dead one, and the mirror became
* `z.boolean().optional()`. The pair is now IN PARITY on that key, so it left
* this entry — this ledger fails on a repair exactly as it fails on new
* drift, which is why correcting this line was part of that change and not
* optional. (`selectable` was likewise a drifted key here until objectui#5927
* widened the mirror to `boolean | 'single' | 'multiple'` —
* `resolveSelectionMode` in `renderers/complex/data-table.tsx` implements
* `'single'` as a real mode.)
*
Expand All@@ -738,7 +748,7 @@ interface KnownDrift {
* `schema.onSelectionChange(selectedData)`, …), so the TS side keeps them callable
* and the mirror refuses them by name.
*/
'data-display.zod.ts#DataTableSchema': 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
'data-display.zod.ts#DataTableSchema': 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
/** RUNTIME SLOT (objectui#6124): the `accordion` renderer spreads leftover props onto the Radix `Accordion` root, where `onValueChange` is a real prop. */
'disclosure.zod.ts#AccordionSchema': 'onValueChange';
/** RUNTIME SLOT (objectui#6124): the `collapsible` renderer spreads leftover props onto the Radix `Collapsible` root. */
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -261,7 +261,7 @@ export const DataTableSchema = BaseSchema.extend({
selectable: z.union([z.boolean(), z.enum(['single', 'multiple'])]).optional().describe('Enable row selection — `true`/`multiple` = multi-select, `single` = replace-on-select with no select-all'),
sortable: z.boolean().optional().describe('Enable sorting'),
exportable: z.boolean().optional().describe('Enable data export'),
rowActions: z.array(z.any()).optional().describe('Row action buttons'),
rowActions: z.boolean().optional().describe('Show the row actions column (edit/delete) — mirrors the boolean the renderer truthiness-tests (objectui#6940)'),
resizableColumns: z.boolean().optional().describe('Allow column resizing'),
reorderableColumns: z.boolean().optional().describe('Allow column reordering'),
onRowEdit: handlerKeyRefusal('onRowEdit', 'runtime-slot', 'Row edit handler'),
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
36 changes: 36 additions & 0 deletions .changeset/6940-rowactions-boolean-mirror.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@object-ui/types': patch
---

`DataTableSchema.rowActions` validates as the boolean it has always been declared to be
(objectui#6940, maintainer ruling 2026-09-02, director seat summon #8, option A).

The hand-written zod mirror in `zod/data-display.zod.ts` declared
`rowActions: z.array(z.any()).optional()`. Every other face of the same key says
**boolean**: the TS declaration it mirrors (`rowActions?: boolean`), the renderer's
destructuring default (`rowActions = false`), its two truthiness gates and two
`colSpan` arithmetic sites, the registered authoring input
(`{ type: 'boolean', label: 'Show Row Actions' }`), `defaultProps: { rowActions: true }`,
and the renderer's own docblock example, which authors `"rowActions": true`. The mirror
was the single outlier — and the published one, so `safeValidateSchema` refused the
exact spelling the component's documentation, defaults and authoring UI all teach. Two
shipped `examples/schema-catalog` entries (`user-table.json`, `full-featured-table.json`)
failed validation for this and no other reason; both now validate **unchanged**.

**Patch, not minor or major, and the reasoning is the ruling's own:** no author can have
relied on an array value. The renderer never reads the array — it only truthiness-tests
the key — so the smallest zod-valid array, `[]`, rendered the actions column identically
to `true` (objectui#6318 measured both at 42 elements with the `Actions` header present,
against 39 with the key absent). An array authored here could therefore never have
carried meaning to any consumer: it either behaved exactly like `true` or, if empty,
still behaved exactly like `true`. Narrowing it takes away a spelling that was accepted
but inert, not one anything could have depended on.

A `boolean | array` union was considered and **not** taken: it would permanently accept
a shape the renderer cannot act on, which is the same second de-facto contract that the
array spelling already was.

The list view's same-named `rowActions` in `zod/objectql.zod.ts` — `z.array(z.string())`,
the legacy bare-name action list on `ObjectGridSchema` — is a **different key** that is
correct as it stands, is in parity with its own TS twin (`rowActions?: string[]`), and is
not touched.
124 changes: 124 additions & 0 deletions packages/types/src/__tests__/data-table-declared-keys-6882.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,12 @@
*/
import { describe, it, expect } from 'vitest';
import type { DataTableSchema } from '../data-display.js';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { DataTableSchema as DataTableMirror } from '../zod/data-display.zod.js';
import { ObjectGridSchema as ObjectGridMirror } from '../zod/objectql.zod.js';
import { safeValidateSchema } from '../zod/index.zod.js';

/**
* `T` with its string/number index signatures removed — the same shape
Expand DownExpand Up@@ -168,3 +174,121 @@ describe('objectui#6882 — DataTableSchema declares the two keys data-table rea
expect(typeof authored.renderCellEditor).toBe('function');
});
});

/* ══════════════════════════════════════════════════════════════════════════
* objectui#6940 — `DataTableSchema.rowActions` is a BOOLEAN on the mirror too
* ══════════════════════════════════════════════════════════════════════════
*
* Maintainer ruling 2026-09-02 (director seat, summon #8, verbatim
* 「7189 A 其他同意」), option A: the zod mirror in `../zod/data-display.zod.ts`
* becomes `z.boolean().optional()`, aligned with the TS declaration
* (`rowActions?: boolean`), the renderer's destructuring default
* (`rowActions = false`), the registered input (`type: 'boolean'`),
* `defaultProps` and the docblock example. Option B (a `boolean | array` union)
* was NOT taken: it would permanently accept a shape the renderer only
* truthiness-tests.
*
* ## Why the REFUSAL is the load-bearing half
*
* This is a NARROWING. A mirror that accepted both `true` and `[]` would
* satisfy a "`true` validates" assertion on its own — that assertion was green
* BEFORE this change for the array spelling and would stay green after a
* union. So the pin that carries the ruling's meaning is
* `_rowActionsArrayIsRefused` below, and it asserts not merely that the parse
* fails but that EVERY issue it raises is ON `rowActions` — a document refused
* for some unrelated reason would otherwise read as a passing narrowing pin.
*
* ## ⚠️ Two different keys are named `rowActions`
*
* `ObjectGridSchema.rowActions` (`../zod/objectql.zod.ts`, TS twin
* `../objectql.ts` `interface ObjectGridSchema`) is `z.array(z.string())` — the
* legacy bare-NAME action list, a genuinely different key that is correct as it
* stands and is NOT touched by this ruling. The last test below pins that
* separation, so a later sweep that "harmonises the two `rowActions`" turns red
* here instead of silently retyping a key no ruling covers.
*/

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');

/** The two entries the ruling names — they must validate UNCHANGED. */
const CATALOG_FIXTURES = [
'examples/schema-catalog/src/schemas/components-complex-data-table/user-table.json',
'examples/schema-catalog/src/schemas/components-complex-data-table/full-featured-table.json',
].map((rel) => ({ rel, abs: path.join(REPO_ROOT, rel) }));

/** A minimal document that is valid except for whatever `rowActions` is set to. */
const baseDoc = {
type: 'data-table',
columns: [{ header: 'Name', accessorKey: 'name' }],
data: [] as unknown[],
};

describe('objectui#6940 — the `rowActions` mirror is the declared boolean', () => {
it('`rowActions: true` validates — the spelling the renderer, inputs and docs all teach', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: true });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('`rowActions: false` validates too — the key is a boolean, not a truthy-only flag', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: false });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('⭐ `rowActions: []` is REFUSED, and refused ON `rowActions`', () => {
// `[]` was the SMALLEST value the pre-#6940 mirror accepted, and #6318
// measured that it renders the actions column identically to `true`
// (because `[]` is truthy) — so it made documents say something the
// renderer cannot act on. Narrowing is the whole point of the ruling;
// this is where that is proved.
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: [] });
expect(parsed.success, '`rowActions: []` still validates — the mirror did not narrow').toBe(false);

if (!parsed.success) {
const paths = parsed.error.issues.map((issue) => issue.path.join('.'));
// Every issue must be about `rowActions`. Without this, a document
// rejected for an unrelated reason would satisfy the assertion above.
expect(paths, `refused, but not on rowActions: ${JSON.stringify(paths)}`).toEqual(['rowActions']);
}
});

it('the published `safeValidateSchema` surface moves with it, in both directions', () => {
// The ruling is stated about THIS entry point ("changes what
// `safeValidateSchema` accepts on a published package"), and it is a
// `z.union` — so the refusal has to be measured here too rather than
// inferred from the member mirror: a sibling union member accepting the
// document would leave the published surface unchanged.
expect(safeValidateSchema({ ...baseDoc, rowActions: true }).success).toBe(true);
expect(safeValidateSchema({ ...baseDoc, rowActions: [] }).success).toBe(false);
});

it('the two schema-catalog entries this card was filed over are on disk', () => {
// Asserted before anything reads them: a path that silently resolved to
// nothing would make the next test a vacuous pass.
for (const { rel, abs } of CATALOG_FIXTURES) {
expect(fs.existsSync(abs), `fixture not found at ${rel}`).toBe(true);
}
});

it('…and both validate UNCHANGED — they author `rowActions: true` and always did', () => {
for (const { rel, abs } of CATALOG_FIXTURES) {
const doc = JSON.parse(fs.readFileSync(abs, 'utf8')) as Record<string, unknown>;
expect(doc.rowActions, `${rel} no longer authors the boolean this pin was written for`).toBe(true);

const parsed = safeValidateSchema(doc);
expect(parsed.success ? null : parsed.error.issues, `${rel} does not validate`).toBe(null);
}
});

it('the list view’s same-named `rowActions` is a DIFFERENT key and still takes `string[]`', () => {
// `ObjectGridSchema.rowActions` is the legacy bare-NAME action list. The
// ruling leaves it alone, and the mirror-parity ratchet agrees it is in
// parity with its TS twin (`rowActions?: string[]`) — it appears in
// NEITHER of that file's drift ledgers. Pinned here so the two keys are not
// later "harmonised" on the strength of sharing a name.
const grid = { type: 'object-grid', objectName: 'accounts', rowActions: ['edit', 'delete'] };
expect(ObjectGridMirror.safeParse(grid).success).toBe(true);

// …and the boolean this card installs on the OTHER key is not valid here.
expect(ObjectGridMirror.safeParse({ ...grid, rowActions: true }).success).toBe(false);
});
});
22 changes: 16 additions & 6 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@
* already pins equal to `keyof Declared`. Nothing asserts it against a written
* number, so this line is prose and can rot; the pin that cannot is the one
* comparing the two halves to each other.
* - **39 entries** in `KnownDrift`, **56 keys** across them. It was 12 / 17 until
* - **39 entries** in `KnownDrift`, **55 keys** across them — 56 until objectui#6940
* REPAIRED `DataTableSchema.rowActions` (the entry kept its other four keys, so
* the entry count did not move). It was 12 / 17 until
* objectui#6124 added the RUNTIME-SLOT class (28 pairs touched, 35 keys) — see
* the class note inside the ledger, above `ButtonSchema` — 36 / 52 until
* objectui#6576 minted `ObjectDataTableSchema` with one such arm (`onRowClick`),
Expand DownExpand Up@@ -726,10 +728,18 @@ interface KnownDrift {
*/
'crud.zod.ts#DetailSchema': 'onBack';
/**
* `rowActions` — DISJOINT: TS declares `rowActions?: boolean` (show the column or
* not), the mirror declares `any[]` (the actions themselves). One of the two is
* dead; which is a ruling. (`selectable` was a second drifted key here until
* objectui#5927 widened the mirror to `boolean | 'single' | 'multiple'` —
* `rowActions` was the FIFTH key here until objectui#6940 settled the ruling
* this entry was explicitly waiting on. It read: DISJOINT — TS declares
* `rowActions?: boolean` (show the column or not), the mirror declared
* `any[]` (the actions themselves); one of the two is dead, which is a
* ruling. The maintainer ruled the TS side live (2026-09-02, director seat
* summon #8, option A): the renderer only truthiness-tests the key, so the
* `any[]` face was the dead one, and the mirror became
* `z.boolean().optional()`. The pair is now IN PARITY on that key, so it left
* this entry — this ledger fails on a repair exactly as it fails on new
* drift, which is why correcting this line was part of that change and not
* optional. (`selectable` was likewise a drifted key here until objectui#5927
* widened the mirror to `boolean | 'single' | 'multiple'` —
* `resolveSelectionMode` in `renderers/complex/data-table.tsx` implements
* `'single'` as a real mode.)
*
Expand All@@ -738,7 +748,7 @@ interface KnownDrift {
* `schema.onSelectionChange(selectedData)`, …), so the TS side keeps them callable
* and the mirror refuses them by name.
*/
'data-display.zod.ts#DataTableSchema': 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
'data-display.zod.ts#DataTableSchema': 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
/** RUNTIME SLOT (objectui#6124): the `accordion` renderer spreads leftover props onto the Radix `Accordion` root, where `onValueChange` is a real prop. */
'disclosure.zod.ts#AccordionSchema': 'onValueChange';
/** RUNTIME SLOT (objectui#6124): the `collapsible` renderer spreads leftover props onto the Radix `Collapsible` root. */
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -261,7 +261,7 @@ export const DataTableSchema = BaseSchema.extend({
selectable: z.union([z.boolean(), z.enum(['single', 'multiple'])]).optional().describe('Enable row selection — `true`/`multiple` = multi-select, `single` = replace-on-select with no select-all'),
sortable: z.boolean().optional().describe('Enable sorting'),
exportable: z.boolean().optional().describe('Enable data export'),
rowActions: z.array(z.any()).optional().describe('Row action buttons'),
rowActions: z.boolean().optional().describe('Show the row actions column (edit/delete) — mirrors the boolean the renderer truthiness-tests (objectui#6940)'),
resizableColumns: z.boolean().optional().describe('Allow column resizing'),
reorderableColumns: z.boolean().optional().describe('Allow column reordering'),
onRowEdit: handlerKeyRefusal('onRowEdit', 'runtime-slot', 'Row edit handler'),
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
36 changes: 36 additions & 0 deletions .changeset/6940-rowactions-boolean-mirror.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@object-ui/types': patch
---

`DataTableSchema.rowActions` validates as the boolean it has always been declared to be
(objectui#6940, maintainer ruling 2026-09-02, director seat summon #8, option A).

The hand-written zod mirror in `zod/data-display.zod.ts` declared
`rowActions: z.array(z.any()).optional()`. Every other face of the same key says
**boolean**: the TS declaration it mirrors (`rowActions?: boolean`), the renderer's
destructuring default (`rowActions = false`), its two truthiness gates and two
`colSpan` arithmetic sites, the registered authoring input
(`{ type: 'boolean', label: 'Show Row Actions' }`), `defaultProps: { rowActions: true }`,
and the renderer's own docblock example, which authors `"rowActions": true`. The mirror
was the single outlier — and the published one, so `safeValidateSchema` refused the
exact spelling the component's documentation, defaults and authoring UI all teach. Two
shipped `examples/schema-catalog` entries (`user-table.json`, `full-featured-table.json`)
failed validation for this and no other reason; both now validate **unchanged**.

**Patch, not minor or major, and the reasoning is the ruling's own:** no author can have
relied on an array value. The renderer never reads the array — it only truthiness-tests
the key — so the smallest zod-valid array, `[]`, rendered the actions column identically
to `true` (objectui#6318 measured both at 42 elements with the `Actions` header present,
against 39 with the key absent). An array authored here could therefore never have
carried meaning to any consumer: it either behaved exactly like `true` or, if empty,
still behaved exactly like `true`. Narrowing it takes away a spelling that was accepted
but inert, not one anything could have depended on.

A `boolean | array` union was considered and **not** taken: it would permanently accept
a shape the renderer cannot act on, which is the same second de-facto contract that the
array spelling already was.

The list view's same-named `rowActions` in `zod/objectql.zod.ts` — `z.array(z.string())`,
the legacy bare-name action list on `ObjectGridSchema` — is a **different key** that is
correct as it stands, is in parity with its own TS twin (`rowActions?: string[]`), and is
not touched.
124 changes: 124 additions & 0 deletions packages/types/src/__tests__/data-table-declared-keys-6882.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,12 @@
*/
import { describe, it, expect } from 'vitest';
import type { DataTableSchema } from '../data-display.js';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { DataTableSchema as DataTableMirror } from '../zod/data-display.zod.js';
import { ObjectGridSchema as ObjectGridMirror } from '../zod/objectql.zod.js';
import { safeValidateSchema } from '../zod/index.zod.js';

/**
* `T` with its string/number index signatures removed — the same shape
Expand DownExpand Up@@ -168,3 +174,121 @@ describe('objectui#6882 — DataTableSchema declares the two keys data-table rea
expect(typeof authored.renderCellEditor).toBe('function');
});
});

/* ══════════════════════════════════════════════════════════════════════════
* objectui#6940 — `DataTableSchema.rowActions` is a BOOLEAN on the mirror too
* ══════════════════════════════════════════════════════════════════════════
*
* Maintainer ruling 2026-09-02 (director seat, summon #8, verbatim
* 「7189 A 其他同意」), option A: the zod mirror in `../zod/data-display.zod.ts`
* becomes `z.boolean().optional()`, aligned with the TS declaration
* (`rowActions?: boolean`), the renderer's destructuring default
* (`rowActions = false`), the registered input (`type: 'boolean'`),
* `defaultProps` and the docblock example. Option B (a `boolean | array` union)
* was NOT taken: it would permanently accept a shape the renderer only
* truthiness-tests.
*
* ## Why the REFUSAL is the load-bearing half
*
* This is a NARROWING. A mirror that accepted both `true` and `[]` would
* satisfy a "`true` validates" assertion on its own — that assertion was green
* BEFORE this change for the array spelling and would stay green after a
* union. So the pin that carries the ruling's meaning is
* `_rowActionsArrayIsRefused` below, and it asserts not merely that the parse
* fails but that EVERY issue it raises is ON `rowActions` — a document refused
* for some unrelated reason would otherwise read as a passing narrowing pin.
*
* ## ⚠️ Two different keys are named `rowActions`
*
* `ObjectGridSchema.rowActions` (`../zod/objectql.zod.ts`, TS twin
* `../objectql.ts` `interface ObjectGridSchema`) is `z.array(z.string())` — the
* legacy bare-NAME action list, a genuinely different key that is correct as it
* stands and is NOT touched by this ruling. The last test below pins that
* separation, so a later sweep that "harmonises the two `rowActions`" turns red
* here instead of silently retyping a key no ruling covers.
*/

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');

/** The two entries the ruling names — they must validate UNCHANGED. */
const CATALOG_FIXTURES = [
'examples/schema-catalog/src/schemas/components-complex-data-table/user-table.json',
'examples/schema-catalog/src/schemas/components-complex-data-table/full-featured-table.json',
].map((rel) => ({ rel, abs: path.join(REPO_ROOT, rel) }));

/** A minimal document that is valid except for whatever `rowActions` is set to. */
const baseDoc = {
type: 'data-table',
columns: [{ header: 'Name', accessorKey: 'name' }],
data: [] as unknown[],
};

describe('objectui#6940 — the `rowActions` mirror is the declared boolean', () => {
it('`rowActions: true` validates — the spelling the renderer, inputs and docs all teach', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: true });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('`rowActions: false` validates too — the key is a boolean, not a truthy-only flag', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: false });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('⭐ `rowActions: []` is REFUSED, and refused ON `rowActions`', () => {
// `[]` was the SMALLEST value the pre-#6940 mirror accepted, and #6318
// measured that it renders the actions column identically to `true`
// (because `[]` is truthy) — so it made documents say something the
// renderer cannot act on. Narrowing is the whole point of the ruling;
// this is where that is proved.
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: [] });
expect(parsed.success, '`rowActions: []` still validates — the mirror did not narrow').toBe(false);

if (!parsed.success) {
const paths = parsed.error.issues.map((issue) => issue.path.join('.'));
// Every issue must be about `rowActions`. Without this, a document
// rejected for an unrelated reason would satisfy the assertion above.
expect(paths, `refused, but not on rowActions: ${JSON.stringify(paths)}`).toEqual(['rowActions']);
}
});

it('the published `safeValidateSchema` surface moves with it, in both directions', () => {
// The ruling is stated about THIS entry point ("changes what
// `safeValidateSchema` accepts on a published package"), and it is a
// `z.union` — so the refusal has to be measured here too rather than
// inferred from the member mirror: a sibling union member accepting the
// document would leave the published surface unchanged.
expect(safeValidateSchema({ ...baseDoc, rowActions: true }).success).toBe(true);
expect(safeValidateSchema({ ...baseDoc, rowActions: [] }).success).toBe(false);
});

it('the two schema-catalog entries this card was filed over are on disk', () => {
// Asserted before anything reads them: a path that silently resolved to
// nothing would make the next test a vacuous pass.
for (const { rel, abs } of CATALOG_FIXTURES) {
expect(fs.existsSync(abs), `fixture not found at ${rel}`).toBe(true);
}
});

it('…and both validate UNCHANGED — they author `rowActions: true` and always did', () => {
for (const { rel, abs } of CATALOG_FIXTURES) {
const doc = JSON.parse(fs.readFileSync(abs, 'utf8')) as Record<string, unknown>;
expect(doc.rowActions, `${rel} no longer authors the boolean this pin was written for`).toBe(true);

const parsed = safeValidateSchema(doc);
expect(parsed.success ? null : parsed.error.issues, `${rel} does not validate`).toBe(null);
}
});

it('the list view’s same-named `rowActions` is a DIFFERENT key and still takes `string[]`', () => {
// `ObjectGridSchema.rowActions` is the legacy bare-NAME action list. The
// ruling leaves it alone, and the mirror-parity ratchet agrees it is in
// parity with its TS twin (`rowActions?: string[]`) — it appears in
// NEITHER of that file's drift ledgers. Pinned here so the two keys are not
// later "harmonised" on the strength of sharing a name.
const grid = { type: 'object-grid', objectName: 'accounts', rowActions: ['edit', 'delete'] };
expect(ObjectGridMirror.safeParse(grid).success).toBe(true);

// …and the boolean this card installs on the OTHER key is not valid here.
expect(ObjectGridMirror.safeParse({ ...grid, rowActions: true }).success).toBe(false);
});
});
22 changes: 16 additions & 6 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@
* already pins equal to `keyof Declared`. Nothing asserts it against a written
* number, so this line is prose and can rot; the pin that cannot is the one
* comparing the two halves to each other.
* - **39 entries** in `KnownDrift`, **56 keys** across them. It was 12 / 17 until
* - **39 entries** in `KnownDrift`, **55 keys** across them — 56 until objectui#6940
* REPAIRED `DataTableSchema.rowActions` (the entry kept its other four keys, so
* the entry count did not move). It was 12 / 17 until
* objectui#6124 added the RUNTIME-SLOT class (28 pairs touched, 35 keys) — see
* the class note inside the ledger, above `ButtonSchema` — 36 / 52 until
* objectui#6576 minted `ObjectDataTableSchema` with one such arm (`onRowClick`),
Expand DownExpand Up@@ -726,10 +728,18 @@ interface KnownDrift {
*/
'crud.zod.ts#DetailSchema': 'onBack';
/**
* `rowActions` — DISJOINT: TS declares `rowActions?: boolean` (show the column or
* not), the mirror declares `any[]` (the actions themselves). One of the two is
* dead; which is a ruling. (`selectable` was a second drifted key here until
* objectui#5927 widened the mirror to `boolean | 'single' | 'multiple'` —
* `rowActions` was the FIFTH key here until objectui#6940 settled the ruling
* this entry was explicitly waiting on. It read: DISJOINT — TS declares
* `rowActions?: boolean` (show the column or not), the mirror declared
* `any[]` (the actions themselves); one of the two is dead, which is a
* ruling. The maintainer ruled the TS side live (2026-09-02, director seat
* summon #8, option A): the renderer only truthiness-tests the key, so the
* `any[]` face was the dead one, and the mirror became
* `z.boolean().optional()`. The pair is now IN PARITY on that key, so it left
* this entry — this ledger fails on a repair exactly as it fails on new
* drift, which is why correcting this line was part of that change and not
* optional. (`selectable` was likewise a drifted key here until objectui#5927
* widened the mirror to `boolean | 'single' | 'multiple'` —
* `resolveSelectionMode` in `renderers/complex/data-table.tsx` implements
* `'single'` as a real mode.)
*
Expand All@@ -738,7 +748,7 @@ interface KnownDrift {
* `schema.onSelectionChange(selectedData)`, …), so the TS side keeps them callable
* and the mirror refuses them by name.
*/
'data-display.zod.ts#DataTableSchema': 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
'data-display.zod.ts#DataTableSchema': 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
/** RUNTIME SLOT (objectui#6124): the `accordion` renderer spreads leftover props onto the Radix `Accordion` root, where `onValueChange` is a real prop. */
'disclosure.zod.ts#AccordionSchema': 'onValueChange';
/** RUNTIME SLOT (objectui#6124): the `collapsible` renderer spreads leftover props onto the Radix `Collapsible` root. */
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -261,7 +261,7 @@ export const DataTableSchema = BaseSchema.extend({
selectable: z.union([z.boolean(), z.enum(['single', 'multiple'])]).optional().describe('Enable row selection — `true`/`multiple` = multi-select, `single` = replace-on-select with no select-all'),
sortable: z.boolean().optional().describe('Enable sorting'),
exportable: z.boolean().optional().describe('Enable data export'),
rowActions: z.array(z.any()).optional().describe('Row action buttons'),
rowActions: z.boolean().optional().describe('Show the row actions column (edit/delete) — mirrors the boolean the renderer truthiness-tests (objectui#6940)'),
resizableColumns: z.boolean().optional().describe('Allow column resizing'),
reorderableColumns: z.boolean().optional().describe('Allow column reordering'),
onRowEdit: handlerKeyRefusal('onRowEdit', 'runtime-slot', 'Row edit handler'),
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
36 changes: 36 additions & 0 deletions .changeset/6940-rowactions-boolean-mirror.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@object-ui/types': patch
---

`DataTableSchema.rowActions` validates as the boolean it has always been declared to be
(objectui#6940, maintainer ruling 2026-09-02, director seat summon #8, option A).

The hand-written zod mirror in `zod/data-display.zod.ts` declared
`rowActions: z.array(z.any()).optional()`. Every other face of the same key says
**boolean**: the TS declaration it mirrors (`rowActions?: boolean`), the renderer's
destructuring default (`rowActions = false`), its two truthiness gates and two
`colSpan` arithmetic sites, the registered authoring input
(`{ type: 'boolean', label: 'Show Row Actions' }`), `defaultProps: { rowActions: true }`,
and the renderer's own docblock example, which authors `"rowActions": true`. The mirror
was the single outlier — and the published one, so `safeValidateSchema` refused the
exact spelling the component's documentation, defaults and authoring UI all teach. Two
shipped `examples/schema-catalog` entries (`user-table.json`, `full-featured-table.json`)
failed validation for this and no other reason; both now validate **unchanged**.

**Patch, not minor or major, and the reasoning is the ruling's own:** no author can have
relied on an array value. The renderer never reads the array — it only truthiness-tests
the key — so the smallest zod-valid array, `[]`, rendered the actions column identically
to `true` (objectui#6318 measured both at 42 elements with the `Actions` header present,
against 39 with the key absent). An array authored here could therefore never have
carried meaning to any consumer: it either behaved exactly like `true` or, if empty,
still behaved exactly like `true`. Narrowing it takes away a spelling that was accepted
but inert, not one anything could have depended on.

A `boolean | array` union was considered and **not** taken: it would permanently accept
a shape the renderer cannot act on, which is the same second de-facto contract that the
array spelling already was.

The list view's same-named `rowActions` in `zod/objectql.zod.ts` — `z.array(z.string())`,
the legacy bare-name action list on `ObjectGridSchema` — is a **different key** that is
correct as it stands, is in parity with its own TS twin (`rowActions?: string[]`), and is
not touched.
124 changes: 124 additions & 0 deletions packages/types/src/__tests__/data-table-declared-keys-6882.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,12 @@
*/
import { describe, it, expect } from 'vitest';
import type { DataTableSchema } from '../data-display.js';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { DataTableSchema as DataTableMirror } from '../zod/data-display.zod.js';
import { ObjectGridSchema as ObjectGridMirror } from '../zod/objectql.zod.js';
import { safeValidateSchema } from '../zod/index.zod.js';

/**
* `T` with its string/number index signatures removed — the same shape
Expand DownExpand Up@@ -168,3 +174,121 @@ describe('objectui#6882 — DataTableSchema declares the two keys data-table rea
expect(typeof authored.renderCellEditor).toBe('function');
});
});

/* ══════════════════════════════════════════════════════════════════════════
* objectui#6940 — `DataTableSchema.rowActions` is a BOOLEAN on the mirror too
* ══════════════════════════════════════════════════════════════════════════
*
* Maintainer ruling 2026-09-02 (director seat, summon #8, verbatim
* 「7189 A 其他同意」), option A: the zod mirror in `../zod/data-display.zod.ts`
* becomes `z.boolean().optional()`, aligned with the TS declaration
* (`rowActions?: boolean`), the renderer's destructuring default
* (`rowActions = false`), the registered input (`type: 'boolean'`),
* `defaultProps` and the docblock example. Option B (a `boolean | array` union)
* was NOT taken: it would permanently accept a shape the renderer only
* truthiness-tests.
*
* ## Why the REFUSAL is the load-bearing half
*
* This is a NARROWING. A mirror that accepted both `true` and `[]` would
* satisfy a "`true` validates" assertion on its own — that assertion was green
* BEFORE this change for the array spelling and would stay green after a
* union. So the pin that carries the ruling's meaning is
* `_rowActionsArrayIsRefused` below, and it asserts not merely that the parse
* fails but that EVERY issue it raises is ON `rowActions` — a document refused
* for some unrelated reason would otherwise read as a passing narrowing pin.
*
* ## ⚠️ Two different keys are named `rowActions`
*
* `ObjectGridSchema.rowActions` (`../zod/objectql.zod.ts`, TS twin
* `../objectql.ts` `interface ObjectGridSchema`) is `z.array(z.string())` — the
* legacy bare-NAME action list, a genuinely different key that is correct as it
* stands and is NOT touched by this ruling. The last test below pins that
* separation, so a later sweep that "harmonises the two `rowActions`" turns red
* here instead of silently retyping a key no ruling covers.
*/

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');

/** The two entries the ruling names — they must validate UNCHANGED. */
const CATALOG_FIXTURES = [
'examples/schema-catalog/src/schemas/components-complex-data-table/user-table.json',
'examples/schema-catalog/src/schemas/components-complex-data-table/full-featured-table.json',
].map((rel) => ({ rel, abs: path.join(REPO_ROOT, rel) }));

/** A minimal document that is valid except for whatever `rowActions` is set to. */
const baseDoc = {
type: 'data-table',
columns: [{ header: 'Name', accessorKey: 'name' }],
data: [] as unknown[],
};

describe('objectui#6940 — the `rowActions` mirror is the declared boolean', () => {
it('`rowActions: true` validates — the spelling the renderer, inputs and docs all teach', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: true });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('`rowActions: false` validates too — the key is a boolean, not a truthy-only flag', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: false });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('⭐ `rowActions: []` is REFUSED, and refused ON `rowActions`', () => {
// `[]` was the SMALLEST value the pre-#6940 mirror accepted, and #6318
// measured that it renders the actions column identically to `true`
// (because `[]` is truthy) — so it made documents say something the
// renderer cannot act on. Narrowing is the whole point of the ruling;
// this is where that is proved.
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: [] });
expect(parsed.success, '`rowActions: []` still validates — the mirror did not narrow').toBe(false);

if (!parsed.success) {
const paths = parsed.error.issues.map((issue) => issue.path.join('.'));
// Every issue must be about `rowActions`. Without this, a document
// rejected for an unrelated reason would satisfy the assertion above.
expect(paths, `refused, but not on rowActions: ${JSON.stringify(paths)}`).toEqual(['rowActions']);
}
});

it('the published `safeValidateSchema` surface moves with it, in both directions', () => {
// The ruling is stated about THIS entry point ("changes what
// `safeValidateSchema` accepts on a published package"), and it is a
// `z.union` — so the refusal has to be measured here too rather than
// inferred from the member mirror: a sibling union member accepting the
// document would leave the published surface unchanged.
expect(safeValidateSchema({ ...baseDoc, rowActions: true }).success).toBe(true);
expect(safeValidateSchema({ ...baseDoc, rowActions: [] }).success).toBe(false);
});

it('the two schema-catalog entries this card was filed over are on disk', () => {
// Asserted before anything reads them: a path that silently resolved to
// nothing would make the next test a vacuous pass.
for (const { rel, abs } of CATALOG_FIXTURES) {
expect(fs.existsSync(abs), `fixture not found at ${rel}`).toBe(true);
}
});

it('…and both validate UNCHANGED — they author `rowActions: true` and always did', () => {
for (const { rel, abs } of CATALOG_FIXTURES) {
const doc = JSON.parse(fs.readFileSync(abs, 'utf8')) as Record<string, unknown>;
expect(doc.rowActions, `${rel} no longer authors the boolean this pin was written for`).toBe(true);

const parsed = safeValidateSchema(doc);
expect(parsed.success ? null : parsed.error.issues, `${rel} does not validate`).toBe(null);
}
});

it('the list view’s same-named `rowActions` is a DIFFERENT key and still takes `string[]`', () => {
// `ObjectGridSchema.rowActions` is the legacy bare-NAME action list. The
// ruling leaves it alone, and the mirror-parity ratchet agrees it is in
// parity with its TS twin (`rowActions?: string[]`) — it appears in
// NEITHER of that file's drift ledgers. Pinned here so the two keys are not
// later "harmonised" on the strength of sharing a name.
const grid = { type: 'object-grid', objectName: 'accounts', rowActions: ['edit', 'delete'] };
expect(ObjectGridMirror.safeParse(grid).success).toBe(true);

// …and the boolean this card installs on the OTHER key is not valid here.
expect(ObjectGridMirror.safeParse({ ...grid, rowActions: true }).success).toBe(false);
});
});
22 changes: 16 additions & 6 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@
* already pins equal to `keyof Declared`. Nothing asserts it against a written
* number, so this line is prose and can rot; the pin that cannot is the one
* comparing the two halves to each other.
* - **39 entries** in `KnownDrift`, **56 keys** across them. It was 12 / 17 until
* - **39 entries** in `KnownDrift`, **55 keys** across them — 56 until objectui#6940
* REPAIRED `DataTableSchema.rowActions` (the entry kept its other four keys, so
* the entry count did not move). It was 12 / 17 until
* objectui#6124 added the RUNTIME-SLOT class (28 pairs touched, 35 keys) — see
* the class note inside the ledger, above `ButtonSchema` — 36 / 52 until
* objectui#6576 minted `ObjectDataTableSchema` with one such arm (`onRowClick`),
Expand DownExpand Up@@ -726,10 +728,18 @@ interface KnownDrift {
*/
'crud.zod.ts#DetailSchema': 'onBack';
/**
* `rowActions` — DISJOINT: TS declares `rowActions?: boolean` (show the column or
* not), the mirror declares `any[]` (the actions themselves). One of the two is
* dead; which is a ruling. (`selectable` was a second drifted key here until
* objectui#5927 widened the mirror to `boolean | 'single' | 'multiple'` —
* `rowActions` was the FIFTH key here until objectui#6940 settled the ruling
* this entry was explicitly waiting on. It read: DISJOINT — TS declares
* `rowActions?: boolean` (show the column or not), the mirror declared
* `any[]` (the actions themselves); one of the two is dead, which is a
* ruling. The maintainer ruled the TS side live (2026-09-02, director seat
* summon #8, option A): the renderer only truthiness-tests the key, so the
* `any[]` face was the dead one, and the mirror became
* `z.boolean().optional()`. The pair is now IN PARITY on that key, so it left
* this entry — this ledger fails on a repair exactly as it fails on new
* drift, which is why correcting this line was part of that change and not
* optional. (`selectable` was likewise a drifted key here until objectui#5927
* widened the mirror to `boolean | 'single' | 'multiple'` —
* `resolveSelectionMode` in `renderers/complex/data-table.tsx` implements
* `'single'` as a real mode.)
*
Expand All@@ -738,7 +748,7 @@ interface KnownDrift {
* `schema.onSelectionChange(selectedData)`, …), so the TS side keeps them callable
* and the mirror refuses them by name.
*/
'data-display.zod.ts#DataTableSchema': 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
'data-display.zod.ts#DataTableSchema': 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
/** RUNTIME SLOT (objectui#6124): the `accordion` renderer spreads leftover props onto the Radix `Accordion` root, where `onValueChange` is a real prop. */
'disclosure.zod.ts#AccordionSchema': 'onValueChange';
/** RUNTIME SLOT (objectui#6124): the `collapsible` renderer spreads leftover props onto the Radix `Collapsible` root. */
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -261,7 +261,7 @@ export const DataTableSchema = BaseSchema.extend({
selectable: z.union([z.boolean(), z.enum(['single', 'multiple'])]).optional().describe('Enable row selection — `true`/`multiple` = multi-select, `single` = replace-on-select with no select-all'),
sortable: z.boolean().optional().describe('Enable sorting'),
exportable: z.boolean().optional().describe('Enable data export'),
rowActions: z.array(z.any()).optional().describe('Row action buttons'),
rowActions: z.boolean().optional().describe('Show the row actions column (edit/delete) — mirrors the boolean the renderer truthiness-tests (objectui#6940)'),
resizableColumns: z.boolean().optional().describe('Allow column resizing'),
reorderableColumns: z.boolean().optional().describe('Allow column reordering'),
onRowEdit: handlerKeyRefusal('onRowEdit', 'runtime-slot', 'Row edit handler'),
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
36 changes: 36 additions & 0 deletions .changeset/6940-rowactions-boolean-mirror.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@object-ui/types': patch
---

`DataTableSchema.rowActions` validates as the boolean it has always been declared to be
(objectui#6940, maintainer ruling 2026-09-02, director seat summon #8, option A).

The hand-written zod mirror in `zod/data-display.zod.ts` declared
`rowActions: z.array(z.any()).optional()`. Every other face of the same key says
**boolean**: the TS declaration it mirrors (`rowActions?: boolean`), the renderer's
destructuring default (`rowActions = false`), its two truthiness gates and two
`colSpan` arithmetic sites, the registered authoring input
(`{ type: 'boolean', label: 'Show Row Actions' }`), `defaultProps: { rowActions: true }`,
and the renderer's own docblock example, which authors `"rowActions": true`. The mirror
was the single outlier — and the published one, so `safeValidateSchema` refused the
exact spelling the component's documentation, defaults and authoring UI all teach. Two
shipped `examples/schema-catalog` entries (`user-table.json`, `full-featured-table.json`)
failed validation for this and no other reason; both now validate **unchanged**.

**Patch, not minor or major, and the reasoning is the ruling's own:** no author can have
relied on an array value. The renderer never reads the array — it only truthiness-tests
the key — so the smallest zod-valid array, `[]`, rendered the actions column identically
to `true` (objectui#6318 measured both at 42 elements with the `Actions` header present,
against 39 with the key absent). An array authored here could therefore never have
carried meaning to any consumer: it either behaved exactly like `true` or, if empty,
still behaved exactly like `true`. Narrowing it takes away a spelling that was accepted
but inert, not one anything could have depended on.

A `boolean | array` union was considered and **not** taken: it would permanently accept
a shape the renderer cannot act on, which is the same second de-facto contract that the
array spelling already was.

The list view's same-named `rowActions` in `zod/objectql.zod.ts` — `z.array(z.string())`,
the legacy bare-name action list on `ObjectGridSchema` — is a **different key** that is
correct as it stands, is in parity with its own TS twin (`rowActions?: string[]`), and is
not touched.
124 changes: 124 additions & 0 deletions packages/types/src/__tests__/data-table-declared-keys-6882.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,12 @@
*/
import { describe, it, expect } from 'vitest';
import type { DataTableSchema } from '../data-display.js';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { DataTableSchema as DataTableMirror } from '../zod/data-display.zod.js';
import { ObjectGridSchema as ObjectGridMirror } from '../zod/objectql.zod.js';
import { safeValidateSchema } from '../zod/index.zod.js';

/**
* `T` with its string/number index signatures removed — the same shape
Expand DownExpand Up@@ -168,3 +174,121 @@ describe('objectui#6882 — DataTableSchema declares the two keys data-table rea
expect(typeof authored.renderCellEditor).toBe('function');
});
});

/* ══════════════════════════════════════════════════════════════════════════
* objectui#6940 — `DataTableSchema.rowActions` is a BOOLEAN on the mirror too
* ══════════════════════════════════════════════════════════════════════════
*
* Maintainer ruling 2026-09-02 (director seat, summon #8, verbatim
* 「7189 A 其他同意」), option A: the zod mirror in `../zod/data-display.zod.ts`
* becomes `z.boolean().optional()`, aligned with the TS declaration
* (`rowActions?: boolean`), the renderer's destructuring default
* (`rowActions = false`), the registered input (`type: 'boolean'`),
* `defaultProps` and the docblock example. Option B (a `boolean | array` union)
* was NOT taken: it would permanently accept a shape the renderer only
* truthiness-tests.
*
* ## Why the REFUSAL is the load-bearing half
*
* This is a NARROWING. A mirror that accepted both `true` and `[]` would
* satisfy a "`true` validates" assertion on its own — that assertion was green
* BEFORE this change for the array spelling and would stay green after a
* union. So the pin that carries the ruling's meaning is
* `_rowActionsArrayIsRefused` below, and it asserts not merely that the parse
* fails but that EVERY issue it raises is ON `rowActions` — a document refused
* for some unrelated reason would otherwise read as a passing narrowing pin.
*
* ## ⚠️ Two different keys are named `rowActions`
*
* `ObjectGridSchema.rowActions` (`../zod/objectql.zod.ts`, TS twin
* `../objectql.ts` `interface ObjectGridSchema`) is `z.array(z.string())` — the
* legacy bare-NAME action list, a genuinely different key that is correct as it
* stands and is NOT touched by this ruling. The last test below pins that
* separation, so a later sweep that "harmonises the two `rowActions`" turns red
* here instead of silently retyping a key no ruling covers.
*/

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');

/** The two entries the ruling names — they must validate UNCHANGED. */
const CATALOG_FIXTURES = [
'examples/schema-catalog/src/schemas/components-complex-data-table/user-table.json',
'examples/schema-catalog/src/schemas/components-complex-data-table/full-featured-table.json',
].map((rel) => ({ rel, abs: path.join(REPO_ROOT, rel) }));

/** A minimal document that is valid except for whatever `rowActions` is set to. */
const baseDoc = {
type: 'data-table',
columns: [{ header: 'Name', accessorKey: 'name' }],
data: [] as unknown[],
};

describe('objectui#6940 — the `rowActions` mirror is the declared boolean', () => {
it('`rowActions: true` validates — the spelling the renderer, inputs and docs all teach', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: true });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('`rowActions: false` validates too — the key is a boolean, not a truthy-only flag', () => {
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: false });
expect(parsed.success ? null : parsed.error.issues).toBe(null);
});

it('⭐ `rowActions: []` is REFUSED, and refused ON `rowActions`', () => {
// `[]` was the SMALLEST value the pre-#6940 mirror accepted, and #6318
// measured that it renders the actions column identically to `true`
// (because `[]` is truthy) — so it made documents say something the
// renderer cannot act on. Narrowing is the whole point of the ruling;
// this is where that is proved.
const parsed = DataTableMirror.safeParse({ ...baseDoc, rowActions: [] });
expect(parsed.success, '`rowActions: []` still validates — the mirror did not narrow').toBe(false);

if (!parsed.success) {
const paths = parsed.error.issues.map((issue) => issue.path.join('.'));
// Every issue must be about `rowActions`. Without this, a document
// rejected for an unrelated reason would satisfy the assertion above.
expect(paths, `refused, but not on rowActions: ${JSON.stringify(paths)}`).toEqual(['rowActions']);
}
});

it('the published `safeValidateSchema` surface moves with it, in both directions', () => {
// The ruling is stated about THIS entry point ("changes what
// `safeValidateSchema` accepts on a published package"), and it is a
// `z.union` — so the refusal has to be measured here too rather than
// inferred from the member mirror: a sibling union member accepting the
// document would leave the published surface unchanged.
expect(safeValidateSchema({ ...baseDoc, rowActions: true }).success).toBe(true);
expect(safeValidateSchema({ ...baseDoc, rowActions: [] }).success).toBe(false);
});

it('the two schema-catalog entries this card was filed over are on disk', () => {
// Asserted before anything reads them: a path that silently resolved to
// nothing would make the next test a vacuous pass.
for (const { rel, abs } of CATALOG_FIXTURES) {
expect(fs.existsSync(abs), `fixture not found at ${rel}`).toBe(true);
}
});

it('…and both validate UNCHANGED — they author `rowActions: true` and always did', () => {
for (const { rel, abs } of CATALOG_FIXTURES) {
const doc = JSON.parse(fs.readFileSync(abs, 'utf8')) as Record<string, unknown>;
expect(doc.rowActions, `${rel} no longer authors the boolean this pin was written for`).toBe(true);

const parsed = safeValidateSchema(doc);
expect(parsed.success ? null : parsed.error.issues, `${rel} does not validate`).toBe(null);
}
});

it('the list view’s same-named `rowActions` is a DIFFERENT key and still takes `string[]`', () => {
// `ObjectGridSchema.rowActions` is the legacy bare-NAME action list. The
// ruling leaves it alone, and the mirror-parity ratchet agrees it is in
// parity with its TS twin (`rowActions?: string[]`) — it appears in
// NEITHER of that file's drift ledgers. Pinned here so the two keys are not
// later "harmonised" on the strength of sharing a name.
const grid = { type: 'object-grid', objectName: 'accounts', rowActions: ['edit', 'delete'] };
expect(ObjectGridMirror.safeParse(grid).success).toBe(true);

// …and the boolean this card installs on the OTHER key is not valid here.
expect(ObjectGridMirror.safeParse({ ...grid, rowActions: true }).success).toBe(false);
});
});
22 changes: 16 additions & 6 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@
* already pins equal to `keyof Declared`. Nothing asserts it against a written
* number, so this line is prose and can rot; the pin that cannot is the one
* comparing the two halves to each other.
* - **39 entries** in `KnownDrift`, **56 keys** across them. It was 12 / 17 until
* - **39 entries** in `KnownDrift`, **55 keys** across them — 56 until objectui#6940
* REPAIRED `DataTableSchema.rowActions` (the entry kept its other four keys, so
* the entry count did not move). It was 12 / 17 until
* objectui#6124 added the RUNTIME-SLOT class (28 pairs touched, 35 keys) — see
* the class note inside the ledger, above `ButtonSchema` — 36 / 52 until
* objectui#6576 minted `ObjectDataTableSchema` with one such arm (`onRowClick`),
Expand DownExpand Up@@ -726,10 +728,18 @@ interface KnownDrift {
*/
'crud.zod.ts#DetailSchema': 'onBack';
/**
* `rowActions` — DISJOINT: TS declares `rowActions?: boolean` (show the column or
* not), the mirror declares `any[]` (the actions themselves). One of the two is
* dead; which is a ruling. (`selectable` was a second drifted key here until
* objectui#5927 widened the mirror to `boolean | 'single' | 'multiple'` —
* `rowActions` was the FIFTH key here until objectui#6940 settled the ruling
* this entry was explicitly waiting on. It read: DISJOINT — TS declares
* `rowActions?: boolean` (show the column or not), the mirror declared
* `any[]` (the actions themselves); one of the two is dead, which is a
* ruling. The maintainer ruled the TS side live (2026-09-02, director seat
* summon #8, option A): the renderer only truthiness-tests the key, so the
* `any[]` face was the dead one, and the mirror became
* `z.boolean().optional()`. The pair is now IN PARITY on that key, so it left
* this entry — this ledger fails on a repair exactly as it fails on new
* drift, which is why correcting this line was part of that change and not
* optional. (`selectable` was likewise a drifted key here until objectui#5927
* widened the mirror to `boolean | 'single' | 'multiple'` —
* `resolveSelectionMode` in `renderers/complex/data-table.tsx` implements
* `'single'` as a real mode.)
*
Expand All@@ -738,7 +748,7 @@ interface KnownDrift {
* `schema.onSelectionChange(selectedData)`, …), so the TS side keeps them callable
* and the mirror refuses them by name.
*/
'data-display.zod.ts#DataTableSchema': 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
'data-display.zod.ts#DataTableSchema': 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder';
/** RUNTIME SLOT (objectui#6124): the `accordion` renderer spreads leftover props onto the Radix `Accordion` root, where `onValueChange` is a real prop. */
'disclosure.zod.ts#AccordionSchema': 'onValueChange';
/** RUNTIME SLOT (objectui#6124): the `collapsible` renderer spreads leftover props onto the Radix `Collapsible` root. */
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -261,7 +261,7 @@ export const DataTableSchema = BaseSchema.extend({
selectable: z.union([z.boolean(), z.enum(['single', 'multiple'])]).optional().describe('Enable row selection — `true`/`multiple` = multi-select, `single` = replace-on-select with no select-all'),
sortable: z.boolean().optional().describe('Enable sorting'),
exportable: z.boolean().optional().describe('Enable data export'),
rowActions: z.array(z.any()).optional().describe('Row action buttons'),
rowActions: z.boolean().optional().describe('Show the row actions column (edit/delete) — mirrors the boolean the renderer truthiness-tests (objectui#6940)'),
resizableColumns: z.boolean().optional().describe('Allow column resizing'),
reorderableColumns: z.boolean().optional().describe('Allow column reordering'),
onRowEdit: handlerKeyRefusal('onRowEdit', 'runtime-slot', 'Row edit handler'),
Expand Down
Loading