diff --git a/.changeset/action-bar-member-declared-visible-gate-3823.md b/.changeset/action-bar-member-declared-visible-gate-3823.md deleted file mode 100644 index 18e5625900..0000000000 --- a/.changeset/action-bar-member-declared-visible-gate-3823.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -"@object-ui/components": patch ---- - -`action:bar` member actions declaring `visible: false` are now hidden instead of rendered - -`action:button` and `action:icon` carried the same truthiness gate objectui#3812 -removed from the member-action leaves — `if (schema.visible && !isVisible) -return null` — so `visible: false`, the most explicit way an author can say -"never show this", fell into the "no gate declared" branch and the action -rendered anyway. - -objectui#3812's triage judged the five component-level `schema.visible` gates a -dormant defensive layer, because `packages/react`'s `SchemaRenderer` evaluates -`newSchema.visible !== undefined` and hides the node before the component ever -mounts. Two of the five are not dormant, and this is the difference: - -`action:bar` does not route through `SchemaRenderer`. It resolves each member's -renderer from the `ComponentRegistry` itself and spreads the whole member action -onto that renderer's schema, so an author's `visible` on a member arrives as the -child's own `schema.visible` and lands on the child's gate. `action:bar` is also -the only gate on that path by design — its `filteredActions` deliberately -filters on `requiredPermissions` and `actionRendersAt` only, leaving `visible` to -the member renderer. The path is reachable end-to-end and is now pinned that way -(registry-mounted `action:bar`, member declaring `visible: false`), so the -reachability does not have to be argued again. - -Both gates now read the same named definition as the rest of the family, -`hasDeclaredVisibilityGate` (`!= null && !== ''`) — the invariant objectui#3492 -established for the selection bar and objectui#3758 applied to the row-action -surfaces. The evaluation entry is untouched and already short-circuits a boolean -rather than handing it to the CEL engine, which `actionPredicate.parity` pins for -both the engine and the renderer path. - -Behaviour change surface, deliberately narrow: only an `action:button` / -`action:icon` whose `visible` is the literal boolean `false` (or another falsy -non-empty value) changes — from rendered to hidden, which is what the -declaration asked for. `visible: true` still renders, `''` and an absent -`visible` are still no gate at all, and no expression-valued `visible` changes -verdict. `ActionSchema.visible` is `ExpressionInputSchema` with no boolean -member, so `objectstack build` cannot emit this shape; hand-written view JSON and -in-process callers constructing action defs can. - -The remaining three component-level gates (`action:group`, `action:menu`, -`action:bar`'s own) stay as they are — they only ever mount through -`SchemaRenderer`, which resolves `visible` first, and the overflow `action:menu` -that `action:bar` synthesizes carries no `visible` at all. diff --git a/.changeset/action-declared-disabled-gate-3842.md b/.changeset/action-declared-disabled-gate-3842.md deleted file mode 100644 index 57d743d7c7..0000000000 --- a/.changeset/action-declared-disabled-gate-3842.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -"@object-ui/app-shell": patch -"@object-ui/components": patch ---- - -An action declaring `disabled: ''` is no longer greyed out forever (objectui#3842) - -The "is a `disabled` gate declared?" test stopped at `!= null`, missing the -`!== ''` half of the invariant the `visible` family converged on -(`hasDeclaredVisibilityGate`, objectui#3492 / #3758 / #3812 / #3823 / #3835). So -`disabled: ''` counted as a declared gate, and the verdict went to the evaluation -entry — which reads an empty predicate as "no condition → `true`" -(`toPredicateInput('')` is `undefined`, `evaluateCondition(undefined)` is `true`). - -The direction is why this half is a defect and the `visible` half was not. On -`visible`, that `true` means SHOW, so an over-broad "declared" test and a -permissive empty predicate cancel out and `visible: ''` renders either way. On -`disabled`, the same `true` means DISABLE — the two mistakes compound, and an -empty predicate stopped meaning "no gate" and started meaning "permanently -greyed out". One empty predicate, opposite treatment under two keys. - -Two gates now ask the shared definition instead: - -- `@object-ui/app-shell`'s `DeclaredActionsBar` — the hot one. Its actions are - SERVER-declared (`objectDef.actions[]`) and its hosts are the approvals inbox's - record sections, so a `disabled: ''` arriving from metadata (an authoring form - left empty, a template that rendered to an empty string) produced an Approve / - Reject button nobody could click, indistinguishable from deliberate metadata. - objectui#3835 was this same surface failing the other way. -- `@object-ui/components`' `action:button` — verified to be the same shape before - it was changed (the issue inferred it from the identical spelling but did not - probe it): with `disabled: ''` the rendered button carried `disabled=""`. - -**Behaviour change surface, deliberately narrow.** Only `disabled: ''` changes — -from disabled to clickable, which is what "no predicate" asked for. `disabled: -true` still disables, `disabled: false` and an absent `disabled` still do not, and -no expression-valued `disabled` changes verdict. One consequence worth naming: on -`action:button`, an empty `disabled` now falls THROUGH to the legacy non-spec -`enabled` fallback instead of short-circuiting on the empty predicate, so an -action spelling both (`disabled: ''` + `enabled: true`) becomes clickable. - -The legacy `enabled` leg of `action:button` was routed through the same -definition for consistency, and that part is behaviour-preserving by derivation -rather than a fix: the leg is negated (`disabled = !isEnabled`), so an empty -predicate's `true` already arrived as "not disabled" — the same verdict "no gate" -produces. All four shapes are identical under either test; the derivation table -and the reason no test can distinguish them are written down next to the pins. - -`hasDeclaredVisibilityGate` keeps its historic name at both call sites (the -objectui#3842 dispatch ruling): the predicate is key-neutral, and one -implementation behind two names is how a repo grows dialects. Each call site says -so in a comment. diff --git a/.changeset/action-member-declared-visible-gate-3812.md b/.changeset/action-member-declared-visible-gate-3812.md deleted file mode 100644 index e0de1e891c..0000000000 --- a/.changeset/action-member-declared-visible-gate-3812.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -"@object-ui/components": patch ---- - -Action-face member actions declaring `visible: false` are now hidden instead of rendered - -The three member-action gates on the action face asked truthiness — -`if (action.visible && !isVisible) return null` — so `visible: false`, the most -explicit way an author can say "never show this", fell into the "no gate -declared" branch and the action rendered anyway: - -- `action:group` in `display: 'inline'` mode (`InlineActionButton`); -- `action:group` in `display: 'dropdown'` mode (`DropdownActionItem`); -- `action:menu`'s items (`ActionMenuItem`). - -These leaves `.map()` the component's own `actions` array, so neither -`SchemaRenderer`'s node-level `visible` handling nor -`ActionEngine.getActionsForLocation` (whose boolean `visible` was always -correct) is in the path — the truthy gate was the only gate. - -All three now read one named definition, `hasDeclaredVisibilityGate` -(`!= null && !== ''`), and let the declaration itself decide. This is not a new -decision: objectui#3492 established the invariant for the selection bar, whose -`hasVisibilityGate` spells out why truthiness cannot answer the question, and -objectui#3758 applied it to both row-action surfaces. The evaluation entry is -untouched and already short-circuits a boolean rather than handing it to the CEL -engine, which `actionPredicate.parity` pins for both the engine and the renderer -path. - -Behaviour change surface, deliberately narrow: only a member action whose -`visible` is the literal boolean `false` (or another falsy non-empty value) -changes — from rendered to hidden, which is what the declaration asked for. -`visible: true` still renders, `''` and an absent `visible` are still no gate at -all, and no expression-valued `visible` changes verdict. -`ActionSchema.visible` is `ExpressionInputSchema` with no boolean member, so -`objectstack build` cannot emit this shape; hand-written view JSON and -in-process callers constructing action defs can. diff --git a/.changeset/auth-family-locale-keys-3546-slice3.md b/.changeset/auth-family-locale-keys-3546-slice3.md deleted file mode 100644 index 557649cc0b..0000000000 --- a/.changeset/auth-family-locale-keys-3546-slice3.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -"@object-ui/i18n": patch ---- - -Backfill the auth family's 54 missing locale keys — `auth` 26 + `oauth` 16 + `acceptInvitation` 12 (objectui#3546, slice three) - -`scripts/check-i18n-call-site-keys.mjs` (objectui#3530) measured 54 keys that a -`t()` call site asks for and that **no locale pack defined** — 54 distinct keys at -54 call sites across the console's six auth pages. All 54 carried an inline -`t(key, { defaultValue: 'English' })`, which is exactly the objectui#3517 class: -English rendered correctly, and **all ten languages were stuck on it** for -months. Nothing here rendered a raw key — slice one (PR #3583) held those sites. - -What that meant on the page: a `zh` user reaching `/login` and switching to the -phone/SMS branch got "Email or phone number", "Get code", "Resend in {seconds}s" -and "Sign in with password instead" in English; the whole `/oauth/consent` screen -— including the four scope sentences describing what a third-party client is -about to be granted — was English-only; so was the `/accept-invitation` page and -the device-authorization dead end. - -- **`packages/i18n/src/locales/en.ts`** gains the 54 keys. `oauth.consent.*` and - `acceptInvitation.*` are new top-level namespaces; the other 26 extend - `auth.login`, `auth.forgotPassword`, `auth.device` and `auth.verifyEmail`. - Every one of the 52 keys whose call site carries a **string** `defaultValue` - gets that exact string, byte for byte (52/52, script-compared), so the pack - path and the inline-default path cannot diverge. The two remaining keys — - `oauth.consent.title` / `oauth.consent.request` — have **template** - defaultValues, where byte identity is structurally impossible (JS `${…}` vs - i18next `{{…}}`); both take the interpolation contract the call site actually - declares in its options. -- **The nine other packs** get real translations, each evidenced against a - neighbour key in the same pack (fr's space before `?`/`:`, de's en dash, ru's - ё, ar's verb-first placement so an RTL sentence does not open on a Latin - client name, zh's full-width punctuation). The one string all ten packs share - is `phonePlaceholder` — the E.164 example number, treated like the - `name@example.com` the packs already keep untranslated. -- **`scripts/i18n-call-site-key-baseline.json`** loses exactly those 54 entries - (163 → 109). The file is a ratchet: an unfixed key missing from it fails the - build, and a fixed key still listed fails it too. -- **No component changed.** An AST sweep of all 122 call sites in these three - namespaces found zero dead `t(key) || 'English'` fallbacks (the construct - slice one had to delete from `ObjectView.tsx`, where i18next's key-as-value - return made `||` unreachable). - -Two holes here are **not** i18next's and must survive translation intact: -`resendOtpCountdownText` carries `{seconds}` in single braces because -`packages/auth/src/LoginForm.tsx` and `ForgotPasswordForm.tsx` substitute it with -a literal `.replace()`, and `oauth.consent.request`'s `{{suffix}}` arrives -pre-formatted from the page. Both are pinned in -`packages/i18n/src/__tests__/auth-namespace-3546.test.tsx`, in both directions. diff --git a/.changeset/bulk-action-param-options-open-3309.md b/.changeset/bulk-action-param-options-open-3309.md deleted file mode 100644 index 7bf97f9edf..0000000000 --- a/.changeset/bulk-action-param-options-open-3309.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -"@object-ui/types": patch ---- - -`BulkActionParam.options` entries now accept the widget config the renderer already forwards - -The entry type was a closed `{ label, value }`, and it was the only layer in the -path that said so. `bulkParamToField` spreads each entry into the metadata it -hands the field widget (`{ ...o, value: String(o.value) }`), so extra keys -survive; the destination shape `SelectOptionMetadata` declares `color` / `icon` / -`disabled` / `visibleWhen` and `@object-ui/fields` genuinely reads them; and -`@objectstack/spec`'s `BulkActionParamSchema` makes the same entry -`.passthrough()`, so the server accepts them. Writing -`options: [{ label: 'Purple', value: 'purple', color: '#8B5CF6' }]` therefore -produced a TypeScript excess-property error on a configuration the renderer -honours — the type rejected working metadata, which is the most expensive -direction for an author (an AI author especially) that trusts it absolutely. - -The entry now carries a `[key: string]: unknown` catch-all, matching the one its -parent `BulkActionParam` has had all along and the idiom `ActionParamOption` -settled one interface over. `label` and `value` stay required and keep their -exact types: open is not optional, and the catch-all is not an invitation to -author new option keys — the authoring gate remains the spec's strict -`SelectOptionSchema`. No runtime behaviour changes; the widening is -backward-compatible for consumers. diff --git a/.changeset/capability-multiselect-widget-retired-3308.md b/.changeset/capability-multiselect-widget-retired-3308.md deleted file mode 100644 index 7fd72e7da6..0000000000 --- a/.changeset/capability-multiselect-widget-retired-3308.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -"@object-ui/fields": minor -"@object-ui/plugin-detail": minor -"@object-ui/components": minor -"@object-ui/plugin-form": minor ---- - -Retire the `capability-multiselect` field widget name, which existed only on the docs-site registration path and which nothing ever stamped (objectui#3308, ADR-0049 enforce-or-remove) - -`field:capability-multiselect` was registered by `registerFields()` and only there. That function's sole caller is the docs site, so the key never existed on the live path (`registerAllFields()`, run at module import, iterates `fieldWidgetMap` — which never listed it). A field authored with `widget: 'capability-multiselect'` therefore resolved to nothing in every real application, while the comment above the registration described it as usable from a record form: a code comment promising a capability that does not exist, which is the worst direction for a metadata renderer AI-authored apps read as authority. - -Nothing stamped the hint either. ADR-0056 P1 stamps `permission-facet-link` on all six `sys_permission_set` facets — `system_permissions` included — through the single `ObjectStackAdapter.getObjectSchema` choke point, and P2 put the capability editor in Studio. The widget name was a leftover from an intermediate iteration of that rollout. - -Removed, with a tombstone at each site: - -- `@object-ui/fields` — the `field:capability-multiselect` registration and the comment that advertised it. **Breaking in name only**: the key was unreachable outside the docs site, so no application could have resolved it. A field still carrying the hint now degrades to its declared `type` renderer, the defined behavior for an unregistered widget. -- `@object-ui/plugin-detail` — `InlineFieldInput`'s `widget === 'capability-multiselect'` branch, the hint's last honoring surface. Leaving one consumer for a name no producer emits and no form resolves is the same declared-vs-enforced split, inverted. The sibling `permission-facet-link` branch is untouched and pinned. -- `@object-ui/components` — the dead `capability-multiselect` entry in the form renderer's `DATA_SOURCE_FIELD_TYPES` set, which could never match a resolvable widget. -- `@object-ui/plugin-form` — a comment naming `capability-multiselect` as the widget stamped onto `sys_permission_set.system_permissions`; it names `permission-facet-link` now, which is what is actually stamped. - -`CapabilityMultiSelectField` itself is **unchanged and still exported**: Studio's `PermissionMatrixEditor` imports and renders it directly, which is ADR-0056 P2's design. Only the widget name is retired — the component is not a registry field widget and its doc comment now says so. - -`registerFields()` is also **kept**, with its `@deprecated Use registerAllFields() instead` note corrected. The two are not interchangeable: it registers `createFieldRenderer(widget)`, which synthesizes the label, description and the local `value`/`onChange` state that lets a bare field node (`{ type: 'currency', label: 'Amount' }`) render standalone in the docs demos. Retiring it needs a decision about where that demo chrome goes; the note now records that instead of implying a drop-in replacement. diff --git a/.changeset/cli-app-generator-manifest-3827.md b/.changeset/cli-app-generator-manifest-3827.md deleted file mode 100644 index a875fa9471..0000000000 --- a/.changeset/cli-app-generator-manifest-3827.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -"@object-ui/cli": patch ---- - -Generated temp apps now declare every package they import, at ranges anchored to this repo - -`objectui dev` / `serve` / `build` write a throwaway app into `/.objectui-tmp`, -and the `package.json` they wrote named neither `lucide-react` nor any of the seven -`@object-ui/plugin-*` packages the generated sources import — while pinning -`@object-ui/react` and `@object-ui/components` at `^0.1.0`, a range that resolves to -nothing at all for packages published at 17.x (the registry has no 0.1.0). Outside -this workspace that manifest could not install; inside it, hoisting to the root -`node_modules` satisfied every missing name, so nothing was ever red. - -**`lucide-react` is now declared** (objectui#3827). Both of its imports in the -generated layout are live — `import * as LucideIcons` feeds a `DynamicIcon` lookup -and four `LucideIcons.*` icons, and the named `{ Moon, Sun }` renders the theme -toggle — so this is the opposite disposition from the sibling generator, where -objectui#3755 removed an equivalent declaration precisely because nothing imported -it. Anchored to `^1.28.0`, the range all 23 in-repo manifests that import lucide -agree on. `commands/dev.ts` had been covering the gap in the consumer, aliasing -`lucide-react` to a path resolved out of `packages/components` "to avoid dependency -not found in temp app" — but only in monorepo mode, leaving every other path with an -unsatisfiable import. The declaration belongs at the producer; the alias is now a -workspace convenience rather than the only thing holding the import up. - -**The seven plugin packages are now declared too**, in both generators. Measuring -the reported defect turned up that `src/App.tsx` side-effect-imports -`@object-ui/plugin-charts`, `-editor`, `-kanban`, `-markdown`, `-form`, `-grid` and -`-view` to register their components, and no manifest ever named them: the -undeclared set was eight packages, not the one the issue reported. - -**`@object-ui/*` ranges are derived from this CLI's own version** instead of being -written out as literals. `.changeset/config.json` puts `@object-ui/cli` in the same -`fixed` group as every platform package a generated app depends on, so they always -publish at one version — which makes `^` both current and guaranteed to -exist on the registry. A literal here is not merely a fossil risk but a fossil -generator: that group re-versions on every release, so any hard-coded range is stale -the next day. This is how `^0.1.0` survived to sit 16 majors behind. - -**The toolchain ranges are anchored to in-repo manifests**, the discipline -objectui#3742/objectui#3754 established: `vite ^5.0.0` → `^8.2.0`, `typescript -~5.7.3` → `^6.0.3`, `@vitejs/plugin-react ^4.2.1` → `^6.0.5`, `react`/`react-dom` -`^18.3.1` → `19.2.8` with `@types/*` to match, `react-router-dom ^7.12.0` → -`^7.18.2`, `postcss ^8.5.6` → `^8.5.26`, `autoprefixer ^10.4.23` → `^10.5.4`. React -quotes the root's installed version rather than the wider `^18 || ^19` the platform -packages accept as a peer: the peer says what can work, the root says what the -generated code has actually run against, and inside this workspace the temp app -resolves React by hoisting to the root. - -`tailwindcss` is deliberately left at `^3.4.19`. This repo is on Tailwind 4 and -`@object-ui/components` peers `^4.2.1`, so the range is not merely behind — it -conflicts. But re-anchoring it is not a version edit: the generated `index.css` uses -v3 directives, the generated `postcss.config.js` names the plugin key v4 moved to -`@tailwindcss/postcss`, and the generated `tailwind.config.js` is a v3 config. Raising -the range without rewriting those three files yields an app that installs and renders -unstyled, which looks fixed and is worse. Filed separately as objectui#3852; kept -internally consistent at v3 until then, and pinned as a deliberate deferral rather -than left to read as drift. - -The generators now build their output as a file map that the writers spill to disk, -so tests assert over the same artifact the CLI writes. Three structural gates port -the ones the sibling generator grew: every bare import must be declared, no versioned -runtime dependency may be declared that nothing imports, and no generated `src/**` -file may be unreachable from `src/main.tsx` — the one module `index.html` loads. Each -is paired with a self-test that plants the defect back. Note for the next port: the -`create-plugin` import scanner matches single-quoted specifiers only, and these -templates mix quote styles, so a verbatim copy would have been blind to -`from "lucide-react"` — one of the two lines this issue reports. diff --git a/.changeset/compareto-kind-convergence.md b/.changeset/compareto-kind-convergence.md deleted file mode 100644 index df1b8fa176..0000000000 --- a/.changeset/compareto-kind-convergence.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@object-ui/core": patch -"@object-ui/plugin-dashboard": patch -"@object-ui/plugin-charts": patch ---- - -Converge dashboard widget `compareTo` on the executor's `{ kind, dimension? }` contract, and make the dataset path actually render a comparison - -`CompareToConfig` was a three-branch union (`'previousPeriod' | 'previousYear' | { offset }`). `@objectstack/spec` collapsed it to the shape the analytics executor already implements — `DatasetCompareTo`, a plain strict object `{ kind: 'previousPeriod' | 'previousYear'; dimension?: string }` (objectstack#5011) — so this renderer now reads that one shape: - -- `shiftFilterByCompareTo` / `compareToTrendLabelKey` dispatch on `compareTo.kind`. The `{ offset }` duration shift is gone: `{ offset: '1y' }` is `kind: 'previousYear'`, while `'7d'` / `'1M'` have no faithful target and are restated by the author on the widget's own `filter` plus `kind: 'previousPeriod'`. No trend label key is retired — the offset arm resolved to `vsPreviousPeriod`, which survives as the `previousPeriod` fallback. -- `DatasetWidget` no longer discards part of `compareTo`. It used to forward only the object form because the two string forms had no meaning downstream; with one shape there is nothing to discard, and a stale string is now invalid metadata rejected where it is authored rather than silently reinterpreted here. -- **The comparison now actually runs on the dataset path.** A widget states its window in its own `filter` (a date macro, or the dashboard date-range filter merged in), but the executor shifts a `timeDimensions` entry carrying a `dateRange` — so a dataset widget asking for a comparison got "compareTo needs a dated window to shift" and rendered none. When (and only when) a comparison is requested, the resolved filter's bounded date windows are lowered into `selection.timeDimensions[].dateRange` and moved out of `runtimeFilter` (a copy left behind would intersect the shifted window with the current one and empty every comparison column). Which dimension gets shifted stays the executor's decision: every window found is lowered under the name the author wrote, and zero or two candidates surfaces the executor's own error, listing them. -- The `__compare` columns that come back are now shown: a delta + window label on KPI widgets, a comparison column on tables, and a `variant: 'comparison'` overlay series on charts — the same treatment and the same `dashboard.trend.*` labels the inline object-provider widgets already use. diff --git a/.changeset/components-export-declared-visibility-gate-3835.md b/.changeset/components-export-declared-visibility-gate-3835.md deleted file mode 100644 index 99678ebc49..0000000000 --- a/.changeset/components-export-declared-visibility-gate-3835.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@object-ui/components": patch ---- - -Export `hasDeclaredVisibilityGate` from the package barrel (objectui#3835) - -`hasDeclaredVisibilityGate(visible)` — "did this action DECLARE a visibility gate -at all?", i.e. `!= null && !== ''`, with the verdict left to the evaluation entry -— is the single definition objectui#3492 established and PR #3816 / #3825 / #3836 -applied to every member-action gate in this package and in `@object-ui/plugin-grid`. -It lived module-private in `src/renderers/action/visibility-gate.ts`. - -The family turned out to have a member outside these packages: -`@object-ui/app-shell`'s `DeclaredActionsBar` gates server-declared actions with -the same question and had the same truthiness bug (objectui#3835). Exporting the -one definition is what keeps that fix from becoming a fifth hand-spelled copy of -it — the drift shape objectui#3142 already had to unpick for `locations` in these -same files. - -Additive only: one `export` line, no behaviour change in this package. The -function is pure and dependency-free. diff --git a/.changeset/console-locale-keys-3546-slice4.md b/.changeset/console-locale-keys-3546-slice4.md deleted file mode 100644 index b007ed766f..0000000000 --- a/.changeset/console-locale-keys-3546-slice4.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -"@object-ui/i18n": patch ---- - -Backfill the `console` namespace's 41 missing locale keys plus the `console.ai.group.` template family (objectui#3546, slice four) - -`scripts/check-i18n-call-site-keys.mjs` (objectui#3530) measured **41 distinct -keys at 47 call sites** under `console.*` that a `t()` call site asks for and -that **no locale pack defined** — five of those keys have more than one site -(`console.ai.dock.maximize` has three), which is why the denominator is measured -and never counted by hand. All 47 carried an inline -`t(key, { defaultValue: 'English' })`, so this is the objectui#3517 class: -English rendered correctly, and **all ten languages were stuck on it**. Nothing -here rendered a raw key — slice one (PR #3583) held those sites. - -What that meant on the page: a `zh` user opening `/ai` got "Waiting for -server…", "Still working…" and "Connection lost — reconnecting…" in the -connection banner, all ten "Designing your app…" progress hints in English, and -"Built" / "Not yet built" / "Published" / "Publish failed" on the plan card; the -ChatDock's whole chrome (title, resize handle, collapse, "Open full page") was -English including two `aria-label`s; the conversation sidebar's date headers read -"Today" / "Yesterday" / "Previous 7 days"; a mistyped URL produced an English -"Page not found"; and the `?` shortcuts dialog's AI group was English inside an -otherwise translated table. Two of these strings ("Not yet built", "TODAY") are -named in objectui#2458's mixed-language list. - -- **`packages/i18n/src/locales/en.ts`** gains 46 keys: the 41 measured ones plus - the five members of the `console.ai.group.` family. `console.notFound` is a new - sub-namespace; the rest extend `console.shortcuts` and `console.ai` (with new - `console.ai.dock`, `console.ai.designingPlanHint` and `console.ai.group` - objects). Every one of the 41 measured keys takes its call site's inline - `defaultValue` **byte for byte** (46/47 sites, script-compared), so the pack - path and the inline-default path cannot diverge. - - The one site that cannot match is `ChatDock.tsx:563`, where a single key - (`console.ai.dock.open`) carries two different English strings: the - `aria-label` says `Open assistant` and the `title` says - `Open assistant (⌘⇧I)`. A key can hold one value, so `en` takes the - `aria-label` spelling — an accessible name must not carry a glyph run that - screen readers announce as symbols, and `⌘` is a mac-only glyph that a - *language* pack cannot vary per platform. The tooltip therefore stops - advertising the shortcut; recorded on objectui#3810 (whose class this - divergence joins) rather than papered over. - -- **`console.ai.group.` leaves the ratchet's `missingPrefixes` (4 → 3).** It is a - template key — ``t(`console.ai.group.${group.key}`)`` in - `ConversationsSidebar.tsx:277` — whose static head matched no `en` key, so every - expansion missed. Its value surface is the **closed** `ConversationGroupKey` - union, so the repair is an enumeration of all five members, not a wildcard; a - test reads the component's own union and label map and fails if a sixth bucket - is ever added without a key. - -- **The nine other packs** get real translations, each evidenced against its own - `console` neighbours: `zh` full-width punctuation and the pack's single-em-dash - status-line style, `ja`/`ko` the pack's `AI ` spacing, `de` formal *Sie* and - its `Wird …` progressive, `fr` straight apostrophes, `es` *usted* (as the - `console.ai` neighbourhood already uses), `pt` the pack's `off-line` spelling, - `ru` ё orthography, `ar` verb-first phrasing that never opens an RTL sentence - with a Latin token. Where `en` repeats a string the packs already translate - (`Go back`, `Publish failed`, `Try again`, `Back to home`, `Assistant`, - `Today`, `Yesterday`), the existing neighbour's wording is reused rather than - re-invented. - -- **`scripts/i18n-call-site-key-baseline.json`** shrinks by exactly 42 entries - (41 keys + 1 prefix family): 109 → 68 keys, 4 → 3 prefixes. - -No component changed: an AST sweep of all 308 `console.*` call sites in the repo -found zero dead `t(key) || 'English'` fallbacks among this slice's keys. diff --git a/.changeset/create-plugin-build-deps-anchored-3742.md b/.changeset/create-plugin-build-deps-anchored-3742.md deleted file mode 100644 index 7230cd8d7e..0000000000 --- a/.changeset/create-plugin-build-deps-anchored-3742.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@object-ui/create-plugin": patch ---- - -Anchor the scaffold's build-side `devDependencies` to this repo's real toolchain, and pin the whole generated manifest against drift - -A freshly scaffolded plugin declared a build stack one to two majors behind the one this monorepo actually builds and tests every in-tree plugin with: `vite ^7.3.1` against the repo's `^8.2.0`, `@vitejs/plugin-react ^4.2.1` against `^6.0.5`, `vite-plugin-dts ^4.5.4` against `^5.0.3`, `typescript ^5.9.3` against `^6.0.3`, `vitest ^4.0.18` against `^4.1.10`. Those five ranges were never sourced from anything — objectui#3716's end-to-end run of the generated artifact only ever exercised the versions installed in this repo, so the declared ranges were not the ones under test. All five now quote an in-repo anchor, the same way the three testing ranges already did. - -Two anchors, because the root manifest does not declare everything. `create-plugin` writes into `/packages/plugin-`, so a generated plugin is a literal sibling of `packages/plugin-*`; those manifests anchor the two build-only tools the root omits (`@vitejs/plugin-react`, `vite-plugin-dts`), and the root anchors the rest. - -The parity test now covers **every** entry of the generated `devDependencies` rather than the three testing ones, including a completeness check that fails when a dependency is added without naming its anchor — the five build ranges drifted precisely because nothing pinned them. It also asserts the two anchors agree wherever both declare a dependency, so which one is read cannot hide a drift. - -The generated `vite.config.ts` resolves its library entry from `import.meta.dirname` instead of `__dirname`. vite 8 still defines `__dirname` under its default `bundle` config loader but warns on it ("unsupported by `configLoader: 'native'`, which is planned to become the default in a future major version of Vite ... Use `import.meta.dirname` instead"), and under `native` — which imports the config with Node's own ESM loader, where no `__dirname` exists — the generated config failed to load outright. `apps/console/vite.config.ts` was converted for the same reason in objectui#3384. - -Not a peer-dependency fix: `@vitejs/plugin-react ^4.2.1` resolved to 4.7.0, whose vite peer had widened to `^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0` and accepted the declared `vite ^7.3.1`, so the old manifest installed cleanly. The cost was a scaffold lagging its own monorepo, not a failing install. diff --git a/.changeset/create-plugin-runnable-test-stack.md b/.changeset/create-plugin-runnable-test-stack.md deleted file mode 100644 index 6061410631..0000000000 --- a/.changeset/create-plugin-runnable-test-stack.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/create-plugin': patch ---- - -create-plugin: make the scaffolded plugin's own test suite runnable - -The generator wrote an example test importing `@testing-library/react` and -asserting with `toBeInTheDocument()`, plus a `test: 'vitest run'` script, while -declaring neither library and giving Vitest no DOM environment — so `pnpm test` -in a freshly scaffolded plugin failed on the very first run, at import -resolution. - -The generated `package.json` now declares `@testing-library/react`, -`@testing-library/jest-dom` and `jsdom` (each range copied from this -monorepo's own manifest), the generated `vite.config.ts` gains a `test` block -with `environment: 'jsdom'`, `globals: true` and `setupFiles`, and a -`vitest.setup.ts` registering the jest-dom matchers is written alongside it. -The templates moved to `src/templates.ts` so the generated artifacts can be -pinned by unit tests without executing the CLI. diff --git a/.changeset/create-plugin-scaffold-dead-artifacts-3755-3759.md b/.changeset/create-plugin-scaffold-dead-artifacts-3755-3759.md deleted file mode 100644 index d9166c86a8..0000000000 --- a/.changeset/create-plugin-scaffold-dead-artifacts-3755-3759.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -"@object-ui/create-plugin": patch ---- - -Remove the scaffold's unused pinned icon dependency, and make its generated schema interface reachable - -Two declared-but-unreachable artifacts in the generated plugin, both on the blind side of -the import gate objectui#3733 added — that gate rejects an import nothing declares, and -never looked for a declaration nothing imports. - -**The generated `dependencies` no longer pin `lucide-react`** (objectui#3755). It was -declared at `^0.563.0` and imported by no generated source file, so every freshly -scaffolded plugin really installed lucide 0.563.x for code that never referenced it — two -majors behind the 23 in-repo declarations, all `^1.28.0`. Worse than ordinary caret drift: -a `0.x` caret does not cross minors, so `^0.563.0` is `>=0.563.0 <0.564.0` and could not -float even within `0.x`. It is removed rather than re-anchored because this repo declares -an icon library where it imports one — of the 24 manifests mentioning `lucide-react`, 23 -import it, and none pre-declares it for code not yet written. An author who wants icons -runs `pnpm add lucide-react` and lands the current version by construction, with no anchor -table to maintain for an unused entry. The generated `dependencies` is now exactly the four -`workspace:*` platform packages, which cannot drift at all. - -**The generated `src/index.tsx` now re-exports the schema interface** from `src/types.ts` -(objectui#3759). The generated `exports` map exposes exactly one key — `.` — so the entry -is a consumer's only door, and nothing walked through it to `src/types.ts`: no generated -source imported it, and the deep paths that would have reached it (`/types`, -`/dist/types`) are closed by that same map. The interface in it is the plugin's schema -contract, and it shipped dead — while the generator's own documentation page told authors to -"export your schema types … make it importable rather than internal". A named type-only -re-export, matching the four in-repo plugins that ship a `src/types.ts` and the worked -example in the plugin-development guide. - -**That interface now extends `BaseSchema` from `@object-ui/types`** instead of re-declaring -a subset of the base node. Unreachable, a hand-rolled `{ type; id?; className? }` was only -dead weight; published, it would be a second dialect of a node the protocol already defines, -silently missing everything else `BaseSchema` carries (`name`, `label`, `visible`, …). Only -the `type` literal is narrowed locally, the same shape every in-repo plugin uses. This also -makes the generated `@object-ui/types` dependency a used declaration. - -Both halves are pinned structurally rather than by string match, so the next dead artifact -fails a test instead of shipping: no versioned runtime dependency may be declared that no -generated source imports (`workspace:*` exempt — it cannot drift), and no generated `src/**` -module may be unreachable from the single entry the `exports` map exposes. Each of those -gates passes over an empty result on today's templates, so each is paired with a self-test -that plants the removed defect back and asserts the rule names it — a gate that is green -because it produces nothing is not a gate. diff --git a/.changeset/data-table-row-menu-empty-guard.md b/.changeset/data-table-row-menu-empty-guard.md deleted file mode 100644 index a36ae5dbc6..0000000000 --- a/.changeset/data-table-row-menu-empty-guard.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/components': patch ---- - -fix(data-table): don't render a row overflow ("⋮") trigger that opens an empty menu - -The row overflow trigger was gated on whether row-action **handlers** were -supplied (`onRowEdit` / `onRowDelete` / `rowActionDefs`), while the menu's items -were filtered a second time — per item, per record — against -`rowEditPredicates` / `rowDeletePredicates` and a custom action's `visible`. On a -row where every item was predicate-suppressed the trigger still rendered and -opened an empty box, which reads as a broken page. - -The trigger is now decided by the items that will actually render for that row, -resolved through the same visibility rule the items gate themselves on, so the -two cannot disagree. The decision is per row: within one table a row that keeps -an action keeps its trigger while a row with nothing left renders none. The -actions cell itself is unchanged, so the column stays aligned with its header. diff --git a/.changeset/dataset-drill-ranges-passthrough-3813.md b/.changeset/dataset-drill-ranges-passthrough-3813.md deleted file mode 100644 index b9c3a13709..0000000000 --- a/.changeset/dataset-drill-ranges-passthrough-3813.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -data-objectstack: pass the server's `drillRanges` date-bucket drill scope through `queryDataset` (restores date drill-through) - -`queryDataset` rebuilds its result by **hand-picking** keys off the REST payload, -and `drillRanges` was never in the list — so the analytics service's date-range -drill sidecar (framework#1752) was dropped by the only real adapter in this repo, -while five consumer call sites were already reading it (`DatasetWidget.tsx:471` -and `:593`, `DatasetReportRenderer.tsx:316`, `:431`, `:855`). - -The user-visible effect was not a degraded drill but a missing one. A -`dateGranularity` dimension groups a **span** of records into one bucket, which -equality filters cannot express, so `service-analytics` deliberately excludes -date dimensions from `dimensionFields`/`drillRawRows` and sends a parallel -half-open `[gte, lt)` range per row instead. For a chart or report grouped **only -by time** that makes `drillRanges` the *only* thing that can make -`canDrill = !!object && (drillDims.length > 0 || !!drillRanges?.length)` true — -with the key dropped, the entire drill entry point disappeared. A mixed -date + non-date grouping kept its drill but built a filter with no time bound, so -clicking June's bar opened every month (a superset). - -Neither side's tests could see it: the dashboard and report tests mock their own -data source and feed `drillRanges` in directly, and the adapter's own suite never -asserted the key. The new adapter-level tests therefore mock the **envelope the -server actually sends** — bare (`res.json(result)`, no `{ success, data }` -wrapper), carrying `sql`, and for a date-only grouping carrying `object` + -`drillRanges` and *no* `dimensionFields`/`drillRawRows` — then assert the key -arrives verbatim and row-aligned, that the consumers' own `canDrill` predicate is -true, and that `buildDatasetDrillFilter` (the shared builder both surfaces call) -scopes the drilled list to the clicked bucket. - -The declared entry type is `@object-ui/core`'s `DatasetDrillRange` **by -reference**, per the objectui#3613/#3752 discipline: it is the single in-repo -declaration of this shape (what the filter builder accepts and what both -renderers type their state with), and nothing in `@objectstack/spec` owns it yet, -so restating `{ field, gte, lt }` locally would create a third dialect of it. - -`drillRawTotals` (the totals-row companion, framework#3214) is deliberately -**not** added: it has zero consumers in this repo, so passing it through would -add a declared-but-unexercised return key with no user-facing effect — it belongs -in the change that lands a totals-row drill and can test it. diff --git a/.changeset/dataset-result-fields-spec-type-3752.md b/.changeset/dataset-result-fields-spec-type-3752.md deleted file mode 100644 index 88f8a368ee..0000000000 --- a/.changeset/dataset-result-fields-spec-type-3752.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -data-objectstack: type `queryDataset`'s result `fields[]` as the spec's `AnalyticsResult.fields[]` element instead of a hand-written copy - -The return-value half of the drift objectui#3613 fixed on the parameter side. The -adapter hand-listed five keys for a result column -(`name`/`type`/`label`/`format`/`currency`) and, like every restatement, stopped -at the contract of the day it was written: it never grew **`percentScale`**, -which `@objectstack/spec@17.0.0-rc.5` carries on -`AnalyticsResult.fields[]` and documents as mandatory reading for renderers — -"renderers that receive it must scale by it instead of guessing from the value" -(objectui#3136). - -That omission was not cosmetic. `percentScale` is the server's answer to a -question a `%` format string cannot express (is the stored number a 0–1 fraction, -or already percentage points?), and objectui#3136 exists because guessing from -the value's magnitude printed a ratio of exactly `1` as "1.0%". Three in-repo -consumers read the field through their own local types -(`DatasetResultField` in `@object-ui/core`), so nothing was red here — but any -author reading columns through the adapter's **declared** return type got -`Property 'percentScale' does not exist`, i.e. the declaration actively steered -them back to the guess the spec bans. - -`fields` is now the spec type by reference, so there is nothing left to re-sync; -the change is additive for existing consumers (one more optional key). -`queryDataset.test.ts` pins structural identity with the spec element, pins -`percentScale` as the `'fraction' | 'whole'` union rather than a widened -`string`, keeps a negative pin against the five-key shape, and adds a runtime -test that reads `percentScale` off a result column **through the declared type**. - -The rest of the envelope stays locally declared, deliberately. It is the REST -envelope, not an `AnalyticsResult`: the route adds ADR-0021 D2 drill metadata -(`object` / `dimensionFields` / `drillRawRows`) on top of the spec result, and -this method rebuilds its own object from the payload without copying `sql` — so -declaring the envelope as `AnalyticsResult & { … }` would advertise a key the -adapter structurally cannot return. A pin records that too. diff --git a/.changeset/dataset-selection-spec-type-3613.md b/.changeset/dataset-selection-spec-type-3613.md deleted file mode 100644 index 1690d31b53..0000000000 --- a/.changeset/dataset-selection-spec-type-3613.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -data-objectstack: type `queryDataset(selection)` as the spec's `DatasetSelection` instead of a hand-written copy - -The adapter restated the selection contract inline, field by field, and the copy -had drifted three ways from the pinned `@objectstack/spec@17.0.0-rc.5`: - -- **`compareTo.dimension` was required.** It has been optional since - objectstack#5011, *because the executor resolves it*: exactly one time - dimension carrying a `dateRange` is the one shifted, and zero or several - raises a loud error naming the candidates. Requiring it made the compiler - demand from every typed caller precisely the consumer-side dimension guess - that change forbids — trading a loud executor error for a silently wrong - comparison window. No runtime path hit this yet (the dashboard's - `DatasetWidget` passes `selection` as `unknown`), but a declaration is a live - instruction to anyone calling this client from TypeScript. -- **`timeDimensions` was widened to `unknown[]`**, erasing the very entry shape - the executor's resolution reads (`{ dimension, granularity?, dateRange? }`), - and **`runtimeFilter` to `Record`**, erasing the - `$and`/`$or`/`$not` vocabulary the server parses. -- **`dateGranularity` was missing entirely** — the copy had simply stopped at - whatever the contract looked like the day it was written, so a typed caller - could not bucket a trend by month at all. - -The parameter is now the spec type by reference, so there is nothing left to -re-sync. The fix is the removal of the dialect rather than a correction to it: -restating a contract owned elsewhere creates a second de-facto dialect of it, and -drift is then only a matter of time (AGENTS.md #0/#0.1). `queryDataset.test.ts` -pins structural identity with `DatasetSelection` plus each of the three drifts -individually, checked by this package's `tsc --noEmit`; a runtime test pins that -a dimension-less `compareTo` reaches the server untouched, so the adapter can -never start guessing on the executor's behalf. - -The response type is deliberately left alone — it is the REST envelope -(`object` / `dimensionFields` / `drillRawRows`), not a restatement of -`AnalyticsResult`. diff --git a/.changeset/dataset-widget-colorvariant-3359.md b/.changeset/dataset-widget-colorvariant-3359.md deleted file mode 100644 index 64c42b3f98..0000000000 --- a/.changeset/dataset-widget-colorvariant-3359.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -"@object-ui/plugin-dashboard": patch ---- - -Dataset-bound metric cards honour their declared `colorVariant` (objectui#3359, objectstack#5010 ruling B) - -`DashboardWidgetSchema.widgets[].colorVariant` has been spec-declared, offered by -every authoring surface (the widget inspector, the dashboard editor, the config -panel) and authored **16 times** in shipped metadata — `system_overview` ×7 in -`platform-objects`, app-showcase's `ops-dashboard` / `revenue-pulse` ×9 — with -every one of those a `type: 'metric'` widget bound to a dataset. None of them -ever rendered a colour. - -The reason is structural rather than a missing branch: `dataset` is **required** -on `DashboardWidgetSchema`, so every legal widget reaches `DatasetWidget` through -one of `DashboardRenderer`'s two dispatch sites, and `DatasetWidget` read the key -nowhere. Only the inline (`object` + `valueField`) path had a colour affordance, -via the `...options` spread into `MetricWidget` — a path the current schema -cannot produce. Declared, authored, offered in the designer, and inert: the -renderer painted all sixteen the same. - -The metric card now maps the declaration onto the accent system this package -already has, instead of a second one: - -- the vocabulary is the spec's `WidgetColorVariantSchema` enum, read from the - spec **in a test** rather than restated in prose — `default`, `blue`, `teal`, - `orange`, `purple`, `success`, `warning`, `danger`; -- the accent lands on the big number, the way `MetricWidget`'s chrome-less - `bare` layout carries it, because a dataset-bound metric renders no icon chip - and no card of its own. A dataset-bound KPI and an inline `bare` KPI declaring - the same variant now read the same; -- the two class tables both layouts use moved into one shared module - (`colorVariants.ts`) rather than being copied — the designer's swatch picker - already calls itself a mirror of "the renderer's colorVariant tokens", and a - second copy of a palette is how a declared-but-unenforced key becomes the - harder bug: a key declared two disagreeing ways. - -Nothing changes for a widget that declares no `colorVariant`: its markup is -pinned byte-for-byte against the pre-change render, as is the enum's own -`'default'` (its name for "no accent"). Off-spec tokens — including the swatch -picker's three display-only aliases `green` / `red` / `amber`, which exist so a -legacy stored value can still be drawn as a swatch — get no accent and no -aliasing here: the spec enum rejects them where metadata is authored and -published, and teaching the renderer a second spelling would hand AI-authored -metadata a dialect the contract does not have. diff --git a/.changeset/declared-actions-bar-visible-gate-3835.md b/.changeset/declared-actions-bar-visible-gate-3835.md deleted file mode 100644 index 93d3ce6711..0000000000 --- a/.changeset/declared-actions-bar-visible-gate-3835.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -Server-declared actions declaring `visible: false` are now hidden instead of rendered as live buttons (objectui#3835) - -`DeclaredActionsBar` — the bar that renders an object's SERVER-declared actions -for one record at a `location`, with no per-action host code — asked truthiness -on the gate: `if (action.visible && !isVisible) return null`. `false && …` is -falsy, so `visible: false`, the most explicit way an author can say "never show -this", fell into the "no gate declared" branch, the verdict was never consulted, -and the action rendered for everyone. - -What that means on the page: the bar's host is the approvals inbox's -record-section toolbar (`apps/console/src/pages/system/ApprovalsInboxPage.tsx`), -so an approval action the metadata had switched off with `visible: false` -rendered as a live Approve / Reject / Reassign button — and this component's own -click handler is what POSTs the decision. One click was a real approve/reject -call on a request the declaration said not to offer a decision on. - -This is the fifth and last member of the objectui#3492 family (after -objectui#3758 / PR #3816 for the row-action surfaces and objectui#3812 / #3823 -for the action face), and the one whose two family-wide mitigations both fail: - -- The action defs are **server-declared** (`objectDef.actions[]`, - `sys_approval_request`), not hand-written view JSON. "`ActionSchema.visible` is - `ExpressionInputSchema` with no boolean member, so `objectstack build` cannot - emit this shape" does not apply on this path — the def arrives from server - metadata and in-process construction, where a boolean is the natural spelling. -- The bar is mounted as **plain JSX** by its hosts, so `packages/react`'s - `SchemaRenderer` — which evaluates a node's `visible` and hides it before the - component mounts, and which is why objectui#3812 judged the component-level - gates a dormant defensive layer — is not on this path at all. This gate was the - only one there. - -The gate now reads the family's one named definition, -`hasDeclaredVisibilityGate` (`!= null && !== ''`), imported from -`@object-ui/components` rather than re-spelled: five gates in three packages -asking one question must not drift into five answers. The evaluation entry is -untouched — `toPredicateInput` passes a boolean through and `useCondition` -short-circuits it instead of calling the expression engine — so a declared -`false` resolves to `false`, and every expression-valued `visible` keeps exactly -the verdict it had. - -Behaviour change surface, deliberately narrow: only a declared action whose -`visible` is the literal boolean `false` (or another falsy non-empty value) -changes, from rendered to hidden, which is what the declaration asked for. -`visible: true` still renders, `''` and an absent `visible` are still no gate at -all, and the bar still renders no chrome when its located set is empty. - -The suite that covered this component could not have caught it: it stubbed the -whole predicate entry constant-true (`useCondition: () => true`), with a comment -saying the test actions omit `visible` "so this is unused" — which made the gate -unreachable from the only tests that mount this component (the objectstack#4984 -family, where a fixture keeps a broken rule green). That stub is gone; the suite -now runs the real `useCondition` / `toPredicateInput` and doubles only the action -dispatch, so all four shapes (`false` hides / `true` renders / undeclared renders -/ `''` is not a gate) are judged by the shipped evaluation semantics. diff --git a/.changeset/default-list-view-identity-3770.md b/.changeset/default-list-view-identity-3770.md deleted file mode 100644 index ef7d3fe8f4..0000000000 --- a/.changeset/default-list-view-identity-3770.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -Ask the view composer for a container's view identities instead of deriving `list.name || 'list'`, so the default list view's translated label resolves - -A `defineView` container declares its default list under the `list` key. That key is a slot in the authoring document, not the view's identity: `expandViewContainer` — the same composer the framework's loader and the i18n extractor call — registers an unnamed default list as `.default`. This renderer derived `list.name || 'list'` instead, a third spelling no producer emits, so a default-list-only object probed `objects.._views.list.label`, missed the published `_views.default.label` key (objectstack#5164 ruling A, migrated in objectstack#6124) and fell back to the English metadata label — for the view's description and empty state too. - -- `MetadataProvider.mergeViewsIntoObjects` now expands a stack-packaged container through `expandViewContainer` and routes the result through the same code path as first-class ViewItems. Both authoring gates therefore key `listViews` / `formViews` by the canonical `.` identity, and the container inherits the composer's folding (a `listViews` entry that merely restates `list` collapses into one view) and collision renaming instead of restating them locally. -- `ObjectView` resolves the primary view's id through the new `defaultListViewId` helper — one derivation shared by the view-override lookup and the view-switcher promotion, with no literal fallback. - -The renamed id is also the key a view override is persisted under (`updateViewConfig(object, viewId, …)` writes a `view` metadata record named by the id). Nothing is orphaned: the retired `'list'` spelling is not a representable view identity at all — `ViewItemNameSchema` requires a dotted `.` name — while the record-gate path, which real backends serve, already used the qualified id. Stale `/view/list` links fall back to the object's default view, which is the same view they named. diff --git a/.changeset/environment-create-cta-loading-skeleton-3482.md b/.changeset/environment-create-cta-loading-skeleton-3482.md deleted file mode 100644 index fa98382098..0000000000 --- a/.changeset/environment-create-cta-loading-skeleton-3482.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -console: hold the environment list's create CTA with a skeleton until entitlements -resolve, instead of showing a label that is about to be overwritten (objectui#3482, -part of cloud#1049). - -`EnvironmentListToolbar` presents a state-aware create affordance — "Set up your -production environment" / "Add development environment" / an upgrade prompt — decided -from `GET /cloud/environment-entitlements`. While that request was in flight the -toolbar rendered the action's metadata label, so the button visibly changed its -wording the moment the response landed. The two texts are owned by different -packages (the cloud translation bundle vs this repo's locale packs), which made the -swap read as an inconsistency rather than a load. - -The in-flight state now renders a `Skeleton` sized like the button it stands in for, -matching the adjacent `cloud:onboarding-next` welcome CTA. Only the create action is -withheld — other toolbar actions never re-label, so they keep rendering — and a -toolbar without a create action gets no skeleton at all. The skeleton is never -terminal: when both entitlement signals fail, the resolution settles as -`{ ready: false, source: 'unknown' }` and the neutral metadata label is shown, which -remains the honest text for a state where "which create is this?" is genuinely -unknown. diff --git a/.changeset/header-action-predicates-speak-cel-3521.md b/.changeset/header-action-predicates-speak-cel-3521.md deleted file mode 100644 index 5d28cdeeb4..0000000000 --- a/.changeset/header-action-predicates-speak-cel-3521.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -"@object-ui/components": patch ---- - -Record page header action predicates now speak CEL, like every other action surface - -`visible` / `hidden` / `disabled` on a `page:header` action were handed to -ObjectUI's legacy JS evaluator, while the row kebab, the selection bar and -conditional formatting have evaluated the identical metadata on the canonical -CEL engine since objectui#1584 / ADR-0058. Every construct that exists only in -CEL therefore worked in a list row and threw on the record page — where the -throw fail-closed hid the button, leaving nothing on screen to notice: - -- method calls — `record.f_tags.size() > 0`, `record.f_textarea.contains("x")` -- the `in` operator — `'"red" in record.f_multiselect'` (a parse error) -- stdlib functions — `record.f_date < today()` (`today is not a function`) - -Both header evaluation sites now go through `evalRowPredicate`: the same entry, -the same bindings (`record.*` + bare field names + `data.*` + the host scope, -with relations bound as the stored foreign key), and the same fail-closed + -warn-once semantics as the row surfaces. One predicate on one record now reaches -the same show/hide verdict in the row menu, the selection bar and the record -header. - -Legacy-dialect strings are unaffected: `${…}`, `===`/`!==`, `?.`, `??` and -JS-only methods such as `.includes()` still route to the legacy evaluator (with -its existing one-time deprecation warning), so authored pages keep working. A -`${…}` template predicate, which the header previously could not evaluate at -all, now resolves through that fallback instead of hiding the button. A -predicate that genuinely cannot be evaluated still hides its action, and now -reports itself once in the same words the other surfaces use, naming the -surface, the action and the predicate. diff --git a/.changeset/highlights-readonly-authoring-surface-3407.md b/.changeset/highlights-readonly-authoring-surface-3407.md deleted file mode 100644 index d4fb034540..0000000000 --- a/.changeset/highlights-readonly-authoring-surface-3407.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -"@object-ui/plugin-detail": patch ---- - -`record:highlights` publishes the `readonly` entry key, so an AI author can discover it from the manifest - -`readonly` on a `fields[]` entry has been enforced for a while — the renderer copies it -through normalization and `HeaderHighlight`'s editability gate refuses inline editing on a -chip carrying it (objectstack#5077) — and `@objectstack/spec` declares it on -`RecordHighlightsField` (objectstack#5176 / PR #5607). The block's own published authoring -surface never mentioned it: the `fields` input still spelled the entry shape -`{name,label?,icon?,type?}`, and since the registry `inputs` are what -`gen-manifest.ts` serializes into `sdui.manifest.json`, an author reading the manifest was -told the key did not exist. The `fields` description now states the full entry shape and -what `readonly` does, which is the discoverability the manifest is for. - -`readonly` is documented **inside** the `fields` description rather than declared as an -input of its own, because that is where the contract puts it. The spec's -`RecordHighlightsProps` has exactly three top-level keys (`fields`, `layout`, `aria`) and -carries `readonly` per ENTRY. A top-level `{ name: 'readonly', type: 'boolean' }` input -would publish a key the platform silently discards: the generated `sdui.manifest.json` and -`sdui-intrinsics.d.ts` would advertise a `readonly` prop, the manifest gate validates -top-level props only and would raise no diagnostic, `RecordHighlightsProps` is a plain -`z.object` so the unknown key is stripped on parse without error, and the renderer — which -reads `field.readonly` per entry — would never see it. An author who trusted that surface -would be left with the machine-owned column still hand-editable and no diagnostic anywhere -explaining why. `ComponentInput` is flat by design, so an array-of-objects input publishes -its member keys in prose, as `record:path.stages` and `record:alert.action` already do. - -A new spec-parity test derives both directions from `@objectstack/spec` at runtime instead -of restating today's key list: every key of `RecordHighlightsField`'s object arm must be -named in the `fields` description, and the block must declare no top-level input that -`RecordHighlightsProps` does not accept. Nothing previously cross-checked the registry -`inputs` against the spec, so both drift directions were silent. No runtime behaviour -changes. diff --git a/.changeset/hitl-decision-outcomes-spec-derived-3783.md b/.changeset/hitl-decision-outcomes-spec-derived-3783.md deleted file mode 100644 index 827aa22d15..0000000000 --- a/.changeset/hitl-decision-outcomes-spec-derived-3783.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -"@object-ui/plugin-chatbot": minor ---- - -`ApproveOutcome` / `RejectOutcome` are now derived from `@objectstack/spec` -instead of hand-transcribed (objectui#3783). Same failure class #3220 cleared -from the same file for `PendingActionRow` / `PendingActionStatus` — but this pair -wore local names rather than spec names, so `check-spec-symbol-derivation.mjs`, -which fires on a spec export name being occupied, had no handle on it. A renamed -hand copy is invisible to a name-based guard by construction. - -Both types now re-export the spec's decision responses -(`ApproveAiPendingActionResponse` / `RejectAiPendingActionResponse` from -`@objectstack/spec/api` — the same schemas `@objectstack/client`'s -`ai.pendingActions.approve()` / `.reject()` type their returns with). The public -export names do not change. The shapes do, in three ways: - -- **`ApproveOutcome` no longer declares `id`.** The approve response has never - carried one — `id` is on the *reject* response. This was the one drift that - was not dormant: `useHitlInChat`'s public `onDecided` callback promised - consumers `id: string` and handed them `undefined` at runtime, with nothing - in the compiler to say so. **If you read `outcome.id` after an approve, that - read was already `undefined` and now fails to compile** — take the id from - `ContinueContext.pendingActionId` or from the row you decided on. -- **`status` is closed.** `'executed' | 'failed' | string` and - `'rejected' | string` were both just `string`: a union with `string` absorbs - the literals, so neither annotation carried any information. They are now - `'executed' | 'failed'` and `'rejected'`. -- **The `[k: string]: unknown` index signature on `ApproveOutcome` is gone.** The - objectstack#4075 mechanism: with it, any structural comparison against the - spec answers "identical" however far the copy has drifted, so a parity test - bolted onto the old type would have been green from its first day. - -**Breaking at the type level for importers of `@object-ui/plugin-chatbot`** — -narrowing a published type is a break even when the old type was lying, which is -why it is spelled out here. Shipped as `minor` per AGENTS.md §版本号策略: the -family's `major` tracks `@objectstack`'s, and objectui's own breaking changes go -out as `minor` with the break named in the changeset. - -Runtime behaviour is unchanged — including the hook's decision handling for a -status outside the spec vocabulary, and the locally synthesized failure envelope -on a non-2xx, both now pinned by tests. The consumer-side tolerances that remain -in `useHitlInChat` are recorded in objectui#3790 for a maintainer decision. diff --git a/.changeset/home-administration-group-3609.md b/.changeset/home-administration-group-3609.md deleted file mode 100644 index 13d892160c..0000000000 --- a/.changeset/home-administration-group-3609.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Render the `/home` Administration group as a real group, so its nine system-administration entries are reachable (objectui#3609). - -`UnifiedSidebar` picks its renderer with one ternary on `context === 'app' && activeApp`. Only the app arm rendered `NavigationRenderer`, the component that descends into `type: 'group'` children; the home arm hand-rolled `homeNavigation.map(item => )` with no recursion. Since home navigation is the only navigation that groups, the whole nine-entry Administration cluster collapsed into one row — and a group carries no `url` of its own, so `|| '/home'` pointed that row back at the page the user was already on. System Settings, Applications, App Marketplace, Object Manager, Datasources, Users, Organizations, Roles and Configuration never reached the DOM. `resolveLandingPath([])` sends a fresh-deployment admin to `/home`, and `HomePage` had deliberately dropped its own System card on the grounds that the sidebar already carried those entries, so the net effect was an admin with no route into system administration at all. - -The home arm now renders through the same `NavigationRenderer` as the app arm rather than growing a second renderer that recurses: the group becomes a Collapsible and every entry passes the same item-level `visible` / `requiredPermissions` / runtime-capability guards. Hrefs are unchanged — home entries are all `type: 'url'`, whose resolution is verbatim. The group states `expanded: true` so it opens by default: the renderer's unauthored default collapses groups of eight or more children, a heuristic for one long section among many, whereas on `/home` this group *is* the navigation. Pinning and drag-reorder stay off in the home context, where their persistence key resolves to the first app rather than to home. Non-admins are unaffected — the cluster is still built behind the `isWorkspaceAdmin` gate and is absent from their item tree. diff --git a/.changeset/host-redirect-canonical-metadata-3639.md b/.changeset/host-redirect-canonical-metadata-3639.md deleted file mode 100644 index 88615c1c75..0000000000 --- a/.changeset/host-redirect-canonical-metadata-3639.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/console': patch -'@object-ui/app-shell': patch ---- - -Send the console host's legacy URL redirects straight to the canonical metadata-admin routes instead of routing them through the deprecated `component/metadata/resource` alias (objectui#3639). - -`apps/console`'s `ObjectRedirect` and `MetadataRedirect` rewrote `system/objects[/:name]` and `system/metadata[/:type[/:name]]` onto `…/component/metadata/resource[/:name]?type=:type`. app-shell declares that spelling as a legacy *alias*, not a page: its route element is `LegacyMetadataRedirect`, which immediately navigates on to `…/metadata/:type[/:name]`. Every one of those URLs therefore took two `` hops (plus a re-render) to reach a destination the host could name directly — and it was this indirection that carried `sys-objects` into the zero-app blank screen fixed in objectui#3610, since the alias was the leg that branch did not recognise. - -Both redirects now construct `…/metadata/:type[/:name]` (and `…/metadata` for the typeless directory arm) themselves. The endpoints are unchanged, byte for byte, including the alias hop's own percent-encoding of `:type` and its verbatim pass-through of `:name`; only the intermediate hop is gone. The alias routes stay declared exactly as they were — bookmarks, external links and the setup left-nav still arrive on them and are still forwarded — this change only stops the console feeding its own traffic through them. - -Also corrects four docblocks that described the alias as "the engine route", in `apps/console`'s two redirects and in app-shell's `datasource` resource registration and page. That wording is not merely stale: the objectui#3610 dispatch read this chain and concluded `component/metadata/resource` was the canonical spelling, which is the exact opposite of what the route table says. diff --git a/.changeset/inputs-reverse-parity-3808.md b/.changeset/inputs-reverse-parity-3808.md deleted file mode 100644 index 05aafc1fdf..0000000000 --- a/.changeset/inputs-reverse-parity-3808.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -"@object-ui/plugin-detail": patch -"@object-ui/components": patch ---- - -Four spec keys the renderers already honoured are now discoverable from the published `inputs` - -`record:details.hideFields`, `record:related_list.relationshipValueField`, -`record:related_list.add` and `element:text_input.defaultValue` were declared by -`@objectstack/spec` and read by their renderers, while the registry `inputs` — -the surface `gen-manifest.ts` serializes into `sdui.manifest.json` and -`sdui-intrinsics.d.ts` — never mentioned them. Nothing anywhere reported the -mismatch, and every layer that reads a manifest said the opposite of the -runtime: the keys were in no designer panel and no generated `.d.ts`, -`sdui-parser`'s prop walk returned `unknown-prop` for an author who wrote one, -and the renderer honoured it regardless. That is objectui#3407's original -complaint (`readonly` was enforced and honoured, the description just never said -so) on four more keys. - -Each description is derived from what the renderer actually does, not from -restating the spec's one-liner, because the two can differ and the published -text is what an AI author reads: - -- `hideFields` documents bare field names only — the renderer tolerates - `{name}` / `{field}` entries but the spec is `z.array(z.string())` and rejects - them, so teaching that spelling would publish a dialect the contract refuses; -- `relationshipValueField` publishes the renderer's `'id'` default and says that - the resolved value drives the list filter, the Add-picker link value and the - pre-filled create form together; -- `add` publishes its member shape in prose (`ComponentInput` is flat and has no - member-shape slot) with each default taken from the renderer — including - `picker.labelField`, where the renderer defaults to `name` while the spec's - own wording says "the object title field". It also names `picker.filter` as a - KNOWN GAP rather than documenting it as a restriction: the spec declares it - and nothing reads it, so an author would otherwise believe their picker is - scoped when it offers every record (objectui#3831); -- `defaultValue` distinguishes the two behaviours an author can get — seeding a - bound page variable once while it is still empty, versus the native - uncontrolled initial value with no variable bound. - -`element:text_input` is not in the public tier, so its gap was not in -`sdui.manifest.json` at all — it was in the JSX-page compiler's prop whitelist, -which `renderers/layout/page.tsx` builds from `getKnownTypes()` plus these same -`inputs`, making the undeclared `defaultValue` a live `unknown-prop` warning. - -The repo-wide parity gate now runs in both directions over one covered set and -one exemption discipline, so neither direction can be forgotten again the way -the reverse half was after PR #3806. Nine spec keys stay deliberately -unpublished, each with a written reason and a tracking issue: two the renderers -do not read at all (objectui#3829), three retired upstream by ADR-0087 -tombstones, `page:tabs.type` (a carrier collision, objectstack#6776), two -`targetVariable` declarative hints (objectui#3834), and -`element:record_picker.filter` (objectui#3830). diff --git a/.changeset/lucky-pugs-shave.md b/.changeset/lucky-pugs-shave.md deleted file mode 100644 index 9f19fccdba..0000000000 --- a/.changeset/lucky-pugs-shave.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -metadata-admin: name the offending key when only one union member ever read the value - -A union with no discriminant reports its failure as one collapsed issue, and the -member diagnostics that would name the problem are buried inside it. PR #3677 -started unpacking those for `config.columns` by reading the value's own content, -but deliberately declined every union where some member had rejected the value's -type outright — which left `config.sort` (`string | ColumnSort[]`) collapsed even -though only one of its two members had read the value at all. - -When exactly one member accepted the value's type, naming it is a fact rather -than a preference: it is the only member whose complaint can be about what the -author wrote. So `sort: [{ field: 'n', order: 'bogus' }]` now reports -`config.sort.0.order` with the spec's own `expected one of "asc" | "desc"` -instead of `config.sort` / `Invalid input`, and the same holds for a sort row -that is not an object, a `columns[].summary` written as a bad enum string, a form -`sections[].fields[]` entry missing its `field`, and an array `filter[].value` -whose offending element is now addressed directly. - -Where two or more members read the value, or where none did, nothing changes: -the previous message is kept rather than inventing a preference between members -that objected equally. Both gates — create and edit — continue to report -identically, and validation verdicts are untouched: the accept/reject decision is -still made by the one gate, and this only changes how an already-failed draft is -presented. diff --git a/.changeset/marketplace-preview-locale-keys-3546-slice5.md b/.changeset/marketplace-preview-locale-keys-3546-slice5.md deleted file mode 100644 index f2fa4531c2..0000000000 --- a/.changeset/marketplace-preview-locale-keys-3546-slice5.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -"@object-ui/i18n": patch ---- - -Backfill the `marketplace` and `preview` namespaces' 37 missing locale keys plus the `marketplace.disclosure.runtime.` template-key family (objectui#3546, slice five) - -`scripts/check-i18n-call-site-keys.mjs` (objectui#3530) measured 37 keys that a -`t()` call site asks for and that **no locale pack defined** — 37 distinct keys at -37 call sites across five console components — plus one `missing-prefix` family -whose static head matched no `en` key at all, so every expansion missed. All 37 -carried an inline `t(key, { defaultValue: 'English' })`, which is exactly the -objectui#3517 class: English rendered correctly, and **all ten languages were -stuck on it** for months. Nothing here rendered a raw key — slice one (PR #3583) -held those sites. - -What that meant on the page for a `zh` (or `ja`, `de`, `ar`, …) user: the -marketplace's "Your organization" strip, its Install / Installing… / Installed -buttons and the version-update affordances were English; the whole ADR-0025 PD4 -**pre-install permission disclosure** was English — "This package contains code", -the trust-tier badge, "Reviewed & approved" / "Not yet reviewed" / "Signed", the -four permission group labels (platform services, lifecycle hooks, network, -filesystem) and the consent checkbox the user ticks to accept them; the ADR-0045 -unpublished-app banner and its publish toasts were English; and the entire -ADR-0067 build-history sheet — title, description, the per-commit labels, the -Revert button and both of its result toasts — was English. - -`marketplace.disclosure.runtime.` is repaired as an **enumeration, not a -wildcard**: its value surface is the closed trust-tier enum -(`PluginRuntimeSchema` = `z.enum(['node', 'sandbox', 'worker'])`, ADR-0025 §3.6), -so all three members are backfilled and the family leaves the ratchet's -`missingPrefixes` (3 → 2). A test reads the component's own fallback map and -fails if a fourth tier is ever added without a key — the job the prefix entry -used to do. - -Each `en` value is byte-identical to the inline `defaultValue` it replaces (36 of -36 literal sites; the 37th's `defaultValue` is a template literal whose -`${pkg.display_name}` becomes the `{{name}}` hole its call site already passes), -so no English string a user sees today changes. The nine translations follow each -pack's own neighbourhood — including two namespaces that legitimately take -**different** second persons in `zh` (`marketplace` 你, `preview` 您) — and reuse -an existing neighbour's translation wherever the `en` string already existed -verbatim, so one English string never renders as two different sentences in the -same language. - -No component changed: an AST sweep of the whole `marketplace.*`/`preview.*` -call-site surface found the slice's own dead-`||`-fallback count to be zero. diff --git a/.changeset/nav-canonical-metadata-routes-3660.md b/.changeset/nav-canonical-metadata-routes-3660.md deleted file mode 100644 index ab423eaee3..0000000000 --- a/.changeset/nav-canonical-metadata-routes-3660.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/console': patch -'@object-ui/app-shell': patch ---- - -Point the last four navigation producers at the canonical metadata-admin routes instead of the deprecated `component/metadata` alias, removing a redirect hop from each (objectui#3660). - -The System hub's "Metadata" and "Datasources" cards aimed at `…/component/metadata/directory` and `…/component/metadata/resource?type=datasource`, and the `sys-datasources` entry in both `AppSidebar.systemFallbackNavigation` and `UnifiedSidebar.homeNavigation` spelled the latter too. app-shell declares those spellings as legacy *aliases*, not pages: their route element is `LegacyMetadataRedirect`, which immediately navigates on to `…/metadata` and `…/metadata/datasource`. Every click on any of the four therefore paid a redundant hop plus a re-render to reach a destination the navigation could name directly. All four now name it. - -The landing pages are unchanged, byte for byte — the new URLs are exactly what the alias hop was already computing (`datasource` percent-encodes to itself, and neither producer carried a query or hash beyond the `?type=` the alias itself consumed). Only the intermediate hop is gone. - -The alias routes stay declared in both `AppContent` branches, untouched: bookmarks and external links still arrive on them and are still forwarded. This completes objectui#3639, which corrected the console host's two redirects and enumerated these four as the remainder. diff --git a/.changeset/nav-sys-objects-canonical-route-3739.md b/.changeset/nav-sys-objects-canonical-route-3739.md deleted file mode 100644 index 0f95492737..0000000000 --- a/.changeset/nav-sys-objects-canonical-route-3739.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Point the `sys-objects` navigation entries at the canonical metadata-admin route instead of the `system/metadata/object` alias, removing a redirect hop from each click (objectui#3739). - -`AppSidebar.systemFallbackNavigation`, `UnifiedSidebar.homeNavigation` and `console/home/QuickActions` all spelled this target `/apps/setup/system/metadata/object`. That is not a page: `apps/console`'s host fragment declares `system/metadata/:metadataType` with `MetadataRedirect` as its element, which immediately navigates on to `/apps/setup/metadata/object` — the engine's real route (`metadata/:type`, `MetadataResourceListPage`). Every click therefore paid a redundant hop plus a re-render to reach a destination the navigation could name directly. All three now name it. - -This is the same defect objectui#3660 fixed for `sys-datasources`, declared on the line immediately below `sys-objects` in both sidebar arrays. It was missed there because the two entries reached their aliases through different route tables — `sys-datasources` through app-shell's own `component/metadata/resource` alias, `sys-objects` through the host's `system/metadata/:type` rewrite. - -The landing page is unchanged, byte for byte: the new URL is exactly what the alias hop was already computing (`object` percent-encodes to itself, and no producer carried a query or hash). Only the intermediate hop is gone. Of the three producers, the two sidebars are live; `QuickActions` has no JSX call site today, so its change is a guard against the dead link returning with the component. - -The alias routes stay declared and untouched: bookmarks and external links still arrive on them and are still forwarded. diff --git a/.changeset/no-app-component-metadata-routes-3610.md b/.changeset/no-app-component-metadata-routes-3610.md deleted file mode 100644 index 47c9263c9c..0000000000 --- a/.changeset/no-app-component-metadata-routes-3610.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Make the zero-app console's "Object Manager" / "Datasources" entries resolve, and give that branch a not-found screen instead of a blank one (objectui#3610). - -On a deployment with no published apps, the system fallback navigation sends `sys-datasources` to `/apps/setup/component/metadata/resource?type=datasource` and `sys-objects` to `/apps/setup/system/metadata/object` (rewritten by the console host onto the same legacy alias). `isMetadataRoute` is a substring test on `/metadata`, so both URLs pass the "No Apps Configured" guard and enter `AppContent`'s no-`activeApp` route table — which declared no `component/…` route at all and, unlike the with-`activeApp` table, carried no trailing catch-all. A `` with no match renders `null`, so an admin building their first object got a fully blank screen: no 404, no error, no empty state. - -Both halves are fixed on the routing side, with no navigation URL changed. The two legacy metadata aliases (`component/metadata/directory`, `component/metadata/resource/*`) are now declared in the no-`activeApp` branch too, mirroring the with-`activeApp` branch — they are redirects, not a second copy of the page, so they forward onto the canonical `metadata/:type` routes that branch already declared. And the branch now ends in the same `path="*"` → "Page not found" screen the with-app branch has always had, so the next unresolved URL in a zero-app console is reportable rather than invisible. diff --git a/.changeset/no-apps-create-first-app-cta-3573.md b/.changeset/no-apps-create-first-app-cta-3573.md deleted file mode 100644 index 6ae29a41cf..0000000000 --- a/.changeset/no-apps-create-first-app-cta-3573.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The no-apps empty state's "Create Your First App" CTA now opens the app-creation -flow instead of silently bouncing the user back to the landing page. It called -`navigate('/create-app')` — an ABSOLUTE path, so it resolved against the HOST's -root route tree, which declares no `/create-app`; the reference host's trailing -`` therefore replaced it with `/`. The `create-app` route is -declared by `AppContent` itself, inside the `/apps/:appName/*` subtree (both the -no-active-app branch and the with-app router), so the CTA now builds the -app-scoped `/apps//create-app` — the platform's canonical app URL -(ADR-0048) and the same target the sidebar's add-app entry already links to. On -a fresh zero-app deployment this was the first screen's only route into app -creation, and it read as a button that does nothing (#3573). - -A plain relative `navigate('create-app')` is deliberately NOT the fix, and the -new routing test pins why: under the installed react-router 7, -`getResolveToMatches` resolves a relative target against the LEAF match's full -`pathname` with the splat INCLUDED (in v6 this was the `v7_relativeSplatPath` -future flag; v7 hardcodes it). The empty state renders across a whole URL family -— `/apps/setup` and any deeper `/apps/setup/` — so the relative form is -right only at the shallowest of them and builds -`/apps/setup//create-app` elsewhere, which matches no route and renders -a blank screen instead of the bounce. The sibling "System Settings" CTA is -unchanged. diff --git a/.changeset/no-apps-go-to-settings-target-3590.md b/.changeset/no-apps-go-to-settings-target-3590.md deleted file mode 100644 index 4d4d4cafa2..0000000000 --- a/.changeset/no-apps-go-to-settings-target-3590.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Point the "System Settings" entries at the system hub `/apps/setup/system` instead of the bare `/apps/setup` (objectui#3590). - -`AppContent` mounts the system hub only under `isSystemRoute`, which keys on a `/system` path segment. A bare `/apps/setup` therefore matched no pseudo-route except `isSetupRoute` and fell back into the "No Apps Configured" guard — i.e. on a zero-app deployment it *is* that empty state's own URL, so the empty state's `go-to-settings-btn` re-rendered the very screen it sits on. Retargeted three call sites: the empty state's CTA, `AppSidebar`'s no-active-app `sys-settings` fallback entry, and `UnifiedSidebar`'s `/home` Administration `sys-settings` entry. Every sibling entry in both clusters already spelled `/apps/setup/system/...`. diff --git a/.changeset/objectstack-family-rc5-refresh.md b/.changeset/objectstack-family-rc5-refresh.md deleted file mode 100644 index a8f04af1bf..0000000000 --- a/.changeset/objectstack-family-rc5-refresh.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -"@object-ui/types": minor -"@object-ui/core": minor -"@object-ui/react": minor -"@object-ui/mobile": minor -"@object-ui/data-objectstack": minor ---- - -Track the `@objectstack` family at `17.0.0-rc.5` (objectui#3560). - -The pin moves from `^17.0.0-rc.2` to `^17.0.0-rc.5` across all 37 declarations in -30 `package.json` files, and the sibling `@objectstack/*` packages (`client` / -`formula` / `lint`) move with it — they pin `@objectstack/spec` **exactly**, so -leaving them behind would keep a second copy of the spec in the tree and have -`@objectstack/lint` validating against schemas that still accept the keys rc.3–rc.5 -retire. `pnpm-lock.yaml` now resolves one copy of each of the six family packages -(`spec` / `client` / `core` / `formula` / `lint` / `sdui-parser`), all at rc.5. - -Bumping the pin and repairing the fallout cannot be split: the pin alone reddens -CI, and the code alone targets a shape that is not in effect yet. - -## A live bug this upgrade fixes - -**`ObjectStackDataSource.delete()` never emitted its mutation event, and resolved -`undefined` instead of a boolean.** `@objectstack/client`'s `DeleteDataResult` -declared a key called `deleted` — a key no schema has ever declared and no server -path has ever returned on `DELETE /data/:object/:id`. So `result.deleted` -compiled and read `undefined` at runtime: the guard never fired, a successful -delete notified no subscriber, and every consumer's cache stayed stale. -objectstack#5638 corrected the interface to the schema's `success`; following the -rename is what restores both behaviours. Nothing in this repo had to change shape -for it — the code was already asking the right question of the wrong key. - -## Breaking, in FROM → TO form - -- **The five `@objectstack/spec/ui` interaction-config modules are gone** — - touch / dnd / keyboard / animation / offline, 32 defs and 64 exports - (objectstack#4988, PR objectstack#5321). None of them had an authoring door: no - metadata document could ever carry one of these blocks, so a stack that parsed - before the retirement parses byte-for-byte the same after it. `@object-ui/types` - drops the 32 `export type` re-exports. The vocabulary each one's only real - consumer needs is now declared by that consumer, which is the remedy the spec's - own retirement ledger prescribes ("declare that union locally — it is your - client's policy, not the platform's"): - - `@object-ui/react`'s `useOffline` owns `OfflineStrategy`, `ConflictResolution`, - `PersistStorageType`, `EvictionPolicyType`, `OfflineConfig`, - `OfflineCacheConfig`, `OfflineSyncConfig`; - - `@object-ui/core`'s `DndProtocol` / `KeyboardProtocol` own `DndConfig`, - `DragItem`, `DropZone`, `DragConstraint`, `DragHandle`, `DropEffect`, - `KeyboardNavigationConfig`, `KeyboardShortcut`, `FocusManagement`, - `FocusTrapConfig`; - - `@object-ui/types`' `mobile` module owns `SpecGestureConfig`, - `SwipeGestureConfig`, `PinchGestureConfig`, `LongPressGestureConfig`, - `TouchTargetConfig`, `TouchInteraction` (plus a new `SPEC_GESTURE_TYPES` - runtime tuple), so `@object-ui/mobile`'s import paths are unchanged. - - Every shape is moved verbatim — same keys, same members, same optionality — so - no hook or bridge changes behaviour. Consumers importing these names from - `@object-ui/types` must import them from the owning package instead. Note the - spec's *surviving* `ConnectorConflictResolution` (`/integration`, connector sync) - and `ConflictResolutionStrategy` (`/api`, route merge policy) are **different - concepts** — do not re-point at them. -- **`@object-ui/types` no longer re-exports `NotificationAction` or `EmbedConfig`** - (objectstack#5015, PR objectstack#5300). Both were published `ui` vocabulary with - no authoring door; no notification action was ever parsed from metadata and no - iframe route ever read an embed config. The presentation enums - (`NotificationType` / `NotificationSeverity` / `NotificationPosition`) and - `SharingConfig` **survive** and are untouched — public form sharing still gates - the anonymous endpoints on `allowAnonymous` + `publicLink`. - `@object-ui/core`'s `SharingProtocol` keeps `resolveEmbedConfig` / - `generateEmbedCode` against a locally declared `EmbedConfig`, so its surface is - unchanged. -- **`ThemeEngine` stops emitting nine retired CSS variable groups** - (objectstack#5021 option 2, PR objectstack#5289). `theme.animation`, - `theme.zIndex` and five typography groups (`fontSize` / `fontWeight` / - `lineHeight` / `letterSpacing`, plus `fontFamily.heading` / `fontFamily.mono`) - are tombstones the schema now rejects by name, so `--duration-*`, `--timing-*`, - `--z-*`, `--font-size-*`, `--font-weight-*`, `--line-height-*`, - `--letter-spacing-*`, `--font-heading` and `--font-mono` had become structurally - dead code — no author could produce the input that reached them. - `generateAnimationVars` and `generateZIndexVars` are removed from - `@object-ui/core`, and `@object-ui/types` drops `Animation` / `ZIndex` / - `AnimationSchema` / `ZIndexSchema`. **`theme.customVars` is the declared — and - since #5021 the only — door**: each entry is emitted verbatim as - `--: `, so a `--z-modal` or a `--duration-fast` goes there now. - LIVE emission is untouched byte for byte: `colors`, `borderRadius`, `shadows`, - `typography.fontFamily.base` (→ `--font-sans`) and `customVars`. -- **`@object-ui/types`' `HttpMethodSchema` now binds the spec's - `HttpMethodSubsetSchema`, and `HttpMethod` binds `HttpMethodSubset`** - (objectstack#5832, PR objectstack#5976 — objectui#3499). The spec renamed its - 5-value UI subset because `schemaNameFromExportKey` strips the `Schema` suffix, - so the 5-value and 7-value enums both published as `shared/HttpMethod` and the - later write won — the emitted JSON Schema and reference page described only one - of them. **The runtime domain is unchanged and this repo's exported names are - unchanged**; this follows the rename without touching cross-package semantics. - Deliberately NOT re-pointed at the spec's bare `HttpMethod`: that is the 7-value - enum, and widening to it would let `method: 'HEAD'` compile and then throw in - `HttpRequestSchema.parse()`. -- **`dashboard.widgets[].actionUrl` / `actionType` / `actionIcon` / `aria` are - refused, not stripped** (objectstack#5010, ADR-0049 enforce-or-remove). A - dashboard widget has no action button and never had one — every action the - dashboard dispatches comes from `header.actions[]` — and no renderer ever applied - the widget `aria`, so it promised accessibility compliance it did not deliver. - A stale dashboard now gets a named error telling it where the affordance moved, - instead of silently losing it. Run `os migrate meta --from 16` to rewrite. diff --git a/.changeset/objectview-value-language-3582.md b/.changeset/objectview-value-language-3582.md deleted file mode 100644 index 61127c4bb7..0000000000 --- a/.changeset/objectview-value-language-3582.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -`console.objectView.systemViewReadonly` and `console.objectView.expandToPage` -are translated in the eight packs that stored English for them, so a Japanese, -Korean, German, French, Spanish, Portuguese, Russian or Arabic session reads -the system-view hint and the expand affordance in its own language (#3582). - -This is a different defect class from #3546's ledger, and no gate in the repo -could see it. There the key was *missing* from a pack and `fallbackLng: 'en'` -rendered English; here the key was **present in all ten** and eight of them -stored English as the value. `all-locales-key-parity` compares key *sets* and -placeholder *shape* — identical before and after this change, and neither key -interpolates anything. `scripts/check-i18n-call-site-keys.mjs` and its baseline -ratchet ask whether a `t()` key exists in `en`; it did, so these two were never -in the 258. - -`systemViewReadonly` carried the sharper half: the eight packs did not hold -`en`'s sentence, they held one `en` had already abandoned. `en` says the view -is read-only; the eight said `System view defined in code - duplicate to -customize.` — pointing eight locales at a duplicate-to-customize path the -product no longer presents. They are translated against `en`'s **current** -read-only meaning, not against the stale English they replaced. - -Each value is built from terminology the same pack already uses rather than -invented: `view.readonlyTooltip` supplies "system view" and -`console.objectView.cannotEditMetaView` (landed in #3583) supplies "defined in -code", so the new hint agrees with the copy beside it in every pack. For -`expandToPage`, `detail.openAsFullPage` is the identical English sentence one -namespace over and was already translated everywhere — `en` and `zh` hold their -two byte-identical to each other, so the eight now do too, and one locale -cannot end up with two different words for one action. - -`en` and `zh` are unchanged, byte for byte. No key is added or removed — -the diff is 16 values in 8 files. A new -`objectView-value-language-3582.test.ts` pins the `en` literal (so the next -rewording of `en` fails loudly instead of silently orphaning nine -translations), asserts that no pack but `en` serves either English spelling, -and requires the zh/ja/ko/ru/ar values to contain characters of their own -script. The repo-wide "no ASCII English sentence in a non-Latin pack" gate that -#3582 also sketched is deliberately **not** here; it is a separate, lands-green -change. diff --git a/.changeset/olive-donkeys-repeat.md b/.changeset/olive-donkeys-repeat.md deleted file mode 100644 index 034859b3bc..0000000000 --- a/.changeset/olive-donkeys-repeat.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -metadata-admin: name the offending column when `config.columns` is rejected - -`config.columns` is `string[] | ColumnDef[]` — a union with no discriminant — so -Zod reported every rejection as one collapsed issue on the field itself: -`config.columns` / `Invalid input`, on the create gate and the edit gate alike. -The field was reachable, but nothing said which column was wrong, which key, or -what was expected. - -The union member is now chosen by the value's own first element — a list of -field names or a list of column objects — and that member's real diagnostics are -reported at their draft-absolute path. A mis-typed key reports -`config.columns.0.field` with `expected string, received number`; a stray number -in a list of field names reports the element that broke it rather than every -element of the shape the author never chose. The aggregated container reaches -the same union as `list.columns.…`, and both gates now report identically. - -Only unions that really are "an array of A or an array of B" are read this way, -so neighbours such as `config.sort` (`string | ColumnSort[]`) are untouched. -Where the content elects nothing — a first element that is neither a string nor -an object — the previous message is kept rather than guessing. - -Validation verdicts are unchanged: the accept/reject decision is still made by -the one gate, and this only changes how an already-failed draft is presented. diff --git a/.changeset/organization-namespace-slice-two.md b/.changeset/organization-namespace-slice-two.md deleted file mode 100644 index 242ec036c7..0000000000 --- a/.changeset/organization-namespace-slice-two.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -The organization-management console is translatable. The 90 keys under -`organization.*` — the org layout and its tabs, the members list, the whole -invitation flow, organization settings including the leave and delete -confirmations, the accept-invitation page, and the workspace switcher — are now -defined in all ten locale packs, so a non-English session reads the org admin -surface in its own language instead of English (part of #3546). - -`scripts/check-i18n-call-site-keys.mjs` measured 258 keys that a `t()` call site -asks for and no pack defines. `organization.*` was the largest namespace in that -tally at 90 keys across 93 call sites in seven components. Every one of them -carried an inline `t(key, { defaultValue: 'English' })`, which is why nothing -looked broken: English rendered correctly at each site and all ten languages -were pinned to it. That is the #3517 class, not the raw-key class slice one -(#3583) held — no organization site rendered an identifier, and none had a dead -`||` fallback to remove, which was measured before deciding not to touch the -components. - -Adding a `defaultValue` is deliberately not the fix; it is the mechanism that -kept these invisible for months. The existing defaults stay where they are, and -each `en` value is byte-identical to the default at its call site so the two -paths cannot render different text. - -`organization` is a new top-level namespace, sitting next to — and distinct -from — `organizations`: the singular one is the management surface, the plural -one the org picker. The ratchet in `scripts/i18n-call-site-key-baseline.json` -shrinks by exactly these 90 entries, from 253 to 163. The -`organization.invitations.status.*` template-key family is untouched and still -baselined: enumerating an invitation status set is a different repair from -backfilling literal keys. diff --git a/.changeset/pivot-compare-stacking-3614.md b/.changeset/pivot-compare-stacking-3614.md deleted file mode 100644 index eb1120e0d7..0000000000 --- a/.changeset/pivot-compare-stacking-3614.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@object-ui/plugin-dashboard": patch ---- - -Show the `compareTo` comparison in a dataset pivot cross-tab instead of dropping it - -A dataset widget with `type: 'pivot'` and two or more `dimensions` renders a true cross-tab, and that branch was the one render path the `compareTo` work left out (objectui#3614, following objectui#3337 / PR #3612). It laid out its columns as `bucket × measure` and never admitted the `__compare` columns the executor returns — so a pivot with a bounded date window and a `compareTo` ran a correct comparison query, received correct comparison data, and displayed none of it: headers, cells and all three subtotals were silent. - -The comparison is now **stacked inside the cell** — current value on top, comparison value and its delta percentage beneath in smaller type: - -- The pivot's column structure is unchanged. Giving the comparison a column of its own would turn `bucket × measure` into `bucket × measure × window`, doubling the width and adding a third header level on the widget family whose width is already the scarce resource. -- **Row, column and grand subtotals stack it the same way.** A Total that alone showed no comparison would read as "this row has none", which is a different and false statement. -- One caption names the comparison window ("vs last year") for the whole table, from the same `dashboard.trend.*` vocabulary the KPI and flat-table paths use, and the delta comes from the same helper — so a KPI and a cross-tab cell comparing the same two windows agree on sign and rounding. -- **CSV export stays data-shaped.** The cross-tab now exports a flat `__compare` column per compared measure, with bare numbers in the cells: a spreadsheet can compute on the export, and no stacked display string ("$120 $100 20%") ever reaches it. - -Presence is detected from the returned data, as on every other path, so there is no new option to set — and a pivot the executor sent no comparison for renders exactly as it did before. diff --git a/.changeset/plugin-report-react-19-peer-3690.md b/.changeset/plugin-report-react-19-peer-3690.md deleted file mode 100644 index 2a49f59fe0..0000000000 --- a/.changeset/plugin-report-react-19-peer-3690.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/plugin-report': patch ---- - -Accept React 19 in `@object-ui/plugin-report`'s peer range, the last UI package still declaring React 18 alone (objectui#3690). - -`peerDependencies.react` and `peerDependencies.react-dom` widen from `^18.0.0` to `^18.0.0 || ^19.0.0`, matching the other 29 packages in the fixed version group. With npm 7+ resolving peers strictly, a React 19 consumer installing this published package hit an `ERESOLVE` on first install while every sibling installed clean — and the package's own README already documented the wider range, so the manifest was the half that was wrong. - -The narrow range was never a constraint anyone stated. `packages/plugin-report/package.json` was hand-authored on 2026-02-06 (`1e557cbda`), by which point nineteen sibling packages already carried `^18.0.0 || ^19.0.0` and every package created afterwards was born with it; the one other package born narrow, `plugin-dashboard`, was corrected on 2026-05-08 (`d2b6ecec6`) in a build fix that touched only itself. No commit in the file's 172-commit history ever revisited the peer line, and no commit message mentions a React 18 requirement. - -Nothing in the package needs React 18. Its entire React surface is `React.FC`, `useState`, `useEffect`, `useMemo`, `useReducer`, `useContext`, `Fragment`, `ComponentType`, `CSSProperties` and `ReactNode` — all unchanged in React 19 — with zero uses of anything React 19 removed (`ReactDOM.render`, `unmountComponentAtNode`, `findDOMNode`, legacy context, string refs, `defaultProps` / `propTypes` on function components, `createFactory`, `useFormState`, `react-dom/test-utils`). `react-dom` is not imported by the source at all; it appears only as a UMD global name in the Vite externals config. The workspace pins `react` to 19.2.8 via a root `pnpm.overrides`, so this package's 78 tests have been running against React 19 the whole time it declared it did not support it. diff --git a/.changeset/pseudo-route-segment-flags-3638.md b/.changeset/pseudo-route-segment-flags-3638.md deleted file mode 100644 index 4d475a7504..0000000000 --- a/.changeset/pseudo-route-segment-flags-3638.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Match the built-in pseudo-routes on whole path segments, so a mistyped app name can no longer render a different app (objectui#3638). - -`AppContent` decides whether a URL is a built-in pseudo-route (`create-app`, `system/*`, `metadata/*`, `setup`) before it decides which app to render, and two of those switches were substring tests: `pathname.includes('/system')` and `pathname.includes('/metadata')`. Both are true for any segment that merely *starts* with the word — `system_log`, `system_setting`, `systems`, `metadata_import`, `metadata-export`. `isSpecialRoute` feeds `requestedAppMissing`, so visiting `/apps//system_log` marked the URL as a pseudo-route, suppressed the "App not available" guard, fell back to the default app and rendered **that** app's shell with `system_log` taken as its object name — the exact "must NOT silently render a DIFFERENT app" case the fallback's own comment exists to prevent, with no indication that the requested app does not exist. - -The two flags now test path *segments* (`pathname.split('/').includes('system' | 'metadata')`); `isCreateAppRoute`'s `endsWith('/create-app')` is unchanged. Every real pseudo-route spells the word as a whole segment — `system/marketplace{,/installed,/:packageId}`, the host's `system/{apps,profile,approvals,ai-approvals,audit-log,settings,objects,metadata/…}`, `metadata/{,_diagnostics,:type,…}` and the legacy `component/metadata/{directory,resource/*}` aliases — so all of them stay special, including in the zero-app branch that keys on these flags directly (objectui#3590 / #3610). Knock-on, in a zero-app console only: a `system`-prefixed near-miss such as `/apps/setup/system_log` now reaches the same "No Apps Configured" screen every other unresolved URL there reaches, instead of the pseudo-route branch's "Page not found". diff --git a/.changeset/quiet-moons-inherit.md b/.changeset/quiet-moons-inherit.md deleted file mode 100644 index 812da90a35..0000000000 --- a/.changeset/quiet-moons-inherit.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -"@object-ui/app-shell": patch -"@object-ui/core": patch ---- - -Action params that inherit a field's options now keep the keys that field declared - -A field-backed action param (`{ field: 'tier' }`) had its inherited option list -rebuilt entry by entry as `{ label, value }`, which silently dropped every other -key the field's options declared — most consequentially the per-option -`visibleWhen` predicate (ADR-0058). A select field whose options narrow by -predicate in an object form therefore offered the FULL list in an action dialog, -including the entries the predicate exists to hide, with no diagnostic on either -side; `color` / `icon` / `disabled` were lost the same way. Options authored -inline on the param were never affected — they always passed through verbatim, -which is the asymmetry this restores. - -The resolver now preserves each inherited entry and only does its two real jobs: -expanding bare strings into label/value pairs and translating the label through -`fieldOptionLabel`. The option widgets already filter on `visibleWhen`, so a -role-gated option (`'admin' in current_user.positions`) inherited by a dialog -param now narrows the offered set and clears a seeded value the predicate hides. - -`ActionParamDef.options` (`@object-ui/core`) and the resolver's `RawActionParam` -are widened to match: `ActionParamOption` names the two keys the param layer -reads and carries the rest of a field's option vocabulary through. diff --git a/.changeset/quota-error-envelope-3491.md b/.changeset/quota-error-envelope-3491.md deleted file mode 100644 index a77ff447c3..0000000000 --- a/.changeset/quota-error-envelope-3491.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch ---- - -`parseAiQuotaError` now reads the AI quota refusal code from all three shapes the -cloud 429 producers use, instead of only the flat `error`-holds-the-code dialect. - -The two live producers fill the same `error` key in opposite ways — the token -guardrail puts the **code** there, `service-ai` puts the **message** there and the -code in a `code` sibling — while ADR-0112 declares a third shape both are -converging on: `{ success: false, error: { code, message } }`. The consumer had to -learn the declared shape **first**, or the producers' convergence would silently -turn every quota refusal back into a generic "Response failed" banner (the same -consumer-first sequencing as objectui#2992). - -- Code lookup order is a total order — declared envelope, then the flat guardrail - code, then the `code` sibling — so a transitional producer that double-emits the - new envelope alongside the legacy top-level keys has one defined outcome. -- Only the code's **location** widens. The recognized code set is unchanged, and - any unrecognized shape still degrades to today's behavior (`null`), so no - non-quota error is newly captured by the quota CTA. -- Companion fields (`upgrade`, `topUp`, `messageEn`) keep their established - top-level read; their position inside the declared envelope is deliberately not - presumed, and is aligned once the producer PR fixes the real shape. diff --git a/.changeset/raw-key-call-sites-slice-one.md b/.changeset/raw-key-call-sites-slice-one.md deleted file mode 100644 index 47ad4c8332..0000000000 --- a/.changeset/raw-key-call-sites-slice-one.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/i18n': patch -'@object-ui/app-shell': patch -'@object-ui/plugin-detail': patch -'@object-ui/plugin-gantt': patch -'@object-ui/plugin-form': patch ---- - -The five locale keys behind #3546's eight no-fallback `t()` call sites are now defined in all ten packs, so the built-in-view toasts, the activity-timeline source link, the wizard's required-field toast and the Gantt refresh button's accessible name are translated instead of falling back to English — or, on two surfaces, to the key itself (part of #3546). - -`scripts/check-i18n-call-site-keys.mjs` measured 258 keys that a `t()` call site asks for and no pack defines. These five were the subset with no working inline default: `console.objectView.cannotEditMetaView`, `console.objectView.cannotDeleteMetaView`, `detail.viewSource`, `gantt.toolbar.refresh` and `wizard.missingRequired`. Adding a `defaultValue` is deliberately not the fix — that mechanism is what kept all 258 invisible for months. - -**Two of the eight sites really did render the raw key**, and both go through a binding with nothing in front of i18next. `ObjectView.tsx` calls `useObjectTranslation()` directly, so five toasts read `console.objectView.cannotEditMetaView` / `cannotDeleteMetaView` on screen; the `|| 'Built-in views cannot be renamed.'` guards next to them were dead on every path, because i18next answers a miss with the key itself and a non-empty string never falls through `||`. Those four unreachable English strings are removed rather than repaired: one key served four call sites (rename / pin / set-as-default / configure), so the pack copy covers any change to a built-in view instead of naming one operation. `RecordActivityTimeline.tsx` fails the same way for a subtler reason — `useDetailTranslation` is `createSafeTranslation(..., 'detail.back')`, and because `detail.back` does resolve, the probe hands back i18next's `t` for every key and bypasses the defaults map wholesale, so `detail.viewSource` reached the user verbatim. - -**The other two sites were not rendering a raw key**, contrary to the issue's description, and are fixed here as the milder "English in all ten languages" class. `wizard.missingRequired` is its own hook's probe key, so the probe failed and `createSafeTranslation` correctly served its English default. `gantt.toolbar.refresh` goes through `useGanttTranslation`, which deliberately does not use `createSafeTranslation` and falls back per key — so the refresh button's `aria-label` was "Refresh", in English, never the key. Screen-reader users heard an English word rather than an identifier; a `zh` session now hears 刷新. - -Regression cover is provider-mounted on purpose: with no `I18nProvider` the defaults maps answer every one of these keys and the assertions pass while the console is broken, which is precisely the false-green the issue documents. For the two sites whose English output was already correct, `en` cannot discriminate before from after — the `zh` assertions are the ones that pin the fix. diff --git a/.changeset/reclaim-natural-gesture-names-3363.md b/.changeset/reclaim-natural-gesture-names-3363.md deleted file mode 100644 index 2b9da77a1a..0000000000 --- a/.changeset/reclaim-natural-gesture-names-3363.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -"@object-ui/types": minor -"@object-ui/mobile": minor ---- - -Reclaim the natural names `GestureType` and `GestureConfig` (objectui#3363). - -`@objectstack/spec` 17.0.0-rc.3 deleted the whole `ui/touch` module -(objectstack#4988, PR objectstack#5321), vacating three names objectui had -renamed **away from** in objectstack#4115 purely to avoid a collision. Two of -those workarounds have now outlived their reason and are undone. - -## Breaking, in FROM → TO form - -- `TouchGestureType` → **`GestureType`** — objectui's direction-fused recogniser - vocabulary (`tap`, `swipe-left`, `swipe-up`, …). -- `TouchGestureConfig` → **`GestureConfig`** — the flat gesture→`action` handler - binding. - -Both are exported from `@object-ui/types` and re-exported by `@object-ui/mobile`. -Nothing about either shape changed: same members, same optionality. Consumers -import the new name; there is no other edit. - -**The old names are gone, not deprecated.** This follows the precedent set by the -objectstack#4115 rename batch that introduced them, whose own migration note reads: -"an alias would preserve exactly the ambiguity being removed". A deprecated alias -would be worse here than in the general case, because the ambiguity these renames -exist to prevent is between two same-named types — leaving `TouchGestureType` -alive next to `GestureType` restores the two-spellings-one-concept problem while -claiming to retire it. - -The retired spec vocabulary that used to hold these names still lives in -`@object-ui/types`' `mobile` module under its deliberate `Spec…` prefix -(`SpecGestureType`, `SpecGestureConfig`, `SwipeGestureConfig`, …), and that prefix -is untouched — it is now the only thing distinguishing the two contracts, so -`useSpecGesture` still maps one onto the other exactly as before. - -## `PWAOfflineConfig` is deliberately NOT reclaimed - -The spec vacated `OfflineConfig` in the same retirement, but the spec was never -its only claimant: that rename was a **cross-package arbitration between two -objectui packages**, and `@object-ui/react` won it. `useOffline`'s config is the -offline data/sync model key for key, so it holds the bare `OfflineConfig`, while -this package's service-worker route cache stays `PWAOfflineConfig` -(objectui#3156 / objectui#3159). - -Before objectui#3560 that name reached `@object-ui/react` from the spec, so the -spec-side tripwire covered it by accident. Since the retirement it is declared -locally in `packages/react/src/hooks/useOffline.ts`, which means the spec's -vacancy no longer says anything about whether the name is free — it is not. -Reclaiming it would put two different `OfflineConfig` shapes on the public -surface of two packages that are routinely imported together, which is the exact -ambiguity objectstack#4115 renamed it away from. - -`page-nav-misc-spec-parity.test.ts` now pins that reason directly instead of -leaving it as prose: it asserts `@object-ui/react` still declares -`OfflineConfig`, and its failure message tells the next reader that the reclaim -has become available if it ever stops. diff --git a/.changeset/record-details-sections-description-3807.md b/.changeset/record-details-sections-description-3807.md deleted file mode 100644 index 8fd00b1ef4..0000000000 --- a/.changeset/record-details-sections-description-3807.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -"@object-ui/plugin-detail": patch ---- - -`record:details` 的 `sections` 输入说明改为从 spec 形状派生的对象形,不再教已被退役的「Section IDs」 - -`inputs` 不是文档,而是发布出去的编写契约:`gen-manifest.ts` 把它序列化进 -`sdui.manifest.json`(保存门 + parser 白名单)和 `sdui-intrinsics.d.ts`。而 -`record:details.sections` 的说明写的是 `Section IDs to show (required when layout -is "custom")` —— 那是 17.x 以前的形状。pin 版 `@objectstack/spec@17.0.0-rc.5` 的 -`RecordDetailsProps.sections` 是对象数组 `{ name?, label?, columns?, fields }`, -objectstack#5611 把 `z.array(z.string())` 那条拼法**删掉**而不是 union 进来(既无 -producer 也无 consumer,一种形状而不是两套事实契约)。 - -照旧说明写 `sections: ['contact_info', 'address']` 的作者,在四层之间拿不到任何 -诊断:`['a','b']` 对 manifest 门是合法 `array`(门只看顶层键名 + 粗类型),上游 -`validateComponentProps` 是 advisory 级,spec 只在真的走 parse 的路径上才拒,而 -`RecordDetailsRenderer` 对每个条目读 `s.name` / `s.label` / `s.fields` —— 字符串上 -三者全 `undefined`,该 section 一个字段都不渲染。`layout: 'custom'` 时 sections 是 -详情页正文的唯一来源,所以结果是一张没有报错的空白详情页。 - -新说明逐键派生自 spec 各成员的 `.describe()` 与渲染器实读:`fields` 必填、按序渲染; -`label` 是标题(省略即无标题、无边框);`name` 是 snake_case 稳定标识与 i18n 锚点 -(标题走 `objects.{object}._sections.{name}.label`);`columns`(1-4)是本 section 的 -字段栅格宽度,省略则由渲染器推导;并明确写出字符串条目不被接受。渲染器另外还认的 -`title` / `showBorder` / `hideEmpty` **故意不写进说明** —— spec 的 section 对象没有 -声明它们,parse 时会被静默剥掉,发布它们等于教作者写契约丢弃的键。 - -同时新增 `recordDetailsInputs.spec-parity.test.ts`:两个方向的断言都在运行时从 spec -schema 派生(每个 spec 成员键都能从说明里发现;本 block 不声明 spec 不接受的顶层 -input),所以下一次 spec 变形会先让测试红,而不是又一次静默张开。仅说明文本变化,无 -运行时行为改动。 diff --git a/.changeset/record-header-manual-refresh-3460.md b/.changeset/record-header-manual-refresh-3460.md deleted file mode 100644 index fbca14bf6b..0000000000 --- a/.changeset/record-header-manual-refresh-3460.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@object-ui/app-shell": patch -"@object-ui/components": patch -"@object-ui/plugin-detail": patch ---- - -Record detail pages: a header ⟳ that refreshes the record, its related lists and its tab counts in place — no browser reload - -Concurrent-editing scenario from the shop floor (MES work orders): operator A sits on a record's detail page while operator B starts or reports the same order. A had no way to see the new state except F5, which throws away the open tab, the scroll position and any in-progress inline edit along with the stale data. - -The pipeline for this already existed — the objectui#2269 invalidation bus refetches every mounted reader in place, and `RecordContext.refresh` had been declared for it — but nothing produced that field and no UI reached for it. Three changes give it a trigger: - -- **`RecordDetailView` produces `RecordContext.refresh`**, publishing `notifyDataChanged({ objectName: '*' })`. The wildcard is deliberate: a user reaches for refresh because of a write made by SOMEONE ELSE, which this client never saw and therefore cannot attribute to particular objects. `'*'` marks everything mounted as stale, so the main record, every related child list and the tab-count badges all refetch — no remount, so tab / scroll / draft state survive. First phase covers the standalone record route; embedded hosts (list drawer, split-pane preview) keep their existing chrome unchanged. -- **`page:header` renders the ⟳** at the far end of the header row when — and only when — the host provides `refresh`. It is page chrome rather than a header action, so its position is the same on every record page regardless of which business actions the object declares, and it can never be collapsed into the `⋯` overflow. Styled as that `⋯` trigger's twin so the row reads as one button family. Its accessible name and tooltip come from the existing `common.refresh` key, so the icon-only button is not English-only in the other nine locales. The icon spins for a short floor after a click, because the bus is fire-and-forget and a warm backend would otherwise finish before the click looked like it landed. -- **`RelatedList` accepts the `'*'` wildcard** on the legacy `objectui:related-changed` event, matching what `dataChangeMatches` already does for the bus's own readers. This listener compared the payload's object name to its own, so a wildcard invalidation reached everything on the page except the related lists — a concrete foreign object name is still ignored. - -Hosts that provide no `refresh` render exactly as before. diff --git a/.changeset/registry-inputs-spec-parity-gate-3797.md b/.changeset/registry-inputs-spec-parity-gate-3797.md deleted file mode 100644 index 91a29c4934..0000000000 --- a/.changeset/registry-inputs-spec-parity-gate-3797.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Internal test-only gate, no behaviour or authoring-surface change (objectui#3797). - -Generalizes PR #3795's single-block `record:highlights` parity check to every -`@objectstack/spec` `ComponentPropsMap` entry this repo registers with a -non-empty `inputs`: a block may not DECLARE a top-level input the spec's props -schema does not accept, with the expectation derived from the spec's own shape at -runtime and every current divergence registered in an explicit exemption list -that carries a reason and a tracking issue per entry. - -No package is declared because no published behaviour changed: the four blocks -objectui#3797 flagged (`page:header`, `page:tabs`, `page:accordion`, -`element:record_picker`) keep their `inputs` byte for byte. Per-block verdicts -came out on the spec side, not this one — the keys are read by the renderers and -reachable by authors, so the fix is upstream declaration (objectstack#6776) or, -for `element:record_picker`, already landed upstream in objectstack#5775 and -merely awaiting a `@objectstack/spec` pin bump here. Narrowing them locally would -have deleted live configuration; widening the spec is not this repo's to do -(AGENTS.md #0 / #0.1). - -The exemptions expire by themselves: the gate fails on any entry whose key the -spec has since declared, so the pin bump and the upstream landing each force -their own cleanup instead of leaving a permanent allowlist. diff --git a/.changeset/remaining-setup-links-3611.md b/.changeset/remaining-setup-links-3611.md deleted file mode 100644 index 0ad340074c..0000000000 --- a/.changeset/remaining-setup-links-3611.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Point the four remaining "Settings" senders at the system hub `/apps/setup/system` instead of the bare `/apps/setup` (objectui#3611). - -Same root cause as objectui#3590, which fixed the three call sites inside its declared file surface: `AppContent` mounts the system hub only under `isSystemRoute`, which keys on a `/system` path segment, so on a zero-app deployment the bare `/apps/setup` *is* the "No Apps Configured" empty state's own URL and every entry spelling it looped in place. - -Three of the four are live defects, all reachable on a zero-app deployment today: - -- `AppSidebar`'s no-active-app sidebar header (`system-sidebar-header`) — the sharpest of them, since it renders *only* when there is no active app, i.e. it was unreachable except in exactly the state where its target was broken. -- `AppSidebar`'s user-menu "Settings" entry. -- `SystemRedirect`'s bare `/system` legacy bookmark. This forwarder was already half right — every *suffixed* bookmark (`/system/users`) was correctly rewritten to `/apps/setup/system/users`, and only the bare one dropped the `system` segment. The bare branch now agrees with the suffixed branch beside it; no new logic. - -The fourth, `QuickActions`' "System Settings" card, is dormant — the component has zero JSX call sites repo-wide, so no user can reach it today. It is corrected in the same pass so the dead link cannot return with the component if it is ever remounted. diff --git a/.changeset/row-action-declared-visible-gate-3758.md b/.changeset/row-action-declared-visible-gate-3758.md deleted file mode 100644 index 4de3afb389..0000000000 --- a/.changeset/row-action-declared-visible-gate-3758.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -"@object-ui/plugin-grid": patch -"@object-ui/components": patch ---- - -Row actions declaring `visible: false` are now hidden instead of rendered - -A custom row action's visibility **gate** was detected by truthiness, so -`visible: false` — the most explicit way an author can say "never show this" — -fell into the "no gate declared" branch and the action rendered for every row. -Both surfaces of the ObjectGrid row cell (the "⋮" overflow item and the inline -`variant:'primary'` button) and the data-table's row overflow menu read the same -gate, so all three rendered it; the `#3562` emptiness guard counts with that same -gate, so a row whose only action was `visible: false` also grew a "⋮" it could -not fill. - -The gate now detects a **declared** gate by `!= null && !== ''` and lets the -declaration itself decide — a boolean short-circuits to its own verdict rather -than being handed to the CEL engine. This is the invariant objectui#3492 already -established for the selection bar, whose `hasVisibilityGate` spells out why -truthiness cannot answer the question, and the same `!= null` posture the -built-in `visibleWhen` gate has always had. `visible: true` still renders, -`''` and an absent `visible` are still no gate at all, and no expression-valued -`visible` changes verdict. - -Behaviour change surface, deliberately narrow: only an action whose `visible` is -the literal boolean `false` (or another falsy non-empty value) changes — it goes -from rendered to hidden, which is what the declaration asked for. -`ActionSchema.visible` is `ExpressionInputSchema` with no boolean member, so -`objectstack build` cannot emit this shape; hand-written view JSON and -in-process callers constructing defs can, and did. The three row surfaces now -reach the same verdict as the selection bar and the record page header for every -non-expression shape, which `predicate-surface-parity` pins. diff --git a/.changeset/row-action-inline-slot-survivors-3762.md b/.changeset/row-action-inline-slot-survivors-3762.md deleted file mode 100644 index b831dd67ad..0000000000 --- a/.changeset/row-action-inline-slot-survivors-3762.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -"@object-ui/plugin-grid": patch ---- - -Grid row actions: the inline button budget is now spent on the primaries that actually render - -`RowActionMenu` allocated its inline slots on the **declared** row actions, before -any `visible` predicate ran: - -```ts -const primaryDefs = gatedActionDefs.filter(d => d.variant === 'primary'); -const inlineDefs = primaryDefs.slice(0, Math.max(0, maxInlineActions)); -``` - -So on a row where the *leading* `variant: 'primary'` action was suppressed by its -own `visible`, that action still held the slot — `RowActionInlineButton` returned -`null` into it — while the next primary, the one that *did* survive the row's -predicates, had already been sliced into the overflow list. The row then rendered -**no inline button and a "⋮" hiding its main CTA**, even though exactly one primary -was visible and `maxInlineActions` (default 1) allowed exactly one inline button. - -Slot allocation now happens inside `planRowActionMenu`, after visibility, so the -budget is only ever spent on a primary that renders. `maxInlineActions` is -unchanged in meaning and default — it is a width budget for real buttons, and -counting an invisible action against it protected no layout. - -Behaviour change surface, deliberately narrow: - -- a row with 2 or more primaries where a *leading* one is suppressed for that row — - the surviving primary moves from the "⋮" menu to an inline button, and the "⋮" - disappears if nothing else is left to fold; -- unchanged: how many primaries may go inline, the menu order (folded primaries - above secondaries), which items render at all, the ADR-0066 D4 capability gate - (still applied once to the declared set, upstream of this decision), and the - #3562 empty-menu guard — a row with nothing renderable still grows no trigger. - -Rows whose primaries are all ungated (the `sys_environment` Open + Upgrade Plan -shape that motivated `maxInlineActions`) are bit-for-bit unaffected: declared order -and surviving order coincide. diff --git a/.changeset/rowactionmenu-empty-guard.md b/.changeset/rowactionmenu-empty-guard.md deleted file mode 100644 index 8292fd3f6f..0000000000 --- a/.changeset/rowactionmenu-empty-guard.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -fix(plugin-grid): don't render a row "⋮" trigger that opens an empty menu - -The object list's row overflow trigger was gated on whether row-action -**handlers** were wired and how many actions were **declared** -(`(canEdit && onEdit) || (canDelete && onDelete) || menuDefs.length > 0 || rowActions.length > 0`), -while the menu's items were filtered a second time — per item, per record — -against `visibleWhen` / `visible`. On a row where every item was -predicate-suppressed the trigger still rendered and opened an empty box, which -reads as a broken page: a platform object whose row actions are gated for one -role showed a "⋮" on every row for everyone else, with nothing inside it. - -The trigger is now decided by the items that will actually render for that row, -resolved through the same visibility functions the items gate themselves on, so -the two cannot disagree. The decision is per row: within one grid a row that -keeps an action keeps its trigger while a row with nothing left renders none. The -inline `variant: 'primary'` button reads that same shared rule. The actions -column is table-level and unchanged, so a row with nothing to offer renders an -empty cell and every row keeps the same cell count. - -Which items render is untouched — only whether the trigger renders when none of -them survive. diff --git a/.changeset/runner-preserve-query-on-navigate.md b/.changeset/runner-preserve-query-on-navigate.md deleted file mode 100644 index ee60632f95..0000000000 --- a/.changeset/runner-preserve-query-on-navigate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/runner': patch ---- - -Runner in-app navigation now carries the current query string across to the pushed URL instead of `pushState`-ing a bare path. Opening the Runner with `?api=` and clicking a sidebar entry no longer drops the parameter from the address bar, so reloading or sharing the resulting URL still reaches the same backend rather than silently falling back to the (normally empty) `LocalBundleLoader` and rendering `Page not found`. The whole query string is preserved, not just `api` — `@object-ui/core`'s `?__debug…` flags survive navigation for the same reason. A navigation target that spells out its own query keeps it and wins on collision, with the remaining current parameters merged in behind it (#3578). diff --git a/.changeset/runner-vite-alias-transitive-closure.md b/.changeset/runner-vite-alias-transitive-closure.md deleted file mode 100644 index a4bc9864c6..0000000000 --- a/.changeset/runner-vite-alias-transitive-closure.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@object-ui/runner": patch ---- - -Complete `packages/runner/vite.config.ts`'s workspace alias table to the full -transitive import closure, so `@object-ui/runner` boots and builds from the -monorepo sources without a prior `pnpm -w build` (objectui#3575). - -The table aliased 7 `@object-ui/*` specifiers to `packages/*/src`, but those -`src` trees import 8 more workspace packages that were not aliased. Those fell -back to Node resolution and landed on `packages//dist`, which does not -exist in a fresh install-only checkout — so the "From Source" flow documented in -`content/docs/utilities/runner.mdx` (`pnpm install` then `pnpm dev`, no build -step) failed with "Failed to run dependency scan" and served HTTP 500 for every -module on the chain. `pnpm --filter @object-ui/runner build` failed the same way. - -Newly aliased: `i18n`, `sdui-parser`, `react-runtime`, `fields`, `plugin-detail` -(first layer), `providers` and `permissions` (only reachable once the first layer -resolves to src), and `data-objectstack` (a type-only import that esbuild erases, -so the dependency scan never reported it). - -This is user-visible in the published artifact, because the alias table is not -scoped by `command` and therefore applies to `vite build` as well. Bundling the -newly aliased packages from src stops the per-icon `lucide-react/dynamic.mjs` -chunks from being inlined, so the build now emits ~1.7k lazy icon micro-chunks -like `apps/console` does. `build.modulePreload` is disabled to match console, so -those chunks are not all preloaded on first paint: the measured initial eager -payload drops from 4231003 to 591795 bytes, while total `dist` size grows about -5.5% because the previously inlined icons are now separate files. diff --git a/.changeset/sour-clouds-yawn.md b/.changeset/sour-clouds-yawn.md deleted file mode 100644 index c3f4457453..0000000000 --- a/.changeset/sour-clouds-yawn.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -metadata-admin: restore per-field diagnostics when editing an invalid stored `view` - -Editing a stored `view` is judged by the wire gate `ViewMetadataSchema`, which is a -union. Zod reports a union failure as a single root issue — no path, message -`Invalid input` — so every field-level diagnostic collapsed into one message that -pointed at nothing: `SchemaForm` had no field to highlight and Monaco had no -position to jump to, and the guided messages the spec writes for these rejections -never reached the editor. - -Failures are now expanded to the union member the draft's own `viewKind` -discriminant selects, so a bad stored view reports `config.type` with the list of -valid layouts, a mis-typed filter reports `config.filter.0.operator`, and a -container key that belongs to a single view gets the spec's full -`defineView(...)` guidance back. Only the selected member's issues are shown, so -the other members' rejections do not become noise. - -Validation verdicts are unchanged: the accept/reject decision is still made by the -one gate, and this only changes how an already-failed draft is presented. diff --git a/.changeset/system-hub-count-error-state-3679.md b/.changeset/system-hub-count-error-state-3679.md deleted file mode 100644 index b879e87f14..0000000000 --- a/.changeset/system-hub-count-error-state-3679.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/console': patch ---- - -System Hub: a card count that failed to load no longer renders as `0` - -Each count on the System Hub fetched one object and caught its own failure with -an empty page, so a 500, a 401, a 403 or a dropped connection all produced the -same confident `0` as a table that really is empty — no error, no retry, and no -way to tell the two apart. The most reachable case was a permission denial on a -single object: an administrator who may open the hub but cannot read -`sys_audit_log` was shown "0 entries" rather than being told anything at all. - -A failed lookup now leaves that card's count unknown, and the badge — which -already renders only for a known count — is omitted, so the card shows no -number instead of a wrong one. The catch stays on each call rather than around -the batch, so one object's failure blanks only its own card and the cards beside -it keep the real numbers they received. - -Unchanged: an object the backend does not have still counts `0`. The adapter -resolves an unregistered object as an empty page by design (callers read empty -as "feature unavailable"), so that never was an error and is not treated as one -here. diff --git a/.changeset/system-hub-org-count-3670.md b/.changeset/system-hub-org-count-3670.md deleted file mode 100644 index 6dbf163173..0000000000 --- a/.changeset/system-hub-org-count-3670.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/console': patch ---- - -Count System Hub's Organizations card through `sys_organization`, the object the framework actually registers — it asked for `sys_org`, which does not exist, so the card read `0` on every deployment (objectui#3670). - -The failure was silent by construction. A missing object answers `404 OBJECT_NOT_FOUND`, and `ObjectStackAdapter.find()` absorbs that on purpose — it caches the name in `missingResources` and resolves `{ data: [], total: 0 }` so callers can treat an uninstalled collection as "no rows". The hub renders `data.length`, so a name the framework never had produced a perfectly ordinary `0`, indistinguishable from a workspace that genuinely has no organizations — which no single-org deployment ever is, since `sys_organization` always holds at least one row. The `.catch` on each call never even saw the 404; it only ever covered non-404 rejections. - -The other three counted names were checked against the framework's object registry and are correct as spelled: `sys_user`, `sys_position`, `sys_audit_log`. - -The Permissions card is **not** fixed here and still reads `0`. Its query names `sys_permission`, which the framework also does not have — it splits that surface into `sys_capability` (lineage: its own docblock says "named `sys_capability`, not `sys_permission`") and `sys_permission_set` (function: the admin-managed grant container). Both would render, so choosing one would silently bind the card to a surface nobody picked; that decision is open on objectui#3655. Until it lands the gap is held visible by a MEASUREMENT case in the page's test rather than quietly re-aimed. diff --git a/.changeset/system-hub-permissions-leg-3655.md b/.changeset/system-hub-permissions-leg-3655.md deleted file mode 100644 index 98bf93d178..0000000000 --- a/.changeset/system-hub-permissions-leg-3655.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/console': patch ---- - -Point System Hub's Permissions card — both its link and its count — at `sys_permission_set`, closing the last of the five `system/*` navigation targets (objectui#3655). - -Four of those URLs became redirects in an earlier change; `system/permissions` was deliberately held back, and so was the count beside it, because the framework splits what this console calls "Permissions" into two Setup entries and picking one would have silently bound every click, bookmark and badge to a surface nobody chose: - -- `sys_capability` — ADR-0066 layer 1, the definition registry of "what can be done". Its own docblock notes it is what the ADR "loosely floats" as `sys_permission`, which is the name the retired page and the count query both used, so lineage pointed here. -- `sys_permission_set` — ADR-0066 layer 2, the grant container the permissions docs call "the only capability container" (object CRUD, field security, access depth, system capabilities), so function pointed here. - -It is decided as `sys_permission_set`: the card reads "Manage permission rules and assignments", and rules-and-assignments is layer 2 — a capability is what a permission set references by name, not what an administrator is assigned. Two user-visible consequences: - -- `/apps/:app/system/permissions` now forwards in one hop to `/apps/:app/sys_permission_set` instead of being rewritten to `…/system/record/permissions` and rendering a record detail page for an object literally named `system` — a dead link that read as a backend fault. -- The Permissions card's badge shows the real number of permission sets. It previously counted `sys_permission`, an object the framework does not register; the adapter absorbs that `404` into an empty page on purpose, so the card printed a confident `0` no administrator could tell apart from "there really are none". - -Recorded as a transitional alias. Retiring this hand-written card wall along with the hub (already `@deprecated` in favour of the metadata-driven navigation) remains open and does not conflict — a redirect keeps old bookmarks resolving either way. diff --git a/.changeset/system-hub-routes-3655.md b/.changeset/system-hub-routes-3655.md deleted file mode 100644 index c8fb1e866d..0000000000 --- a/.changeset/system-hub-routes-3655.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/console': patch ---- - -Declare the retired `system/{users,organizations,roles,positions}` console URLs as redirects onto the framework-owned system objects (objectui#3655). - -`SystemHubPage`'s cards and both sidebars' `sys-*` cluster emit five `/apps/setup/system/…` targets. Four of them were real routes until `apps/console` was slimmed for third-party customisation, which deleted the bespoke wrapper pages because "these objects are now contributed by framework plugins … and resolved via the generic `/apps/setup/` route" — but the producers were never retargeted and nothing redeclared the URLs. All five fell through to app-shell's tail, where the failure they got depended on how long the word was, because `ShorthandRecordRedirect` treats any URL-safe segment of 6+ characters as a record id: - -- `users` (5) and `roles` (5) rendered "Page not found". -- `organizations` (13) and `positions` (9) were rewritten to `…/system/record/` and rendered a record detail page for an object literally named `system` — the worse of the two, because it reads as a backend/data problem rather than a dead link. - -Each now forwards in one hop to the object the framework's own Setup navigation names: `sys_user`, `sys_organization` (the list entry — the record-scoped one needs a runtime `{current_org_id}` a static redirect cannot resolve), and `sys_position` for both `roles` and `positions` (ADR-0090 D3 renamed `sys_role` to `sys_position`, so the sidebar's "Roles" and the hub's "Positions" are one surface in two vocabularies). Same shape as the `system/objects` and `system/metadata` redirects beside them: the URL is translated, the deleted page is not resurrected, and the navigation producers are untouched. - -`system/permissions` is deliberately left as it was. The framework splits what this console calls "Permissions" into two Setup entries — `sys_capability` and `sys_permission_set` — and picking one here would silently commit every click and bookmark to a surface nobody chose. Its unchanged landing is pinned in the tests so the gap stays visible. diff --git a/.changeset/view-edit-wire-gate-5316.md b/.changeset/view-edit-wire-gate-5316.md deleted file mode 100644 index 2195175fde..0000000000 --- a/.changeset/view-edit-wire-gate-5316.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -metadata-admin no longer false-rejects a stored `view` that has been pinned or -reordered. The editor's live client-side validation judged BOTH the create and -the edit draft with the AUTHORING schema (`ViewItemSchema` via -`viewSchemaForDraft`). That is right for create and wrong for edit: the editor -opens a body that came back out of `sys_metadata`, and the platform itself -writes keys into stored view bodies — `isPinned` from the view switcher's pin -action, `sortOrder` from the reorder write, and a per-row `id` that the console -filter/sort builders stamp on `config.filter[]` for React. `updateView` GETs the -stored item and PUTs `{ ...current, ...partial }`, and `saveMetaItem` persists -the accepted body verbatim, so those keys are in storage by design. - -Before the authoring schemas were tightened these keys were silently stripped -and the draft passed. Once the gate became strict, opening a pinned view in the -editor reported unrecognized keys — while the SERVER accepted the very same body, -because it validates against `ViewMetadataSchema`. The client was strictly -stricter than the server; the direction was inverted. - -`validateMetadataDraft` now takes an optional `{ mode: 'create' | 'edit' }`. -Create keeps the authoring gate unchanged. Edit is judged by -`ViewMetadataSchema` — the schema the `view` metadata type registers, i.e. the -same one the server runs — so the client and the server accept the same set by -construction. `mode` defaults to `'create'`, the strict gate, so a caller that -omits it can only ever over-report, never silently widen the door. - -The edit gate keeps its teeth: a wrong `config.type` and a container carrying an -unknown key are both still rejected. diff --git a/.changeset/view-overrides-readback.md b/.changeset/view-overrides-readback.md deleted file mode 100644 index b70a20d7fa..0000000000 --- a/.changeset/view-overrides-readback.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@object-ui/data-objectstack": patch -"@object-ui/app-shell": patch -"@object-ui/types": patch ---- - -Fix saved list-view preferences never reading back (density, column widths, sort, hidden columns, inline edit) - -`listViewOverrides` in the ObjectStack adapter enumerated `GET /api/v1/meta/{objectName}` — putting the object name in the metadata **type** slot — while `updateViewConfig` persists under `type='view'`. The two key spaces are disjoint, so the batch map came back empty for every object and every personalization a user saved on a list view was written to the server but never read back, showing up as "the setting didn't save". - -The read now enumerates `type='view'` once and narrows to the object client-side, through the same accessor `listViews()` uses over the same rows — the metadata index is name-only, so there is no server-side `?object=` filter to push it into. - -Second half: the batch read no longer swallows its own failures into an empty map. An empty map is an authoritative "this object has no overrides" and callers may still trust it and skip the per-view reads (the batch optimization is intact), but a transport failure now rejects, so the per-view `getView` fallback it was silently disabling becomes reachable again. `DataSource.listViewOverrides` documents both terms so other adapters implement the same contract. diff --git a/.changeset/view-readonly-tooltip-semantics-3625.md b/.changeset/view-readonly-tooltip-semantics-3625.md deleted file mode 100644 index 787ba81d36..0000000000 --- a/.changeset/view-readonly-tooltip-semantics-3625.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -`view.readonlyTooltip` — the tooltip on a view tab's read-only lock — is -retranslated in the eight packs (ja/ko/de/fr/es/pt/ru/ar) that still described -the retired "duplicate to customize" workflow, so a Japanese, Korean, German, -French, Spanish, Portuguese, Russian or Arabic session is told the view is -defined in code and read-only, which is what `en` says and what the product -does (#3625). - -This is the same stale sentence #3582 fixed one namespace over, but it hid -behind a much better disguise. In #3582 the eight packs stored the **English** -string, so two cheap criteria could see it: "value equals `en`" and "a -non-Latin pack holds pure ASCII". Neither can see this key. Its eight values -were **idiomatic translations** — real Japanese, real Cyrillic, real Arabic — -of a sentence `en` itself had already abandoned. Nothing about their form was -wrong; only their meaning was. Key sets were complete, so -`all-locales-key-parity` was green; the key exists in `en`, so the call-site -guard and its ratchet were green; the values are distinct and in their own -scripts, so every heuristic #3582 sketched would have been green too. Eight -locales spent those releases pointing users at a path the product no longer -offers, with every gate reporting success. - -Each value is translated against `en`'s **current** meaning and built from -words the same pack already uses — "read-only" from its own `view.readOnly` / -`view.readonlyAriaLabel`, "defined in code" from -`console.objectView.systemViewReadonly` / `cannotEditMetaView` — so the tooltip -agrees with the copy beside it instead of introducing a ninth way to say -read-only. Nothing is rewritten from the stale text. - -`en` and `zh` are unchanged, byte for byte, and no key is added or removed — -the diff is eight values in eight files. A new -`viewReadonlyTooltip-semantics-3625.test.ts` tests **meaning** rather than -form, in both directions: no pack may name the duplicate/copy workflow in its -own language, and every pack must positively carry all three pieces of the -sentence ("system view", "defined in code", "read-only") so the negative check -cannot pass on a gutted string. It also pins the `en` literal, so the next -rewording of `en` fails in the PR that does the rewording rather than orphaning -nine translations for another release — which is the invariant this family of -defects has actually been missing. diff --git a/.changeset/view-translation-keys-bare-only-3502.md b/.changeset/view-translation-keys-bare-only-3502.md deleted file mode 100644 index 5cce40db9a..0000000000 --- a/.changeset/view-translation-keys-bare-only-3502.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@object-ui/i18n": patch ---- - -Resolve `_views` translation keys by the bare view name only — the prefixed full name is no longer a second candidate - -`useObjectLabel().viewLabel` / `viewDescription` / `viewEmptyState` build their key by stripping the object prefix off the runtime view id (`crm_opportunity.pipeline_kanban` → `objects.crm_opportunity._views.pipeline_kanban.`). Until now, if that bare key missed, the resolver **also** tried the prefixed full name — `objects.crm_opportunity._views.crm_opportunity.pipeline_kanban.` — so a bundle authored against the prefixed spelling resolved too. - -**Behavior change:** it no longer does. A `_views` entry keyed by the prefixed full name is not read at all; the label falls back to the metadata default, exactly as it would if no translation had been written. Bundles keyed by the bare view name — the only spelling the extractor emits and `os lint` accepts — are unaffected. - -This closes an asymmetry, not a feature. The server-side resolver reads the one bare key (objectstack#5165), so a prefixed-key bundle produced a **translated label in the Console and English everywhere else**: the REST boundary, mobile, plain HTTP and SDUI consumers do not run this second resolution pass. The half-success was harder to notice than a clean miss, and it fossilized a second de-facto spelling of a key the platform has now converged on: per the objectstack#5164 ruling (2026-08-06, option A), the canonical `_views` key is the runtime view identity's bare name, with the i18n extractor deriving it from the view composer (objectstack#6124) and `packages/lint` enforcing that single spelling (objectstack#6038). This is the third and last leg of that convergence. - -The object-name axis is untouched: a bundle written against the short object name (`objects.opportunity._views.…`) still resolves when the runtime presents the namespaced name (`crm__opportunity`). - -**If a label stopped translating after this upgrade,** its `_views` key is written with the object prefix. Drop the prefix — `_views.crm_opportunity.pipeline_kanban.label` becomes `_views.pipeline_kanban.label`. `os lint` names these for you: a prefixed key is reported as `translation-target-unknown`, because no view of the object declares it. diff --git a/apps/console/CHANGELOG.md b/apps/console/CHANGELOG.md index fffdca657e..44946dcc41 100644 --- a/apps/console/CHANGELOG.md +++ b/apps/console/CHANGELOG.md @@ -1,5 +1,81 @@ # @object-ui/console +## 17.4.0 + +### Patch Changes + +- 7883c02: Send the console host's legacy URL redirects straight to the canonical metadata-admin routes instead of routing them through the deprecated `component/metadata/resource` alias (objectui#3639). + + `apps/console`'s `ObjectRedirect` and `MetadataRedirect` rewrote `system/objects[/:name]` and `system/metadata[/:type[/:name]]` onto `…/component/metadata/resource[/:name]?type=:type`. app-shell declares that spelling as a legacy _alias_, not a page: its route element is `LegacyMetadataRedirect`, which immediately navigates on to `…/metadata/:type[/:name]`. Every one of those URLs therefore took two `` hops (plus a re-render) to reach a destination the host could name directly — and it was this indirection that carried `sys-objects` into the zero-app blank screen fixed in objectui#3610, since the alias was the leg that branch did not recognise. + + Both redirects now construct `…/metadata/:type[/:name]` (and `…/metadata` for the typeless directory arm) themselves. The endpoints are unchanged, byte for byte, including the alias hop's own percent-encoding of `:type` and its verbatim pass-through of `:name`; only the intermediate hop is gone. The alias routes stay declared exactly as they were — bookmarks, external links and the setup left-nav still arrive on them and are still forwarded — this change only stops the console feeding its own traffic through them. + + Also corrects four docblocks that described the alias as "the engine route", in `apps/console`'s two redirects and in app-shell's `datasource` resource registration and page. That wording is not merely stale: the objectui#3610 dispatch read this chain and concluded `component/metadata/resource` was the canonical spelling, which is the exact opposite of what the route table says. + +- d2fd044: Point the last four navigation producers at the canonical metadata-admin routes instead of the deprecated `component/metadata` alias, removing a redirect hop from each (objectui#3660). + + The System hub's "Metadata" and "Datasources" cards aimed at `…/component/metadata/directory` and `…/component/metadata/resource?type=datasource`, and the `sys-datasources` entry in both `AppSidebar.systemFallbackNavigation` and `UnifiedSidebar.homeNavigation` spelled the latter too. app-shell declares those spellings as legacy _aliases_, not pages: their route element is `LegacyMetadataRedirect`, which immediately navigates on to `…/metadata` and `…/metadata/datasource`. Every click on any of the four therefore paid a redundant hop plus a re-render to reach a destination the navigation could name directly. All four now name it. + + The landing pages are unchanged, byte for byte — the new URLs are exactly what the alias hop was already computing (`datasource` percent-encodes to itself, and neither producer carried a query or hash beyond the `?type=` the alias itself consumed). Only the intermediate hop is gone. + + The alias routes stay declared in both `AppContent` branches, untouched: bookmarks and external links still arrive on them and are still forwarded. This completes objectui#3639, which corrected the console host's two redirects and enumerated these four as the remainder. + +- c1a18ed: System Hub: a card count that failed to load no longer renders as `0` + + Each count on the System Hub fetched one object and caught its own failure with + an empty page, so a 500, a 401, a 403 or a dropped connection all produced the + same confident `0` as a table that really is empty — no error, no retry, and no + way to tell the two apart. The most reachable case was a permission denial on a + single object: an administrator who may open the hub but cannot read + `sys_audit_log` was shown "0 entries" rather than being told anything at all. + + A failed lookup now leaves that card's count unknown, and the badge — which + already renders only for a known count — is omitted, so the card shows no + number instead of a wrong one. The catch stays on each call rather than around + the batch, so one object's failure blanks only its own card and the cards beside + it keep the real numbers they received. + + Unchanged: an object the backend does not have still counts `0`. The adapter + resolves an unregistered object as an empty page by design (callers read empty + as "feature unavailable"), so that never was an error and is not treated as one + here. + +- 278f57c: Count System Hub's Organizations card through `sys_organization`, the object the framework actually registers — it asked for `sys_org`, which does not exist, so the card read `0` on every deployment (objectui#3670). + + The failure was silent by construction. A missing object answers `404 OBJECT_NOT_FOUND`, and `ObjectStackAdapter.find()` absorbs that on purpose — it caches the name in `missingResources` and resolves `{ data: [], total: 0 }` so callers can treat an uninstalled collection as "no rows". The hub renders `data.length`, so a name the framework never had produced a perfectly ordinary `0`, indistinguishable from a workspace that genuinely has no organizations — which no single-org deployment ever is, since `sys_organization` always holds at least one row. The `.catch` on each call never even saw the 404; it only ever covered non-404 rejections. + + The other three counted names were checked against the framework's object registry and are correct as spelled: `sys_user`, `sys_position`, `sys_audit_log`. + + The Permissions card is **not** fixed here and still reads `0`. Its query names `sys_permission`, which the framework also does not have — it splits that surface into `sys_capability` (lineage: its own docblock says "named `sys_capability`, not `sys_permission`") and `sys_permission_set` (function: the admin-managed grant container). Both would render, so choosing one would silently bind the card to a surface nobody picked; that decision is open on objectui#3655. Until it lands the gap is held visible by a MEASUREMENT case in the page's test rather than quietly re-aimed. + +- cc95c2c: Point System Hub's Permissions card — both its link and its count — at `sys_permission_set`, closing the last of the five `system/*` navigation targets (objectui#3655). + + Four of those URLs became redirects in an earlier change; `system/permissions` was deliberately held back, and so was the count beside it, because the framework splits what this console calls "Permissions" into two Setup entries and picking one would have silently bound every click, bookmark and badge to a surface nobody chose: + + - `sys_capability` — ADR-0066 layer 1, the definition registry of "what can be done". Its own docblock notes it is what the ADR "loosely floats" as `sys_permission`, which is the name the retired page and the count query both used, so lineage pointed here. + - `sys_permission_set` — ADR-0066 layer 2, the grant container the permissions docs call "the only capability container" (object CRUD, field security, access depth, system capabilities), so function pointed here. + + It is decided as `sys_permission_set`: the card reads "Manage permission rules and assignments", and rules-and-assignments is layer 2 — a capability is what a permission set references by name, not what an administrator is assigned. Two user-visible consequences: + + - `/apps/:app/system/permissions` now forwards in one hop to `/apps/:app/sys_permission_set` instead of being rewritten to `…/system/record/permissions` and rendering a record detail page for an object literally named `system` — a dead link that read as a backend fault. + - The Permissions card's badge shows the real number of permission sets. It previously counted `sys_permission`, an object the framework does not register; the adapter absorbs that `404` into an empty page on purpose, so the card printed a confident `0` no administrator could tell apart from "there really are none". + + Recorded as a transitional alias. Retiring this hand-written card wall along with the hub (already `@deprecated` in favour of the metadata-driven navigation) remains open and does not conflict — a redirect keeps old bookmarks resolving either way. + +- 9961df2: Declare the retired `system/{users,organizations,roles,positions}` console URLs as redirects onto the framework-owned system objects (objectui#3655). + + `SystemHubPage`'s cards and both sidebars' `sys-*` cluster emit five `/apps/setup/system/…` targets. Four of them were real routes until `apps/console` was slimmed for third-party customisation, which deleted the bespoke wrapper pages because "these objects are now contributed by framework plugins … and resolved via the generic `/apps/setup/` route" — but the producers were never retargeted and nothing redeclared the URLs. All five fell through to app-shell's tail, where the failure they got depended on how long the word was, because `ShorthandRecordRedirect` treats any URL-safe segment of 6+ characters as a record id: + + - `users` (5) and `roles` (5) rendered "Page not found". + - `organizations` (13) and `positions` (9) were rewritten to `…/system/record/` and rendered a record detail page for an object literally named `system` — the worse of the two, because it reads as a backend/data problem rather than a dead link. + + Each now forwards in one hop to the object the framework's own Setup navigation names: `sys_user`, `sys_organization` (the list entry — the record-scoped one needs a runtime `{current_org_id}` a static redirect cannot resolve), and `sys_position` for both `roles` and `positions` (ADR-0090 D3 renamed `sys_role` to `sys_position`, so the sidebar's "Roles" and the hub's "Positions" are one surface in two vocabularies). Same shape as the `system/objects` and `system/metadata` redirects beside them: the URL is translated, the deleted page is not resurrected, and the navigation producers are untouched. + + `system/permissions` is deliberately left as it was. The framework splits what this console calls "Permissions" into two Setup entries — `sys_capability` and `sys_permission_set` — and picking one here would silently commit every click and bookmark to a surface nobody chose. Its unchanged landing is pinned in the tests so the gap stays visible. + + - @object-ui/sdui-parser@17.4.0 + - @object-ui/react-runtime@17.4.0 + ## 17.3.0 ### Minor Changes @@ -110,6 +186,7 @@ `severity` were dropped and the toast read the bare HTTP status text. ## What changed + - `jsonOrThrow` unwraps the envelope with the exact predicate `ObjectStackClient.unwrapResponse` uses — `success` is a boolean **and** `data` is present. Requiring both is what keeps error envelopes @@ -303,7 +380,7 @@ ### Patch Changes -- 752e18f: fix(console,app-shell): readable reassign hand-off + "System" label for svc:* audit actors — objectstack#4365 / objectstack#4366 +- 752e18f: 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 @@ -871,6 +948,7 @@ is byte-identical, it just comes from the pack now. Adds two guards, both mutation-verified: + - `en` ↔ `zh` full key parity, asserted in both directions. The other eight packs are still ~357 keys behind and are tracked separately (objectui#2872 part a), so they are deliberately not asserted yet. @@ -926,6 +1004,7 @@ all four of its failure modes, and re-introducing the `maplibre-gl` import turns the job red again, as does a fresh error injected into `plugin-ai` — a package that had no type checking whatsoever before this change. + - @object-ui/react-runtime@17.0.0 - @object-ui/sdui-parser@17.0.0 @@ -979,6 +1058,7 @@ `record.viewer.*` — and correctly recognizes position/team-addressed approvers that the client heuristic couldn't resolve. The heuristic remains as a fallback for a backend that predates `viewer`. + - @object-ui/react-runtime@16.1.0 - @object-ui/sdui-parser@16.1.0 @@ -1110,6 +1190,7 @@ - **Permission matrix OWD badges**: every object row now shows its record-level baseline (`OWD Public read`, `Ext Private`, or `OWD Private (default)` for the D1 fail-closed unset case) so grant edits carry their record-reach context. The flow designer's approval assignee `role` kind is intentionally unchanged — spec 13 keeps it as the sole D3 exception (better-auth `sys_member.role` org-membership tier). + - @object-ui/react-runtime@13.0.0 - @object-ui/sdui-parser@13.0.0 @@ -1424,6 +1505,7 @@ - efb4c00: feat(observability): Sentry integration + bundle splitting for production launch **Sentry (opt-in via `VITE_SENTRY_DSN`)** + - New `initSentry()` / `captureError()` / `setSentryUser()` / `getSentry()` helpers exported from `@object-ui/app-shell`. - Dynamic-import design: when `VITE_SENTRY_DSN` is unset, `@sentry/react` @@ -1441,6 +1523,7 @@ stripped from breadcrumb URLs before send. **Bundle splitting** + - `plugin-dashboard` (8 component types) now lazy-registered via `ComponentRegistry.registerLazy()` — only loads on dashboard pages. - `plugin-dashboard` and `plugin-report` each get their own chunk @@ -1537,6 +1620,7 @@ CONCURRENT_UPDATE` response shape with `currentVersion` / detail view (`TypeError: titleFormat.replace is not a function`) and printed `Failed to evaluate expression: ${[object Object]}` for every action visibility predicate. + - `@object-ui/core`: `ExpressionEvaluator.evaluate` / `evaluateCondition` now unwrap Expression envelopes transparently. - `@object-ui/react`: new `toPredicateInput()` helper to safely normalize diff --git a/apps/console/package.json b/apps/console/package.json index e7b4920828..7e1985705d 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/console", - "version": "17.3.0", + "version": "17.4.0", "description": "ObjectStack Console — opinionated, fork-ready runtime console built on @object-ui/app-shell with the full plugin set wired up. Ships as a Hono UI plugin serving a pre-built SPA.", "license": "MIT", "type": "module", diff --git a/packages/app-shell/CHANGELOG.md b/packages/app-shell/CHANGELOG.md index 4b79dd45e7..7d719dd8b4 100644 --- a/packages/app-shell/CHANGELOG.md +++ b/packages/app-shell/CHANGELOG.md @@ -1,5 +1,413 @@ # @object-ui/app-shell — Changelog +## 17.4.0 + +### Patch Changes + +- 993336f: An action declaring `disabled: ''` is no longer greyed out forever (objectui#3842) + + The "is a `disabled` gate declared?" test stopped at `!= null`, missing the + `!== ''` half of the invariant the `visible` family converged on + (`hasDeclaredVisibilityGate`, objectui#3492 / #3758 / #3812 / #3823 / #3835). So + `disabled: ''` counted as a declared gate, and the verdict went to the evaluation + entry — which reads an empty predicate as "no condition → `true`" + (`toPredicateInput('')` is `undefined`, `evaluateCondition(undefined)` is `true`). + + The direction is why this half is a defect and the `visible` half was not. On + `visible`, that `true` means SHOW, so an over-broad "declared" test and a + permissive empty predicate cancel out and `visible: ''` renders either way. On + `disabled`, the same `true` means DISABLE — the two mistakes compound, and an + empty predicate stopped meaning "no gate" and started meaning "permanently + greyed out". One empty predicate, opposite treatment under two keys. + + Two gates now ask the shared definition instead: + + - `@object-ui/app-shell`'s `DeclaredActionsBar` — the hot one. Its actions are + SERVER-declared (`objectDef.actions[]`) and its hosts are the approvals inbox's + record sections, so a `disabled: ''` arriving from metadata (an authoring form + left empty, a template that rendered to an empty string) produced an Approve / + Reject button nobody could click, indistinguishable from deliberate metadata. + objectui#3835 was this same surface failing the other way. + - `@object-ui/components`' `action:button` — verified to be the same shape before + it was changed (the issue inferred it from the identical spelling but did not + probe it): with `disabled: ''` the rendered button carried `disabled=""`. + + **Behaviour change surface, deliberately narrow.** Only `disabled: ''` changes — + from disabled to clickable, which is what "no predicate" asked for. `disabled: +true` still disables, `disabled: false` and an absent `disabled` still do not, and + no expression-valued `disabled` changes verdict. One consequence worth naming: on + `action:button`, an empty `disabled` now falls THROUGH to the legacy non-spec + `enabled` fallback instead of short-circuiting on the empty predicate, so an + action spelling both (`disabled: ''` + `enabled: true`) becomes clickable. + + The legacy `enabled` leg of `action:button` was routed through the same + definition for consistency, and that part is behaviour-preserving by derivation + rather than a fix: the leg is negated (`disabled = !isEnabled`), so an empty + predicate's `true` already arrived as "not disabled" — the same verdict "no gate" + produces. All four shapes are identical under either test; the derivation table + and the reason no test can distinguish them are written down next to the pins. + + `hasDeclaredVisibilityGate` keeps its historic name at both call sites (the + objectui#3842 dispatch ruling): the predicate is key-neutral, and one + implementation behind two names is how a repo grows dialects. Each call site says + so in a comment. + +- d3e738a: Server-declared actions declaring `visible: false` are now hidden instead of rendered as live buttons (objectui#3835) + + `DeclaredActionsBar` — the bar that renders an object's SERVER-declared actions + for one record at a `location`, with no per-action host code — asked truthiness + on the gate: `if (action.visible && !isVisible) return null`. `false && …` is + falsy, so `visible: false`, the most explicit way an author can say "never show + this", fell into the "no gate declared" branch, the verdict was never consulted, + and the action rendered for everyone. + + What that means on the page: the bar's host is the approvals inbox's + record-section toolbar (`apps/console/src/pages/system/ApprovalsInboxPage.tsx`), + so an approval action the metadata had switched off with `visible: false` + rendered as a live Approve / Reject / Reassign button — and this component's own + click handler is what POSTs the decision. One click was a real approve/reject + call on a request the declaration said not to offer a decision on. + + This is the fifth and last member of the objectui#3492 family (after + objectui#3758 / PR #3816 for the row-action surfaces and objectui#3812 / #3823 + for the action face), and the one whose two family-wide mitigations both fail: + + - The action defs are **server-declared** (`objectDef.actions[]`, + `sys_approval_request`), not hand-written view JSON. "`ActionSchema.visible` is + `ExpressionInputSchema` with no boolean member, so `objectstack build` cannot + emit this shape" does not apply on this path — the def arrives from server + metadata and in-process construction, where a boolean is the natural spelling. + - The bar is mounted as **plain JSX** by its hosts, so `packages/react`'s + `SchemaRenderer` — which evaluates a node's `visible` and hides it before the + component mounts, and which is why objectui#3812 judged the component-level + gates a dormant defensive layer — is not on this path at all. This gate was the + only one there. + + The gate now reads the family's one named definition, + `hasDeclaredVisibilityGate` (`!= null && !== ''`), imported from + `@object-ui/components` rather than re-spelled: five gates in three packages + asking one question must not drift into five answers. The evaluation entry is + untouched — `toPredicateInput` passes a boolean through and `useCondition` + short-circuits it instead of calling the expression engine — so a declared + `false` resolves to `false`, and every expression-valued `visible` keeps exactly + the verdict it had. + + Behaviour change surface, deliberately narrow: only a declared action whose + `visible` is the literal boolean `false` (or another falsy non-empty value) + changes, from rendered to hidden, which is what the declaration asked for. + `visible: true` still renders, `''` and an absent `visible` are still no gate at + all, and the bar still renders no chrome when its located set is empty. + + The suite that covered this component could not have caught it: it stubbed the + whole predicate entry constant-true (`useCondition: () => true`), with a comment + saying the test actions omit `visible` "so this is unused" — which made the gate + unreachable from the only tests that mount this component (the objectstack#4984 + family, where a fixture keeps a broken rule green). That stub is gone; the suite + now runs the real `useCondition` / `toPredicateInput` and doubles only the action + dispatch, so all four shapes (`false` hides / `true` renders / undeclared renders + / `''` is not a gate) are judged by the shipped evaluation semantics. + +- b691f06: Ask the view composer for a container's view identities instead of deriving `list.name || 'list'`, so the default list view's translated label resolves + + A `defineView` container declares its default list under the `list` key. That key is a slot in the authoring document, not the view's identity: `expandViewContainer` — the same composer the framework's loader and the i18n extractor call — registers an unnamed default list as `.default`. This renderer derived `list.name || 'list'` instead, a third spelling no producer emits, so a default-list-only object probed `objects.._views.list.label`, missed the published `_views.default.label` key (objectstack#5164 ruling A, migrated in objectstack#6124) and fell back to the English metadata label — for the view's description and empty state too. + + - `MetadataProvider.mergeViewsIntoObjects` now expands a stack-packaged container through `expandViewContainer` and routes the result through the same code path as first-class ViewItems. Both authoring gates therefore key `listViews` / `formViews` by the canonical `.` identity, and the container inherits the composer's folding (a `listViews` entry that merely restates `list` collapses into one view) and collision renaming instead of restating them locally. + - `ObjectView` resolves the primary view's id through the new `defaultListViewId` helper — one derivation shared by the view-override lookup and the view-switcher promotion, with no literal fallback. + + The renamed id is also the key a view override is persisted under (`updateViewConfig(object, viewId, …)` writes a `view` metadata record named by the id). Nothing is orphaned: the retired `'list'` spelling is not a representable view identity at all — `ViewItemNameSchema` requires a dotted `.` name — while the record-gate path, which real backends serve, already used the qualified id. Stale `/view/list` links fall back to the object's default view, which is the same view they named. + +- 0ef94ca: console: hold the environment list's create CTA with a skeleton until entitlements + resolve, instead of showing a label that is about to be overwritten (objectui#3482, + part of cloud#1049). + + `EnvironmentListToolbar` presents a state-aware create affordance — "Set up your + production environment" / "Add development environment" / an upgrade prompt — decided + from `GET /cloud/environment-entitlements`. While that request was in flight the + toolbar rendered the action's metadata label, so the button visibly changed its + wording the moment the response landed. The two texts are owned by different + packages (the cloud translation bundle vs this repo's locale packs), which made the + swap read as an inconsistency rather than a load. + + The in-flight state now renders a `Skeleton` sized like the button it stands in for, + matching the adjacent `cloud:onboarding-next` welcome CTA. Only the create action is + withheld — other toolbar actions never re-label, so they keep rendering — and a + toolbar without a create action gets no skeleton at all. The skeleton is never + terminal: when both entitlement signals fail, the resolution settles as + `{ ready: false, source: 'unknown' }` and the neutral metadata label is shown, which + remains the honest text for a state where "which create is this?" is genuinely + unknown. + +- 13b72c7: Render the `/home` Administration group as a real group, so its nine system-administration entries are reachable (objectui#3609). + + `UnifiedSidebar` picks its renderer with one ternary on `context === 'app' && activeApp`. Only the app arm rendered `NavigationRenderer`, the component that descends into `type: 'group'` children; the home arm hand-rolled `homeNavigation.map(item => )` with no recursion. Since home navigation is the only navigation that groups, the whole nine-entry Administration cluster collapsed into one row — and a group carries no `url` of its own, so `|| '/home'` pointed that row back at the page the user was already on. System Settings, Applications, App Marketplace, Object Manager, Datasources, Users, Organizations, Roles and Configuration never reached the DOM. `resolveLandingPath([])` sends a fresh-deployment admin to `/home`, and `HomePage` had deliberately dropped its own System card on the grounds that the sidebar already carried those entries, so the net effect was an admin with no route into system administration at all. + + The home arm now renders through the same `NavigationRenderer` as the app arm rather than growing a second renderer that recurses: the group becomes a Collapsible and every entry passes the same item-level `visible` / `requiredPermissions` / runtime-capability guards. Hrefs are unchanged — home entries are all `type: 'url'`, whose resolution is verbatim. The group states `expanded: true` so it opens by default: the renderer's unauthored default collapses groups of eight or more children, a heuristic for one long section among many, whereas on `/home` this group _is_ the navigation. Pinning and drag-reorder stay off in the home context, where their persistence key resolves to the first app rather than to home. Non-admins are unaffected — the cluster is still built behind the `isWorkspaceAdmin` gate and is absent from their item tree. + +- 7883c02: Send the console host's legacy URL redirects straight to the canonical metadata-admin routes instead of routing them through the deprecated `component/metadata/resource` alias (objectui#3639). + + `apps/console`'s `ObjectRedirect` and `MetadataRedirect` rewrote `system/objects[/:name]` and `system/metadata[/:type[/:name]]` onto `…/component/metadata/resource[/:name]?type=:type`. app-shell declares that spelling as a legacy _alias_, not a page: its route element is `LegacyMetadataRedirect`, which immediately navigates on to `…/metadata/:type[/:name]`. Every one of those URLs therefore took two `` hops (plus a re-render) to reach a destination the host could name directly — and it was this indirection that carried `sys-objects` into the zero-app blank screen fixed in objectui#3610, since the alias was the leg that branch did not recognise. + + Both redirects now construct `…/metadata/:type[/:name]` (and `…/metadata` for the typeless directory arm) themselves. The endpoints are unchanged, byte for byte, including the alias hop's own percent-encoding of `:type` and its verbatim pass-through of `:name`; only the intermediate hop is gone. The alias routes stay declared exactly as they were — bookmarks, external links and the setup left-nav still arrive on them and are still forwarded — this change only stops the console feeding its own traffic through them. + + Also corrects four docblocks that described the alias as "the engine route", in `apps/console`'s two redirects and in app-shell's `datasource` resource registration and page. That wording is not merely stale: the objectui#3610 dispatch read this chain and concluded `component/metadata/resource` was the canonical spelling, which is the exact opposite of what the route table says. + +- be9cd38: metadata-admin: name the offending key when only one union member ever read the value + + A union with no discriminant reports its failure as one collapsed issue, and the + member diagnostics that would name the problem are buried inside it. PR #3677 + started unpacking those for `config.columns` by reading the value's own content, + but deliberately declined every union where some member had rejected the value's + type outright — which left `config.sort` (`string | ColumnSort[]`) collapsed even + though only one of its two members had read the value at all. + + When exactly one member accepted the value's type, naming it is a fact rather + than a preference: it is the only member whose complaint can be about what the + author wrote. So `sort: [{ field: 'n', order: 'bogus' }]` now reports + `config.sort.0.order` with the spec's own `expected one of "asc" | "desc"` + instead of `config.sort` / `Invalid input`, and the same holds for a sort row + that is not an object, a `columns[].summary` written as a bad enum string, a form + `sections[].fields[]` entry missing its `field`, and an array `filter[].value` + whose offending element is now addressed directly. + + Where two or more members read the value, or where none did, nothing changes: + the previous message is kept rather than inventing a preference between members + that objected equally. Both gates — create and edit — continue to report + identically, and validation verdicts are untouched: the accept/reject decision is + still made by the one gate, and this only changes how an already-failed draft is + presented. + +- d2fd044: Point the last four navigation producers at the canonical metadata-admin routes instead of the deprecated `component/metadata` alias, removing a redirect hop from each (objectui#3660). + + The System hub's "Metadata" and "Datasources" cards aimed at `…/component/metadata/directory` and `…/component/metadata/resource?type=datasource`, and the `sys-datasources` entry in both `AppSidebar.systemFallbackNavigation` and `UnifiedSidebar.homeNavigation` spelled the latter too. app-shell declares those spellings as legacy _aliases_, not pages: their route element is `LegacyMetadataRedirect`, which immediately navigates on to `…/metadata` and `…/metadata/datasource`. Every click on any of the four therefore paid a redundant hop plus a re-render to reach a destination the navigation could name directly. All four now name it. + + The landing pages are unchanged, byte for byte — the new URLs are exactly what the alias hop was already computing (`datasource` percent-encodes to itself, and neither producer carried a query or hash beyond the `?type=` the alias itself consumed). Only the intermediate hop is gone. + + The alias routes stay declared in both `AppContent` branches, untouched: bookmarks and external links still arrive on them and are still forwarded. This completes objectui#3639, which corrected the console host's two redirects and enumerated these four as the remainder. + +- b7b05da: Point the `sys-objects` navigation entries at the canonical metadata-admin route instead of the `system/metadata/object` alias, removing a redirect hop from each click (objectui#3739). + + `AppSidebar.systemFallbackNavigation`, `UnifiedSidebar.homeNavigation` and `console/home/QuickActions` all spelled this target `/apps/setup/system/metadata/object`. That is not a page: `apps/console`'s host fragment declares `system/metadata/:metadataType` with `MetadataRedirect` as its element, which immediately navigates on to `/apps/setup/metadata/object` — the engine's real route (`metadata/:type`, `MetadataResourceListPage`). Every click therefore paid a redundant hop plus a re-render to reach a destination the navigation could name directly. All three now name it. + + This is the same defect objectui#3660 fixed for `sys-datasources`, declared on the line immediately below `sys-objects` in both sidebar arrays. It was missed there because the two entries reached their aliases through different route tables — `sys-datasources` through app-shell's own `component/metadata/resource` alias, `sys-objects` through the host's `system/metadata/:type` rewrite. + + The landing page is unchanged, byte for byte: the new URL is exactly what the alias hop was already computing (`object` percent-encodes to itself, and no producer carried a query or hash). Only the intermediate hop is gone. Of the three producers, the two sidebars are live; `QuickActions` has no JSX call site today, so its change is a guard against the dead link returning with the component. + + The alias routes stay declared and untouched: bookmarks and external links still arrive on them and are still forwarded. + +- fa3ba5b: Make the zero-app console's "Object Manager" / "Datasources" entries resolve, and give that branch a not-found screen instead of a blank one (objectui#3610). + + On a deployment with no published apps, the system fallback navigation sends `sys-datasources` to `/apps/setup/component/metadata/resource?type=datasource` and `sys-objects` to `/apps/setup/system/metadata/object` (rewritten by the console host onto the same legacy alias). `isMetadataRoute` is a substring test on `/metadata`, so both URLs pass the "No Apps Configured" guard and enter `AppContent`'s no-`activeApp` route table — which declared no `component/…` route at all and, unlike the with-`activeApp` table, carried no trailing catch-all. A `` with no match renders `null`, so an admin building their first object got a fully blank screen: no 404, no error, no empty state. + + Both halves are fixed on the routing side, with no navigation URL changed. The two legacy metadata aliases (`component/metadata/directory`, `component/metadata/resource/*`) are now declared in the no-`activeApp` branch too, mirroring the with-`activeApp` branch — they are redirects, not a second copy of the page, so they forward onto the canonical `metadata/:type` routes that branch already declared. And the branch now ends in the same `path="*"` → "Page not found" screen the with-app branch has always had, so the next unresolved URL in a zero-app console is reportable rather than invisible. + +- 9089d85: The no-apps empty state's "Create Your First App" CTA now opens the app-creation + flow instead of silently bouncing the user back to the landing page. It called + `navigate('/create-app')` — an ABSOLUTE path, so it resolved against the HOST's + root route tree, which declares no `/create-app`; the reference host's trailing + `` therefore replaced it with `/`. The `create-app` route is + declared by `AppContent` itself, inside the `/apps/:appName/*` subtree (both the + no-active-app branch and the with-app router), so the CTA now builds the + app-scoped `/apps//create-app` — the platform's canonical app URL + (ADR-0048) and the same target the sidebar's add-app entry already links to. On + a fresh zero-app deployment this was the first screen's only route into app + creation, and it read as a button that does nothing (#3573). + + A plain relative `navigate('create-app')` is deliberately NOT the fix, and the + new routing test pins why: under the installed react-router 7, + `getResolveToMatches` resolves a relative target against the LEAF match's full + `pathname` with the splat INCLUDED (in v6 this was the `v7_relativeSplatPath` + future flag; v7 hardcodes it). The empty state renders across a whole URL family + — `/apps/setup` and any deeper `/apps/setup/` — so the relative form is + right only at the shallowest of them and builds + `/apps/setup//create-app` elsewhere, which matches no route and renders + a blank screen instead of the bounce. The sibling "System Settings" CTA is + unchanged. + +- d1be436: Point the "System Settings" entries at the system hub `/apps/setup/system` instead of the bare `/apps/setup` (objectui#3590). + + `AppContent` mounts the system hub only under `isSystemRoute`, which keys on a `/system` path segment. A bare `/apps/setup` therefore matched no pseudo-route except `isSetupRoute` and fell back into the "No Apps Configured" guard — i.e. on a zero-app deployment it _is_ that empty state's own URL, so the empty state's `go-to-settings-btn` re-rendered the very screen it sits on. Retargeted three call sites: the empty state's CTA, `AppSidebar`'s no-active-app `sys-settings` fallback entry, and `UnifiedSidebar`'s `/home` Administration `sys-settings` entry. Every sibling entry in both clusters already spelled `/apps/setup/system/...`. + +- 949b2f1: metadata-admin: name the offending column when `config.columns` is rejected + + `config.columns` is `string[] | ColumnDef[]` — a union with no discriminant — so + Zod reported every rejection as one collapsed issue on the field itself: + `config.columns` / `Invalid input`, on the create gate and the edit gate alike. + The field was reachable, but nothing said which column was wrong, which key, or + what was expected. + + The union member is now chosen by the value's own first element — a list of + field names or a list of column objects — and that member's real diagnostics are + reported at their draft-absolute path. A mis-typed key reports + `config.columns.0.field` with `expected string, received number`; a stray number + in a list of field names reports the element that broke it rather than every + element of the shape the author never chose. The aggregated container reaches + the same union as `list.columns.…`, and both gates now report identically. + + Only unions that really are "an array of A or an array of B" are read this way, + so neighbours such as `config.sort` (`string | ColumnSort[]`) are untouched. + Where the content elects nothing — a first element that is neither a string nor + an object — the previous message is kept rather than guessing. + + Validation verdicts are unchanged: the accept/reject decision is still made by + the one gate, and this only changes how an already-failed draft is presented. + +- 5f752a0: Match the built-in pseudo-routes on whole path segments, so a mistyped app name can no longer render a different app (objectui#3638). + + `AppContent` decides whether a URL is a built-in pseudo-route (`create-app`, `system/*`, `metadata/*`, `setup`) before it decides which app to render, and two of those switches were substring tests: `pathname.includes('/system')` and `pathname.includes('/metadata')`. Both are true for any segment that merely _starts_ with the word — `system_log`, `system_setting`, `systems`, `metadata_import`, `metadata-export`. `isSpecialRoute` feeds `requestedAppMissing`, so visiting `/apps//system_log` marked the URL as a pseudo-route, suppressed the "App not available" guard, fell back to the default app and rendered **that** app's shell with `system_log` taken as its object name — the exact "must NOT silently render a DIFFERENT app" case the fallback's own comment exists to prevent, with no indication that the requested app does not exist. + + The two flags now test path _segments_ (`pathname.split('/').includes('system' | 'metadata')`); `isCreateAppRoute`'s `endsWith('/create-app')` is unchanged. Every real pseudo-route spells the word as a whole segment — `system/marketplace{,/installed,/:packageId}`, the host's `system/{apps,profile,approvals,ai-approvals,audit-log,settings,objects,metadata/…}`, `metadata/{,_diagnostics,:type,…}` and the legacy `component/metadata/{directory,resource/*}` aliases — so all of them stay special, including in the zero-app branch that keys on these flags directly (objectui#3590 / #3610). Knock-on, in a zero-app console only: a `system`-prefixed near-miss such as `/apps/setup/system_log` now reaches the same "No Apps Configured" screen every other unresolved URL there reaches, instead of the pseudo-route branch's "Page not found". + +- fbc23e0: Action params that inherit a field's options now keep the keys that field declared + + A field-backed action param (`{ field: 'tier' }`) had its inherited option list + rebuilt entry by entry as `{ label, value }`, which silently dropped every other + key the field's options declared — most consequentially the per-option + `visibleWhen` predicate (ADR-0058). A select field whose options narrow by + predicate in an object form therefore offered the FULL list in an action dialog, + including the entries the predicate exists to hide, with no diagnostic on either + side; `color` / `icon` / `disabled` were lost the same way. Options authored + inline on the param were never affected — they always passed through verbatim, + which is the asymmetry this restores. + + The resolver now preserves each inherited entry and only does its two real jobs: + expanding bare strings into label/value pairs and translating the label through + `fieldOptionLabel`. The option widgets already filter on `visibleWhen`, so a + role-gated option (`'admin' in current_user.positions`) inherited by a dialog + param now narrows the offered set and clears a seeded value the predicate hides. + + `ActionParamDef.options` (`@object-ui/core`) and the resolver's `RawActionParam` + are widened to match: `ActionParamOption` names the two keys the param layer + reads and carries the rest of a field's option vocabulary through. + +- 6d762da: The five locale keys behind #3546's eight no-fallback `t()` call sites are now defined in all ten packs, so the built-in-view toasts, the activity-timeline source link, the wizard's required-field toast and the Gantt refresh button's accessible name are translated instead of falling back to English — or, on two surfaces, to the key itself (part of #3546). + + `scripts/check-i18n-call-site-keys.mjs` measured 258 keys that a `t()` call site asks for and no pack defines. These five were the subset with no working inline default: `console.objectView.cannotEditMetaView`, `console.objectView.cannotDeleteMetaView`, `detail.viewSource`, `gantt.toolbar.refresh` and `wizard.missingRequired`. Adding a `defaultValue` is deliberately not the fix — that mechanism is what kept all 258 invisible for months. + + **Two of the eight sites really did render the raw key**, and both go through a binding with nothing in front of i18next. `ObjectView.tsx` calls `useObjectTranslation()` directly, so five toasts read `console.objectView.cannotEditMetaView` / `cannotDeleteMetaView` on screen; the `|| 'Built-in views cannot be renamed.'` guards next to them were dead on every path, because i18next answers a miss with the key itself and a non-empty string never falls through `||`. Those four unreachable English strings are removed rather than repaired: one key served four call sites (rename / pin / set-as-default / configure), so the pack copy covers any change to a built-in view instead of naming one operation. `RecordActivityTimeline.tsx` fails the same way for a subtler reason — `useDetailTranslation` is `createSafeTranslation(..., 'detail.back')`, and because `detail.back` does resolve, the probe hands back i18next's `t` for every key and bypasses the defaults map wholesale, so `detail.viewSource` reached the user verbatim. + + **The other two sites were not rendering a raw key**, contrary to the issue's description, and are fixed here as the milder "English in all ten languages" class. `wizard.missingRequired` is its own hook's probe key, so the probe failed and `createSafeTranslation` correctly served its English default. `gantt.toolbar.refresh` goes through `useGanttTranslation`, which deliberately does not use `createSafeTranslation` and falls back per key — so the refresh button's `aria-label` was "Refresh", in English, never the key. Screen-reader users heard an English word rather than an identifier; a `zh` session now hears 刷新. + + Regression cover is provider-mounted on purpose: with no `I18nProvider` the defaults maps answer every one of these keys and the assertions pass while the console is broken, which is precisely the false-green the issue documents. For the two sites whose English output was already correct, `en` cannot discriminate before from after — the `zh` assertions are the ones that pin the fix. + +- 54233b1: Record detail pages: a header ⟳ that refreshes the record, its related lists and its tab counts in place — no browser reload + + Concurrent-editing scenario from the shop floor (MES work orders): operator A sits on a record's detail page while operator B starts or reports the same order. A had no way to see the new state except F5, which throws away the open tab, the scroll position and any in-progress inline edit along with the stale data. + + The pipeline for this already existed — the objectui#2269 invalidation bus refetches every mounted reader in place, and `RecordContext.refresh` had been declared for it — but nothing produced that field and no UI reached for it. Three changes give it a trigger: + + - **`RecordDetailView` produces `RecordContext.refresh`**, publishing `notifyDataChanged({ objectName: '*' })`. The wildcard is deliberate: a user reaches for refresh because of a write made by SOMEONE ELSE, which this client never saw and therefore cannot attribute to particular objects. `'*'` marks everything mounted as stale, so the main record, every related child list and the tab-count badges all refetch — no remount, so tab / scroll / draft state survive. First phase covers the standalone record route; embedded hosts (list drawer, split-pane preview) keep their existing chrome unchanged. + - **`page:header` renders the ⟳** at the far end of the header row when — and only when — the host provides `refresh`. It is page chrome rather than a header action, so its position is the same on every record page regardless of which business actions the object declares, and it can never be collapsed into the `⋯` overflow. Styled as that `⋯` trigger's twin so the row reads as one button family. Its accessible name and tooltip come from the existing `common.refresh` key, so the icon-only button is not English-only in the other nine locales. The icon spins for a short floor after a click, because the bus is fire-and-forget and a warm backend would otherwise finish before the click looked like it landed. + - **`RelatedList` accepts the `'*'` wildcard** on the legacy `objectui:related-changed` event, matching what `dataChangeMatches` already does for the bus's own readers. This listener compared the payload's object name to its own, so a wildcard invalidation reached everything on the page except the related lists — a concrete foreign object name is still ignored. + + Hosts that provide no `refresh` render exactly as before. + +- 6b3d47b: Point the four remaining "Settings" senders at the system hub `/apps/setup/system` instead of the bare `/apps/setup` (objectui#3611). + + Same root cause as objectui#3590, which fixed the three call sites inside its declared file surface: `AppContent` mounts the system hub only under `isSystemRoute`, which keys on a `/system` path segment, so on a zero-app deployment the bare `/apps/setup` _is_ the "No Apps Configured" empty state's own URL and every entry spelling it looped in place. + + Three of the four are live defects, all reachable on a zero-app deployment today: + + - `AppSidebar`'s no-active-app sidebar header (`system-sidebar-header`) — the sharpest of them, since it renders _only_ when there is no active app, i.e. it was unreachable except in exactly the state where its target was broken. + - `AppSidebar`'s user-menu "Settings" entry. + - `SystemRedirect`'s bare `/system` legacy bookmark. This forwarder was already half right — every _suffixed_ bookmark (`/system/users`) was correctly rewritten to `/apps/setup/system/users`, and only the bare one dropped the `system` segment. The bare branch now agrees with the suffixed branch beside it; no new logic. + + The fourth, `QuickActions`' "System Settings" card, is dormant — the component has zero JSX call sites repo-wide, so no user can reach it today. It is corrected in the same pass so the dead link cannot return with the component if it is ever remounted. + +- c993ff2: metadata-admin: restore per-field diagnostics when editing an invalid stored `view` + + Editing a stored `view` is judged by the wire gate `ViewMetadataSchema`, which is a + union. Zod reports a union failure as a single root issue — no path, message + `Invalid input` — so every field-level diagnostic collapsed into one message that + pointed at nothing: `SchemaForm` had no field to highlight and Monaco had no + position to jump to, and the guided messages the spec writes for these rejections + never reached the editor. + + Failures are now expanded to the union member the draft's own `viewKind` + discriminant selects, so a bad stored view reports `config.type` with the list of + valid layouts, a mis-typed filter reports `config.filter.0.operator`, and a + container key that belongs to a single view gets the spec's full + `defineView(...)` guidance back. Only the selected member's issues are shown, so + the other members' rejections do not become noise. + + Validation verdicts are unchanged: the accept/reject decision is still made by the + one gate, and this only changes how an already-failed draft is presented. + +- 4cf76ce: metadata-admin no longer false-rejects a stored `view` that has been pinned or + reordered. The editor's live client-side validation judged BOTH the create and + the edit draft with the AUTHORING schema (`ViewItemSchema` via + `viewSchemaForDraft`). That is right for create and wrong for edit: the editor + opens a body that came back out of `sys_metadata`, and the platform itself + writes keys into stored view bodies — `isPinned` from the view switcher's pin + action, `sortOrder` from the reorder write, and a per-row `id` that the console + filter/sort builders stamp on `config.filter[]` for React. `updateView` GETs the + stored item and PUTs `{ ...current, ...partial }`, and `saveMetaItem` persists + the accepted body verbatim, so those keys are in storage by design. + + Before the authoring schemas were tightened these keys were silently stripped + and the draft passed. Once the gate became strict, opening a pinned view in the + editor reported unrecognized keys — while the SERVER accepted the very same body, + because it validates against `ViewMetadataSchema`. The client was strictly + stricter than the server; the direction was inverted. + + `validateMetadataDraft` now takes an optional `{ mode: 'create' | 'edit' }`. + Create keeps the authoring gate unchanged. Edit is judged by + `ViewMetadataSchema` — the schema the `view` metadata type registers, i.e. the + same one the server runs — so the client and the server accept the same set by + construction. `mode` defaults to `'create'`, the strict gate, so a caller that + omits it can only ever over-report, never silently widen the door. + + The edit gate keeps its teeth: a wrong `config.type` and a container carrying an + unknown key are both still rejected. + +- 7e2b7e9: Fix saved list-view preferences never reading back (density, column widths, sort, hidden columns, inline edit) + + `listViewOverrides` in the ObjectStack adapter enumerated `GET /api/v1/meta/{objectName}` — putting the object name in the metadata **type** slot — while `updateViewConfig` persists under `type='view'`. The two key spaces are disjoint, so the batch map came back empty for every object and every personalization a user saved on a list view was written to the server but never read back, showing up as "the setting didn't save". + + The read now enumerates `type='view'` once and narrows to the object client-side, through the same accessor `listViews()` uses over the same rows — the metadata index is name-only, so there is no server-side `?object=` filter to push it into. + + Second half: the batch read no longer swallows its own failures into an empty map. An empty map is an authoritative "this object has no overrides" and callers may still trust it and skip the per-view reads (the batch optimization is intact), but a transport failure now rejects, so the per-view `getView` fallback it was silently disabling becomes reachable again. `DataSource.listViewOverrides` documents both terms so other adapters implement the same contract. + +- Updated dependencies [794c497] +- Updated dependencies [993336f] +- Updated dependencies [b5980f4] +- Updated dependencies [7864f03] +- Updated dependencies [d229dfa] +- Updated dependencies [ecae400] +- Updated dependencies [4bc6c23] +- Updated dependencies [d3e738a] +- Updated dependencies [f5f8744] +- Updated dependencies [7ed3360] +- Updated dependencies [3765678] +- Updated dependencies [d83f6b3] +- Updated dependencies [5f08c05] +- Updated dependencies [e24d767] +- Updated dependencies [aca561a] +- Updated dependencies [844d17f] +- Updated dependencies [48132f7] +- Updated dependencies [4dcd52a] +- Updated dependencies [42ae5c6] +- Updated dependencies [fbc23e0] +- Updated dependencies [6d762da] +- Updated dependencies [e6fdbdc] +- Updated dependencies [54233b1] +- Updated dependencies [97b63d7] +- Updated dependencies [7e2b7e9] +- Updated dependencies [33526fd] +- Updated dependencies [32413ec] + - @object-ui/components@17.4.0 + - @object-ui/i18n@17.4.0 + - @object-ui/types@17.4.0 + - @object-ui/fields@17.4.0 + - @object-ui/core@17.4.0 + - @object-ui/data-objectstack@17.4.0 + - @object-ui/react@17.4.0 + - @object-ui/layout@17.4.0 + - @object-ui/plugin-editor@17.4.0 + - @object-ui/collaboration@17.4.0 + - @object-ui/auth@17.4.0 + - @object-ui/permissions@17.4.0 + - @object-ui/providers@17.4.0 + ## 17.3.0 ### Minor Changes @@ -19,6 +427,7 @@ moves the producer there. ## What changed + - All entitlement context is read from `error.details.` and **nowhere else**. `code` and `message` are declared `ApiErrorSchema` fields and stay on `error` itself. @@ -178,6 +587,7 @@ auto-activated, landing the user on an empty sidebar. ## What changed + - **Shared predicate, not a second implementation.** Both switchers now call `hasVisibleNavigationItems` from `@object-ui/layout` — the exact guards `NavigationRenderer` applies per item — so the switcher can never disagree @@ -257,6 +667,7 @@ ten packs missing it identically kept parity fully green. ## What changed + - `createTargetOrg` is backfilled into `en` as `Creates in {{org}}`, which makes the parity gate demand it from the other nine; each is translated to its pack's existing `form`-section tone rather than copied or machine-filled. @@ -2226,7 +2637,7 @@ VALIDATION_FAILED` means _this adapter_ sent an off-contract body. Degrading `AnalyticsQueryRejectedError`, `isAnalyticsNotInstalledError`, `classifyAnalyticsFailure`. -- 752e18f: fix(console,app-shell): readable reassign hand-off + "System" label for svc:* audit actors — objectstack#4365 / objectstack#4366 +- 752e18f: 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 @@ -5708,6 +6119,7 @@ unauthenticated` in the token-based console, while the runtime data adapter's objects+fields were editable in Studio; this reworks both surfaces. **Setup (assign + read-only):** + - The six facets (`object_permissions`, `field_permissions`, `system_permissions`, `row_level_security`, `tab_permissions`, `admin_scope`) now render read-only on the `sys_permission_set` record page as a compact summary (counts, or capability @@ -5722,6 +6134,7 @@ unauthenticated` in the token-based console, while the runtime data adapter's **Studio (design every facet):** the permission matrix editor gains structured editors for the facets that were JSON-only — + - **System Capabilities**: a multi-select over the live `sys_capability` registry (scope-grouped, labelled chips). - **Row-Level Security**: per-policy rows (object · operation · enabled) with CEL @@ -6325,7 +6738,7 @@ unauthenticated` in the token-based console, while the runtime data adapter's the "new set" creator both call `client.save(..., { mode: 'draft', packageId })` — the framework stamps the draft with the package, and the top-bar **Publish** promotes it atomically (materialized into `sys_permission_set` by the framework - side, ADR-0086 P2 块1). The **environment-admin** door (no `packageId`) is + side, ADR-0086 P2 块 1). The **environment-admin** door (no `packageId`) is unchanged: it stays **live** (config), per D7. - Reads are draft-aware: the editor loads any pending draft over the published baseline, and the pillar rail merges published ∪ draft sets — so a set created @@ -7074,6 +7487,7 @@ unauthenticated` in the token-based console, while the runtime data adapter's `PackageDetailSheet` gains the user-facing affordances for the package-as- lifecycle-unit work: + - **Duplicate** → `POST /packages/:id/duplicate` (clone a base into a new writable package; D4). - **Adopt loose items** → `POST /packages/:id/adopt-orphans` (migrate every @@ -7990,6 +8404,7 @@ unauthenticated` in the token-based console, while the runtime data adapter's `recordId`. Pairs with the framework screen-flow runtime (`@objectstack/service-automation` + - `@objectstack/runtime`). Verified in-browser: showcase task row → "Reassign…" → form → submit → the task is reassigned. @@ -8583,6 +8998,7 @@ formViews }`) and ignored `viewKind` entirely. As a result a form-family view the Setup app → _All Metadata Types_. ### New: `@object-ui/app-shell` views/metadata-admin + - **`MetadataDirectoryPage`** — auto-grouped tile directory by domain, with free-text search, domain chips, and a _Writable only_ filter. - **`MetadataResourceListPage` / `MetadataResourceEditPage` / `…CreatePage` / `…HistoryPage`** — @@ -8606,6 +9022,7 @@ formViews }`) and ignored `viewKind` entirely. As a result a form-family view `t(key)` helper. ### New routing variant + - App nav now supports `{ type: 'component', componentRef, params? }` items. `AppContent` resolves them through the existing `ComponentRegistry`. - Built-in components registered: `metadata:directory`, `metadata:resource`, @@ -8614,6 +9031,7 @@ formViews }`) and ignored `viewKind` entirely. As a result a form-family view / page. ### Plugin-designer + - Lazy-exported `ObjectManager`, `FieldDesigner`, `ObjectViewConfigurator`, `DashboardEditor`, `PageCanvasEditor`, `MetadataObjectsPage`, and `MetadataFieldsPage` so the engine can mount them on demand. @@ -8622,6 +9040,7 @@ formViews }`) and ignored `viewKind` entirely. As a result a form-family view through the new component routes. - ca685ab: Add ChatGPT-style AI chat history surface at `/ai` and `/ai/:conversationId`. + - New `DefaultAiChatPage` with conversations sidebar (list, create, select, delete) and chat pane on the right. - New `ConversationsSidebar` component and `useConversationList` hook for listing and managing `ai_conversations`. - `useChatConversation` now accepts an optional `activeId` to hydrate a specific conversation (bypassing the localStorage cache), and guards against duplicate conversation creation when sibling state (e.g. selected agent / scope) changes during the same visit. @@ -8633,6 +9052,7 @@ formViews }`) and ignored `viewKind` entirely. As a result a form-family view - 0335ec4: Polish the AI chat surface based on real-world dogfooding feedback. **`@object-ui/plugin-chatbot`** — new display helpers shared by `ChatbotEnhanced`: + - `unwrapToolResult(value)` peels the MCP-style `{ type: 'text', value: '' }` envelope that backend tools emit (`@objectstack/service-ai`'s data/metadata tools, in particular), and JSON-parses the inner payload. The result panel @@ -8653,6 +9073,7 @@ formViews }`) and ignored `viewKind` entirely. As a result a form-family view so wrappers can compose richer titles. **`@object-ui/app-shell`** — `AiChatPage`: + - Removes the fake "Hello! I'm X" assistant welcome bubble so the empty-state suggestion chips can actually render. - Adds per-agent default suggestion sets (`data_chat`, `metadata_assistant`) @@ -8933,6 +9354,7 @@ Assistant… (try "系统里有多少个用户?")`). - efb4c00: feat(observability): Sentry integration + bundle splitting for production launch **Sentry (opt-in via `VITE_SENTRY_DSN`)** + - New `initSentry()` / `captureError()` / `setSentryUser()` / `getSentry()` helpers exported from `@object-ui/app-shell`. - Dynamic-import design: when `VITE_SENTRY_DSN` is unset, `@sentry/react` @@ -8950,6 +9372,7 @@ Assistant… (try "系统里有多少个用户?")`). stripped from breadcrumb URLs before send. **Bundle splitting** + - `plugin-dashboard` (8 component types) now lazy-registered via `ComponentRegistry.registerLazy()` — only loads on dashboard pages. - `plugin-dashboard` and `plugin-report` each get their own chunk @@ -8983,6 +9406,7 @@ Assistant… (try "系统里有多少个用户?")`). Resolves 8 moderate-severity GHSA advisories against the transitive `dompurify@3.2.7` pulled in by `monaco-editor`. Vulnerabilities covered: + - SAFE_FOR_TEMPLATES bypass in RETURN_DOM mode - FORBID_TAGS bypassed by function-based ADD_TAGS predicate - Prototype Pollution to XSS via CUSTOM_ELEMENT_HANDLING fallback @@ -8993,6 +9417,7 @@ Assistant… (try "系统里有多少个用户?")`). - Generic XSS vector No API changes; override is transparent to consumers. + - @object-ui/types@5.2.1 - @object-ui/core@5.2.1 - @object-ui/i18n@5.2.1 @@ -9015,12 +9440,14 @@ Assistant… (try "系统里有多少个用户?")`). wired by `RecentItemsProvider` + `useTrackRouteAsRecent` + `RecordDetailView`). Multi-device by construction: open a record on laptop, see it in `⌘K → Recently viewed` on phone. + - Group renders only when input is empty (no competition with search). - Limited to the 5 most recent record-type entries. - New i18n key `console.commandPalette.recentRecords` (en + zh seeded; other locales fall back to `defaultValue: "Recently viewed"`). - b2d1704: feat(cmdk): record search across objects in the Command Palette + - New `useRecordSearch` hook in `@object-ui/react` debounces a query, fans out to `dataSource.find(name, { $search, $top })` across candidate objects, and aggregates hits. Race-safe via a monotonic runId; per-object 404s are @@ -9045,6 +9472,7 @@ Assistant… (try "系统里有多少个用户?")`). Three related fixes that all addressed the same UX: a user follows a URL shaped `/{object}/{recordId}` and sees a completely blank content area. + 1. **`useNavigationOverlay` produced the broken URL itself.** When middle-click / Cmd-click opened a gallery card in a new tab and no `onNavigate` was provided, the hook built `/{object}/{id}` — a URL @@ -9086,6 +9514,7 @@ Assistant… (try "系统里有多少个用户?")`). When `useRecordSearch` is mid-flight (debounced fetch across objects hasn't returned yet), the palette now surfaces a subtle visual: + - A small pulsing primary-coloured dot next to the **Records** group heading, so the user sees that more results may still appear. - A `Searching…` placeholder inside the empty state when the user has @@ -9098,6 +9527,7 @@ Assistant… (try "系统里有多少个用户?")`). `ConsoleToaster` now ships UX-positive defaults that match the Linear / Notion pattern users expect from an enterprise console: + - `position="top-right"` — keeps the user's primary work area (centre - bottom) unobstructed. - `closeButton` — every toast has an explicit X so users can dismiss @@ -9112,6 +9542,7 @@ Assistant… (try "系统里有多少个用户?")`). All of these are still overridable via `` props. - 5425608: CRM UX polish pass — calmer enterprise look across detail + kanban. + - **plugin-kanban**: column headers now use a 2px muted accent stripe with neutral foreground titles + a quiet grey count pill instead of full rainbow gradient + colored title + colored count. Pipeline boards @@ -9136,6 +9567,7 @@ Assistant… (try "系统里有多少个用户?")`). - 710fbe6: feat(app-shell): notification center animation polish InboxPopover now animates every signal that matters for "noticing": + - Bell button **bounces once** when total pressure increases (new notification or approval arrives). Tracks previous total via a ref so the very first render — when the server-side counts hydrate — @@ -9169,6 +9601,7 @@ pending approvals`) fade in instead of popping in. unknown @-tokens. 9 unit tests. `@object-ui/app-shell` `RecordDetailView` now: + 1. Serializes the resolved mention ids into `sys_comment.mentions` (previously hard-coded `'[]'`, so servers had no idea who was being pinged). @@ -9195,6 +9628,7 @@ pending approvals`) fade in instead of popping in. are merged in automatically when available. - 54e3dfb: Remove unused stub renderers from `@object-ui/app-shell`: + - `ObjectRenderer` / `ObjectRendererProps` - `DashboardRenderer` / `DashboardRendererProps` - `PageRenderer` / `PageRendererProps` @@ -9210,6 +9644,7 @@ pending approvals`) fade in instead of popping in. If you were importing one of the removed stubs (and somehow got past the "TODO" placeholder render), the real renderers ship from the respective plugin packages: + - Dashboard → `@object-ui/plugin-dashboard` (`DashboardRenderer`) - Page / Object / Form → `@object-ui/react` (`SchemaRenderer`) + `@object-ui/plugin-form` / `@object-ui/plugin-grid` etc. @@ -9273,6 +9708,7 @@ pending approvals`) fade in instead of popping in. - d1ec6a2: Fold inline-edit into the page-header overflow menu (HubSpot/Lightning pattern) and remove the orphan "Edit fields" toolbar row that previously floated between the tab strip and the first detail section. + - `@object-ui/app-shell` `RecordDetailView`: injects a new `sys_inline_edit` system action that appears in the ⋯ overflow menu and dispatches a `objectui:record:inline-edit-toggle` window CustomEvent (filtered by @@ -9289,6 +9725,7 @@ pending approvals`) fade in instead of popping in. in `page:accordion` / `page:tabs` items. - cf30cc2: Polish Lightning record detail page layout. + - `record:details` sections now render with Card chrome by default when a `title` is present, restoring visual grouping that was missing on pages like the opportunity detail page. - Section labels can be translated via the `{ns}.objects.{objectName}._sections.{name}.label` convention. Author each section with a stable `name` (e.g. `info`, `forecast`) and the renderer picks up the locale-specific label automatically. Falls back to the literal `label` when no translation exists. - The `page:header` action toolbar now collapses into a `⋯` overflow menu when more than two actions are present. The first business action stays inline; secondary system actions (Edit / Share / Delete) move into the menu, with destructive styling applied to Delete. @@ -9305,6 +9742,7 @@ pending approvals`) fade in instead of popping in. ### Patch Changes - d51a577: feat(platform): Discussion attachments + @mention directory + Reference Rail aside + - **Discussion attachments** — `RichTextCommentInput` now accepts an `extraSlot` and a `canSubmitEmpty` flag so hosts can mount the existing `CommentAttachment` composer beneath the editor without forking the toolbar. @@ -9324,6 +9762,7 @@ pending approvals`) fade in instead of popping in. - 1976691: Fix the drawer "Open as full page" (maximize) button on the record drawer which threw `TypeError: name.indexOf is not a function` and prevented navigation to the dedicated detail page. + - `@object-ui/app-shell` `ObjectView`: pass `objectDef.name` (string) — not the whole `objectDef` — into `viewLabel(...)` when computing the `originState.from.label` for both drawer-navigate and list-navigate @@ -9339,6 +9778,7 @@ pending approvals`) fade in instead of popping in. custom `Page`. Catalog-style objects (Product, Task) ship with the rail off by default; hub objects (Account, Opportunity, Contact, Case) keep it on. + - `RecordDetailView` now reads `(objectDef as any)?.detail?.hideReferenceRail` and `…?.hideRelatedTab` and threads them to `buildDefaultPageSchema`. - The Reference Rail renderer also accepts entries authored as either a @@ -9446,6 +9886,7 @@ pending approvals`) fade in instead of popping in. default detail view. They were missing cross-cutting affordances and shipped with English-only tab labels and heavy bordered section cards even when the host locale was Chinese. Track 1 closes the visible gap: + - **app-shell `RecordDetailView`**: the `assignedPage` branch now wears the same chrome as the default branch — lifecycle managed-by badge and presence avatars in the top-right, `MetadataPanel` debug panel, @@ -9518,6 +9959,7 @@ pending approvals`) fade in instead of popping in. page so the two renderers share state. - 74962b0: feat(detail): record:discussion schema component + flush accordion variant + - New `record:discussion` schema type lets authors place the record chatter feed anywhere in a custom Page schema. Wired through a shared `DiscussionContext` provider on the `assignedPage` branch @@ -9544,6 +9986,7 @@ pending approvals`) fade in instead of popping in. accordion, discussion slot) that custom Lightning pages already enjoy. Changes: + - `buildDefaultPageSchema` now emits `page:tabs.items` (correct shape for the renderer) rather than `tabs`. - `PageHeaderRenderer.resolvedTitle` honors `objectSchema.primaryField` @@ -9567,6 +10010,7 @@ pending approvals`) fade in instead of popping in. customize the header or one tab. **Slot menu (v1):** + - `header` — replaces `page:header` - `actions` — replaces the `record:quick_actions` action bar - `highlights` — replaces the chips + chevron path strip @@ -9594,6 +10038,7 @@ pending approvals`) fade in instead of popping in. ``` **API changes:** + - `PageSchema` (in `@object-ui/types`): adds `kind?: 'full' | 'slotted'` (default `'full'`) and `slots?: PageSlotMap`. - `usePageAssignment` (in `@object-ui/react`): result now exposes a @@ -9603,6 +10048,7 @@ pending approvals`) fade in instead of popping in. `options.slots` map that overrides individual regions at synthesis time. - 34b66bf: feat(detail): synthesize Related / Activity / History tabs + record:quick_actions header (Track 3 Phase G slice 4) + - `buildDefaultPageSchema` now accepts `headerActions`, `related`, `showActivity`, and `history` options. When provided, the synthesizer emits a `record:quick_actions` node after `page:header` and appends @@ -9631,6 +10077,7 @@ pending approvals`) fade in instead of popping in. their settings. The adapter now defaults to: + - `resource`: `sys_user_preference` - field shape: `(user_id, key, value)` instead of `(user_id, kind, payload)` - option name: **`key`** instead of `kind` @@ -9675,6 +10122,7 @@ pending approvals`) fade in instead of popping in. This patch injects gated system actions into `synthHeaderActions` for both the synth and slotted paths: + - `sys_edit` — visible when `affordances.edit`. Calls the existing `onEdit` prop, opening the same form modal as before. - `sys_share` — always visible. Uses `navigator.share` when available; @@ -9746,6 +10194,7 @@ pending approvals`) fade in instead of popping in. chrome on phones: title + 1 primary action, plus content. We were shipping ~5 rows of toolbars + chips + tabs above the data. This commit hides the desktop-only chrome at the `