Skip to content

Commit b76c26a

Browse files
committed
feat(spec): the name-leg url target's D3 entry, changeset and regenerated component reference
Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H Co-authored-by: Claude <noreply@anthropic.com>
1 parent 28251e5 commit b76c26a

3 files changed

Lines changed: 77 additions & 8 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: a page action row's url `target` is judged on the executor the console's action runner actually runs, so a row with no executor key whose `name` is `url` or `navigation` has its `target` judged by the url scheme set
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: registered ui-page-action-row-name-leg-url-target-judged -->
10+
11+
**BREAKING**: an accept-set narrowing on one key of four published page component rows, shipped as `minor` under the launch-window convention for accept-set narrowings.
12+
13+
The page action rows are `action:button` and `action:icon` (`properties`), and each member of an `action:group` or `action:menu` (`properties.actions[]`). The console's action runner dispatches a row on `type || actionType || name`, and every row forwards its executor key (`actionType` on a node, `type` on a member) as `type` beside its `name`. So a row with no executor key runs the handler its `name` names, and `name: 'url'` or `name: 'navigation'` runs the url navigator. The rows judged `target` by the url scheme set only when the executor KEY read `'url'`, so such a row parsed with a target the click refuses.
14+
15+
The rows now judge `target` against the row's effective executor: the executor key, or, when that key is omitted or empty, the `name`, exactly as the runner resolves it. When it resolves to the url navigator, `target` is judged by the same url scheme set as an `actionType: 'url'` row, with no second grammar. A declared executor key still outranks the `name`. `name` is not refused as an executor spelling, and a row whose `name` names any other handler (`navigate_edit`, `convert_lead`, …) is untouched. An `actionType: 'url'` row's refusal is unchanged.
16+
17+
### FROM → TO
18+
19+
| before | what to write instead |
20+
| --- | --- |
21+
| `{ name: 'url', target: 'javascript:…' }` with no `actionType` (or no `type` on a member), or `data:…`, `tel:…` or any other scheme | an `https://` page, a `mailto:` address, or an in-app path that starts with `/` or `./`; or, for a button meant to run something, the executor that does the work (`actionType: 'script'`, `'flow'`, `'api'`) |
22+
| the same with `name: 'navigation'` | the same; the `name` is the runner's url alias, so the target is judged as a url |
23+
| `{ name: 'url', target: 'forecast' }`, `'../forecast'`, `'?tab=2'` | the rooted in-app path, `'/forecast'`, or a `./` path; or declare the executor the row was meant to run |
24+
| `{ name: 'url', target: '//cdn.example.com/x' }` | `'https://cdn.example.com/x'`, or `'/x'` for an in-app path |
25+
| `{ name: 'url', target: '' }` | the address the button was meant to open |
26+
27+
**The one-line fix: give every page action row that has no executor key and is named `url` or `navigation` an `http(s)://` URL, a `mailto:` address, an in-app path that starts with `/` or `./`, or a template that opens with `${` as its `target`, or declare the executor key it was meant to run.** `os migrate meta --from 17` lists the D3 entry `ui-page-action-row-name-leg-url-target-judged`; the rewrite is by hand, because which address a refused target meant is the author's call.
28+
29+
**What an author now sees.** `os validate`'s props gate reports the refusal at the row's `target` (`properties.target`, or `properties.actions.N.target` for a member). For example, `{ name: 'url', target: 'javascript:alert(1)' }` on an `action:button` gets: ``An `action:button`'s `target` (`name: 'url'` and no `actionType`, so the runner runs the url navigator) opens with `javascript:`, a scheme a url target never opens.``, followed by the reason and the admitted set. Every admitted target parses unchanged.
30+
31+
**Measured producers: none.** No page action row in this repository, in objectui (at its pin `1fb1ced36243` and its main `866763bd`) or in hotcrm (at `0f84c9c`) is named `url` or `navigation`.

‎content/docs/references/ui/component.mdx‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const result = ActionButtonPropsSchema.parse(data);
2828

2929
| Property | Type | Required | Description |
3030
| :--- | :--- | :--- | :--- |
31-
| **name** | `string` | optional | Action name, forwarded to the action runner. Optional: an inline page button is not a registered object action, and without a name the runner dispatches on `actionType` alone |
31+
| **name** | `string` | optional | Action name, forwarded to the action runner. Optional: an inline page button is not a registered object action, and without a name the runner dispatches on `actionType` alone. With no `actionType` the runner dispatches on the name instead, so a `name` of `url` or `navigation` runs the url navigator and its `target` is judged as a url target |
3232
| **label** | `string` | optional | Button text. A literal string, placed as-is — localize through the translation bundle entry for this component id, not an inline locale map |
3333
| **icon** | `string` | optional | Lucide icon name drawn left of the label, resolved through the shared action-icon resolver (an unknown name draws no icon) |
3434
| **actionType** | `string` | optional | Executor the action runner dispatches to — built in: `script`, `url`, `modal`, `flow`, `api`, `form`; a handler registered under another name is dispatched too. `navigation`, the runner's alias of `url`, is refused: write `url`. Write it here, not as `type`: on a page component `type` is the component itself |
@@ -38,7 +38,7 @@ const result = ActionButtonPropsSchema.parse(data);
3838
| **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Disabled predicate — a boolean, a CEL string, or a `{ dialect, source }` envelope; the button is shown but cannot be pressed while it is TRUE. Omit for never-disabled |
3939
| **params** | `any` | optional | Action parameters, forwarded to the runner: an array is the list of inputs to collect from the user before the action runs; an object is forwarded as the static parameter values |
4040
| **description** | `string` | optional | Action description, forwarded to the runner — the parameter dialog shows it under its title |
41-
| **target** | `string` | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per `actionType`. For `actionType: 'url'`: an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ — every other scheme and a protocol-relative //host are refused |
41+
| **target** | `string` | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per `actionType`. For the url navigator (`actionType: 'url'`, or no `actionType` and a `name` of `url` or `navigation`): an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ — every other scheme and a protocol-relative //host are refused |
4242
| **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For a `url` action: `self` navigates in place, `new-tab` opens a new browser tab |
4343
| **method** | `string` | optional | HTTP method for an `api` action, forwarded to the runner |
4444
| **bodyExtra** | `any` | optional | Static request-body fields for an `api` action, forwarded to the runner |
@@ -86,7 +86,7 @@ const result = ActionButtonPropsSchema.parse(data);
8686

8787
| Property | Type | Required | Description |
8888
| :--- | :--- | :--- | :--- |
89-
| **name** | `string` | optional | Action name, forwarded to the action runner, and the item's text when there is no `label`. Optional: a member is not a registered object action, and the runner dispatches a nameless one on its `type` |
89+
| **name** | `string` | optional | Action name, forwarded to the action runner, and the item's text when there is no `label`. Optional: a member is not a registered object action, and the runner dispatches a nameless one on its `type`. With no `type` the runner dispatches on the name instead, so a `name` of `url` or `navigation` runs the url navigator and its `target` is judged as a url target |
9090
| **label** | `string` | optional | The item's text. A literal string, placed as-is — localize through the translation bundle entry for this component id |
9191
| **icon** | `string` | optional | Lucide icon name drawn before the label, resolved through the shared action-icon resolver (an unknown name draws no icon) |
9292
| **type** | `string` | optional | Executor the action runner dispatches to — built in: `script`, `url`, `modal`, `flow`, `api`, `form`; a handler registered under another name is dispatched too. `navigation`, the runner's alias of `url`, is refused: write `url`. A member is an action entry, so its executor is `type` (on an `action:button` node it is `actionType`) |
@@ -96,7 +96,7 @@ const result = ActionButtonPropsSchema.parse(data);
9696
| **tags** | `Enum<'separator-before'>[]` | optional | Item tags — `separator-before` draws a divider above the item in a dropdown or menu (not above the first item); no other tag is drawn |
9797
| **params** | `any` | optional | Action parameters, forwarded to the runner: an array is the list of inputs to collect from the user before the action runs. Only a `type: 'api'` member takes any other value, forwarded as its request payload (use `bodyExtra` for that); on every other member a non-array `params` is refused — author an action with static parameter values as its own `action:button` node |
9898
| **description** | `string` | optional | Action description, forwarded to the runner — the parameter dialog shows it under its title |
99-
| **target** | `string` | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per `type`. For `type: 'url'`: an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ — every other scheme and a protocol-relative //host are refused |
99+
| **target** | `string` | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per `type`. For the url navigator (`type: 'url'`, or no `type` and a `name` of `url` or `navigation`): an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ — every other scheme and a protocol-relative //host are refused |
100100
| **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For a `url` action: `self` navigates in place, `new-tab` opens a new browser tab |
101101
| **method** | `string` | optional | HTTP method for an `api` action, forwarded to the runner |
102102
| **bodyExtra** | `any` | optional | Static request-body fields for an `api` action, forwarded to the runner |
@@ -123,7 +123,7 @@ const result = ActionButtonPropsSchema.parse(data);
123123

124124
| Property | Type | Required | Description |
125125
| :--- | :--- | :--- | :--- |
126-
| **name** | `string` | optional | Action name, forwarded to the action runner; also the `aria-label` when there is no `label`. Optional, as for `action:button` |
126+
| **name** | `string` | optional | Action name, forwarded to the action runner; also the `aria-label` when there is no `label`. Optional, as for `action:button`. With no `actionType` the runner dispatches on the name instead, so a `name` of `url` or `navigation` runs the url navigator and its `target` is judged as a url target |
127127
| **label** | `string` | optional | Accessible label and tooltip text (and its first letter stands in when no icon resolves). A literal string — localize through the translation bundle entry for this component id |
128128
| **icon** | `string` | optional | Lucide icon name, resolved through the shared action-icon resolver (renderer registration default `play`) |
129129
| **actionType** | `string` | optional | Executor the action runner dispatches to — built in: `script`, `url`, `modal`, `flow`, `api`, `form`; a handler registered under another name is dispatched too. `navigation`, the runner's alias of `url`, is refused: write `url`. Write it here, not as `type` |
@@ -132,7 +132,7 @@ const result = ActionButtonPropsSchema.parse(data);
132132
| **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Disabled predicate — a boolean, a CEL string, or a `{ dialect, source }` envelope; the icon is shown but cannot be pressed while it is TRUE. Omit for never-disabled |
133133
| **description** | `string` | optional | Action description — the tooltip text when there is no `label`, and forwarded to the runner for the parameter dialog |
134134
| **params** | `any` | optional | Action parameters, forwarded to the runner: an array is the list of inputs to collect from the user; an object is forwarded as the static parameter values |
135-
| **target** | `string` | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per `actionType`. For `actionType: 'url'`: an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ — every other scheme and a protocol-relative //host are refused |
135+
| **target** | `string` | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per `actionType`. For the url navigator (`actionType: 'url'`, or no `actionType` and a `name` of `url` or `navigation`): an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ — every other scheme and a protocol-relative //host are refused |
136136
| **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For a `url` action: `self` navigates in place, `new-tab` opens a new browser tab |
137137
| **method** | `string` | optional | HTTP method for an `api` action, forwarded to the runner |
138138
| **bodyExtra** | `any` | optional | Static request-body fields for an `api` action, forwarded to the runner |
@@ -176,7 +176,7 @@ const result = ActionButtonPropsSchema.parse(data);
176176

177177
| Property | Type | Required | Description |
178178
| :--- | :--- | :--- | :--- |
179-
| **name** | `string` | optional | Action name, forwarded to the action runner, and the item's text when there is no `label`. Optional: a member is not a registered object action, and the runner dispatches a nameless one on its `type` |
179+
| **name** | `string` | optional | Action name, forwarded to the action runner, and the item's text when there is no `label`. Optional: a member is not a registered object action, and the runner dispatches a nameless one on its `type`. With no `type` the runner dispatches on the name instead, so a `name` of `url` or `navigation` runs the url navigator and its `target` is judged as a url target |
180180
| **label** | `string` | optional | The item's text. A literal string, placed as-is — localize through the translation bundle entry for this component id |
181181
| **icon** | `string` | optional | Lucide icon name drawn before the label, resolved through the shared action-icon resolver (an unknown name draws no icon) |
182182
| **type** | `string` | optional | Executor the action runner dispatches to — built in: `script`, `url`, `modal`, `flow`, `api`, `form`; a handler registered under another name is dispatched too. `navigation`, the runner's alias of `url`, is refused: write `url`. A member is an action entry, so its executor is `type` (on an `action:button` node it is `actionType`) |
@@ -186,7 +186,7 @@ const result = ActionButtonPropsSchema.parse(data);
186186
| **tags** | `Enum<'separator-before'>[]` | optional | Item tags — `separator-before` draws a divider above the item in a dropdown or menu (not above the first item); no other tag is drawn |
187187
| **params** | `any` | optional | Action parameters, forwarded to the runner: an array is the list of inputs to collect from the user before the action runs. Only a `type: 'api'` member takes any other value, forwarded as its request payload (use `bodyExtra` for that); on every other member a non-array `params` is refused — author an action with static parameter values as its own `action:button` node |
188188
| **description** | `string` | optional | Action description, forwarded to the runner — the parameter dialog shows it under its title |
189-
| **target** | `string` | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per `type`. For `type: 'url'`: an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ — every other scheme and a protocol-relative //host are refused |
189+
| **target** | `string` | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per `type`. For the url navigator (`type: 'url'`, or no `type` and a `name` of `url` or `navigation`): an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ — every other scheme and a protocol-relative //host are refused |
190190
| **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For a `url` action: `self` navigates in place, `new-tab` opens a new browser tab |
191191
| **method** | `string` | optional | HTTP method for an `api` action, forwarded to the runner |
192192
| **bodyExtra** | `any` | optional | Static request-body fields for an `api` action, forwarded to the runner |
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
export const entry: SemanticMigration = {
6+
id: 'ui-page-action-row-name-leg-url-target-judged',
7+
surface: 'the `target` of a page action row with no executor key (or an empty one) whose `name` is `\'url\'` or '
8+
+ '`\'navigation\'` — an `action:button` or `action:icon` with no `actionType`, or an `action:group` / '
9+
+ '`action:menu` member with no `type` — holding a target the console\'s action runner does not open: a scheme '
10+
+ 'other than `http:`, `https:` or `mailto:` (`javascript:`, `data:`, `tel:` and every other), `http:` or '
11+
+ '`https:` without the `//`, a protocol-relative `//host`, `/` followed by a backslash, a tab / line feed / '
12+
+ 'carriage return anywhere, or a bare relative target with no leading `/` or `./` (the empty string included)',
13+
replacement: 'an `http://` or `https://` URL, a `mailto:` address, an in-app path that starts with `/` or `./`, or '
14+
+ 'a template that opens with `${` (judged after interpolation, at the click) — the set a url row\'s `target` '
15+
+ 'already takes (`ui-page-action-row-url-target-scheme-set-closed`). Or, for a row whose `name` merely happens '
16+
+ 'to read `url` and that was meant to run something else, declare its executor (`actionType` on a node, '
17+
+ '`type` on a member): a declared executor key outranks the `name` at the runner and here alike. ⛔ NOT '
18+
+ 'mechanically convertible: which address a refused target meant, or which executor the row was meant to '
19+
+ 'run, is the author\'s call.',
20+
reason:
21+
'The runner dispatches on `type || actionType || name`, and every row forwards its executor key as `type` '
22+
+ 'beside its `name`, so a row with no executor key runs the handler its `name` names; `url` and `navigation` '
23+
+ 'reach the url navigator. The rows judged `target` only when the executor KEY read `\'url\'`, so such a row '
24+
+ 'carried a target the click refuses past the parse. The rows now judge `target` against the executor the '
25+
+ 'runner runs, whichever key spelled it, with the one url-target grammar. `name` is not refused as an '
26+
+ 'executor spelling: a row whose `name` names any other handler is untouched. A page\'s `properties` is not '
27+
+ 'parsed on the metadata save or load path; `os validate`\'s props gate parses it against these same rows '
28+
+ 'and reports the refusal at the row\'s `target`. No measured producer writes such a row: no page action row '
29+
+ 'in this repository, in objectui or in hotcrm is named `url` or `navigation` — so the entry is owed for rows '
30+
+ 'authored outside them.',
31+
acceptanceCriteria:
32+
'Every `action:button` / `action:icon` with no (or an empty) `actionType` and every `action:group` / '
33+
+ '`action:menu` member with no (or an empty) `type` whose `name` is `\'url\'` or `\'navigation\'` carries a '
34+
+ '`target` that opens with `http://`, `https://` or `mailto:` (case-insensitive), starts with `/` (not `//`, '
35+
+ 'not `/` followed by a backslash) or `./`, or opens with `${`, and none holds a tab, line feed or carriage '
36+
+ 'return — or declares its executor key. `os validate`\'s props gate reports nothing at those rows\' `target`, '
37+
+ 'and every row whose `name` names another handler parses as before.',
38+
};

0 commit comments

Comments
 (0)