@object-ui/react 17.5.0 → 17.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4100 -0
- package/README.md +303 -46
- package/dist/LazyPluginLoader.d.ts +14 -7
- package/dist/LazyPluginLoader.js +14 -7
- package/dist/SchemaRenderer.d.ts +38 -7
- package/dist/SchemaRenderer.js +1603 -60
- 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/HostNavigationContext.d.ts +82 -0
- package/dist/context/HostNavigationContext.js +65 -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 +20 -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/context/index.d.ts +13 -12
- package/dist/context/index.js +13 -12
- package/dist/element-data-source/ElementDataSourceGate.d.ts +163 -3
- package/dist/element-data-source/ElementDataSourceGate.js +407 -15
- package/dist/hooks/index.d.ts +37 -35
- package/dist/hooks/index.js +44 -35
- package/dist/hooks/useActionEngine.js +47 -4
- 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/useCapabilityGate.js +6 -5
- package/dist/hooks/useConfigBagEvaluator.d.ts +30 -0
- package/dist/hooks/useConfigBagEvaluator.js +39 -0
- package/dist/hooks/useCrudShortcuts.js +1 -1
- package/dist/hooks/useDatasetDimensionLabels.js +1 -1
- 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/useDiscovery.js +1 -1
- package/dist/hooks/useETagCache.js +25 -4
- package/dist/hooks/useElementDataSource.d.ts +15 -2
- package/dist/hooks/useElementDataSource.js +29 -9
- package/dist/hooks/useExpression.d.ts +52 -38
- package/dist/hooks/useExpression.js +62 -42
- 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 +77 -12
- 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 +38 -17
- package/dist/hooks/usePageTransition.js +1 -1
- package/dist/hooks/usePageVariableActionBridge.js +2 -2
- 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/hooks/useViewData.js +1 -1
- package/dist/hooks/useViewTransition.js +1 -1
- package/dist/index.d.ts +18 -11
- package/dist/index.js +54 -10
- package/dist/schema-input.d.ts +55 -14
- package/dist/schema-input.js +55 -12
- package/dist/utils/configBag.d.ts +68 -0
- package/dist/utils/configBag.js +70 -0
- package/dist/utils/error-message.d.ts +59 -0
- package/dist/utils/error-message.js +116 -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 +110 -0
- package/dist/utils/unevaluatedExpression.js +257 -0
- package/dist/utils/visibilityDiagnostic.d.ts +371 -0
- package/dist/utils/visibilityDiagnostic.js +551 -0
- 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 -82
- package/dist/spec-bridge/bridges/form-view.js +0 -126
- package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
- package/dist/spec-bridge/bridges/list-view.js +0 -241
- 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
package/dist/hooks/index.js
CHANGED
|
@@ -5,46 +5,55 @@
|
|
|
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
|
-
export * from './useExpression';
|
|
8
|
+
export * from './useExpression.js';
|
|
9
9
|
// Session scope for filter placeholders ({current_user_id}/{current_org_id}).
|
|
10
|
-
export * from './useFilterScope';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export * from './
|
|
14
|
-
export * from './
|
|
15
|
-
export * from './
|
|
10
|
+
export * from './useFilterScope.js';
|
|
11
|
+
// The ONE hold a data node resolves its own authored `filter` through
|
|
12
|
+
// (objectui#10666), fed by `useFilterScope()`.
|
|
13
|
+
export * from './useResolvedFilter.js';
|
|
14
|
+
export * from './useActionRunner.js';
|
|
15
|
+
export * from './useNavigationOverlay.js';
|
|
16
|
+
export * from './usePageVariables.js';
|
|
17
|
+
export * from './usePageVariableActionBridge.js';
|
|
18
|
+
export * from './useViewData.js';
|
|
16
19
|
// PageComponentSchema.dataSource — the spec's per-element data binding.
|
|
17
|
-
export * from './useElementDataSource';
|
|
18
|
-
export * from './useDynamicApp';
|
|
19
|
-
export * from './useDiscovery';
|
|
20
|
-
export * from './useFocusTrap';
|
|
21
|
-
export * from './useFocusManagement';
|
|
22
|
-
export * from './useKeyboardShortcuts';
|
|
23
|
-
export * from './useCrudShortcuts';
|
|
24
|
-
export * from './useReducedMotion';
|
|
25
|
-
export * from './useAnimation';
|
|
26
|
-
export * from './useDensityMode';
|
|
27
|
-
export * from './useViewSharing';
|
|
28
|
-
export * from './
|
|
29
|
-
export * from './
|
|
30
|
-
export * from './
|
|
31
|
-
export * from './
|
|
32
|
-
export * from './
|
|
33
|
-
export * from './
|
|
34
|
-
export * from './
|
|
35
|
-
export * from './
|
|
36
|
-
export * from './
|
|
37
|
-
export * from './
|
|
38
|
-
export * from './useActionEngine';
|
|
20
|
+
export * from './useElementDataSource.js';
|
|
21
|
+
export * from './useDynamicApp.js';
|
|
22
|
+
export * from './useDiscovery.js';
|
|
23
|
+
export * from './useFocusTrap.js';
|
|
24
|
+
export * from './useFocusManagement.js';
|
|
25
|
+
export * from './useKeyboardShortcuts.js';
|
|
26
|
+
export * from './useCrudShortcuts.js';
|
|
27
|
+
export * from './useReducedMotion.js';
|
|
28
|
+
export * from './useAnimation.js';
|
|
29
|
+
export * from './useDensityMode.js';
|
|
30
|
+
export * from './useViewSharing.js';
|
|
31
|
+
export * from './useOffline.js';
|
|
32
|
+
export * from './usePerformance.js';
|
|
33
|
+
export * from './usePerformanceBudget.js';
|
|
34
|
+
export * from './usePageTransition.js';
|
|
35
|
+
export * from './useViewTransition.js';
|
|
36
|
+
export * from './useETagCache.js';
|
|
37
|
+
export * from './useSchemaPersistence.js';
|
|
38
|
+
export * from './useGlobalUndo.js';
|
|
39
|
+
export * from './useDebugMode.js';
|
|
40
|
+
export * from './useActionEngine.js';
|
|
39
41
|
// The ONE application of `useObjectLabel`'s action resolvers over the three
|
|
40
42
|
// authored strings an action carries (label / confirmText / successMessage).
|
|
41
|
-
export * from './useActionTextLocalizer';
|
|
42
|
-
export * from './useCapabilityGate';
|
|
43
|
+
export * from './useActionTextLocalizer.js';
|
|
44
|
+
export * from './useCapabilityGate.js';
|
|
45
|
+
// The `SchemaRenderer` memo's `properties` evaluation, for an action container's
|
|
46
|
+
// members, which are drawn without `SchemaRenderer` (objectui#10290).
|
|
47
|
+
export * from './useConfigBagEvaluator.js';
|
|
43
48
|
// The analytics label net's React glue, consumed by BOTH plugin-dashboard's
|
|
44
49
|
// `DatasetWidget` and plugin-report's dataset block (objectui#4389). It lives
|
|
45
50
|
// here rather than in `@object-ui/core` because it reads `SchemaRendererContext`
|
|
46
51
|
// — see the file header for the measured dependency direction.
|
|
47
|
-
export * from './useDatasetDimensionLabels';
|
|
48
|
-
export * from './useDataRefresh';
|
|
49
|
-
export * from './usePageAssignment';
|
|
50
|
-
export * from './useRecordSearch';
|
|
52
|
+
export * from './useDatasetDimensionLabels.js';
|
|
53
|
+
export * from './useDataRefresh.js';
|
|
54
|
+
export * from './usePageAssignment.js';
|
|
55
|
+
export * from './useRecordSearch.js';
|
|
56
|
+
// The settled-schema RESOLUTION half shared across ObjectKanban / ObjectView /
|
|
57
|
+
// ObjectCalendar's hand copies (objectui#6482, maintainer ruling Option A).
|
|
58
|
+
// Gate PLACEMENT stays per-component — see the hook's own doc comment.
|
|
59
|
+
export * from './useSettledSchema.js';
|
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
import { useCallback, useContext, useMemo } from 'react';
|
|
25
|
-
import { ActionEngine, } from '@object-ui/core';
|
|
26
|
-
import { ActionCtxReact } from '../context/ActionContext';
|
|
25
|
+
import { ActionEngine, subjectPermissionsOf, } from '@object-ui/core';
|
|
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
|
}
|
|
@@ -23,13 +23,14 @@
|
|
|
23
23
|
*
|
|
24
24
|
* **Fail-OPEN when unknown.** No runner, no user, no `systemPermissions` array:
|
|
25
25
|
* the action shows. Unknown is not denied, the server is the authority
|
|
26
|
-
* (ADR-
|
|
27
|
-
*
|
|
28
|
-
* nothing" and gates
|
|
26
|
+
* (the framework's ADR-0124 D1 — server enforces, client is courtesy), and
|
|
27
|
+
* hiding a permitted user's button on missing client data is the worse
|
|
28
|
+
* failure. An EMPTY array is not unknown — it means "holds nothing" and gates
|
|
29
|
+
* normally.
|
|
29
30
|
*/
|
|
30
31
|
import { useCallback, useContext } from 'react';
|
|
31
|
-
import { ActionCtxReact } from '../context/ActionContext';
|
|
32
|
-
import { usePredicateScope } from './useExpression';
|
|
32
|
+
import { ActionCtxReact } from '../context/ActionContext.js';
|
|
33
|
+
import { usePredicateScope } from './useExpression.js';
|
|
33
34
|
/**
|
|
34
35
|
* The caller's system capabilities, or `undefined` when the host never supplied
|
|
35
36
|
* them (→ callers must fail open).
|
|
@@ -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
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import { useMemo } from 'react';
|
|
9
|
-
import { useKeyboardShortcuts, } from './useKeyboardShortcuts';
|
|
9
|
+
import { useKeyboardShortcuts, } from './useKeyboardShortcuts.js';
|
|
10
10
|
/**
|
|
11
11
|
* Registers standard CRUD keyboard shortcuts using the useKeyboardShortcuts hook.
|
|
12
12
|
*
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
import { useContext, useEffect, useMemo, useState } from 'react';
|
|
62
62
|
import { deriveDimensionLabelMaps, loadDimensionFieldMeta, } from '@object-ui/core';
|
|
63
63
|
import { useSafeFieldLabel } from '@object-ui/i18n';
|
|
64
|
-
import { SchemaRendererContext } from '../context/SchemaRendererContext';
|
|
64
|
+
import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
|
|
65
65
|
/**
|
|
66
66
|
* Resolve one dataset query's dimension field metadata — the LOCALE-FREE half.
|
|
67
67
|
*
|
|
@@ -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 */
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import { useState, useEffect, useContext } from 'react';
|
|
9
|
-
import { SchemaRendererContext } from '../context/SchemaRendererContext';
|
|
9
|
+
import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
|
|
10
10
|
/**
|
|
11
11
|
* The backward-compatible "can I actually use this service?" check
|
|
12
12
|
* (ADR-0076 D12 console slice):
|
|
@@ -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,8 +20,8 @@
|
|
|
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';
|
|
24
|
-
import { SchemaRendererContext } from '../context/SchemaRendererContext';
|
|
23
|
+
import { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, FilterOperatorError, isElementDataSourceConfig, resolveSavedView, } from '@object-ui/core';
|
|
24
|
+
import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
|
|
25
25
|
const isRecord = (v) => !!v && typeof v === 'object';
|
|
26
26
|
/**
|
|
27
27
|
* Read every saved view for `object` from the two places they live, in the same
|
|
@@ -45,7 +45,10 @@ async function fetchSavedViews(dataSource, object) {
|
|
|
45
45
|
if (!canReadObjectDef && !canReadOverlay)
|
|
46
46
|
return { capable: false, views: {} };
|
|
47
47
|
const embedded = canReadObjectDef
|
|
48
|
-
? dataSource.getObjectSchema(object).then(
|
|
48
|
+
? dataSource.getObjectSchema(object).then(
|
|
49
|
+
// `listViews` is canonical (#5362); the snake leg is a compatibility READ for
|
|
50
|
+
// stored pre-settlement documents (never censused: objectstack#7917).
|
|
51
|
+
(def) => (isRecord(def) ? (def.listViews ?? def.list_views) : undefined), () => undefined)
|
|
49
52
|
: Promise.resolve(undefined);
|
|
50
53
|
const overlay = canReadOverlay
|
|
51
54
|
? dataSource.listViews(object).then((rows) => rows, () => undefined)
|
|
@@ -68,6 +71,9 @@ async function fetchSavedViews(dataSource, object) {
|
|
|
68
71
|
* ```
|
|
69
72
|
*/
|
|
70
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`.
|
|
71
77
|
const context = useContext(SchemaRendererContext);
|
|
72
78
|
const adapter = (dataSource ?? context?.dataSource ?? null);
|
|
73
79
|
const config = useMemo(() => {
|
|
@@ -128,11 +134,25 @@ export function useElementDataSource(schema, dataSource) {
|
|
|
128
134
|
return { status: 'loading', config };
|
|
129
135
|
if (resolved.view === null)
|
|
130
136
|
return { status: 'missing', config, error: resolved.error };
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 };
|
|
137
157
|
}, [config, resolved, requestKey]);
|
|
138
158
|
}
|