@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
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -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 './
|
|
11
|
-
export * from './
|
|
12
|
-
export * from './
|
|
13
|
-
export * from './
|
|
14
|
-
export * from './
|
|
15
|
-
export * from './
|
|
16
|
-
export * from './
|
|
17
|
-
export * from './
|
|
18
|
-
export * from './
|
|
19
|
-
export * from './
|
|
20
|
-
export * from './
|
|
21
|
-
export * from './
|
|
22
|
-
export * from './
|
|
23
|
-
export * from './
|
|
24
|
-
export * from './
|
|
25
|
-
export * from './
|
|
26
|
-
export * from './
|
|
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 './
|
|
40
|
-
export * from './
|
|
41
|
-
export * from './
|
|
42
|
-
export * from './
|
|
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';
|