@openmrs/esm-react-utils 4.0.0-pre.0 → 4.0.1-pre.206
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/.turbo/turbo-build.log +16 -13
- package/__mocks__/openmrs-esm-state.mock.ts +1 -0
- package/dist/openmrs-esm-react-utils.js +1 -1
- package/dist/openmrs-esm-react-utils.js.LICENSE.txt +0 -9
- package/dist/openmrs-esm-react-utils.js.map +1 -1
- package/jest.config.js +3 -2
- package/package.json +7 -7
- package/src/ConfigurableLink.test.tsx +8 -4
- package/src/ConfigurableLink.tsx +22 -13
- package/src/Extension.tsx +30 -6
- package/src/ExtensionSlot.tsx +108 -68
- package/src/UserHasAccess.tsx +6 -4
- package/src/extensions.test.tsx +276 -0
- package/src/public.ts +1 -2
- package/src/setup-tests.js +2 -0
- package/src/useAssignedExtensionIds.ts +1 -1
- package/src/useAssignedExtensions.ts +1 -1
- package/src/useConfig.test.tsx +8 -7
- package/src/useConfig.ts +1 -1
- package/src/useSession.tsx +2 -2
- package/src/useVisit.ts +20 -7
- package/.turbo/turbo-lint.log +0 -2
- package/.turbo/turbo-test.log +0 -45
package/jest.config.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
transform: {
|
|
3
|
-
"^.+\\.
|
|
3
|
+
"^.+\\.(j|t)sx?$": ["@swc/jest"],
|
|
4
4
|
},
|
|
5
|
-
|
|
5
|
+
setupFilesAfterEnv: ["<rootDir>/src/setup-tests.js"],
|
|
6
6
|
moduleNameMapper: {
|
|
7
7
|
"lodash-es": "lodash",
|
|
8
|
+
"^lodash-es/(.*)$": "lodash/$1",
|
|
8
9
|
"@openmrs/esm-error-handling":
|
|
9
10
|
"<rootDir>/__mocks__/openmrs-esm-error-handling.mock.ts",
|
|
10
11
|
"@openmrs/esm-state": "<rootDir>/__mocks__/openmrs-esm-state.mock.ts",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-react-utils",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1-pre.206",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "React utilities for OpenMRS.",
|
|
6
6
|
"browser": "dist/openmrs-esm-react-utils.js",
|
|
@@ -55,11 +55,11 @@
|
|
|
55
55
|
"react-i18next": "11.x"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@openmrs/esm-api": "^4.0.
|
|
59
|
-
"@openmrs/esm-config": "^4.0.
|
|
60
|
-
"@openmrs/esm-error-handling": "^4.0.
|
|
61
|
-
"@openmrs/esm-extensions": "^4.0.
|
|
62
|
-
"@openmrs/esm-globals": "^4.0.
|
|
58
|
+
"@openmrs/esm-api": "^4.0.1-pre.206",
|
|
59
|
+
"@openmrs/esm-config": "^4.0.1-pre.206",
|
|
60
|
+
"@openmrs/esm-error-handling": "^4.0.1-pre.206",
|
|
61
|
+
"@openmrs/esm-extensions": "^4.0.1-pre.206",
|
|
62
|
+
"@openmrs/esm-globals": "^4.0.1-pre.206",
|
|
63
63
|
"dayjs": "^1.10.8",
|
|
64
64
|
"i18next": "^19.6.0",
|
|
65
65
|
"react": "^18.1.0",
|
|
@@ -68,5 +68,5 @@
|
|
|
68
68
|
"rxjs": "^6.5.3",
|
|
69
69
|
"unistore": "^3.5.2"
|
|
70
70
|
},
|
|
71
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "7ac6e01b41238739cdf5d20efdff50062f46f500"
|
|
72
72
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import "@testing-library/jest-dom/extend-expect";
|
|
3
3
|
import { render, screen } from "@testing-library/react";
|
|
4
|
-
import { navigate, interpolateUrl } from "@openmrs/esm-config";
|
|
5
4
|
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { navigate, interpolateUrl } from "@openmrs/esm-config";
|
|
6
6
|
import { ConfigurableLink } from "./ConfigurableLink";
|
|
7
7
|
|
|
8
8
|
jest.mock("single-spa");
|
|
@@ -37,17 +37,21 @@ describe(`ConfigurableLink`, () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it(`calls navigate on normal click but not special clicks`, async () => {
|
|
40
|
+
const user = userEvent.setup();
|
|
41
|
+
|
|
40
42
|
const link = screen.getByRole("link", { name: /spa home/i });
|
|
41
|
-
await
|
|
43
|
+
await user.pointer({ target: link, keys: "[MouseRight]" });
|
|
42
44
|
expect(navigate).not.toHaveBeenCalled();
|
|
43
|
-
await
|
|
45
|
+
await user.click(link);
|
|
44
46
|
expect(navigate).toHaveBeenCalledWith({ to: path });
|
|
45
47
|
});
|
|
46
48
|
|
|
47
49
|
it(`calls navigate on enter`, async () => {
|
|
50
|
+
const user = userEvent.setup();
|
|
51
|
+
|
|
48
52
|
expect(navigate).not.toHaveBeenCalled();
|
|
49
53
|
const link = screen.getByRole("link", { name: /spa home/i });
|
|
50
|
-
await
|
|
54
|
+
await user.type(link, "{enter}");
|
|
51
55
|
expect(navigate).toHaveBeenCalledWith({ to: path });
|
|
52
56
|
});
|
|
53
57
|
});
|
package/src/ConfigurableLink.tsx
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/** @module @category Navigation */
|
|
2
2
|
import React, { MouseEvent, AnchorHTMLAttributes } from "react";
|
|
3
|
-
import { navigate, interpolateUrl } from "@openmrs/esm-config";
|
|
3
|
+
import { navigate, interpolateUrl, TemplateParams } from "@openmrs/esm-config";
|
|
4
4
|
|
|
5
|
-
function handleClick(
|
|
5
|
+
function handleClick(
|
|
6
|
+
event: MouseEvent,
|
|
7
|
+
to: string,
|
|
8
|
+
templateParams?: TemplateParams
|
|
9
|
+
) {
|
|
6
10
|
if (
|
|
7
11
|
!event.metaKey &&
|
|
8
12
|
!event.ctrlKey &&
|
|
@@ -10,7 +14,7 @@ function handleClick(event: MouseEvent, to: string) {
|
|
|
10
14
|
event.button == 0
|
|
11
15
|
) {
|
|
12
16
|
event.preventDefault();
|
|
13
|
-
navigate({ to });
|
|
17
|
+
navigate({ to, templateParams });
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
20
|
|
|
@@ -20,25 +24,30 @@ function handleClick(event: MouseEvent, to: string) {
|
|
|
20
24
|
export interface ConfigurableLinkProps
|
|
21
25
|
extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
22
26
|
to: string;
|
|
27
|
+
templateParams?: TemplateParams;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
/**
|
|
26
31
|
* A React link component which calls [[navigate]] when clicked
|
|
27
32
|
*
|
|
28
33
|
* @param to The target path or URL. Supports interpolation. See [[navigate]]
|
|
34
|
+
* @param urlParams: A dictionary of values to interpolate into the URL, in addition to the default keys `openmrsBase` and `openmrsSpaBase`.
|
|
29
35
|
* @param children Inline elements within the link
|
|
30
36
|
* @param otherProps Any other valid props for an <a> tag except `href` and `onClick`
|
|
31
37
|
*/
|
|
32
|
-
export
|
|
38
|
+
export function ConfigurableLink({
|
|
33
39
|
to,
|
|
40
|
+
templateParams,
|
|
34
41
|
children,
|
|
35
42
|
...otherProps
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
}: ConfigurableLinkProps) {
|
|
44
|
+
return (
|
|
45
|
+
<a
|
|
46
|
+
onClick={(event) => handleClick(event, to, templateParams)}
|
|
47
|
+
href={interpolateUrl(to, templateParams)}
|
|
48
|
+
{...otherProps}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</a>
|
|
52
|
+
);
|
|
53
|
+
}
|
package/src/Extension.tsx
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { renderExtension } from "@openmrs/esm-extensions";
|
|
2
|
-
import React, {
|
|
2
|
+
import React, {
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { Parcel } from "single-spa";
|
|
3
10
|
import { ComponentContext } from ".";
|
|
4
11
|
import { ExtensionData } from "./ComponentContext";
|
|
5
12
|
|
|
6
13
|
export interface ExtensionProps {
|
|
7
14
|
state?: Record<string, any>;
|
|
15
|
+
/** @deprecated Pass a function as the child of `ExtensionSlot` instead. */
|
|
8
16
|
wrap?(
|
|
9
17
|
slot: React.ReactNode,
|
|
10
18
|
extension: ExtensionData
|
|
@@ -23,14 +31,21 @@ export interface ExtensionProps {
|
|
|
23
31
|
export const Extension: React.FC<ExtensionProps> = ({ state, wrap }) => {
|
|
24
32
|
const [domElement, setDomElement] = useState<HTMLDivElement>();
|
|
25
33
|
const { extension } = useContext(ComponentContext);
|
|
34
|
+
const parcel = useRef<Parcel | null>();
|
|
35
|
+
|
|
36
|
+
if (wrap) {
|
|
37
|
+
console.warn(
|
|
38
|
+
"`wrap` prop of Extension is being used. This will be removed in a future release."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
26
41
|
|
|
27
42
|
const ref = useCallback((node) => {
|
|
28
43
|
setDomElement(node);
|
|
29
44
|
}, []);
|
|
30
45
|
|
|
31
46
|
useEffect(() => {
|
|
32
|
-
if (domElement != null && extension) {
|
|
33
|
-
|
|
47
|
+
if (domElement != null && extension && !parcel.current) {
|
|
48
|
+
parcel.current = renderExtension(
|
|
34
49
|
domElement,
|
|
35
50
|
extension.extensionSlotName,
|
|
36
51
|
extension.extensionSlotModuleName,
|
|
@@ -38,6 +53,9 @@ export const Extension: React.FC<ExtensionProps> = ({ state, wrap }) => {
|
|
|
38
53
|
undefined,
|
|
39
54
|
state
|
|
40
55
|
);
|
|
56
|
+
return () => {
|
|
57
|
+
parcel.current && parcel.current.unmount();
|
|
58
|
+
};
|
|
41
59
|
}
|
|
42
60
|
}, [
|
|
43
61
|
extension?.extensionSlotName,
|
|
@@ -47,9 +65,15 @@ export const Extension: React.FC<ExtensionProps> = ({ state, wrap }) => {
|
|
|
47
65
|
domElement,
|
|
48
66
|
]);
|
|
49
67
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (parcel.current && parcel.current.update) {
|
|
70
|
+
parcel.current.update({ ...state });
|
|
71
|
+
}
|
|
72
|
+
}, [parcel.current, state]);
|
|
73
|
+
|
|
74
|
+
// The extension is rendered into the `<div>`. The `<div>` has relative
|
|
75
|
+
// positioning in order to allow the UI Editor to absolutely position
|
|
76
|
+
// elements within it.
|
|
53
77
|
const slot = (
|
|
54
78
|
<div
|
|
55
79
|
ref={ref}
|
package/src/ExtensionSlot.tsx
CHANGED
|
@@ -4,101 +4,141 @@ import { ComponentContext } from "./ComponentContext";
|
|
|
4
4
|
import { Extension } from "./Extension";
|
|
5
5
|
import { useExtensionSlot } from "./useExtensionSlot";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (prevDeps && !nextDeps) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (typeof prevDeps !== "object" || typeof nextDeps !== "object") {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const prev = Object.keys(prevDeps);
|
|
25
|
-
const next = Object.keys(nextDeps);
|
|
26
|
-
|
|
27
|
-
if (prev.length !== next.length) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
for (let i = 0; i < prev.length; i++) {
|
|
32
|
-
const key = prev[i];
|
|
33
|
-
|
|
34
|
-
if (!(key in nextDeps) || nextDeps[key] !== prevDeps[key]) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return true;
|
|
7
|
+
export interface ExtensionSlotBaseProps {
|
|
8
|
+
name: string;
|
|
9
|
+
/** @deprecated Use `name` */
|
|
10
|
+
extensionSlotName?: string;
|
|
11
|
+
select?: (extensions: Array<ConnectedExtension>) => Array<ConnectedExtension>;
|
|
12
|
+
state?: Record<string, any>;
|
|
40
13
|
}
|
|
41
14
|
|
|
42
|
-
export interface
|
|
15
|
+
export interface OldExtensionSlotBaseProps {
|
|
16
|
+
name?: string;
|
|
17
|
+
/** @deprecated Use `name` */
|
|
43
18
|
extensionSlotName: string;
|
|
44
19
|
select?: (extensions: Array<ConnectedExtension>) => Array<ConnectedExtension>;
|
|
45
20
|
state?: Record<string, any>;
|
|
46
21
|
}
|
|
47
22
|
|
|
48
|
-
export type ExtensionSlotProps =
|
|
49
|
-
|
|
23
|
+
export type ExtensionSlotProps = (
|
|
24
|
+
| OldExtensionSlotBaseProps
|
|
25
|
+
| ExtensionSlotBaseProps
|
|
26
|
+
) &
|
|
27
|
+
Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
|
|
28
|
+
children?:
|
|
29
|
+
| React.ReactNode
|
|
30
|
+
| ((extension: ConnectedExtension) => React.ReactNode);
|
|
31
|
+
};
|
|
50
32
|
|
|
51
33
|
function defaultSelect(extensions: Array<ConnectedExtension>) {
|
|
52
34
|
return extensions;
|
|
53
35
|
}
|
|
54
36
|
|
|
55
|
-
|
|
56
|
-
|
|
37
|
+
/**
|
|
38
|
+
* An [extension slot](https://o3-dev.docs.openmrs.org/#/main/extensions).
|
|
39
|
+
* A place with a name. Extensions that get connected to that name
|
|
40
|
+
* will be rendered into this.
|
|
41
|
+
*
|
|
42
|
+
* @param props.name The name of the extension slot
|
|
43
|
+
* @param props.select An optional function for filtering or otherwise modifying
|
|
44
|
+
* the list of extensions that will be rendered.
|
|
45
|
+
* @param props.state *Only works if no children are provided*. Passes data
|
|
46
|
+
* through as props to the extensions that are mounted here. If `ExtensionSlot`
|
|
47
|
+
* has children, you must pass the state through the `state` param of the
|
|
48
|
+
* `Extension` component.
|
|
49
|
+
* @param props.children There are two different ways to use `ExtensionSlot`
|
|
50
|
+
* children.
|
|
51
|
+
* - Passing a `ReactNode`, the "normal" way. The child must contain the component
|
|
52
|
+
* `Extension`. Whatever is passed as the child will be rendered once per extension.
|
|
53
|
+
* See the first example below.
|
|
54
|
+
* - Passing a function, the "render props" way. The child must be a function
|
|
55
|
+
* which takes a [[ConnectedExtension]] as argument and returns a `ReactNode`.
|
|
56
|
+
* the resulting react node must contain the component `Extension`. It will
|
|
57
|
+
* be run for each extension. See the second example below.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* Passing a react node as children
|
|
61
|
+
*
|
|
62
|
+
* ```tsx
|
|
63
|
+
* <ExtensionSlot name="Foo">
|
|
64
|
+
* <div style={{ width: 10rem }}>
|
|
65
|
+
* <Extension />
|
|
66
|
+
* </div>
|
|
67
|
+
* </ExtensionSlot>
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* Passing a function as children
|
|
72
|
+
*
|
|
73
|
+
* ```tsx
|
|
74
|
+
* <ExtensionSlot name="Bar">
|
|
75
|
+
* {(extension) => (
|
|
76
|
+
* <h1>{extension.name}</h1>
|
|
77
|
+
* <div style={{ color: extension.meta.color }}>
|
|
78
|
+
* <Extension />
|
|
79
|
+
* </div>
|
|
80
|
+
* )}
|
|
81
|
+
* </ExtensionSlot>
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
*/
|
|
85
|
+
export function ExtensionSlot({
|
|
86
|
+
name: extensionSlotName,
|
|
87
|
+
extensionSlotName: legacyExtensionSlotName,
|
|
57
88
|
select = defaultSelect,
|
|
58
89
|
children,
|
|
59
90
|
state,
|
|
60
91
|
style,
|
|
61
92
|
...divProps
|
|
62
|
-
}: ExtensionSlotProps)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!isShallowEqual(stateRef.current, state)) {
|
|
69
|
-
stateRef.current = state;
|
|
93
|
+
}: ExtensionSlotProps) {
|
|
94
|
+
if (children && state) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"Both children and state have been provided. If children are provided, the state must be passed as a prop to the `Extension` component."
|
|
97
|
+
);
|
|
70
98
|
}
|
|
71
99
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
moduleName: extensionSlotModuleName, // moduleName is not used by the receiving Extension
|
|
80
|
-
extension: {
|
|
81
|
-
extensionId: extension.id,
|
|
82
|
-
extensionSlotName,
|
|
83
|
-
extensionSlotModuleName,
|
|
84
|
-
},
|
|
85
|
-
}}
|
|
86
|
-
>
|
|
87
|
-
{children ?? <Extension state={stateRef.current} />}
|
|
88
|
-
</ComponentContext.Provider>
|
|
89
|
-
)),
|
|
90
|
-
[select, extensions, extensionSlotName, stateRef.current]
|
|
100
|
+
const name = (extensionSlotName ?? legacyExtensionSlotName) as string;
|
|
101
|
+
const slotRef = useRef(null);
|
|
102
|
+
const { extensions, extensionSlotModuleName } = useExtensionSlot(name);
|
|
103
|
+
|
|
104
|
+
const extensionsToRender = useMemo(
|
|
105
|
+
() => select(extensions),
|
|
106
|
+
[select, extensions]
|
|
91
107
|
);
|
|
92
108
|
|
|
109
|
+
const extensionsFromChildrenFunction = useMemo(() => {
|
|
110
|
+
if (typeof children == "function" && !React.isValidElement(children)) {
|
|
111
|
+
return extensionsToRender.map((extension) => children(extension));
|
|
112
|
+
}
|
|
113
|
+
}, [children, extensionsToRender]);
|
|
114
|
+
|
|
93
115
|
return (
|
|
94
116
|
<div
|
|
95
117
|
ref={slotRef}
|
|
96
|
-
data-extension-slot-name={
|
|
118
|
+
data-extension-slot-name={name}
|
|
97
119
|
data-extension-slot-module-name={extensionSlotModuleName}
|
|
98
120
|
style={{ ...style, position: "relative" }}
|
|
99
121
|
{...divProps}
|
|
100
122
|
>
|
|
101
|
-
{
|
|
123
|
+
{name &&
|
|
124
|
+
extensionsToRender.map((extension, i) => (
|
|
125
|
+
<ComponentContext.Provider
|
|
126
|
+
key={extension.id}
|
|
127
|
+
value={{
|
|
128
|
+
moduleName: extensionSlotModuleName, // moduleName is not used by the receiving Extension
|
|
129
|
+
extension: {
|
|
130
|
+
extensionId: extension.id,
|
|
131
|
+
extensionSlotName: name,
|
|
132
|
+
extensionSlotModuleName,
|
|
133
|
+
},
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{extensionsFromChildrenFunction?.[i] ??
|
|
137
|
+
(typeof children != "function" ? children : null) ?? (
|
|
138
|
+
<Extension state={state} />
|
|
139
|
+
)}
|
|
140
|
+
</ComponentContext.Provider>
|
|
141
|
+
))}
|
|
102
142
|
</div>
|
|
103
143
|
);
|
|
104
|
-
}
|
|
144
|
+
}
|
package/src/UserHasAccess.tsx
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/** @module @category API */
|
|
2
|
+
import { getCurrentUser, LoggedInUser, userHasAccess } from "@openmrs/esm-api";
|
|
2
3
|
import React, { useEffect, useState } from "react";
|
|
3
|
-
import { getCurrentUser, userHasAccess, LoggedInUser } from "@openmrs/esm-api";
|
|
4
4
|
|
|
5
5
|
export interface UserHasAccessProps {
|
|
6
|
-
privilege: string;
|
|
6
|
+
privilege: string | string[];
|
|
7
|
+
fallback?: React.ReactNode;
|
|
7
8
|
children?: React.ReactNode;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export const UserHasAccess: React.FC<UserHasAccessProps> = ({
|
|
11
12
|
privilege,
|
|
13
|
+
fallback,
|
|
12
14
|
children,
|
|
13
15
|
}) => {
|
|
14
16
|
const [user, setUser] = useState<LoggedInUser | null>(null);
|
|
@@ -22,7 +24,7 @@ export const UserHasAccess: React.FC<UserHasAccessProps> = ({
|
|
|
22
24
|
|
|
23
25
|
if (user && userHasAccess(privilege, user)) {
|
|
24
26
|
return <>{children}</>;
|
|
27
|
+
} else {
|
|
28
|
+
return fallback ? <>{fallback}</> : null;
|
|
25
29
|
}
|
|
26
|
-
|
|
27
|
-
return null;
|
|
28
30
|
};
|