@object-ui/react 17.6.0 → 18.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 +4263 -0
- package/README.md +383 -42
- package/dist/SchemaRenderer.d.ts +64 -7
- package/dist/SchemaRenderer.js +1326 -106
- package/dist/context/ActionContext.js +40 -1
- package/dist/context/AppShellContext.d.ts +27 -0
- package/dist/context/DiscussionContext.d.ts +27 -0
- package/dist/context/DiscussionContext.js +4 -0
- package/dist/context/DrillNavigationContext.d.ts +13 -0
- package/dist/context/InlineEditContext.d.ts +151 -1
- package/dist/context/InlineEditContext.js +36 -2
- package/dist/context/PrintPageContext.d.ts +60 -0
- package/dist/context/PrintPageContext.js +32 -0
- package/dist/context/RecordContext.d.ts +48 -5
- package/dist/context/RecordContext.js +10 -1
- package/dist/context/RelatedRecordActionsContext.d.ts +72 -2
- package/dist/context/RelatedRecordActionsContext.js +4 -0
- package/dist/context/SchemaRendererContext.d.ts +61 -6
- package/dist/context/SchemaRendererContext.js +39 -6
- package/dist/context/ThemeContext.js +1 -1
- package/dist/context/actionRunnerTranslator.d.ts +27 -0
- package/dist/context/actionRunnerTranslator.js +37 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +1 -0
- package/dist/element-data-source/ElementApiDataSourcePanel.d.ts +28 -0
- package/dist/element-data-source/ElementApiDataSourcePanel.js +10 -0
- package/dist/element-data-source/ElementDataSourceGate.d.ts +118 -3
- package/dist/element-data-source/ElementDataSourceGate.js +401 -19
- package/dist/element-data-source/useElementApiDataSource.d.ts +119 -0
- package/dist/element-data-source/useElementApiDataSource.js +198 -0
- package/dist/hooks/index.d.ts +3 -1
- package/dist/hooks/index.js +10 -1
- package/dist/hooks/useActionEngine.js +46 -3
- package/dist/hooks/useActionRunner.js +4 -0
- package/dist/hooks/useActionTextLocalizer.d.ts +35 -9
- package/dist/hooks/useActionTextLocalizer.js +70 -14
- package/dist/hooks/useConfigBagEvaluator.d.ts +31 -0
- package/dist/hooks/useConfigBagEvaluator.js +40 -0
- package/dist/hooks/useDatasetDimensionLabels.js +25 -10
- package/dist/hooks/useDensityMode.d.ts +8 -2
- package/dist/hooks/useDensityMode.js +8 -2
- package/dist/hooks/useDiscovery.d.ts +1 -10
- package/dist/hooks/useETagCache.js +25 -4
- package/dist/hooks/useElementDataSource.d.ts +15 -2
- package/dist/hooks/useElementDataSource.js +24 -7
- package/dist/hooks/useExpression.d.ts +52 -59
- package/dist/hooks/useExpression.js +58 -90
- package/dist/hooks/useFilterScope.d.ts +22 -2
- package/dist/hooks/useFilterScope.js +29 -4
- package/dist/hooks/useGlobalUndo.d.ts +10 -0
- package/dist/hooks/useGlobalUndo.js +112 -46
- package/dist/hooks/useNavigationOverlay.d.ts +76 -11
- package/dist/hooks/useNavigationOverlay.js +119 -9
- package/dist/hooks/useOffline.js +65 -11
- package/dist/hooks/usePageAssignment.d.ts +33 -7
- package/dist/hooks/usePageAssignment.js +85 -21
- package/dist/hooks/useRecordSearch.d.ts +34 -1
- package/dist/hooks/useRecordSearch.js +42 -7
- package/dist/hooks/useResolvedFilter.d.ts +46 -0
- package/dist/hooks/useResolvedFilter.js +130 -0
- package/dist/hooks/useSchemaPersistence.d.ts +7 -0
- package/dist/hooks/useSchemaPersistence.js +144 -5
- package/dist/hooks/useSettledSchema.d.ts +123 -0
- package/dist/hooks/useSettledSchema.js +186 -0
- package/dist/hooks/useViewData.d.ts +10 -3
- package/dist/hooks/useViewData.js +5 -14
- package/dist/index.d.ts +9 -1
- package/dist/index.js +48 -1
- package/dist/schema-input.d.ts +38 -8
- package/dist/schema-input.js +39 -7
- package/dist/utils/configBag.d.ts +68 -0
- package/dist/utils/configBag.js +70 -0
- package/dist/utils/error-message.d.ts +1 -1
- package/dist/utils/error-message.js +1 -1
- package/dist/utils/inlineAria.d.ts +61 -0
- package/dist/utils/inlineAria.js +70 -0
- package/dist/utils/localeMap.d.ts +27 -0
- package/dist/utils/localeMap.js +106 -0
- package/dist/utils/nonGridRowCeiling.d.ts +44 -0
- package/dist/utils/nonGridRowCeiling.js +78 -0
- package/dist/utils/paramsBag.d.ts +28 -0
- package/dist/utils/paramsBag.js +146 -0
- package/dist/utils/propsBagDiagnostic.d.ts +196 -0
- package/dist/utils/propsBagDiagnostic.js +422 -0
- package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
- package/dist/utils/refusedDataPropDiagnostic.js +94 -0
- package/dist/utils/unevaluatedExpression.d.ts +44 -1
- package/dist/utils/unevaluatedExpression.js +92 -11
- package/dist/utils/visibilityDiagnostic.d.ts +282 -21
- package/dist/utils/visibilityDiagnostic.js +321 -46
- package/package.json +10 -10
- package/dist/hooks/useClientNotifications.d.ts +0 -47
- package/dist/hooks/useClientNotifications.js +0 -162
- package/dist/spec-bridge/SpecBridge.d.ts +0 -28
- package/dist/spec-bridge/SpecBridge.js +0 -56
- package/dist/spec-bridge/bridges/form-view.d.ts +0 -158
- package/dist/spec-bridge/bridges/form-view.js +0 -170
- package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
- package/dist/spec-bridge/bridges/list-view.js +0 -268
- package/dist/spec-bridge/index.d.ts +0 -11
- package/dist/spec-bridge/index.js +0 -10
- package/dist/spec-bridge/types.d.ts +0 -55
- package/dist/spec-bridge/types.js +0 -8
package/README.md
CHANGED
|
@@ -25,10 +25,11 @@ npm install @object-ui/react @object-ui/core
|
|
|
25
25
|
|
|
26
26
|
```tsx
|
|
27
27
|
import { SchemaRenderer } from '@object-ui/react'
|
|
28
|
+
import type { DeclaredNode } from '@object-ui/types'
|
|
28
29
|
|
|
29
|
-
const schema = {
|
|
30
|
+
const schema: DeclaredNode = {
|
|
30
31
|
type: 'text',
|
|
31
|
-
|
|
32
|
+
content: 'Hello, Object UI!'
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function App() {
|
|
@@ -36,29 +37,57 @@ function App() {
|
|
|
36
37
|
}
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
`schema` takes a `DeclaredNode` from `@object-ui/types`, a bare string, or nothing. A
|
|
41
|
+
`DeclaredNode` is a node of a declared type, keyed by its `type`: a component schema that
|
|
42
|
+
package declares, a spec page block such as `element:text` with its typed `properties` bag, a
|
|
43
|
+
stored page document under its page kind, or a type your application registers and declares
|
|
44
|
+
in `CustomNodeRegistry`. Each node, nested ones included, is checked against its own type; a
|
|
45
|
+
`type` nothing declares is refused.
|
|
46
|
+
|
|
39
47
|
### With Data
|
|
40
48
|
|
|
49
|
+
Expression scope reaches the renderer through `PredicateScopeProvider`, never through a prop
|
|
50
|
+
on the element: `SchemaRenderer` declares only `schema`, so anything else is forwarded to the
|
|
51
|
+
component the schema names (see the open forwarding surface below). Every key of the `scope`
|
|
52
|
+
you publish becomes a root the evaluator can read.
|
|
53
|
+
|
|
54
|
+
⛔ `SchemaRendererProvider`'s `dataSource` is **not** an expression root. It carries the
|
|
55
|
+
host's `DataSource` adapter, and objectui#9308 removed the binding that used to publish that
|
|
56
|
+
adapter under the name `data` — an adapter answers no `data.*` path, so the root was constant
|
|
57
|
+
for every conformant host.
|
|
58
|
+
|
|
41
59
|
```tsx
|
|
42
|
-
import { SchemaRenderer } from '@object-ui/react'
|
|
60
|
+
import { SchemaRenderer, PredicateScopeProvider } from '@object-ui/react'
|
|
61
|
+
import type { DeclaredNode } from '@object-ui/types'
|
|
43
62
|
|
|
44
|
-
const schema = {
|
|
63
|
+
const schema: DeclaredNode = {
|
|
45
64
|
type: 'form',
|
|
46
|
-
|
|
65
|
+
children: [
|
|
66
|
+
{
|
|
67
|
+
// `content` is evaluated on every component type. `input` has no row in
|
|
68
|
+
// the spec's expression carriage map, so a `${…}` in ITS `value` would be
|
|
69
|
+
// rendered as those characters rather than resolved.
|
|
70
|
+
type: 'text',
|
|
71
|
+
content: 'Editing ${user.name}'
|
|
72
|
+
},
|
|
47
73
|
{
|
|
48
74
|
type: 'input',
|
|
49
75
|
name: 'name',
|
|
50
|
-
label: 'Name'
|
|
51
|
-
value: '${user.name}'
|
|
76
|
+
label: 'Name'
|
|
52
77
|
}
|
|
53
78
|
]
|
|
54
79
|
}
|
|
55
80
|
|
|
56
|
-
const
|
|
81
|
+
const scope = {
|
|
57
82
|
user: { name: 'John Doe' }
|
|
58
83
|
}
|
|
59
84
|
|
|
60
85
|
function App() {
|
|
61
|
-
return
|
|
86
|
+
return (
|
|
87
|
+
<PredicateScopeProvider scope={scope}>
|
|
88
|
+
<SchemaRenderer schema={schema} />
|
|
89
|
+
</PredicateScopeProvider>
|
|
90
|
+
)
|
|
62
91
|
}
|
|
63
92
|
```
|
|
64
93
|
|
|
@@ -66,14 +95,19 @@ function App() {
|
|
|
66
95
|
|
|
67
96
|
```tsx
|
|
68
97
|
import { SchemaRenderer } from '@object-ui/react'
|
|
98
|
+
import type { DeclaredNode } from '@object-ui/types'
|
|
99
|
+
|
|
100
|
+
declare const formSchema: DeclaredNode
|
|
69
101
|
|
|
70
102
|
function App() {
|
|
71
|
-
const handleSubmit = (data) => {
|
|
103
|
+
const handleSubmit = (data: Record<string, unknown>) => {
|
|
72
104
|
console.log('Form submitted:', data)
|
|
73
105
|
}
|
|
74
106
|
|
|
107
|
+
// `onSubmit` is not a prop `SchemaRenderer` reads: it is forwarded to the
|
|
108
|
+
// component the schema names, which is why the props type stays open.
|
|
75
109
|
return (
|
|
76
|
-
<SchemaRenderer
|
|
110
|
+
<SchemaRenderer
|
|
77
111
|
schema={formSchema}
|
|
78
112
|
onSubmit={handleSubmit}
|
|
79
113
|
/>
|
|
@@ -87,20 +121,37 @@ Injects the host's data source (and optional capabilities) into every renderer
|
|
|
87
121
|
below it:
|
|
88
122
|
|
|
89
123
|
```tsx
|
|
90
|
-
import { SchemaRendererProvider } from '@object-ui/react'
|
|
124
|
+
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'
|
|
125
|
+
import type { ApiFetch } from '@object-ui/react'
|
|
126
|
+
import type { DataSource, DeclaredNode } from '@object-ui/types'
|
|
127
|
+
|
|
128
|
+
declare const adapter: DataSource
|
|
129
|
+
declare const authenticatedFetch: ApiFetch
|
|
130
|
+
declare const schema: DeclaredNode
|
|
91
131
|
|
|
92
132
|
<SchemaRendererProvider
|
|
93
133
|
dataSource={adapter}
|
|
94
|
-
// Optional: host-authenticated fetch
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
134
|
+
// Optional: host-authenticated fetch the renderers use for their own reads
|
|
135
|
+
// of the host's API (chart metadata, dataset dimension labels, permission
|
|
136
|
+
// probes), so they carry the same credentials (Authorization, tenant,
|
|
137
|
+
// locale headers) as the native data channel. When omitted, those reads
|
|
138
|
+
// fall back to the bare global fetch (cookies only).
|
|
98
139
|
apiFetch={authenticatedFetch}
|
|
99
140
|
>
|
|
100
141
|
<SchemaRenderer schema={schema} />
|
|
101
142
|
</SchemaRendererProvider>
|
|
102
143
|
```
|
|
103
144
|
|
|
145
|
+
`dataSource` carries the host's adapter as the published `DataSource` contract
|
|
146
|
+
(`@object-ui/types`), typed `DataSource | null | undefined`: a host either hands
|
|
147
|
+
over an adapter or states that it has none — a Studio preview, a page rendered
|
|
148
|
+
before the host's adapter has connected, or a widget probe driving `apiFetch`
|
|
149
|
+
alone. Both spellings of "none" are accepted because every reader in the tree
|
|
150
|
+
guards for it. The prop used to be typed `any`, so a string passed where the
|
|
151
|
+
adapter belongs — the shape of a config value read from the wrong place —
|
|
152
|
+
raised nothing until the first `find()` at runtime (objectui#7912).
|
|
153
|
+
`useSchemaContext()` hands the same type back to every consumer.
|
|
154
|
+
|
|
104
155
|
Nested providers inherit `apiFetch` from their parent when they don't supply
|
|
105
156
|
their own, so re-wrapped subtrees (embedded pages, preview surfaces) keep the
|
|
106
157
|
host's authentication.
|
|
@@ -109,15 +160,23 @@ host's authentication.
|
|
|
109
160
|
|
|
110
161
|
### useSchemaContext
|
|
111
162
|
|
|
112
|
-
Access
|
|
163
|
+
Access what `SchemaRendererProvider` injected — `dataSource`, `debug`,
|
|
164
|
+
`debugFlags` and `apiFetch`. It does **not** carry the page's values: read those
|
|
165
|
+
with `useDataScope`, which addresses the ambient expression scope by path. The
|
|
166
|
+
two are different channels on purpose — `dataSource` is the adapter a renderer
|
|
167
|
+
queries, `useDataScope` is what a node's `bind` resolves against
|
|
168
|
+
(objectui#9308).
|
|
113
169
|
|
|
114
170
|
```tsx
|
|
115
|
-
import { useSchemaContext } from '@object-ui/react'
|
|
171
|
+
import { useDataScope, useSchemaContext } from '@object-ui/react'
|
|
116
172
|
|
|
117
173
|
function MyComponent() {
|
|
118
|
-
const {
|
|
119
|
-
|
|
120
|
-
|
|
174
|
+
const { dataSource, debug } = useSchemaContext()
|
|
175
|
+
// Resolves `scope.value` from the nearest PredicateScopeProvider above.
|
|
176
|
+
const value = useDataScope('value')
|
|
177
|
+
|
|
178
|
+
if (!dataSource) return null
|
|
179
|
+
return <div data-debug={debug}>{String(value)}</div>
|
|
121
180
|
}
|
|
122
181
|
```
|
|
123
182
|
|
|
@@ -130,25 +189,65 @@ views so `view` can be matched); these two apply the composed result to the
|
|
|
130
189
|
block's schema and render the two non-final states.
|
|
131
190
|
|
|
132
191
|
```tsx
|
|
192
|
+
import type { FC } from 'react'
|
|
133
193
|
import { ElementDataSourceGate } from '@object-ui/react'
|
|
194
|
+
import type { ElementDataSourceMapping } from '@object-ui/react'
|
|
195
|
+
// The seam is imported from CORE, not from here — see the note below.
|
|
196
|
+
import { elementDataSourceBlock } from '@object-ui/core'
|
|
197
|
+
import type { BaseSchema } from '@object-ui/types'
|
|
198
|
+
|
|
199
|
+
// The block this renderer wraps — whatever the registry resolves for its type.
|
|
200
|
+
declare const ObjectGrid: FC<{ schema: BaseSchema }>
|
|
134
201
|
|
|
135
202
|
// `mapping` names ONLY the keys this block reads. A composed value written onto
|
|
136
203
|
// a key the block ignores would be accepted and silently dropped — the defect
|
|
137
204
|
// the binding exists to remove.
|
|
138
|
-
const OBJECT_GRID_BINDING = {
|
|
205
|
+
const OBJECT_GRID_BINDING: ElementDataSourceMapping = {
|
|
139
206
|
columns: true, // the view's FIELD list may fill `schema.columns`
|
|
140
207
|
filter: true, // AND-combined, never replaced ("additional criteria")
|
|
141
208
|
sort: true,
|
|
142
209
|
limit: 'pagination.pageSize' as const,
|
|
143
210
|
}
|
|
144
211
|
|
|
145
|
-
|
|
212
|
+
// `elementDataSourceBlock` is not optional decoration — see below.
|
|
213
|
+
const ObjectGridRenderer = elementDataSourceBlock<FC<{ schema: BaseSchema }>>(({ schema, ...props }) => (
|
|
146
214
|
<ElementDataSourceGate schema={schema} mapping={OBJECT_GRID_BINDING} testId="object-grid">
|
|
147
215
|
{(bound) => <ObjectGrid schema={bound} {...props} />}
|
|
148
216
|
</ElementDataSourceGate>
|
|
149
|
-
)
|
|
217
|
+
))
|
|
150
218
|
```
|
|
151
219
|
|
|
220
|
+
**Say when the placement cannot draw anything.** Two boolean props let the call
|
|
221
|
+
site, which knows its block's other record sources, ask the gate to answer instead
|
|
222
|
+
of mounting an empty block. `requiresDataSource` draws a "no data source" panel
|
|
223
|
+
when no adapter resolves. `requiresObject` draws a short "No object named: set
|
|
224
|
+
objectName or dataSource.object." hint when the node names its object in neither
|
|
225
|
+
place, read after the binding lands on the mapping's object key, so a node bound
|
|
226
|
+
by `dataSource.object` never sees it (objectui#11605). Leave either one false for
|
|
227
|
+
a placement that draws from inline rows or another source the block reads.
|
|
228
|
+
|
|
229
|
+
**Wrap the registered renderer in `elementDataSourceBlock`.** It is what makes
|
|
230
|
+
`ComponentRegistry.register` emit the `dataSource` input on that block's
|
|
231
|
+
authoring surface, so the key the gate READS is also the key the manifest, the
|
|
232
|
+
save gate, the generated JSX types and the designer DECLARE. Skip it and the
|
|
233
|
+
binding still works at runtime while the html tier reports `dataSource` as a prop
|
|
234
|
+
that does not exist — the one spelling that resolves a saved view, reported
|
|
235
|
+
exactly like the spellings that do nothing (objectui#6678). The declaration is
|
|
236
|
+
emitted from this one seam, so never hand-write a `dataSource` entry in a block's
|
|
237
|
+
`inputs`. `pnpm check:element-data-source-declaration` fails any source that
|
|
238
|
+
consumes the gate without reaching the seam.
|
|
239
|
+
|
|
240
|
+
⚠️ **Import the seam from `@object-ui/core`, not from this package** — it is one
|
|
241
|
+
function under one name, re-exported here for discoverability, and the check
|
|
242
|
+
above enforces the core import at call sites. The reason is measured, not
|
|
243
|
+
stylistic: a registration runs at MODULE SCOPE, 101 suites in this repo partially
|
|
244
|
+
mock `@object-ui/react` by hand-listing the exports they return, and a
|
|
245
|
+
module-scope read of a name absent from such a list throws at COLLECTION time —
|
|
246
|
+
the importing test file dies before running a single assertion. Taking the seam
|
|
247
|
+
from `@object-ui/react` reddened 17 files across all four CI shards with zero
|
|
248
|
+
failed assertions among them. Nothing mocks `@object-ui/core`, and every
|
|
249
|
+
registration module already imports `ComponentRegistry` from it.
|
|
250
|
+
|
|
152
251
|
`object` lands on `objectName` by default (pass `object: 'apiName'` for another
|
|
153
252
|
key, or `object: false` for a block that reads the composed binding itself).
|
|
154
253
|
Precedence: binding keys beat the component's own, view-sourced values are only a
|
|
@@ -157,6 +256,255 @@ A `view` name that does not resolve renders a configuration error rather than
|
|
|
157
256
|
falling back to the object's full scope. Use `useElementDataSourceSchema` (plus
|
|
158
257
|
the exported `ElementDataSourceErrorPanel` / `ElementDataSourceLoadingPanel`) when
|
|
159
258
|
a block cannot be wrapped — a renderer whose hooks must run before the panels.
|
|
259
|
+
That form still reads `dataSource`, so it still owes the seam: mark the renderer
|
|
260
|
+
at its registration, `register('x', elementDataSourceBlock(XRenderer), { … })` —
|
|
261
|
+
again importing `elementDataSourceBlock` from `@object-ui/core`.
|
|
262
|
+
|
|
263
|
+
### useElementApiDataSource — the API binding
|
|
264
|
+
|
|
265
|
+
`SchemaRenderer` honours the spec's API binding of a page element,
|
|
266
|
+
`dataSource: { provider: 'api', read: { url }, path? }`, on every node: it reads
|
|
267
|
+
the same-origin `url` with `GET` through the provider's `apiFetch` (the global
|
|
268
|
+
`fetch` when none is given), selects `path` from the answer, and binds the value
|
|
269
|
+
as `data` in the node's `${…}` holes and `visibleWhen` — and in its subtree's.
|
|
270
|
+
A binding the spec refuses (a cross-origin `url`, a credential key) is refused
|
|
271
|
+
with the spec's message and nothing is read; a failed read or an empty `path`
|
|
272
|
+
draws an error panel in place of the node.
|
|
273
|
+
|
|
274
|
+
`useElementApiDataSource(schema)` is the hook behind it, exported for renderers
|
|
275
|
+
that need the same reading; `selectElementApiPath` and `readElementApi` are its
|
|
276
|
+
pure halves.
|
|
277
|
+
|
|
278
|
+
### useSettledSchema
|
|
279
|
+
|
|
280
|
+
The settled-schema RESOLUTION half, shared by every view that gates a record
|
|
281
|
+
query on its object definition — `ObjectKanban`, `ObjectView`,
|
|
282
|
+
`ObjectCalendar`, `ObjectTree` and `ObjectGantt` (objectui#6482, converged in
|
|
283
|
+
objectui#7225). Tracks whether an object's definition has finished resolving
|
|
284
|
+
FOR THE KEY THE CURRENT RENDER IS ASKING ABOUT — `ready` and `def` are two views of one piece of state, so a
|
|
285
|
+
stale key can never read as ready. GATE PLACEMENT — which effect actually
|
|
286
|
+
waits on `ready` — stays a per-component decision; this hook only owns the
|
|
287
|
+
resolution.
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
import { useEffect } from 'react'
|
|
291
|
+
import { useSettledSchema } from '@object-ui/react'
|
|
292
|
+
import type { DataSource } from '@object-ui/types'
|
|
293
|
+
|
|
294
|
+
function ObjectSomething({
|
|
295
|
+
schema,
|
|
296
|
+
dataSource,
|
|
297
|
+
}: {
|
|
298
|
+
schema: { objectName?: string }
|
|
299
|
+
dataSource: DataSource
|
|
300
|
+
}) {
|
|
301
|
+
const key = schema.objectName ?? ''
|
|
302
|
+
const { ready, def } = useSettledSchema(key, dataSource)
|
|
303
|
+
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
if (!ready) return // gate placement is local to this component
|
|
306
|
+
// issue the record query, e.g. buildExpandFields(def?.fields)
|
|
307
|
+
}, [ready, def])
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Pass `dataSource: undefined` for a render that should settle immediately with
|
|
312
|
+
no definition (e.g. a provider that issues no metadata read at all) instead of
|
|
313
|
+
adding a separate enable flag.
|
|
314
|
+
|
|
315
|
+
### useFilterScope / useResolvedFilter
|
|
316
|
+
|
|
317
|
+
A data node that sends its own authored `filter` into a query resolves the
|
|
318
|
+
spec's context tokens in it first: `{current_user_id}`, `{current_org_id}`,
|
|
319
|
+
`{record_id}` and the date macros such as `{today}` (objectui#10666).
|
|
320
|
+
`useFilterScope()` reads the session scope a host mounts with
|
|
321
|
+
`FilterScopeProvider`, plus the id of the record in view when a
|
|
322
|
+
`RecordContextProvider` is mounted above the node (objectui#7297);
|
|
323
|
+
`useResolvedFilter(filter, scope)` resolves the filter through
|
|
324
|
+
`@object-ui/core`'s `resolveFilterPlaceholders` and HOLDS the result.
|
|
325
|
+
|
|
326
|
+
`{record_id}` is the id of the record a `type: 'record'` page shows, taken from
|
|
327
|
+
that page's mounted record context and never from a URL parameter or a page
|
|
328
|
+
variable: on a person's record page, `{ assignee: '{record_id}' }` counts that
|
|
329
|
+
person's tasks. With no record in context (a list view, a dashboard, a page
|
|
330
|
+
that is not a record page) it is refused by name, with the same warning an
|
|
331
|
+
unresolved `{current_user_id}` gets, and left as written, so the filter never
|
|
332
|
+
widens. It scopes what a component shows; it is not access control, which
|
|
333
|
+
stays with the server's row-level security. `FilterScopeProvider` carries the
|
|
334
|
+
session values only.
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
import { useEffect } from 'react'
|
|
338
|
+
import { useFilterScope, useResolvedFilter } from '@object-ui/react'
|
|
339
|
+
import type { DataSource, QueryParams } from '@object-ui/types'
|
|
340
|
+
|
|
341
|
+
function ObjectSomething({
|
|
342
|
+
schema,
|
|
343
|
+
dataSource,
|
|
344
|
+
}: {
|
|
345
|
+
schema: { objectName: string; filter?: QueryParams['$filter'] }
|
|
346
|
+
dataSource: DataSource
|
|
347
|
+
}) {
|
|
348
|
+
const filterScope = useFilterScope()
|
|
349
|
+
const filter = useResolvedFilter(schema.filter, filterScope)
|
|
350
|
+
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
// Query with the held value and key the effect on it, never on schema.filter.
|
|
353
|
+
void dataSource.find(schema.objectName, { $filter: filter })
|
|
354
|
+
}, [dataSource, schema.objectName, filter])
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
The held value keeps its reference while the authored filter is structurally
|
|
359
|
+
equal (a filter rebuilt inline on every render included) and the scope's user,
|
|
360
|
+
organization, record and `onUnresolved` are unchanged, so the effect above runs
|
|
361
|
+
once. A structurally different filter, a new signed-in user or organization, or
|
|
362
|
+
another record in view resolves again. A filter with no token to resolve is handed back as the value
|
|
363
|
+
passed in. A token the scope cannot resolve is left in place, and the resolver
|
|
364
|
+
logs one warning naming it.
|
|
365
|
+
|
|
366
|
+
### NON_GRID_ROW_CEILING
|
|
367
|
+
|
|
368
|
+
The platform's hard row ceiling for a NON-GRID visualisation — gantt, calendar,
|
|
369
|
+
map and tree (objectui#7210). Those four fetch the whole FILTERED result set,
|
|
370
|
+
because a truthful range or layout needs all of it, but the fetch is bounded:
|
|
371
|
+
past the ceiling they draw the first N rows and say so.
|
|
372
|
+
|
|
373
|
+
The mechanism lives in `@object-ui/core` (objectui#7508): `NON_GRID_ROW_CEILING`,
|
|
374
|
+
`nonGridRowCeilingQuery` and `applyNonGridRowCeiling`, with the result type
|
|
375
|
+
`NonGridCeilingResult`. This package re-exports `NON_GRID_ROW_CEILING`,
|
|
376
|
+
`applyNonGridRowCeiling` and `NonGridCeilingResult`, and owns the React half —
|
|
377
|
+
the footnote, `NonGridRowCeilingNote`.
|
|
378
|
+
|
|
379
|
+
```tsx
|
|
380
|
+
import { NonGridRowCeilingNote, applyNonGridRowCeiling } from '@object-ui/react'
|
|
381
|
+
import { nonGridRowCeilingQuery } from '@object-ui/core'
|
|
382
|
+
import type { DataSource, QueryParams } from '@object-ui/types'
|
|
383
|
+
|
|
384
|
+
declare const dataSource: DataSource
|
|
385
|
+
declare const objectName: string
|
|
386
|
+
declare const schema: { filter?: QueryParams['$filter'] }
|
|
387
|
+
|
|
388
|
+
const result = await dataSource.find(objectName, {
|
|
389
|
+
$filter: schema.filter,
|
|
390
|
+
...nonGridRowCeilingQuery(), // `$top`: the ceiling plus ONE probe row
|
|
391
|
+
})
|
|
392
|
+
// The semicolon is load-bearing: the next statement opens with `<`, so without
|
|
393
|
+
// it the call above is parsed as the left side of a relational expression.
|
|
394
|
+
const capped = applyNonGridRowCeiling(result);
|
|
395
|
+
// …draw `capped.rows`, then hand the note the whole result:
|
|
396
|
+
<NonGridRowCeilingNote result={capped} />
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
`nonGridRowCeilingQuery()` is the one place the probe row is written: `$top` is
|
|
400
|
+
the ceiling plus one, because the extra row is what makes truncation a fact about
|
|
401
|
+
the rows in hand — `QueryResult.total` is optional and a bare-array response
|
|
402
|
+
carries none. Spread it into the query; never spell the `+ 1` at a call site.
|
|
403
|
+
|
|
404
|
+
`NonGridRowCeilingNote` takes the `NonGridCeilingResult` and nothing else. The
|
|
405
|
+
count it prints is `result.rows.length` and the total is `result.total`, so the
|
|
406
|
+
footnote cannot name numbers that disagree with the rows drawn. It renders `null`
|
|
407
|
+
when nothing was truncated, so it can be mounted whenever a result is in hand.
|
|
408
|
+
|
|
409
|
+
⛔ The ceiling is not authorable and must not become so. Silent truncation is
|
|
410
|
+
the failure it exists to prevent — a cut-off schedule still looks like a
|
|
411
|
+
schedule — so a view that caps rows without rendering the note is a defect, not
|
|
412
|
+
an optimisation.
|
|
413
|
+
|
|
414
|
+
### resolveInlineAriaProps
|
|
415
|
+
|
|
416
|
+
Maps the spec's nested `aria` bag (`AriaPropsSchema`, the `aria` prop a block
|
|
417
|
+
such as `element:text` declares) to the DOM attributes it names
|
|
418
|
+
(objectui#11051):
|
|
419
|
+
|
|
420
|
+
- `ariaLabel` → `aria-label`. A plain string is used as written; an inline
|
|
421
|
+
locale map (`{ en: 'Order total', 'zh-CN': '订单合计' }`) gives the entry for
|
|
422
|
+
`locale`, through the spec's own `resolveI18nLabel`.
|
|
423
|
+
- `ariaDescribedBy` → `aria-describedby`.
|
|
424
|
+
- `role` → `role`.
|
|
425
|
+
|
|
426
|
+
Only attributes that have a value are returned, so the result can be spread
|
|
427
|
+
onto an element directly. It is a pure function: pass the locale yourself, in
|
|
428
|
+
React from `useDisplayLocale()`.
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
import { resolveInlineAriaProps } from '@object-ui/react'
|
|
432
|
+
import { useDisplayLocale } from '@object-ui/i18n'
|
|
433
|
+
import type { AriaProps } from '@object-ui/types'
|
|
434
|
+
|
|
435
|
+
function CloseButton({ aria, onClose }: { aria?: AriaProps; onClose: () => void }) {
|
|
436
|
+
const locale = useDisplayLocale()
|
|
437
|
+
return (
|
|
438
|
+
<button type="button" onClick={onClose} {...resolveInlineAriaProps(aria, locale)}>
|
|
439
|
+
×
|
|
440
|
+
</button>
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
This is not the reader `SchemaRenderer` applies to a node's FLAT `ariaLabel`,
|
|
446
|
+
which is objectui's keyed `{ key, defaultValue }` form. Each resolver returns
|
|
447
|
+
nothing useful for the other's shape, so the two stay separate (objectui#4580).
|
|
448
|
+
|
|
449
|
+
### usePrintPage / PrintPageProvider
|
|
450
|
+
|
|
451
|
+
`usePrintPage()` returns the `print` declaration of the page the component
|
|
452
|
+
renders in, or `null` outside a print page (objectui#11958). `PageRenderer` in
|
|
453
|
+
`@object-ui/components` mounts `PrintPageProvider` for a page whose `print` is
|
|
454
|
+
an object. A block that draws only part of its data on screen reads it so that
|
|
455
|
+
a printed copy does not drop data in silence:
|
|
456
|
+
|
|
457
|
+
```tsx
|
|
458
|
+
import type { ReactNode } from 'react'
|
|
459
|
+
import { usePrintPage } from '@object-ui/react'
|
|
460
|
+
|
|
461
|
+
function Section({ collapsed, children }: { collapsed: boolean; children: ReactNode }) {
|
|
462
|
+
// A collapsed section never reaches paper, so a print page draws it open.
|
|
463
|
+
const open = usePrintPage() !== null || !collapsed
|
|
464
|
+
return open ? <section>{children}</section> : null
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### RelatedRecordActionsProvider / useRelatedRecordActions
|
|
469
|
+
|
|
470
|
+
The host's navigation seam for low-level renderers that have no router. The
|
|
471
|
+
host (the console's record, list and custom pages) publishes a
|
|
472
|
+
`RelatedRecordActionsValue`; renderers read it with `useRelatedRecordActions()`,
|
|
473
|
+
which returns `null` when no host provides one.
|
|
474
|
+
|
|
475
|
+
- `resolve(input)` returns the CRUD and action handlers for a related list.
|
|
476
|
+
- `recordHref(objectName, recordId)` / `openRecord(...)` address one record's
|
|
477
|
+
page: a document href for a real anchor, and the SPA navigation a plain left
|
|
478
|
+
click uses.
|
|
479
|
+
- `listHref(objectName, viewName?)` / `openList(...)` address an object's list
|
|
480
|
+
page, or one of its saved views when `viewName` is given. The compact list a
|
|
481
|
+
page draws in a narrow region links "View all" through them. They take an
|
|
482
|
+
object and a view name only: a caller cannot hand them a filter, columns or a
|
|
483
|
+
sort to encode.
|
|
484
|
+
|
|
485
|
+
Every member but `resolve` is optional. An absent member, or one that answers
|
|
486
|
+
`null`, means the host cannot route there, and the renderer draws no link.
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
import type { ReactNode } from 'react'
|
|
490
|
+
import { RelatedRecordActionsProvider } from '@object-ui/react'
|
|
491
|
+
import type { RelatedRecordActionsValue } from '@object-ui/react'
|
|
492
|
+
|
|
493
|
+
declare const navigate: (to: string) => void
|
|
494
|
+
|
|
495
|
+
const listPath = (objectName: string, viewName?: string): string =>
|
|
496
|
+
viewName ? `/apps/crm/${objectName}/view/${encodeURIComponent(viewName)}` : `/apps/crm/${objectName}`
|
|
497
|
+
|
|
498
|
+
const host: RelatedRecordActionsValue = {
|
|
499
|
+
resolve: () => ({}),
|
|
500
|
+
listHref: (objectName: string, viewName?: string) => listPath(objectName, viewName),
|
|
501
|
+
openList: (objectName: string, viewName?: string) => navigate(listPath(objectName, viewName)),
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function PageHost({ children }: { children: ReactNode }) {
|
|
505
|
+
return <RelatedRecordActionsProvider value={host}>{children}</RelatedRecordActionsProvider>
|
|
506
|
+
}
|
|
507
|
+
```
|
|
160
508
|
|
|
161
509
|
### ComponentRegistry
|
|
162
510
|
|
|
@@ -176,7 +524,7 @@ function MyComponent(props: Record<string, unknown>) {
|
|
|
176
524
|
|
|
177
525
|
### useDiscovery
|
|
178
526
|
|
|
179
|
-
Access server discovery information
|
|
527
|
+
Access server discovery information — service availability and server identity:
|
|
180
528
|
|
|
181
529
|
```tsx
|
|
182
530
|
import { useDiscovery } from '@object-ui/react'
|
|
@@ -184,11 +532,6 @@ import { useDiscovery } from '@object-ui/react'
|
|
|
184
532
|
function MyComponent() {
|
|
185
533
|
const { discovery, isLoading, isAuthEnabled, isAiEnabled } = useDiscovery()
|
|
186
534
|
|
|
187
|
-
// Check if the server is in preview mode
|
|
188
|
-
if (discovery?.mode === 'preview') {
|
|
189
|
-
console.log('Preview mode active:', discovery.previewMode)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
535
|
// Check if AI service is available
|
|
193
536
|
if (isAiEnabled) {
|
|
194
537
|
console.log('AI service route:', discovery?.services?.ai?.route)
|
|
@@ -204,21 +547,17 @@ function MyComponent() {
|
|
|
204
547
|
| --- | --- | --- |
|
|
205
548
|
| `name` | `string` | Server name |
|
|
206
549
|
| `version` | `string` | Server version |
|
|
207
|
-
| `mode` | `string` | Runtime mode
|
|
208
|
-
| `previewMode` | `object` | Preview mode configuration (present when mode is `'preview'`) |
|
|
550
|
+
| `mode` | `string` | Runtime mode the server reports (`RuntimeMode` in `@objectstack/spec`) |
|
|
209
551
|
| `services` | `object` | Service availability status (auth, data, metadata, ai) |
|
|
210
552
|
| `capabilities` | `string[]` | API capabilities |
|
|
211
553
|
|
|
212
|
-
The `previewMode`
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
| `readOnly` | `boolean` | `false` | Read-only mode |
|
|
220
|
-
| `expiresInSeconds` | `number` | `0` | Session duration (0 = no expiry) |
|
|
221
|
-
| `bannerMessage` | `string` | — | UI banner message |
|
|
554
|
+
> The `previewMode` block and the `'preview'` runtime mode were retired in
|
|
555
|
+
> objectui#6654, following their retirement in `@objectstack/spec`
|
|
556
|
+
> (objectstack#11846). A server that still sends them is read as any other
|
|
557
|
+
> payload: authentication follows `services.auth` alone, so a preview
|
|
558
|
+
> deployment requires login rather than silently running with auth off.
|
|
559
|
+
> `AuthProvider`'s `previewMode` **prop** in `@object-ui/auth` is a separate,
|
|
560
|
+
> host-supplied capability and is unaffected.
|
|
222
561
|
|
|
223
562
|
### useNotifications / useNotificationsByPresentation
|
|
224
563
|
|
|
@@ -240,6 +579,8 @@ belong (a banner is in flow, an inline notification sits next to its raiser).
|
|
|
240
579
|
why every type looked like a toast.
|
|
241
580
|
|
|
242
581
|
```tsx
|
|
582
|
+
import { useNotifications } from '@object-ui/react'
|
|
583
|
+
|
|
243
584
|
const { notify } = useNotifications()
|
|
244
585
|
|
|
245
586
|
notify({ title: 'Saved', severity: 'success' }) // toast (spec default)
|
package/dist/SchemaRenderer.d.ts
CHANGED
|
@@ -6,7 +6,50 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import React, { Component, type ForwardRefExoticComponent, type RefAttributes } from 'react';
|
|
9
|
-
import type {
|
|
9
|
+
import type { DeclaredNode } from '@object-ui/types';
|
|
10
|
+
/**
|
|
11
|
+
* The scope a page node's values are evaluated against: its `visibleWhen`
|
|
12
|
+
* gate and every other expression the evaluation memo in
|
|
13
|
+
* {@link SchemaRenderer} evaluates on the node. Four roots, in this order:
|
|
14
|
+
*
|
|
15
|
+
* 1. **the ambient predicate scope**: every root `predicateScope` carries (the
|
|
16
|
+
* host scope `usePredicateScope()` publishes, plus a node's API-bound
|
|
17
|
+
* `data` once it has answered), spread first;
|
|
18
|
+
* 2. **`current_user`**: aliased to the ambient `user`, so `user.email` and
|
|
19
|
+
* `current_user.email` read the same value;
|
|
20
|
+
* 3. **`record`**: the page's bound row, ONLY when there is one (a non-array
|
|
21
|
+
* object). No row binds nothing, so a `record` the ambient scope supplies
|
|
22
|
+
* is not shadowed by an empty one; a row, written after the spread, wins;
|
|
23
|
+
* 4. **`page`**: the page variables (`PageSchema.variables`) as `page.<var>`.
|
|
24
|
+
*
|
|
25
|
+
* Why each root is (and `data` is not) bound is stated where the memo builds
|
|
26
|
+
* its evaluator from this.
|
|
27
|
+
*
|
|
28
|
+
* The ONE builder of this scope. The memo and {@link evaluateConfigBagInScope}
|
|
29
|
+
* call it, so they cannot build two different scopes (objectui#10290), and it
|
|
30
|
+
* is exported for the `page:tabs` derived badge in `@object-ui/components`,
|
|
31
|
+
* which decides the `visibleWhen` gates in front of a related list the way the
|
|
32
|
+
* tab body decides them (objectui#12987, objectui#12992). A root added here
|
|
33
|
+
* reaches every one of them.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildNodeEvaluationScope(predicateScope: Record<string, unknown>, boundRecord: unknown, pageVariables: unknown): Record<string, unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* The `properties` evaluation of the {@link SchemaRenderer} memo, for a config
|
|
38
|
+
* bag that does not pass through `SchemaRenderer` (objectui#10290): the same
|
|
39
|
+
* per-key rule, the same `ExpressionEvaluator`, and the same scope, built from
|
|
40
|
+
* the same three inputs. It is not a second template engine.
|
|
41
|
+
*
|
|
42
|
+
* The plain half of `useConfigBagEvaluator()` (`hooks/useConfigBagEvaluator`),
|
|
43
|
+
* which reads those inputs from the contexts the memo reads them from. Call
|
|
44
|
+
* the hook; this function exists because the rule it applies is this module's.
|
|
45
|
+
* (The hook is not declared in this module: a hook here puts this file under
|
|
46
|
+
* the React Compiler lint, which then reports the render-time
|
|
47
|
+
* `performance.now()` / `Date.now()` debug probes below.)
|
|
48
|
+
*
|
|
49
|
+
* A value that is not a config bag is returned unchanged; a bag comes back as a
|
|
50
|
+
* new, evaluated object and the authored one is never mutated.
|
|
51
|
+
*/
|
|
52
|
+
export declare function evaluateConfigBagInScope(bag: unknown, predicateScope: Record<string, unknown>, boundRecord: unknown, pageVariables: unknown): unknown;
|
|
10
53
|
/**
|
|
11
54
|
* Per-component Error Boundary for SchemaRenderer.
|
|
12
55
|
* Catches render errors in individual components, preventing one broken
|
|
@@ -35,9 +78,9 @@ export declare class SchemaErrorBoundary extends Component<{
|
|
|
35
78
|
*
|
|
36
79
|
* ## Why `schema` is spelled as this union and not as a `SchemaNode`
|
|
37
80
|
*
|
|
38
|
-
* The repo
|
|
39
|
-
* interface (which requires `type: string`) and `@object-ui/types`' union
|
|
40
|
-
*
|
|
81
|
+
* The repo carried two competing `SchemaNode` types: `@object-ui/core`'s
|
|
82
|
+
* interface (which requires `type: string`) and `@object-ui/types`' union (then
|
|
83
|
+
* `BaseSchema | string | number | boolean | null | undefined`). This component
|
|
41
84
|
* matched NEITHER. It declared core's — narrower than what it accepts, so every
|
|
42
85
|
* caller holding the types union was wrong and could not be told — while its
|
|
43
86
|
* runtime returns early for strings and nullish, which core's interface forbids.
|
|
@@ -48,11 +91,25 @@ export declare class SchemaErrorBoundary extends Component<{
|
|
|
48
91
|
* handles: an object schema, a bare string (rendered as text), or nothing at
|
|
49
92
|
* all. `number` / `boolean` are deliberately excluded — the runtime tolerates
|
|
50
93
|
* them defensively (see the primitive guard in the evaluation memo) but no
|
|
51
|
-
* author should be invited to pass them
|
|
52
|
-
*
|
|
94
|
+
* author should be invited to pass them; `toRenderableSchema` (`./schema-input`)
|
|
95
|
+
* is the bridge from a `SchemaNode` that may hold one.
|
|
96
|
+
*
|
|
97
|
+
* ## The object member is `DeclaredNode` (objectui#11466)
|
|
98
|
+
*
|
|
99
|
+
* The object member is `DeclaredNode` from `@object-ui/types`, the same type a
|
|
100
|
+
* node slot takes: the discriminated union, keyed by the literal `type`, of the
|
|
101
|
+
* component schemas that package declares, the spec-declared `AuthoringNode`s
|
|
102
|
+
* (objectui#11364: public blocks, `element:text_input` /
|
|
103
|
+
* `element:record_picker`, a stored page document under its page kind), and
|
|
104
|
+
* the types an application declares in `CustomNodeRegistry`. It has no
|
|
105
|
+
* `type: string` arm, so a node whose `type` no declaration names is refused
|
|
106
|
+
* here, and each node's own keys are judged: since objectui#8347 removed
|
|
107
|
+
* `BaseSchema`'s index signature, a misspelled key is refused while the
|
|
108
|
+
* declared spelling compiles. ⛔ The prop is widened by declared types only,
|
|
109
|
+
* never by an index signature, a `Record` or a `type: string` arm.
|
|
53
110
|
*/
|
|
54
111
|
export interface SchemaRendererProps {
|
|
55
|
-
schema:
|
|
112
|
+
schema: DeclaredNode | string | null | undefined;
|
|
56
113
|
}
|
|
57
114
|
/**
|
|
58
115
|
* The open forwarding surface, named once so it reads as the decision it is.
|