From 94aeec6dec8c4c4f9fc70e2870c1556b0da75bf1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 15:14:55 +0000 Subject: [PATCH] fix(spec): react-tier ListView `onNavigate` declares the mode union it actually emits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `onNavigate` overlay in `REACT_BLOCKS` declared the second argument as `(recordId, action: 'view' | 'edit') => void`. `'edit'` is emitted by no call site in the reference implementation and read by no branch; `'new_window'` — what a modifier-click and an authored `navigation: { mode: 'new_window' }` actually send — was not declared at all. The union becomes `'view' | 'new_window'`, and the description now names both modes and states that the slot is a closed navigation-MODE vocabulary. The generated `skills/objectstack-ui/references/react-blocks.md` is regenerated by `gen:react-blocks`; it carries no hand edits. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- .changeset/19057-onnavigate-mode-union.md | 39 +++++++++++++++++++ packages/spec/src/ui/react-blocks.ts | 2 +- .../objectstack-ui/references/react-blocks.md | 2 +- 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 .changeset/19057-onnavigate-mode-union.md diff --git a/.changeset/19057-onnavigate-mode-union.md b/.changeset/19057-onnavigate-mode-union.md new file mode 100644 index 00000000000..b1652bf7dbd --- /dev/null +++ b/.changeset/19057-onnavigate-mode-union.md @@ -0,0 +1,39 @@ +--- +'@objectstack/spec': minor +--- + +React-tier ``: the `onNavigate` declaration becomes +`(recordId, action: 'view' | 'new_window') => void` — a declared value **no branch ever +emitted** is removed, and the value **two reference call sites do emit** is added. + +`REACT_BLOCKS`' ListView overlay declared the second argument as `'view' | 'edit'`. That +sentence was false in both directions. `'edit'` is emitted by no call site in the +reference implementation and read by no branch; `'new_window'` — what a Cmd/Ctrl- or +middle-click, and an authored `navigation: { mode: 'new_window' }`, actually send — was +not declared at all. An author reading this contract wrote a handler with one dead arm +and one missing arm. + +The second argument is a navigation-MODE token with a **closed vocabulary**, and the +declaration now says so. That closedness is not new: the protocol's own retirement note +for `view.list.navigation.view` (removed in 17.5.0, ADR-0049) records that anything +outside the mode vocabulary "matched no branch". What this change corrects is the +membership of the vocabulary, not its closedness. + +## FROM → TO + +| you wrote | write instead | +| --- | --- | +| `onNavigate={(id, action) => { if (action === 'edit') … }}` | delete that arm — nothing ever called it | +| a handler with no `'new_window'` arm | handle `'new_window'`: open the record in a new browser tab. Omitting the arm leaves the modifier-click path doing nothing | +| `onNavigate={(id) => …}` (one argument) | unchanged — the arity is untouched | + +**The one-line fix:** replace the `'edit'` arm with a `'new_window'` arm. + +Scope: this moves a **declaration**, not a type or a runtime check. `REACT_BLOCKS` types +this prop as a documentation string (`ReactBlockDef[]`), so no `.d.ts` signature moves +and nothing that compiles today stops compiling. The behaviour it describes is the +reference implementation's, which already emits exactly these two values; the sibling's +four declaration faces are corrected under objectui#9547 and its bump to +`@objectstack/spec` >= 17.5.0. + +Clause-②: yes diff --git a/packages/spec/src/ui/react-blocks.ts b/packages/spec/src/ui/react-blocks.ts index 448ce844831..348b7d8f85b 100644 --- a/packages/spec/src/ui/react-blocks.ts +++ b/packages/spec/src/ui/react-blocks.ts @@ -318,7 +318,7 @@ export const REACT_BLOCKS: ReactBlockDef[] = [ { name: 'filters', type: "FilterArray e.g. ['status','=','active']", kind: 'controlled', description: 'ObjectQL base filter; drive from React state for tabbed/searched lists. ([field, op, value]; ops =, !=, >, <, contains, in; compound: [\"and\", […], […]]).' }, { name: 'navigation', type: "{ mode: 'page' | 'drawer' | 'modal' | 'split' | 'none' }", kind: 'binding', description: 'What a row click does. Use { mode: \"none\" } when you handle clicks via onRowClick.' }, { name: 'onRowClick', type: '(record) => void', kind: 'callback', description: "Called with the clicked row's record — the hook for master/detail." }, - { name: 'onNavigate', type: "(recordId, action: 'view' | 'edit') => void", kind: 'callback', description: 'Called for page-level navigation.' }, + { name: 'onNavigate', type: "(recordId, action: 'view' | 'new_window') => void", kind: 'callback', description: "Called for page-level navigation. The second argument is a navigation-MODE token from a CLOSED vocabulary — 'view' opens the record page, 'new_window' opens it in a new browser tab; the reference implementation emits no other value and reads no other branch." }, { name: 'fields', type: 'string[]', kind: 'binding', description: 'Limit/order the columns shown (defaults to the object list fields).' }, { name: 'options', type: 'Record', kind: 'binding', description: 'View-type-specific options bag (kanban/calendar/gantt extras); prefer the typed spec props where they exist.' }, ], diff --git a/skills/objectstack-ui/references/react-blocks.md b/skills/objectstack-ui/references/react-blocks.md index 88cccfffd19..3e5637c38df 100644 --- a/skills/objectstack-ui/references/react-blocks.md +++ b/skills/objectstack-ui/references/react-blocks.md @@ -64,7 +64,7 @@ Server-connected object table with toolbar and switchable visualizations (grid/k | `options` | `Record` | binding | | View-type-specific options bag (kanban/calendar/gantt extras); prefer the typed spec props where they exist. | | `filters` | `FilterArray e.g. ['status','=','active']` | controlled | | ObjectQL base filter; drive from React state for tabbed/searched lists. ([field, op, value]; ops =, !=, >, <, contains, in; compound: ["and", […], […]]). | | `onRowClick` | `(record) => void` | callback | | Called with the clicked row's record — the hook for master/detail. | -| `onNavigate` | `(recordId, action: 'view' \| 'edit') => void` | callback | | Called for page-level navigation. | +| `onNavigate` | `(recordId, action: 'view' \| 'new_window') => void` | callback | | Called for page-level navigation. The second argument is a navigation-MODE token from a CLOSED vocabulary — 'view' opens the record page, 'new_window' opens it in a new browser tab; the reference implementation emits no other value and reads no other branch. | | `columns` | `string[] \| object[]` | data | ✓ | Fields to display as columns | | `type` | `'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'` | data | | | | `sort` | `object[]` | data | | |