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
73 changes: 73 additions & 0 deletions .changeset/17260-object-kanban-quick-add-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
"@objectstack/spec": minor
---

feat(spec)!: retire `ObjectKanbanProps.quickAdd` — the `object-kanban` board forwarded it and nothing ever read it (ADR-0049)

<!-- adr-0087: registered object-kanban-quick-add-removed -->

**BREAKING** — `quickAdd` is retired from the `object-kanban` component props. Executes the
objectui#8285 director-seat ruling (decision batch #91, 2026-09-08, standing maintainer
delegation), ruled **option B**: the key leaves the board and stays only on the `kanban-ui`
block, where a React host can supply the runtime function the control needs.

| | before | after |
|:--|:--|:--|
| `object-kanban` | `quickAdd: true` parsed clean and did nothing | refused by the tombstone, with the prescription |
| `kanban-ui` (objectui block) | the control works when the host passes `onQuickAdd` | **unchanged** |

**What was actually wrong.** Measured at the `.objectui-sha` pin this repo builds against
(`53ded82bf`): the board FORWARDS the key — `ObjectKanban.tsx:931` spreads the authored bag
into `KanbanRenderer`, which passes `quickAdd={schema.quickAdd}` alongside
`onQuickAdd={schema.onQuickAdd}` (`plugin-kanban/src/index.tsx:196`) — but `KanbanImpl`
gates the affordance on **both** (`:355`, `:368`), and `onQuickAdd` is a host-supplied
FUNCTION that JSON cannot carry and that no producer puts on an `object-kanban` node.
`ObjectKanban.tsx` names neither half of the pair (0 occurrences each, against 6 for the
sibling `onCardClick` in the same file), so the gate was permanently false.

**And the drop was not silent, which is what made it worse than silence.** objectui's html
tier reported the published key as `unknown-prop` — the same diagnostic a typo gets — and
its registry↔spec ledger records it as `ESCALATED (object-kanban.quickAdd — measured NOT
honoured)`. An author following the published contract met a tool that contradicted it, with
nothing in either message to say which side was wrong. The tombstone collapses both halves
onto one answer.

## What to write instead

Nothing, on this board: there is no per-column quick-add affordance on `object-kanban` and
there never was one. Delete the key.

```ts
// before — parsed clean, rendered nothing
{ type: 'object-kanban', properties: { objectName: 'crm_task', groupBy: 'status', quickAdd: true } }
// after
{ type: 'object-kanban', properties: { objectName: 'crm_task', groupBy: 'status' } }
```

The control itself is not withdrawn from the platform. It stays on the `kanban-ui` block,
which a React host renders directly and can hand the `onQuickAdd` slot to — that is what the
ruling preserved deliberately.

Existing sources: `os migrate meta --from 17` lists the mechanical edits; apply them by hand.

The retirement kit:

- a `retiredKey()` tombstone on `ObjectKanbanPropsSchema` — `tsc` types the key `never`, and
a value reaching the parse raises the prescription rather than a bare unknown-key verdict
- the D2 conversion `object-kanban-quick-add-removed` (`RETIRED_KEYS_BY_MAJOR[18]` entry
`ui/ObjectKanbanProps:quickAdd`, wired into the protocol-18 chain step) — a **pure lossless
delete**, since the key never had an effect to preserve, scoped by component `type` so the
live `kanban-ui` spelling stays out of its reach
- the `authorable-surface/ui.json` row becomes `ui/ObjectKanbanProps:quickAdd [RETIRED]`, and
the generated reference page prints the prescription in place of the old describe
- the schema docblock's read-point list is corrected in the same stroke: it named `quickAdd`
among the keys reached "via the forwarded schema", a sentence true about the FORWARD and
false about the READ — which is how the key kept re-authorizing itself
- pin tests (`ui/component.test.ts`): the refusal carries the prescription; a clean parse does
not materialize the key; and the control pair separating the tombstone's answer from the
strict unknown-key arm's, so a shape that had merely DROPPED the key could not pass
- no liveness-ledger row (component props are not an enrolled ledger type) and no form or
i18n edit: zero `object-kanban` components are authored anywhere under `examples/` or
`apps/` (control: `object-grid` 3, `object-metric` 8 in the same corpora, same instrument)
- `api-surface/` is unchanged, correctly: it ratchets export existence, and no export leaves —
`ObjectKanbanProps` still exists, one key narrower
2 changes: 1 addition & 1 deletion content/docs/references/ui/component.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,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** | `any` | optional | View grouping config; its first field is the swimlane fallback |
| **quickAdd** | `boolean` | optional | Show the per-column quick-add affordance |
| **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. |
| **coverImageField** | `string` | optional | Image field rendered as the card cover |
| **conditionalFormatting** | `any` | optional | Card conditional formatting rules |

Expand Down
2 changes: 1 addition & 1 deletion packages/spec/authorable-surface/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@
"ui/ObjectKanbanProps:grouping",
"ui/ObjectKanbanProps:limit",
"ui/ObjectKanbanProps:objectName",
"ui/ObjectKanbanProps:quickAdd",
"ui/ObjectKanbanProps:quickAdd [RETIRED]",
"ui/ObjectKanbanProps:swimlaneField",
"ui/ObjectKanbanProps:titleField",
"ui/ObjectListView:addRecord",
Expand Down
184 changes: 184 additions & 0 deletions packages/spec/src/conversions/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8010,6 +8010,189 @@ 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).
*
* **A pure lossless delete.** The key never had an effect to preserve.
* Measured at the `.objectui-sha` pin (`53ded82bf`): the board FORWARDS it —
* `ObjectKanban.tsx:931` spreads the authored bag into `KanbanRenderer`,
* which passes `quickAdd={schema.quickAdd}` alongside
* `onQuickAdd={schema.onQuickAdd}` (`plugin-kanban/src/index.tsx:196`) — but
* `KanbanImpl` gates the affordance on BOTH (`:355`, `:368`), and
* `onQuickAdd` is a host-supplied FUNCTION that JSON cannot carry and that no
* producer puts on an `object-kanban` node (`ObjectKanban.tsx` names neither
* half of the pair: 0 occurrences each, against 6 for the sibling
* `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
* fixture's non-carrier control is an `object-grid` authoring the same key
* name.
*
* The neighbouring forwarded keys `coverImageField` and `conditionalFormatting`
* are LIVE and survive untouched (both are read on this very path, by
* `KanbanRenderer` / `bucketCardsIntoColumns`).
*
* Zero authored occurrences in this repo's corpora — no `object-kanban`
* component is authored anywhere under `examples/` or `apps/` at all (control:
* `object-grid` 3, `object-metric` 8 in the same corpora, same instrument) —
* so this entry exists for stored `sys_metadata` rows and for authors outside
* the repo, which the filing seat explicitly could not measure.
*/
const objectKanbanQuickAddRemoved: MetadataConversion = {
id: 'object-kanban-quick-add-removed',
toMajor: 18,
retiredFromLoadPath: true,
surface: 'page.component.object-kanban.quickAdd',
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)",
apply(stack, emit) {
return mapPageComponents(stack, (component, path) => {
if (component.type !== 'object-kanban') return component;
const properties = component.properties;
if (!isDict(properties) || !('quickAdd' in properties)) return component;
const stripped = stripKeys(properties, ['quickAdd'], emit, `${path}.properties`);
return { ...component, properties: stripped };
});
},
fixture: {
before: {
pages: [
{
name: 'delivery_board',
regions: [
{
name: 'main',
components: [
// The carrier: an `object-kanban` authoring the retired key.
{
type: 'object-kanban',
id: 'k1',
properties: { objectName: 'crm_task', groupBy: 'status', quickAdd: true },
},
// ⚠️ 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.
{
type: 'object-grid',
id: 'g1',
properties: { objectName: 'crm_task', quickAdd: true },
},
// A board WITHOUT the key rides through untouched — the strip
// dispatches on key presence and the copy-on-write contract
// keeps the reference.
{
type: 'object-kanban',
id: 'k3',
properties: { objectName: 'crm_task', cardFields: ['title'] },
},
// The nested position (#6775's lesson): a board inside a
// card's `children` is still a component.
{
type: 'page:card',
id: 'c1',
properties: {
children: [
{
type: 'object-kanban',
id: 'k4',
properties: { objectName: 'crm_lead', groupBy: 'stage', quickAdd: false },
},
],
},
},
],
},
],
},
// The named-slot shape (#6776): a board authored into a slotted page.
{
name: 'delivery_board_detail',
kind: 'slotted',
regions: [],
slots: {
details: {
type: 'object-kanban',
id: 'k5',
properties: { objectName: 'crm_task', groupBy: 'status', quickAdd: true },
},
},
},
],
},
after: {
pages: [
{
name: 'delivery_board',
regions: [
{
name: 'main',
components: [
{
type: 'object-kanban',
id: 'k1',
properties: { objectName: 'crm_task', groupBy: 'status' },
},
{
type: 'object-grid',
id: 'g1',
properties: { objectName: 'crm_task', quickAdd: true },
},
{
type: 'object-kanban',
id: 'k3',
properties: { objectName: 'crm_task', cardFields: ['title'] },
},
{
type: 'page:card',
id: 'c1',
properties: {
children: [
{
type: 'object-kanban',
id: 'k4',
properties: { objectName: 'crm_lead', groupBy: 'stage' },
},
],
},
},
],
},
],
},
{
name: 'delivery_board_detail',
kind: 'slotted',
regions: [],
slots: {
details: {
type: 'object-kanban',
id: 'k5',
properties: { objectName: 'crm_task', groupBy: 'status' },
},
},
},
],
},
// Three notices: the region-level board, the nested one and the slotted
// one. The `object-grid` sibling and the board without the key emit none.
expectedNotices: 3,
},
};

/**
* Object-permission lifecycle bits `allowRestore` / `allowPurge` removed
* (protocol 18, #12497 — ADR-0049 enforce-or-remove, maintainer ruling
Expand Down Expand Up @@ -9170,6 +9353,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly<Record<number, readonly MetadataConv
translationComponentSubmitLabelRemoved,
pageComponentResponsiveRemoved,
objectGridDefaultSortRemoved,
objectKanbanQuickAddRemoved,
permissionAllowRestorePurgeRemoved,
formViewOptionDefaultRemoved,
fieldReferenceToAlias,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

// #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 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
// `quickAdd` and `onQuickAdd`), and `KanbanImpl` gates the affordance on BOTH
// (`:355`, `:368`) — while `onQuickAdd` is a host-supplied FUNCTION that JSON
// cannot carry and no producer puts on an `object-kanban` node
// (`ObjectKanban.tsx` names neither half: 0 each, against 6 for the sibling
// `onCardClick` in the same file). The drop was not silent, which is the sharp
// edge: objectui's html tier reported the published key as `unknown-prop` —
// the SAME diagnostic a typo gets — and its registry-spec ledger records it as
// `ESCALATED (object-kanban.quickAdd — measured NOT honoured)`, so the author
// met a tool contradicting the contract with no way to tell which side was
// 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.
//
// 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
// narrowings ride minor releases) and the prescription lives at the major
// boundary where `migrate meta` users look — the `ui/ObjectGridProps:defaultSort`
// precedent one entry over.
export const entry = 'ui/ObjectKanbanProps:quickAdd';
Loading
Loading