@object-ui/react 16.1.0 → 17.1.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 (38) hide show
  1. package/CHANGELOG.md +639 -1
  2. package/README.md +41 -0
  3. package/dist/SchemaRenderer.js +12 -2
  4. package/dist/context/InlineEditContext.d.ts +21 -0
  5. package/dist/context/InlineEditContext.js +7 -2
  6. package/dist/context/NotificationContext.d.ts +245 -17
  7. package/dist/context/NotificationContext.js +222 -20
  8. package/dist/context/ThemeContext.js +4 -8
  9. package/dist/hooks/index.d.ts +2 -1
  10. package/dist/hooks/index.js +3 -1
  11. package/dist/hooks/useAnimation.d.ts +40 -4
  12. package/dist/hooks/useAnimation.js +41 -10
  13. package/dist/hooks/useCapabilityGate.d.ts +16 -0
  14. package/dist/hooks/useCapabilityGate.js +60 -0
  15. package/dist/hooks/useClientNotifications.d.ts +0 -6
  16. package/dist/hooks/useClientNotifications.js +7 -53
  17. package/dist/hooks/useFilterScope.d.ts +30 -0
  18. package/dist/hooks/useFilterScope.js +55 -0
  19. package/dist/hooks/useNavigationOverlay.d.ts +11 -0
  20. package/dist/hooks/useNavigationOverlay.js +35 -1
  21. package/dist/hooks/usePageVariables.js +23 -11
  22. package/dist/hooks/useRecordSearch.js +98 -4
  23. package/dist/index.d.ts +2 -1
  24. package/dist/index.js +1 -1
  25. package/dist/spec-bridge/SpecBridge.d.ts +0 -4
  26. package/dist/spec-bridge/SpecBridge.js +0 -12
  27. package/dist/spec-bridge/bridges/list-view.js +5 -11
  28. package/dist/spec-bridge/index.d.ts +0 -2
  29. package/dist/spec-bridge/index.js +0 -2
  30. package/dist/utils/error-message.d.ts +31 -0
  31. package/dist/utils/error-message.js +49 -0
  32. package/package.json +7 -7
  33. package/dist/hooks/useColumnSummary.d.ts +0 -38
  34. package/dist/hooks/useColumnSummary.js +0 -54
  35. package/dist/spec-bridge/bridges/dashboard.d.ts +0 -81
  36. package/dist/spec-bridge/bridges/dashboard.js +0 -121
  37. package/dist/spec-bridge/bridges/page.d.ts +0 -58
  38. package/dist/spec-bridge/bridges/page.js +0 -101
package/CHANGELOG.md CHANGED
@@ -1,5 +1,643 @@
1
1
  # @object-ui/react
2
2
 
3
+ ## 17.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c4db402: refactor(views): ListView's `aria` and `sharing` are the spec sub-shapes (#2890 scope A step 5)
8
+
9
+ Last rename batch in the ListView vocabulary migration.
10
+
11
+ **`aria`** is now the spec's `AriaPropsSchema`: `label` → `ariaLabel`,
12
+ `describedBy` → `ariaDescribedBy`, folded at the ListView boundary like every
13
+ other legacy key. Two things fall out of adopting the spec shape:
14
+
15
+ - `role` becomes authorable. The list region hardcoded `role="region"`; it now
16
+ reads `aria.role` and falls back to `region`.
17
+ - `aria.live` stays as a documented local extension — it has no spec
18
+ counterpart, and dropping it would silently disable a shipped capability.
19
+ Promote it rather than growing that extension.
20
+
21
+ **`sharing`** is now the spec's `ViewSharingSchema` (`{ type, lockedBy }`),
22
+ imported by reference — the local four-key object is gone. The legacy pair folds
23
+ in: `visibility` collapses onto the two ownership kinds the spec models (only
24
+ `private` is `personal`; `team` / `organization` / `public` are all
25
+ `collaborative`), and a bare `enabled: true` maps to `personal`, which is the
26
+ badge the user already saw (the old title fell back to `'private'`).
27
+
28
+ _Visible change_: the share badge's tooltip shows the spec ownership type, so a
29
+ view authored with `visibility: 'team'` reads "Sharing: collaborative" instead
30
+ of "Sharing: team". The four-value audience has no spec home and nothing but
31
+ that tooltip consumed it; keeping a second audience enum alive would re-open the
32
+ fork this issue closes.
33
+
34
+ Also fixes the **spec bridge**, which was doing the opposite of its job: given a
35
+ spec-shaped `sharing`, `transformListView` _downgraded_ it — inventing a legacy
36
+ `visibility` audience and an `enabled` flag that the renderer then had to fold
37
+ back. Both sides speak `ViewSharing` now, so it passes through.
38
+
39
+ `conditionalFormatting` and `exportOptions` are deliberately **not** folded.
40
+ Both objectui shapes are supersets carrying capability the spec cannot express —
41
+ the `{ field, operator, value }` rule form, and `maxRecords` / `includeHeaders`
42
+ / `fileNamePrefix`. Folding them onto the narrower spec shapes would delete
43
+ working features; they want promotion upstream, not a rename.
44
+
45
+ - 07de839: fix(notifications): the config, `position` and action `variant` are read instead of forked or ignored (#3014 follow-up)
46
+
47
+ The last of the notification contract. After `displayType` (#3071) and `icon`
48
+ (#3076), four gaps of the same family were left:
49
+
50
+ - **the config was 3/4 inert** — only `defaultDuration` was ever read.
51
+ `maxVisible` and `stacking` were carried and ignored, while
52
+ `NotificationBanners` capped at a hard-coded `3` of its own;
53
+ - **its field names forked from `NotificationConfigSchema`** — `position` vs
54
+ `defaultPosition`, a renderer-local `stacking` boolean with no spec
55
+ counterpart, and no `pauseOnHover` at all;
56
+ - **a notification could not declare a `position`.** The #3008 parity guard
57
+ asserted the position _vocabulary_ matched the spec while nothing positioned
58
+ anything by it — a guard passing over an unused value;
59
+ - **`NotificationActionButton.variant` was the shadcn Button vocabulary**
60
+ (`default | destructive | outline`) under a spec-shaped name, forking
61
+ `NotificationActionSchema.variant` (`primary | secondary | link`).
62
+
63
+ **How positioning resolves now** — `notification.position ?? config.defaultPosition
64
+ ?? nothing`, and "nothing" is a real answer:
65
+
66
+ - **declared** → the surface pins itself there, always. `presentNotificationToast`
67
+ passes it per-toast so the contract wins over the container;
68
+ - **undeclared** → the surface keeps its own anchor (a snackbar's bottom edge) or
69
+ defers to the host's toast chrome.
70
+
71
+ That asymmetry is the design decision. The host's sonner container also serves
72
+ toasts that are _not_ spec notifications (the console action runtime's own
73
+ `toast.*` calls), so it stays the fallback authority for placement — never a
74
+ competing one. A declared position a component prop could silently override
75
+ would be the same "validates, then does nothing" shape this whole area is about.
76
+ Hence `defaultPosition` has no fabricated default: "the host didn't say" has to
77
+ be representable.
78
+
79
+ Also: `maxVisible` / `stackDirection` now drive every stacking surface through
80
+ one shared `visibleNotificationStack` (cap keeps the NEWEST, stack grows in the
81
+ declared direction); `pauseOnHover` holds a transient notification's timer and
82
+ resumes it with the time it had left, which needed the provider to track live
83
+ timers rather than fire-and-forget `setTimeout`s. Legacy spellings still resolve:
84
+ `position` folds into `defaultPosition`, and `stacking: false` reads as
85
+ `maxVisible: 1` rather than being ignored.
86
+
87
+ `onToast` now receives the resolved config as a second argument, so the delegate
88
+ can apply the parts of the contract only it can. Existing one-argument handlers
89
+ are unaffected. The spec-parity guard gained the action-variant vocabulary, the
90
+ one notification enum it did not cover.
91
+
92
+ - 2a40b5e: feat(notifications): each spec `displayType` gets its own presentation instead of a toast (#3014)
93
+
94
+ #3008 closed the **contract** half of this: `NotificationContext`'s union matched
95
+ `NotificationTypeSchema`, and `notify()` materialized the declared type so a
96
+ consumer _could_ branch on it. Nothing did. `NotificationProvider` handed every
97
+ item to the host's `onToast` delegate regardless of type, so an author picking
98
+ `banner` or `inline` got a transient overlay — plausible output, wrong output.
99
+
100
+ Each of the five spec types now has a presentation of its own:
101
+
102
+ | `displayType` | Presentation | Rendered by |
103
+ | ------------- | ------------------------------------------------------ | ---------------------------------- |
104
+ | `toast` | transient overlay (unchanged) | the host's `onToast` delegate |
105
+ | `snackbar` | bottom-anchored bar, one at a time, at most one action | `<NotificationSnackbar />` |
106
+ | `banner` | page-width strip **in the content flow** | `<NotificationBanners />` |
107
+ | `alert` | blocking acknowledgement dialog, FIFO queue | `<NotificationAlerts />` |
108
+ | `inline` | in place, at the raising surface | `<NotificationInline scope="…" />` |
109
+
110
+ The four surface components ship from `@object-ui/components` and subscribe via
111
+ `useNotificationsByPresentation(type, scope?)`.
112
+
113
+ **Answers to the three questions the issue left open:**
114
+
115
+ 1. **Banner/inline placement is the host's.** They are not overlays: a banner takes
116
+ space at the top of the content area and an `inline` notification belongs next to
117
+ the thing that raised it. So the context exposes the items and the surfaces
118
+ subscribe, rather than one `onToast`-style delegate positioning everything. An
119
+ `inline` notification carries a `scope` that pairs it with its outlet, so two
120
+ forms on one page don't show each other's messages.
121
+ 2. **`alert` is modal-ish but NOT the action system's `ModalHandler`.** That handler
122
+ resolves a page/object, renders it, and reports an `ActionResult` back to the
123
+ `ActionRunner`; a notification alert has no schema, no target and no result.
124
+ Routing it there would mean synthesizing a page just to say "OK". It renders
125
+ through the `AlertDialog` primitive instead — no second action-modal path.
126
+ 3. **`snackbar` earns its own component.** It supersedes rather than stacks, anchors
127
+ bottom regardless of the toast position config, and takes at most one action.
128
+ Making it a sonner variant is what "presents as a toast" means.
129
+
130
+ **Also fixed:** auto-dismiss now follows the presentation. `toast`/`snackbar` keep
131
+ the transient timer; `banner`/`alert`/`inline` are persistent unless the raiser sets
132
+ `duration` explicitly — a persistent banner used to evaporate on the shared 5s toast
133
+ timer. `dismissible` is honored on the persistent surfaces (an `alert` always keeps
134
+ its acknowledge button; `dismissible: false` only closes the Escape route).
135
+
136
+ `onToast` now receives **only** `toast` items. A provider with no `onToast` remains
137
+ the supported store-only mode (a bell reading `notifications`/`unreadCount`), but
138
+ raising one of the other four types with its surface unmounted warns in dev, naming
139
+ the component to mount — that failure used to be silent.
140
+
141
+ `NOTIFICATION_PRESENTATIONS` is typed `Record<NotificationPresentation, …>`, so a new
142
+ member in the spec enum fails type-check until its presentation is decided; a parity
143
+ test additionally asserts the table covers `NotificationTypeSchema` exactly and that
144
+ no two types share a surface.
145
+
146
+ - ce08d55: chore(deps): upgrade `@objectstack/*` to 17.0.0-rc.0, and let the spec take back what it now owns
147
+
148
+ `spec` / `client` / `formula` / `lint` move from `^16.x` to `^17.0.0-rc.0`. Two
149
+ groups of v17 changes reach this repo, and they pull in opposite directions —
150
+ the spec pruned surface objectui re-exported, and adopted surface objectui had
151
+ been carrying locally.
152
+
153
+ **The spec pruned dead Theme config (objectstack#3494), so the re-exports went
154
+ with it.** `ThemeSchema` dropped `spacing`, `breakpoints`, `logo`, `density`,
155
+ `wcagContrast`, `rtl`, `touchTarget` and `keyboardNavigation` — authorable but
156
+ never enforced, so authoring them was already a silent no-op. `@object-ui/types`
157
+ re-exported those sub-schemas _by reference_ (issue #2231), so they could not
158
+ survive the prune without becoming hand-written mirrors — exactly the second
159
+ de-facto contract AGENTS.md #0.1 forbids. Removed from the public surface:
160
+
161
+ - Types: `Spacing`, `Breakpoints`, `DensityMode`, `WcagContrastLevel`,
162
+ `ThemeLogo`, and the deprecated `SpacingScale` alias
163
+ - Schemas: `SpacingSchema`, `SpacingScaleSchema`, `BreakpointsSchema`,
164
+ `ThemeLogoSchema`, and the `SpacingSchemaType` / `BreakpointsSchemaType` helpers
165
+ - `Theme.spacing`, `Theme.breakpoints` and `Theme.logo`
166
+
167
+ `mergeThemes` no longer merges the three dropped keys. `generateThemeVars` is
168
+ unaffected — it never emitted them, which is why the liveness audit called them
169
+ dead. The one real consumer was `ThemeProvider`, which set the favicon from
170
+ `theme.logo.favicon`; that path is gone, because v17 strips the key at parse and
171
+ it could never arrive again. The live favicon is unaffected: it comes from
172
+ operator branding (`getFaviconUrl()`), applied in the console's `index.html`,
173
+ `main.tsx`, and on route change.
174
+
175
+ Nothing else read the pruned types. In particular the list-density feature is
176
+ untouched — `useDensityMode` and `rowHeightToDensityMode` use `@object-ui/core`'s
177
+ own local `DensityMode`, which never came from the spec.
178
+
179
+ **The spec adopted objectui's ListColumn extensions (objectui#2231), so the
180
+ extension collapsed.** `ListColumnSchema` used to `.extend()` the spec with two
181
+ fields, each carrying a note to promote it upstream rather than grow the
182
+ extension; v17 did exactly that. `summary` is now the spec's
183
+ `union([ColumnSummarySchema, ColumnSummaryConfigSchema])` — the same enum ∪
184
+ `{ type, field }` form `useColumnSummary` reads — and `prefix` is the spec's
185
+ `ColumnPrefixSchema`. `ListColumnSchema` is now a plain by-reference re-export.
186
+ One behavior change rides along: `prefix.type` defaults to `'text'` on parse
187
+ instead of staying `undefined`, so the cell renderer always gets a value.
188
+
189
+ **Node 22 is now the floor.** Every `@objectstack` package declares
190
+ `engines.node: ">=22.0.0"` (objectstack#3825; Node 20 reached EOL 2026-04-30).
191
+ This repo claimed `>=20` and ran CI on Node 20.x, so it promised — and validated
192
+ — a runtime its own core dependency does not support. `engines.node` is now
193
+ `>=22`, CI runs Node 22.x, and the CI/deployment docs say so.
194
+
195
+ The major stays 17: per AGENTS.md the major tracks `@objectstack`'s major, which
196
+ is also 17, and that convention deliberately outranks semver purity — so the
197
+ removals above ship as a minor rather than desyncing the two.
198
+
199
+ - 0ded602: fix(form): a server rejection that names fields now marks those fields (objectstack#3896)
200
+
201
+ The server has always said which field it rejected. `@objectstack/objectql`'s
202
+ validators throw `VALIDATION_FAILED` with `fields[]` — one entry per offending
203
+ field, each with a human `message` — and both the REST layer and the runtime
204
+ dispatcher serve that as a 400 with the entries intact.
205
+
206
+ Every form dropped them. The submit handler caught the rejection, ran the
207
+ message through `extractWriteErrorMessage`, and showed **one undirected toast**:
208
+ the user was told something was wrong but not _what_, on a surface that already
209
+ knows how to mark an input — and already does exactly that for client-side
210
+ validation. On a long form the offending field was often off-screen, so "创建"
211
+ appeared to do nothing.
212
+
213
+ **Now the two failures behave identically, because they share one
214
+ implementation.** The per-field marking, the toast naming the fields, and the
215
+ scroll-and-focus of the first offender (#2793) were extracted from the
216
+ client-side invalid handler; the server path calls the same function. As far as
217
+ the person filling in the form is concerned these are the same event — only the
218
+ referee differs.
219
+
220
+ Three layers, each of which was dropping the detail:
221
+
222
+ - **`@object-ui/react`** — new `extractFieldErrors(err)` (exported alongside
223
+ `extractWriteErrorMessage` / `isPermissionError`) normalises the three shapes
224
+ the error can arrive in: a typed `ValidationError` from the ObjectStack
225
+ adapter, the raw `@objectstack/client` error (whose `details` falls back to the
226
+ whole response body, which is where `fields[]` lands), and a hand-rolled error
227
+ carrying `fields` directly — the server duck-types that shape identically, so
228
+ the client must not be pickier than the server. Entries with no usable `field`
229
+ are **dropped rather than guessed at**: marking an innocent input is worse than
230
+ the generic toast.
231
+ - **`@object-ui/data-objectstack`** — `normaliseClientError` now maps a 400
232
+ `VALIDATION_FAILED` onto the `ValidationError` class that has sat in
233
+ `errors.ts` since the package was written, exported and **never once
234
+ constructed**. Its `validationErrors: Array<{ field, message }>` shape was
235
+ already exactly right. `create` also now normalises at all: only `update` did,
236
+ so a rejected insert reached callers as the raw client error — and a create is
237
+ the path that most often trips required-field validation.
238
+ - **`@object-ui/components`** — the form renderer maps the entries onto
239
+ `form.setError` and takes over the failure, **but only when every rejected
240
+ field has a visible input to carry it**. If the server also rejected something
241
+ the form does not render, it falls through to the banner, whose top-level
242
+ message concatenates every field's reason — so the part the user cannot see
243
+ inline is still said out loud instead of silently dropped.
244
+
245
+ This also removes the need for the client-side predicate mirroring added in
246
+ #2962: a form no longer has to guess what the server will reject in order to
247
+ warn about it beforehand, and mirrored predicates drift.
248
+
249
+ Non-field failures (403 / permission denials / anything without `fields[]`) take
250
+ exactly the path they took before.
251
+
252
+ ### Patch Changes
253
+
254
+ - fc0272a: fix(actions): apply the ADR-0066 D4 capability gate on every action surface (framework#3923)
255
+
256
+ An action declaring `requiredPermissions` is supposed to be one declaration with
257
+ two enforcement surfaces: 403 on the server, hidden button in the UI. The UI half
258
+ only ever ran inside `ActionEngine.getActionsForLocation` — and the surfaces
259
+ `record_header`, `record_more`, `list_item` and `list_toolbar` actually render on
260
+ do not go through the engine. They filter their own action lists. So a button
261
+ declaring a capability nobody holds rendered, live and clickable, on the record
262
+ header, in every grid row menu, and on the list toolbar. For a `type: 'api'`
263
+ action pointed at a self-authored endpoint, nothing else was checking either: the
264
+ platform's action route (which is where the 403 comes from) never sees that
265
+ request.
266
+
267
+ `page:header`, `action:bar` (business _and_ `systemActions`) and the grid's
268
+ `RowActionMenu` now apply the same gate, via a shared `useCapabilityGate()` so
269
+ the surfaces cannot drift apart. The rule is the engine's, unchanged: hide unless
270
+ the caller holds **all** declared capabilities; an empty held set is "holds
271
+ nothing" and gates; **unknown** — no action runtime, no resolved capabilities —
272
+ fails OPEN, because the server is the authority and hiding a permitted user's
273
+ button on missing client data is the worse failure.
274
+
275
+ The record surface was also feeding the gate nothing to work with.
276
+ `RecordDetailView` mounts its own `<ActionProvider>`, which shadows the shell's
277
+ for every action on that page, and seeded it with identity only — no
278
+ `systemPermissions`. Since unknown fails open, that alone un-gated every
279
+ `record_header` / `record_more` / `record_section` action on the one page those
280
+ locations exist on. It now forwards the caller's resolved capabilities (and only
281
+ once they have actually resolved, so a standalone embed without a
282
+ `PermissionProvider` keeps failing open rather than hiding everything).
283
+
284
+ `useRecordEditable`'s record-level explain probe went out on a bare
285
+ `fetch(..., { credentials: 'include' })`. A bearer-token session carries its
286
+ credential in the `Authorization` header, not a cookie, so the probe came back
287
+ 401 on a perfectly valid admin session and the verdict silently failed open —
288
+ the hook was inert in exactly the deployments it was written for. It now rides
289
+ the host's authenticated fetch (`SchemaRendererProvider`'s `apiFetch`), falling
290
+ back to the global one for standalone embeds.
291
+
292
+ - 9eb932b: fix(console): three real-user console failures — 403 blamed on the network, ⌘K search capped at 8 objects, nav gating fields inert
293
+
294
+ 1. **List error panel classifies the failure** (`plugin-list`, `i18n`): a 403/401 from the data source used to render the same "check your connection" copy as a genuine outage, sending users to debug their network while the server was correctly denying access. The panel now classifies by `httpStatus`/`status`/`statusCode`, the `PERMISSION_DENIED`/`UNAUTHORIZED` error codes, or an `HTTP <status>` message prefix, and renders dedicated permission-denied / sign-in-required copy (all nine locales).
295
+
296
+ 2. **⌘K / full-page search scope is no longer truncated** (`react`): `maxObjectsQueried` caps the per-object fanout fallback, not the search scope — it used to slice the candidate pool itself, so the `objects` whitelist sent to the platform's `/api/v1/search` only ever named the first 8 nav objects. Which sidebar group came first decided which records were findable; everything later in the nav was unsearchable no matter what the user typed.
297
+
298
+ 3. **Nav gating fields finally gate** (`app-shell`): `evaluateVisibility` only evaluated `${…}` template strings, so the `{ dialect: 'cel', source }` envelopes the spec normalizes every authored `visible` predicate into fell through to a blanket "visible" — a constant-false predicate still rendered for everyone. It now delegates to `ExpressionEvaluator.evaluateCondition`, which routes CEL envelopes to the canonical `@objectstack/formula` engine. And the sidebars' `requiredPermissions` check treats a bare name as an ADR-0066 system capability (union of the user's permission-set `systemPermissions` from `/me/permissions`) — the same subset rule the server applies to `AppSchema.requiredPermissions` — instead of misreading it as `can(<name>, 'read')`, which had degraded `requiredPermissions` into a hide-from-everyone switch (admins included). The `object:action` form and the legacy object-read fallback keep working.
299
+
300
+ - 4952edf: fix(errors): error-code branches survive the framework's ADR-0112 rename — objectstack#3841
301
+
302
+ Framework ADR-0112 renamed the whole `error.code` vocabulary from lowercase
303
+ `snake_case` to `SCREAMING_SNAKE` (`destructive_change` → `DESTRUCTIVE_CHANGE`).
304
+ Eleven places compared `err.code` against the old spelling with `===`, so against
305
+ a swept server they simply stopped matching — and nothing threw. The affordance
306
+ each branch guards just vanished and the user got the generic error toast instead:
307
+
308
+ - the destructive-change confirm dialog (resource editor, permission matrix)
309
+ - the "create a writable package first" hint
310
+ - field-scoped validation issues on embedded item saves
311
+ - the all-or-nothing publish summary naming the causal item
312
+ - unknown-object tolerance in the app header and in record search
313
+ - the marketplace's local-install messages for conflict / auth / unavailable
314
+ - `isNotFoundError` in the data layer
315
+
316
+ `RECORD_NOT_FOUND` had already been renamed a release earlier, so that branch was
317
+ already dead before this fix.
318
+
319
+ New `errorCodeIs` / `errorCodeIsAnyOf` in `@object-ui/types` compare
320
+ case-insensitively, so the console keeps working against servers on either side
321
+ of the rename — the console ships separately from the server it talks to. Every
322
+ call site now passes the catalog (SCREAMING) spelling, and `error-code.ts` is the
323
+ single file to delete once no supported server emits the old vocabulary.
324
+
325
+ - 2374a49: fix(sdui): a react page no longer loses its state to a memo that never held, and a source that exports nothing fails loudly
326
+
327
+ Writing the regression guard for objectui#2954's "latent hazard" found it was
328
+ already real.
329
+
330
+ **`evaluatedSchema` was memoised on values rebuilt every render.**
331
+ `SchemaRenderer` fell back to a fresh `{}` when no `SchemaRendererProvider` sat
332
+ above it, and `usePageVariables()` returned a brand-new object literal outside a
333
+ `PageVariablesProvider`. Both feed the `evaluatedSchema` memo's dependency list,
334
+ so for any tree without those providers the memo never hit: the schema was
335
+ re-cloned and the ExpressionEvaluator re-run on every render, and children got a
336
+ new schema identity every time. A `kind:'react'` page memoises its compiled
337
+ source on that identity, so the page was recompiled — a new page function, a new
338
+ element type — and React remounted it, silently discarding the user's `useState`.
339
+ Any registry notification (every lazy plugin's first load) triggered it. Both
340
+ fallbacks are now module constants.
341
+
342
+ **A source that exports nothing now throws instead of rendering blank.**
343
+ `generateElement` inserts the implicit `export default` only when the source
344
+ _starts with_ JSX, a `function` declaration, `()` or `class` — so the very
345
+ common `const Page = () => …` exported nothing, and the page rendered blank with
346
+ no error reported anywhere. It now throws with a message naming the fix, which
347
+ `ReactRunner`'s error panel surfaces. `export default null` still means "render
348
+ nothing"; a default export that is not a component throws too.
349
+
350
+ **`PageSchema['kind']` matches `@objectstack/spec`.** It declared
351
+ `'full' | 'slotted'` while the renderer had shipped `'react'` and
352
+ `'html'`/`'jsx'` since ADR-0080 and read the field through a cast. The union now
353
+ spells all five and the cast is gone.
354
+
355
+ Docs: new `content/docs/guide/react-pages.md` (choosing between the executed and
356
+ parsed tiers, the capability gate, the injected scope, flat props, `Block`,
357
+ `useAdapter`, source shapes, error handling) and a `@object-ui/react-runtime`
358
+ README — the package had neither, while being the tier AI-authored pages target.
359
+
360
+ - Updated dependencies [62311b6]
361
+ - Updated dependencies [9e7349e]
362
+ - Updated dependencies [8864971]
363
+ - Updated dependencies [9b773f9]
364
+ - Updated dependencies [1cf0de7]
365
+ - Updated dependencies [752e18f]
366
+ - Updated dependencies [c785740]
367
+ - Updated dependencies [b41f401]
368
+ - Updated dependencies [19e9fa0]
369
+ - Updated dependencies [d61efd1]
370
+ - Updated dependencies [95b7214]
371
+ - Updated dependencies [7d9734d]
372
+ - Updated dependencies [6ae818e]
373
+ - Updated dependencies [9eb932b]
374
+ - Updated dependencies [746dd00]
375
+ - Updated dependencies [aebfa4f]
376
+ - Updated dependencies [38ca8be]
377
+ - Updated dependencies [3cb9646]
378
+ - Updated dependencies [4952edf]
379
+ - Updated dependencies [7f0252e]
380
+ - Updated dependencies [7d35010]
381
+ - Updated dependencies [c4d7b20]
382
+ - Updated dependencies [7639a61]
383
+ - Updated dependencies [94e63ef]
384
+ - Updated dependencies [02aef0c]
385
+ - Updated dependencies [6f29aa5]
386
+ - Updated dependencies [d21794c]
387
+ - Updated dependencies [c4db402]
388
+ - Updated dependencies [5319bf1]
389
+ - Updated dependencies [49e5671]
390
+ - Updated dependencies [b5b97e2]
391
+ - Updated dependencies [f59f2c1]
392
+ - Updated dependencies [4874117]
393
+ - Updated dependencies [ad0183a]
394
+ - Updated dependencies [ce08d55]
395
+ - Updated dependencies [a17ef09]
396
+ - Updated dependencies [aa1240a]
397
+ - Updated dependencies [2374a49]
398
+ - Updated dependencies [390c071]
399
+ - Updated dependencies [d10f526]
400
+ - Updated dependencies [2d5d594]
401
+ - Updated dependencies [ea7f477]
402
+ - Updated dependencies [7f23cd0]
403
+ - Updated dependencies [0ded602]
404
+ - Updated dependencies [24e0e0a]
405
+ - Updated dependencies [f8a95e5]
406
+ - Updated dependencies [3a6cf24]
407
+ - Updated dependencies [aa35561]
408
+ - Updated dependencies [03bd53b]
409
+ - Updated dependencies [3c1f321]
410
+ - Updated dependencies [a045a32]
411
+ - Updated dependencies [912496d]
412
+ - Updated dependencies [9867281]
413
+ - @object-ui/core@17.1.0
414
+ - @object-ui/types@17.1.0
415
+ - @object-ui/data-objectstack@17.1.0
416
+ - @object-ui/i18n@17.1.0
417
+
418
+ ## 17.0.0
419
+
420
+ ### Major Changes
421
+
422
+ - c77108c: refactor(spec-bridge): remove the dead page/dashboard bridges (#1892)
423
+
424
+ `SpecBridge`'s `page` and `dashboard` bridges — `bridgePage`, `bridgeDashboard`,
425
+ and the `SpecBridge#transformPage` / `#transformDashboard` methods — had no
426
+ runtime consumer. Pages render through their own renderer and dashboards
427
+ through `DashboardView → DashboardRenderer → DatasetWidget` (ADR-0021); neither
428
+ path routes through `SpecBridge`. The dashboard bridge's input shape
429
+ (`object` / `categoryField` / `valueField` / `aggregate`) is the pre-ADR-0021
430
+ widget model, which the strict `DashboardWidgetSchema` now rejects — so the
431
+ bridge could not receive a spec-valid dashboard even in principle.
432
+
433
+ Flagged dead by the metadata-liveness audit (framework #1878 / #1892). The
434
+ `list` and `form` bridges are unaffected and remain the live authoring path.
435
+
436
+ BREAKING CHANGE: the public exports `bridgePage`, `bridgeDashboard`, and the
437
+ `SpecBridge#transformPage` / `#transformDashboard` methods are removed. There
438
+ is no replacement — render pages and dashboards through their renderers
439
+ (`DashboardRenderer` / the page renderer) directly.
440
+
441
+ ### Minor Changes
442
+
443
+ - 952b978: fix(detail): the approval band honors the node's `lockRecord` instead of assuming every approval locks (#2902)
444
+
445
+ A record detail page treated "a pending approval request exists" as "this
446
+ record is locked". An approval node declares `lockRecord` (default `true`), and
447
+ on `lockRecord: false` the server keeps accepting writes for the whole time
448
+ that node waits — so the console was asserting a lock the backend did not
449
+ enforce.
450
+
451
+ The label was the smaller half of it. The same conflated signal fed `canEdit`,
452
+ so the record-level inline-edit session was suppressed too: no pencils,
453
+ `enter()` a no-op. On a single-approver step — a department head or plant
454
+ manager, exactly the case `lockRecord: false` exists for, where the approver is
455
+ meant to fill in the missing detail before deciding — the capability was
456
+ unreachable from the UI. And a flow chaining nodes with different policies drew
457
+ one identical band for "edit freely" and "the server will reject your save with
458
+ `RECORD_LOCKED`", so the two states were indistinguishable until Save failed.
459
+
460
+ Approval state is now two signals:
461
+
462
+ - **`approvalPending`** — an approval is running. Drives the band and the recall
463
+ button, both meaningful whether or not the record is editable.
464
+ - **`locked`** — that approval also forbids edits, from the pending node's
465
+ `lock_record` (framework#3814, read off the same `node_config_json` snapshot
466
+ the server's record-lock hook reads).
467
+
468
+ The band renders two states: amber lock + "Locked for approval", or sky clock +
469
+ "In approval · editable", each with its own tooltip. Recall moved out of the
470
+ locked branch — an editable pending approval is just as recallable. Inline
471
+ editing stays live in the editable state.
472
+
473
+ `InlineEditProvider` takes a new optional `approvalPending` prop, defaulting to
474
+ `locked`, so a host that threads only `locked` renders exactly as before. The
475
+ record's `approval_status` field remains the fallback for backends with no
476
+ approvals API; it carries no node granularity, so it still reads as locked — as
477
+ does a pending request from a backend too old to report the policy.
478
+
479
+ New `detail.approvalPendingEditable` / `detail.approvalPendingTooltip` keys are
480
+ translated in all ten locales.
481
+
482
+ - 1767124: feat(grid): compute all eleven spec column summary aggregations (#2890)
483
+
484
+ `ColumnSummarySchema` accepts eleven aggregation names; `useColumnSummary` computed
485
+ five. The other six — `none`, `count_empty`, `count_filled`, `count_unique`,
486
+ `percent_empty`, `percent_filled` — passed validation at authoring time and then
487
+ rendered a blank footer cell, with no error raised on either side.
488
+
489
+ The computation now splits into two families. Count and percent read _raw_ cell
490
+ values, before the numeric parse, so they work on text, select and lookup columns and
491
+ a value that does not parse as a number still counts as a filled row; a cell is empty
492
+ when it is `null`, `undefined`, `""` or an empty array. `sum`/`avg`/`min`/`max` keep
493
+ the existing numeric parse and column formatting.
494
+
495
+ Two behavior changes follow from the enum carrying both `count` and `count_filled`,
496
+ which cannot mean the same thing:
497
+
498
+ - `count` is now every row; `count_filled` is the non-empty variant. Only a column
499
+ whose values are all empty renders differently than before.
500
+ - a zero count renders `Empty: 0` instead of collapsing to a blank cell.
501
+
502
+ Column currency/percent formatting is gated to the numeric family, so `count_unique`
503
+ on a currency column reads `Unique: 3` and not `$3.00`. `none` and unrecognized names
504
+ skip the entry entirely, so a view whose columns all opt out renders no footer row.
505
+
506
+ `ListColumnSchema`'s objectui-local `{ type, field }` arm now takes its vocabulary
507
+ from `SpecColumnSummarySchema` by reference — it was stuck at the same five names,
508
+ which left the per-column `field` override unavailable for the six new aggregations.
509
+
510
+ A parity test asserts the renderer's supported set equals the spec enum in both
511
+ directions: a spec name the renderer omits is the bug above, and a renderer name the
512
+ spec omits would be local dialect (Commandment #0).
513
+
514
+ **Removed:** `useColumnSummary` from `@object-ui/react`. It was a second, unrelated
515
+ hook of the same name with no callers — a different API, a comment claiming it
516
+ implemented spec v2.0.7, and a `distinct` aggregation that is not in the spec
517
+ vocabulary at all (the spec calls it `count_unique`). Use `useColumnSummary` from
518
+ `@object-ui/plugin-grid`, which implements the spec enum.
519
+
520
+ - c6cfdf1: feat(react)!: trim the dead device/preference delegates from
521
+ `useClientNotifications` (objectstack#3612 companion)
522
+
523
+ `registerDevice`, `getPreferences`, and `updatePreferences` delegated to
524
+ `@objectstack/client` methods that were deleted in objectstack#3612 — the
525
+ `/notifications/devices` and `/notifications/preferences` server routes they
526
+ targeted were never built, so every call already surfaced an error at
527
+ runtime. The hook keeps `fetchNotifications` and `markAsRead` (both
528
+ dispatcher-served and route-ledgered). Breaking only for code destructuring
529
+ the removed functions from the hook result; nothing in this repo did.
530
+
531
+ ### Patch Changes
532
+
533
+ - 8ecf5a6: Command palette (⌘K) now surfaces record search hits from the platform's global
534
+ search endpoint (`GET /api/v1/search`).
535
+
536
+ Previously the palette only ran a per-object `find({ $search })` fanout (the
537
+ metadata-driven ADR-0061 search), which misses records that only the global
538
+ search index knows about — so typing a well-known record name returned no
539
+ records even though `/api/v1/search` served them. `ObjectStackAdapter` now
540
+ exposes a `searchAll(query, { limit, objects })` method that calls the unified
541
+ endpoint, `useRecordSearch` prefers it when present (falling back to the fanout
542
+ otherwise), and the palette renders the resulting record hits grouped by object.
543
+
544
+ - 7b35e4b: fix(dashboard,charts): resolve `{current_user_id}` in widget filters (framework #3574)
545
+
546
+ A dashboard widget filtered on `{current_user_id}` rendered `0`. The token
547
+ reached SQL as a literal, matched no row, and nothing was logged on the client
548
+ or the server — a silent zero that reads as "you have no work" rather than
549
+ "this filter did not resolve". The same token in a list-view filter resolved
550
+ correctly, so a user-scoped list and a user-scoped widget over the same data
551
+ disagreed.
552
+
553
+ There was no shared resolver. Three ad-hoc implementations had grown up
554
+ independently — `ObjectView` for list views, `ObjectDataPage` for URL filter
555
+ triples, `NavigationRenderer` for hrefs — and each understood only the filter
556
+ shape its own surface used. `ObjectView`'s opened with
557
+ `if (!Array.isArray(filter)) return filter`, so it could not have been reused
558
+ by dashboard widgets even in principle: widget filters are MongoDB-style
559
+ objects. Widgets therefore got no resolution at all — `DatasetWidget` called
560
+ `resolveDateMacros` and nothing else, which is why `{today}` worked in a widget
561
+ and `{current_user_id}` silently did not.
562
+
563
+ - **`@object-ui/core`** — new `utils/filter-tokens.ts` with
564
+ `resolveContextTokens` and `resolveFilterPlaceholders`. The latter expands
565
+ _every_ placeholder vocabulary in one call and is what surfaces should use;
566
+ resolving only some of them is the whole defect. The walk handles arrays and
567
+ plain objects uniformly, so one resolver covers both platform filter shapes.
568
+ - **`@object-ui/react`** — new `FilterScopeProvider` / `useFilterScope`. The
569
+ renderer packages deliberately do not depend on `@object-ui/auth`, so the
570
+ shell supplies the session values. This is a separate context from
571
+ `PredicateScopeContext`, which is the expression evaluation scope and carries
572
+ no organization.
573
+ - **`@object-ui/plugin-dashboard` / `@object-ui/plugin-charts`** — all six
574
+ widgets that previously resolved date macros only now resolve both
575
+ vocabularies: `DatasetWidget`, `ObjectMetricWidget`, `ObjectDataTable`,
576
+ `ObjectPivotTable`, and `ObjectChart` (dataset-bound and inline paths). The
577
+ chart's `compareTo` comparison filter gets the session pass too — otherwise
578
+ the overlay series silently ignored the owner clause the primary series
579
+ honoured.
580
+ - **`@object-ui/app-shell`** — `ObjectView`'s local `substituteFilterTokens`
581
+ and `ObjectDataPage`'s inline `=== '{current_user_id}'` ternary now delegate
582
+ to the shared resolver, so both also gain `{current_org_id}` and date macros.
583
+ Two of the three ad-hoc implementations are gone rather than joined by a
584
+ fourth.
585
+
586
+ An unresolvable token is left intact rather than dropped: leaving it yields an
587
+ empty result, whereas dropping the clause would _widen_ the result set and show
588
+ a signed-out viewer everyone's data. It is no longer silent — the resolver
589
+ warns, naming the token, and suggests the intended spelling for known
590
+ near-misses (`{current_user}`, `{user_id}`, `{organization_id}`). Authoring-time
591
+ enforcement lands separately as `filter-token-unknown` in `@objectstack/lint`.
592
+
593
+ - Updated dependencies [0b3be01]
594
+ - Updated dependencies [3c4d935]
595
+ - Updated dependencies [4b60d2d]
596
+ - Updated dependencies [952b978]
597
+ - Updated dependencies [de5e40c]
598
+ - Updated dependencies [1a03af6]
599
+ - Updated dependencies [3e886eb]
600
+ - Updated dependencies [cfc675e]
601
+ - Updated dependencies [20df08c]
602
+ - Updated dependencies [1767124]
603
+ - Updated dependencies [8ecf5a6]
604
+ - Updated dependencies [af705b9]
605
+ - Updated dependencies [0502a7c]
606
+ - Updated dependencies [7b35e4b]
607
+ - Updated dependencies [e16ed2d]
608
+ - Updated dependencies [c6fd752]
609
+ - Updated dependencies [f9bbddb]
610
+ - Updated dependencies [dfd3705]
611
+ - Updated dependencies [2735de6]
612
+ - Updated dependencies [6dee2cb]
613
+ - Updated dependencies [e05f052]
614
+ - Updated dependencies [0502a7c]
615
+ - Updated dependencies [faad45e]
616
+ - Updated dependencies [09c6a17]
617
+ - Updated dependencies [c7cff19]
618
+ - Updated dependencies [ba73a02]
619
+ - Updated dependencies [cd09a7b]
620
+ - Updated dependencies [f1abf0e]
621
+ - Updated dependencies [f05b84e]
622
+ - Updated dependencies [9b4b952]
623
+ - Updated dependencies [7d46648]
624
+ - Updated dependencies [6e8fd3c]
625
+ - Updated dependencies [bb4aa25]
626
+ - Updated dependencies [75f1cdf]
627
+ - Updated dependencies [662bdf9]
628
+ - Updated dependencies [059a052]
629
+ - Updated dependencies [53642d4]
630
+ - Updated dependencies [8aae006]
631
+ - Updated dependencies [d62fb1f]
632
+ - Updated dependencies [d147a13]
633
+ - Updated dependencies [c6aaed8]
634
+ - Updated dependencies [263f885]
635
+ - Updated dependencies [dc334da]
636
+ - @object-ui/i18n@17.0.0
637
+ - @object-ui/types@17.0.0
638
+ - @object-ui/data-objectstack@17.0.0
639
+ - @object-ui/core@17.0.0
640
+
3
641
  ## 16.1.0
4
642
 
5
643
  ### Minor Changes
@@ -63,7 +701,7 @@
63
701
 
64
702
  Together with the `^15.1.1` alignment (#2662), a renderer CEL predicate now
65
703
  reaches the identical verdict as the server — including the framework's
66
- `dateField == today()` equality fix (objectstack-ai/framework#3205) once it
704
+ `dateField == today()` equality fix (objectstack-ai/objectstack#3205) once it
67
705
  lands in a published 15.x. The broader home-grown-vs-canonical divergence
68
706
  motivation is #2661.
69
707