@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.
- package/CHANGELOG.md +497 -0
- package/README.md +89 -4
- package/dist/SchemaRenderer.d.ts +27 -1
- package/dist/SchemaRenderer.js +139 -50
- package/dist/context/AppShellContext.d.ts +27 -0
- package/dist/context/DiscussionContext.d.ts +15 -0
- package/dist/context/DiscussionContext.js +2 -0
- package/dist/context/InlineEditContext.d.ts +70 -0
- package/dist/context/InlineEditContext.js +30 -2
- package/dist/context/PrintPageContext.d.ts +60 -0
- package/dist/context/PrintPageContext.js +32 -0
- package/dist/context/RelatedRecordActionsContext.d.ts +67 -1
- package/dist/context/RelatedRecordActionsContext.js +4 -0
- package/dist/context/SchemaRendererContext.d.ts +7 -4
- package/dist/context/SchemaRendererContext.js +1 -1
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +1 -0
- package/dist/element-data-source/ElementApiDataSourcePanel.d.ts +28 -0
- package/dist/element-data-source/ElementApiDataSourcePanel.js +10 -0
- package/dist/element-data-source/ElementDataSourceGate.d.ts +30 -2
- package/dist/element-data-source/ElementDataSourceGate.js +75 -6
- package/dist/element-data-source/useElementApiDataSource.d.ts +119 -0
- package/dist/element-data-source/useElementApiDataSource.js +198 -0
- package/dist/hooks/useActionTextLocalizer.d.ts +20 -7
- package/dist/hooks/useActionTextLocalizer.js +43 -12
- package/dist/hooks/useConfigBagEvaluator.d.ts +5 -4
- package/dist/hooks/useConfigBagEvaluator.js +5 -4
- package/dist/hooks/useDatasetDimensionLabels.js +25 -10
- package/dist/hooks/useDensityMode.d.ts +4 -2
- package/dist/hooks/useDensityMode.js +4 -2
- package/dist/hooks/usePageAssignment.d.ts +31 -8
- package/dist/hooks/usePageAssignment.js +68 -25
- package/dist/hooks/useViewData.d.ts +3 -1
- package/dist/hooks/useViewData.js +5 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/utils/localeMap.d.ts +27 -0
- package/dist/utils/localeMap.js +106 -0
- package/dist/utils/propsBagDiagnostic.js +3 -2
- package/dist/utils/unevaluatedExpression.d.ts +3 -1
- package/dist/utils/unevaluatedExpression.js +18 -0
- package/dist/utils/visibilityDiagnostic.d.ts +14 -5
- package/dist/utils/visibilityDiagnostic.js +10 -2
- package/package.json +6 -6
package/dist/SchemaRenderer.d.ts
CHANGED
|
@@ -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:
|
|
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.
|
package/dist/SchemaRenderer.js
CHANGED
|
@@ -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'
|
|
228
|
+
const VISIBILITY_SHOW_KEYS = ['visibleWhen', 'visible', 'visibleOn'];
|
|
225
229
|
const VISIBILITY_HIDE_KEYS = ['hidden', 'hiddenOn'];
|
|
226
230
|
/**
|
|
227
|
-
* The same
|
|
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
|
|
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
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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,
|
|
503
|
-
*
|
|
504
|
-
*
|
|
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(
|
|
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
|
|
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
|
|
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 `
|
|
944
|
-
// an action container's members evaluate against this same
|
|
945
|
-
// (objectui#10290)
|
|
946
|
-
|
|
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.
|
|
1248
|
-
//
|
|
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 /
|
|
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` (
|
|
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
|
|
1589
|
-
// deprecated
|
|
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`.
|
|
1596
|
-
//
|
|
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`
|
|
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,
|
|
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,
|
|
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
|
|
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
|
/**
|