@openmrs/esm-react-utils 3.4.1-pre.151 → 3.4.1-pre.161
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 +13 -10
- 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/Extension.tsx +30 -6
- package/src/ExtensionSlot.tsx +92 -67
- package/src/extensions.test.tsx +272 -0
- package/src/public.ts +0 -1
- package/src/setup-tests.js +2 -0
- package/src/useAssignedExtensionIds.ts +1 -1
- package/src/useAssignedExtensions.ts +1 -1
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": "3.4.1-pre.
|
|
3
|
+
"version": "3.4.1-pre.161",
|
|
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": "^3.4.1-pre.
|
|
59
|
-
"@openmrs/esm-config": "^3.4.1-pre.
|
|
60
|
-
"@openmrs/esm-error-handling": "^3.4.1-pre.
|
|
61
|
-
"@openmrs/esm-extensions": "^3.4.1-pre.
|
|
62
|
-
"@openmrs/esm-globals": "^3.4.1-pre.
|
|
58
|
+
"@openmrs/esm-api": "^3.4.1-pre.161",
|
|
59
|
+
"@openmrs/esm-config": "^3.4.1-pre.161",
|
|
60
|
+
"@openmrs/esm-error-handling": "^3.4.1-pre.161",
|
|
61
|
+
"@openmrs/esm-extensions": "^3.4.1-pre.161",
|
|
62
|
+
"@openmrs/esm-globals": "^3.4.1-pre.161",
|
|
63
63
|
"dayjs": "^1.10.8",
|
|
64
64
|
"i18next": "^19.6.0",
|
|
65
65
|
"react": "^16.13.1",
|
|
@@ -68,5 +68,5 @@
|
|
|
68
68
|
"rxjs": "^6.5.3",
|
|
69
69
|
"unistore": "^3.5.2"
|
|
70
70
|
},
|
|
71
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "6db8e72832eb5cd8ab262dbaab7e8eaf841f92fd"
|
|
72
72
|
}
|
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,41 +4,6 @@ import { ComponentContext } from "./ComponentContext";
|
|
|
4
4
|
import { Extension } from "./Extension";
|
|
5
5
|
import { useExtensionSlot } from "./useExtensionSlot";
|
|
6
6
|
|
|
7
|
-
function isShallowEqual(prevDeps: any, nextDeps: any) {
|
|
8
|
-
if (prevDeps === nextDeps) {
|
|
9
|
-
return true;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (!prevDeps && nextDeps) {
|
|
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;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
7
|
export interface ExtensionSlotBaseProps {
|
|
43
8
|
name: string;
|
|
44
9
|
/** @deprecated Use `name` */
|
|
@@ -59,51 +24,94 @@ export type ExtensionSlotProps = (
|
|
|
59
24
|
| OldExtensionSlotBaseProps
|
|
60
25
|
| ExtensionSlotBaseProps
|
|
61
26
|
) &
|
|
62
|
-
React.HTMLAttributes<HTMLDivElement
|
|
27
|
+
React.HTMLAttributes<HTMLDivElement> & {
|
|
28
|
+
children?:
|
|
29
|
+
| React.ReactNode
|
|
30
|
+
| ((extension: ConnectedExtension) => React.ReactNode);
|
|
31
|
+
};
|
|
63
32
|
|
|
64
33
|
function defaultSelect(extensions: Array<ConnectedExtension>) {
|
|
65
34
|
return extensions;
|
|
66
35
|
}
|
|
67
36
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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,
|
|
71
88
|
select = defaultSelect,
|
|
72
89
|
children,
|
|
73
90
|
state,
|
|
74
91
|
style,
|
|
75
92
|
...divProps
|
|
76
|
-
}: ExtensionSlotProps)
|
|
77
|
-
|
|
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
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const name = (extensionSlotName ?? legacyExtensionSlotName) as string;
|
|
78
101
|
const slotRef = useRef(null);
|
|
79
102
|
const { extensions, extensionSlotModuleName } = useExtensionSlot(name);
|
|
80
|
-
const stateRef = useRef(state);
|
|
81
|
-
|
|
82
|
-
if (!isShallowEqual(stateRef.current, state)) {
|
|
83
|
-
stateRef.current = state;
|
|
84
|
-
}
|
|
85
103
|
|
|
86
|
-
const
|
|
87
|
-
() =>
|
|
88
|
-
|
|
89
|
-
select(extensions).map((extension) => (
|
|
90
|
-
<ComponentContext.Provider
|
|
91
|
-
key={extension.id}
|
|
92
|
-
value={{
|
|
93
|
-
moduleName: extensionSlotModuleName, // moduleName is not used by the receiving Extension
|
|
94
|
-
extension: {
|
|
95
|
-
extensionId: extension.id,
|
|
96
|
-
extensionSlotName: name,
|
|
97
|
-
extensionSlotModuleName,
|
|
98
|
-
},
|
|
99
|
-
}}
|
|
100
|
-
>
|
|
101
|
-
{children ?? <Extension state={stateRef.current} />}
|
|
102
|
-
</ComponentContext.Provider>
|
|
103
|
-
)),
|
|
104
|
-
[select, extensions, name, stateRef.current]
|
|
104
|
+
const extensionsToRender = useMemo(
|
|
105
|
+
() => select(extensions),
|
|
106
|
+
[select, extensions]
|
|
105
107
|
);
|
|
106
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
|
+
|
|
107
115
|
return (
|
|
108
116
|
<div
|
|
109
117
|
ref={slotRef}
|
|
@@ -112,7 +120,24 @@ export const ExtensionSlot: React.FC<ExtensionSlotProps> = ({
|
|
|
112
120
|
style={{ ...style, position: "relative" }}
|
|
113
121
|
{...divProps}
|
|
114
122
|
>
|
|
115
|
-
{
|
|
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] ?? children ?? (
|
|
137
|
+
<Extension state={state} />
|
|
138
|
+
)}
|
|
139
|
+
</ComponentContext.Provider>
|
|
140
|
+
))}
|
|
116
141
|
</div>
|
|
117
142
|
);
|
|
118
|
-
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import React, { useCallback, useReducer } from "react";
|
|
2
|
+
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
3
|
+
import {
|
|
4
|
+
attach,
|
|
5
|
+
ConnectedExtension,
|
|
6
|
+
getExtensionNameFromId,
|
|
7
|
+
registerExtension,
|
|
8
|
+
updateInternalExtensionStore,
|
|
9
|
+
} from "@openmrs/esm-extensions";
|
|
10
|
+
import {
|
|
11
|
+
getSyncLifecycle,
|
|
12
|
+
Extension,
|
|
13
|
+
ExtensionSlot,
|
|
14
|
+
openmrsComponentDecorator,
|
|
15
|
+
useExtensionSlotMeta,
|
|
16
|
+
ExtensionData,
|
|
17
|
+
} from ".";
|
|
18
|
+
import userEvent from "@testing-library/user-event";
|
|
19
|
+
|
|
20
|
+
// For some reason in the text context `isEqual` always returns true
|
|
21
|
+
// when using the import substitution in jest.config.js. Here's a custom
|
|
22
|
+
// mock.
|
|
23
|
+
jest.mock(
|
|
24
|
+
"lodash-es/isEqual",
|
|
25
|
+
() => (a, b) => JSON.stringify(a) == JSON.stringify(b)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
describe("ExtensionSlot, Extension, and useExtensionSlotMeta", () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
updateInternalExtensionStore(() => ({ slots: {}, extensions: {} }));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("Extension receives state changes passed through (not using <Extension>)", async () => {
|
|
34
|
+
function EnglishExtension({ suffix }) {
|
|
35
|
+
return <div>English{suffix}</div>;
|
|
36
|
+
}
|
|
37
|
+
registerSimpleExtension("English", "esm-languages-app", EnglishExtension);
|
|
38
|
+
attach("Box", "English");
|
|
39
|
+
const App = openmrsComponentDecorator({
|
|
40
|
+
moduleName: "esm-languages-app",
|
|
41
|
+
featureName: "Languages",
|
|
42
|
+
disableTranslations: true,
|
|
43
|
+
})(() => {
|
|
44
|
+
const [suffix, toggleSuffix] = useReducer(
|
|
45
|
+
(suffix) => (suffix == "!" ? "?" : "!"),
|
|
46
|
+
"!"
|
|
47
|
+
);
|
|
48
|
+
return (
|
|
49
|
+
<div>
|
|
50
|
+
<ExtensionSlot name="Box" state={{ suffix }} />
|
|
51
|
+
<button onClick={toggleSuffix}>Toggle suffix</button>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
render(<App />);
|
|
56
|
+
|
|
57
|
+
await waitFor(() =>
|
|
58
|
+
expect(screen.getByText(/English/)).toBeInTheDocument()
|
|
59
|
+
);
|
|
60
|
+
expect(screen.getByText(/English/)).toHaveTextContent("English!");
|
|
61
|
+
userEvent.click(screen.getByText("Toggle suffix"));
|
|
62
|
+
await waitFor(() =>
|
|
63
|
+
expect(screen.getByText(/English/)).toHaveTextContent("English?")
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("Extension receives state changes (using <Extension>)", async () => {
|
|
68
|
+
function HaitianCreoleExtension({ suffix }) {
|
|
69
|
+
return <div>Haitian Creole{suffix}</div>;
|
|
70
|
+
}
|
|
71
|
+
registerSimpleExtension(
|
|
72
|
+
"Haitian",
|
|
73
|
+
"esm-languages-app",
|
|
74
|
+
HaitianCreoleExtension
|
|
75
|
+
);
|
|
76
|
+
attach("Box", "Haitian");
|
|
77
|
+
const App = openmrsComponentDecorator({
|
|
78
|
+
moduleName: "esm-languages-app",
|
|
79
|
+
featureName: "Languages",
|
|
80
|
+
disableTranslations: true,
|
|
81
|
+
})(() => {
|
|
82
|
+
const [suffix, toggleSuffix] = useReducer(
|
|
83
|
+
(suffix) => (suffix == "!" ? "?" : "!"),
|
|
84
|
+
"!"
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div>
|
|
89
|
+
<ExtensionSlot name="Box">
|
|
90
|
+
{suffix}
|
|
91
|
+
<Extension state={{ suffix }} />
|
|
92
|
+
</ExtensionSlot>
|
|
93
|
+
<button onClick={toggleSuffix}>Toggle suffix</button>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
render(<App />);
|
|
98
|
+
|
|
99
|
+
await waitFor(() =>
|
|
100
|
+
expect(screen.getByText(/Haitian/)).toBeInTheDocument()
|
|
101
|
+
);
|
|
102
|
+
expect(screen.getByText(/Haitian/)).toHaveTextContent("Haitian Creole!");
|
|
103
|
+
userEvent.click(screen.getByText("Toggle suffix"));
|
|
104
|
+
await waitFor(() =>
|
|
105
|
+
expect(screen.getByText(/Haitian/)).toHaveTextContent("Haitian Creole?")
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("ExtensionSlot throws error if both state and children provided", () => {
|
|
110
|
+
const App = () => (
|
|
111
|
+
<ExtensionSlot name="Box" state={{ color: "red" }}>
|
|
112
|
+
<Extension />
|
|
113
|
+
</ExtensionSlot>
|
|
114
|
+
);
|
|
115
|
+
expect(() => render(<App />)).toThrowError(
|
|
116
|
+
expect.objectContaining({
|
|
117
|
+
message: expect.stringMatching(/children.*state/),
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("Extension Slot receives meta", async () => {
|
|
123
|
+
registerSimpleExtension("Spanish", "esm-languages-app", undefined, {
|
|
124
|
+
code: "es",
|
|
125
|
+
});
|
|
126
|
+
attach("Box", "Spanish");
|
|
127
|
+
const App = openmrsComponentDecorator({
|
|
128
|
+
moduleName: "esm-languages-app",
|
|
129
|
+
featureName: "Languages",
|
|
130
|
+
disableTranslations: true,
|
|
131
|
+
})(() => {
|
|
132
|
+
const metas = useExtensionSlotMeta("Box");
|
|
133
|
+
const wrapItem = useCallback(
|
|
134
|
+
(slot: React.ReactNode, extension: ExtensionData) => {
|
|
135
|
+
return (
|
|
136
|
+
<div>
|
|
137
|
+
<h1>
|
|
138
|
+
{metas[getExtensionNameFromId(extension.extensionId)].code}
|
|
139
|
+
</h1>
|
|
140
|
+
{slot}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
[metas]
|
|
145
|
+
);
|
|
146
|
+
return (
|
|
147
|
+
<div>
|
|
148
|
+
<ExtensionSlot name="Box">
|
|
149
|
+
<Extension wrap={wrapItem} />
|
|
150
|
+
</ExtensionSlot>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
render(<App />);
|
|
155
|
+
|
|
156
|
+
await waitFor(() =>
|
|
157
|
+
expect(screen.getByRole("heading")).toBeInTheDocument()
|
|
158
|
+
);
|
|
159
|
+
expect(screen.getByRole("heading")).toHaveTextContent("es");
|
|
160
|
+
expect(screen.getByText("Spanish")).toBeInTheDocument();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("Both meta and state can be used at the same time", async () => {
|
|
164
|
+
function SwahiliExtension({ suffix }) {
|
|
165
|
+
return <div>Swahili{suffix}</div>;
|
|
166
|
+
}
|
|
167
|
+
registerSimpleExtension("Swahili", "esm-languages-app", SwahiliExtension, {
|
|
168
|
+
code: "sw",
|
|
169
|
+
});
|
|
170
|
+
attach("Box", "Swahili");
|
|
171
|
+
const App = openmrsComponentDecorator({
|
|
172
|
+
moduleName: "esm-languages-app",
|
|
173
|
+
featureName: "Languages",
|
|
174
|
+
disableTranslations: true,
|
|
175
|
+
})(() => {
|
|
176
|
+
const [suffix, toggleSuffix] = useReducer(
|
|
177
|
+
(suffix) => (suffix == "!" ? "?" : "!"),
|
|
178
|
+
"!"
|
|
179
|
+
);
|
|
180
|
+
const metas = useExtensionSlotMeta("Box");
|
|
181
|
+
const wrapItem = useCallback(
|
|
182
|
+
(slot: React.ReactNode, extension: ExtensionData) => {
|
|
183
|
+
return (
|
|
184
|
+
<div>
|
|
185
|
+
<h1>
|
|
186
|
+
{metas[getExtensionNameFromId(extension.extensionId)].code}
|
|
187
|
+
</h1>
|
|
188
|
+
{slot}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
[metas]
|
|
193
|
+
);
|
|
194
|
+
return (
|
|
195
|
+
<div>
|
|
196
|
+
<ExtensionSlot name="Box">
|
|
197
|
+
<Extension wrap={wrapItem} state={{ suffix }} />
|
|
198
|
+
</ExtensionSlot>
|
|
199
|
+
<button onClick={toggleSuffix}>Toggle suffix</button>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
render(<App />);
|
|
204
|
+
|
|
205
|
+
await waitFor(() =>
|
|
206
|
+
expect(screen.getByRole("heading")).toBeInTheDocument()
|
|
207
|
+
);
|
|
208
|
+
expect(screen.getByRole("heading")).toHaveTextContent("sw");
|
|
209
|
+
expect(screen.getByText(/Swahili/)).toHaveTextContent("Swahili!");
|
|
210
|
+
userEvent.click(screen.getByText("Toggle suffix"));
|
|
211
|
+
await waitFor(() =>
|
|
212
|
+
expect(screen.getByText(/Swahili/)).toHaveTextContent("Swahili?")
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("Extension Slot renders function children", async () => {
|
|
217
|
+
registerSimpleExtension("Urdu", "esm-languages-app", undefined, {
|
|
218
|
+
code: "urd",
|
|
219
|
+
});
|
|
220
|
+
registerSimpleExtension("Hindi", "esm-languages-app", undefined, {
|
|
221
|
+
code: "hi",
|
|
222
|
+
});
|
|
223
|
+
attach("Box", "Urdu");
|
|
224
|
+
attach("Box", "Hindi");
|
|
225
|
+
const App = openmrsComponentDecorator({
|
|
226
|
+
moduleName: "esm-languages-app",
|
|
227
|
+
featureName: "Languages",
|
|
228
|
+
disableTranslations: true,
|
|
229
|
+
})(() => {
|
|
230
|
+
return (
|
|
231
|
+
<div>
|
|
232
|
+
<ExtensionSlot name="Box">
|
|
233
|
+
{(extension: ConnectedExtension) => (
|
|
234
|
+
<div data-testid={extension.name}>
|
|
235
|
+
<h2>{extension.meta.code}</h2>
|
|
236
|
+
<Extension />
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</ExtensionSlot>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
render(<App />);
|
|
244
|
+
|
|
245
|
+
await waitFor(() => expect(screen.getByTestId("Urdu")).toBeInTheDocument());
|
|
246
|
+
expect(
|
|
247
|
+
within(screen.getByTestId("Urdu")).getByRole("heading")
|
|
248
|
+
).toHaveTextContent("urd");
|
|
249
|
+
expect(
|
|
250
|
+
within(screen.getByTestId("Hindi")).getByRole("heading")
|
|
251
|
+
).toHaveTextContent("hi");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
function registerSimpleExtension(
|
|
256
|
+
name: string,
|
|
257
|
+
moduleName: string,
|
|
258
|
+
Component?: React.ComponentType<any>,
|
|
259
|
+
meta: object = {}
|
|
260
|
+
) {
|
|
261
|
+
const SimpleComponent = () => <div>{name}</div>;
|
|
262
|
+
registerExtension({
|
|
263
|
+
name,
|
|
264
|
+
moduleName,
|
|
265
|
+
load: getSyncLifecycle(Component ?? SimpleComponent, {
|
|
266
|
+
moduleName,
|
|
267
|
+
featureName: moduleName,
|
|
268
|
+
disableTranslations: true,
|
|
269
|
+
}),
|
|
270
|
+
meta,
|
|
271
|
+
});
|
|
272
|
+
}
|
package/src/public.ts
CHANGED
|
@@ -12,7 +12,6 @@ export * from "./useConnectedExtensions";
|
|
|
12
12
|
export * from "./useConnectivity";
|
|
13
13
|
export * from "./usePatient";
|
|
14
14
|
export * from "./useCurrentPatient";
|
|
15
|
-
export * from "./useExtensionSlot";
|
|
16
15
|
export * from "./useExtensionSlotMeta";
|
|
17
16
|
export * from "./useExtensionStore";
|
|
18
17
|
export * from "./useLayoutType";
|
package/src/setup-tests.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** @module @category Extension */
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { getExtensionStore } from "@openmrs/esm-extensions";
|
|
4
|
-
import
|
|
4
|
+
import isEqual from "lodash-es/isEqual";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Gets the assigned extension ids for a given extension slot name.
|