@object-ui/react 17.6.0 → 18.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4263 -0
- package/README.md +383 -42
- package/dist/SchemaRenderer.d.ts +64 -7
- package/dist/SchemaRenderer.js +1326 -106
- package/dist/context/ActionContext.js +40 -1
- package/dist/context/AppShellContext.d.ts +27 -0
- package/dist/context/DiscussionContext.d.ts +27 -0
- package/dist/context/DiscussionContext.js +4 -0
- package/dist/context/DrillNavigationContext.d.ts +13 -0
- package/dist/context/InlineEditContext.d.ts +151 -1
- package/dist/context/InlineEditContext.js +36 -2
- package/dist/context/PrintPageContext.d.ts +60 -0
- package/dist/context/PrintPageContext.js +32 -0
- package/dist/context/RecordContext.d.ts +48 -5
- package/dist/context/RecordContext.js +10 -1
- package/dist/context/RelatedRecordActionsContext.d.ts +72 -2
- package/dist/context/RelatedRecordActionsContext.js +4 -0
- package/dist/context/SchemaRendererContext.d.ts +61 -6
- package/dist/context/SchemaRendererContext.js +39 -6
- 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 +1 -0
- package/dist/context/index.js +1 -0
- package/dist/element-data-source/ElementApiDataSourcePanel.d.ts +28 -0
- package/dist/element-data-source/ElementApiDataSourcePanel.js +10 -0
- package/dist/element-data-source/ElementDataSourceGate.d.ts +118 -3
- package/dist/element-data-source/ElementDataSourceGate.js +401 -19
- package/dist/element-data-source/useElementApiDataSource.d.ts +119 -0
- package/dist/element-data-source/useElementApiDataSource.js +198 -0
- 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 +35 -9
- package/dist/hooks/useActionTextLocalizer.js +70 -14
- package/dist/hooks/useConfigBagEvaluator.d.ts +31 -0
- package/dist/hooks/useConfigBagEvaluator.js +40 -0
- package/dist/hooks/useDatasetDimensionLabels.js +25 -10
- package/dist/hooks/useDensityMode.d.ts +8 -2
- package/dist/hooks/useDensityMode.js +8 -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 +33 -7
- package/dist/hooks/usePageAssignment.js +85 -21
- 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 +10 -3
- package/dist/hooks/useViewData.js +5 -14
- package/dist/index.d.ts +9 -1
- package/dist/index.js +48 -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/localeMap.d.ts +27 -0
- package/dist/utils/localeMap.js +106 -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 +422 -0
- package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
- package/dist/utils/refusedDataPropDiagnostic.js +94 -0
- package/dist/utils/unevaluatedExpression.d.ts +44 -1
- package/dist/utils/unevaluatedExpression.js +92 -11
- package/dist/utils/visibilityDiagnostic.d.ts +282 -21
- package/dist/utils/visibilityDiagnostic.js +321 -46
- 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
package/dist/SchemaRenderer.js
CHANGED
|
@@ -7,13 +7,22 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
7
7
|
* LICENSE file in the root directory of this source tree.
|
|
8
8
|
*/
|
|
9
9
|
import React, { forwardRef, useContext, useMemo, useEffect, useReducer, useState, Component } from 'react';
|
|
10
|
-
import { ComponentRegistry, ExpressionEvaluator, isObjectUIError, ERROR_CODES, debugLog, debugTime, debugTimeEnd, DebugCollector, validateSchema, hasDeclaredPredicate, hasResponsiveStyles, scopeClassFor, compileScopedStyles, } from '@object-ui/core';
|
|
10
|
+
import { ComponentRegistry, ExpressionEvaluator, isObjectUIError, ERROR_CODES, debugLog, debugTime, debugTimeEnd, DebugCollector, validateSchema, hasDeclaredPredicate, hasResponsiveStyles, scopeClassFor, compileScopedStyles, recordSourceDataArmForType, } from '@object-ui/core';
|
|
11
11
|
import { SchemaRendererContext } from './context/SchemaRendererContext.js';
|
|
12
12
|
import { useRecordContext } from './context/RecordContext.js';
|
|
13
|
-
import { usePredicateScope } from './hooks/useExpression.js';
|
|
13
|
+
import { usePredicateScope, PredicateScopeProvider } from './hooks/useExpression.js';
|
|
14
|
+
import { useElementApiDataSource } from './element-data-source/useElementApiDataSource.js';
|
|
15
|
+
import { ElementDataSourceLoadingPanel } from './element-data-source/ElementDataSourceGate.js';
|
|
16
|
+
import { ElementApiDataSourcePanel } from './element-data-source/ElementApiDataSourcePanel.js';
|
|
14
17
|
import { usePageVariables } from './hooks/usePageVariables.js';
|
|
15
18
|
import { resolveKeyedI18nLabel } from './utils/i18n.js';
|
|
19
|
+
import { isConfigBag } from './utils/configBag.js';
|
|
20
|
+
import { PARAMS_KEY, isParamsBag, mapParamsLeaves } from './utils/paramsBag.js';
|
|
21
|
+
import { isInlineLocaleMap, mapLocaleMapMembers } from './utils/localeMap.js';
|
|
16
22
|
import { reportUnevaluatedExpressions } from './utils/unevaluatedExpression.js';
|
|
23
|
+
import { reportDroppedPropsBag, reportRefusedPropsPredicate } from './utils/propsBagDiagnostic.js';
|
|
24
|
+
import { reportRefusedDataPropSpread } from './utils/refusedDataPropDiagnostic.js';
|
|
25
|
+
import { expressionBindableTextKeysFor } from '@objectstack/spec/ui';
|
|
17
26
|
import { reportUnresolvableVisibilityPredicate, reportAdapterOnlyDataPredicate, } from './utils/visibilityDiagnostic.js';
|
|
18
27
|
/**
|
|
19
28
|
* Dev-mode schema validation.
|
|
@@ -41,6 +50,7 @@ const _validationCache = typeof WeakMap !== 'undefined'
|
|
|
41
50
|
: { set() { }, get() { return undefined; }, has() { return false; } };
|
|
42
51
|
const _warnedSchemas = typeof WeakSet !== 'undefined' ? new WeakSet() : { add() { }, has() { return false; } };
|
|
43
52
|
function validateSchemaOnce(schema) {
|
|
53
|
+
// Render is not a validation door: a dev-only STRUCTURAL check, not the contract — the doors are named in "Render is not a validation door", content/docs/guide/schema-rendering.md.
|
|
44
54
|
if (!__DEV__ || !schema || typeof schema !== 'object') {
|
|
45
55
|
return { valid: true, messages: [] };
|
|
46
56
|
}
|
|
@@ -81,6 +91,18 @@ function validateSchemaOnce(schema) {
|
|
|
81
91
|
* ariaLabel: string | I18nLabel (→ aria-label)
|
|
82
92
|
* ariaDescribedBy: string (→ aria-describedby)
|
|
83
93
|
* role: string (→ role)
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ This reads the FLAT node keys, and resolves `ariaLabel` in objectui's
|
|
96
|
+
* KEYED vocabulary (`resolveKeyedI18nLabel`), which returns `undefined` for an
|
|
97
|
+
* inline locale map. It does not read the NESTED `aria` bag a block's props
|
|
98
|
+
* carry. That bag is read by `resolveInlineAriaProps` (`utils/inlineAria.ts`,
|
|
99
|
+
* objectui#11051), and objectui#4580 Q2-B keeps the two readers separate.
|
|
100
|
+
*
|
|
101
|
+
* It takes the interpreter's working copy (objectui#11349). That copy used to
|
|
102
|
+
* arrive through a `Record<string, any>` parameter, which typed these reads
|
|
103
|
+
* `any`. `ariaLabel` is a `BaseSchema` member. `ariaDescribedBy` and `role`
|
|
104
|
+
* are not, so they read as `unknown` and are forwarded exactly as authored,
|
|
105
|
+
* the same bytes as before. The record is typed to say so.
|
|
84
106
|
*/
|
|
85
107
|
function resolveAriaProps(schema) {
|
|
86
108
|
const aria = {};
|
|
@@ -144,9 +166,13 @@ const HOIST_PROTECTED_KEYS = new Set(['type', 'id']);
|
|
|
144
166
|
* - `type`/`id` are skipped, because the hoist never copied a canonical value
|
|
145
167
|
* up for them, so dropping the alias would DELETE the prop rather than
|
|
146
168
|
* replace it;
|
|
147
|
-
* - a degenerate (non-object) `properties` is left alone —
|
|
148
|
-
*
|
|
149
|
-
* to
|
|
169
|
+
* - a degenerate (non-object) `properties` is left alone — there is no
|
|
170
|
+
* canonical bag to prefer, because a degenerate bag declares no key for
|
|
171
|
+
* either spelling to win. (This bullet used to explain the carve-out by
|
|
172
|
+
* saying "the hoist and `readProps()` both merely object-spread it". Half
|
|
173
|
+
* of that stopped being true at objectui#6760, which stopped the hoist from
|
|
174
|
+
* enumerating a degenerate bag; the carve-out itself is unaffected, since
|
|
175
|
+
* it never rested on what the hoist does with the value.)
|
|
150
176
|
*
|
|
151
177
|
* Adjacent but NOT decided here: objectui#4795's pending question ② (whether
|
|
152
178
|
* the `properties` envelope is an official `ui:*` authoring channel at all).
|
|
@@ -154,15 +180,22 @@ const HOIST_PROTECTED_KEYS = new Set(['type', 'id']);
|
|
|
154
180
|
* position on whether either should exist.
|
|
155
181
|
*/
|
|
156
182
|
function propsWithoutCanonicalKeys(propsBag, propertiesBag) {
|
|
157
|
-
|
|
183
|
+
// A degenerate `props` contributes NO keys (objectui#6752). This used to be a
|
|
184
|
+
// bare `if (!propsBag)`, so a non-object bag was returned as-is and the
|
|
185
|
+
// `createElement` spread below re-enumerated it: measured on `b76ca6764`,
|
|
186
|
+
// `props: 'not-a-bag'` reached the element as nine React props named `0` … `8`.
|
|
187
|
+
// Returning `{}` also makes the declared return type true — the old signature
|
|
188
|
+
// said `Record<string, any>` while this line could hand back a string.
|
|
189
|
+
//
|
|
190
|
+
// This does NOT move objectui#5123's precedence. That rule only decides which
|
|
191
|
+
// of two co-present values a key carries, and a degenerate bag declares no
|
|
192
|
+
// key for either bag to win: the indices were never authored, they are the
|
|
193
|
+
// object spread's reading of a string.
|
|
194
|
+
if (!isConfigBag(propsBag))
|
|
158
195
|
return {};
|
|
159
|
-
// Only a real object bag can win a key
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
// its shape reinterpreted here.
|
|
163
|
-
if (!propertiesBag ||
|
|
164
|
-
typeof propertiesBag !== 'object' ||
|
|
165
|
-
Array.isArray(propertiesBag)) {
|
|
196
|
+
// Only a real object bag can win a key — the same predicate, for the reason
|
|
197
|
+
// stated there: a degenerate bag must not have its shape reinterpreted here.
|
|
198
|
+
if (!isConfigBag(propertiesBag)) {
|
|
166
199
|
return propsBag;
|
|
167
200
|
}
|
|
168
201
|
let narrowed = null;
|
|
@@ -192,8 +225,394 @@ function propsWithoutCanonicalKeys(propsBag, propertiesBag) {
|
|
|
192
225
|
* means *hidden* and the wider `!== undefined` test would vanish a node for
|
|
193
226
|
* `hidden: ''` (objectui#3955). See {@link winningVisibilityKey}.
|
|
194
227
|
*/
|
|
195
|
-
const VISIBILITY_SHOW_KEYS = ['visibleWhen', 'visible', 'visibleOn'
|
|
228
|
+
const VISIBILITY_SHOW_KEYS = ['visibleWhen', 'visible', 'visibleOn'];
|
|
196
229
|
const VISIBILITY_HIDE_KEYS = ['hidden', 'hiddenOn'];
|
|
230
|
+
/**
|
|
231
|
+
* The same five legs as a lookup, so the config-bag evaluation loops below can
|
|
232
|
+
* ask "is this key a visibility predicate?" off the SAME declaration the chain
|
|
233
|
+
* is built from rather than a second list that can drift from it.
|
|
234
|
+
*
|
|
235
|
+
* Since objectui#9107 those loops ask {@link PREDICATE_CHAIN_KEYS} — this set
|
|
236
|
+
* unioned with the enablement chain's — so this one is the VISIBILITY HALF of
|
|
237
|
+
* that union rather than the whole of it. Each chain still has exactly one
|
|
238
|
+
* declaration, which is the property this constant was added for.
|
|
239
|
+
*/
|
|
240
|
+
const VISIBILITY_CHAIN_KEYS = new Set([
|
|
241
|
+
...VISIBILITY_SHOW_KEYS,
|
|
242
|
+
...VISIBILITY_HIDE_KEYS,
|
|
243
|
+
]);
|
|
244
|
+
/**
|
|
245
|
+
* The ENABLEMENT-chain keys — the SECOND predicate chain this file carries, and
|
|
246
|
+
* deliberately not a seventh leg of the visibility chain above (objectui#9107).
|
|
247
|
+
*
|
|
248
|
+
* Split the way this file actually consults them, which is not a polarity split:
|
|
249
|
+
*
|
|
250
|
+
* * {@link ENABLEMENT_NODE_GATE_KEYS} are the two legs
|
|
251
|
+
* {@link evaluateEnablementPredicate} consults HERE, in this precedence
|
|
252
|
+
* order, as one early-return chain — the enablement counterpart of
|
|
253
|
+
* `shouldHide`. Their `true` means DISABLED, un-negated.
|
|
254
|
+
* * {@link ENABLEMENT_RENDERER_KEYS} is the legacy, non-spec `enabled` alias.
|
|
255
|
+
* This component never evaluates it: the node gate does not consult it at
|
|
256
|
+
* all, and the action renderers (`action:button`, `action:icon`) read it one
|
|
257
|
+
* layer down off the schema and NEGATE it (`disabled = !isEnabled`). It is
|
|
258
|
+
* declared here because the config-bag loops below flatten it exactly like
|
|
259
|
+
* the other two — which is the whole defect — not because anything in this
|
|
260
|
+
* file decides with it.
|
|
261
|
+
*
|
|
262
|
+
* ## Why a second declaration instead of widening the visibility chain
|
|
263
|
+
*
|
|
264
|
+
* {@link VisibilityChainKey} exists "so a seventh cannot be added to the chain
|
|
265
|
+
* without also being classified below", and {@link visibilityGateKind} is that
|
|
266
|
+
* classification — it hands each leg the consequence copy for ITS polarity.
|
|
267
|
+
* These three keys have neither property: they route through
|
|
268
|
+
* {@link evaluateEnablementPredicate}, which states `'enablement'` at its own
|
|
269
|
+
* call site, and `enabled` is not consulted here at all. Adding them to
|
|
270
|
+
* {@link VISIBILITY_SHOW_KEYS} / {@link VISIBILITY_HIDE_KEYS} to reach the
|
|
271
|
+
* guard would make that closed type assert something false about them, and
|
|
272
|
+
* would silently enrol them in `shouldHide`'s early-return chain and in the
|
|
273
|
+
* visibility consequence copy as a side effect of a config-bag repair.
|
|
274
|
+
*
|
|
275
|
+
* So the two chains stay two declarations, and the GUARD takes their union
|
|
276
|
+
* ({@link PREDICATE_CHAIN_KEYS}) — one lookup, derived, declared nowhere twice.
|
|
277
|
+
* The alternative shape (one flat predicate-chain set that both chains derive
|
|
278
|
+
* FROM) was rejected for the opposite reason: the chains do not share an order,
|
|
279
|
+
* a declared-test or a consequence, so the thing they genuinely share is only
|
|
280
|
+
* the guard's question, and that is exactly what a derived union expresses.
|
|
281
|
+
*/
|
|
282
|
+
const ENABLEMENT_NODE_GATE_KEYS = ['disabled', 'disabledOn'];
|
|
283
|
+
const ENABLEMENT_RENDERER_KEYS = ['enabled'];
|
|
284
|
+
/**
|
|
285
|
+
* Every key either predicate chain owns, as ONE lookup for the config-bag
|
|
286
|
+
* evaluation loops. DERIVED from the four declarations above and declared
|
|
287
|
+
* nowhere else, so a leg added to either chain is covered by the guard in the
|
|
288
|
+
* same edit that adds it.
|
|
289
|
+
*/
|
|
290
|
+
const PREDICATE_CHAIN_KEYS = new Set([
|
|
291
|
+
...VISIBILITY_CHAIN_KEYS,
|
|
292
|
+
...ENABLEMENT_NODE_GATE_KEYS,
|
|
293
|
+
...ENABLEMENT_RENDERER_KEYS,
|
|
294
|
+
]);
|
|
295
|
+
/**
|
|
296
|
+
* Every key a NODE GATE in this file actually consults, as ONE lookup for the
|
|
297
|
+
* objectui#9108 refusal below. DERIVED from the same two declarations the gates
|
|
298
|
+
* are built from, so a leg added to either chain is refused under `props` by the
|
|
299
|
+
* same edit that adds it.
|
|
300
|
+
*
|
|
301
|
+
* {@link PREDICATE_CHAIN_KEYS} minus {@link ENABLEMENT_RENDERER_KEYS}, and the
|
|
302
|
+
* subtraction is the whole reason this is a second derivation rather than a
|
|
303
|
+
* reuse: `enabled` is in that union because the config-bag evaluation loops
|
|
304
|
+
* flatten it, but NO gate here consults it - the action renderers read it one
|
|
305
|
+
* layer down off the schema and negate it. Refusing it here would state, of a
|
|
306
|
+
* key this file never asks about, that a gate in this file could not see it.
|
|
307
|
+
* Its own `props` drop is objectui#6708's subject and is reported there.
|
|
308
|
+
*/
|
|
309
|
+
const NODE_GATE_PREDICATE_KEYS = new Set([
|
|
310
|
+
...VISIBILITY_CHAIN_KEYS,
|
|
311
|
+
...ENABLEMENT_NODE_GATE_KEYS,
|
|
312
|
+
]);
|
|
313
|
+
/**
|
|
314
|
+
* Is this value the canonical CEL envelope — `{ dialect: 'cel', source }` —
|
|
315
|
+
* that `@objectstack/spec` normalizes an authored predicate into?
|
|
316
|
+
*
|
|
317
|
+
* Deliberately narrower than "has a `source`": `cron` and `template` envelopes
|
|
318
|
+
* are NOT matched, so the `${…}` / template behaviour of the loops below is
|
|
319
|
+
* byte-for-byte what it was. Only the dialect that `evaluateCondition` routes
|
|
320
|
+
* to the canonical `@objectstack/formula` engine is held back from the
|
|
321
|
+
* flattening described in {@link preservePredicateEnvelope}.
|
|
322
|
+
*
|
|
323
|
+
* The object-shape half is {@link isConfigBag}, ASKED rather than re-spelled:
|
|
324
|
+
* objectui#6761 pins that predicate to one definition, and an envelope is the
|
|
325
|
+
* same question about the same kind of value (a real object, never an array).
|
|
326
|
+
* This is a third READ of that one answer, not a fourth spelling of it.
|
|
327
|
+
*/
|
|
328
|
+
const isCelEnvelope = (value) => isConfigBag(value)
|
|
329
|
+
&& value.dialect === 'cel'
|
|
330
|
+
&& typeof value.source === 'string';
|
|
331
|
+
/**
|
|
332
|
+
* Evaluate ONE value of a node's config bag (`properties` — the spec spelling
|
|
333
|
+
* — or its legacy `props` alias), EXCEPT a CEL predicate envelope, which is
|
|
334
|
+
* returned untouched.
|
|
335
|
+
*
|
|
336
|
+
* ## The defect this closes (objectui#9100)
|
|
337
|
+
*
|
|
338
|
+
* `ExpressionEvaluator.evaluate` unwraps ANY `{ source: string }` object to its
|
|
339
|
+
* bare `source` before it does anything else — that unwrap is what lets a
|
|
340
|
+
* `{ dialect: 'template', source: '${data.total}' }` value interpolate. Applied
|
|
341
|
+
* to a `{ dialect: 'cel' }` PREDICATE it is destructive instead: the envelope
|
|
342
|
+
* is the only thing that tells {@link ExpressionEvaluator.evaluateCondition}
|
|
343
|
+
* to route to the canonical `@objectstack/formula` engine, and the bag loops
|
|
344
|
+
* run BEFORE the hoist, so by the time either consumer sees the key the
|
|
345
|
+
* envelope is gone.
|
|
346
|
+
*
|
|
347
|
+
* Both consumers then read a bare string and both take the legacy JS path:
|
|
348
|
+
*
|
|
349
|
+
* 1. this component's own `shouldHide` chain, off the post-hoist node; and
|
|
350
|
+
* 2. the RENDERER's `toPredicateInput(…)` → `useCondition(…)` call, one
|
|
351
|
+
* layer down (`record:alert`, the four `action:*` blocks, …), which wraps
|
|
352
|
+
* the bare string as `${…}` exactly as it is documented to.
|
|
353
|
+
*
|
|
354
|
+
* On the legacy engine a CEL stdlib call is simply not a function, so
|
|
355
|
+
* `has(record.x) && …` throws — and the two polarities of the chain then fail
|
|
356
|
+
* in OPPOSITE directions, both silently:
|
|
357
|
+
*
|
|
358
|
+
* * on the three SHOW legs the fail-soft `true` is negated to "do not hide",
|
|
359
|
+
* so a gate authored to conceal renders on every row — measured in a real
|
|
360
|
+
* browser against a real app instance (the card), where a duplicate-lead
|
|
361
|
+
* banner showed on 6 of 6 leads whose field was `null`;
|
|
362
|
+
* * on the two HIDE legs the same `true` sets `_hidden`, so the node is not
|
|
363
|
+
* on screen at all.
|
|
364
|
+
*
|
|
365
|
+
* Neither is distinguishable from a predicate that said so on purpose. That is
|
|
366
|
+
* what makes this a fail-OPEN visibility gate rather than a rendering glitch.
|
|
367
|
+
*
|
|
368
|
+
* ## Why the fix is HERE and not in the normalizer
|
|
369
|
+
*
|
|
370
|
+
* `toPredicateInput` already preserves a `cel` envelope — that is its whole
|
|
371
|
+
* documented reason to exist (#2661 / #3314) — and the ACTION path that works
|
|
372
|
+
* calls the very same function. Measured: mounting the renderer DIRECTLY with
|
|
373
|
+
* the envelope gates correctly, and only the `SchemaRenderer` route fails, so
|
|
374
|
+
* the envelope dies upstream of every consumer, in the loops below. What makes
|
|
375
|
+
* the action path survive is that those loops are per-value and SHALLOW: an
|
|
376
|
+
* action's predicate sits one level down inside the `actions[]` ARRAY, and
|
|
377
|
+
* `evaluate` returns a non-string untouched, so nothing walks into it. A
|
|
378
|
+
* `record:alert` `properties.visible` is a TOP-LEVEL bag key and is hit
|
|
379
|
+
* head-on. Same normalizer, different depth.
|
|
380
|
+
*
|
|
381
|
+
* ## The enablement chain carries the identical defect (objectui#9107)
|
|
382
|
+
*
|
|
383
|
+
* `disabled` / `disabledOn` / `enabled` sit in the same two bags and are
|
|
384
|
+
* flattened by the same two loops, but they are NOT visibility legs — they
|
|
385
|
+
* route through {@link evaluateEnablementPredicate} and the action renderers'
|
|
386
|
+
* own `useCondition` call. Measured on the pre-fix tree through this component,
|
|
387
|
+
* with a CEL stdlib predicate authored at `properties` top level, and the two
|
|
388
|
+
* halves fail in OPPOSITE directions just as the visibility polarities do:
|
|
389
|
+
*
|
|
390
|
+
* * `disabled` / `disabledOn` — BOTH a holding and a failing predicate left
|
|
391
|
+
* the control DISABLED. The flattened source faults on the legacy engine
|
|
392
|
+
* and `evaluateCondition`'s fail-soft `true` is un-negated here, so every
|
|
393
|
+
* CEL-authored enablement gate greyed its control out on every row, with
|
|
394
|
+
* no predicate an author could write to re-enable it.
|
|
395
|
+
* * `enabled` — BOTH left the control ENABLED, because the renderers negate
|
|
396
|
+
* that leg (`disabled = !isEnabled`), so the same fail-soft `true` lands on
|
|
397
|
+
* "not disabled": a control the author disabled stayed pressable.
|
|
398
|
+
*
|
|
399
|
+
* ⇒ neither polarity alone detects this. A disabled-only suite is green on the
|
|
400
|
+
* broken `disabled` legs; an enabled-only suite is green on the broken
|
|
401
|
+
* `enabled` leg. Both are pinned, per key, in
|
|
402
|
+
* `__tests__/SchemaRenderer.enablementEnvelopeConfigBag.test.tsx`.
|
|
403
|
+
*
|
|
404
|
+
* ## Scope
|
|
405
|
+
*
|
|
406
|
+
* Restricted to {@link PREDICATE_CHAIN_KEYS} — the union of the two closed
|
|
407
|
+
* chain declarations this file already carries, which `shouldHide` /
|
|
408
|
+
* {@link winningVisibilityKey} and {@link evaluateEnablementPredicate} already
|
|
409
|
+
* consult. Every consumer of those nine keys takes the envelope by contract
|
|
410
|
+
* (`evaluateCondition`, `toPredicateInput`). A non-predicate key keeps the
|
|
411
|
+
* flattening it has always had.
|
|
412
|
+
*
|
|
413
|
+
* Eight of the nine are stripped by the metadata destructure near
|
|
414
|
+
* `createElement`, so no object value can reach the DOM through them. The
|
|
415
|
+
* ninth, the legacy `enabled` alias, is not stripped — the action renderers
|
|
416
|
+
* read it off the schema, not off React props. Measured on a renderer that
|
|
417
|
+
* spreads what it is handed onto a DOM node: that attribute already carried the
|
|
418
|
+
* raw CEL SOURCE TEXT before this change and carries `[object Object]` after
|
|
419
|
+
* it, which is the same inert shape the `properties` bag itself already puts on
|
|
420
|
+
* every such node, with no React warning in either direction. Both spellings
|
|
421
|
+
* are meaningless to the DOM; the leak is pre-existing and is deliberately NOT
|
|
422
|
+
* repaired here.
|
|
423
|
+
*/
|
|
424
|
+
const preservePredicateEnvelope = (key, value, evaluate) => (PREDICATE_CHAIN_KEYS.has(key) && isCelEnvelope(value) ? value : evaluate(value));
|
|
425
|
+
/**
|
|
426
|
+
* Is this value an Expression envelope in the spec's sense: an object carrying
|
|
427
|
+
* a string `dialect` AND a string `source`? (objectui#10288)
|
|
428
|
+
*
|
|
429
|
+
* `@objectstack/spec`'s `ExpressionSchema` declares `dialect` REQUIRED, so every
|
|
430
|
+
* envelope a spec-parsed artifact carries has one. The shape test is the one
|
|
431
|
+
* `@object-ui/core` already applies to a declared `defaultValue`
|
|
432
|
+
* (`isRuntimeDefault`, "Same shape test the engine applies before handing a
|
|
433
|
+
* default to `ExpressionEngine`"), where an object with a `source` and no
|
|
434
|
+
* `dialect` is a literal value, not an instruction. {@link isCelEnvelope} is
|
|
435
|
+
* this question narrowed to one dialect.
|
|
436
|
+
*/
|
|
437
|
+
const isExpressionEnvelope = (value) => isConfigBag(value)
|
|
438
|
+
&& typeof value.dialect === 'string'
|
|
439
|
+
&& typeof value.source === 'string';
|
|
440
|
+
/**
|
|
441
|
+
* Is this config-bag value a DATA OBJECT, which the per-value loops hand to the
|
|
442
|
+
* renderer exactly as authored instead of passing it to
|
|
443
|
+
* `ExpressionEvaluator.evaluate`? (objectui#10288)
|
|
444
|
+
*
|
|
445
|
+
* ## The defect this closes
|
|
446
|
+
*
|
|
447
|
+
* `evaluate` unwraps ANY object with a string `source` to that bare string
|
|
448
|
+
* before it does anything else (see {@link preservePredicateEnvelope}). The
|
|
449
|
+
* loops handed it every value, so an authored data object that happened to
|
|
450
|
+
* carry a `source` field was silently replaced by that one string. Measured
|
|
451
|
+
* through the real `SchemaRenderer` -> `action:button` -> runner:
|
|
452
|
+
* `properties.bodyExtra: { source: 'web', campaign: 'spring' }` reached the
|
|
453
|
+
* handler as `bodyExtra: "web"`, while the same object written at node level
|
|
454
|
+
* (which no loop visits) arrived intact. So the canonical channel was the
|
|
455
|
+
* broken one. That pair is pinned end to end in `@object-ui/components`
|
|
456
|
+
* (`action-config-bag-source-key-10288.test.tsx`), and this guard by
|
|
457
|
+
* `__tests__/SchemaRenderer.configBagDataObject-10288.test.tsx`.
|
|
458
|
+
* Every object-valued key a node carries in `properties` / `props`
|
|
459
|
+
* goes through the same loop, so the defect was never one key's: form
|
|
460
|
+
* `defaultValues`, a declarative `patch`, filter `values` and a detail view's
|
|
461
|
+
* `data` are all keyed by FIELD NAME, and a field named `source` is ordinary.
|
|
462
|
+
*
|
|
463
|
+
* ## What still reaches `evaluate`
|
|
464
|
+
*
|
|
465
|
+
* - every value that is not an object in {@link isConfigBag}'s sense (a
|
|
466
|
+
* string, any other primitive, an array), exactly as before: `evaluate`
|
|
467
|
+
* interpolates a string and returns the rest untouched;
|
|
468
|
+
* - a spec Expression envelope ({@link isExpressionEnvelope}), so a
|
|
469
|
+
* `{ dialect: 'template', source: '${…}' }` value interpolates as it always
|
|
470
|
+
* did;
|
|
471
|
+
* - every value of a predicate-chain key ({@link PREDICATE_CHAIN_KEYS}),
|
|
472
|
+
* unchanged. `BaseSchema.visible` / `.hidden` / `.disabled` declare the
|
|
473
|
+
* dialect-less `{ source }` envelope as an authorable wire form
|
|
474
|
+
* (`ExpressionWire`, objectui#7530), so on those keys it stays an
|
|
475
|
+
* expression, and {@link preservePredicateEnvelope} still holds back a CEL
|
|
476
|
+
* envelope (objectui#9100 / #9107).
|
|
477
|
+
*
|
|
478
|
+
* So the only value whose meaning changes is an object on a non-predicate key
|
|
479
|
+
* with a string `source` and no string `dialect`, and that object now arrives
|
|
480
|
+
* whole. Nothing is walked: a template nested inside a data object stays raw,
|
|
481
|
+
* as it always did for an object without a `source`. The loops stay per-value
|
|
482
|
+
* and shallow, which is the radius the unevaluated-expression diagnostic reads.
|
|
483
|
+
*
|
|
484
|
+
* One object this answers `true` for is not handed over as authored: an inline
|
|
485
|
+
* locale map (`utils/localeMap.ts`, objectui#12936). It is a display string
|
|
486
|
+
* written once per language, so {@link evaluateConfigValueWith} evaluates each
|
|
487
|
+
* member like a plain string on the same key. That is one level, and only for
|
|
488
|
+
* a map whose every key is a locale tag and every value a string, in the
|
|
489
|
+
* spec's own `InlineLocaleMapSchema` sense; every other data object keeps this
|
|
490
|
+
* rule, and the diagnostic reads the members through the same module.
|
|
491
|
+
*/
|
|
492
|
+
const isDataObjectValue = (key, value) => !PREDICATE_CHAIN_KEYS.has(key) && isConfigBag(value) && !isExpressionEnvelope(value);
|
|
493
|
+
/**
|
|
494
|
+
* The scope a page node's values are evaluated against: its `visibleWhen`
|
|
495
|
+
* gate and every other expression the evaluation memo in
|
|
496
|
+
* {@link SchemaRenderer} evaluates on the node. Four roots, in this order:
|
|
497
|
+
*
|
|
498
|
+
* 1. **the ambient predicate scope**: every root `predicateScope` carries (the
|
|
499
|
+
* host scope `usePredicateScope()` publishes, plus a node's API-bound
|
|
500
|
+
* `data` once it has answered), spread first;
|
|
501
|
+
* 2. **`current_user`**: aliased to the ambient `user`, so `user.email` and
|
|
502
|
+
* `current_user.email` read the same value;
|
|
503
|
+
* 3. **`record`**: the page's bound row, ONLY when there is one (a non-array
|
|
504
|
+
* object). No row binds nothing, so a `record` the ambient scope supplies
|
|
505
|
+
* is not shadowed by an empty one; a row, written after the spread, wins;
|
|
506
|
+
* 4. **`page`**: the page variables (`PageSchema.variables`) as `page.<var>`.
|
|
507
|
+
*
|
|
508
|
+
* Why each root is (and `data` is not) bound is stated where the memo builds
|
|
509
|
+
* its evaluator from this.
|
|
510
|
+
*
|
|
511
|
+
* The ONE builder of this scope. The memo and {@link evaluateConfigBagInScope}
|
|
512
|
+
* call it, so they cannot build two different scopes (objectui#10290), and it
|
|
513
|
+
* is exported for the `page:tabs` derived badge in `@object-ui/components`,
|
|
514
|
+
* which decides the `visibleWhen` gates in front of a related list the way the
|
|
515
|
+
* tab body decides them (objectui#12987, objectui#12992). A root added here
|
|
516
|
+
* reaches every one of them.
|
|
517
|
+
*/
|
|
518
|
+
export function buildNodeEvaluationScope(predicateScope, boundRecord, pageVariables) {
|
|
519
|
+
return {
|
|
520
|
+
...predicateScope,
|
|
521
|
+
current_user: predicateScope?.user,
|
|
522
|
+
...(boundRecord && typeof boundRecord === 'object' && !Array.isArray(boundRecord)
|
|
523
|
+
? { record: boundRecord }
|
|
524
|
+
: null),
|
|
525
|
+
page: pageVariables,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Evaluate ONE config value by its key. This is the per-key rule of the
|
|
530
|
+
* evaluation memo (the `params` rule, objectui#7867, is stated there): a
|
|
531
|
+
* `params` bag has every string leaf evaluated, an inline locale map has each
|
|
532
|
+
* member evaluated like a plain string on the same key (objectui#12936,
|
|
533
|
+
* `utils/localeMap.ts`), any other data object is handed over as authored, and
|
|
534
|
+
* every other value is evaluated per value and shallow, with a CEL predicate
|
|
535
|
+
* envelope preserved.
|
|
536
|
+
*/
|
|
537
|
+
const evaluateConfigValueWith = (evaluator, key, value) => key === PARAMS_KEY && isParamsBag(value)
|
|
538
|
+
? mapParamsLeaves(value, (leaf) => evaluator.evaluate(leaf))
|
|
539
|
+
: isDataObjectValue(key, value)
|
|
540
|
+
? isInlineLocaleMap(value)
|
|
541
|
+
? mapLocaleMapMembers(value, (member) => evaluator.evaluate(member))
|
|
542
|
+
: value
|
|
543
|
+
: preservePredicateEnvelope(key, value, (v) => evaluator.evaluate(v));
|
|
544
|
+
/**
|
|
545
|
+
* Evaluate every value of a config bag with {@link evaluateConfigValueWith}.
|
|
546
|
+
* The memo's `properties` loop, as a function. Returns a new object; the
|
|
547
|
+
* authored bag is never mutated.
|
|
548
|
+
*/
|
|
549
|
+
const evaluateConfigBagWith = (evaluator, bag) => {
|
|
550
|
+
const evaluated = { ...bag };
|
|
551
|
+
for (const [key, val] of Object.entries(evaluated)) {
|
|
552
|
+
evaluated[key] = evaluateConfigValueWith(evaluator, key, val);
|
|
553
|
+
}
|
|
554
|
+
return evaluated;
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* The `properties` evaluation of the {@link SchemaRenderer} memo, for a config
|
|
558
|
+
* bag that does not pass through `SchemaRenderer` (objectui#10290): the same
|
|
559
|
+
* per-key rule, the same `ExpressionEvaluator`, and the same scope, built from
|
|
560
|
+
* the same three inputs. It is not a second template engine.
|
|
561
|
+
*
|
|
562
|
+
* The plain half of `useConfigBagEvaluator()` (`hooks/useConfigBagEvaluator`),
|
|
563
|
+
* which reads those inputs from the contexts the memo reads them from. Call
|
|
564
|
+
* the hook; this function exists because the rule it applies is this module's.
|
|
565
|
+
* (The hook is not declared in this module: a hook here puts this file under
|
|
566
|
+
* the React Compiler lint, which then reports the render-time
|
|
567
|
+
* `performance.now()` / `Date.now()` debug probes below.)
|
|
568
|
+
*
|
|
569
|
+
* A value that is not a config bag is returned unchanged; a bag comes back as a
|
|
570
|
+
* new, evaluated object and the authored one is never mutated.
|
|
571
|
+
*/
|
|
572
|
+
export function evaluateConfigBagInScope(bag, predicateScope, boundRecord, pageVariables) {
|
|
573
|
+
if (!isConfigBag(bag))
|
|
574
|
+
return bag;
|
|
575
|
+
const evaluator = new ExpressionEvaluator(buildNodeEvaluationScope(predicateScope, boundRecord, pageVariables));
|
|
576
|
+
return evaluateConfigBagWith(evaluator, bag);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Which CONSEQUENCE the diagnostic should print for a faulting predicate on
|
|
580
|
+
* this leg (objectui#6503).
|
|
581
|
+
*
|
|
582
|
+
* ## The defect this closes
|
|
583
|
+
*
|
|
584
|
+
* `evaluateCondition` answers an unevaluable predicate with `true` on every
|
|
585
|
+
* path. On the three SHOW legs the chain negates that answer, so the node is
|
|
586
|
+
* SHOWN and the reporter's "the gate did NOT bite" is true. On the two HIDE
|
|
587
|
+
* legs the chain returns it UN-negated (see `shouldHide` below), so the same
|
|
588
|
+
* `true` sets `_hidden` and this component returns `null`: the gate bit, and
|
|
589
|
+
* bit harder than on either sibling gate — the node is not on screen at all.
|
|
590
|
+
* Both were routed to `'visibility'`, so an author whose block VANISHED read a
|
|
591
|
+
* line telling them the gate did not bite and went looking for a rendering bug
|
|
592
|
+
* that does not exist. The polarity was documented in
|
|
593
|
+
* `visibilityDiagnostic.ts` and printed anyway; this is the routing that makes
|
|
594
|
+
* the documented split the one that is actually applied.
|
|
595
|
+
*
|
|
596
|
+
* ## Why derived from the two constants, when the reporter refuses to deduce
|
|
597
|
+
*
|
|
598
|
+
* `PredicateGateKind`'s docblock refuses to deduce the gate from `key` INSIDE
|
|
599
|
+
* the reporter, and that refusal stands: the reporter is exported and called
|
|
600
|
+
* from other packages, where an unheard-of spelling would silently inherit
|
|
601
|
+
* some other gate's sentence. Here the caller IS the chain. The two arrays
|
|
602
|
+
* above are the same declaration `shouldHide` and {@link winningVisibilityKey}
|
|
603
|
+
* consult, and {@link VisibilityChainKey} closes the parameter over them, so
|
|
604
|
+
* this is not a second table that can drift from the first — it is the same
|
|
605
|
+
* one, read for a second question. It also removes the failure mode that
|
|
606
|
+
* produced this card: a leg added to the chain and left with a consequence
|
|
607
|
+
* sentence written about a different polarity is now a TYPE error here, not a
|
|
608
|
+
* line of false console copy.
|
|
609
|
+
*
|
|
610
|
+
* The `disabled` / `disabledOn` gate is deliberately not reachable from this
|
|
611
|
+
* function: those legs are not in the visibility chain and route through
|
|
612
|
+
* `evaluateEnablementPredicate`, which states `'enablement'` at its own call
|
|
613
|
+
* site.
|
|
614
|
+
*/
|
|
615
|
+
const visibilityGateKind = (key) => VISIBILITY_HIDE_KEYS.includes(key) ? 'concealment' : 'visibility';
|
|
197
616
|
/**
|
|
198
617
|
* Which ONE visibility key actually decides a node's fate, mirroring both the
|
|
199
618
|
* `properties` hoist's precedence (same-named key: `properties.<key>`
|
|
@@ -242,7 +661,7 @@ const VISIBILITY_HIDE_KEYS = ['hidden', 'hiddenOn'];
|
|
|
242
661
|
*/
|
|
243
662
|
function winningVisibilityKey(node) {
|
|
244
663
|
const propertiesBag = node.properties;
|
|
245
|
-
const hasPropertiesBag =
|
|
664
|
+
const hasPropertiesBag = isConfigBag(propertiesBag);
|
|
246
665
|
const effective = (key) => hasPropertiesBag && Object.prototype.hasOwnProperty.call(propertiesBag, key)
|
|
247
666
|
? propertiesBag[key]
|
|
248
667
|
: node[key];
|
|
@@ -256,6 +675,46 @@ function winningVisibilityKey(node) {
|
|
|
256
675
|
}
|
|
257
676
|
return undefined;
|
|
258
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* The registry key that differs from `type` only in case, when there is one.
|
|
680
|
+
*
|
|
681
|
+
* objectui#5247 ruled Option C — keep lookup strict, make the failure teach.
|
|
682
|
+
* Registry lookup stays exactly case-sensitive (`ComponentRegistry.get` is a
|
|
683
|
+
* plain `Map.get`), so a node typed `Page` still MISSES `page` and still
|
|
684
|
+
* renders the OBJUI-001 panel below. What changes is only what that panel
|
|
685
|
+
* SAYS: when the miss is a case mismatch and nothing else, it names the
|
|
686
|
+
* spelling that would have worked. Making `Page` resolve is the REJECTED
|
|
687
|
+
* option B — it would legalise `PAGE` / `pAge` across docs, the designer,
|
|
688
|
+
* `sdui-intrinsics.d.ts` and every registration site, permanently.
|
|
689
|
+
*
|
|
690
|
+
* The candidate set is read from the LIVE registry — `getKnownTypes()`, which
|
|
691
|
+
* is loaded registrations plus pending `registerLazy` stubs — never from a
|
|
692
|
+
* list typed alongside it. A hand-kept copy goes stale silently and then
|
|
693
|
+
* confidently suggests a type nothing registers any more; objectui#5115
|
|
694
|
+
* measured exactly that drift on the CLI's copy, in both directions at once.
|
|
695
|
+
*
|
|
696
|
+
* Case is the ONLY trigger the ruling grants. This is deliberately not an edit
|
|
697
|
+
* distance: `pge` suggests nothing.
|
|
698
|
+
*
|
|
699
|
+
* `objectui check` emits the same clause from
|
|
700
|
+
* `packages/cli/src/utils/known-type-case-suggestion.ts`. The two surfaces
|
|
701
|
+
* cannot share one implementation because they cannot share a candidate SET:
|
|
702
|
+
* the published CLI runs inside a USER's project and depends on neither
|
|
703
|
+
* `@object-ui/core` nor the plugin packages that register most types, so it
|
|
704
|
+
* answers from the generated `KNOWN_SCHEMA_TYPES` snapshot instead — the
|
|
705
|
+
* measurement is in `scripts/regenerate-known-schema-types.mjs`. Keep the
|
|
706
|
+
* emitted wording in step with that file; both are pinned by tests.
|
|
707
|
+
*/
|
|
708
|
+
function suggestTypeByCase(type) {
|
|
709
|
+
if (typeof type !== 'string' || type === '')
|
|
710
|
+
return undefined;
|
|
711
|
+
const wanted = type.toLowerCase();
|
|
712
|
+
for (const candidate of ComponentRegistry.getKnownTypes()) {
|
|
713
|
+
if (candidate !== type && candidate.toLowerCase() === wanted)
|
|
714
|
+
return candidate;
|
|
715
|
+
}
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
259
718
|
export class SchemaErrorBoundary extends Component {
|
|
260
719
|
constructor() {
|
|
261
720
|
super(...arguments);
|
|
@@ -308,6 +767,64 @@ export class SchemaErrorBoundary extends Component {
|
|
|
308
767
|
* `useState` wiped (objectui#2954's latent hazard, made real by this line).
|
|
309
768
|
*/
|
|
310
769
|
const NO_DATA_SOURCE = {};
|
|
770
|
+
/**
|
|
771
|
+
* One outgoing bag, minus an authored `data` key, on the object arm
|
|
772
|
+
* (objectui#9571, extended to the legacy alias by objectui#9758).
|
|
773
|
+
*
|
|
774
|
+
* ## Why a shared helper and not two copies of four lines
|
|
775
|
+
*
|
|
776
|
+
* `createElement` below spreads the node's own non-metadata keys and the legacy
|
|
777
|
+
* `props` alias bag as two separate spreads, in that order. objectui#9571
|
|
778
|
+
* stripped the first; the alias spread ran AFTER it, so the identical authored
|
|
779
|
+
* key spelled `props: { data }` walked straight back into the seat the ruling
|
|
780
|
+
* had just taken away — the same defect, one alias over (objectui#9758,
|
|
781
|
+
* decision batch #167 item 2, letter 剥, maintainer 「其他同意」). Two call
|
|
782
|
+
* sites answering the same question is exactly how the first carrier survived
|
|
783
|
+
* the first ruling, so the question is asked once, here.
|
|
784
|
+
*
|
|
785
|
+
* ## Identity is the signal, and that is load-bearing
|
|
786
|
+
*
|
|
787
|
+
* The `in` test comes FIRST, so a bag that declares no `data` — the
|
|
788
|
+
* overwhelmingly common one — is handed back ITSELF, with no copy allocated and
|
|
789
|
+
* no key moved. Callers read that same identity (`result !== bag`) to decide
|
|
790
|
+
* whether to warn, so the decision is never computed twice and cannot disagree
|
|
791
|
+
* with what was actually spread.
|
|
792
|
+
*
|
|
793
|
+
* ⛔ The HOST path is NOT routed through here. `...props` — this component's own
|
|
794
|
+
* React props, spread LAST — is how `plugin-list`'s `ListView` and `ObjectView`
|
|
795
|
+
* hand down rows they already fetched; gating that was Option B, and it was
|
|
796
|
+
* REFUSED on objectui#9571. Only the AUTHORED key loses its seat.
|
|
797
|
+
*/
|
|
798
|
+
function withoutAuthoredDataKey(bag, refuse) {
|
|
799
|
+
if (!refuse || !('data' in bag))
|
|
800
|
+
return bag;
|
|
801
|
+
const { data: _refusedAuthoredData, ...rest } = bag;
|
|
802
|
+
return rest;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* The legacy `props` alias bag, minus an authored `objectFields`
|
|
806
|
+
* (objectui#8818, completing decision batch #70 / objectui#7742).
|
|
807
|
+
*
|
|
808
|
+
* `objectFields` is the object's field catalogue — the predicate layer reads it
|
|
809
|
+
* to decide how a conditional-formatting rule compares a relation field — and
|
|
810
|
+
* batch #70 ruled it a RUNTIME React prop a host injects, never authorable
|
|
811
|
+
* metadata. The metadata strip in `SchemaRenderer` closes the top-level and
|
|
812
|
+
* hoisted `properties` spellings; this closes the third, because the alias bag
|
|
813
|
+
* is spread as its own bag after that strip and never passes through it.
|
|
814
|
+
*
|
|
815
|
+
* Every arm, unconditionally — unlike {@link withoutAuthoredDataKey}, the key
|
|
816
|
+
* has no authorable reading on any type. Identity is preserved when the bag
|
|
817
|
+
* declares no `objectFields`, so the common node allocates nothing.
|
|
818
|
+
*
|
|
819
|
+
* ⛔ The HOST path is NOT routed through here: `...props`, spread LAST, is how
|
|
820
|
+
* a host hands the catalogue down, and it still arrives.
|
|
821
|
+
*/
|
|
822
|
+
function withoutAuthoredObjectFields(bag) {
|
|
823
|
+
if (!('objectFields' in bag))
|
|
824
|
+
return bag;
|
|
825
|
+
const { objectFields: _runtimeOnlyObjectFields, ...rest } = bag;
|
|
826
|
+
return rest;
|
|
827
|
+
}
|
|
311
828
|
/**
|
|
312
829
|
* The renderer loop.
|
|
313
830
|
*
|
|
@@ -352,10 +869,41 @@ const NO_DATA_SOURCE = {};
|
|
|
352
869
|
export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
353
870
|
const context = useContext(SchemaRendererContext);
|
|
354
871
|
const dataSource = context?.dataSource || NO_DATA_SOURCE;
|
|
355
|
-
// Ambient host scope (user /
|
|
872
|
+
// Ambient host scope (user / features), fed by app-shell's
|
|
356
873
|
// ExpressionProvider. Threaded into `visible`/expression evaluation so
|
|
357
874
|
// component predicates can gate on the signed-in user & deployment flags.
|
|
358
|
-
const
|
|
875
|
+
const ambientScope = usePredicateScope();
|
|
876
|
+
// ── The API binding (objectui#12197) ──────────────────────────────────────
|
|
877
|
+
//
|
|
878
|
+
// `@objectstack/spec`'s `ElementApiDataSourceSchema` declares what a node
|
|
879
|
+
// with `dataSource: { provider: 'api', read: { url }, path? }` does: it reads
|
|
880
|
+
// that one same-origin endpoint with the host's signed-in fetch and binds the
|
|
881
|
+
// selected value, "which the element reads as `data` in its expression holes
|
|
882
|
+
// and its `visibleWhen`". Both are evaluated in the memo below, so this is
|
|
883
|
+
// where the binding is honoured, for every node type: the read is the hook's
|
|
884
|
+
// (`element-data-source/useElementApiDataSource.ts`), and its answer becomes
|
|
885
|
+
// the `data` root of this node's evaluation scope.
|
|
886
|
+
//
|
|
887
|
+
// ⚠️ This is NOT the adapter-as-`data` root objectui#9308 retired. That root
|
|
888
|
+
// published the injected adapter, against which every `data.*` path resolved
|
|
889
|
+
// `undefined`; this one is the value the node's own binding selected, and it
|
|
890
|
+
// exists only on a node that declares one. A node with no API binding is
|
|
891
|
+
// byte-for-byte what it was: `ambientScope` passes through untouched.
|
|
892
|
+
//
|
|
893
|
+
// The answer is also published to the node's SUBTREE through
|
|
894
|
+
// `PredicateScopeProvider` (below), because a block's members are evaluated
|
|
895
|
+
// there — `useConfigBagEvaluator` reads the predicate scope, as does every
|
|
896
|
+
// nested `SchemaRenderer` — so a member's `${data.x}` resolves against the
|
|
897
|
+
// same value as the node's own.
|
|
898
|
+
//
|
|
899
|
+
// The evaluation memo builds its scope from the answer's VALUE (the hook's
|
|
900
|
+
// state) and keys on it, never on a memo's identity (AGENTS.md #10). The
|
|
901
|
+
// subtree's copy is memoised for cost only: an evaluation keyed on it
|
|
902
|
+
// recomputes the same values if React discards it.
|
|
903
|
+
const apiBinding = useElementApiDataSource(schema);
|
|
904
|
+
const apiStatus = apiBinding.status;
|
|
905
|
+
const apiData = apiBinding.data;
|
|
906
|
+
const subtreeScope = useMemo(() => (apiStatus === 'ready' ? { ...ambientScope, data: apiData } : ambientScope), [ambientScope, apiStatus, apiData]);
|
|
359
907
|
// The row the page is about, from `RecordContextProvider` (objectui#5454).
|
|
360
908
|
//
|
|
361
909
|
// `@objectstack/spec` has always DECLARED that a component node's
|
|
@@ -418,11 +966,36 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
418
966
|
// an object spread.
|
|
419
967
|
if (!schema || typeof schema !== 'object')
|
|
420
968
|
return schema;
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
//
|
|
425
|
-
//
|
|
969
|
+
// The scope's host roots, plus the node's API-bound `data` once it has
|
|
970
|
+
// answered (see "The API binding" above).
|
|
971
|
+
const predicateScope = apiStatus === 'ready' ? { ...ambientScope, data: apiData } : ambientScope;
|
|
972
|
+
// The ambient host scope, plus the roots this tier can answer itself.
|
|
973
|
+
// `current_user` is aliased to `user` so both `user.email` and
|
|
974
|
+
// `current_user.email` resolve in component `visible`/`visibleOn`
|
|
975
|
+
// expressions. `page` exposes page-local state so predicates can gate on
|
|
976
|
+
// `page.<var>` (e.g. a record picker's selection toggling another
|
|
977
|
+
// component's visibility).
|
|
978
|
+
//
|
|
979
|
+
// ⛔ `data` is NOT here, and the absence is the decision (objectui#9308,
|
|
980
|
+
// maintainer ruling 2026-09-13 option B). This used to read
|
|
981
|
+
// `data: dataSource` — the host's injected ADAPTER, published as an
|
|
982
|
+
// expression root. `ExpressionProvider` states the governing principle for
|
|
983
|
+
// the tier above: "Every root below is one the engine accepts AND one this
|
|
984
|
+
// tier can actually answer", and objectui#8155 (`app`) and objectui#8166
|
|
985
|
+
// (`data`) applied it there. Against a conformant `DataSource` adapter
|
|
986
|
+
// every `data.*` path resolves `undefined`, so this tier could not answer
|
|
987
|
+
// the root it bound: it published a name that was silently constant on
|
|
988
|
+
// every row. ADR-0089 D3 puts `data` at the METADATA layer
|
|
989
|
+
// (`CANONICAL_ROOT_BY_LAYER = { runtime: 'record', metadata: 'data' }`) and
|
|
990
|
+
// the engine's per-surface `FIELD_RULE_BOUND_ROOTS` is
|
|
991
|
+
// `['record','previous','parent']`. The row is `record`.
|
|
992
|
+
//
|
|
993
|
+
// ⭐ Ordering consequence, and the second half of the same ruling: the
|
|
994
|
+
// spread below used to be followed by `data: dataSource`, so a host that
|
|
995
|
+
// legitimately published `data` through the documented scope channel
|
|
996
|
+
// (`PredicateScopeProvider`) was silently OVERWRITTEN by the adapter.
|
|
997
|
+
// Removing the line un-shadows that channel — a host root named `data` now
|
|
998
|
+
// survives, like every other root a host publishes.
|
|
426
999
|
//
|
|
427
1000
|
// `record` is written AFTER the ambient spread so a page's own row wins
|
|
428
1001
|
// over anything a host put in the scope — the same precedence
|
|
@@ -433,17 +1006,23 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
433
1006
|
// (how `action:group`'s dropdown leaf is driven), turning "this surface has
|
|
434
1007
|
// no row of its own" into "this surface's row is empty" — only the latter
|
|
435
1008
|
// is entitled to shadow.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
//
|
|
1009
|
+
//
|
|
1010
|
+
// Built by `buildNodeEvaluationScope`, which `useConfigBagEvaluator`
|
|
1011
|
+
// shares, so an action container's members evaluate against this same
|
|
1012
|
+
// scope (objectui#10290), and which the `page:tabs` badge in
|
|
1013
|
+
// `@object-ui/components` calls for its `visibleWhen` gates
|
|
1014
|
+
// (objectui#12992).
|
|
1015
|
+
const evaluator = new ExpressionEvaluator(buildNodeEvaluationScope(predicateScope, boundRecord, pageVariables));
|
|
1016
|
+
// Shallow copy: the interpreter's working copy, typed as renderer state
|
|
1017
|
+
// rather than as the authoring face (objectui#11349). See
|
|
1018
|
+
// `InterpreterWorkingCopy` for what it declares and why.
|
|
446
1019
|
const newSchema = { ...schema };
|
|
1020
|
+
// A node whose API binding has not answered is not evaluated: its holes
|
|
1021
|
+
// name a `data` that does not exist yet, so evaluating them would report
|
|
1022
|
+
// every one as unresolvable and decide its `visibleWhen` against nothing.
|
|
1023
|
+
// The render pass below draws the binding's state instead.
|
|
1024
|
+
if (apiStatus !== 'absent' && apiStatus !== 'ready')
|
|
1025
|
+
return newSchema;
|
|
447
1026
|
/**
|
|
448
1027
|
* Evaluate ONE visibility predicate, and make an unresolvable one LOUD
|
|
449
1028
|
* (objectui#5454, leg 3 of the 2026-08-21 ruling).
|
|
@@ -473,7 +1052,20 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
473
1052
|
* on which dialect they happened to write it in.
|
|
474
1053
|
*
|
|
475
1054
|
* Deduped per (node type, key, predicate source): a broken predicate is
|
|
476
|
-
* re-evaluated on every render, and the point is one line, not a wall.
|
|
1055
|
+
* re-evaluated on every render, and the point is one line, not a wall. The
|
|
1056
|
+
* key is the predicate SOURCE TEXT plus the gate it was authored on — never
|
|
1057
|
+
* the render and never the schema object — so the same broken predicate
|
|
1058
|
+
* rendered over two hundred rows reports once, and a SECOND distinct
|
|
1059
|
+
* predicate still reports (objectui#6038 pins both halves).
|
|
1060
|
+
*
|
|
1061
|
+
* ## Production is loud too, since objectui#6038
|
|
1062
|
+
*
|
|
1063
|
+
* It was `__DEV__`-only, and the maintainer's 2026-08-25 ruling retired
|
|
1064
|
+
* that silence: a gate that stops biting in production used to leave
|
|
1065
|
+
* nothing on the console for the bare-string dialect, so a class-1 defect
|
|
1066
|
+
* could sit live and undiscovered (measured in objectstack#11254). The
|
|
1067
|
+
* `__DEV__` gate below no longer decides WHETHER the fault is reported,
|
|
1068
|
+
* only HOW it is detected — see the two branches.
|
|
477
1069
|
*
|
|
478
1070
|
* ## Defined HERE, ahead of the `properties` evaluation loop below
|
|
479
1071
|
*
|
|
@@ -485,16 +1077,41 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
485
1077
|
* on the POST-evaluation, POST-hoist schema, for the real verdict.
|
|
486
1078
|
*/
|
|
487
1079
|
const evaluateVisibilityPredicate = (raw, key) => {
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
|
|
1080
|
+
// WHICH consequence this leg's fail-soft default earns (objectui#6503).
|
|
1081
|
+
// Computed once, used by both branches, so the production and the
|
|
1082
|
+
// development report cannot disagree about what the default DID.
|
|
1083
|
+
const gate = visibilityGateKind(key);
|
|
1084
|
+
// PRODUCTION STILL MAKES THE SINGLE CALL — it just no longer makes it
|
|
1085
|
+
// in silence (objectui#6038, maintainer ruling 2026-08-25, option B).
|
|
1086
|
+
//
|
|
1087
|
+
// `throwOnError` remains the DEV probe and remains too expensive to ship:
|
|
1088
|
+
// on the CEL branch `evaluateCelCondition` implements it by evaluating
|
|
1089
|
+
// TWICE (once with each fallback — a value that tracks the fallback both
|
|
1090
|
+
// times is a fault), and spec-parsed metadata normalizes `visibleWhen`
|
|
1091
|
+
// into a `{ dialect: 'cel' }` envelope, so that branch is the common one
|
|
1092
|
+
// in production. Paying for the probe here would double the engine calls
|
|
1093
|
+
// for every predicate of every node.
|
|
1094
|
+
//
|
|
1095
|
+
// `onFault` is the way out of that trade: the evaluator hands back the
|
|
1096
|
+
// reason at the point it ALREADY knows the predicate faulted, inside the
|
|
1097
|
+
// catch it already runs, so the fault becomes observable at ONE engine
|
|
1098
|
+
// call. The verdict is `evaluateCondition(raw)`'s, unchanged — this card
|
|
1099
|
+
// is observability only, and the fail-open semantics are not its to move.
|
|
1100
|
+
//
|
|
1101
|
+
// The reporter is the SAME one the dev branch below uses: same message,
|
|
1102
|
+
// same severity, same dedupe `Set`, same key. Production and development
|
|
1103
|
+
// now print the identical line for the identical fault, which is the
|
|
1104
|
+
// property the `__DEV__` gate used to cost us.
|
|
1105
|
+
if (!__DEV__) {
|
|
1106
|
+
return evaluator.evaluateCondition(raw, {
|
|
1107
|
+
onFault: (reason) =>
|
|
1108
|
+
// `'page-component'` stated rather than defaulted (objectui#6487):
|
|
1109
|
+
// this is the tier whose roots the spec declares for a node gate,
|
|
1110
|
+
// and saying so here is what keeps the advice paragraph a decision
|
|
1111
|
+
// this call site owns.
|
|
1112
|
+
reportUnresolvableVisibilityPredicate(newSchema.type, newSchema.id, key, raw, reason, 'page-component', gate),
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
498
1115
|
try {
|
|
499
1116
|
const verdict = evaluator.evaluateCondition(raw, { throwOnError: true });
|
|
500
1117
|
// objectui#5687 — the NON-throwing half of the same silence, and it is
|
|
@@ -504,11 +1121,17 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
504
1121
|
// `false`. Verdict untouched — `verdict` is returned exactly as
|
|
505
1122
|
// computed, which is what keeps the ruling's "no verdict changes" true
|
|
506
1123
|
// by construction rather than by review.
|
|
507
|
-
|
|
1124
|
+
//
|
|
1125
|
+
// objectui#9308: the object handed over is the `data` the HOST
|
|
1126
|
+
// published in the ambient scope — the one the evaluator above
|
|
1127
|
+
// actually resolved `data.*` against — and no longer the adapter. The
|
|
1128
|
+
// renderer binds no `data` of its own, so passing the adapter here
|
|
1129
|
+
// would report reads the evaluator never made against it.
|
|
1130
|
+
reportAdapterOnlyDataPredicate(newSchema.type, newSchema.id, key, raw, predicateScope?.data);
|
|
508
1131
|
return verdict;
|
|
509
1132
|
}
|
|
510
1133
|
catch (err) {
|
|
511
|
-
reportUnresolvableVisibilityPredicate(newSchema.type, newSchema.id, key, raw, err);
|
|
1134
|
+
reportUnresolvableVisibilityPredicate(newSchema.type, newSchema.id, key, raw, err, 'page-component', gate);
|
|
512
1135
|
// The historical fail-soft answer, unchanged — and identical to what
|
|
513
1136
|
// the production branch above returns for the same input, which is what
|
|
514
1137
|
// keeps the two branches one behaviour rather than two. See the
|
|
@@ -517,6 +1140,156 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
517
1140
|
return true;
|
|
518
1141
|
}
|
|
519
1142
|
};
|
|
1143
|
+
/**
|
|
1144
|
+
* Evaluate ONE enablement predicate (`disabled` / `disabledOn`), and make an
|
|
1145
|
+
* unresolvable one LOUD (objectui#6445).
|
|
1146
|
+
*
|
|
1147
|
+
* ## Why this pair needed its own card at all
|
|
1148
|
+
*
|
|
1149
|
+
* Six legs of the visibility chain above route through
|
|
1150
|
+
* {@link evaluateVisibilityPredicate} and report. These two called
|
|
1151
|
+
* `evaluateCondition` BARE, so a faulting `disabled` predicate had never
|
|
1152
|
+
* been reported in any build, in any dialect that does not report on its
|
|
1153
|
+
* own — the only uninstrumented predicate pair left in this file.
|
|
1154
|
+
*
|
|
1155
|
+
* And it is the pair where the fail-soft default BITES. `evaluateCondition`
|
|
1156
|
+
* answers an unevaluable predicate with `true`; on the negated visibility
|
|
1157
|
+
* legs that means SHOWN, here it means GREYED OUT. The asymmetry this file
|
|
1158
|
+
* already records for objectui#3862/#3955 cuts the other way for the
|
|
1159
|
+
* console: "a greyed-out control is still on screen", so the user has a
|
|
1160
|
+
* symptom and the author has nothing to grep for. That is why the reporter
|
|
1161
|
+
* is handed a different consequence paragraph (`'enablement'`) rather than
|
|
1162
|
+
* the visibility copy, which states the opposite of what this gate did.
|
|
1163
|
+
*
|
|
1164
|
+
* ## The verdict is byte-for-byte what it was
|
|
1165
|
+
*
|
|
1166
|
+
* `evaluateCondition(raw)` with an `onFault` callback returns exactly what
|
|
1167
|
+
* `evaluateCondition(raw)` returned: `onFault` is invoked for its side
|
|
1168
|
+
* effect and its return value is ignored (objectui#6038's seam). Fail-soft
|
|
1169
|
+
* is PRESERVED deliberately — flipping it is a shipped-behaviour change on
|
|
1170
|
+
* a live surface and is not this card's to make.
|
|
1171
|
+
*
|
|
1172
|
+
* ## One engine call, both builds — still true after objectui#6504
|
|
1173
|
+
*
|
|
1174
|
+
* The FAULT report above is unconditional either way: `onFault` alone
|
|
1175
|
+
* covers both builds, one engine call, one code path, dev and production
|
|
1176
|
+
* print the same line by construction. objectui#6504 adds a SECOND,
|
|
1177
|
+
* dev-only check after that same call returns — not a second evaluation,
|
|
1178
|
+
* a lexical scan of the already-available predicate source and the
|
|
1179
|
+
* already-available `dataSource` — so production keeps its one-call,
|
|
1180
|
+
* one-path shape exactly as this docblock described before this leg
|
|
1181
|
+
* existed; only development pays for the extra check, and only after a
|
|
1182
|
+
* clean (non-faulting) evaluation.
|
|
1183
|
+
*
|
|
1184
|
+
* ## objectui#5687's diagnostic, extended (objectui#6504, maintainer
|
|
1185
|
+
* ruling 2026-08-27 option A)
|
|
1186
|
+
*
|
|
1187
|
+
* Was deliberately NOT called here — see the card this replaces filed at
|
|
1188
|
+
* objectui#6504: the 2026-08-22 ruling was scoped to the visibility gate,
|
|
1189
|
+
* and its message text ("a constant `false` hides the node on every row")
|
|
1190
|
+
* is not what a constant does to a `disabled` gate. The 2026-08-27 ruling
|
|
1191
|
+
* answers both gaps: extend to `disabled` / `disabledOn`, dev-only exactly
|
|
1192
|
+
* as the visibility leg is (⛔ always-on was excluded, outside the #5687
|
|
1193
|
+
* precedent), with its OWN copy — the constant-`true` direction, since on
|
|
1194
|
+
* THIS gate that is the polarity that greys the control out in silence.
|
|
1195
|
+
* `ADAPTER_ONLY_GATE_COPY['enablement']` (`visibilityDiagnostic.ts`) is
|
|
1196
|
+
* that copy, and it carries the same #5330 dissolution pointer #5687's
|
|
1197
|
+
* entry does: both legs retire together when that window closes.
|
|
1198
|
+
*
|
|
1199
|
+
* ## The `key` parameter is CLOSED (objectui#9107)
|
|
1200
|
+
*
|
|
1201
|
+
* Typed {@link EnablementNodeGateKey} rather than `string`, mirroring what
|
|
1202
|
+
* {@link VisibilityChainKey} does for the sibling chain: the two legs this
|
|
1203
|
+
* function serves are declared in ONE place, and the config-bag guard
|
|
1204
|
+
* ({@link preservePredicateEnvelope}) reads that same declaration, so a
|
|
1205
|
+
* third leg cannot arrive here without the same edit also carrying its CEL
|
|
1206
|
+
* envelope through the bag loops.
|
|
1207
|
+
*
|
|
1208
|
+
* Reachable only on the branch where the predicate evaluated CLEANLY —
|
|
1209
|
+
* `faulted` mirrors {@link evaluateVisibilityPredicate}'s try/catch split
|
|
1210
|
+
* without paying for a second (`throwOnError`) engine call: `onFault` is
|
|
1211
|
+
* already told exactly when the single call it wraps did not produce a
|
|
1212
|
+
* clean verdict, so a local flag reuses that same signal instead of
|
|
1213
|
+
* re-deriving it. A predicate that faults is the OTHER reporter's case,
|
|
1214
|
+
* immediately above, in the same way it already is on the visibility leg.
|
|
1215
|
+
*/
|
|
1216
|
+
const evaluateEnablementPredicate = (raw, key) => {
|
|
1217
|
+
let faulted = false;
|
|
1218
|
+
const verdict = evaluator.evaluateCondition(raw, {
|
|
1219
|
+
onFault: (reason) => {
|
|
1220
|
+
faulted = true;
|
|
1221
|
+
reportUnresolvableVisibilityPredicate(newSchema.type, newSchema.id, key, raw, reason,
|
|
1222
|
+
// Stated, not defaulted, for both arguments (objectui#6487,
|
|
1223
|
+
// objectui#6445): this is the node tier, whose roots the spec
|
|
1224
|
+
// declares, and the gate whose safe default disables the control.
|
|
1225
|
+
'page-component', 'enablement');
|
|
1226
|
+
},
|
|
1227
|
+
});
|
|
1228
|
+
if (__DEV__ && !faulted) {
|
|
1229
|
+
// objectui#9308 — same re-aim as the visibility leg above.
|
|
1230
|
+
reportAdapterOnlyDataPredicate(newSchema.type, newSchema.id, key, raw, predicateScope?.data, 'enablement');
|
|
1231
|
+
}
|
|
1232
|
+
return verdict;
|
|
1233
|
+
};
|
|
1234
|
+
/**
|
|
1235
|
+
* Evaluate ONE config value by its key — THE place the `params` rule is
|
|
1236
|
+
* stated (objectui#7867, ruling A, maintainer 「其他同意」 2026-09-20):
|
|
1237
|
+
* *an action's `params` values are templates, evaluated where `properties`
|
|
1238
|
+
* are.*
|
|
1239
|
+
*
|
|
1240
|
+
* A `params` BAG (a plain object — {@link isParamsBag}) has every string
|
|
1241
|
+
* leaf evaluated at any depth, through the same `evaluator` and so against
|
|
1242
|
+
* the same scope as every other value in this memo; every other value
|
|
1243
|
+
* evaluates exactly as it did (per-value and shallow, with a CEL predicate
|
|
1244
|
+
* envelope preserved — {@link preservePredicateEnvelope}). The walk, and
|
|
1245
|
+
* what it will not look inside (non-plain objects, keys, cycles, an ARRAY
|
|
1246
|
+
* `params`, which is the `ActionParam[]` definition list), are defined once
|
|
1247
|
+
* in `utils/paramsBag.ts`, which the unevaluated-expression diagnostic reads
|
|
1248
|
+
* too — so what is evaluated and what is reported as left unresolved are
|
|
1249
|
+
* one radius, not two.
|
|
1250
|
+
*
|
|
1251
|
+
* Three carriers, one rule — the ruling names two of them and the third
|
|
1252
|
+
* follows its canonical bag, as every per-key rule in these loops does:
|
|
1253
|
+
*
|
|
1254
|
+
* 1. node-level `params`, immediately below;
|
|
1255
|
+
* 2. `properties.params`, in the `properties` loop, BEFORE the hoist —
|
|
1256
|
+
* so the object the hoist copies onto the node (and the renderer
|
|
1257
|
+
* reads as `schema.params`) is the evaluated one, and
|
|
1258
|
+
* `schema.properties.params` agrees with it;
|
|
1259
|
+
* 3. `props.params`, in the legacy-alias loop — the two bag loops must
|
|
1260
|
+
* not disagree about a key (objectui#5123, objectui#9100).
|
|
1261
|
+
*
|
|
1262
|
+
* Each authored value is evaluated exactly once: the node-level leg runs
|
|
1263
|
+
* before the hoist, so when `properties.params` wins the hoist it replaces
|
|
1264
|
+
* an evaluated node-level bag with an evaluated bag of its own, and nothing
|
|
1265
|
+
* already evaluated is evaluated again.
|
|
1266
|
+
*
|
|
1267
|
+
* Before this, the node-level bag was never evaluated at all, and
|
|
1268
|
+
* `properties.params` was ONE value of a shallow loop: measured through the
|
|
1269
|
+
* real `SchemaRenderer` -> `action:button` -> runner on a record page bound
|
|
1270
|
+
* to `{ id: 'rec_1' }`, both `params.recordId: '${record.id}'` spellings
|
|
1271
|
+
* reached the handler raw while `properties.label: 'L-${record.id}'`
|
|
1272
|
+
* rendered `L-rec_1`. A template that still cannot resolve keeps its source
|
|
1273
|
+
* text (that is the evaluator's own contract) and is reported by the
|
|
1274
|
+
* unevaluated-expression diagnostic, so a wrong template stays loud.
|
|
1275
|
+
*/
|
|
1276
|
+
//
|
|
1277
|
+
// A DATA OBJECT on any other key ({@link isDataObjectValue},
|
|
1278
|
+
// objectui#10288) is handed over as authored: `evaluate` would collapse
|
|
1279
|
+
// one that carries a string `source` to that string. An inline locale map
|
|
1280
|
+
// is the one object that is not: each member is evaluated like a plain
|
|
1281
|
+
// string on the same key (objectui#12936, `utils/localeMap.ts`).
|
|
1282
|
+
//
|
|
1283
|
+
// The rule itself is `evaluateConfigValueWith`, at module scope, so that
|
|
1284
|
+
// `useConfigBagEvaluator` applies the same one to an action container's
|
|
1285
|
+
// members (objectui#10290).
|
|
1286
|
+
const evaluateConfigValue = (key, value) => evaluateConfigValueWith(evaluator, key, value);
|
|
1287
|
+
// Carrier 1 of the `params` rule above: the node-level bag. A non-bag
|
|
1288
|
+
// node-level `params` (the `ActionParam[]` definition list, or anything
|
|
1289
|
+
// degenerate) is left exactly as authored, as it always was.
|
|
1290
|
+
if (isParamsBag(newSchema[PARAMS_KEY])) {
|
|
1291
|
+
newSchema[PARAMS_KEY] = evaluateConfigValue(PARAMS_KEY, newSchema[PARAMS_KEY]);
|
|
1292
|
+
}
|
|
520
1293
|
// Evaluate 'properties' — the SPEC spelling of a node's config bag, of
|
|
521
1294
|
// which `props` (evaluated below) is the legacy alias.
|
|
522
1295
|
//
|
|
@@ -546,24 +1319,59 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
546
1319
|
// source today). Deepening that is a separate decision and would have to be
|
|
547
1320
|
// taken for both spellings at once — not smuggled in on one side here.
|
|
548
1321
|
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
1322
|
+
// ONE key is deep, by that separate decision, and on both spellings at once:
|
|
1323
|
+
// `params` (objectui#7867, ruling A) — every string leaf of a `params` bag
|
|
1324
|
+
// is evaluated, through {@link evaluateConfigValue} above. ONE value shape
|
|
1325
|
+
// is one level deep, on any non-predicate key and both spellings: an inline
|
|
1326
|
+
// locale map's members (objectui#12936). Every other value keeps the
|
|
1327
|
+
// shallow reading this paragraph describes.
|
|
1328
|
+
//
|
|
1329
|
+
// "Passed through" used to have one exception: `evaluate` unwraps an object
|
|
1330
|
+
// with a string `source` to that string, so a data object carrying a
|
|
1331
|
+
// `source` field was collapsed rather than passed. Since objectui#10288 an
|
|
1332
|
+
// object on a non-predicate key reaches `evaluate` only when it is a spec
|
|
1333
|
+
// Expression envelope (it carries a `dialect`); see {@link isDataObjectValue}.
|
|
1334
|
+
//
|
|
1335
|
+
// Guarded by {@link isConfigBag}: a degenerate value must not have its shape
|
|
1336
|
+
// reinterpreted by an object spread. Non-objects skip evaluation and reach
|
|
1337
|
+
// the hoist — which, since objectui#6760, refuses them on its own.
|
|
1338
|
+
//
|
|
1339
|
+
// ⚠️ This guard used to say it was wider than the `props` branch BECAUSE
|
|
1340
|
+
// this value feeds the hoist. That reason did not survive measurement
|
|
1341
|
+
// (objectui#6752). Ablating this guard to bare truthiness and re-rendering
|
|
1342
|
+
// `{ type, properties: 'not-a-bag' }` on `b76ca6764` left the indexed
|
|
1343
|
+
// keys the hoist puts on the node completely UNCHANGED — `0` … `8` either
|
|
1344
|
+
// way, because the hoist's own `Object.entries` walk enumerated a string's
|
|
1345
|
+
// character indices whatever this line did — and moved exactly one thing:
|
|
1346
|
+
// whether `schema.properties` still holds the value the author wrote
|
|
1347
|
+
// (`'not-a-bag'` guarded, `{ '0': 'n', … }` ablated). So what this buys is
|
|
1348
|
+
// the AUTHORED value's shape, which is channel-independent, and the `props`
|
|
1349
|
+
// branch below carries the same guard for the same reason.
|
|
1350
|
+
//
|
|
1351
|
+
// ⭐ That measurement is also why objectui#6760 gave the HOIST its own
|
|
1352
|
+
// guard instead of widening this one: this line never reached the indexed
|
|
1353
|
+
// keys, so nothing written here could have removed them. With both guards
|
|
1354
|
+
// in place the two are in SERIES, not redundant, and re-running the same
|
|
1355
|
+
// ablation now reads the opposite way — measured on this card's branch,
|
|
1356
|
+
// ablating THIS line to bare truthiness brings `0` … `8` BACK, because the
|
|
1357
|
+
// bare spread manufactures a real `{ '0': 'n', … }` bag out of the string
|
|
1358
|
+
// and the hoist below then enumerates it legitimately. Each guard is load
|
|
1359
|
+
// bearing for a different half: this one keeps the authored value's shape,
|
|
1360
|
+
// the hoist's one declines to enumerate a value that never had keys.
|
|
553
1361
|
// Snapshotted BEFORE evaluation — objectui#5756's diagnostic below reads
|
|
554
1362
|
// the RAW (pre-collapse) text from this reference, since `newSchema.properties`
|
|
555
1363
|
// is about to be replaced with the evaluated copy.
|
|
556
|
-
const rawPropertiesBag = newSchema.properties
|
|
557
|
-
typeof newSchema.properties === 'object' &&
|
|
558
|
-
!Array.isArray(newSchema.properties)
|
|
1364
|
+
const rawPropertiesBag = isConfigBag(newSchema.properties)
|
|
559
1365
|
? newSchema.properties
|
|
560
1366
|
: undefined;
|
|
561
1367
|
if (rawPropertiesBag) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
1368
|
+
// objectui#9100 — a CEL predicate envelope survives this loop; see
|
|
1369
|
+
// `preservePredicateEnvelope`. objectui#7867 — a `params` bag has every
|
|
1370
|
+
// string leaf evaluated (carrier 2 of `evaluateConfigValue`). Every
|
|
1371
|
+
// other value evaluates as before. The loop is `evaluateConfigBagWith`,
|
|
1372
|
+
// the one `useConfigBagEvaluator` runs over an action container's
|
|
1373
|
+
// member (objectui#10290).
|
|
1374
|
+
newSchema.properties = evaluateConfigBagWith(evaluator, rawPropertiesBag);
|
|
567
1375
|
}
|
|
568
1376
|
/**
|
|
569
1377
|
* objectui#5756 — the `${…}` HALF of the #5687/#5454 silence, reached at
|
|
@@ -621,7 +1429,59 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
621
1429
|
// (e.g. tab visual style: 'line' | 'card' | 'pill'). Keep `properties`
|
|
622
1430
|
// intact on the schema so renderers can still read these collision-prone
|
|
623
1431
|
// keys via `schema.properties.<key>`.
|
|
624
|
-
|
|
1432
|
+
//
|
|
1433
|
+
// Guarded by {@link isConfigBag}: a DEGENERATE `properties` contributes no
|
|
1434
|
+
// node keys at all (objectui#6760). Measured on `c6732825d`, this branch's
|
|
1435
|
+
// base, with no part of that card in the tree: `properties: 'not-a-bag'`
|
|
1436
|
+
// reached the element as nine React props named `0` … `8`, and
|
|
1437
|
+
// `properties: ['x', 'y']` as `0`, `1`. Nobody authored those keys — they
|
|
1438
|
+
// are `Object.entries`' reading of a string's character indices, and the
|
|
1439
|
+
// values this loop writes onto the node are spread as React props at the
|
|
1440
|
+
// `createElement` below.
|
|
1441
|
+
//
|
|
1442
|
+
// ## Which arm, and why this one
|
|
1443
|
+
//
|
|
1444
|
+
// objectui#6760 left two open, both cheap, both reversible, neither on the
|
|
1445
|
+
// manual floor (the objectui#6708 census walked every JSON document, every
|
|
1446
|
+
// `json` doc fence and every TypeScript object literal and found ZERO
|
|
1447
|
+
// authored degenerate config bags):
|
|
1448
|
+
//
|
|
1449
|
+
// (a) guard the hoist the way the evaluation memo above is guarded;
|
|
1450
|
+
// (b) rule that the hoist may enumerate whatever it is handed, and say
|
|
1451
|
+
// so here.
|
|
1452
|
+
//
|
|
1453
|
+
// Arm (a), for three reasons, in the order they were weighed:
|
|
1454
|
+
//
|
|
1455
|
+
// 1. **One answer per key, whichever channel reads it.** That is
|
|
1456
|
+
// objectui#5123's ruling (maintainer, 2026-08-18), and arm (b) breaks
|
|
1457
|
+
// it in the loudest available place: after objectui#6752,
|
|
1458
|
+
// `props: 'not-a-bag'` contributes no keys, so arm (b) would answer the
|
|
1459
|
+
// SAME authored mistake two ways depending on which of two spellings of
|
|
1460
|
+
// ONE bag the author used — and the reinterpreting half would be
|
|
1461
|
+
// `properties`, the SPEC spelling, while the quiet half is `props`, the
|
|
1462
|
+
// annotated legacy alias. A rule that punishes the canonical spelling
|
|
1463
|
+
// is not a rule anyone can teach.
|
|
1464
|
+
// 2. **The reason for the guard was measured, and it is channel-**
|
|
1465
|
+
// **independent.** objectui#6752 established it by ablation rather than
|
|
1466
|
+
// by reading a comment (see the evaluation memo above): what a config-bag
|
|
1467
|
+
// guard buys is the AUTHORED value's shape, which has nothing to do with
|
|
1468
|
+
// which bag or which site is asking. The hoist is a third site asking the
|
|
1469
|
+
// same question about the same authored value; a third answer would be
|
|
1470
|
+
// drift, which is what {@link isConfigBag} exists to end (objectui#6761).
|
|
1471
|
+
// 3. **The failure it prevents is silent and lands on generated metadata.**
|
|
1472
|
+
// A degenerate bag is a mistake an author (increasingly, an AI writing
|
|
1473
|
+
// metadata) makes by writing `properties: 'text'` where a bag belongs.
|
|
1474
|
+
// Arm (b) turns that into nine plausible-looking props with no warning
|
|
1475
|
+
// anywhere; arm (a) makes it inert, so the mistake stays legible as the
|
|
1476
|
+
// `properties` value the author actually wrote.
|
|
1477
|
+
//
|
|
1478
|
+
// What arm (a) does NOT change, measured rather than assumed: a real object
|
|
1479
|
+
// bag hoists exactly as before (`type`/`id` still protected above), and
|
|
1480
|
+
// `properties: 42` / `properties: true` were already contributing nothing
|
|
1481
|
+
// — `Object.entries` yields no entries for them, so this guard only ever
|
|
1482
|
+
// moves the string and array cases. Pinned in
|
|
1483
|
+
// `__tests__/SchemaRenderer.degeneratePropertiesHoist.test.tsx`.
|
|
1484
|
+
if (isConfigBag(newSchema.properties)) {
|
|
625
1485
|
const outerType = newSchema.type;
|
|
626
1486
|
const outerId = newSchema.id;
|
|
627
1487
|
const props = newSchema.properties;
|
|
@@ -640,20 +1500,152 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
640
1500
|
if (typeof newSchema.content === 'string') {
|
|
641
1501
|
newSchema.content = evaluator.evaluate(newSchema.content);
|
|
642
1502
|
}
|
|
643
|
-
|
|
644
|
-
|
|
1503
|
+
/**
|
|
1504
|
+
* Evaluate the SPEC-DECLARED, expression-bindable TOP-LEVEL text keys
|
|
1505
|
+
* (objectui#4795 Direction 1, maintainer ruling 2026-08-25).
|
|
1506
|
+
*
|
|
1507
|
+
* ## The hole this closes
|
|
1508
|
+
*
|
|
1509
|
+
* A value reaches the screen only if it is BOTH evaluated here AND read
|
|
1510
|
+
* back by the renderer off the node. `content` above satisfies both; the
|
|
1511
|
+
* other text keys satisfied neither at once, so
|
|
1512
|
+
* `{ type: 'statistic', value: '${data.total}' }` — whose renderer reads
|
|
1513
|
+
* `schema.value` (`data-display/statistic.tsx`) — put the literal
|
|
1514
|
+
* `${data.total}` on screen, and the `props`-envelope workaround the
|
|
1515
|
+
* objectui#4786 teaching rewrite retired rendered BLANK instead (evaluated,
|
|
1516
|
+
* then spread as a React prop nobody reads). `statistic` is the dashboard
|
|
1517
|
+
* workhorse and had no way at all to bind a dynamic value.
|
|
1518
|
+
*
|
|
1519
|
+
* ## Why this is ONE leg here and not a patch in each renderer
|
|
1520
|
+
*
|
|
1521
|
+
* The ruling's own implementation caution: the read-back sites must be
|
|
1522
|
+
* "converged on evaluated values, not patched per component". They already
|
|
1523
|
+
* agree on WHERE to read (the node's top level) — what they lacked was
|
|
1524
|
+
* anything writing an evaluated value there. Writing it at the single
|
|
1525
|
+
* producer of evaluated schema means `statistic.tsx`, `card.tsx` and
|
|
1526
|
+
* `button.tsx` are UNCHANGED by this card and every future top-level reader
|
|
1527
|
+
* is covered by construction. Four fixed components and no contract was the
|
|
1528
|
+
* failure mode named at dispatch.
|
|
1529
|
+
*
|
|
1530
|
+
* ## The vocabulary is the spec's, and is consumed rather than copied
|
|
1531
|
+
*
|
|
1532
|
+
* `@objectstack/spec` declares it (objectstack#9599): the closed set
|
|
1533
|
+
* `title` / `label` / `value` / `description`, plus the per-component
|
|
1534
|
+
* carriage map saying which of them each component type actually reads
|
|
1535
|
+
* back. The 2026-08-18 ruling is explicit that this memo CONSUMES the
|
|
1536
|
+
* declaration "rather than hard-coding a twin list" — so the only thing
|
|
1537
|
+
* this file knows is the name of the lookup. A row added upstream starts
|
|
1538
|
+
* working here with no edit; a row this file invented would be the second
|
|
1539
|
+
* dialect the declaration exists to prevent.
|
|
1540
|
+
*
|
|
1541
|
+
* ## The type string is passed VERBATIM
|
|
1542
|
+
*
|
|
1543
|
+
* `expressionBindableTextKeysFor` keys on the bare registry name, and the
|
|
1544
|
+
* spec states the answer for an unlisted type is the empty set — "closed
|
|
1545
|
+
* and mechanically answerable in both directions, never inferred from what
|
|
1546
|
+
* a renderer happens to read". So no prefix-stripping normalization: it
|
|
1547
|
+
* would look harmless (`ui:statistic` → `statistic`) and would in the same
|
|
1548
|
+
* motion grant rows to `element:button` and `page:card`, whose renderers
|
|
1549
|
+
* read their config out of the bag via `readProps()` and never touch these
|
|
1550
|
+
* keys on the node — re-manufacturing the evaluated-but-not-read-back half
|
|
1551
|
+
* of the very table this card exists to close. Measured on this tree: the
|
|
1552
|
+
* authored corpus spells these types bare (`statistic` 42, `card` 135,
|
|
1553
|
+
* `button` 158 nodes) and `ui:*` zero times, so verbatim is also the
|
|
1554
|
+
* spelling authors actually use. `action:button` (5 nodes) has no row and
|
|
1555
|
+
* is reported upstream rather than inferred here.
|
|
1556
|
+
*
|
|
1557
|
+
* ## Ordering and idempotence
|
|
1558
|
+
*
|
|
1559
|
+
* After the `properties` hoist deliberately, exactly like `content`: a
|
|
1560
|
+
* value arriving through that channel was already evaluated by the
|
|
1561
|
+
* `properties` leg, so it no longer carries a `${…}` and
|
|
1562
|
+
* `evaluator.evaluate` returns it unchanged. The `typeof === 'string'`
|
|
1563
|
+
* guard is doing real work rather than mirroring the line above — it stops
|
|
1564
|
+
* this loop CREATING an absent key as `undefined`, which would change what
|
|
1565
|
+
* `{ ...schema }` spreads and what `key in schema` answers downstream.
|
|
1566
|
+
*/
|
|
1567
|
+
//
|
|
1568
|
+
// Read once into a local, so the `typeof` guard narrows the value it
|
|
1569
|
+
// hands to `evaluate`. Two of the four keys are `BaseSchema` members, and
|
|
1570
|
+
// the other two are `unknown` on the working copy (objectui#11349). The
|
|
1571
|
+
// spec's carriage map is their declaration, and the guard narrows them.
|
|
1572
|
+
for (const key of expressionBindableTextKeysFor(typeof newSchema.type === 'string' ? newSchema.type : '')) {
|
|
1573
|
+
const authoredText = newSchema[key];
|
|
1574
|
+
if (typeof authoredText === 'string') {
|
|
1575
|
+
newSchema[key] = evaluator.evaluate(authoredText);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
// Evaluate 'props' — the legacy alias of the config bag.
|
|
1579
|
+
//
|
|
1580
|
+
// The guard MIRRORS the `properties` branch above rather than testing bare
|
|
1581
|
+
// truthiness (objectui#6752). Same predicate, both channels: a degenerate
|
|
1582
|
+
// bag must not have its shape reinterpreted by an object spread.
|
|
1583
|
+
//
|
|
1584
|
+
// Measured on `b76ca6764` through the real `SchemaRenderer`, before this
|
|
1585
|
+
// guard existed: a node written `{ type: 'card', props: 'not-a-bag' }`
|
|
1586
|
+
// reached `createElement` carrying NINE React props named `0` … `8`, one
|
|
1587
|
+
// per character, because `{ ...'not-a-bag' }` enumerates a string's
|
|
1588
|
+
// character indices. Nothing threw and nothing was logged, so the symptom
|
|
1589
|
+
// (a component handed nine props it never declared) sits a long way from
|
|
1590
|
+
// the `props` value that caused it.
|
|
1591
|
+
//
|
|
1592
|
+
// This guard is HALF the fix and does not on its own remove those props —
|
|
1593
|
+
// also measured, on the same base with only this line widened: the bag
|
|
1594
|
+
// reached `propsWithoutCanonicalKeys` as the authored string, was returned
|
|
1595
|
+
// unchanged, and `{ ...outgoingPropsBag }` at the `createElement` call
|
|
1596
|
+
// re-enumerated it. The other half is that function's own guard. What this
|
|
1597
|
+
// line buys is that `schema.props` keeps the value the author wrote instead
|
|
1598
|
+
// of the spread's reading of it.
|
|
1599
|
+
if (isConfigBag(newSchema.props)) {
|
|
645
1600
|
const newProps = { ...newSchema.props };
|
|
646
1601
|
for (const [key, val] of Object.entries(newProps)) {
|
|
647
|
-
|
|
1602
|
+
// objectui#9100, same guard as the `properties` branch above — the two
|
|
1603
|
+
// channels must not disagree about whether a `cel` envelope survives,
|
|
1604
|
+
// nor (objectui#7867, carrier 3) about how deep a `params` bag goes.
|
|
1605
|
+
newProps[key] = evaluateConfigValue(key, val);
|
|
648
1606
|
}
|
|
649
1607
|
newSchema.props = newProps;
|
|
650
1608
|
}
|
|
651
|
-
|
|
1609
|
+
/**
|
|
1610
|
+
* REFUSE, by name, a node-gate predicate parked under the legacy `props`
|
|
1611
|
+
* alias (objectui#9108, maintainer ruling 2026-09-13, verbatim 「同意」 on
|
|
1612
|
+
* the `domain:spec` seat's recommendation).
|
|
1613
|
+
*
|
|
1614
|
+
* ## Sited HERE, immediately in front of the two gates
|
|
1615
|
+
*
|
|
1616
|
+
* This is the one point where the gates' own input is final: the
|
|
1617
|
+
* `properties` hoist above has run, both config-bag evaluation loops have
|
|
1618
|
+
* run, and neither gate has consulted anything yet. It is also the only
|
|
1619
|
+
* placement that survives its own subject - the late diagnostics near
|
|
1620
|
+
* `createElement` are downstream of `if (shouldHide) return null`, so a node
|
|
1621
|
+
* that parks `visible` under `props` while ALSO hiding through the canonical
|
|
1622
|
+
* spelling would never reach them, and the refusal would go missing on the
|
|
1623
|
+
* one shape that carries both spellings at once.
|
|
1624
|
+
*
|
|
1625
|
+
* ## Read-only, and that is the ruled outcome rather than a limitation
|
|
1626
|
+
*
|
|
1627
|
+
* Nothing below changes. The gates still read the post-hoist node only, so
|
|
1628
|
+
* every verdict, every hoisted value and every byte the element receives is
|
|
1629
|
+
* what it was - the alias is REFUSED, not honoured. The opposite arm was
|
|
1630
|
+
* built and closed (PR objectui#9144): honouring it would have made *"8
|
|
1631
|
+
* predicate keys work while the rest stayed silently dropped - and partly
|
|
1632
|
+
* working is harder to learn from than not working"*.
|
|
1633
|
+
*
|
|
1634
|
+
* The bag handed over is {@link propsWithoutCanonicalKeys}'s, the SAME
|
|
1635
|
+
* subtraction the outgoing props bag uses, so a key the canonical bag also
|
|
1636
|
+
* declares is not reported as parked: there the author is already getting
|
|
1637
|
+
* the canonical answer (objectui#5123). The key SET is
|
|
1638
|
+
* {@link NODE_GATE_PREDICATE_KEYS}, derived from the two chain declarations
|
|
1639
|
+
* above rather than re-listed here.
|
|
1640
|
+
*/
|
|
1641
|
+
reportRefusedPropsPredicate(newSchema.type, newSchema.id, NODE_GATE_PREDICATE_KEYS, propsWithoutCanonicalKeys(newSchema.props, newSchema.properties));
|
|
1642
|
+
// Evaluate visibility: visibleWhen / visible / visibleOn / hidden / hiddenOn
|
|
652
1643
|
const shouldHide = (() => {
|
|
653
1644
|
// `visibleWhen` is the single canonical conditional-visibility predicate
|
|
654
1645
|
// across every layer since ADR-0089 (show-when-truthy), and it is the one
|
|
655
1646
|
// this shape DECLARES: `PageComponentSchema` is a closed object that
|
|
656
|
-
// declares `visibleWhen` (
|
|
1647
|
+
// declares `visibleWhen` (its old `visibility` spelling is retired, see
|
|
1648
|
+
// `WorkingCopyAuthoredKeys`) and REFUSES a
|
|
657
1649
|
// node-level `visible` outright, with the prescription "move it up one
|
|
658
1650
|
// level to the component node's own `visibleWhen` … inside `properties`
|
|
659
1651
|
// it is hoisted onto the node by the renderer but evaluated by nothing"
|
|
@@ -673,23 +1665,17 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
673
1665
|
return !evaluateVisibilityPredicate(newSchema.visibleWhen, 'visibleWhen');
|
|
674
1666
|
}
|
|
675
1667
|
// `visible` — objectui's own `BaseSchema` tier (`@object-ui/types`), and
|
|
676
|
-
// the landing spot of a hoisted `properties.visible`. Kept ABOVE
|
|
677
|
-
// deprecated
|
|
678
|
-
// spec-parsed page never reaches them, and re-ranking them would move
|
|
1668
|
+
// the landing spot of a hoisted `properties.visible`. Kept ABOVE
|
|
1669
|
+
// `BaseSchema`'s deprecated `visibleOn`: re-ranking it would move
|
|
679
1670
|
// verdicts for raw metadata that objectui#5454 did not rule on.
|
|
680
1671
|
if (newSchema.visible !== undefined) {
|
|
681
1672
|
return !evaluateVisibilityPredicate(newSchema.visible, 'visible');
|
|
682
1673
|
}
|
|
683
|
-
// @deprecated ADR-0089 → `visibleWhen`.
|
|
684
|
-
//
|
|
1674
|
+
// `BaseSchema.visibleOn`, @deprecated ADR-0089 → `visibleWhen`.
|
|
1675
|
+
// objectui's own node key; the spec's page component never declared it.
|
|
685
1676
|
if (newSchema.visibleOn !== undefined) {
|
|
686
1677
|
return !evaluateVisibilityPredicate(newSchema.visibleOn, 'visibleOn');
|
|
687
1678
|
}
|
|
688
|
-
// @deprecated ADR-0089 → `visibleWhen` (was PageNodeSchema.visibility,
|
|
689
|
-
// an ExpressionInput) — show-when-truthy, same semantics as `visibleOn`.
|
|
690
|
-
if (newSchema.visibility !== undefined) {
|
|
691
|
-
return !evaluateVisibilityPredicate(newSchema.visibility, 'visibility');
|
|
692
|
-
}
|
|
693
1679
|
// Ask "is a `hidden` gate DECLARED?" — not "is the key present?"
|
|
694
1680
|
// (objectui#3955). These two legs are the only ones in this chain whose
|
|
695
1681
|
// verdict is NOT negated, so the evaluator's single default for "there is
|
|
@@ -733,7 +1719,7 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
733
1719
|
// as an internal flag either: `_disabled` is forwarded below as a real
|
|
734
1720
|
// `disabled` prop.
|
|
735
1721
|
//
|
|
736
|
-
// The `visible` / `visibleWhen` / `visibleOn`
|
|
1722
|
+
// The `visible` / `visibleWhen` / `visibleOn` legs above keep
|
|
737
1723
|
// `!== undefined` deliberately: their `true` is NEGATED, so an empty predicate
|
|
738
1724
|
// already lands on "shown", which is what "no gate" means anyway, and
|
|
739
1725
|
// narrowing them would change ALIAS PRECEDENCE rather than fix anything. The
|
|
@@ -750,12 +1736,20 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
750
1736
|
// value, exactly as before — only the gate in front of it narrowed. An
|
|
751
1737
|
// undeclared `disabled` now falls through to `disabledOn` instead of
|
|
752
1738
|
// short-circuiting on an empty predicate.
|
|
1739
|
+
//
|
|
1740
|
+
// Both legs route through `evaluateEnablementPredicate` (objectui#6445), so
|
|
1741
|
+
// a predicate that cannot be evaluated is reported instead of silently
|
|
1742
|
+
// greying the control out. The verdicts below are unchanged: that helper
|
|
1743
|
+
// returns `evaluateCondition`'s own answer, and an UNDECLARED gate never
|
|
1744
|
+
// reaches it at all — `hasDeclaredPredicate` still decides that, one line
|
|
1745
|
+
// earlier, which is what keeps the objectui#3862 empty-shape rows silent
|
|
1746
|
+
// as well as enabled.
|
|
753
1747
|
const isDisabled = (() => {
|
|
754
1748
|
if (hasDeclaredPredicate(newSchema.disabled)) {
|
|
755
|
-
return
|
|
1749
|
+
return evaluateEnablementPredicate(newSchema.disabled, 'disabled');
|
|
756
1750
|
}
|
|
757
1751
|
if (hasDeclaredPredicate(newSchema.disabledOn)) {
|
|
758
|
-
return
|
|
1752
|
+
return evaluateEnablementPredicate(newSchema.disabledOn, 'disabledOn');
|
|
759
1753
|
}
|
|
760
1754
|
return false;
|
|
761
1755
|
})();
|
|
@@ -763,7 +1757,73 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
763
1757
|
newSchema._disabled = true;
|
|
764
1758
|
}
|
|
765
1759
|
return newSchema;
|
|
766
|
-
}, [schema, dataSource,
|
|
1760
|
+
}, [schema, dataSource, ambientScope, pageVariables, boundRecord, apiStatus, apiData]);
|
|
1761
|
+
/**
|
|
1762
|
+
* SDUI scoped styling (ADR-0065): a node's `responsiveStyles` compiles to
|
|
1763
|
+
* id-scoped CSS injected as a `<style>` tag, and a scope class is appended to
|
|
1764
|
+
* the node's className.
|
|
1765
|
+
*
|
|
1766
|
+
* ## Why this is a memo, and why it is HERE and not at its use site
|
|
1767
|
+
*
|
|
1768
|
+
* The scope-class branch rebuilds the schema object (`{ ...evaluatedSchema,
|
|
1769
|
+
* className: mergedClassName }`) so renderers that read `schema.className`
|
|
1770
|
+
* see the scope class. Unmemoised, that spread allocated a NEW object on
|
|
1771
|
+
* every render of this component — even when the `evaluatedSchema` memo
|
|
1772
|
+
* directly above it HELD — so every downstream renderer that memoises on
|
|
1773
|
+
* `[schema]` (e.g. `ObjectMap`'s `dataConfig` / `mapConfig`, and the whole
|
|
1774
|
+
* marker cascade below them) saw a fresh identity and re-ran (objectui#6270).
|
|
1775
|
+
* Only nodes taking this branch were affected: a plain node was handed
|
|
1776
|
+
* `evaluatedSchema` itself and was already stable.
|
|
1777
|
+
*
|
|
1778
|
+
* It is hoisted ABOVE the early returns below because it is a HOOK. Its use
|
|
1779
|
+
* site sits after `if (!evaluatedSchema) return null`, the `_hidden` return
|
|
1780
|
+
* and the unresolved-component returns, so a `useMemo` written there is a
|
|
1781
|
+
* CONDITIONAL hook: a node toggling hidden -> visible would call one more
|
|
1782
|
+
* hook than the previous render and React would throw "Rendered more hooks
|
|
1783
|
+
* than during the previous render". Hoisting is what makes the memo legal,
|
|
1784
|
+
* not a stylistic preference.
|
|
1785
|
+
*
|
|
1786
|
+
* `[evaluatedSchema, autoStyleId]` is the complete dependency set: every
|
|
1787
|
+
* value below is a pure function of the evaluated node (`className`, `id`,
|
|
1788
|
+
* `responsiveStyles`) and the `useId` fallback. `evaluatedSchema` is itself a
|
|
1789
|
+
* fresh object whenever anything it interpolates changes, so a genuinely
|
|
1790
|
+
* changed className, value or breakpoint still produces a NEW identity here —
|
|
1791
|
+
* memoising cannot make a live value go stale.
|
|
1792
|
+
*
|
|
1793
|
+
* Non-object / primitive nodes fall through untouched: the render pass below
|
|
1794
|
+
* returns them as text before any of this is read.
|
|
1795
|
+
*/
|
|
1796
|
+
const scopedStyling = useMemo(() => {
|
|
1797
|
+
const node = evaluatedSchema;
|
|
1798
|
+
if (!node || typeof node !== 'object') {
|
|
1799
|
+
return { scopeClass: '', scopedCss: '', mergedClassName: undefined, schemaForComponent: node };
|
|
1800
|
+
}
|
|
1801
|
+
// `unknown` on the working copy (objectui#11349); `hasResponsiveStyles`
|
|
1802
|
+
// narrows it to the spec's `ResponsiveStyles` before anything compiles it.
|
|
1803
|
+
const responsiveStyles = node.responsiveStyles;
|
|
1804
|
+
if (!hasResponsiveStyles(responsiveStyles)) {
|
|
1805
|
+
// No scope class: hand the component `evaluatedSchema` ITSELF, which the
|
|
1806
|
+
// memo above already keeps stable. Never a copy — a copy here would
|
|
1807
|
+
// reintroduce the same instability for every node in the tree.
|
|
1808
|
+
return {
|
|
1809
|
+
scopeClass: '',
|
|
1810
|
+
scopedCss: '',
|
|
1811
|
+
mergedClassName: node.className,
|
|
1812
|
+
schemaForComponent: node,
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
const scopeClass = scopeClassFor(node.id ?? autoStyleId);
|
|
1816
|
+
const mergedClassName = [node.className, scopeClass].filter(Boolean).join(' ');
|
|
1817
|
+
return {
|
|
1818
|
+
scopeClass,
|
|
1819
|
+
scopedCss: compileScopedStyles(`.${scopeClass}`, responsiveStyles),
|
|
1820
|
+
mergedClassName,
|
|
1821
|
+
// Some renderers read `schema.className` directly (e.g. element:text)
|
|
1822
|
+
// while others read the `className` prop (e.g. flex/container). Set both
|
|
1823
|
+
// so the scope class lands regardless of which channel a renderer honours.
|
|
1824
|
+
schemaForComponent: { ...node, className: mergedClassName },
|
|
1825
|
+
};
|
|
1826
|
+
}, [evaluatedSchema, autoStyleId]);
|
|
767
1827
|
if (!evaluatedSchema)
|
|
768
1828
|
return null;
|
|
769
1829
|
// If schema is just a string, render it as text
|
|
@@ -776,6 +1836,17 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
776
1836
|
// component type: undefined" box, which said nothing true about the input.
|
|
777
1837
|
if (typeof evaluatedSchema !== 'object')
|
|
778
1838
|
return _jsx(_Fragment, { children: String(evaluatedSchema) });
|
|
1839
|
+
// The API binding's non-final states (objectui#12197), drawn in place of the
|
|
1840
|
+
// node — before its visibility, which may read `data` and so cannot be
|
|
1841
|
+
// decided yet. A refused binding names the spec's refusal; nothing was read.
|
|
1842
|
+
// The error panel titles itself in the page's language: its translation
|
|
1843
|
+
// hook mounts with it, never in this body, which runs for every node.
|
|
1844
|
+
if (apiStatus === 'loading') {
|
|
1845
|
+
return _jsx(ElementDataSourceLoadingPanel, { testId: String(evaluatedSchema.type) });
|
|
1846
|
+
}
|
|
1847
|
+
if (apiStatus === 'refused' || apiStatus === 'error') {
|
|
1848
|
+
return (_jsx(ElementApiDataSourcePanel, { testId: String(evaluatedSchema.type), state: apiStatus, message: apiBinding.error }));
|
|
1849
|
+
}
|
|
779
1850
|
// Handle visibility: if evaluated schema is hidden, render nothing.
|
|
780
1851
|
//
|
|
781
1852
|
// Reads AFTER the primitive narrowing above (objectui#4548) so the property
|
|
@@ -806,12 +1877,15 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
806
1877
|
}
|
|
807
1878
|
debugLog('schema', 'Component not found in registry', { type: evaluatedSchema.type });
|
|
808
1879
|
const errorInfo = ERROR_CODES['OBJUI-001'];
|
|
809
|
-
|
|
1880
|
+
// objectui#5247 — the lookup above still missed and this node still fails.
|
|
1881
|
+
// The suggestion only names the spelling that would have resolved.
|
|
1882
|
+
const caseSuggestion = suggestTypeByCase(evaluatedSchema.type);
|
|
1883
|
+
return (_jsxs("div", { className: "p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2", role: "alert", children: [_jsxs("p", { className: "font-medium", children: ["Unknown component type: ", _jsx("strong", { children: evaluatedSchema.type }), caseSuggestion !== undefined ? (_jsxs(_Fragment, { children: [" — did you mean '", _jsx("strong", { children: caseSuggestion }), "'?"] })) : null] }), lazyError && (_jsxs("p", { className: "text-xs mt-1", children: ["Failed to load plugin: ", lazyError.message] })), globalThis.process?.env?.NODE_ENV !== 'production' && (_jsxs("p", { className: "text-xs mt-1", children: ["\uD83D\uDCA1 ", errorInfo.suggestion, " (OBJUI-001)"] })), _jsx("pre", { className: "text-xs mt-2 overflow-auto", children: JSON.stringify(evaluatedSchema, null, 2) })] }));
|
|
810
1884
|
}
|
|
811
1885
|
// Note: We don't forward the ref to the Component because components in the registry
|
|
812
1886
|
// may not support refs. The SchemaRenderer itself can still receive refs for its own use.
|
|
813
1887
|
// Extract schema metadata properties that should NOT be passed as React props
|
|
814
|
-
const { type: _type, children: _children, body: _body, schema: _schema, visible: _visible, visibleWhen: _visibleWhen, visibleOn: _visibleOn,
|
|
1888
|
+
const { type: _type, children: _children, body: _body, schema: _schema, visible: _visible, visibleWhen: _visibleWhen, visibleOn: _visibleOn, hidden: _hidden, hiddenOn: _hiddenOn, disabled: _disabled, disabledOn: _disabledOn,
|
|
815
1889
|
// stripped: `PageComponentSchema.dataSource` is the spec's per-element data
|
|
816
1890
|
// BINDING (`{ object, view, filter, sort, limit }`) — schema metadata, like
|
|
817
1891
|
// `visibleWhen` above, not a visual prop. Renderers that consume it read
|
|
@@ -823,7 +1897,20 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
823
1897
|
// block reported "Couldn't load records" — writing the binding the spec
|
|
824
1898
|
// documents BROKE the component (objectstack#5576). An explicit React
|
|
825
1899
|
// `dataSource` prop is unaffected: it arrives via `...props`, spread last.
|
|
826
|
-
dataSource: _dataSource,
|
|
1900
|
+
dataSource: _dataSource,
|
|
1901
|
+
// stripped: `BaseSchema.testId` is the author's stable-locator handle, and
|
|
1902
|
+
// this renderer is what makes its declaration true — it is re-emitted as
|
|
1903
|
+
// `data-testid` beside `data-obj-id` below (objectui#8268, ADR-0054 C4).
|
|
1904
|
+
// Leaving it in the spread was the defect: React does not recognise a
|
|
1905
|
+
// `testId` DOM prop, so it landed as a non-standard lowercase `testid`
|
|
1906
|
+
// attribute that no test-library query helper looks for, while React's own
|
|
1907
|
+
// dev warning told the author to spell it `testid` — steering them further
|
|
1908
|
+
// from the documented `data-testid`.
|
|
1909
|
+
testId: _testId,
|
|
1910
|
+
// stripped: a RUNTIME prop a host injects, never authorable metadata —
|
|
1911
|
+
// decision batch #70 (objectui#7742); objectui#8818 closes the key here.
|
|
1912
|
+
// The `props` alias carrier is closed by `withoutAuthoredObjectFields`.
|
|
1913
|
+
objectFields: _objectFields, _hidden: __hidden, // stripped: internal visibility flag
|
|
827
1914
|
_disabled: __disabled, // stripped: internal disabled flag
|
|
828
1915
|
responsiveStyles: _responsiveStyles, // stripped: compiled to scoped CSS, not a DOM prop
|
|
829
1916
|
...componentProps } = evaluatedSchema;
|
|
@@ -831,35 +1918,146 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
831
1918
|
// `${…}` about to be placed verbatim in front of a user.
|
|
832
1919
|
//
|
|
833
1920
|
// Sited HERE, after the metadata destructure, on purpose. `componentProps` is
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
1921
|
+
// the set of values the node OFFERS the component — spread as React props
|
|
1922
|
+
// below, and the same values renderers read as `schema.<key>`. Everything the
|
|
1923
|
+
// destructure stripped is schema METADATA:
|
|
837
1924
|
// `visible` / `visibleWhen` / `hidden` / `disabled` / … hold raw predicate
|
|
838
1925
|
// SOURCE by design (they are evaluated as conditions, never placed), so
|
|
839
1926
|
// scanning before the strip would report every correctly-authored predicate
|
|
840
1927
|
// in the repo. The strip list is therefore the diagnostic's exclusion list,
|
|
841
1928
|
// for free and without a second copy of it to drift.
|
|
842
1929
|
//
|
|
1930
|
+
// ⚠️ It scans `componentProps` and the raw `props` / `properties` bags, NOT
|
|
1931
|
+
// the narrowed bags the objectui#9571 / objectui#9758 strips build below.
|
|
1932
|
+
// Deliberate, and the objectui#8268 `testId` precedent applied a second time:
|
|
1933
|
+
// `data` loses its PROPS seat on the object arm, but it is still authored,
|
|
1934
|
+
// still evaluated, and still read off the schema by the block — so an
|
|
1935
|
+
// unevaluated expression inside it is still in front of a user, and excluding
|
|
1936
|
+
// it here would narrow objectui#4795's coverage by one key for exactly the
|
|
1937
|
+
// blocks the ruling touches. That holds for BOTH carriers: an authored
|
|
1938
|
+
// `props: { data: '${…}' }` is still reported after objectui#9758 refused it
|
|
1939
|
+
// a prop seat.
|
|
1940
|
+
//
|
|
843
1941
|
// Read-only: it reports what evaluation already produced and changes nothing
|
|
844
1942
|
// about what is rendered — no DOM attribute either, so no snapshot moves.
|
|
845
1943
|
if (__DEV__) {
|
|
846
|
-
|
|
1944
|
+
// `testId` is stripped above but stays in THIS scan set. The strip list is
|
|
1945
|
+
// this diagnostic's exclusion list because every other member holds raw
|
|
1946
|
+
// predicate SOURCE by design; `testId` holds a literal, so excluding it
|
|
1947
|
+
// would silently narrow objectui#4795's coverage by one key — measured on
|
|
1948
|
+
// `93127bd6f`, an authored `testId: '${data.x}'` was reported before this
|
|
1949
|
+
// change, and must still be.
|
|
1950
|
+
reportUnevaluatedExpressions(schema, evaluatedSchema.type, evaluatedSchema.id, _testId === undefined ? componentProps : { ...componentProps, testId: _testId }, evaluatedSchema.properties, evaluatedSchema.props);
|
|
1951
|
+
}
|
|
1952
|
+
// The legacy `props` alias, narrowed exactly as objectui#5123 ruled: for a
|
|
1953
|
+
// key BOTH bags declare, `properties` wins here as it already wins in
|
|
1954
|
+
// `readProps()`, so one key has one answer on both channels.
|
|
1955
|
+
//
|
|
1956
|
+
// HOISTED out of the `createElement` call below (objectui#6708) so the
|
|
1957
|
+
// diagnostic and the spread read the SAME bag. It is the same pure call with
|
|
1958
|
+
// the same arguments producing the same object in the same spread position —
|
|
1959
|
+
// nothing about what any renderer receives moves — but it removes the one way
|
|
1960
|
+
// this diagnostic could go wrong: reporting a set of keys that is not the set
|
|
1961
|
+
// actually handed to the component.
|
|
1962
|
+
//
|
|
1963
|
+
// objectui#9758 narrows it once more, for the same key and on the same arm as
|
|
1964
|
+
// the strip below — see {@link withoutAuthoredDataKey}. The arm is read ONCE,
|
|
1965
|
+
// here, and handed to both call sites: two independent lookups of the same
|
|
1966
|
+
// question is the shape that let the alias keep the seat in the first place.
|
|
1967
|
+
const refusesAuthoredDataProp = recordSourceDataArmForType(evaluatedSchema.type) === 'view-data';
|
|
1968
|
+
// objectui#8818 strips `objectFields` HERE, before the `data` strip below:
|
|
1969
|
+
// that strip's diagnostic decides by identity, so it must compare against a
|
|
1970
|
+
// bag this strip has already settled.
|
|
1971
|
+
const aliasBagAsAuthored = withoutAuthoredObjectFields(propsWithoutCanonicalKeys(evaluatedSchema.props, evaluatedSchema.properties));
|
|
1972
|
+
const outgoingPropsBag = withoutAuthoredDataKey(aliasBagAsAuthored, refusesAuthoredDataProp);
|
|
1973
|
+
if (__DEV__ && outgoingPropsBag !== aliasBagAsAuthored) {
|
|
1974
|
+
reportRefusedDataPropSpread(evaluatedSchema.type, evaluatedSchema.id);
|
|
1975
|
+
}
|
|
1976
|
+
// Dev-build diagnostic (objectui#6708, maintainer ruling 2026-08-29, option
|
|
1977
|
+
// 2): those keys are spread as React props and never hoisted onto the node,
|
|
1978
|
+
// so a renderer that reads its config from `schema` — every family except
|
|
1979
|
+
// `element:*`'s `readProps()` — drops them without a word.
|
|
1980
|
+
//
|
|
1981
|
+
// Sited HERE, beside its objectui#4795 neighbour and after the metadata
|
|
1982
|
+
// destructure, for the same reason: this is the point where what leaves this
|
|
1983
|
+
// component for the renderer is finally known. Read-only — it reports what
|
|
1984
|
+
// the line above already computed and changes nothing that is rendered.
|
|
1985
|
+
//
|
|
1986
|
+
// The AUTHORED bag comes from `schema`, not `evaluatedSchema`: the evaluation
|
|
1987
|
+
// memo rebuilds `props` with an object spread, which turns a degenerate
|
|
1988
|
+
// `props: 'text'` into `{ '0': 't', … }` long before this line. See
|
|
1989
|
+
// `collectDroppedPropsKeys`.
|
|
1990
|
+
//
|
|
1991
|
+
// ⚠️ It reads the POST-strip bag (objectui#9758), and that direction is the
|
|
1992
|
+
// opposite of its objectui#4795 neighbour above ON PURPOSE. This message
|
|
1993
|
+
// states that the key "is spread as React props on the created element" and
|
|
1994
|
+
// tells the author to "write them under `properties` instead" — after the
|
|
1995
|
+
// strip BOTH sentences are false for `data` on this arm: it is not spread,
|
|
1996
|
+
// and the canonical spelling loses the seat as well. Its subject stops being
|
|
1997
|
+
// true, so it stops naming the key, and `reportRefusedDataPropSpread` is what
|
|
1998
|
+
// tells the author instead. The objectui#4795 scan reads the WIDER bag for
|
|
1999
|
+
// the mirror-image reason: an unevaluated `${…}` is still in front of a user
|
|
2000
|
+
// after the strip, so its subject survives.
|
|
2001
|
+
if (__DEV__) {
|
|
2002
|
+
reportDroppedPropsBag(evaluatedSchema.type, evaluatedSchema.id, schema?.props, outgoingPropsBag);
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* objectui#9571 (ruling objectui#8348 Q2-C, decision batch #136 item 3,
|
|
2006
|
+
* maintainer 「同意」): an authored `data` key does NOT take a React prop seat
|
|
2007
|
+
* on a block whose published `data` row is the OBJECT arm (`ViewData`).
|
|
2008
|
+
*
|
|
2009
|
+
* ## The defect this closes
|
|
2010
|
+
*
|
|
2011
|
+
* The spread below hands every non-metadata node key to the component, so an
|
|
2012
|
+
* authored `data` reached such a block TWICE — as `schema.data`, which the
|
|
2013
|
+
* shared ladder judges against the block's row (`resolveRecordSourceConfig`,
|
|
2014
|
+
* objectui#8348), and as the `data` PROP, which `ObjectGrid` (`passedData`),
|
|
2015
|
+
* `ObjectMap` (`dataProp`) and `ObjectGantt` each lift with an unconditional
|
|
2016
|
+
* `Array.isArray` at HIGHER priority than that ladder. The ruling had one
|
|
2017
|
+
* carrier it did not reach, and it was the one that wins: a bare array under
|
|
2018
|
+
* `data` still drew through this renderer after the ladder had retired it.
|
|
2019
|
+
*
|
|
2020
|
+
* ## ⛔ The HOST path is NOT touched, and that is the ruled half
|
|
2021
|
+
*
|
|
2022
|
+
* Option B — gating the PROP on the arm — was refused, because the prop is how
|
|
2023
|
+
* a host (`plugin-list`'s `ListView`, `ObjectView`) hands down rows it already
|
|
2024
|
+
* fetched. Only the AUTHORED key loses its seat: `...props` below is this
|
|
2025
|
+
* component's own React props, spread LAST, so a host rendering
|
|
2026
|
+
* `<SchemaRenderer data={rows} …/>` still delivers `rows`, and a host that
|
|
2027
|
+
* renders the block component directly never passes through here at all.
|
|
2028
|
+
*
|
|
2029
|
+
* ## Scope is the ARM, not a block list
|
|
2030
|
+
*
|
|
2031
|
+
* `recordSourceDataArmForType` answers per registered type, and every type it
|
|
2032
|
+
* does not list keeps today's behaviour verbatim. ⛔ No allowlist of block
|
|
2033
|
+
* NAMES is minted here — that is the second de-facto contract AGENTS.md #0.1
|
|
2034
|
+
* forbids, and the ruling is stated over the arm, not over a census.
|
|
2035
|
+
*
|
|
2036
|
+
* ## Byte-identical when nothing is authored
|
|
2037
|
+
*
|
|
2038
|
+
* The `in` test comes FIRST, so a node that authors no `data` is handed
|
|
2039
|
+
* `componentProps` itself — the same object, in the same spread position, with
|
|
2040
|
+
* no copy allocated. Same discipline as the conditional `data-testid` below,
|
|
2041
|
+
* and for the same measured reason.
|
|
2042
|
+
*
|
|
2043
|
+
* ## ⚠️ TWO bags reach the spread, and this one is only the first
|
|
2044
|
+
*
|
|
2045
|
+
* objectui#9758: the legacy `props` alias bag is spread AFTER this one, so for
|
|
2046
|
+
* the first ruling's whole life an author who spelled the identical key
|
|
2047
|
+
* `props: { data: [...] }` kept the seat this block takes away — the arm
|
|
2048
|
+
* predicate was right and the corpus was one bag short. Both bags now go
|
|
2049
|
+
* through {@link withoutAuthoredDataKey} with the ONE arm reading computed
|
|
2050
|
+
* above (decision batch #167 item 2, letter 剥, maintainer 「其他同意」).
|
|
2051
|
+
* ⛔ Still no validator refusal: whether the alias exists at all is
|
|
2052
|
+
* objectui#4795's pending question ②, and nothing else it carries moves.
|
|
2053
|
+
*/
|
|
2054
|
+
const outgoingComponentProps = withoutAuthoredDataKey(componentProps, refusesAuthoredDataProp);
|
|
2055
|
+
if (__DEV__ && outgoingComponentProps !== componentProps) {
|
|
2056
|
+
reportRefusedDataPropSpread(evaluatedSchema.type, evaluatedSchema.id);
|
|
847
2057
|
}
|
|
848
|
-
// SDUI scoped styling (ADR-0065)
|
|
849
|
-
//
|
|
850
|
-
|
|
851
|
-
const hasScopedStyles = hasResponsiveStyles(_responsiveStyles);
|
|
852
|
-
const scopeClass = hasScopedStyles ? scopeClassFor(evaluatedSchema.id ?? autoStyleId) : '';
|
|
853
|
-
const scopedCss = hasScopedStyles ? compileScopedStyles(`.${scopeClass}`, _responsiveStyles) : '';
|
|
854
|
-
const mergedClassName = scopeClass
|
|
855
|
-
? [evaluatedSchema.className, scopeClass].filter(Boolean).join(' ')
|
|
856
|
-
: evaluatedSchema.className;
|
|
857
|
-
// Some renderers read `schema.className` directly (e.g. element:text) while
|
|
858
|
-
// others read the `className` prop (e.g. flex/container). Set both so the
|
|
859
|
-
// scope class lands regardless of which channel a renderer honours.
|
|
860
|
-
const schemaForComponent = scopeClass
|
|
861
|
-
? { ...evaluatedSchema, className: mergedClassName }
|
|
862
|
-
: evaluatedSchema;
|
|
2058
|
+
// SDUI scoped styling (ADR-0065) — computed in the memo hoisted above the
|
|
2059
|
+
// early returns; see the doc comment there for why it cannot live here.
|
|
2060
|
+
const { scopeClass, scopedCss, mergedClassName, schemaForComponent } = scopedStyling;
|
|
863
2061
|
// Extract AriaPropsSchema properties for accessibility
|
|
864
2062
|
const ariaProps = resolveAriaProps(evaluatedSchema);
|
|
865
2063
|
// Debug-mode enhancements: extra data attributes + perf tracking
|
|
@@ -875,23 +2073,45 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
875
2073
|
const renderStart = isDebug ? performance.now() : 0;
|
|
876
2074
|
const rendered = (_jsxs(SchemaErrorBoundary, { componentType: evaluatedSchema.type, resetKey: evaluatedSchema.id ?? null, children: [scopedCss ? (_jsx("style", { "data-os-scope": scopeClass, dangerouslySetInnerHTML: { __html: scopedCss } })) : null, React.createElement(Component, {
|
|
877
2075
|
schema: schemaForComponent,
|
|
878
|
-
|
|
2076
|
+
// Spread non-metadata schema properties as props — minus an authored
|
|
2077
|
+
// `data` on the object arm, which the objectui#9571 strip above removed.
|
|
2078
|
+
...outgoingComponentProps,
|
|
879
2079
|
// The legacy `props` alias still overrides plain top-level keys, but no
|
|
880
|
-
// longer overrides the canonical `properties` bag
|
|
881
|
-
//
|
|
882
|
-
// `
|
|
883
|
-
//
|
|
884
|
-
|
|
2080
|
+
// longer overrides the canonical `properties` bag (objectui#5123,
|
|
2081
|
+
// maintainer ruling 2026-08-18) — and, since objectui#9758, no longer
|
|
2082
|
+
// re-seats an authored `data` on the object arm that the spread above
|
|
2083
|
+
// just refused. Computed above rather than inline, so the objectui#6708
|
|
2084
|
+
// diagnostic names this exact bag — see there.
|
|
2085
|
+
...outgoingPropsBag,
|
|
885
2086
|
...ariaProps, // Inject ARIA attributes from AriaPropsSchema
|
|
886
2087
|
...debugAttrs, // Debug-mode data attributes
|
|
887
2088
|
disabled: __disabled || undefined,
|
|
888
2089
|
className: mergedClassName,
|
|
889
2090
|
'data-obj-id': evaluatedSchema.id,
|
|
890
2091
|
'data-obj-type': evaluatedSchema.type,
|
|
2092
|
+
// objectui#8268 — `BaseSchema.testId`'s declaration promises this
|
|
2093
|
+
// attribute. Emitted here, not spread above, so the author gets the
|
|
2094
|
+
// `data-testid` every query helper looks for instead of the
|
|
2095
|
+
// non-standard `testid` the raw spread produced.
|
|
2096
|
+
//
|
|
2097
|
+
// ⚠️ CONDITIONALLY, and that is load-bearing, not tidiness. Passing
|
|
2098
|
+
// `'data-testid': undefined` unconditionally would put the key in every
|
|
2099
|
+
// node's props bag, and a component that sets its own `data-testid` and
|
|
2100
|
+
// then spreads what it is handed — the ADR-0054 C4 shape, which many
|
|
2101
|
+
// renderers here have — would have its locator overwritten with
|
|
2102
|
+
// `undefined` and lose it. Measured: the unconditional form turned 9
|
|
2103
|
+
// tests red across 5 files, including the three that assert the props
|
|
2104
|
+
// bag is byte-for-byte unchanged (objectui#6708 / #6752 / #6760). A node
|
|
2105
|
+
// that authors no `testId` must be byte-identical to before this card.
|
|
2106
|
+
...(_testId === undefined ? {} : { 'data-testid': _testId }),
|
|
891
2107
|
...(__DEV__ && !_validation.valid ? { 'data-obj-schema-invalid': 'true' } : {}),
|
|
892
2108
|
...props
|
|
893
2109
|
})] }));
|
|
894
2110
|
debugTimeEnd(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
|
|
2111
|
+
// The bound value reaches the subtree as `data` (see "The API binding").
|
|
2112
|
+
const output = apiStatus === 'ready'
|
|
2113
|
+
? _jsx(PredicateScopeProvider, { scope: subtreeScope, children: rendered })
|
|
2114
|
+
: rendered;
|
|
895
2115
|
// Report render perf to DebugCollector when debug mode is active
|
|
896
2116
|
if (isDebug && renderStart) {
|
|
897
2117
|
const durationMs = performance.now() - renderStart;
|
|
@@ -902,6 +2122,6 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
902
2122
|
timestamp: Date.now(),
|
|
903
2123
|
});
|
|
904
2124
|
}
|
|
905
|
-
return
|
|
2125
|
+
return output;
|
|
906
2126
|
});
|
|
907
2127
|
SchemaRenderer.displayName = 'SchemaRenderer';
|