@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 +192 -0
- package/dist/SchemaRenderer.d.ts +1 -1
- package/dist/SchemaRenderer.js +64 -18
- package/dist/context/ActionContext.d.ts +4 -1
- package/dist/context/ActionContext.js +18 -3
- package/dist/context/AppShellContext.d.ts +10 -0
- package/dist/context/AppShellContext.js +1 -0
- package/dist/context/DrillNavigationContext.d.ts +39 -0
- package/dist/context/DrillNavigationContext.js +31 -0
- package/dist/context/PreviewModeContext.d.ts +29 -0
- package/dist/context/PreviewModeContext.js +27 -0
- package/dist/context/SchemaRendererContext.d.ts +1 -1
- package/dist/context/ThemeContext.d.ts +1 -1
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/hooks/useActionEngine.js +46 -3
- package/dist/hooks/useActionRunner.d.ts +5 -1
- package/dist/hooks/useActionRunner.js +5 -1
- package/dist/hooks/useClientNotifications.js +20 -2
- package/dist/hooks/usePageVariables.d.ts +40 -0
- package/dist/hooks/usePageVariables.js +39 -1
- package/dist/spec-bridge/bridges/list-view.d.ts +0 -1
- package/dist/spec-bridge/bridges/list-view.js +0 -7
- package/dist/spec-bridge/bridges/page.d.ts +0 -14
- package/dist/spec-bridge/bridges/page.js +2 -6
- package/package.json +8 -8
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
|
package/dist/SchemaRenderer.d.ts
CHANGED
|
@@ -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> |
|
|
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;
|
package/dist/SchemaRenderer.js
CHANGED
|
@@ -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
|
-
|
|
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 = (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
-
}) =>
|
|
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):
|
|
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
|
*
|
package/dist/context/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/context/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
|
@@ -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
|
-
//
|
|
76
|
-
|
|
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": "
|
|
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": "^
|
|
28
|
-
"react-hook-form": "^7.
|
|
29
|
-
"@object-ui/core": "
|
|
30
|
-
"@object-ui/data-objectstack": "
|
|
31
|
-
"@object-ui/i18n": "
|
|
32
|
-
"@object-ui/types": "
|
|
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.
|
|
39
|
+
"@types/react": "19.2.17",
|
|
40
40
|
"@types/react-dom": "19.2.3",
|
|
41
41
|
"typescript": "^6.0.3"
|
|
42
42
|
},
|