@object-ui/react 17.2.0 → 17.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +956 -0
  2. package/README.md +39 -13
  3. package/dist/SchemaRenderer.d.ts +78 -5
  4. package/dist/SchemaRenderer.js +139 -13
  5. package/dist/context/AppShellContext.d.ts +21 -2
  6. package/dist/context/RecordContext.d.ts +0 -4
  7. package/dist/context/RecordContext.js +0 -2
  8. package/dist/context/RelatedRecordActionsContext.d.ts +29 -0
  9. package/dist/context/RelatedRecordActionsContext.js +6 -0
  10. package/dist/element-data-source/ElementDataSourceGate.d.ts +199 -0
  11. package/dist/element-data-source/ElementDataSourceGate.js +195 -0
  12. package/dist/hooks/index.d.ts +3 -0
  13. package/dist/hooks/index.js +10 -0
  14. package/dist/hooks/useActionTextLocalizer.d.ts +69 -0
  15. package/dist/hooks/useActionTextLocalizer.js +91 -0
  16. package/dist/hooks/useDatasetDimensionLabels.d.ts +104 -0
  17. package/dist/hooks/useDatasetDimensionLabels.js +154 -0
  18. package/dist/hooks/useElementDataSource.d.ts +68 -0
  19. package/dist/hooks/useElementDataSource.js +138 -0
  20. package/dist/hooks/useExpression.d.ts +94 -17
  21. package/dist/hooks/useExpression.js +96 -34
  22. package/dist/hooks/useNavigationOverlay.d.ts +58 -10
  23. package/dist/hooks/useOffline.d.ts +62 -36
  24. package/dist/hooks/useOffline.js +2 -2
  25. package/dist/hooks/usePageAssignment.js +4 -4
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.js +5 -1
  28. package/dist/schema-input.d.ts +31 -0
  29. package/dist/schema-input.js +31 -0
  30. package/dist/spec-bridge/SpecBridge.d.ts +11 -5
  31. package/dist/spec-bridge/SpecBridge.js +7 -1
  32. package/dist/spec-bridge/bridges/list-view.js +106 -14
  33. package/dist/spec-bridge/types.d.ts +24 -3
  34. package/dist/utils/i18n.d.ts +43 -8
  35. package/dist/utils/i18n.js +42 -4
  36. package/package.json +10 -10
package/CHANGELOG.md CHANGED
@@ -1,5 +1,946 @@
1
1
  # @object-ui/react
2
2
 
3
+ ## 17.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d9d3463: Retire four zero-consumer declared surfaces (dead-surface sweep batch 3, #4328). Each was
8
+ measured as declared-but-never-read at the branch point, and each is removed rather than
9
+ left as an authoring surface whose values nothing acts on.
10
+
11
+ Breaking for anyone who typed against the removed declarations, marked `minor` per this
12
+ repository's version-alignment convention (the major tracks `@objectstack`, never an
13
+ API-break count):
14
+
15
+ - `@object-ui/core` no longer exports `mergeViewsIntoObjects`. It was a second copy left
16
+ behind by the move of that step to the provider layer, and it had drifted: it ignored a
17
+ view container's default `list` and keyed views by the authored bare key instead of the
18
+ composer's `<object>.<key>` identity. The live implementation — `MetadataProvider`'s, in
19
+ `@object-ui/app-shell` — is unchanged and remains the only one. (#3775)
20
+ - `@object-ui/types`' `RoleDefinition` no longer declares `permissions`. A role's grants
21
+ live in `ObjectPermissionConfig.roles`, keyed by object; that is the only home any
22
+ consumer reads (`resolveRoles` walks `inherits` and matches on `name`). The removed
23
+ field was _required_, so five fixtures across three packages had been declaring an empty
24
+ array for a value nothing would ever look at. Role-attached grants are now a compile
25
+ error rather than silently ignored data. (#4288)
26
+ - `@object-ui/react`'s `RecordContextValue` no longer declares `loading` / `error`. Both
27
+ had zero producers and zero consumers — no host passed them, no `record:*` renderer read
28
+ them — and only the provider's memo dependency list still named them. Record-level
29
+ loading and error state stays where it is actually expressed: each renderer's own data
30
+ source. (#3773)
31
+
32
+ No behaviour change, no request-count change:
33
+
34
+ - `@object-ui/data-objectstack` drops five `metadataCache.invalidate('views:<object>')`
35
+ calls across `updateViewConfig` / `createView` / `updateView` / `deleteView`. No read
36
+ path has ever populated that key — `listViews` fetches directly, uncached — so all five
37
+ were permanent no-ops. The invalidations of the keys that do have readers
38
+ (`view:<object>:<viewId>` for `getView`, `view-overrides:<object>` for
39
+ `listViewOverrides`) are untouched and now pinned. (#3778)
40
+
41
+ - b953a97: fix(detail): lookup field values link to the referenced record
42
+
43
+ A valued lookup on a record detail page rendered as plain text plus a copy
44
+ button — the referenced document's name was visible but unreachable, so users
45
+ copied the number and searched for it from the list page instead. Lookup cells
46
+ inside a related list that pointed at a third object were dead the same way.
47
+
48
+ `LookupCellRenderer` — the one cell renderer both surfaces resolve through —
49
+ now renders the display value as a link to the referenced record. The display
50
+ name resolution, the copy affordance and every non-lookup field are unchanged,
51
+ and a lookup with no value still renders its placeholder rather than an empty
52
+ link.
53
+
54
+ The URL is not assembled in the renderer. `RelatedRecordActionsContext` gains
55
+ an optional `recordHref` / `openRecord` pair, published by the console's
56
+ `RelatedRecordActionsBridge` from the SAME builder its related-list row
57
+ navigation already used, so there is one record-route shape rather than a
58
+ second one. A host that does not provide it (Studio designer, embedded
59
+ renderers, standalone grids) renders exactly what it rendered before.
60
+
61
+ - d7f3e30: `bridgeListView` maps the five row heights the spec admits, and only those — the four dead spellings are gone
62
+
63
+ `mapDensity` carried a nine-key table: `compact`, `short`, `comfortable`,
64
+ `spacious`, `small`, `medium`, `large`, `tall`, `extra_tall`. `RowHeightSchema`
65
+ in `@objectstack/spec` admits five — `short | compact | medium | tall |
66
+ extra_tall` — so `comfortable`, `spacious`, `small` and `large` were unreachable
67
+ from any spec-valid list view. The bridge's own parameter type said as much
68
+ (`Partial< ListView >`), and `mapDensity` widened it back to `rowHeight?: string`
69
+ to let them in. They survived because the fixture asserting three of them
70
+ compiled against nothing: the package's build tsconfig excludes tests and no
71
+ other `tsc` read them, so four branches of renderer-side dialect read as live
72
+ capability (objectui#4352, surfaced by objectui#4040 / PR #4351).
73
+
74
+ They are deleted. The parameter takes the spec type honestly, and the table is
75
+ now `Record< RowHeight, … >`, so a row height added upstream fails the build here
76
+ instead of arriving with no density. This is AGENTS.md #0.1: a lenient reading
77
+ for off-spec metadata is a second de-facto contract, and one strict contract
78
+ beats N dialects — a bad `rowHeight` gets fixed at the producer, where the schema
79
+ already rejects it.
80
+
81
+ **Breaking semantics, deliberately graded `minor`** (this repo never publishes
82
+ `major` — its major tracks `@objectstack`). Nothing narrows in the published type
83
+ surface: `mapDensity` is module-local and never appeared in the emitted `.d.ts`,
84
+ and `bridgeListView`'s declaration is unchanged. What changes is runtime output,
85
+ and only for input the spec already rejects: a host handing the bridge
86
+ `rowHeight: 'comfortable'` (or `'spacious'` / `'small'` / `'large'`) used to get
87
+ `density: 'comfortable'` / `'spacious'` / `'compact'` back, and now gets no
88
+ `density` key at all, so the renderer's own default applies. A sweep of this repo,
89
+ the `objectstack` example apps and the console's view metadata found zero authored
90
+ uses of any of the four; the legacy `densityMode` alias cannot produce one either,
91
+ since `DENSITY_MODE_TO_ROW_HEIGHT` is typed `Record< DensityMode, RowHeight >` and
92
+ folds onto `compact` / `medium` / `tall`.
93
+
94
+ - a84385b: `NavigationConfig.mode` is optional — the type now says what the hook does
95
+
96
+ `@object-ui/react` published a `NavigationConfig` that required `mode`, in front of a `useNavigationOverlay` that has always defaulted it. The declaration took the spec's authored config, `Omit`ted `mode`, and re-added it as `NonNullable< … >`; 140 lines below, the hook read `navigation?.mode ?? 'page'`. The type was strictly stricter than the implementation it fronted, and `'page'` is meaningful behaviour rather than a placeholder.
97
+
98
+ The spec never asked for that. `NavigationConfigSchema` declares `mode: NavigationModeSchema.default('page')`, and a `.default()` lands on the authoring side as `| undefined` — so `navigation: { view: 'summary_view' }` is legal authored metadata that lets the mode default. `@object-ui/types` already re-exported the spec's own `NavigationConfig` unchanged, which meant one monorepo shipped two published types of the same name that disagreed about whether `mode` could be omitted.
99
+
100
+ The alias is now the spec's authored config verbatim, with no divergence of its own:
101
+
102
+ ```ts
103
+ export type NavigationConfig = SpecAuthoredInput<
104
+ typeof NavigationConfigSchema
105
+ >;
106
+ ```
107
+
108
+ The cost of the old spelling was paid by callers. `ListView` carried `schema.navigation as NavigationConfig | undefined` for no reason except to get a valid spec-shaped value past the declaration; that assertion is deleted here, not replaced. A type in front of an implementation must not be stricter than the implementation — when it is, every caller pays in casts, and a cast is exactly the renderer-side workaround that belongs back at the producer.
109
+
110
+ **Nothing changes at runtime.** `navigation?.mode ?? 'page'` is untouched, and the default is now pinned as observable behaviour (`useNavigationOverlay.modeDefault.test.tsx`) rather than only as a comment — the explicit modes, the `preventNavigation` and `none` short-circuits, the `onRowClick` priority, and the Cmd/Ctrl/middle-click and `new_window` branches are all pinned alongside it.
111
+
112
+ **Why minor rather than patch**, from the measured `.d.ts`. Optional-izing a property is looser for writers and narrower for readers, so the grade turns on which role the published surface actually plays. In this package `NavigationConfig` occurs only in input positions — `useNavigationOverlay`'s `navigation?:` option and `resolveOverlayWidth`'s parameter — and never in a return type; the package consumes these values and never hands one back. For consumers the change is therefore purely permissive: every call that compiled before still compiles, and spec-shaped configs that previously needed an assertion now compile without one. That gained input shape is a real capability rather than an internal repair, which is more than a patch describes. The reader-side narrowing is real but secondary: code that imports the bare type, annotates its own value with it and reads `.mode` now sees `NavigationMode | undefined`. The in-repo census found exactly one such importer — `ListView` — and it imported the type only to write the assertion this change removes.
113
+
114
+ - c1d939f: One `SchemaNode`, and one label vocabulary — the union wins, and labels resolve where the locale lives
115
+
116
+ Two packages published a type called `SchemaNode` and they were not the same type. `@object-ui/core` hand-declared `interface SchemaNode { type: string; … [key: string]: any }`; `@object-ui/types` exported `type SchemaNode = BaseSchema | string | number | boolean | null | undefined`, whose own doc comment names `'Plain string'` a valid node. Both were exported under one name from packages the same consumers import together, so which declaration a call site got depended on which package it happened to import from — #4548's canary measured 19 of 35 errors as exactly that collision. Core's declaration is now a re-export of types', so there is one declaration left to disagree with. Core's entry surface is unchanged: `dist/index.d.ts` is byte-identical across the change.
117
+
118
+ Reconciling it exposed a real defect rather than a mechanical narrowing, which is why the first attempt was withdrawn instead of forced. The spec bridges write `spec.label` — the spec's `I18nLabel`, an INLINE locale map like `{ en: 'Owner', 'zh-CN': '负责人' }` — into `node.label`, and `BaseSchema.label` declared `string`. Under core's old index signature that assignment was invisibly `any`; under one honest `SchemaNode` it is a type error. `BaseSchema.label` and `.description` therefore now accept `string | I18nLabel`, and the two bridge assignments compile with their expressions untouched.
119
+
120
+ Resolution happens at READ time, in the renderer, against the display locale — not at the bridge. Resolving at the bridge was measured unimplementable: it is a plain class method that cannot call a hook, `BridgeContext` declares no locale, and `updateContext()` has zero callers, so a bridge-resolved label would freeze one audience's language into the node tree with no re-translation channel. React's own invalidation re-translates for free at the read site.
121
+
122
+ The widening turned every blind `schema.label`-as-string read into a named compiler error, and that inventory is the audit: it named four sites repo-wide, all one class — the label reaching a React child position, where a map does not render as `[object Object]` but THROWS `Objects are not valid as a React child`, failing the whole subtree. Three are `@object-ui/components` renderers (`filter-builder`, `sidebar-group`, `dropdown-menu`), which now resolve with the spec's own `resolveI18nLabel` against `useDisplayLocale()`. The fourth is `plugin-dashboard`'s `DashboardGridLayout` heading, which resolves with `pickLocalized` against the active UI language — matching the widget-title resolution already in that same component rather than putting two resolvers and two disagreeing locale channels in one render; the two resolvers are limb-for-limb twins with a parity test pinning them.
123
+
124
+ One interface now carries both label vocabularies two properties apart — `label`/`description` are the spec's INLINE map, `ariaLabel` is the KEYED bundle reference — and each accepts the other's shape vacuously. That confusability is objectui#4167's known hazard, inherent to the spec's `I18nLabel` design; both shapes are named with cross-referenced doc comments stating which resolver owns which slot, and a pin asserts the two unions do not collapse into each other.
125
+
126
+ Finally, the spec bridges declare their return type as `BaseSchema` instead of the union. Both bridges end in a single `return node` on an object literal, so the union described nothing real while forcing a narrowing at every read — 272 mechanical errors across five suites in the first round. That change is a type annotation only; the emitted JavaScript is byte-identical.
127
+
128
+ - 3f5f87c: `SchemaRenderer` states its real contract — a typed, required `schema` and a deliberate forwarding surface
129
+
130
+ `SchemaRenderer` is the renderer loop: every registered SDUI component is rendered through it. It handed `forwardRef` a props type of `{ schema: SchemaNode } & Record<string, any>`, which puts `string` into `keyof Props`, so `'ref' extends keyof Props` was always true, React's `PropsWithoutRef` took its `Omit` branch, and `Omit` over a type carrying a string index signature keeps only the index signature. Every declared prop was erased. Measured on the pre-fix source: `keyof ComponentProps<typeof SchemaRenderer>` was `string` and `ComponentProps<typeof SchemaRenderer>['schema']` was `any`, while the type argument went on declaring `SchemaNode`. The other half is the same defect seen from the call site — `<SchemaRenderer />` with no schema at all, `<SchemaRenderer schema={12345} />`, and an arbitrary misspelled prop each type-checked in silence. This is objectui#4422 / PR #4438's trap in the most central component in the repo, spelled `Record<string, any>` rather than `[key: string]: any`, which is why every previous sweep's grep and both shipped guards' detector reported the site as clean.
131
+
132
+ Graded **minor, not major**, on objectui#4528's reasoning: the type argument has always DECLARED `schema`; the index signature erased it from the resolved type, and restoring what the declaration documents is a fix to the published contract rather than a contract break.
133
+
134
+ **The forwarding surface is kept, deliberately.** This component forwards every prop it does not read to the component the schema names, resolved at runtime from a plugin-extensible registry — `packages/react/README.md` documents exactly that, and `@object-ui/components`' form renderer consumes the `onSubmit` it shows being forwarded. Closing that surface would state a false contract and would force every leaf plugin's props into this package. So the two halves are separated: the `forwardRef` type argument is the honest `SchemaRendererProps`, with no index signature for `PropsWithoutRef` to collapse, and the open surface is stated once in an explicit export annotation, which nothing routes through `Omit`. The published `.d.ts` shows the erasure disappearing: `ForwardRefExoticComponent<Omit<{ schema: SchemaNode } & Record<string, any>, "ref"> & RefAttributes<any>>` becomes `ForwardRefExoticComponent<SchemaRendererProps & Record<string, any> & RefAttributes<any>>`.
135
+
136
+ `SchemaRendererProps.schema` is declared as `BaseSchema | string | null | undefined` — what this component actually handles. It previously declared `@object-ui/core`'s `SchemaNode` interface, which requires `type: string` and so contradicted the component's own early returns for strings and nullish, while every caller held `@object-ui/types`' wider union. The erasure hid that mismatch completely.
137
+
138
+ **One declared behaviour change.** A non-object, non-string primitive schema now renders as its own text. It previously fell through to the shallow copy `{ ...schema }`, which spreads a primitive to an empty object, lost the `type` the renderer then looked up, and surfaced the red "Unknown component type: undefined" box — an accident of the spread rather than a decision. The declared props type excludes `number` / `boolean` so no author is invited to pass them; the runtime handling is defence-in-depth for untyped callers and stored metadata. Strings, `null`, `undefined`, `0` and `false` render exactly as before, and an object naming an unregistered type still gets the error box; all four are pinned.
139
+
140
+ Latent defects the erasure had been hiding, each surfaced by the repo-wide type-check and fixed at its call site: `DashboardRenderer` cast its widget schema to `Record<string, any>`, dropping the `type` every branch of `getComponentSchema` sets; `DashboardGridLayout`'s equivalent now states its return type instead of inferring a union that admitted a shape with no `type`; and `ReportViewer` handed a section's `content` array to the renderer whole, so a multi-node section rendered the unknown-component box instead of its content — arrays are mapped rather than widened into the renderer's declared input.
141
+
142
+ A repo-wide structural guard replaces the two per-package siblings' blocked direction: it judges every `forwardRef` in `packages/*/src` (219 sites) and its detector resolves `Record<string, …>` and `string`-keyed mapped types in addition to literal index signatures — the spelling the previous detector went blind on. It judges the type argument only, where an index signature is an accidental eraser, and never an export annotation, where one is a stated contract.
143
+
144
+ - bb68488: Stop declaring 14 symbols under names `@objectstack/spec` owns at `17.0.0-rc.6`
145
+ (objectui#4167, objectstack#4115).
146
+
147
+ The rc.6 bump published nine names this repo already declared locally, on top of
148
+ four that predate it — `check:spec-symbols` reported all thirteen at once, and a
149
+ fourteenth (`GlobalFilterSchema`) appeared during the bump itself. Each was
150
+ triaged on its own rather than blanket-renamed, because the right answer differs
151
+ per symbol: five bind to the spec, three are renamed because the spec's
152
+ same-named export means something else, five arrive by derivation, and one is a
153
+ declared dialect with a written reason.
154
+
155
+ **Breaking for importers of `@object-ui/react`, `@object-ui/app-shell` and
156
+ `@object-ui/types`** — three exported names changed, because the spec exports the
157
+ same name for a _different_ thing:
158
+
159
+ | package | was | now | what the spec's same-named export actually is |
160
+ | :-------------------- | :----------------- | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
161
+ | `react` / `app-shell` | `MetadataState` | `MetadataCacheState` | a metadata item's LIFECYCLE state — `'draft' \| 'active' \| 'deprecated' \| 'archived'` (`MetadataStateSchema`, `@objectstack/spec/system`) |
162
+ | `react` / `app-shell` | `resolveI18nLabel` | `resolveKeyedI18nLabel` | a resolver for the INLINE per-locale map (`{ en: 'Owner', 'zh-CN': '负责人' }`) against a BCP-47 locale |
163
+ | `types` | `DateRangePreset` | `FilterBuilderDateRangePreset` | the thirteen HISTORICAL dashboard filter-bar presets; this one is the filter-builder set, which adds eight FUTURE windows the dashboard schema rejects |
164
+
165
+ `resolveI18nLabel` is the one where the collision had already started costing
166
+ something. rc.6 widened `I18nLabel` from `string` to
167
+ `string | Record< string, string >`, so the same authored value now reaches
168
+ either resolver — and each answers wrongly, silently, for the other's input: the
169
+ keyed one returns `undefined` for `{ en: 'Owner' }` (no `key`, no
170
+ `defaultValue`), and the spec's reads `key` / `defaultValue` / `params` as locale
171
+ tags. The rc.6 bump PR met this and aliased the spec's import as
172
+ `resolveInlineI18nLabel` in five files, with hand-written comments at two of
173
+ them. That is a review convention, which is what objectstack#4115 exists to
174
+ replace with a rule — so `Keyed` is now the counterpart of that `Inline`, and the
175
+ name says which vocabulary it resolves at every call site.
176
+
177
+ **Eleven keep their names and are now imported or derived from the spec** instead
178
+ of re-declared: `DATE_RANGE_PRESETS`, `NavigationMode`, `AddressValue`,
179
+ `BreakpointColumnMap`, `BreakpointOrderMap`, `KanbanConfig`, `CalendarConfig`,
180
+ `GanttConfig`, plus the three renamed above at their new names.
181
+
182
+ **Four of the copies were losing information, not just duplicating it.**
183
+
184
+ - **`GanttConfig` declared six keys and called itself canonical; rc.6's
185
+ `GanttConfigSchema` declares seventeen.** The eleven it never mentioned —
186
+ `parentField`, `typeField`, `baselineStartField`, `baselineEndField`,
187
+ `groupByField`, `resourceView`, `assigneeField`, `effortField`, `capacity`,
188
+ `quickFilters`, `autoZoomToFilter` — are all read by
189
+ `plugin-gantt/src/ObjectGantt.tsx`, through a local `GanttConfigEx`
190
+ intersection that existed only because this type did not carry them. It now
191
+ derives from the spec, with `timeSegments` (shift segmentation) as the one
192
+ genuinely local extension; the schema is `$loose` upstream, so that key is
193
+ legal metadata rather than a second dialect.
194
+ - **`GanttConfig.tooltipFields` carried the comment "not part of the upstream
195
+ GanttConfigSchema".** It is, as of rc.6, so the key now arrives from the spec.
196
+ - **`AddressValue` declared five of the spec's seven parts** — `countryCode` and
197
+ `formatted` were missing, under a comment already claiming to be "the part
198
+ names of `AddressSchema`". The widget still renders five inputs; binding the
199
+ type stops it from asserting the platform cannot store the other two, and makes
200
+ the `{ ...address }` write-through say so.
201
+ - **`DATE_RANGE_PRESETS` was `Object.keys(PRESET_RANGES)`,** a third copy of a
202
+ vocabulary the spec extracted in objectstack#4614 precisely to collapse — its
203
+ own doc comment names this module as one of the three. It is now the spec's
204
+ array by reference, and the local date-macro bounds table is pinned complete
205
+ against it with `satisfies`, so a preset the schema gains without bounds here
206
+ is a compile error rather than a filter that validates clean and then selects
207
+ nothing.
208
+
209
+ `NavigationMode` was one hop from the spec already (`NavigationConfig['mode']`);
210
+ it is bound directly, with a both-directions type pin that it stays the same type
211
+ as the config's own `mode`. `KanbanConfig` / `CalendarConfig` /
212
+ `BreakpointColumnMap` / `BreakpointOrderMap` were exact hand copies of `$strict`
213
+ schemas and are now re-exports — "still exact" is the argument for binding them,
214
+ since a copy with nothing to protect can only drift.
215
+
216
+ `GlobalFilterSchema` is the one ALLOW entry. It is the same spread-composition
217
+ dialect as `SelectOptionSchema` next to it, and it collided only because rc.6's
218
+ new refinement forced `.extend()` to be respelled as a `.shape` spread — which
219
+ moved a derivation the guard could see into an object literal it deliberately
220
+ does not descend into. The dialect is unchanged and its three divergences are
221
+ pinned; which side moves on the refinement itself is objectui#4165.
222
+
223
+ `@objectstack/spec` moves from `devDependencies` to `dependencies` in
224
+ `@object-ui/layout`: its public type surface now references the spec.
225
+
226
+ ### Patch Changes
227
+
228
+ - ceccdcf: Action confirm dialogs and success toasts now honour the bundle's translated
229
+ `confirmText` / `successMessage`, not just `label` (objectui#4265).
230
+
231
+ A TranslationBundle entry for an action carries three keys under one
232
+ `_actions.<name>` node — `label`, `confirmText`, `successMessage` — and
233
+ `useObjectLabel()` has always exposed a resolver for each. What had drifted was
234
+ the call sites: `page:header` (authored record pages), `record:quick_actions`
235
+ and the related-list row menu resolved the button `label` only and dispatched
236
+ the authored `confirmText` / `successMessage` untouched. One bundle entry met
237
+ two fates: the button rendered the translation, the confirm dialog rendered the
238
+ authored English.
239
+
240
+ All action-rendering surfaces now go through one resolver,
241
+ `useActionTextLocalizer()` (new, exported from `@object-ui/react`), which
242
+ applies the existing `actionLabel` / `actionConfirm` / `actionSuccess`
243
+ resolvers over the three keys together. Fallback is unchanged: with no bundle
244
+ entry — or an entry lacking a key — the authored text renders. A bundle cannot
245
+ introduce a `confirmText` or `successMessage` the metadata never declared.
246
+
247
+ - ee26e65: Analytics: the dimension label net's fetch-and-memo glue is written once, not once per surface
248
+
249
+ PR #4388 (objectui#4330) put the same React glue on two surfaces — the dashboard's `DatasetWidget` and plugin-report's dataset block. The resolution RULES were never duplicated (both call the same `@object-ui/core` helpers), but the wiring around them was: read the object schema through the host's authenticated `apiFetch`, keep the fetched metadata locale-free in state, derive the label maps in a render memo. Two copies meant two statements of the same two bug fixes, which is a drift surface rather than a defect — nothing a user could hit today, filed as objectui#4389 so it was retired deliberately.
250
+
251
+ It is now split along the layer that can actually hold each half. `@object-ui/core` gains the React-free parts — `loadDimensionFieldMeta` (the base-object read composed with the dimension walk), `deriveDimensionLabelMaps` (the locale-applying derivation) and `dimensionOptionTranslator` (binding the bundle resolver to the object that OWNS a terminal field, which for a dotted path is the relationship target). `@object-ui/react` gains `useDatasetDimensionLabels` / `useDatasetDimensionMeta`, the React wiring that cannot live in core, beside the `useViewData` / `useElementDataSource` / `useDiscovery` hooks that already read `SchemaRendererContext` the same way. Both plugins consume it; the dashboard keeps its chart-only per-category colour and category-order derivation layered locally, since a table renders no palette.
252
+
253
+ The card originally proposed `@object-ui/core` as the whole glue's home. That home was disproven by measurement and retired in the card's PM RULING #2: `SchemaRendererContext` is defined in `@object-ui/react`, which depends on core, so core importing it back is a cycle — and core is React-free by declaration, by content, and by the topology in AGENTS.md. objectui#3367 had already ruled this direction for the same family (core-canonical logic, react re-exports).
254
+
255
+ Behaviour is unchanged by construction: same read count, same best-effort fallback, same memoization boundary. The two bug fixes are now stated once and pinned at the shared hook — the read rides the host's authenticated `apiFetch` (objectui#4121, pinned by asserting that a new channel re-issues the read, i.e. that it really is in the effect's deps), and the fetched metadata stays locale-free (objectui#4030 / PR #4324, pinned by switching language at runtime and asserting the labels flip with no second metadata read). All 39 assertions PR #4388 landed across both surfaces pass unchanged, and their files are byte-identical to before.
256
+
257
+ - f650253: `BaseSchema.ariaLabel` declares the keyed i18n vocabulary the renderer actually
258
+ resolves, `.disabled` accepts the predicate string it actually evaluates, and the
259
+ keyed shape finally has a name (objectui#4581)
260
+
261
+ Three slots on one base type had drifted from what the renderer does with them.
262
+ PR #4593 fixed `visible` and measured the rest; these are the rest.
263
+
264
+ `ariaLabel` was `string`, but `SchemaRenderer.tsx:111` resolves it with
265
+ `resolveKeyedI18nLabel`, whose input is the KEYED form
266
+ `{ key, defaultValue?, params? }` — a reference into a translation bundle. It is
267
+ now `string | KeyedI18nLabel`, and `KeyedI18nLabel` is a new exported type in
268
+ `@object-ui/types` rather than a fourth inline copy of one object literal: the
269
+ three that existed (`@object-ui/react`'s resolver, `@object-ui/layout`'s
270
+ `resolveLabel`, `@object-ui/app-shell`'s `t`-taking twin) were verified identical
271
+ in their object half first, and two of them now import the name.
272
+
273
+ The vocabulary matters more than the widening. `#4581` originally asked for
274
+ `string | I18nLabel`, and that spelling was withdrawn as measured-wrong: the
275
+ spec's `I18nLabel` is the INLINE LOCALE MAP (`string | Record<string, string>`),
276
+ a different vocabulary resolved against a BCP-47 locale by a different function
277
+ of a confusingly similar name. Under it the shipped keyed fixture type-checked
278
+ only vacuously — as a locale map whose "locales" are named `key` and
279
+ `defaultValue` — the same label carrying `params` was rejected outright, and a
280
+ genuine `{ en: 'Owner' }` compiled while rendering an EMPTY `aria-label`. Naming
281
+ the keyed shape is the declaration half of the fix objectui#4167 started on the
282
+ naming side; `@object-ui/app-shell`'s copy keeps its inline spelling for now
283
+ because an open PR has a pending change to that file, and the comment there says
284
+ so.
285
+
286
+ `disabled` was `boolean` on a key the renderer never reads as one:
287
+ `SchemaRenderer.tsx:466` evaluates it through the same `evaluateCondition` as
288
+ `visible`, and a `disabledOn?: string` sibling exists for the same reason. It is
289
+ now `boolean | string`. The asymmetry with `visible` was accidental rather than
290
+ deliberate.
291
+
292
+ Both are widenings on authored-input-dominant properties: authors gain a
293
+ spelling, nothing that type-checked before stops doing so, and readers already
294
+ coped with `any` through `BaseSchema`'s index signature. Three test fixtures that
295
+ had been casting past these declarations with `as unknown as BaseSchema` state
296
+ their values directly now, and the declared unions are pinned invariantly so
297
+ neither a missing widening nor an overshoot to `any` can pass unnoticed.
298
+
299
+ Declaring the vocabulary honestly also surfaced a real one: the `toggle`
300
+ renderer writes `aria-label` itself instead of going through SchemaRenderer's
301
+ resolver, and it forwarded the raw value. Invoked directly it emitted
302
+ `aria-label="[object Object]"` for a keyed label — announced verbatim by a
303
+ screen reader. It resolves now. Through `SchemaRenderer` the bug was invisible,
304
+ because SchemaRenderer injects its own resolved `aria-label` afterwards; a
305
+ downstream type-check sweep found it, not a test.
306
+
307
+ `BaseSchema.label` and `.description` are deliberately unchanged and pinned that
308
+ way. They receive the spec's inline `I18nLabel` from the view bridges, which is a
309
+ real defect, but resolving it belongs at the spec-to-schema boundary rather than
310
+ in this declaration — and that work is still blocked on a design question about
311
+ where the display locale enters, so it is not in this release.
312
+
313
+ - 8f85f8b: The spec bridge abstains on prototype-member `rowHeight` spellings instead of leaking a
314
+ function into `density`.
315
+
316
+ `bridgeListView`'s `mapDensity` indexed a plain object literal with an unchecked key, so
317
+ the lookup reached `Object.prototype`. The parameter is typed `RowHeight`, but the
318
+ boundary a host's stored view definition actually crosses is `SpecBridge.transformListView`,
319
+ whose parameter is `any` — so `rowHeight: 'toString'` came back as `Object.prototype.toString`,
320
+ a **function**, out of a read whose return type is three strings or nothing. `bridgeListView`
321
+ then writes the key under `if (density)`, and a function is truthy, so the bad value was not
322
+ merely returned: it was stored on a `SchemaNode` whose renderer expects
323
+ `'compact' | 'comfortable' | 'spacious'`. Same for `constructor`, `valueOf`,
324
+ `hasOwnProperty`, `isPrototypeOf`, `propertyIsEnumerable` and `toLocaleString`.
325
+
326
+ The lookup is now guarded with `Object.prototype.hasOwnProperty.call(...)` — the same guard
327
+ `@object-ui/core`'s `rowHeightToDensityMode` grew in objectui#4440, and the repo's existing
328
+ convention at eight other sites. Both `rowHeight` surfaces now abstain identically on every
329
+ off-spec **string** spelling, and objectui#4440's agreement pin covers the prototype-member
330
+ family instead of excluding it (objectui#4442).
331
+
332
+ Runtime-only: no public type moved, and no spec-valid `rowHeight` changes its answer.
333
+
334
+ - d0c3b26: Every plain `<button>` now declares its `type`. HTML defaults an untyped button to
335
+ `type="submit"`, so any of these buttons would submit the form it was composed into
336
+ instead of running its own handler — a real risk for renderers (`drawer`, `tree-view`,
337
+ `navigation-overlay`) whose placement inside a form is a JSON metadata decision. 114
338
+ sites were converted to `type="button"`; no site was a genuine submit button, and the
339
+ DOM is otherwise unchanged.
340
+
341
+ The defect class is now closed mechanically by a new `object-ui/button-has-type` ESLint
342
+ rule (error), so the next untyped button fails CI at write time rather than being found
343
+ by a fourth audit round (objectui#4045, closing the objectui#3344 family).
344
+
345
+ - f148a64: SpecBridge lifts a legacy bare `exportOptions` array to the spec's object form, so a
346
+ spec-authored view's declared export formats reach the grid (objectui#4585).
347
+
348
+ A spec `ListView` may spell `exportOptions` either way, and `@objectstack/spec` lifts the
349
+ legacy bare format array to `{ formats: [...] }` when it parses one (objectstack#8010).
350
+ That lift never ran on the bridge path: the bridge's input is a TypeScript type, not a
351
+ parsed value — there is no `parse`/`safeParse` anywhere under `spec-bridge/` — so a host
352
+ forwarding raw stored metadata handed the array straight through, and the bridge copied it
353
+ onto the `object-grid` node verbatim. ObjectGrid reads the object form and only that, and
354
+ `.formats` on an array is `undefined`, so the renderer's `['csv', 'json']` default won.
355
+
356
+ A view declaring `['csv', 'xlsx']` therefore rendered an export menu offering CSV and
357
+ JSON: the declared xlsx never appeared, an undeclared json did, and nothing said so — the
358
+ export button still showed, because a non-empty array is truthy. The bridge now applies
359
+ the spec's own transform at the assignment site, so both spellings leave it as one shape.
360
+
361
+ Deliberately narrow: this mirrors the spec's lift and nothing else. The object form passes
362
+ through by reference, unread and unrewritten; a view with no `exportOptions` is untouched;
363
+ and a `'pdf'` stored before its retirement is carried rather than filtered, because the
364
+ spec refuses that value at parse with a migration prescription instead of silently
365
+ dropping it — such a format still dies downstream in ObjectGrid's format-agnostic menu
366
+ filter (objectui#4535). The fix is at the producer for the same reason: a tolerant
367
+ `Array.isArray` fallback in the renderer would make a second de-facto contract out of one
368
+ spec key.
369
+
370
+ One behavior follows from reading the lift literally: `exportOptions: []` now lifts to
371
+ `{ formats: [] }` and the export button is hidden, where before the unreadable `[]` was
372
+ merely truthy and produced a menu built entirely from the `['csv', 'json']` default. A
373
+ view that declares zero formats now offers zero.
374
+
375
+ - 47f551b: fix(react): the spec bridge abstains on a non-string `rowHeight` instead of coercing it to a density, matching core
376
+
377
+ `mapDensity` opened with a truthiness guard, so any **truthy non-string** survived it and was
378
+ then coerced into a lookup key — both `Object.prototype.hasOwnProperty.call` and the table index
379
+ run `String(...)`. `rowHeight: ['compact']`, a boxed `String('compact')` or
380
+ `{ toString: () => 'compact' }` therefore each selected a real density, while
381
+ `@object-ui/core`'s `rowHeightToDensityMode` — which opens with `typeof rowHeight !== 'string'` —
382
+ abstained for the same input. Two published surfaces, two answers for one input.
383
+
384
+ The bridge now opens with core's type guard. An off-spec non-string `rowHeight` renders exactly
385
+ like an absent one, and the producer is where it gets fixed (AGENTS.md #0.1). Behaviour for the
386
+ five spec row heights and for off-spec **strings** is unchanged; `''` keeps its answer by a
387
+ different route (a string now, refused one line later because it is not one of the five keys).
388
+
389
+ Note the direction against the previous fix in this function: that leak returned a _function_,
390
+ visibly wrong to everything downstream. This one returned a legitimate-looking `'compact'` that
391
+ nothing downstream could tell apart from an authored density.
392
+
393
+ - Updated dependencies [0e67b53]
394
+ - Updated dependencies [ee66e2e]
395
+ - Updated dependencies [ee26e65]
396
+ - Updated dependencies [5900ac5]
397
+ - Updated dependencies [932cbcd]
398
+ - Updated dependencies [734d186]
399
+ - Updated dependencies [f650253]
400
+ - Updated dependencies [3d9769a]
401
+ - Updated dependencies [3fc2971]
402
+ - Updated dependencies [aca27fa]
403
+ - Updated dependencies [dde7283]
404
+ - Updated dependencies [f7c6430]
405
+ - Updated dependencies [ae10a01]
406
+ - Updated dependencies [92876f0]
407
+ - Updated dependencies [f279deb]
408
+ - Updated dependencies [eb7f586]
409
+ - Updated dependencies [e901131]
410
+ - Updated dependencies [d9d3463]
411
+ - Updated dependencies [2a40f69]
412
+ - Updated dependencies [537a0d1]
413
+ - Updated dependencies [bec3e14]
414
+ - Updated dependencies [613b167]
415
+ - Updated dependencies [1f9b905]
416
+ - Updated dependencies [828549a]
417
+ - Updated dependencies [e1ade8f]
418
+ - Updated dependencies [abb0f81]
419
+ - Updated dependencies [38ab505]
420
+ - Updated dependencies [3e19fe7]
421
+ - Updated dependencies [bb58d1d]
422
+ - Updated dependencies [5cc847c]
423
+ - Updated dependencies [fa21254]
424
+ - Updated dependencies [33c32bf]
425
+ - Updated dependencies [66fb4fa]
426
+ - Updated dependencies [6d641c9]
427
+ - Updated dependencies [479cc7b]
428
+ - Updated dependencies [7e4f0e5]
429
+ - Updated dependencies [45e1949]
430
+ - Updated dependencies [92250d6]
431
+ - Updated dependencies [c1d939f]
432
+ - Updated dependencies [58bebf6]
433
+ - Updated dependencies [405e808]
434
+ - Updated dependencies [49ae9f4]
435
+ - Updated dependencies [c0f9a4b]
436
+ - Updated dependencies [2459a3e]
437
+ - Updated dependencies [2776b11]
438
+ - Updated dependencies [ac853ce]
439
+ - Updated dependencies [fa51109]
440
+ - Updated dependencies [d6aa172]
441
+ - Updated dependencies [fe52a04]
442
+ - Updated dependencies [d46f9b8]
443
+ - Updated dependencies [605b747]
444
+ - Updated dependencies [2fea4d2]
445
+ - Updated dependencies [7f1cb33]
446
+ - Updated dependencies [bb68488]
447
+ - Updated dependencies [2e3b0c0]
448
+ - Updated dependencies [9461dd3]
449
+ - Updated dependencies [78fa331]
450
+ - Updated dependencies [31ab1ac]
451
+ - Updated dependencies [0082db8]
452
+ - Updated dependencies [b42558a]
453
+ - Updated dependencies [d2f6e6b]
454
+ - Updated dependencies [ab04728]
455
+ - Updated dependencies [85a3082]
456
+ - Updated dependencies [06915b0]
457
+ - Updated dependencies [ff84b05]
458
+ - @object-ui/i18n@17.5.0
459
+ - @object-ui/core@17.5.0
460
+ - @object-ui/data-objectstack@17.5.0
461
+ - @object-ui/types@17.5.0
462
+
463
+ ## 17.4.0
464
+
465
+ ### Minor Changes
466
+
467
+ - 5bfaabd: `PageComponentSchema.dataSource` now reaches every object-bound block, not just
468
+ `list-view` — and `element:record_picker` stops discarding `view`
469
+ (objectstack#6953).
470
+
471
+ objectstack#5576 wired the spec's per-element data binding
472
+ (`dataSource: { object, view?, filter?, sort?, limit? }`) to `list-view` and left
473
+ the same declaration inert on every other page component. Two gaps remained, and
474
+ both were silent:
475
+
476
+ - **`element:record_picker` read four of the five keys and dropped `view`.** So
477
+ `dataSource: { object: 'account', view: 'hot' }` — the spec's own example —
478
+ built a picker over EVERY account instead of the rows the saved view selects.
479
+ Nothing threw and nothing rendered an error; the option list was simply wider
480
+ than what was authored, which also means a user could select a record the page
481
+ said was out of scope.
482
+ - **`object-grid` / `object-form` / `object-kanban` / `object-calendar` /
483
+ `object-chart` / `object-metric` / `record:related_list` read none of it.**
484
+ Each gates its fetch on its own `objectName`, and nothing mapped
485
+ `dataSource.object` onto it, so a page written the way the spec documents
486
+ rendered an empty grid / a field-less form / a board with no cards / an empty
487
+ month / an empty chart / a static metric number — with no request and no
488
+ diagnostic anywhere. Spec-valid metadata rendering nothing is the
489
+ objectstack#4413 shape.
490
+
491
+ Composition follows objectstack#5576's landed semantics unchanged on every block:
492
+ a named saved view supplies the baseline, a key written on the component itself
493
+ overrides it, an explicit binding key overrides both, `filter` AND-combines
494
+ ("additional filter criteria" — a binding can narrow a view, never widen it), and
495
+ a `view` name that does not resolve renders a configuration error instead of
496
+ degrading to the object's full scope.
497
+
498
+ - `@object-ui/react` — new `useElementDataSourceSchema(schema, mapping, dataSource?)`
499
+ and `ElementDataSourceGate` apply a resolved binding to the schema keys a given
500
+ block reads, plus `ElementDataSourceErrorPanel` / `ElementDataSourceLoadingPanel`
501
+ for the two non-final states. One precedence table for all blocks rather than
502
+ one copy per block — that copy is how "additional filter criteria" would have
503
+ become two dialects.
504
+ - A mapping names **only** keys its block genuinely reads. A composed value
505
+ written onto a key the block ignores would be accepted and dropped, which is
506
+ the defect being removed, one layer deeper — so a kanban's swimlane `columns`
507
+ never receive a view's field list, and a block with no row cap leaves `limit`
508
+ unmapped. The per-block coverage table, including two residual gaps that are
509
+ named rather than papered over, is in `content/docs/guide/data-source.md`.
510
+
511
+ No behaviour changes for a block that carries no `dataSource`: the binding-free
512
+ path returns the schema by reference, so nothing remounts and nothing refetches.
513
+
514
+ - 48132f7: Track the `@objectstack` family at `17.0.0-rc.5` (objectui#3560).
515
+
516
+ The pin moves from `^17.0.0-rc.2` to `^17.0.0-rc.5` across all 37 declarations in
517
+ 30 `package.json` files, and the sibling `@objectstack/*` packages (`client` /
518
+ `formula` / `lint`) move with it — they pin `@objectstack/spec` **exactly**, so
519
+ leaving them behind would keep a second copy of the spec in the tree and have
520
+ `@objectstack/lint` validating against schemas that still accept the keys rc.3–rc.5
521
+ retire. `pnpm-lock.yaml` now resolves one copy of each of the six family packages
522
+ (`spec` / `client` / `core` / `formula` / `lint` / `sdui-parser`), all at rc.5.
523
+
524
+ Bumping the pin and repairing the fallout cannot be split: the pin alone reddens
525
+ CI, and the code alone targets a shape that is not in effect yet.
526
+
527
+ ## A live bug this upgrade fixes
528
+
529
+ **`ObjectStackDataSource.delete()` never emitted its mutation event, and resolved
530
+ `undefined` instead of a boolean.** `@objectstack/client`'s `DeleteDataResult`
531
+ declared a key called `deleted` — a key no schema has ever declared and no server
532
+ path has ever returned on `DELETE /data/:object/:id`. So `result.deleted`
533
+ compiled and read `undefined` at runtime: the guard never fired, a successful
534
+ delete notified no subscriber, and every consumer's cache stayed stale.
535
+ objectstack#5638 corrected the interface to the schema's `success`; following the
536
+ rename is what restores both behaviours. Nothing in this repo had to change shape
537
+ for it — the code was already asking the right question of the wrong key.
538
+
539
+ ## Breaking, in FROM → TO form
540
+
541
+ - **The five `@objectstack/spec/ui` interaction-config modules are gone** —
542
+ touch / dnd / keyboard / animation / offline, 32 defs and 64 exports
543
+ (objectstack#4988, PR objectstack#5321). None of them had an authoring door: no
544
+ metadata document could ever carry one of these blocks, so a stack that parsed
545
+ before the retirement parses byte-for-byte the same after it. `@object-ui/types`
546
+ drops the 32 `export type` re-exports. The vocabulary each one's only real
547
+ consumer needs is now declared by that consumer, which is the remedy the spec's
548
+ own retirement ledger prescribes ("declare that union locally — it is your
549
+ client's policy, not the platform's"):
550
+
551
+ - `@object-ui/react`'s `useOffline` owns `OfflineStrategy`, `ConflictResolution`,
552
+ `PersistStorageType`, `EvictionPolicyType`, `OfflineConfig`,
553
+ `OfflineCacheConfig`, `OfflineSyncConfig`;
554
+ - `@object-ui/core`'s `DndProtocol` / `KeyboardProtocol` own `DndConfig`,
555
+ `DragItem`, `DropZone`, `DragConstraint`, `DragHandle`, `DropEffect`,
556
+ `KeyboardNavigationConfig`, `KeyboardShortcut`, `FocusManagement`,
557
+ `FocusTrapConfig`;
558
+ - `@object-ui/types`' `mobile` module owns `SpecGestureConfig`,
559
+ `SwipeGestureConfig`, `PinchGestureConfig`, `LongPressGestureConfig`,
560
+ `TouchTargetConfig`, `TouchInteraction` (plus a new `SPEC_GESTURE_TYPES`
561
+ runtime tuple), so `@object-ui/mobile`'s import paths are unchanged.
562
+
563
+ Every shape is moved verbatim — same keys, same members, same optionality — so
564
+ no hook or bridge changes behaviour. Consumers importing these names from
565
+ `@object-ui/types` must import them from the owning package instead. Note the
566
+ spec's _surviving_ `ConnectorConflictResolution` (`/integration`, connector sync)
567
+ and `ConflictResolutionStrategy` (`/api`, route merge policy) are **different
568
+ concepts** — do not re-point at them.
569
+
570
+ - **`@object-ui/types` no longer re-exports `NotificationAction` or `EmbedConfig`**
571
+ (objectstack#5015, PR objectstack#5300). Both were published `ui` vocabulary with
572
+ no authoring door; no notification action was ever parsed from metadata and no
573
+ iframe route ever read an embed config. The presentation enums
574
+ (`NotificationType` / `NotificationSeverity` / `NotificationPosition`) and
575
+ `SharingConfig` **survive** and are untouched — public form sharing still gates
576
+ the anonymous endpoints on `allowAnonymous` + `publicLink`.
577
+ `@object-ui/core`'s `SharingProtocol` keeps `resolveEmbedConfig` /
578
+ `generateEmbedCode` against a locally declared `EmbedConfig`, so its surface is
579
+ unchanged.
580
+ - **`ThemeEngine` stops emitting nine retired CSS variable groups**
581
+ (objectstack#5021 option 2, PR objectstack#5289). `theme.animation`,
582
+ `theme.zIndex` and five typography groups (`fontSize` / `fontWeight` /
583
+ `lineHeight` / `letterSpacing`, plus `fontFamily.heading` / `fontFamily.mono`)
584
+ are tombstones the schema now rejects by name, so `--duration-*`, `--timing-*`,
585
+ `--z-*`, `--font-size-*`, `--font-weight-*`, `--line-height-*`,
586
+ `--letter-spacing-*`, `--font-heading` and `--font-mono` had become structurally
587
+ dead code — no author could produce the input that reached them.
588
+ `generateAnimationVars` and `generateZIndexVars` are removed from
589
+ `@object-ui/core`, and `@object-ui/types` drops `Animation` / `ZIndex` /
590
+ `AnimationSchema` / `ZIndexSchema`. **`theme.customVars` is the declared — and
591
+ since #5021 the only — door**: each entry is emitted verbatim as
592
+ `--<key>: <value>`, so a `--z-modal` or a `--duration-fast` goes there now.
593
+ LIVE emission is untouched byte for byte: `colors`, `borderRadius`, `shadows`,
594
+ `typography.fontFamily.base` (→ `--font-sans`) and `customVars`.
595
+ - **`@object-ui/types`' `HttpMethodSchema` now binds the spec's
596
+ `HttpMethodSubsetSchema`, and `HttpMethod` binds `HttpMethodSubset`**
597
+ (objectstack#5832, PR objectstack#5976 — objectui#3499). The spec renamed its
598
+ 5-value UI subset because `schemaNameFromExportKey` strips the `Schema` suffix,
599
+ so the 5-value and 7-value enums both published as `shared/HttpMethod` and the
600
+ later write won — the emitted JSON Schema and reference page described only one
601
+ of them. **The runtime domain is unchanged and this repo's exported names are
602
+ unchanged**; this follows the rename without touching cross-package semantics.
603
+ Deliberately NOT re-pointed at the spec's bare `HttpMethod`: that is the 7-value
604
+ enum, and widening to it would let `method: 'HEAD'` compile and then throw in
605
+ `HttpRequestSchema.parse()`.
606
+ - **`dashboard.widgets[].actionUrl` / `actionType` / `actionIcon` / `aria` are
607
+ refused, not stripped** (objectstack#5010, ADR-0049 enforce-or-remove). A
608
+ dashboard widget has no action button and never had one — every action the
609
+ dashboard dispatches comes from `header.actions[]` — and no renderer ever applied
610
+ the widget `aria`, so it promised accessibility compliance it did not deliver.
611
+ A stale dashboard now gets a named error telling it where the affordance moved,
612
+ instead of silently losing it. Run `os migrate meta --from 16` to rewrite.
613
+
614
+ ### Patch Changes
615
+
616
+ - 8aad9fd: Action-face predicates written against the canonical `record.` root now evaluate
617
+
618
+ `action:button`, `action:icon`, `action:menu` and `action:group` gated their
619
+ actions on `useCondition(pred, context)`, which evaluates on
620
+ `new ExpressionEvaluator({ ...scope, ...context })` — and the context each of
621
+ them passed was the row spread flat, or nothing at all. Only the shorthand
622
+ spelling resolved:
623
+
624
+ | predicate | verdict, before |
625
+ | ---------------------------- | -------------------------------- |
626
+ | `status == "pending"` | evaluates (`action:button` only) |
627
+ | `record.status == "pending"` | throws `record is not defined` |
628
+ | `data.status == "pending"` | throws `data is not defined` |
629
+
630
+ `record.` is not a mistaken spelling — it is the canonical one. It is what
631
+ `ExpressionEvaluator`'s CEL path binds (`bag.record` as the record namespace),
632
+ what `evalRowPredicate` binds on the record header, list rows, the row kebab
633
+ and conditional formatting (`record.status` / bare `status` / `data.status`),
634
+ and what the server enforces with. A `visible` that fails CLOSED turns the throw
635
+ into "hidden", so a correctly-authored predicate deleted its own button —
636
+ indistinguishable from the gate having said no. On the fail-soft legs the same
637
+ throw lands the other way: `disabled` greyed a control out for everyone.
638
+
639
+ Live rather than theoretical: every declared action on framework's
640
+ `sys_approval_request` gates on `record.viewer.*`, so the whole server-declared
641
+ approval decision set was invisible wherever the declared-action bar rendered
642
+ until objectui#4077 fixed that bar. These four generic renderers carried the
643
+ same binding.
644
+
645
+ What changed:
646
+
647
+ - all four bind the row the three canonical ways, through one named helper
648
+ (`usePredicateRecordContext`, exported from `@object-ui/react` beside
649
+ `useCondition`), so the action face and the row surfaces answer an author's
650
+ `visible:` the same way;
651
+ - `action:icon` reads the row at all. It evaluated against an empty bag, so not
652
+ even the bare-field shorthand resolved — and its `data` prop was landing in
653
+ the props spread onto the DOM button;
654
+ - `action:menu`'s items and `action:group`'s two leaves receive the row from
655
+ their host, which they previously never got;
656
+ - `action:bar` forwards the row into the overflow menu it builds, not just to
657
+ its inline members. An action's predicate had been answering a different
658
+ question purely because it spilled past `maxVisible` — which on mobile
659
+ defaults to 1, making the verdict a function of the viewport.
660
+
661
+ Deliberately unchanged: the evaluation entry and each site's error policy. A
662
+ predicate that genuinely faults still fails closed on `action:button` /
663
+ `action:menu` `visible` and still fails soft on the other legs, exactly as
664
+ before; `toPredicateInput`, `hasDeclaredVisibilityGate` and the empty-predicate
665
+ rules keep their pinned semantics. Binding the row is a separate question from
666
+ what to do when the predicate faults.
667
+
668
+ A surface with no row of its own binds nothing rather than an empty record, so
669
+ a host that supplies the row through the ambient predicate scope is not blanked
670
+ out; a row passed explicitly still wins over the scope.
671
+
672
+ - e06810e: `PageComponentSchema.dataSource` is now consumed instead of discarded — a
673
+ `list-view` page component can reference a **saved view by name** for the first
674
+ time, and writing the binding no longer breaks the component
675
+ (objectstack#5576).
676
+
677
+ The spec declares a per-element data binding on every page component —
678
+ `dataSource: { object, view?, filter?, sort?, limit? }` — and objectui read none
679
+ of it. `ViewDataProvider.resolveElementDataSource` forwarded
680
+ `filter`/`sort`/`limit` and dropped `view` entirely, and had no caller outside its
681
+ own test; nothing mapped `object` onto the `objectName` a list actually reads. So
682
+ "reference a saved view by name" was published, validated and inert, and every
683
+ page that wanted a saved view's columns/filter/sort had to inline a second copy of
684
+ them — the drift the binding exists to remove.
685
+
686
+ Writing the binding also **broke** the block, for a reason unrelated to `view`:
687
+ `SchemaRenderer` spread the schema's `dataSource` metadata onto the component as a
688
+ React prop, and that is the prop name the host uses to inject the data-source
689
+ ADAPTER. The plain `{ object, view }` object shadowed the adapter, so the first
690
+ `dataSource.find(…)` threw `dataSource.find is not a function` and `list-view`
691
+ rendered "Couldn't load records" — a spec-compliant component failing next to
692
+ identical ones that omitted the binding.
693
+
694
+ - `@object-ui/react` — `SchemaRenderer` no longer spreads `schema.dataSource` as a
695
+ prop (it is metadata, like `visibleWhen`); renderers read it off `schema`. An
696
+ explicit React `dataSource` prop is unaffected. New
697
+ `useElementDataSource(schema, dataSource?)` hook resolves a binding, fetching
698
+ the named saved view from the object definition's `listViews` and the metadata
699
+ overlay's `listViews()`.
700
+ - `@object-ui/core` — new `isElementDataSourceConfig` / `collectSavedViews` /
701
+ `resolveSavedView` / `composeElementDataSource`, and `resolveElementDataSource`
702
+ now honours `view` through an optional `DataFetcher.fetchViews`, reporting an
703
+ unresolvable view as an error instead of silently returning every record.
704
+ `resolveViewId` moved here from `@object-ui/app-shell` (re-exported there) so
705
+ one matcher serves both the object page and a page component.
706
+ - `@object-ui/plugin-list` — `list-view` maps the binding onto the props
707
+ `ListView` reads. `dataSource.*` keys are authoritative, view-supplied values
708
+ are a baseline the component's own keys override, and `filter` AND-combines at
709
+ every level (the spec calls the binding's filter "additional criteria"), so a
710
+ binding can narrow a saved view but never widen it. A `view` name that does not
711
+ resolve renders a configuration error naming the object's actual views and
712
+ issues no query — it never falls back to the object's default view, because that
713
+ turns a typo into a silently wider answer.
714
+
715
+ - ab3ad4f: An empty predicate is no longer a declared gate anywhere (objectui#3850, objectui#3862)
716
+
717
+ "Is a gate DECLARED on this key — is there a condition to reach a verdict on?" was
718
+ answered three times in this repo, with three different scopes, and the widest
719
+ answers sat on `disabled`, where the mistake is not benign:
720
+
721
+ - `hasDeclaredVisibilityGate` (the action face) asked `!= null && !== ''`, so every
722
+ OBJECT counted — including `{ dialect: 'cel', source: '' }`. That envelope is not
723
+ a hand-written curiosity: `@objectstack/spec`'s `ExpressionInputSchema` normalizes
724
+ every authored predicate into one, so "the author left the predicate empty"
725
+ compiles to exactly it. The verdict path normalized the same value back to
726
+ `undefined`, and `evaluateCondition(undefined)` answers `true` — "no condition, so
727
+ visible/enabled". On `visible` that `true` means SHOW, so the two mistakes
728
+ cancelled; on `disabled` it means GREY, so they compounded: a button disabled
729
+ forever that no author asked to disable (objectui#3850, the residue objectui#3842
730
+ left behind).
731
+ - `SchemaRenderer` asked `disabled !== undefined` inline, one notch wider again, so
732
+ `disabled: null` greyed out too — on the GENERIC rendering path, since that block
733
+ runs for every node type, and not as an internal flag either: `_disabled` is
734
+ forwarded to the component as a real `disabled` prop (objectui#3862).
735
+ - `ActionRunner`'s execution gates asked "does this normalize to something
736
+ evaluable?" — the scope that turned out to be right (objectui#3848 / objectui#3872).
737
+
738
+ There is now ONE definition, `hasDeclaredPredicate`, exported from
739
+ `@object-ui/core` (`evaluator/declaredPredicate.ts`, beside the `toPredicateInput`
740
+ normalizer it is derived from): a gate is declared when normalization still leaves a
741
+ condition to evaluate. `''`, a whitespace-only string, an empty-`source` envelope
742
+ and any non-predicate value (`0`, `{}`) are NOT declared; `false` IS (a verdict is
743
+ not a missing gate — objectui#3812). `hasDeclaredVisibilityGate` keeps its name as a
744
+ re-export of it, so the five member-action renderer call sites, `DeclaredActionsBar`
745
+ and `record-quick-actions` are unchanged and inherit the scope;
746
+ `SchemaRenderer`'s `disabled` / `disabledOn` chain and `ActionRunner`'s two gates
747
+ read the same function. No consumer got a local "and also check for empty" test —
748
+ that fourth dialect is what objectui#3842 / objectui#3849 spent two PRs merging away.
749
+
750
+ Measured behaviour change, `action:button` and the generic path, before → after:
751
+
752
+ | value | `visible` | `disabled` | `enabled` | `SchemaRenderer` `disabled` prop |
753
+ | --------------------------------------------------------- | -------------- | ---------- | --------- | -------------------------------- |
754
+ | `''` | shown → shown | on → on | on → on | forwarded → absent |
755
+ | `null` | shown → shown | on → on | on → on | forwarded → absent |
756
+ | `{ dialect: 'cel', source: '' }` | shown → shown | GREY → on | on → on | forwarded → absent |
757
+ | `{ source: '' }` | shown → shown | GREY → on | on → on | forwarded → absent |
758
+ | `' '` (whitespace) | HIDDEN → shown | on → on | GREY → on | forwarded → absent |
759
+ | `0` / `{}` (not predicates) | shown → shown | GREY → on | on → on | forwarded → absent |
760
+ | `true` / `false` / bare CEL / `${…}` / non-empty envelope | unchanged | unchanged | unchanged | unchanged |
761
+
762
+ Every row moves toward "there is no gate here", never away from it, and no value
763
+ that HAS a verdict changes it — the verdict is still read from the raw value, only
764
+ the gate in front of it narrowed. Two rows are behaviour changes rather than the
765
+ equivalence the ruling expected, and are pinned as such: the whitespace string moves
766
+ on `visible` / `enabled` (it used to normalize to `'${ }'`, which evaluates falsy,
767
+ so a predicate that says nothing HID the action from everyone), and non-predicate
768
+ junk stops greying controls out (fail-open, the posture `ActionRunner` already
769
+ committed to).
770
+
771
+ One blank spelling is knowingly still outside the scope: an envelope whose `source`
772
+ is blank but not EMPTY (`{ dialect: 'cel', source: ' ' }`) — the normalizer folds a
773
+ `source` of `''` and does not trim, so the string spelling of a blank predicate is
774
+ trimmed and the envelope spelling is not, and `disabled` still greys out for that one
775
+ value. The ruling enumerated three empty spellings; this is a fourth, measured and
776
+ filed as objectui#3960 rather than widened in here.
777
+
778
+ One chain is deliberately untouched: `SchemaRenderer`'s `visible` / `visibleWhen` /
779
+ `visibleOn` / `visibility` / `hidden` / `hiddenOn` legs keep `!== undefined`, because
780
+ narrowing them would change ALIAS PRECEDENCE, not just emptiness. The `hidden` legs
781
+ are not negated and therefore carry this same defect with the polarity that makes the
782
+ node vanish — measured, out of this ruling's scope, filed as objectui#3955.
783
+
784
+ - 0109f54: Blank predicates and non-predicate values are no longer gates, at the last three entries that still judged them (objectui#3955, objectui#3957, objectui#3960)
785
+
786
+ objectui#3850 sank "is a predicate gate DECLARED here?" into one definition,
787
+ `@object-ui/core`'s `hasDeclaredPredicate`. Three places were left out of that
788
+ ruling's placement clause, each with the same shape of defect: the evaluator's
789
+ single default for "there is nothing here to evaluate" is `true`, meaning
790
+ _visible/enabled_, and wherever a too-wide "declared" test hands it an empty
791
+ predicate on an INVERTED key, that `true` turns a control off for a value the
792
+ metadata never used to say anything.
793
+
794
+ **`SchemaRenderer`'s `hidden` / `hiddenOn` legs (objectui#3955)** asked
795
+ `!== undefined` and did NOT negate the verdict, so an empty predicate meant HIDE
796
+ and the node disappeared — on the generic rendering path, since that block runs
797
+ for every schema type. Harder to diagnose than the `disabled` twin objectui#3862
798
+ fixed: a greyed-out control is still on screen, while a node that never rendered
799
+ is indistinguishable from metadata that meant to hide it. Both legs now read the
800
+ shared definition.
801
+
802
+ **The "blank" criterion now covers the envelope spelling (objectui#3960).** The
803
+ definition trimmed a whitespace-only STRING and not an envelope's whitespace-only
804
+ `source`, because `toPredicateInput` folds a `source` of `''` and does not trim.
805
+ So `{ dialect: 'cel', source: ' ' }` was a declared gate whose verdict came from
806
+ core's own CEL entry calling that exact value "no predicate" (`if (!source.trim())
807
+ return true`) — `disabled` greyed out forever and `ActionRunner.execute` answered
808
+ `{ success: false, error: 'Action is disabled' }` with the handler never invoked.
809
+ Blankness is now decided once for both spellings, at the definition. The
810
+ NORMALIZER's contract is deliberately unchanged: "what shape does the evaluator
811
+ accept" is not the same question as "is there a condition", and moving the trim
812
+ there would have flipped verdicts for every
813
+ `useCondition(toPredicateInput(…))` call site, including container-level `visible`
814
+ reads that never asked this question at all.
815
+
816
+ **`ActionEngine.getActionsForLocation`'s `visible` filter (objectui#3957)** was the
817
+ last consumer answering the question with a range of its own — three empty
818
+ spellings folded by hand, everything else coerced with `Boolean(raw)`. It now reads
819
+ the shared definition and the coercion branch is gone, so one value no longer gets
820
+ two answers depending on whether an action was surfaced by the engine or rendered
821
+ standalone (the invariant objectui#3314 established). Its fail-CLOSED posture on a
822
+ predicate that THROWS is untouched (`throwOnError: true` + `warnHiddenPredicate`):
823
+ "the predicate faulted" and "there is no predicate" are different facts.
824
+
825
+ Behaviour changes, before → after. Observation-class: each needs an author to write
826
+ an empty/blank predicate or a non-predicate value, and there is no known user path
827
+ today.
828
+
829
+ | value | `ActionEngine` `visible` | `SchemaRenderer` `hidden` | `disabled` (action face + generic path) | `ActionRunner.execute` `disabled` |
830
+ | ----------------------------------------------------------- | ------------------------ | ------------------------- | --------------------------------------- | --------------------------------- |
831
+ | `''` / `null` | shown → shown | HIDDEN → rendered | unchanged | unchanged |
832
+ | `' '` (blank text) | HIDDEN → shown | HIDDEN → rendered | unchanged | unchanged |
833
+ | `0` / `NaN` | HIDDEN → shown | HIDDEN → rendered | unchanged | unchanged |
834
+ | `{}` / `[]` | shown → shown | HIDDEN → rendered | unchanged | unchanged |
835
+ | `{ dialect: 'cel', source: '' }` | shown → shown | HIDDEN → rendered | unchanged | unchanged |
836
+ | `{ dialect: 'cel', source: ' ' }` | shown → shown | HIDDEN → rendered | GREY → on | refused → runs |
837
+ | `{ source: ' ' }` (no dialect) | HIDDEN → shown | HIDDEN → rendered | GREY → on | refused → runs |
838
+ | `true` / `false` / bare CEL / `${…}` / a non-blank envelope | unchanged | unchanged | unchanged | unchanged |
839
+
840
+ Every row moves toward "there is no gate here", never away from it, and no value
841
+ that HAS a verdict changes it — a declared `false` is still a verdict, not a
842
+ missing gate (objectui#3812), and blankness is `trim()`, not "short": `{ dialect:
843
+ 'cel', source: ' x ' }` is a predicate. One alias precedence changes with the
844
+ `hidden` legs and is pinned rather than claimed as an equivalence: an undeclared
845
+ `hidden` no longer short-circuits the chain, so a declared `hiddenOn` is finally
846
+ consulted.
847
+
848
+ `SchemaRenderer`'s four `visible*` legs keep `!== undefined` deliberately, as
849
+ objectui#3850 ruled: their `true` is negated, so an empty predicate already lands
850
+ on "shown", and narrowing them would change alias precedence rather than fix
851
+ anything.
852
+
853
+ - Updated dependencies [6719877]
854
+ - Updated dependencies [56ff091]
855
+ - Updated dependencies [7864f03]
856
+ - Updated dependencies [d229dfa]
857
+ - Updated dependencies [4bc6c23]
858
+ - Updated dependencies [c3b01a7]
859
+ - Updated dependencies [f5f8744]
860
+ - Updated dependencies [3765678]
861
+ - Updated dependencies [d83f6b3]
862
+ - Updated dependencies [5f08c05]
863
+ - Updated dependencies [69becd2]
864
+ - Updated dependencies [5e52495]
865
+ - Updated dependencies [b750823]
866
+ - Updated dependencies [e06810e]
867
+ - Updated dependencies [ab3ad4f]
868
+ - Updated dependencies [c2fd122]
869
+ - Updated dependencies [ac2139c]
870
+ - Updated dependencies [b14ab3a]
871
+ - Updated dependencies [8c60819]
872
+ - Updated dependencies [41d6022]
873
+ - Updated dependencies [e64a52e]
874
+ - Updated dependencies [844d17f]
875
+ - Updated dependencies [48132f7]
876
+ - Updated dependencies [4dcd52a]
877
+ - Updated dependencies [42ae5c6]
878
+ - Updated dependencies [1d723e3]
879
+ - Updated dependencies [0109f54]
880
+ - Updated dependencies [7e5bb5d]
881
+ - Updated dependencies [fbc23e0]
882
+ - Updated dependencies [6d762da]
883
+ - Updated dependencies [e6fdbdc]
884
+ - Updated dependencies [f9faa7d]
885
+ - Updated dependencies [6bb454a]
886
+ - Updated dependencies [523be48]
887
+ - Updated dependencies [7e2b7e9]
888
+ - Updated dependencies [33526fd]
889
+ - Updated dependencies [32413ec]
890
+ - Updated dependencies [c1e1e6b]
891
+ - @object-ui/core@17.4.0
892
+ - @object-ui/i18n@17.4.0
893
+ - @object-ui/types@17.4.0
894
+ - @object-ui/data-objectstack@17.4.0
895
+
896
+ ## 17.3.0
897
+
898
+ ### Patch Changes
899
+
900
+ - d915c47: Relation fields (`lookup` / `master_detail` / `user` / `tree`) are now usable in action and conditional-formatting predicates: they bind as the stored foreign key on every surface, and the fields a predicate reads are included in the query projection (#3501).
901
+
902
+ Before this, one predicate over one relation field had four different fates, decided by things its author does not control. `$expand` **replaces** the id in place with the whole related record, and a view expands exactly the relations it shows as COLUMNS — so `record.owner == "U1"` was **true** where the column was absent, **false** where it was displayed, and a **fault** where the field was neither displayed nor projected (a list's `$select` was built from its columns alone, and CEL treats an absent key as a fault, not as null). A fault is fail-CLOSED on the row kebab and the selection bar and fail-OPEN on the lenient paths, so the same authoring mistake hid the button from everyone on one surface and showed it to everyone on the next, with nothing on screen to point at either. The server, meanwhile, only ever sees the id — so client and server could not agree, which is the one thing ADR-0036 / ADR-0058 exist to guarantee.
903
+
904
+ Two changes close it. `toPredicateRecord` (new, `@object-ui/core`) collapses expanded relation values back to their ids when a record is bound for evaluation — driven by the object's own field types, not by sniffing for an `id` key, so a `json` field that happens to carry one is untouched. It is threaded through `evalRowPredicate` / `resolveConditionalFormatting` (via a new `fields` option), `useRowPredicate`, `partitionBulkRows`, and both `page:header` evaluators, with the object schema supplied by `ObjectGrid` / `ListView` / `ObjectKanban` / the record context. Kanban card formatting is threaded the same way, so a rule cannot match on the grid view of a list and silently never match on its board. Display is unaffected — a detail-page title still renders the related record's name, and the schema-only `kanban-ui` entry point (which has no object schema to offer) keeps using the payload verbatim. `collectPredicateFieldRefs` / `listViewPredicates` (new) harvest the `record.x` / `data.x` references out of a view's conditional formatting, row-action defs, bulk-action defs, promoted object actions and `userActions` overrides, and add them to `$select` — intersected with the object's declared fields plus the platform columns every object carries (`isProjectableField`), because an unknown key is not ignored by every backend. No `$expand` is added: a predicate wants the foreign key, which is what an unexpanded relation already is.
905
+
906
+ - 175bd79: `toPredicateInput` is now re-exported from `@object-ui/core` instead of being reimplemented in `@object-ui/react`. Behaviour is byte-for-byte identical — the renderer-side copy in `packages/react/src/hooks/useExpression.ts` had item-for-item the same semantics as the canonical `packages/core/src/evaluator/predicateInput.ts` (booleans short-circuit, bare strings and non-`cel` dialects become `${…}`, a `{ dialect: 'cel', source }` envelope survives so `useCondition` routes it to the canonical `@objectstack/formula` engine, empty/absent predicates become `undefined`), and every existing import path (`import { toPredicateInput } from '@object-ui/react'`) keeps working with an unchanged signature. What changes is that there is now ONE implementation rather than two held in step by a parity table: #3314 is the record of what two normalizations do when left alone — they drift, and the same `visible:` predicate reaches different verdicts depending on whether the action was surfaced by `ActionEngine.getActionsForLocation` or rendered standalone. The 14-shape normalization parity table degenerated once both of its columns called the same function, so it is replaced by a single identity assertion (`react`'s export IS `core`'s function object); the engine-path-vs-renderer-path verdict parity suite is untouched and still proves the two call paths agree (#3367).
907
+ - Updated dependencies [18cd432]
908
+ - Updated dependencies [d915c47]
909
+ - Updated dependencies [b71fc92]
910
+ - Updated dependencies [65516ba]
911
+ - Updated dependencies [94c5b7c]
912
+ - Updated dependencies [ca0fa8f]
913
+ - Updated dependencies [3889ffb]
914
+ - Updated dependencies [5781fb1]
915
+ - Updated dependencies [7e2406a]
916
+ - Updated dependencies [9e9e9a9]
917
+ - Updated dependencies [4eeb932]
918
+ - Updated dependencies [5c856ec]
919
+ - Updated dependencies [23018cc]
920
+ - Updated dependencies [68b6a28]
921
+ - Updated dependencies [0554e88]
922
+ - Updated dependencies [d915c47]
923
+ - Updated dependencies [f44d872]
924
+ - Updated dependencies [28b2e65]
925
+ - Updated dependencies [509104a]
926
+ - Updated dependencies [825bbe3]
927
+ - Updated dependencies [6195841]
928
+ - Updated dependencies [5dd0127]
929
+ - Updated dependencies [a415684]
930
+ - Updated dependencies [a4cff5b]
931
+ - Updated dependencies [5af2852]
932
+ - Updated dependencies [f833d3a]
933
+ - Updated dependencies [a6ec93d]
934
+ - Updated dependencies [2a9513d]
935
+ - Updated dependencies [d22ae31]
936
+ - Updated dependencies [c7ed4c3]
937
+ - Updated dependencies [2409e1d]
938
+ - Updated dependencies [789fe3e]
939
+ - @object-ui/core@17.3.0
940
+ - @object-ui/types@17.3.0
941
+ - @object-ui/i18n@17.3.0
942
+ - @object-ui/data-objectstack@17.3.0
943
+
3
944
  ## 17.2.0
4
945
 
5
946
  ### Minor Changes
@@ -1507,6 +2448,7 @@
1507
2448
  result and the "warn-once" tracking are now stored separately: a
1508
2449
  `WeakMap` caches the validation outcome (so the visual flag is stable),
1509
2450
  while a `WeakSet` continues to dedupe `console.warn` output.
2451
+
1510
2452
  - @object-ui/types@6.2.2
1511
2453
  - @object-ui/core@6.2.2
1512
2454
  - @object-ui/i18n@6.2.2
@@ -1656,6 +2598,7 @@
1656
2598
  ### Minor Changes
1657
2599
 
1658
2600
  - b2d1704: feat(cmdk): record search across objects in the Command Palette
2601
+
1659
2602
  - New `useRecordSearch` hook in `@object-ui/react` debounces a query, fans out
1660
2603
  to `dataSource.find(name, { $search, $top })` across candidate objects, and
1661
2604
  aggregates hits. Race-safe via a monotonic runId; per-object 404s are
@@ -1670,6 +2613,7 @@
1670
2613
 
1671
2614
  Three related fixes that all addressed the same UX: a user follows a URL
1672
2615
  shaped `/{object}/{recordId}` and sees a completely blank content area.
2616
+
1673
2617
  1. **`useNavigationOverlay` produced the broken URL itself.** When
1674
2618
  middle-click / Cmd-click opened a gallery card in a new tab and no
1675
2619
  `onNavigate` was provided, the hook built `/{object}/{id}` — a URL
@@ -1689,6 +2633,7 @@
1689
2633
 
1690
2634
  - aa063db: `useRecordSearch` now orders hits by relevance instead of object-fanout
1691
2635
  order. Tiers (higher wins):
2636
+
1692
2637
  - 110: exact recordId paste
1693
2638
  - 100: display exactly equals the query
1694
2639
  - 80: display starts with the query
@@ -1748,17 +2693,20 @@
1748
2693
  - bd8447d: Three platform-wide detail polish items.
1749
2694
 
1750
2695
  **Tighter page rhythm**
2696
+
1751
2697
  - Outer `PageRenderer` padding `p-4 md:p-6 lg:p-8` → `p-3 md:p-4 lg:p-6`
1752
2698
  and outer body wrap `space-y-8` → `space-y-6` so list / detail / home
1753
2699
  pages share the same edge rhythm. Cuts ~16px of edge slack on lg.
1754
2700
 
1755
2701
  **Highlights KPI treatment**
2702
+
1756
2703
  - `HeaderHighlight` now renders numeric / currency / percent / decimal
1757
2704
  values as KPI numbers (`text-xl md:text-2xl font-semibold tabular-nums`)
1758
2705
  instead of the uniform `text-sm font-semibold`, so amount / probability
1759
2706
  / count fields read as headline stats — Salesforce-style key facts.
1760
2707
 
1761
2708
  **Discussion footer upgrade**
2709
+
1762
2710
  - `RecordActivityTimeline` now uses `RichTextCommentInput` (bold / italic /
1763
2711
  list / code, `@`-mention autocomplete, preview toggle, Send) instead of
1764
2712
  a bare `<textarea>`.
@@ -1769,6 +2717,7 @@
1769
2717
  and sidebar positions.
1770
2718
 
1771
2719
  - d51a577: feat(platform): Discussion attachments + @mention directory + Reference Rail aside
2720
+
1772
2721
  - **Discussion attachments** — `RichTextCommentInput` now accepts an `extraSlot`
1773
2722
  and a `canSubmitEmpty` flag so hosts can mount the existing
1774
2723
  `CommentAttachment` composer beneath the editor without forking the toolbar.
@@ -1858,6 +2807,7 @@
1858
2807
  page so the two renderers share state.
1859
2808
 
1860
2809
  - 74962b0: feat(detail): record:discussion schema component + flush accordion variant
2810
+
1861
2811
  - New `record:discussion` schema type lets authors place the record
1862
2812
  chatter feed anywhere in a custom Page schema. Wired through a
1863
2813
  shared `DiscussionContext` provider on the `assignedPage` branch
@@ -1878,6 +2828,7 @@
1878
2828
  customize the header or one tab.
1879
2829
 
1880
2830
  **Slot menu (v1):**
2831
+
1881
2832
  - `header` — replaces `page:header`
1882
2833
  - `actions` — replaces the `record:quick_actions` action bar
1883
2834
  - `highlights` — replaces the chips + chevron path strip
@@ -1905,6 +2856,7 @@
1905
2856
  ```
1906
2857
 
1907
2858
  **API changes:**
2859
+
1908
2860
  - `PageSchema` (in `@object-ui/types`): adds `kind?: 'full' | 'slotted'`
1909
2861
  (default `'full'`) and `slots?: PageSlotMap`.
1910
2862
  - `usePageAssignment` (in `@object-ui/react`): result now exposes a
@@ -2084,6 +3036,7 @@
2084
3036
  detail view (`TypeError: titleFormat.replace is not a function`) and printed
2085
3037
  `Failed to evaluate expression: ${[object Object]}` for every action visibility
2086
3038
  predicate.
3039
+
2087
3040
  - `@object-ui/core`: `ExpressionEvaluator.evaluate` / `evaluateCondition` now
2088
3041
  unwrap Expression envelopes transparently.
2089
3042
  - `@object-ui/react`: new `toPredicateInput()` helper to safely normalize
@@ -2335,6 +3288,7 @@
2335
3288
  - New plugin-object and ObjectQL SDK updates
2336
3289
 
2337
3290
  **Added:**
3291
+
2338
3292
  - New Plugin: @object-ui/plugin-object - ObjectQL plugin for automatic table and form generation
2339
3293
  - ObjectTable: Auto-generates tables from ObjectQL object schemas
2340
3294
  - ObjectForm: Auto-generates forms from ObjectQL object schemas with create/edit/view modes
@@ -2343,6 +3297,7 @@
2343
3297
  - ObjectQL Integration: Enhanced ObjectQLDataSource with getObjectSchema() method using MetadataApiClient
2344
3298
 
2345
3299
  **Changed:**
3300
+
2346
3301
  - Updated @objectql/sdk from ^1.8.3 to ^1.9.1
2347
3302
  - Updated @objectql/types from ^1.8.3 to ^1.9.1
2348
3303
 
@@ -2356,6 +3311,7 @@
2356
3311
  - Patch release: Add automated changeset workflow and CI/CD improvements
2357
3312
 
2358
3313
  This release includes infrastructure improvements:
3314
+
2359
3315
  - Added changeset-based version management
2360
3316
  - Enhanced CI/CD workflows with GitHub Actions
2361
3317
  - Improved documentation for contributing and releasing