@object-ui/react 6.2.2 → 7.0.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,176 @@
1
1
  # @object-ui/react
2
2
 
3
+ ## 7.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c12986e: Add resultDialog + target interpolation for one-shot action reveals
8
+
9
+ Some platform actions return values the user MUST copy now because the
10
+ server will not surface them again — 2FA TOTP URI + backup codes, freshly
11
+ minted OAuth client_secret, regenerated recovery codes. Previously these
12
+ had to ship as bespoke pages in `apps/account` because actions only
13
+ emitted a fire-and-forget toast.
14
+
15
+ **`@object-ui/core` — ActionRunner**
16
+
17
+ - New `ActionDef.resultDialog: ResultDialogSpec` field. When set on a
18
+ successful action, the runner suppresses the `successMessage` toast and
19
+ awaits the registered `ResultDialogHandler` instead. Missing handler is
20
+ non-fatal (logs a warning); rejected handler is treated as acknowledged.
21
+ - New `setResultDialogHandler(handler)` setter.
22
+ - New types: `ResultDialogSpec`, `ResultDialogFieldSpec`,
23
+ `ResultDialogHandler`.
24
+ - `executeUrl` and `executeAPI` now run `${param.X}` and `${ctx.X}`
25
+ interpolation against `target` before fetching / navigating. Values are
26
+ `encodeURIComponent`'d, missing keys resolve to empty string. `ctx`
27
+ exposes `origin`, `user`, `org`, `recordId` by default; consumers can
28
+ inject more via `context.ctx`.
29
+
30
+ **`@object-ui/react`**
31
+
32
+ - `ActionProvider` and `useActionRunner` both gained an `onResultDialog`
33
+ option that wires straight through to the runner.
34
+
35
+ **`@object-ui/app-shell`**
36
+
37
+ - New `ActionResultDialog` component — promise-based, blocks click-outside
38
+ and Escape (the user MUST click acknowledge), renders five field
39
+ formats: `qrcode` (client-side via the `qrcode` package — never sent
40
+ off-device, so 2FA URIs stay secret), `code-list`, `secret`, `text`,
41
+ `json`. Falls back to `json` when a value's shape doesn't match its
42
+ declared format.
43
+ - `ObjectView` and `RecordDetailView` install the handler and mount the
44
+ dialog automatically, so any action with `resultDialog` declared in
45
+ metadata now works without code changes.
46
+ - New dependency: `qrcode@^1.5.x` for client-side QR rendering.
47
+
48
+ Pairs with the framework-side `Action.resultDialog` schema added in
49
+ `@objectstack/spec` and the `sys_two_factor` / `sys_oauth_application` /
50
+ `sys_account` updates in `@objectstack/platform-objects`.
51
+
52
+ ### Patch Changes
53
+
54
+ - 6cfa330: feat(dashboard): drill "Open in list" escape hatch + unify report drill
55
+
56
+ Adopts the mainstream BI peek-then-escalate drill model. Drill-through opens an
57
+ in-place drawer (keep context) and offers an "Open in list →" affordance to
58
+ escalate to the object's full list page (sort / bulk-select / export / shareable
59
+ URL) — the Looker / Power BI "see records → open in page" pattern.
60
+
61
+ - New `DrillNavigationContext` (`@object-ui/react`): the app shell provides
62
+ `openRecordList`; the renderer stays decoupled from console routing.
63
+ - The drill drawers (pivot / dataset / chart / KPI) render the escape hatch when
64
+ a host navigation handler is present, and hide it otherwise (self-contained
65
+ peek). `DashboardView` provides the handler via `useOpenRecordList`.
66
+ - `DrillDownConfig.target` gains `'navigate'` — skip the drawer and open the
67
+ list directly; degrades to `'drawer'` when no host handler is available.
68
+ - `ReportView` drill-through now opens the same in-place drawer (peek records →
69
+ click a row to open a record) instead of navigating away; the escape hatch
70
+ preserves the previous navigate-to-list behavior. Dashboard and report drill
71
+ are now unified.
72
+ - i18n: `dashboard.openInList` (en / zh).
73
+
74
+ - 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`.
75
+ - abe8ebc: Repoint `useClientNotifications` to the ADR-0030 `@objectstack/client` surface
76
+
77
+ The `useClientNotifications` bridge hook called `client.notifications.*` with
78
+ signatures that no longer exist on `@objectstack/client@7.x`:
79
+
80
+ - `registerDevice(token, platform)` → the SDK takes a single
81
+ `RegisterDeviceRequest` object (`{ token, platform }`).
82
+ - `markAsRead(id)` → there is no single-id method; the SDK exposes
83
+ `markRead(ids: string[])`. The hook keeps its friendly single-id API and
84
+ adapts to the batch call.
85
+
86
+ These helpers are the stable transport contract for ADR-0030 (Notification
87
+ Convergence): server-side they route to the L5 `sys_inbox_message`
88
+ materialization and the `sys_notification_receipt` read-state spine — the
89
+ re-modeled `sys_notification` L2 event no longer carries recipient/read
90
+ columns. (The Console bell itself reads the inbox + receipts directly via the
91
+ generic data API; see the `@object-ui/app-shell` bell cut-over.)
92
+
93
+ ## Cut-over sequence (operational — run in this order)
94
+
95
+ The Console UI repoint must land together with the framework pipeline **and**
96
+ the data migration so the bell is never blank and read-state is never lost:
97
+
98
+ 1. Deploy the framework ADR-0030 change (objects + `emit()` + producers). New
99
+ notifications now land in `sys_inbox_message` + `sys_notification_receipt`.
100
+ 2. Run the data migration **once** to carry existing notifications across —
101
+ `migrateSysNotificationToEvent({ driver, data })` from
102
+ `@objectstack/metadata/migrations`. It splits each legacy `sys_notification`
103
+ inbox row into a `sys_inbox_message` + receipt, rewrites the row to the event
104
+ shape, and clears the legacy columns. It is **idempotent** and reports
105
+ `not_applicable` on fresh installs. This runs against the ObjectStack
106
+ **server/data engine** — it is not a Console (frontend) step.
107
+ 3. Deploy the objectui repoint (this change + the `@object-ui/app-shell` bell
108
+ cut-over).
109
+
110
+ - 2270239: feat: scoped style-object rendering (ADR-0065)
111
+
112
+ A metadata node may carry `responsiveStyles` (per-breakpoint CSS-property maps);
113
+ `SchemaRenderer` compiles it to **id-scoped CSS** injected as a `<style>` tag and
114
+ appends a scope class to the node. Build-independent (arbitrary values + design
115
+ tokens pass through verbatim — no Tailwind JIT), collision-free (per-node scope,
116
+ unlayered so it beats base utilities), responsive-correct (model breakpoint maps
117
+ → generated `@media`, never `md:` variant classes). Adds `compileScopedStyles`/
118
+ `scopeClassFor`/`hasResponsiveStyles` to `@object-ui/core` and an SDUI design-token
119
+ palette (`--space-*`, `--surface`, `--brand`, …) to the theme. Mirrors Builder.io.
120
+
121
+ - Updated dependencies [5976ba3]
122
+ - Updated dependencies [eaccefd]
123
+ - Updated dependencies [f7f325d]
124
+ - Updated dependencies [c12986e]
125
+ - Updated dependencies [71d7ce0]
126
+ - Updated dependencies [30ee761]
127
+ - Updated dependencies [053c948]
128
+ - Updated dependencies [b99d9bd]
129
+ - Updated dependencies [053c948]
130
+ - Updated dependencies [89e113c]
131
+ - Updated dependencies [ddbe4a2]
132
+ - Updated dependencies [2d47e94]
133
+ - Updated dependencies [9049bbe]
134
+ - Updated dependencies [77cc6bb]
135
+ - Updated dependencies [97c6831]
136
+ - Updated dependencies [cb2fdb1]
137
+ - Updated dependencies [a58c6b8]
138
+ - Updated dependencies [c3749eb]
139
+ - Updated dependencies [c09f44e]
140
+ - Updated dependencies [6cfa330]
141
+ - Updated dependencies [ad8ade6]
142
+ - Updated dependencies [d54346c]
143
+ - Updated dependencies [5332639]
144
+ - Updated dependencies [3870c20]
145
+ - Updated dependencies [b88c560]
146
+ - Updated dependencies [0ad72a6]
147
+ - Updated dependencies [3fa23a7]
148
+ - Updated dependencies [18d0339]
149
+ - Updated dependencies [59b6bbb]
150
+ - Updated dependencies [d16566f]
151
+ - Updated dependencies [1394e34]
152
+ - Updated dependencies [300d755]
153
+ - Updated dependencies [bd8b054]
154
+ - Updated dependencies [4eb9cb6]
155
+ - Updated dependencies [7c239fd]
156
+ - Updated dependencies [858ad94]
157
+ - Updated dependencies [2270239]
158
+ - Updated dependencies [2f31406]
159
+ - Updated dependencies [8d1195d]
160
+ - @object-ui/core@7.0.0
161
+ - @object-ui/data-objectstack@7.0.0
162
+ - @object-ui/i18n@7.0.0
163
+ - @object-ui/types@7.0.0
164
+
165
+ ## 6.2.3
166
+
167
+ ### Patch Changes
168
+
169
+ - @object-ui/types@6.2.3
170
+ - @object-ui/core@6.2.3
171
+ - @object-ui/i18n@6.2.3
172
+ - @object-ui/data-objectstack@6.2.3
173
+
3
174
  ## 6.2.2
4
175
 
5
176
  ### 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,9 @@ 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';
12
13
  import { resolveI18nLabel } from './utils/i18n';
13
14
  /**
14
15
  * Dev-mode schema validation.
@@ -137,6 +138,10 @@ export class SchemaErrorBoundary extends Component {
137
138
  export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
138
139
  const context = useContext(SchemaRendererContext);
139
140
  const dataSource = context?.dataSource || {};
141
+ // Ambient host scope (user / app / features), fed by app-shell's
142
+ // ExpressionProvider. Threaded into `visible`/expression evaluation so
143
+ // component predicates can gate on the signed-in user & deployment flags.
144
+ const predicateScope = usePredicateScope();
140
145
  // Re-render trigger when the global ComponentRegistry mutates (e.g. a
141
146
  // lazy-loaded plugin finishes registering its components).
142
147
  const [, forceUpdate] = useReducer((n) => n + 1, 0);
@@ -151,11 +156,20 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
151
156
  return unsubscribe;
152
157
  }, []);
153
158
  const [lazyError, setLazyError] = useState(null);
159
+ // Stable fallback id for scoping a styled node that didn't declare an `id`.
160
+ const autoStyleId = React.useId();
154
161
  // Evaluate schema expressions against the data source
155
162
  const evaluatedSchema = useMemo(() => {
156
163
  if (!schema || typeof schema === 'string')
157
164
  return schema;
158
- const evaluator = new ExpressionEvaluator({ data: dataSource });
165
+ // `data` (record/datasource) plus the ambient host scope. `current_user`
166
+ // is aliased to `user` so both `user.email` and `current_user.email`
167
+ // resolve in component `visible`/`visibleOn` expressions.
168
+ const evaluator = new ExpressionEvaluator({
169
+ ...predicateScope,
170
+ current_user: predicateScope?.user,
171
+ data: dataSource,
172
+ });
159
173
  // Shallow copy
160
174
  const newSchema = { ...schema };
161
175
  // COMPAT: Hoist 'properties' up to schema level
@@ -226,7 +240,7 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
226
240
  newSchema._disabled = true;
227
241
  }
228
242
  return newSchema;
229
- }, [schema, dataSource]);
243
+ }, [schema, dataSource, predicateScope]);
230
244
  if (!evaluatedSchema)
231
245
  return null;
232
246
  // Handle visibility: if evaluated schema is hidden, render nothing
@@ -263,7 +277,23 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
263
277
  // Extract schema metadata properties that should NOT be passed as React props
264
278
  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
265
279
  _disabled: __disabled, // stripped: internal disabled flag
280
+ responsiveStyles: _responsiveStyles, // stripped: compiled to scoped CSS, not a DOM prop
266
281
  ...componentProps } = evaluatedSchema;
282
+ // SDUI scoped styling (ADR-0065): a node's `responsiveStyles` compiles to
283
+ // id-scoped CSS injected as a <style> tag, and a scope class is appended to
284
+ // the node's className. Build-independent, collision-free, responsive-correct.
285
+ const hasScopedStyles = hasResponsiveStyles(_responsiveStyles);
286
+ const scopeClass = hasScopedStyles ? scopeClassFor(evaluatedSchema.id ?? autoStyleId) : '';
287
+ const scopedCss = hasScopedStyles ? compileScopedStyles(`.${scopeClass}`, _responsiveStyles) : '';
288
+ const mergedClassName = scopeClass
289
+ ? [evaluatedSchema.className, scopeClass].filter(Boolean).join(' ')
290
+ : evaluatedSchema.className;
291
+ // Some renderers read `schema.className` directly (e.g. element:text) while
292
+ // others read the `className` prop (e.g. flex/container). Set both so the
293
+ // scope class lands regardless of which channel a renderer honours.
294
+ const schemaForComponent = scopeClass
295
+ ? { ...evaluatedSchema, className: mergedClassName }
296
+ : evaluatedSchema;
267
297
  // Extract AriaPropsSchema properties for accessibility
268
298
  const ariaProps = resolveAriaProps(evaluatedSchema);
269
299
  // Debug-mode enhancements: extra data attributes + perf tracking
@@ -277,19 +307,19 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
277
307
  }
278
308
  debugTime(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
279
309
  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
- }) }));
310
+ 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, {
311
+ schema: schemaForComponent,
312
+ ...componentProps, // Spread non-metadata schema properties as props
313
+ ...(evaluatedSchema.props || {}), // Override with explicit props if provided
314
+ ...ariaProps, // Inject ARIA attributes from AriaPropsSchema
315
+ ...debugAttrs, // Debug-mode data attributes
316
+ disabled: __disabled || undefined,
317
+ className: mergedClassName,
318
+ 'data-obj-id': evaluatedSchema.id,
319
+ 'data-obj-type': evaluatedSchema.type,
320
+ ...(__DEV__ && !_validation.valid ? { 'data-obj-schema-invalid': 'true' } : {}),
321
+ ...props
322
+ })] }));
293
323
  debugTimeEnd(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
294
324
  // Report render perf to DebugCollector when debug mode is active
295
325
  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');
@@ -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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/react",
3
- "version": "6.2.2",
3
+ "version": "7.0.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.2",
30
- "@object-ui/data-objectstack": "6.2.2",
31
- "@object-ui/i18n": "6.2.2",
32
- "@object-ui/types": "6.2.2"
27
+ "@objectstack/spec": "^10.0.0",
28
+ "react-hook-form": "^7.80.0",
29
+ "@object-ui/core": "7.0.0",
30
+ "@object-ui/data-objectstack": "7.0.0",
31
+ "@object-ui/i18n": "7.0.0",
32
+ "@object-ui/types": "7.0.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
  },