@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
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
* The renderer half of the spec's API binding of a page element (objectui#12197):
|
|
9
|
+
* `PageComponentSchema.dataSource` as `ElementApiDataSourceSchema`,
|
|
10
|
+
*
|
|
11
|
+
* ```json
|
|
12
|
+
* { "provider": "api", "read": { "url": "/api/v1/cloud/home" }, "path": "data" }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* The spec states what a renderer does with it: the element reads ONE
|
|
16
|
+
* same-origin endpoint with the console's signed-in fetch and binds the selected
|
|
17
|
+
* value, "which the element reads as `data` in its expression holes and its
|
|
18
|
+
* `visibleWhen`". `SchemaRenderer` is the one place both of those are evaluated,
|
|
19
|
+
* so it calls {@link useElementApiDataSource} for every node and binds the
|
|
20
|
+
* answer as the `data` root (see "The API binding" there). This module owns the
|
|
21
|
+
* read itself.
|
|
22
|
+
*
|
|
23
|
+
* ## The binding is judged by the spec's own schema
|
|
24
|
+
*
|
|
25
|
+
* A node's `dataSource` reaches the renderer as stored metadata, unparsed. The
|
|
26
|
+
* hook parses it with `ElementApiDataSourceSchema` before it reads anything,
|
|
27
|
+
* so a binding the spec refuses — an absolute or protocol-relative `url`, a
|
|
28
|
+
* credential key, a `method` other than `GET`, a malformed `path` — is
|
|
29
|
+
* `refused` here with the spec's own message, and no request is sent. There is
|
|
30
|
+
* no second, hand-kept same-origin rule in this repo to drift from the
|
|
31
|
+
* contract's.
|
|
32
|
+
*
|
|
33
|
+
* A `dataSource` that is not an API binding (absent, or the object binding
|
|
34
|
+
* `useElementDataSource` reads) is `absent` here: the two shapes are told apart
|
|
35
|
+
* by `provider: 'api'`, the spec's discriminator.
|
|
36
|
+
*
|
|
37
|
+
* ## The read
|
|
38
|
+
*
|
|
39
|
+
* Through `SchemaRendererContext.apiFetch`, the host's authenticated fetch the
|
|
40
|
+
* console hands every renderer (`ConsoleShell`), falling back to the global
|
|
41
|
+
* fetch only when no host supplies one — the rule every `apiFetch` reader in
|
|
42
|
+
* the tree follows. `GET`, `Accept: application/json`, `credentials: 'include'`.
|
|
43
|
+
* A non-2xx answer, a body that is not JSON, or a `path` that selects nothing
|
|
44
|
+
* is `error`, with a sentence naming which; the value is never guessed.
|
|
45
|
+
*
|
|
46
|
+
* Concurrent elements that bind the same URL share ONE request: {@link
|
|
47
|
+
* readElementApi} keeps a pending request per URL and forgets it the moment it
|
|
48
|
+
* settles, so it caches no answer (the shape `readStorageUsage` uses).
|
|
49
|
+
*
|
|
50
|
+
* @module
|
|
51
|
+
*/
|
|
52
|
+
import { useContext, useEffect, useMemo, useState } from 'react';
|
|
53
|
+
import { ElementApiDataSourceSchema } from '@objectstack/spec/ui';
|
|
54
|
+
import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
|
|
55
|
+
import { isConfigBag } from '../utils/configBag.js';
|
|
56
|
+
/**
|
|
57
|
+
* Is this `dataSource` value the API binding — whatever it says beyond
|
|
58
|
+
* `provider`? `provider: 'api'` is the spec's discriminator between the two
|
|
59
|
+
* shapes of `PageComponentSchema.dataSource`; the object binding carries no
|
|
60
|
+
* `provider`.
|
|
61
|
+
*/
|
|
62
|
+
export function isElementApiDataSourceShape(value) {
|
|
63
|
+
return isConfigBag(value) && value.provider === 'api';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Judge a node's `dataSource` as an API binding: `absent` when it is not one,
|
|
67
|
+
* `refused` with the spec's message when the spec refuses it, otherwise the
|
|
68
|
+
* parsed binding.
|
|
69
|
+
*/
|
|
70
|
+
export function judgeElementApiDataSource(value) {
|
|
71
|
+
if (!isElementApiDataSourceShape(value))
|
|
72
|
+
return { status: 'absent' };
|
|
73
|
+
const parsed = ElementApiDataSourceSchema.safeParse(value);
|
|
74
|
+
if (!parsed.success) {
|
|
75
|
+
return {
|
|
76
|
+
status: 'refused',
|
|
77
|
+
error: parsed.error.issues.map((issue) => issue.message).join(' '),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { status: 'parsed', config: parsed.data };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Select `path` (dotted segments; a numeric segment indexes an array) from a
|
|
84
|
+
* response body. `found: false` when any segment has nothing under it — an
|
|
85
|
+
* absent key, not a key whose value is `undefined`, is what "selects nothing"
|
|
86
|
+
* means.
|
|
87
|
+
*/
|
|
88
|
+
export function selectElementApiPath(body, path) {
|
|
89
|
+
if (!path)
|
|
90
|
+
return { found: true, value: body };
|
|
91
|
+
let current = body;
|
|
92
|
+
for (const segment of path.split('.')) {
|
|
93
|
+
if (Array.isArray(current) && /^\d+$/.test(segment)) {
|
|
94
|
+
const index = Number(segment);
|
|
95
|
+
if (index >= current.length)
|
|
96
|
+
return { found: false, segment };
|
|
97
|
+
current = current[index];
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!isConfigBag(current) || !Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
101
|
+
return { found: false, segment };
|
|
102
|
+
}
|
|
103
|
+
current = current[segment];
|
|
104
|
+
}
|
|
105
|
+
return { found: true, value: current };
|
|
106
|
+
}
|
|
107
|
+
/** Pending reads, keyed by URL. An entry lives exactly as long as its request. */
|
|
108
|
+
const IN_FLIGHT = new Map();
|
|
109
|
+
/**
|
|
110
|
+
* `GET url` through `doFetch`, resolving with the parsed JSON body. Rejects on
|
|
111
|
+
* a network failure, a non-2xx answer or a body that is not JSON. Callers that
|
|
112
|
+
* ask while a read of the same URL is pending share it.
|
|
113
|
+
*/
|
|
114
|
+
export function readElementApi(url, doFetch) {
|
|
115
|
+
const pending = IN_FLIGHT.get(url);
|
|
116
|
+
if (pending)
|
|
117
|
+
return pending;
|
|
118
|
+
const request = doFetch(url, {
|
|
119
|
+
method: 'GET',
|
|
120
|
+
headers: { Accept: 'application/json' },
|
|
121
|
+
credentials: 'include',
|
|
122
|
+
})
|
|
123
|
+
.then(async (res) => {
|
|
124
|
+
if (!res.ok)
|
|
125
|
+
throw new Error(`GET ${url} answered ${res.status}.`);
|
|
126
|
+
try {
|
|
127
|
+
return await res.json();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new Error(`GET ${url} did not answer JSON.`);
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
.finally(() => {
|
|
134
|
+
IN_FLIGHT.delete(url);
|
|
135
|
+
});
|
|
136
|
+
IN_FLIGHT.set(url, request);
|
|
137
|
+
return request;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Resolve a node's API binding: read its endpoint and select its `path`.
|
|
141
|
+
*
|
|
142
|
+
* Called unconditionally by `SchemaRenderer` for every node; a node without an
|
|
143
|
+
* API binding costs one `isPlainObject` test and issues no request.
|
|
144
|
+
*/
|
|
145
|
+
export function useElementApiDataSource(schema) {
|
|
146
|
+
const context = useContext(SchemaRendererContext);
|
|
147
|
+
const doFetch = context?.apiFetch ?? fetch;
|
|
148
|
+
const raw = isConfigBag(schema) ? schema.dataSource : undefined;
|
|
149
|
+
// Keyed on the binding's CONTENT, never on its identity (AGENTS.md #10): a
|
|
150
|
+
// re-render that hands over an equal binding is the same read.
|
|
151
|
+
const bindingKey = isElementApiDataSourceShape(raw) ? JSON.stringify(raw) : '';
|
|
152
|
+
const judged = useMemo(() => judgeElementApiDataSource(bindingKey ? JSON.parse(bindingKey) : undefined), [bindingKey]);
|
|
153
|
+
const url = judged.status === 'parsed' ? judged.config.read.url : '';
|
|
154
|
+
const path = judged.status === 'parsed' ? judged.config.path : undefined;
|
|
155
|
+
const requestKey = url ? JSON.stringify([url, path ?? null]) : '';
|
|
156
|
+
// The settled answer carries its own key instead of being cleared when the
|
|
157
|
+
// binding changes, so the effect never sets state synchronously; an answer
|
|
158
|
+
// whose key no longer matches reads as `loading` (useElementDataSource's rule).
|
|
159
|
+
const [settled, setSettled] = useState(null);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (!url)
|
|
162
|
+
return;
|
|
163
|
+
let cancelled = false;
|
|
164
|
+
readElementApi(url, doFetch).then((body) => {
|
|
165
|
+
if (cancelled)
|
|
166
|
+
return;
|
|
167
|
+
const selected = selectElementApiPath(body, path);
|
|
168
|
+
setSettled(selected.found
|
|
169
|
+
? { key: requestKey, ok: true, data: selected.value }
|
|
170
|
+
: {
|
|
171
|
+
key: requestKey,
|
|
172
|
+
ok: false,
|
|
173
|
+
error: `dataSource.path "${path}" selects nothing in the answer of GET ${url}: `
|
|
174
|
+
+ `it has no "${selected.segment}".`,
|
|
175
|
+
});
|
|
176
|
+
}, (error) => {
|
|
177
|
+
if (cancelled)
|
|
178
|
+
return;
|
|
179
|
+
setSettled({
|
|
180
|
+
key: requestKey,
|
|
181
|
+
ok: false,
|
|
182
|
+
error: error instanceof Error ? error.message : `GET ${url} failed.`,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
return () => {
|
|
186
|
+
cancelled = true;
|
|
187
|
+
};
|
|
188
|
+
}, [url, path, requestKey, doFetch]);
|
|
189
|
+
if (judged.status === 'absent')
|
|
190
|
+
return { status: 'absent' };
|
|
191
|
+
if (judged.status === 'refused')
|
|
192
|
+
return { status: 'refused', error: judged.error };
|
|
193
|
+
if (settled?.key !== requestKey)
|
|
194
|
+
return { status: 'loading', config: judged.config };
|
|
195
|
+
if (!settled.ok)
|
|
196
|
+
return { status: 'error', config: judged.config, error: settled.error };
|
|
197
|
+
return { status: 'ready', config: judged.config, data: settled.data };
|
|
198
|
+
}
|
|
@@ -40,7 +40,17 @@
|
|
|
40
40
|
* AUTHORED text. That is `useObjectLabel`'s own fallback contract; nothing
|
|
41
41
|
* here overrides it.
|
|
42
42
|
* - **A nameless action is not translatable.** Without `action.name` there is
|
|
43
|
-
* no `_actions.<name>` key to look up, so the literals pass through
|
|
43
|
+
* no `_actions.<name>` key to look up, so the literals pass through —
|
|
44
|
+
* collapsed to the active language like every other caller's (below), since
|
|
45
|
+
* an inline action (`element:button`) is often nameless (objectui#12757).
|
|
46
|
+
* - **The entry is keyed on the action's own object** (objectui#11439): its
|
|
47
|
+
* declared `objectName`, else the host object the caller draws it on — the
|
|
48
|
+
* object `translateObject` in `@objectstack/spec` stamps on an embedded
|
|
49
|
+
* action. Only an action with neither reads `globalActions.<name>`; a bound
|
|
50
|
+
* action never does (see `actionSuffixes` in `@object-ui/i18n`). The
|
|
51
|
+
* runtime-side resolvers (`useConsoleActionRuntime`, `RecordDetailView`)
|
|
52
|
+
* already key the param / description / result-dialog copy the same way, so
|
|
53
|
+
* one action reads one bundle node on every surface.
|
|
44
54
|
* - **`confirmText` / `successMessage` are resolved only when the action
|
|
45
55
|
* DECLARES them.** A bundle must not be able to bolt a confirmation gate
|
|
46
56
|
* onto an action whose metadata never asked for one (nor a success toast).
|
|
@@ -52,13 +62,14 @@
|
|
|
52
62
|
* declares resolves `_actions.<name>.outcomeMessages.<outcome>`; a bundle
|
|
53
63
|
* entry for an outcome the action does not declare is never read, so a
|
|
54
64
|
* bundle cannot add copy for an answer the author never wrote one for.
|
|
55
|
-
* - **`label`, `successMessage` and each
|
|
56
|
-
*
|
|
57
|
-
* or an inline per-locale map. The map is collapsed to the active language
|
|
65
|
+
* - **`label`, `confirmText`, `successMessage`, `errorMessage` and each
|
|
66
|
+
* `outcomeMessages` entry are reduced through `pickLocalized` first**,
|
|
67
|
+
* because each is an `I18nLabel` — a string or an inline per-locale map. The map is collapsed to the active language
|
|
58
68
|
* BEFORE it is offered to the bundle as the fallback, so a per-locale
|
|
59
69
|
* literal and a bundle entry cannot disagree about what "the authored text"
|
|
60
70
|
* is, and the action runner, which has no language, only ever receives a
|
|
61
|
-
* string.
|
|
71
|
+
* string. `errorMessage` has no bundle key (the spec's action translation
|
|
72
|
+
* declares none), so the collapse is all it gets (objectui#12757).
|
|
62
73
|
* - **The success copy stays a template.** A `${result.*}` token, in a bundle
|
|
63
74
|
* entry or in the authored text, passes through this hook untouched: the
|
|
64
75
|
* action has not run yet, so there is no result to read. The runner fills
|
|
@@ -74,9 +85,11 @@ export interface ActionTextLocalizerOptions {
|
|
|
74
85
|
}
|
|
75
86
|
/**
|
|
76
87
|
* Returns a copy of `action` with `label` — plus `confirmText`,
|
|
77
|
-
* `successMessage` and `outcomeMessages` when the action
|
|
88
|
+
* `successMessage`, `errorMessage` and `outcomeMessages` when the action
|
|
89
|
+
* declares them —
|
|
78
90
|
* resolved from the active locale's bundle, falling back to the authored
|
|
79
|
-
* literals.
|
|
91
|
+
* literals. `objectName` is the host object the action is drawn on; the
|
|
92
|
+
* action's own declared `objectName` wins over it when present.
|
|
80
93
|
*/
|
|
81
94
|
export type ActionTextLocalizer = <T extends Record<string, any>>(objectName: string | undefined, action: T, options?: ActionTextLocalizerOptions) => T;
|
|
82
95
|
export declare function useActionTextLocalizer(): ActionTextLocalizer;
|
|
@@ -40,7 +40,17 @@
|
|
|
40
40
|
* AUTHORED text. That is `useObjectLabel`'s own fallback contract; nothing
|
|
41
41
|
* here overrides it.
|
|
42
42
|
* - **A nameless action is not translatable.** Without `action.name` there is
|
|
43
|
-
* no `_actions.<name>` key to look up, so the literals pass through
|
|
43
|
+
* no `_actions.<name>` key to look up, so the literals pass through —
|
|
44
|
+
* collapsed to the active language like every other caller's (below), since
|
|
45
|
+
* an inline action (`element:button`) is often nameless (objectui#12757).
|
|
46
|
+
* - **The entry is keyed on the action's own object** (objectui#11439): its
|
|
47
|
+
* declared `objectName`, else the host object the caller draws it on — the
|
|
48
|
+
* object `translateObject` in `@objectstack/spec` stamps on an embedded
|
|
49
|
+
* action. Only an action with neither reads `globalActions.<name>`; a bound
|
|
50
|
+
* action never does (see `actionSuffixes` in `@object-ui/i18n`). The
|
|
51
|
+
* runtime-side resolvers (`useConsoleActionRuntime`, `RecordDetailView`)
|
|
52
|
+
* already key the param / description / result-dialog copy the same way, so
|
|
53
|
+
* one action reads one bundle node on every surface.
|
|
44
54
|
* - **`confirmText` / `successMessage` are resolved only when the action
|
|
45
55
|
* DECLARES them.** A bundle must not be able to bolt a confirmation gate
|
|
46
56
|
* onto an action whose metadata never asked for one (nor a success toast).
|
|
@@ -52,13 +62,14 @@
|
|
|
52
62
|
* declares resolves `_actions.<name>.outcomeMessages.<outcome>`; a bundle
|
|
53
63
|
* entry for an outcome the action does not declare is never read, so a
|
|
54
64
|
* bundle cannot add copy for an answer the author never wrote one for.
|
|
55
|
-
* - **`label`, `successMessage` and each
|
|
56
|
-
*
|
|
57
|
-
* or an inline per-locale map. The map is collapsed to the active language
|
|
65
|
+
* - **`label`, `confirmText`, `successMessage`, `errorMessage` and each
|
|
66
|
+
* `outcomeMessages` entry are reduced through `pickLocalized` first**,
|
|
67
|
+
* because each is an `I18nLabel` — a string or an inline per-locale map. The map is collapsed to the active language
|
|
58
68
|
* BEFORE it is offered to the bundle as the fallback, so a per-locale
|
|
59
69
|
* literal and a bundle entry cannot disagree about what "the authored text"
|
|
60
70
|
* is, and the action runner, which has no language, only ever receives a
|
|
61
|
-
* string.
|
|
71
|
+
* string. `errorMessage` has no bundle key (the spec's action translation
|
|
72
|
+
* declares none), so the collapse is all it gets (objectui#12757).
|
|
62
73
|
* - **The success copy stays a template.** A `${result.*}` token, in a bundle
|
|
63
74
|
* entry or in the authored text, passes through this hook untouched: the
|
|
64
75
|
* action has not run yet, so there is no result to read. The runner fills
|
|
@@ -68,6 +79,8 @@
|
|
|
68
79
|
import { useMemo } from 'react';
|
|
69
80
|
import { useObjectLabel, useObjectTranslation, pickLocalized } from '@object-ui/i18n';
|
|
70
81
|
import { isConfigBag } from '../utils/configBag.js';
|
|
82
|
+
/** The declared `I18nLabel` copy, besides `label`, that reaches the runner. */
|
|
83
|
+
const DECLARED_COPY_KEYS = ['confirmText', 'successMessage', 'errorMessage'];
|
|
71
84
|
export function useActionTextLocalizer() {
|
|
72
85
|
const { actionLabel, actionConfirm, actionSuccess, actionOutcome } = useObjectLabel();
|
|
73
86
|
const { language } = useObjectTranslation();
|
|
@@ -82,21 +95,39 @@ export function useActionTextLocalizer() {
|
|
|
82
95
|
? action.name
|
|
83
96
|
: undefined;
|
|
84
97
|
const displayLabel = authoredLabel || name || options?.fallbackLabel || '';
|
|
85
|
-
//
|
|
98
|
+
// Every declared `I18nLabel` copy collapses the same way, so neither the
|
|
99
|
+
// bundle fallbacks below nor the nameless return hand the runner a map.
|
|
100
|
+
const collapsed = { ...action, label: displayLabel };
|
|
101
|
+
let changed = displayLabel !== action.label;
|
|
102
|
+
for (const key of DECLARED_COPY_KEYS) {
|
|
103
|
+
const declaredCopy = action[key];
|
|
104
|
+
if (declaredCopy === undefined)
|
|
105
|
+
continue;
|
|
106
|
+
collapsed[key] = pickLocalized(declaredCopy, language);
|
|
107
|
+
if (collapsed[key] !== declaredCopy)
|
|
108
|
+
changed = true;
|
|
109
|
+
}
|
|
110
|
+
// No name, no translation key: the literals are the answer. They are
|
|
86
111
|
// still normalized (map to string, last-resort placeholder applied) so
|
|
87
112
|
// every caller reads one shape.
|
|
88
113
|
if (!name) {
|
|
89
|
-
return
|
|
114
|
+
return (changed ? collapsed : action);
|
|
90
115
|
}
|
|
116
|
+
// The key object: the action's declared `objectName`, else the host the
|
|
117
|
+
// caller passes (objectui#11439). An action declared on `sales_order`
|
|
118
|
+
// drawn where the caller knows no object still reads `sales_order`'s
|
|
119
|
+
// `_actions` node, never `globalActions`.
|
|
120
|
+
const declared = action.objectName;
|
|
121
|
+
const keyObject = (typeof declared === 'string' && declared) || objectName;
|
|
91
122
|
const out = {
|
|
92
|
-
...
|
|
93
|
-
label: actionLabel(
|
|
123
|
+
...collapsed,
|
|
124
|
+
label: actionLabel(keyObject, name, displayLabel),
|
|
94
125
|
};
|
|
95
126
|
if (action.confirmText !== undefined) {
|
|
96
|
-
out.confirmText = actionConfirm(
|
|
127
|
+
out.confirmText = actionConfirm(keyObject, name, collapsed.confirmText);
|
|
97
128
|
}
|
|
98
129
|
if (action.successMessage !== undefined) {
|
|
99
|
-
out.successMessage = actionSuccess(
|
|
130
|
+
out.successMessage = actionSuccess(keyObject, name, collapsed.successMessage);
|
|
100
131
|
}
|
|
101
132
|
// The map is an authored bag (outcome → copy), so the one "is this a
|
|
102
133
|
// real bag?" predicate in this package answers whether to read it.
|
|
@@ -104,7 +135,7 @@ export function useActionTextLocalizer() {
|
|
|
104
135
|
if (isConfigBag(authoredOutcomes)) {
|
|
105
136
|
const outcomeMessages = {};
|
|
106
137
|
for (const [outcome, copy] of Object.entries(authoredOutcomes)) {
|
|
107
|
-
const text = actionOutcome(
|
|
138
|
+
const text = actionOutcome(keyObject, name, outcome, pickLocalized(copy, language));
|
|
108
139
|
if (text !== undefined)
|
|
109
140
|
outcomeMessages[outcome] = text;
|
|
110
141
|
}
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* The `properties` evaluation of the `SchemaRenderer` memo, for a node that is
|
|
10
10
|
* rendered WITHOUT `SchemaRenderer` (objectui#10290).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `properties
|
|
12
|
+
* `action:bar` (in `@object-ui/components`) mounts its member actions itself,
|
|
13
|
+
* so a member's `properties` never passes through the memo. Its static
|
|
14
|
+
* execution values ride `properties.params` (objectui#10289; an `action:group`
|
|
15
|
+
* / `action:menu` member carries no `properties` bag and its container reads
|
|
16
|
+
* none, objectui#11638), and those values are templates
|
|
16
17
|
* evaluated where `properties` are (objectui#7867). This hook is that
|
|
17
18
|
* evaluation: the memo's per-key rule, its `ExpressionEvaluator`, and its
|
|
18
19
|
* scope (the host's ambient roots, `current_user`, the page's bound row as
|
|
@@ -13,10 +13,11 @@ import { usePageVariables } from './usePageVariables.js';
|
|
|
13
13
|
* The `properties` evaluation of the `SchemaRenderer` memo, for a node that is
|
|
14
14
|
* rendered WITHOUT `SchemaRenderer` (objectui#10290).
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* `properties
|
|
16
|
+
* `action:bar` (in `@object-ui/components`) mounts its member actions itself,
|
|
17
|
+
* so a member's `properties` never passes through the memo. Its static
|
|
18
|
+
* execution values ride `properties.params` (objectui#10289; an `action:group`
|
|
19
|
+
* / `action:menu` member carries no `properties` bag and its container reads
|
|
20
|
+
* none, objectui#11638), and those values are templates
|
|
20
21
|
* evaluated where `properties` are (objectui#7867). This hook is that
|
|
21
22
|
* evaluation: the memo's per-key rule, its `ExpressionEvaluator`, and its
|
|
22
23
|
* scope (the host's ambient roots, `current_user`, the page's bound row as
|
|
@@ -77,7 +77,12 @@ import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
|
|
|
77
77
|
export function useDatasetDimensionMeta(object, dimensionFields, dimensions, options) {
|
|
78
78
|
// The host's AUTHENTICATED fetch (objectui#4121) — see property 1 in the
|
|
79
79
|
// file header for why it is read off the context directly.
|
|
80
|
-
const
|
|
80
|
+
const context = useContext(SchemaRendererContext);
|
|
81
|
+
const apiFetch = context?.apiFetch;
|
|
82
|
+
// The host data source, when it can answer `getObjectSchema` (objectui#13039):
|
|
83
|
+
// the data layer's shared request-key cache, so N dataset widgets over one
|
|
84
|
+
// object make ONE `meta/object/NAME` request instead of one each.
|
|
85
|
+
const dataSource = context?.dataSource;
|
|
81
86
|
const enabled = options?.enabled ?? true;
|
|
82
87
|
// LOCALE-FREE state (objectui#4030) — see property 2 in the file header.
|
|
83
88
|
const [meta, setMeta] = useState(null);
|
|
@@ -103,14 +108,22 @@ export function useDatasetDimensionMeta(object, dimensionFields, dimensions, opt
|
|
|
103
108
|
(async () => {
|
|
104
109
|
try {
|
|
105
110
|
const doFetch = apiFetch ?? fetch;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
// objectui#13039 — through the data source's `getObjectSchema` when one
|
|
112
|
+
// that answers it is in scope: the read the dashboard's chart, metric
|
|
113
|
+
// and table widgets make, which the adapter's metadata cache joins while
|
|
114
|
+
// it is in flight. It answers the bare object document, which is what
|
|
115
|
+
// the raw leg's unwrap yields, so core gets one shape from either leg.
|
|
116
|
+
// The raw read stays as the fallback for a surface with no such source.
|
|
117
|
+
const loadObjectSchema = typeof dataSource?.getObjectSchema === 'function'
|
|
118
|
+
? (name) => dataSource.getObjectSchema(name)
|
|
119
|
+
: async (name) => {
|
|
120
|
+
const r = await doFetch(`/api/v1/meta/object/${encodeURIComponent(name)}`, {
|
|
121
|
+
headers: { accept: 'application/json' },
|
|
122
|
+
credentials: 'include',
|
|
123
|
+
});
|
|
124
|
+
const doc = await r.json().catch(() => null);
|
|
125
|
+
return doc?.item ?? doc?.data ?? doc;
|
|
126
|
+
};
|
|
114
127
|
// ONE walk for every dimension, memoized per call inside core — sibling
|
|
115
128
|
// dimensions sharing a relationship prefix read that object once, and
|
|
116
129
|
// the base object is seeded so it is never re-read.
|
|
@@ -133,8 +146,10 @@ export function useDatasetDimensionMeta(object, dimensionFields, dimensions, opt
|
|
|
133
146
|
// (objectui#4030 / PR #4324). `apiFetch` comes from the provider's
|
|
134
147
|
// memoized context value, and this hook's own `setState` cannot re-render
|
|
135
148
|
// the provider, so its identity is stable across the effect's own updates.
|
|
149
|
+
// `dataSource` joins for the same reason and from the same value
|
|
150
|
+
// (objectui#13039): it decides which leg the read takes.
|
|
136
151
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
|
-
}, [signature, apiFetch]);
|
|
152
|
+
}, [signature, apiFetch, dataSource]);
|
|
138
153
|
return meta;
|
|
139
154
|
}
|
|
140
155
|
/**
|
|
@@ -53,9 +53,11 @@ export interface DensityResult {
|
|
|
53
53
|
* import { DENSITY_MODE_TO_ROW_HEIGHT, rowHeightToDensityMode } from '@object-ui/core';
|
|
54
54
|
*
|
|
55
55
|
* // A view stores the protocol's `rowHeight`; the legacy `densityMode` key is
|
|
56
|
-
* // retired on `NamedListView` (objectui#7924), so persist the spec value
|
|
56
|
+
* // retired on `NamedListView` (objectui#7924), so persist the spec value —
|
|
57
|
+
* // here into the host's own per-user store (`saveUserRowHeight` is the
|
|
58
|
+
* // host's; the console keeps the choice on the user's own list state).
|
|
57
59
|
* const density = useDensityMode(rowHeightToDensityMode(activeView?.rowHeight) ?? 'comfortable', {
|
|
58
|
-
* onChange: (m) =>
|
|
60
|
+
* onChange: (m) => saveUserRowHeight(vid, DENSITY_MODE_TO_ROW_HEIGHT[m]),
|
|
59
61
|
* });
|
|
60
62
|
* ```
|
|
61
63
|
*/
|
|
@@ -38,9 +38,11 @@ const DENSITY_ORDER = ['compact', 'comfortable', 'spacious'];
|
|
|
38
38
|
* import { DENSITY_MODE_TO_ROW_HEIGHT, rowHeightToDensityMode } from '@object-ui/core';
|
|
39
39
|
*
|
|
40
40
|
* // A view stores the protocol's `rowHeight`; the legacy `densityMode` key is
|
|
41
|
-
* // retired on `NamedListView` (objectui#7924), so persist the spec value
|
|
41
|
+
* // retired on `NamedListView` (objectui#7924), so persist the spec value —
|
|
42
|
+
* // here into the host's own per-user store (`saveUserRowHeight` is the
|
|
43
|
+
* // host's; the console keeps the choice on the user's own list state).
|
|
42
44
|
* const density = useDensityMode(rowHeightToDensityMode(activeView?.rowHeight) ?? 'comfortable', {
|
|
43
|
-
* onChange: (m) =>
|
|
45
|
+
* onChange: (m) => saveUserRowHeight(vid, DENSITY_MODE_TO_ROW_HEIGHT[m]),
|
|
44
46
|
* });
|
|
45
47
|
* ```
|
|
46
48
|
*/
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*
|
|
8
8
|
* `usePageAssignment` — resolve the record PageSchema that should be rendered
|
|
9
|
-
* for a given object. Walks the metadata cache exposed by `<MetadataProvider
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* for a given object. Walks the metadata cache exposed by `<MetadataProvider>`,
|
|
10
|
+
* keeps the PageSchemas whose `type === 'record'` and `object` matches the
|
|
11
|
+
* requested name, and returns the object's DEFAULT record page among them
|
|
12
|
+
* (`isDefault: true`), else the first of them.
|
|
12
13
|
*
|
|
13
14
|
* Returns a discriminated result by `PageSchema.kind`:
|
|
14
15
|
* - `kind === 'full'` (default): the schema fully describes the page;
|
|
@@ -18,13 +19,17 @@
|
|
|
18
19
|
* `buildDefaultPageSchema(objectDef, { slots })` so omitted slots
|
|
19
20
|
* fall through to synthesized defaults.
|
|
20
21
|
*
|
|
21
|
-
* Selection is by
|
|
22
|
-
*
|
|
23
|
-
* record
|
|
22
|
+
* Selection is by the declared `PageSchema.isDefault`, then by DECLARATION
|
|
23
|
+
* ORDER among pages that are not the default: the spec allows one default
|
|
24
|
+
* record page per object, so the flag needs no tie-break here, and with no
|
|
25
|
+
* default the first matching page wins. No match returns null, so callers can
|
|
26
|
+
* deterministically fall back to the auto-generated DetailView when no record
|
|
27
|
+
* Page is authored.
|
|
24
28
|
*
|
|
25
29
|
* Future work (deferred): recordType / profile / app / formFactor filtering.
|
|
26
|
-
* Any author-driven ordering would have to be DECLARED on
|
|
27
|
-
* `@objectstack/spec` first — see the note on the selection
|
|
30
|
+
* Any further author-driven ordering would have to be DECLARED on
|
|
31
|
+
* `PageSchema` in `@objectstack/spec` first — see the note on the selection
|
|
32
|
+
* itself below.
|
|
28
33
|
*/
|
|
29
34
|
export interface PageAssignmentOptions {
|
|
30
35
|
/** Salesforce-style record type filter (reserved for future use). */
|
|
@@ -62,6 +67,24 @@ export interface PageAssignmentResult {
|
|
|
62
67
|
/** Loader error, if any. */
|
|
63
68
|
error: Error | null;
|
|
64
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* True when `page` is a record page bound to `objectName`: its `type` is
|
|
72
|
+
* `'record'` and its `object` names that object.
|
|
73
|
+
*
|
|
74
|
+
* This is the ONE reading of "a record page of object O" in the console. The
|
|
75
|
+
* object-based lookup below filters its candidates with it, and a route that
|
|
76
|
+
* names a page explicitly (the console's named record page route,
|
|
77
|
+
* objectui#12715) asks it before rendering the named page for a record of O,
|
|
78
|
+
* so a name that is a page of another object, or not a record page at all, is
|
|
79
|
+
* refused rather than rendered. The `pageName` override of
|
|
80
|
+
* {@link usePageAssignment} does not ask it: that option returns the page with
|
|
81
|
+
* that name whatever it is, and a caller that renders a record on the page it
|
|
82
|
+
* gets back asks this first.
|
|
83
|
+
*
|
|
84
|
+
* `type` is the one discriminator, as the lookup's own comment explains
|
|
85
|
+
* (objectui#9674): `pageType` is a key `PageSchema` refuses, so it is not read.
|
|
86
|
+
*/
|
|
87
|
+
export declare function isRecordPageOf(page: unknown, objectName: string | undefined | null): boolean;
|
|
65
88
|
/**
|
|
66
89
|
* Resolve the record PageSchema for the given object name. Returns `null`
|
|
67
90
|
* when no record Page is configured, signalling the caller to fall back
|