@object-ui/react 17.5.0 → 17.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4100 -0
- package/README.md +303 -46
- package/dist/LazyPluginLoader.d.ts +14 -7
- package/dist/LazyPluginLoader.js +14 -7
- package/dist/SchemaRenderer.d.ts +38 -7
- package/dist/SchemaRenderer.js +1603 -60
- package/dist/context/ActionContext.js +40 -1
- package/dist/context/DiscussionContext.d.ts +12 -0
- package/dist/context/DiscussionContext.js +2 -0
- package/dist/context/DrillNavigationContext.d.ts +13 -0
- package/dist/context/HostNavigationContext.d.ts +82 -0
- package/dist/context/HostNavigationContext.js +65 -0
- package/dist/context/InlineEditContext.d.ts +81 -1
- package/dist/context/InlineEditContext.js +8 -2
- package/dist/context/RecordContext.d.ts +48 -5
- package/dist/context/RecordContext.js +10 -1
- package/dist/context/RelatedRecordActionsContext.d.ts +20 -1
- package/dist/context/SchemaRendererContext.d.ts +54 -2
- package/dist/context/SchemaRendererContext.js +38 -5
- package/dist/context/ThemeContext.js +1 -1
- package/dist/context/actionRunnerTranslator.d.ts +27 -0
- package/dist/context/actionRunnerTranslator.js +37 -0
- package/dist/context/index.d.ts +13 -12
- package/dist/context/index.js +13 -12
- package/dist/element-data-source/ElementDataSourceGate.d.ts +163 -3
- package/dist/element-data-source/ElementDataSourceGate.js +407 -15
- package/dist/hooks/index.d.ts +37 -35
- package/dist/hooks/index.js +44 -35
- package/dist/hooks/useActionEngine.js +47 -4
- package/dist/hooks/useActionRunner.js +4 -0
- package/dist/hooks/useActionTextLocalizer.d.ts +21 -8
- package/dist/hooks/useActionTextLocalizer.js +33 -8
- package/dist/hooks/useCapabilityGate.js +6 -5
- package/dist/hooks/useConfigBagEvaluator.d.ts +30 -0
- package/dist/hooks/useConfigBagEvaluator.js +39 -0
- package/dist/hooks/useCrudShortcuts.js +1 -1
- package/dist/hooks/useDatasetDimensionLabels.js +1 -1
- package/dist/hooks/useDensityMode.d.ts +6 -2
- package/dist/hooks/useDensityMode.js +6 -2
- package/dist/hooks/useDiscovery.d.ts +1 -10
- package/dist/hooks/useDiscovery.js +1 -1
- package/dist/hooks/useETagCache.js +25 -4
- package/dist/hooks/useElementDataSource.d.ts +15 -2
- package/dist/hooks/useElementDataSource.js +29 -9
- package/dist/hooks/useExpression.d.ts +52 -38
- package/dist/hooks/useExpression.js +62 -42
- package/dist/hooks/useFilterScope.d.ts +22 -2
- package/dist/hooks/useFilterScope.js +29 -4
- package/dist/hooks/useGlobalUndo.d.ts +10 -0
- package/dist/hooks/useGlobalUndo.js +112 -46
- package/dist/hooks/useNavigationOverlay.d.ts +77 -12
- package/dist/hooks/useNavigationOverlay.js +119 -9
- package/dist/hooks/useOffline.js +65 -11
- package/dist/hooks/usePageAssignment.d.ts +8 -5
- package/dist/hooks/usePageAssignment.js +38 -17
- package/dist/hooks/usePageTransition.js +1 -1
- package/dist/hooks/usePageVariableActionBridge.js +2 -2
- package/dist/hooks/useRecordSearch.d.ts +34 -1
- package/dist/hooks/useRecordSearch.js +42 -7
- package/dist/hooks/useResolvedFilter.d.ts +46 -0
- package/dist/hooks/useResolvedFilter.js +130 -0
- package/dist/hooks/useSchemaPersistence.d.ts +7 -0
- package/dist/hooks/useSchemaPersistence.js +144 -5
- package/dist/hooks/useSettledSchema.d.ts +123 -0
- package/dist/hooks/useSettledSchema.js +186 -0
- package/dist/hooks/useViewData.d.ts +7 -2
- package/dist/hooks/useViewData.js +1 -1
- package/dist/hooks/useViewTransition.js +1 -1
- package/dist/index.d.ts +18 -11
- package/dist/index.js +54 -10
- package/dist/schema-input.d.ts +55 -14
- package/dist/schema-input.js +55 -12
- package/dist/utils/configBag.d.ts +68 -0
- package/dist/utils/configBag.js +70 -0
- package/dist/utils/error-message.d.ts +59 -0
- package/dist/utils/error-message.js +116 -1
- package/dist/utils/inlineAria.d.ts +61 -0
- package/dist/utils/inlineAria.js +70 -0
- package/dist/utils/nonGridRowCeiling.d.ts +44 -0
- package/dist/utils/nonGridRowCeiling.js +78 -0
- package/dist/utils/paramsBag.d.ts +28 -0
- package/dist/utils/paramsBag.js +146 -0
- package/dist/utils/propsBagDiagnostic.d.ts +196 -0
- package/dist/utils/propsBagDiagnostic.js +421 -0
- package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
- package/dist/utils/refusedDataPropDiagnostic.js +94 -0
- package/dist/utils/unevaluatedExpression.d.ts +110 -0
- package/dist/utils/unevaluatedExpression.js +257 -0
- package/dist/utils/visibilityDiagnostic.d.ts +371 -0
- package/dist/utils/visibilityDiagnostic.js +551 -0
- package/package.json +10 -10
- package/dist/hooks/useClientNotifications.d.ts +0 -47
- package/dist/hooks/useClientNotifications.js +0 -162
- package/dist/spec-bridge/SpecBridge.d.ts +0 -28
- package/dist/spec-bridge/SpecBridge.js +0 -56
- package/dist/spec-bridge/bridges/form-view.d.ts +0 -82
- package/dist/spec-bridge/bridges/form-view.js +0 -126
- package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
- package/dist/spec-bridge/bridges/list-view.js +0 -241
- package/dist/spec-bridge/index.d.ts +0 -11
- package/dist/spec-bridge/index.js +0 -10
- package/dist/spec-bridge/types.d.ts +0 -55
- package/dist/spec-bridge/types.js +0 -8
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Dev-build diagnostic: an authored `data` key that did NOT reach the block as
|
|
10
|
+
* a React prop, because that block's published `data` row is the `ViewData`
|
|
11
|
+
* OBJECT arm (objectui#9571, extended to the legacy `props` alias bag by
|
|
12
|
+
* objectui#9758).
|
|
13
|
+
*
|
|
14
|
+
* ## Why the diagnostic lives HERE and not in `SchemaRenderer.tsx` (objectui#9959)
|
|
15
|
+
*
|
|
16
|
+
* It was born as module state inside `SchemaRenderer.tsx`, next to the two call
|
|
17
|
+
* sites that emit it. Every other dev diagnostic in this directory —
|
|
18
|
+
* `propsBagDiagnostic.ts`'s two and `visibilityDiagnostic.ts`'s one — pairs its
|
|
19
|
+
* dedupe `Set` with an exported test-only reset, and each of those resets says
|
|
20
|
+
* in its own docblock what it exists to prevent. This one had no reset, and it
|
|
21
|
+
* could not simply grow one where it stood: `index.ts` re-exports
|
|
22
|
+
* `SchemaRenderer.js` with a star, so a `__reset` added there would have been a
|
|
23
|
+
* new member of this package's PUBLISHED surface — a contract change, for a
|
|
24
|
+
* test affordance. Moving the `Set`, the emit and the reset into this module
|
|
25
|
+
* puts all three where their siblings already live and adds nothing to the
|
|
26
|
+
* published entry, which is the whole shape of the remedy.
|
|
27
|
+
*
|
|
28
|
+
* ## The caller owns the production gate
|
|
29
|
+
*
|
|
30
|
+
* Same contract as `reportDroppedPropsBag` next door: `SchemaRenderer` applies
|
|
31
|
+
* `__DEV__` at the call site, so this stays one dev-only branch there and the
|
|
32
|
+
* whole module is dead code in a production build.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Warn ONCE per distinct refused node, not once per render (objectui#9571).
|
|
36
|
+
*
|
|
37
|
+
* The strip at the call site runs on every render of the node, and a warning
|
|
38
|
+
* that floods the console is a warning that gets muted — the same warn-once
|
|
39
|
+
* discipline `ObjectMap`'s legacy-config notice and the visibility-predicate
|
|
40
|
+
* diagnostics already use. Keyed on the node's type plus its id, which is the
|
|
41
|
+
* pair an author can act on; a node with no `id` is keyed on its type alone and
|
|
42
|
+
* so warns once per type, which is the honest ceiling for something that cannot
|
|
43
|
+
* be told apart.
|
|
44
|
+
*/
|
|
45
|
+
const _warnedRefusedDataPropNodes = new Set();
|
|
46
|
+
/**
|
|
47
|
+
* Test-only reset for the dedupe above. Without it the second test to assert
|
|
48
|
+
* the same warning reads the first test's dedupe entry and sees silence — a
|
|
49
|
+
* green run that checked nothing.
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ The dangerous shape is the ABSENCE assertion, and it is why this export is
|
|
52
|
+
* a correctness affordance rather than a convenience: a test that asserts this
|
|
53
|
+
* warning did NOT fire for a `type#id` an earlier test already warned for is
|
|
54
|
+
* green whatever the renderer does, and it reports that green as coverage
|
|
55
|
+
* (objectui#9959). `SchemaRenderer.refusedDataPropWarnReset-9959.test.tsx`
|
|
56
|
+
* renders one `type#id` across separate `it()` blocks and goes red when this
|
|
57
|
+
* body stops clearing.
|
|
58
|
+
*/
|
|
59
|
+
export function __resetRefusedDataPropWarnings() {
|
|
60
|
+
_warnedRefusedDataPropNodes.clear();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Say why an authored `data` key did not reach the block (objectui#9571,
|
|
64
|
+
* ruling objectui#8348 Q2-C, decision batch #136 item 3, maintainer 「同意」).
|
|
65
|
+
*
|
|
66
|
+
* ## Why this diagnostic is part of the change and not decoration
|
|
67
|
+
*
|
|
68
|
+
* ⛔ MEASURED, and it corrects the card's own premise: the ladder emits NO
|
|
69
|
+
* runtime signal when it refuses an off-arm `data`. `resolveRecordSourceConfig`
|
|
70
|
+
* returns `null` and falls through silently, and `validateSchema` — the
|
|
71
|
+
* `__DEV__` pass in `SchemaRenderer.tsx` — never reads `data` against the
|
|
72
|
+
* block's row at all. The refusal is loud at AUTHORING time (`os validate`, the
|
|
73
|
+
* save gate and the zod row all reject a bare array) and mute at render time.
|
|
74
|
+
*
|
|
75
|
+
* So without this line, retiring the props carrier is exactly the failure shape
|
|
76
|
+
* AGENTS.md #0.1 and `ObjectGrid`'s own column diagnostic exist to prevent:
|
|
77
|
+
* renderer and author disagree, and the author gets a success receipt — a page
|
|
78
|
+
* whose rows were on screen yesterday is blank today, with nothing anywhere
|
|
79
|
+
* naming the key that was dropped or the spelling that would work.
|
|
80
|
+
*
|
|
81
|
+
* Read-only and `__DEV__`-only: it reports the decision made at the call site
|
|
82
|
+
* and changes nothing about what is rendered.
|
|
83
|
+
*/
|
|
84
|
+
export function reportRefusedDataPropSpread(type, id) {
|
|
85
|
+
const key = id === undefined ? type : `${type}#${id}`;
|
|
86
|
+
if (_warnedRefusedDataPropNodes.has(key))
|
|
87
|
+
return;
|
|
88
|
+
_warnedRefusedDataPropNodes.add(key);
|
|
89
|
+
console.warn(`[ObjectUI] SchemaRenderer: the authored \`data\` key on <${type}${id === undefined ? '' : ` id="${id}"`}> was NOT passed to the component as a React prop. This block's published ` +
|
|
90
|
+
'`data` row is the `ViewData` OBJECT arm, so `data` is read from the schema ' +
|
|
91
|
+
'and judged by that row (objectui#8348). Inline rows go at ' +
|
|
92
|
+
'`data: { provider: "value", items: [...] }`; a bare array under `data` is ' +
|
|
93
|
+
'refused. A HOST passing rows down as a React `data` prop is unaffected.');
|
|
94
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Mirrors `ExpressionEvaluator.evaluate`: a `${` with no closing brace, or an
|
|
10
|
+
* empty `${}`, is not an expression to the evaluator (its interpolation regex
|
|
11
|
+
* requires at least one character), so it is not one here either. Matching the
|
|
12
|
+
* PRODUCER's definition is what keeps this from reporting strings the engine
|
|
13
|
+
* never intended to touch — prose and code samples that merely contain `${`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function findExpressionSources(value: string): string[];
|
|
16
|
+
/** Which authored channel a residual expression was found on. */
|
|
17
|
+
export type UnevaluatedChannel = 'schema' | 'properties' | 'props';
|
|
18
|
+
export interface UnevaluatedExpressionFinding {
|
|
19
|
+
/**
|
|
20
|
+
* The key holding the raw source — or, for a leaf of a `params` bag, that
|
|
21
|
+
* leaf's path from the bag (`params.target.id`, `params.ids[0]`).
|
|
22
|
+
*/
|
|
23
|
+
key: string;
|
|
24
|
+
/** The authored channel the key was read from. */
|
|
25
|
+
channel: UnevaluatedChannel;
|
|
26
|
+
/** The value, verbatim, as it will be handed to the renderer. */
|
|
27
|
+
value: string;
|
|
28
|
+
/** The `${…}` fragments inside it. */
|
|
29
|
+
expressions: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Collect every unevaluated `${…}` that is about to be handed to a renderer.
|
|
33
|
+
*
|
|
34
|
+
* Pure and side-effect free — the reporting half is {@link reportUnevaluatedExpressions}.
|
|
35
|
+
* Kept separate so the scan can be asserted directly: a diagnostic whose only
|
|
36
|
+
* test is "a spy was called" goes green the moment someone no-ops it.
|
|
37
|
+
*
|
|
38
|
+
* @param spread The post-strip top-level bag — exactly what `SchemaRenderer`
|
|
39
|
+
* spreads as React props, which is also what renderers read as `schema.<key>`.
|
|
40
|
+
* Schema METADATA (`visible` / `hidden` / `disabled` / … ) must already be
|
|
41
|
+
* removed by the caller: those keys carry raw predicate source BY DESIGN and
|
|
42
|
+
* are evaluated as conditions, never placed.
|
|
43
|
+
* @param properties The node's `properties` bag, if any. Reported in
|
|
44
|
+
* preference to the top-level copy the hoist makes of it, so the finding
|
|
45
|
+
* names the key the author actually wrote.
|
|
46
|
+
* @param props The node's legacy `props` bag, if any.
|
|
47
|
+
*/
|
|
48
|
+
export declare function collectUnevaluatedExpressions(spread: Record<string, unknown> | undefined | null, properties?: unknown, props?: unknown): UnevaluatedExpressionFinding[];
|
|
49
|
+
/**
|
|
50
|
+
* Prefix every diagnostic line starts with. Exported so tests can match on it
|
|
51
|
+
* and so an app can filter it out of its console transport if it must.
|
|
52
|
+
*/
|
|
53
|
+
export declare const UNEVALUATED_EXPRESSION_PREFIX = "[ObjectUI] Unevaluated expression reached the DOM";
|
|
54
|
+
/**
|
|
55
|
+
* Build the message. Separate from the emit so a test can assert the words a
|
|
56
|
+
* developer is going to read, not merely that something was logged.
|
|
57
|
+
*
|
|
58
|
+
* ## Why `properties.*` is in the channel list (objectui#7849)
|
|
59
|
+
*
|
|
60
|
+
* It was missing, and the omission CONTRADICTED the sibling diagnostic in
|
|
61
|
+
* `propsBagDiagnostic.ts`, which tells an author who wrote the same key one
|
|
62
|
+
* envelope out: *"`props` is NOT hoisted onto the node — only `properties.*`
|
|
63
|
+
* is … Write them under `properties` instead."* An author whose node trips
|
|
64
|
+
* both messages was told to use `properties` by one and, by the other, that
|
|
65
|
+
* the only channels that work are `content` or host-side resolution.
|
|
66
|
+
*
|
|
67
|
+
* The enumeration was the wrong half. `properties` is not a channel on its way
|
|
68
|
+
* out and there is no retirement on record for it: `@objectstack/spec@17.2.0`
|
|
69
|
+
* calls the vocabulary carried there *"ALIVE — this is not dead surface to
|
|
70
|
+
* retire under ADR-0049"*, keeps `PageComponentSchema.properties` as the open
|
|
71
|
+
* carrier ON PURPOSE, gates it at the authoring door
|
|
72
|
+
* (`validate-component-props.ts`), and tombstones no part of it via
|
|
73
|
+
* `retiredKey()`. In THIS repo `props` — not `properties` — is the spelling
|
|
74
|
+
* annotated as the legacy alias (`SchemaRenderer.tsx`: "the legacy `props`
|
|
75
|
+
* bag, minus every key the canonical `properties` bag also …").
|
|
76
|
+
*
|
|
77
|
+
* Measured on this branch's base through the real renderers, not inferred:
|
|
78
|
+
* `badge` with `properties.label = '${data.status}'` renders `completed` and
|
|
79
|
+
* applies the variant classes; `card` with a conditional
|
|
80
|
+
* `properties.className` renders `class="… border-red-500"`; the same keys
|
|
81
|
+
* under `props` render an EMPTY badge body and no `border-red-500`.
|
|
82
|
+
*
|
|
83
|
+
* The words are borrowed from the sibling message on purpose — one hoist, one
|
|
84
|
+
* vocabulary, so the two diagnostics cannot drift apart again. Pinned by
|
|
85
|
+
* `__tests__/diagnosticChannelConsistency.test.ts`.
|
|
86
|
+
*
|
|
87
|
+
* ## Why `params` is in it too (objectui#7867)
|
|
88
|
+
*
|
|
89
|
+
* The same defect #7849 closed, one channel later: once every string leaf of a
|
|
90
|
+
* `params` bag evaluates, an enumeration that left `params` out would tell an
|
|
91
|
+
* author whose `params.recordId` template THREW that `params` is not a channel
|
|
92
|
+
* at all — and send them to move a correct key somewhere else. A finding on a
|
|
93
|
+
* `params` leaf means the second sentence above (the expression threw, or the
|
|
94
|
+
* value it produced still reads as a template), never the first.
|
|
95
|
+
*/
|
|
96
|
+
export declare function formatUnevaluatedExpressionMessage(type: unknown, id: unknown, findings: UnevaluatedExpressionFinding[]): string;
|
|
97
|
+
/**
|
|
98
|
+
* Dev-build only. Scans, and on a hit reports once per schema object via
|
|
99
|
+
* `console.error`.
|
|
100
|
+
*
|
|
101
|
+
* `console.error` and not `warn`: this is the loud refusal objectui#4795's
|
|
102
|
+
* ruling asked for, and it follows the assertion shape objectui#5092 landed in
|
|
103
|
+
* SchemaForm. (It is also the level that survives happy-dom, which swallows
|
|
104
|
+
* `console.log`.)
|
|
105
|
+
*
|
|
106
|
+
* The caller applies the production gate, so this stays a single dev-only
|
|
107
|
+
* branch at the call site and the whole module is dead code in a production
|
|
108
|
+
* build.
|
|
109
|
+
*/
|
|
110
|
+
export declare function reportUnevaluatedExpressions(schema: object | null | undefined, type: unknown, id: unknown, spread: Record<string, unknown> | undefined | null, properties?: unknown, props?: unknown): UnevaluatedExpressionFinding[];
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
import { isConfigBag } from './configBag.js';
|
|
9
|
+
import { PARAMS_KEY, isParamsBag, mapParamsLeaves } from './paramsBag.js';
|
|
10
|
+
/**
|
|
11
|
+
* Dev-build diagnostic: an UNEVALUATED template expression reached the DOM.
|
|
12
|
+
*
|
|
13
|
+
* ## The defect this names (objectui#4795, Direction 3)
|
|
14
|
+
*
|
|
15
|
+
* A value only reaches the user if it passes two gates: `SchemaRenderer`
|
|
16
|
+
* must EVALUATE it, and the renderer must READ IT BACK. Those two sets do not
|
|
17
|
+
* fully overlap, and where they miss, the failure is SILENT — measured on a
|
|
18
|
+
* real render with `dataSource: { total: 99 }`:
|
|
19
|
+
*
|
|
20
|
+
* { type: 'ui:statistic', value: '${data.n}' } -> screen shows
|
|
21
|
+
* `${data.n}`
|
|
22
|
+
* { type: 'ui:card', props: { title: '${data.total}' } } -> card body EMPTY
|
|
23
|
+
* (the evaluated
|
|
24
|
+
* `99` landed on a
|
|
25
|
+
* DOM attribute)
|
|
26
|
+
*
|
|
27
|
+
* The first shape is what this module catches: the memo in `SchemaRenderer`
|
|
28
|
+
* evaluates `content`, the `properties` / `props` bags and the predicate keys,
|
|
29
|
+
* and passes every other top-level key through untouched — so the raw source
|
|
30
|
+
* text is what the renderer receives, and it is placed verbatim. An author
|
|
31
|
+
* (increasingly, an AI authoring metadata) gets no signal at all: a literal
|
|
32
|
+
* `${data.n}` on screen reads like a data problem, not a contract violation.
|
|
33
|
+
*
|
|
34
|
+
* The second shape this module also catches, in its residue form: when an
|
|
35
|
+
* expression IS evaluated but the evaluation THROWS, `ExpressionEvaluator`
|
|
36
|
+
* returns the original source verbatim (`return match` in its interpolation
|
|
37
|
+
* branch, `defaultValue ?? expression` in its outer catch), so a failed
|
|
38
|
+
* evaluation is indistinguishable on screen from a key that was never
|
|
39
|
+
* evaluated at all. Both end as raw `${…}` in front of a user.
|
|
40
|
+
*
|
|
41
|
+
* ## What this deliberately does NOT do
|
|
42
|
+
*
|
|
43
|
+
* It changes NO evaluation behaviour — it reads the schema after the memo has
|
|
44
|
+
* run and reports. Widening the set of evaluated keys is objectui#4795's
|
|
45
|
+
* Direction 1, which the maintainer DEFERRED behind a named restart condition
|
|
46
|
+
* (ruling of 2026-08-17); this diagnostic is Direction 3 and must not
|
|
47
|
+
* pre-empt it. Merging `props` into the node (Direction 2) is permanently
|
|
48
|
+
* rejected.
|
|
49
|
+
*
|
|
50
|
+
* ## Depth: exactly the evaluator's own radius, and not one level more
|
|
51
|
+
*
|
|
52
|
+
* The scan is SHALLOW because evaluation is shallow. `ExpressionEvaluator`
|
|
53
|
+
* returns every non-string untouched, so a nested `aria: { label: '${…}' }`
|
|
54
|
+
* under either bag keeps its raw source today — deliberately, and pinned as
|
|
55
|
+
* such by objectui#4799's parity tests. A deeper scan would therefore report,
|
|
56
|
+
* as a defect, a shape the repo has explicitly decided not to change yet: the
|
|
57
|
+
* diagnostic would be louder than the contract it speaks for. Deepening
|
|
58
|
+
* evaluation is a separate decision, and this scan follows it the day it is
|
|
59
|
+
* taken, not before.
|
|
60
|
+
*
|
|
61
|
+
* That decision has been taken for exactly ONE key: `params` (objectui#7867,
|
|
62
|
+
* ruling A). Every string leaf of a `params` bag is evaluated, at any depth,
|
|
63
|
+
* on every channel — so this scan walks the same leaves, through the same
|
|
64
|
+
* walker (`paramsBag.ts`, which the evaluation memo also uses), and a template
|
|
65
|
+
* that still cannot resolve is reported by its path (`params.target.id`,
|
|
66
|
+
* `properties.params.ids[0]`). Radius equal to evaluation by construction,
|
|
67
|
+
* not by two copies agreeing; every other key stays shallow on both sides.
|
|
68
|
+
*/
|
|
69
|
+
/** What counts as an expression — the evaluator's own definition. */
|
|
70
|
+
const EXPRESSION_PATTERN = /\$\{([^}]+)\}/g;
|
|
71
|
+
/**
|
|
72
|
+
* Mirrors `ExpressionEvaluator.evaluate`: a `${` with no closing brace, or an
|
|
73
|
+
* empty `${}`, is not an expression to the evaluator (its interpolation regex
|
|
74
|
+
* requires at least one character), so it is not one here either. Matching the
|
|
75
|
+
* PRODUCER's definition is what keeps this from reporting strings the engine
|
|
76
|
+
* never intended to touch — prose and code samples that merely contain `${`.
|
|
77
|
+
*/
|
|
78
|
+
export function findExpressionSources(value) {
|
|
79
|
+
const found = [];
|
|
80
|
+
// `matchAll` needs the /g flag and a fresh lastIndex per call; the literal
|
|
81
|
+
// above is module-scope, so reset before use rather than allocating a regex
|
|
82
|
+
// per value (this runs per key, per node, per render, in dev).
|
|
83
|
+
EXPRESSION_PATTERN.lastIndex = 0;
|
|
84
|
+
let match = EXPRESSION_PATTERN.exec(value);
|
|
85
|
+
while (match !== null) {
|
|
86
|
+
found.push(match[0]);
|
|
87
|
+
match = EXPRESSION_PATTERN.exec(value);
|
|
88
|
+
}
|
|
89
|
+
return found;
|
|
90
|
+
}
|
|
91
|
+
function pushIfUnevaluated(key, channel, value, into) {
|
|
92
|
+
const expressions = findExpressionSources(value);
|
|
93
|
+
if (expressions.length > 0) {
|
|
94
|
+
into.push({ key, channel, value, expressions });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function scanBag(bag, channel, into, skip) {
|
|
98
|
+
if (!isConfigBag(bag))
|
|
99
|
+
return;
|
|
100
|
+
for (const [key, value] of Object.entries(bag)) {
|
|
101
|
+
if (skip?.(key, value))
|
|
102
|
+
continue;
|
|
103
|
+
// objectui#7867 — `params` is evaluated leaf-deep, so it is scanned
|
|
104
|
+
// leaf-deep, by the evaluator's own walker; the finding's `key` is the
|
|
105
|
+
// leaf's path (`params.target.id`). The visitor hands every leaf back
|
|
106
|
+
// unchanged, so the walk allocates nothing.
|
|
107
|
+
if (key === PARAMS_KEY && isParamsBag(value)) {
|
|
108
|
+
mapParamsLeaves(value, (leaf, path) => {
|
|
109
|
+
pushIfUnevaluated(path, channel, leaf, into);
|
|
110
|
+
return leaf;
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (typeof value !== 'string')
|
|
115
|
+
continue;
|
|
116
|
+
pushIfUnevaluated(key, channel, value, into);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Collect every unevaluated `${…}` that is about to be handed to a renderer.
|
|
121
|
+
*
|
|
122
|
+
* Pure and side-effect free — the reporting half is {@link reportUnevaluatedExpressions}.
|
|
123
|
+
* Kept separate so the scan can be asserted directly: a diagnostic whose only
|
|
124
|
+
* test is "a spy was called" goes green the moment someone no-ops it.
|
|
125
|
+
*
|
|
126
|
+
* @param spread The post-strip top-level bag — exactly what `SchemaRenderer`
|
|
127
|
+
* spreads as React props, which is also what renderers read as `schema.<key>`.
|
|
128
|
+
* Schema METADATA (`visible` / `hidden` / `disabled` / … ) must already be
|
|
129
|
+
* removed by the caller: those keys carry raw predicate source BY DESIGN and
|
|
130
|
+
* are evaluated as conditions, never placed.
|
|
131
|
+
* @param properties The node's `properties` bag, if any. Reported in
|
|
132
|
+
* preference to the top-level copy the hoist makes of it, so the finding
|
|
133
|
+
* names the key the author actually wrote.
|
|
134
|
+
* @param props The node's legacy `props` bag, if any.
|
|
135
|
+
*/
|
|
136
|
+
export function collectUnevaluatedExpressions(spread, properties, props) {
|
|
137
|
+
const findings = [];
|
|
138
|
+
scanBag(properties, 'properties', findings);
|
|
139
|
+
// The hoist copies every `properties.*` value onto the node's top level, so
|
|
140
|
+
// the same residue is visible twice. Report the authored spelling only.
|
|
141
|
+
const hoisted = isConfigBag(properties) ? properties : undefined;
|
|
142
|
+
scanBag(spread, 'schema', findings, (key, value) => hoisted?.[key] === value);
|
|
143
|
+
scanBag(props, 'props', findings);
|
|
144
|
+
return findings;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Prefix every diagnostic line starts with. Exported so tests can match on it
|
|
148
|
+
* and so an app can filter it out of its console transport if it must.
|
|
149
|
+
*/
|
|
150
|
+
export const UNEVALUATED_EXPRESSION_PREFIX = '[ObjectUI] Unevaluated expression reached the DOM';
|
|
151
|
+
/** Where a finding sits, spelled the way an author would search for it. */
|
|
152
|
+
function locate(finding) {
|
|
153
|
+
switch (finding.channel) {
|
|
154
|
+
case 'properties':
|
|
155
|
+
return 'properties.' + finding.key;
|
|
156
|
+
case 'props':
|
|
157
|
+
return 'props.' + finding.key;
|
|
158
|
+
default:
|
|
159
|
+
return finding.key;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Build the message. Separate from the emit so a test can assert the words a
|
|
164
|
+
* developer is going to read, not merely that something was logged.
|
|
165
|
+
*
|
|
166
|
+
* ## Why `properties.*` is in the channel list (objectui#7849)
|
|
167
|
+
*
|
|
168
|
+
* It was missing, and the omission CONTRADICTED the sibling diagnostic in
|
|
169
|
+
* `propsBagDiagnostic.ts`, which tells an author who wrote the same key one
|
|
170
|
+
* envelope out: *"`props` is NOT hoisted onto the node — only `properties.*`
|
|
171
|
+
* is … Write them under `properties` instead."* An author whose node trips
|
|
172
|
+
* both messages was told to use `properties` by one and, by the other, that
|
|
173
|
+
* the only channels that work are `content` or host-side resolution.
|
|
174
|
+
*
|
|
175
|
+
* The enumeration was the wrong half. `properties` is not a channel on its way
|
|
176
|
+
* out and there is no retirement on record for it: `@objectstack/spec@17.2.0`
|
|
177
|
+
* calls the vocabulary carried there *"ALIVE — this is not dead surface to
|
|
178
|
+
* retire under ADR-0049"*, keeps `PageComponentSchema.properties` as the open
|
|
179
|
+
* carrier ON PURPOSE, gates it at the authoring door
|
|
180
|
+
* (`validate-component-props.ts`), and tombstones no part of it via
|
|
181
|
+
* `retiredKey()`. In THIS repo `props` — not `properties` — is the spelling
|
|
182
|
+
* annotated as the legacy alias (`SchemaRenderer.tsx`: "the legacy `props`
|
|
183
|
+
* bag, minus every key the canonical `properties` bag also …").
|
|
184
|
+
*
|
|
185
|
+
* Measured on this branch's base through the real renderers, not inferred:
|
|
186
|
+
* `badge` with `properties.label = '${data.status}'` renders `completed` and
|
|
187
|
+
* applies the variant classes; `card` with a conditional
|
|
188
|
+
* `properties.className` renders `class="… border-red-500"`; the same keys
|
|
189
|
+
* under `props` render an EMPTY badge body and no `border-red-500`.
|
|
190
|
+
*
|
|
191
|
+
* The words are borrowed from the sibling message on purpose — one hoist, one
|
|
192
|
+
* vocabulary, so the two diagnostics cannot drift apart again. Pinned by
|
|
193
|
+
* `__tests__/diagnosticChannelConsistency.test.ts`.
|
|
194
|
+
*
|
|
195
|
+
* ## Why `params` is in it too (objectui#7867)
|
|
196
|
+
*
|
|
197
|
+
* The same defect #7849 closed, one channel later: once every string leaf of a
|
|
198
|
+
* `params` bag evaluates, an enumeration that left `params` out would tell an
|
|
199
|
+
* author whose `params.recordId` template THREW that `params` is not a channel
|
|
200
|
+
* at all — and send them to move a correct key somewhere else. A finding on a
|
|
201
|
+
* `params` leaf means the second sentence above (the expression threw, or the
|
|
202
|
+
* value it produced still reads as a template), never the first.
|
|
203
|
+
*/
|
|
204
|
+
export function formatUnevaluatedExpressionMessage(type, id, findings) {
|
|
205
|
+
const node = typeof type === 'string' && type ? '"' + type + '"' : '(untyped node)';
|
|
206
|
+
const where = typeof id === 'string' && id ? ' (id: "' + id + '")' : '';
|
|
207
|
+
const lines = findings.map(f => {
|
|
208
|
+
const site = locate(f);
|
|
209
|
+
return (' - ' + site + ': ' + JSON.stringify(f.value) +
|
|
210
|
+
' [' + f.expressions.join(', ') + ']');
|
|
211
|
+
});
|
|
212
|
+
return (UNEVALUATED_EXPRESSION_PREFIX + ' - node ' + node + where + '\n' +
|
|
213
|
+
lines.join('\n') + '\n' +
|
|
214
|
+
'The value above is placed VERBATIM: it is handed to the renderer, and spread\n' +
|
|
215
|
+
'to the DOM, with its source text intact. Either SchemaRenderer does not\n' +
|
|
216
|
+
'evaluate this key, or the expression threw and the evaluator returned the\n' +
|
|
217
|
+
'source unchanged.\n' +
|
|
218
|
+
'Channels that do evaluate and read back today: `content`; `properties.*`,\n' +
|
|
219
|
+
'which is evaluated and then HOISTED onto the node, so a renderer declared\n' +
|
|
220
|
+
'as `({ schema })` reads it back as `schema.<key>`; every string leaf of a\n' +
|
|
221
|
+
'`params` bag, at any depth, node-level or under `properties`; or resolve\n' +
|
|
222
|
+
'the value in the host before handing the schema to SchemaRenderer.');
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Reported nodes, so a re-render (or the post-mount forceUpdate that picks up
|
|
226
|
+
* lazy plugin registrations) does not repeat the message. Keyed on the ORIGINAL
|
|
227
|
+
* schema object, matching `validateSchemaOnce`'s dedup: the evaluated copy is a
|
|
228
|
+
* fresh object on every memo recompute and would dedup nothing.
|
|
229
|
+
*/
|
|
230
|
+
const _reportedNodes = typeof WeakSet !== 'undefined'
|
|
231
|
+
? new WeakSet()
|
|
232
|
+
: { add() { }, has() { return false; } };
|
|
233
|
+
/**
|
|
234
|
+
* Dev-build only. Scans, and on a hit reports once per schema object via
|
|
235
|
+
* `console.error`.
|
|
236
|
+
*
|
|
237
|
+
* `console.error` and not `warn`: this is the loud refusal objectui#4795's
|
|
238
|
+
* ruling asked for, and it follows the assertion shape objectui#5092 landed in
|
|
239
|
+
* SchemaForm. (It is also the level that survives happy-dom, which swallows
|
|
240
|
+
* `console.log`.)
|
|
241
|
+
*
|
|
242
|
+
* The caller applies the production gate, so this stays a single dev-only
|
|
243
|
+
* branch at the call site and the whole module is dead code in a production
|
|
244
|
+
* build.
|
|
245
|
+
*/
|
|
246
|
+
export function reportUnevaluatedExpressions(schema, type, id, spread, properties, props) {
|
|
247
|
+
const findings = collectUnevaluatedExpressions(spread, properties, props);
|
|
248
|
+
if (findings.length === 0)
|
|
249
|
+
return findings;
|
|
250
|
+
if (schema && typeof schema === 'object') {
|
|
251
|
+
if (_reportedNodes.has(schema))
|
|
252
|
+
return findings;
|
|
253
|
+
_reportedNodes.add(schema);
|
|
254
|
+
}
|
|
255
|
+
console.error(formatUnevaluatedExpressionMessage(type, id, findings));
|
|
256
|
+
return findings;
|
|
257
|
+
}
|