@openmrs/esm-react-utils 5.1.0 → 5.1.1-pre.1003
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 +5 -5
- package/dist/openmrs-esm-react-utils.js +1 -1
- package/dist/openmrs-esm-react-utils.js.map +1 -1
- package/package.json +8 -8
- package/src/ConfigurableLink.tsx +3 -3
- package/src/Extension.tsx +2 -1
- package/src/getLifecycle.ts +4 -4
- package/src/index.ts +0 -1
- package/src/openmrsComponentDecorator.tsx +3 -3
- package/src/public.ts +0 -1
- package/src/useConfig.test.tsx +42 -0
- package/src/useConfig.ts +17 -9
- package/src/useExtensionInternalStore.ts +1 -1
- package/src/useExtensionStore.ts +1 -1
- package/src/usePagination.ts +42 -20
- package/src/useStore.test.ts +50 -0
- package/src/useStore.ts +41 -8
- package/src/useVisit.ts +63 -21
- package/src/createUseStore.ts +0 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-react-utils",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.1-pre.1003",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "React utilities for OpenMRS.",
|
|
6
6
|
"browser": "dist/openmrs-esm-react-utils.js",
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
"react-i18next": "11.x"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@openmrs/esm-api": "^5.1.
|
|
60
|
-
"@openmrs/esm-config": "^5.1.
|
|
61
|
-
"@openmrs/esm-error-handling": "^5.1.
|
|
62
|
-
"@openmrs/esm-extensions": "^5.1.
|
|
63
|
-
"@openmrs/esm-feature-flags": "^5.1.
|
|
64
|
-
"@openmrs/esm-globals": "^5.1.
|
|
59
|
+
"@openmrs/esm-api": "^5.1.1-pre.1003",
|
|
60
|
+
"@openmrs/esm-config": "^5.1.1-pre.1003",
|
|
61
|
+
"@openmrs/esm-error-handling": "^5.1.1-pre.1003",
|
|
62
|
+
"@openmrs/esm-extensions": "^5.1.1-pre.1003",
|
|
63
|
+
"@openmrs/esm-feature-flags": "^5.1.1-pre.1003",
|
|
64
|
+
"@openmrs/esm-globals": "^5.1.1-pre.1003",
|
|
65
65
|
"dayjs": "^1.10.8",
|
|
66
66
|
"i18next": "^19.6.0",
|
|
67
67
|
"react": "^18.1.0",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"rxjs": "^6.5.3",
|
|
71
71
|
"webpack": "^5.88.0"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "1bd2d52c33783c93102f9c696db84faf26932c82"
|
|
74
74
|
}
|
package/src/ConfigurableLink.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** @module @category Navigation */
|
|
2
2
|
import React, {
|
|
3
|
-
MouseEvent,
|
|
4
|
-
AnchorHTMLAttributes,
|
|
5
|
-
PropsWithChildren,
|
|
3
|
+
type MouseEvent,
|
|
4
|
+
type AnchorHTMLAttributes,
|
|
5
|
+
type PropsWithChildren,
|
|
6
6
|
} from "react";
|
|
7
7
|
import { navigate, interpolateUrl, TemplateParams } from "@openmrs/esm-config";
|
|
8
8
|
|
package/src/Extension.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import React, {
|
|
|
5
5
|
useEffect,
|
|
6
6
|
useRef,
|
|
7
7
|
useState,
|
|
8
|
+
type ReactElement,
|
|
8
9
|
} from "react";
|
|
9
10
|
import type { Parcel } from "single-spa";
|
|
10
11
|
import { ComponentContext } from ".";
|
|
@@ -16,7 +17,7 @@ export type ExtensionProps = {
|
|
|
16
17
|
wrap?(
|
|
17
18
|
slot: React.ReactNode,
|
|
18
19
|
extension: ExtensionData
|
|
19
|
-
):
|
|
20
|
+
): ReactElement<any, any> | null;
|
|
20
21
|
} & Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
|
|
21
22
|
children?:
|
|
22
23
|
| React.ReactNode
|
package/src/getLifecycle.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @module @category Framework */
|
|
2
|
-
import React from "react";
|
|
2
|
+
import React, { ComponentType } from "react";
|
|
3
3
|
import ReactDOMClient from "react-dom/client";
|
|
4
4
|
import singleSpaReact from "single-spa-react";
|
|
5
5
|
import {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "./openmrsComponentDecorator";
|
|
9
9
|
|
|
10
10
|
export function getLifecycle<T>(
|
|
11
|
-
Component:
|
|
11
|
+
Component: ComponentType<T>,
|
|
12
12
|
options: ComponentDecoratorOptions
|
|
13
13
|
) {
|
|
14
14
|
return singleSpaReact({
|
|
@@ -19,7 +19,7 @@ export function getLifecycle<T>(
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function getAsyncLifecycle<T>(
|
|
22
|
-
lazy: () => Promise<{ default:
|
|
22
|
+
lazy: () => Promise<{ default: ComponentType<T> }>,
|
|
23
23
|
options: ComponentDecoratorOptions
|
|
24
24
|
) {
|
|
25
25
|
return () =>
|
|
@@ -27,7 +27,7 @@ export function getAsyncLifecycle<T>(
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function getSyncLifecycle<T>(
|
|
30
|
-
Component:
|
|
30
|
+
Component: ComponentType<T>,
|
|
31
31
|
options: ComponentDecoratorOptions
|
|
32
32
|
) {
|
|
33
33
|
return () => Promise.resolve(getLifecycle(Component, options));
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { type ComponentType, Suspense } from "react";
|
|
2
2
|
import { I18nextProvider } from "react-i18next";
|
|
3
3
|
import type {} from "@openmrs/esm-globals";
|
|
4
4
|
import {
|
|
@@ -42,8 +42,8 @@ export function openmrsComponentDecorator(userOpts: ComponentDecoratorOptions) {
|
|
|
42
42
|
const opts = Object.assign({}, defaultOpts, userOpts);
|
|
43
43
|
|
|
44
44
|
return function decorateComponent(
|
|
45
|
-
Comp:
|
|
46
|
-
):
|
|
45
|
+
Comp: ComponentType<any>
|
|
46
|
+
): ComponentType<any> {
|
|
47
47
|
return class OpenmrsReactComponent extends React.Component<
|
|
48
48
|
OpenmrsReactComponentProps,
|
|
49
49
|
OpenmrsReactComponentState
|
package/src/public.ts
CHANGED
package/src/useConfig.test.tsx
CHANGED
|
@@ -20,6 +20,12 @@ function RenderConfig(props) {
|
|
|
20
20
|
return <button>{config[props.configKey]}</button>;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function RenderExternalConfig(props) {
|
|
24
|
+
const config = useConfig({ externalModuleName: props.externalModuleName });
|
|
25
|
+
|
|
26
|
+
return <button>{config[props.configKey]}</button>;
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
function clearConfig() {
|
|
24
30
|
mockConfigInternalStore.resetMock();
|
|
25
31
|
}
|
|
@@ -297,4 +303,40 @@ describe(`useConfig in an extension`, () => {
|
|
|
297
303
|
expect(screen.findByText("Yet another thing")).toBeTruthy()
|
|
298
304
|
);
|
|
299
305
|
});
|
|
306
|
+
|
|
307
|
+
it("can optionally load an external module's configuration", async () => {
|
|
308
|
+
defineConfigSchema("first-module", {
|
|
309
|
+
thing: {
|
|
310
|
+
_default: "first thing",
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
defineConfigSchema("second-module", {
|
|
315
|
+
thing: {
|
|
316
|
+
_default: "second thing",
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
render(
|
|
321
|
+
<React.Suspense fallback={<div>Suspense!</div>}>
|
|
322
|
+
<ComponentContext.Provider
|
|
323
|
+
value={{
|
|
324
|
+
moduleName: "first-module",
|
|
325
|
+
extension: {
|
|
326
|
+
extensionSlotName: "fooSlot",
|
|
327
|
+
extensionSlotModuleName: "slot-mod",
|
|
328
|
+
extensionId: "fooExt#id1",
|
|
329
|
+
},
|
|
330
|
+
}}
|
|
331
|
+
>
|
|
332
|
+
<RenderExternalConfig
|
|
333
|
+
externalModuleName="second-module"
|
|
334
|
+
configKey="thing"
|
|
335
|
+
/>
|
|
336
|
+
</ComponentContext.Provider>
|
|
337
|
+
</React.Suspense>
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
await waitFor(() => expect(screen.findByText("second thing")).toBeTruthy());
|
|
341
|
+
});
|
|
300
342
|
});
|
package/src/useConfig.ts
CHANGED
|
@@ -157,16 +157,24 @@ function useNormalConfig(moduleName: string) {
|
|
|
157
157
|
return state;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
interface UseConfigOptions {
|
|
161
|
+
/** An external module to load the configuration from. This option should only be used if
|
|
162
|
+
absolutely necessary as it can end up making frontend modules coupled to one another. */
|
|
163
|
+
externalModuleName?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
160
166
|
/**
|
|
161
167
|
* Use this React Hook to obtain your module's configuration.
|
|
168
|
+
*
|
|
169
|
+
* @param options Additional options that can be passed to useConfig()
|
|
162
170
|
*/
|
|
163
|
-
export function useConfig<
|
|
164
|
-
T = Omit<ConfigObject, "Display conditions" | "Translation overrides">
|
|
165
|
-
>() {
|
|
171
|
+
export function useConfig<T = Record<string, any>>(options?: UseConfigOptions) {
|
|
166
172
|
// This hook uses the config of the MF defining the component.
|
|
167
173
|
// If the component is used in an extension slot then the slot
|
|
168
174
|
// may override (part of) its configuration.
|
|
169
|
-
const { moduleName, extension } =
|
|
175
|
+
const { moduleName: contextModuleName, extension } =
|
|
176
|
+
useContext(ComponentContext);
|
|
177
|
+
const moduleName = options?.externalModuleName ?? contextModuleName;
|
|
170
178
|
|
|
171
179
|
if (!moduleName && !extension) {
|
|
172
180
|
throw Error(errorMessage);
|
|
@@ -175,11 +183,11 @@ export function useConfig<
|
|
|
175
183
|
const normalConfig = useNormalConfig(moduleName);
|
|
176
184
|
const extensionConfig = useExtensionConfig(extension);
|
|
177
185
|
const config = useMemo(
|
|
178
|
-
() =>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
[normalConfig, extensionConfig]
|
|
186
|
+
() =>
|
|
187
|
+
options?.externalModuleName && moduleName === options.externalModuleName
|
|
188
|
+
? { ...normalConfig }
|
|
189
|
+
: { ...normalConfig, ...extensionConfig },
|
|
190
|
+
[moduleName, options?.externalModuleName, normalConfig, extensionConfig]
|
|
183
191
|
);
|
|
184
192
|
|
|
185
193
|
return config as T;
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
ExtensionInternalStore,
|
|
3
3
|
getExtensionInternalStore,
|
|
4
4
|
} from "@openmrs/esm-extensions";
|
|
5
|
-
import { createUseStore } from "./
|
|
5
|
+
import { createUseStore } from "./useStore";
|
|
6
6
|
|
|
7
7
|
/** @internal
|
|
8
8
|
* @deprecated Use `useStore(getExtensionInternalStore())`
|
package/src/useExtensionStore.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @module @category Extension */
|
|
2
2
|
import { ExtensionStore, getExtensionStore } from "@openmrs/esm-extensions";
|
|
3
|
-
import { createUseStore } from "./
|
|
3
|
+
import { createUseStore } from "./useStore";
|
|
4
4
|
|
|
5
5
|
export const useExtensionStore = createUseStore<ExtensionStore>(
|
|
6
6
|
getExtensionStore()
|
package/src/usePagination.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @module @category UI */
|
|
2
|
-
import { useMemo, useState } from "react";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
3
|
|
|
4
4
|
const defaultResultsPerPage = 10;
|
|
5
5
|
|
|
@@ -22,25 +22,47 @@ export function usePagination<T>(
|
|
|
22
22
|
return data.slice(lowerBound, upperBound);
|
|
23
23
|
}, [data, page, resultsPerPage]);
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
totalPages,
|
|
28
|
-
currentPage: page,
|
|
29
|
-
paginated: data.length > resultsPerPage,
|
|
30
|
-
showNextButton: page < totalPages,
|
|
31
|
-
showPreviousButton: page > 1,
|
|
32
|
-
goTo(page: number) {
|
|
25
|
+
const goTo = useCallback(
|
|
26
|
+
(page: number) => {
|
|
33
27
|
setPage(Math.max(1, Math.min(totalPages, page)));
|
|
34
28
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
29
|
+
[setPage, totalPages]
|
|
30
|
+
);
|
|
31
|
+
const goToNext = useCallback(() => {
|
|
32
|
+
if (page < totalPages) {
|
|
33
|
+
setPage(page + 1);
|
|
34
|
+
}
|
|
35
|
+
}, [page, totalPages, setPage]);
|
|
36
|
+
|
|
37
|
+
const goToPrevious = useCallback(() => {
|
|
38
|
+
if (page > 1) {
|
|
39
|
+
setPage(page - 1);
|
|
40
|
+
}
|
|
41
|
+
}, [page, setPage]);
|
|
42
|
+
|
|
43
|
+
const memoisedPaginatedData = useMemo(
|
|
44
|
+
() => ({
|
|
45
|
+
results,
|
|
46
|
+
totalPages,
|
|
47
|
+
currentPage: page,
|
|
48
|
+
paginated: data.length > resultsPerPage,
|
|
49
|
+
showNextButton: page < totalPages,
|
|
50
|
+
showPreviousButton: page > 1,
|
|
51
|
+
goTo,
|
|
52
|
+
goToNext,
|
|
53
|
+
goToPrevious,
|
|
54
|
+
}),
|
|
55
|
+
[
|
|
56
|
+
results,
|
|
57
|
+
totalPages,
|
|
58
|
+
data.length,
|
|
59
|
+
resultsPerPage,
|
|
60
|
+
page,
|
|
61
|
+
goTo,
|
|
62
|
+
goToNext,
|
|
63
|
+
goToPrevious,
|
|
64
|
+
]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return memoisedPaginatedData;
|
|
46
68
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { createGlobalStore } from "@openmrs/esm-state";
|
|
3
|
+
import { useStore, useStoreWithActions } from "@openmrs/esm-react-utils";
|
|
4
|
+
|
|
5
|
+
describe("useStore", () => {
|
|
6
|
+
it("updates state, selects, and correctly binds actions", () => {
|
|
7
|
+
const store = createGlobalStore("scoreboard", {
|
|
8
|
+
good: { count: 0 },
|
|
9
|
+
evil: { count: 0 },
|
|
10
|
+
});
|
|
11
|
+
const actions = {
|
|
12
|
+
tally: (state, team, number) => ({
|
|
13
|
+
[team]: { count: state[team].count + number },
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const { result } = renderHook(() =>
|
|
18
|
+
useStore(store, (state) => state.good, actions)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(result.current.count).toBe(0);
|
|
22
|
+
act(() => {
|
|
23
|
+
result.current.tally("good", 2);
|
|
24
|
+
});
|
|
25
|
+
expect(result.current.count).toBe(2);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("useStoreWithActions", () => {
|
|
30
|
+
it("should correctly bind actions", () => {
|
|
31
|
+
const store = createGlobalStore("counter", { count: 0 });
|
|
32
|
+
const actions = {
|
|
33
|
+
increment: (state) => ({ count: state.count + 1 }),
|
|
34
|
+
incrementBy: (state, number) => ({ count: state.count + number }),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const { result } = renderHook(() => useStoreWithActions(store, actions));
|
|
38
|
+
|
|
39
|
+
expect(result.current.count).toBe(0);
|
|
40
|
+
act(() => {
|
|
41
|
+
result.current.increment();
|
|
42
|
+
});
|
|
43
|
+
expect(store.getState().count).toBe(1);
|
|
44
|
+
expect(result.current.count).toBe(1);
|
|
45
|
+
act(() => {
|
|
46
|
+
result.current.incrementBy(3);
|
|
47
|
+
});
|
|
48
|
+
expect(result.current.count).toBe(4);
|
|
49
|
+
});
|
|
50
|
+
});
|
package/src/useStore.ts
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
import { subscribeTo } from "@openmrs/esm-state";
|
|
3
3
|
import { useEffect, useMemo, useState } from "react";
|
|
4
4
|
import type { StoreApi } from "zustand";
|
|
5
|
-
import type { Actions, BoundActions } from "./createUseStore";
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
export type ActionFunction<T> = (state: T, ...args: any[]) => Partial<T>;
|
|
7
|
+
export type Actions<T> =
|
|
8
|
+
| ((store: StoreApi<T>) => Record<string, ActionFunction<T>>)
|
|
9
|
+
| Record<string, ActionFunction<T>>;
|
|
10
|
+
export type BoundActions = { [key: string]: (...args: any[]) => void };
|
|
11
|
+
|
|
12
|
+
function bindActions<T>(store: StoreApi<T>, actions: Actions<T>): BoundActions {
|
|
8
13
|
if (typeof actions == "function") {
|
|
9
14
|
actions = actions(store);
|
|
10
15
|
}
|
|
@@ -12,7 +17,7 @@ function bindActions<T>(store: StoreApi<T>, actions: Actions): BoundActions {
|
|
|
12
17
|
const bound = {};
|
|
13
18
|
|
|
14
19
|
for (let i in actions) {
|
|
15
|
-
bound[i] = ()
|
|
20
|
+
bound[i] = function () {
|
|
16
21
|
const args = arguments;
|
|
17
22
|
store.setState((state) => {
|
|
18
23
|
let _args = [state];
|
|
@@ -35,17 +40,17 @@ function useStore<T, U>(store: StoreApi<T>, select: (state: T) => U): U;
|
|
|
35
40
|
function useStore<T, U>(
|
|
36
41
|
store: StoreApi<T>,
|
|
37
42
|
select: undefined,
|
|
38
|
-
actions: Actions
|
|
43
|
+
actions: Actions<T>
|
|
39
44
|
): T & BoundActions;
|
|
40
45
|
function useStore<T, U>(
|
|
41
46
|
store: StoreApi<T>,
|
|
42
47
|
select: (state: T) => U,
|
|
43
|
-
actions: Actions
|
|
48
|
+
actions: Actions<T>
|
|
44
49
|
): U & BoundActions;
|
|
45
50
|
function useStore<T, U>(
|
|
46
51
|
store: StoreApi<T>,
|
|
47
52
|
select: (state: T) => U = defaultSelectFunction,
|
|
48
|
-
actions?: Actions
|
|
53
|
+
actions?: Actions<T>
|
|
49
54
|
) {
|
|
50
55
|
const [state, setState] = useState<U>(() => select(store.getState()));
|
|
51
56
|
useEffect(() => subscribeTo(store, select, setState), [store, select]);
|
|
@@ -58,11 +63,39 @@ function useStore<T, U>(
|
|
|
58
63
|
return { ...state, ...boundActions };
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
/**
|
|
67
|
+
*
|
|
68
|
+
* @param store A zustand store
|
|
69
|
+
* @param actions
|
|
70
|
+
* @returns
|
|
71
|
+
*/
|
|
61
72
|
function useStoreWithActions<T>(
|
|
62
73
|
store: StoreApi<T>,
|
|
63
|
-
actions: Actions
|
|
74
|
+
actions: Actions<T>
|
|
64
75
|
): T & BoundActions {
|
|
65
76
|
return useStore(store, defaultSelectFunction, actions);
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Whenever possible, use `useStore(yourStore)` instead. This function is for creating a
|
|
81
|
+
* custom hook for a specific store.
|
|
82
|
+
*/
|
|
83
|
+
function createUseStore<T>(store: StoreApi<T>) {
|
|
84
|
+
function useStore(): T;
|
|
85
|
+
function useStore(actions: Actions<T>): T & BoundActions;
|
|
86
|
+
function useStore(actions?: Actions<T>): T & BoundActions;
|
|
87
|
+
function useStore(actions?: Actions<T>) {
|
|
88
|
+
const [state, set] = useState(store.getState());
|
|
89
|
+
useEffect(() => store.subscribe((state) => set(state)), []);
|
|
90
|
+
let boundActions: BoundActions = useMemo(
|
|
91
|
+
() => (actions ? bindActions(store, actions) : {}),
|
|
92
|
+
[actions]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return { ...state, ...boundActions };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return useStore;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { createUseStore, useStore, useStoreWithActions };
|
package/src/useVisit.ts
CHANGED
|
@@ -17,17 +17,30 @@ export interface VisitReturnType {
|
|
|
17
17
|
error: Error;
|
|
18
18
|
mutate: () => void;
|
|
19
19
|
isValidating: boolean;
|
|
20
|
+
activeVisit: Visit | null;
|
|
20
21
|
currentVisit: Visit | null;
|
|
21
|
-
|
|
22
|
+
currentVisitIsRetrospective: boolean;
|
|
22
23
|
isLoading: boolean;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
|
-
* This React hook returns
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* This React hook returns visit information if the patient UUID is not null. There are
|
|
28
|
+
* potentially two relevant visits at a time: "active" and "current".
|
|
29
|
+
*
|
|
30
|
+
* The active visit is the most recent visit without an end date. The presence of an active
|
|
31
|
+
* visit generally means that the patient is in the facility.
|
|
32
|
+
*
|
|
33
|
+
* The current visit is the active visit, unless a retrospective visit has been selected.
|
|
34
|
+
* If there is no active visit and no selected retrospective visit, then there is no
|
|
35
|
+
* current visit. If there is no active visit but there is a retrospective visit, then
|
|
36
|
+
* the retrospective visit is the current visit. `currentVisitIsRetrospective` tells you
|
|
37
|
+
* whether the current visit is a retrospective visit.
|
|
38
|
+
*
|
|
39
|
+
* The active visit and current visit require two separate API calls. `error` contains
|
|
40
|
+
* the error from either one, if there is an error. `isValidating` is true if either
|
|
41
|
+
* API call is in progress. `mutate` refreshes the data from both API calls.
|
|
42
|
+
*
|
|
29
43
|
* @param patientUuid Unique patient identifier `string`
|
|
30
|
-
* @returns Object {`error` `isValidating`, `currentVisit`, `mutate`}
|
|
31
44
|
*/
|
|
32
45
|
export function useVisit(patientUuid: string): VisitReturnType {
|
|
33
46
|
const { patientUuid: visitStorePatientUuid, manuallySetVisitUuid } = useStore(
|
|
@@ -38,31 +51,60 @@ export function useVisit(patientUuid: string): VisitReturnType {
|
|
|
38
51
|
patientUuid && visitStorePatientUuid == patientUuid
|
|
39
52
|
? manuallySetVisitUuid
|
|
40
53
|
: null;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const {
|
|
54
|
+
const activeVisitUrlSuffix = `?patient=${patientUuid}&v=${defaultVisitCustomRepresentation}&includeInactive=false`;
|
|
55
|
+
const retrospectiveVisitUrlSuffix = `/${retrospectiveVisitUuid}`;
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
data: activeData,
|
|
59
|
+
error: activeError,
|
|
60
|
+
mutate: activeMutate,
|
|
61
|
+
isValidating: activeIsValidating,
|
|
62
|
+
} = useSWR<{
|
|
45
63
|
data: Visit | { results: Array<Visit> };
|
|
46
64
|
}>(
|
|
47
|
-
patientUuid ? `/ws/rest/v1/visit${
|
|
65
|
+
patientUuid ? `/ws/rest/v1/visit${activeVisitUrlSuffix}` : null,
|
|
48
66
|
openmrsFetch
|
|
49
67
|
);
|
|
50
68
|
|
|
51
|
-
const
|
|
69
|
+
const {
|
|
70
|
+
data: retroData,
|
|
71
|
+
error: retroError,
|
|
72
|
+
mutate: retroMutate,
|
|
73
|
+
isValidating: retroIsValidating,
|
|
74
|
+
} = useSWR<{
|
|
75
|
+
data: Visit | { results: Array<Visit> };
|
|
76
|
+
}>(
|
|
77
|
+
patientUuid && retrospectiveVisitUuid
|
|
78
|
+
? `/ws/rest/v1/visit${retrospectiveVisitUrlSuffix}`
|
|
79
|
+
: null,
|
|
80
|
+
openmrsFetch
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const activeVisit = useMemo(
|
|
52
84
|
() =>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
activeData?.data.results.find((visit) => visit.stopDatetime === null) ??
|
|
86
|
+
null,
|
|
87
|
+
[activeData]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const currentVisit = useMemo(
|
|
91
|
+
() => (retrospectiveVisitUuid ? retroData?.data : activeVisit ?? null),
|
|
92
|
+
[retroData, activeVisit, retrospectiveVisitUuid]
|
|
58
93
|
);
|
|
59
94
|
|
|
60
95
|
return {
|
|
61
|
-
error,
|
|
62
|
-
mutate
|
|
63
|
-
|
|
96
|
+
error: activeError || retroError,
|
|
97
|
+
mutate: () => {
|
|
98
|
+
activeMutate();
|
|
99
|
+
retroMutate();
|
|
100
|
+
},
|
|
101
|
+
isValidating: activeIsValidating || retroIsValidating,
|
|
102
|
+
activeVisit,
|
|
64
103
|
currentVisit,
|
|
65
|
-
|
|
66
|
-
isLoading:
|
|
104
|
+
currentVisitIsRetrospective: Boolean(retrospectiveVisitUuid),
|
|
105
|
+
isLoading: Boolean(
|
|
106
|
+
(!activeData || (retrospectiveVisitUuid && !retroData)) &&
|
|
107
|
+
(!activeError || !retroError)
|
|
108
|
+
),
|
|
67
109
|
};
|
|
68
110
|
}
|
package/src/createUseStore.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/** @module @category Store */
|
|
2
|
-
import { useEffect, useMemo, useState } from "react";
|
|
3
|
-
import type { StoreApi } from "zustand";
|
|
4
|
-
|
|
5
|
-
export type Actions = Function | Record<string, Function>;
|
|
6
|
-
export type BoundActions = { [key: string]: (...args: any[]) => void };
|
|
7
|
-
|
|
8
|
-
function bindActions<T>(store: StoreApi<T>, actions: Actions): BoundActions {
|
|
9
|
-
if (typeof actions == "function") {
|
|
10
|
-
actions = actions(store);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const bound = {};
|
|
14
|
-
|
|
15
|
-
for (let i in actions) {
|
|
16
|
-
bound[i] = () => {
|
|
17
|
-
const args = arguments;
|
|
18
|
-
store.setState((state) => {
|
|
19
|
-
let _args = [state];
|
|
20
|
-
for (let i = 0; i < args.length; i++) {
|
|
21
|
-
_args.push(args[i]);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return actions[i](_args);
|
|
25
|
-
});
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return bound;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Avoid this; generally prefer to have clients use `useStore(yourStore)` */
|
|
33
|
-
export function createUseStore<T>(store: StoreApi<T>) {
|
|
34
|
-
function useStore(): T;
|
|
35
|
-
function useStore(actions: Actions): T & BoundActions;
|
|
36
|
-
function useStore(actions?: Actions): T & BoundActions;
|
|
37
|
-
function useStore(actions?: Actions) {
|
|
38
|
-
const [state, set] = useState(store.getState());
|
|
39
|
-
useEffect(() => store.subscribe((state) => set(state)), []);
|
|
40
|
-
let boundActions: BoundActions = useMemo(
|
|
41
|
-
() => (actions ? bindActions(store, actions) : {}),
|
|
42
|
-
[actions]
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
return { ...state, ...boundActions };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return useStore;
|
|
49
|
-
}
|