@object-ui/react 17.6.0 → 17.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3766 -0
- package/README.md +294 -38
- package/dist/SchemaRenderer.d.ts +38 -7
- package/dist/SchemaRenderer.js +1218 -87
- package/dist/context/ActionContext.js +40 -1
- package/dist/context/DiscussionContext.d.ts +12 -0
- package/dist/context/DiscussionContext.js +2 -0
- package/dist/context/DrillNavigationContext.d.ts +13 -0
- package/dist/context/InlineEditContext.d.ts +81 -1
- package/dist/context/InlineEditContext.js +8 -2
- package/dist/context/RecordContext.d.ts +48 -5
- package/dist/context/RecordContext.js +10 -1
- package/dist/context/RelatedRecordActionsContext.d.ts +5 -1
- package/dist/context/SchemaRendererContext.d.ts +54 -2
- package/dist/context/SchemaRendererContext.js +38 -5
- package/dist/context/ThemeContext.js +1 -1
- package/dist/context/actionRunnerTranslator.d.ts +27 -0
- package/dist/context/actionRunnerTranslator.js +37 -0
- package/dist/element-data-source/ElementDataSourceGate.d.ts +88 -1
- package/dist/element-data-source/ElementDataSourceGate.js +326 -13
- package/dist/hooks/index.d.ts +3 -1
- package/dist/hooks/index.js +10 -1
- package/dist/hooks/useActionEngine.js +46 -3
- package/dist/hooks/useActionRunner.js +4 -0
- package/dist/hooks/useActionTextLocalizer.d.ts +21 -8
- package/dist/hooks/useActionTextLocalizer.js +33 -8
- package/dist/hooks/useConfigBagEvaluator.d.ts +30 -0
- package/dist/hooks/useConfigBagEvaluator.js +39 -0
- package/dist/hooks/useDensityMode.d.ts +6 -2
- package/dist/hooks/useDensityMode.js +6 -2
- package/dist/hooks/useDiscovery.d.ts +1 -10
- package/dist/hooks/useETagCache.js +25 -4
- package/dist/hooks/useElementDataSource.d.ts +15 -2
- package/dist/hooks/useElementDataSource.js +24 -7
- package/dist/hooks/useExpression.d.ts +52 -59
- package/dist/hooks/useExpression.js +58 -90
- package/dist/hooks/useFilterScope.d.ts +22 -2
- package/dist/hooks/useFilterScope.js +29 -4
- package/dist/hooks/useGlobalUndo.d.ts +10 -0
- package/dist/hooks/useGlobalUndo.js +112 -46
- package/dist/hooks/useNavigationOverlay.d.ts +76 -11
- package/dist/hooks/useNavigationOverlay.js +119 -9
- package/dist/hooks/useOffline.js +65 -11
- package/dist/hooks/usePageAssignment.d.ts +8 -5
- package/dist/hooks/usePageAssignment.js +37 -16
- package/dist/hooks/useRecordSearch.d.ts +34 -1
- package/dist/hooks/useRecordSearch.js +42 -7
- package/dist/hooks/useResolvedFilter.d.ts +46 -0
- package/dist/hooks/useResolvedFilter.js +130 -0
- package/dist/hooks/useSchemaPersistence.d.ts +7 -0
- package/dist/hooks/useSchemaPersistence.js +144 -5
- package/dist/hooks/useSettledSchema.d.ts +123 -0
- package/dist/hooks/useSettledSchema.js +186 -0
- package/dist/hooks/useViewData.d.ts +7 -2
- package/dist/index.d.ts +8 -1
- package/dist/index.js +45 -1
- package/dist/schema-input.d.ts +38 -8
- package/dist/schema-input.js +39 -7
- package/dist/utils/configBag.d.ts +68 -0
- package/dist/utils/configBag.js +70 -0
- package/dist/utils/error-message.d.ts +1 -1
- package/dist/utils/error-message.js +1 -1
- package/dist/utils/inlineAria.d.ts +61 -0
- package/dist/utils/inlineAria.js +70 -0
- package/dist/utils/nonGridRowCeiling.d.ts +44 -0
- package/dist/utils/nonGridRowCeiling.js +78 -0
- package/dist/utils/paramsBag.d.ts +28 -0
- package/dist/utils/paramsBag.js +146 -0
- package/dist/utils/propsBagDiagnostic.d.ts +196 -0
- package/dist/utils/propsBagDiagnostic.js +421 -0
- package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
- package/dist/utils/refusedDataPropDiagnostic.js +94 -0
- package/dist/utils/unevaluatedExpression.d.ts +42 -1
- package/dist/utils/unevaluatedExpression.js +74 -11
- package/dist/utils/visibilityDiagnostic.d.ts +271 -19
- package/dist/utils/visibilityDiagnostic.js +311 -44
- package/package.json +10 -10
- package/dist/hooks/useClientNotifications.d.ts +0 -47
- package/dist/hooks/useClientNotifications.js +0 -162
- package/dist/spec-bridge/SpecBridge.d.ts +0 -28
- package/dist/spec-bridge/SpecBridge.js +0 -56
- package/dist/spec-bridge/bridges/form-view.d.ts +0 -158
- package/dist/spec-bridge/bridges/form-view.js +0 -170
- package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
- package/dist/spec-bridge/bridges/list-view.js +0 -268
- package/dist/spec-bridge/index.d.ts +0 -11
- package/dist/spec-bridge/index.js +0 -10
- package/dist/spec-bridge/types.d.ts +0 -55
- package/dist/spec-bridge/types.js +0 -8
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
import { useCallback, useContext, useMemo } from 'react';
|
|
25
|
-
import { ActionEngine, } from '@object-ui/core';
|
|
25
|
+
import { ActionEngine, subjectPermissionsOf, } from '@object-ui/core';
|
|
26
26
|
import { ActionCtxReact } from '../context/ActionContext.js';
|
|
27
|
+
import { usePredicateScope } from './useExpression.js';
|
|
27
28
|
export function useActionEngine(options = {}) {
|
|
28
29
|
const { actions = [], context = {} } = options;
|
|
29
30
|
// When wrapped in an <ActionProvider>, reuse its ActionRunner so that:
|
|
@@ -39,7 +40,42 @@ export function useActionEngine(options = {}) {
|
|
|
39
40
|
// fall back to a self-contained runner constructed from the `context` arg.
|
|
40
41
|
const providerCtx = useContext(ActionCtxReact);
|
|
41
42
|
const sharedRunner = providerCtx?.runner ?? null;
|
|
43
|
+
// The acting subject the host's predicate scope binds as `current_user`
|
|
44
|
+
// (objectui#11212): the ONE object `ExpressionProvider` publishes under
|
|
45
|
+
// `current_user` / `user` / `ctx.user` / `os.user`, carrying the caller's
|
|
46
|
+
// effective object permissions once they have loaded. The engine filters
|
|
47
|
+
// `visible` against the RUNNER's bag, which a host seeds with its own `user`
|
|
48
|
+
// and never with `current_user` — so `current_user.can(…)`, and any other
|
|
49
|
+
// `current_user.*` predicate, faulted on this path in every state and the
|
|
50
|
+
// action was hidden even for a grant holder, while the same predicate
|
|
51
|
+
// answered on every predicate-scope surface. Binding the scope's subject
|
|
52
|
+
// itself — the same object, not a copy, because the engine answers `can`
|
|
53
|
+
// only for a receiver IDENTICAL to the bound `current_user` — is what makes
|
|
54
|
+
// the two paths one bag (objectui#6493). With no host scope (`{}`) nothing
|
|
55
|
+
// is bound and the path behaves as it always did.
|
|
56
|
+
//
|
|
57
|
+
// Only `current_user` is added. The runner's `user` / `ctx.user` / `os.user`
|
|
58
|
+
// stay the host's own object: it carries what the runner itself reads (the
|
|
59
|
+
// `systemPermissions` capability set), which the scope's subject does not.
|
|
60
|
+
//
|
|
61
|
+
// Since objectui#11242 `<ActionProvider>` binds the same subject on its own
|
|
62
|
+
// runner, so under a provider mounted INSIDE the predicate scope this write
|
|
63
|
+
// re-binds the object that is already there. It stays for the two runners
|
|
64
|
+
// no provider binds: this hook's own standalone runner (no provider at
|
|
65
|
+
// all), and a shared runner whose provider sits ABOVE the scope — the
|
|
66
|
+
// console's global provider does (`GlobalActionRuntimeProvider` wraps the
|
|
67
|
+
// routes, and `ExpressionProvider` is mounted inside them) — where the
|
|
68
|
+
// provider has no scope to read.
|
|
69
|
+
const subject = usePredicateScope().current_user;
|
|
70
|
+
// Keyed on what the binding READS, never on the subject's identity (a
|
|
71
|
+
// memoised value upstream — AGENTS.md #10): its serialisable fields, and the
|
|
72
|
+
// permissions map it carries, which the upstream adapter caches per payload
|
|
73
|
+
// outside React. A discarded-and-recomputed subject with the same content
|
|
74
|
+
// keeps the engine; the permissions arriving (or leaving) rebuilds it.
|
|
75
|
+
const subjectKey = subject === undefined ? '' : JSON.stringify(subject);
|
|
76
|
+
const subjectPermissions = subjectPermissionsOf(subject);
|
|
42
77
|
const engine = useMemo(() => {
|
|
78
|
+
const bound = subject !== undefined ? { current_user: subject } : {};
|
|
43
79
|
// When standalone (no surrounding `<ActionProvider>`), normalize the
|
|
44
80
|
// context so predicates can use both `record`/`user` and `ctx.*`.
|
|
45
81
|
const normalizedStandalone = (context && Object.keys(context).length > 0)
|
|
@@ -48,8 +84,9 @@ export function useActionEngine(options = {}) {
|
|
|
48
84
|
ctx: (context.ctx && typeof context.ctx === 'object')
|
|
49
85
|
? { ...context, ...context.ctx }
|
|
50
86
|
: { ...context },
|
|
87
|
+
...bound,
|
|
51
88
|
}
|
|
52
|
-
: context;
|
|
89
|
+
: { ...context, ...bound };
|
|
53
90
|
const e = sharedRunner ? new ActionEngine(sharedRunner) : new ActionEngine(normalizedStandalone);
|
|
54
91
|
// When sharing a provider runner, MERGE per-render flat keys into the
|
|
55
92
|
// existing `ctx` instead of overwriting it. The provider seeds
|
|
@@ -67,13 +104,19 @@ export function useActionEngine(options = {}) {
|
|
|
67
104
|
const merged = {
|
|
68
105
|
...context,
|
|
69
106
|
ctx: { ...existingCtx, ...callerCtx },
|
|
107
|
+
...bound,
|
|
70
108
|
};
|
|
71
109
|
runner.updateContext(merged);
|
|
72
110
|
}
|
|
111
|
+
else if (sharedRunner && subject !== undefined) {
|
|
112
|
+
// A caller that passes no per-render keys: the subject is still bound,
|
|
113
|
+
// onto the same shared runner the location filter reads.
|
|
114
|
+
e.getRunner().updateContext(bound);
|
|
115
|
+
}
|
|
73
116
|
e.registerActions(actions);
|
|
74
117
|
return e;
|
|
75
118
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
|
-
}, [sharedRunner, JSON.stringify(actions), JSON.stringify(context)]);
|
|
119
|
+
}, [sharedRunner, JSON.stringify(actions), JSON.stringify(context), subjectKey, subjectPermissions]);
|
|
77
120
|
const getActionsForLocation = useCallback((location) => engine.getActionsForLocation(location), [engine]);
|
|
78
121
|
const getBulkActions = useCallback(() => engine.getBulkActions(), [engine]);
|
|
79
122
|
const executeAction = useCallback((name, contextOverride) => engine.executeAction(name, contextOverride), [engine]);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { useState, useCallback, useMemo } from 'react';
|
|
9
9
|
import { ActionRunner, } from '@object-ui/core';
|
|
10
|
+
import { useActionRunnerTranslator } from '../context/actionRunnerTranslator.js';
|
|
10
11
|
/**
|
|
11
12
|
* Hook for executing actions with loading state, typed with ActionDef.
|
|
12
13
|
*
|
|
@@ -35,8 +36,11 @@ export function useActionRunner(optionsOrContext = {}) {
|
|
|
35
36
|
const [loading, setLoading] = useState(false);
|
|
36
37
|
const [error, setError] = useState(null);
|
|
37
38
|
const [result, setResult] = useState(null);
|
|
39
|
+
// The runner's own copy (the generic success toast) in the session's language.
|
|
40
|
+
const translate = useActionRunnerTranslator();
|
|
38
41
|
const runner = useMemo(() => {
|
|
39
42
|
const r = new ActionRunner(context);
|
|
43
|
+
r.setTranslator(translate);
|
|
40
44
|
if (onConfirm)
|
|
41
45
|
r.setConfirmHandler(onConfirm);
|
|
42
46
|
if (onToast)
|
|
@@ -47,11 +47,23 @@
|
|
|
47
47
|
* Translation localizes what exists; it does not add behaviour. This mirrors
|
|
48
48
|
* what the already-correct call sites (`RecordDetailView`, `ObjectView`,
|
|
49
49
|
* `DeclaredActionsBar`) each spelled out by hand.
|
|
50
|
-
* - **`
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
50
|
+
* - **`outcomeMessages` follows the same rule, entry by entry**
|
|
51
|
+
* (`@objectstack/spec` 17.6.0, objectui#11344): each outcome the action
|
|
52
|
+
* declares resolves `_actions.<name>.outcomeMessages.<outcome>`; a bundle
|
|
53
|
+
* entry for an outcome the action does not declare is never read, so a
|
|
54
|
+
* bundle cannot add copy for an answer the author never wrote one for.
|
|
55
|
+
* - **`label`, `successMessage` and each `outcomeMessages` entry are reduced
|
|
56
|
+
* through `pickLocalized` first**, because each is an `I18nLabel` — a string
|
|
57
|
+
* or an inline per-locale map. The map is collapsed to the active language
|
|
58
|
+
* BEFORE it is offered to the bundle as the fallback, so a per-locale
|
|
59
|
+
* literal and a bundle entry cannot disagree about what "the authored text"
|
|
60
|
+
* is, and the action runner, which has no language, only ever receives a
|
|
61
|
+
* string.
|
|
62
|
+
* - **The success copy stays a template.** A `${result.*}` token, in a bundle
|
|
63
|
+
* entry or in the authored text, passes through this hook untouched: the
|
|
64
|
+
* action has not run yet, so there is no result to read. The runner fills
|
|
65
|
+
* the token in after the action succeeds, through the same `${result.*}`
|
|
66
|
+
* scope `onSuccess.navigate` reads — one interpolation, in one place.
|
|
55
67
|
*/
|
|
56
68
|
export interface ActionTextLocalizerOptions {
|
|
57
69
|
/**
|
|
@@ -61,9 +73,10 @@ export interface ActionTextLocalizerOptions {
|
|
|
61
73
|
fallbackLabel?: string;
|
|
62
74
|
}
|
|
63
75
|
/**
|
|
64
|
-
* Returns a copy of `action` with `label` — plus `confirmText
|
|
65
|
-
* `successMessage` when the action declares them —
|
|
66
|
-
* locale's bundle, falling back to the authored
|
|
76
|
+
* Returns a copy of `action` with `label` — plus `confirmText`,
|
|
77
|
+
* `successMessage` and `outcomeMessages` when the action declares them —
|
|
78
|
+
* resolved from the active locale's bundle, falling back to the authored
|
|
79
|
+
* literals.
|
|
67
80
|
*/
|
|
68
81
|
export type ActionTextLocalizer = <T extends Record<string, any>>(objectName: string | undefined, action: T, options?: ActionTextLocalizerOptions) => T;
|
|
69
82
|
export declare function useActionTextLocalizer(): ActionTextLocalizer;
|
|
@@ -47,16 +47,29 @@
|
|
|
47
47
|
* Translation localizes what exists; it does not add behaviour. This mirrors
|
|
48
48
|
* what the already-correct call sites (`RecordDetailView`, `ObjectView`,
|
|
49
49
|
* `DeclaredActionsBar`) each spelled out by hand.
|
|
50
|
-
* - **`
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
50
|
+
* - **`outcomeMessages` follows the same rule, entry by entry**
|
|
51
|
+
* (`@objectstack/spec` 17.6.0, objectui#11344): each outcome the action
|
|
52
|
+
* declares resolves `_actions.<name>.outcomeMessages.<outcome>`; a bundle
|
|
53
|
+
* entry for an outcome the action does not declare is never read, so a
|
|
54
|
+
* bundle cannot add copy for an answer the author never wrote one for.
|
|
55
|
+
* - **`label`, `successMessage` and each `outcomeMessages` entry are reduced
|
|
56
|
+
* through `pickLocalized` first**, because each is an `I18nLabel` — a string
|
|
57
|
+
* or an inline per-locale map. The map is collapsed to the active language
|
|
58
|
+
* BEFORE it is offered to the bundle as the fallback, so a per-locale
|
|
59
|
+
* literal and a bundle entry cannot disagree about what "the authored text"
|
|
60
|
+
* is, and the action runner, which has no language, only ever receives a
|
|
61
|
+
* string.
|
|
62
|
+
* - **The success copy stays a template.** A `${result.*}` token, in a bundle
|
|
63
|
+
* entry or in the authored text, passes through this hook untouched: the
|
|
64
|
+
* action has not run yet, so there is no result to read. The runner fills
|
|
65
|
+
* the token in after the action succeeds, through the same `${result.*}`
|
|
66
|
+
* scope `onSuccess.navigate` reads — one interpolation, in one place.
|
|
55
67
|
*/
|
|
56
68
|
import { useMemo } from 'react';
|
|
57
69
|
import { useObjectLabel, useObjectTranslation, pickLocalized } from '@object-ui/i18n';
|
|
70
|
+
import { isConfigBag } from '../utils/configBag.js';
|
|
58
71
|
export function useActionTextLocalizer() {
|
|
59
|
-
const { actionLabel, actionConfirm, actionSuccess } = useObjectLabel();
|
|
72
|
+
const { actionLabel, actionConfirm, actionSuccess, actionOutcome } = useObjectLabel();
|
|
60
73
|
const { language } = useObjectTranslation();
|
|
61
74
|
return useMemo(() => {
|
|
62
75
|
return ((objectName, action, options) => {
|
|
@@ -83,9 +96,21 @@ export function useActionTextLocalizer() {
|
|
|
83
96
|
out.confirmText = actionConfirm(objectName, name, action.confirmText);
|
|
84
97
|
}
|
|
85
98
|
if (action.successMessage !== undefined) {
|
|
86
|
-
out.successMessage = actionSuccess(objectName, name, action.successMessage);
|
|
99
|
+
out.successMessage = actionSuccess(objectName, name, pickLocalized(action.successMessage, language));
|
|
100
|
+
}
|
|
101
|
+
// The map is an authored bag (outcome → copy), so the one "is this a
|
|
102
|
+
// real bag?" predicate in this package answers whether to read it.
|
|
103
|
+
const authoredOutcomes = action.outcomeMessages;
|
|
104
|
+
if (isConfigBag(authoredOutcomes)) {
|
|
105
|
+
const outcomeMessages = {};
|
|
106
|
+
for (const [outcome, copy] of Object.entries(authoredOutcomes)) {
|
|
107
|
+
const text = actionOutcome(objectName, name, outcome, pickLocalized(copy, language));
|
|
108
|
+
if (text !== undefined)
|
|
109
|
+
outcomeMessages[outcome] = text;
|
|
110
|
+
}
|
|
111
|
+
out.outcomeMessages = outcomeMessages;
|
|
87
112
|
}
|
|
88
113
|
return out;
|
|
89
114
|
});
|
|
90
|
-
}, [actionLabel, actionConfirm, actionSuccess, language]);
|
|
115
|
+
}, [actionLabel, actionConfirm, actionSuccess, actionOutcome, language]);
|
|
91
116
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The `properties` evaluation of the `SchemaRenderer` memo, for a node that is
|
|
10
|
+
* rendered WITHOUT `SchemaRenderer` (objectui#10290).
|
|
11
|
+
*
|
|
12
|
+
* The action containers (`action:bar`, `action:group`, `action:menu` in
|
|
13
|
+
* `@object-ui/components`) draw their member actions themselves, so a member's
|
|
14
|
+
* `properties` never passes through the memo. Its static execution values ride
|
|
15
|
+
* `properties.params` (objectui#10289), and those values are templates
|
|
16
|
+
* evaluated where `properties` are (objectui#7867). This hook is that
|
|
17
|
+
* evaluation: the memo's per-key rule, its `ExpressionEvaluator`, and its
|
|
18
|
+
* scope (the host's ambient roots, `current_user`, the page's bound row as
|
|
19
|
+
* `record`, and page variables as `page`), read from the same contexts the memo
|
|
20
|
+
* reads. It is not a second template engine. See `evaluateConfigBagInScope`.
|
|
21
|
+
*
|
|
22
|
+
* The returned function takes a config bag and returns a new, evaluated bag. A
|
|
23
|
+
* value that is not a config bag is returned unchanged. Call it once per
|
|
24
|
+
* authored bag: the result is already evaluated, and evaluating it again would
|
|
25
|
+
* interpolate text that came out of the row.
|
|
26
|
+
*
|
|
27
|
+
* A fresh function each render. Nothing may depend on its identity
|
|
28
|
+
* (AGENTS.md #10).
|
|
29
|
+
*/
|
|
30
|
+
export declare function useConfigBagEvaluator(): (bag: unknown) => unknown;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
import { useRecordContext } from '../context/RecordContext.js';
|
|
9
|
+
import { evaluateConfigBagInScope } from '../SchemaRenderer.js';
|
|
10
|
+
import { usePredicateScope } from './useExpression.js';
|
|
11
|
+
import { usePageVariables } from './usePageVariables.js';
|
|
12
|
+
/**
|
|
13
|
+
* The `properties` evaluation of the `SchemaRenderer` memo, for a node that is
|
|
14
|
+
* rendered WITHOUT `SchemaRenderer` (objectui#10290).
|
|
15
|
+
*
|
|
16
|
+
* The action containers (`action:bar`, `action:group`, `action:menu` in
|
|
17
|
+
* `@object-ui/components`) draw their member actions themselves, so a member's
|
|
18
|
+
* `properties` never passes through the memo. Its static execution values ride
|
|
19
|
+
* `properties.params` (objectui#10289), and those values are templates
|
|
20
|
+
* evaluated where `properties` are (objectui#7867). This hook is that
|
|
21
|
+
* evaluation: the memo's per-key rule, its `ExpressionEvaluator`, and its
|
|
22
|
+
* scope (the host's ambient roots, `current_user`, the page's bound row as
|
|
23
|
+
* `record`, and page variables as `page`), read from the same contexts the memo
|
|
24
|
+
* reads. It is not a second template engine. See `evaluateConfigBagInScope`.
|
|
25
|
+
*
|
|
26
|
+
* The returned function takes a config bag and returns a new, evaluated bag. A
|
|
27
|
+
* value that is not a config bag is returned unchanged. Call it once per
|
|
28
|
+
* authored bag: the result is already evaluated, and evaluating it again would
|
|
29
|
+
* interpolate text that came out of the row.
|
|
30
|
+
*
|
|
31
|
+
* A fresh function each render. Nothing may depend on its identity
|
|
32
|
+
* (AGENTS.md #10).
|
|
33
|
+
*/
|
|
34
|
+
export function useConfigBagEvaluator() {
|
|
35
|
+
const predicateScope = usePredicateScope();
|
|
36
|
+
const boundRecord = useRecordContext()?.data;
|
|
37
|
+
const { variables: pageVariables } = usePageVariables();
|
|
38
|
+
return (bag) => evaluateConfigBagInScope(bag, predicateScope, boundRecord, pageVariables);
|
|
39
|
+
}
|
|
@@ -50,8 +50,12 @@ export interface DensityResult {
|
|
|
50
50
|
*
|
|
51
51
|
* @example
|
|
52
52
|
* ```tsx
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* import { DENSITY_MODE_TO_ROW_HEIGHT, rowHeightToDensityMode } from '@object-ui/core';
|
|
54
|
+
*
|
|
55
|
+
* // A view stores the protocol's `rowHeight`; the legacy `densityMode` key is
|
|
56
|
+
* // retired on `NamedListView` (objectui#7924), so persist the spec value.
|
|
57
|
+
* const density = useDensityMode(rowHeightToDensityMode(activeView?.rowHeight) ?? 'comfortable', {
|
|
58
|
+
* onChange: (m) => dataSource.updateViewConfig(obj, vid, { rowHeight: DENSITY_MODE_TO_ROW_HEIGHT[m] }),
|
|
55
59
|
* });
|
|
56
60
|
* ```
|
|
57
61
|
*/
|
|
@@ -35,8 +35,12 @@ const DENSITY_ORDER = ['compact', 'comfortable', 'spacious'];
|
|
|
35
35
|
*
|
|
36
36
|
* @example
|
|
37
37
|
* ```tsx
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* import { DENSITY_MODE_TO_ROW_HEIGHT, rowHeightToDensityMode } from '@object-ui/core';
|
|
39
|
+
*
|
|
40
|
+
* // A view stores the protocol's `rowHeight`; the legacy `densityMode` key is
|
|
41
|
+
* // retired on `NamedListView` (objectui#7924), so persist the spec value.
|
|
42
|
+
* const density = useDensityMode(rowHeightToDensityMode(activeView?.rowHeight) ?? 'comfortable', {
|
|
43
|
+
* onChange: (m) => dataSource.updateViewConfig(obj, vid, { rowHeight: DENSITY_MODE_TO_ROW_HEIGHT[m] }),
|
|
40
44
|
* });
|
|
41
45
|
* ```
|
|
42
46
|
*/
|
|
@@ -40,17 +40,8 @@ export interface DiscoveryInfo {
|
|
|
40
40
|
/** Server name and version */
|
|
41
41
|
name?: string;
|
|
42
42
|
version?: string;
|
|
43
|
-
/** Runtime mode (
|
|
43
|
+
/** Runtime mode the server reports (`RuntimeMode` in `@objectstack/spec`) */
|
|
44
44
|
mode?: string;
|
|
45
|
-
/** Preview mode configuration from the kernel (present when mode is 'preview') */
|
|
46
|
-
previewMode?: {
|
|
47
|
-
autoLogin?: boolean;
|
|
48
|
-
simulatedRole?: 'admin' | 'user' | 'viewer';
|
|
49
|
-
simulatedUserName?: string;
|
|
50
|
-
readOnly?: boolean;
|
|
51
|
-
expiresInSeconds?: number;
|
|
52
|
-
bannerMessage?: string;
|
|
53
|
-
};
|
|
54
45
|
/** Service availability status */
|
|
55
46
|
services?: {
|
|
56
47
|
/** Authentication service status */
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
|
+
import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from 'react';
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// Defaults
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
@@ -141,9 +141,30 @@ export function useETagCache(userConfig = {}) {
|
|
|
141
141
|
const swUrl = userConfig.serviceWorkerUrl ?? DEFAULTS.serviceWorkerUrl;
|
|
142
142
|
const [cacheSize, setCacheSize] = useState(0);
|
|
143
143
|
const [serviceWorkerActive, setServiceWorkerActive] = useState(false);
|
|
144
|
-
// Keep config in a ref so callbacks always see latest values
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
// Keep config in a ref so callbacks always see latest values. Every reader
|
|
145
|
+
// below is a `useCallback` with `[]` deps whose identity consumers may key
|
|
146
|
+
// effects on, so the config has to reach them without changing it — that is
|
|
147
|
+
// the ref's job. Only the WRITE moves: done in the render body it also ran
|
|
148
|
+
// for renders React discards or replays, publishing config from a tree that
|
|
149
|
+
// never committed. `useInsertionEffect` runs in the mutation phase, ahead of
|
|
150
|
+
// every layout effect, ref attachment and paint, so the sole window deferred
|
|
151
|
+
// is the render phase — where `fetchWithETag` / `clearCache` and the other
|
|
152
|
+
// side effects are not callable anyway.
|
|
153
|
+
//
|
|
154
|
+
// The object itself is built by `useMemo` rather than inline at the two
|
|
155
|
+
// places that consume it. `useRef({ ... })` evaluates its argument on EVERY
|
|
156
|
+
// render and keeps only the first result, so every later render allocated a
|
|
157
|
+
// five-key object that was thrown away (objectui#6817) — the same shape PR
|
|
158
|
+
// objectui#6796 repaired in `useSchemaPersistence`. Its identity is
|
|
159
|
+
// unobservable from outside the hook: the ref is private and every reader
|
|
160
|
+
// only reads fields off `.current`, so a memo React chooses to discard is
|
|
161
|
+
// harmless — it rebuilds an equal object, which is what every render used
|
|
162
|
+
// to do unconditionally.
|
|
163
|
+
const config = useMemo(() => ({ enabled, storage, storagePrefix, maxEntries, ttl }), [enabled, storage, storagePrefix, maxEntries, ttl]);
|
|
164
|
+
const configRef = useRef(config);
|
|
165
|
+
useInsertionEffect(() => {
|
|
166
|
+
configRef.current = config;
|
|
167
|
+
});
|
|
147
168
|
// Hydrate memory cache from localStorage on mount
|
|
148
169
|
useEffect(() => {
|
|
149
170
|
if (storage === 'localStorage') {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* still ignores `view`), and the remaining object-bound blocks will need the same
|
|
20
20
|
* resolution. One resolver in the runtime layer, not one per plugin.
|
|
21
21
|
*/
|
|
22
|
-
import { type ComposedElementDataSource, type ElementDataSourceConfig, type ElementSavedView } from '@object-ui/core';
|
|
22
|
+
import { FilterOperatorError, type ComposedElementDataSource, type ElementDataSourceConfig, type ElementSavedView } from '@object-ui/core';
|
|
23
23
|
/**
|
|
24
24
|
* Where a resolution attempt stands. Distinguishing `loading` from `missing`
|
|
25
25
|
* matters: a component that treated "not resolved yet" as "does not exist" would
|
|
@@ -34,7 +34,12 @@ export type ElementDataSourceStatus =
|
|
|
34
34
|
| 'loading'
|
|
35
35
|
/** The named `view` was found and composed in. */
|
|
36
36
|
| 'resolved'
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The binding cannot be applied: the named `view` does not exist on the
|
|
39
|
+
* object (or could not be read), or a filter it combines is REFUSED by the
|
|
40
|
+
* filter converter — then `filterRefusal` carries the refusal (objectui#10789).
|
|
41
|
+
* Either way `composed` is withheld, so no query runs without the filter.
|
|
42
|
+
*/
|
|
38
43
|
| 'missing';
|
|
39
44
|
export interface UseElementDataSourceResult {
|
|
40
45
|
status: ElementDataSourceStatus;
|
|
@@ -50,6 +55,14 @@ export interface UseElementDataSourceResult {
|
|
|
50
55
|
view?: ElementSavedView;
|
|
51
56
|
/** Author-facing explanation, set only for `missing`. */
|
|
52
57
|
error?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Why the binding could not be composed, when the reason is a refused
|
|
60
|
+
* filter — set only with `missing`, and `error` then carries its message
|
|
61
|
+
* (objectui#10789). A caller that draws the malformed-filter notice reads the
|
|
62
|
+
* refused operator or field off it (`filterRefusalSubject`) rather than
|
|
63
|
+
* scraping `error`.
|
|
64
|
+
*/
|
|
65
|
+
filterRefusal?: FilterOperatorError;
|
|
53
66
|
}
|
|
54
67
|
/**
|
|
55
68
|
* Resolve a schema node's `dataSource` binding, fetching the named saved view.
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* resolution. One resolver in the runtime layer, not one per plugin.
|
|
21
21
|
*/
|
|
22
22
|
import { useContext, useEffect, useMemo, useState } from 'react';
|
|
23
|
-
import { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, isElementDataSourceConfig, resolveSavedView, } from '@object-ui/core';
|
|
23
|
+
import { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, FilterOperatorError, isElementDataSourceConfig, resolveSavedView, } from '@object-ui/core';
|
|
24
24
|
import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
|
|
25
25
|
const isRecord = (v) => !!v && typeof v === 'object';
|
|
26
26
|
/**
|
|
@@ -71,6 +71,9 @@ async function fetchSavedViews(dataSource, object) {
|
|
|
71
71
|
* ```
|
|
72
72
|
*/
|
|
73
73
|
export function useElementDataSource(schema, dataSource) {
|
|
74
|
+
// Read AS DECLARED (objectui#7209) — no cast between the hook and the value,
|
|
75
|
+
// so a member the context does not declare is a compile error here. Pinned by
|
|
76
|
+
// `useElementDataSource.schemaRendererContextRead-7209.test.ts`.
|
|
74
77
|
const context = useContext(SchemaRendererContext);
|
|
75
78
|
const adapter = (dataSource ?? context?.dataSource ?? null);
|
|
76
79
|
const config = useMemo(() => {
|
|
@@ -131,11 +134,25 @@ export function useElementDataSource(schema, dataSource) {
|
|
|
131
134
|
return { status: 'loading', config };
|
|
132
135
|
if (resolved.view === null)
|
|
133
136
|
return { status: 'missing', config, error: resolved.error };
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
// objectui#10789 — composing the view's filter with the binding's own is a
|
|
138
|
+
// MERGE, and the merge lowers both through the throwing converter form.
|
|
139
|
+
// This is a render-time `useMemo`, so an uncaught refusal was a render
|
|
140
|
+
// error: the block's error boundary instead of a sentence naming the
|
|
141
|
+
// operator. The refusal is kept as a VALUE and reported as `missing`,
|
|
142
|
+
// which every caller already answers by withholding the query — never as
|
|
143
|
+
// `undefined`, which would read as "no filter" and widen the read. Only a
|
|
144
|
+
// `FilterOperatorError` is caught, the rule `toFilterNodeSafely` states:
|
|
145
|
+
// anything else is a defect, not a statement about the author's filter.
|
|
146
|
+
// (The `ready` branch above composes no view, so nothing is merged there.)
|
|
147
|
+
let composed;
|
|
148
|
+
try {
|
|
149
|
+
composed = composeElementDataSource(config, resolved.view);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (!(error instanceof FilterOperatorError))
|
|
153
|
+
throw error;
|
|
154
|
+
return { status: 'missing', config, error: error.message, filterRefusal: error };
|
|
155
|
+
}
|
|
156
|
+
return { status: 'resolved', config, view: resolved.view, composed };
|
|
140
157
|
}, [config, resolved, requestKey]);
|
|
141
158
|
}
|
|
@@ -60,56 +60,49 @@ export { toPredicateInput } from '@object-ui/core';
|
|
|
60
60
|
* Build the predicate context for a **row record** — the bag to hand
|
|
61
61
|
* {@link useCondition} when the thing being gated is scoped to one record.
|
|
62
62
|
*
|
|
63
|
-
* The row is bound
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* ##
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* `
|
|
85
|
-
*
|
|
86
|
-
* `
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* `
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* full measurement and the layer scoping (`data` stays canonical one layer
|
|
107
|
-
* over, in a metadata-editing form — ADR-0089 D3).
|
|
108
|
-
*
|
|
109
|
-
* `record` and `data` are written AFTER the spread deliberately: a row that
|
|
110
|
-
* happens to carry a field literally named `record` or `data` must not shadow
|
|
111
|
-
* the root every predicate is written against. Same precedence as
|
|
112
|
-
* `evalRowPredicate`.
|
|
63
|
+
* The row is bound ONE way: `record.status` — the canon (objectui#5330, ruled
|
|
64
|
+
* 2026-08-20; Phase 2 executed by `83fe6e741`). This is not a dialect choice;
|
|
65
|
+
* it is one rule, and it is `evalRowPredicate`'s rule
|
|
66
|
+
* (`core/evaluator/listConditional.ts` — the record header, list rows, the row
|
|
67
|
+
* kebab and conditional formatting all evaluate through it), restated here for
|
|
68
|
+
* the `useCondition` tier so both tiers answer an author's `visible:` the same
|
|
69
|
+
* way.
|
|
70
|
+
*
|
|
71
|
+
* ## The two retired spellings (`83fe6e741`)
|
|
72
|
+
*
|
|
73
|
+
* Until Phase 2 this bag also carried the row spread flat (bare `status`, the
|
|
74
|
+
* row-action shorthand) and as `data` (legacy), and `useCondition` below warned
|
|
75
|
+
* once per non-canonical spelling (Phase 1, `d1ab06f0f`). Both bindings and the
|
|
76
|
+
* warning are gone. A bare-field or `data.*` predicate against this bag now
|
|
77
|
+
* faults exactly as it always did on the server (`buildScope({ record })`
|
|
78
|
+
* mounts exactly `['record']`: `Unknown variable: status` / `Unknown variable:
|
|
79
|
+
* data`), and each `useCondition` leg applies its EXISTING fault policy — the
|
|
80
|
+
* throwing legs (`throwOnError`: every action `visible` — `action:button`,
|
|
81
|
+
* `action:menu`, `action:bar`, `DeclaredActionsBar`, and since objectui#11212
|
|
82
|
+
* `action:group`, `action:icon` and the related-list toolbar) hide and report
|
|
83
|
+
* `was hidden/disabled: its predicate threw`, naming the variable; the
|
|
84
|
+
* non-throwing legs fail soft to `true`. The same holds for a legacy `${data.x}` / `${x}` string: one bag
|
|
85
|
+
* shape, both dialects. Nothing detects a retired spelling here; it is simply
|
|
86
|
+
* unbound. `@object-ui/core`'s `evaluator/rowPredicateCanon.ts` carries the
|
|
87
|
+
* canon statement, the server measurement, the layer scoping (`data` stays
|
|
88
|
+
* canonical one layer over, in a metadata-editing form — ADR-0089 D3) and the
|
|
89
|
+
* offline detector.
|
|
90
|
+
*
|
|
91
|
+
* ## Why a helper and not "just `{ record }`" (objectui#4075 / #4080)
|
|
92
|
+
*
|
|
93
|
+
* Because the rule has two halves and every predicate face has to get both:
|
|
94
|
+
* the SHAPE — `record` is the row's one name, so `record.viewer.can_act`, what
|
|
95
|
+
* every declared action on framework's `sys_approval_request` gates on
|
|
96
|
+
* (framework#3310 / objectstack-ai/objectstack#3424), reaches the row — and the NO-ROW case below.
|
|
97
|
+
* `DeclaredActionsBar` and the four generic action renderers once carried
|
|
98
|
+
* inline copies of this bag and drifted (objectui#4077 / #4079); one named
|
|
99
|
+
* helper is what keeps a fifth copy from drifting again.
|
|
100
|
+
*
|
|
101
|
+
* Note the direction of the merge: `useCondition` evaluates on
|
|
102
|
+
* `new ExpressionEvaluator({ ...scope, ...context })`, so this bag's `record`
|
|
103
|
+
* shadows an ambient `record` a host put in the predicate scope — the row is
|
|
104
|
+
* the subject — while every OTHER ambient key (`features`, `user`, a host's own
|
|
105
|
+
* `data`) survives to the predicate as the host's own.
|
|
113
106
|
*
|
|
114
107
|
* This is the BINDING rule only. It deliberately does not touch the evaluation
|
|
115
108
|
* entry — `useCondition` / `toPredicateInput` / `hasDeclaredVisibilityGate`
|
|
@@ -121,10 +114,10 @@ export { toPredicateInput } from '@object-ui/core';
|
|
|
121
114
|
* ## No row → bind NOTHING, do not bind an empty one
|
|
122
115
|
*
|
|
123
116
|
* `null` / `undefined` / a non-object returns an EMPTY bag, not
|
|
124
|
-
* `{ record: {}
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
117
|
+
* `{ record: {} }`. The difference is load-bearing: `useCondition` merges this
|
|
118
|
+
* context OVER the ambient predicate scope, so binding an empty record would
|
|
119
|
+
* blank out a `record` that a host had put in the scope itself — which is
|
|
120
|
+
* exactly how `action:group`'s dropdown leaf is driven in
|
|
128
121
|
* `action-group-dropdown-visible.test.tsx`, and it is a legitimate way for a
|
|
129
122
|
* host to supply the row. "This surface has no row of its own" must stay
|
|
130
123
|
* distinct from "this surface's row is empty"; only the latter is entitled to
|
|
@@ -159,11 +152,11 @@ export declare function useCondition(condition: string | boolean | undefined | {
|
|
|
159
152
|
* routes to `@object-ui/core`'s `evalRowPredicate`: a bare string is CEL (the
|
|
160
153
|
* spec contract for `ActionSchema.visible`), a `{ dialect: 'cel', source }`
|
|
161
154
|
* envelope is always CEL, and only a legacy-dialect string falls back to the
|
|
162
|
-
* old engine (with a deprecation warning). The row is bound as `record.*`
|
|
163
|
-
*
|
|
164
|
-
* `
|
|
165
|
-
* (`features` / `user` / …) is merged alongside so
|
|
166
|
-
* resolving.
|
|
155
|
+
* old engine (with a deprecation warning). The row is bound as `record.*` only
|
|
156
|
+
* — the canon (objectui#5330; the bare-field and `data.*` spellings retired in
|
|
157
|
+
* `83fe6e741` and fault here like any unknown variable); the ambient
|
|
158
|
+
* predicate scope (`features` / `user` / …) is merged alongside so
|
|
159
|
+
* deployment-level gates keep resolving.
|
|
167
160
|
*
|
|
168
161
|
* @param pred The raw predicate: `boolean` (returned as-is), a CEL string,
|
|
169
162
|
* an `{ dialect, source }` envelope, or `null`/`undefined`/`''`.
|