@object-ui/react 17.6.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 +3766 -0
- package/README.md +294 -38
- package/dist/SchemaRenderer.d.ts +38 -7
- package/dist/SchemaRenderer.js +1218 -87
- 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/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 +5 -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/element-data-source/ElementDataSourceGate.d.ts +88 -1
- package/dist/element-data-source/ElementDataSourceGate.js +326 -13
- package/dist/hooks/index.d.ts +3 -1
- package/dist/hooks/index.js +10 -1
- package/dist/hooks/useActionEngine.js +46 -3
- package/dist/hooks/useActionRunner.js +4 -0
- package/dist/hooks/useActionTextLocalizer.d.ts +21 -8
- package/dist/hooks/useActionTextLocalizer.js +33 -8
- package/dist/hooks/useConfigBagEvaluator.d.ts +30 -0
- package/dist/hooks/useConfigBagEvaluator.js +39 -0
- 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/useETagCache.js +25 -4
- package/dist/hooks/useElementDataSource.d.ts +15 -2
- package/dist/hooks/useElementDataSource.js +24 -7
- package/dist/hooks/useExpression.d.ts +52 -59
- package/dist/hooks/useExpression.js +58 -90
- package/dist/hooks/useFilterScope.d.ts +22 -2
- package/dist/hooks/useFilterScope.js +29 -4
- package/dist/hooks/useGlobalUndo.d.ts +10 -0
- package/dist/hooks/useGlobalUndo.js +112 -46
- package/dist/hooks/useNavigationOverlay.d.ts +76 -11
- package/dist/hooks/useNavigationOverlay.js +119 -9
- package/dist/hooks/useOffline.js +65 -11
- package/dist/hooks/usePageAssignment.d.ts +8 -5
- package/dist/hooks/usePageAssignment.js +37 -16
- 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/index.d.ts +8 -1
- package/dist/index.js +45 -1
- package/dist/schema-input.d.ts +38 -8
- package/dist/schema-input.js +39 -7
- package/dist/utils/configBag.d.ts +68 -0
- package/dist/utils/configBag.js +70 -0
- package/dist/utils/error-message.d.ts +1 -1
- package/dist/utils/error-message.js +1 -1
- package/dist/utils/inlineAria.d.ts +61 -0
- package/dist/utils/inlineAria.js +70 -0
- package/dist/utils/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 +42 -1
- package/dist/utils/unevaluatedExpression.js +74 -11
- package/dist/utils/visibilityDiagnostic.d.ts +271 -19
- package/dist/utils/visibilityDiagnostic.js +311 -44
- package/package.json +10 -10
- package/dist/hooks/useClientNotifications.d.ts +0 -47
- package/dist/hooks/useClientNotifications.js +0 -162
- package/dist/spec-bridge/SpecBridge.d.ts +0 -28
- package/dist/spec-bridge/SpecBridge.js +0 -56
- package/dist/spec-bridge/bridges/form-view.d.ts +0 -158
- package/dist/spec-bridge/bridges/form-view.js +0 -170
- package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
- package/dist/spec-bridge/bridges/list-view.js +0 -268
- package/dist/spec-bridge/index.d.ts +0 -11
- package/dist/spec-bridge/index.js +0 -10
- package/dist/spec-bridge/types.d.ts +0 -55
- package/dist/spec-bridge/types.js +0 -8
|
@@ -50,6 +50,27 @@
|
|
|
50
50
|
* designer emits for an unconfigured column list, and supplying the columns is
|
|
51
51
|
* exactly why a view was named.
|
|
52
52
|
*
|
|
53
|
+
* The row cap answers "did the component author one?" the same way, and for the
|
|
54
|
+
* same reason (objectui#9899). A cap the contract REFUSES — `pageSize: 0`,
|
|
55
|
+
* a negative, a fraction — is not a cap this relay may treat as the author's
|
|
56
|
+
* intent, so it does not suppress the one the bound view supplies. Deciding
|
|
57
|
+
* that question by PRESENCE is how a view's legitimate cap was dropped in
|
|
58
|
+
* favour of the consuming renderer's own default: measured end to end on
|
|
59
|
+
* `list-view` before the repair, a component carrying `pagination.pageSize: 0`
|
|
60
|
+
* under a view supplying `7` put `$top: 100` on the wire — the view's cap never
|
|
61
|
+
* reached it, so the read was WIDER than the view asked for.
|
|
62
|
+
*
|
|
63
|
+
* The VIEW's own cap can be refused too, and `@object-ui/core` drops it before
|
|
64
|
+
* it reaches this relay (objectui#9928). The drop is right, but it left the
|
|
65
|
+
* consuming renderer nothing to refuse, so it went silent — and this relay
|
|
66
|
+
* reports the view's refusal instead (objectui#10015). See
|
|
67
|
+
* {@link describeRefusedViewRowLimit} for when that message applies.
|
|
68
|
+
*
|
|
69
|
+
* The BINDING's own cap follows the same rule (objectui#10016): a `limit` the
|
|
70
|
+
* contract refuses is not authored, so `@object-ui/core` lets the view's cap
|
|
71
|
+
* through in its place, and that cap is a baseline here like any other
|
|
72
|
+
* view-sourced value. See {@link describeRefusedBindingRowLimit}.
|
|
73
|
+
*
|
|
53
74
|
* ## What a mapping may NOT do
|
|
54
75
|
*
|
|
55
76
|
* {@link ElementDataSourceMapping} names only keys the target block genuinely
|
|
@@ -79,7 +100,7 @@
|
|
|
79
100
|
* like it works.
|
|
80
101
|
*/
|
|
81
102
|
import * as React from 'react';
|
|
82
|
-
import { type ElementDataSourceConfig } from '@object-ui/core';
|
|
103
|
+
import { type ElementDataSourceConfig, type FilterOperatorError } from '@object-ui/core';
|
|
83
104
|
import { type ElementDataSourceStatus } from '../hooks/useElementDataSource.js';
|
|
84
105
|
/**
|
|
85
106
|
* Where a block's row cap lives. Three spellings are real in this repo and each
|
|
@@ -139,6 +160,15 @@ export interface UseElementDataSourceSchemaResult<S> {
|
|
|
139
160
|
config?: ElementDataSourceConfig;
|
|
140
161
|
/** Author-facing explanation, set only for `missing`. */
|
|
141
162
|
error?: string;
|
|
163
|
+
/**
|
|
164
|
+
* The refused filter that stopped the binding from applying — set only with
|
|
165
|
+
* `missing`, and `error` then carries its message (objectui#10789). Either
|
|
166
|
+
* merge can refuse: the view's filter with the binding's own
|
|
167
|
+
* ({@link useElementDataSource}), or the component's filter with that result
|
|
168
|
+
* (this hook). {@link ElementDataSourceGate} draws the malformed-filter
|
|
169
|
+
* notice from it.
|
|
170
|
+
*/
|
|
171
|
+
filterRefusal?: FilterOperatorError;
|
|
142
172
|
}
|
|
143
173
|
/**
|
|
144
174
|
* Resolve a block's `dataSource` binding and apply it to the block's own schema
|
|
@@ -225,6 +255,63 @@ export declare function noDataSourceMessage(blockKey: string, objectName?: unkno
|
|
|
225
255
|
* metadata is answerable, so the answer is rendered where the author can see it.
|
|
226
256
|
*/
|
|
227
257
|
export declare function NoDataSourcePanel({ testId, title, message, }: ElementDataSourceStatusPanelProps): React.ReactElement;
|
|
258
|
+
/**
|
|
259
|
+
* The wrapping seam — mark `renderer` as one that wraps {@link
|
|
260
|
+
* ElementDataSourceGate}, so its registrations DECLARE the `dataSource` key this
|
|
261
|
+
* gate READS (objectui#6678).
|
|
262
|
+
*
|
|
263
|
+
* ```tsx
|
|
264
|
+
* export const ObjectMapRenderer = elementDataSourceBlock<React.FC<any>>(({ schema }) => (
|
|
265
|
+
* <ElementDataSourceGate schema={schema} testId="object-map" …>
|
|
266
|
+
* {(bound) => <ObjectMap schema={bound} />}
|
|
267
|
+
* </ElementDataSourceGate>
|
|
268
|
+
* ));
|
|
269
|
+
* ```
|
|
270
|
+
*
|
|
271
|
+
* ## What this closes
|
|
272
|
+
*
|
|
273
|
+
* `PageComponentSchema.dataSource` is the one spelling that resolves a saved
|
|
274
|
+
* view for an object-bound block. It works — and because no registration
|
|
275
|
+
* declared it, `sdui-parser`'s `validateTree` reported it with the SAME
|
|
276
|
+
* `unknown-prop` warning it gives the spellings that do nothing. On the tier
|
|
277
|
+
* built to accept AI-authored pages the diagnostic IS the contract, so the
|
|
278
|
+
* tier's only signal pointed away from the one key that works.
|
|
279
|
+
*
|
|
280
|
+
* ## Why a seam and not nine declarations
|
|
281
|
+
*
|
|
282
|
+
* Maintainer ruling, 2026-08-29: option B **in the injection form**. Nine
|
|
283
|
+
* hand-written copies across nine packages is the shape that ruling refused —
|
|
284
|
+
* they drift, and the tenth block forgets. Passing through here is the only
|
|
285
|
+
* thing a block does; `Registry.register` emits the declaration
|
|
286
|
+
* (`withElementDataSourceInput`), and `ELEMENT_DATA_SOURCE_INPUT` in
|
|
287
|
+
* `@object-ui/core` is its single copy, beside the binding's own semantics.
|
|
288
|
+
*
|
|
289
|
+
* ## And it cannot be forgotten
|
|
290
|
+
*
|
|
291
|
+
* `scripts/check-element-data-source-declaration.mjs` fails any source that
|
|
292
|
+
* renders `ElementDataSourceGate` for a registration without passing that
|
|
293
|
+
* registration's renderer through this function. That is the mechanical half of
|
|
294
|
+
* "a tenth block gets it automatically"; this function is the seam it enforces.
|
|
295
|
+
*
|
|
296
|
+
* Returns the renderer UNCHANGED (the mark is held in a `WeakSet`), so it is safe
|
|
297
|
+
* over a `React.forwardRef` object, over `React.memo`, and over a component that
|
|
298
|
+
* something else already re-exports by reference.
|
|
299
|
+
*
|
|
300
|
+
* ⚠️ Re-exported from `@object-ui/core` — where the marker and the declaration
|
|
301
|
+
* live — rather than wrapped here: ONE function under ONE name. This export
|
|
302
|
+
* exists for DISCOVERABILITY, so the seam is findable beside the gate it is
|
|
303
|
+
* about; **call sites must import it from `@object-ui/core`**, and
|
|
304
|
+
* `check:element-data-source-declaration` enforces that.
|
|
305
|
+
*
|
|
306
|
+
* The rule is measured, not stylistic. A registration runs at MODULE SCOPE, 101
|
|
307
|
+
* suites in this repo partially mock `@object-ui/react` by hand-listing the
|
|
308
|
+
* exports they return, and a module-scope read of a name absent from such a list
|
|
309
|
+
* throws at COLLECTION time — the importing test file dies before running one
|
|
310
|
+
* assertion. Taking the seam from here reddened 17 files across all four CI
|
|
311
|
+
* shards with zero failed assertions among them. Nothing mocks `@object-ui/core`,
|
|
312
|
+
* and every registration module already imports `ComponentRegistry` from it.
|
|
313
|
+
*/
|
|
314
|
+
export { elementDataSourceBlock } from '@object-ui/core';
|
|
228
315
|
export interface ElementDataSourceGateProps<S> {
|
|
229
316
|
/** The block's schema node. */
|
|
230
317
|
schema: S;
|
|
@@ -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
|
|
@@ -80,9 +101,135 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
80
101
|
* like it works.
|
|
81
102
|
*/
|
|
82
103
|
import * as React from 'react';
|
|
83
|
-
import { isElementDataSourceConfig, mergeFilterNodes, } from '@object-ui/core';
|
|
104
|
+
import { elementDataSourceRefusedLimitMessage, filterRefusalSubject, isElementDataSourceConfig, mergeFilterNodes, toFilterNodeSafely, } from '@object-ui/core';
|
|
105
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
84
106
|
import { useElementDataSource, } from '../hooks/useElementDataSource.js';
|
|
85
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
|
+
};
|
|
86
233
|
const readLimit = (base, key) => {
|
|
87
234
|
if (key === 'pagination.pageSize')
|
|
88
235
|
return base.pagination?.pageSize;
|
|
@@ -122,10 +269,16 @@ export function useElementDataSourceSchema(schema, mapping = {}, dataSource) {
|
|
|
122
269
|
const { object: objectKey = 'objectName', columns, filter, sort, limit, viewType } = mapping;
|
|
123
270
|
const mapped = React.useMemo(() => {
|
|
124
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.
|
|
125
275
|
if (!composed)
|
|
126
|
-
return schema;
|
|
276
|
+
return { schema, capMessage: null, viewCapMessage: null, bindingCapMessage: null };
|
|
127
277
|
const base = (schema ?? {});
|
|
128
278
|
const next = { ...base };
|
|
279
|
+
let capMessage = null;
|
|
280
|
+
let viewCapMessage = null;
|
|
281
|
+
let bindingCapMessage = null;
|
|
129
282
|
if (objectKey !== false)
|
|
130
283
|
next[objectKey] = composed.object;
|
|
131
284
|
if (columns) {
|
|
@@ -139,7 +292,23 @@ export function useElementDataSourceSchema(schema, mapping = {}, dataSource) {
|
|
|
139
292
|
// Component filter AND (view filter AND binding filter). `composed.filter`
|
|
140
293
|
// already carries the latter pair; a single surviving source comes back
|
|
141
294
|
// unwrapped, so the common "only the view filters" case stays flat.
|
|
142
|
-
|
|
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);
|
|
143
312
|
if (merged !== undefined)
|
|
144
313
|
next.filter = merged;
|
|
145
314
|
else
|
|
@@ -153,22 +322,65 @@ export function useElementDataSourceSchema(schema, mapping = {}, dataSource) {
|
|
|
153
322
|
next.sort = composed.sort;
|
|
154
323
|
}
|
|
155
324
|
if (limit && composed.limit !== undefined) {
|
|
156
|
-
|
|
157
|
-
|
|
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)) {
|
|
158
334
|
writeLimit(next, base, limit, composed.limit);
|
|
335
|
+
capMessage = describeDisplacedRowLimit(authored, limit, composed.limit, base.type, composed.object);
|
|
159
336
|
}
|
|
160
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
|
+
}
|
|
161
345
|
if (viewType && composed.viewType !== undefined && base.viewType === undefined) {
|
|
162
346
|
next.viewType = composed.viewType;
|
|
163
347
|
}
|
|
164
|
-
return next;
|
|
165
|
-
}, [schema, binding.composed, binding.config, objectKey, columns, filter, sort, limit, viewType]);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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]);
|
|
172
384
|
}
|
|
173
385
|
/**
|
|
174
386
|
* The "named view does not resolve" panel. Same posture (and shape) as
|
|
@@ -178,6 +390,41 @@ export function useElementDataSourceSchema(schema, mapping = {}, dataSource) {
|
|
|
178
390
|
export function ElementDataSourceErrorPanel({ testId, title = 'This component’s data source could not be resolved', message, }) {
|
|
179
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] }));
|
|
180
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
|
+
}
|
|
181
428
|
/**
|
|
182
429
|
* The "saved views are still being fetched" placeholder. Distinct from the error
|
|
183
430
|
* panel because a component that treated "not resolved yet" as "does not exist"
|
|
@@ -203,6 +450,9 @@ export function ElementDataSourceLoadingPanel({ testId, }) {
|
|
|
203
450
|
* it off and you get `unknown`, which is the honest default.
|
|
204
451
|
*/
|
|
205
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`.
|
|
206
456
|
const context = React.useContext(SchemaRendererContext);
|
|
207
457
|
const named = isElementDataSourceConfig(explicit) ? undefined : explicit;
|
|
208
458
|
return (named ?? context?.dataSource ?? undefined);
|
|
@@ -244,6 +494,63 @@ export function noDataSourceMessage(blockKey, objectName) {
|
|
|
244
494
|
export function NoDataSourcePanel({ testId, title = 'No data source resolved', message, }) {
|
|
245
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] }));
|
|
246
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';
|
|
247
554
|
/**
|
|
248
555
|
* Wrap an object-bound block so its `dataSource` binding reaches the keys it
|
|
249
556
|
* reads, and so the two non-final resolution states render once, the same way,
|
|
@@ -264,6 +571,12 @@ export function ElementDataSourceGate({ schema, mapping, dataSource, testId, err
|
|
|
264
571
|
if (requiresDataSource && adapter == null) {
|
|
265
572
|
return _jsx(NoDataSourcePanel, { testId: testId, message: noDataSourceText });
|
|
266
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
|
+
}
|
|
267
580
|
if (bound.status === 'missing') {
|
|
268
581
|
return _jsx(ElementDataSourceErrorPanel, { testId: testId, title: errorTitle, message: bound.error });
|
|
269
582
|
}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export * from './useExpression.js';
|
|
9
9
|
export * from './useFilterScope.js';
|
|
10
|
+
export * from './useResolvedFilter.js';
|
|
10
11
|
export * from './useActionRunner.js';
|
|
11
12
|
export * from './useNavigationOverlay.js';
|
|
12
13
|
export * from './usePageVariables.js';
|
|
@@ -23,7 +24,6 @@ export * from './useReducedMotion.js';
|
|
|
23
24
|
export * from './useAnimation.js';
|
|
24
25
|
export * from './useDensityMode.js';
|
|
25
26
|
export * from './useViewSharing.js';
|
|
26
|
-
export * from './useClientNotifications.js';
|
|
27
27
|
export * from './useOffline.js';
|
|
28
28
|
export * from './usePerformance.js';
|
|
29
29
|
export * from './usePerformanceBudget.js';
|
|
@@ -36,7 +36,9 @@ export * from './useDebugMode.js';
|
|
|
36
36
|
export * from './useActionEngine.js';
|
|
37
37
|
export * from './useActionTextLocalizer.js';
|
|
38
38
|
export * from './useCapabilityGate.js';
|
|
39
|
+
export * from './useConfigBagEvaluator.js';
|
|
39
40
|
export * from './useDatasetDimensionLabels.js';
|
|
40
41
|
export * from './useDataRefresh.js';
|
|
41
42
|
export * from './usePageAssignment.js';
|
|
42
43
|
export * from './useRecordSearch.js';
|
|
44
|
+
export * from './useSettledSchema.js';
|
package/dist/hooks/index.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
export * from './useExpression.js';
|
|
9
9
|
// Session scope for filter placeholders ({current_user_id}/{current_org_id}).
|
|
10
10
|
export * from './useFilterScope.js';
|
|
11
|
+
// The ONE hold a data node resolves its own authored `filter` through
|
|
12
|
+
// (objectui#10666), fed by `useFilterScope()`.
|
|
13
|
+
export * from './useResolvedFilter.js';
|
|
11
14
|
export * from './useActionRunner.js';
|
|
12
15
|
export * from './useNavigationOverlay.js';
|
|
13
16
|
export * from './usePageVariables.js';
|
|
@@ -25,7 +28,6 @@ export * from './useReducedMotion.js';
|
|
|
25
28
|
export * from './useAnimation.js';
|
|
26
29
|
export * from './useDensityMode.js';
|
|
27
30
|
export * from './useViewSharing.js';
|
|
28
|
-
export * from './useClientNotifications.js';
|
|
29
31
|
export * from './useOffline.js';
|
|
30
32
|
export * from './usePerformance.js';
|
|
31
33
|
export * from './usePerformanceBudget.js';
|
|
@@ -40,6 +42,9 @@ export * from './useActionEngine.js';
|
|
|
40
42
|
// authored strings an action carries (label / confirmText / successMessage).
|
|
41
43
|
export * from './useActionTextLocalizer.js';
|
|
42
44
|
export * from './useCapabilityGate.js';
|
|
45
|
+
// The `SchemaRenderer` memo's `properties` evaluation, for an action container's
|
|
46
|
+
// members, which are drawn without `SchemaRenderer` (objectui#10290).
|
|
47
|
+
export * from './useConfigBagEvaluator.js';
|
|
43
48
|
// The analytics label net's React glue, consumed by BOTH plugin-dashboard's
|
|
44
49
|
// `DatasetWidget` and plugin-report's dataset block (objectui#4389). It lives
|
|
45
50
|
// here rather than in `@object-ui/core` because it reads `SchemaRendererContext`
|
|
@@ -48,3 +53,7 @@ export * from './useDatasetDimensionLabels.js';
|
|
|
48
53
|
export * from './useDataRefresh.js';
|
|
49
54
|
export * from './usePageAssignment.js';
|
|
50
55
|
export * from './useRecordSearch.js';
|
|
56
|
+
// The settled-schema RESOLUTION half shared across ObjectKanban / ObjectView /
|
|
57
|
+
// ObjectCalendar's hand copies (objectui#6482, maintainer ruling Option A).
|
|
58
|
+
// Gate PLACEMENT stays per-component — see the hook's own doc comment.
|
|
59
|
+
export * from './useSettledSchema.js';
|