@openmrs/esm-react-utils 3.4.1-pre.98 → 4.0.1-pre.204
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 +7 -2
- package/package.json +18 -18
- package/src/ConfigurableLink.test.tsx +11 -5
- package/src/ConfigurableLink.tsx +22 -13
- package/src/Extension.tsx +30 -6
- package/src/ExtensionSlot.tsx +108 -68
- package/src/UserHasAccess.tsx +7 -4
- package/src/extensions.test.tsx +276 -0
- package/src/openmrsComponentDecorator.tsx +2 -1
- 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/useLayoutType.ts +8 -2
|
@@ -0,0 +1,276 @@
|
|
|
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
|
+
await waitFor(() =>
|
|
161
|
+
expect(screen.getByText("Spanish")).toBeInTheDocument()
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("Both meta and state can be used at the same time", async () => {
|
|
166
|
+
function SwahiliExtension({ suffix }) {
|
|
167
|
+
return <div>Swahili{suffix}</div>;
|
|
168
|
+
}
|
|
169
|
+
registerSimpleExtension("Swahili", "esm-languages-app", SwahiliExtension, {
|
|
170
|
+
code: "sw",
|
|
171
|
+
});
|
|
172
|
+
attach("Box", "Swahili");
|
|
173
|
+
const App = openmrsComponentDecorator({
|
|
174
|
+
moduleName: "esm-languages-app",
|
|
175
|
+
featureName: "Languages",
|
|
176
|
+
disableTranslations: true,
|
|
177
|
+
})(() => {
|
|
178
|
+
const [suffix, toggleSuffix] = useReducer(
|
|
179
|
+
(suffix) => (suffix == "!" ? "?" : "!"),
|
|
180
|
+
"!"
|
|
181
|
+
);
|
|
182
|
+
const metas = useExtensionSlotMeta("Box");
|
|
183
|
+
const wrapItem = useCallback(
|
|
184
|
+
(slot: React.ReactNode, extension: ExtensionData) => {
|
|
185
|
+
return (
|
|
186
|
+
<div>
|
|
187
|
+
<h1>
|
|
188
|
+
{metas[getExtensionNameFromId(extension.extensionId)].code}
|
|
189
|
+
</h1>
|
|
190
|
+
{slot}
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
},
|
|
194
|
+
[metas]
|
|
195
|
+
);
|
|
196
|
+
return (
|
|
197
|
+
<div>
|
|
198
|
+
<ExtensionSlot name="Box">
|
|
199
|
+
<Extension wrap={wrapItem} state={{ suffix }} />
|
|
200
|
+
</ExtensionSlot>
|
|
201
|
+
<button onClick={toggleSuffix}>Toggle suffix</button>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
render(<App />);
|
|
206
|
+
|
|
207
|
+
await waitFor(() =>
|
|
208
|
+
expect(screen.getByRole("heading")).toBeInTheDocument()
|
|
209
|
+
);
|
|
210
|
+
expect(screen.getByRole("heading")).toHaveTextContent("sw");
|
|
211
|
+
await waitFor(() =>
|
|
212
|
+
expect(screen.getByText(/Swahili/)).toHaveTextContent("Swahili!")
|
|
213
|
+
);
|
|
214
|
+
userEvent.click(screen.getByText("Toggle suffix"));
|
|
215
|
+
await waitFor(() =>
|
|
216
|
+
expect(screen.getByText(/Swahili/)).toHaveTextContent("Swahili?")
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("Extension Slot renders function children", async () => {
|
|
221
|
+
registerSimpleExtension("Urdu", "esm-languages-app", undefined, {
|
|
222
|
+
code: "urd",
|
|
223
|
+
});
|
|
224
|
+
registerSimpleExtension("Hindi", "esm-languages-app", undefined, {
|
|
225
|
+
code: "hi",
|
|
226
|
+
});
|
|
227
|
+
attach("Box", "Urdu");
|
|
228
|
+
attach("Box", "Hindi");
|
|
229
|
+
const App = openmrsComponentDecorator({
|
|
230
|
+
moduleName: "esm-languages-app",
|
|
231
|
+
featureName: "Languages",
|
|
232
|
+
disableTranslations: true,
|
|
233
|
+
})(() => {
|
|
234
|
+
return (
|
|
235
|
+
<div>
|
|
236
|
+
<ExtensionSlot name="Box">
|
|
237
|
+
{(extension: ConnectedExtension) => (
|
|
238
|
+
<div data-testid={extension.name}>
|
|
239
|
+
<h2>{extension.meta.code}</h2>
|
|
240
|
+
<Extension />
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
</ExtensionSlot>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
render(<App />);
|
|
248
|
+
|
|
249
|
+
await waitFor(() => expect(screen.getByTestId("Urdu")).toBeInTheDocument());
|
|
250
|
+
expect(
|
|
251
|
+
within(screen.getByTestId("Urdu")).getByRole("heading")
|
|
252
|
+
).toHaveTextContent("urd");
|
|
253
|
+
expect(
|
|
254
|
+
within(screen.getByTestId("Hindi")).getByRole("heading")
|
|
255
|
+
).toHaveTextContent("hi");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
function registerSimpleExtension(
|
|
260
|
+
name: string,
|
|
261
|
+
moduleName: string,
|
|
262
|
+
Component?: React.ComponentType<any>,
|
|
263
|
+
meta: object = {}
|
|
264
|
+
) {
|
|
265
|
+
const SimpleComponent = () => <div>{name}</div>;
|
|
266
|
+
registerExtension({
|
|
267
|
+
name,
|
|
268
|
+
moduleName,
|
|
269
|
+
load: getSyncLifecycle(Component ?? SimpleComponent, {
|
|
270
|
+
moduleName,
|
|
271
|
+
featureName: moduleName,
|
|
272
|
+
disableTranslations: true,
|
|
273
|
+
}),
|
|
274
|
+
meta,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
@@ -18,6 +18,7 @@ const defaultOpts = {
|
|
|
18
18
|
interface I18nextLoadNamespaceProps {
|
|
19
19
|
forceUpdate(): void;
|
|
20
20
|
ns: string;
|
|
21
|
+
children?: React.ReactNode;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const I18nextLoadNamespace: React.FC<I18nextLoadNamespaceProps> = (props) => {
|
|
@@ -80,7 +81,7 @@ export function openmrsComponentDecorator(userOpts: ComponentDecoratorOptions) {
|
|
|
80
81
|
const opts = Object.assign({}, defaultOpts, userOpts);
|
|
81
82
|
|
|
82
83
|
return function decorateComponent(
|
|
83
|
-
Comp: React.ComponentType
|
|
84
|
+
Comp: React.ComponentType<any>
|
|
84
85
|
): React.ComponentType<any> {
|
|
85
86
|
return class OpenmrsReactComponent extends React.Component<
|
|
86
87
|
OpenmrsReactComponentProps,
|
package/src/public.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ExtensionData } from "./ComponentContext";
|
|
1
|
+
export { type ExtensionData } from "./ComponentContext";
|
|
2
2
|
export * from "./ConfigurableLink";
|
|
3
3
|
export * from "./createUseStore";
|
|
4
4
|
export * from "./Extension";
|
|
@@ -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.
|
package/src/useConfig.test.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { render, cleanup, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { render, cleanup, screen, waitFor, act } from "@testing-library/react";
|
|
3
3
|
import {
|
|
4
4
|
defineConfigSchema,
|
|
5
5
|
temporaryConfigStore,
|
|
@@ -26,7 +26,6 @@ function clearConfig() {
|
|
|
26
26
|
|
|
27
27
|
describe(`useConfig in root context`, () => {
|
|
28
28
|
afterEach(clearConfig);
|
|
29
|
-
afterEach(cleanup);
|
|
30
29
|
|
|
31
30
|
it(`can return config as a react hook`, async () => {
|
|
32
31
|
defineConfigSchema("foo-module", {
|
|
@@ -103,9 +102,11 @@ describe(`useConfig in root context`, () => {
|
|
|
103
102
|
expect(screen.findByText("The first thing")).toBeTruthy()
|
|
104
103
|
);
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
act(() =>
|
|
106
|
+
temporaryConfigStore.setState({
|
|
107
|
+
config: { "foo-module": { thing: "A new thing" } },
|
|
108
|
+
})
|
|
109
|
+
);
|
|
109
110
|
|
|
110
111
|
await waitFor(() => expect(screen.findByText("A new thing")).toBeTruthy());
|
|
111
112
|
});
|
|
@@ -273,7 +274,7 @@ describe(`useConfig in an extension`, () => {
|
|
|
273
274
|
);
|
|
274
275
|
|
|
275
276
|
const newConfig = { "ext-module": { thing: "A new thing" } };
|
|
276
|
-
temporaryConfigStore.setState({ config: newConfig });
|
|
277
|
+
act(() => temporaryConfigStore.setState({ config: newConfig }));
|
|
277
278
|
|
|
278
279
|
await waitFor(() => expect(screen.findByText("A new thing")).toBeTruthy());
|
|
279
280
|
|
|
@@ -290,7 +291,7 @@ describe(`useConfig in an extension`, () => {
|
|
|
290
291
|
},
|
|
291
292
|
},
|
|
292
293
|
};
|
|
293
|
-
temporaryConfigStore.setState({ config: newConfig2 });
|
|
294
|
+
act(() => temporaryConfigStore.setState({ config: newConfig2 }));
|
|
294
295
|
|
|
295
296
|
await waitFor(() =>
|
|
296
297
|
expect(screen.findByText("Yet another thing")).toBeTruthy()
|
package/src/useConfig.ts
CHANGED
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
getConfigStore,
|
|
5
5
|
getExtensionsConfigStore,
|
|
6
6
|
ConfigStore,
|
|
7
|
+
ConfigObject,
|
|
7
8
|
ExtensionsConfigStore,
|
|
8
9
|
getExtensionConfigFromStore,
|
|
9
10
|
} from "@openmrs/esm-config";
|
|
10
11
|
import { ComponentContext, ExtensionData } from "./ComponentContext";
|
|
11
|
-
import { ConfigObject } from "@openmrs/esm-config";
|
|
12
12
|
import { Store } from "unistore";
|
|
13
13
|
import isEqual from "lodash-es/isEqual";
|
|
14
14
|
|
package/src/useLayoutType.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** @module @category UI */
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
|
|
4
|
-
export type LayoutType = "tablet" | "
|
|
4
|
+
export type LayoutType = "phone" | "tablet" | "small-desktop" | "large-desktop";
|
|
5
5
|
|
|
6
6
|
function getLayout() {
|
|
7
7
|
let layout: LayoutType = "tablet";
|
|
@@ -11,8 +11,11 @@ function getLayout() {
|
|
|
11
11
|
case "omrs-breakpoint-lt-tablet":
|
|
12
12
|
layout = "phone";
|
|
13
13
|
break;
|
|
14
|
+
case "omrs-breakpoint-gt-small-desktop":
|
|
15
|
+
layout = "large-desktop";
|
|
16
|
+
break;
|
|
14
17
|
case "omrs-breakpoint-gt-tablet":
|
|
15
|
-
layout = "desktop";
|
|
18
|
+
layout = "small-desktop";
|
|
16
19
|
break;
|
|
17
20
|
}
|
|
18
21
|
});
|
|
@@ -33,3 +36,6 @@ export function useLayoutType() {
|
|
|
33
36
|
|
|
34
37
|
return type;
|
|
35
38
|
}
|
|
39
|
+
|
|
40
|
+
export const isDesktop = (layout: LayoutType) =>
|
|
41
|
+
layout === "small-desktop" || layout === "large-desktop";
|