diff --git a/.changeset/action-failure-inner-envelope.md b/.changeset/action-failure-inner-envelope.md deleted file mode 100644 index f695206a66..0000000000 --- a/.changeset/action-failure-inner-envelope.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(actions): a failed server action no longer reports as success (green toast) — objectstack#3913 - -`useConsoleActionRuntime.serverActionHandler` — the console's **main** action -path (list toolbars, row actions, page actions) — decided success from -`res.ok` and the OUTER envelope only: - -```ts -if (!res.ok || (json && json.success === false)) { /* failure */ } -``` - -A server older than objectstack#3913 reports a handler failure as HTTP **200** -with the failure nested one level down: - -```json -{"success":true,"data":{"success":false,"error":"Action 'log_call' on object '*' not found"}} -``` - -Both guards pass, so the action was reported as completed: the ActionRunner -fired its green "completed" toast, the list refreshed, and the real error was -swallowed. `RecordDetailView`'s copy of the same handler already inspected the -inner envelope; the shared runtime now does too, and the marketplace install -call (`marketplaceApi.installPackage`), which had the identical hole and could -report a package as installed when it was not. - -Current servers answer a failed action with a real HTTP status, which `!res.ok` -catches first — the inner-envelope check is what keeps the console honest -against a runtime that has not been upgraded yet. - -**Also fixed:** with objectstack#3913 the failure body is -`{success: false, error: {message, code}}`. `RecordDetailView` read `json?.error` -raw and would have handed that **object** to `toast.error()` as a React child, -crashing the page (React #31) — the exact failure the console runtime's -`errorDetail` helper existed to prevent. That helper is now a shared util -(`utils/actionErrorDetail`) and both call sites go through it, so a nested -`{message}` always resolves to a string. diff --git a/.changeset/action-key-inventory-and-unknown-key-warning.md b/.changeset/action-key-inventory-and-unknown-key-warning.md deleted file mode 100644 index 3cc7afd249..0000000000 --- a/.changeset/action-key-inventory-and-unknown-key-warning.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -"@object-ui/core": minor ---- - -feat(core): inventory `ActionDef`'s keys and warn on the ones nothing reads — objectstack#4075 step 1 - -`ActionDef` ends with `[key: string]: any`, so it accepts any key of any type. -Deleting `ActionDef.execute` produced **zero** compile errors even though the -field had just been removed (objectui#2990), and stale metadata still authoring -`execute: 'markDone'` type-checks today. The same deletion against -`@object-ui/types`' `ActionSchema` — which has no index signature — correctly -produced `TS2353` at the authoring site. One of the two readers can catch a -retired key; the other is structurally incapable, which is how a typo (`targt`) -and a tombstoned key both reach a runner that then silently does nothing. - -This is the non-breaking first step of the staged narrowing: it makes the key set -**visible** and warns on anything outside it, without changing a single type. - -New exports from `@object-ui/core`: - -- `ACTION_DEF_KEYS`, `SPEC_ACTION_KEYS`, `NAVIGATION_ALIAS_KEYS`, - `RETIRED_ACTION_KEYS`, `KNOWN_ACTION_KEYS` — the inventory. -- `classifyActionKeys(action)` — splits an action's own keys into `unknown` and - `retired`. -- `warnOnUnknownActionKeys(action)` — dev-mode only, warn-once. Called by - `ActionRunner.execute`, so no consumer wiring is needed. - -A retired key gets a louder, more specific warning than an unknown one: an -unknown key is probably a typo, a retired key is metadata that used to work. -`execute` is not simply gone from the spec — it is a live **tombstone**, still -present in `ActionSchema` so the parser can reject it by name with the rename -prescription. - -Nothing is rejected and no types changed, so existing metadata behaves exactly as -before. Promoting the legitimate keys to explicit optional fields, then removing -the index signature so `tsc` catches both typos and retired keys, are steps 2 and -3 of objectstack#4075. diff --git a/.changeset/action-required-permissions-ui-gate.md b/.changeset/action-required-permissions-ui-gate.md deleted file mode 100644 index 7c65ceda71..0000000000 --- a/.changeset/action-required-permissions-ui-gate.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -"@object-ui/app-shell": patch -"@object-ui/components": patch -"@object-ui/plugin-detail": patch -"@object-ui/plugin-grid": patch -"@object-ui/react": patch ---- - -fix(actions): apply the ADR-0066 D4 capability gate on every action surface (framework#3923) - -An action declaring `requiredPermissions` is supposed to be one declaration with -two enforcement surfaces: 403 on the server, hidden button in the UI. The UI half -only ever ran inside `ActionEngine.getActionsForLocation` — and the surfaces -`record_header`, `record_more`, `list_item` and `list_toolbar` actually render on -do not go through the engine. They filter their own action lists. So a button -declaring a capability nobody holds rendered, live and clickable, on the record -header, in every grid row menu, and on the list toolbar. For a `type: 'api'` -action pointed at a self-authored endpoint, nothing else was checking either: the -platform's action route (which is where the 403 comes from) never sees that -request. - -`page:header`, `action:bar` (business *and* `systemActions`) and the grid's -`RowActionMenu` now apply the same gate, via a shared `useCapabilityGate()` so -the surfaces cannot drift apart. The rule is the engine's, unchanged: hide unless -the caller holds **all** declared capabilities; an empty held set is "holds -nothing" and gates; **unknown** — no action runtime, no resolved capabilities — -fails OPEN, because the server is the authority and hiding a permitted user's -button on missing client data is the worse failure. - -The record surface was also feeding the gate nothing to work with. -`RecordDetailView` mounts its own ``, which shadows the shell's -for every action on that page, and seeded it with identity only — no -`systemPermissions`. Since unknown fails open, that alone un-gated every -`record_header` / `record_more` / `record_section` action on the one page those -locations exist on. It now forwards the caller's resolved capabilities (and only -once they have actually resolved, so a standalone embed without a -`PermissionProvider` keeps failing open rather than hiding everything). - -`useRecordEditable`'s record-level explain probe went out on a bare -`fetch(..., { credentials: 'include' })`. A bearer-token session carries its -credential in the `Authorization` header, not a cookie, so the probe came back -401 on a perfectly valid admin session and the verdict silently failed open — -the hook was inert in exactly the deployments it was written for. It now rides -the host's authenticated fetch (`SchemaRendererProvider`'s `apiFetch`), falling -back to the global one for standalone embeds. diff --git a/.changeset/action-target-is-the-only-handler-slot.md b/.changeset/action-target-is-the-only-handler-slot.md deleted file mode 100644 index 7d13129254..0000000000 --- a/.changeset/action-target-is-the-only-handler-slot.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/core': minor -'@object-ui/components': minor -'@object-ui/app-shell': minor ---- - -**`target` is the only action handler slot — the `execute` alias is gone from the renderer (framework#3856).** - -`ActionRunner.executeScript` read `action.target || action.execute`. That fallback -is unreachable against `@objectstack/spec` 17: `execute` is now a tombstoned key -(framework#3855) that the parser **rejects** with the rename prescription, so no -parsed action can carry it and the `||` could only ever yield `target`. Verified -against 17.0.0-rc.0 — an action declaring `execute` fails `ActionSchema.safeParse`, -and a `target` action's parsed output has no `execute` key at all. - -Deleted rather than left as harmless residue: two handler slots is what let one -action run one script server-side and a different one client-side (framework#3713, -where this renderer preferred the alias while the spec transform preferred -`target`). A dead slot still reads as a live contract to the next maintainer. - -`execute` is also **removed from the types**, which is the part that had never -landed. framework#3856 predicted a compile error here; there wasn't one, because -neither reader was typed against the spec's `z.infer`: - -- `@object-ui/types` `ActionSchema` hand-declared `execute?: string`. Removed, so - `execute: '…'` now fails `tsc` at the authoring site (TS2353). -- `@object-ui/core` `ActionDef` hand-declared it too. Removed — but `ActionDef` - carries a `[key: string]: any` index signature, so stale hand-authored metadata - that never passed through the parser still compiles. For that path - `executeScript` now returns the rename prescription instead of a bare - "No script provided", matching the spec tombstone's rule that removing an - authorable key must be audible: silently binding no handler is the - "Mark Done does nothing" shape (framework#2169). - -The four action renderers (`action:button`, `action:icon`, `action:menu`, -`action:group`) no longer forward `execute` into the runner, and Studio's -`ActionPreview` no longer falls back to it — previewing an alias-only draft as -"bound" contradicted the parse that rejects it on save. - -Requires `@objectstack/spec` 17. Metadata still on the alias is rewritten by -`os migrate meta --from 16`. diff --git a/.changeset/action-url-identifies-by-name.md b/.changeset/action-url-identifies-by-name.md deleted file mode 100644 index 1e7ea28e80..0000000000 --- a/.changeset/action-url-identifies-by-name.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -**[ADR-0110 D1] The server-action URL identifies an action by `name`, not `target`.** - -`serverActionHandler` posted `action.target || action.name` — the handler's -registration KEY — to `/api/v1/actions/:object/:action`. For a target-bound -action (`{ name: 'complete_task', target: 'completeTask' }`) the server resolves -the declaration by name, so posting the target meant it resolved **no** -declaration and silently skipped both the ADR-0066 D4 capability gate and the -ADR-0104 param contract: a Console button correctly hidden from users without -the capability posted to an endpoint that accepted anyone (framework#3935). - -`target` is a binding expression — a handler key here, a flow id for -`type: 'flow'`, a URL for `type: 'url'`, `${param.X}`-interpolatable, and -legitimately non-unique — so it can never identify a declaration. The URL now -carries `action.name`, and the server derives the handler key from the -declaration it resolves. An action with no `name` is refused rather than -falling back to `target`. - -`apiHandler` and `flowHandler` are unchanged: their `target` genuinely is the -endpoint / flow id they dispatch on. - -Requires a framework with the ADR-0110 handler-key rotation (protocol 17); the -two ship in lockstep. diff --git a/.changeset/action-vocabularies-derive-from-spec.md b/.changeset/action-vocabularies-derive-from-spec.md deleted file mode 100644 index ddb305be90..0000000000 --- a/.changeset/action-vocabularies-derive-from-spec.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**The action sub-vocabularies derive from `@objectstack/spec` instead of restating it (framework#4074).** - -`packages/types/src/ui-action.ts` imported exactly one of the spec's action -vocabularies — `ActionType`, derived in #2231/#2901 — and hand-declared the rest -under doc comments claiming spec canonicity. `ActionLocation`'s comment said -"Single source of truth lives in `@objectstack/spec/ui` … **re-export** here" -while the code re-*declared* a parallel union, `as const` tuple, and `z.enum`. - -That is why framework#3856 predicted a compile error when spec 17 removed -`action.execute` and there wasn't one: nothing in this package was bound to the -spec's `z.infer`, so a key removal upstream produced no signal here. - -**Already drifted, not merely drift-prone.** `ActionParamSchema.type` is -`FieldType.optional()` and `FieldType` carries **49** members; the hand-written -`ActionParamFieldType` listed **16**. A spec-valid param typed `lookup`, -`multiselect`, `currency`, `user`, `tags` or `json` failed `tsc` against this -package even though `ActionParamDialog` renders it — the same failure `ActionType` -had before it was derived (missing `form` while `ActionRunner.executeForm` -implemented it). - -- `ActionLocation` / `ACTION_LOCATIONS` / `ActionLocationSchema` are now the spec's - own three symbols, re-exported. `ACTION_LOCATIONS` and `ActionLocationSchema` - stay **value** exports, as #2561 decision (a) explicitly keeps them. -- `ActionComponent` is `NonNullable`. Read off the spec's - resolved `Action` rather than `ActionSchema.shape.component`, because spec - exports `ActionSchema` as a `lazySchema` proxy that does not forward `.shape`. -- `ActionParamFieldType` is the spec's `FieldType` (16 → 49 members), with - `ACTION_PARAM_FIELD_TYPES` as its runtime witness. -- `ActionParam` gains the 13 optional capability fields it could not express — - `visible`, `accept`, `maxSize`, `multiple`, and the lookup-picker group - (`referenceTo`, `displayField`, `idField`, `descriptionField`, `titleFormat`, - `lookupColumns`, `lookupFilters`, `lookupPageSize`, `dependsOn`) — all of which - `@object-ui/core`'s `ActionParamDef` already declares and app-shell's - `paramToField.ts` maps into the shared field renderer (ADR-0059). - -**The legacy param spellings are now named, not hidden.** `paramToField.ts` folds -`checkbox` → `boolean`, `reference` → `lookup`, `datetime-local` → `datetime`. -None is a spec `FieldType`, so deriving `ActionParamFieldType` alone would have -made authored metadata a type error. They are declared as -`ObjectUiLocalParamFieldType` / `OBJECTUI_LOCAL_PARAM_FIELD_TYPES` and -`ActionParam.type` accepts `ResolvableParamFieldType` (spec ∪ local) — the same -shape `ObjectUiLocalActionType` / `RunnableActionType` already use for -`navigation`, and for the same reason: a dialect hidden inside a -`Record` in another package is invisible to an importer. - -**Breaking:** `ActionParamFieldType` widens from 16 members to 49, so an -exhaustive `switch` over a param `type` in a host app stops being exhaustive. The -16 old members are all still valid, so no authored metadata breaks. The added -`ActionParam` fields are optional and additive. - -Not included, and still open on framework#4074: `ActionParam`'s `name` / `label` / -`type` stay required where the spec makes them optional, and the -`field` / `objectOverride` field-reference form remains unrepresentable. Both are -breaking in a way that needs its own migration note. diff --git a/.changeset/actions-envelope-single-source.md b/.changeset/actions-envelope-single-source.md deleted file mode 100644 index 152cf2ae9d..0000000000 --- a/.changeset/actions-envelope-single-source.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(actions): one source for the `/actions` envelope rule, and `redirectUrl` finally works (objectstack#3913 follow-up) - -The `/actions` response wraps **twice** — the route's own `{success, data}` -inside the dispatcher's — and a failure has three shapes, only one of which -`res.ok` catches. That rule was hand-rolled in two places -(`useConsoleActionRuntime.serverActionHandler` and `RecordDetailView`'s copy of -the same handler), and the two drifted. Four hand-rolled copies produced three -distinct bugs: - -1. **A failed action reported as success** — the copy that didn't inspect the - inner envelope was the console's *main* action path, so a failure fired the - green "completed" toast on every list and page surface (fixed in #2963). -2. **React #31 crash** — the nested `{message, code}` object handed to - `toast.error()` as a React child (fixed in #2963). -3. **`redirectUrl` never fired** — *fixed here.* - -Both handlers now call `interpretActionResponse` from `utils/actionResponse`, -and a ratchet test (`actions-envelope.ratchet.test.ts`) fails if a third -hand-rolled copy appears. - -## `redirectUrl` was unreachable - -A script action can return `{ redirectUrl: 'https://…' }` to ask the console to -open a URL. Both handlers read it off `body.data` — the **action** envelope, -one level too shallow: - -``` -{ success: true, data: { success: true, data: { redirectUrl: '…' } } } - ^^^^ read here ^^^^ actually lives here -``` - -`body.data` is constructed by the server and only ever holds `success` / `data`, -so `body.data.redirectUrl` was **always** undefined — the convention could never -fire, and no handler could work around it. An `opensInNewTab` action was worse -than a no-op: it pre-opens a tab on a spinner page for popup-blocker safety, and -with no redirect to drive it to, that tab sat on the spinner forever. - -`ActionResult.data` still carries the **action envelope**, unchanged — some -`resultDialog` field paths in the wild may have adapted to that depth, so it is -not silently re-pointed here. diff --git a/.changeset/actions-single-wrap-3962.md b/.changeset/actions-single-wrap-3962.md deleted file mode 100644 index 478d41f031..0000000000 --- a/.changeset/actions-single-wrap-3962.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(actions): read objectstack#3962's single-wrapped /actions responses; legacy double wrap detected narrowly - -objectstack#3962 made `/actions` failures speak HTTP (400 rejection / 404 / 403 -/ 503 / 500) and single-wrapped success — `body.data` IS the handler's return -value. `interpretActionResponse` / `readActionPayload` now treat that as the -primary shape: the pre-#3962 double envelope is detected NARROWLY (a boolean -`success` and no keys beyond the envelope's own) and unwrapped for older -runtimes, so a handler value that merely contains a `success` key is -handler-owned and passes through untouched. `ActionResult.data`'s depth quirk -self-heals on #3962 servers. diff --git a/.changeset/agent-catalog-envelope-tolerance.md b/.changeset/agent-catalog-envelope-tolerance.md deleted file mode 100644 index b4b1f8ffea..0000000000 --- a/.changeset/agent-catalog-envelope-tolerance.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch ---- - -**Read the agent catalog in the declared envelope too, before the server converts.** - -`GET /api/v1/ai/agents` is served by two producers — the framework dispatcher's -degraded fallback when no AI service is registered, and cloud's `service-ai` — and -it is one of the last SDK-addressable routes still answering outside the platform's -declared `{ success: true, data }` envelope (objectstack#4053). `useAgents` read -only `{ agents }` and a bare array, so the day either producer converts, the parse -would miss. - -That miss is unusually dangerous on this particular route, which is why it is worth -getting ahead of rather than fixing after. The catalog is not just data: -`useAiSurfaceEnabled` gates the **entire AI surface** on `agents.length > 0`, -because the route is access-filtered per caller and is therefore the only signal -that is both edition- and user-aware (ADR-0068). An empty list is the correct -answer for a seat-less user or a Community-Edition deployment with no `service-ai` -— so a parse miss and the legitimate hidden state are **indistinguishable**: no -error, no 403, no log, just the FAB, the top-bar link and the designer's "Ask AI" -quietly gone for everyone. - -`extractAgentList` now folds all four shapes to the same list — a bare array, -`{ agents }`, `{ success: true, data: [...] }`, and `{ success: true, data: -{ agents } }` — detecting the envelope the way `ObjectStackClient.unwrapResponse` -does (a **boolean** `success`), so the two readers cannot disagree about what -counts as one. Nine tests cover it; reverting to the previous two-shape read fails -five of them. - -No behaviour change against any server shipping today: the shapes that worked -before still parse identically. This only removes the lockstep requirement, so the -server side can convert on its own schedule. diff --git a/.changeset/analytics-capability-absent-hint.md b/.changeset/analytics-capability-absent-hint.md deleted file mode 100644 index 52e010c13c..0000000000 --- a/.changeset/analytics-capability-absent-hint.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -"@object-ui/data-objectstack": minor -"@object-ui/app-shell": patch ---- - -fix(analytics): a missing analytics capability no longer renders as an empty KPI — objectstack#3891 - -The framework retired its degraded in-kernel analytics fallback (objectstack#3891): -it dropped the caller's RLS/tenant scope and ignored the contract filter, so it -answered `200` with over-broad numbers. `@objectstack/service-analytics` is now -the only implementation, and a deployment without it answers `404` on -`/analytics/query` (objectstack#4019 stops mounting the routes) or `501` on -`/analytics/dataset/query`. - -Three things were wrong on this side of that boundary: - -**① A KPI on such a deployment rendered a confident zero.** `aggregate()`'s -`catch` promises a client-side fallback, and the fallback is correct — but the -adapter never got there for the most likely failure. It now classifies the -failure (`classifyAnalyticsFailure`) instead of treating every error alike: -capability-absent (404/501) degrades to a client-side aggregate over a -**server-scoped** `find()` — same rows, same filter, RLS still applied — and -says so **once per adapter** in the console, naming the package to install, -rather than once per widget or not at all. - -**② A rejected query was answered with plausible numbers.** The framework -validates `/analytics/query` at the entry now (objectstack#4010), so a `400 -VALIDATION_FAILED` means *this adapter* sent an off-contract body. Degrading -there would bury our own bug behind output from a different code path — the -misdirection objectstack#3878 documented. It now throws -`AnalyticsQueryRejectedError` and never falls back. Transient failures (5xx, -network) degrade exactly as before. - -**③ The dataset preview blamed the author for a missing capability.** -`queryDataset` mapped `501`/`404` to `Dataset query failed: 501 Not Implemented -— …`; it now throws the typed `AnalyticsNotInstalledError` -(`code: 'ANALYTICS_NOT_INSTALLED'`) with a message a UI can render verbatim, and -`DatasetPreview` shows it as a "analytics capability not installed" empty state -instead of a red error banner. A real compile error (e.g. "relationship not -declared in include") keeps its server detail and its banner. - -New exports from `@object-ui/data-objectstack`: `AnalyticsNotInstalledError`, -`AnalyticsQueryRejectedError`, `isAnalyticsNotInstalledError`, -`classifyAnalyticsFailure`. diff --git a/.changeset/approval-band-truth-and-write-warnings.md b/.changeset/approval-band-truth-and-write-warnings.md deleted file mode 100644 index 7f5aa3350e..0000000000 --- a/.changeset/approval-band-truth-and-write-warnings.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -"@object-ui/plugin-detail": minor -"@object-ui/app-shell": minor -"@object-ui/data-objectstack": minor -"@object-ui/i18n": minor ---- - -fix(detail): finish the approval-lock story, and warn on silently stripped fields (framework#3794) - -The Console reported record writability wrong in both directions during an -approval, so a user had nothing to go on: what they *could* edit said "locked", -and what they *couldn't* said "updated successfully". - -**The lock band told the truth; the Edit button did not.** objectui#2902 split -the band into "in approval · editable" vs locked, but the header **Edit** CTA -still keyed off nothing at all — on a genuinely locked record it stayed live, so -the user opened the form, filled a screen, and got `RECORD_LOCKED` back on Save. -It is now `disabled` on a locked record: visible-but-off, with the band beside it -saying why. This is the LOCK, not the mere presence of an approval — a -`lockRecord: false` node keeps Edit live, which is the point of that setting. - -**And the band could still re-lock itself.** `DetailView` OR-ed the record's own -`approval_status` mirror into `isLocked` unconditionally. That mirror is written -on submit by any flow configuring an `approvalStatusField`, *regardless of* -`lockRecord` — so on a `lockRecord: false` node the host correctly resolved "not -locked" from the request's `lock_record` while the mirror dragged the band back -to "Locked for approval", with the pencils live and saves landing underneath it. -The host is now authoritative whenever it threads `approvalPending`; the mirror -is consulted only for bare/legacy `DetailView` hosts that thread nothing, where -it still reads as locked (no node granularity — the safe direction). - -Recall's tooltip no longer promises to unlock a record the node never locked -(`detail.cancelApprovalTooltipUnlocked`). - -**Silently stripped fields now surface on the record form's save path.** The -adapter emitted a write-warning for `create`/`update` responses carrying -`droppedFields`, but not for `batchTransaction` — which is how the record form -saves a master-detail record, i.e. the one surface where a user actually edits a -`readonlyWhen`-locked field. `batchTransaction` now emits one warning per event, -resolving each back to its operation via the response's `index`. - -The toast itself was hardcoded English and called every strip "read-only". It is -now localized (`detail.writeStripped*`, ten locales) and worded by reason: -`readonly_when` says the field is not editable *in this record's current state*, -which is what actually happened — the field is editable in other states and the -form rendered it as an ordinary input, so "read-only" sent the user hunting for a -permission problem that does not exist. - -**And it stopped crying wolf.** `createObjectStackUserStateAdapter` hand-stamped -the server-managed `updated_at` on every recents/favorites write, which the -server strips and reports — so the console popped "Some fields were not saved" -about a field no user ever touched, on page loads, drowning the signal the toast -exists for. It no longer sends the column; the server stamps it anyway. diff --git a/.changeset/approvals-reassign-and-system-actor-display.md b/.changeset/approvals-reassign-and-system-actor-display.md deleted file mode 100644 index 3ba1e22f97..0000000000 --- a/.changeset/approvals-reassign-and-system-actor-display.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@object-ui/console": patch -"@object-ui/app-shell": patch -"@object-ui/i18n": patch ---- - -fix(console,app-shell): readable reassign hand-off + "System" label for svc:* audit actors — objectstack#4365 / objectstack#4366 - -- **Approvals inbox** (`ApprovalsInboxPage`): a reassign timeline entry now - renders "from A to B" from the structured - `reassign_from`/`reassign_to` fields (and their server-resolved - `*_name` companions) that objectstack#4365 added to - `sys_approval_action`, instead of relying on the old default comment that - baked two raw user ids into user-facing text. Legacy rows without the - structured fields keep the comment fallback. New i18n key - `approvalsInbox.reassignFromTo` across all ten locales. -- **Record history** (`RecordDetailView`): an audit row attributed to a - service principal (`svc:*` on the `actor` column — e.g. a - `runAs:'system'` flow's `svc:flow:` label from objectstack#4366) now - renders the localized "System" label instead of the raw principal string; - the raw value stays on the entry for tooling. diff --git a/.changeset/attachments-beside-discussion-and-i18n.md b/.changeset/attachments-beside-discussion-and-i18n.md deleted file mode 100644 index 78ef4f1898..0000000000 --- a/.changeset/attachments-beside-discussion-and-i18n.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -"@object-ui/app-shell": patch -"@object-ui/plugin-detail": patch -"@object-ui/components": patch -"@object-ui/i18n": patch ---- - -fix(detail): record Attachments become their own tab (with count badge) and their copy is translated — objectstack#4358 - -Two defects on `enable.files: true` record detail pages: - -1. **Buried placement.** `RecordDetailView` appended `RecordAttachmentsPanel` - AFTER the schema-rendered page tree, whose synthesized default embeds - `record:discussion` as the last main component — so the panel always - landed below an ever-growing feed timeline, undiscoverable without - scrolling to the very bottom, with no metadata knob to move it. - - `buildDefaultTabs` now emits a peer **Attachments** tab (a new - `record:attachments` node rendered by an app-shell registration wrapping - the existing panel via RecordContext) between Related and - Activity/History. `PageTabsRenderer` derives the tab's count badge from a - `sys_attachment` probe scoped to `(parent_object, parent_id)`, riding the - same RelatedCountStore cache/invalidation bus as related-list badges — so - uploads and deletes update the badge live. A `hideAttachments` synthesizer - option suppresses the tab; RecordDetailView keeps its legacy bottom append - only as the fallback for authored pages without the node - (`hasExplicitAttachments`). - -2. **Untranslated copy.** The panel's eleven `detail.*` keys (`attachments`, - `uploadAttachment`, `loadingAttachments`, `noAttachments`, - `downloadAttachment`, `deleteAttachment`, and the five - `attachment*Denied/Required` friendly errors) existed only as inline - English `defaultValue`s — no locale bundle carried them, so non-English - consoles always showed English. All ten locales now define them; the tab - label rides the existing well-known-label dictionary (→ 附件 etc.). diff --git a/.changeset/authoring-types-are-input-types.md b/.changeset/authoring-types-are-input-types.md deleted file mode 100644 index 721649fe6d..0000000000 --- a/.changeset/authoring-types-are-input-types.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/core': patch -'@object-ui/components': patch ---- - -**Authoring types are input types (framework#4074 steps 2–3): `ActionParam` takes the spec's declaration forms, `ListViewSchema` stops promising parse-output defaults, and `FormField.dependsOn` matches its runtime reader.** - -Three public types said something different from what the platform accepts. All -three divergences were found by making `packages/types`' tests compile (#3009) -and then resolving the declared `p1-spec-alignment.test.ts` debt site-by-site -instead of papering over it. - -**`ActionParam` is now the authoring shape, aligned with the spec's input.** -`name` / `label` / `type` become optional and `field` / `objectOverride` appear: -the spec's primary way to declare a param — a bare field reference that inherits -label/type/validation/options from an object field — was unrepresentable while -all three were required. The *resolved* shape the dialog consumes (after -app-shell's `resolveActionParams()` inlines the reference) remains -`@object-ui/core`'s `ActionParamDef`, with all three required. Authoring and -resolved are different types on purpose. `label` and option labels take the -spec's `I18nLabel` by import — which the new compile-time guard promptly -revealed to be aliased to plain `string` in the current spec (the per-locale -record is the separate `I18nObject`), so this is not a behavioural widening -today; importing the alias means objectui tracks any future widening -automatically. - -**Breaking:** code destructuring `param.name` / `param.label` / `param.type` as -guaranteed must now handle the field-backed form (or consume the resolved -`ActionParamDef` instead, which is what dialog-side code should be doing). - -**`ListViewInferred` is `z.input`, not `z.infer`.** The spec sub-schemas that -flow into the list-view surface (`userActions`, `tabs` → `ViewTab`, `sharing`) -carry `.default()`s, so the inferred output type made fields like -`userActions.refresh` or a tab's `pinned`/`visible` *required* — but nothing on -the render path ever runs `.parse()`: `normalizeListViewSchema` deliberately -applies no defaults ("an absent flag stays absent", its own suite). The output -type therefore rejected valid authored metadata (`userActions: { sort: true }`) -while promising renderers defaults that never arrive. Typing the surface as -input matches both the author and the runtime object. Code that *trusted* those -phantom defaults now gets an optionality error — which is a latent bug surfacing, -not a regression: the value really could be absent. - -**`FormField.dependsOn` is `DependsOnInput`.** The runtime reader -(`resolveCascadingOptions`) has always accepted a bare name, a list of names, or -lookup-parameter entries `{ field, param }` — its parameter type says so. The -public property said `string`, so array-authored metadata type-errored while -working, and the form renderer read the key through `(f as any).dependsOn` to -get past its own type. The shape now lives in `@object-ui/types` (single source -of truth next to `FormField`), `@object-ui/core` imports and re-exports it, and -the two `as any` reads in the components form renderer are typed. - -**The `p1-spec-alignment.test.ts` exclusion is gone.** Its 14 errors resolved: -the two "sharing in ObjectUI format" tests and the legacy-ARIA-spelling fixture -are deleted/rewritten — those dialects are *normalizer input*, folded by -`normalizeListViewSchema` and asserted branch-by-branch in core's -`normalize-list-view.test.ts`, the seam where the fold actually runs; asserting -them on the canonical type only ever "passed" because nothing compiled the file. -One fixture claimed a shape no surface ever admitted (an ObjectQL triplet as a -spec `ViewTab.filter`) and was corrected to the rule-object form. Every test -file in `@object-ui/types` is now compiled, with no exclusions. - -Discrimination-checked: reverting `ListViewInferred` to `z.infer`, `dependsOn` -to `string`, or `ActionParam.name` to required each produces the expected -compile error in the now-compiled test files (`TS2739` / `TS2322` / `TS2741`); -restored, all projects are clean. diff --git a/.changeset/bulk-dialog-shared-field-widgets.md b/.changeset/bulk-dialog-shared-field-widgets.md deleted file mode 100644 index 5c1b034348..0000000000 --- a/.changeset/bulk-dialog-shared-field-widgets.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -"@object-ui/plugin-grid": patch ---- - -fix(grid): bulk-action params render the shared form field widgets — a failed lookup fetch shows an error + Retry instead of a permanent "Loading…" (#3064, ADR-0059) - -`BulkActionDialog`'s hand-rolled param controls (a 2026-05 MVP predating the -PeoplePicker and ADR-0059) are replaced by the same field-widget renderer the -object form and `ActionParamDialog` use, via a new pure `bulkParamToField()` -adapter + `getLazyFieldWidget()`: - -- `lookup` params get the real searchable `LookupField` (server-side search, - record-picker dialog, loading/error/empty states owned by `useRecordQuery`); - a `sys_user` target — or a `user`-typed param — is promoted to the form's - search-first PeoplePicker (avatar + subtitle rows, recents, banned users - excluded). Every other param type (date/datetime/boolean/select/multiselect/ - textarea/number/…) renders its form widget too, so param support can no - longer drift behind the form surface. -- The #3064 failure pipeline is gone by construction: no more eager - `find($top:200)` prefetch on open, no error swallowed into an empty option - list rendering as permanent "Loading…", and no per-param failure cache — - reopening or Retry refetches. -- Preserved semantics: #2204 schema-fallback multi-value detection, required - gating, #2185 nested-popper dismissal guard, and human-readable confirm-step - labels (now resolved per selected id via `findOne`, replacing the removed - candidate prefetch). diff --git a/.changeset/bulk-enabled-is-a-tombstone.md b/.changeset/bulk-enabled-is-a-tombstone.md deleted file mode 100644 index 695e57d962..0000000000 --- a/.changeset/bulk-enabled-is-a-tombstone.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -"@object-ui/plugin-grid": patch -"@object-ui/types": patch -"@object-ui/app-shell": patch ---- - -fix(grid): drop the `bulkEnabled` derivation — the spec key is a tombstone - -Follow-up to objectui#3002 / #3031. That change folded two sources into the -selection bar: a view's `bulkActions` names resolved against -`objectDef.actions`, and object actions declaring `ActionSchema.bulkEnabled`. -The second source is dead. - -`@objectstack/spec` 17.0.0 retired `action.bulkEnabled` in the #3896 audit -close-out (framework#4054, landed while #3031 was in flight — the spec source -still carried the key when its design was settled). It is now a `retiredKey()` -tombstone, so it is not merely ignored: `defineStack` **hard-rejects** a config -that sets it, and the backend refuses to boot. Browser verification against a -real showcase backend is what surfaced this — the derivation branch could never -run, and #3031's changeset pointed authors at a key that breaks their app. - -The tombstone's own prescription is the path that survives: - -> the multi-select toolbar is driven by the LIST VIEW's `bulkActions` / -> `bulkActionDefs`, never by this flag … declare the action in the view's -> `bulkActions` instead. - -So `resolveBulkActions` now folds exactly two vocabularies — inline-authored -`bulkActionDefs`, and `bulkActions` names promoted to their declared object -action — which is what #3031's other half already did and what the end-to-end -run exercised: naming `showcase_mark_done` in the view's `bulkActions` issued -one `POST /api/v1/actions/showcase_task/showcase_mark_done` per selected -record (10/10 → `done: true, progress: 100` server-side). Everything downstream -of the fold is unchanged: promoted defs still carry the action's label, icon, -`visible`, confirm text and params; still run through `BulkActionDialog` -(params → confirm → progress → result); still dispatch per record with -`_rowRecord` attached; still attribute failures per record. - -A stale `bulkEnabled: true` on an object action is now inert rather than a -second path into the bar. Note tsc cannot catch this class of drift here — the -fold reads a loosely-typed `NamedActionDef` with an index signature, so the -retired key never surfaces as `never`. diff --git a/.changeset/bulk-selection-reset-both-sources.md b/.changeset/bulk-selection-reset-both-sources.md deleted file mode 100644 index 155a0b627e..0000000000 --- a/.changeset/bulk-selection-reset-both-sources.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@object-ui/plugin-grid": patch ---- - -fix(grid): a bulk delete / by-name action clears the row checkboxes, not just the toolbar — objectui#3056 - -After a successful bulk delete (or a bulk action dispatched to a -consumer-registered runner handler), the selection toolbar vanished but every -row stayed visibly ticked. The user was left on a page of selected rows with no -toolbar to act on them, and no way back except unticking each row or reloading. - -`ObjectGrid` carries two selection sources that must move together: -`selectedRows` (ours — drives the toolbar) and the data-table's internal -`selectedRowIds` (drives the checkboxes, cleared only when the host bumps -`selectionResetKey`). `handleBulkDialogClose` reset both; `dispatchBulkAction` -reset only the first, on both of its branches. - -Both now go through one `resetSelection()` helper — including the dialog path, -so the invariant is structural rather than three call sites remembering to -agree. Failure semantics are untouched: a by-name action that reports -`success: false` still keeps the toolbar AND the checkboxes so the user can fix -the cause and retry the same rows. diff --git a/.changeset/bulk-visible-per-record.md b/.changeset/bulk-visible-per-record.md deleted file mode 100644 index 738e818c1b..0000000000 --- a/.changeset/bulk-visible-per-record.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -"@object-ui/plugin-grid": patch -"@object-ui/i18n": patch ---- - -fix(grid): a bulk action's `visible` is evaluated per selected record — objectui#3067 - -The selection bar evaluated a def's `visible` against the ambient scope with no -record bound. That does not fail open, it answers wrongly: with no `record` in -scope the lenient evaluator returned `true` for **every** row-scoped predicate, -including the ones that should be false — `${record.done}` and -`${record.owner == user.id}` both came back `true`. An authored gate was not -weakened, it was inverted for half its inputs, and nothing distinguished that -from a real verdict. - -`visible` is now evaluated **once per selected record, with that record in -scope**, fail-closed per record and warning once on a fault — the same contract -the row kebab uses. One evaluation answers both questions: - -- **Is the button offered?** When at least one selected record passes. A - record-free predicate (`features.x`, `current_user.y`) returns the same - verdict for every row, so it still behaves as a plain button-level gate — no - syntactic sniffing for `record` references is involved. -- **Which records does it run on?** The ones that passed. The confirm step - states how many were skipped, so a run over fewer records than the user - ticked says so instead of quietly shrinking the selection. - -Eligibility is re-applied to the EXPANDED set after "select all N matching", -not just the page selection the button could see. - -The mechanism predates objectui#3002, but only inline-authored -`bulkActionDefs[].visible` used to reach it — written by authors who knew there -was no record. #3031 began promoting object actions into the bar, and their -`visible` is typically written for a row/record surface, which is what put -row-scoped predicates in front of a record-free evaluation. diff --git a/.changeset/canonical-to-builder-follows-2942.md b/.changeset/canonical-to-builder-follows-2942.md deleted file mode 100644 index 7dc0c034b8..0000000000 --- a/.changeset/canonical-to-builder-follows-2942.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -"@object-ui/plugin-view": patch ---- - -fix(view): the spec→FilterBuilder map follows the four operators #2942 added - -`CANONICAL_TO_BUILDER` mapped `starts_with`, `ends_with`, `is_null` and -`is_not_null` to `null`, with a comment asserting the FilterBuilder had no such -operator. #2942 gave it `startsWith`, `endsWith`, `isNull` and `isNotNull` — -and this table did not follow, so a stored view carrying any of the four still -reached the builder as a raw spelling it could by then have rendered, and the -comment claiming otherwise was simply false. - -All four now map. `is_null`/`is_not_null` go to `isNull`/`isNotNull` and **not** -to `isEmpty`/`isNotEmpty`: the builder draws both pairs, and folding the NULL -predicate onto the empty-string one would silently rewrite the author's operator -the next time the view was saved. - -**The guard could not have caught this, and now can.** The parity test asserted -the unmapped set equalled a hand-kept list of gaps — which stays true when the -*builder* gains an operator, because neither side of that comparison moves. The -new assertion is derived instead: `starts_with` and `startsWith` fold to the same -key, so an unmapped canonical operator whose folded name matches a folded builder -id is an omission by definition. Verified by reverting the four mappings, which -reproduces the drift as four named failures. - -The unmapped set is now empty — all 19 canonical `VIEW_FILTER_OPERATORS` members -translate. - -Refs #2945, #2942, #2989 diff --git a/.changeset/column-identity-consumers-read-one-key.md b/.changeset/column-identity-consumers-read-one-key.md deleted file mode 100644 index 5870efe76c..0000000000 --- a/.changeset/column-identity-consumers-read-one-key.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -"@object-ui/core": patch -"@object-ui/plugin-list": patch -"@object-ui/plugin-grid": patch -"@object-ui/plugin-detail": patch -"@object-ui/plugin-tree": patch ---- - -fix(list,grid,detail,tree,core): every column resolver reads one key (#3104 PR2) - -PR1 (#3119) put a canonicalizing fold at ListView's ingestion boundary. This -converges the 22 read sites themselves onto `columnIdentity()` from -`@object-ui/core`, so a surface that is NOT downstream of that fold resolves -the same identity anyway. - -That distinction is the user-visible part. A standalone `object-grid` node — -authored directly on a page, with no `list-view` above it — never passed -through `normalizeListViewSchema`. Its `getSelectFields` read `c.field` alone -while the `ensureId` probe one line above read `f?.name || f?.field`, so a -legacy `{ name: 'account' }` column reached `$select` as a literal `undefined` -hole: the server never returned the field and every cell in that column came -back empty. Same for `ObjectTree`, `RelatedList` and the `record:details` / -`record:related_list` renderers. - -Converged: - -| Surface | Was | Now | -|---|---|---| -| `ListView` ×9 + its 2 request builders | `name \|\| fieldName \|\| field` vs `f?.field` | `columnIdentity()` | -| `RelatedList` ×8 | `accessorKey \|\| field \|\| name` | `accessorKey \|\| columnIdentity()` | -| `ObjectGrid` | name-first probe vs `c.field` projection | `columnIdentity()` | -| `ObjectTree` | `name \|\| fieldName \|\| field \|\| key` | `columnIdentity() \|\| key` | -| `buildExpandFields` | `field ?? name ?? fieldName` | `columnIdentity()` | -| `record-details` / `record-related-list` | `field \|\| name (\|\| key)` | `columnIdentity() (\|\| key)` | - -`accessorKey` keeps its precedence in `RelatedList` — it is TanStack Table's -column key, not ObjectStack metadata identity, and only the `field || name` -tail was converged. `key` stays a tail fallback in `ObjectTree` and -`record-related-list` for the same reason: it is a generic entry key. - -Two incidental fixes that TypeScript surfaced once the resolver stopped -returning `any`: ListView's filter-field options and its hide-fields popover -both built entries keyed `undefined` for a column with no resolvable identity. -Those entries could never match a column; they are now dropped. - -**Inventory re-triage.** PR1 recorded 24 family members. Two were mis-classified -and are reclassified here rather than converged — reading what they actually -feed shows they are not column reads at all: - -- `ViewPreview.tsx` adapts a ViewItem **form** section to what `object-form` - selects by (`field` → `name`) — the #3090 two-layer join. -- `SchemaForm.tsx` renders an arbitrary metadata **array** into a popover - summary and guesses at a display key; the entries are validations, actions, - or whatever the JSON schema declares. - -So the family was 22, and it is now **0**. The ratchet asserts that, asserts -each converged surface actually routes through the shared reader (a surface -that dropped identity resolution instead of converging it goes red), and pins -`accessorKey`'s precedence in `RelatedList`. diff --git a/.changeset/column-identity-say-who-wins.md b/.changeset/column-identity-say-who-wins.md deleted file mode 100644 index 660ea3e18b..0000000000 --- a/.changeset/column-identity-say-who-wins.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -"@object-ui/core": patch ---- - -feat(core): say which column identity key won, out loud (#3104 PR3) - -Closes the battle opened in #3104. PR1 (#3119) put the canonicalizing fold at -ingestion; PR2 (#3122) converged all 22 read sites onto `columnIdentity()`. -This is the audible half. - -A column carrying two identity keys that **disagree** — `{ field: 'account', -name: 'account_name' }` — now logs a one-time dev-mode warning naming which key -won and what to change: - -``` -[ObjectUI] Column carries two identities: `field: 'account'` and -`name: 'account_name'`. `field` wins — it is the only key `ListColumnSchema` -declares — and `name` has been rewritten to match, so the rendered column and -the requested field agree. Fix the producer: drop `name` and author `field` -only. (objectui#3104) -``` - -The fold making the two halves agree is what stops the bug, but silently -rewriting `name` to match `field` also hides that the producer is emitting a -contradiction. The renderer recovering is not the same as the metadata being -right, so the recovery says so. - -Deliberately narrow: - -- **Only contradictions.** A legacy-only column (`{ name: 'stage' }`) is legacy, - not conflicting — it is stamped without noise. -- **Warn once per (identity, conflicting spelling).** Columns are re-normalized - on every render; a warning that floods the console is a warning that gets - muted. Keyed by the pair rather than the identity alone, so a column carrying - two different stale spellings reports both — the author needs to fix every - producer, not just the first one seen. -- **Silent under `NODE_ENV=production`**, and the fold still runs there. - -`resetColumnIdentityWarnings()` is exported for tests. - -**No lint rule, and that is a measured decision.** #3104 asked for -`no-restricted-syntax` on `.field ?? .name` to be evaluated on its -false-positive rate first. With the family at zero, all 12 remaining scanner -hits are legitimate — a syntactic rule cannot tell a two-layer join from a dual -read, because the distinction is what the keys mean in that layer, not how the -expression is spelled. Adopting it would mean 12 inline disables on correct -code, which trains the next author to reach for the disable. The ratchet carries -a `verdict` and a `why` per site instead, so a new hit gets triaged rather than -silenced. The evaluation is written into the ratchet's header. - -**Ledger item resolved with no change needed.** #3104 flagged `ListColumn` for -disposition under objectstack#4115 (spec-named symbols must be imports, not -declarations). `ListColumnSchema` is already a by-reference re-export of -`@objectstack/spec/ui`, and `spec-subschema-parity.test.ts` already pins it by -reference identity — the only check that distinguishes a re-export from a -faithful fork. Already compliant; nothing to do. diff --git a/.changeset/column-identity-single-key-at-ingestion.md b/.changeset/column-identity-single-key-at-ingestion.md deleted file mode 100644 index 0deeaeca2e..0000000000 --- a/.changeset/column-identity-single-key-at-ingestion.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -"@object-ui/core": minor ---- - -feat(core): one column identity per column — `field` stamped at ingestion (#3104) - -A column's field identity was resolved twice, with two different precedences -over the same `schema.columns` array, and the two halves disagreed: - -- **request path** — `ListView`'s `$expand` and `$select` builders, and - `ObjectGrid.getSelectFields`, read `f?.field` and only `f?.field`. -- **render path** — the FLS gate, the hidden-field filter, `fieldOrder`, both - export branches and the hide-fields popover read - `f.name || f.fieldName || f.field` — name FIRST. - -So `{ field: 'account', name: 'account_name' }` fetched `account` while the -renderer keyed off `account_name`, and `{ name: 'account' }` rendered a column -the request dropped entirely — neither `$select` nor `$expand` carried it. That -is the mechanism behind the "relation column shows a bare id / column is empty -/ sort does nothing / export is missing a column" defect class. - -Per AGENTS.md #0.1 the fix is not another `?? name` at the read sites. Legacy -acceptance moves to the one boundary that already folds this view's vocabulary, -`normalizeListViewSchema`, which now also canonicalizes each column's identity. - -New in `@object-ui/core`: - -- `columnIdentity(entry)` — the single reader. Resolves `field` → `name` → - `fieldName`, canonical-first, so it agrees with `buildExpandFields` instead - of racing it. Handles bare-string columns. -- `normalizeColumnIdentity(entry)` / `normalizeColumnIdentities(columns)` — the - fold. Stamps `field`; a legacy key that is **already present** is mirrored - onto the same identity so name-first readers resolve what the request asked - for; a legacy key that is **absent is never invented**, and an - already-canonical column is returned by reference. -- `hasConflictingColumnIdentity(entry)` — true when a column's keys disagree. -- `CANONICAL_COLUMN_IDENTITY_KEY`, `LEGACY_COLUMN_IDENTITY_KEYS`, - `TABLE_ADAPTER_COLUMN_KEY`. - -The fold **mirrors** rather than deleting the legacy key, unlike the other -folds in `normalizeListViewSchema`. Deleting would work inside this repo (every -name-first read falls through to `field`), but `columns` entries cross the -package boundary into host renderers and dropping `name` from under them is a -breaking change with no inventory. Deletion is a later call, once the in-repo -consumers read `columnIdentity()`. - -Behaviour is unchanged for any column carrying a single identity key — every -read site resolves the same string it did before. The entries whose resolution -moves are exactly the ones where two sites already disagreed. - -`accessorKey` is deliberately untouched: it is TanStack Table's own column key -(`TableColumn.accessorKey`), not ObjectStack metadata identity, and folding -across that boundary would fossilize the merge. diff --git a/.changeset/combo-derived-from-series.md b/.changeset/combo-derived-from-series.md deleted file mode 100644 index afe66381a6..0000000000 --- a/.changeset/combo-derived-from-series.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -"@object-ui/plugin-charts": minor ---- - -fix(charts): a spec `series[].type` override actually draws, and a spec-shape `series` plots at all (#2945) - -#2945 listed `combo` (`plugin-charts`) as renderer-local dialect to "promote or -delete". Neither: the spec **already models a combo chart**, per-series, and its -own field comment says so — - -```ts -// spec/src/ui/chart.zod.ts — ChartSeriesSchema -/** Series type override (combo charts) */ -type: ChartTypeSchema.optional().describe('Override chart type for this series'), -``` - -— exactly as it models stacking with `ChartSeries.stack` rather than a -`stacked-bar` family. So `combo` is not a name an author should reach for; it is -what "the series disagree about their family" looks like from the renderer's -side. `effectiveChartFamily` now derives it, and `combo` stays a documented -renderer-local marker (internal callers pass it directly today). - -Chasing that turned up two live bugs, both silent, on the path a spec author -takes. - -**1. The per-series override was parsed, carried, and then dropped.** Only the -renderer's `chartType === 'combo'` branch read `series[].chartType`, so - -```ts -{ type: 'bar', series: [{ name: 'revenue' }, { name: 'margin', type: 'line' }] } -``` - -drew `margin` as a bar. Nothing was wrong at any layer but the last — a unit test -even asserted the value was carried. - -**2. A spec-shape `series` rendered nothing at all.** `series` is the one binding -both shapes spell with the same key, so `ChartRenderer`'s blanket "internal props -win" rule let the author's `[{ name }]` shadow the normalized `[{ dataKey }]` and -reach a renderer that reads `dataKey`. Blank chart. Every other spec binding has a -distinct name (`xAxis` vs `xAxisKey`), which is why only this one broke — and why -the isolated normalization tests all passed over a dead path. The raw array is now -preferred only when it already speaks the internal shape, so internal callers are -byte-for-byte unchanged and a mixed array works too. - -**Also fixed in passing:** the combo branch had an `area` arm under a `BarChart` -container, and Recharts renders an `` child of `BarChart` as nothing — so an -authored combo with an `area` series drew a blank series. The container is now -`ComposedChart`, which is what Recharts provides for mixed marks. - -Widening only. A chart whose series all resolve to one family keeps its own -family, an explicit `combo` is untouched, and a family with no per-series meaning -(`pie`, `horizontal-bar`, …) is never widened. A derived combo binds series to the -left axis unless one asks for `yAxis: 'right'` — the spec's own default — so -widening changes the series' mark and not its scale; the legacy bar→left/line→right -guess is kept for an authored `combo`, where it was historically the only way to -reach a second axis. - -Guards: `effectiveChartFamily` / `comboBaseFamily` are unit-tested over the whole -family matrix; DOM-level tests assert the **marks** rather than the derived family, -since the carry was already covered and the drawing was what broke; and -`spec-derived-unions.test.ts` asserts `combo` is absent from the spec's -`ChartTypeSchema`, so the day it is adopted upstream the derivation is named as the -thing to retire. diff --git a/.changeset/console-403-search-scope-nav-gating.md b/.changeset/console-403-search-scope-nav-gating.md deleted file mode 100644 index 0d3f37aa51..0000000000 --- a/.changeset/console-403-search-scope-nav-gating.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@object-ui/app-shell": patch -"@object-ui/plugin-list": patch -"@object-ui/react": patch -"@object-ui/i18n": patch ---- - -fix(console): three real-user console failures — 403 blamed on the network, ⌘K search capped at 8 objects, nav gating fields inert - -1. **List error panel classifies the failure** (`plugin-list`, `i18n`): a 403/401 from the data source used to render the same "check your connection" copy as a genuine outage, sending users to debug their network while the server was correctly denying access. The panel now classifies by `httpStatus`/`status`/`statusCode`, the `PERMISSION_DENIED`/`UNAUTHORIZED` error codes, or an `HTTP ` message prefix, and renders dedicated permission-denied / sign-in-required copy (all nine locales). - -2. **⌘K / full-page search scope is no longer truncated** (`react`): `maxObjectsQueried` caps the per-object fanout fallback, not the search scope — it used to slice the candidate pool itself, so the `objects` whitelist sent to the platform's `/api/v1/search` only ever named the first 8 nav objects. Which sidebar group came first decided which records were findable; everything later in the nav was unsearchable no matter what the user typed. - -3. **Nav gating fields finally gate** (`app-shell`): `evaluateVisibility` only evaluated `${…}` template strings, so the `{ dialect: 'cel', source }` envelopes the spec normalizes every authored `visible` predicate into fell through to a blanket "visible" — a constant-false predicate still rendered for everyone. It now delegates to `ExpressionEvaluator.evaluateCondition`, which routes CEL envelopes to the canonical `@objectstack/formula` engine. And the sidebars' `requiredPermissions` check treats a bare name as an ADR-0066 system capability (union of the user's permission-set `systemPermissions` from `/me/permissions`) — the same subset rule the server applies to `AppSchema.requiredPermissions` — instead of misreading it as `can(, 'read')`, which had degraded `requiredPermissions` into a hide-from-everyone switch (admins included). The `object:action` form and the legacy object-read fallback keep working. diff --git a/.changeset/console-mounts-notification-surfaces.md b/.changeset/console-mounts-notification-surfaces.md deleted file mode 100644 index e4d85468a6..0000000000 --- a/.changeset/console-mounts-notification-surfaces.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -"@object-ui/app-shell": minor ---- - -feat(app-shell): the console mounts the notification surfaces, so `displayType` works there (#3014 follow-up) - -#3071 gave each spec `NotificationTypeSchema` member its own presentation, but no -host mounted `NotificationProvider` — the capability existed and the console -could not reach it. `ConsoleShell` now mounts the provider and the surfaces with -a single global home; `ConsoleLayout` mounts the one that belongs in the content -area: - -| `displayType` | Surface | Mounted by | -|---|---|---| -| `toast` | sonner, via the new `presentNotificationToast` | `ConsoleShell` | -| `snackbar` | `` | `ConsoleShell` | -| `alert` | `` | `ConsoleShell` | -| `banner` | `` | `ConsoleLayout`, beside the draft / unpublished bars | -| `inline` | `` | the raising surface — **not** mounted globally | - -`inline` is left out deliberately: rendering in place at the raiser is the whole -difference between it and a banner, so a global inline outlet would collapse the -two again. - -`presentNotificationToast` is the single place a notification becomes a sonner -call — severity → variant, `duration: 0` → `Infinity` (the contract's -"persistent", which passed through raw would have made the toast vanish on the -next tick), first action → the one action slot sonner offers, an absent duration -left to the `ConsoleToaster` default rather than reinvented. Its severity table -is `Record`, so a new spec severity fails -type-check instead of silently rendering neutral. - -The banners go through `ConsoleNotificationBanners`, which gates on -`useHasNotificationProvider()`. `ConsoleShell` is deliberately a set of -composable pieces a host assembles in its own `App.tsx`, so `ConsoleLayout` can -legitimately render without the provider above it — and `useNotifications()` -throws there, which would white-screen the whole app instead of simply showing -no banners. - -Both pieces are exported (`presentNotificationToast`, `ConsoleNotificationBanners`) -for hand-assembled shells. The provider's `defaultDuration` matches -`ConsoleToaster`'s 4s, so a snackbar and a toast raised together disappear -together. diff --git a/.changeset/curate-page-element-action.md b/.changeset/curate-page-element-action.md deleted file mode 100644 index f340543e06..0000000000 --- a/.changeset/curate-page-element-action.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -"@object-ui/core": minor -"@object-ui/console": patch ---- - -feat(sdui): curate the page:\*, element:\* and action:\* families into the public contract - -The AI-authoring vocabulary and the Studio page designer disagreed by thirteen -blocks: `PUBLIC_BLOCKS` carried one `page:` tag and one `element:` tag while -the designer palette — and @objectstack/spec's page schema — offered the whole -families. A block a human can drag in Studio was invisible to a model writing -the same page, which is the objectui#3006 state at 10× the scale. - -Fifteen tags join the contract (36 → 42 → **57**), every one shipping a -renderer with declared inputs (objectui#3065): - -- `page:` — tabs, card, accordion, section, footer, sidebar -- `element:` — text, number, button, definition-list, repeater -- `action:` — button, group, menu, icon - -Five stay out, each with its reason recorded and guarded: `action:bar` -(`record:quick_actions` covers the record action strip; the spec blesses the -other four), `element:image` (duplicates the curated `image` — one spelling -per concept), and `element:record_picker` / `element:text_input` / -`element:metadata_viewer` (mirroring the Studio palette's own exclusions, so -the two vocabularies stay out for the same reasons rather than by -coincidence). - -The console's reverse-coverage guard now sweeps all four semantic namespaces -instead of `record:` alone — checking only the namespace you just fixed is -exactly how the last 22 doubled keys went unnoticed (objectui#3037). A new -prop-less allowlist (`element:divider`, `page:section`, `page:footer`, -`page:sidebar`) keeps "declares no inputs" a pinned decision in both -directions: those four must stay at zero, everything else curated must declare -a surface. diff --git a/.changeset/decision-outputs-reach-both-decision-surfaces.md b/.changeset/decision-outputs-reach-both-decision-surfaces.md deleted file mode 100644 index 8a2a5c1e33..0000000000 --- a/.changeset/decision-outputs-reach-both-decision-surfaces.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -"@object-ui/app-shell": minor ---- - -fix(approvals): decision outputs reach both decision surfaces (objectui#2955, framework#3447 P2) - -An approval node can ask the approver for structured data with their decision -(`decisionOutputs`) — typically to route the next node's approvers, which the -flow then reads as `vars..`. The server has shipped this since -framework#3447 P2 and surfaces the typed declaration on the request row -(`decision_output_defs`), but neither Console decision surface actually -delivered it. - -**The Approval Center asked for a record id instead of showing a picker.** The -typed pickers landed in objectui#2831 and the drawer really did synthesize a -`lookup` param per declared output — but it spelled the picker target -`referenceTo`, and `resolveActionParams()` (which every collected param passes -through before the dialog renders it) rebuilds an inline param from a fixed key -list, reading the target from `reference`. The target was dropped there, and -`paramToField()` degrades a targetless picker to a plain text input — so a -`position` output rendered as a box labelled "