@object-ui/react 17.5.0 → 17.7.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 +4100 -0
- package/README.md +303 -46
- package/dist/LazyPluginLoader.d.ts +14 -7
- package/dist/LazyPluginLoader.js +14 -7
- package/dist/SchemaRenderer.d.ts +38 -7
- package/dist/SchemaRenderer.js +1603 -60
- package/dist/context/ActionContext.js +40 -1
- package/dist/context/DiscussionContext.d.ts +12 -0
- package/dist/context/DiscussionContext.js +2 -0
- package/dist/context/DrillNavigationContext.d.ts +13 -0
- package/dist/context/HostNavigationContext.d.ts +82 -0
- package/dist/context/HostNavigationContext.js +65 -0
- package/dist/context/InlineEditContext.d.ts +81 -1
- package/dist/context/InlineEditContext.js +8 -2
- package/dist/context/RecordContext.d.ts +48 -5
- package/dist/context/RecordContext.js +10 -1
- package/dist/context/RelatedRecordActionsContext.d.ts +20 -1
- package/dist/context/SchemaRendererContext.d.ts +54 -2
- package/dist/context/SchemaRendererContext.js +38 -5
- 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 +13 -12
- package/dist/context/index.js +13 -12
- package/dist/element-data-source/ElementDataSourceGate.d.ts +163 -3
- package/dist/element-data-source/ElementDataSourceGate.js +407 -15
- package/dist/hooks/index.d.ts +37 -35
- package/dist/hooks/index.js +44 -35
- package/dist/hooks/useActionEngine.js +47 -4
- package/dist/hooks/useActionRunner.js +4 -0
- package/dist/hooks/useActionTextLocalizer.d.ts +21 -8
- package/dist/hooks/useActionTextLocalizer.js +33 -8
- package/dist/hooks/useCapabilityGate.js +6 -5
- package/dist/hooks/useConfigBagEvaluator.d.ts +30 -0
- package/dist/hooks/useConfigBagEvaluator.js +39 -0
- package/dist/hooks/useCrudShortcuts.js +1 -1
- package/dist/hooks/useDatasetDimensionLabels.js +1 -1
- package/dist/hooks/useDensityMode.d.ts +6 -2
- package/dist/hooks/useDensityMode.js +6 -2
- package/dist/hooks/useDiscovery.d.ts +1 -10
- package/dist/hooks/useDiscovery.js +1 -1
- package/dist/hooks/useETagCache.js +25 -4
- package/dist/hooks/useElementDataSource.d.ts +15 -2
- package/dist/hooks/useElementDataSource.js +29 -9
- package/dist/hooks/useExpression.d.ts +52 -38
- package/dist/hooks/useExpression.js +62 -42
- 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 +77 -12
- package/dist/hooks/useNavigationOverlay.js +119 -9
- package/dist/hooks/useOffline.js +65 -11
- package/dist/hooks/usePageAssignment.d.ts +8 -5
- package/dist/hooks/usePageAssignment.js +38 -17
- package/dist/hooks/usePageTransition.js +1 -1
- package/dist/hooks/usePageVariableActionBridge.js +2 -2
- 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 +7 -2
- package/dist/hooks/useViewData.js +1 -1
- package/dist/hooks/useViewTransition.js +1 -1
- package/dist/index.d.ts +18 -11
- package/dist/index.js +54 -10
- package/dist/schema-input.d.ts +55 -14
- package/dist/schema-input.js +55 -12
- package/dist/utils/configBag.d.ts +68 -0
- package/dist/utils/configBag.js +70 -0
- package/dist/utils/error-message.d.ts +59 -0
- package/dist/utils/error-message.js +116 -1
- package/dist/utils/inlineAria.d.ts +61 -0
- package/dist/utils/inlineAria.js +70 -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 +421 -0
- package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
- package/dist/utils/refusedDataPropDiagnostic.js +94 -0
- package/dist/utils/unevaluatedExpression.d.ts +110 -0
- package/dist/utils/unevaluatedExpression.js +257 -0
- package/dist/utils/visibilityDiagnostic.d.ts +371 -0
- package/dist/utils/visibilityDiagnostic.js +551 -0
- 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 -82
- package/dist/spec-bridge/bridges/form-view.js +0 -126
- package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
- package/dist/spec-bridge/bridges/list-view.js +0 -241
- 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,7 +121,13 @@ 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}
|
|
@@ -101,6 +141,16 @@ import { SchemaRendererProvider } from '@object-ui/react'
|
|
|
101
141
|
</SchemaRendererProvider>
|
|
102
142
|
```
|
|
103
143
|
|
|
144
|
+
`dataSource` carries the host's adapter as the published `DataSource` contract
|
|
145
|
+
(`@object-ui/types`), typed `DataSource | null | undefined`: a host either hands
|
|
146
|
+
over an adapter or states that it has none — a Studio preview, a page rendered
|
|
147
|
+
before the host's adapter has connected, or a widget probe driving `apiFetch`
|
|
148
|
+
alone. Both spellings of "none" are accepted because every reader in the tree
|
|
149
|
+
guards for it. The prop used to be typed `any`, so a string passed where the
|
|
150
|
+
adapter belongs — the shape of a config value read from the wrong place —
|
|
151
|
+
raised nothing until the first `find()` at runtime (objectui#7912).
|
|
152
|
+
`useSchemaContext()` hands the same type back to every consumer.
|
|
153
|
+
|
|
104
154
|
Nested providers inherit `apiFetch` from their parent when they don't supply
|
|
105
155
|
their own, so re-wrapped subtrees (embedded pages, preview surfaces) keep the
|
|
106
156
|
host's authentication.
|
|
@@ -109,15 +159,23 @@ host's authentication.
|
|
|
109
159
|
|
|
110
160
|
### useSchemaContext
|
|
111
161
|
|
|
112
|
-
Access
|
|
162
|
+
Access what `SchemaRendererProvider` injected — `dataSource`, `debug`,
|
|
163
|
+
`debugFlags` and `apiFetch`. It does **not** carry the page's values: read those
|
|
164
|
+
with `useDataScope`, which addresses the ambient expression scope by path. The
|
|
165
|
+
two are different channels on purpose — `dataSource` is the adapter a renderer
|
|
166
|
+
queries, `useDataScope` is what a node's `bind` resolves against
|
|
167
|
+
(objectui#9308).
|
|
113
168
|
|
|
114
169
|
```tsx
|
|
115
|
-
import { useSchemaContext } from '@object-ui/react'
|
|
170
|
+
import { useDataScope, useSchemaContext } from '@object-ui/react'
|
|
116
171
|
|
|
117
172
|
function MyComponent() {
|
|
118
|
-
const {
|
|
119
|
-
|
|
120
|
-
|
|
173
|
+
const { dataSource, debug } = useSchemaContext()
|
|
174
|
+
// Resolves `scope.value` from the nearest PredicateScopeProvider above.
|
|
175
|
+
const value = useDataScope('value')
|
|
176
|
+
|
|
177
|
+
if (!dataSource) return null
|
|
178
|
+
return <div data-debug={debug}>{String(value)}</div>
|
|
121
179
|
}
|
|
122
180
|
```
|
|
123
181
|
|
|
@@ -130,25 +188,56 @@ views so `view` can be matched); these two apply the composed result to the
|
|
|
130
188
|
block's schema and render the two non-final states.
|
|
131
189
|
|
|
132
190
|
```tsx
|
|
191
|
+
import type { FC } from 'react'
|
|
133
192
|
import { ElementDataSourceGate } from '@object-ui/react'
|
|
193
|
+
import type { ElementDataSourceMapping } from '@object-ui/react'
|
|
194
|
+
// The seam is imported from CORE, not from here — see the note below.
|
|
195
|
+
import { elementDataSourceBlock } from '@object-ui/core'
|
|
196
|
+
import type { BaseSchema } from '@object-ui/types'
|
|
197
|
+
|
|
198
|
+
// The block this renderer wraps — whatever the registry resolves for its type.
|
|
199
|
+
declare const ObjectGrid: FC<{ schema: BaseSchema }>
|
|
134
200
|
|
|
135
201
|
// `mapping` names ONLY the keys this block reads. A composed value written onto
|
|
136
202
|
// a key the block ignores would be accepted and silently dropped — the defect
|
|
137
203
|
// the binding exists to remove.
|
|
138
|
-
const OBJECT_GRID_BINDING = {
|
|
204
|
+
const OBJECT_GRID_BINDING: ElementDataSourceMapping = {
|
|
139
205
|
columns: true, // the view's FIELD list may fill `schema.columns`
|
|
140
206
|
filter: true, // AND-combined, never replaced ("additional criteria")
|
|
141
207
|
sort: true,
|
|
142
208
|
limit: 'pagination.pageSize' as const,
|
|
143
209
|
}
|
|
144
210
|
|
|
145
|
-
|
|
211
|
+
// `elementDataSourceBlock` is not optional decoration — see below.
|
|
212
|
+
const ObjectGridRenderer = elementDataSourceBlock<FC<{ schema: BaseSchema }>>(({ schema, ...props }) => (
|
|
146
213
|
<ElementDataSourceGate schema={schema} mapping={OBJECT_GRID_BINDING} testId="object-grid">
|
|
147
214
|
{(bound) => <ObjectGrid schema={bound} {...props} />}
|
|
148
215
|
</ElementDataSourceGate>
|
|
149
|
-
)
|
|
216
|
+
))
|
|
150
217
|
```
|
|
151
218
|
|
|
219
|
+
**Wrap the registered renderer in `elementDataSourceBlock`.** It is what makes
|
|
220
|
+
`ComponentRegistry.register` emit the `dataSource` input on that block's
|
|
221
|
+
authoring surface, so the key the gate READS is also the key the manifest, the
|
|
222
|
+
save gate, the generated JSX types and the designer DECLARE. Skip it and the
|
|
223
|
+
binding still works at runtime while the html tier reports `dataSource` as a prop
|
|
224
|
+
that does not exist — the one spelling that resolves a saved view, reported
|
|
225
|
+
exactly like the spellings that do nothing (objectui#6678). The declaration is
|
|
226
|
+
emitted from this one seam, so never hand-write a `dataSource` entry in a block's
|
|
227
|
+
`inputs`. `pnpm check:element-data-source-declaration` fails any source that
|
|
228
|
+
consumes the gate without reaching the seam.
|
|
229
|
+
|
|
230
|
+
⚠️ **Import the seam from `@object-ui/core`, not from this package** — it is one
|
|
231
|
+
function under one name, re-exported here for discoverability, and the check
|
|
232
|
+
above enforces the core import at call sites. The reason is measured, not
|
|
233
|
+
stylistic: a registration runs at MODULE SCOPE, 101 suites in this repo partially
|
|
234
|
+
mock `@object-ui/react` by hand-listing the exports they return, and a
|
|
235
|
+
module-scope read of a name absent from such a list throws at COLLECTION time —
|
|
236
|
+
the importing test file dies before running a single assertion. Taking the seam
|
|
237
|
+
from `@object-ui/react` reddened 17 files across all four CI shards with zero
|
|
238
|
+
failed assertions among them. Nothing mocks `@object-ui/core`, and every
|
|
239
|
+
registration module already imports `ComponentRegistry` from it.
|
|
240
|
+
|
|
152
241
|
`object` lands on `objectName` by default (pass `object: 'apiName'` for another
|
|
153
242
|
key, or `object: false` for a block that reads the composed binding itself).
|
|
154
243
|
Precedence: binding keys beat the component's own, view-sourced values are only a
|
|
@@ -157,25 +246,200 @@ A `view` name that does not resolve renders a configuration error rather than
|
|
|
157
246
|
falling back to the object's full scope. Use `useElementDataSourceSchema` (plus
|
|
158
247
|
the exported `ElementDataSourceErrorPanel` / `ElementDataSourceLoadingPanel`) when
|
|
159
248
|
a block cannot be wrapped — a renderer whose hooks must run before the panels.
|
|
249
|
+
That form still reads `dataSource`, so it still owes the seam: mark the renderer
|
|
250
|
+
at its registration, `register('x', elementDataSourceBlock(XRenderer), { … })` —
|
|
251
|
+
again importing `elementDataSourceBlock` from `@object-ui/core`.
|
|
160
252
|
|
|
161
|
-
###
|
|
253
|
+
### useSettledSchema
|
|
162
254
|
|
|
163
|
-
|
|
255
|
+
The settled-schema RESOLUTION half, shared by every view that gates a record
|
|
256
|
+
query on its object definition — `ObjectKanban`, `ObjectView`,
|
|
257
|
+
`ObjectCalendar`, `ObjectTree` and `ObjectGantt` (objectui#6482, converged in
|
|
258
|
+
objectui#7225). Tracks whether an object's definition has finished resolving
|
|
259
|
+
FOR THE KEY THE CURRENT RENDER IS ASKING ABOUT — `ready` and `def` are two views of one piece of state, so a
|
|
260
|
+
stale key can never read as ready. GATE PLACEMENT — which effect actually
|
|
261
|
+
waits on `ready` — stays a per-component decision; this hook only owns the
|
|
262
|
+
resolution.
|
|
164
263
|
|
|
165
264
|
```tsx
|
|
166
|
-
import {
|
|
265
|
+
import { useEffect } from 'react'
|
|
266
|
+
import { useSettledSchema } from '@object-ui/react'
|
|
267
|
+
import type { DataSource } from '@object-ui/types'
|
|
268
|
+
|
|
269
|
+
function ObjectSomething({
|
|
270
|
+
schema,
|
|
271
|
+
dataSource,
|
|
272
|
+
}: {
|
|
273
|
+
schema: { objectName?: string }
|
|
274
|
+
dataSource: DataSource
|
|
275
|
+
}) {
|
|
276
|
+
const key = schema.objectName ?? ''
|
|
277
|
+
const { ready, def } = useSettledSchema(key, dataSource)
|
|
278
|
+
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
if (!ready) return // gate placement is local to this component
|
|
281
|
+
// issue the record query, e.g. buildExpandFields(def?.fields)
|
|
282
|
+
}, [ready, def])
|
|
283
|
+
}
|
|
284
|
+
```
|
|
167
285
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
286
|
+
Pass `dataSource: undefined` for a render that should settle immediately with
|
|
287
|
+
no definition (e.g. a provider that issues no metadata read at all) instead of
|
|
288
|
+
adding a separate enable flag.
|
|
289
|
+
|
|
290
|
+
### useFilterScope / useResolvedFilter
|
|
291
|
+
|
|
292
|
+
A data node that sends its own authored `filter` into a query resolves the
|
|
293
|
+
spec's context tokens in it first: `{current_user_id}`, `{current_org_id}`,
|
|
294
|
+
`{record_id}` and the date macros such as `{today}` (objectui#10666).
|
|
295
|
+
`useFilterScope()` reads the session scope a host mounts with
|
|
296
|
+
`FilterScopeProvider`, plus the id of the record in view when a
|
|
297
|
+
`RecordContextProvider` is mounted above the node (objectui#7297);
|
|
298
|
+
`useResolvedFilter(filter, scope)` resolves the filter through
|
|
299
|
+
`@object-ui/core`'s `resolveFilterPlaceholders` and HOLDS the result.
|
|
300
|
+
|
|
301
|
+
`{record_id}` is the id of the record a `type: 'record'` page shows, taken from
|
|
302
|
+
that page's mounted record context and never from a URL parameter or a page
|
|
303
|
+
variable: on a person's record page, `{ assignee: '{record_id}' }` counts that
|
|
304
|
+
person's tasks. With no record in context (a list view, a dashboard, a page
|
|
305
|
+
that is not a record page) it is refused by name, with the same warning an
|
|
306
|
+
unresolved `{current_user_id}` gets, and left as written, so the filter never
|
|
307
|
+
widens. It scopes what a component shows; it is not access control, which
|
|
308
|
+
stays with the server's row-level security. `FilterScopeProvider` carries the
|
|
309
|
+
session values only.
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
import { useEffect } from 'react'
|
|
313
|
+
import { useFilterScope, useResolvedFilter } from '@object-ui/react'
|
|
314
|
+
import type { DataSource, QueryParams } from '@object-ui/types'
|
|
315
|
+
|
|
316
|
+
function ObjectSomething({
|
|
317
|
+
schema,
|
|
318
|
+
dataSource,
|
|
319
|
+
}: {
|
|
320
|
+
schema: { objectName: string; filter?: QueryParams['$filter'] }
|
|
321
|
+
dataSource: DataSource
|
|
322
|
+
}) {
|
|
323
|
+
const filterScope = useFilterScope()
|
|
324
|
+
const filter = useResolvedFilter(schema.filter, filterScope)
|
|
325
|
+
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
// Query with the held value and key the effect on it, never on schema.filter.
|
|
328
|
+
void dataSource.find(schema.objectName, { $filter: filter })
|
|
329
|
+
}, [dataSource, schema.objectName, filter])
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The held value keeps its reference while the authored filter is structurally
|
|
334
|
+
equal (a filter rebuilt inline on every render included) and the scope's user,
|
|
335
|
+
organization, record and `onUnresolved` are unchanged, so the effect above runs
|
|
336
|
+
once. A structurally different filter, a new signed-in user or organization, or
|
|
337
|
+
another record in view resolves again. A filter with no token to resolve is handed back as the value
|
|
338
|
+
passed in. A token the scope cannot resolve is left in place, and the resolver
|
|
339
|
+
logs one warning naming it.
|
|
340
|
+
|
|
341
|
+
### NON_GRID_ROW_CEILING
|
|
342
|
+
|
|
343
|
+
The platform's hard row ceiling for a NON-GRID visualisation — gantt, calendar,
|
|
344
|
+
map and tree (objectui#7210). Those four fetch the whole FILTERED result set,
|
|
345
|
+
because a truthful range or layout needs all of it, but the fetch is bounded:
|
|
346
|
+
past the ceiling they draw the first N rows and say so.
|
|
347
|
+
|
|
348
|
+
The mechanism lives in `@object-ui/core` (objectui#7508): `NON_GRID_ROW_CEILING`,
|
|
349
|
+
`nonGridRowCeilingQuery` and `applyNonGridRowCeiling`, with the result type
|
|
350
|
+
`NonGridCeilingResult`. This package re-exports `NON_GRID_ROW_CEILING`,
|
|
351
|
+
`applyNonGridRowCeiling` and `NonGridCeilingResult`, and owns the React half —
|
|
352
|
+
the footnote, `NonGridRowCeilingNote`.
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
import { NonGridRowCeilingNote, applyNonGridRowCeiling } from '@object-ui/react'
|
|
356
|
+
import { nonGridRowCeilingQuery } from '@object-ui/core'
|
|
357
|
+
import type { DataSource, QueryParams } from '@object-ui/types'
|
|
358
|
+
|
|
359
|
+
declare const dataSource: DataSource
|
|
360
|
+
declare const objectName: string
|
|
361
|
+
declare const schema: { filter?: QueryParams['$filter'] }
|
|
362
|
+
|
|
363
|
+
const result = await dataSource.find(objectName, {
|
|
364
|
+
$filter: schema.filter,
|
|
365
|
+
...nonGridRowCeilingQuery(), // `$top`: the ceiling plus ONE probe row
|
|
366
|
+
})
|
|
367
|
+
// The semicolon is load-bearing: the next statement opens with `<`, so without
|
|
368
|
+
// it the call above is parsed as the left side of a relational expression.
|
|
369
|
+
const capped = applyNonGridRowCeiling(result);
|
|
370
|
+
// …draw `capped.rows`, then hand the note the whole result:
|
|
371
|
+
<NonGridRowCeilingNote result={capped} />
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`nonGridRowCeilingQuery()` is the one place the probe row is written: `$top` is
|
|
375
|
+
the ceiling plus one, because the extra row is what makes truncation a fact about
|
|
376
|
+
the rows in hand — `QueryResult.total` is optional and a bare-array response
|
|
377
|
+
carries none. Spread it into the query; never spell the `+ 1` at a call site.
|
|
378
|
+
|
|
379
|
+
`NonGridRowCeilingNote` takes the `NonGridCeilingResult` and nothing else. The
|
|
380
|
+
count it prints is `result.rows.length` and the total is `result.total`, so the
|
|
381
|
+
footnote cannot name numbers that disagree with the rows drawn. It renders `null`
|
|
382
|
+
when nothing was truncated, so it can be mounted whenever a result is in hand.
|
|
383
|
+
|
|
384
|
+
⛔ The ceiling is not authorable and must not become so. Silent truncation is
|
|
385
|
+
the failure it exists to prevent — a cut-off schedule still looks like a
|
|
386
|
+
schedule — so a view that caps rows without rendering the note is a defect, not
|
|
387
|
+
an optimisation.
|
|
388
|
+
|
|
389
|
+
### resolveInlineAriaProps
|
|
390
|
+
|
|
391
|
+
Maps the spec's nested `aria` bag (`AriaPropsSchema`, the `aria` prop a block
|
|
392
|
+
such as `element:text` declares) to the DOM attributes it names
|
|
393
|
+
(objectui#11051):
|
|
394
|
+
|
|
395
|
+
- `ariaLabel` → `aria-label`. A plain string is used as written; an inline
|
|
396
|
+
locale map (`{ en: 'Order total', 'zh-CN': '订单合计' }`) gives the entry for
|
|
397
|
+
`locale`, through the spec's own `resolveI18nLabel`.
|
|
398
|
+
- `ariaDescribedBy` → `aria-describedby`.
|
|
399
|
+
- `role` → `role`.
|
|
400
|
+
|
|
401
|
+
Only attributes that have a value are returned, so the result can be spread
|
|
402
|
+
onto an element directly. It is a pure function: pass the locale yourself, in
|
|
403
|
+
React from `useDisplayLocale()`.
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
import { resolveInlineAriaProps } from '@object-ui/react'
|
|
407
|
+
import { useDisplayLocale } from '@object-ui/i18n'
|
|
408
|
+
import type { AriaProps } from '@object-ui/types'
|
|
409
|
+
|
|
410
|
+
function CloseButton({ aria, onClose }: { aria?: AriaProps; onClose: () => void }) {
|
|
411
|
+
const locale = useDisplayLocale()
|
|
412
|
+
return (
|
|
413
|
+
<button type="button" onClick={onClose} {...resolveInlineAriaProps(aria, locale)}>
|
|
414
|
+
×
|
|
415
|
+
</button>
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
This is not the reader `SchemaRenderer` applies to a node's FLAT `ariaLabel`,
|
|
421
|
+
which is objectui's keyed `{ key, defaultValue }` form. Each resolver returns
|
|
422
|
+
nothing useful for the other's shape, so the two stay separate (objectui#4580).
|
|
423
|
+
|
|
424
|
+
### ComponentRegistry
|
|
425
|
+
|
|
426
|
+
There is no registry hook: the registry is a process-level singleton exported
|
|
427
|
+
by `@object-ui/core`, so read it directly. Subscribe to it only when a lazily
|
|
428
|
+
registered plugin must trigger a re-render.
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
import { ComponentRegistry } from '@object-ui/core'
|
|
432
|
+
|
|
433
|
+
function MyComponent(props: Record<string, unknown>) {
|
|
434
|
+
const Component = ComponentRegistry.get('button')
|
|
435
|
+
|
|
436
|
+
return Component ? <Component {...props} /> : null
|
|
173
437
|
}
|
|
174
438
|
```
|
|
175
439
|
|
|
176
440
|
### useDiscovery
|
|
177
441
|
|
|
178
|
-
Access server discovery information
|
|
442
|
+
Access server discovery information — service availability and server identity:
|
|
179
443
|
|
|
180
444
|
```tsx
|
|
181
445
|
import { useDiscovery } from '@object-ui/react'
|
|
@@ -183,11 +447,6 @@ import { useDiscovery } from '@object-ui/react'
|
|
|
183
447
|
function MyComponent() {
|
|
184
448
|
const { discovery, isLoading, isAuthEnabled, isAiEnabled } = useDiscovery()
|
|
185
449
|
|
|
186
|
-
// Check if the server is in preview mode
|
|
187
|
-
if (discovery?.mode === 'preview') {
|
|
188
|
-
console.log('Preview mode active:', discovery.previewMode)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
450
|
// Check if AI service is available
|
|
192
451
|
if (isAiEnabled) {
|
|
193
452
|
console.log('AI service route:', discovery?.services?.ai?.route)
|
|
@@ -203,21 +462,17 @@ function MyComponent() {
|
|
|
203
462
|
| --- | --- | --- |
|
|
204
463
|
| `name` | `string` | Server name |
|
|
205
464
|
| `version` | `string` | Server version |
|
|
206
|
-
| `mode` | `string` | Runtime mode
|
|
207
|
-
| `previewMode` | `object` | Preview mode configuration (present when mode is `'preview'`) |
|
|
465
|
+
| `mode` | `string` | Runtime mode the server reports (`RuntimeMode` in `@objectstack/spec`) |
|
|
208
466
|
| `services` | `object` | Service availability status (auth, data, metadata, ai) |
|
|
209
467
|
| `capabilities` | `string[]` | API capabilities |
|
|
210
468
|
|
|
211
|
-
The `previewMode`
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
| `readOnly` | `boolean` | `false` | Read-only mode |
|
|
219
|
-
| `expiresInSeconds` | `number` | `0` | Session duration (0 = no expiry) |
|
|
220
|
-
| `bannerMessage` | `string` | — | UI banner message |
|
|
469
|
+
> The `previewMode` block and the `'preview'` runtime mode were retired in
|
|
470
|
+
> objectui#6654, following their retirement in `@objectstack/spec`
|
|
471
|
+
> (objectstack#11846). A server that still sends them is read as any other
|
|
472
|
+
> payload: authentication follows `services.auth` alone, so a preview
|
|
473
|
+
> deployment requires login rather than silently running with auth off.
|
|
474
|
+
> `AuthProvider`'s `previewMode` **prop** in `@object-ui/auth` is a separate,
|
|
475
|
+
> host-supplied capability and is unaffected.
|
|
221
476
|
|
|
222
477
|
### useNotifications / useNotificationsByPresentation
|
|
223
478
|
|
|
@@ -239,6 +494,8 @@ belong (a banner is in flow, an inline notification sits next to its raiser).
|
|
|
239
494
|
why every type looked like a toast.
|
|
240
495
|
|
|
241
496
|
```tsx
|
|
497
|
+
import { useNotifications } from '@object-ui/react'
|
|
498
|
+
|
|
242
499
|
const { notify } = useNotifications()
|
|
243
500
|
|
|
244
501
|
notify({ title: 'Saved', severity: 'success' }) // toast (spec default)
|
|
@@ -30,26 +30,33 @@ export interface LazyPluginOptions {
|
|
|
30
30
|
/**
|
|
31
31
|
* Create a lazy-loaded plugin component with Suspense wrapper
|
|
32
32
|
*
|
|
33
|
-
* @param importFn - Dynamic import function
|
|
33
|
+
* @param importFn - Dynamic import function resolving to a module whose
|
|
34
|
+
* `default` is the component to render. ObjectUI plugin packages export
|
|
35
|
+
* their components by NAME and have no `default`, so a bare
|
|
36
|
+
* `() => import('@object-ui/plugin-<x>')` does NOT satisfy this — it hands
|
|
37
|
+
* over the module namespace object, which is neither a component at runtime
|
|
38
|
+
* nor assignable here. Name the component you want, as the examples do.
|
|
34
39
|
* @param options - Configuration options for the lazy plugin
|
|
35
40
|
* @returns A component that lazy loads the plugin
|
|
36
41
|
*
|
|
37
42
|
* @example
|
|
38
43
|
* ```tsx
|
|
39
|
-
* // Basic usage
|
|
44
|
+
* // Basic usage. `@object-ui/plugin-grid` has no default export, so unwrap the
|
|
45
|
+
* // named component. Use the `async` form: the `.then((m) => ({ default: m.X }))`
|
|
46
|
+
* // spelling infers `P` as `never` on one branch of the `then` overload.
|
|
40
47
|
* const ObjectGrid = createLazyPlugin(
|
|
41
|
-
* () => import('@object-ui/plugin-grid')
|
|
48
|
+
* async () => ({ default: (await import('@object-ui/plugin-grid')).ObjectGrid })
|
|
42
49
|
* );
|
|
43
50
|
*
|
|
44
51
|
* // With custom fallback
|
|
45
|
-
* const
|
|
46
|
-
* () => import('@object-ui/plugin-grid'),
|
|
52
|
+
* const ObjectGridWithFallback = createLazyPlugin(
|
|
53
|
+
* async () => ({ default: (await import('@object-ui/plugin-grid')).ObjectGrid }),
|
|
47
54
|
* { fallback: <div>Loading grid...</div> }
|
|
48
55
|
* );
|
|
49
56
|
*
|
|
50
57
|
* // With retry and error handling
|
|
51
|
-
* const
|
|
52
|
-
* () => import('@object-ui/plugin-grid'),
|
|
58
|
+
* const ObjectGridWithRetry = createLazyPlugin(
|
|
59
|
+
* async () => ({ default: (await import('@object-ui/plugin-grid')).ObjectGrid }),
|
|
53
60
|
* {
|
|
54
61
|
* retries: 3,
|
|
55
62
|
* errorFallback: ({ error, retry }) => (
|
package/dist/LazyPluginLoader.js
CHANGED
|
@@ -58,26 +58,33 @@ function createRetryImport(importFn, retries, retryDelay) {
|
|
|
58
58
|
/**
|
|
59
59
|
* Create a lazy-loaded plugin component with Suspense wrapper
|
|
60
60
|
*
|
|
61
|
-
* @param importFn - Dynamic import function
|
|
61
|
+
* @param importFn - Dynamic import function resolving to a module whose
|
|
62
|
+
* `default` is the component to render. ObjectUI plugin packages export
|
|
63
|
+
* their components by NAME and have no `default`, so a bare
|
|
64
|
+
* `() => import('@object-ui/plugin-<x>')` does NOT satisfy this — it hands
|
|
65
|
+
* over the module namespace object, which is neither a component at runtime
|
|
66
|
+
* nor assignable here. Name the component you want, as the examples do.
|
|
62
67
|
* @param options - Configuration options for the lazy plugin
|
|
63
68
|
* @returns A component that lazy loads the plugin
|
|
64
69
|
*
|
|
65
70
|
* @example
|
|
66
71
|
* ```tsx
|
|
67
|
-
* // Basic usage
|
|
72
|
+
* // Basic usage. `@object-ui/plugin-grid` has no default export, so unwrap the
|
|
73
|
+
* // named component. Use the `async` form: the `.then((m) => ({ default: m.X }))`
|
|
74
|
+
* // spelling infers `P` as `never` on one branch of the `then` overload.
|
|
68
75
|
* const ObjectGrid = createLazyPlugin(
|
|
69
|
-
* () => import('@object-ui/plugin-grid')
|
|
76
|
+
* async () => ({ default: (await import('@object-ui/plugin-grid')).ObjectGrid })
|
|
70
77
|
* );
|
|
71
78
|
*
|
|
72
79
|
* // With custom fallback
|
|
73
|
-
* const
|
|
74
|
-
* () => import('@object-ui/plugin-grid'),
|
|
80
|
+
* const ObjectGridWithFallback = createLazyPlugin(
|
|
81
|
+
* async () => ({ default: (await import('@object-ui/plugin-grid')).ObjectGrid }),
|
|
75
82
|
* { fallback: <div>Loading grid...</div> }
|
|
76
83
|
* );
|
|
77
84
|
*
|
|
78
85
|
* // With retry and error handling
|
|
79
|
-
* const
|
|
80
|
-
* () => import('@object-ui/plugin-grid'),
|
|
86
|
+
* const ObjectGridWithRetry = createLazyPlugin(
|
|
87
|
+
* async () => ({ default: (await import('@object-ui/plugin-grid')).ObjectGrid }),
|
|
81
88
|
* {
|
|
82
89
|
* retries: 3,
|
|
83
90
|
* errorFallback: ({ error, retry }) => (
|
package/dist/SchemaRenderer.d.ts
CHANGED
|
@@ -6,7 +6,24 @@
|
|
|
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 `properties` evaluation of the {@link SchemaRenderer} memo, for a config
|
|
12
|
+
* bag that does not pass through `SchemaRenderer` (objectui#10290): the same
|
|
13
|
+
* per-key rule, the same `ExpressionEvaluator`, and the same scope, built from
|
|
14
|
+
* the same three inputs. It is not a second template engine.
|
|
15
|
+
*
|
|
16
|
+
* The plain half of `useConfigBagEvaluator()` (`hooks/useConfigBagEvaluator`),
|
|
17
|
+
* which reads those inputs from the contexts the memo reads them from. Call
|
|
18
|
+
* the hook; this function exists because the rule it applies is this module's.
|
|
19
|
+
* (The hook is not declared in this module: a hook here puts this file under
|
|
20
|
+
* the React Compiler lint, which then reports the render-time
|
|
21
|
+
* `performance.now()` / `Date.now()` debug probes below.)
|
|
22
|
+
*
|
|
23
|
+
* A value that is not a config bag is returned unchanged; a bag comes back as a
|
|
24
|
+
* new, evaluated object and the authored one is never mutated.
|
|
25
|
+
*/
|
|
26
|
+
export declare function evaluateConfigBagInScope(bag: unknown, predicateScope: Record<string, unknown>, boundRecord: unknown, pageVariables: unknown): unknown;
|
|
10
27
|
/**
|
|
11
28
|
* Per-component Error Boundary for SchemaRenderer.
|
|
12
29
|
* Catches render errors in individual components, preventing one broken
|
|
@@ -35,9 +52,9 @@ export declare class SchemaErrorBoundary extends Component<{
|
|
|
35
52
|
*
|
|
36
53
|
* ## Why `schema` is spelled as this union and not as a `SchemaNode`
|
|
37
54
|
*
|
|
38
|
-
* The repo
|
|
39
|
-
* interface (which requires `type: string`) and `@object-ui/types`' union
|
|
40
|
-
*
|
|
55
|
+
* The repo carried two competing `SchemaNode` types: `@object-ui/core`'s
|
|
56
|
+
* interface (which requires `type: string`) and `@object-ui/types`' union (then
|
|
57
|
+
* `BaseSchema | string | number | boolean | null | undefined`). This component
|
|
41
58
|
* matched NEITHER. It declared core's — narrower than what it accepts, so every
|
|
42
59
|
* caller holding the types union was wrong and could not be told — while its
|
|
43
60
|
* runtime returns early for strings and nullish, which core's interface forbids.
|
|
@@ -48,11 +65,25 @@ export declare class SchemaErrorBoundary extends Component<{
|
|
|
48
65
|
* handles: an object schema, a bare string (rendered as text), or nothing at
|
|
49
66
|
* all. `number` / `boolean` are deliberately excluded — the runtime tolerates
|
|
50
67
|
* them defensively (see the primitive guard in the evaluation memo) but no
|
|
51
|
-
* author should be invited to pass them
|
|
52
|
-
*
|
|
68
|
+
* author should be invited to pass them; `toRenderableSchema` (`./schema-input`)
|
|
69
|
+
* is the bridge from a `SchemaNode` that may hold one.
|
|
70
|
+
*
|
|
71
|
+
* ## The object member is `DeclaredNode` (objectui#11466)
|
|
72
|
+
*
|
|
73
|
+
* The object member is `DeclaredNode` from `@object-ui/types`, the same type a
|
|
74
|
+
* node slot takes: the discriminated union, keyed by the literal `type`, of the
|
|
75
|
+
* component schemas that package declares, the spec-declared `AuthoringNode`s
|
|
76
|
+
* (objectui#11364: public blocks, `element:text_input` /
|
|
77
|
+
* `element:record_picker`, a stored page document under its page kind), and
|
|
78
|
+
* the types an application declares in `CustomNodeRegistry`. It has no
|
|
79
|
+
* `type: string` arm, so a node whose `type` no declaration names is refused
|
|
80
|
+
* here, and each node's own keys are judged: once objectui#8347 removes
|
|
81
|
+
* `BaseSchema`'s index signature, a misspelled key is refused while the
|
|
82
|
+
* declared spelling compiles. ⛔ The prop is widened by declared types only,
|
|
83
|
+
* never by an index signature, a `Record` or a `type: string` arm.
|
|
53
84
|
*/
|
|
54
85
|
export interface SchemaRendererProps {
|
|
55
|
-
schema:
|
|
86
|
+
schema: DeclaredNode | string | null | undefined;
|
|
56
87
|
}
|
|
57
88
|
/**
|
|
58
89
|
* The open forwarding surface, named once so it reads as the decision it is.
|