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
11 changes: 11 additions & 0 deletions .changeset/21017-quickadd-remedy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@objectstack/spec': patch
---

fix(spec): the `object-kanban` `quickAdd` retirement no longer sends authors to the `kanban-ui` block, which objectui does not register

Clause-②: no

- The refusal of `quickAdd` on `object-kanban` now ends "Delete the key; `object-kanban` offers no quick-add control." It used to say the control "is unchanged on the `kanban-ui` block". objectui retired that block (objectui#8257), so a node of that type saves clean and renders nothing. The refusal itself is unchanged: the same key is still refused, with the same code and path.
- The same sentence replaces the old one in the `os migrate meta --from 17` output (the `object-kanban-quick-add-retired` entry) and in the summary of the `object-kanban-quick-add-removed` conversion. That entry no longer offers "move the board to a host that renders the `kanban-ui` block" as a second way out.
- No author action beyond the existing one. `quickAdd: true` on an `object-kanban` is still a parse error. Delete the key.
2 changes: 1 addition & 1 deletion content/docs/references/ui/component.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,7 @@ Sort field and direction pair
| **cardFields** | `string[]` | optional | Fields rendered on each card |
| **swimlaneField** | `string` | optional | Field for horizontal swimlanes (in addition to columns) |
| **grouping** | `{ fields: object[] }` | optional | View grouping config; its first field is the swimlane fallback |
| **quickAdd** | `never` | optional | [REMOVED] `object-kanban` property `quickAdd` was removed in @objectstack/spec 17 (ADR-0049) — the board forwarded it, but the per-column affordance is gated on both `quickAdd` and `onQuickAdd`, and `onQuickAdd` is a host-supplied function JSON cannot carry and no producer ever put on an `object-kanban` node, so authoring it was a parse-clean no-op. Delete the key. The quick-add control is unchanged on the `kanban-ui` block, where a React host supplies the `onQuickAdd` slot the control needs. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
| **quickAdd** | `never` | optional | [REMOVED] `object-kanban` property `quickAdd` was removed in @objectstack/spec 17 (ADR-0049) — the board forwarded it, but the per-column affordance is gated on both `quickAdd` and `onQuickAdd`, and `onQuickAdd` is a host-supplied function JSON cannot carry and no producer ever put on an `object-kanban` node, so authoring it was a parse-clean no-op. Delete the key; `object-kanban` offers no quick-add control. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
| **coverImageField** | `string` | optional | Image field rendered as the card cover |
| **conditionalFormatting** | `any` | optional | Card conditional formatting rules |
| **navigation** | `{ mode: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation: boolean; openNewTab: boolean; size: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Card-click navigation config — the same block `ListViewSchema.navigation` declares (`{ mode, size, openNewTab, preventNavigation }`). The renderer's own default is `{ mode: 'drawer' }` when the key is absent; it is documented rather than declared, so a parsed board carries the key only when the author wrote it |
Expand Down
23 changes: 12 additions & 11 deletions packages/spec/src/conversions/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8929,9 +8929,11 @@ const objectGridDefaultSortRemoved: MetadataConversion = {
* `object-kanban`'s per-column quick-add switch leaves the contract (protocol
* 18, #17260, ADR-0049 enforce-or-remove; the spec half of the objectui#8285
* director-seat ruling, decision batch #91, 2026-09-08 — ruled option B,
* `quickAdd` is retired from the board and stays only on the `kanban-ui`
* block, where a React host can supply the runtime function the control
* needs).
* `quickAdd` is retired from the board. The ruling kept the control on the
* `kanban-ui` block, where a React host can supply the runtime function it
* needs; objectui has since retired that block (objectui#8257), so
* `object-kanban` offers no quick-add control and no block a document can
* name offers one either).
*
* **A pure lossless delete.** The key never had an effect to preserve.
* Measured at the `.objectui-sha` pin (`53ded82bf`): the board FORWARDS it —
Expand All @@ -8945,12 +8947,11 @@ const objectGridDefaultSortRemoved: MetadataConversion = {
* `onCardClick` in the same file). So the gate was permanently false and
* deleting the key preserves observed behaviour exactly.
*
* ⚠️ Scoped by component `type`, never by key name. `quickAdd` is LIVE on the
* `kanban-ui` block — the same renderer chain, reached by a React host that
* hands in `onQuickAdd` — and the ruling keeps it there deliberately. That
* block is objectui-side and is not a component type this spec declares, so no
* stack this walk reaches can carry it; the type scoping is what keeps the
* strip from generalising into a name-keyed one if it ever is declared. The
* ⚠️ Scoped by component `type`, never by key name, so the strip cannot
* generalise into a name-keyed one: a node of any other type that authors the
* same key is left alone. The `kanban-ui` block the ruling kept the control on
* is retired (objectui#8257) and was never a component type this spec
* declares, so no stack this walk reaches is expected to carry it. The
* fixture's non-carrier control is an `object-grid` authoring the same key
* name.
*
Expand All @@ -8973,7 +8974,7 @@ const objectKanbanQuickAddRemoved: MetadataConversion = {
summary:
"object-kanban component prop 'quickAdd' removed (#17260 — the affordance is gated on a "
+ "host-supplied 'onQuickAdd' function no producer puts on an object-kanban node, so the key "
+ "was accepted and dropped; the quick-add control stays on the React-host 'kanban-ui' block)",
+ "was accepted and dropped; delete the key — object-kanban offers no quick-add control)",
apply(stack, emit) {
return mapPageComponents(stack, (component, path) => {
if (component.type !== 'object-kanban') return component;
Expand Down Expand Up @@ -9001,7 +9002,7 @@ const objectKanbanQuickAddRemoved: MetadataConversion = {
// ⚠️ The same key name on a component that is NOT an
// `object-kanban` — not this entry's key. Untouched: the strip
// is scoped by component type, never by key name, which is what
// keeps the LIVE `kanban-ui` spelling out of its reach.
// keeps this key out of every other component type's reach.
{
type: 'object-grid',
id: 'g1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
// #17260 — ADR-0049 enforce-or-remove, executing the objectui#8285
// director-seat ruling (comment 5583979207, decision batch #91, 2026-09-08,
// standing maintainer delegation): ruled option B — `quickAdd` is retired from
// the `object-kanban` board and stays only on the `kanban-ui` block, where a
// React host can supply the runtime function the control needs.
// the `object-kanban` board. The ruling kept the control on the `kanban-ui`
// block, where a React host can supply the runtime function it needs; objectui
// has since retired that block (objectui#8257), so `object-kanban` offers no
// quick-add control and no block a document can name offers one either.
// The board FORWARDED the key but never honoured it: measured at the
// `.objectui-sha` pin `53ded82bf`, `ObjectKanban.tsx:931` spreads the authored
// bag into `KanbanRenderer` (`plugin-kanban/src/index.tsx:196` passes both
Expand All @@ -20,7 +22,7 @@
// wrong. Tombstoned with `retiredKey()` in `ObjectKanbanPropsSchema` (the
// surface baseline line carries `[RETIRED]`); sources are stripped by the D2
// conversion `object-kanban-quick-add-removed`, a pure lossless delete scoped
// by component `type` so the LIVE `kanban-ui` spelling is untouched.
// by component `type` so the same key on any other component type is untouched.
//
// Registered under 18, not 17: v17.0.0 was cut before this landed, so the
// removal ships on the 17.x line (launch-window convention: accept-set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,19 @@ export const entry: SemanticMigration = {
id: 'object-kanban-quick-add-retired',
surface: 'page.component.object-kanban.quickAdd — the per-column quick-add switch on the '
+ 'metadata-driven board',
replacement: '(removed from the metadata board.) The quick-add control exists on the `kanban-ui` '
+ 'block, where a React host supplies the `onQuickAdd` function the control calls. On a '
+ 'metadata board, records are created through the object\'s ordinary create action.',
replacement: '(removed from the metadata board.) Delete the key; `object-kanban` offers no '
+ 'quick-add control. On a metadata board, records are created through the object\'s ordinary '
+ 'create action.',
reason: 'The D2 conversion `object-kanban-quick-add-removed` deletes `quickAdd` from every '
+ '`object-kanban` component, and the delete is lossless: the board forwarded the flag, but the '
+ 'control also needs a host-supplied `onQuickAdd` function that JSON cannot carry and no '
+ 'producer ever put on an object-kanban node, so the gate was permanently false and no board '
+ 'ever showed the control. The residue is the requirement behind the flag. An author who set '
+ '`quickAdd: true` wanted users to add a card inside a column; that never happened and still '
+ 'does not. Whether the board can live without it, or needs a React host rendering the '
+ '`kanban-ui` block with a real `onQuickAdd`, is a product decision about that board — not '
+ 'something a key delete can make.',
+ 'does not. Whether the board can live without it is a product decision about that board — '
+ 'not something a key delete can make.',
acceptanceCriteria: 'No `object-kanban` component carries `quickAdd`; the parse refuses it. Each '
+ 'board renders the same columns and cards as before the upgrade. For each board that had set '
+ 'the flag, the author has either accepted creating records through the object\'s create '
+ 'action, or moved that board to a host that renders the `kanban-ui` block with `onQuickAdd` '
+ 'supplied — where clicking a column\'s add control creates a record in that column.',
+ 'the flag, the author has accepted creating records through the object\'s create action: '
+ '`object-kanban` offers no quick-add control.',
};
27 changes: 13 additions & 14 deletions packages/spec/src/migrations/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5693,8 +5693,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [
+ 'an author following the contract met a tool contradicting it with no way to tell which side '
+ 'was wrong. A retiredKey tombstone on `ObjectKanbanPropsSchema` with one D2 conversion that '
+ 'is a pure lossless DELETE (the key never had an effect to preserve) scoped by component '
+ '`type`: `quickAdd` stays LIVE on the `kanban-ui` block, where a React host supplies the '
+ 'runtime slot, and the ruling keeps it there deliberately.',
+ '`type`. Delete the key; `object-kanban` offers no quick-add control.',
},
{
id: 'object-tenancy-organization-field-retired',
Expand Down Expand Up @@ -14118,23 +14117,21 @@ const step18: MigrationStep = {
id: 'object-kanban-quick-add-retired',
surface: 'page.component.object-kanban.quickAdd — the per-column quick-add switch on the '
+ 'metadata-driven board',
replacement: '(removed from the metadata board.) The quick-add control exists on the `kanban-ui` '
+ 'block, where a React host supplies the `onQuickAdd` function the control calls. On a '
+ 'metadata board, records are created through the object\'s ordinary create action.',
replacement: '(removed from the metadata board.) Delete the key; `object-kanban` offers no '
+ 'quick-add control. On a metadata board, records are created through the object\'s ordinary '
+ 'create action.',
reason: 'The D2 conversion `object-kanban-quick-add-removed` deletes `quickAdd` from every '
+ '`object-kanban` component, and the delete is lossless: the board forwarded the flag, but the '
+ 'control also needs a host-supplied `onQuickAdd` function that JSON cannot carry and no '
+ 'producer ever put on an object-kanban node, so the gate was permanently false and no board '
+ 'ever showed the control. The residue is the requirement behind the flag. An author who set '
+ '`quickAdd: true` wanted users to add a card inside a column; that never happened and still '
+ 'does not. Whether the board can live without it, or needs a React host rendering the '
+ '`kanban-ui` block with a real `onQuickAdd`, is a product decision about that board — not '
+ 'something a key delete can make.',
+ 'does not. Whether the board can live without it is a product decision about that board — '
+ 'not something a key delete can make.',
acceptanceCriteria: 'No `object-kanban` component carries `quickAdd`; the parse refuses it. Each '
+ 'board renders the same columns and cards as before the upgrade. For each board that had set '
+ 'the flag, the author has either accepted creating records through the object\'s create '
+ 'action, or moved that board to a host that renders the `kanban-ui` block with `onQuickAdd` '
+ 'supplied — where clicking a column\'s add control creates a record in that column.',
+ 'the flag, the author has accepted creating records through the object\'s create action: '
+ '`object-kanban` offers no quick-add control.',
},
// #19054 (ADR-0049 enforce-or-remove; maintainer ruling 2026-09-18) — the D3
// entry of the `object-tenancy-organization-field-removed` family (ruling B on
Expand Down Expand Up @@ -23125,8 +23122,10 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>>
// #17260 — ADR-0049 enforce-or-remove, executing the objectui#8285
// director-seat ruling (comment 5583979207, decision batch #91, 2026-09-08,
// standing maintainer delegation): ruled option B — `quickAdd` is retired from
// the `object-kanban` board and stays only on the `kanban-ui` block, where a
// React host can supply the runtime function the control needs.
// the `object-kanban` board. The ruling kept the control on the `kanban-ui`
// block, where a React host can supply the runtime function it needs; objectui
// has since retired that block (objectui#8257), so `object-kanban` offers no
// quick-add control and no block a document can name offers one either.
// The board FORWARDED the key but never honoured it: measured at the
// `.objectui-sha` pin `53ded82bf`, `ObjectKanban.tsx:931` spreads the authored
// bag into `KanbanRenderer` (`plugin-kanban/src/index.tsx:196` passes both
Expand All @@ -23142,7 +23141,7 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>>
// wrong. Tombstoned with `retiredKey()` in `ObjectKanbanPropsSchema` (the
// surface baseline line carries `[RETIRED]`); sources are stripped by the D2
// conversion `object-kanban-quick-add-removed`, a pure lossless delete scoped
// by component `type` so the LIVE `kanban-ui` spelling is untouched.
// by component `type` so the same key on any other component type is untouched.
//
// Registered under 18, not 17: v17.0.0 was cut before this landed, so the
// removal ships on the 17.x line (launch-window convention: accept-set
Expand Down
46 changes: 42 additions & 4 deletions packages/spec/src/ui/component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
} from './view.zod';
import { FieldSchema } from '../data/field.zod';
import { ALL_CONVERSIONS } from '../conversions/registry';
import { MIGRATIONS_BY_MAJOR } from '../migrations/registry';
import { strictObjectDeclarations } from '../shared/strict-object';

describe('PageHeaderProps', () => {
Expand Down Expand Up @@ -3578,8 +3579,9 @@ describe('ObjectKanbanPropsSchema limit — the row cap four objectui faces alre

// #17260 — the board's per-column quick-add switch, retired by the
// objectui#8285 director-seat ruling (comment 5583979207, decision batch #91,
// 2026-09-08; ruled option B: `quickAdd` leaves `object-kanban` and stays only
// on the React-host `kanban-ui` block). Unlike `limit` above — a key four
// 2026-09-08; ruled option B: `quickAdd` leaves `object-kanban`; the ruling
// named the React-host `kanban-ui` block as where it stays, and objectui has
// since retired that block, objectui#8257). Unlike `limit` above — a key four
// objectui faces already implemented, so the spec was the half that was wrong
// — `quickAdd` was FORWARDED and never read: at the pin this repo builds
// against (`.objectui-sha` = `e420df310`; re-measured there 2026-09-30 —
Expand Down Expand Up @@ -3627,9 +3629,45 @@ describe('ObjectKanbanPropsSchema quickAdd is retired (#17260)', () => {
it('rejects the retired `quickAdd` with the prescription, not a bare unknown-key verdict', () => {
// The prescription IS the payload: the author who hits this got
// `unknown-prop` from objectui's html tier before — the same message a
// typo gets — so the refusal has to say where the control still works.
// typo gets — so the refusal has to say what to do instead: delete the key.
expect(() => kanban.parse({ objectName: 'showcase_task', quickAdd: true }))
.toThrow(/`quickAdd`.*removed.*`kanban-ui`/s);
.toThrow(/`quickAdd`.*removed.*Delete the key/s);
});

it('prescribes no block objectui does not register — the remedy names no `kanban-ui`', () => {
// `kanban-ui` is a node type objectui retired (objectui#8257) and this spec
// never declared, so `PageComponentSchema` accepts it as an unregistered
// custom string: an author steered there writes a node that saves clean and
// resolves no renderer. The control itself has no metadata route, so the
// one sentence every copy shares is "delete the key" — pinned on the
// refusal, the D2 conversion's summary, the D3 entry's three texts and
// step 18's rationale (the paragraph the upgrade guide prints).
const refused = kanban.safeParse({ objectName: 'showcase_task', quickAdd: true });
expect(refused.success).toBe(false);
const issue = (refused.error?.issues ?? []).find((i) => i.path.join('.') === 'quickAdd');
expect(issue?.code).toBe('invalid_type'); // the refusal itself is unchanged
const tombstone = issue?.message ?? '';
expect(tombstone).toContain('Delete the key; `object-kanban` offers no quick-add control.');

const conversion = ALL_CONVERSIONS.find((c) => c.id === 'object-kanban-quick-add-removed');
const step18 = MIGRATIONS_BY_MAJOR[18];
const semantic = step18?.semantic.find((m) => m.id === 'object-kanban-quick-add-retired');
expect(conversion).toBeDefined();
expect(semantic).toBeDefined();
expect(step18?.rationale).toContain('retires `object-kanban`\'s `quickAdd`');
const remedyTexts = [
tombstone,
conversion?.summary ?? '',
semantic?.replacement ?? '',
semantic?.reason ?? '',
semantic?.acceptanceCriteria ?? '',
step18?.rationale ?? '',
];
for (const text of remedyTexts) {
expect(text.length).toBeGreaterThan(0);
expect(text).not.toContain('kanban-ui');
}
expect(semantic?.replacement).toContain('Delete the key; `object-kanban` offers no quick-add control.');
});

it('does not materialize the retired `quickAdd` on a clean parse', () => {
Expand Down
Loading
Loading