@openmrs/esm-react-utils 3.3.2-pre.1184 → 3.3.2-pre.1193
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 +8 -6
- package/dist/openmrs-esm-react-utils.js +1 -1
- package/dist/openmrs-esm-react-utils.js.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +1 -1
- package/src/public.ts +1 -1
- package/src/useSession.test.tsx +84 -0
- package/src/useSession.tsx +117 -0
- package/src/useSessionUser.tsx +0 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-react-utils",
|
|
3
|
-
"version": "3.3.2-pre.
|
|
3
|
+
"version": "3.3.2-pre.1193",
|
|
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.3.2-pre.
|
|
59
|
-
"@openmrs/esm-config": "^3.3.2-pre.
|
|
60
|
-
"@openmrs/esm-error-handling": "^3.3.2-pre.
|
|
61
|
-
"@openmrs/esm-extensions": "^3.3.2-pre.
|
|
62
|
-
"@openmrs/esm-globals": "^3.3.2-pre.
|
|
58
|
+
"@openmrs/esm-api": "^3.3.2-pre.1193",
|
|
59
|
+
"@openmrs/esm-config": "^3.3.2-pre.1193",
|
|
60
|
+
"@openmrs/esm-error-handling": "^3.3.2-pre.1193",
|
|
61
|
+
"@openmrs/esm-extensions": "^3.3.2-pre.1193",
|
|
62
|
+
"@openmrs/esm-globals": "^3.3.2-pre.1193",
|
|
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": "8a30755ac68941f2b6d70373ef98126410608d24"
|
|
72
72
|
}
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ export * from "./useLayoutType";
|
|
|
22
22
|
export * from "./useLocations";
|
|
23
23
|
export * from "./useOnClickOutside";
|
|
24
24
|
export * from "./UserHasAccess";
|
|
25
|
-
export
|
|
25
|
+
export { useSession } from "./useSession";
|
|
26
26
|
export * from "./useStore";
|
|
27
27
|
export * from "./useVisit";
|
|
28
28
|
export * from "./useVisitTypes";
|
package/src/public.ts
CHANGED
|
@@ -19,7 +19,7 @@ export * from "./useLayoutType";
|
|
|
19
19
|
export * from "./useLocations";
|
|
20
20
|
export * from "./useOnClickOutside";
|
|
21
21
|
export * from "./UserHasAccess";
|
|
22
|
-
export
|
|
22
|
+
export { useSession } from "./useSession";
|
|
23
23
|
export * from "./useStore";
|
|
24
24
|
export * from "./useVisit";
|
|
25
25
|
export * from "./useVisitTypes";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { Suspense } from "react";
|
|
2
|
+
import { act, render, screen } from "@testing-library/react";
|
|
3
|
+
import "@testing-library/jest-dom";
|
|
4
|
+
import { useSession, __cleanup } from "./useSession.tsx";
|
|
5
|
+
import { createGlobalStore } from "@openmrs/esm-state";
|
|
6
|
+
import { SessionStore } from "@openmrs/esm-api";
|
|
7
|
+
|
|
8
|
+
const mockSessionStore = createGlobalStore<SessionStore>("mockSessionStore", {
|
|
9
|
+
loaded: false,
|
|
10
|
+
session: null,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
jest.mock("@openmrs/esm-api", () => ({
|
|
14
|
+
getSessionStore: jest.fn(() => mockSessionStore),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
function Component() {
|
|
18
|
+
const session = useSession();
|
|
19
|
+
return <div>{JSON.stringify(session)}</div>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("useSession", () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
__cleanup();
|
|
25
|
+
mockSessionStore.setState({ loaded: false, session: null });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should suspend and then resolve to the session", async () => {
|
|
29
|
+
render(
|
|
30
|
+
<Suspense fallback={"suspended"}>
|
|
31
|
+
<Component />
|
|
32
|
+
</Suspense>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(screen.getByText("suspended")).toBeInTheDocument();
|
|
36
|
+
act(() => {
|
|
37
|
+
mockSessionStore.setState({
|
|
38
|
+
loaded: true,
|
|
39
|
+
session: { authenticated: false, sessionId: "test1" },
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
await screen.findByText(/"authenticated":false/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should resolve immediately when the session is present", async () => {
|
|
46
|
+
mockSessionStore.setState({
|
|
47
|
+
loaded: true,
|
|
48
|
+
session: { authenticated: false, sessionId: "test2" },
|
|
49
|
+
});
|
|
50
|
+
render(
|
|
51
|
+
<Suspense fallback={"suspended"}>
|
|
52
|
+
<Component />
|
|
53
|
+
</Suspense>
|
|
54
|
+
);
|
|
55
|
+
expect(screen.getByText(/"authenticated":false/)).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should not return stale data when re-created", async () => {
|
|
59
|
+
const { unmount } = render(
|
|
60
|
+
<Suspense fallback={"suspended"}>
|
|
61
|
+
<Component />
|
|
62
|
+
</Suspense>
|
|
63
|
+
);
|
|
64
|
+
expect(screen.getByText("suspended")).toBeInTheDocument();
|
|
65
|
+
act(() => {
|
|
66
|
+
mockSessionStore.setState({
|
|
67
|
+
loaded: true,
|
|
68
|
+
session: { authenticated: true, sessionId: "test3" },
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
await screen.findByText(/"authenticated":true/);
|
|
72
|
+
unmount();
|
|
73
|
+
mockSessionStore.setState({
|
|
74
|
+
loaded: true,
|
|
75
|
+
session: { authenticated: false, sessionId: "test3" },
|
|
76
|
+
});
|
|
77
|
+
render(
|
|
78
|
+
<Suspense fallback={"suspended"}>
|
|
79
|
+
<Component />
|
|
80
|
+
</Suspense>
|
|
81
|
+
);
|
|
82
|
+
expect(screen.getByText(/"authenticated":false/)).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** @module @category API */
|
|
2
|
+
import { getSessionStore, Session } from "@openmrs/esm-api";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
let promise: undefined | Promise<Session>;
|
|
6
|
+
let unsubscribe: undefined | (() => void);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Gets the current user session information. Returns an object with
|
|
10
|
+
* property `authenticated` == `false` if the user is not logged in.
|
|
11
|
+
*
|
|
12
|
+
* Uses Suspense. This hook will always either return a Session object
|
|
13
|
+
* or throw for Suspense. It will never return `null`/`undefined`.
|
|
14
|
+
*
|
|
15
|
+
* @returns Current user session information
|
|
16
|
+
*/
|
|
17
|
+
export function useSession(): Session {
|
|
18
|
+
// We have two separate variables for the session.
|
|
19
|
+
//
|
|
20
|
+
// `session` is a temporary variable, which starts as `null` every time this
|
|
21
|
+
// hook is executed. It is important that we can set and return this
|
|
22
|
+
// variable synchronously, because every time we `throw` for Suspense, this
|
|
23
|
+
// hook will unmount and a new instance will be created, destroying whatever
|
|
24
|
+
// state existed. Thus, if this hook were to try to always set and return
|
|
25
|
+
// `stateSession`, it would cause an infinite loop:
|
|
26
|
+
// 1. instance A mounts
|
|
27
|
+
// 2. instance A receives value, calls `setStateSession`
|
|
28
|
+
// 3. instance A throws
|
|
29
|
+
// 4. instance A unmounts
|
|
30
|
+
// 5. instance B mounts
|
|
31
|
+
// ...
|
|
32
|
+
// What would happen if we moved `session` to the module scope, so that it
|
|
33
|
+
// could be re-used across instances of this hook? Then we would have no way
|
|
34
|
+
// to tell whether the session was fresh.
|
|
35
|
+
//
|
|
36
|
+
// `stateSession` is React state, which is needed to update components using
|
|
37
|
+
// this hook when the session changes.
|
|
38
|
+
const [stateSession, setStateSession] = useState<Session | null>(null);
|
|
39
|
+
let session: Session | null = null;
|
|
40
|
+
|
|
41
|
+
if (!stateSession) {
|
|
42
|
+
if (!promise) {
|
|
43
|
+
// If we haven't created a promise to throw yet, do that.
|
|
44
|
+
promise = new Promise<Session>((resolve) => {
|
|
45
|
+
const handleNewSession = ({ loaded, session: newSession }) => {
|
|
46
|
+
if (loaded) {
|
|
47
|
+
resolve(newSession);
|
|
48
|
+
session = newSession;
|
|
49
|
+
unsubscribe && unsubscribe();
|
|
50
|
+
unsubscribe = undefined;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
handleNewSession(getSessionStore().getState());
|
|
54
|
+
if (!session) {
|
|
55
|
+
unsubscribe = getSessionStore().subscribe(handleNewSession);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
// However, if we have created a promise to throw, but there's no `stateSession`
|
|
60
|
+
// yet, then it's probably just this hook's first render. Check to see if
|
|
61
|
+
// there's already a session that we can return.
|
|
62
|
+
const currentState = getSessionStore().getState();
|
|
63
|
+
if (currentState.loaded) {
|
|
64
|
+
session = currentState.session;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// If the session got set synchronously in the above block, then we can just
|
|
69
|
+
// return it rather than throwing. Otherwise, throw for Suspense.
|
|
70
|
+
if (session) {
|
|
71
|
+
setStateSession(session);
|
|
72
|
+
} else {
|
|
73
|
+
throw promise;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Once this hook is established (no longer throwing and getting re-created)
|
|
78
|
+
// we need to set up a subscription that will update its value the good
|
|
79
|
+
// old-fashioned React way.
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!unsubscribe) {
|
|
82
|
+
unsubscribe = getSessionStore().subscribe(
|
|
83
|
+
({ loaded, session: newSession }) => {
|
|
84
|
+
if (loaded) {
|
|
85
|
+
session = newSession;
|
|
86
|
+
setStateSession(newSession);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return () => {
|
|
92
|
+
unsubscribe && unsubscribe();
|
|
93
|
+
unsubscribe = undefined;
|
|
94
|
+
};
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const result = stateSession || session;
|
|
98
|
+
if (!result) {
|
|
99
|
+
if (promise) {
|
|
100
|
+
console.warn(
|
|
101
|
+
"useSessionUser is in an unexpected state. Attempting to recover."
|
|
102
|
+
);
|
|
103
|
+
throw promise;
|
|
104
|
+
} else {
|
|
105
|
+
throw Error("useSessionUser is in an invalid state.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* For testing.
|
|
113
|
+
*/
|
|
114
|
+
export function __cleanup() {
|
|
115
|
+
promise = undefined;
|
|
116
|
+
unsubscribe = undefined;
|
|
117
|
+
}
|
package/src/useSessionUser.tsx
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/** @module @category API */
|
|
2
|
-
import { getCurrentUser, Session } from "@openmrs/esm-api";
|
|
3
|
-
import { useState, useEffect } from "react";
|
|
4
|
-
|
|
5
|
-
export function useSession() {
|
|
6
|
-
const [session, setSession] = useState<Session | null>(null);
|
|
7
|
-
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
const sub = getCurrentUser({ includeAuthStatus: true }).subscribe(
|
|
10
|
-
(session) => setSession(session)
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
return () => sub.unsubscribe();
|
|
14
|
-
}, [setSession]);
|
|
15
|
-
|
|
16
|
-
return session;
|
|
17
|
-
}
|