@object-ui/react 17.3.0 → 17.6.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 +1242 -0
- package/README.md +48 -21
- package/dist/LazyPluginLoader.d.ts +14 -7
- package/dist/LazyPluginLoader.js +14 -7
- package/dist/SchemaRenderer.d.ts +78 -5
- package/dist/SchemaRenderer.js +572 -34
- package/dist/context/AppShellContext.d.ts +21 -2
- package/dist/context/HostNavigationContext.d.ts +82 -0
- package/dist/context/HostNavigationContext.js +65 -0
- package/dist/context/RecordContext.d.ts +0 -4
- package/dist/context/RecordContext.js +0 -2
- package/dist/context/RelatedRecordActionsContext.d.ts +44 -0
- package/dist/context/RelatedRecordActionsContext.js +6 -0
- package/dist/context/index.d.ts +13 -12
- package/dist/context/index.js +13 -12
- package/dist/element-data-source/ElementDataSourceGate.d.ts +272 -0
- package/dist/element-data-source/ElementDataSourceGate.js +274 -0
- package/dist/hooks/index.d.ts +35 -32
- package/dist/hooks/index.js +42 -32
- package/dist/hooks/useActionEngine.js +1 -1
- package/dist/hooks/useActionTextLocalizer.d.ts +69 -0
- package/dist/hooks/useActionTextLocalizer.js +91 -0
- package/dist/hooks/useCapabilityGate.js +6 -5
- package/dist/hooks/useClientNotifications.js +2 -2
- package/dist/hooks/useCrudShortcuts.js +1 -1
- package/dist/hooks/useDatasetDimensionLabels.d.ts +104 -0
- package/dist/hooks/useDatasetDimensionLabels.js +154 -0
- package/dist/hooks/useDiscovery.js +1 -1
- package/dist/hooks/useElementDataSource.d.ts +68 -0
- package/dist/hooks/useElementDataSource.js +141 -0
- package/dist/hooks/useExpression.d.ts +82 -3
- package/dist/hooks/useExpression.js +121 -4
- package/dist/hooks/useNavigationOverlay.d.ts +59 -11
- package/dist/hooks/useOffline.d.ts +62 -36
- package/dist/hooks/useOffline.js +2 -2
- package/dist/hooks/usePageAssignment.js +5 -5
- package/dist/hooks/usePageTransition.js +1 -1
- package/dist/hooks/usePageVariableActionBridge.js +2 -2
- package/dist/hooks/useViewData.js +1 -1
- package/dist/hooks/useViewTransition.js +1 -1
- package/dist/index.d.ts +11 -9
- package/dist/index.js +12 -8
- package/dist/schema-input.d.ts +42 -0
- package/dist/schema-input.js +42 -0
- package/dist/spec-bridge/SpecBridge.d.ts +12 -6
- package/dist/spec-bridge/SpecBridge.js +9 -3
- package/dist/spec-bridge/bridges/form-view.d.ts +99 -23
- package/dist/spec-bridge/bridges/form-view.js +51 -7
- package/dist/spec-bridge/bridges/list-view.d.ts +1 -1
- package/dist/spec-bridge/bridges/list-view.js +143 -24
- package/dist/spec-bridge/index.d.ts +4 -4
- package/dist/spec-bridge/index.js +3 -3
- package/dist/spec-bridge/types.d.ts +24 -3
- package/dist/utils/error-message.d.ts +59 -0
- package/dist/utils/error-message.js +116 -1
- package/dist/utils/i18n.d.ts +43 -8
- package/dist/utils/i18n.js +42 -4
- package/dist/utils/unevaluatedExpression.d.ts +69 -0
- package/dist/utils/unevaluatedExpression.js +194 -0
- package/dist/utils/visibilityDiagnostic.d.ts +119 -0
- package/dist/utils/visibilityDiagnostic.js +284 -0
- package/package.json +8 -8
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ObjectUI
|
|
4
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* Mapping half of consuming `PageComponentSchema.dataSource` — the spec's
|
|
10
|
+
* `ElementDataSourceSchema` per-element data binding (objectstack#5576 landed
|
|
11
|
+
* the resolution; objectstack#6953 wires it to the remaining blocks).
|
|
12
|
+
*
|
|
13
|
+
* `@object-ui/core` owns the pure half (telling the binding apart from a runtime
|
|
14
|
+
* adapter, matching a view name, composing the view with the binding's own
|
|
15
|
+
* keys). {@link useElementDataSource} owns the fetch half. This module owns the
|
|
16
|
+
* LAST hop: writing the composed result onto the schema keys a given block
|
|
17
|
+
* actually reads, and rendering the two non-final states.
|
|
18
|
+
*
|
|
19
|
+
* It exists because that hop was the part every block would otherwise copy.
|
|
20
|
+
* `dataSource` is declared on EVERY page component, so eight blocks need the
|
|
21
|
+
* same precedence table (below) with only the KEY NAMES differing — which is a
|
|
22
|
+
* mapping description, not eight algorithms. A per-block copy is how the two
|
|
23
|
+
* halves drift: one block ANDs the filters and the next replaces them, and the
|
|
24
|
+
* spec's "additional filter criteria" quietly becomes two dialects.
|
|
25
|
+
*
|
|
26
|
+
* `plugin-list`'s `ListViewBlock` carried the ORIGINAL copy of the table
|
|
27
|
+
* (objectstack#5576, which predates this module) and was collapsed onto this one
|
|
28
|
+
* by objectui#4038 — on the acceptance criterion that objectstack#5576's whole
|
|
29
|
+
* suite passes against this implementation untouched, which it does. Every
|
|
30
|
+
* object-bound block now reads the rules below from here, so this is the one
|
|
31
|
+
* place a change to them belongs.
|
|
32
|
+
*
|
|
33
|
+
* ## Precedence — one table, applied everywhere
|
|
34
|
+
*
|
|
35
|
+
* Three kinds of value arrive and they do not have the same standing:
|
|
36
|
+
*
|
|
37
|
+
* - **`dataSource.*` keys are authoritative.** The author wrote them on THIS
|
|
38
|
+
* placement and the spec says the binding "overrides page-level object
|
|
39
|
+
* context", so they beat the component's own same-named key.
|
|
40
|
+
* - **View-sourced values are a baseline.** A `view` is a *reference*; a key
|
|
41
|
+
* written on the component itself is more specific than the view it points
|
|
42
|
+
* at, so the component's key wins over one the view supplied.
|
|
43
|
+
* - **`filter` never overrides — it combines.** The spec describes the
|
|
44
|
+
* binding's filter as "*Additional* filter criteria", so component filter,
|
|
45
|
+
* view filter and binding filter all AND together through
|
|
46
|
+
* {@link mergeFilterNodes}. A binding can only narrow what the view already
|
|
47
|
+
* restricts, never widen it: a mistyped per-element filter cannot expose rows
|
|
48
|
+
* the saved view excluded.
|
|
49
|
+
*
|
|
50
|
+
* An authored-but-EMPTY `columns` counts as "not authored": `[]` is what the
|
|
51
|
+
* designer emits for an unconfigured column list, and supplying the columns is
|
|
52
|
+
* exactly why a view was named.
|
|
53
|
+
*
|
|
54
|
+
* ## What a mapping may NOT do
|
|
55
|
+
*
|
|
56
|
+
* {@link ElementDataSourceMapping} names only keys the target block genuinely
|
|
57
|
+
* reads. Writing a composed key onto a schema key the block ignores would
|
|
58
|
+
* reproduce the very defect this wiring removes — a value accepted and dropped —
|
|
59
|
+
* one layer further in, where it is even harder to see. A block with no row cap
|
|
60
|
+
* therefore leaves `limit` unmapped rather than parking it somewhere plausible,
|
|
61
|
+
* and the gap is recorded at the call site instead of being papered over here.
|
|
62
|
+
*
|
|
63
|
+
* ## A block with no ADAPTER fails loudly too (objectui#5378)
|
|
64
|
+
*
|
|
65
|
+
* The section below is about an unresolvable view NAME. The coarser failure —
|
|
66
|
+
* no runtime adapter reached the block at all — used to render an empty shell
|
|
67
|
+
* in exactly the same silence, and that is what `requiresDataSource` +
|
|
68
|
+
* {@link NoDataSourcePanel} now report. Measured on the published
|
|
69
|
+
* getting-started guide: all five of its blocks resolved no adapter, fetched
|
|
70
|
+
* nothing, and said nothing. The two panels are deliberately the same shape;
|
|
71
|
+
* they answer "which view?" and "which data source?", and an author who sees
|
|
72
|
+
* either one is being told where to look.
|
|
73
|
+
*
|
|
74
|
+
* ## An unresolvable `view` fails loudly
|
|
75
|
+
*
|
|
76
|
+
* When the named view does not exist the gate renders a configuration error
|
|
77
|
+
* instead of letting the block fall back to the object's default scope. Silently
|
|
78
|
+
* widening a named view to "all records" is the failure class the binding exists
|
|
79
|
+
* to remove, and it is the one an AI-authored page hides best: the page looks
|
|
80
|
+
* like it works.
|
|
81
|
+
*/
|
|
82
|
+
import * as React from 'react';
|
|
83
|
+
import { isElementDataSourceConfig, mergeFilterNodes, } from '@object-ui/core';
|
|
84
|
+
import { useElementDataSource, } from '../hooks/useElementDataSource.js';
|
|
85
|
+
import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
|
|
86
|
+
const readLimit = (base, key) => {
|
|
87
|
+
if (key === 'pagination.pageSize')
|
|
88
|
+
return base.pagination?.pageSize;
|
|
89
|
+
return base[key];
|
|
90
|
+
};
|
|
91
|
+
const writeLimit = (next, base, key, limit) => {
|
|
92
|
+
if (key === 'pagination.pageSize') {
|
|
93
|
+
next.pagination = { ...(base.pagination ?? {}), pageSize: limit };
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
next[key] = limit;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Resolve a block's `dataSource` binding and apply it to the block's own schema
|
|
100
|
+
* keys, per {@link ElementDataSourceMapping}.
|
|
101
|
+
*
|
|
102
|
+
* @param schema The block's schema node (its `dataSource` is read).
|
|
103
|
+
* @param mapping Which schema keys this block reads.
|
|
104
|
+
* @param dataSource Explicit adapter; falls back to `SchemaRendererContext`.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```tsx
|
|
108
|
+
* const bound = useElementDataSourceSchema(schema, { filter: true, sort: true });
|
|
109
|
+
* if (bound.status === 'missing') return <ElementDataSourceErrorPanel message={bound.error} />;
|
|
110
|
+
* return <ObjectCalendar schema={bound.schema} />;
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function useElementDataSourceSchema(schema, mapping = {}, dataSource) {
|
|
114
|
+
// Defence in depth for the collision the binding and the adapter share by
|
|
115
|
+
// NAME: even though `SchemaRenderer` no longer spreads `schema.dataSource` as
|
|
116
|
+
// a prop, a host (or an older cached bundle) handing us the spec BINDING under
|
|
117
|
+
// this argument must never be mistaken for an adapter — that is how a
|
|
118
|
+
// spec-compliant page reported "this data source cannot list the saved views".
|
|
119
|
+
// Same guard `ListViewBlock` carries, applied for every block at once.
|
|
120
|
+
const adapter = isElementDataSourceConfig(dataSource) ? undefined : dataSource;
|
|
121
|
+
const binding = useElementDataSource(schema, adapter);
|
|
122
|
+
const { object: objectKey = 'objectName', columns, filter, sort, limit, viewType } = mapping;
|
|
123
|
+
const mapped = React.useMemo(() => {
|
|
124
|
+
const composed = binding.composed;
|
|
125
|
+
if (!composed)
|
|
126
|
+
return schema;
|
|
127
|
+
const base = (schema ?? {});
|
|
128
|
+
const next = { ...base };
|
|
129
|
+
if (objectKey !== false)
|
|
130
|
+
next[objectKey] = composed.object;
|
|
131
|
+
if (columns) {
|
|
132
|
+
// `[]` is the designer's "not configured yet", and supplying the columns
|
|
133
|
+
// is the reason a view was named — so an empty authored list yields.
|
|
134
|
+
const authored = Array.isArray(base.columns) && base.columns.length > 0;
|
|
135
|
+
if (!authored && composed.columns !== undefined)
|
|
136
|
+
next.columns = composed.columns;
|
|
137
|
+
}
|
|
138
|
+
if (filter) {
|
|
139
|
+
// Component filter AND (view filter AND binding filter). `composed.filter`
|
|
140
|
+
// already carries the latter pair; a single surviving source comes back
|
|
141
|
+
// unwrapped, so the common "only the view filters" case stays flat.
|
|
142
|
+
const merged = mergeFilterNodes(base.filter, composed.filter);
|
|
143
|
+
if (merged !== undefined)
|
|
144
|
+
next.filter = merged;
|
|
145
|
+
else
|
|
146
|
+
delete next.filter;
|
|
147
|
+
}
|
|
148
|
+
// `composed.sort`/`composed.limit` are the BINDING's when it declared one,
|
|
149
|
+
// else the view's — so the component's own key may only win over the latter.
|
|
150
|
+
if (sort && composed.sort !== undefined) {
|
|
151
|
+
const fromView = binding.config?.sort === undefined;
|
|
152
|
+
if (!fromView || base.sort === undefined)
|
|
153
|
+
next.sort = composed.sort;
|
|
154
|
+
}
|
|
155
|
+
if (limit && composed.limit !== undefined) {
|
|
156
|
+
const fromView = binding.config?.limit === undefined;
|
|
157
|
+
if (!fromView || readLimit(base, limit) === undefined) {
|
|
158
|
+
writeLimit(next, base, limit, composed.limit);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (viewType && composed.viewType !== undefined && base.viewType === undefined) {
|
|
162
|
+
next.viewType = composed.viewType;
|
|
163
|
+
}
|
|
164
|
+
return next;
|
|
165
|
+
}, [schema, binding.composed, binding.config, objectKey, columns, filter, sort, limit, viewType]);
|
|
166
|
+
return React.useMemo(() => ({
|
|
167
|
+
status: binding.status,
|
|
168
|
+
schema: mapped,
|
|
169
|
+
config: binding.config,
|
|
170
|
+
error: binding.error,
|
|
171
|
+
}), [binding.status, binding.config, binding.error, mapped]);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The "named view does not resolve" panel. Same posture (and shape) as
|
|
175
|
+
* `SchemaRenderer`'s "Unknown component type": authored metadata pointing at
|
|
176
|
+
* something that is not there, reported where the author can see it.
|
|
177
|
+
*/
|
|
178
|
+
export function ElementDataSourceErrorPanel({ testId, title = 'This component’s data source could not be resolved', message, }) {
|
|
179
|
+
return (_jsxs("div", { className: "p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2", role: "alert", "data-testid": `${testId}-datasource-error`, children: [_jsx("p", { className: "font-medium", children: title }), message ? _jsx("p", { className: "text-sm mt-1", children: message }) : null] }));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The "saved views are still being fetched" placeholder. Distinct from the error
|
|
183
|
+
* panel because a component that treated "not resolved yet" as "does not exist"
|
|
184
|
+
* would flash a configuration error on every mount.
|
|
185
|
+
*/
|
|
186
|
+
export function ElementDataSourceLoadingPanel({ testId, }) {
|
|
187
|
+
return (_jsx("div", { className: "p-2 text-sm text-muted-foreground animate-pulse", role: "status", "aria-live": "polite", "data-testid": `${testId}-resolving-view`, children: "Loading view\u2026" }));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The adapter this block would use, resolved the one way every block in the
|
|
191
|
+
* family resolves it: an explicit value first, the `SchemaRendererProvider`
|
|
192
|
+
* context second.
|
|
193
|
+
*
|
|
194
|
+
* The `isElementDataSourceConfig` guard is the same one
|
|
195
|
+
* {@link useElementDataSourceSchema} applies — a host handing us the spec
|
|
196
|
+
* BINDING under this name is handing us metadata, not an adapter, and counting
|
|
197
|
+
* it as one is how "no data source" reads as "resolved".
|
|
198
|
+
*
|
|
199
|
+
* `T` is what the CALLER expects to have been injected, and the assertion is
|
|
200
|
+
* the caller's, not this hook's: an adapter arrives here as a `dataSource` prop
|
|
201
|
+
* or a context value that nothing in this package can narrow, so the type has
|
|
202
|
+
* to be declared by whoever knows which interface their block consumes. Leave
|
|
203
|
+
* it off and you get `unknown`, which is the honest default.
|
|
204
|
+
*/
|
|
205
|
+
export function useResolvedDataSource(explicit) {
|
|
206
|
+
const context = React.useContext(SchemaRendererContext);
|
|
207
|
+
const named = isElementDataSourceConfig(explicit) ? undefined : explicit;
|
|
208
|
+
return (named ?? context?.dataSource ?? undefined);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The author-facing sentence a block with no adapter says out loud.
|
|
212
|
+
*
|
|
213
|
+
* One wording for the whole object-bound family, because the reader's mistake
|
|
214
|
+
* is the same one every time and the fix is the same one every time: the
|
|
215
|
+
* adapter is injected by an ANCESTOR (`SchemaRendererProvider`), so nothing
|
|
216
|
+
* written on the block itself can supply it. Naming the block and the object
|
|
217
|
+
* it was about to read is what turns "nothing happened" into an address.
|
|
218
|
+
*/
|
|
219
|
+
export function noDataSourceMessage(blockKey, objectName) {
|
|
220
|
+
const target = typeof objectName === 'string' && objectName.trim().length > 0
|
|
221
|
+
? `“${objectName}”`
|
|
222
|
+
: 'its object';
|
|
223
|
+
return (`“${blockKey}” has no data source, so it cannot read ${target}. ` +
|
|
224
|
+
`Inject one from an ancestor — <SchemaRendererProvider dataSource={…}> from ` +
|
|
225
|
+
`@object-ui/react — or hand this block a dataSource of its own.`);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The "this block resolved no adapter" panel — objectui#5378 item 2, and the
|
|
229
|
+
* #5349 shape it belongs to.
|
|
230
|
+
*
|
|
231
|
+
* Until this existed, a block that resolved no adapter rendered an EMPTY SHELL:
|
|
232
|
+
* `object-grid` painted a header-only table, `object-form` a field-less card,
|
|
233
|
+
* `detail-view` nothing at all — no error, no warning, no empty state that
|
|
234
|
+
* explained itself. Measured on the published getting-started guide
|
|
235
|
+
* (`content/docs/guide/building-crud-app.md`), every one of its five blocks was
|
|
236
|
+
* in that state and the page reported success while fetching nothing.
|
|
237
|
+
*
|
|
238
|
+
* The empty shell is the defect, not a side effect of it: it is indistinguishable
|
|
239
|
+
* from "the query ran and matched no rows", which is the reading an author (very
|
|
240
|
+
* often an AI author) takes, and it points at the DATA when the fault is in the
|
|
241
|
+
* WIRING one level up. Same posture as {@link ElementDataSourceErrorPanel}: the
|
|
242
|
+
* metadata is answerable, so the answer is rendered where the author can see it.
|
|
243
|
+
*/
|
|
244
|
+
export function NoDataSourcePanel({ testId, title = 'No data source resolved', message, }) {
|
|
245
|
+
return (_jsxs("div", { className: "p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2", role: "alert", "data-testid": `${testId}-no-data-source`, children: [_jsx("p", { className: "font-medium", children: title }), message ? _jsx("p", { className: "text-sm mt-1", children: message }) : null] }));
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Wrap an object-bound block so its `dataSource` binding reaches the keys it
|
|
249
|
+
* reads, and so the two non-final resolution states render once, the same way,
|
|
250
|
+
* for every block.
|
|
251
|
+
*
|
|
252
|
+
* The block itself is not mounted while a named view is unresolved: rendering it
|
|
253
|
+
* against a half-resolved query is how a page shows a wider answer than the one
|
|
254
|
+
* that was authored.
|
|
255
|
+
*/
|
|
256
|
+
export function ElementDataSourceGate({ schema, mapping, dataSource, testId, errorTitle, requiresDataSource, noDataSourceMessage: noDataSourceText, children, }) {
|
|
257
|
+
const bound = useElementDataSourceSchema(schema, mapping, dataSource);
|
|
258
|
+
const adapter = useResolvedDataSource(dataSource);
|
|
259
|
+
// Reported BEFORE the view state, because with no adapter every downstream
|
|
260
|
+
// answer is a symptom of this one: `useElementDataSource` cannot list the
|
|
261
|
+
// object's saved views either, so a block carrying a `view` would otherwise
|
|
262
|
+
// report "this data source cannot list the saved views" — true, and pointing
|
|
263
|
+
// at the view name instead of at the missing injection.
|
|
264
|
+
if (requiresDataSource && adapter == null) {
|
|
265
|
+
return _jsx(NoDataSourcePanel, { testId: testId, message: noDataSourceText });
|
|
266
|
+
}
|
|
267
|
+
if (bound.status === 'missing') {
|
|
268
|
+
return _jsx(ElementDataSourceErrorPanel, { testId: testId, title: errorTitle, message: bound.error });
|
|
269
|
+
}
|
|
270
|
+
if (bound.status === 'loading') {
|
|
271
|
+
return _jsx(ElementDataSourceLoadingPanel, { testId: testId });
|
|
272
|
+
}
|
|
273
|
+
return children(bound.schema);
|
|
274
|
+
}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -5,35 +5,38 @@
|
|
|
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';
|
|
9
|
-
export * from './useFilterScope';
|
|
10
|
-
export * from './useActionRunner';
|
|
11
|
-
export * from './useNavigationOverlay';
|
|
12
|
-
export * from './usePageVariables';
|
|
13
|
-
export * from './usePageVariableActionBridge';
|
|
14
|
-
export * from './useViewData';
|
|
15
|
-
export * from './
|
|
16
|
-
export * from './
|
|
17
|
-
export * from './
|
|
18
|
-
export * from './
|
|
19
|
-
export * from './
|
|
20
|
-
export * from './
|
|
21
|
-
export * from './
|
|
22
|
-
export * from './
|
|
23
|
-
export * from './
|
|
24
|
-
export * from './
|
|
25
|
-
export * from './
|
|
26
|
-
export * from './
|
|
27
|
-
export * from './
|
|
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 './
|
|
39
|
-
export * from './
|
|
8
|
+
export * from './useExpression.js';
|
|
9
|
+
export * from './useFilterScope.js';
|
|
10
|
+
export * from './useActionRunner.js';
|
|
11
|
+
export * from './useNavigationOverlay.js';
|
|
12
|
+
export * from './usePageVariables.js';
|
|
13
|
+
export * from './usePageVariableActionBridge.js';
|
|
14
|
+
export * from './useViewData.js';
|
|
15
|
+
export * from './useElementDataSource.js';
|
|
16
|
+
export * from './useDynamicApp.js';
|
|
17
|
+
export * from './useDiscovery.js';
|
|
18
|
+
export * from './useFocusTrap.js';
|
|
19
|
+
export * from './useFocusManagement.js';
|
|
20
|
+
export * from './useKeyboardShortcuts.js';
|
|
21
|
+
export * from './useCrudShortcuts.js';
|
|
22
|
+
export * from './useReducedMotion.js';
|
|
23
|
+
export * from './useAnimation.js';
|
|
24
|
+
export * from './useDensityMode.js';
|
|
25
|
+
export * from './useViewSharing.js';
|
|
26
|
+
export * from './useClientNotifications.js';
|
|
27
|
+
export * from './useOffline.js';
|
|
28
|
+
export * from './usePerformance.js';
|
|
29
|
+
export * from './usePerformanceBudget.js';
|
|
30
|
+
export * from './usePageTransition.js';
|
|
31
|
+
export * from './useViewTransition.js';
|
|
32
|
+
export * from './useETagCache.js';
|
|
33
|
+
export * from './useSchemaPersistence.js';
|
|
34
|
+
export * from './useGlobalUndo.js';
|
|
35
|
+
export * from './useDebugMode.js';
|
|
36
|
+
export * from './useActionEngine.js';
|
|
37
|
+
export * from './useActionTextLocalizer.js';
|
|
38
|
+
export * from './useCapabilityGate.js';
|
|
39
|
+
export * from './useDatasetDimensionLabels.js';
|
|
40
|
+
export * from './useDataRefresh.js';
|
|
41
|
+
export * from './usePageAssignment.js';
|
|
42
|
+
export * from './useRecordSearch.js';
|
package/dist/hooks/index.js
CHANGED
|
@@ -5,36 +5,46 @@
|
|
|
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
|
-
export * from './useActionRunner';
|
|
12
|
-
export * from './useNavigationOverlay';
|
|
13
|
-
export * from './usePageVariables';
|
|
14
|
-
export * from './usePageVariableActionBridge';
|
|
15
|
-
export * from './useViewData';
|
|
16
|
-
|
|
17
|
-
export * from './
|
|
18
|
-
export * from './
|
|
19
|
-
export * from './
|
|
20
|
-
export * from './
|
|
21
|
-
export * from './
|
|
22
|
-
export * from './
|
|
23
|
-
export * from './
|
|
24
|
-
export * from './
|
|
25
|
-
export * from './
|
|
26
|
-
export * from './
|
|
27
|
-
export * from './
|
|
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 './
|
|
39
|
-
|
|
40
|
-
|
|
10
|
+
export * from './useFilterScope.js';
|
|
11
|
+
export * from './useActionRunner.js';
|
|
12
|
+
export * from './useNavigationOverlay.js';
|
|
13
|
+
export * from './usePageVariables.js';
|
|
14
|
+
export * from './usePageVariableActionBridge.js';
|
|
15
|
+
export * from './useViewData.js';
|
|
16
|
+
// PageComponentSchema.dataSource — the spec's per-element data binding.
|
|
17
|
+
export * from './useElementDataSource.js';
|
|
18
|
+
export * from './useDynamicApp.js';
|
|
19
|
+
export * from './useDiscovery.js';
|
|
20
|
+
export * from './useFocusTrap.js';
|
|
21
|
+
export * from './useFocusManagement.js';
|
|
22
|
+
export * from './useKeyboardShortcuts.js';
|
|
23
|
+
export * from './useCrudShortcuts.js';
|
|
24
|
+
export * from './useReducedMotion.js';
|
|
25
|
+
export * from './useAnimation.js';
|
|
26
|
+
export * from './useDensityMode.js';
|
|
27
|
+
export * from './useViewSharing.js';
|
|
28
|
+
export * from './useClientNotifications.js';
|
|
29
|
+
export * from './useOffline.js';
|
|
30
|
+
export * from './usePerformance.js';
|
|
31
|
+
export * from './usePerformanceBudget.js';
|
|
32
|
+
export * from './usePageTransition.js';
|
|
33
|
+
export * from './useViewTransition.js';
|
|
34
|
+
export * from './useETagCache.js';
|
|
35
|
+
export * from './useSchemaPersistence.js';
|
|
36
|
+
export * from './useGlobalUndo.js';
|
|
37
|
+
export * from './useDebugMode.js';
|
|
38
|
+
export * from './useActionEngine.js';
|
|
39
|
+
// The ONE application of `useObjectLabel`'s action resolvers over the three
|
|
40
|
+
// authored strings an action carries (label / confirmText / successMessage).
|
|
41
|
+
export * from './useActionTextLocalizer.js';
|
|
42
|
+
export * from './useCapabilityGate.js';
|
|
43
|
+
// The analytics label net's React glue, consumed by BOTH plugin-dashboard's
|
|
44
|
+
// `DatasetWidget` and plugin-report's dataset block (objectui#4389). It lives
|
|
45
|
+
// here rather than in `@object-ui/core` because it reads `SchemaRendererContext`
|
|
46
|
+
// — see the file header for the measured dependency direction.
|
|
47
|
+
export * from './useDatasetDimensionLabels.js';
|
|
48
|
+
export * from './useDataRefresh.js';
|
|
49
|
+
export * from './usePageAssignment.js';
|
|
50
|
+
export * from './useRecordSearch.js';
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { useCallback, useContext, useMemo } from 'react';
|
|
25
25
|
import { ActionEngine, } from '@object-ui/core';
|
|
26
|
-
import { ActionCtxReact } from '../context/ActionContext';
|
|
26
|
+
import { ActionCtxReact } from '../context/ActionContext.js';
|
|
27
27
|
export function useActionEngine(options = {}) {
|
|
28
28
|
const { actions = [], context = {} } = options;
|
|
29
29
|
// When wrapped in an <ActionProvider>, reuse its ActionRunner so that:
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
* `useActionTextLocalizer` — the ONE place a declared action's authored,
|
|
9
|
+
* user-visible strings are resolved against the active locale's bundle.
|
|
10
|
+
*
|
|
11
|
+
* ## Why this hook exists (objectui#4265)
|
|
12
|
+
*
|
|
13
|
+
* A TranslationBundle entry for an action carries THREE keys, all under the
|
|
14
|
+
* same `_actions.<name>.*` node:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* convert_lead: {
|
|
18
|
+
* label: '...', // the button
|
|
19
|
+
* confirmText: '...', // the confirm dialog BODY
|
|
20
|
+
* successMessage: '...', // the success toast
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* `useObjectLabel()` has always exposed a resolver for each of them
|
|
25
|
+
* (`actionLabel` / `actionConfirm` / `actionSuccess`, one `resolve()` and one
|
|
26
|
+
* key convention behind all three). What drifted is the CALL SITES: some
|
|
27
|
+
* render surfaces called all three, others called only `actionLabel`. On those
|
|
28
|
+
* surfaces one bundle entry met two fates — the button rendered the
|
|
29
|
+
* translation while the confirm dialog rendered the authored English literal,
|
|
30
|
+
* which is exactly what objectui#4265 reports.
|
|
31
|
+
*
|
|
32
|
+
* So this is deliberately NOT a new resolution dialect. It is a single
|
|
33
|
+
* application of the existing resolvers over the three keys, so a surface can
|
|
34
|
+
* no longer localize one of them and forget the others: there is one function
|
|
35
|
+
* to call, and it can only do all three.
|
|
36
|
+
*
|
|
37
|
+
* ## Pinned semantics
|
|
38
|
+
*
|
|
39
|
+
* - **Fallback**: no bundle entry, or an entry lacking the key, renders the
|
|
40
|
+
* AUTHORED text. That is `useObjectLabel`'s own fallback contract; nothing
|
|
41
|
+
* here overrides it.
|
|
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.
|
|
44
|
+
* - **`confirmText` / `successMessage` are resolved only when the action
|
|
45
|
+
* DECLARES them.** A bundle must not be able to bolt a confirmation gate
|
|
46
|
+
* onto an action whose metadata never asked for one (nor a success toast).
|
|
47
|
+
* Translation localizes what exists; it does not add behaviour. This mirrors
|
|
48
|
+
* what the already-correct call sites (`RecordDetailView`, `ObjectView`,
|
|
49
|
+
* `DeclaredActionsBar`) each spelled out by hand.
|
|
50
|
+
* - **`label` is reduced through `pickLocalized` first**, because since rc.6 an
|
|
51
|
+
* authored `label` may be an `I18nLabel` map rather than a string. The map is
|
|
52
|
+
* collapsed to the active language BEFORE it is offered to the bundle as the
|
|
53
|
+
* fallback, so a per-locale literal and a bundle entry cannot disagree about
|
|
54
|
+
* what "the authored label" is.
|
|
55
|
+
*/
|
|
56
|
+
export interface ActionTextLocalizerOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Last-resort display text, used only when the action declares neither a
|
|
59
|
+
* `label` nor a `name` (e.g. a positional `Action 3` placeholder).
|
|
60
|
+
*/
|
|
61
|
+
fallbackLabel?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Returns a copy of `action` with `label` — plus `confirmText` and
|
|
65
|
+
* `successMessage` when the action declares them — resolved from the active
|
|
66
|
+
* locale's bundle, falling back to the authored literals.
|
|
67
|
+
*/
|
|
68
|
+
export type ActionTextLocalizer = <T extends Record<string, any>>(objectName: string | undefined, action: T, options?: ActionTextLocalizerOptions) => T;
|
|
69
|
+
export declare function useActionTextLocalizer(): ActionTextLocalizer;
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
* `useActionTextLocalizer` — the ONE place a declared action's authored,
|
|
9
|
+
* user-visible strings are resolved against the active locale's bundle.
|
|
10
|
+
*
|
|
11
|
+
* ## Why this hook exists (objectui#4265)
|
|
12
|
+
*
|
|
13
|
+
* A TranslationBundle entry for an action carries THREE keys, all under the
|
|
14
|
+
* same `_actions.<name>.*` node:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* convert_lead: {
|
|
18
|
+
* label: '...', // the button
|
|
19
|
+
* confirmText: '...', // the confirm dialog BODY
|
|
20
|
+
* successMessage: '...', // the success toast
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* `useObjectLabel()` has always exposed a resolver for each of them
|
|
25
|
+
* (`actionLabel` / `actionConfirm` / `actionSuccess`, one `resolve()` and one
|
|
26
|
+
* key convention behind all three). What drifted is the CALL SITES: some
|
|
27
|
+
* render surfaces called all three, others called only `actionLabel`. On those
|
|
28
|
+
* surfaces one bundle entry met two fates — the button rendered the
|
|
29
|
+
* translation while the confirm dialog rendered the authored English literal,
|
|
30
|
+
* which is exactly what objectui#4265 reports.
|
|
31
|
+
*
|
|
32
|
+
* So this is deliberately NOT a new resolution dialect. It is a single
|
|
33
|
+
* application of the existing resolvers over the three keys, so a surface can
|
|
34
|
+
* no longer localize one of them and forget the others: there is one function
|
|
35
|
+
* to call, and it can only do all three.
|
|
36
|
+
*
|
|
37
|
+
* ## Pinned semantics
|
|
38
|
+
*
|
|
39
|
+
* - **Fallback**: no bundle entry, or an entry lacking the key, renders the
|
|
40
|
+
* AUTHORED text. That is `useObjectLabel`'s own fallback contract; nothing
|
|
41
|
+
* here overrides it.
|
|
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.
|
|
44
|
+
* - **`confirmText` / `successMessage` are resolved only when the action
|
|
45
|
+
* DECLARES them.** A bundle must not be able to bolt a confirmation gate
|
|
46
|
+
* onto an action whose metadata never asked for one (nor a success toast).
|
|
47
|
+
* Translation localizes what exists; it does not add behaviour. This mirrors
|
|
48
|
+
* what the already-correct call sites (`RecordDetailView`, `ObjectView`,
|
|
49
|
+
* `DeclaredActionsBar`) each spelled out by hand.
|
|
50
|
+
* - **`label` is reduced through `pickLocalized` first**, because since rc.6 an
|
|
51
|
+
* authored `label` may be an `I18nLabel` map rather than a string. The map is
|
|
52
|
+
* collapsed to the active language BEFORE it is offered to the bundle as the
|
|
53
|
+
* fallback, so a per-locale literal and a bundle entry cannot disagree about
|
|
54
|
+
* what "the authored label" is.
|
|
55
|
+
*/
|
|
56
|
+
import { useMemo } from 'react';
|
|
57
|
+
import { useObjectLabel, useObjectTranslation, pickLocalized } from '@object-ui/i18n';
|
|
58
|
+
export function useActionTextLocalizer() {
|
|
59
|
+
const { actionLabel, actionConfirm, actionSuccess } = useObjectLabel();
|
|
60
|
+
const { language } = useObjectTranslation();
|
|
61
|
+
return useMemo(() => {
|
|
62
|
+
return ((objectName, action, options) => {
|
|
63
|
+
if (!action || typeof action !== 'object')
|
|
64
|
+
return action;
|
|
65
|
+
// An `I18nLabel` map collapses to the active language here, so what the
|
|
66
|
+
// bundle receives as its fallback is always a plain string.
|
|
67
|
+
const authoredLabel = pickLocalized(action.label, language);
|
|
68
|
+
const name = typeof action.name === 'string' && action.name
|
|
69
|
+
? action.name
|
|
70
|
+
: undefined;
|
|
71
|
+
const displayLabel = authoredLabel || name || options?.fallbackLabel || '';
|
|
72
|
+
// No name, no translation key: the literals are the answer. The label is
|
|
73
|
+
// still normalized (map to string, last-resort placeholder applied) so
|
|
74
|
+
// every caller reads one shape.
|
|
75
|
+
if (!name) {
|
|
76
|
+
return displayLabel === action.label ? action : { ...action, label: displayLabel };
|
|
77
|
+
}
|
|
78
|
+
const out = {
|
|
79
|
+
...action,
|
|
80
|
+
label: actionLabel(objectName, name, displayLabel),
|
|
81
|
+
};
|
|
82
|
+
if (action.confirmText !== undefined) {
|
|
83
|
+
out.confirmText = actionConfirm(objectName, name, action.confirmText);
|
|
84
|
+
}
|
|
85
|
+
if (action.successMessage !== undefined) {
|
|
86
|
+
out.successMessage = actionSuccess(objectName, name, action.successMessage);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
});
|
|
90
|
+
}, [actionLabel, actionConfirm, actionSuccess, language]);
|
|
91
|
+
}
|
|
@@ -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).
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
* server side exists.
|
|
27
27
|
*/
|
|
28
28
|
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
29
|
-
import { SchemaRendererContext } from '../context/SchemaRendererContext';
|
|
30
|
-
import { useNotifications } from '../context/NotificationContext';
|
|
29
|
+
import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
|
|
30
|
+
import { useNotifications } from '../context/NotificationContext.js';
|
|
31
31
|
/* ------------------------------------------------------------------ */
|
|
32
32
|
/* Helpers */
|
|
33
33
|
/* ------------------------------------------------------------------ */
|
|
@@ -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
|
*
|