@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.
Files changed (103) hide show
  1. package/CHANGELOG.md +4100 -0
  2. package/README.md +303 -46
  3. package/dist/LazyPluginLoader.d.ts +14 -7
  4. package/dist/LazyPluginLoader.js +14 -7
  5. package/dist/SchemaRenderer.d.ts +38 -7
  6. package/dist/SchemaRenderer.js +1603 -60
  7. package/dist/context/ActionContext.js +40 -1
  8. package/dist/context/DiscussionContext.d.ts +12 -0
  9. package/dist/context/DiscussionContext.js +2 -0
  10. package/dist/context/DrillNavigationContext.d.ts +13 -0
  11. package/dist/context/HostNavigationContext.d.ts +82 -0
  12. package/dist/context/HostNavigationContext.js +65 -0
  13. package/dist/context/InlineEditContext.d.ts +81 -1
  14. package/dist/context/InlineEditContext.js +8 -2
  15. package/dist/context/RecordContext.d.ts +48 -5
  16. package/dist/context/RecordContext.js +10 -1
  17. package/dist/context/RelatedRecordActionsContext.d.ts +20 -1
  18. package/dist/context/SchemaRendererContext.d.ts +54 -2
  19. package/dist/context/SchemaRendererContext.js +38 -5
  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 +13 -12
  24. package/dist/context/index.js +13 -12
  25. package/dist/element-data-source/ElementDataSourceGate.d.ts +163 -3
  26. package/dist/element-data-source/ElementDataSourceGate.js +407 -15
  27. package/dist/hooks/index.d.ts +37 -35
  28. package/dist/hooks/index.js +44 -35
  29. package/dist/hooks/useActionEngine.js +47 -4
  30. package/dist/hooks/useActionRunner.js +4 -0
  31. package/dist/hooks/useActionTextLocalizer.d.ts +21 -8
  32. package/dist/hooks/useActionTextLocalizer.js +33 -8
  33. package/dist/hooks/useCapabilityGate.js +6 -5
  34. package/dist/hooks/useConfigBagEvaluator.d.ts +30 -0
  35. package/dist/hooks/useConfigBagEvaluator.js +39 -0
  36. package/dist/hooks/useCrudShortcuts.js +1 -1
  37. package/dist/hooks/useDatasetDimensionLabels.js +1 -1
  38. package/dist/hooks/useDensityMode.d.ts +6 -2
  39. package/dist/hooks/useDensityMode.js +6 -2
  40. package/dist/hooks/useDiscovery.d.ts +1 -10
  41. package/dist/hooks/useDiscovery.js +1 -1
  42. package/dist/hooks/useETagCache.js +25 -4
  43. package/dist/hooks/useElementDataSource.d.ts +15 -2
  44. package/dist/hooks/useElementDataSource.js +29 -9
  45. package/dist/hooks/useExpression.d.ts +52 -38
  46. package/dist/hooks/useExpression.js +62 -42
  47. package/dist/hooks/useFilterScope.d.ts +22 -2
  48. package/dist/hooks/useFilterScope.js +29 -4
  49. package/dist/hooks/useGlobalUndo.d.ts +10 -0
  50. package/dist/hooks/useGlobalUndo.js +112 -46
  51. package/dist/hooks/useNavigationOverlay.d.ts +77 -12
  52. package/dist/hooks/useNavigationOverlay.js +119 -9
  53. package/dist/hooks/useOffline.js +65 -11
  54. package/dist/hooks/usePageAssignment.d.ts +8 -5
  55. package/dist/hooks/usePageAssignment.js +38 -17
  56. package/dist/hooks/usePageTransition.js +1 -1
  57. package/dist/hooks/usePageVariableActionBridge.js +2 -2
  58. package/dist/hooks/useRecordSearch.d.ts +34 -1
  59. package/dist/hooks/useRecordSearch.js +42 -7
  60. package/dist/hooks/useResolvedFilter.d.ts +46 -0
  61. package/dist/hooks/useResolvedFilter.js +130 -0
  62. package/dist/hooks/useSchemaPersistence.d.ts +7 -0
  63. package/dist/hooks/useSchemaPersistence.js +144 -5
  64. package/dist/hooks/useSettledSchema.d.ts +123 -0
  65. package/dist/hooks/useSettledSchema.js +186 -0
  66. package/dist/hooks/useViewData.d.ts +7 -2
  67. package/dist/hooks/useViewData.js +1 -1
  68. package/dist/hooks/useViewTransition.js +1 -1
  69. package/dist/index.d.ts +18 -11
  70. package/dist/index.js +54 -10
  71. package/dist/schema-input.d.ts +55 -14
  72. package/dist/schema-input.js +55 -12
  73. package/dist/utils/configBag.d.ts +68 -0
  74. package/dist/utils/configBag.js +70 -0
  75. package/dist/utils/error-message.d.ts +59 -0
  76. package/dist/utils/error-message.js +116 -1
  77. package/dist/utils/inlineAria.d.ts +61 -0
  78. package/dist/utils/inlineAria.js +70 -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 +421 -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 +110 -0
  88. package/dist/utils/unevaluatedExpression.js +257 -0
  89. package/dist/utils/visibilityDiagnostic.d.ts +371 -0
  90. package/dist/utils/visibilityDiagnostic.js +551 -0
  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 -82
  97. package/dist/spec-bridge/bridges/form-view.js +0 -126
  98. package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
  99. package/dist/spec-bridge/bridges/list-view.js +0 -241
  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
@@ -51,6 +51,27 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
51
51
  * designer emits for an unconfigured column list, and supplying the columns is
52
52
  * exactly why a view was named.
53
53
  *
54
+ * The row cap answers "did the component author one?" the same way, and for the
55
+ * same reason (objectui#9899). A cap the contract REFUSES — `pageSize: 0`,
56
+ * a negative, a fraction — is not a cap this relay may treat as the author's
57
+ * intent, so it does not suppress the one the bound view supplies. Deciding
58
+ * that question by PRESENCE is how a view's legitimate cap was dropped in
59
+ * favour of the consuming renderer's own default: measured end to end on
60
+ * `list-view` before the repair, a component carrying `pagination.pageSize: 0`
61
+ * under a view supplying `7` put `$top: 100` on the wire — the view's cap never
62
+ * reached it, so the read was WIDER than the view asked for.
63
+ *
64
+ * The VIEW's own cap can be refused too, and `@object-ui/core` drops it before
65
+ * it reaches this relay (objectui#9928). The drop is right, but it left the
66
+ * consuming renderer nothing to refuse, so it went silent — and this relay
67
+ * reports the view's refusal instead (objectui#10015). See
68
+ * {@link describeRefusedViewRowLimit} for when that message applies.
69
+ *
70
+ * The BINDING's own cap follows the same rule (objectui#10016): a `limit` the
71
+ * contract refuses is not authored, so `@object-ui/core` lets the view's cap
72
+ * through in its place, and that cap is a baseline here like any other
73
+ * view-sourced value. See {@link describeRefusedBindingRowLimit}.
74
+ *
54
75
  * ## What a mapping may NOT do
55
76
  *
56
77
  * {@link ElementDataSourceMapping} names only keys the target block genuinely
@@ -60,6 +81,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
60
81
  * therefore leaves `limit` unmapped rather than parking it somewhere plausible,
61
82
  * and the gap is recorded at the call site instead of being papered over here.
62
83
  *
84
+ * ## A block with no ADAPTER fails loudly too (objectui#5378)
85
+ *
86
+ * The section below is about an unresolvable view NAME. The coarser failure —
87
+ * no runtime adapter reached the block at all — used to render an empty shell
88
+ * in exactly the same silence, and that is what `requiresDataSource` +
89
+ * {@link NoDataSourcePanel} now report. Measured on the published
90
+ * getting-started guide: all five of its blocks resolved no adapter, fetched
91
+ * nothing, and said nothing. The two panels are deliberately the same shape;
92
+ * they answer "which view?" and "which data source?", and an author who sees
93
+ * either one is being told where to look.
94
+ *
63
95
  * ## An unresolvable `view` fails loudly
64
96
  *
65
97
  * When the named view does not exist the gate renders a configuration error
@@ -69,8 +101,135 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
69
101
  * like it works.
70
102
  */
71
103
  import * as React from 'react';
72
- import { isElementDataSourceConfig, mergeFilterNodes, } from '@object-ui/core';
73
- import { useElementDataSource, } from '../hooks/useElementDataSource';
104
+ import { elementDataSourceRefusedLimitMessage, filterRefusalSubject, isElementDataSourceConfig, mergeFilterNodes, toFilterNodeSafely, } from '@object-ui/core';
105
+ import { useObjectTranslation } from '@object-ui/i18n';
106
+ import { useElementDataSource, } from '../hooks/useElementDataSource.js';
107
+ import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
108
+ /**
109
+ * What the contract admits as a row cap (objectui#9899).
110
+ *
111
+ * `@objectstack/spec` has already answered what `0` means for the keys this
112
+ * branch writes. Its view pagination config declares `pageSize` a positive
113
+ * integer with a default; every component row cap its component props map
114
+ * declares is `z.number().int().positive()`; and that flat `limit` is the key a
115
+ * view's `pagination.pageSize` is LOWERED INTO for the blocks that read it. So
116
+ * `0`, a negative and a fraction are not spellings this relay may assign a
117
+ * meaning to — they are values the contract refuses.
118
+ *
119
+ * ⚠️ Said precisely, because it is easy to overstate: the `object-grid` props
120
+ * face declares its OWN `pagination` key `z.unknown()`, so a save gate does not
121
+ * refuse a component-level `pagination.pageSize: 0` on that face, and the flat
122
+ * `pageSize` shorthand beside it is a bare `z.number()`. The refusal rests on
123
+ * the pagination CONFIG contract and on the `limit` the value is lowered into —
124
+ * the same ground objectui#9853, objectui#9897 and objectui#9925 already stood
125
+ * on at the renderer sites.
126
+ *
127
+ * ⛔ Deliberately a LOCAL restatement of the predicate objectui#9925 landed at
128
+ * the renderer sites, not an import and not a shared helper: hoisting it would
129
+ * be a cross-package extraction this card may not make. The cost is stated
130
+ * rather than hidden — one rule now has a spelling at every site that enforces
131
+ * it, and `git grep 'isUsableRowLimit\|isUsablePageSize'` is what enumerates
132
+ * them; the day one is meant to change, all of them are.
133
+ */
134
+ function isUsableRowLimit(value) {
135
+ return typeof value === 'number' && Number.isInteger(value) && value > 0;
136
+ }
137
+ /**
138
+ * The loud half, and the reason a RELAY owes one at all.
139
+ *
140
+ * A relay that merely declines to treat a value as authored could reasonably
141
+ * say nothing — the renderer it forwards to has its own refusal and its own
142
+ * message. Measured rather than assumed: BEFORE this repair, a `list-view`
143
+ * carrying `pagination.pageSize: 0` under a bound view was told about it, once,
144
+ * by `ListView`'s own diagnostic. AFTER it, the gate writes the view's usable
145
+ * cap, that renderer sees a value it accepts, and its message correctly goes
146
+ * silent — so the repair REMOVES the only place the author was being told.
147
+ * This restores that one message at the layer that now makes the decision.
148
+ *
149
+ * ⛔ NOT a second guard: the predicate lives once, in {@link isUsableRowLimit},
150
+ * and this reads it. `null` means "nothing to say" — an absent cap is not a
151
+ * mistake and a usable one is not either, which is what keeps this from
152
+ * becoming an always-on marker that states nothing.
153
+ */
154
+ const describeDisplacedRowLimit = (authored, key, used, componentType, objectName) => {
155
+ if (authored === undefined || authored === null)
156
+ return null;
157
+ if (isUsableRowLimit(authored))
158
+ return null;
159
+ const type = typeof componentType === 'string' && componentType ? componentType : 'component';
160
+ const where = typeof objectName === 'string' && objectName ? `${type} on ${objectName}` : type;
161
+ return (`[ObjectUI] ElementDataSourceGate row cap: ${where} declared ${key}: ${String(authored)}, `
162
+ + 'which is not a positive integer. A row cap must be a positive integer '
163
+ + '(the spec refuses zero and negative values), so it was ignored and the cap '
164
+ + `from this component's bound data source (${used}) was used instead.`);
165
+ };
166
+ /**
167
+ * The loud half for a cap the SAVED VIEW declared and the contract refuses
168
+ * (objectui#10015).
169
+ *
170
+ * `@object-ui/core` drops such a cap while composing, so `composed.limit`
171
+ * never carries it and the renderer below never sees it. Its own diagnostic
172
+ * treats an absent key as "not a mistake", so after that drop nothing said
173
+ * anything. The wording is the core builder's, not this file's: it is the one
174
+ * sentence both consumers of the composed binding use, and `ViewDataProvider`
175
+ * already reports it on the path with no renderer. The builder is pure because
176
+ * the composer runs inside a `useMemo`; this relay reports its result from an
177
+ * effect, the same way it reports {@link describeDisplacedRowLimit}.
178
+ *
179
+ * It speaks only when the refusal CHANGED what this block receives — when a
180
+ * usable view cap would have been written here:
181
+ *
182
+ * - the binding declared no usable `limit` of its own (if it did, the
183
+ * binding's cap is what the block gets whatever the view carries). The
184
+ * builder applies this condition itself, because `ViewDataProvider` needs
185
+ * it too (objectui#10016), so this relay hands it the binding; and
186
+ * - the component's own cap is not a usable one (a usable one wins over any
187
+ * view cap).
188
+ *
189
+ * In both of those excluded cases the builder's sentence — the block falls
190
+ * back to its own default — would not be true, so silence is the honest
191
+ * answer, not a gap. The caller adds the third condition: the block reads a
192
+ * row cap at all.
193
+ *
194
+ * ## Never together with {@link describeDisplacedRowLimit}
195
+ *
196
+ * That message needs a usable cap to have been WRITTEN. With a refused view
197
+ * cap, the only usable cap left is a binding's usable `limit`, and that is
198
+ * excluded above. So the two are exclusive by construction. When the component
199
+ * AND the view both carry a refused cap, this message names the view and the
200
+ * renderer names the component: two declarations, one message each.
201
+ */
202
+ const describeRefusedViewRowLimit = (view, config, authored) => {
203
+ if (!config)
204
+ return null;
205
+ if (isUsableRowLimit(authored))
206
+ return null;
207
+ return elementDataSourceRefusedLimitMessage(view, config.view, config.object, config);
208
+ };
209
+ /**
210
+ * The loud half for a cap the BINDING declared and the contract refuses
211
+ * (objectui#10016), in the core builder's words for that operand.
212
+ *
213
+ * `@object-ui/core` treats such a cap as not authored: it lets the view's
214
+ * usable cap through in its place, or none. Nothing downstream can see the
215
+ * refused value any more, so this relay reports it, from an effect, the same
216
+ * way it reports the view's refusal.
217
+ *
218
+ * It needs no condition beyond the caller's (the block reads a row cap at
219
+ * all). A usable binding cap is written whatever the component or the view
220
+ * carries, so a refused one is always a cap this block did not get.
221
+ *
222
+ * It can fire beside the other two, and that is one message per declaration,
223
+ * not two per mistake: beside {@link describeRefusedViewRowLimit} when the view
224
+ * ALSO carries a refused cap, and beside {@link describeDisplacedRowLimit} when
225
+ * the component ALSO carries a refused cap and the view's usable one displaced
226
+ * it.
227
+ */
228
+ const describeRefusedBindingRowLimit = (view, config) => {
229
+ if (!config)
230
+ return null;
231
+ return elementDataSourceRefusedLimitMessage(view, config.view, config.object, config, 'binding');
232
+ };
74
233
  const readLimit = (base, key) => {
75
234
  if (key === 'pagination.pageSize')
76
235
  return base.pagination?.pageSize;
@@ -110,10 +269,16 @@ export function useElementDataSourceSchema(schema, mapping = {}, dataSource) {
110
269
  const { object: objectKey = 'objectName', columns, filter, sort, limit, viewType } = mapping;
111
270
  const mapped = React.useMemo(() => {
112
271
  const composed = binding.composed;
272
+ // BY REFERENCE when there is nothing to apply — a fresh object every render
273
+ // would remount the block and refetch. The wrapper is memoised alongside it,
274
+ // so the identity this carries is the one the caller sees.
113
275
  if (!composed)
114
- return schema;
276
+ return { schema, capMessage: null, viewCapMessage: null, bindingCapMessage: null };
115
277
  const base = (schema ?? {});
116
278
  const next = { ...base };
279
+ let capMessage = null;
280
+ let viewCapMessage = null;
281
+ let bindingCapMessage = null;
117
282
  if (objectKey !== false)
118
283
  next[objectKey] = composed.object;
119
284
  if (columns) {
@@ -127,7 +292,23 @@ export function useElementDataSourceSchema(schema, mapping = {}, dataSource) {
127
292
  // Component filter AND (view filter AND binding filter). `composed.filter`
128
293
  // already carries the latter pair; a single surviving source comes back
129
294
  // unwrapped, so the common "only the view filters" case stays flat.
130
- const merged = mergeFilterNodes(base.filter, composed.filter);
295
+ //
296
+ // ⚠️ Each source is lowered through `toFilterNodeSafely` first, the way
297
+ // `RelatedList` and `LineItemsPanel` lower their own filter before they
298
+ // merge it — objectui#10789. This is a RENDER-time `useMemo`, and the
299
+ // lowering refuses malformed authored shapes with a `FilterOperatorError`:
300
+ // uncaught, that was a render error, thrown BEFORE the wrapped block
301
+ // reached its own safe lowering and its malformed-filter state. The
302
+ // refusal is returned as a VALUE and the block is not rendered at all —
303
+ // never merged as "no filter", which would run it unconstrained. The
304
+ // merge below then only re-reads nodes the lowering already produced.
305
+ const own = toFilterNodeSafely(base.filter);
306
+ if (!own.ok)
307
+ return { schema, capMessage: null, viewCapMessage: null, bindingCapMessage: null, filterRefusal: own.refusal };
308
+ const bound = toFilterNodeSafely(composed.filter);
309
+ if (!bound.ok)
310
+ return { schema, capMessage: null, viewCapMessage: null, bindingCapMessage: null, filterRefusal: bound.refusal };
311
+ const merged = mergeFilterNodes(own.node, bound.node);
131
312
  if (merged !== undefined)
132
313
  next.filter = merged;
133
314
  else
@@ -141,22 +322,65 @@ export function useElementDataSourceSchema(schema, mapping = {}, dataSource) {
141
322
  next.sort = composed.sort;
142
323
  }
143
324
  if (limit && composed.limit !== undefined) {
144
- const fromView = binding.config?.limit === undefined;
145
- if (!fromView || readLimit(base, limit) === undefined) {
325
+ // For the row cap, "declared one" means a cap the contract admits
326
+ // (objectui#10016): a refused binding `limit` is not authored, so the
327
+ // composer let the view's cap through in its place, and that cap is a
328
+ // baseline the component's usable cap still wins over.
329
+ const fromView = !isUsableRowLimit(binding.config?.limit);
330
+ // PRESENCE is not authorship (objectui#9899) — the same question the
331
+ // `columns` branch above answers by CONTENT, answered the same way here.
332
+ const authored = readLimit(base, limit);
333
+ if (!fromView || !isUsableRowLimit(authored)) {
146
334
  writeLimit(next, base, limit, composed.limit);
335
+ capMessage = describeDisplacedRowLimit(authored, limit, composed.limit, base.type, composed.object);
147
336
  }
148
337
  }
338
+ // Outside the branch above on purpose: a refused cap never reaches
339
+ // `composed.limit`, so that branch does not run for it (objectui#10015,
340
+ // and objectui#10016 for the binding's).
341
+ if (limit) {
342
+ viewCapMessage = describeRefusedViewRowLimit(binding.view, binding.config, readLimit(base, limit));
343
+ bindingCapMessage = describeRefusedBindingRowLimit(binding.view, binding.config);
344
+ }
149
345
  if (viewType && composed.viewType !== undefined && base.viewType === undefined) {
150
346
  next.viewType = composed.viewType;
151
347
  }
152
- return next;
153
- }, [schema, binding.composed, binding.config, objectKey, columns, filter, sort, limit, viewType]);
154
- return React.useMemo(() => ({
155
- status: binding.status,
156
- schema: mapped,
157
- config: binding.config,
158
- error: binding.error,
159
- }), [binding.status, binding.config, binding.error, mapped]);
348
+ return { schema: next, capMessage, viewCapMessage, bindingCapMessage };
349
+ }, [schema, binding.composed, binding.config, binding.view, objectKey, columns, filter, sort, limit, viewType]);
350
+ // Keyed on the MESSAGE, so it is one warning per declaration rather than one
351
+ // per render — and it fires from an effect, never from render, which is the
352
+ // same shape the renderer sites use for "you declared it, we dropped it".
353
+ // One effect per message, so a change to one never re-emits another.
354
+ const { schema: boundSchema, capMessage, viewCapMessage, bindingCapMessage, filterRefusal: mergeRefusal } = mapped;
355
+ React.useEffect(() => {
356
+ if (capMessage)
357
+ console.warn(capMessage);
358
+ }, [capMessage]);
359
+ React.useEffect(() => {
360
+ if (viewCapMessage)
361
+ console.warn(viewCapMessage);
362
+ }, [viewCapMessage]);
363
+ React.useEffect(() => {
364
+ if (bindingCapMessage)
365
+ console.warn(bindingCapMessage);
366
+ }, [bindingCapMessage]);
367
+ // A refused filter — from either merge — is `missing`, the one state every
368
+ // caller already answers by withholding the block (objectui#10789).
369
+ const filterRefusal = binding.filterRefusal ?? mergeRefusal;
370
+ return React.useMemo(() => filterRefusal
371
+ ? {
372
+ status: 'missing',
373
+ schema: boundSchema,
374
+ config: binding.config,
375
+ error: filterRefusal.message,
376
+ filterRefusal,
377
+ }
378
+ : {
379
+ status: binding.status,
380
+ schema: boundSchema,
381
+ config: binding.config,
382
+ error: binding.error,
383
+ }, [binding.status, binding.config, binding.error, boundSchema, filterRefusal]);
160
384
  }
161
385
  /**
162
386
  * The "named view does not resolve" panel. Same posture (and shape) as
@@ -166,6 +390,41 @@ export function useElementDataSourceSchema(schema, mapping = {}, dataSource) {
166
390
  export function ElementDataSourceErrorPanel({ testId, title = 'This component’s data source could not be resolved', message, }) {
167
391
  return (_jsxs("div", { className: "p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2", role: "alert", "data-testid": `${testId}-datasource-error`, children: [_jsx("p", { className: "font-medium", children: title }), message ? _jsx("p", { className: "text-sm mt-1", children: message }) : null] }));
168
392
  }
393
+ /**
394
+ * The malformed-filter notice, drawn IN PLACE OF the block when a filter the
395
+ * gate had to merge is refused (objectui#10789).
396
+ *
397
+ * It is the notice the wrapped blocks draw themselves (objectui#9050 step 2 —
398
+ * `record:related_list`, the line-items panel, `object-grid`): the same
399
+ * sentence (`view.malformedFilter`), naming the refused operator or field in a
400
+ * separately addressable headline, with the refusal's own message beneath it.
401
+ * The gate draws it because its merge refuses FIRST — before the block's own
402
+ * safe lowering ever runs — so without it the author would get the error
403
+ * boundary's "failed to render" instead of the sentence those blocks give.
404
+ *
405
+ * ⚠️ A separate component, so `useObjectTranslation` runs only on this path:
406
+ * the gate wraps every object-bound block, and a translation hook in the
407
+ * gate's own body would run for all of them, including under the many suites
408
+ * that mock `@object-ui/i18n` by hand.
409
+ *
410
+ * The provider-less default is the sentence itself, written the way the
411
+ * sibling blocks carry it: a LITERAL copy of the `en` pack's value, never a
412
+ * read of the pack object. The siblings keep theirs in a module-scope
413
+ * `createSafeTranslation` table, which this module cannot have: it is
414
+ * re-exported from the package entry, so a factory call at module scope would
415
+ * run on import for every consumer (`NonGridRowCeilingNote` records that
416
+ * hazard). So the copy sits in the call's inline `defaultValue`, where
417
+ * `pnpm check:i18n-keys` holds it byte-identical to the pack, as the siblings'
418
+ * tables are held. With an `I18nProvider` the pack's value wins, and without
419
+ * one this literal is interpolated (objectui#6219).
420
+ */
421
+ function ElementDataSourceMalformedFilterPanel({ testId, refusal, }) {
422
+ const { t } = useObjectTranslation();
423
+ return (_jsxs("div", { role: "alert", className: "rounded-md border border-amber-300 bg-amber-50 p-3 my-2 text-sm text-amber-800", "data-testid": `${testId}-malformed-filter`, children: [_jsx("p", { className: "font-medium", "data-testid": `${testId}-malformed-filter-subject`, children: t('view.malformedFilter', {
424
+ subject: filterRefusalSubject(refusal) ?? '',
425
+ defaultValue: 'This view’s filter is malformed, so no records are shown: the {{subject}} condition cannot be applied.',
426
+ }) }), _jsx("p", { className: "mt-1 text-xs opacity-80", children: refusal.message })] }));
427
+ }
169
428
  /**
170
429
  * The "saved views are still being fetched" placeholder. Distinct from the error
171
430
  * panel because a component that treated "not resolved yet" as "does not exist"
@@ -174,6 +433,124 @@ export function ElementDataSourceErrorPanel({ testId, title = 'This component’
174
433
  export function ElementDataSourceLoadingPanel({ testId, }) {
175
434
  return (_jsx("div", { className: "p-2 text-sm text-muted-foreground animate-pulse", role: "status", "aria-live": "polite", "data-testid": `${testId}-resolving-view`, children: "Loading view\u2026" }));
176
435
  }
436
+ /**
437
+ * The adapter this block would use, resolved the one way every block in the
438
+ * family resolves it: an explicit value first, the `SchemaRendererProvider`
439
+ * context second.
440
+ *
441
+ * The `isElementDataSourceConfig` guard is the same one
442
+ * {@link useElementDataSourceSchema} applies — a host handing us the spec
443
+ * BINDING under this name is handing us metadata, not an adapter, and counting
444
+ * it as one is how "no data source" reads as "resolved".
445
+ *
446
+ * `T` is what the CALLER expects to have been injected, and the assertion is
447
+ * the caller's, not this hook's: an adapter arrives here as a `dataSource` prop
448
+ * or a context value that nothing in this package can narrow, so the type has
449
+ * to be declared by whoever knows which interface their block consumes. Leave
450
+ * it off and you get `unknown`, which is the honest default.
451
+ */
452
+ export function useResolvedDataSource(explicit) {
453
+ // Read AS DECLARED (objectui#7209): the only cast in this hook is the
454
+ // caller's `T` on the way out, never one erasing the context on the way in.
455
+ // Pinned by `useResolvedDataSource.schemaRendererContextRead-7209.test.ts`.
456
+ const context = React.useContext(SchemaRendererContext);
457
+ const named = isElementDataSourceConfig(explicit) ? undefined : explicit;
458
+ return (named ?? context?.dataSource ?? undefined);
459
+ }
460
+ /**
461
+ * The author-facing sentence a block with no adapter says out loud.
462
+ *
463
+ * One wording for the whole object-bound family, because the reader's mistake
464
+ * is the same one every time and the fix is the same one every time: the
465
+ * adapter is injected by an ANCESTOR (`SchemaRendererProvider`), so nothing
466
+ * written on the block itself can supply it. Naming the block and the object
467
+ * it was about to read is what turns "nothing happened" into an address.
468
+ */
469
+ export function noDataSourceMessage(blockKey, objectName) {
470
+ const target = typeof objectName === 'string' && objectName.trim().length > 0
471
+ ? `“${objectName}”`
472
+ : 'its object';
473
+ return (`“${blockKey}” has no data source, so it cannot read ${target}. ` +
474
+ `Inject one from an ancestor — <SchemaRendererProvider dataSource={…}> from ` +
475
+ `@object-ui/react — or hand this block a dataSource of its own.`);
476
+ }
477
+ /**
478
+ * The "this block resolved no adapter" panel — objectui#5378 item 2, and the
479
+ * #5349 shape it belongs to.
480
+ *
481
+ * Until this existed, a block that resolved no adapter rendered an EMPTY SHELL:
482
+ * `object-grid` painted a header-only table, `object-form` a field-less card,
483
+ * `detail-view` nothing at all — no error, no warning, no empty state that
484
+ * explained itself. Measured on the published getting-started guide
485
+ * (`content/docs/guide/building-crud-app.md`), every one of its five blocks was
486
+ * in that state and the page reported success while fetching nothing.
487
+ *
488
+ * The empty shell is the defect, not a side effect of it: it is indistinguishable
489
+ * from "the query ran and matched no rows", which is the reading an author (very
490
+ * often an AI author) takes, and it points at the DATA when the fault is in the
491
+ * WIRING one level up. Same posture as {@link ElementDataSourceErrorPanel}: the
492
+ * metadata is answerable, so the answer is rendered where the author can see it.
493
+ */
494
+ export function NoDataSourcePanel({ testId, title = 'No data source resolved', message, }) {
495
+ return (_jsxs("div", { className: "p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2", role: "alert", "data-testid": `${testId}-no-data-source`, children: [_jsx("p", { className: "font-medium", children: title }), message ? _jsx("p", { className: "text-sm mt-1", children: message }) : null] }));
496
+ }
497
+ /**
498
+ * The wrapping seam — mark `renderer` as one that wraps {@link
499
+ * ElementDataSourceGate}, so its registrations DECLARE the `dataSource` key this
500
+ * gate READS (objectui#6678).
501
+ *
502
+ * ```tsx
503
+ * export const ObjectMapRenderer = elementDataSourceBlock<React.FC<any>>(({ schema }) => (
504
+ * <ElementDataSourceGate schema={schema} testId="object-map" …>
505
+ * {(bound) => <ObjectMap schema={bound} />}
506
+ * </ElementDataSourceGate>
507
+ * ));
508
+ * ```
509
+ *
510
+ * ## What this closes
511
+ *
512
+ * `PageComponentSchema.dataSource` is the one spelling that resolves a saved
513
+ * view for an object-bound block. It works — and because no registration
514
+ * declared it, `sdui-parser`'s `validateTree` reported it with the SAME
515
+ * `unknown-prop` warning it gives the spellings that do nothing. On the tier
516
+ * built to accept AI-authored pages the diagnostic IS the contract, so the
517
+ * tier's only signal pointed away from the one key that works.
518
+ *
519
+ * ## Why a seam and not nine declarations
520
+ *
521
+ * Maintainer ruling, 2026-08-29: option B **in the injection form**. Nine
522
+ * hand-written copies across nine packages is the shape that ruling refused —
523
+ * they drift, and the tenth block forgets. Passing through here is the only
524
+ * thing a block does; `Registry.register` emits the declaration
525
+ * (`withElementDataSourceInput`), and `ELEMENT_DATA_SOURCE_INPUT` in
526
+ * `@object-ui/core` is its single copy, beside the binding's own semantics.
527
+ *
528
+ * ## And it cannot be forgotten
529
+ *
530
+ * `scripts/check-element-data-source-declaration.mjs` fails any source that
531
+ * renders `ElementDataSourceGate` for a registration without passing that
532
+ * registration's renderer through this function. That is the mechanical half of
533
+ * "a tenth block gets it automatically"; this function is the seam it enforces.
534
+ *
535
+ * Returns the renderer UNCHANGED (the mark is held in a `WeakSet`), so it is safe
536
+ * over a `React.forwardRef` object, over `React.memo`, and over a component that
537
+ * something else already re-exports by reference.
538
+ *
539
+ * ⚠️ Re-exported from `@object-ui/core` — where the marker and the declaration
540
+ * live — rather than wrapped here: ONE function under ONE name. This export
541
+ * exists for DISCOVERABILITY, so the seam is findable beside the gate it is
542
+ * about; **call sites must import it from `@object-ui/core`**, and
543
+ * `check:element-data-source-declaration` enforces that.
544
+ *
545
+ * The rule is measured, not stylistic. A registration runs at MODULE SCOPE, 101
546
+ * suites in this repo partially mock `@object-ui/react` by hand-listing the
547
+ * exports they return, and a module-scope read of a name absent from such a list
548
+ * throws at COLLECTION time — the importing test file dies before running one
549
+ * assertion. Taking the seam from here reddened 17 files across all four CI
550
+ * shards with zero failed assertions among them. Nothing mocks `@object-ui/core`,
551
+ * and every registration module already imports `ComponentRegistry` from it.
552
+ */
553
+ export { elementDataSourceBlock } from '@object-ui/core';
177
554
  /**
178
555
  * Wrap an object-bound block so its `dataSource` binding reaches the keys it
179
556
  * reads, and so the two non-final resolution states render once, the same way,
@@ -183,8 +560,23 @@ export function ElementDataSourceLoadingPanel({ testId, }) {
183
560
  * against a half-resolved query is how a page shows a wider answer than the one
184
561
  * that was authored.
185
562
  */
186
- export function ElementDataSourceGate({ schema, mapping, dataSource, testId, errorTitle, children, }) {
563
+ export function ElementDataSourceGate({ schema, mapping, dataSource, testId, errorTitle, requiresDataSource, noDataSourceMessage: noDataSourceText, children, }) {
187
564
  const bound = useElementDataSourceSchema(schema, mapping, dataSource);
565
+ const adapter = useResolvedDataSource(dataSource);
566
+ // Reported BEFORE the view state, because with no adapter every downstream
567
+ // answer is a symptom of this one: `useElementDataSource` cannot list the
568
+ // object's saved views either, so a block carrying a `view` would otherwise
569
+ // report "this data source cannot list the saved views" — true, and pointing
570
+ // at the view name instead of at the missing injection.
571
+ if (requiresDataSource && adapter == null) {
572
+ return _jsx(NoDataSourcePanel, { testId: testId, message: noDataSourceText });
573
+ }
574
+ // A refused filter is `missing` too, but it gets the malformed-filter notice
575
+ // rather than the "could not be resolved" panel: the binding resolved, and
576
+ // what the author has to fix is a filter condition (objectui#10789).
577
+ if (bound.filterRefusal) {
578
+ return _jsx(ElementDataSourceMalformedFilterPanel, { testId: testId, refusal: bound.filterRefusal });
579
+ }
188
580
  if (bound.status === 'missing') {
189
581
  return _jsx(ElementDataSourceErrorPanel, { testId: testId, title: errorTitle, message: bound.error });
190
582
  }
@@ -5,38 +5,40 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- export * from './useExpression';
9
- export * from './useFilterScope';
10
- export * from './useActionRunner';
11
- export * from './useNavigationOverlay';
12
- export * from './usePageVariables';
13
- export * from './usePageVariableActionBridge';
14
- export * from './useViewData';
15
- export * from './useElementDataSource';
16
- export * from './useDynamicApp';
17
- export * from './useDiscovery';
18
- export * from './useFocusTrap';
19
- export * from './useFocusManagement';
20
- export * from './useKeyboardShortcuts';
21
- export * from './useCrudShortcuts';
22
- export * from './useReducedMotion';
23
- export * from './useAnimation';
24
- export * from './useDensityMode';
25
- export * from './useViewSharing';
26
- export * from './useClientNotifications';
27
- export * from './useOffline';
28
- export * from './usePerformance';
29
- export * from './usePerformanceBudget';
30
- export * from './usePageTransition';
31
- export * from './useViewTransition';
32
- export * from './useETagCache';
33
- export * from './useSchemaPersistence';
34
- export * from './useGlobalUndo';
35
- export * from './useDebugMode';
36
- export * from './useActionEngine';
37
- export * from './useActionTextLocalizer';
38
- export * from './useCapabilityGate';
39
- export * from './useDatasetDimensionLabels';
40
- export * from './useDataRefresh';
41
- export * from './usePageAssignment';
42
- export * from './useRecordSearch';
8
+ export * from './useExpression.js';
9
+ export * from './useFilterScope.js';
10
+ export * from './useResolvedFilter.js';
11
+ export * from './useActionRunner.js';
12
+ export * from './useNavigationOverlay.js';
13
+ export * from './usePageVariables.js';
14
+ export * from './usePageVariableActionBridge.js';
15
+ export * from './useViewData.js';
16
+ export * from './useElementDataSource.js';
17
+ export * from './useDynamicApp.js';
18
+ export * from './useDiscovery.js';
19
+ export * from './useFocusTrap.js';
20
+ export * from './useFocusManagement.js';
21
+ export * from './useKeyboardShortcuts.js';
22
+ export * from './useCrudShortcuts.js';
23
+ export * from './useReducedMotion.js';
24
+ export * from './useAnimation.js';
25
+ export * from './useDensityMode.js';
26
+ export * from './useViewSharing.js';
27
+ export * from './useOffline.js';
28
+ export * from './usePerformance.js';
29
+ export * from './usePerformanceBudget.js';
30
+ export * from './usePageTransition.js';
31
+ export * from './useViewTransition.js';
32
+ export * from './useETagCache.js';
33
+ export * from './useSchemaPersistence.js';
34
+ export * from './useGlobalUndo.js';
35
+ export * from './useDebugMode.js';
36
+ export * from './useActionEngine.js';
37
+ export * from './useActionTextLocalizer.js';
38
+ export * from './useCapabilityGate.js';
39
+ export * from './useConfigBagEvaluator.js';
40
+ export * from './useDatasetDimensionLabels.js';
41
+ export * from './useDataRefresh.js';
42
+ export * from './usePageAssignment.js';
43
+ export * from './useRecordSearch.js';
44
+ export * from './useSettledSchema.js';