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
39 changes: 39 additions & 0 deletions .changeset/19057-onnavigate-mode-union.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
'@objectstack/spec': minor
---

React-tier `<ListView>`: 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
2 changes: 1 addition & 1 deletion packages/spec/src/ui/react-blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>', kind: 'binding', description: 'View-type-specific options bag (kanban/calendar/gantt extras); prefer the typed spec props where they exist.' },
],
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-ui/references/react-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Server-connected object table with toolbar and switchable visualizations (grid/k
| `options` | `Record<string, any>` | 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 | | |
Expand Down
Loading