@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.
Files changed (103) hide show
  1. package/CHANGELOG.md +4263 -0
  2. package/README.md +383 -42
  3. package/dist/SchemaRenderer.d.ts +64 -7
  4. package/dist/SchemaRenderer.js +1326 -106
  5. package/dist/context/ActionContext.js +40 -1
  6. package/dist/context/AppShellContext.d.ts +27 -0
  7. package/dist/context/DiscussionContext.d.ts +27 -0
  8. package/dist/context/DiscussionContext.js +4 -0
  9. package/dist/context/DrillNavigationContext.d.ts +13 -0
  10. package/dist/context/InlineEditContext.d.ts +151 -1
  11. package/dist/context/InlineEditContext.js +36 -2
  12. package/dist/context/PrintPageContext.d.ts +60 -0
  13. package/dist/context/PrintPageContext.js +32 -0
  14. package/dist/context/RecordContext.d.ts +48 -5
  15. package/dist/context/RecordContext.js +10 -1
  16. package/dist/context/RelatedRecordActionsContext.d.ts +72 -2
  17. package/dist/context/RelatedRecordActionsContext.js +4 -0
  18. package/dist/context/SchemaRendererContext.d.ts +61 -6
  19. package/dist/context/SchemaRendererContext.js +39 -6
  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 +1 -0
  24. package/dist/context/index.js +1 -0
  25. package/dist/element-data-source/ElementApiDataSourcePanel.d.ts +28 -0
  26. package/dist/element-data-source/ElementApiDataSourcePanel.js +10 -0
  27. package/dist/element-data-source/ElementDataSourceGate.d.ts +118 -3
  28. package/dist/element-data-source/ElementDataSourceGate.js +401 -19
  29. package/dist/element-data-source/useElementApiDataSource.d.ts +119 -0
  30. package/dist/element-data-source/useElementApiDataSource.js +198 -0
  31. package/dist/hooks/index.d.ts +3 -1
  32. package/dist/hooks/index.js +10 -1
  33. package/dist/hooks/useActionEngine.js +46 -3
  34. package/dist/hooks/useActionRunner.js +4 -0
  35. package/dist/hooks/useActionTextLocalizer.d.ts +35 -9
  36. package/dist/hooks/useActionTextLocalizer.js +70 -14
  37. package/dist/hooks/useConfigBagEvaluator.d.ts +31 -0
  38. package/dist/hooks/useConfigBagEvaluator.js +40 -0
  39. package/dist/hooks/useDatasetDimensionLabels.js +25 -10
  40. package/dist/hooks/useDensityMode.d.ts +8 -2
  41. package/dist/hooks/useDensityMode.js +8 -2
  42. package/dist/hooks/useDiscovery.d.ts +1 -10
  43. package/dist/hooks/useETagCache.js +25 -4
  44. package/dist/hooks/useElementDataSource.d.ts +15 -2
  45. package/dist/hooks/useElementDataSource.js +24 -7
  46. package/dist/hooks/useExpression.d.ts +52 -59
  47. package/dist/hooks/useExpression.js +58 -90
  48. package/dist/hooks/useFilterScope.d.ts +22 -2
  49. package/dist/hooks/useFilterScope.js +29 -4
  50. package/dist/hooks/useGlobalUndo.d.ts +10 -0
  51. package/dist/hooks/useGlobalUndo.js +112 -46
  52. package/dist/hooks/useNavigationOverlay.d.ts +76 -11
  53. package/dist/hooks/useNavigationOverlay.js +119 -9
  54. package/dist/hooks/useOffline.js +65 -11
  55. package/dist/hooks/usePageAssignment.d.ts +33 -7
  56. package/dist/hooks/usePageAssignment.js +85 -21
  57. package/dist/hooks/useRecordSearch.d.ts +34 -1
  58. package/dist/hooks/useRecordSearch.js +42 -7
  59. package/dist/hooks/useResolvedFilter.d.ts +46 -0
  60. package/dist/hooks/useResolvedFilter.js +130 -0
  61. package/dist/hooks/useSchemaPersistence.d.ts +7 -0
  62. package/dist/hooks/useSchemaPersistence.js +144 -5
  63. package/dist/hooks/useSettledSchema.d.ts +123 -0
  64. package/dist/hooks/useSettledSchema.js +186 -0
  65. package/dist/hooks/useViewData.d.ts +10 -3
  66. package/dist/hooks/useViewData.js +5 -14
  67. package/dist/index.d.ts +9 -1
  68. package/dist/index.js +48 -1
  69. package/dist/schema-input.d.ts +38 -8
  70. package/dist/schema-input.js +39 -7
  71. package/dist/utils/configBag.d.ts +68 -0
  72. package/dist/utils/configBag.js +70 -0
  73. package/dist/utils/error-message.d.ts +1 -1
  74. package/dist/utils/error-message.js +1 -1
  75. package/dist/utils/inlineAria.d.ts +61 -0
  76. package/dist/utils/inlineAria.js +70 -0
  77. package/dist/utils/localeMap.d.ts +27 -0
  78. package/dist/utils/localeMap.js +106 -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 +422 -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 +44 -1
  88. package/dist/utils/unevaluatedExpression.js +92 -11
  89. package/dist/utils/visibilityDiagnostic.d.ts +282 -21
  90. package/dist/utils/visibilityDiagnostic.js +321 -46
  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 -158
  97. package/dist/spec-bridge/bridges/form-view.js +0 -170
  98. package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
  99. package/dist/spec-bridge/bridges/list-view.js +0 -268
  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
@@ -6,14 +6,16 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  /**
9
- * Dev-build diagnostic: a visibility predicate could not be evaluated
10
- * (objectui#5454, leg 3 of the 2026-08-21 ruling).
9
+ * Diagnostic: a node-gate predicate could not be evaluated (objectui#5454,
10
+ * leg 3 of the 2026-08-21 ruling; production coverage added by objectui#6038,
11
+ * maintainer ruling 2026-08-25 option B; the `disabled` / `disabledOn` gate
12
+ * added by objectui#6445).
11
13
  *
12
14
  * ## The defect this names
13
15
  *
14
16
  * `SchemaRenderer`'s visibility chain is FAIL-SOFT: `evaluateCondition` answers
15
- * an unevaluable predicate with `true`. On the four negated legs (`visible` /
16
- * `visibleWhen` / `visibleOn` / `visibility`) that `true` means SHOWN — so "this
17
+ * an unevaluable predicate with `true`. On the negated legs (`visible` /
18
+ * `visibleWhen` / `visibleOn`) that `true` means SHOWN — so "this
17
19
  * predicate is broken" and "this predicate said yes" reach the screen as the
18
20
  * same pixel. An author who mistyped a root, or who wrote a `record.*` gate
19
21
  * before objectui#5454 bound the row, saw their block render and had no signal
@@ -30,9 +32,11 @@
30
32
  * It changes NO verdict. `evaluateCondition` already returned `true` for every
31
33
  * unevaluable predicate on every one of its paths, and the caller reproduces
32
34
  * exactly that from its catch — including on the two NON-negated legs
33
- * (`hidden` / `hiddenOn`), where the same `true` means HIDE. Flipping fail-soft
34
- * to fail-closed is a shipped-behaviour change tracked separately upstream
35
- * (objectstack#5149, appeal 1, undecided); this is the diagnostic half only.
35
+ * (`hidden` / `hiddenOn`), where the same `true` means HIDE — and, since
36
+ * objectui#6445, on the `disabled` / `disabledOn` gate, where it means GREYED
37
+ * OUT. Flipping fail-soft to fail-closed is a shipped-behaviour change tracked
38
+ * separately upstream (objectstack#5149, appeal 1, undecided); this module is
39
+ * the diagnostic half only, on every gate wired to it.
36
40
  *
37
41
  * ## Why a separate module
38
42
  *
@@ -48,6 +52,19 @@
48
52
  * and so an app can filter it out of its console transport if it must.
49
53
  */
50
54
  export const UNRESOLVABLE_VISIBILITY_PREFIX = '[ObjectUI] A visibility predicate could not be evaluated';
55
+ /**
56
+ * The same opening line for the ENABLEMENT gate (`disabled` / `disabledOn`,
57
+ * objectui#6445). A sibling constant rather than a widened one: the six
58
+ * visibility legs keep the bytes they ship today (objectui#6487 pinned them),
59
+ * and calling a `disabled` predicate a "visibility predicate" would send an
60
+ * author to the wrong gate on the first line - the one line a console filter
61
+ * and a `grep` both read.
62
+ *
63
+ * Same posture as its sibling in every other respect: same reporter, same
64
+ * severity, same dedupe `Set`, same key shape, same test-only reset. Only the
65
+ * words that would be FALSE on this gate differ.
66
+ */
67
+ export const UNRESOLVABLE_ENABLEMENT_PREFIX = '[ObjectUI] An enablement predicate could not be evaluated';
51
68
  /** The raw predicate as an author would recognise it, envelope or not. */
52
69
  function predicateSourceText(raw) {
53
70
  if (raw && typeof raw === 'object') {
@@ -57,22 +74,104 @@ function predicateSourceText(raw) {
57
74
  }
58
75
  return typeof raw === 'string' ? raw : String(raw);
59
76
  }
77
+ /**
78
+ * The closing paragraph, per tier. The ONLY part of the message that varies —
79
+ * everything above it (the node, the key, the source, the engine's reason, and
80
+ * the "gate did NOT bite" sentence) is true on every surface wired to this
81
+ * reporter, including the fail-open app-shell gate.
82
+ *
83
+ * Both entries end on the same sentence on purpose: whichever tier an author is
84
+ * on, the two things to check are the roots and the CEL syntax.
85
+ *
86
+ * Indexed WITHOUT a `??` fallback to the node tier, deliberately (AGENTS.md
87
+ * #0.1). A tier this table does not answer is a type violation, and quietly
88
+ * substituting the node tier's paragraph for it would reproduce, one caller
89
+ * further along, the exact defect this card fixes: confident advice about a
90
+ * scope the predicate was never evaluated against. The default lives on the
91
+ * PARAMETER, where it is a stated compatibility choice, not in the lookup.
92
+ */
93
+ const SCOPE_TIER_ADVICE = {
94
+ 'page-component': 'Page-component predicates bind `record` (the row on a record page),\n' +
95
+ '`current_user`, and page state as `page.<var>`. Check those roots and the\n' +
96
+ 'CEL syntax.',
97
+ 'app-shell': 'App-shell predicates bind `current_user` - also spelled `user`, `ctx.user`\n' +
98
+ 'and `os.user` - plus `features` (the deployment flags).\n' +
99
+ 'Neither `record` nor `page.<var>` exists at this tier.\n' +
100
+ 'Check those roots and the CEL syntax.',
101
+ };
102
+ /**
103
+ * The two parts of the message that vary with the gate: the opening line (what
104
+ * KIND of predicate failed) and the consequence paragraph (what the fail-soft
105
+ * default DID to the node). Everything between them - the node, the key, the
106
+ * source, the engine's reason - and the scope advice after them are true on
107
+ * every gate.
108
+ *
109
+ * Indexed WITHOUT a `??` fallback, for the same reason {@link SCOPE_TIER_ADVICE}
110
+ * is: quietly substituting one gate's consequence for another's IS the defect
111
+ * being fixed here, and doing it at the lookup would only move it one caller
112
+ * further along. The default lives on the PARAMETER, where it is a stated
113
+ * compatibility choice.
114
+ */
115
+ const GATE_KIND_COPY = {
116
+ visibility: {
117
+ prefix: UNRESOLVABLE_VISIBILITY_PREFIX,
118
+ consequence: 'The node was treated as its safe default, which on this surface means the\n' +
119
+ 'gate did NOT bite - a predicate that cannot be evaluated reads on screen\n' +
120
+ 'exactly like one that said yes.\n',
121
+ },
122
+ // The two NON-negated visibility legs (objectui#6503). SAME opening line as
123
+ // `visibility` above, deliberately: these ARE visibility predicates, the six
124
+ // legs of that chain keep the bytes objectui#6487 pinned, and an app - or a
125
+ // test - filtering the console by `UNRESOLVABLE_VISIBILITY_PREFIX` has to go
126
+ // on catching them. objectui#6038's pin that the `hidden` leg reports AT ALL
127
+ // reads through exactly that filter, and it must stay green: this card moves
128
+ // the sentence that was false for these legs, and nothing else.
129
+ concealment: {
130
+ prefix: UNRESOLVABLE_VISIBILITY_PREFIX,
131
+ consequence: 'The node was treated as its safe default, which on THIS leg is the one\n' +
132
+ 'that BITES: `hidden` / `hiddenOn` are NOT negated, so that default\n' +
133
+ 'REMOVED the node - it is not on the page at all, and an absent node is\n' +
134
+ 'indistinguishable from metadata that meant to hide it. Nothing is broken\n' +
135
+ 'in the renderer: the block is missing because the predicate above could\n' +
136
+ 'not be evaluated.\n',
137
+ },
138
+ // The app-shell navigation gate (objectui#12700): fail-CLOSED on a fault.
139
+ withholding: {
140
+ prefix: UNRESOLVABLE_VISIBILITY_PREFIX,
141
+ consequence: 'The entry was treated as its safe default, which on THIS gate is the one\n' +
142
+ 'that BITES: a navigation entry whose predicate cannot be evaluated is\n' +
143
+ 'WITHHELD - not drawn - until it can be, so a missing entry may be this\n' +
144
+ 'fault rather than a gate that said no.\n',
145
+ },
146
+ enablement: {
147
+ prefix: UNRESOLVABLE_ENABLEMENT_PREFIX,
148
+ consequence: 'The node was treated as its safe default, which on THIS gate is the one\n' +
149
+ 'that BITES: the node renders DISABLED - on screen, greyed out, refusing\n' +
150
+ 'input - and that is indistinguishable from a gate the author meant to\n' +
151
+ 'close. No pixel says a predicate failed, so this line is the only thing\n' +
152
+ 'that will ever name the one that did it.\n',
153
+ },
154
+ };
60
155
  /**
61
156
  * Build the message. Separate from the emit so a test can assert the words,
62
157
  * not merely that something was logged.
158
+ *
159
+ * `tier` defaults to `'page-component'` and `gate` to `'visibility'` so the
160
+ * historical five-argument call keeps printing the exact bytes it printed
161
+ * before (objectui#6487, objectui#6445) — the defaults are a compatibility
162
+ * shim for callers outside this repo, not something this repo leans on: every
163
+ * in-repo call site passes both explicitly, which is what makes each one a
164
+ * stated decision rather than an inherited one.
63
165
  */
64
- export function formatUnresolvableVisibilityMessage(type, id, key, raw, reason) {
166
+ export function formatUnresolvableVisibilityMessage(type, id, key, raw, reason, tier = 'page-component', gate = 'visibility') {
65
167
  const node = typeof type === 'string' && type ? '"' + type + '"' : '(untyped node)';
66
168
  const where = typeof id === 'string' && id ? ' (id: "' + id + '")' : '';
67
- return (UNRESOLVABLE_VISIBILITY_PREFIX + ' - node ' + node + where + '\n' +
169
+ const copy = GATE_KIND_COPY[gate];
170
+ return (copy.prefix + ' - node ' + node + where + '\n' +
68
171
  ' ' + key + ': ' + JSON.stringify(predicateSourceText(raw)) + '\n' +
69
172
  ' Reason: ' + reason + '\n' +
70
- 'The node was treated as its safe default, which on this surface means the\n' +
71
- 'gate did NOT bite - a predicate that cannot be evaluated reads on screen\n' +
72
- 'exactly like one that said yes.\n' +
73
- 'Page-component predicates bind `record` (the row on a record page),\n' +
74
- '`current_user`, and page state as `page.<var>`. Check those roots and the\n' +
75
- 'CEL syntax.');
173
+ copy.consequence +
174
+ SCOPE_TIER_ADVICE[tier]);
76
175
  }
77
176
  /**
78
177
  * Reported (node type, key, predicate source) triples, so a re-render — or the
@@ -80,22 +179,71 @@ export function formatUnresolvableVisibilityMessage(type, id, key, raw, reason)
80
179
  * repeat the line. Keyed on the predicate TEXT rather than the schema object:
81
180
  * the same broken predicate authored once and rendered over many rows is ONE
82
181
  * authoring bug, and an object key would report it once per row.
182
+ *
183
+ * This is the RATE LIMIT the 2026-08-25 ruling requires of the production leg,
184
+ * and it is why that leg can be a plain `console.warn`: the ceiling is not "one
185
+ * line per render" but "one line per distinct authored predicate", for the
186
+ * lifetime of the page. Two properties have to hold together, and a test that
187
+ * pins only the first cannot tell a working dedupe from one that suppresses
188
+ * everything — so objectui#6038 pins both: N renders of ONE faulting predicate
189
+ * emit exactly one line, and a SECOND distinct predicate source still emits.
83
190
  */
84
191
  const _warnedVisibilityPredicates = new Set();
85
192
  /**
86
- * Dev-build only; the caller applies the gate, so this module is dead code in a
87
- * production build. `console.warn`, not `error`: the verdict is unchanged and
88
- * the page still renders, so this is a diagnostic about a predicate — not the
89
- * refusal `reportUnevaluatedExpressions` emits once raw source has reached the
90
- * DOM.
193
+ * Reports a NODE-GATE predicate that could not be evaluated — in DEVELOPMENT
194
+ * AND IN PRODUCTION since objectui#6038 (maintainer ruling 2026-08-25, option
195
+ * B: "the silence is no longer an accepted property").
196
+ *
197
+ * The visibility chain was the first gate wired to it and is still the default
198
+ * {@link PredicateGateKind}; `disabled` / `disabledOn` joined it in
199
+ * objectui#6445, which is why the name is now narrower than the function. It is
200
+ * kept anyway: this symbol is exported from the package entry and called from
201
+ * `@object-ui/app-shell` and `@object-ui/components`, so renaming it would be a
202
+ * cross-package edit that changes no behaviour, on a card scoped to one file's
203
+ * wiring. What has to stay true is the posture the name once described — ONE
204
+ * reporter, ONE dedupe `Set`, ONE severity, ONE reset — and a second reporter
205
+ * for the second gate is exactly what that rules out.
206
+ *
207
+ * `console.warn`, not `error`: the verdict is unchanged and the page still
208
+ * renders, so this is a diagnostic about a predicate — not the refusal
209
+ * `reportUnevaluatedExpressions` emits once raw source has reached the DOM.
210
+ *
211
+ * ## `err` takes a reason, not only an `Error`
212
+ *
213
+ * The dev caller catches a throw and passes the `Error`; the production caller
214
+ * is handed the evaluator's own reason STRING through `EvaluationOptions.onFault`
215
+ * (no throw is raised there, because raising one would cost a second
216
+ * evaluation). `String(err)` already covered that shape, so both callers reach
217
+ * the same `Reason:` text and the same dedupe entry — which is what makes "dev
218
+ * and production print the identical line" true rather than approximately true.
219
+ *
220
+ * ## `tier` is NOT in the dedupe key, and that is not an oversight
221
+ *
222
+ * The key stays `(type, key, predicate source)` — the rate limit objectui#6038
223
+ * pinned in both directions. Adding `tier` could only ever LOOSEN it, and it
224
+ * would take a `type` shared across two tiers to loosen anything: the app-shell
225
+ * site's `type` is the constant `'app-shell:visible'`, which no node tier can
226
+ * produce, and the two `'page-component'` callers are the same tier by
227
+ * definition. So the tier is free of the key by measurement, not by assumption.
228
+ *
229
+ * ## Neither is `gate`, and for a stronger reason (objectui#6445)
230
+ *
231
+ * `gate` is a FUNCTION of `key`, which is already in the dedupe key: the
232
+ * enablement gate is exactly `disabled` / `disabledOn` and the visibility gate
233
+ * is exactly the six legs of the visibility chain, two disjoint sets. So adding
234
+ * it could not separate two entries that `key` does not already separate, and
235
+ * the cross-gate direction is pinned rather than argued: the SAME predicate
236
+ * source authored on `disabled` and on `visibleWhen` produces TWO lines, not
237
+ * one - the new reporting site cannot be swallowed by a visibility entry, and
238
+ * cannot swallow one.
91
239
  */
92
- export function reportUnresolvableVisibilityPredicate(type, id, key, raw, err) {
240
+ export function reportUnresolvableVisibilityPredicate(type, id, key, raw, err, tier = 'page-component', gate = 'visibility') {
93
241
  const reason = err instanceof Error ? err.message : String(err);
94
242
  const dedupeKey = JSON.stringify([type, key, predicateSourceText(raw)]);
95
243
  if (_warnedVisibilityPredicates.has(dedupeKey))
96
244
  return;
97
245
  _warnedVisibilityPredicates.add(dedupeKey);
98
- console.warn(formatUnresolvableVisibilityMessage(type, id, key, raw, reason));
246
+ console.warn(formatUnresolvableVisibilityMessage(type, id, key, raw, reason, tier, gate));
99
247
  }
100
248
  /**
101
249
  * Test-only reset for the dedupe above — for BOTH legs, which is the point of
@@ -117,14 +265,100 @@ export function __resetVisibilityPredicateWarnings() {
117
265
  * dev-only unresolvable-predicate report". What is load-bearing there — and
118
266
  * what the dispatch restated — is the reporter's POSTURE: same module, same
119
267
  * severity (`console.warn`), same dedupe key shape, same dedupe Set, same
120
- * dev-only gate at the same call site, same test-only reset. All of that is
121
- * shared below. The first LINE is not reused, because on this path it would
268
+ * gate at the same call site, same test-only reset. All of that is shared
269
+ * below.
270
+ *
271
+ * ⚠️ The two legs stopped sharing that gate's VALUE in objectui#6038, and only
272
+ * that. The unresolvable leg now reports in production as well, because the
273
+ * 2026-08-25 ruling retired its silence; THIS leg stays dev-only under its own
274
+ * 2026-08-22 ruling, and the difference is not an oversight. A fault is a
275
+ * predicate that could not be evaluated, and shipping a live gate that has
276
+ * stopped biting is the class-1 defect production has to be able to see. This
277
+ * leg reports something else: a predicate that evaluated perfectly, against the
278
+ * wrong object. Its trigger is a LEXICAL scan of the predicate source with a
279
+ * stated false-positive residue (the deliberate-absence idioms below), which is
280
+ * a cost worth paying for an author at their keyboard and not for every user of
281
+ * every production page. The first LINE is not reused, because on this path it would
122
282
  * state something false: the predicate did not fail to evaluate. It evaluated
123
283
  * perfectly, against the wrong object, and produced a constant. Telling an
124
284
  * author "could not be evaluated" would send them hunting for a syntax error
125
285
  * that is not there.
126
286
  */
127
- export const ADAPTER_ONLY_DATA_PREDICATE_PREFIX = '[ObjectUI] A visibility predicate resolved `data.*` against the data-source adapter';
287
+ export const ADAPTER_ONLY_DATA_PREDICATE_PREFIX = '[ObjectUI] A visibility predicate read `data.*` that the bound `data` does not answer';
288
+ /**
289
+ * The `disabled` / `disabledOn` sibling of the prefix above (objectui#6504,
290
+ * maintainer ruling 2026-08-27 option A). Same reason
291
+ * {@link UNRESOLVABLE_ENABLEMENT_PREFIX} is a sibling of
292
+ * {@link UNRESOLVABLE_VISIBILITY_PREFIX} rather than a reuse: calling a
293
+ * `disabled` predicate "a visibility predicate" would send an author to the
294
+ * wrong gate on the first line — the one line a console filter and a `grep`
295
+ * both read.
296
+ */
297
+ export const ADAPTER_ONLY_ENABLEMENT_PREDICATE_PREFIX = '[ObjectUI] An enablement predicate read `data.*` that the bound `data` does not answer';
298
+ /**
299
+ * The two parts of the message that vary with the gate, mirroring
300
+ * {@link GATE_KIND_COPY}'s machinery (#6445) for this SIBLING diagnostic —
301
+ * same discipline (indexed without a `??` fallback; the default lives on the
302
+ * parameter), a separate table because the message TEMPLATE differs (this one
303
+ * carries an "Unanswered by the bound `data`:" line and no {@link SCOPE_TIER_ADVICE}
304
+ * tail; see {@link formatAdapterOnlyDataMessage}).
305
+ *
306
+ * ⛔⛔ BINDING INHERITANCE CLAUSE (2026-08-27 ruling, objectui#6504, verbatim
307
+ * from the card thread): "the extension inherits #5687's dissolution note
308
+ * verbatim — when #5330's deprecation window closes, both legs dissolve
309
+ * together; the new copy entry must carry the same pointer so the teardown
310
+ * finds it." The pointer, carried from this module's own docblock (see
311
+ * {@link unresolvedDataPaths}'s "why this discriminator" section): objectui#5687
312
+ * is "a diagnostic on a card that dissolves when objectui#5330's deprecation
313
+ * window closes." BOTH entries below dissolve on that same clock, together —
314
+ * this is not an independently-scheduled diagnostic. When that window closes,
315
+ * delete this table, {@link ADAPTER_ONLY_ENABLEMENT_PREDICATE_PREFIX},
316
+ * {@link ADAPTER_ONLY_DATA_PREDICATE_PREFIX}, {@link unresolvedDataPaths},
317
+ * {@link formatAdapterOnlyDataMessage} and {@link reportAdapterOnlyDataPredicate}
318
+ * in this file, and BOTH call sites in `SchemaRenderer.tsx`
319
+ * (`evaluateVisibilityPredicate` and `evaluateEnablementPredicate`), in the
320
+ * same stroke.
321
+ */
322
+ const ADAPTER_ONLY_GATE_COPY = {
323
+ visibility: {
324
+ prefix: ADAPTER_ONLY_DATA_PREDICATE_PREFIX,
325
+ consequence: 'At the node tier `data` is whatever the HOST published under that name\n' +
326
+ 'in the ambient predicate scope, and it is NOT the row. Since\n' +
327
+ 'objectui#9308 it is not the data-source adapter either - the renderer\n' +
328
+ 'binds no `data` of its own. The reads above are undefined on it, so\n' +
329
+ 'this predicate is a CONSTANT: it does not\n' +
330
+ 'depend on the row at all, and on this surface a constant `false` hides the\n' +
331
+ 'node on every row while looking exactly like a gate that said no.\n' +
332
+ 'Write the row as `record.*` (e.g. `record.status`), which page-component\n' +
333
+ 'predicates bind alongside `current_user` and page state as `page.<var>`.\n' +
334
+ '`data.*` as a spelling for the row is deprecated (objectui#5330) and was\n' +
335
+ 'never bound to the row on this tier.',
336
+ },
337
+ // The constant-`true` direction, not a reuse of the sentence above: on THIS
338
+ // gate the fail-soft default that BITES is `true` (a control that greys
339
+ // out), the mirror image of the negated visibility legs where it is
340
+ // `false` (a node that hides). Written to describe that direction, since it
341
+ // is the one that leaves nothing on screen for an author to notice —
342
+ // "greyed out" looks exactly like a gate that closed on purpose, the same
343
+ // way "not on screen" looks exactly like a gate that hid on purpose.
344
+ enablement: {
345
+ prefix: ADAPTER_ONLY_ENABLEMENT_PREDICATE_PREFIX,
346
+ consequence: 'At the node tier `data` is whatever the HOST published under that name\n' +
347
+ 'in the ambient predicate scope, and it is NOT the row. Since\n' +
348
+ 'objectui#9308 it is not the data-source adapter either - the renderer\n' +
349
+ 'binds no `data` of its own. The reads above are undefined on it, so\n' +
350
+ 'this predicate is a CONSTANT: it does not\n' +
351
+ 'depend on the row at all, and on THIS gate the dangerous polarity is a\n' +
352
+ 'constant `true`: the node renders DISABLED - on screen, greyed out,\n' +
353
+ 'refusing input - on every row, in every build, while looking exactly\n' +
354
+ 'like a gate that said no. No pixel says the predicate is a constant, so\n' +
355
+ 'this line is the only thing that will ever name it.\n' +
356
+ 'Write the row as `record.*` (e.g. `record.status`), which page-component\n' +
357
+ 'predicates bind alongside `current_user` and page state as `page.<var>`.\n' +
358
+ '`data.*` as a spelling for the row is deprecated (objectui#5330) and was\n' +
359
+ 'never bound to the row on this tier.',
360
+ },
361
+ };
128
362
  /**
129
363
  * Strip string literals before scanning for `data.*` reads.
130
364
  *
@@ -172,12 +406,14 @@ const DATA_ROOT_PATH_RE = /(^|[^A-Za-z0-9_$.])data\.([A-Za-z_$][A-Za-z0-9_$]*(?:
172
406
  * * "the comparison OPERAND is undefined" — the most precise reading, and it
173
407
  * needs the expression engine to expose its operands. That is a change to
174
408
  * `@object-ui/core`'s evaluator for a diagnostic on a card that dissolves
175
- * when objectui#5330's deprecation window closes.
409
+ * when objectui#5330's deprecation window closes — BOTH legs together,
410
+ * the enablement one objectui#6504 added included; see
411
+ * {@link ADAPTER_ONLY_GATE_COPY}'s binding inheritance clause.
176
412
  * * "a `data.*` read the bound object does not answer" — this one. On the
177
413
  * card's repro (`data.status` against the adapter) it fires; on
178
414
  * `${data.total}` against `{ total: 99 }` it does not; and it is
179
- * STRUCTURALLY incapable of firing on the `record.*` bucket (objectui#5401
180
- * → #5454), which is the adjacent card with a different correct fix.
415
+ * STRUCTURALLY incapable of firing on the `record.*` bucket
416
+ * (objectui#5454), which is the adjacent card with a different correct fix.
181
417
  *
182
418
  * Residue, stated rather than hidden: deliberate absence idioms
183
419
  * (`data.status == null`, `!data.status`) are reported. They are not really
@@ -186,8 +422,26 @@ const DATA_ROOT_PATH_RE = /(^|[^A-Za-z0-9_$.])data\.([A-Za-z_$][A-Za-z0-9_$]*(?:
186
422
  * the shapes an author could have meant.
187
423
  *
188
424
  * The walk mirrors `useDataScope`'s own `path.split('.').reduce(…)` resolution
189
- * of a `data.*` path, so "undefined here" means what it means everywhere else
190
- * this repo resolves against the adapter.
425
+ * of a path, so "undefined here" means what it means everywhere else this repo
426
+ * resolves a path against a scope.
427
+ *
428
+ * ⭐ WHAT `boundData` IS, since objectui#9308. It used to be the injected
429
+ * ADAPTER (`SchemaRendererContext.dataSource`), because the renderer bound that
430
+ * object as the `data` root. It no longer binds one: `boundData` is now the
431
+ * `data` the HOST published in the ambient predicate scope, i.e. the exact
432
+ * object the evaluator resolved `data.*` against. Passing the adapter after
433
+ * that ruling would have inverted this diagnostic — silent for the hosts whose
434
+ * predicates actually broke (their bag still answers), and loud for hosts whose
435
+ * scope-channel `data` answers perfectly.
436
+ *
437
+ * ⚠️ Reachability, stated rather than assumed: when the host publishes NO
438
+ * `data` root, a `data.*` predicate does not evaluate cleanly at all — it
439
+ * throws `data is not defined`, which is objectui#5454's leg, reported by
440
+ * {@link reportUnresolvableVisibilityPredicate} with a message that is true for
441
+ * it. Both call sites of this function sit on the NON-throwing branch, so this
442
+ * leg is reached only for a host that published a `data` root which does not
443
+ * answer the read. That is the population it can still speak for, and the one
444
+ * it now speaks about accurately.
191
445
  */
192
446
  function unresolvedDataPaths(source, boundData) {
193
447
  const scannable = stripStringLiterals(source);
@@ -217,22 +471,25 @@ function unresolvedDataPaths(source, boundData) {
217
471
  }
218
472
  return out;
219
473
  }
220
- /** Built separately from the emit, for the same reason as the message above. */
221
- export function formatAdapterOnlyDataMessage(type, id, key, raw, unresolved) {
474
+ /**
475
+ * Built separately from the emit, for the same reason as the message above.
476
+ *
477
+ * `gate` defaults to `'visibility'` so the historical four-argument call
478
+ * (objectui#5687) keeps printing the exact bytes it printed before —
479
+ * objectui#6504's own call site in `evaluateVisibilityPredicate`
480
+ * (`SchemaRenderer.tsx`) is one of those unchanged callers, passing no `gate`
481
+ * at all. `evaluateEnablementPredicate` is the one caller that states
482
+ * `'enablement'` explicitly, matching {@link formatUnresolvableVisibilityMessage}'s
483
+ * own compatibility-default pattern.
484
+ */
485
+ export function formatAdapterOnlyDataMessage(type, id, key, raw, unresolved, gate = 'visibility') {
222
486
  const node = typeof type === 'string' && type ? '"' + type + '"' : '(untyped node)';
223
487
  const where = typeof id === 'string' && id ? ' (id: "' + id + '")' : '';
224
- return (ADAPTER_ONLY_DATA_PREDICATE_PREFIX + ' - node ' + node + where + '\n' +
488
+ const copy = ADAPTER_ONLY_GATE_COPY[gate];
489
+ return (copy.prefix + ' - node ' + node + where + '\n' +
225
490
  ' ' + key + ': ' + JSON.stringify(predicateSourceText(raw)) + '\n' +
226
- ' Undefined on the adapter: ' + unresolved.join(', ') + '\n' +
227
- 'At the node tier `data` is the DATA-SOURCE ADAPTER - the object\n' +
228
- '`${data.total}` in a props bag reads - and it is NOT the row. The reads\n' +
229
- 'above are undefined on it, so this predicate is a CONSTANT: it does not\n' +
230
- 'depend on the row at all, and on this surface a constant `false` hides the\n' +
231
- 'node on every row while looking exactly like a gate that said no.\n' +
232
- 'Write the row as `record.*` (e.g. `record.status`), which page-component\n' +
233
- 'predicates bind alongside `current_user` and page state as `page.<var>`.\n' +
234
- '`data.*` as a spelling for the row is deprecated (objectui#5330) and was\n' +
235
- 'never bound to the row on this tier.');
491
+ ' Unanswered by the bound `data`: ' + unresolved.join(', ') + '\n' +
492
+ copy.consequence);
236
493
  }
237
494
  /**
238
495
  * Dev-build diagnostic for a node-gate predicate that reads `data.*` and gets
@@ -259,12 +516,30 @@ export function formatAdapterOnlyDataMessage(type, id, key, raw, unresolved) {
259
516
  * objectui#5454 existed to remove, one path further along.
260
517
  *
261
518
  * Sharing the module means sharing the LIFECYCLE, which is the part that has to
262
- * match: one dedupe Set, one reset, one severity, one dev-only gate. The dedupe
519
+ * match: one dedupe Set, one reset, one severity. (One gate, too, until
520
+ * objectui#6038 — see the prefix constant above for why only the sibling leg
521
+ * crossed into production.) The dedupe
263
522
  * key is tagged with this leg's name so the two diagnostics cannot silence each
264
523
  * other for the same (type, key, source) triple — they are different faults,
265
524
  * and a node that faults one way is not evidence about the other.
525
+ *
526
+ * ## `gate` (objectui#6504) — NOT in the dedupe key, for the reason
527
+ * {@link reportUnresolvableVisibilityPredicate}'s own docblock gives for the
528
+ * same omission
529
+ *
530
+ * `gate` is a FUNCTION of `key`: the enablement gate is exactly `disabled` /
531
+ * `disabledOn` and the visibility gate this leg is called for is the six-leg
532
+ * chain, two disjoint sets. Adding it to the dedupe key could not separate two
533
+ * entries `key` does not already separate — and the cross-gate direction is
534
+ * the one that would matter if it could: the SAME predicate source authored on
535
+ * `disabled` and on (say) `visibleWhen` must produce TWO lines, which it does
536
+ * today because `key` differs, with or without `gate` in the tuple.
537
+ *
538
+ * Defaults to `'visibility'` — the objectui#5687 call site
539
+ * (`evaluateVisibilityPredicate`) is unchanged by this card and still calls
540
+ * this function with five arguments.
266
541
  */
267
- export function reportAdapterOnlyDataPredicate(type, id, key, raw, boundData) {
542
+ export function reportAdapterOnlyDataPredicate(type, id, key, raw, boundData, gate = 'visibility') {
268
543
  const source = predicateSourceText(raw);
269
544
  // Fast reject: the overwhelming majority of predicates never mention `data.`
270
545
  // at all, and this runs for every visibility predicate of every node in dev.
@@ -280,5 +555,5 @@ export function reportAdapterOnlyDataPredicate(type, id, key, raw, boundData) {
280
555
  if (_warnedVisibilityPredicates.has(dedupeKey))
281
556
  return;
282
557
  _warnedVisibilityPredicates.add(dedupeKey);
283
- console.warn(formatAdapterOnlyDataMessage(type, id, key, raw, unresolved));
558
+ console.warn(formatAdapterOnlyDataMessage(type, id, key, raw, unresolved, gate));
284
559
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/react",
3
- "version": "17.6.0",
3
+ "version": "18.0.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "React bindings and SchemaRenderer component for Object UI",
@@ -24,12 +24,11 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@objectstack/spec": "^17.0.0",
28
- "react-hook-form": "^7.85.0",
29
- "@object-ui/core": "17.6.0",
30
- "@object-ui/i18n": "17.6.0",
31
- "@object-ui/types": "17.6.0",
32
- "@object-ui/data-objectstack": "17.6.0"
27
+ "@objectstack/spec": "^18.0.0",
28
+ "@object-ui/data-objectstack": "18.0.0",
29
+ "@object-ui/types": "18.0.0",
30
+ "@object-ui/core": "18.0.0",
31
+ "@object-ui/i18n": "18.0.0"
33
32
  },
34
33
  "peerDependencies": {
35
34
  "react": "^18.0.0 || ^19.0.0",
@@ -38,7 +37,8 @@
38
37
  "devDependencies": {
39
38
  "@types/react": "19.2.18",
40
39
  "@types/react-dom": "19.2.4",
41
- "typescript": "^6.0.3"
40
+ "typescript": "^6.0.3",
41
+ "@object-ui/test-support": "17.4.0"
42
42
  },
43
43
  "keywords": [
44
44
  "objectui",
@@ -63,8 +63,8 @@
63
63
  "LICENSE"
64
64
  ],
65
65
  "scripts": {
66
- "build": "tsc",
67
- "test": "vitest run",
66
+ "build": "tsc && node ../../scripts/check-dist-completeness.mjs",
67
+ "test": "vitest run --root ../.. packages/react/",
68
68
  "type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
69
69
  "lint": "eslint ."
70
70
  }
@@ -1,47 +0,0 @@
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
- export interface UseClientNotificationsOptions {
9
- /** ObjectStack client instance (optional, from context if available) */
10
- client?: any;
11
- /** Whether to auto-fetch notifications on mount */
12
- autoFetch?: boolean;
13
- /** Polling interval in ms (0 to disable) */
14
- pollInterval?: number;
15
- }
16
- export interface UseClientNotificationsResult {
17
- /** Fetch notifications from server */
18
- fetchNotifications: () => Promise<void>;
19
- /** Mark a notification as read on the server */
20
- markAsRead: (id: string) => Promise<void>;
21
- /** Loading state */
22
- loading: boolean;
23
- /** Error state */
24
- error: Error | null;
25
- }
26
- /**
27
- * Bridge between the `@objectstack/client` notifications API and the
28
- * local {@link NotificationContext}.
29
- *
30
- * The hook resolves the ObjectStack client either from the explicit
31
- * `client` option or from the `SchemaRendererContext` dataSource, then
32
- * delegates to `client.notifications.*` methods. Fetched notifications
33
- * are fed into the local provider via `useNotifications().notify()`.
34
- *
35
- * @example
36
- * ```tsx
37
- * function NotificationBell() {
38
- * const { fetchNotifications, loading } = useClientNotifications({
39
- * autoFetch: true,
40
- * pollInterval: 30_000,
41
- * });
42
- *
43
- * return <Button onClick={fetchNotifications} disabled={loading}>🔔</Button>;
44
- * }
45
- * ```
46
- */
47
- export declare function useClientNotifications(options?: UseClientNotificationsOptions): UseClientNotificationsResult;