@object-ui/react 17.5.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +4100 -0
  2. package/README.md +303 -46
  3. package/dist/LazyPluginLoader.d.ts +14 -7
  4. package/dist/LazyPluginLoader.js +14 -7
  5. package/dist/SchemaRenderer.d.ts +38 -7
  6. package/dist/SchemaRenderer.js +1603 -60
  7. package/dist/context/ActionContext.js +40 -1
  8. package/dist/context/DiscussionContext.d.ts +12 -0
  9. package/dist/context/DiscussionContext.js +2 -0
  10. package/dist/context/DrillNavigationContext.d.ts +13 -0
  11. package/dist/context/HostNavigationContext.d.ts +82 -0
  12. package/dist/context/HostNavigationContext.js +65 -0
  13. package/dist/context/InlineEditContext.d.ts +81 -1
  14. package/dist/context/InlineEditContext.js +8 -2
  15. package/dist/context/RecordContext.d.ts +48 -5
  16. package/dist/context/RecordContext.js +10 -1
  17. package/dist/context/RelatedRecordActionsContext.d.ts +20 -1
  18. package/dist/context/SchemaRendererContext.d.ts +54 -2
  19. package/dist/context/SchemaRendererContext.js +38 -5
  20. package/dist/context/ThemeContext.js +1 -1
  21. package/dist/context/actionRunnerTranslator.d.ts +27 -0
  22. package/dist/context/actionRunnerTranslator.js +37 -0
  23. package/dist/context/index.d.ts +13 -12
  24. package/dist/context/index.js +13 -12
  25. package/dist/element-data-source/ElementDataSourceGate.d.ts +163 -3
  26. package/dist/element-data-source/ElementDataSourceGate.js +407 -15
  27. package/dist/hooks/index.d.ts +37 -35
  28. package/dist/hooks/index.js +44 -35
  29. package/dist/hooks/useActionEngine.js +47 -4
  30. package/dist/hooks/useActionRunner.js +4 -0
  31. package/dist/hooks/useActionTextLocalizer.d.ts +21 -8
  32. package/dist/hooks/useActionTextLocalizer.js +33 -8
  33. package/dist/hooks/useCapabilityGate.js +6 -5
  34. package/dist/hooks/useConfigBagEvaluator.d.ts +30 -0
  35. package/dist/hooks/useConfigBagEvaluator.js +39 -0
  36. package/dist/hooks/useCrudShortcuts.js +1 -1
  37. package/dist/hooks/useDatasetDimensionLabels.js +1 -1
  38. package/dist/hooks/useDensityMode.d.ts +6 -2
  39. package/dist/hooks/useDensityMode.js +6 -2
  40. package/dist/hooks/useDiscovery.d.ts +1 -10
  41. package/dist/hooks/useDiscovery.js +1 -1
  42. package/dist/hooks/useETagCache.js +25 -4
  43. package/dist/hooks/useElementDataSource.d.ts +15 -2
  44. package/dist/hooks/useElementDataSource.js +29 -9
  45. package/dist/hooks/useExpression.d.ts +52 -38
  46. package/dist/hooks/useExpression.js +62 -42
  47. package/dist/hooks/useFilterScope.d.ts +22 -2
  48. package/dist/hooks/useFilterScope.js +29 -4
  49. package/dist/hooks/useGlobalUndo.d.ts +10 -0
  50. package/dist/hooks/useGlobalUndo.js +112 -46
  51. package/dist/hooks/useNavigationOverlay.d.ts +77 -12
  52. package/dist/hooks/useNavigationOverlay.js +119 -9
  53. package/dist/hooks/useOffline.js +65 -11
  54. package/dist/hooks/usePageAssignment.d.ts +8 -5
  55. package/dist/hooks/usePageAssignment.js +38 -17
  56. package/dist/hooks/usePageTransition.js +1 -1
  57. package/dist/hooks/usePageVariableActionBridge.js +2 -2
  58. package/dist/hooks/useRecordSearch.d.ts +34 -1
  59. package/dist/hooks/useRecordSearch.js +42 -7
  60. package/dist/hooks/useResolvedFilter.d.ts +46 -0
  61. package/dist/hooks/useResolvedFilter.js +130 -0
  62. package/dist/hooks/useSchemaPersistence.d.ts +7 -0
  63. package/dist/hooks/useSchemaPersistence.js +144 -5
  64. package/dist/hooks/useSettledSchema.d.ts +123 -0
  65. package/dist/hooks/useSettledSchema.js +186 -0
  66. package/dist/hooks/useViewData.d.ts +7 -2
  67. package/dist/hooks/useViewData.js +1 -1
  68. package/dist/hooks/useViewTransition.js +1 -1
  69. package/dist/index.d.ts +18 -11
  70. package/dist/index.js +54 -10
  71. package/dist/schema-input.d.ts +55 -14
  72. package/dist/schema-input.js +55 -12
  73. package/dist/utils/configBag.d.ts +68 -0
  74. package/dist/utils/configBag.js +70 -0
  75. package/dist/utils/error-message.d.ts +59 -0
  76. package/dist/utils/error-message.js +116 -1
  77. package/dist/utils/inlineAria.d.ts +61 -0
  78. package/dist/utils/inlineAria.js +70 -0
  79. package/dist/utils/nonGridRowCeiling.d.ts +44 -0
  80. package/dist/utils/nonGridRowCeiling.js +78 -0
  81. package/dist/utils/paramsBag.d.ts +28 -0
  82. package/dist/utils/paramsBag.js +146 -0
  83. package/dist/utils/propsBagDiagnostic.d.ts +196 -0
  84. package/dist/utils/propsBagDiagnostic.js +421 -0
  85. package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
  86. package/dist/utils/refusedDataPropDiagnostic.js +94 -0
  87. package/dist/utils/unevaluatedExpression.d.ts +110 -0
  88. package/dist/utils/unevaluatedExpression.js +257 -0
  89. package/dist/utils/visibilityDiagnostic.d.ts +371 -0
  90. package/dist/utils/visibilityDiagnostic.js +551 -0
  91. package/package.json +10 -10
  92. package/dist/hooks/useClientNotifications.d.ts +0 -47
  93. package/dist/hooks/useClientNotifications.js +0 -162
  94. package/dist/spec-bridge/SpecBridge.d.ts +0 -28
  95. package/dist/spec-bridge/SpecBridge.js +0 -56
  96. package/dist/spec-bridge/bridges/form-view.d.ts +0 -82
  97. package/dist/spec-bridge/bridges/form-view.js +0 -126
  98. package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
  99. package/dist/spec-bridge/bridges/list-view.js +0 -241
  100. package/dist/spec-bridge/index.d.ts +0 -11
  101. package/dist/spec-bridge/index.js +0 -10
  102. package/dist/spec-bridge/types.d.ts +0 -55
  103. package/dist/spec-bridge/types.js +0 -8
@@ -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
+ }