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
62 changes: 62 additions & 0 deletions .changeset/9342-kanban-oncardmove-prop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
'@object-ui/types': minor
'@object-ui/plugin-kanban': minor
'@object-ui/components': minor
---

**BREAKING (scored `minor` per this repo's version-alignment convention)** —
`KanbanRenderer` takes `onCardMove` as an explicit React prop, and the
`object-kanban` document face tombstones the key (objectui#9342, executing the
`domain:ui` seat's option-B ruling on PR objectui#9338; the objectui#7742 remedy
`objectFields` took one member over, maintainer decision batch #70).

A `major` is unavailable by convention, not by preference: every package in
`.changeset/config.json`'s `fixed` group ships as one family whose major tracks
`@objectstack`, so `scripts/check-changeset-no-major.mjs` rejects a `major`
declaration outright. Breaking semantics are stated here instead.

## `@object-ui/plugin-kanban` — the move, and what it breaks

`onCardMove` is now a **React prop on `KanbanRendererProps`**, a sibling of
`schema`, and is **no longer a member of the `schema` bag**. `ObjectKanban`
passes its own `handleCardMove` through that prop.

⚠️ **This narrows a published props surface.** A host that renders
`KanbanRenderer` directly and wrote the handler inside `schema` must move it to
the prop: `schema={board} onCardMove={handler}`. A typed host gets a TS error; an
untyped one gets a silent drop, which is why the change carries
`needs:contract-review`.

## `@object-ui/types` — the key is refused by name, as a TOMBSTONE

`ObjectKanbanSchema.onCardMove` is `?: never` on the TypeScript face and
`handlerKeyRefusal('onCardMove', 'retired', …)` on the `@object-ui/types/zod`
mirror. An authored `onCardMove` was **accepted and silently dropped** before
this: `BaseSchema` is `.passthrough()`, so an undeclared key is not refused — it
stops being judged and the value is KEPT — and `ObjectKanban` then substituted
its own mover over it.

⛔ **RETIRED, not a RUNTIME SLOT**, and the two are not interchangeable here. A
slot keeps the TypeScript twin callable, which would publish a key the
object-bound board DROPS — the resolution this package's `quickAdd` carve-out
forbids in as many words. The sibling `onCardClick` is a slot because its
function reaches the board through a React prop `ObjectKanban` declares;
`onCardMove` has no such prop, and objectui#7804 measured that by driving the
handler the board was actually handed, with `onCardClick` as the lit control on
the same document and the same render.

⭐ **Why it took a second card.** The disposition did not move — objectui#7804
already measured `'retired'`. What blocked it was `check:handler-key-reads`,
which refuses a tombstone while a renderer still reads the key off the document
("a tombstone exists precisely because nothing reads the key — it has no read
site BY CONSTRUCTION"). What that gate cannot see is that the value at the read
was substituted one hop earlier. Moving the READ is what makes both sides true
at once, and it drains the key's `KNOWN_UNDECLARED_READS` row.

## `@object-ui/components` — the doc this makes wrong

`src/renderers/complex/README-KANBAN.md` taught an `object-kanban` document
carrying `"onCardMove": "(event) => …"` — a function spelled as a string,
accepted and silently dropped on the day it was written and refused by name from
now on. It teaches the React prop instead, and its prop table spells the key
`never`.
2 changes: 1 addition & 1 deletion content/docs/api/schema-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,7 @@ A drag-and-drop Kanban board. The `object-kanban` type key validates the shape t
>
> The other keys the retired `kanban` arm alone declared — `cardTitle`, `swimlaneField`, `grouping` and `navigation` — are still undeclared on this face. The renderer reads them, so a board may carry them; they are simply not judged.

> **Handler keys are not authorable in JSON, and two of the three now say so by name.** Since objectui#7804 this face declares `onCardClick` and `onQuickAdd` as objectui#6124 **runtime slots**: a React host supplies the function through the TypeScript interface or as a React prop, and this validator **refuses the key by name** with a message pointing at the node-type spelling (`{ "type": "toast", … }`, an `action:button` node). Until then an authored `onCardClick: { "action": "toast" }` parsed **green** — `BaseSchema` is `.passthrough()`, so a key no arm declares is not refused, it stops being judged and the value is kept, then reaches a call site expecting a function. ⚠️ `onCardMove` is the third key the board component reads and it is **still undeclared**: an authored one is accepted and dropped, because on an object-bound board the renderer substitutes its own mover. That gap is open on objectui#7804.
> **Handler keys are not authorable in JSON, and all three now say so by name.** Since objectui#7804 this face declares `onCardClick` and `onQuickAdd` as objectui#6124 **runtime slots**: a React host supplies the function through the TypeScript interface or as a React prop, and this validator **refuses the key by name** with a message pointing at the node-type spelling (`{ "type": "toast", … }`, an `action:button` node). Until then an authored `onCardClick: { "action": "toast" }` parsed **green** — `BaseSchema` is `.passthrough()`, so a key no arm declares is not refused, it stops being judged and the value is kept, then reaches a call site expecting a function. ⭐ The third key, `onCardMove`, is a **tombstone** since objectui#9342, not a runtime slot: an authored one reached **nothing** even as a function, because an object-bound board substitutes its own mover — so the TypeScript twin is `?: never` rather than callable, and the mover lives on `KanbanRenderer`'s React prop of the same name, a sibling of its `schema`.

> `data` and `bind` are [`BaseSchema`](#baseschema) members, not narrowed here, but this face requires **one of** `bind`, `data`, `objectName` — the renderer's own record-source ladder (an external `data` prop → `bind` via `useDataScope` → this schema's own `data` → a fetch keyed by `objectName`). A purely static board (lanes carrying their own cards, no record source) authors `"groupBy"` and `"data": []`. ⚠️ The record-source rule is **separate** from the lane key and is unaffected by objectui#8990: omitting `groupBy` is fine, omitting all of `bind` / `data` / `objectName` is still refused, at the refinement rather than at `groupBy`.

Expand Down
14 changes: 10 additions & 4 deletions content/docs/plugins/plugin-kanban.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,16 @@ objectui#6124 **runtime slots**, so an authored `onCardClick: { "action":
"toast" }` is now refused with a message pointing at the node-type spelling,
where before it parsed green and was handed to a call site expecting a function.
A React host still supplies either function through the TypeScript interface or
as a React prop; only the JSON face refuses them. ⚠️ `onCardMove` itself is
still *undeclared* on that validator — an authored one is accepted and silently
dropped, because an object-bound board substitutes its own mover — and that gap
is open on objectui#7804.
as a React prop; only the JSON face refuses them.

⭐ `onCardMove` is declared too, since objectui#9342 — but as a **tombstone**,
not a runtime slot. An authored one used to be accepted and silently dropped,
because an object-bound board substitutes its own mover and `ObjectKanban`
declares no `onCardMove` React prop; the key reached **nothing**, so the
TypeScript twin is `?: never` rather than callable and the validator refuses the
key by name. The mover is a prop on **`KanbanRenderer`**, a sibling of its
`schema` — which is where a host that mounts the board component directly
supplies it, and the move that let the arm carry the tombstone at all.

`columns` are the board's **swimlanes**, not a field projection — the fields drawn
on a card are `cardFields`. It is **one of two array shapes**, the pair
Expand Down
41 changes: 37 additions & 4 deletions packages/components/src/renderers/complex/README-KANBAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,49 @@ A fully functional, schema-driven Kanban board component for Object UI with drag

### With Event Handling

⛔ **`onCardMove` is not a document key, and never was.** JSON has no function
value, so it cannot be authored — not as a function, and not as the string
spelling this page used to teach:

```json
{
"type": "object-kanban",
"groupBy": "status",
"data": [],
"columns": [...],
"onCardMove": "(event) => { console.log('Card moved:', event); }"
}
```

That document was **accepted and silently dropped** until objectui#9342: the
`object-kanban` validator did not declare the key, `BaseSchema` is
`.passthrough()`, and `ObjectKanban` substituted its own mover on the schema it
hands the board — so an author who wrote it got a board that never called it and
no error saying why. It is now **refused by name**, with the remedy in the
message.

The board's mover is a **React prop**, supplied by the host that owns the write:

```tsx
import { KanbanRenderer } from '@object-ui/plugin-kanban';

const board = { type: 'object-kanban', groupBy: 'status', data: [], columns };

// A sibling of `schema`, never a member of it (objectui#9342).
const onCardMove = (
cardId: string,
fromColumnId: string,
toColumnId: string,
newIndex: number,
) => {
console.log(`Card ${cardId} moved from ${fromColumnId} to ${toColumnId} at ${newIndex}`);
};

<KanbanRenderer schema={board} onCardMove={onCardMove} />;
```

⚠️ On an **object-bound** board (`<ObjectKanban>`, the `object-kanban` registry
key) the host does **not** supply it: `ObjectKanban` owns the mover, because the
same function owns the optimistic write, the required-fields dialog and the
rollback. The prop above is for a host that mounts `KanbanRenderer` directly.

## Schema Reference

### Kanban Props
Expand All @@ -75,7 +108,7 @@ A fully functional, schema-driven Kanban board component for Object UI with drag
| `type` | `"object-kanban"` | Yes | Component type identifier. ⚠️ The bare `kanban` spelling was RETIRED in objectui#8802 (ruled 2026-09-09) and is refused by name. |
| `columns` | `KanbanColumn[]` | Yes | Array of column configurations |
| `className` | `string` | No | Custom CSS classes |
| `onCardMove` | `function` | No | Callback when a card is moved |
| `onCardMove` | `never` | | ⛔ RETIRED (objectui#9342) — refused by name. Not a document key: the mover is a React prop on `KanbanRenderer`, or `ObjectKanban`'s own on an object-bound board. See "With Event Handling" above. |

### KanbanColumn

Expand Down
16 changes: 13 additions & 3 deletions packages/plugin-kanban/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,12 @@ declare const columns: KanbanColumn[];
//
// ⚠️ `onCardMove` is NOT a document key: it is a React prop the host supplies
// (JSON has no function value), which is why it is spelled with explicit
// parameter types below rather than inferred from the annotation. The
// annotation is the type this package's renderer consumes; an unknown key does
// not fail it — `ObjectKanbanSchema` extends `BaseSchema`, whose index
// parameter types below rather than inferred from the annotation. Since
// objectui#9342 the document face says so BY NAME — `ObjectKanbanSchema` carries
// a `?: never` tombstone and the zod mirror refuses the key with the remedy in
// the message, where before an authored one was accepted and silently dropped.
// The annotation is the type this package's renderer consumes; an unknown key
// does not fail it — `ObjectKanbanSchema` extends `BaseSchema`, whose index
// signature deliberately accepts type-specific extensions, so the compiler is
// not what catches a misspelt board key.
const board: ObjectKanbanSchema = {
Expand Down Expand Up @@ -243,12 +246,19 @@ const schema: ObjectKanbanSchema = {
};

// The host supplies the handler as a React prop — JSON has no function value.
// It is a SIBLING of `schema` on `KanbanRenderer`, never a member of it
// (objectui#9342): `<KanbanRenderer schema={schema} onCardMove={onCardMove} />`.
const onCardMove = (cardId: string, fromColumnId: string, toColumnId: string, newIndex: number) => {
console.log(`Card ${cardId} moved from ${fromColumnId} to ${toColumnId} at index ${newIndex}`);
// Update your backend or state here
};
```

⚠️ An **object-bound** board does not take this prop. `ObjectKanban` owns the
mover — the same function owns the optimistic write, the required-fields dialog
and the rollback — so on the `object-kanban` registry key there is no host
handler to supply, and the document key is refused by name.

## Links

- 📚 [Documentation](https://www.objectui.org/docs/plugins/plugin-kanban)
Expand Down
10 changes: 9 additions & 1 deletion packages/plugin-kanban/src/ObjectKanban.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1428,6 +1428,15 @@ export const ObjectKanban: React.FC<ObjectKanbanComponentProps> = ({
// definition, never an authoring surface. On the schema bag it was
// reachable by an author through `BaseSchema`'s passthrough.
objectFields={objectDef?.fields}
// A PROP, not a schema key (objectui#9342, executing the ruling on PR
// objectui#9338) — the same remedy `objectFields` above took one card
// earlier. `handleCardMove` owns the optimistic write, the
// required-fields dialog and the objectui#4138 rollback, so it is this
// board's mover and never an authored one. While it rode the `schema`
// bag below, an authored `onCardMove` was accepted by the passthrough,
// substituted here, and silently dropped; the arm can only tombstone the
// key once no renderer reads it off the document.
onCardMove={handleCardMove}
schema={{
...effectiveSchema,
// objectui#8307 — the lane headers count rows that came back, so when
Expand Down Expand Up @@ -1455,7 +1464,6 @@ export const ObjectKanban: React.FC<ObjectKanbanComponentProps> = ({
captureAnchor(event);
navigation.handleClick(card, event);
},
onCardMove: handleCardMove,
}}
/>
</KanbanRecordsSettledContext.Provider>
Expand Down
Loading
Loading