@object-ui/react 17.7.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 (44) hide show
  1. package/CHANGELOG.md +497 -0
  2. package/README.md +89 -4
  3. package/dist/SchemaRenderer.d.ts +27 -1
  4. package/dist/SchemaRenderer.js +139 -50
  5. package/dist/context/AppShellContext.d.ts +27 -0
  6. package/dist/context/DiscussionContext.d.ts +15 -0
  7. package/dist/context/DiscussionContext.js +2 -0
  8. package/dist/context/InlineEditContext.d.ts +70 -0
  9. package/dist/context/InlineEditContext.js +30 -2
  10. package/dist/context/PrintPageContext.d.ts +60 -0
  11. package/dist/context/PrintPageContext.js +32 -0
  12. package/dist/context/RelatedRecordActionsContext.d.ts +67 -1
  13. package/dist/context/RelatedRecordActionsContext.js +4 -0
  14. package/dist/context/SchemaRendererContext.d.ts +7 -4
  15. package/dist/context/SchemaRendererContext.js +1 -1
  16. package/dist/context/index.d.ts +1 -0
  17. package/dist/context/index.js +1 -0
  18. package/dist/element-data-source/ElementApiDataSourcePanel.d.ts +28 -0
  19. package/dist/element-data-source/ElementApiDataSourcePanel.js +10 -0
  20. package/dist/element-data-source/ElementDataSourceGate.d.ts +30 -2
  21. package/dist/element-data-source/ElementDataSourceGate.js +75 -6
  22. package/dist/element-data-source/useElementApiDataSource.d.ts +119 -0
  23. package/dist/element-data-source/useElementApiDataSource.js +198 -0
  24. package/dist/hooks/useActionTextLocalizer.d.ts +20 -7
  25. package/dist/hooks/useActionTextLocalizer.js +43 -12
  26. package/dist/hooks/useConfigBagEvaluator.d.ts +5 -4
  27. package/dist/hooks/useConfigBagEvaluator.js +5 -4
  28. package/dist/hooks/useDatasetDimensionLabels.js +25 -10
  29. package/dist/hooks/useDensityMode.d.ts +4 -2
  30. package/dist/hooks/useDensityMode.js +4 -2
  31. package/dist/hooks/usePageAssignment.d.ts +31 -8
  32. package/dist/hooks/usePageAssignment.js +68 -25
  33. package/dist/hooks/useViewData.d.ts +3 -1
  34. package/dist/hooks/useViewData.js +5 -14
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +3 -0
  37. package/dist/utils/localeMap.d.ts +27 -0
  38. package/dist/utils/localeMap.js +106 -0
  39. package/dist/utils/propsBagDiagnostic.js +3 -2
  40. package/dist/utils/unevaluatedExpression.d.ts +3 -1
  41. package/dist/utils/unevaluatedExpression.js +18 -0
  42. package/dist/utils/visibilityDiagnostic.d.ts +14 -5
  43. package/dist/utils/visibilityDiagnostic.js +10 -2
  44. package/package.json +6 -6
@@ -7,6 +7,32 @@
7
7
  */
8
8
  import React, { Component, type ForwardRefExoticComponent, type RefAttributes } from 'react';
9
9
  import type { DeclaredNode } from '@object-ui/types';
10
+ /**
11
+ * The scope a page node's values are evaluated against: its `visibleWhen`
12
+ * gate and every other expression the evaluation memo in
13
+ * {@link SchemaRenderer} evaluates on the node. Four roots, in this order:
14
+ *
15
+ * 1. **the ambient predicate scope**: every root `predicateScope` carries (the
16
+ * host scope `usePredicateScope()` publishes, plus a node's API-bound
17
+ * `data` once it has answered), spread first;
18
+ * 2. **`current_user`**: aliased to the ambient `user`, so `user.email` and
19
+ * `current_user.email` read the same value;
20
+ * 3. **`record`**: the page's bound row, ONLY when there is one (a non-array
21
+ * object). No row binds nothing, so a `record` the ambient scope supplies
22
+ * is not shadowed by an empty one; a row, written after the spread, wins;
23
+ * 4. **`page`**: the page variables (`PageSchema.variables`) as `page.<var>`.
24
+ *
25
+ * Why each root is (and `data` is not) bound is stated where the memo builds
26
+ * its evaluator from this.
27
+ *
28
+ * The ONE builder of this scope. The memo and {@link evaluateConfigBagInScope}
29
+ * call it, so they cannot build two different scopes (objectui#10290), and it
30
+ * is exported for the `page:tabs` derived badge in `@object-ui/components`,
31
+ * which decides the `visibleWhen` gates in front of a related list the way the
32
+ * tab body decides them (objectui#12987, objectui#12992). A root added here
33
+ * reaches every one of them.
34
+ */
35
+ export declare function buildNodeEvaluationScope(predicateScope: Record<string, unknown>, boundRecord: unknown, pageVariables: unknown): Record<string, unknown>;
10
36
  /**
11
37
  * The `properties` evaluation of the {@link SchemaRenderer} memo, for a config
12
38
  * bag that does not pass through `SchemaRenderer` (objectui#10290): the same
@@ -77,7 +103,7 @@ export declare class SchemaErrorBoundary extends Component<{
77
103
  * `element:record_picker`, a stored page document under its page kind), and
78
104
  * the types an application declares in `CustomNodeRegistry`. It has no
79
105
  * `type: string` arm, so a node whose `type` no declaration names is refused
80
- * here, and each node's own keys are judged: once objectui#8347 removes
106
+ * here, and each node's own keys are judged: since objectui#8347 removed
81
107
  * `BaseSchema`'s index signature, a misspelled key is refused while the
82
108
  * declared spelling compiles. ⛔ The prop is widened by declared types only,
83
109
  * never by an index signature, a `Record` or a `type: string` arm.
@@ -10,11 +10,15 @@ import React, { forwardRef, useContext, useMemo, useEffect, useReducer, useState
10
10
  import { ComponentRegistry, ExpressionEvaluator, isObjectUIError, ERROR_CODES, debugLog, debugTime, debugTimeEnd, DebugCollector, validateSchema, hasDeclaredPredicate, hasResponsiveStyles, scopeClassFor, compileScopedStyles, recordSourceDataArmForType, } from '@object-ui/core';
11
11
  import { SchemaRendererContext } from './context/SchemaRendererContext.js';
12
12
  import { useRecordContext } from './context/RecordContext.js';
13
- import { usePredicateScope } from './hooks/useExpression.js';
13
+ import { usePredicateScope, PredicateScopeProvider } from './hooks/useExpression.js';
14
+ import { useElementApiDataSource } from './element-data-source/useElementApiDataSource.js';
15
+ import { ElementDataSourceLoadingPanel } from './element-data-source/ElementDataSourceGate.js';
16
+ import { ElementApiDataSourcePanel } from './element-data-source/ElementApiDataSourcePanel.js';
14
17
  import { usePageVariables } from './hooks/usePageVariables.js';
15
18
  import { resolveKeyedI18nLabel } from './utils/i18n.js';
16
19
  import { isConfigBag } from './utils/configBag.js';
17
20
  import { PARAMS_KEY, isParamsBag, mapParamsLeaves } from './utils/paramsBag.js';
21
+ import { isInlineLocaleMap, mapLocaleMapMembers } from './utils/localeMap.js';
18
22
  import { reportUnevaluatedExpressions } from './utils/unevaluatedExpression.js';
19
23
  import { reportDroppedPropsBag, reportRefusedPropsPredicate } from './utils/propsBagDiagnostic.js';
20
24
  import { reportRefusedDataPropSpread } from './utils/refusedDataPropDiagnostic.js';
@@ -221,10 +225,10 @@ function propsWithoutCanonicalKeys(propsBag, propertiesBag) {
221
225
  * means *hidden* and the wider `!== undefined` test would vanish a node for
222
226
  * `hidden: ''` (objectui#3955). See {@link winningVisibilityKey}.
223
227
  */
224
- const VISIBILITY_SHOW_KEYS = ['visibleWhen', 'visible', 'visibleOn', 'visibility'];
228
+ const VISIBILITY_SHOW_KEYS = ['visibleWhen', 'visible', 'visibleOn'];
225
229
  const VISIBILITY_HIDE_KEYS = ['hidden', 'hiddenOn'];
226
230
  /**
227
- * The same six legs as a lookup, so the config-bag evaluation loops below can
231
+ * The same five legs as a lookup, so the config-bag evaluation loops below can
228
232
  * ask "is this key a visibility predicate?" off the SAME declaration the chain
229
233
  * is built from rather than a second list that can drift from it.
230
234
  *
@@ -351,7 +355,7 @@ const isCelEnvelope = (value) => isConfigBag(value)
351
355
  * `has(record.x) && …` throws — and the two polarities of the chain then fail
352
356
  * in OPPOSITE directions, both silently:
353
357
  *
354
- * * on the four SHOW legs the fail-soft `true` is negated to "do not hide",
358
+ * * on the three SHOW legs the fail-soft `true` is negated to "do not hide",
355
359
  * so a gate authored to conceal renders on every row — measured in a real
356
360
  * browser against a real app instance (the card), where a duplicate-lead
357
361
  * banner showed on 6 of 6 leads whose field was `null`;
@@ -476,37 +480,66 @@ const isExpressionEnvelope = (value) => isConfigBag(value)
476
480
  * whole. Nothing is walked: a template nested inside a data object stays raw,
477
481
  * as it always did for an object without a `source`. The loops stay per-value
478
482
  * and shallow, which is the radius the unevaluated-expression diagnostic reads.
483
+ *
484
+ * One object this answers `true` for is not handed over as authored: an inline
485
+ * locale map (`utils/localeMap.ts`, objectui#12936). It is a display string
486
+ * written once per language, so {@link evaluateConfigValueWith} evaluates each
487
+ * member like a plain string on the same key. That is one level, and only for
488
+ * a map whose every key is a locale tag and every value a string, in the
489
+ * spec's own `InlineLocaleMapSchema` sense; every other data object keeps this
490
+ * rule, and the diagnostic reads the members through the same module.
479
491
  */
480
492
  const isDataObjectValue = (key, value) => !PREDICATE_CHAIN_KEYS.has(key) && isConfigBag(value) && !isExpressionEnvelope(value);
481
493
  /**
482
- * The scope the evaluation memo in {@link SchemaRenderer} evaluates a node's
483
- * values against: the ambient host scope, `current_user` aliased to `user`, the
484
- * page's bound row as `record` (only when there is one), and page variables as
485
- * `page`. Why each root is (and `data` is not) bound is stated where the memo
486
- * builds its evaluator from this.
487
- *
488
- * One function, so the memo and {@link evaluateConfigBagInScope} cannot build
489
- * two different scopes (objectui#10290).
494
+ * The scope a page node's values are evaluated against: its `visibleWhen`
495
+ * gate and every other expression the evaluation memo in
496
+ * {@link SchemaRenderer} evaluates on the node. Four roots, in this order:
497
+ *
498
+ * 1. **the ambient predicate scope**: every root `predicateScope` carries (the
499
+ * host scope `usePredicateScope()` publishes, plus a node's API-bound
500
+ * `data` once it has answered), spread first;
501
+ * 2. **`current_user`**: aliased to the ambient `user`, so `user.email` and
502
+ * `current_user.email` read the same value;
503
+ * 3. **`record`**: the page's bound row, ONLY when there is one (a non-array
504
+ * object). No row binds nothing, so a `record` the ambient scope supplies
505
+ * is not shadowed by an empty one; a row, written after the spread, wins;
506
+ * 4. **`page`**: the page variables (`PageSchema.variables`) as `page.<var>`.
507
+ *
508
+ * Why each root is (and `data` is not) bound is stated where the memo builds
509
+ * its evaluator from this.
510
+ *
511
+ * The ONE builder of this scope. The memo and {@link evaluateConfigBagInScope}
512
+ * call it, so they cannot build two different scopes (objectui#10290), and it
513
+ * is exported for the `page:tabs` derived badge in `@object-ui/components`,
514
+ * which decides the `visibleWhen` gates in front of a related list the way the
515
+ * tab body decides them (objectui#12987, objectui#12992). A root added here
516
+ * reaches every one of them.
490
517
  */
491
- const configEvaluationScope = (predicateScope, boundRecord, pageVariables) => ({
492
- ...predicateScope,
493
- current_user: predicateScope?.user,
494
- ...(boundRecord && typeof boundRecord === 'object' && !Array.isArray(boundRecord)
495
- ? { record: boundRecord }
496
- : null),
497
- page: pageVariables,
498
- });
518
+ export function buildNodeEvaluationScope(predicateScope, boundRecord, pageVariables) {
519
+ return {
520
+ ...predicateScope,
521
+ current_user: predicateScope?.user,
522
+ ...(boundRecord && typeof boundRecord === 'object' && !Array.isArray(boundRecord)
523
+ ? { record: boundRecord }
524
+ : null),
525
+ page: pageVariables,
526
+ };
527
+ }
499
528
  /**
500
529
  * Evaluate ONE config value by its key. This is the per-key rule of the
501
530
  * evaluation memo (the `params` rule, objectui#7867, is stated there): a
502
- * `params` bag has every string leaf evaluated, a data object is handed over as
503
- * authored, and every other value is evaluated per value and shallow, with a
504
- * CEL predicate envelope preserved.
531
+ * `params` bag has every string leaf evaluated, an inline locale map has each
532
+ * member evaluated like a plain string on the same key (objectui#12936,
533
+ * `utils/localeMap.ts`), any other data object is handed over as authored, and
534
+ * every other value is evaluated per value and shallow, with a CEL predicate
535
+ * envelope preserved.
505
536
  */
506
537
  const evaluateConfigValueWith = (evaluator, key, value) => key === PARAMS_KEY && isParamsBag(value)
507
538
  ? mapParamsLeaves(value, (leaf) => evaluator.evaluate(leaf))
508
539
  : isDataObjectValue(key, value)
509
- ? value
540
+ ? isInlineLocaleMap(value)
541
+ ? mapLocaleMapMembers(value, (member) => evaluator.evaluate(member))
542
+ : value
510
543
  : preservePredicateEnvelope(key, value, (v) => evaluator.evaluate(v));
511
544
  /**
512
545
  * Evaluate every value of a config bag with {@link evaluateConfigValueWith}.
@@ -539,7 +572,7 @@ const evaluateConfigBagWith = (evaluator, bag) => {
539
572
  export function evaluateConfigBagInScope(bag, predicateScope, boundRecord, pageVariables) {
540
573
  if (!isConfigBag(bag))
541
574
  return bag;
542
- const evaluator = new ExpressionEvaluator(configEvaluationScope(predicateScope, boundRecord, pageVariables));
575
+ const evaluator = new ExpressionEvaluator(buildNodeEvaluationScope(predicateScope, boundRecord, pageVariables));
543
576
  return evaluateConfigBagWith(evaluator, bag);
544
577
  }
545
578
  /**
@@ -549,7 +582,7 @@ export function evaluateConfigBagInScope(bag, predicateScope, boundRecord, pageV
549
582
  * ## The defect this closes
550
583
  *
551
584
  * `evaluateCondition` answers an unevaluable predicate with `true` on every
552
- * path. On the four SHOW legs the chain negates that answer, so the node is
585
+ * path. On the three SHOW legs the chain negates that answer, so the node is
553
586
  * SHOWN and the reporter's "the gate did NOT bite" is true. On the two HIDE
554
587
  * legs the chain returns it UN-negated (see `shouldHide` below), so the same
555
588
  * `true` sets `_hidden` and this component returns `null`: the gate bit, and
@@ -839,7 +872,38 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
839
872
  // Ambient host scope (user / features), fed by app-shell's
840
873
  // ExpressionProvider. Threaded into `visible`/expression evaluation so
841
874
  // component predicates can gate on the signed-in user & deployment flags.
842
- const predicateScope = usePredicateScope();
875
+ const ambientScope = usePredicateScope();
876
+ // ── The API binding (objectui#12197) ──────────────────────────────────────
877
+ //
878
+ // `@objectstack/spec`'s `ElementApiDataSourceSchema` declares what a node
879
+ // with `dataSource: { provider: 'api', read: { url }, path? }` does: it reads
880
+ // that one same-origin endpoint with the host's signed-in fetch and binds the
881
+ // selected value, "which the element reads as `data` in its expression holes
882
+ // and its `visibleWhen`". Both are evaluated in the memo below, so this is
883
+ // where the binding is honoured, for every node type: the read is the hook's
884
+ // (`element-data-source/useElementApiDataSource.ts`), and its answer becomes
885
+ // the `data` root of this node's evaluation scope.
886
+ //
887
+ // ⚠️ This is NOT the adapter-as-`data` root objectui#9308 retired. That root
888
+ // published the injected adapter, against which every `data.*` path resolved
889
+ // `undefined`; this one is the value the node's own binding selected, and it
890
+ // exists only on a node that declares one. A node with no API binding is
891
+ // byte-for-byte what it was: `ambientScope` passes through untouched.
892
+ //
893
+ // The answer is also published to the node's SUBTREE through
894
+ // `PredicateScopeProvider` (below), because a block's members are evaluated
895
+ // there — `useConfigBagEvaluator` reads the predicate scope, as does every
896
+ // nested `SchemaRenderer` — so a member's `${data.x}` resolves against the
897
+ // same value as the node's own.
898
+ //
899
+ // The evaluation memo builds its scope from the answer's VALUE (the hook's
900
+ // state) and keys on it, never on a memo's identity (AGENTS.md #10). The
901
+ // subtree's copy is memoised for cost only: an evaluation keyed on it
902
+ // recomputes the same values if React discards it.
903
+ const apiBinding = useElementApiDataSource(schema);
904
+ const apiStatus = apiBinding.status;
905
+ const apiData = apiBinding.data;
906
+ const subtreeScope = useMemo(() => (apiStatus === 'ready' ? { ...ambientScope, data: apiData } : ambientScope), [ambientScope, apiStatus, apiData]);
843
907
  // The row the page is about, from `RecordContextProvider` (objectui#5454).
844
908
  //
845
909
  // `@objectstack/spec` has always DECLARED that a component node's
@@ -902,6 +966,9 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
902
966
  // an object spread.
903
967
  if (!schema || typeof schema !== 'object')
904
968
  return schema;
969
+ // The scope's host roots, plus the node's API-bound `data` once it has
970
+ // answered (see "The API binding" above).
971
+ const predicateScope = apiStatus === 'ready' ? { ...ambientScope, data: apiData } : ambientScope;
905
972
  // The ambient host scope, plus the roots this tier can answer itself.
906
973
  // `current_user` is aliased to `user` so both `user.email` and
907
974
  // `current_user.email` resolve in component `visible`/`visibleOn`
@@ -940,14 +1007,22 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
940
1007
  // no row of its own" into "this surface's row is empty" — only the latter
941
1008
  // is entitled to shadow.
942
1009
  //
943
- // Built by `configEvaluationScope`, which `useConfigBagEvaluator` shares, so
944
- // an action container's members evaluate against this same scope
945
- // (objectui#10290).
946
- const evaluator = new ExpressionEvaluator(configEvaluationScope(predicateScope, boundRecord, pageVariables));
1010
+ // Built by `buildNodeEvaluationScope`, which `useConfigBagEvaluator`
1011
+ // shares, so an action container's members evaluate against this same
1012
+ // scope (objectui#10290), and which the `page:tabs` badge in
1013
+ // `@object-ui/components` calls for its `visibleWhen` gates
1014
+ // (objectui#12992).
1015
+ const evaluator = new ExpressionEvaluator(buildNodeEvaluationScope(predicateScope, boundRecord, pageVariables));
947
1016
  // Shallow copy: the interpreter's working copy, typed as renderer state
948
1017
  // rather than as the authoring face (objectui#11349). See
949
1018
  // `InterpreterWorkingCopy` for what it declares and why.
950
1019
  const newSchema = { ...schema };
1020
+ // A node whose API binding has not answered is not evaluated: its holes
1021
+ // name a `data` that does not exist yet, so evaluating them would report
1022
+ // every one as unresolvable and decide its `visibleWhen` against nothing.
1023
+ // The render pass below draws the binding's state instead.
1024
+ if (apiStatus !== 'absent' && apiStatus !== 'ready')
1025
+ return newSchema;
951
1026
  /**
952
1027
  * Evaluate ONE visibility predicate, and make an unresolvable one LOUD
953
1028
  * (objectui#5454, leg 3 of the 2026-08-21 ruling).
@@ -1201,7 +1276,9 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
1201
1276
  //
1202
1277
  // A DATA OBJECT on any other key ({@link isDataObjectValue},
1203
1278
  // objectui#10288) is handed over as authored: `evaluate` would collapse
1204
- // one that carries a string `source` to that string.
1279
+ // one that carries a string `source` to that string. An inline locale map
1280
+ // is the one object that is not: each member is evaluated like a plain
1281
+ // string on the same key (objectui#12936, `utils/localeMap.ts`).
1205
1282
  //
1206
1283
  // The rule itself is `evaluateConfigValueWith`, at module scope, so that
1207
1284
  // `useConfigBagEvaluator` applies the same one to an action container's
@@ -1244,8 +1321,10 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
1244
1321
  //
1245
1322
  // ONE key is deep, by that separate decision, and on both spellings at once:
1246
1323
  // `params` (objectui#7867, ruling A) — every string leaf of a `params` bag
1247
- // is evaluated, through {@link evaluateConfigValue} above. Every other key
1248
- // keeps the shallow reading this paragraph describes.
1324
+ // is evaluated, through {@link evaluateConfigValue} above. ONE value shape
1325
+ // is one level deep, on any non-predicate key and both spellings: an inline
1326
+ // locale map's members (objectui#12936). Every other value keeps the
1327
+ // shallow reading this paragraph describes.
1249
1328
  //
1250
1329
  // "Passed through" used to have one exception: `evaluate` unwraps an object
1251
1330
  // with a string `source` to that string, so a data object carrying a
@@ -1560,12 +1639,13 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
1560
1639
  * above rather than re-listed here.
1561
1640
  */
1562
1641
  reportRefusedPropsPredicate(newSchema.type, newSchema.id, NODE_GATE_PREDICATE_KEYS, propsWithoutCanonicalKeys(newSchema.props, newSchema.properties));
1563
- // Evaluate visibility: visibleWhen / visible / visibleOn / visibility / hidden / hiddenOn
1642
+ // Evaluate visibility: visibleWhen / visible / visibleOn / hidden / hiddenOn
1564
1643
  const shouldHide = (() => {
1565
1644
  // `visibleWhen` is the single canonical conditional-visibility predicate
1566
1645
  // across every layer since ADR-0089 (show-when-truthy), and it is the one
1567
1646
  // this shape DECLARES: `PageComponentSchema` is a closed object that
1568
- // declares `visibleWhen` (+ the deprecated `visibility`) and REFUSES a
1647
+ // declares `visibleWhen` (its old `visibility` spelling is retired, see
1648
+ // `WorkingCopyAuthoredKeys`) and REFUSES a
1569
1649
  // node-level `visible` outright, with the prescription "move it up one
1570
1650
  // level to the component node's own `visibleWhen` … inside `properties`
1571
1651
  // it is hoisted onto the node by the renderer but evaluated by nothing"
@@ -1585,23 +1665,17 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
1585
1665
  return !evaluateVisibilityPredicate(newSchema.visibleWhen, 'visibleWhen');
1586
1666
  }
1587
1667
  // `visible` — objectui's own `BaseSchema` tier (`@object-ui/types`), and
1588
- // the landing spot of a hoisted `properties.visible`. Kept ABOVE the two
1589
- // deprecated aliases: they normalize into `visibleWhen` at parse, so a
1590
- // spec-parsed page never reaches them, and re-ranking them would move
1668
+ // the landing spot of a hoisted `properties.visible`. Kept ABOVE
1669
+ // `BaseSchema`'s deprecated `visibleOn`: re-ranking it would move
1591
1670
  // verdicts for raw metadata that objectui#5454 did not rule on.
1592
1671
  if (newSchema.visible !== undefined) {
1593
1672
  return !evaluateVisibilityPredicate(newSchema.visible, 'visible');
1594
1673
  }
1595
- // @deprecated ADR-0089 → `visibleWhen`. Defensive read for raw /
1596
- // un-normalized metadata reaching the renderer.
1674
+ // `BaseSchema.visibleOn`, @deprecated ADR-0089 → `visibleWhen`.
1675
+ // objectui's own node key; the spec's page component never declared it.
1597
1676
  if (newSchema.visibleOn !== undefined) {
1598
1677
  return !evaluateVisibilityPredicate(newSchema.visibleOn, 'visibleOn');
1599
1678
  }
1600
- // @deprecated ADR-0089 → `visibleWhen` (was PageNodeSchema.visibility,
1601
- // an ExpressionInput) — show-when-truthy, same semantics as `visibleOn`.
1602
- if (newSchema.visibility !== undefined) {
1603
- return !evaluateVisibilityPredicate(newSchema.visibility, 'visibility');
1604
- }
1605
1679
  // Ask "is a `hidden` gate DECLARED?" — not "is the key present?"
1606
1680
  // (objectui#3955). These two legs are the only ones in this chain whose
1607
1681
  // verdict is NOT negated, so the evaluator's single default for "there is
@@ -1645,7 +1719,7 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
1645
1719
  // as an internal flag either: `_disabled` is forwarded below as a real
1646
1720
  // `disabled` prop.
1647
1721
  //
1648
- // The `visible` / `visibleWhen` / `visibleOn` / `visibility` legs above keep
1722
+ // The `visible` / `visibleWhen` / `visibleOn` legs above keep
1649
1723
  // `!== undefined` deliberately: their `true` is NEGATED, so an empty predicate
1650
1724
  // already lands on "shown", which is what "no gate" means anyway, and
1651
1725
  // narrowing them would change ALIAS PRECEDENCE rather than fix anything. The
@@ -1683,7 +1757,7 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
1683
1757
  newSchema._disabled = true;
1684
1758
  }
1685
1759
  return newSchema;
1686
- }, [schema, dataSource, predicateScope, pageVariables, boundRecord]);
1760
+ }, [schema, dataSource, ambientScope, pageVariables, boundRecord, apiStatus, apiData]);
1687
1761
  /**
1688
1762
  * SDUI scoped styling (ADR-0065): a node's `responsiveStyles` compiles to
1689
1763
  * id-scoped CSS injected as a `<style>` tag, and a scope class is appended to
@@ -1762,6 +1836,17 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
1762
1836
  // component type: undefined" box, which said nothing true about the input.
1763
1837
  if (typeof evaluatedSchema !== 'object')
1764
1838
  return _jsx(_Fragment, { children: String(evaluatedSchema) });
1839
+ // The API binding's non-final states (objectui#12197), drawn in place of the
1840
+ // node — before its visibility, which may read `data` and so cannot be
1841
+ // decided yet. A refused binding names the spec's refusal; nothing was read.
1842
+ // The error panel titles itself in the page's language: its translation
1843
+ // hook mounts with it, never in this body, which runs for every node.
1844
+ if (apiStatus === 'loading') {
1845
+ return _jsx(ElementDataSourceLoadingPanel, { testId: String(evaluatedSchema.type) });
1846
+ }
1847
+ if (apiStatus === 'refused' || apiStatus === 'error') {
1848
+ return (_jsx(ElementApiDataSourcePanel, { testId: String(evaluatedSchema.type), state: apiStatus, message: apiBinding.error }));
1849
+ }
1765
1850
  // Handle visibility: if evaluated schema is hidden, render nothing.
1766
1851
  //
1767
1852
  // Reads AFTER the primitive narrowing above (objectui#4548) so the property
@@ -1800,7 +1885,7 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
1800
1885
  // Note: We don't forward the ref to the Component because components in the registry
1801
1886
  // may not support refs. The SchemaRenderer itself can still receive refs for its own use.
1802
1887
  // Extract schema metadata properties that should NOT be passed as React props
1803
- const { type: _type, children: _children, body: _body, schema: _schema, visible: _visible, visibleWhen: _visibleWhen, visibleOn: _visibleOn, visibility: _visibility, hidden: _hidden, hiddenOn: _hiddenOn, disabled: _disabled, disabledOn: _disabledOn,
1888
+ const { type: _type, children: _children, body: _body, schema: _schema, visible: _visible, visibleWhen: _visibleWhen, visibleOn: _visibleOn, hidden: _hidden, hiddenOn: _hiddenOn, disabled: _disabled, disabledOn: _disabledOn,
1804
1889
  // stripped: `PageComponentSchema.dataSource` is the spec's per-element data
1805
1890
  // BINDING (`{ object, view, filter, sort, limit }`) — schema metadata, like
1806
1891
  // `visibleWhen` above, not a visual prop. Renderers that consume it read
@@ -2023,6 +2108,10 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
2023
2108
  ...props
2024
2109
  })] }));
2025
2110
  debugTimeEnd(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
2111
+ // The bound value reaches the subtree as `data` (see "The API binding").
2112
+ const output = apiStatus === 'ready'
2113
+ ? _jsx(PredicateScopeProvider, { scope: subtreeScope, children: rendered })
2114
+ : rendered;
2026
2115
  // Report render perf to DebugCollector when debug mode is active
2027
2116
  if (isDebug && renderStart) {
2028
2117
  const durationMs = performance.now() - renderStart;
@@ -2033,6 +2122,6 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
2033
2122
  timestamp: Date.now(),
2034
2123
  });
2035
2124
  }
2036
- return rendered;
2125
+ return output;
2037
2126
  });
2038
2127
  SchemaRenderer.displayName = 'SchemaRenderer';
@@ -45,6 +45,33 @@ export interface MetadataContextValue extends MetadataCacheState {
45
45
  * context values in tests keep working; absent means "always ready".
46
46
  */
47
47
  getTypeStatus?: (type: string) => MetadataTypeStatus;
48
+ /**
49
+ * The object definitions one app reads (objectui#12811): the objects of
50
+ * `packageId` (`GET /api/v1/meta/object?package=`), plus each of `names`
51
+ * that package does not hold, fetched by name. Each is shaped exactly as the
52
+ * same definition in `objects`; `served` holds the same definitions as the
53
+ * server served them, before views are merged (as `getItemsByType('object')`
54
+ * holds the whole list). `missing` lists the `names` the server answered
55
+ * without a definition; `settled` is true once the package list and every
56
+ * name have been answered.
57
+ *
58
+ * With `names` and no `packageId` it is a names-only scope (objectui#12980):
59
+ * no package list, each of `names` fetched by name, for a reader with no
60
+ * package to scope by (a page route in an app item without `_packageId`).
61
+ * With neither it is the whole registry, as `objects` is; `objects` keeps
62
+ * that meaning everywhere. Optional, as `getTypeStatus` is: a
63
+ * hand-rolled context without it has nothing narrower to offer, and its
64
+ * readers read `objects`.
65
+ */
66
+ readObjectScope?: (scope: {
67
+ packageId?: string;
68
+ names?: readonly string[];
69
+ }) => {
70
+ objects: any[];
71
+ served: any[];
72
+ missing: readonly string[];
73
+ settled: boolean;
74
+ };
48
75
  }
49
76
  export declare const MetadataCtx: import("react").Context<MetadataContextValue | null>;
50
77
  export declare function useMetadata(): MetadataContextValue;
@@ -40,6 +40,21 @@ export interface DiscussionContextValue {
40
40
  onAddComment?: (text: string, attachments?: DiscussionAttachment[]) => void | Promise<void>;
41
41
  onAddReply?: (parentId: string, text: string) => void | Promise<void>;
42
42
  onToggleReaction?: (itemId: string, reaction: string) => void | Promise<void>;
43
+ /**
44
+ * Edit the text of a comment the viewer wrote (objectui#12268). The feed
45
+ * offers it on a `comment` row whose `authoredByViewer` the host set. A
46
+ * returned promise that REJECTS means the edit was not stored: the editor
47
+ * keeps the draft and the row keeps its old text, and reporting why is the
48
+ * host's job. Typed by the feed's own item id (`FeedItem['id']`), so the
49
+ * feed hands it on without a cast.
50
+ */
51
+ onEditComment?: (itemId: string | number, text: string) => void | Promise<void>;
52
+ /**
53
+ * Delete a comment the viewer wrote (objectui#12268), offered on the same
54
+ * rows. The host asks for confirmation before it deletes; a rejection means
55
+ * nothing was deleted.
56
+ */
57
+ onDeleteComment?: (itemId: string | number) => void | Promise<void>;
43
58
  /** Optional list of users (or any mentionable entities) the comment
44
59
  * input can suggest after the user types `@`. When omitted the input
45
60
  * falls back to free-text @-mentions without autocompletion. */
@@ -24,6 +24,8 @@ export const DiscussionContextProvider = ({ children, ...value }) => {
24
24
  value.onAddComment,
25
25
  value.onAddReply,
26
26
  value.onToggleReaction,
27
+ value.onEditComment,
28
+ value.onDeleteComment,
27
29
  value.mentionSuggestions,
28
30
  value.onUploadAttachments,
29
31
  value.loading,
@@ -50,6 +50,57 @@ export interface ApprovalProgress {
50
50
  satisfied: boolean;
51
51
  }>;
52
52
  }
53
+ /**
54
+ * The approval state a record surface shows (objectui#2902): whether an
55
+ * approval is running on the record (`pending`, which drives the status band
56
+ * and the recall lever) and whether it also forbids edits (`locked`).
57
+ */
58
+ export interface ApprovalLockState {
59
+ pending: boolean;
60
+ locked: boolean;
61
+ }
62
+ /** Input to {@link resolveApprovalLock}. */
63
+ export interface ApprovalLockInput {
64
+ /**
65
+ * Whether the approvals read ANSWERED (a 2xx) for this record. When it did,
66
+ * the open request is the whole answer and the record's `approval_status`
67
+ * column is not consulted. `false` covers a route that is absent (404 /
68
+ * 501), a read that has not landed yet, and a read that failed before it
69
+ * ever answered.
70
+ */
71
+ readAnswered: boolean;
72
+ /**
73
+ * The open approval on the record, reduced to its lock policy, or `null`
74
+ * when there is none. For a host this is the pending request it found
75
+ * (`locked` from the node's `lock_record`); for a renderer it is the verdict
76
+ * a host threaded through {@link InlineEditContextValue}.
77
+ */
78
+ openApproval: {
79
+ locked: boolean;
80
+ } | null;
81
+ /** The record's own `approval_status` value, read only as the fallback. */
82
+ recordStatus: unknown;
83
+ }
84
+ /**
85
+ * THE approval-lock rule of a record surface (objectui#12220). The record page
86
+ * host (`@object-ui/app-shell` `RecordDetailView`) and the detail body's
87
+ * approval band (`@object-ui/plugin-detail` `DetailView`) both call this, so
88
+ * the two cannot disagree about when a record is locked.
89
+ *
90
+ * 1. An open approval decides: it is pending, and it locks exactly when its
91
+ * node's `lockRecord` policy says so (objectui#2902).
92
+ * 2. With none, an approvals read that ANSWERED means no approval is running:
93
+ * not pending, not locked. The record's `approval_status` column is not a
94
+ * second opinion there. A composition without the approvals service answers
95
+ * the read `200 { data: [] }` and enforces no lock, so a `pending` left in
96
+ * the column by any writer would otherwise lock the record with nothing to
97
+ * recall and nothing the server refuses.
98
+ * 3. Only when no read answered does the column stand in: `pending` or
99
+ * `in_approval` reads as pending AND locked. It carries no node granularity,
100
+ * so it can only mean "locked", the conservative read for a backend that
101
+ * mirrors the status but exposes no approvals API (objectui#2618).
102
+ */
103
+ export declare function resolveApprovalLock({ readAnswered, openApproval, recordStatus, }: ApprovalLockInput): ApprovalLockState;
53
104
  export interface InlineEditContextValue {
54
105
  /** True while the record is in inline-edit mode. */
55
106
  editing: boolean;
@@ -86,6 +137,18 @@ export interface InlineEditContextValue {
86
137
  * still get a coherent band. Defaults to `false`.
87
138
  */
88
139
  approvalPending: boolean;
140
+ /**
141
+ * Whether the host's `locked` / `approvalPending` come from an approvals
142
+ * read that ANSWERED (a 2xx) for this record (objectui#12220). When `true`,
143
+ * the host's pair is the whole answer and a renderer must not fall back to
144
+ * the record's `approval_status` column: `false` / `false` then means "no
145
+ * approval is running", not "this host has no opinion". Feed it to
146
+ * {@link resolveApprovalLock} as `readAnswered`.
147
+ *
148
+ * Omitted ⇒ `false` ⇒ unchanged: a renderer keeps the column as its
149
+ * fallback, so a host that never learns of this prop renders as before.
150
+ */
151
+ approvalReadAnswered?: boolean;
89
152
  /**
90
153
  * Quorum / per-group tally of the pending approval node, when it aggregates
91
154
  * more than one decision (objectstack#4478). Threaded verbatim from the
@@ -218,6 +281,13 @@ export interface InlineEditProviderProps {
218
281
  * only know about the lock rendering exactly as before.
219
282
  */
220
283
  approvalPending?: boolean;
284
+ /**
285
+ * Whether `locked` / `approvalPending` come from an approvals read that
286
+ * answered (objectui#12220; see `InlineEditContextValue`). Omitted ⇒ `false`,
287
+ * which keeps a renderer's `approval_status` column fallback exactly as
288
+ * before.
289
+ */
290
+ approvalReadAnswered?: boolean;
221
291
  /**
222
292
  * The pending node's server-computed decision tally (objectstack#4478).
223
293
  * Surfaced verbatim so the band can show "2 of 3" / per-group ticks. Omitted
@@ -23,8 +23,35 @@ import { jsx as _jsx } from "react/jsx-runtime";
23
23
  * context for the draft/editing flags and drives `saving`/`error`/`reset`.
24
24
  */
25
25
  import React from 'react';
26
+ /**
27
+ * THE approval-lock rule of a record surface (objectui#12220). The record page
28
+ * host (`@object-ui/app-shell` `RecordDetailView`) and the detail body's
29
+ * approval band (`@object-ui/plugin-detail` `DetailView`) both call this, so
30
+ * the two cannot disagree about when a record is locked.
31
+ *
32
+ * 1. An open approval decides: it is pending, and it locks exactly when its
33
+ * node's `lockRecord` policy says so (objectui#2902).
34
+ * 2. With none, an approvals read that ANSWERED means no approval is running:
35
+ * not pending, not locked. The record's `approval_status` column is not a
36
+ * second opinion there. A composition without the approvals service answers
37
+ * the read `200 { data: [] }` and enforces no lock, so a `pending` left in
38
+ * the column by any writer would otherwise lock the record with nothing to
39
+ * recall and nothing the server refuses.
40
+ * 3. Only when no read answered does the column stand in: `pending` or
41
+ * `in_approval` reads as pending AND locked. It carries no node granularity,
42
+ * so it can only mean "locked", the conservative read for a backend that
43
+ * mirrors the status but exposes no approvals API (objectui#2618).
44
+ */
45
+ export function resolveApprovalLock({ readAnswered, openApproval, recordStatus, }) {
46
+ if (openApproval)
47
+ return { pending: true, locked: openApproval.locked };
48
+ if (readAnswered)
49
+ return { pending: false, locked: false };
50
+ const columnPending = recordStatus === 'pending' || recordStatus === 'in_approval';
51
+ return { pending: columnPending, locked: columnPending };
52
+ }
26
53
  const InlineEditContext = React.createContext(null);
27
- export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPending, approvalProgress, approvalIsSubmitter, lockedReason, children, }) => {
54
+ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPending, approvalReadAnswered = false, approvalProgress, approvalIsSubmitter, lockedReason, children, }) => {
28
55
  // A locked record always has an approval running; the converse is what
29
56
  // #2902 added. Defaulting this way means an un-migrated host keeps its
30
57
  // previous behavior with no code change.
@@ -65,6 +92,7 @@ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPen
65
92
  canEdit,
66
93
  locked,
67
94
  approvalPending: pending,
95
+ approvalReadAnswered,
68
96
  approvalProgress,
69
97
  approvalIsSubmitter,
70
98
  lockedReason,
@@ -80,7 +108,7 @@ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPen
80
108
  setSaving,
81
109
  setError,
82
110
  setFieldErrors,
83
- }), [editing, canEdit, locked, pending, approvalProgress, approvalIsSubmitter, lockedReason, draft, autoFocusField, saving, error, fieldErrors, enter, setField, teardown]);
111
+ }), [editing, canEdit, locked, pending, approvalReadAnswered, approvalProgress, approvalIsSubmitter, lockedReason, draft, autoFocusField, saving, error, fieldErrors, enter, setField, teardown]);
84
112
  return _jsx(InlineEditContext.Provider, { value: value, children: children });
85
113
  };
86
114
  /**