@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,60 @@
|
|
|
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 type { PagePrint } from '@objectstack/spec/ui';
|
|
9
|
+
/**
|
|
10
|
+
* PrintPageContext — the `print` declaration of the page this subtree renders
|
|
11
|
+
* in, or `null` outside a print page (objectui#11958, card ② of the ruling of
|
|
12
|
+
* record on objectstack#8346).
|
|
13
|
+
*
|
|
14
|
+
* A page that declares `print` is a document: its blocks come from the spec's
|
|
15
|
+
* printable block subset (`PRINTABLE_PAGE_COMPONENT_TYPES` in
|
|
16
|
+
* `@objectstack/spec/ui`), and each of those blocks has to reach paper WHOLE.
|
|
17
|
+
* Most of them already do. The ones that draw a window of their data on screen
|
|
18
|
+
* read this context to stop doing so silently: `record:line_items` names its
|
|
19
|
+
* row cap when the cap cut lines off, and a collapsible `record:details`
|
|
20
|
+
* section draws open, because a closed section never reaches paper.
|
|
21
|
+
*
|
|
22
|
+
* Published by `PageRenderer` (`@object-ui/components`) for a page whose
|
|
23
|
+
* `print` is an object. The value is the declaration itself, so a block that
|
|
24
|
+
* ever needs one of its keys reads it from here rather than from a second
|
|
25
|
+
* channel. Defaults to `null`, so every page that does not declare `print` is
|
|
26
|
+
* untouched.
|
|
27
|
+
*/
|
|
28
|
+
declare const PrintPageContext: import("react").Context<{
|
|
29
|
+
paperSize?: "A4" | "A5" | "Letter" | "Legal" | undefined;
|
|
30
|
+
orientation?: "portrait" | "landscape" | undefined;
|
|
31
|
+
margins?: {
|
|
32
|
+
top?: number | undefined;
|
|
33
|
+
right?: number | undefined;
|
|
34
|
+
bottom?: number | undefined;
|
|
35
|
+
left?: number | undefined;
|
|
36
|
+
} | undefined;
|
|
37
|
+
repeatHeader?: boolean | undefined;
|
|
38
|
+
repeatFooter?: boolean | undefined;
|
|
39
|
+
pageNumbers?: boolean | undefined;
|
|
40
|
+
repeatTableHeaders?: boolean | undefined;
|
|
41
|
+
avoidBreakInside?: boolean | undefined;
|
|
42
|
+
} | null>;
|
|
43
|
+
export declare const PrintPageProvider: import("react").Provider<{
|
|
44
|
+
paperSize?: "A4" | "A5" | "Letter" | "Legal" | undefined;
|
|
45
|
+
orientation?: "portrait" | "landscape" | undefined;
|
|
46
|
+
margins?: {
|
|
47
|
+
top?: number | undefined;
|
|
48
|
+
right?: number | undefined;
|
|
49
|
+
bottom?: number | undefined;
|
|
50
|
+
left?: number | undefined;
|
|
51
|
+
} | undefined;
|
|
52
|
+
repeatHeader?: boolean | undefined;
|
|
53
|
+
repeatFooter?: boolean | undefined;
|
|
54
|
+
pageNumbers?: boolean | undefined;
|
|
55
|
+
repeatTableHeaders?: boolean | undefined;
|
|
56
|
+
avoidBreakInside?: boolean | undefined;
|
|
57
|
+
} | null>;
|
|
58
|
+
/** The enclosing page's `print` declaration, or `null` outside a print page. Safe outside a provider. */
|
|
59
|
+
export declare const usePrintPage: () => PagePrint | null;
|
|
60
|
+
export { PrintPageContext };
|
|
@@ -0,0 +1,32 @@
|
|
|
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 { createContext, useContext } from 'react';
|
|
9
|
+
/**
|
|
10
|
+
* PrintPageContext — the `print` declaration of the page this subtree renders
|
|
11
|
+
* in, or `null` outside a print page (objectui#11958, card ② of the ruling of
|
|
12
|
+
* record on objectstack#8346).
|
|
13
|
+
*
|
|
14
|
+
* A page that declares `print` is a document: its blocks come from the spec's
|
|
15
|
+
* printable block subset (`PRINTABLE_PAGE_COMPONENT_TYPES` in
|
|
16
|
+
* `@objectstack/spec/ui`), and each of those blocks has to reach paper WHOLE.
|
|
17
|
+
* Most of them already do. The ones that draw a window of their data on screen
|
|
18
|
+
* read this context to stop doing so silently: `record:line_items` names its
|
|
19
|
+
* row cap when the cap cut lines off, and a collapsible `record:details`
|
|
20
|
+
* section draws open, because a closed section never reaches paper.
|
|
21
|
+
*
|
|
22
|
+
* Published by `PageRenderer` (`@object-ui/components`) for a page whose
|
|
23
|
+
* `print` is an object. The value is the declaration itself, so a block that
|
|
24
|
+
* ever needs one of its keys reads it from here rather than from a second
|
|
25
|
+
* channel. Defaults to `null`, so every page that does not declare `print` is
|
|
26
|
+
* untouched.
|
|
27
|
+
*/
|
|
28
|
+
const PrintPageContext = createContext(null);
|
|
29
|
+
export const PrintPageProvider = PrintPageContext.Provider;
|
|
30
|
+
/** The enclosing page's `print` declaration, or `null` outside a print page. Safe outside a provider. */
|
|
31
|
+
export const usePrintPage = () => useContext(PrintPageContext);
|
|
32
|
+
export { PrintPageContext };
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
* it can route to, so the low-level renderers on a record page can turn a
|
|
31
31
|
* lookup VALUE into a link to the referenced record without assembling a URL
|
|
32
32
|
* of their own. Same provider, same builder, one route shape.
|
|
33
|
+
*
|
|
34
|
+
* Since objectui#12703 it also publishes the object-list URL builder
|
|
35
|
+
* ({@link RelatedRecordActionsValue.listHref} / `openList`), for a renderer
|
|
36
|
+
* that shows a few of an object's records and links to the rest.
|
|
33
37
|
*/
|
|
34
38
|
import React from 'react';
|
|
35
39
|
/** A child-object action surfaced in a related list's row menu. */
|
|
@@ -100,8 +104,40 @@ export interface RelatedRecordHandlers {
|
|
|
100
104
|
toolbarActions?: RelatedRowActionDef[];
|
|
101
105
|
/** Execute one of {@link toolbarActions} (no row context). */
|
|
102
106
|
onToolbarAction?: (action: RelatedRowActionDef) => void | Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Open the child object's full list page, scoped to THIS parent on the
|
|
109
|
+
* relationship `resolve` was handed (objectui#12741). The related list's
|
|
110
|
+
* "View all" link calls it, unless the block declares `showViewAll: false`.
|
|
111
|
+
*
|
|
112
|
+
* The host builds the destination because only the host knows the console's
|
|
113
|
+
* routes, as it does for {@link onView}. It omits the handler when no route
|
|
114
|
+
* it has can show that scoped list, and the link is then not drawn. One case
|
|
115
|
+
* is a multi-value relationship: the list route's URL filter has no
|
|
116
|
+
* membership operator. Another is a relationship field the viewer cannot
|
|
117
|
+
* read: the list route drops that condition and would show every row.
|
|
118
|
+
*/
|
|
119
|
+
onViewAll?: () => void;
|
|
103
120
|
}
|
|
104
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Input to {@link RelatedRecordActionsValue.resolve}.
|
|
123
|
+
*
|
|
124
|
+
* Its SHAPE says which kind of list asks (objectui#13089):
|
|
125
|
+
*
|
|
126
|
+
* - **A related list always names its `relationshipField`.** The spec's
|
|
127
|
+
* `record:related_list` declares it required, so a spec-valid related list
|
|
128
|
+
* resolves with it, with or without a resolved `parentId`.
|
|
129
|
+
* - **An input with no `relationshipField` (absent or empty) asks for an
|
|
130
|
+
* OBJECT-SCOPED list's handlers**, a list of the object's own records with
|
|
131
|
+
* no parent: a page `list-view` node resolves `{ objectName }` for its
|
|
132
|
+
* `addRecord` Add button. A host may serve such an input a create that
|
|
133
|
+
* links no parent, which it must not serve a related list whose parent did
|
|
134
|
+
* not resolve.
|
|
135
|
+
*
|
|
136
|
+
* ⚠️ The limit of reading the shape: a future consumer that resolves a list
|
|
137
|
+
* with no parent and no relationship, and must NOT be offered a create, cannot
|
|
138
|
+
* say so here. It needs an explicit member on this input, not a second reading
|
|
139
|
+
* of the same shape.
|
|
140
|
+
*/
|
|
105
141
|
export interface ResolveRelatedRecordActionsInput {
|
|
106
142
|
/** Child object machine name (the related list's `api`). */
|
|
107
143
|
objectName: string;
|
|
@@ -132,6 +168,11 @@ export interface RelatedRecordActionsValue {
|
|
|
132
168
|
*
|
|
133
169
|
* Returns `null` when the host cannot route to that object (unknown object,
|
|
134
170
|
* no app context), which consumers must render as the plain value.
|
|
171
|
+
*
|
|
172
|
+
* The value is a document href, read by the browser on "open in new tab" and
|
|
173
|
+
* "copy link address", so it carries the host router's basename (for
|
|
174
|
+
* example `/_console/apps/…`), while {@link openRecord} navigates by the
|
|
175
|
+
* router path (objectui#12233).
|
|
135
176
|
*/
|
|
136
177
|
recordHref?: (objectName: string, recordId: string | number) => string | null;
|
|
137
178
|
/**
|
|
@@ -140,6 +181,31 @@ export interface RelatedRecordActionsValue {
|
|
|
140
181
|
* in-app; a modifier click is left to the browser and the href.
|
|
141
182
|
*/
|
|
142
183
|
openRecord?: (objectName: string, recordId: string | number) => void;
|
|
184
|
+
/**
|
|
185
|
+
* The host's list-page URL for an object it can route to, optionally opened
|
|
186
|
+
* on one of the object's saved views (objectui#12703). The list-side twin of
|
|
187
|
+
* {@link recordHref}: a renderer that shows a few of an object's records (the
|
|
188
|
+
* compact list a page draws in a narrow region) presents "View all" as a real
|
|
189
|
+
* anchor to this destination, without assembling a URL of its own.
|
|
190
|
+
*
|
|
191
|
+
* `viewName` names a saved view of that object, as the block binding states
|
|
192
|
+
* it (`dataSource.view`); absent, the destination is the object's own list.
|
|
193
|
+
* The destination is a view or the object, never a query: a caller does not
|
|
194
|
+
* encode its own filter, columns or sort into it, and the host has no
|
|
195
|
+
* argument to receive them.
|
|
196
|
+
*
|
|
197
|
+
* Returns `null` when the host cannot route to that object (unknown object,
|
|
198
|
+
* no app context); consumers then draw no link. Like {@link recordHref} it is
|
|
199
|
+
* a document href that carries the router's basename, while {@link openList}
|
|
200
|
+
* navigates by the router path.
|
|
201
|
+
*/
|
|
202
|
+
listHref?: (objectName: string, viewName?: string) => string | null;
|
|
203
|
+
/**
|
|
204
|
+
* SPA-navigate to the destination {@link listHref} addresses. Consumers call
|
|
205
|
+
* this from a plain left click on the anchor; a modifier click is left to the
|
|
206
|
+
* browser and the href.
|
|
207
|
+
*/
|
|
208
|
+
openList?: (objectName: string, viewName?: string) => void;
|
|
143
209
|
}
|
|
144
210
|
export declare const RelatedRecordActionsProvider: React.FC<{
|
|
145
211
|
value: RelatedRecordActionsValue;
|
|
@@ -31,6 +31,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
31
31
|
* it can route to, so the low-level renderers on a record page can turn a
|
|
32
32
|
* lookup VALUE into a link to the referenced record without assembling a URL
|
|
33
33
|
* of their own. Same provider, same builder, one route shape.
|
|
34
|
+
*
|
|
35
|
+
* Since objectui#12703 it also publishes the object-list URL builder
|
|
36
|
+
* ({@link RelatedRecordActionsValue.listHref} / `openList`), for a renderer
|
|
37
|
+
* that shows a few of an object's records and links to the rest.
|
|
34
38
|
*/
|
|
35
39
|
import { createContext, useContext } from 'react';
|
|
36
40
|
const RelatedRecordActionsContext = createContext(null);
|
|
@@ -2,10 +2,13 @@ import React from 'react';
|
|
|
2
2
|
import type { DebugFlags } from '@object-ui/core';
|
|
3
3
|
import type { DataSource } from '@object-ui/types';
|
|
4
4
|
/**
|
|
5
|
-
* Host-provided fetch
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Host-provided fetch the renderers use for their own reads of the host's API
|
|
6
|
+
* (a chart's metadata reads, dataset dimension labels, record-permission
|
|
7
|
+
* probes), so those reads carry the same credentials (Authorization, tenant,
|
|
8
|
+
* locale headers) as the native data channel. Optional — when absent, those
|
|
9
|
+
* reads fall back to the bare global fetch (cookies only). A view does not
|
|
10
|
+
* reach it: a view binds `provider: 'object'` or `provider: 'value'`, and a
|
|
11
|
+
* code-built `ApiDataSource` takes its own `fetch` in its config.
|
|
9
12
|
*/
|
|
10
13
|
export type ApiFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
11
14
|
interface SchemaRendererContextType {
|
|
@@ -6,7 +6,7 @@ export { SchemaRendererContext };
|
|
|
6
6
|
export const SchemaRendererProvider = ({ children, dataSource, debug, debugFlags, apiFetch, }) => {
|
|
7
7
|
// Nested providers (react-page, studio preview surfaces) re-wrap with their
|
|
8
8
|
// own dataSource but rarely know about the host's authenticated fetch —
|
|
9
|
-
// inherit it from the parent context so
|
|
9
|
+
// inherit it from the parent context so the host's auth survives nesting.
|
|
10
10
|
const parent = useContext(SchemaRendererContext);
|
|
11
11
|
const effectiveApiFetch = apiFetch ?? parent?.apiFetch;
|
|
12
12
|
const value = useMemo(() => ({ dataSource, debug, debugFlags, apiFetch: effectiveApiFetch }), [dataSource, debug, debugFlags, effectiveApiFetch]);
|
package/dist/context/index.d.ts
CHANGED
package/dist/context/index.js
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
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 error panel of a page element's API binding (objectui#12197), titled in
|
|
9
|
+
* the page's language.
|
|
10
|
+
*
|
|
11
|
+
* `SchemaRenderer` draws it in place of a node whose binding the spec refused
|
|
12
|
+
* or whose read failed. The translation hook lives HERE, not in
|
|
13
|
+
* `SchemaRenderer`'s body: that body runs for every node on every page, and a
|
|
14
|
+
* language subscription there would re-render the whole tree on a language
|
|
15
|
+
* change. This component mounts only on the error path, so only it subscribes.
|
|
16
|
+
* The panel itself is {@link ElementDataSourceErrorPanel}; the inline
|
|
17
|
+
* `defaultValue`s are the `en` pack's values (`pnpm check:i18n-keys`).
|
|
18
|
+
*/
|
|
19
|
+
import * as React from 'react';
|
|
20
|
+
export interface ElementApiDataSourcePanelProps {
|
|
21
|
+
/** `data-testid` stem — the node's registry key. */
|
|
22
|
+
testId: string;
|
|
23
|
+
/** `refused`: the spec's schema refused the binding. `error`: the read failed. */
|
|
24
|
+
state: 'refused' | 'error';
|
|
25
|
+
/** Why — the spec's refusal, or the failed read's sentence. */
|
|
26
|
+
message?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function ElementApiDataSourcePanel({ testId, state, message, }: ElementApiDataSourcePanelProps): React.ReactElement;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
3
|
+
import { ElementDataSourceErrorPanel } from './ElementDataSourceGate.js';
|
|
4
|
+
export function ElementApiDataSourcePanel({ testId, state, message, }) {
|
|
5
|
+
const { t } = useObjectTranslation();
|
|
6
|
+
const title = state === 'refused'
|
|
7
|
+
? t('element.component.apiRefused', { defaultValue: 'This component’s API data source is refused' })
|
|
8
|
+
: t('element.component.apiUnreadable', { defaultValue: 'This component’s API data source could not be read' });
|
|
9
|
+
return _jsx(ElementDataSourceErrorPanel, { testId: testId, title: title, message: message });
|
|
10
|
+
}
|
|
@@ -193,7 +193,10 @@ export interface ElementDataSourceStatusPanelProps {
|
|
|
193
193
|
* asserting about rather than a shared anonymous id.
|
|
194
194
|
*/
|
|
195
195
|
testId: string;
|
|
196
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Panel heading; defaults to a block-neutral sentence read from the bundle
|
|
198
|
+
* (objectui#12920). A passed `title`, an empty string included, wins.
|
|
199
|
+
*/
|
|
197
200
|
title?: string;
|
|
198
201
|
/** The author-facing explanation from {@link useElementDataSourceSchema}. */
|
|
199
202
|
message?: string;
|
|
@@ -208,6 +211,13 @@ export declare function ElementDataSourceErrorPanel({ testId, title, message, }:
|
|
|
208
211
|
* The "saved views are still being fetched" placeholder. Distinct from the error
|
|
209
212
|
* panel because a component that treated "not resolved yet" as "does not exist"
|
|
210
213
|
* would flash a configuration error on every mount.
|
|
214
|
+
*
|
|
215
|
+
* Its copy reads `common.loading` (objectui#12889), the key the rest of the
|
|
216
|
+
* tree reads for a bare "Loading…": it printed an English "Loading view…" on
|
|
217
|
+
* every locale before. `useObjectTranslation` runs only while a view resolves,
|
|
218
|
+
* for the reason {@link ElementDataSourceMalformedFilterPanel} gives, and the
|
|
219
|
+
* inline `defaultValue` is the `en` pack's value, held byte-identical to it by
|
|
220
|
+
* `pnpm check:i18n-keys`.
|
|
211
221
|
*/
|
|
212
222
|
export declare function ElementDataSourceLoadingPanel({ testId, }: Pick<ElementDataSourceStatusPanelProps, 'testId'>): React.ReactElement;
|
|
213
223
|
/**
|
|
@@ -340,6 +350,24 @@ export interface ElementDataSourceGateProps<S> {
|
|
|
340
350
|
requiresDataSource?: boolean;
|
|
341
351
|
/** Explanation for the no-adapter panel; see {@link noDataSourceMessage}. */
|
|
342
352
|
noDataSourceMessage?: string;
|
|
353
|
+
/**
|
|
354
|
+
* Whether THIS placement has nothing to show unless it names an object — when
|
|
355
|
+
* true and the bound schema names none on the mapping's object key (neither
|
|
356
|
+
* the node's own key nor `dataSource.object` landing on it), the gate renders
|
|
357
|
+
* the "no object named" hint instead of the block (objectui#11605).
|
|
358
|
+
*
|
|
359
|
+
* Read AFTER the binding is applied, so a node bound by `dataSource.object`
|
|
360
|
+
* is never told it names no object; and AFTER the view states, so a binding
|
|
361
|
+
* that is still resolving or failed to resolve keeps its own panel.
|
|
362
|
+
*
|
|
363
|
+
* The call site states it, for the reason {@link requiresDataSource} gives:
|
|
364
|
+
* the other record sources a block can draw from (inline `data`, inline
|
|
365
|
+
* `customFields`, a `dataset`, a `bind` path) are each block's own, and a
|
|
366
|
+
* predicate guessed here would paint a configuration hint over a block that
|
|
367
|
+
* is working. A mapping whose `object` is `false` lands no object key, so
|
|
368
|
+
* this prop does nothing there.
|
|
369
|
+
*/
|
|
370
|
+
requiresObject?: boolean;
|
|
343
371
|
/**
|
|
344
372
|
* Renders the block with the bound schema. Called during the gate's own
|
|
345
373
|
* render, so it must RETURN AN ELEMENT and never call hooks itself — the
|
|
@@ -356,4 +384,4 @@ export interface ElementDataSourceGateProps<S> {
|
|
|
356
384
|
* against a half-resolved query is how a page shows a wider answer than the one
|
|
357
385
|
* that was authored.
|
|
358
386
|
*/
|
|
359
|
-
export declare function ElementDataSourceGate<S>({ schema, mapping, dataSource, testId, errorTitle, requiresDataSource, noDataSourceMessage: noDataSourceText, children, }: ElementDataSourceGateProps<S>): React.ReactElement | null;
|
|
387
|
+
export declare function ElementDataSourceGate<S>({ schema, mapping, dataSource, testId, errorTitle, requiresDataSource, noDataSourceMessage: noDataSourceText, requiresObject, children, }: ElementDataSourceGateProps<S>): React.ReactElement | null;
|
|
@@ -387,8 +387,19 @@ export function useElementDataSourceSchema(schema, mapping = {}, dataSource) {
|
|
|
387
387
|
* `SchemaRenderer`'s "Unknown component type": authored metadata pointing at
|
|
388
388
|
* something that is not there, reported where the author can see it.
|
|
389
389
|
*/
|
|
390
|
-
export function ElementDataSourceErrorPanel({ testId, title
|
|
391
|
-
|
|
390
|
+
export function ElementDataSourceErrorPanel({ testId, title, message, }) {
|
|
391
|
+
// objectui#12920 — the default reads the bundle; it was an English literal.
|
|
392
|
+
// Only an ABSENT `title` takes it, as the parameter default did. The hook
|
|
393
|
+
// runs on this error path only, for the reason
|
|
394
|
+
// {@link ElementDataSourceMalformedFilterPanel} gives, and the inline
|
|
395
|
+
// `defaultValue` is the `en` pack's value (`pnpm check:i18n-keys`).
|
|
396
|
+
const { t } = useObjectTranslation();
|
|
397
|
+
const heading = title === undefined
|
|
398
|
+
? t('element.component.sourceUnresolved', {
|
|
399
|
+
defaultValue: 'This component’s data source could not be resolved',
|
|
400
|
+
})
|
|
401
|
+
: title;
|
|
402
|
+
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: heading }), message ? _jsx("p", { className: "text-sm mt-1", children: message }) : null] }));
|
|
392
403
|
}
|
|
393
404
|
/**
|
|
394
405
|
* The malformed-filter notice, drawn IN PLACE OF the block when a filter the
|
|
@@ -425,13 +436,58 @@ function ElementDataSourceMalformedFilterPanel({ testId, refusal, }) {
|
|
|
425
436
|
defaultValue: 'This view’s filter is malformed, so no records are shown: the {{subject}} condition cannot be applied.',
|
|
426
437
|
}) }), _jsx("p", { className: "mt-1 text-xs opacity-80", children: refusal.message })] }));
|
|
427
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* The "no object named" hint, drawn IN PLACE OF the block when a placement
|
|
441
|
+
* that opted in with `requiresObject` names its object in neither place: not
|
|
442
|
+
* on the block's own object key, and not through `dataSource.object`
|
|
443
|
+
* (objectui#11605).
|
|
444
|
+
*
|
|
445
|
+
* Why it exists: the object-bound registrations no longer declare their object
|
|
446
|
+
* key `required`, because the binding can supply it and the manifest has no
|
|
447
|
+
* "this key or that binding" form. So the page compile accepts a node that
|
|
448
|
+
* names no object at all, and this hint is the one signal left for it. Before
|
|
449
|
+
* it, those blocks drew an empty board, a field-less form, a dash or a "no
|
|
450
|
+
* data yet" state, which reads as an empty query rather than a missing object.
|
|
451
|
+
*
|
|
452
|
+
* The wording is `element:number`'s own no-object notice (objectui#10951) with
|
|
453
|
+
* the property as a hole, so the two blocks that answer this question say it
|
|
454
|
+
* the same way. The property name is interpolated, never translated.
|
|
455
|
+
*
|
|
456
|
+
* A separate component for the reason {@link ElementDataSourceMalformedFilterPanel}
|
|
457
|
+
* gives: `useObjectTranslation` runs only on this path, and the provider-less
|
|
458
|
+
* default is a literal copy of the `en` pack's value, held byte-identical to it
|
|
459
|
+
* by `pnpm check:i18n-keys`.
|
|
460
|
+
*/
|
|
461
|
+
function ElementDataSourceNoObjectPanel({ testId, property, }) {
|
|
462
|
+
const { t } = useObjectTranslation();
|
|
463
|
+
return (_jsx("div", { className: "p-4 text-sm text-muted-foreground my-2", "data-testid": `${testId}-no-object`, children: t('view.noObject', {
|
|
464
|
+
property,
|
|
465
|
+
defaultValue: 'No object named: set {{property}} or dataSource.object.',
|
|
466
|
+
}) }));
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Does the bound schema name an object on `key`? A blank string names nothing,
|
|
470
|
+
* the same reading `noDataSourceMessage` gives an object name.
|
|
471
|
+
*/
|
|
472
|
+
const namesObject = (schema, key) => {
|
|
473
|
+
const value = schema?.[key];
|
|
474
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
475
|
+
};
|
|
428
476
|
/**
|
|
429
477
|
* The "saved views are still being fetched" placeholder. Distinct from the error
|
|
430
478
|
* panel because a component that treated "not resolved yet" as "does not exist"
|
|
431
479
|
* would flash a configuration error on every mount.
|
|
480
|
+
*
|
|
481
|
+
* Its copy reads `common.loading` (objectui#12889), the key the rest of the
|
|
482
|
+
* tree reads for a bare "Loading…": it printed an English "Loading view…" on
|
|
483
|
+
* every locale before. `useObjectTranslation` runs only while a view resolves,
|
|
484
|
+
* for the reason {@link ElementDataSourceMalformedFilterPanel} gives, and the
|
|
485
|
+
* inline `defaultValue` is the `en` pack's value, held byte-identical to it by
|
|
486
|
+
* `pnpm check:i18n-keys`.
|
|
432
487
|
*/
|
|
433
488
|
export function ElementDataSourceLoadingPanel({ testId, }) {
|
|
434
|
-
|
|
489
|
+
const { t } = useObjectTranslation();
|
|
490
|
+
return (_jsx("div", { className: "p-2 text-sm text-muted-foreground animate-pulse", role: "status", "aria-live": "polite", "data-testid": `${testId}-resolving-view`, children: t('common.loading', { defaultValue: 'Loading…' }) }));
|
|
435
491
|
}
|
|
436
492
|
/**
|
|
437
493
|
* The adapter this block would use, resolved the one way every block in the
|
|
@@ -491,8 +547,14 @@ export function noDataSourceMessage(blockKey, objectName) {
|
|
|
491
547
|
* WIRING one level up. Same posture as {@link ElementDataSourceErrorPanel}: the
|
|
492
548
|
* metadata is answerable, so the answer is rendered where the author can see it.
|
|
493
549
|
*/
|
|
494
|
-
export function NoDataSourcePanel({ testId, title
|
|
495
|
-
|
|
550
|
+
export function NoDataSourcePanel({ testId, title, message, }) {
|
|
551
|
+
// objectui#12920 — same reading as {@link ElementDataSourceErrorPanel}: an
|
|
552
|
+
// absent `title` reads the bundle, a passed one wins.
|
|
553
|
+
const { t } = useObjectTranslation();
|
|
554
|
+
const heading = title === undefined
|
|
555
|
+
? t('element.component.noDataSource', { defaultValue: 'No data source resolved' })
|
|
556
|
+
: title;
|
|
557
|
+
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: heading }), message ? _jsx("p", { className: "text-sm mt-1", children: message }) : null] }));
|
|
496
558
|
}
|
|
497
559
|
/**
|
|
498
560
|
* The wrapping seam — mark `renderer` as one that wraps {@link
|
|
@@ -560,7 +622,7 @@ export { elementDataSourceBlock } from '@object-ui/core';
|
|
|
560
622
|
* against a half-resolved query is how a page shows a wider answer than the one
|
|
561
623
|
* that was authored.
|
|
562
624
|
*/
|
|
563
|
-
export function ElementDataSourceGate({ schema, mapping, dataSource, testId, errorTitle, requiresDataSource, noDataSourceMessage: noDataSourceText, children, }) {
|
|
625
|
+
export function ElementDataSourceGate({ schema, mapping, dataSource, testId, errorTitle, requiresDataSource, noDataSourceMessage: noDataSourceText, requiresObject, children, }) {
|
|
564
626
|
const bound = useElementDataSourceSchema(schema, mapping, dataSource);
|
|
565
627
|
const adapter = useResolvedDataSource(dataSource);
|
|
566
628
|
// Reported BEFORE the view state, because with no adapter every downstream
|
|
@@ -583,5 +645,12 @@ export function ElementDataSourceGate({ schema, mapping, dataSource, testId, err
|
|
|
583
645
|
if (bound.status === 'loading') {
|
|
584
646
|
return _jsx(ElementDataSourceLoadingPanel, { testId: testId });
|
|
585
647
|
}
|
|
648
|
+
// Named in neither place: not on the block's own key, and not by a binding
|
|
649
|
+
// (whose `object` the hook above has already landed on that key). See
|
|
650
|
+
// `requiresObject` for why the call site, not this gate, decides it applies.
|
|
651
|
+
const objectKey = mapping?.object ?? 'objectName';
|
|
652
|
+
if (requiresObject && objectKey !== false && !namesObject(bound.schema, objectKey)) {
|
|
653
|
+
return _jsx(ElementDataSourceNoObjectPanel, { testId: testId, property: objectKey });
|
|
654
|
+
}
|
|
586
655
|
return children(bound.schema);
|
|
587
656
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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 { type ElementApiDataSource } from '@objectstack/spec/ui';
|
|
53
|
+
import { type ApiFetch } from '../context/SchemaRendererContext.js';
|
|
54
|
+
/**
|
|
55
|
+
* Where an API binding stands.
|
|
56
|
+
*
|
|
57
|
+
* - `absent` — the node carries no API binding; nothing is read.
|
|
58
|
+
* - `refused` — the node carries one the spec refuses; nothing is read.
|
|
59
|
+
* - `loading` — the read is in flight.
|
|
60
|
+
* - `ready` — the read answered and `path` selected a value.
|
|
61
|
+
* - `error` — the read failed, or `path` selected nothing.
|
|
62
|
+
*/
|
|
63
|
+
export type ElementApiDataSourceStatus = 'absent' | 'refused' | 'loading' | 'ready' | 'error';
|
|
64
|
+
export interface UseElementApiDataSourceResult {
|
|
65
|
+
status: ElementApiDataSourceStatus;
|
|
66
|
+
/** The parsed binding, for every status but `absent` and `refused`. */
|
|
67
|
+
config?: ElementApiDataSource;
|
|
68
|
+
/** The selected value, set only for `ready`. */
|
|
69
|
+
data?: unknown;
|
|
70
|
+
/** Author-facing explanation, set for `refused` and `error`. */
|
|
71
|
+
error?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Is this `dataSource` value the API binding — whatever it says beyond
|
|
75
|
+
* `provider`? `provider: 'api'` is the spec's discriminator between the two
|
|
76
|
+
* shapes of `PageComponentSchema.dataSource`; the object binding carries no
|
|
77
|
+
* `provider`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isElementApiDataSourceShape(value: unknown): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Judge a node's `dataSource` as an API binding: `absent` when it is not one,
|
|
82
|
+
* `refused` with the spec's message when the spec refuses it, otherwise the
|
|
83
|
+
* parsed binding.
|
|
84
|
+
*/
|
|
85
|
+
export declare function judgeElementApiDataSource(value: unknown): {
|
|
86
|
+
status: 'absent';
|
|
87
|
+
} | {
|
|
88
|
+
status: 'refused';
|
|
89
|
+
error: string;
|
|
90
|
+
} | {
|
|
91
|
+
status: 'parsed';
|
|
92
|
+
config: ElementApiDataSource;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Select `path` (dotted segments; a numeric segment indexes an array) from a
|
|
96
|
+
* response body. `found: false` when any segment has nothing under it — an
|
|
97
|
+
* absent key, not a key whose value is `undefined`, is what "selects nothing"
|
|
98
|
+
* means.
|
|
99
|
+
*/
|
|
100
|
+
export declare function selectElementApiPath(body: unknown, path: string | undefined): {
|
|
101
|
+
found: true;
|
|
102
|
+
value: unknown;
|
|
103
|
+
} | {
|
|
104
|
+
found: false;
|
|
105
|
+
segment: string;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* `GET url` through `doFetch`, resolving with the parsed JSON body. Rejects on
|
|
109
|
+
* a network failure, a non-2xx answer or a body that is not JSON. Callers that
|
|
110
|
+
* ask while a read of the same URL is pending share it.
|
|
111
|
+
*/
|
|
112
|
+
export declare function readElementApi(url: string, doFetch: ApiFetch): Promise<unknown>;
|
|
113
|
+
/**
|
|
114
|
+
* Resolve a node's API binding: read its endpoint and select its `path`.
|
|
115
|
+
*
|
|
116
|
+
* Called unconditionally by `SchemaRenderer` for every node; a node without an
|
|
117
|
+
* API binding costs one `isPlainObject` test and issues no request.
|
|
118
|
+
*/
|
|
119
|
+
export declare function useElementApiDataSource(schema: unknown): UseElementApiDataSourceResult;
|