Skip to content

Commit 22ae443

Browse files
committed
Merge main into the 2g PR 2 branch; carry #26274 onto the one parser
#26274 retired the node-level visibleOn / hiddenOn / disabledOn spellings and edited the ui copy of the parser, which this branch deletes. Its parser half is carried onto packages/sdui-parser so it is not lost: the three leave SDUI_BASE_PROPS, validateTree answers each with an unknown-prop naming the canonical key, retired-visibility-dialects.ts moves in with its MIT notice, and base-props-one-list-11044 takes the same test edits. The pending changeset 25926 names @objectstack/sdui-parser, where that behaviour now ships. Claude-Session: https://claude.ai/code/session_01Xe3LjBvLrRVA6dmFUEGskm Co-authored-by: Claude <noreply@anthropic.com>
2 parents 0cfec97 + 6390fe5 commit 22ae443

108 files changed

Lines changed: 4185 additions & 953 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/react': minor
4+
'@objectstack/sdui-parser': minor
5+
'@object-ui/app-shell': patch
6+
---
7+
8+
feat(types,react,sdui-parser,app-shell)!: the node-level `visibleOn` / `hiddenOn` / `disabledOn` spellings are retired; `visibleWhen` / `hidden` / `disabled` are the one predicate each
9+
10+
Clause-②: no (narrowing)
11+
Metadata-protocol: narrowed
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) The three keys are `@object-ui/types` schema members on objectui's own node face, not an `@objectstack/spec` key, object definition, stored row or stored shape: the spec declares `visibleWhen` as the one conditional-visibility predicate at every layer (ADR-0089) and never declared a `*On` suffix on a page component, so the ADR-0087 registries in `packages/spec` have no entry to write and `objectstack migrate meta` has nothing to rewrite. Each old spelling is refused by name on both faces (the TypeScript compiler, the Zod mirror behind `objectui validate`), and the refusal names the surviving key; that is the same disposition the objectui retirements took when the package merged into this repository (maintainer ruling 28 = B on objectstack#25831). The guidance below is for the author of a page, not for a metadata upgrader. The other categories are closed on facts: the four packages publish (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and what narrows is a declared schema key on a published metadata face, not a runtime-only interface, a type annotation or a runtime export (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
14+
15+
`@objectstack/spec` declares exactly one conditional-visibility predicate at every layer, `visibleWhen`, and objectui carried three spellings of its own beside it: `visibleOn` beside `visibleWhen`, `hiddenOn` beside `hidden`, `disabledOn` beside `disabled`. Each pair was evaluated by the same code with the same polarity, so the second spelling added a dialect and nothing else. The three are retired, with no alias window.
16+
17+
- **`@object-ui/types`**: `BaseSchema.visibleOn`, `hiddenOn` and `disabledOn` are `never` on the TypeScript face and alias refusals on the Zod mirror. A document that writes one is refused at that key, and the message names the key that survives (`visibleWhen`, `hidden`, `disabled`), says the polarity is the same, and says the value is unchanged. `objectui validate` reads the same mirror, so it prints the same refusal. The `object-view` table slot's refusal of the same three keys now names the surviving node key too (write `visibleWhen` / `hidden` / `disabled` on the object-view node itself) instead of sending the author to a retired spelling. Boolean `visible` stays: the spec declares it on actions, navigation entries, tabs, alerts and settings, and the renderer keeps evaluating it.
18+
- **`@object-ui/react`**: `SchemaRenderer`'s visibility chain has three legs, `visibleWhen` and `visible` (shown when truthy) and `hidden` (hidden when truthy), and the enablement gate has one, `disabled`. A stale retired key that reaches the renderer unvalidated is not consulted and is held out of the component's props, so it neither gates the node nor lands in the DOM.
19+
- **`@objectstack/sdui-parser`**: the three keys are no longer base props. `validateTree` reports each as an `unknown-prop` diagnostic that names the surviving key, so a TSX tree is told the same thing a JSON document is. `checkRetiredVisibilityDialect` and `RETIRED_VISIBILITY_DIALECTS` are exported for that reading.
20+
- **`@object-ui/app-shell`**: the command palette reads a navigation entry's `visible` only; the `visibleOn` fallback it carried read a key `NavigationItem` never declared.
21+
22+
**BREAKING** (an accept-set narrowing) for any page, view or component document that writes `visibleOn`, `hiddenOn` or `disabledOn` on a node. No shipped example, fixture or skill example in this repository wrote one; the ones in the documentation and the `objectui` skill are rewritten in this change.
23+
24+
- **Before.** The three keys parsed, were evaluated beside their canonical twin, and the docs taught both spellings.
25+
- **Now.** Each is refused by name on `objectui validate`, on the Zod mirror, by the TypeScript compiler and by the SDUI parser, and the renderer does not read it.
26+
- **Fix.** Rename the key and keep the value: `visibleOn` becomes `visibleWhen`, `hiddenOn` becomes `hidden`, `disabledOn` becomes `disabled`. A form field's `fields[].visibleOn` is a different, view-level slot and is unchanged.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): the Setup position page shows, read-only, what each permission set the position names grants
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A position grants access only through the permission sets its definition names in `permissionSets`. The position page in Setup → Positions listed those names but showed nothing of what they grant, so an admin had to open every set to see its objects, capabilities and record scopes.
11+
12+
- **One preview per named set.** Under the position's definition, beside its holders, the page now shows each named set's permission preview: the object × capability grid with each object's read and write scope, field-level overrides, row-level policies, system permissions and tabs. The previews follow the order the position names the sets. Each is headed by the set's name, which links to the set's own page.
13+
- **No merge, no editing.** Each preview shows one set as stored. The page does not compute what the sets add up to, and nothing in the previews is editable. A set is changed on its own page.
14+
- **A set that does not load.** A named set the server does not serve shows one line, `Permission set "NAME" was not found.`, for that set only; the other previews still show. A read the server refuses says that, with the server's sentence, instead of "not found".
15+
- **No sets.** A position that names no permission sets says so.
16+
17+
The permission-set page itself is unchanged: its matrix already shows each object's read and write scope.
18+
19+
What to do: nothing. No spec key, stored shape or API changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
'@object-ui/app-shell': minor
5+
---
6+
7+
feat(spec,automation,app-shell): a read-only `record_link` screen field shows a record the run created, by its display name, as a link to its record page
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: added
11+
12+
A screen flow can now end on a result screen that links the records it created, instead of a toast that points at nothing.
13+
14+
```ts
15+
{ id: 'done', type: 'screen', config: { title: 'Lead converted', fields: [
16+
{ name: 'account_link', label: 'Account', type: 'record_link',
17+
reference: 'crm_account', defaultValue: { dialect: 'cel', source: 'account.id' } },
18+
] } },
19+
```
20+
21+
- **`@objectstack/spec`**: `ScreenFieldConfigSchema` gains the `type` member `'record_link'` (`SCREEN_FIELD_RECORD_LINK_TYPE`). On it, `reference` (the record's object) and `defaultValue` (a value slot that evaluates to the record's id) are required, and `required`, `options`, `placeholder`, `min` and `max` are refused, each at its own key with the exported refusal text (`SCREEN_FIELD_RECORD_LINK_REFERENCE_REQUIRED`, `SCREEN_FIELD_RECORD_LINK_DEFAULT_VALUE_REQUIRED`, `screenFieldRecordLinkRefusedKey`). The name is `record_link`, not `record`, because `record` is already an object field type (a name-keyed map of sub-objects) and a screen field's `type` uses the same vocabulary. Every other `type` value parses as before.
22+
- **`@objectstack/service-automation`**: the resume refuses any value sent under a `record_link` field's name, `null` included and visible or not, with `400 INVALID_SCREEN_INPUT` and the field code `invalid_value` ("shows a record and takes no value"). The value is never bound to a flow variable, the refusal keeps the pause, and a resume that leaves the field out still lands. A caller's param that shares the field's name never counts as answering the screen, so a result screen always pauses.
23+
- **`@object-ui/app-shell`**: the flow screen dialog shows a `record_link` field as the record's display name (the object's `nameField`, through the lookup widget's read-only branch) inside a link to `/apps/APP/OBJECT/record/ID`. An empty id shows "—" with no link, and an object the console cannot route to shows the name without a link. `visibleWhen` works as on any field. The dialog never seeds or posts a value for the field.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/plugin-grid': patch
3+
---
4+
5+
fix(plugin-grid): a bulk action's confirm sentence formats its numbers and dates in the viewer's display locale
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A bulk `confirmText` such as `Set the amount to {{ amount | currency:EUR }} on {{ count }} records?` formatted its numbers and dates in `en-US` for every viewer, while the same action's single-record confirm formats them in the viewer's language. `BulkActionDialog` now reads `useDisplayLocale()`, the locale every date, number and currency renderer and the console's action runners read, and `bulkConfirmText` takes it as an optional third argument, handed to `fillTemplateHoles`'s locale channel apart from the template roots. A German viewer reads `1.234.567,50 €`; with no locale handed, the sentence formats in `en-US` as before.
11+
12+
What to do: nothing.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/plugin-approvals': patch
4+
'@objectstack/rest': patch
5+
'@objectstack/client': patch
6+
---
7+
8+
feat(plugin-approvals,rest,client): a record-anchored approval lock read, `{ locked }` for every caller who can read the record
9+
10+
Clause-②: yes (widening)
11+
Metadata-protocol: none
12+
13+
A member who could read and edit a record but was not on its approval was offered an edit the server then refused with `409 RECORD_LOCKED`. The lock's only carrier was the request row's `lock_record`, and that row is rightly not served to a non-participant unless the object turns on `approvalsVisibleToReaders`. The lock now has a carrier of its own.
14+
15+
- **`@objectstack/spec`.** `IApprovalService` gains the optional member `getRecordLock(object, recordId, context)`, which answers `{ locked: boolean }`, or `null` when the caller cannot read the record. No exported schema or type is added.
16+
- **`@objectstack/plugin-approvals`.** `ApprovalService.getRecordLock` reads the record as the caller. A record the caller cannot read answers `null`, the same answer as an id that does not exist. A caller with no read on the object is refused for every id, whether it exists or not. `locked` is `true` while a pending request's node has `lockRecord` other than `false`. The lock judgement is now one function, `nodeConfigLocksRecord` (not exported from the package), and the record-lock hook, the request row's `lock_record` and the new read all call it. Request content stays behind `approvalsVisibleToReaders`: the answer carries no request id, node, approvers or `pending` flag.
17+
- **`@objectstack/rest`.** New route `GET /api/v1/approvals/records/:object/:recordId/lock`. It answers `{ success: true, data: { locked } }`. A `null` answers `404 RECORD_NOT_FOUND` with one body, whether the record is unreadable or missing. A service without the member answers `501`. The route's closed query-parameter set is empty, so any query parameter is refused with a `400`. The route is ledgered as `sdk`.
18+
- **`@objectstack/client`.** `client.approvals.getRecordLock(object, recordId)` calls that route.

‎.github/workflows/ci.yml‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,21 @@ jobs:
472472
- '.github/workflows/**'
473473
- '.github/prompts/**'
474474
- 'skills/objectui/**'
475+
# turbo.json's `globalDependencies`: every build task hashes
476+
# them, so they move what the ui jobs build, the SDUI manifest's
477+
# regeneration included (#26137).
478+
- 'tsconfig.json'
479+
- 'tsconfig.base.json'
480+
- 'tsconfig.test.base.json'
481+
- 'tsup.config.ts'
482+
# The committed SDUI manifest, its record, its producer and its
483+
# offline gate. Build & E2E UI regenerates the manifest and
484+
# compares it with the committed bytes (#26137), so a diff that
485+
# moves only one of these still runs that comparison.
486+
- 'sdui.manifest.json'
487+
- 'scripts/sdui-manifest.record.json'
488+
- 'scripts/gen-sdui-manifest-node.mjs'
489+
- 'scripts/check-sdui-manifest.mjs'
475490
# The seven packages objectui installs from this tree (the set its
476491
# `.objectstack-pin` used to pack). A change here compiles and builds
477492
# the Console in the same PR — the check the retired Spec Shape Gate
@@ -3734,6 +3749,10 @@ jobs:
37343749
# - Build & E2E UI: n=51, min 106 s, median 170 s, p95 423 s, max 457 s
37353750
# (7.6 min) => max(22.9, 22.6) => 25, down from objectui's 30 plus the
37363751
# 15 the port added for the in-job build, which no derivation backed.
3752+
# Re-derived for the SDUI manifest regeneration (objectstack#26137),
3753+
# which adds 213 s cold (the 27 @object-ui builds plus the enumeration,
3754+
# measured on main 81bba6c3f): 457 + 213 = 670 s (11.2 min)
3755+
# => max(33.5, 26.2) => 35.
37373756
# Re-derive when a job's work changes; ui/scripts/__tests__/
37383757
# turbo-cache-save-bound.test.ts holds both values.
37393758
lint-ui:
@@ -4063,7 +4082,7 @@ jobs:
40634082
if: ${{ !cancelled() && needs.filter.outputs.ui_tree != 'false' && (needs.filter.outputs.ui != 'false' || needs.filter.outputs.ui_consumers != 'false') }}
40644083
runs-on: ubuntu-latest
40654084
# Derived above Lint UI.
4066-
timeout-minutes: 25
4085+
timeout-minutes: 35
40674086
permissions:
40684087
contents: read
40694088
env:
@@ -4122,6 +4141,30 @@ jobs:
41224141
--filter=@objectstack/sdui-parser... \
41234142
--filter=@objectstack/types...
41244143
4144+
# The repo-root SDUI manifest, regenerated from this tree and compared
4145+
# with the committed bytes (#26137). The generator builds the registry
4146+
# with turbo: the 36 tasks of the step above replay, and the 27
4147+
# @object-ui packages build (3m25s cold, measured on 4 cores, on main
4148+
# 81bba6c3f), then it enumerates the registry under plain Node (~8 s; ~6 s
4149+
# for the whole run when nothing moved). It serialises through the
4150+
# @objectstack/sdui-parser the step above built, so a parser edit moves
4151+
# this verdict too, which no committed digest does. On merge_group it runs
4152+
# on the queue's combined tree, the one run that sees two entries' effect
4153+
# on the manifest together. Only the artefact is compared: the records the
4154+
# generator rewrites beside it are held by `check-sdui-manifest` in
4155+
# lint.yml. The Console build below reads the registry through its src/
4156+
# aliases, so the dist this step builds does not reach it.
4157+
- name: The committed SDUI manifest is the one this tree generates
4158+
run: |
4159+
node scripts/gen-sdui-manifest-node.mjs
4160+
if ! git diff --quiet -- sdui.manifest.json; then
4161+
echo "::error file=sdui.manifest.json::sdui.manifest.json is not the manifest this tree generates. Run 'node scripts/gen-sdui-manifest-node.mjs' and commit what it writes."
4162+
git diff --stat -- sdui.manifest.json
4163+
git diff -- sdui.manifest.json | head -n 200
4164+
exit 1
4165+
fi
4166+
echo "sdui.manifest.json is byte-identical to this tree's regeneration."
4167+
41254168
- name: Build the Console for E2E
41264169
env:
41274170
VITE_BASE_PATH: /console/

‎.github/workflows/lint.yml‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -966,15 +966,13 @@ jobs:
966966
# repo-root `sdui.manifest.json` is absent or unreadable, so nothing in
967967
# the production path ever says "the gate lost its whitelist" — this step
968968
# is the thing that notices. Offline record checks only (existence, shape,
969-
# sha256 vs the provenance record, record pin vs `.objectui-sha`): the
970-
# regenerate-and-compare half runs where a regeneration can (the Node
971-
# generator reads objectui's BUILT tree at the pin, which only
972-
# `pnpm objectui:build` materialises — see
973-
# scripts/gen-sdui-manifest-node.mjs), and a pin bump goes red HERE until
974-
# it has been re-run — the same moment the lockstep step above forces the
975-
# parser parity re-verification, with an objectui checkout guaranteed in
976-
# hand. Unfiltered for the same reason as its neighbour: filtering to the
977-
# artefact would go dormant on exactly the PR that moves the pin.
969+
# sha256 vs the provenance record, and with no pin each registry
970+
# package's source digest vs its record): the regenerate-and-compare half
971+
# runs where the registry is built, as ci.yml's Build & E2E UI step "The
972+
# committed SDUI manifest is the one this tree generates" (#26137), on
973+
# pull_request and on the merge queue's combined tree. Unfiltered:
974+
# filtering it to the artefact would go dormant on exactly the PR that
975+
# moves the artefact's inputs and not the artefact.
978976
# Direct invocation per the GATE INVOCATION IDIOM at the top of this file.
979977
- name: SDUI manifest is present, intact and fresh at the objectui pin
980978
id: sdui-manifest-is-present-intact-and-fresh

‎content/docs/automation/approvals.mdx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,27 @@ never deadlock against itself. The exemption is keyed on run identity rather tha
376376
elevation, so a `runAs:'user'` run stays row-level-security scoped while it
377377
writes — it does not become a system write.
378378

379+
**Reading the lock.** Anyone who can read the record can ask whether it is
380+
locked, including a member who is not on the approval and so is served no
381+
request row:
382+
383+
```bash
384+
curl -b cookies.txt \
385+
"https://your-app.example.com/api/v1/approvals/records/expense/rec_123/lock"
386+
# → { "success": true, "data": { "locked": true } }
387+
```
388+
389+
The answer is `locked` and nothing else: no request, approvers or history,
390+
which stay behind the object's `approvalsVisibleToReaders` switch. `locked` is
391+
`true` while a pending request's node has `lockRecord` other than `false`. That
392+
is the same check the server applies when it refuses an edit with `409
393+
RECORD_LOCKED`, so a page that reads it never offers an edit the server will
394+
refuse. The record is read as the caller: a record they cannot read answers
395+
`404 RECORD_NOT_FOUND`, the same body a record id that does not exist gets. The
396+
SDK call is `client.approvals.getRecordLock(object, recordId)`, and the service
397+
member is `IApprovalService.getRecordLock`. An approvals service without that
398+
member answers `501`.
399+
379400
Only `approvers` is required on the node; everything else has a default
380401
(`behavior: 'first_response'`, `lockRecord: true`, `maxRevisions: 3`).
381402

0 commit comments

Comments
 (0)