@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.
Files changed (103) hide show
  1. package/CHANGELOG.md +4263 -0
  2. package/README.md +383 -42
  3. package/dist/SchemaRenderer.d.ts +64 -7
  4. package/dist/SchemaRenderer.js +1326 -106
  5. package/dist/context/ActionContext.js +40 -1
  6. package/dist/context/AppShellContext.d.ts +27 -0
  7. package/dist/context/DiscussionContext.d.ts +27 -0
  8. package/dist/context/DiscussionContext.js +4 -0
  9. package/dist/context/DrillNavigationContext.d.ts +13 -0
  10. package/dist/context/InlineEditContext.d.ts +151 -1
  11. package/dist/context/InlineEditContext.js +36 -2
  12. package/dist/context/PrintPageContext.d.ts +60 -0
  13. package/dist/context/PrintPageContext.js +32 -0
  14. package/dist/context/RecordContext.d.ts +48 -5
  15. package/dist/context/RecordContext.js +10 -1
  16. package/dist/context/RelatedRecordActionsContext.d.ts +72 -2
  17. package/dist/context/RelatedRecordActionsContext.js +4 -0
  18. package/dist/context/SchemaRendererContext.d.ts +61 -6
  19. package/dist/context/SchemaRendererContext.js +39 -6
  20. package/dist/context/ThemeContext.js +1 -1
  21. package/dist/context/actionRunnerTranslator.d.ts +27 -0
  22. package/dist/context/actionRunnerTranslator.js +37 -0
  23. package/dist/context/index.d.ts +1 -0
  24. package/dist/context/index.js +1 -0
  25. package/dist/element-data-source/ElementApiDataSourcePanel.d.ts +28 -0
  26. package/dist/element-data-source/ElementApiDataSourcePanel.js +10 -0
  27. package/dist/element-data-source/ElementDataSourceGate.d.ts +118 -3
  28. package/dist/element-data-source/ElementDataSourceGate.js +401 -19
  29. package/dist/element-data-source/useElementApiDataSource.d.ts +119 -0
  30. package/dist/element-data-source/useElementApiDataSource.js +198 -0
  31. package/dist/hooks/index.d.ts +3 -1
  32. package/dist/hooks/index.js +10 -1
  33. package/dist/hooks/useActionEngine.js +46 -3
  34. package/dist/hooks/useActionRunner.js +4 -0
  35. package/dist/hooks/useActionTextLocalizer.d.ts +35 -9
  36. package/dist/hooks/useActionTextLocalizer.js +70 -14
  37. package/dist/hooks/useConfigBagEvaluator.d.ts +31 -0
  38. package/dist/hooks/useConfigBagEvaluator.js +40 -0
  39. package/dist/hooks/useDatasetDimensionLabels.js +25 -10
  40. package/dist/hooks/useDensityMode.d.ts +8 -2
  41. package/dist/hooks/useDensityMode.js +8 -2
  42. package/dist/hooks/useDiscovery.d.ts +1 -10
  43. package/dist/hooks/useETagCache.js +25 -4
  44. package/dist/hooks/useElementDataSource.d.ts +15 -2
  45. package/dist/hooks/useElementDataSource.js +24 -7
  46. package/dist/hooks/useExpression.d.ts +52 -59
  47. package/dist/hooks/useExpression.js +58 -90
  48. package/dist/hooks/useFilterScope.d.ts +22 -2
  49. package/dist/hooks/useFilterScope.js +29 -4
  50. package/dist/hooks/useGlobalUndo.d.ts +10 -0
  51. package/dist/hooks/useGlobalUndo.js +112 -46
  52. package/dist/hooks/useNavigationOverlay.d.ts +76 -11
  53. package/dist/hooks/useNavigationOverlay.js +119 -9
  54. package/dist/hooks/useOffline.js +65 -11
  55. package/dist/hooks/usePageAssignment.d.ts +33 -7
  56. package/dist/hooks/usePageAssignment.js +85 -21
  57. package/dist/hooks/useRecordSearch.d.ts +34 -1
  58. package/dist/hooks/useRecordSearch.js +42 -7
  59. package/dist/hooks/useResolvedFilter.d.ts +46 -0
  60. package/dist/hooks/useResolvedFilter.js +130 -0
  61. package/dist/hooks/useSchemaPersistence.d.ts +7 -0
  62. package/dist/hooks/useSchemaPersistence.js +144 -5
  63. package/dist/hooks/useSettledSchema.d.ts +123 -0
  64. package/dist/hooks/useSettledSchema.js +186 -0
  65. package/dist/hooks/useViewData.d.ts +10 -3
  66. package/dist/hooks/useViewData.js +5 -14
  67. package/dist/index.d.ts +9 -1
  68. package/dist/index.js +48 -1
  69. package/dist/schema-input.d.ts +38 -8
  70. package/dist/schema-input.js +39 -7
  71. package/dist/utils/configBag.d.ts +68 -0
  72. package/dist/utils/configBag.js +70 -0
  73. package/dist/utils/error-message.d.ts +1 -1
  74. package/dist/utils/error-message.js +1 -1
  75. package/dist/utils/inlineAria.d.ts +61 -0
  76. package/dist/utils/inlineAria.js +70 -0
  77. package/dist/utils/localeMap.d.ts +27 -0
  78. package/dist/utils/localeMap.js +106 -0
  79. package/dist/utils/nonGridRowCeiling.d.ts +44 -0
  80. package/dist/utils/nonGridRowCeiling.js +78 -0
  81. package/dist/utils/paramsBag.d.ts +28 -0
  82. package/dist/utils/paramsBag.js +146 -0
  83. package/dist/utils/propsBagDiagnostic.d.ts +196 -0
  84. package/dist/utils/propsBagDiagnostic.js +422 -0
  85. package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
  86. package/dist/utils/refusedDataPropDiagnostic.js +94 -0
  87. package/dist/utils/unevaluatedExpression.d.ts +44 -1
  88. package/dist/utils/unevaluatedExpression.js +92 -11
  89. package/dist/utils/visibilityDiagnostic.d.ts +282 -21
  90. package/dist/utils/visibilityDiagnostic.js +321 -46
  91. package/package.json +10 -10
  92. package/dist/hooks/useClientNotifications.d.ts +0 -47
  93. package/dist/hooks/useClientNotifications.js +0 -162
  94. package/dist/spec-bridge/SpecBridge.d.ts +0 -28
  95. package/dist/spec-bridge/SpecBridge.js +0 -56
  96. package/dist/spec-bridge/bridges/form-view.d.ts +0 -158
  97. package/dist/spec-bridge/bridges/form-view.js +0 -170
  98. package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
  99. package/dist/spec-bridge/bridges/list-view.js +0 -268
  100. package/dist/spec-bridge/index.d.ts +0 -11
  101. package/dist/spec-bridge/index.js +0 -10
  102. package/dist/spec-bridge/types.d.ts +0 -55
  103. 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
- value: 'Hello, Object UI!'
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
- body: [
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 data = {
81
+ const scope = {
57
82
  user: { name: 'John Doe' }
58
83
  }
59
84
 
60
85
  function App() {
61
- return <SchemaRenderer schema={schema} data={data} />
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 used by `provider: 'api'` view data
95
- // sources, so custom endpoints carry the same credentials (Authorization,
96
- // tenant, locale headers) as the native data channel. When omitted,
97
- // ApiDataSource falls back to the bare global fetch (cookies only).
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 the current schema context:
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 { data, updateData } = useSchemaContext()
119
-
120
- return <div>{data.value}</div>
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
- const ObjectGridRenderer = ({ schema, ...props }) => (
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 including preview mode detection:
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 (e.g. `'development'`, `'production'`, `'preview'`) |
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` object contains:
213
-
214
- | Property | Type | Default | Description |
215
- | --- | --- | --- | --- |
216
- | `autoLogin` | `boolean` | `true` | Skip login/registration pages |
217
- | `simulatedRole` | `'admin' \| 'user' \| 'viewer'` | `'admin'` | Simulated user role |
218
- | `simulatedUserName` | `string` | `'Preview User'` | Display name |
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)
@@ -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 { BaseSchema } from '@object-ui/types';
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 carries two competing `SchemaNode` types: `@object-ui/core`'s
39
- * interface (which requires `type: string`) and `@object-ui/types`' union
40
- * (`BaseSchema | string | number | boolean | null | undefined`). This component
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. Reconciling the two repo-wide
52
- * `SchemaNode` spellings is a separate concern and deliberately not done here.
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: BaseSchema | string | null | undefined;
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.