@object-ui/react 6.2.3 → 7.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,197 @@
1
1
  # @object-ui/react
2
2
 
3
+ ## 7.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a71be60: chore: drop the unrendered `blank` / `record_review` page types and their config
8
+
9
+ The `blank` and `record_review` page types have no renderer and were removed
10
+ from `@objectstack/spec`'s `PageTypeSchema` (framework#2265, enforce-or-remove).
11
+ This drops their now-dead references in objectui so the upstream spec can hard-
12
+ remove `BlankPageLayoutSchema` / `RecordReviewConfigSchema`:
13
+
14
+ - `PageType` union: removed `dashboard` / `form` / `record_detail` /
15
+ `record_review` / `overview` / `blank` (grid/gallery/kanban/calendar/timeline
16
+ remain — those are list _visualizations_, a separate cleanup).
17
+ - Removed `blankLayout` from `PageLayout` and the `blankLayout` / `recordReview`
18
+ handling in the spec→SDUI page bridge.
19
+ - Removed the redundant `BlankPageLayout{,Schema,Item,ItemSchema}` re-import from
20
+ `@objectstack/spec/ui` (it was never used).
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies [677f7ed]
25
+ - Updated dependencies [08c47da]
26
+ - Updated dependencies [a71be60]
27
+ - Updated dependencies [cb03bc3]
28
+ - @object-ui/types@7.1.0
29
+ - @object-ui/core@7.1.0
30
+ - @object-ui/data-objectstack@7.1.0
31
+ - @object-ui/i18n@7.1.0
32
+
33
+ ## 7.0.0
34
+
35
+ ### Minor Changes
36
+
37
+ - c12986e: Add resultDialog + target interpolation for one-shot action reveals
38
+
39
+ Some platform actions return values the user MUST copy now because the
40
+ server will not surface them again — 2FA TOTP URI + backup codes, freshly
41
+ minted OAuth client_secret, regenerated recovery codes. Previously these
42
+ had to ship as bespoke pages in `apps/account` because actions only
43
+ emitted a fire-and-forget toast.
44
+
45
+ **`@object-ui/core` — ActionRunner**
46
+
47
+ - New `ActionDef.resultDialog: ResultDialogSpec` field. When set on a
48
+ successful action, the runner suppresses the `successMessage` toast and
49
+ awaits the registered `ResultDialogHandler` instead. Missing handler is
50
+ non-fatal (logs a warning); rejected handler is treated as acknowledged.
51
+ - New `setResultDialogHandler(handler)` setter.
52
+ - New types: `ResultDialogSpec`, `ResultDialogFieldSpec`,
53
+ `ResultDialogHandler`.
54
+ - `executeUrl` and `executeAPI` now run `${param.X}` and `${ctx.X}`
55
+ interpolation against `target` before fetching / navigating. Values are
56
+ `encodeURIComponent`'d, missing keys resolve to empty string. `ctx`
57
+ exposes `origin`, `user`, `org`, `recordId` by default; consumers can
58
+ inject more via `context.ctx`.
59
+
60
+ **`@object-ui/react`**
61
+
62
+ - `ActionProvider` and `useActionRunner` both gained an `onResultDialog`
63
+ option that wires straight through to the runner.
64
+
65
+ **`@object-ui/app-shell`**
66
+
67
+ - New `ActionResultDialog` component — promise-based, blocks click-outside
68
+ and Escape (the user MUST click acknowledge), renders five field
69
+ formats: `qrcode` (client-side via the `qrcode` package — never sent
70
+ off-device, so 2FA URIs stay secret), `code-list`, `secret`, `text`,
71
+ `json`. Falls back to `json` when a value's shape doesn't match its
72
+ declared format.
73
+ - `ObjectView` and `RecordDetailView` install the handler and mount the
74
+ dialog automatically, so any action with `resultDialog` declared in
75
+ metadata now works without code changes.
76
+ - New dependency: `qrcode@^1.5.x` for client-side QR rendering.
77
+
78
+ Pairs with the framework-side `Action.resultDialog` schema added in
79
+ `@objectstack/spec` and the `sys_two_factor` / `sys_oauth_application` /
80
+ `sys_account` updates in `@objectstack/platform-objects`.
81
+
82
+ ### Patch Changes
83
+
84
+ - 6cfa330: feat(dashboard): drill "Open in list" escape hatch + unify report drill
85
+
86
+ Adopts the mainstream BI peek-then-escalate drill model. Drill-through opens an
87
+ in-place drawer (keep context) and offers an "Open in list →" affordance to
88
+ escalate to the object's full list page (sort / bulk-select / export / shareable
89
+ URL) — the Looker / Power BI "see records → open in page" pattern.
90
+
91
+ - New `DrillNavigationContext` (`@object-ui/react`): the app shell provides
92
+ `openRecordList`; the renderer stays decoupled from console routing.
93
+ - The drill drawers (pivot / dataset / chart / KPI) render the escape hatch when
94
+ a host navigation handler is present, and hide it otherwise (self-contained
95
+ peek). `DashboardView` provides the handler via `useOpenRecordList`.
96
+ - `DrillDownConfig.target` gains `'navigate'` — skip the drawer and open the
97
+ list directly; degrades to `'drawer'` when no host handler is available.
98
+ - `ReportView` drill-through now opens the same in-place drawer (peek records →
99
+ click a row to open a record) instead of navigating away; the escape hatch
100
+ preserves the previous navigate-to-list behavior. Dashboard and report drill
101
+ are now unified.
102
+ - i18n: `dashboard.openInList` (en / zh).
103
+
104
+ - e95cc25: Fix the NavigationSyncEffect baseline race: lazily-loaded `page`/`dashboard` metadata (and the empty cache during `invalidate()` refetch) could seed a partial diff baseline, making platform `sys_` pages look "user added" — the effect then wrote them into every app's navigation, 403ing on ADR-0010 locked apps (red "Failed to update navigation" toasts) and polluting writable apps. The effect now diffs only while both types are `status === 'ready'` (new optional `MetadataContextValue.getTypeStatus`), never treats `sys_`-prefixed artifacts as user creations, and skips apps whose `_lock`/`protection.lock` is `full`/`no-overlay`.
105
+ - abe8ebc: Repoint `useClientNotifications` to the ADR-0030 `@objectstack/client` surface
106
+
107
+ The `useClientNotifications` bridge hook called `client.notifications.*` with
108
+ signatures that no longer exist on `@objectstack/client@7.x`:
109
+
110
+ - `registerDevice(token, platform)` → the SDK takes a single
111
+ `RegisterDeviceRequest` object (`{ token, platform }`).
112
+ - `markAsRead(id)` → there is no single-id method; the SDK exposes
113
+ `markRead(ids: string[])`. The hook keeps its friendly single-id API and
114
+ adapts to the batch call.
115
+
116
+ These helpers are the stable transport contract for ADR-0030 (Notification
117
+ Convergence): server-side they route to the L5 `sys_inbox_message`
118
+ materialization and the `sys_notification_receipt` read-state spine — the
119
+ re-modeled `sys_notification` L2 event no longer carries recipient/read
120
+ columns. (The Console bell itself reads the inbox + receipts directly via the
121
+ generic data API; see the `@object-ui/app-shell` bell cut-over.)
122
+
123
+ ## Cut-over sequence (operational — run in this order)
124
+
125
+ The Console UI repoint must land together with the framework pipeline **and**
126
+ the data migration so the bell is never blank and read-state is never lost:
127
+
128
+ 1. Deploy the framework ADR-0030 change (objects + `emit()` + producers). New
129
+ notifications now land in `sys_inbox_message` + `sys_notification_receipt`.
130
+ 2. Run the data migration **once** to carry existing notifications across —
131
+ `migrateSysNotificationToEvent({ driver, data })` from
132
+ `@objectstack/metadata/migrations`. It splits each legacy `sys_notification`
133
+ inbox row into a `sys_inbox_message` + receipt, rewrites the row to the event
134
+ shape, and clears the legacy columns. It is **idempotent** and reports
135
+ `not_applicable` on fresh installs. This runs against the ObjectStack
136
+ **server/data engine** — it is not a Console (frontend) step.
137
+ 3. Deploy the objectui repoint (this change + the `@object-ui/app-shell` bell
138
+ cut-over).
139
+
140
+ - 2270239: feat: scoped style-object rendering (ADR-0065)
141
+
142
+ A metadata node may carry `responsiveStyles` (per-breakpoint CSS-property maps);
143
+ `SchemaRenderer` compiles it to **id-scoped CSS** injected as a `<style>` tag and
144
+ appends a scope class to the node. Build-independent (arbitrary values + design
145
+ tokens pass through verbatim — no Tailwind JIT), collision-free (per-node scope,
146
+ unlayered so it beats base utilities), responsive-correct (model breakpoint maps
147
+ → generated `@media`, never `md:` variant classes). Adds `compileScopedStyles`/
148
+ `scopeClassFor`/`hasResponsiveStyles` to `@object-ui/core` and an SDUI design-token
149
+ palette (`--space-*`, `--surface`, `--brand`, …) to the theme. Mirrors Builder.io.
150
+
151
+ - Updated dependencies [5976ba3]
152
+ - Updated dependencies [eaccefd]
153
+ - Updated dependencies [f7f325d]
154
+ - Updated dependencies [c12986e]
155
+ - Updated dependencies [71d7ce0]
156
+ - Updated dependencies [30ee761]
157
+ - Updated dependencies [053c948]
158
+ - Updated dependencies [b99d9bd]
159
+ - Updated dependencies [053c948]
160
+ - Updated dependencies [89e113c]
161
+ - Updated dependencies [ddbe4a2]
162
+ - Updated dependencies [2d47e94]
163
+ - Updated dependencies [9049bbe]
164
+ - Updated dependencies [77cc6bb]
165
+ - Updated dependencies [97c6831]
166
+ - Updated dependencies [cb2fdb1]
167
+ - Updated dependencies [a58c6b8]
168
+ - Updated dependencies [c3749eb]
169
+ - Updated dependencies [c09f44e]
170
+ - Updated dependencies [6cfa330]
171
+ - Updated dependencies [ad8ade6]
172
+ - Updated dependencies [d54346c]
173
+ - Updated dependencies [5332639]
174
+ - Updated dependencies [3870c20]
175
+ - Updated dependencies [b88c560]
176
+ - Updated dependencies [0ad72a6]
177
+ - Updated dependencies [3fa23a7]
178
+ - Updated dependencies [18d0339]
179
+ - Updated dependencies [59b6bbb]
180
+ - Updated dependencies [d16566f]
181
+ - Updated dependencies [1394e34]
182
+ - Updated dependencies [300d755]
183
+ - Updated dependencies [bd8b054]
184
+ - Updated dependencies [4eb9cb6]
185
+ - Updated dependencies [7c239fd]
186
+ - Updated dependencies [858ad94]
187
+ - Updated dependencies [2270239]
188
+ - Updated dependencies [2f31406]
189
+ - Updated dependencies [8d1195d]
190
+ - @object-ui/core@7.0.0
191
+ - @object-ui/data-objectstack@7.0.0
192
+ - @object-ui/i18n@7.0.0
193
+ - @object-ui/types@7.0.0
194
+
3
195
  ## 6.2.3
4
196
 
5
197
  ### Patch Changes
@@ -28,7 +28,7 @@ export declare class SchemaErrorBoundary extends Component<{
28
28
  resetKey?: any;
29
29
  }): void;
30
30
  handleRetry: () => void;
31
- render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element | null | undefined;
31
+ render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
32
32
  }
33
33
  export declare const SchemaRenderer: React.ForwardRefExoticComponent<Omit<{
34
34
  schema: SchemaNode;
@@ -7,8 +7,10 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
7
7
  * LICENSE file in the root directory of this source tree.
8
8
  */
9
9
  import React, { forwardRef, useContext, useMemo, useEffect, useReducer, useState, Component } from 'react';
10
- import { ComponentRegistry, ExpressionEvaluator, isObjectUIError, ERROR_CODES, debugLog, debugTime, debugTimeEnd, DebugCollector, validateSchema, } from '@object-ui/core';
10
+ import { ComponentRegistry, ExpressionEvaluator, isObjectUIError, ERROR_CODES, debugLog, debugTime, debugTimeEnd, DebugCollector, validateSchema, hasResponsiveStyles, scopeClassFor, compileScopedStyles, } from '@object-ui/core';
11
11
  import { SchemaRendererContext } from './context/SchemaRendererContext';
12
+ import { usePredicateScope } from './hooks/useExpression';
13
+ import { usePageVariables } from './hooks/usePageVariables';
12
14
  import { resolveI18nLabel } from './utils/i18n';
13
15
  /**
14
16
  * Dev-mode schema validation.
@@ -137,6 +139,15 @@ export class SchemaErrorBoundary extends Component {
137
139
  export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
138
140
  const context = useContext(SchemaRendererContext);
139
141
  const dataSource = context?.dataSource || {};
142
+ // Ambient host scope (user / app / features), fed by app-shell's
143
+ // ExpressionProvider. Threaded into `visible`/expression evaluation so
144
+ // component predicates can gate on the signed-in user & deployment flags.
145
+ const predicateScope = usePredicateScope();
146
+ // Page-local state (PageSchema.variables), provided by PageVariablesProvider.
147
+ // Exposed to predicates/bindings under `page.<var>` so an interactive element
148
+ // (e.g. element:record_picker) writing a variable can drive another
149
+ // component's `visible`/`visibility`. Empty object outside a Page.
150
+ const { variables: pageVariables } = usePageVariables();
140
151
  // Re-render trigger when the global ComponentRegistry mutates (e.g. a
141
152
  // lazy-loaded plugin finishes registering its components).
142
153
  const [, forceUpdate] = useReducer((n) => n + 1, 0);
@@ -151,11 +162,23 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
151
162
  return unsubscribe;
152
163
  }, []);
153
164
  const [lazyError, setLazyError] = useState(null);
165
+ // Stable fallback id for scoping a styled node that didn't declare an `id`.
166
+ const autoStyleId = React.useId();
154
167
  // Evaluate schema expressions against the data source
155
168
  const evaluatedSchema = useMemo(() => {
156
169
  if (!schema || typeof schema === 'string')
157
170
  return schema;
158
- const evaluator = new ExpressionEvaluator({ data: dataSource });
171
+ // `data` (record/datasource) plus the ambient host scope. `current_user`
172
+ // is aliased to `user` so both `user.email` and `current_user.email`
173
+ // resolve in component `visible`/`visibleOn` expressions. `page` exposes
174
+ // page-local state so predicates can gate on `page.<var>` (e.g. a record
175
+ // picker's selection toggling another component's visibility).
176
+ const evaluator = new ExpressionEvaluator({
177
+ ...predicateScope,
178
+ current_user: predicateScope?.user,
179
+ data: dataSource,
180
+ page: pageVariables,
181
+ });
159
182
  // Shallow copy
160
183
  const newSchema = { ...schema };
161
184
  // COMPAT: Hoist 'properties' up to schema level
@@ -193,7 +216,7 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
193
216
  }
194
217
  newSchema.props = newProps;
195
218
  }
196
- // Evaluate visibility: visible / visibleOn / hidden / hiddenOn
219
+ // Evaluate visibility: visible / visibleOn / visibility / hidden / hiddenOn
197
220
  const shouldHide = (() => {
198
221
  if (newSchema.visible !== undefined) {
199
222
  return !evaluator.evaluateCondition(newSchema.visible);
@@ -201,6 +224,13 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
201
224
  if (newSchema.visibleOn !== undefined) {
202
225
  return !evaluator.evaluateCondition(newSchema.visibleOn);
203
226
  }
227
+ // `visibility` is the spec-canonical predicate (PageComponentSchema.visibility,
228
+ // an ExpressionInput) — show-when-truthy, same semantics as `visibleOn`. The
229
+ // spec→node bridge preserves it verbatim on the node, so this is where it
230
+ // finally gates rendering (previously it was inert and leaked as a DOM prop).
231
+ if (newSchema.visibility !== undefined) {
232
+ return !evaluator.evaluateCondition(newSchema.visibility);
233
+ }
204
234
  if (newSchema.hidden !== undefined) {
205
235
  return evaluator.evaluateCondition(newSchema.hidden);
206
236
  }
@@ -226,7 +256,7 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
226
256
  newSchema._disabled = true;
227
257
  }
228
258
  return newSchema;
229
- }, [schema, dataSource]);
259
+ }, [schema, dataSource, predicateScope, pageVariables]);
230
260
  if (!evaluatedSchema)
231
261
  return null;
232
262
  // Handle visibility: if evaluated schema is hidden, render nothing
@@ -261,9 +291,25 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
261
291
  // Note: We don't forward the ref to the Component because components in the registry
262
292
  // may not support refs. The SchemaRenderer itself can still receive refs for its own use.
263
293
  // Extract schema metadata properties that should NOT be passed as React props
264
- const { type: _type, children: _children, body: _body, schema: _schema, visible: _visible, visibleOn: _visibleOn, hidden: _hidden, hiddenOn: _hiddenOn, disabled: _disabled, disabledOn: _disabledOn, _hidden: __hidden, // stripped: internal visibility flag
294
+ const { type: _type, children: _children, body: _body, schema: _schema, visible: _visible, visibleOn: _visibleOn, visibility: _visibility, hidden: _hidden, hiddenOn: _hiddenOn, disabled: _disabled, disabledOn: _disabledOn, _hidden: __hidden, // stripped: internal visibility flag
265
295
  _disabled: __disabled, // stripped: internal disabled flag
296
+ responsiveStyles: _responsiveStyles, // stripped: compiled to scoped CSS, not a DOM prop
266
297
  ...componentProps } = evaluatedSchema;
298
+ // SDUI scoped styling (ADR-0065): a node's `responsiveStyles` compiles to
299
+ // id-scoped CSS injected as a <style> tag, and a scope class is appended to
300
+ // the node's className. Build-independent, collision-free, responsive-correct.
301
+ const hasScopedStyles = hasResponsiveStyles(_responsiveStyles);
302
+ const scopeClass = hasScopedStyles ? scopeClassFor(evaluatedSchema.id ?? autoStyleId) : '';
303
+ const scopedCss = hasScopedStyles ? compileScopedStyles(`.${scopeClass}`, _responsiveStyles) : '';
304
+ const mergedClassName = scopeClass
305
+ ? [evaluatedSchema.className, scopeClass].filter(Boolean).join(' ')
306
+ : evaluatedSchema.className;
307
+ // Some renderers read `schema.className` directly (e.g. element:text) while
308
+ // others read the `className` prop (e.g. flex/container). Set both so the
309
+ // scope class lands regardless of which channel a renderer honours.
310
+ const schemaForComponent = scopeClass
311
+ ? { ...evaluatedSchema, className: mergedClassName }
312
+ : evaluatedSchema;
267
313
  // Extract AriaPropsSchema properties for accessibility
268
314
  const ariaProps = resolveAriaProps(evaluatedSchema);
269
315
  // Debug-mode enhancements: extra data attributes + perf tracking
@@ -277,19 +323,19 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
277
323
  }
278
324
  debugTime(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
279
325
  const renderStart = isDebug ? performance.now() : 0;
280
- const rendered = (_jsx(SchemaErrorBoundary, { componentType: evaluatedSchema.type, resetKey: evaluatedSchema.id ?? null, children: React.createElement(Component, {
281
- schema: evaluatedSchema,
282
- ...componentProps, // Spread non-metadata schema properties as props
283
- ...(evaluatedSchema.props || {}), // Override with explicit props if provided
284
- ...ariaProps, // Inject ARIA attributes from AriaPropsSchema
285
- ...debugAttrs, // Debug-mode data attributes
286
- disabled: __disabled || undefined,
287
- className: evaluatedSchema.className,
288
- 'data-obj-id': evaluatedSchema.id,
289
- 'data-obj-type': evaluatedSchema.type,
290
- ...(__DEV__ && !_validation.valid ? { 'data-obj-schema-invalid': 'true' } : {}),
291
- ...props
292
- }) }));
326
+ const rendered = (_jsxs(SchemaErrorBoundary, { componentType: evaluatedSchema.type, resetKey: evaluatedSchema.id ?? null, children: [scopedCss ? (_jsx("style", { "data-os-scope": scopeClass, dangerouslySetInnerHTML: { __html: scopedCss } })) : null, React.createElement(Component, {
327
+ schema: schemaForComponent,
328
+ ...componentProps, // Spread non-metadata schema properties as props
329
+ ...(evaluatedSchema.props || {}), // Override with explicit props if provided
330
+ ...ariaProps, // Inject ARIA attributes from AriaPropsSchema
331
+ ...debugAttrs, // Debug-mode data attributes
332
+ disabled: __disabled || undefined,
333
+ className: mergedClassName,
334
+ 'data-obj-id': evaluatedSchema.id,
335
+ 'data-obj-type': evaluatedSchema.type,
336
+ ...(__DEV__ && !_validation.valid ? { 'data-obj-schema-invalid': 'true' } : {}),
337
+ ...props
338
+ })] }));
293
339
  debugTimeEnd(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
294
340
  // Report render perf to DebugCollector when debug mode is active
295
341
  if (isDebug && renderStart) {
@@ -12,7 +12,7 @@
12
12
  * Components can consume actions without creating their own runner instances.
13
13
  */
14
14
  import React from 'react';
15
- import { ActionRunner, type ActionContext as ActionCtx, type ActionDef, type ActionResult, type ConfirmationHandler, type ToastHandler, type ModalHandler, type NavigationHandler, type ParamCollectionHandler } from '@object-ui/core';
15
+ import { ActionRunner, type ActionContext as ActionCtx, type ActionDef, type ActionResult, type ConfirmationHandler, type ToastHandler, type ModalHandler, type NavigationHandler, type ParamCollectionHandler, type ResultDialogHandler } from '@object-ui/core';
16
16
  export interface ActionProviderProps {
17
17
  children: React.ReactNode;
18
18
  /** Initial action context (record, user, data, etc.) */
@@ -27,6 +27,8 @@ export interface ActionProviderProps {
27
27
  onNavigate?: NavigationHandler;
28
28
  /** Custom param collection handler — show dialog to collect ActionParam values */
29
29
  onParamCollection?: ParamCollectionHandler;
30
+ /** Custom result-dialog handler — show one-shot reveal of API response after success */
31
+ onResultDialog?: ResultDialogHandler;
30
32
  /** Pre-registered custom action handlers */
31
33
  handlers?: Record<string, (action: ActionDef, ctx: ActionCtx) => Promise<ActionResult>>;
32
34
  }
@@ -46,6 +48,7 @@ interface ActionContextValue {
46
48
  /** Update context data */
47
49
  updateContext: (ctx: Partial<ActionCtx>) => void;
48
50
  }
51
+ export declare const ActionCtxReact: React.Context<ActionContextValue | null>;
49
52
  /**
50
53
  * ActionProvider — Provides a shared ActionRunner to the tree.
51
54
  *
@@ -14,7 +14,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
14
14
  */
15
15
  import { createContext, useCallback, useContext, useMemo, useState } from 'react';
16
16
  import { ActionRunner, } from '@object-ui/core';
17
- const ActionCtxReact = createContext(null);
17
+ export const ActionCtxReact = createContext(null);
18
18
  /**
19
19
  * ActionProvider — Provides a shared ActionRunner to the tree.
20
20
  *
@@ -30,12 +30,25 @@ const ActionCtxReact = createContext(null);
30
30
  * </ActionProvider>
31
31
  * ```
32
32
  */
33
- export const ActionProvider = ({ children, context = {}, onConfirm, onToast, onModal, onNavigate, onParamCollection, handlers, }) => {
33
+ export const ActionProvider = ({ children, context = {}, onConfirm, onToast, onModal, onNavigate, onParamCollection, onResultDialog, handlers, }) => {
34
34
  const [loading, setLoading] = useState(false);
35
35
  const [error, setError] = useState(null);
36
36
  const [result, setResult] = useState(null);
37
37
  const runner = useMemo(() => {
38
- const r = new ActionRunner(context);
38
+ // Normalize the evaluator scope so predicates can use *either* flat
39
+ // (`record`, `user`, `objectName`) *or* the canonical `ctx.*` namespace
40
+ // (`ctx.user`, `ctx.record`, `ctx.objectName`). The CEL/predicate
41
+ // convention used across the platform (sys-user, sys-invitation,
42
+ // skills/objectstack-formula, …) is `ctx.user.id` — exposing both
43
+ // shapes from one place keeps every consumer working without forcing
44
+ // every call site to repeat the same `{ ..., ctx: { ... } }` boilerplate.
45
+ const normalizedContext = {
46
+ ...context,
47
+ ctx: (context && typeof context.ctx === 'object')
48
+ ? { ...context, ...context.ctx }
49
+ : { ...context },
50
+ };
51
+ const r = new ActionRunner(normalizedContext);
39
52
  if (onConfirm)
40
53
  r.setConfirmHandler(onConfirm);
41
54
  if (onToast)
@@ -46,6 +59,8 @@ export const ActionProvider = ({ children, context = {}, onConfirm, onToast, onM
46
59
  r.setNavigationHandler(onNavigate);
47
60
  if (onParamCollection)
48
61
  r.setParamCollectionHandler(onParamCollection);
62
+ if (onResultDialog)
63
+ r.setResultDialogHandler(onResultDialog);
49
64
  if (handlers) {
50
65
  Object.entries(handlers).forEach(([name, handler]) => {
51
66
  r.registerHandler(name, handler);
@@ -10,12 +10,22 @@ export interface MetadataState {
10
10
  loading: boolean;
11
11
  error: Error | null;
12
12
  }
13
+ export type MetadataTypeStatus = 'idle' | 'loading' | 'ready' | 'error';
13
14
  export interface MetadataContextValue extends MetadataState {
14
15
  refresh: (type?: string) => Promise<void>;
15
16
  invalidate: (type: string, name?: string) => void;
16
17
  ensureType: (type: string) => Promise<any[]>;
17
18
  getItem: (type: string, name: string) => Promise<any | null>;
18
19
  getItemsByType: (type: string) => any[];
20
+ /**
21
+ * Per-type load status. Lazy types ('page', 'dashboard', …) return their
22
+ * items array immediately — empty or stale while a (re)fetch is in flight —
23
+ * so consumers that DIFF the list over time (e.g. NavigationSyncEffect)
24
+ * must distinguish "empty because unloaded" from "actually empty" and only
25
+ * trust snapshots taken while the type is 'ready'. Optional so hand-rolled
26
+ * context values in tests keep working; absent means "always ready".
27
+ */
28
+ getTypeStatus?: (type: string) => MetadataTypeStatus;
19
29
  }
20
30
  export declare const MetadataCtx: import("react").Context<MetadataContextValue | null>;
21
31
  export declare function useMetadata(): MetadataContextValue;
@@ -27,6 +27,7 @@ export function useMetadata() {
27
27
  ensureType: async () => [],
28
28
  getItem: async () => null,
29
29
  getItemsByType: () => [],
30
+ getTypeStatus: () => 'ready',
30
31
  };
31
32
  }
32
33
  return ctx;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * DrillNavigationContext — host-provided navigation for drill "escape hatches".
10
+ *
11
+ * Dashboard/report drill-through opens an in-place drawer listing the records
12
+ * behind a clicked aggregate (the mainstream "peek without losing context"
13
+ * default). But sometimes the user wants the *full* object list page — to sort,
14
+ * bulk-select, switch views, export, or get a shareable URL. That requires
15
+ * console-specific URL building + SPA routing, which lives in the app shell,
16
+ * not the renderer.
17
+ *
18
+ * The app shell provides `openRecordList`; the drill drawers consume it to
19
+ * render an "Open in list →" action (and to honor `drillDown.target:
20
+ * 'navigate'`, which skips the drawer entirely). When no provider is present
21
+ * (e.g. an embedded/standalone renderer with no router) the affordance is
22
+ * simply hidden — drill stays in the drawer.
23
+ */
24
+ import React from 'react';
25
+ export interface DrillNavigationValue {
26
+ /**
27
+ * Open the full list page for `objectName`, scoped by `filter` (an object
28
+ * FIELD name → raw stored value map). Implemented by the app shell as a
29
+ * SPA navigation to the object's list route. Absent when no host wired it.
30
+ */
31
+ openRecordList?: (objectName: string, filter?: Record<string, unknown>) => void;
32
+ }
33
+ export declare const DrillNavigationContext: React.Context<DrillNavigationValue>;
34
+ export declare const DrillNavigationProvider: React.FC<{
35
+ value: DrillNavigationValue;
36
+ children: React.ReactNode;
37
+ }>;
38
+ /** Consume the host-provided drill navigation handlers (empty object if none). */
39
+ export declare function useDrillNavigation(): DrillNavigationValue;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ /**
10
+ * DrillNavigationContext — host-provided navigation for drill "escape hatches".
11
+ *
12
+ * Dashboard/report drill-through opens an in-place drawer listing the records
13
+ * behind a clicked aggregate (the mainstream "peek without losing context"
14
+ * default). But sometimes the user wants the *full* object list page — to sort,
15
+ * bulk-select, switch views, export, or get a shareable URL. That requires
16
+ * console-specific URL building + SPA routing, which lives in the app shell,
17
+ * not the renderer.
18
+ *
19
+ * The app shell provides `openRecordList`; the drill drawers consume it to
20
+ * render an "Open in list →" action (and to honor `drillDown.target:
21
+ * 'navigate'`, which skips the drawer entirely). When no provider is present
22
+ * (e.g. an embedded/standalone renderer with no router) the affordance is
23
+ * simply hidden — drill stays in the drawer.
24
+ */
25
+ import { createContext, useContext } from 'react';
26
+ export const DrillNavigationContext = createContext({});
27
+ export const DrillNavigationProvider = ({ value, children }) => (_jsx(DrillNavigationContext.Provider, { value: value, children: children }));
28
+ /** Consume the host-provided drill navigation handlers (empty object if none). */
29
+ export function useDrillNavigation() {
30
+ return useContext(DrillNavigationContext);
31
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import React from 'react';
9
+ /**
10
+ * PreviewModeContext — signals that the subtree is rendered inside a design /
11
+ * metadata preview surface (the Studio canvas, a view/page preview pane) rather
12
+ * than a live runtime route.
13
+ *
14
+ * Overlay form components (DrawerForm, ModalForm) consume this to render their
15
+ * body inline instead of mounting a portalled modal Sheet/Dialog. A modal in a
16
+ * preview escapes the canvas and locks the whole editor — Radix sets body
17
+ * `pointer-events:none` and traps focus while open — which reads as a frozen UI.
18
+ *
19
+ * Defaults to `false`, so live runtime usage (record pages, action modals,
20
+ * the field/object designers) is completely unaffected.
21
+ */
22
+ declare const PreviewModeContext: React.Context<boolean>;
23
+ export declare const PreviewModeProvider: ({ value, children, }: {
24
+ value?: boolean;
25
+ children: React.ReactNode;
26
+ }) => React.JSX.Element;
27
+ /** True when rendering inside a design / preview surface. Safe outside a provider. */
28
+ export declare const usePreviewMode: () => boolean;
29
+ export { PreviewModeContext };
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ import { createContext, useContext } from 'react';
10
+ /**
11
+ * PreviewModeContext — signals that the subtree is rendered inside a design /
12
+ * metadata preview surface (the Studio canvas, a view/page preview pane) rather
13
+ * than a live runtime route.
14
+ *
15
+ * Overlay form components (DrawerForm, ModalForm) consume this to render their
16
+ * body inline instead of mounting a portalled modal Sheet/Dialog. A modal in a
17
+ * preview escapes the canvas and locks the whole editor — Radix sets body
18
+ * `pointer-events:none` and traps focus while open — which reads as a frozen UI.
19
+ *
20
+ * Defaults to `false`, so live runtime usage (record pages, action modals,
21
+ * the field/object designers) is completely unaffected.
22
+ */
23
+ const PreviewModeContext = createContext(false);
24
+ export const PreviewModeProvider = ({ value = true, children, }) => (_jsx(PreviewModeContext.Provider, { value: value, children: children }));
25
+ /** True when rendering inside a design / preview surface. Safe outside a provider. */
26
+ export const usePreviewMode = () => useContext(PreviewModeContext);
27
+ export { PreviewModeContext };
@@ -12,6 +12,6 @@ export declare const SchemaRendererProvider: ({ children, dataSource, debug, deb
12
12
  dataSource: any;
13
13
  debug?: boolean;
14
14
  debugFlags?: DebugFlags;
15
- }) => import("react/jsx-runtime").JSX.Element;
15
+ }) => React.JSX.Element;
16
16
  export declare const useSchemaContext: () => SchemaRendererContextType;
17
17
  export declare const useDataScope: (path?: string) => any;
@@ -75,7 +75,7 @@ export interface ThemeProviderProps {
75
75
  * </ThemeProvider>
76
76
  * ```
77
77
  */
78
- export declare function ThemeProvider({ themes: themesInput, defaultTheme, defaultMode, persist, storageKey, target, children, }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
78
+ export declare function ThemeProvider({ themes: themesInput, defaultTheme, defaultMode, persist, storageKey, target, children, }: ThemeProviderProps): React.JSX.Element;
79
79
  /**
80
80
  * useTheme — access the current theme context.
81
81
  *
@@ -1,4 +1,5 @@
1
1
  export * from './SchemaRendererContext';
2
+ export * from './PreviewModeContext';
2
3
  export * from './ActionContext';
3
4
  export * from './ThemeContext';
4
5
  export * from './NotificationContext';
@@ -6,3 +7,4 @@ export * from './DndContext';
6
7
  export * from './AppShellContext';
7
8
  export * from './RecordContext';
8
9
  export * from './DiscussionContext';
10
+ export * from './DrillNavigationContext';
@@ -1,4 +1,5 @@
1
1
  export * from './SchemaRendererContext';
2
+ export * from './PreviewModeContext';
2
3
  export * from './ActionContext';
3
4
  export * from './ThemeContext';
4
5
  export * from './NotificationContext';
@@ -6,3 +7,4 @@ export * from './DndContext';
6
7
  export * from './AppShellContext';
7
8
  export * from './RecordContext';
8
9
  export * from './DiscussionContext';
10
+ export * from './DrillNavigationContext';
@@ -21,16 +21,59 @@
21
21
  * const toolbarActions = getActionsForLocation('list_toolbar');
22
22
  * ```
23
23
  */
24
- import { useCallback, useMemo } from 'react';
24
+ import { useCallback, useContext, useMemo } from 'react';
25
25
  import { ActionEngine, } from '@object-ui/core';
26
+ import { ActionCtxReact } from '../context/ActionContext';
26
27
  export function useActionEngine(options = {}) {
27
28
  const { actions = [], context = {} } = options;
29
+ // When wrapped in an <ActionProvider>, reuse its ActionRunner so that:
30
+ // • Visibility / disabled / condition CEL expressions evaluate against
31
+ // the same context (user, datasource, etc.) the provider was seeded
32
+ // with — not just the per-call `context` arg.
33
+ // • Action execution (executeAction / handleShortcut) inherits the
34
+ // provider's confirm / param-collection / modal / result-dialog /
35
+ // toast / navigate handlers. Without sharing, a nested engine would
36
+ // silently no-op on any action that declares `params: [...]` or
37
+ // `confirmText`, because its local runner has no handlers installed.
38
+ // When no provider is present (unit tests, standalone playgrounds), we
39
+ // fall back to a self-contained runner constructed from the `context` arg.
40
+ const providerCtx = useContext(ActionCtxReact);
41
+ const sharedRunner = providerCtx?.runner ?? null;
28
42
  const engine = useMemo(() => {
29
- const e = new ActionEngine(context);
43
+ // When standalone (no surrounding `<ActionProvider>`), normalize the
44
+ // context so predicates can use both `record`/`user` and `ctx.*`.
45
+ const normalizedStandalone = (context && Object.keys(context).length > 0)
46
+ ? {
47
+ ...context,
48
+ ctx: (context.ctx && typeof context.ctx === 'object')
49
+ ? { ...context, ...context.ctx }
50
+ : { ...context },
51
+ }
52
+ : context;
53
+ const e = sharedRunner ? new ActionEngine(sharedRunner) : new ActionEngine(normalizedStandalone);
54
+ // When sharing a provider runner, MERGE per-render flat keys into the
55
+ // existing `ctx` instead of overwriting it. The provider seeds
56
+ // `ctx: { record, user, objectName, … }` once; if we replaced it with
57
+ // just `ctx: { record, recordId, objectName }` here we would erase
58
+ // `ctx.user` and every `record.id == ctx.user.id` predicate would
59
+ // evaluate against `undefined.id` (→ throws → hidden).
60
+ if (sharedRunner && context && Object.keys(context).length > 0) {
61
+ const runner = e.getRunner();
62
+ const existing = runner.getEvaluator().getContext().toObject();
63
+ const existingCtx = (existing && typeof existing.ctx === 'object') ? existing.ctx : {};
64
+ const callerCtx = (context.ctx && typeof context.ctx === 'object')
65
+ ? context.ctx
66
+ : context;
67
+ const merged = {
68
+ ...context,
69
+ ctx: { ...existingCtx, ...callerCtx },
70
+ };
71
+ runner.updateContext(merged);
72
+ }
30
73
  e.registerActions(actions);
31
74
  return e;
32
75
  // eslint-disable-next-line react-hooks/exhaustive-deps
33
- }, [JSON.stringify(actions), JSON.stringify(context)]);
76
+ }, [sharedRunner, JSON.stringify(actions), JSON.stringify(context)]);
34
77
  const getActionsForLocation = useCallback((location) => engine.getActionsForLocation(location), [engine]);
35
78
  const getBulkActions = useCallback(() => engine.getBulkActions(), [engine]);
36
79
  const executeAction = useCallback((name, contextOverride) => engine.executeAction(name, contextOverride), [engine]);
@@ -5,7 +5,7 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- import { ActionRunner, type ActionContext, type ActionDef, type ActionResult, type ConfirmationHandler, type ToastHandler, type ModalHandler, type NavigationHandler } from '@object-ui/core';
8
+ import { ActionRunner, type ActionContext, type ActionDef, type ActionResult, type ConfirmationHandler, type ToastHandler, type ModalHandler, type NavigationHandler, type ParamCollectionHandler, type ResultDialogHandler } from '@object-ui/core';
9
9
  export interface UseActionRunnerOptions {
10
10
  context?: ActionContext;
11
11
  /** Custom confirmation handler — wire to Shadcn AlertDialog or similar */
@@ -16,6 +16,10 @@ export interface UseActionRunnerOptions {
16
16
  onModal?: ModalHandler;
17
17
  /** Custom navigation handler — SPA-aware routing */
18
18
  onNavigate?: NavigationHandler;
19
+ /** Custom param-collection handler — show dialog to gather params before execution */
20
+ onParamCollection?: ParamCollectionHandler;
21
+ /** Custom result-dialog handler — one-shot reveal of API response on success */
22
+ onResultDialog?: ResultDialogHandler;
19
23
  }
20
24
  /**
21
25
  * Hook for executing actions with loading state, typed with ActionDef.
@@ -31,7 +31,7 @@ export function useActionRunner(optionsOrContext = {}) {
31
31
  const options = 'context' in optionsOrContext
32
32
  ? optionsOrContext
33
33
  : { context: optionsOrContext };
34
- const { context = {}, onConfirm, onToast, onModal, onNavigate } = options;
34
+ const { context = {}, onConfirm, onToast, onModal, onNavigate, onParamCollection, onResultDialog } = options;
35
35
  const [loading, setLoading] = useState(false);
36
36
  const [error, setError] = useState(null);
37
37
  const [result, setResult] = useState(null);
@@ -45,6 +45,10 @@ export function useActionRunner(optionsOrContext = {}) {
45
45
  r.setModalHandler(onModal);
46
46
  if (onNavigate)
47
47
  r.setNavigationHandler(onNavigate);
48
+ if (onParamCollection)
49
+ r.setParamCollectionHandler(onParamCollection);
50
+ if (onResultDialog)
51
+ r.setResultDialogHandler(onResultDialog);
48
52
  return r;
49
53
  // eslint-disable-next-line react-hooks/exhaustive-deps
50
54
  }, [JSON.stringify(context)]);
@@ -11,6 +11,14 @@
11
11
  * Bridge hook between the @objectstack/client notifications API
12
12
  * and the local NotificationContext state. Fetches server-side
13
13
  * notifications and surfaces them through the existing provider.
14
+ *
15
+ * ADR-0030 (Notification Convergence): the `client.notifications.*` helpers
16
+ * are the stable transport contract — the server routes them to the L5
17
+ * `sys_inbox_message` materialization and the `sys_notification_receipt`
18
+ * read-state spine (the re-modeled `sys_notification` L2 event carries no
19
+ * recipient/read columns). This hook only needs to call the SDK with its
20
+ * current signatures: `list(options)`, `markRead(ids[])`, `markAllRead()`,
21
+ * and `registerDevice(request)`.
14
22
  */
15
23
  import { useCallback, useContext, useEffect, useRef, useState } from 'react';
16
24
  import { SchemaRendererContext } from '../context/SchemaRendererContext';
@@ -111,7 +119,13 @@ export function useClientNotifications(options = {}) {
111
119
  return;
112
120
  setError(null);
113
121
  try {
114
- await client.notifications.registerDevice(token, platform);
122
+ // ADR-0030 / @objectstack/client v7+: registerDevice takes a single
123
+ // RegisterDeviceRequest object (`{ token, platform }`), not positional
124
+ // args. `platform` is the device platform enum.
125
+ await client.notifications.registerDevice({
126
+ token,
127
+ platform: platform,
128
+ });
115
129
  }
116
130
  catch (err) {
117
131
  const wrapped = err instanceof Error ? err : new Error('Failed to register device');
@@ -155,7 +169,11 @@ export function useClientNotifications(options = {}) {
155
169
  return;
156
170
  setError(null);
157
171
  try {
158
- await client.notifications.markAsRead(id);
172
+ // ADR-0030 / @objectstack/client v7+: the SDK exposes `markRead(ids[])`
173
+ // (batch) — there is no single-id `markAsRead`. Server-side this writes
174
+ // the `read` state to the `sys_notification_receipt` spine. We keep the
175
+ // friendly single-id hook API and adapt to the batch call.
176
+ await client.notifications.markRead([id]);
159
177
  }
160
178
  catch (err) {
161
179
  const wrapped = err instanceof Error ? err : new Error('Failed to mark as read');
@@ -14,6 +14,13 @@ import type { PageVariable } from '@object-ui/types';
14
14
  export interface PageVariablesContextValue {
15
15
  /** Current variable values */
16
16
  variables: Record<string, any>;
17
+ /**
18
+ * The variable definitions backing this provider. Exposed so writer
19
+ * elements can resolve which variable they feed via `PageVariable.source`
20
+ * (the component id that writes to the variable) — see
21
+ * {@link usePageVariableBinding}.
22
+ */
23
+ definitions: PageVariable[];
17
24
  /** Set a single variable value */
18
25
  setVariable: (name: string, value: any) => void;
19
26
  /** Set multiple variable values at once */
@@ -65,3 +72,36 @@ export declare function usePageVariables(): PageVariablesContextValue;
65
72
  * Hook to check if a PageVariablesProvider is available.
66
73
  */
67
74
  export declare function useHasPageVariables(): boolean;
75
+ /**
76
+ * A writer binding for a page variable, resolved from a component id.
77
+ */
78
+ export interface PageVariableBinding {
79
+ /** The bound variable's name */
80
+ name: string;
81
+ /** The bound variable's current value */
82
+ value: any;
83
+ /** Write a new value into the bound variable */
84
+ setValue: (value: any) => void;
85
+ }
86
+ /**
87
+ * Resolve the page variable that a given component writes to.
88
+ *
89
+ * Per `@objectstack/spec`'s `PageVariableSchema`, a variable's `source`
90
+ * names the **component id** that feeds it — e.g. a `variables` entry
91
+ * `{ name: 'selectedProjectId', source: 'project_picker' }` is written by the
92
+ * component whose `id` is `project_picker`. An interactive element (record
93
+ * picker, filter, …) calls this with its own `schema.id` and writes the
94
+ * user's selection through the returned `setValue`.
95
+ *
96
+ * Returns `null` when no variable targets the given component (or when called
97
+ * outside a {@link PageVariablesProvider}), so callers can treat themselves as
98
+ * uncontrolled in that case.
99
+ *
100
+ * @example
101
+ * ```tsx
102
+ * const binding = usePageVariableBinding(schema.id);
103
+ * // on selection:
104
+ * binding?.setValue(record.id);
105
+ * ```
106
+ */
107
+ export declare function usePageVariableBinding(componentId?: string): PageVariableBinding | null;
@@ -71,7 +71,8 @@ export const PageVariablesProvider = ({ definitions, children, }) => {
71
71
  const resetVariables = useCallback(() => {
72
72
  setVariablesState(initializeVariables(definitions));
73
73
  }, [definitions]);
74
- const value = useMemo(() => ({ variables, setVariable, setVariables, resetVariables }), [variables, setVariable, setVariables, resetVariables]);
74
+ const defs = useMemo(() => definitions ?? [], [definitions]);
75
+ const value = useMemo(() => ({ variables, definitions: defs, setVariable, setVariables, resetVariables }), [variables, defs, setVariable, setVariables, resetVariables]);
75
76
  return (_jsx(PageVariablesContext.Provider, { value: value, children: children }));
76
77
  };
77
78
  PageVariablesProvider.displayName = 'PageVariablesProvider';
@@ -94,6 +95,7 @@ export function usePageVariables() {
94
95
  // Graceful fallback — allows components to work outside a Page
95
96
  return {
96
97
  variables: {},
98
+ definitions: [],
97
99
  setVariable: () => { },
98
100
  setVariables: () => { },
99
101
  resetVariables: () => { },
@@ -107,3 +109,39 @@ export function usePageVariables() {
107
109
  export function useHasPageVariables() {
108
110
  return useContext(PageVariablesContext) !== null;
109
111
  }
112
+ /**
113
+ * Resolve the page variable that a given component writes to.
114
+ *
115
+ * Per `@objectstack/spec`'s `PageVariableSchema`, a variable's `source`
116
+ * names the **component id** that feeds it — e.g. a `variables` entry
117
+ * `{ name: 'selectedProjectId', source: 'project_picker' }` is written by the
118
+ * component whose `id` is `project_picker`. An interactive element (record
119
+ * picker, filter, …) calls this with its own `schema.id` and writes the
120
+ * user's selection through the returned `setValue`.
121
+ *
122
+ * Returns `null` when no variable targets the given component (or when called
123
+ * outside a {@link PageVariablesProvider}), so callers can treat themselves as
124
+ * uncontrolled in that case.
125
+ *
126
+ * @example
127
+ * ```tsx
128
+ * const binding = usePageVariableBinding(schema.id);
129
+ * // on selection:
130
+ * binding?.setValue(record.id);
131
+ * ```
132
+ */
133
+ export function usePageVariableBinding(componentId) {
134
+ const { variables, definitions, setVariable } = usePageVariables();
135
+ return useMemo(() => {
136
+ if (!componentId)
137
+ return null;
138
+ const def = definitions.find((d) => d.source === componentId);
139
+ if (!def)
140
+ return null;
141
+ return {
142
+ name: def.name,
143
+ value: variables[def.name],
144
+ setValue: (value) => setVariable(def.name, value),
145
+ };
146
+ }, [componentId, definitions, variables, setVariable]);
147
+ }
@@ -32,7 +32,6 @@ interface ListViewSpec {
32
32
  sort?: any;
33
33
  searchableFields?: string[];
34
34
  filterableFields?: string[];
35
- quickFilters?: any[];
36
35
  selection?: any;
37
36
  pagination?: any;
38
37
  rowHeight?: string;
@@ -5,7 +5,6 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- import { normalizeQuickFilters } from '@object-ui/core';
9
8
  function mapColumn(col) {
10
9
  const mapped = {
11
10
  accessorKey: col.field,
@@ -81,10 +80,6 @@ export const bridgeListView = (spec, _context) => {
81
80
  node.searchableFields = spec.searchableFields;
82
81
  if (spec.filterableFields)
83
82
  node.filterableFields = spec.filterableFields;
84
- // Normalize quickFilters: spec format { field, operator, value } → ObjectUI format
85
- const normalizedQF = normalizeQuickFilters(spec.quickFilters);
86
- if (normalizedQF)
87
- node.quickFilters = normalizedQF;
88
83
  if (spec.resizable != null)
89
84
  node.resizable = spec.resizable;
90
85
  if (spec.striped != null)
@@ -126,8 +121,6 @@ export const bridgeListView = (spec, _context) => {
126
121
  node.appearance = spec.appearance;
127
122
  if (spec.compactToolbar != null)
128
123
  node.compactToolbar = spec.compactToolbar;
129
- if (spec.tabs)
130
- node.tabs = spec.tabs;
131
124
  if (spec.addRecord)
132
125
  node.addRecord = spec.addRecord;
133
126
  if (spec.showRecordCount != null)
@@ -30,13 +30,6 @@ interface PageVariable {
30
30
  defaultValue?: any;
31
31
  source?: string;
32
32
  }
33
- interface BlankPageLayoutItem {
34
- componentId: string;
35
- x: number;
36
- y: number;
37
- width: number;
38
- height: number;
39
- }
40
33
  interface PageSpec {
41
34
  name?: string;
42
35
  label?: string;
@@ -47,13 +40,6 @@ interface PageSpec {
47
40
  regions?: PageRegion[];
48
41
  template?: string;
49
42
  object?: string;
50
- blankLayout?: {
51
- columns?: number;
52
- rowHeight?: number;
53
- gap?: number;
54
- items?: BlankPageLayoutItem[];
55
- };
56
- recordReview?: any;
57
43
  isDefault?: boolean;
58
44
  assignedProfiles?: string[];
59
45
  interfaceConfig?: any;
@@ -72,12 +72,8 @@ export const bridgePage = (spec, _context) => {
72
72
  node.template = spec.template;
73
73
  if (spec.object)
74
74
  node.object = spec.object;
75
- // P1.4 — Blank page layout
76
- if (spec.blankLayout)
77
- node.blankLayout = spec.blankLayout;
78
- // P1.4 — Additional page properties
79
- if (spec.recordReview)
80
- node.recordReview = spec.recordReview;
75
+ // Additional page properties (blankLayout/recordReview dropped — their page
76
+ // types have no renderer and were removed from @objectstack/spec, framework#2265)
81
77
  if (spec.isDefault != null)
82
78
  node.isDefault = spec.isDefault;
83
79
  if (spec.assignedProfiles)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/react",
3
- "version": "6.2.3",
3
+ "version": "7.1.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "React bindings and SchemaRenderer component for Object UI",
@@ -24,19 +24,19 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@objectstack/spec": "^6.7.1",
28
- "react-hook-form": "^7.76.1",
29
- "@object-ui/core": "6.2.3",
30
- "@object-ui/data-objectstack": "6.2.3",
31
- "@object-ui/i18n": "6.2.3",
32
- "@object-ui/types": "6.2.3"
27
+ "@objectstack/spec": "^10.0.0",
28
+ "react-hook-form": "^7.80.0",
29
+ "@object-ui/core": "7.1.0",
30
+ "@object-ui/data-objectstack": "7.1.0",
31
+ "@object-ui/i18n": "7.1.0",
32
+ "@object-ui/types": "7.1.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": "^18.0.0 || ^19.0.0",
36
36
  "react-dom": "^18.0.0 || ^19.0.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/react": "19.2.15",
39
+ "@types/react": "19.2.17",
40
40
  "@types/react-dom": "19.2.3",
41
41
  "typescript": "^6.0.3"
42
42
  },