diff --git a/.changeset/10202-studio-use-picklist.md b/.changeset/10202-studio-use-picklist.md deleted file mode 100644 index 51cb75a933..0000000000 --- a/.changeset/10202-studio-use-picklist.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -Studio reads shared picklists: a select field can use one, an object with a picklist-bound field saves again, and the picklist page is read-only (objectui#10202, the objectui half of objectstack#18164 phase 2). - -A shared picklist is one list of options that select fields on several objects name with `picklist: 'NAME'` instead of carrying their own (`@objectstack/spec` 17.6.0, `data/picklist.zod.ts`). The kind is package-owned: it is authored in a package, and the runtime takes no create, edit or delete for it. - -- **"Use picklist" in the select-field editor.** For a `select`, `radio`, `multiselect` or `checkboxes` field, an "Options from" picker lists the picklists the runtime serves, by name, beside "This field's own options". Choosing a picklist sets `picklist` and removes the field's `options`. A bound field shows the list's values read-only, and offers no inline options editor, because the spec refuses `picklist` together with `options`. Its default value is chosen from the list's values. Changing the field to a type that takes no options drops the binding. A stored name the served list no longer has is shown flagged as not found. If the list cannot be loaded, the picker says so and still offers the field's own options. -- **Saving an object with a picklist-bound field works again.** The runtime serves such a field with the options it resolved from the list and its extensions. The metadata-admin object designer and the Studio data page sent those back, and the save was refused with `422 INVALID_METADATA` whatever had been edited. Both now leave `options` out of every field that names a picklist. Every other field and key is sent as it was. The metadata-admin designer's live check now judges the body it sends, so it no longer reports "`picklist` and `options` cannot both be declared" on such an object. -- **The picklist page is read-only.** Its detail view shows the list's label, name, description and owning package, each option with its label and value, and the options other packages add to it (`picklistExtensions`), each under the package that declares it. It offers no create, edit or delete. The list page already offered no create for this kind. - -Nothing is added to the package entry: no export, prop, type member or language-pack key. The new copy lives in the metadata-admin designer's own string tables (en and zh). diff --git a/.changeset/11002-storage-usage-flag.md b/.changeset/11002-storage-usage-flag.md deleted file mode 100644 index 530ee875ea..0000000000 --- a/.changeset/11002-storage-usage-flag.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -The console asks `GET /api/v1/usage/storage` only when the runtime serves it (objectui#11002). On a self-hosted or open-source runtime, an environment admin's console used to request that cloud-only endpoint on every shell mount and log a 404 each time. Now neither the storage-capacity banner nor the read-rate report asks unless the runtime config's `features.storageUsage` is `true`. The cloud distribution sends that key exactly when it mounts the endpoint (cloud#2481), and every other runtime sends no key. - -**Clause-②: yes (widening)** — the exported `RuntimeFeatures` type gains one optional member, `storageUsage?: boolean`, so `AppShellRuntimeConfig.features` and the value `getRuntimeConfig()` returns carry it too. It is `false` by default and after any runtime-config payload whose `features.storageUsage` is not the literal `true` (absent, `false`, `'true'` and `1` all read as `false`). It is `true` only when the payload sends `true`. As with the other feature keys, a payload with no `features` object leaves the current value. Nothing else on the package entry changes: no export is added or removed, and no existing member changes type. - -**Behaviour change.** `ConsoleShell`'s two banners request the endpoint only when the viewer is the environment admin, as before, and `features.storageUsage` is on. Both read the same flag and still share one request. A cloud runtime older than the one that serves the key (cloud#2517) sends no key, so its admins see neither banner until that runtime is upgraded. - -Not published: the accessor the banners read, `isStorageUsageServed()`. Like its siblings `isMarketplaceEnabled()` and `isAiStudioEnabled()`, it ships inside `dist/` but is not exported from the package entry. `useStorageUsageReading` and `useReadRateReading` are unchanged and keep their `enabled = true` default. diff --git a/.changeset/11092-flow-label-reader.md b/.changeset/11092-flow-label-reader.md deleted file mode 100644 index d41aa3d33c..0000000000 --- a/.changeset/11092-flow-label-reader.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/console': patch ---- - -The screen-flow runner names the flow by its label, in the user's language (objectui#11092, the objectui half of objectstack#20318). - -Since `@objectstack/spec` 17.6.0 every answer that evaluated a flow carries the flow's authored label as `AutomationResult.flowLabel` (objectstack#20633). `FlowRunner` now resolves the flow's display name in this order: the active language's `flows.FLOW.label` from the app's translation bundle, then the served `flowLabel`, then the flow's API name. The bundle is the one the runner already reads for screen headings and field copy, and the lookup is the spec's own `translateFlow`. - -Where it shows: - -- **The runner header.** A line above the screen's heading names the flow. The heading is still the step's own title (or the `flowRunner.title` fallback), and it is still the dialog's accessible name. -- **The completion toast.** `Flow "{{flow}}" completed` names the flow by the resolved label instead of its API name. A flow's own `successMessage` still takes precedence. The message key and its translations are unchanged. - -All three places that open the runner pass the label through: a flow action on a list or toolbar, a flow action on a record page, and the developer Flow Runs page's Test Run panel. A resume answer that pauses on a further screen carries the label forward. - -Against a server older than objectstack#20633 no label is served, so the header line and the toast show the flow's API name, unless the app's bundle translates `flows.FLOW.label` for the active language. - -**Clause-②: yes (widening).** The exported `ScreenFlowState` type gains one optional member, `flowLabel`, typed by the contract as `Pick` of `AutomationResult`'s `flowLabel` (an optional string). `FlowRunnerProps.state` accepts it through that type. No prop, export or i18n key is added or removed, and no existing member changes type. diff --git a/.changeset/11095-kpi-tile-invalidation-pin.md b/.changeset/11095-kpi-tile-invalidation-pin.md deleted file mode 100644 index ea0853a999..0000000000 --- a/.changeset/11095-kpi-tile-invalidation-pin.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -No package released: tests and one doc comment, no behaviour change. `@object-ui/plugin-dashboard` gains a pin that a dataset-bound KPI tile (a `metric` widget with no dimensions) re-reads on the data-invalidation bus once its query's answer names the dataset's base object. The answer names it on every dataset query since objectstack-ai/objectstack#20644, which `@objectstack/spec` 17.6.0 declares as `AnalyticsResult.object`. The widget already subscribed on that key, so no executable code changed (objectui#11095). - -`@object-ui/app-shell`'s `refreshDashboardData` doc comment stops describing that gap as current. The comment is not inert text: `tsc` keeps it in the emitted `dist/views/DashboardView.js`, which is published. It sits above a non-exported function, so it is in no `.d.ts`. It changes no export, type or behaviour, so it releases nothing and ships with the next release of the group. diff --git a/.changeset/11170-node-slot-keys.md b/.changeset/11170-node-slot-keys.md deleted file mode 100644 index 7b858f2bd1..0000000000 --- a/.changeset/11170-node-slot-keys.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@object-ui/types": minor -"@object-ui/cli": minor -"@object-ui/core": minor -"@object-ui/sdui-parser": minor -"@object-ui/components": minor ---- - -**Clause-②: yes (narrowing)** - -One declaration of the per-type NODE SLOTS — the keys other than `children` through which a renderer hands authored nodes back to `SchemaRenderer` — and three readers that walk it instead of stopping at `children` (objectui#11170, the follow-up PR #11126's Acceptance notes filed). - -New on `@object-ui/types`, beside `BaseSchema.children`: `NODE_SLOT_DECLARATIONS` (one row per renderer, under every registry spelling that resolves to it), `nodeSlotsFor(type)`, `nodeSlotPathSegments(path)` and `nodeSlotValues(node, path)`, with the types `NodeSlotDeclaration`, `NodeSlotRow`, `NodeSlotSegment` and `NodeSlotValue`. A position is spelled as a key path — `trigger`, `items[].content`, `regions[].components`, `items[]`, `report.sections[].content` — and the value at its end is one node or a list of nodes. The `page:*` rows are `@objectstack/spec`'s `pageComponentSlotPositions()` placed on the type whose renderer reads each position, pinned against that export in both directions; every other row is objectui's own, pinned against the live renderer. `body` stays retired as the generic child-list key (objectui#6771): it appears only on the four `page:*` types whose renderer still paints it for stored documents, marked `retired`. - -Accept sets that narrow, each reader FROM → TO: - -- `@object-ui/cli` — `objectui check`'s unevaluated-expression refusal (`findUnbindableTextExpressions`). FROM: the document root and every node its `children` hold. TO: those, and every node under a slot its type declares — so a `${…}` on `title` / `label` / `value` / `description` of a node under a dialog's `content`, a tab item's `content`, a page's `regions[].components`, a carousel item, a detail view's `tabs[].content` is now refused with the slot path (`items → 0 → content → value`). The false-refusal rows of PR #11126's ablation 2 stay green: a form's `fields[]`, a grid's `columns[]` and `{ "type": "multiple" }` are not slots. Measured over this repository's own JSON corpus and docs fences: no new finding. -- `@object-ui/core` — `validateSchema`. FROM: `validateChildren` recursed through `children` only. TO: it also recurses through the declared slots, so an invalid node under one (a retired `crud` spelling under `dialog.content`, an `INVALID_SCHEMA` member) is reported with its own path, spelled as `schema.items[0].content`. Measured over the same corpus: no new finding. -- `@object-ui/sdui-parser` — `validateTree`. FROM: the walk descended `children` alone, and a manifest entry carried no slot. TO: `ManifestComponent` gains `slots?: readonly string[]`, `manifestFromConfigs` gains `opts.slotsFor` (hand it `nodeSlotsFor`) and projects each entry's non-retired positions, and `validateTree` descends them — an unknown component, an unknown or mis-typed prop or an illegal enum under a slot now draws its diagnostic. A manifest built without the option serialises byte-identically and keeps the `children`-only reach. The `RETIRED_CHILD_LIST_KEY` refusals are unchanged. -- `@object-ui/components` — the `kind:'html'` page's compile manifest (`getJsxManifest`) is built with `slotsFor`, so an html-tier page whose slot-held node fails validation now fails to compile the way one under `children` does. Narrowing: a page that compiled with an unknown tag under a `dialog`'s `content` no longer does. - -Docs: `content/docs/utilities/cli.mdx`'s "Component nodes only" rule, the gate's own docblock, `validateChildren`'s comment and the parser's header now say the walk follows `children` and the declared slots; the declaration's header is where the slot list is explained. diff --git a/.changeset/11266-subforms-columns-mirror.md b/.changeset/11266-subforms-columns-mirror.md deleted file mode 100644 index 0b64d2d845..0000000000 --- a/.changeset/11266-subforms-columns-mirror.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/types': minor ---- - -A form view's `subforms[].columns` entry is judged by `@objectstack/spec`'s `InlineGridColumnSchema` now, by reference, so `objectui validate` and `os validate` give one verdict on a column (objectui#11266). - -BREAKING (`@object-ui/types`): the accept set of the tolerant face narrows. (The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.) - -`@objectstack/spec` 17.6.0 holds `FormViewSchema.subforms[].columns` to its closed inline grid column schema (objectstack-ai/objectstack#20927). The `object-form` mirror still read `z.array(z.any())` there, so `objectui validate` accepted columns that `os validate` refuses. - -What each face does now: - -- **zod (`@object-ui/types/zod`).** NARROWS on the tolerant face (`safeValidateSchema`, which `objectui validate` runs) and on the strict authoring face, wherever `subforms` is read: the object-view `form` slot and the `object-form` mirror. A column with an undeclared key is refused at the column, with one `unrecognized_keys` issue naming the key. A column that declares `type: 'currency'` and carries `scale` is refused at that `scale`, in the spec's own words. A bare field-name string is refused at the column with `invalid_type`. Each verdict is the spec schema's, because the column is handed to it. -- **TypeScript.** A `subforms[].columns` entry on `ObjectFormSchema` (and so on `ObjectViewSchema['form']`) is `InlineGridColumn` from `@objectstack/spec/data`, by reference, where it was `any`. A string column no longer compiles, and neither does an object literal with an undeclared column key. - -**Migration.** - -- FROM `columns: ['product', 'quantity']` → TO `columns: [{ name: 'product' }, { name: 'quantity' }]` -- FROM a column carrying a key `InlineGridColumn` does not declare → TO the same column without that key. A column that declares no `type` takes its label, type and the rest from the child object's field. -- FROM `{ name: 'amount', type: 'currency', scale: 2 }` → TO `{ name: 'amount', type: 'currency' }`. A currency amount's decimal places come from its currency's minor unit, not from the column. - -Not refused here: a `scale` on a column that declares no `type` (`{ name: 'amount', scale: 2 }`) when its child field is a currency. Seeing that takes the child object's fields, which are not in the document the validator judges. `defineStack` refuses it at publish, and the master-detail form reports it at render. - -The parse output is the spec schema's too: a column `readonlyWhen` or `requiredWhen` written as a string comes back from `safeValidateSchema` as the spec's `{ dialect: 'cel', source }` envelope. The document you pass in is not changed. - -**Clause-②: yes (narrowing)**: a `subforms[].columns` entry that is not a valid `InlineGridColumn` used to parse with its value kept, and is now refused at the column. diff --git a/.changeset/11336-served-embedded-listviews.md b/.changeset/11336-served-embedded-listviews.md deleted file mode 100644 index a20bdf1956..0000000000 --- a/.changeset/11336-served-embedded-listviews.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -An object page's view tab, and the breadcrumb that names the open view, draw the label of a view the object document embeds as the server served it (objectui#11336). - -Since `@objectstack/spec` 17.6.0 (objectstack#21072) the server translates the `listViews` an object document embeds, from `objects.OBJECT._views.KEY` and with a published edit kept over the packaged catalog, as it already did for `/meta/view` documents. The console still ran its own catalog over those labels a second time, so a published edit to such a view drew as the packaged string. A view counts as served now when its key is in the served `/meta/object` document's own `listViews`, read before the console merges view documents into it, as well as when a `/meta/view` document carries its name. Its label is drawn as given. A view only the console derives, such as a stack container's expansion, is still named by the client catalog. - -Against a server older than that release, a view the object document embeds arrives untranslated and is now drawn as authored. - -**Clause-②: no.** Nothing on the package entry changes. The predicate (`isServedView`) and its hook (`useServedViewItems`) are not exported from `@object-ui/app-shell`. diff --git a/.changeset/11340-book-tree-prefilter.md b/.changeset/11340-book-tree-prefilter.md deleted file mode 100644 index 5697452e6a..0000000000 --- a/.changeset/11340-book-tree-prefilter.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The docs portal's book sidebar now shows what the book resolver answers, with nothing narrowing the docs in front of it (objectui#11340, ADR-0046 §6.4). The resolver decides book membership. Since objectstack#20980 (`@objectstack/spec` 17.6.0), the framework's `resolveBookTree` keeps a doc in a book's synthetic Uncategorized group only when the doc belongs to one of the book's packages: the book's own, or a group's `package`. The console's port of that resolver now scopes its Uncategorized group the same way. The console's pre-filter, which dropped other packages' docs before resolving, is gone. - -What a reader sees: - -- **Unchanged: another package's ungrouped doc** stays out of a book's Uncategorized group, and the book's own ungrouped docs stay in it. -- **Unchanged: a group's `package` (corner 1).** That package's unplaced docs are listed under the book's Uncategorized group, as the resolver answers. The portal already listed them, because the pre-filter kept every package the book draws from. -- **Changed: a doc of another package pinned by a group's `pages` (corner 2)** is still listed where the pin places it, and now under its own label. Before, the pre-filter had dropped the doc, so the entry showed the doc's bare name. -- **Changed: a book with no package of its own whose group names a `package`.** The groups that name no package now match docs from every package by `include`, as the resolver answers. Before, they matched only the docs of the packages the groups name. -- **Changed: a doc of another package whose `group` names a `pages` group with no `...`.** It is no longer listed under the book's Uncategorized group. That group collects no doc by its key, and the resolver keeps another package's unplaced doc out of Uncategorized. - -The book cards' doc counts and the book a doc opens in follow the same answer. Nothing else changes: no export, prop, type member or i18n key is added or removed. diff --git a/.changeset/11345-inline-grid-columns-spec-rule.md b/.changeset/11345-inline-grid-columns-spec-rule.md deleted file mode 100644 index 36fe4feb37..0000000000 --- a/.changeset/11345-inline-grid-columns-spec-rule.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -`deriveColumns`, the default columns of a master-detail inline grid whose author listed none, now takes which columns it draws, their order and which of them are `defaultHidden` from `@objectstack/spec`'s `deriveInlineGridColumns`, and its visible budget from the spec's `DEFAULT_MAX_INLINE_GRID_COLUMNS` (objectui#11345). The rule is the spec's now, so objectstack's `field-no-consumers` lint credits exactly the columns this grid draws. - -The output does not change. The signature is the same, and each column's label, cell type, options, lookup target, conditional rules and computed expression are still built from the child field here, including a plain text column for a field whose definition is falsy. The module-level `DEFAULT_MAX_INLINE_COLUMNS` constant, which the package entry never exported, is removed. - -`@object-ui/plugin-form` raises its `@objectstack/spec` floor from `^17.0.0` to `^17.6.0`, because its published entry now imports `deriveInlineGridColumns` and `DEFAULT_MAX_INLINE_GRID_COLUMNS`, which the spec first exports in 17.6.0. diff --git a/.changeset/11383-record-chrome-image.md b/.changeset/11383-record-chrome-image.md deleted file mode 100644 index ece815f8e0..0000000000 --- a/.changeset/11383-record-chrome-image.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/components': minor ---- - -The record page header draws the record's picture beside its title, from the field the object names in its object-level `imageField` (`@objectstack/spec` 17.6.0, objectstack#21182) (objectui#11383). The picture is the served row's value of that `image` or `avatar` field: the expanded `{ url }`, a bare file id (drawn from the stable download path), a URL string, or the first drawable entry of a list. An object that declares no `imageField`, an empty value, or a field the reader may not see draws nothing, with no initials or placeholder; `recordChrome: false` keeps the bare header. The declaration is the only channel: no `page:header` prop is read and no field is read by a conventional name such as `logo` or `avatar`. diff --git a/.changeset/11396-master-detail-details-entry.md b/.changeset/11396-master-detail-details-entry.md deleted file mode 100644 index 6b6b4ef2d1..0000000000 --- a/.changeset/11396-master-detail-details-entry.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -No release. `@object-ui/plugin-form`'s exported `MasterDetailDetailConfig` is now derived from `@objectstack/spec`'s `details` entry by reference (`ComponentPropsMap['object-master-detail-form'].details[]`, a closed shape since 17.6.0) instead of restating it by hand, minus the one member the renderer does not read (`sortField`: retired here by objectui#11070, and on objectstack `main` by objectstack-ai/objectstack#21589, unreleased after 17.6.0; the subtraction holds on both spec shapes). Member for member the type is the same as before — the same keys, the same member types, `columns` the same inline grid column — so what a caller may write does not move. The `object-master-detail-form` registration declares `of: 'object'` and a description for `details`; tests and the console's parity prose follow (objectui#11396). diff --git a/.changeset/11428-inline-row-form-spec-rule.md b/.changeset/11428-inline-row-form-spec-rule.md deleted file mode 100644 index 3742ba9313..0000000000 --- a/.changeset/11428-inline-row-form-spec-rule.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -The per-row expand form of an inline master-detail collection now takes both of its rules from `@objectstack/spec` (objectui#11428), as the collection's grid columns already do. `deriveFormFields`, the form's fields when its author listed none, returns the spec's `deriveInlineRowFormFields` answer, and `MasterDetailForm` offers a row the form when the spec's `isInlineRowFormOffered` says so. The rules are the spec's now, so objectstack's `field-no-consumers` lint credits exactly the fields this form draws. - -The output does not change. `deriveFormFields` keeps its signature and returns the same names in the same order, and a row is offered the form exactly when it was before: always in `form` mode, and in `grid` mode only when the form has more fields than the grid has columns. How each named field renders is still decided here. - -`@object-ui/plugin-form` raises its `@objectstack/spec` floor from `^17.6.0` to `^17.7.0`, because its published entry now imports `deriveInlineRowFormFields` and `isInlineRowFormOffered`, which the spec first exports in 17.7.0. diff --git a/.changeset/11439-bound-action-copy.md b/.changeset/11439-bound-action-copy.md deleted file mode 100644 index 9a088fe05f..0000000000 --- a/.changeset/11439-bound-action-copy.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/i18n': patch -'@object-ui/react': patch ---- - -An action's translated copy is now read from ONE bundle node, chosen by the object the action belongs to, the same way `@objectstack/spec` 17.7.0 reads it on the server (objectui#11439). That object is the action's declared `objectName`, else the object whose `actions` embed it. - -- An action that belongs to an object reads `objects.OBJECT._actions.ACTION.*` only. Copy for it filed under `globalActions.ACTION` no longer applies: its label, confirm text, success message, outcome messages, description, parameters and result dialog show the authored text instead, as they already did everywhere the server translates. Move that copy to `objects.OBJECT._actions.ACTION`, which is where `os validate` asks for it. -- An action with no object (no `objectName`, not embedded in an object) still reads `globalActions.ACTION.*`. -- `useActionTextLocalizer` keys on the action's declared `objectName` before the object its caller passes, so an action declared on one object reads that object's copy wherever it is drawn. - -`useObjectLabel()` and `useActionTextLocalizer()` keep their signatures; no input, export or translation key is added. diff --git a/.changeset/11546-readonly-canvas-select.md b/.changeset/11546-readonly-canvas-select.md deleted file mode 100644 index a5c0199732..0000000000 --- a/.changeset/11546-readonly-canvas-select.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -On a read-only package, a click on a flow canvas node in Studio Automations selects the node and opens its inspector read-only again (objectui#11546). - -On a read-only canvas the node's press stopped short of claiming the event, so it reached the -canvas background. The background cleared the selection and took the pointer, and the browser -then fired the click at the canvas instead of the node, so the inspector rail kept its empty -state and a packaged flow's node configuration could not be read. - -A press on a node in the designer now belongs to the node on a read-only canvas too. The click -selects the node and the inspector opens with every input disabled, as objectui#11124 set out. -Only the drag is withheld: a press-and-drag on a read-only node moves nothing and writes nothing. -The editable designer drags and selects as before, and the Delete key still deletes only on an -editable canvas. Outside design mode, where a node click selects nothing, a press on a node still -pans the canvas. diff --git a/.changeset/11564-flex-bag-children-list.md b/.changeset/11564-flex-bag-children-list.md deleted file mode 100644 index a509c73404..0000000000 --- a/.changeset/11564-flex-bag-children-list.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`FlexBlockNode`, the TypeScript type of an authored `flex` node, types its bag's child list as nodes: `properties.children` is `SchemaNode | SchemaNode[]`, where it was `unknown[] | SchemaNode` (objectui#11564). - -**Clause-②: yes (narrowing)**, TypeScript authoring face only, shipped as `minor` per this repository's version policy. The list is the form a `flex` node is authored in (`{ type: 'flex', properties: { children: [ … ] } }`), and until now each entry was `unknown`, while a single child was already judged as a node. Each list entry is now judged as a node too, against its own `type`: - -- an entry whose `type` no declaration names, or an object with no `type`, no longer compiles; -- an entry that is itself a list no longer compiles; -- a misspelled key on a node type without an index signature (the spec-derived nodes, such as `element:text`, and a closed `CustomNodeRegistry` entry) no longer compiles. Node types that extend `BaseSchema` keep its index signature until objectui#8347 removes it, so a misspelled key on one of them still compiles, in the list exactly as in a single child; -- a primitive entry (a string, a number, `null`) still compiles, as in every node slot. - -The type is the flat `FlexSchema` mirror's own `children` member, by reference, which is also the member `FlexLayoutProps` declares. Every other member of `FlexBlockNode` and of its bag is still read off the `FlexBlockSchema` arm. - -What does NOT move: every zod face and every runtime path. `FlexBlockSchema` keeps `z.array(z.unknown())` for the list, because `@objectstack/spec`'s page walk already judges each entry there, once, at its real path, on the tolerant and the strict face (objectui#11223). The one entry kind where the faces now differ is a nested list, which the walk passes through unvisited and the TypeScript face refuses; `@object-ui/types`' mirror-parity ledger records the divergence. - -**Migration.** Give each list entry its declared node type (or `DeclaredNode`), declare a custom type in `CustomNodeRegistry`, and flatten a nested list into the one list. - -⚠️ **Dated note, 2026-10-04 — `BaseSchema` loses its index signature — objectui#8347.** At this change, "Node types that extend `BaseSchema` keep its index signature until objectui#8347 removes it, so a misspelled key on one of them still compiles" held. Later in this same release objectui#8347 removed that signature, so a misspelled key on a node type that extends `BaseSchema` no longer compiles either, in the list exactly as in a single child. `.changeset/8347-baseschema-closed-face.md` states what ships. The rest of this entry is kept as the reading of this change. diff --git a/.changeset/11569-line-items-child-object-optional.md b/.changeset/11569-line-items-child-object-optional.md deleted file mode 100644 index a56ee25ec7..0000000000 --- a/.changeset/11569-line-items-child-object-optional.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/plugin-form': minor ---- - -The `record:line_items` registration no longer declares `childObject` required, -so the page compile accepts a node whose `dataSource` binding names the child -object (objectui#11569). - -`@objectstack/spec`'s `record:line_items` row leaves `childObject` optional, -because the node's `dataSource` binding can supply it, and the renderer agrees: -`dataSource.object` lands on `childObject` before the panel reads the node. The -registration still declared `required: true`, and the page compile reads the -registration, so a bound node with no `childObject` of its own was refused with -`missing-required-prop` and the save failed. - -**Clause-②: yes (widening)** — a `record:line_items` node that names its child object -through `dataSource.object` and sets no `childObject` now compiles and saves. A -node that names its child object in neither place also compiles now: the panel -shows its configuration hint naming `childObject` and loads nothing, as it did -before when such a node reached it. `relationshipField` and `columns` are still -required. The published `childObject` input now carries a description that says -the binding can supply it. diff --git a/.changeset/11573-any-component-emit-headroom.md b/.changeset/11573-any-component-emit-headroom.md deleted file mode 100644 index 3c34851bbe..0000000000 --- a/.changeset/11573-any-component-emit-headroom.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/types': patch ---- - -fix(types): `AnyComponentSchema`'s declaration prints every category union by name, so `@object-ui/types` no longer sits at the edge of TypeScript's serialization ceiling (objectui#11573) - -`tsc` prints an inferred type in full wherever a declaration uses it, and past -its serialization ceiling it refuses with TS7056 ("The inferred type of this -node exceeds the maximum length the compiler will serialize"). `@object-ui/types` -then emits no declarations, and every consumer of `@object-ui/types/zod` fails -with TS7016. `AnyComponentSchema` listed every category union inline, so its -declaration printed the sum of all of them: measured with TypeScript's own -counter, it read under one percent short of the ceiling against -`@objectstack/spec` built from objectstack `main`, and the Spec Main Shape Gate -had already gone red on it once. - -Each of the sixteen category unions `AnyComponentSchema` lists now has a named -type in its own module — an interface that extends the union's inferred type -and adds no member (`LayoutZodType`, `PublicBlockComponentZodType` and their -siblings) — and `AnyComponentSchema`'s declaration prints a reference to each -instead of its body. - -What changes for a consumer is the printed `.d.ts` TEXT of existing exports, -and nothing else: `AnyComponentSchema` and the sixteen unions are the same -values, and every type read through `z.input` / `z.output` is unchanged. The -new names are exported from their own modules, which is what lets the -declaration reference them; they are not added to the `@object-ui/types/zod` -entry, so nothing new is importable. - -`packages/types/src/__tests__/any-component-emit-headroom-11573.test.ts` reads -the declaration's size with the compiler's own counter, fails when it crosses a -tenth of the ceiling below it, and lists every member as named or inline. diff --git a/.changeset/11577-textarea-read-breaks.md b/.changeset/11577-textarea-read-breaks.md deleted file mode 100644 index a8a182c385..0000000000 --- a/.changeset/11577-textarea-read-breaks.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -fix(plugin-detail): `record:details` read mode shows a `textarea` value with its line breaks (objectui#11577) - -A multi-line `textarea` value, such as a note with a blank line in it, read as one line on the record page: the details body drew every `textarea` value with the grid's one-line cell, which folds line breaks into spaces and cuts a long value off with an ellipsis. The record form's read-only field and, since objectui#11562, the inline editor already kept the breaks. - -The details body now draws a `textarea` value with the fields package's own read display, the read-only branch of `TextAreaField`, which is the display the record form uses. Line breaks, blank lines and a trailing newline show as stored. A single-line value reads as the same line; a long one now wraps at the column width instead of ending in an ellipsis. Other field types, plain text included, render exactly as before, and grid cells keep their one-line display. diff --git a/.changeset/11581-one-view-builder.md b/.changeset/11581-one-view-builder.md deleted file mode 100644 index b6af894c4a..0000000000 --- a/.changeset/11581-one-view-builder.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -"Save as view" now saves a Kanban view the platform accepts, and both Create View doors save the same view from the same dialog choices (objectui#11581). - -The Create View dialog persists a new view through two doors: "Save as view" on the object data -page, and Create View on the object page (the view tab bar's add button, and the view-config -panel's create mode). The two assembled the saved view separately, and only the object page's -door copied the view's columns into the type blocks that carry a field list of their own. So: - -- a Kanban saved through "Save as view" had no `kanban.columns`, which `@objectstack/spec` - requires ("Fields to show on cards"), and the platform's view write door refused it; -- a Gallery saved through "Save as view" had no `gallery.visibleFields`, so its cards showed - nothing under the title, while the same choices on the object page showed the view's columns. - -Both doors now build the view through one builder. Each door still supplies its own columns (the -data page's current columns, or the object's default business columns), and "Save as view" still -folds the page's URL conditions into the view's `filter`. The builder writes those columns into -`kanban.columns`, and into `gallery.visibleFields` when the gallery declares none. A view created -on the object page is saved exactly as before. diff --git a/.changeset/11583-refused-writes-said.md b/.changeset/11583-refused-writes-said.md deleted file mode 100644 index 642bd69e61..0000000000 --- a/.changeset/11583-refused-writes-said.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -A refused save, pin, reorder, view setting, report save, publish or discard in the console is now said to the user, and the view-config panel no longer reports a refused save as saved (objectui#11583). - -objectui#11578 made the two Create View doors say a refused save. The console's other metadata -writes on the object page, the report page and the draft bar still caught a refusal with a -console line and nothing else, so a permission refusal or a spec refusal looked like a saved -change: - -- the view-config panel's Save on an existing view; -- pinning or unpinning a view, and reordering views in "Manage views"; -- a toolbar setting on a list view (density, sort, columns, hidden fields), when the server - refuses it (the console's own permission check already said its refusal, and still does); -- the report editor's Save; -- Publish and Discard draft on the draft bar of those two editors. - -Each now raises the refusal through the console's error toast, with the save door's own -message: the field-anchored issues of a validation refusal, one per line, or the refusal's text. -The draft bar's toasts lead with "Publish failed" or "Discard failed", two new strings in all -ten language packs; the others lead with "Failed to save". Set as default, which already raised -an untranslated "Failed to set default view" with no reason, now does the same. - -The report editor waits for its save. It closes once the report is saved; a refused save leaves -it open with the edit in place, so Save can be pressed again (it used to close at once, and -reopening it showed the stored report). Save is disabled, and the editor read-only, while the -save is in flight. - -The view-config panel waits for the save before it reports the edit as saved. A refused save -leaves the panel dirty, so Save stays enabled for a retry, and the "unpublished changes" -indicator is not raised for a draft that was never written. Save is disabled while the save is -in flight. `ViewConfigPanel`'s `onSave` accepts any return, as it did when it was typed `void`: -the panel awaits it, and `false` (returned, or resolved by a promise) or a rejection means the -save was refused; anything else, nothing included, is read as saved. diff --git a/.changeset/11588-summary-hints-field-only.md b/.changeset/11588-summary-hints-field-only.md deleted file mode 100644 index 41caf8086e..0000000000 --- a/.changeset/11588-summary-hints-field-only.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -The `object-grid` summary footer reads `currency`, `defaultCurrency`, -`precision` and `scale` from the object field only. A column that carries one -of these keys no longer changes the footer (objectui#11588). - -`ListColumnSchema` (`@objectstack/spec/ui`) is a strict object, and it declares -none of the four keys. A view that authors one on a column is refused at publish -with `unrecognized_keys`. `useColumnSummary` read them anyway, and the column's -value won over the field's, so a column `currency` re-coded the total and a -column `scale` re-sized it. That read is retired rather than declared upstream, -and the footer now takes these hints the way it already took `currencyConfig` -and `max`, and the way the list cell above it reads them. The census behind the -ruling found no view that writes one of these keys on a grid column. - -**Behaviour change.** A grid handed a column that carries `currency`, -`defaultCurrency`, `precision` or `scale` (which validation refuses) now formats -its footer from the object field's definition, falling back to the tenant -currency as before. A column's declared `type` still decides the footer's unit, -and a grid whose columns carry none of the four keys is unchanged. The exported -`useColumnSummary` signature does not change. Its `fieldMetadata` argument is -where these hints go. diff --git a/.changeset/11591-org-flows-copy.md b/.changeset/11591-org-flows-copy.md deleted file mode 100644 index cacd51d0b6..0000000000 --- a/.changeset/11591-org-flows-copy.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -Studio's "Organization flows" page no longer says its drafts publish atomically, and a deep link to a flow that is not on the page no longer says no metadata designers are registered (objectui#11591). - -On the package-less page (`/studio/~org/automations`), the pending-changes sheet read "Publishing releases the 1 pending draft of this package atomically." That page has no package, and its Publish promotes each draft by itself: a draft that fails stays pending while the others go live. The sheet now says so there, through a new `preview.changes.confirmNoteSeparate` plural family in all ten language packs. A package's sheet keeps its sentence unchanged. `DraftChangesPanel` picks the sentence from its `packageId` prop: with a package, the atomic sentence; without one, the per-draft sentence. - -On the Automations pillar, the canvas chip read the designer registry for the open flow's type. With no flow open (a deep link naming a flow the list does not hold, or an empty list), it found none and showed "No metadata designers are registered in this session…" beside the right message, on a page whose flow designer is registered. The chip now reads the registry for the pillar's own type, `flow`, as the configuration panel beside it already did. The notice still shows when no designer is registered, whether or not a flow is open. diff --git a/.changeset/11598-dashboard-slot-entry-reads.md b/.changeset/11598-dashboard-slot-entry-reads.md deleted file mode 100644 index ca09124af2..0000000000 --- a/.changeset/11598-dashboard-slot-entry-reads.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-designer': patch -'@object-ui/types': patch ---- - -The dashboard surfaces read every `widgets[]` entry without leaning on `BaseSchema`'s index signature: a widget key is read on the widget arm alone, and the `chart` node is built as a private hand-off type (objectui#11598). - -**N1, the `chart` producers.** `DashboardRenderer` and `DashboardGridLayout` build a `chart` node for a series widget bound to inline rows, and compose two render keys onto it: the dashboard palette (`colors`) and `isAnimationActive: false`, the deterministic first paint inside the grid (#2756). `ChartSchema` declares neither key, on either face, and the chart renderer reads both. Each producer now checks its literal against a hand-off type private to this package, `ChartSchema` plus those two keys, and hands the node on with no cast; the type is not exported, and the keys stay off `@object-ui/types`, because the strict authoring face refuses both on an authored `chart` node. Nothing drawn changes. - -**N2, widget keys on the slot entry.** An entry of `widgets[]` is a widget or a component node placed in the slot (a `metric-card`), and only the widget declares the widget keys (`dataset`, `options`, `chartConfig`, `filter`, `component`, `colorVariant`, `values`, `dimensions`, …). Every read of one now narrows the entry to the widget first, on `DashboardRenderer`, `DashboardGridLayout`, `DashboardWithConfig` and `DashboardEditor`. What changes is confined to a `metric-card` entry that carries a widget key, which `@object-ui/types/zod`'s strict face refuses and only the tolerant face accepts: - -- a `metric-card` carrying `dataset` draws its card; it used to draw the dataset tile in the card's place, on both dashboard surfaces; -- a `metric-card` carrying `options` draws its own keys; `options` used to be spread over them, so `options.value` replaced `value`; -- a `metric-card` carrying a `component` draws its card; the envelope's node used to be drawn instead. - -A document the strict face accepts draws exactly as before. `@object-ui/types`' docblocks on `DASHBOARD_COMPONENT_WIDGET_TYPES` and the Zod widget vocabulary, which described the dataset tile in the card's place and the `options` spread as live, were corrected to match; no type in it changes. In `DashboardEditor`, a `metric-card` entry is no longer offered the Color Variant select: the card declares no `colorVariant`, `MetricCard` draws nothing from it, and a pick stored a key publish refuses. A widget is offered it as before. diff --git a/.changeset/11601-ref-dataset-widget.md b/.changeset/11601-ref-dataset-widget.md deleted file mode 100644 index 94ad2ff040..0000000000 --- a/.changeset/11601-ref-dataset-widget.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -A Studio form field that declares the `ref:dataset` widget now renders a dataset picker instead of the JSON editor fallback (objectui#11601). - -The metadata forms had no renderer for `ref:dataset`, so a field declaring it fell back to a raw -JSON box with a "falling back to JSON" note. The new widget offers the analytics dataset catalog, -showing each dataset's label beside its name and its description after it, and writes the dataset -name. It works for a field in a form section and for a column of a repeater row, in both the grid -and the card layouts. A stored dataset the catalog does not list stays visible and is marked -"(not found)". While the catalog is loading the field shows the stored name, and if the catalog -fails to load the field shows the load-failure notice beside a text box that stays editable. A -form whose host does not supply a catalog shows a plain text input for the dataset name. - -The report inspector now supplies its dataset catalog to the report's form. A joined report's -block row picks up the dataset picker once the spec's "Joined blocks" row declares `ref:dataset` -(objectstack#21714). Until objectui is built against a spec that carries that declaration, a -block's `dataset` is still entered as free text. - -The dataset option list that the report and dashboard-widget inspectors already show is unchanged. - -`SchemaForm`'s `widgetContext` prop gains one optional member, `datasets`, the catalog the new widget -reads. It is additive: a host that passes no `datasets` renders exactly as before, except that a -`ref:dataset` field now shows the text input instead of the JSON fallback. diff --git a/.changeset/11605-i18n-view-no-object.md b/.changeset/11605-i18n-view-no-object.md deleted file mode 100644 index ea28b0f885..0000000000 --- a/.changeset/11605-i18n-view-no-object.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/i18n': minor ---- - -All ten locale packs gain `view.noObject`, the hint an object-bound block shows -when its node names its object in neither place (objectui#11605). - -**Clause-②: yes (widening)** — a new key, `view.noObject`, in every pack. English -reads "No object named: set {{property}} or dataSource.object.", the wording of -`element.number.noObject` with the property as a hole; each translation is that -key's own translation with the same hole. `{{property}}` is the block's object -key, interpolated and never translated. No existing key changes. diff --git a/.changeset/11605-plugin-charts-objectname-optional.md b/.changeset/11605-plugin-charts-objectname-optional.md deleted file mode 100644 index 77eb3c0afa..0000000000 --- a/.changeset/11605-plugin-charts-objectname-optional.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/plugin-charts': minor ---- - -The `object-chart` and `view:chart` registrations no longer declare `objectName` -required, so the page compile accepts a node whose `dataSource` binding names the -object, and a chart that names its object in neither place shows a hint instead -of an empty frame (objectui#11605). - -`object-chart` has no spec row. The binding doc says a node bound by -`dataSource.object` needs no `objectName` of its own, and the renderer agrees: -`dataSource.object` lands on `objectName` before the chart reads the node. The -registrations still declared `required: true`, and the page compile reads them, -so a bound chart with no `objectName` of its own was refused with -`missing-required-prop` and the save failed. - -**Clause-②: yes (widening)** — an `object-chart` (or `view:chart`) node that -names its object through `dataSource.object` and sets no `objectName` now -compiles and saves. A node that names its object in neither place also compiles -now, and the chart shows "No object named: set objectName or dataSource.object." -where it used to draw an empty frame with no message. A chart with inline -`data`, a `dataset` or a `bind` path shows no hint and renders as before. The -published `objectName` inputs now carry a description that says the binding can -supply them. diff --git a/.changeset/11605-plugin-dashboard-objectname-optional.md b/.changeset/11605-plugin-dashboard-objectname-optional.md deleted file mode 100644 index 99b04d4df2..0000000000 --- a/.changeset/11605-plugin-dashboard-objectname-optional.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/plugin-dashboard': minor ---- - -The `object-metric` and `object-pivot` registrations no longer declare -`objectName` required, so the page compile accepts a node whose `dataSource` -binding names the object, and a node that names its object in neither place -shows a hint instead of a value or an empty table (objectui#11605). - -`@objectstack/spec`'s `object-metric` row leaves `objectName` optional, because -the node's `dataSource` binding can supply it; `object-pivot` has no spec row, -and the binding doc says a bound node needs no `objectName` of its own. Both -renderers agree: `dataSource.object` lands on `objectName` before the block -reads the node. The registrations still declared `required: true`, and the page -compile reads them, so a bound node with no `objectName` of its own was refused -with `missing-required-prop` and the save failed. - -**Clause-②: yes (widening)** — an `object-metric` or `object-pivot` node that -names its object through `dataSource.object` and sets no `objectName` now -compiles and saves. A node that names its object in neither place also compiles -now, and shows "No object named: set objectName or dataSource.object." where the -metric used to draw a bare dash and the pivot an empty state saying its query -returned no records. A metric with an authored `fallbackValue`, and a pivot with -inline `data` or a `bind` path, show no hint and render as before. -`object-data-table` is unchanged: it reads no binding, so its `objectName` stays -required. The published `objectName` inputs now carry a description that says -the binding can supply them. diff --git a/.changeset/11605-plugin-form-objectname-optional.md b/.changeset/11605-plugin-form-objectname-optional.md deleted file mode 100644 index 1ae0ac887f..0000000000 --- a/.changeset/11605-plugin-form-objectname-optional.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/plugin-form': minor ---- - -The `object-form`, `view:form`, `embeddable-form` and -`object-master-detail-form` registrations no longer declare `objectName` -required, so the page compile accepts a node whose `dataSource` binding names the -object, and a form that names its object in neither place shows a hint instead of -a form with no fields (objectui#11605). - -`@objectstack/spec`'s `object-form` and `object-master-detail-form` rows leave -`objectName` optional, because the node's `dataSource` binding can supply it; -`embeddable-form` has no spec row, and the binding doc says a bound node needs no -`objectName` of its own. Each renderer agrees: `dataSource.object` lands on -`objectName` before the form reads the node. The registrations still declared -`required: true`, and the page compile reads them, so a bound form with no -`objectName` of its own was refused with `missing-required-prop` and the save -failed. - -**Clause-②: yes (widening)** — an `object-form`, `view:form`, `embeddable-form` -or `object-master-detail-form` node that names its object through -`dataSource.object` and sets no `objectName` now compiles and saves. A node that -names its object in neither place also compiles now, and shows "No object named: -set objectName or dataSource.object." where it used to draw a field-less card, a -public form that could not submit, or an empty parent form. An `object-form` -or `view:form` whose fields are declared inline shows no hint and renders as -before: non-empty `customFields`, or `sections` whose every field is an inline -field, the target-less collector the `tabbed`, `wizard`, `split`, `drawer` and -`modal` variants render. `formId` on `embeddable-form` and `details` on -`object-master-detail-form` are still required. The published `objectName` -inputs now carry a description that says the binding can supply them. diff --git a/.changeset/11605-plugin-grid-objectname-optional.md b/.changeset/11605-plugin-grid-objectname-optional.md deleted file mode 100644 index 2286715b68..0000000000 --- a/.changeset/11605-plugin-grid-objectname-optional.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/plugin-grid': minor ---- - -The `object-grid` and `view:grid` registrations no longer declare `objectName` -required, so the page compile accepts a node whose `dataSource` binding names the -object (objectui#11605). - -`@objectstack/spec`'s `object-grid` row leaves `objectName` optional, because the -node's `dataSource` binding can supply it, and the renderer agrees: -`dataSource.object` lands on `objectName` before the grid reads the node. The -registration still declared `required: true`, and the page compile reads the -registration, so a bound node with no `objectName` of its own was refused with -`missing-required-prop` and the save failed. - -**Clause-②: yes (widening)** — an `object-grid` (or `view:grid`) node that names -its object through `dataSource.object` and sets no `objectName` now compiles and -saves. A node that names its object in neither place also compiles now; the grid -answers it at runtime with its own "Object name required for data fetching" -error, as it did before when such a node reached it. The published `objectName` -input now carries a description that says the binding can supply it. diff --git a/.changeset/11605-plugin-kanban-objectname-optional.md b/.changeset/11605-plugin-kanban-objectname-optional.md deleted file mode 100644 index 16c4ae3fd2..0000000000 --- a/.changeset/11605-plugin-kanban-objectname-optional.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/plugin-kanban': minor ---- - -The `object-kanban` registration no longer declares `objectName` required, so -the page compile accepts a board whose `dataSource` binding names the object, and -a board that names its object in neither place shows a hint instead of an empty -board (objectui#11605). - -`@objectstack/spec`'s `object-kanban` row leaves `objectName` optional, because -the node's `dataSource` binding can supply it, and the renderer agrees: -`dataSource.object` lands on `objectName` before the board reads the node. The -registration still declared `required: true`, and the page compile reads the -registration, so a bound board with no `objectName` of its own was refused with -`missing-required-prop` and the save failed. - -**Clause-②: yes (widening)** — an `object-kanban` node that names its object -through `dataSource.object` and sets no `objectName` now compiles and saves. A -node that names its object in neither place also compiles now, and the board -shows "No object named: set objectName or dataSource.object." where it used to -draw an empty board reading "No cards". A board with rows from inline `data` -(an empty array included), a `bind` path or a parent view shows no hint and -renders as before. The published `objectName` input now carries a description -that says the binding can supply it. diff --git a/.changeset/11605-plugin-list-objectname-optional.md b/.changeset/11605-plugin-list-objectname-optional.md deleted file mode 100644 index 6834e706dd..0000000000 --- a/.changeset/11605-plugin-list-objectname-optional.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/plugin-list': minor ---- - -The `list-view` and `view:list` registrations no longer declare `objectName` -required, so the page compile accepts a node whose `dataSource` binding names the -object, and a list that names its object in neither place shows a hint instead of -an empty list (objectui#11605). - -`list-view` has no spec row. The binding doc says a node bound by -`dataSource.object` needs no `objectName` of its own, and the schema validator -counts the binding as a `list-view` record source. The renderer agrees: -`dataSource.object` lands on `objectName` before the list reads the node. The -registrations still declared `required: true`, and the page compile reads them, -so a bound list with no `objectName` of its own was refused with -`missing-required-prop` and the save failed. - -**Clause-②: yes (widening)** — a `list-view` (or `view:list`) node that names its -object through `dataSource.object` and sets no `objectName` now compiles and -saves. A node that names its object in neither place also compiles now, and the -list shows "No object named: set objectName or dataSource.object." where it used -to draw the "Nothing here yet" empty state. A list with inline `data` shows no -hint and renders as before. The published `objectName` input now carries a -description that says the binding can supply it. diff --git a/.changeset/11605-react-requires-object.md b/.changeset/11605-react-requires-object.md deleted file mode 100644 index 1ab4b16b6a..0000000000 --- a/.changeset/11605-react-requires-object.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/react': minor ---- - -`ElementDataSourceGate` takes a `requiresObject` prop: when a placement opts in -and its node names its object in neither place, the gate renders a short "no -object named" hint instead of the block (objectui#11605). - -The object-bound registrations stopped declaring their object key required, -because the `dataSource` binding can supply it and the page compile has no "this -key or that binding" form. So the page compile accepts a node that names no -object at all, and the runtime's answer is the only signal left for it. Several -blocks answered such a node with an empty list, board, form, chart, dash or pivot, -which reads as an empty query. - -**Clause-②: yes (widening)** — `ElementDataSourceGateProps` gains the optional -`requiresObject` boolean. With it set, the gate reads the mapping's object key -(`objectName` unless the mapping names another) on the node after the binding -lands, so a node bound by `dataSource.object` renders as before; a node that names -no object gets the hint, "No object named: set objectName or dataSource.object." -(`data-testid` `{testId}-no-object`), and the block is not mounted. Without the -prop, or with a mapping whose `object` is `false`, nothing changes. The hint is -drawn after the view states, so a binding that is still resolving or failed to -resolve keeps its own panel. diff --git a/.changeset/11608-partialschema-retire.md b/.changeset/11608-partialschema-retire.md deleted file mode 100644 index 8454b6c333..0000000000 --- a/.changeset/11608-partialschema-retire.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**BREAKING — `PartialSchema` is RETIRED from `@object-ui/types`** (objectui#11608, enforce-or-remove). The utility type leaves the `.` entry, the one entry that published it, with no replacement alias. - -**Clause-②: yes (narrowing)**, shipped as `minor` per this repository's version policy: one name leaves the published surface, and the break is stated here. - -- **Why.** The alias had no reader: no producer, doc or skill in this repository, and none in the sibling repositories the census could read. While `BaseSchema` carried an index signature it declared `type` alone, whatever `T` was (objectui#6397). objectui#8347 removed that signature, which made the alias work as written and brought its published-export question due. A published capability with no reader is retired, not kept for its sunk cost. -- **objectui#8347's note.** That release note says `PartialSchema` works as written once `BaseSchema` lost its index signature. This removal supersedes it. - -**FROM** `import type { PartialSchema } from '@object-ui/types'`, annotating a value as `PartialSchema`. -**TO** the node type's own declared members: annotate a whole node with its node type (`ButtonSchema`, `InputSchema`, …). For a partial value, write `Partial & { type: T['type'] }` inline. It keeps every member `T` declares, with `type` required and the rest optional, and a misspelled key is still refused. - -```ts -// before -import type { ButtonSchema, PartialSchema } from '@object-ui/types'; -const patch: PartialSchema = { type: 'button', label: 'Save' }; - -// after: the import above is a compile error naming the symbol -import type { ButtonSchema } from '@object-ui/types'; -const patch: Partial & { type: ButtonSchema['type'] } = { type: 'button', label: 'Save' }; -``` diff --git a/.changeset/11610-grid-keys-camelcase.md b/.changeset/11610-grid-keys-camelcase.md deleted file mode 100644 index b190a6e41b..0000000000 --- a/.changeset/11610-grid-keys-camelcase.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/fields': minor ---- - -The `grid` field's eight field-level keys are camelCase now, and their snake_case spellings are retired and refused by name on every face (objectui#11610). - -BREAKING (`@object-ui/types`, `@object-ui/fields`): a `grid` field's metadata, and a `form` `fields[]` entry of `type: 'grid'`, must spell these keys in camelCase. (The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.) - -- FROM `min_rows` → TO `minRows` -- FROM `max_rows` → TO `maxRows` -- FROM `allow_add` → TO `allowAdd` -- FROM `allow_delete` → TO `allowDelete` -- FROM `allow_reorder` → TO `allowReorder` -- FROM `total_field` → TO `totalField` -- FROM `add_label` → TO `addLabel` -- FROM `sort_field` → TO `sortField` - -Why: `@objectstack/spec`'s runtime form field declares config keys in camelCase only, so it could not declare these keys as they were written (objectstack-ai/objectstack#21704, fork 2, ruled B). There is no alias window and no dual read: no reader reads the snake_case spellings any more, and no stored producer outside this repository's own fixtures, which move with this change, was found to write them. - -**Migration.** Rename each key; its value stays the same. `totalField` keeps its meaning: the CHILD column summed into the grid's footer, which is the value a spec `amountField` carries. It is not the parent field the spec's own `totalField` names on a master-detail subform. - -What each face does with a snake_case key now: - -- **TypeScript.** `GridFieldMetadata` and `FormField` declare each as a `never` member, so an authored value no longer compiles. The camelCase members carry the value types the snake_case members had, and `FormField` still takes each one by reference to `GridFieldMetadata`. -- **zod (`@object-ui/types/zod`).** NARROWS on the tolerant face (`safeValidateSchema`, which `objectui validate` runs) and on the strict authoring face: a form field entry carrying a snake_case key used to parse with the value kept, and is now refused with one `invalid_type` issue at that key. The message leads with ``Did you mean `min_rows` → `minRows`?`` (each key names its own replacement). WIDENS on both faces: the camelCase keys parse, judged by the same value types. -- **The `grid` widget (`@object-ui/fields`).** `GridField` reads the camelCase keys only. A field whose metadata still carries a snake_case key is drawn as an inline alert naming each retired key beside its replacement (`role="alert"`, `data-testid="grid-field-retired-keys"`) instead of the grid, and the same text goes to `console.error` once. Nothing is thrown, so the rest of the form still draws, and the rows are not changed. - -New export from `@object-ui/types`: `GRID_FIELD_RETIRED_KEYS`, the snake_case to camelCase map that the zod refusals and the widget both read, with its key type `GridFieldRetiredKey`. - -`@object-ui/plugin-form`'s master-detail and line-items adapters now hand the grid the camelCase keys, typed against `GridFieldMetadata` instead of cast through `any`. What they draw does not change. - -**Clause-②: yes (narrowing)**: the camelCase spellings widen each face, and the snake_case spellings narrow it. diff --git a/.changeset/11613-plugin-detail-related-list-columns-optional.md b/.changeset/11613-plugin-detail-related-list-columns-optional.md deleted file mode 100644 index cd4865ee0a..0000000000 --- a/.changeset/11613-plugin-detail-related-list-columns-optional.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/plugin-detail': minor ---- - -The `record:related_list` registration no longer declares `columns` required, so -the page compile accepts a related list that lists no columns of its own -(objectui#11613). - -`@objectstack/spec`'s `record:related_list` row leaves `columns` optional, and the -renderer agrees: a `dataSource` binding that names a view lands that view's -columns on the node, and with neither the list derives its columns from the -related object (its `highlightFields`, otherwise its listable fields). The -registration still declared `required: true`, and the page compile reads the -registration, so a node with no `columns` was refused with -`missing-required-prop` and the save failed. - -**Clause-②: yes (widening)** — a `record:related_list` node that sets no `columns` -now compiles and saves, whether a `dataSource` binding names a view (the list -draws the view's columns) or not (the list draws columns derived from the related -object, as it already did when such a node reached it). Authored `columns` still -win over both. `objectName` and `relationshipField` are still required, as the -spec row requires them. The published `columns` input now carries a description -that says where the columns come from when it is absent. diff --git a/.changeset/11615-simple-inline-section-entry.md b/.changeset/11615-simple-inline-section-entry.md deleted file mode 100644 index e9a4be6164..0000000000 --- a/.changeset/11615-simple-inline-section-entry.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/plugin-form': minor -'@object-ui/types': minor ---- - -The default (`simple`) `object-form` draws a self-describing inline section entry, as the `tabbed`, `wizard`, `split`, `drawer` and `modal` forms already did (objectui#11615). Before, the default form resolved every section entry against its parent field pool and skipped an inline `{ name, … }` entry whose name the pool did not hold. The same section drew that entry on every other form type and drew nothing for it on `simple`, apart from a console warning when the object declared the name. - -**Clause-②: yes (widening, with one break for TypeScript readers).** Authored input is only widened: nothing either package accepted before is refused now. Code that READS `ObjectFormSection.fields` can break at compile time, described under "Breaking for TypeScript readers" below. - -- `@object-ui/types`: `ObjectFormSection.fields` is `(string | SpecFormFieldInput | FormField)[]`. The new arm is the form view's `{ field, … }` entry, and it is `@objectstack/spec`'s `FormFieldInput` by reference, not a copy. The form already drew that entry. Before, a TypeScript author could not annotate it, because `FormField` requires `name` and types `field` as an object. The zod mirror is unchanged: a section's `fields` entry is still `z.any()` there. -- `@object-ui/plugin-form`, layout types: the section `fields` of the five layout configs is `NonNullable`, by reference. Those configs are `FormSectionConfig` (tabbed), `WizardStepConfig`, `SplitFormSectionConfig`, `DrawerFormSectionConfig` and `ModalFormSectionConfig`, reached through the exported `TabbedFormSchema`, `WizardFormSchema`, `SplitFormSchema`, `DrawerFormSchema` and `ModalFormSchema`. Each of these layouts already drew the `{ field }` entry. Before, their types refused it, and `ObjectForm` passing an authored section to them would no longer compile once the section type named the entry. -- `@object-ui/plugin-form`, section drawing: on `simple`, an entry that names itself is drawn as it stands, whatever the field pool holds. Such an entry is an object whose `field` is not a string and whose `name` is a string. This is the existing `isInlineFieldDef` predicate that the submit-target rule already reads. It does not require `type`: the spec's inline arm makes `type` optional, and the other five forms draw a typeless entry as the default input. -- `@object-ui/plugin-form`, inline collector: a `simple` form with no data source and no `submitHandler`, whose sections list only inline entries, is now a self-contained collector, as on the other five forms. It opens on `initialValues` / `initialData`, and its `onSuccess` receives the collected values. Before, that form drew no fields and refused the submit. Its submit carve-out now reads the shared `hasInlineFieldSource`. - -**Breaking for TypeScript readers of `ObjectFormSection.fields`** (still `minor`: objectui's major follows the `@objectstack` family major, so its own breaks ship as `minor` and are stated here). A consumer that narrowed an entry with `typeof entry === 'string' ? entry : entry.name` compiled while every object entry was typed as an inline `FormField`. It no longer compiles: `Property 'name' does not exist on type 'FormFieldInput | FormField'`. The read was already wrong at runtime for a `{ field }` entry, where it gave `undefined`. Remedy: name an entry by its arm. The string is the name itself, the `{ field }` entry names its field by `field`, and the inline entry names it by `name`. `@object-ui/plugin-form` now exports `sectionEntryName(entry)`, which applies exactly that rule and returns `undefined` for an entry that names nothing. It sits beside `resolveSectionGroupReferences`, whose result it reads. The repo's own reader, an app-shell test over that resolver's result, is respelled this way. - -**What stays refused or warned.** - -- A field name and a `{ field }` entry still resolve against the pool on `simple`. A name the pool does not hold is still dropped, and still warned about once when the object declares it, because top-level `fields` and `sections` still intersect (objectui#9884). -- An inline entry with no `name` is malformed and is still not drawn on `simple`. -- A form with no data source and no `submitHandler` still refuses its submit with `DataSource is required for form submission (inline mode not configured)` unless every section entry is inline. One name or `{ field }` entry among inline ones is enough to refuse, on all six forms. - -**Behaviour change for an existing schema.** On `simple`, an inline entry whose name the object declares but top-level `fields` leaves out used to be dropped with the intersection warning. It is now drawn as its own definition, with no warning, as on the other five forms. With a data source, its value is still written only if the object declares the field. As on every form, a key the object does not declare is stripped from the write. diff --git a/.changeset/11625-density-inside-config.md b/.changeset/11625-density-inside-config.md deleted file mode 100644 index 0a7884c763..0000000000 --- a/.changeset/11625-density-inside-config.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -A grid toolbar change on a served view is stored where the save door keeps it, so it survives a reload (objectui#11625). This covers density, a column-header sort and the hide-fields toggle, on a packaged view such as the showcase's `showcase_task.default` and on a view a user created. - -The console saves a toolbar change on a served view by sending the whole stored row, the ViewItem envelope `{ name, object, viewKind, config }`, back to `PUT /api/v1/meta/view/NAME`. It wrote the changed key beside `config` instead of inside it. The door judges a body that has a `config` by the spec's `viewItem` member and drops undeclared top-level keys, as ADR-0005 appendix (c) designs it. So it answered `200`, stored no change, and the view reverted on reload. - -The changed key now goes into `config` when it is a key `ListViewSchema` declares: `rowHeight`, `sort`, `hiddenFields` or `inlineEdit`. Every other key stays on the envelope, where the row keeps it: `columnState`, `isDefault`, `isPinned`, `sortOrder` and `visibility`. A row without a `config` is still written flat. So is the personalization overlay (the row with `_isOverride`), whose flat body the door already keeps. - -Still open: the console builds each save from the view as it was when the page loaded. A second toolbar change to the same view in the same session therefore overwrites the first one. For example, changing density and then sorting keeps only the sort after a reload. This was true before this fix, on every kind of row. - -**Clause-②: no.** Nothing on the package entry changes. `buildPersistedViewBody` is exported from `ObjectView.tsx` but not from `@object-ui/app-shell`. diff --git a/.changeset/11626-grouped-nav-reorder.md b/.changeset/11626-grouped-nav-reorder.md deleted file mode 100644 index e08eb79673..0000000000 --- a/.changeset/11626-grouped-nav-reorder.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/layout': patch -'@object-ui/app-shell': patch ---- - -Drag-to-reorder works on a grouped sidebar menu, within each level (objectui#11626). `NavigationRenderer` had a sortable path only in its group-free arm. Every stock app's menu is grouped, so the console's `enableReorder` drew no grip anywhere a user could reach. - -**`@object-ui/layout`.** With `enableReorder` on, each group's children are now a sortable list of their own, and so is each run of top-level entries between two groups. An entry moves within its level only. It never moves into or out of a group, because which group an entry sits in is the app's structure, not a personal order. A move is reported through the existing `onReorder(reorderedItems)`, always with the top-level list. After a move within a group, that list is as drawn and the group's `children` are reordered. The moved level's entries carry their new positions as `order` (0, 1, 2, …), as a move in a group-free menu already did. While `searchQuery` narrows a grouped menu, the menu offers no grip, because a narrowed group shows only some of its children. - -The drag grip is now the row's drag activator, in both arms. dnd-kit's sortable attributes (`role="button"`, `tabIndex={0}`, the sortable description) used to sit on the row wrapper while the listeners sat on the grip, so every row was a focusable button that no key could start a drag from. The attributes and listeners now sit together on the grip. The one element a keyboard can focus per row is now the element the keyboard sensor listens on. - -**`@object-ui/app-shell`.** The sidebar's personal order store (localStorage `objectui-nav-order-APP`) keeps an order for each group under the group's `id`, beside the top level's `__root__`. The `id` is the one key a group has that holds across reloads and locales, because its label is translated. A move writes only the level that moved, so the rest of the menu keeps following the app. Every stored level is applied on load. A group-free app's `__root__` record is written and read exactly as before, so orders users already saved keep working. - -A saved order now holds where the app authors `order` on its navigation entries. The renderer sorts each level by `order`, and the store used to apply a saved order by array position only, so on such an app a drag was stored and then sorted straight back, in a group-free menu as well. A level with a saved order now carries its saved positions as `order`. A level with no saved order is passed on untouched. - -**Clause-②: no.** No export, prop, type member, callback parameter or i18n key is added or removed, and no signature changes. `NavigationRendererProps.enableReorder` and `onReorder` keep their types. Their doc comments, which ship in the published `.d.ts`, now state the within-level scope and what `onReorder` receives. diff --git a/.changeset/11627-offline-local-package-menu.md b/.changeset/11627-offline-local-package-menu.md deleted file mode 100644 index 2f6f9c1fe1..0000000000 --- a/.changeset/11627-offline-local-package-menu.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -On a runtime with no marketplace that still mounts install-local (an offline boot, `OS_CLOUD_URL=off`), a local install's Details page now offers its local menu: re-seed sample data, purge sample data and uninstall from this runtime (objectui#11627). - -Installed Apps links each local install's Details to the marketplace package page. That page answered a marketplace-off runtime with the "App Marketplace is turned off" notice and nothing else, so the local menu below that check could not be reached and no install-local request was ever issued. Now, when the runtime config reports `features.installLocal` and the viewer is an admin, the page draws the package's header (manifest id and installed version) and its local menu above the same notice. The menu issues the same `POST /api/v1/marketplace/install-local/MANIFEST_ID/reseed-sample-data` and `.../purge-sample-data` calls as on a marketplace-on runtime. The page finds the install by the id Installed Apps put in the link (the ledger entry's `packageId`), and the menu acts on its manifest id. - -Everything that needs a marketplace stays refused as before: the page sends no catalog request and no cloud-installation probe, and it draws no install-to-cloud button, readme or version list. A viewer who is not an admin, a runtime that does not mount install-local, and a package that is not a local install all see the notice alone, as before. - -**Clause-②: no.** Nothing on the package entry changes: no export, prop, type member or i18n key is added. `MarketplacePackagePage` takes no props, and the strings it draws already existed. diff --git a/.changeset/11628-external-envelope.md b/.changeset/11628-external-envelope.md deleted file mode 100644 index 82d7d8e2d2..0000000000 --- a/.changeset/11628-external-envelope.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The External Datasource panel in Setup and Studio reads the `{ success, data }` envelope its routes answer (objectui#11628). On a federated datasource such as the showcase's `showcase_external`, the Tables tab listed no remote tables. "Refresh catalog" showed no snapshot time, and "Run validation" replaced the panel with a render error (`Cannot read properties of undefined (reading 'length')`). Because the list was empty, the import dialog could not be opened. The client read `tables`, `draft`, `catalog` and the validation verdict from the top of the body, but the server puts each one under `data`. The panel now lists the remote tables, shows the snapshot time after a refresh, renders the validation rows, and opens the import dialog on the generated draft. - -Refusals on these routes now reach the user. Every refusal used to read `[object Object]`, because the client turned the ADR-0112 `{ error: { code, message } }` object into a string. That covered a missing capability, an unknown remote table, an unreachable datasource, and a capability refusal from "Import as Object". The panel now shows the server's message and code. The "federation is not enabled on this server" hint now appears when the server answers `503 SERVICE_UNAVAILABLE`. Before, it never did, because the client compared against a code the server no longer sends. - -A successful response that is not the `{ success: true, data }` envelope is now an error that names the route. Before, the panel showed it as an empty list. - -**Clause-②: no.** Nothing is added to or removed from the package entry. The changed module is not exported from it. diff --git a/.changeset/11629-kanban-column-sum.md b/.changeset/11629-kanban-column-sum.md deleted file mode 100644 index 3e0e3e40ff..0000000000 --- a/.changeset/11629-kanban-column-sum.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch -'@object-ui/app-shell': patch ---- - -The `object-kanban` board totals the view's `summarizeField` in each column header (objectui#11629). - -`@objectstack/spec` declares `summarizeField` on the view-level `KanbanConfig` ("Field to sum at top of column"), and `ListView`'s kanban branch passes it onto the `object-kanban` node it generates. The board never read it, so a view that set it showed the card count and no total. Each column header now shows the sum of that field over the column's cards, beside the count, on both the flat layout and the swimlane layout's column-title row. - -- The total is written by the field's own cell renderer, the one the cards use for that field. A currency field totals as currency, and a number field keeps its declared `scale`. A sum over a number field with no declared `scale` is rounded to the widest input, so `0.1 + 0.2` reads `0.3`. -- An absent, `null` or empty value counts as `0`, and an empty column totals `0`. A numeric string counts as the number the card shows for it. A column holding any other value shows no total, never `NaN`. -- The total covers the cards the board loaded. When the board's own fetch filled its window, the total carries the same `+` the count carries (`6+`). -- No total is shown for a field the viewer may not read, or a field the object does not declare. The rows never carry such a field, so the column would read `0`. -- A `Σ` glyph sets the total apart from the count badge beside it. The field's label is the total's tooltip and its screen-reader name, and the glyph is hidden from assistive technology. No translation key is added. - -A board whose node carries no `summarizeField` renders exactly as before. The console's object page now passes a view's `summarizeField` on to the list view with the lane, title and card fields it already relayed, so a board opened there shows the totals (until now the key was dropped on that page). Nothing is added to the package entry: the total reaches the header through a package-private context, the same channel the records-settled signal uses. diff --git a/.changeset/11630-detail-group-visible-when.md b/.changeset/11630-detail-group-visible-when.md deleted file mode 100644 index 5b891f783e..0000000000 --- a/.changeset/11630-detail-group-visible-when.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-detail': minor -'@object-ui/app-shell': minor ---- - -A field group's `visibleWhen` now gates its section on the record detail page, the same way it gates the section on the entry form (objectui#11630). Take an object whose `fieldGroups` entry declares `visibleWhen: "record.kind == 'pro'"`. The edit form hid the "Pro details" section on a `basic` row, but the detail page drew it on every row, header included. The detail page now draws it only where the predicate holds. - -**How it works.** The detail body now writes each field group as `@objectstack/spec`'s own reference form, `{ group: KEY }` (`RecordDetailsProps.sections[].group`). `record:details` resolves the reference against the object's `fieldGroups`: the group's members, label, icon, description and collapse state, and its `visibleWhen`. The predicate is evaluated per record with the form's own evaluator (`resolveFieldRuleState` from `@object-ui/core`, under `usePredicateScope`), and both spellings work as they do on the form: a bare CEL string or a `{ dialect: 'cel', source }` envelope. - -- Values bind under `record.`, so a bare identifier is unbound. -- A declared field the row does not carry compares as `null`. -- A relation binds as its stored id, even when the page's record arrived expanded. -- `previous` binds the persisted row, as on the record's edit form. -- A predicate that cannot be evaluated SHOWS the section and warns once, which is what the form does. - -A FALSE verdict removes the whole section, heading and members. This is display only: the record API serves those fields either way. - -**Clause-②: yes (output shape change).** BREAKING for code that reads the synthesized sections of a grouped object. The bump is still `minor`: under this repo's release model, objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here. - -- `buildDefaultPageSchema`, `buildDefaultTabs`, `buildDefaultDetails` and `resolveDetailSections` (`@object-ui/plugin-detail`) change the `record:details` `properties.sections[]` they emit for an object with `fieldGroups`: - - Each declared group becomes `{ group: KEY, columns }`. It used to be an enumerated copy of the group: `name`, `label`, `icon`, `description`, `collapsible`, `defaultCollapsed`, `columns`, and `fields` as rich `{ name, label, type, … }` descriptors. - - The trailing ungrouped section keeps `columns` and lists `fields` as bare names. - - The node now parses as `RecordDetailsProps`; the old one was refused at every field object. Pages Studio seeds from it (`createSeed`) change the same way. An explicit `sections` option is still returned unchanged. -- `record:details` gates a `{ group }` reference with the group's `visibleWhen`. It does not read `visibleWhen` off an enumerated section, a key the spec refuses there. -- The console's default record page (`RecordDetailView`, `@object-ui/app-shell`) writes `{ group: KEY, showBorder: true }` for each declared group. Its ungrouped primary and "More details" sections are unchanged. -- The `record:details` registration's `sections` input description now says a `{ group }` reference inherits the group's `visibleWhen`, and that one written on a section itself is not read. -- Unchanged: `deriveFieldGroupDetailSections` still returns the resolved, enumerated sections (what a reference renders as), with no `visibleWhen` on them. No type member, zod member, registered `inputs` entry, prop or i18n key is added. - -**Migration.** - -- FROM reading a group's heading or members off a synthesized section (`sections[i].label`, `sections[i].fields`) → TO `deriveFieldGroupDetailSections(def)`, which returns the resolved sections, or the group's own entry in `def.fieldGroups`. -- FROM a stored page section copied from an older seed, `{ name: 'pro', label: 'Pro details', fields: [ … ] }` → TO `{ group: 'pro' }`, with `columns` / `showBorder` / `headerColor` kept if you set them. Only the reference form inherits the group's `visibleWhen`; an enumerated copy renders on every row, as before. diff --git a/.changeset/11631-lookup-cascade-clear.md b/.changeset/11631-lookup-cascade-clear.md deleted file mode 100644 index ddeab673df..0000000000 --- a/.changeset/11631-lookup-cascade-clear.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/components': patch ---- - -A lookup whose `dependsOn` names a parent field drops its selection when that parent changes or is cleared (objectui#11631). - -Before, the picker re-scoped its candidate list to the new parent but kept the record already chosen. An invoice whose Account was switched from Northwind to Contoso saved Contoso beside a Northwind contact. The server checks only that a reference exists, so it accepted the pair. Now the form clears the dependent lookup as soon as any parent that scopes it takes a different value. It writes `null`, or `[]` for a multi-value lookup, so an edit clears the stored value instead of leaving it unchanged. A cleared lookup that scopes another lookup clears that one too. - -What does not clear it: opening an existing record, whether its values are present when the form mounts or arrive after; switching a mounted drawer to another record; a `resetOnSubmit` or Cancel reset; a change to any other field. A lookup that holds nothing is not written to. Which parents count is read from the same field-level `dependsOn` array the picker scopes its query by, so a `dependsOn` the picker ignores clears nothing either. - -The rule covers every form the `form` renderer draws, including the object form and its drawer, modal, split, tabbed and wizard variants. The console's form-view page (`/forms/:name`, `/f/:slug`) has its own renderer and is not changed here. - -**Clause-②: no.** Nothing on the package entry changes. No export, prop, type member or i18n key is added, and `CASCADE_OPTION_WIDGET_TYPES` is unchanged. diff --git a/.changeset/11632-dataset-compare-label.md b/.changeset/11632-dataset-compare-label.md deleted file mode 100644 index 2386d5677e..0000000000 --- a/.changeset/11632-dataset-compare-label.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -A dataset-bound dashboard widget names its comparison window from `compareTo.kind` (objectui#11632). `previousPeriod` reads "vs previous period" and `previousYear` reads "vs last year", in every locale the `dashboard.trend.*` keys already cover. The label used to be guessed from the widget filter's date-macro tokens. A dashboard date range of `last_30_days` (`{30_days_ago}` to `{today}`) with `compareTo: { kind: 'previousPeriod' }` therefore read "vs yesterday", although the analytics executor had compared the previous 30 days. A quarter's macros read "vs last quarter" in the same way, although the executor compares the equal-length window before the quarter. The fix applies to every place the widget names the window: the KPI delta, the table's comparison column header and its CSV export, the cross-tab caption, and the chart's comparison series. The compared values were already right and do not change. - -Inline (non-dataset) metric and chart widgets keep their filter-based label. There the comparison filter really does swap `{today}` for `{yesterday}` and `current_*` tokens for `last_*`, so the label matches what was compared. - -**Clause-②: no.** No export, prop, type member or i18n key is added or removed. diff --git a/.changeset/11633-verify-email-get.md b/.changeset/11633-verify-email-get.md deleted file mode 100644 index dffa5494cb..0000000000 --- a/.changeset/11633-verify-email-get.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The console's `/verify-email?token=…` page verifies the address again (objectui#11633). It used to send the token as `POST /api/v1/auth/verify-email` with a JSON body. better-auth serves that route as GET only, so the server answered 404. Every valid token then showed "Verification failed: 404", and the account stayed unverified. - -The page now calls `GET /api/v1/auth/verify-email?token=…`, the route the server already serves and the one the mailed link targets. It sends no `callbackURL`, so the route answers JSON instead of redirecting. The page shows the success state only for that JSON receipt (`{ status: true }`), which the route also returns when the address is already verified. A garbage or expired token gets a 401 from the server, and the page shows the error state with the server's reason. A 2xx that is not the receipt, such as an HTML page, also shows the error state. The page's states, copy and links are unchanged. - -**Clause-②: no.** Nothing on any package entry changes. No export, prop, type member or i18n key is added or removed. diff --git a/.changeset/11634-loginform-register-default.md b/.changeset/11634-loginform-register-default.md deleted file mode 100644 index 99f9c50bb2..0000000000 --- a/.changeset/11634-loginform-register-default.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@object-ui/auth': minor ---- - -`LoginForm` shows its "Don't have an account? Sign up" row only when the caller passes `registerUrl` (objectui#11634). The prop used to default to `'/register'`, so a caller that passed `undefined` to withhold the link got it back. Both console login pages pass `undefined` when the server's `/auth/config` reports `emailPassword.disableSignUp: true`, so a deployment with sign-up turned off still offered a sign-up link. The server refused the sign-up and `/register` sent the user back to the login page. - -**Behaviour change.** A `LoginForm` rendered without `registerUrl` no longer shows a sign-up link. A caller that left the prop out and relied on the `'/register'` default now passes the URL itself, `registerUrl="/register"`, to keep the link. The console app's login page and `@object-ui/app-shell`'s `DefaultLoginPage` already pass the URL whenever sign-up is on, so neither changes. Leaving the prop out, or passing `undefined`, is the one way to render no link: no second "off" value is added. - -**Clause-②: no.** The prop's type is unchanged (`registerUrl?: string`). No export, prop, type member or i18n key is added or removed. diff --git a/.changeset/11638-member-properties-params-retired.md b/.changeset/11638-member-properties-params-retired.md deleted file mode 100644 index 464e29cbc5..0000000000 --- a/.changeset/11638-member-properties-params-retired.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/components': minor ---- - -`action:group` and `action:menu` no longer read a member's `properties.params` (objectui#11638). A container member's `properties.params` no longer reaches the action runner. - -**Why.** `@objectstack/spec` refuses a `properties` key on an `action:group` / `action:menu` member, with this prescription: "A member carries no `properties` bag: its static parameter values (`properties.params`) are not part of the inline action vocabulary. For a `type: 'api'` member's request body write `bodyExtra`; to run an action with static parameter values, author it as its own `action:button` node, whose `params` object carries them." The census behind that ruling found no writer of the key. The renderers kept a read the spec refuses, so the read is retired. - -**Behaviour change**, shipped as `minor` per this repository's version policy: - -- A member's `properties.params` is not forwarded as the runner's `params`, and it is no longer template-evaluated. This holds for an `action:group` member (inline and dropdown), an `action:menu` item, and an `action:bar` member that lands in the overflow menu. -- A member whose `params` is an array forwards it as `actionParams` alone. The runner's `params` then carries only what the user answers in the parameter dialog. -- A `type: 'api'` member's object `params` is its request payload (the objectstack#5777 window), and a `properties.params` beside it no longer replaces that payload. -- Unchanged: an `action:button` / `action:icon` node reads its static values from `properties.params` as before, and so does an `action:bar` member drawn inline, which the bar mounts on one of those two renderers. - -**FROM** a container member carrying static values: - -```json -{ "type": "action:group", "actions": [ - { "name": "edit", "label": "Edit", "type": "navigate_edit", "properties": { "params": { "recordId": "${record.id}" } } } -] } -``` - -**TO** its own `action:button` node: - -```json -{ "type": "action:button", "properties": { "label": "Edit", "actionType": "navigate_edit", "params": { "recordId": "${record.id}" } } } -``` - -For a `type: 'api'` member's request body, write `bodyExtra` on the member. - -**Clause-②: no.** No export, prop, type member or i18n key is added or removed. The retired helper lived in a module that `@object-ui/components` does not re-export from its entry. diff --git a/.changeset/11642-toolbar-writes-compose.md b/.changeset/11642-toolbar-writes-compose.md deleted file mode 100644 index e405e36680..0000000000 --- a/.changeset/11642-toolbar-writes-compose.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Two grid toolbar changes to one view in one session now both survive a reload (objectui#11642). Before, the second change overwrote the first: changing density and then sorting by a column header kept only the sort. This held on every kind of view row: a served view's ViewItem envelope, a saved view stored flat, and a personalization overlay. - -The console saves a toolbar change with `PUT /api/v1/meta/view/NAME`, which replaces the whole row. It built each body from the view as it was when the page loaded, or, for an overlay, from the new change alone. Nothing refreshed that starting point after a save landed, so each later save dropped the earlier one. - -Each toolbar save now starts from the row the store holds when the save runs. The console reads it back with the same `GET /api/v1/meta/view` request the page load makes, because the save answer carries no row and the save door drops undeclared keys from the body it receives. A saved view's change is placed on that row. An overlay keeps the keys it already stored, the ones `VIEW_OVERLAY_OWNED_KEYS` in `@object-ui/data-objectstack` names, and the new change is added to them, so the overlay still holds nothing the view it shadows owns. Saves to the same view run one after another, so a change made while the previous save is still in flight starts from the row that save stored. Two changes made within the save debounce are still sent as one save. - -**Clause-②: no.** Nothing on the package entry changes. diff --git a/.changeset/11643-fallback-tab-session-only.md b/.changeset/11643-fallback-tab-session-only.md deleted file mode 100644 index 846b10f813..0000000000 --- a/.changeset/11643-fallback-tab-session-only.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -On an object that declares no list view, a grid toolbar change no longer sends a save the door refuses (objectui#11643). Such an object opens on the "All Records" tab the console makes for it, whose id is `all`. A density change there used to send `PUT /api/v1/meta/view/all` with no `viewKind`, the door answered `422 INVALID_METADATA`, an error toast appeared, and the density was gone on reload. - -That tab is not a stored or served view, so there is no row to save into. Its toolbar changes (density, sort, hidden fields, column order and widths, inline edit) now apply for the session only: the list keeps them, no request is sent and no error is shown. No view is created to hold them; an object that wants saved toolbar settings declares a list view, and the served-view save path applies to it. - -The console tells its own tab apart by where it was made, not by its id. A served view whose tab id is also `all`, a saved view beside the console's tab, and a stored row named `all` that shadows that tab all keep saving as before. - -**Clause-②: no.** Nothing on the package entry changes. diff --git a/.changeset/11645-installed-not-loaded.md b/.changeset/11645-installed-not-loaded.md deleted file mode 100644 index 0fef83d2d0..0000000000 --- a/.changeset/11645-installed-not-loaded.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -Installed Apps now reads a package the runtime refused to load at startup as "Not loaded", says why, and keeps its Uninstall (objectui#11645). - -After a restart whose startup check refused a package built for another platform protocol, the install-local listing still lists the package, so it can be uninstalled or replaced. Since `@objectstack/*` 17.7.0 the listing marks such an entry with `notLoaded: { code, requiredRange }` in place of `withSampleData`. Installed Apps drew every listed entry as installed. Now: - -- the row carries a "Not loaded" badge beside its version, and one sentence naming the reason: for `OS_PROTOCOL_INCOMPATIBLE`, that the package targets protocol `requiredRange`, which this runtime does not support. A refusal code the console has no sentence for still reads "Not loaded" and names the code; -- Uninstall stays on the row. Its confirm and its result no longer say that the package stays loaded until the next restart; -- the package's Details page no longer offers re-seed or purge of sample data for such an entry, since both act on objects the runtime never registered. Uninstall and Reinstall stay. - -A loaded entry renders as before. - -The language packs gain `marketplace.notLoaded.badge`, `marketplace.notLoaded.protocolIncompatible`, `marketplace.notLoaded.otherReason`, `marketplace.uninstall.confirmNotLoaded` and `marketplace.uninstall.successNotLoaded`, in all ten languages. diff --git a/.changeset/11649-share-link-password-header.md b/.changeset/11649-share-link-password-header.md deleted file mode 100644 index beaed72051..0000000000 --- a/.changeset/11649-share-link-password-header.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The console's share-link landing page (`/s/TOKEN`) sends a link's password in a request header, and a link that needs sign-in shows the sign-in path (objectui#11649). - -- **The password leaves the URL.** The page used to send the password as a `?password=` query parameter on `GET /api/v1/share-links/TOKEN/resolve`. It now sends it in the `X-Share-Password` request header, the name the server reads, and never in a request URL, so it no longer ends up in a server, proxy or CDN log that records request URLs. The conversation `/messages` request sends the same header. That route checks the password too, so a password-protected shared conversation used to show "This conversation has no messages yet." It now shows its messages. -- **A sign-in-required link shows the sign-in path.** The server refuses with `401` for three different reasons, and the page used to show a password prompt for all of them. It now reads the response's `error.code`. `NEEDS_PASSWORD` shows the prompt and `WRONG_PASSWORD` shows it with "Wrong password.". `SIGN_IN_REQUIRED`, a link shared with signed-in users only, shows "Sign in required" and a "Sign in" button that opens `/login?redirect=` back to the link, with no password field. A `401` with any other code shows the server's message. -- **A password a header cannot carry is not sent.** A header value cannot hold a character above U+00FF, such as a Chinese character or an emoji, and the browser refuses to send it. The page does not send such a password and does not fall back to the URL. The prompt says that the password cannot be sent and asks the visitor to get a different password from the link's owner. Until the server defines an encoding for this header, a link whose password has such a character cannot be opened from the console. - -**Clause-②: no.** No export, prop, type member or i18n key is added or removed. The page's labels stay English literals. diff --git a/.changeset/11650-import-cancel-counts.md b/.changeset/11650-import-cancel-counts.md deleted file mode 100644 index ec66129649..0000000000 --- a/.changeset/11650-import-cancel-counts.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -Cancelling a background import shows the rows the server already committed, with Undo (objectui#11650). `ImportWizard`'s Cancel used to show "Import cancelled · 0 imported" without reading the job back, while the job it had just cancelled read `cancelled` with the rows its worker had written. Users took the zero at its word and imported the file again. - -After the cancel answers, the wizard now reads the job (`getImportJobProgress`, the `GET /api/v1/data/import/jobs/:id` read). It keeps reading until the outcome is final, at most ten reads, one per poll interval. The result then shows the job's created and updated counts. When the job can be undone, it also shows an **Undo import** button. That button runs the same confirm-and-undo action as the History list and then reads "Undone". A `cancelled` read counts as final once it is undoable, or once it repeats the previous `cancelled` read's counts. The server marks the job `cancelled` before its worker stops writing, so the first read after a cancel can still be short. The poll loop applies the same rule to a job cancelled from elsewhere, and both paths build the result the same way. - -**Behaviour change for hosts.** `onComplete` now fires once after a user cancel that reads the job back, with the cancelled result (`cancelled: true` and the committed counts). Before, it fired only when the poll loop itself saw a job end `cancelled`. In the console this refreshes the list, so the committed rows show up, and it raises the usual import toast. If no read settles within the bound, the result still says "Import cancelled", but it shows no count, because the wizard never read one. `onComplete` does not fire in that case, as before. The poll loop and the cancel handler can no longer both publish a result for the same run. - -**Undo refreshes the list it changed.** A successful Undo, from the History list or from the cancelled result, now calls `notifyDataChanged` for the imported object. That is the data-invalidation bus from `@object-ui/react`, so a mounted list of that object refetches in place. Before, the list kept showing the rows the Undo had just deleted until something else refreshed it. A failed Undo announces nothing. - -**Clause-②: no.** No export, prop, type member or i18n key is added or removed. The new copy reuses the existing `grid.import.importCancelled`, `grid.import.createdCount`, `grid.import.updatedCount`, `grid.import.undoImport`, `grid.import.undoing`, `grid.import.undoConfirm` and `grid.import.reverted` strings. diff --git a/.changeset/11658-ai-build-surface-ux.md b/.changeset/11658-ai-build-surface-ux.md deleted file mode 100644 index 2fe7fb47d0..0000000000 --- a/.changeset/11658-ai-build-surface-ux.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/plugin-chatbot': minor -'@object-ui/i18n': minor ---- - -Five fixes on the AI build surface, from the 2026-10-05 cloud acceptance run (objectui#11658). - -- **The first AI build lands on the running app.** The built-moment transition still moves the conversation into the Studio workbench at `/studio/PKG/interfaces`. The Interfaces canvas now opens in **Run** mode with the properties panel collapsed, where it used to open in Design with the panel open. One click on **Design** brings back the design overlays and the properties panel. Every other way into Studio, the "Design in Studio" button included, still opens in Design. The transition asks for this with router state, which the pillar reads once at mount; it is not a URL param, so a reload or a shared link opens the designer. -- **The in-app composer names the task.** Inside an app, the composer placeholder reads "Ask about your data, or ask me to change this app…". This covers the console dock in a running app, the Studio dock and `/ai/build?package=`. It used to read "Ask {agent}…", which showed as「向 构建 提问…」in Chinese. The cold-start build surface and the ask agent keep the placeholders they had. New key: `console.ai.askOrChangeApp`. -- **The AI usage popover shows one figure.** The popover shows the share of the single AI pool used so far (`console.ai.usage.poolUsed`, for example "19% used"). It no longer shows the build / data-Q&A split, because that split cannot honestly say which part a single-composer turn used. The keys `console.ai.usage.meterBuild`, `meterAsk` and `breakdownTitle` are retired from all ten packs. `useAiUsage` still reads `breakdown` from the wire and validates it strictly, but nothing renders it. -- **The usage gauge no longer looks like a loading spinner.** The header glyph is now a closed outline in the tone colour with a pie wedge inside it. It used to be a stroked arc over a faint track, which at low usage had the outline of a spinner. -- **The plan card's scope chip is localized.** The extend-mode chip on the proposed-plan card and on the live design panel reads the pack's `chatbot.plan.extendTarget` sentence, where it used to show the English literal "Adding to existing app". It names the target app by its label, and the internal name moves to the chip's tooltip. `ChatbotEnhanced` gains an optional `resolveAppLabel(appName)` prop, and the console passes one over its metadata apps. A host `planExtendLabel` still takes precedence, but the prop no longer defaults to an English string. diff --git a/.changeset/11659-console-record-ux.md b/.changeset/11659-console-record-ux.md deleted file mode 100644 index fea85d89fe..0000000000 --- a/.changeset/11659-console-record-ux.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch -'@object-ui/plugin-detail': patch ---- - -Console, record page and Studio copy from the 2026-10-05 cloud acceptance run (objectui#11659). - -**The cloud home's primary button reads "Open workspace" (zh 「进入工作区」), not "Open Production".** A new customer has exactly one environment, and "production" is control-plane vocabulary. The `cloud:onboarding-next` widget's ready-state button now resolves `cloudOnboarding.openWorkspace`, which replaces `cloudOnboarding.openProduction` in all ten language packs, and the hint under it (`cloudOnboarding.hintReady`) says "workspace" instead of "production environment". The button still navigates to the page's `openProductionUrl`; the page-metadata contract is unchanged. - -**The create-workspace dialog asks for the name only.** `CreateWorkspaceDialog` no longer shows the "URL slug" field: the customer never sees the slug take effect at this step. The slug is still generated from the name, by the same rule as before, and sent with the create call; the owner can change it later in organization settings. Because the slug is no longer the user's to fix in the dialog, a slug collision (`ORGANIZATION_ALREADY_EXISTS` or `ORGANIZATION_SLUG_ALREADY_TAKEN`) is retried with a short random suffix, up to three attempts in all; any other refusal is shown as before. The `workspace.slugLabel` and `workspace.slugHint` pack keys are left in place, now unread. - -**The backup-password reminder no longer appears in the user's first session.** 「建议设置一个备用密码」 showed on the environment home right after a new user built their first app: the reminder was gated on a home-visit count (quiet on the first home mount, shown on the second), and coming back to home after building is the second mount inside the first sitting. It now stays quiet for 12 hours after the first home visit on the device, so it first shows on a later day's visit. The first-visit record (`os:recovery-pw-first-seen`) now holds a timestamp; a device that holds the old `'1'` flag starts the 12 hours over rather than reading it as long ago. When storage is unavailable it stays quiet. `RecoveryPasswordReminder` moves out of `HomePage.tsx` into its own module; it is not exported from the package entry, and its other conditions (dismissed, SSO-enforced, has a local password) are unchanged. - -**The record header's highlight row no longer cuts a phone number.** A drawer's highlight row read 「0574-876」 for the stored `0574-8765-4321`, with no ellipsis. The phone cell draws a dial icon, the number and a copy button in one `inline-flex` box, and the chip's single-line clip cannot put an ellipsis on such a box, so a 9rem chip cut the number mid-way. `HeaderHighlight` now gives a `phone` chip the wide basis that `email` and `url` already take, so a whole number shows; the full value stays in the chip's hover title. Other field types keep their chip width. - -**Studio's dashboard widgets list names each widget's kind in the designer's language.** The list read 「按客户状态统计数量 · bar」: beside each title, `DashboardDefaultInspector` printed the stored `type` id. It now prints the name the add-widget picker shows for that kind (`engine.widgetPicker.type.*`, zh 「柱状图」, en "Bar chart"), with the id on hover. A type the catalogue does not know still prints its id. The stored `type` is unchanged. - -**Studio's automation pillar copy is plain language in zh.** The list heading read 「自动化 · flow」 and the top bar 「默认 OFF · 审阅后再启用」: an internal metadata type and an English switch state inside Chinese copy. They now read 「自动化」 and 「默认停用 · 审阅后再启用」, matching the pillar's own 已启用 / 已停用, and the canvas hint 「可视化编排 · 点选节点配置」 reads 「点选画布上的节点即可配置」. The en heading drops the type too ("Automations"). Whether the pillar is offered on a given plan is not decided here. diff --git a/.changeset/11660-escalation-off-removes-block.md b/.changeset/11660-escalation-off-removes-block.md deleted file mode 100644 index 8bb56bf217..0000000000 --- a/.changeset/11660-escalation-off-removes-block.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The flow designer's SLA escalation switch on an approval node no longer writes a refused `escalation: { enabled: false }` block, and switching it off keeps the values the author entered (objectui#11660). - -`ApprovalNodeConfigSchema` requires `escalation.timeoutHours` whether or not `enabled` is `false`. So `{ enabled: false, timeoutHours: 24 }` is a valid way to switch escalation off, and the runtime skips escalation for it. Having no block at all is also valid. The bare `{ enabled: false }` is not. The inspector wrote that bare block when an author switched off a node with nothing entered, because the switch drew on over a node that had no block. The flow saved and then failed at the approval node on every run, or, once the platform judges the block at save time, the save is refused. - -- **No block reads off.** A node without an `escalation` block used to draw the switch on, with its four fields showing. The inspector applied the declared default (`true`) although there was no block for it to apply to. It now draws the switch off, and rendering writes nothing. A block that omits `enabled` still reads on, and a stored `enabled: false` still reads off. -- **Switching off with nothing entered writes no block.** Switching on writes `{ enabled: true }` alone. Switching back off before anything is entered removes the block instead of writing `{ enabled: false }`. A `config` left empty is dropped, as it is for any other cleared field. -- **Switching off a block that holds values keeps them.** The block gets `enabled: false`, and every entered value stays exactly as stored. They show as kept but not in effect, with a Clear action (objectui#6499). A block that has values but no `timeoutHours` yet keeps them too: no hours are filled in, and the block stays refused at `escalation.timeoutHours` until hours are entered, as it was before the switch. -- **Clearing the last kept value removes the block**, instead of leaving `{ enabled: false }` behind. A block that still holds another value is kept. - -The offline field table and the `configSchema` the engine publishes for the approval node behave the same. A stored block is never rewritten just because the node is opened. - -**Clause-②: no.** Nothing on the package entry changes. The new helpers (`switchedBlockOf`, `readFieldValue`, `isBareSwitchedOffBlock`) live in the inspector's own module, which `@object-ui/app-shell` does not export. diff --git a/.changeset/11664-objectlist-number-column.md b/.changeset/11664-objectlist-number-column.md deleted file mode 100644 index da3358e294..0000000000 --- a/.changeset/11664-objectlist-number-column.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The flow designer saves a screen field's `Min` and `Max` as numbers (objectui#11664). - -A screen node's `fields` list is edited as a table, one row per field. The screen descriptor's `configSchema` declares `min` and `max` as `type: 'number'`, but the table had no number column, so it rendered them as text cells. Authoring `Min` = 1 saved `min: '1'`, and the text cell read a code-authored field's stored `min: 0` back as `'0'`. The screen contract (`ScreenFieldConfigSchema.min` / `.max` are `z.number()`) refuses strings, so every run of such a flow failed at the screen node. - -- **A number or integer item property is now a number column**, the same mapping the designer already used for a node's top-level number fields. Its cell is a number input. -- **A number column commits a number.** A typed `0` is saved as `0`. Clearing the cell removes the key; it does not save `''`, `null` or `NaN`. An entry the browser cannot read as a number saves nothing, as in the top-level number field. -- **A stored number stays a number** when another cell of its row is edited. -- **Editing a code-authored field no longer drops its other settings.** The designer shows its built-in screen form until the server's `configSchema` arrives, then the server's form. Rows read while the built-in form was showing had no `Min` or `Max` cell, so the next edit saved the field without its `min` and `max`, and without its `options`, `defaultValue`, `placeholder`, `inlineHelpText` and `reference`. A row now reads a column that appears later from the stored field. Nothing is saved until the author edits. -- **Stored strings are not converted.** A string already stored in a number column (as the old text cell saved it) is kept exactly as it is until the author types over it, and the screen contract still refuses it. Strings in other columns are unchanged. - -This applies to every object-list column the engine publishes with a number or integer type, not only the screen's `min` and `max`. The designer's built-in screen form, used when the server publishes no `configSchema`, has no `Min` or `Max` column and is unchanged. - -**Clause-②: no.** Nothing on the package entry changes. The new `number` column kind is on `FlowConfigColumn`, in the inspector's own module, which `@object-ui/app-shell` does not export. The object-list row changes are internal to that module. diff --git a/.changeset/11666-launcher-plan-awaiting-approval.md b/.changeset/11666-launcher-plan-awaiting-approval.md deleted file mode 100644 index ae5f1f38db..0000000000 --- a/.changeset/11666-launcher-plan-awaiting-approval.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/plugin-chatbot': minor -'@object-ui/i18n': minor ---- - -The chat launchers show a marker while a proposed plan awaits the user's approval (objectui#11666, item 6 of objectui#2458). A user who closed the chat with a blueprint still waiting on them used to get no sign of it from the launcher they return to. - -What a user sees: while the newest proposed plan of a conversation still offers "Build it", the console's assistant button (the floating launcher on app pages and Home) and the ChatDock edge launcher Studio uses carry a small amber dot. Assistive tech reads it as the button's description, from the new `console.ai.dock.planAwaitingApproval` key in the active language. With no plan awaiting, neither launcher shows anything. The dot goes away when the plan stops awaiting: the user approves it, its build runs, or a newer proposal takes its place and is itself approved or built. Opening the chat clears nothing by itself, and neither does opening another conversation. Deleting the conversation from the `/ai` sidebar drops it. - -Where the reading comes from: the chat's own plan card. `ChatbotEnhanced` derives "awaiting approval" from the same producer its card header and body read (`resolveProposalCardState` reads `pending`), and reports it to its host. The console's chat pane publishes that reading on the assistant bus, per conversation and per signed-in user, and the launchers read the bus. They import no chat code. The reading is exact for everything that happens in the tab. It does not see a decision made in another tab or on another device, and a page reload starts it empty until a chat on that conversation is opened again. The durable copy is the server conversation, which the launchers do not read. - -**Clause-②: yes (widening).** Two published surfaces widen: - -- `@object-ui/plugin-chatbot`: the exported `ChatbotEnhancedProps` type gains one optional member, `onPlanApprovalPendingChange`, a callback that takes one boolean and returns nothing. `ChatbotEnhanced` calls it once on mount and again whenever the boolean changes. Without it nothing changes. -- `@object-ui/i18n`: every built-in locale pack gains one key, `console.ai.dock.planAwaitingApproval`. The `en` pack is exported from the package entry, so the key also joins the translation-key type derived from it. - -`@object-ui/app-shell` adds no export: the bus functions the pane and the launchers use (`publishPlanApprovalPending`, `usePlanApprovalPending`) ship inside `dist/` but are not on the package entry, and the exported `assistantBus` object and `AssistantSnapshot` type are unchanged. No prop, export or key is removed, and no existing member changes type. diff --git a/.changeset/11667-ai-chat-labels-i18n.md b/.changeset/11667-ai-chat-labels-i18n.md deleted file mode 100644 index cf264b71fa..0000000000 --- a/.changeset/11667-ai-chat-labels-i18n.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -The AI chat's tool-approval card and its "Open in Builder" handoff card are translated in every language (objectui#11667). - -The console's AI chat passed three English literals to the inline approval card: "Approve & run", "Reject", and the deny reason "Operator rejected from chat". It also left the handoff card on its English defaults: "Build this in the Builder", "Open in Builder →", and the superseded card's tooltip "A newer request is available". A zh-CN reader met all six in English, among labels that were already translated. - -All six now come from the language packs: - -- The approve and reject buttons reuse the AI Approvals inbox's keys `aiApprovals.approveAndExecute` and `aiApprovals.reject`. They make the same decision on the same pending action, through the same endpoint. English readers now see "Approve & Execute" in place of "Approve & run", the inbox's wording. -- The deny reason, sent as `reason` on the reject request and stored as the pending action's `rejection_reason`, follows the UI language. People read it in the AI Approvals inbox, and the model reads it as prose on its next turn. No code parses it. -- The handoff card's three strings are new keys. - -**Widened public surface (`@object-ui/i18n`).** Four new keys in all ten language packs, so the exported `en` pack and the `TranslationKeys` type derived from it gain four members: - -- `console.ai.toolDenyReason` -- `console.ai.builderHandoffTitle` -- `console.ai.builderHandoffOpen` -- `console.ai.builderHandoffSuperseded` - -No component prop or exported type changes. `ChatbotEnhanced` already took all six strings as props. diff --git a/.changeset/11674-shortcuts-dialog-wired-only.md b/.changeset/11674-shortcuts-dialog-wired-only.md deleted file mode 100644 index c55ad8b181..0000000000 --- a/.changeset/11674-shortcuts-dialog-wired-only.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -The keyboard-shortcuts dialog (`?`) lists only shortcuts that do something (objectui#11674). - -**Clause-②: no (narrowing)**: no accept set changes; the dialog shows fewer rows, and seven unused `console.shortcuts.*` keys leave all ten locale packs. - -The dialog was a static list, separate from every key handler, and six of its rows did nothing. `N` (create record), `R` (refresh data), `⌘/Ctrl+E` (edit record), `⌘/Ctrl+/` (focus search) and `⌘/Ctrl+D` (dark mode) had no handler anywhere, and the sidebar row said `B` while the sidebar toggles on `⌘/Ctrl+B`. The AI-assistant rows (`⌘/Ctrl+Shift+O`, `⌘/Ctrl+Shift+S`) were listed inside apps, where their only handler, on the AI chat page, is not mounted. - -`@object-ui/app-shell`: each shortcut is now advertised beside its handler, for as long as that handler is mounted, and `KeyboardShortcutsDialog` lists what is advertised. Inside an app it lists `⌘K` (command palette), `?` (this dialog), `Esc` (close a dialog or panel) and `⌘B` (toggle the sidebar). The AI chat page advertises its own two shortcuts, so they are listed only where that page is mounted. Rows are grouped as before and sorted by their text. A host that mounts `KeyboardShortcutsDialog` outside the console layout now sees the shortcuts whose handlers it mounts, not a fixed list. - -`@object-ui/i18n` (BREAKING; `minor` under this repository's release model, where objectui's major follows the `@objectstack` major): `console.shortcuts.focusSearch`, `createRecord`, `refreshData`, `editRecord`, `toggleDarkMode`, `groups.dataViews` and `groups.preferences` are removed from all ten packs. Nothing reads them now. A host that calls `t()` with one of them gets the key back; supply the string from your own resources if you still need it. diff --git a/.changeset/11675-week-start-from-locale.md b/.changeset/11675-week-start-from-locale.md deleted file mode 100644 index 879cb68bb3..0000000000 --- a/.changeset/11675-week-start-from-locale.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/i18n': minor -'@object-ui/plugin-calendar': minor -'@object-ui/plugin-timeline': minor ---- - -The calendar and the timeline start the week on the first day of the week of the user's locale (objectui#11675), instead of a fixed Sunday (calendar) and a fixed Monday (timeline). Under `en-US` both start on Sunday; under `en-GB` or `zh-CN` both start on Monday; under `ar-EG` both start on Saturday. There is no separate week-start setting: the first day comes from the locale the dates are already formatted with. - -- **`@object-ui/i18n` exports `firstDayOfWeek(locale)` and its `WeekdayIndex` type.** It returns the first day of the week for a BCP-47 tag, numbered as `Date.prototype.getDay` numbers weekdays (0 is Sunday), which is also what react-day-picker's `weekStartsOn` takes. It reads the engine's `Intl.Locale` week info (`getWeekInfo()`, else the `weekInfo` accessor), including a `-u-fw-` keyword on the tag. Where the engine has neither, it reads CLDR's first-day table by the tag's region, or the region CLDR's likely subtags give it (`en` reads `US`, `zh` reads `CN`), and a region the table does not list reads Monday, CLDR's world default. A malformed tag throws the same `RangeError` that formatting a date with it would. -- **The calendar's weeks start on the locale's first day.** `CalendarView` (and `object-calendar` through it) reads the first day from its `locale` prop, else the display locale, and the month grid's rows, its weekday heads, the week view's columns, the header's week range, the day a span that wraps into a new row shows its title on, and the header's date popover all start there. The popover used to take date-fns's own week start for the tag, which is not CLDR's for every tag (date-fns reads `es-MX` as `es`, a Monday start, where CLDR starts Mexico's week on Sunday) and which read Sunday until the date-fns locale had loaded. -- **The object timeline's "This week" and "Next week" start on the display locale's first day.** The bucket bounds are also stepped on the local calendar now, so across a DST change the day after it is "Tomorrow" again, and the first day of next week no longer falls into "This week". - -A host that relied on the calendar always starting on Sunday, or on the timeline's week always starting on Monday, now sees the locale's first day. The gantt variant's `week` axis is unchanged: it still counts plan weeks from the axis's first day. diff --git a/.changeset/11676-retire-overdue-key.md b/.changeset/11676-retire-overdue-key.md deleted file mode 100644 index ff03ca2232..0000000000 --- a/.changeset/11676-retire-overdue-key.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/i18n': minor -'@object-ui/plugin-timeline': minor ---- - -The timeline's unused "Overdue" bucket label is removed: `timeline.bucket.overdue` is gone from all ten language packs and from the timeline's built-in English defaults (objectui#11676). - -Nothing has read the key since a past date on a timeline went under "Earlier". No timeline heads a group "Overdue": a date is overdue only when it is a due date and the record is still open, and a timeline declares neither fact. Overdue records are still marked where record state lives: the date cell's due treatment, the gantt's alert colour and conditional formatting. - -**Narrowed public surface (`@object-ui/i18n`).** The exported `en` pack and the `TranslationKeys` type derived from it lose one member, `timeline.bucket.overdue`. Code that reads `en.timeline.bucket.overdue`, or passes that key to `t()` expecting a translation, has to drop it. This is released as `minor` under objectui's version policy, which keeps the major aligned with `@objectstack`. The date cell's "Overdue 3d" phrase is a different key, `fields.relativeDate.overdue`, and stays in every pack. diff --git a/.changeset/11676-timeline-past-bucket.md b/.changeset/11676-timeline-past-bucket.md deleted file mode 100644 index 1668a17c16..0000000000 --- a/.changeset/11676-timeline-past-bucket.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/plugin-timeline': minor -'@object-ui/i18n': minor ---- - -An object-bound timeline no longer heads every past date "Overdue": a day before today goes under a neutral "Earlier" bucket, translated in every language (objectui#11676). - -Without a `groupByField`, `ObjectTimeline` groups its entries into date buckets, and every day before today went under "Overdue", whatever the date meant. The showcase's Activity Timeline, bound to `created_at`, put all ten tasks under "Overdue", the two Done ones included. A creation date cannot be overdue, and a closed record cannot be either. - -"Overdue" needs two facts: that the date is a due date, and that the record is still open. Nothing the timeline reads declares either one. The timeline configuration (`startDateField`, `endDateField`, `titleField`, `groupByField`, `colorField`, `scale`) names no due-date role, and no field option marks a closed state. So a past day is now "Earlier", and the timeline does not guess either fact from a field name or a status value. Today, Tomorrow, This week, Next week, Later and No date are unchanged. - -**Widened public surface (`@object-ui/i18n`).** One new key in all ten language packs, `timeline.bucket.earlier`, so the exported `en` pack and the `TranslationKeys` type derived from it gain one member. The timeline no longer reads `timeline.bucket.overdue`, and this release also removes that key from every pack (the "Overdue" bucket label retirement entry). No component prop or exported type changes. diff --git a/.changeset/11677-approvals-summary-faces.md b/.changeset/11677-approvals-summary-faces.md deleted file mode 100644 index aba889b577..0000000000 --- a/.changeset/11677-approvals-summary-faces.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The approvals inbox's request drawer shows its record summary the way the record page shows the record (objectui#11677). - -The summary card now reads the field declarations of the request's object. It asks the same cached metadata read the drawer already makes for `hidden: true`, so no network request is added. With those declarations: - -- **Labels.** Each row takes its field's declared label, translated as the record page translates it, instead of the server's snapshot label or a title-cased key. -- **Values.** Each value is drawn by the record page's own cell for its field type. An option shows its label (`Sent`, `EMEA` instead of `sent`, `emea`), a date shows as the date cell shows it, and a currency field shows in its currency. A lookup still shows the record title the server resolved. The lead amount at the top of the card follows the same rule. A `summary` roll-up shows as the record page shows it: the plain stored number, without digit grouping. -- **Rows.** A snapshot key the object does not declare gets no row. Neither does a reference the server could not resolve to a title, because its value is an id. - -If the object's metadata cannot be read, the card renders the snapshot as before. - -Two rows never share a label. The card now drops the platform's injected ownership column `owner_id` (label "Owner") as bookkeeping, the same way the default list columns do, so an object that also declares its own `owner` field no longer shows two "Owner" rows. If two remaining fields still share a label, each of those rows adds its field key in parentheses. The rows are keyed by field key rather than by label, so React no longer warns about two children with the same key. diff --git a/.changeset/11678-recent-items-identity.md b/.changeset/11678-recent-items-identity.md deleted file mode 100644 index f73eb84a08..0000000000 --- a/.changeset/11678-recent-items-identity.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -"Recently Accessed" shows each item's own label, in the current language, and a visit that does not change the list no longer writes the `ui.recent` preference (objectui#11678). - -The route tracker used to store a label it made from the route's machine name at visit time: a dashboard labelled "Delivery Operations" was listed as "Showcase Ops Dashboard", and stayed in English after switching to 中文. An object, dashboard, page or report entry now stores its identity only, `type` and `name`, and every surface that lists recent items (the Home cards and rail, the sidebar's Recent group, Studio home, the command palette) labels it when it renders, through the new `useRecentItemLabel` hook. That hook reuses the resolver each kind already has: the navigation target label for an object or a dashboard, and the page's or report's metadata label through the `pageLabel` / `reportLabel` bundle lookup. An item whose metadata is not loaded shows its machine name until it is. A record keeps the title it was visited under, and a Studio metadata item its name. - -Lists already stored in the old shape are read by identity, which was always in the entry's `id`; their stored label is dropped, so they show the current label on first render. - -`RecentItemsProvider` now refuses to write a list equal to the one it holds, whoever reports the visit: revisiting the item already at the head of the list changes nothing and writes nothing, and its `visitedAt` stays the time it became the most recent entry. Going from an object's list to one of its views no longer saves the list a second time. - -**Breaking (types):** `RecentItem` is now a union. `RecentNamedItem` (`object`, `dashboard`, `page`, `report`) carries `name` and no `label`; `RecentTextItem` (`record`, `metadata`) carries `label`. `addRecentItem` takes `RecentItemInput`, so a call that passes a `label` for an object, dashboard, page or report no longer compiles, and code that reads `item.label` off a recent entry should call `useRecentItemLabel()` instead. `useRecentItemLabel`, `RecentItemLabelResolver`, `RecentItemInput`, `RecentItemType`, `RecentNamedItem` and `RecentTextItem` are new exports of `@object-ui/app-shell`. - -**Clause-②: yes (narrowing).** The package entry gains six exports, and `addRecentItem` refuses the label-carrying shape it used to take for those four kinds. diff --git a/.changeset/11679-console-settings-icons.md b/.changeset/11679-console-settings-icons.md deleted file mode 100644 index 5b3239e368..0000000000 --- a/.changeset/11679-console-settings-icons.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/console': minor -'@object-ui/app-shell': minor ---- - -The console's settings pages resolve icons through the shared `getLazyIcon` helper and no longer log `[lucide-react]: Name in Lucide DynamicIcon not found` (objectui#11679). - -- **Settings hub and namespace pages.** The settings hub, each settings namespace page and its action buttons used the console's own icon helper. That helper passed every name to lucide's `DynamicIcon` without checking that Lucide has it. Its tokeniser also turned `Building2`, the icon the framework's company settings declare, into `building2`, which is not a Lucide name. `DynamicIcon` then logged the error above on the settings hub and on the company page. It drew a bare fallback glyph that ignored the page's sizing classes. The pages now use `getLazyIcon` from `@object-ui/components`: `Building2` shows the building icon, and a name Lucide does not have shows the `Database` fallback at the page's size, with no console error. The console's own helper is removed. -- **App shell icons.** App-shell's internal `getIcon` now re-exports `getLazyIcon` instead of keeping a copy. Its copy already checked the name before calling `DynamicIcon`, but used the same tokeniser, so a digit-suffixed Lucide name such as `Building2` showed the `Database` fallback in the sidebar, app switcher and other shell chrome. Those names now resolve to their own icons. A name that resolved before resolves to the same icon; `lazy-icon-digit-boundary-9414.test.ts` in `@object-ui/components` re-derives that against the installed Lucide on every run. - -No export is added to or removed from either package's entry. diff --git a/.changeset/11680-placeholder-lazy-stub-guard.md b/.changeset/11680-placeholder-lazy-stub-guard.md deleted file mode 100644 index 0772865655..0000000000 --- a/.changeset/11680-placeholder-lazy-stub-guard.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/core': patch ---- - -An authored `view:calendar` or `view:timeline` node loads its plugin and renders the calendar or the timeline, and a console boot no longer logs the registry's race warning for those two keys (objectui#11680). - -The console declares both views as lazy stubs, then registers a protocol placeholder for each protocol key nothing renders yet. The placeholder registrar asked the registry about loaded components only, so it read the two stubbed keys as free and took them, and the registry cleared the stubs under them. Until some other node happened to load the calendar or the timeline chunk, an authored `view:calendar` or `view:timeline` drew the dashed "Component Placeholder" box instead of the view. `registerPlaceholders()` and the eager palette placeholders now skip a key a pending lazy stub holds (`ComponentRegistry.hasLazy`). The key stays with the plugin that declared it, and `SchemaRenderer` loads that plugin the first time the node renders. A protocol key that nothing declares still gets its placeholder. - -The registry's collision warnings now name a stub by the full type it declared. A stub found under its own namespaced key was named with its namespace written twice (`view:view:calendar`), and `register`, `registerLazy` and `unregister` compared ownership against that doubled spelling. - -**Clause-②: no.** No export is added or removed, and no accepted input changes. The placeholder registrar is not exported, and the field the registry now records on a lazy stub lives on a type the package does not export. diff --git a/.changeset/11682-auto-width-drawn-content.md b/.changeset/11682-auto-width-drawn-content.md deleted file mode 100644 index bf98e38f64..0000000000 --- a/.changeset/11682-auto-width-drawn-content.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/components': patch ---- - -The data table sizes an auto-width column from what its cells draw, and a right-pinned column no longer covers another one (objectui#11682). - -**Auto width.** A column with no `width` was estimated from the length of its stored value. A currency cell stores `200000` and draws `200,000.00`, so it truncated. A lookup stores an id and draws a name, and a date stores an ISO string and draws `Feb 3, 2027`, so those columns were too wide. On the showcase's Tasks list at a 1440px viewport the columns added up to more than the list's width, and the right-aligned Progress percentage sat under the right-pinned Actions column. - -The table now reads back what each auto-sized column's header and cells drew. Where the page is laid out, the column gets the width its widest drawn cell and its header need to render whole, padding included. Where nothing is laid out, the drawn text's length is the estimate's input, at the same 8px a character as before. The stored value is read only before the first draw. The 80px floor and the 400px cap stay. A masked column is still sized from its header alone. An explicit `width`, a `fitContent` column and a width the user dragged are unchanged. The width follows the rows that are drawn, so on a table that pages, searches or sorts its own rows it can change with the page, the search or the sort. - -**Right-pinned columns.** Every right-pinned column stuck at `right: 0`, so a column an author pinned right beside the auto-pinned row-actions column was drawn under it. Each one now sticks at the measured width of the pinned columns after it, as left-pinned columns already do. A table with one right-pinned column renders as before. - -Nothing is added to the package entry: no export, prop, type member or schema key. diff --git a/.changeset/11683-formula-datetime-cell-text.md b/.changeset/11683-formula-datetime-cell-text.md deleted file mode 100644 index 8b8695261e..0000000000 --- a/.changeset/11683-formula-datetime-cell-text.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/fields': patch ---- - -Two cells now draw the right text (objectui#11683). - -- **Formula cell (`FormulaCellRenderer`).** A numeric result is drawn by `NumberCellRenderer`, so it is grouped in the viewer's locale and set in the number cell's tabular figures, no longer printed raw in monospace: a formula with no `returnType` holding `200000` reads `200,000` in en-US and zh-CN. A result counts as numeric when the field declares `returnType: 'number'`, or when it declares no `returnType` and the value is a JS number. A declared `scale` sets the width, as on a number field. A string of digits from a formula with no `returnType` stays text, and any other declared `returnType` is drawn as before. `summary` fields use this renderer, so a numeric roll-up is formatted the same way. No type is inferred from the formula's expression or its inputs: the spec's `returnType` has no currency value, so a formula over currency fields reads as a plain number. -- **Datetime cell (`DateTimeCellRenderer`).** On the compact face, the default, the date and the time are separated by a space in the text, not by a margin alone. Copied text, screen readers and `textContent` get `10/6/2026 1:42 am` (en-US) and `2026/10/6 上午1:42` (zh-CN) where they got the two halves run together. The text is now exactly what `formatDateTime(value, { style: 'compact' })` returns. The time keeps its muted colour; its margin narrows from `ml-2` to `ml-1` beside the space. - -No export, prop or language-pack key is added. diff --git a/.changeset/11684-highlight-share-row.md b/.changeset/11684-highlight-share-row.md deleted file mode 100644 index 8b67b81c8c..0000000000 --- a/.changeset/11684-highlight-share-row.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -**The record header's highlight chips share the row's free width and truncate only when it runs out (objectui#11684).** A Product with SKU "QA Widget 1" read "QA Wid…" in the record drawer's highlight row, with most of the row empty. Every chip in `HeaderHighlight` was a fixed column: a 9rem basis (16rem for wide types and for a column being edited), no grow, and a 16rem / 24rem cap. A value wider than the chip was clipped, however much room the row had left. - -A chip's basis is now its floor, and the chips grow into the line's free width. Each chip is capped at its own content, so a chip that fits stops growing and the rest of the free width goes to the chips that still need it. Which chips share a line does not change, because line breaking still reads the floors. A short value keeps its 9rem column, and a sparse strip still packs left, because no chip is ever wider than what it shows. A value is cut with an ellipsis only once its line has no free width left. The whole value stays available as the hover title. A column being edited still takes the 16rem floor, and it can grow to its editor's natural width. diff --git a/.changeset/11685-toast-clears-drawer-chrome.md b/.changeset/11685-toast-clears-drawer-chrome.md deleted file mode 100644 index b91ed85788..0000000000 --- a/.changeset/11685-toast-clears-drawer-chrome.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The console's success toast no longer covers an open drawer's expand and close buttons and title (objectui#11685). - -The toaster stays in the top-right corner (objectui#7482 moved it there, away from the assistant composer). That corner is also where every right-side drawer keeps its chrome, so a toast raised with the record drawer open, after creating a record or running a record action such as Approve, sat on the drawer's expand and close buttons, its title and the record's own header actions. While a right-edge drawer is open, `ConsoleToaster` now offsets the toaster instead: - -- When the page left of the drawer is wide enough for a toast, the toaster moves into that strip, against the drawer's left edge. It covers no part of the drawer, and the strip is under the drawer's overlay, so no control sits there. -- When that strip is too narrow (a phone, or a drawer nearly as wide as the window), the toaster drops below the drawer's header. - -When the drawer closes, the toaster returns to the corner. It follows the drawer when the window is resized or the drawer is dragged wider or narrower. Every `side="right"` sheet in the console gets the same treatment, the record drawer included. Centred dialogs, popovers, and left or bottom sheets leave the toaster where it is. An `offset` passed to `ConsoleToaster` by its caller still wins. - -Nothing is added to the package entry: no export, prop, type member or language-pack key. diff --git a/.changeset/11686-tree-cell-display-faces.md b/.changeset/11686-tree-cell-display-faces.md deleted file mode 100644 index 178ce26b36..0000000000 --- a/.changeset/11686-tree-cell-display-faces.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/plugin-tree': patch ---- - -fix(plugin-tree): tree cells draw the same field faces as list cells, so a boolean no longer shows as `true` (objectui#11686) - -A tree view, such as the Org Chart tab on Setup → Business Units, printed most column values as raw text. Its cells knew only option labels and referenced records; every other type was printed as stored, so the boolean "Active" column read `true`, and dates, numbers and currency amounts showed their stored form while the list tabs on the same page drew them properly. - -Each tree cell whose column the object defines now draws through the field package's cell display for that field type, the same one the grid, kanban and gallery cards use. A boolean draws the read-only checkbox, a date the locale-aware date, a currency the formatted amount, a select its option badge and a lookup the referenced record's name. Where a display names its own column, as the boolean display's badge for an inactive `active` field does, it uses the column label the tree's header shows, so it reads in the session's language. A column the object does not define keeps the plain text it showed before. - -`@object-ui/plugin-tree` now declares `@object-ui/fields` as a dependency; it already reached it through `@object-ui/plugin-detail`. Nothing is added to the package entry: no export, prop, type member or language-pack key. diff --git a/.changeset/11687-empty-state-copy-matches-page.md b/.changeset/11687-empty-state-copy-matches-page.md deleted file mode 100644 index caaa42978c..0000000000 --- a/.changeset/11687-empty-state-copy-matches-page.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/plugin-list': minor -'@object-ui/i18n': minor ---- - -An empty list's copy no longer contradicts the page it sits on (objectui#11687). - -An identity list (an object managed by the authentication provider) said its records are "not added by hand here", even beside an "Invite User" or "Register OAuth Application" button on the same page. The console now asks whether the page offers a way to add a row: its New button, or a toolbar action the page draws, judged by the same placement, capability and `visible` gates the toolbar applies. When it does, the identity copy is not used and the list shows its own empty state instead. When the toolbar action is hidden (for example a multi-organization-only Invite User in a single-organization deployment), the identity copy stays. The Teams list keeps its own copy, which already names its Create Team button. - -A list view emptied by its own declared filter said "No records match your current filters or search." when no filter or search had been applied. That message is now said only when the user applied a filter or a search. A view emptied by its own filter alone keeps the "No matching records" title and reads "No records match this view’s filter.", through a new `list.viewFilterNoMatchesMessage` key in all ten language packs and in `LIST_DEFAULT_TRANSLATIONS`. A list with no filter at all still gets the first-run copy. diff --git a/.changeset/11688-marketplace-load-error-cause.md b/.changeset/11688-marketplace-load-error-cause.md deleted file mode 100644 index b969dc64de..0000000000 --- a/.changeset/11688-marketplace-load-error-cause.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Browse Marketplace now says why the catalog failed to load, and no longer draws "No apps have been approved for the marketplace yet." under the error (objectui#11688). - -A refusal the server answered in plain text, such as an egress proxy's `403` "Host not in allowlist: cloud.objectos.ai", was shown as the bare status text ("Forbidden"), and every load failure carried the "check that it is online" hint. Now: - -- the error shows the server's own text: a JSON error's message, as before, or the body of a `text/plain` refusal. Any other body that is not JSON, such as an HTML error page, still shows the status text; -- the "check that it is online" hint is shown only when no server answered (a network failure, a CORS refusal) or the server answered `502`, `503` or `504`. A refusal such as a `401` or `403` shows its cause without it; -- the empty state is drawn only when the catalog loaded empty, and the "Your organization" packages and the installed count still appear when the public catalog fails to load. - -The package detail page reads its load through the same request helper, so a plain-text refusal there now shows its text too. diff --git a/.changeset/11689-boolean-greeting-locale.md b/.changeset/11689-boolean-greeting-locale.md deleted file mode 100644 index 67b6f59fc3..0000000000 --- a/.changeset/11689-boolean-greeting-locale.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/fields': minor -'@object-ui/plugin-detail': minor -'@object-ui/plugin-grid': minor -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -Read-only boolean values, the boolean list face and the Home greeting's punctuation follow the language (objectui#11689). - -Under zh-CN a record showed its boolean values as "Yes" / "No", and the Home greeting joined a Chinese greeting to the person's name with an ASCII comma and closed it with an ASCII period. - -- **Booleans.** The read-only surfaces that draw a boolean as a word now read the existing `common.yes` / `common.no` keys: `BooleanField`'s readonly display, a `FormulaField` whose `returnType` is `boolean`, the lookup column's plain-text fallback, and the record-detail highlights chip. zh-CN shows the Chinese words; English is unchanged. Without an `I18nProvider` the words stay "Yes" / "No". -- **The boolean list face.** `BooleanCellRenderer`, which every list surface draws, spelled its status badge ("Active — Off") and its completion indicator's accessible names ("Completed" / "Not completed") in English. All three now come from the language packs. The grid also handed that face the authored field label while its header printed the translated one, so the badge named the column in the authored language under a translated header. The face now receives the label the header prints. -- **Greeting.** The comma before the name and the closing mark are two new keys, so zh-CN reads the full-width comma and full stop, Japanese its own marks and Arabic its own comma. English is unchanged, and the name keeps its own colour. - -**Widened public surface.** - -- `@object-ui/i18n`: five new keys in all ten language packs, so the exported `en` pack and the `TranslationKeys` type derived from it gain `home.greetingSeparator`, `home.greetingEnd`, `fields.boolean.offBadge`, `fields.boolean.completed` and `fields.boolean.notCompleted`. -- `@object-ui/fields`: a new export, `useBooleanValueLabel()`, with its type `BooleanValueLabel`. It returns the current language's word for a boolean value, and `@object-ui/plugin-detail`'s highlights chip reads it. diff --git a/.changeset/11690-a11y-names-roles.md b/.changeset/11690-a11y-names-roles.md deleted file mode 100644 index d9e7b1480e..0000000000 --- a/.changeset/11690-a11y-names-roles.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/plugin-view': minor -'@object-ui/console': minor -'@object-ui/layout': minor -'@object-ui/components': minor -'@object-ui/fields': minor ---- - -Screen readers can name and reach the controls of the view tab bar, the settings form, the sidebar menus, the table's selection column and the percent cell (objectui#11690). axe-core (wcag2a + wcag2aa) on an object list page and on a Setup settings page reported the faults below; each is fixed where it is produced and pinned by an axe run on that component. - -- **View tab bar (`ViewTabBar`).** The "+" add-view button is named, through the existing `view.addView` key, and its tooltip reads the same translated words instead of a hard-coded "Add View". Each view is now a `