@multiplatform.one/backoffice 7.34.3 → 7.34.4
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/dist/cjs/chrome/BackofficeAwesomebar.cjs +7 -5
- package/dist/cjs/chrome/BackofficeAwesomebar.native.js +7 -5
- package/dist/cjs/chrome/BackofficeAwesomebar.native.js.map +1 -1
- package/dist/cjs/chrome/CommandPalette.cjs +4 -2
- package/dist/cjs/chrome/CommandPalette.native.js +4 -2
- package/dist/cjs/chrome/CommandPalette.native.js.map +1 -1
- package/dist/cjs/chrome/NotificationsButton.cjs +1 -0
- package/dist/cjs/chrome/NotificationsButton.native.js +1 -0
- package/dist/cjs/chrome/NotificationsButton.native.js.map +1 -1
- package/dist/cjs/chrome/UserMenu.cjs +1 -1
- package/dist/cjs/chrome/UserMenu.native.js +1 -1
- package/dist/cjs/chrome/UserMenu.native.js.map +1 -1
- package/dist/cjs/chrome/awesomebar-data.cjs +45 -18
- package/dist/cjs/chrome/awesomebar-data.native.js +45 -17
- package/dist/cjs/chrome/awesomebar-data.native.js.map +1 -1
- package/dist/cjs/chrome/command-palette-model.cjs +8 -7
- package/dist/cjs/chrome/command-palette-model.native.js +10 -7
- package/dist/cjs/chrome/command-palette-model.native.js.map +1 -1
- package/dist/cjs/chrome/notifications-data.cjs +2 -38
- package/dist/cjs/chrome/notifications-data.native.js +2 -39
- package/dist/cjs/chrome/notifications-data.native.js.map +1 -1
- package/dist/cjs/chrome/user-session.cjs +19 -11
- package/dist/cjs/chrome/user-session.native.js +20 -13
- package/dist/cjs/chrome/user-session.native.js.map +1 -1
- package/dist/cjs/list/DoctypeListScreen.cjs +11 -5
- package/dist/cjs/list/DoctypeListScreen.native.js +13 -5
- package/dist/cjs/list/DoctypeListScreen.native.js.map +1 -1
- package/dist/cjs/shared/sessionUser.cjs +89 -0
- package/dist/cjs/shared/sessionUser.native.js +96 -0
- package/dist/cjs/shared/sessionUser.native.js.map +1 -0
- package/dist/cjs/shared/useSessionUserId.cjs +2 -26
- package/dist/cjs/shared/useSessionUserId.native.js +2 -28
- package/dist/cjs/shared/useSessionUserId.native.js.map +1 -1
- package/dist/cjs/shell/BackofficeShell.cjs +6 -2
- package/dist/cjs/shell/BackofficeShell.native.js +6 -2
- package/dist/cjs/shell/BackofficeShell.native.js.map +1 -1
- package/dist/cjs/shell/useWorkspaces.cjs +2 -31
- package/dist/cjs/shell/useWorkspaces.native.js +2 -35
- package/dist/cjs/shell/useWorkspaces.native.js.map +1 -1
- package/dist/esm/chrome/BackofficeAwesomebar.mjs +7 -5
- package/dist/esm/chrome/BackofficeAwesomebar.mjs.map +1 -1
- package/dist/esm/chrome/BackofficeAwesomebar.native.js +7 -5
- package/dist/esm/chrome/BackofficeAwesomebar.native.js.map +1 -1
- package/dist/esm/chrome/CommandPalette.mjs +4 -2
- package/dist/esm/chrome/CommandPalette.mjs.map +1 -1
- package/dist/esm/chrome/CommandPalette.native.js +4 -2
- package/dist/esm/chrome/CommandPalette.native.js.map +1 -1
- package/dist/esm/chrome/NotificationsButton.mjs +1 -0
- package/dist/esm/chrome/NotificationsButton.mjs.map +1 -1
- package/dist/esm/chrome/NotificationsButton.native.js +1 -0
- package/dist/esm/chrome/NotificationsButton.native.js.map +1 -1
- package/dist/esm/chrome/UserMenu.mjs +1 -1
- package/dist/esm/chrome/UserMenu.mjs.map +1 -1
- package/dist/esm/chrome/UserMenu.native.js +1 -1
- package/dist/esm/chrome/UserMenu.native.js.map +1 -1
- package/dist/esm/chrome/awesomebar-data.mjs +45 -18
- package/dist/esm/chrome/awesomebar-data.mjs.map +1 -1
- package/dist/esm/chrome/awesomebar-data.native.js +45 -17
- package/dist/esm/chrome/awesomebar-data.native.js.map +1 -1
- package/dist/esm/chrome/command-palette-model.mjs +8 -7
- package/dist/esm/chrome/command-palette-model.mjs.map +1 -1
- package/dist/esm/chrome/command-palette-model.native.js +10 -7
- package/dist/esm/chrome/command-palette-model.native.js.map +1 -1
- package/dist/esm/chrome/notifications-data.mjs +3 -39
- package/dist/esm/chrome/notifications-data.mjs.map +1 -1
- package/dist/esm/chrome/notifications-data.native.js +3 -40
- package/dist/esm/chrome/notifications-data.native.js.map +1 -1
- package/dist/esm/chrome/user-session.mjs +19 -11
- package/dist/esm/chrome/user-session.mjs.map +1 -1
- package/dist/esm/chrome/user-session.native.js +20 -13
- package/dist/esm/chrome/user-session.native.js.map +1 -1
- package/dist/esm/list/DoctypeListScreen.mjs +12 -6
- package/dist/esm/list/DoctypeListScreen.mjs.map +1 -1
- package/dist/esm/list/DoctypeListScreen.native.js +14 -6
- package/dist/esm/list/DoctypeListScreen.native.js.map +1 -1
- package/dist/esm/shared/sessionUser.mjs +61 -0
- package/dist/esm/shared/sessionUser.mjs.map +1 -0
- package/dist/esm/shared/sessionUser.native.js +65 -0
- package/dist/esm/shared/sessionUser.native.js.map +1 -0
- package/dist/esm/shared/useSessionUserId.mjs +2 -26
- package/dist/esm/shared/useSessionUserId.mjs.map +1 -1
- package/dist/esm/shared/useSessionUserId.native.js +2 -28
- package/dist/esm/shared/useSessionUserId.native.js.map +1 -1
- package/dist/esm/shell/BackofficeShell.mjs +6 -2
- package/dist/esm/shell/BackofficeShell.mjs.map +1 -1
- package/dist/esm/shell/BackofficeShell.native.js +6 -2
- package/dist/esm/shell/BackofficeShell.native.js.map +1 -1
- package/dist/esm/shell/useWorkspaces.mjs +4 -33
- package/dist/esm/shell/useWorkspaces.mjs.map +1 -1
- package/dist/esm/shell/useWorkspaces.native.js +4 -37
- package/dist/esm/shell/useWorkspaces.native.js.map +1 -1
- package/dist/jsx/chrome/BackofficeAwesomebar.mjs +7 -5
- package/dist/jsx/chrome/BackofficeAwesomebar.mjs.map +1 -1
- package/dist/jsx/chrome/BackofficeAwesomebar.native.js +7 -5
- package/dist/jsx/chrome/BackofficeAwesomebar.native.js.map +1 -1
- package/dist/jsx/chrome/CommandPalette.mjs +4 -2
- package/dist/jsx/chrome/CommandPalette.mjs.map +1 -1
- package/dist/jsx/chrome/CommandPalette.native.js +4 -2
- package/dist/jsx/chrome/CommandPalette.native.js.map +1 -1
- package/dist/jsx/chrome/NotificationsButton.mjs +1 -0
- package/dist/jsx/chrome/NotificationsButton.mjs.map +1 -1
- package/dist/jsx/chrome/NotificationsButton.native.js +1 -0
- package/dist/jsx/chrome/NotificationsButton.native.js.map +1 -1
- package/dist/jsx/chrome/UserMenu.mjs +1 -1
- package/dist/jsx/chrome/UserMenu.mjs.map +1 -1
- package/dist/jsx/chrome/UserMenu.native.js +1 -1
- package/dist/jsx/chrome/UserMenu.native.js.map +1 -1
- package/dist/jsx/chrome/awesomebar-data.mjs +45 -18
- package/dist/jsx/chrome/awesomebar-data.mjs.map +1 -1
- package/dist/jsx/chrome/awesomebar-data.native.js +45 -17
- package/dist/jsx/chrome/awesomebar-data.native.js.map +1 -1
- package/dist/jsx/chrome/command-palette-model.mjs +8 -7
- package/dist/jsx/chrome/command-palette-model.mjs.map +1 -1
- package/dist/jsx/chrome/command-palette-model.native.js +10 -7
- package/dist/jsx/chrome/command-palette-model.native.js.map +1 -1
- package/dist/jsx/chrome/notifications-data.mjs +3 -39
- package/dist/jsx/chrome/notifications-data.mjs.map +1 -1
- package/dist/jsx/chrome/notifications-data.native.js +2 -39
- package/dist/jsx/chrome/notifications-data.native.js.map +1 -1
- package/dist/jsx/chrome/user-session.mjs +19 -11
- package/dist/jsx/chrome/user-session.mjs.map +1 -1
- package/dist/jsx/chrome/user-session.native.js +20 -13
- package/dist/jsx/chrome/user-session.native.js.map +1 -1
- package/dist/jsx/list/DoctypeListScreen.mjs +12 -6
- package/dist/jsx/list/DoctypeListScreen.mjs.map +1 -1
- package/dist/jsx/list/DoctypeListScreen.native.js +13 -5
- package/dist/jsx/list/DoctypeListScreen.native.js.map +1 -1
- package/dist/jsx/shared/sessionUser.mjs +61 -0
- package/dist/jsx/shared/sessionUser.mjs.map +1 -0
- package/dist/jsx/shared/sessionUser.native.js +96 -0
- package/dist/jsx/shared/sessionUser.native.js.map +1 -0
- package/dist/jsx/shared/useSessionUserId.mjs +2 -26
- package/dist/jsx/shared/useSessionUserId.mjs.map +1 -1
- package/dist/jsx/shared/useSessionUserId.native.js +2 -28
- package/dist/jsx/shared/useSessionUserId.native.js.map +1 -1
- package/dist/jsx/shell/BackofficeShell.mjs +6 -2
- package/dist/jsx/shell/BackofficeShell.mjs.map +1 -1
- package/dist/jsx/shell/BackofficeShell.native.js +6 -2
- package/dist/jsx/shell/BackofficeShell.native.js.map +1 -1
- package/dist/jsx/shell/useWorkspaces.mjs +4 -33
- package/dist/jsx/shell/useWorkspaces.mjs.map +1 -1
- package/dist/jsx/shell/useWorkspaces.native.js +2 -35
- package/dist/jsx/shell/useWorkspaces.native.js.map +1 -1
- package/package.json +12 -12
- package/src/chrome/BackofficeAwesomebar.tsx +11 -6
- package/src/chrome/CommandPalette.spec.tsx +12 -0
- package/src/chrome/CommandPalette.tsx +9 -7
- package/src/chrome/NotificationsButton.tsx +1 -0
- package/src/chrome/UserMenu.screen.spec.tsx +1 -1
- package/src/chrome/UserMenu.tsx +1 -1
- package/src/chrome/awesomebar-data.tsx +28 -19
- package/src/chrome/command-palette-model.spec.tsx +148 -0
- package/src/chrome/command-palette-model.ts +13 -5
- package/src/chrome/notifications-data.tsx +3 -50
- package/src/chrome/user-session.tsx +20 -15
- package/src/form/DocFormScreen.guest.screen.spec.tsx +3 -3
- package/src/list/DoctypeListScreen.guest.screen.spec.tsx +82 -3
- package/src/list/DoctypeListScreen.tsx +19 -13
- package/src/report/DoctypeReportView.screen.spec.tsx +12 -0
- package/src/shared/sessionUser.spec.tsx +106 -0
- package/src/shared/sessionUser.ts +59 -0
- package/src/shared/useSessionUserId.ts +2 -40
- package/src/shell/BackofficeShell.spec.tsx +31 -2
- package/src/shell/BackofficeShell.tsx +5 -3
- package/src/shell/useWorkspaces.ts +5 -56
- package/types/chrome/BackofficeAwesomebar.d.ts.map +1 -1
- package/types/chrome/NotificationsButton.d.ts.map +1 -1
- package/types/chrome/awesomebar-data.d.ts +4 -3
- package/types/chrome/awesomebar-data.d.ts.map +1 -1
- package/types/chrome/command-palette-model.d.ts +1 -0
- package/types/chrome/command-palette-model.d.ts.map +1 -1
- package/types/chrome/notifications-data.d.ts.map +1 -1
- package/types/chrome/user-session.d.ts.map +1 -1
- package/types/list/DoctypeListScreen.d.ts.map +1 -1
- package/types/shared/sessionUser.d.ts +9 -0
- package/types/shared/sessionUser.d.ts.map +1 -0
- package/types/shared/useSessionUserId.d.ts +0 -4
- package/types/shared/useSessionUserId.d.ts.map +1 -1
- package/types/shell/BackofficeShell.d.ts.map +1 -1
- package/types/shell/useWorkspaces.d.ts +0 -5
- package/types/shell/useWorkspaces.d.ts.map +1 -1
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
type FrappeApp,
|
|
25
25
|
} from "@multiplatform.one/frappe";
|
|
26
26
|
import { useFrappeConfig } from "@multiplatform.one/frappe-ui";
|
|
27
|
-
import { useCallback,
|
|
27
|
+
import { useCallback, useMemo, useState } from "react";
|
|
28
|
+
import { useSessionIdentity } from "../shared/sessionUser";
|
|
28
29
|
import { useFrappeApp } from "../shared/useFrappeApp";
|
|
29
30
|
import { parseFrappeTimestamp, stripHtmlToText } from "./notifications-utils";
|
|
30
31
|
|
|
@@ -72,54 +73,6 @@ const MARK_ALL_AS_READ_METHOD =
|
|
|
72
73
|
// Session user (module cache — one fetch per FrappeApp per session)
|
|
73
74
|
// ---------------------------------------------------------------------------
|
|
74
75
|
|
|
75
|
-
const sessionUserCache = new WeakMap<FrappeApp, Promise<string | undefined>>();
|
|
76
|
-
|
|
77
|
-
interface SessionUserState {
|
|
78
|
-
user?: string;
|
|
79
|
-
/** True once the probe settled (Guest / unreachable resolve to no user). */
|
|
80
|
-
resolved: boolean;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function useNotificationsSessionUser(fixtureMode: boolean): SessionUserState {
|
|
84
|
-
const app = useFrappeApp();
|
|
85
|
-
const [state, setState] = useState<SessionUserState>({ resolved: false });
|
|
86
|
-
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
// Fixture mode has no session endpoint; report resolved-with-no-user.
|
|
89
|
-
// No app (no provider config) settles the same way so the panel never
|
|
90
|
-
// skeletons forever.
|
|
91
|
-
if (fixtureMode || !app) {
|
|
92
|
-
setState({ resolved: true });
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
let cancelled = false;
|
|
96
|
-
let promise = sessionUserCache.get(app);
|
|
97
|
-
if (!promise) {
|
|
98
|
-
promise = app
|
|
99
|
-
.call()
|
|
100
|
-
.get<{ message?: string }>("frappe.auth.get_logged_user")
|
|
101
|
-
.then((res) => {
|
|
102
|
-
const user = res.payload?.message;
|
|
103
|
-
return user && user !== "Guest" ? user : undefined;
|
|
104
|
-
})
|
|
105
|
-
.catch(() => {
|
|
106
|
-
// Evict failed probes so a later mount can retry.
|
|
107
|
-
sessionUserCache.delete(app);
|
|
108
|
-
return undefined;
|
|
109
|
-
});
|
|
110
|
-
sessionUserCache.set(app, promise);
|
|
111
|
-
}
|
|
112
|
-
promise.then((user) => {
|
|
113
|
-
if (!cancelled) setState({ user, resolved: true });
|
|
114
|
-
});
|
|
115
|
-
return () => {
|
|
116
|
-
cancelled = true;
|
|
117
|
-
};
|
|
118
|
-
}, [app, fixtureMode]);
|
|
119
|
-
|
|
120
|
-
return state;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
76
|
// ---------------------------------------------------------------------------
|
|
124
77
|
// Mark-as-read RPCs
|
|
125
78
|
// ---------------------------------------------------------------------------
|
|
@@ -174,7 +127,7 @@ export function useNotifications(): UseNotificationsResult {
|
|
|
174
127
|
const config = useFrappeConfig();
|
|
175
128
|
const app = useFrappeApp();
|
|
176
129
|
const fixtureMode = Boolean(config?.fixtures);
|
|
177
|
-
const { user: sessionUser, resolved: sessionResolved } =
|
|
130
|
+
const { user: sessionUser, resolved: sessionResolved } = useSessionIdentity();
|
|
178
131
|
|
|
179
132
|
// Stable connection config (fresh identities would churn the collection).
|
|
180
133
|
const connectionConfig = useMemo(
|
|
@@ -7,8 +7,9 @@ import React from "react";
|
|
|
7
7
|
* frappe.client.get_value(User, <name>, ["full_name","user_image","email"]).
|
|
8
8
|
*/
|
|
9
9
|
import type { FrappeApp } from "@multiplatform.one/frappe";
|
|
10
|
-
import { useFrappeConfig } from "@multiplatform.one/frappe-ui";
|
|
10
|
+
import { useFrappeConfig, type FrappeConfig } from "@multiplatform.one/frappe-ui";
|
|
11
11
|
import { useEffect, useMemo, useState } from "react";
|
|
12
|
+
import { clearSessionIdentityCache, loadSessionUser, sessionUserKey } from "../shared/sessionUser";
|
|
12
13
|
import { useFrappeApp } from "../shared/useFrappeApp";
|
|
13
14
|
|
|
14
15
|
export interface SessionUserInfo {
|
|
@@ -26,14 +27,12 @@ interface UserDocBits {
|
|
|
26
27
|
email?: string;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
// entry (useFrappeApp memoizes apps per config). Failed fetches self-evict
|
|
31
|
-
// so the next mount retries.
|
|
32
|
-
let sessionCache = new WeakMap<FrappeApp, Promise<SessionUserInfo>>();
|
|
30
|
+
const sessionCache = new Map<string, Promise<SessionUserInfo>>();
|
|
33
31
|
|
|
34
32
|
/** Logout hygiene: drops every cached session-user fetch in this module. */
|
|
35
33
|
export function clearSessionUserCache(): void {
|
|
36
|
-
sessionCache
|
|
34
|
+
sessionCache.clear();
|
|
35
|
+
clearSessionIdentityCache();
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/** Resolve a User.user_image value (usually "/files/…") to an absolute URL. */
|
|
@@ -47,8 +46,13 @@ export function resolveUserImageUrl(
|
|
|
47
46
|
return `${baseURL.replace(/\/+$/, "")}${userImage.startsWith("/") ? "" : "/"}${userImage}`;
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
async function fetchSessionUserInfo(
|
|
51
|
-
|
|
49
|
+
async function fetchSessionUserInfo(
|
|
50
|
+
app: FrappeApp,
|
|
51
|
+
config: FrappeConfig,
|
|
52
|
+
): Promise<SessionUserInfo> {
|
|
53
|
+
const user = await loadSessionUser(config);
|
|
54
|
+
const baseURL = config.baseURL ?? "";
|
|
55
|
+
if (user === "Guest") return { user, fullName: user };
|
|
52
56
|
if (!user) throw new Error("No session user");
|
|
53
57
|
// Param format probed against the bench: filters takes the plain docname,
|
|
54
58
|
// fieldname a JSON array (the http client JSON-encodes non-string params).
|
|
@@ -74,26 +78,27 @@ export function useSessionUserInfo(): {
|
|
|
74
78
|
} {
|
|
75
79
|
const app = useFrappeApp();
|
|
76
80
|
const config = useFrappeConfig();
|
|
77
|
-
const
|
|
81
|
+
const key = config ? sessionUserKey(config) : "";
|
|
78
82
|
const [info, setInfo] = useState<SessionUserInfo | undefined>(undefined);
|
|
79
83
|
const [loading, setLoading] = useState(true);
|
|
80
84
|
const [error, setError] = useState<Error | undefined>(undefined);
|
|
81
85
|
|
|
82
86
|
useEffect(() => {
|
|
83
|
-
if (!app) {
|
|
87
|
+
if (!app || !config || config.fixtures) {
|
|
84
88
|
setInfo(undefined);
|
|
85
89
|
setLoading(false);
|
|
86
90
|
return;
|
|
87
91
|
}
|
|
88
92
|
let cancelled = false;
|
|
89
|
-
let promise = sessionCache.get(
|
|
93
|
+
let promise = sessionCache.get(key);
|
|
90
94
|
if (!promise) {
|
|
91
|
-
promise = fetchSessionUserInfo(app,
|
|
92
|
-
sessionCache.set(
|
|
95
|
+
promise = fetchSessionUserInfo(app, config);
|
|
96
|
+
sessionCache.set(key, promise);
|
|
93
97
|
promise.catch(() => {
|
|
94
|
-
if (sessionCache.get(
|
|
98
|
+
if (sessionCache.get(key) === promise) sessionCache.delete(key);
|
|
95
99
|
});
|
|
96
100
|
}
|
|
101
|
+
setInfo(undefined);
|
|
97
102
|
setLoading(true);
|
|
98
103
|
setError(undefined);
|
|
99
104
|
promise
|
|
@@ -110,7 +115,7 @@ export function useSessionUserInfo(): {
|
|
|
110
115
|
return () => {
|
|
111
116
|
cancelled = true;
|
|
112
117
|
};
|
|
113
|
-
}, [app,
|
|
118
|
+
}, [app, key, config]);
|
|
114
119
|
|
|
115
120
|
return useMemo(() => ({ info, loading, error }), [info, loading, error]);
|
|
116
121
|
}
|
|
@@ -149,13 +149,13 @@ describe("DocFormScreen with DocType meta refused (LC-96)", () => {
|
|
|
149
149
|
expect(screen.queryByRole("button", { name: /retry/i })).toBeNull();
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
-
it("
|
|
152
|
+
it("explains a create refusal without exposing server internals", async () => {
|
|
153
153
|
renderForm("new");
|
|
154
154
|
const state = await screen.findByTestId("frappe-form-not-permitted", {}, settle);
|
|
155
155
|
expect(state.textContent).toContain(`You can't create ${doctype}`);
|
|
156
156
|
const lines = Array.from(state.querySelectorAll("p")).map((node) => node.textContent);
|
|
157
|
-
expect(lines).toContain("You
|
|
158
|
-
expect(
|
|
157
|
+
expect(lines).toContain("You don't have permission to create this document.");
|
|
158
|
+
expect(state.textContent).not.toMatch(/frappe\.|whitelisted|Function/);
|
|
159
159
|
expect(lines.some((line) => line?.includes("accessFunction"))).toBe(false);
|
|
160
160
|
expect(button("Save")).toBeUndefined();
|
|
161
161
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
1
2
|
// LC-96 on the desk list (MPO-477), the list twin of MPO-473. Signed out,
|
|
2
3
|
// pokemon's bench refuses getdoctype, get_count and every desk RPC to Guest
|
|
3
4
|
// while Guest can still read the rows through /api/resource, and the desk list
|
|
@@ -11,6 +12,12 @@ import { BackofficeProvider } from "../config/BackofficeProvider";
|
|
|
11
12
|
import { createMemoryNavigator } from "../navigation/navigator";
|
|
12
13
|
import { DoctypeListScreen } from "./DoctypeListScreen";
|
|
13
14
|
|
|
15
|
+
const layout = vi.hoisted(() => ({ size: "wide" }));
|
|
16
|
+
vi.mock("@multiplatform.one/theme", async (importOriginal) => ({
|
|
17
|
+
...(await importOriginal<typeof import("@multiplatform.one/theme")>()),
|
|
18
|
+
useLayoutSizeClass: () => layout.size,
|
|
19
|
+
}));
|
|
20
|
+
|
|
14
21
|
const doctype = "Pokemon";
|
|
15
22
|
|
|
16
23
|
const rows = [
|
|
@@ -29,6 +36,13 @@ const rows = [
|
|
|
29
36
|
pokemon_name: "Bulbasaur",
|
|
30
37
|
species: "Seed Pokémon",
|
|
31
38
|
base_experience: 64,
|
|
39
|
+
image: "/files/bulbasaur.png",
|
|
40
|
+
height: 7,
|
|
41
|
+
weight: 69,
|
|
42
|
+
habitat: "grassland",
|
|
43
|
+
color: "green",
|
|
44
|
+
ability: "overgrow",
|
|
45
|
+
title: "Seed monster",
|
|
32
46
|
},
|
|
33
47
|
{
|
|
34
48
|
name: "ivysaur",
|
|
@@ -61,6 +75,8 @@ let run = 0;
|
|
|
61
75
|
let metaAnswer: "refused" | "broken" = "refused";
|
|
62
76
|
let rowsReadable = true;
|
|
63
77
|
let metaReads = 0;
|
|
78
|
+
let countReads = 0;
|
|
79
|
+
let countGate: Promise<void> | undefined;
|
|
64
80
|
|
|
65
81
|
function json(body: unknown, status = 200) {
|
|
66
82
|
return new Response(JSON.stringify(body), {
|
|
@@ -83,9 +99,12 @@ function refused(method: string) {
|
|
|
83
99
|
|
|
84
100
|
beforeEach(() => {
|
|
85
101
|
run += 1;
|
|
102
|
+
layout.size = "wide";
|
|
86
103
|
metaAnswer = "refused";
|
|
87
104
|
rowsReadable = true;
|
|
88
105
|
metaReads = 0;
|
|
106
|
+
countReads = 0;
|
|
107
|
+
countGate = undefined;
|
|
89
108
|
vi.stubGlobal("fetch", async (input: string | URL | Request) => {
|
|
90
109
|
const url = new URL(String(input instanceof Request ? input.url : input));
|
|
91
110
|
const method = url.pathname.replace(/^\/api\/method\//, "");
|
|
@@ -95,6 +114,11 @@ beforeEach(() => {
|
|
|
95
114
|
? refused(method)
|
|
96
115
|
: json({ exc_type: "ValidationError", exception: "Pokemon is being migrated" }, 417);
|
|
97
116
|
}
|
|
117
|
+
if (method === "frappe.client.get_count") {
|
|
118
|
+
countReads += 1;
|
|
119
|
+
await countGate;
|
|
120
|
+
return refused(method);
|
|
121
|
+
}
|
|
98
122
|
if (url.pathname.startsWith("/api/method/")) return refused(method);
|
|
99
123
|
if (url.pathname === `/api/resource/${doctype}`) {
|
|
100
124
|
if (!rowsReadable) {
|
|
@@ -112,15 +136,16 @@ afterEach(() => {
|
|
|
112
136
|
vi.unstubAllGlobals();
|
|
113
137
|
});
|
|
114
138
|
|
|
115
|
-
function renderList() {
|
|
116
|
-
|
|
139
|
+
function renderList(strict = false) {
|
|
140
|
+
const tree = (
|
|
117
141
|
<BackofficeProvider
|
|
118
142
|
frappe={{ baseURL: `https://desk-list-guest-${run}.spec.test` }}
|
|
119
143
|
navigation={createMemoryNavigator("/backoffice")}
|
|
120
144
|
>
|
|
121
145
|
<DoctypeListScreen doctype={doctype} />
|
|
122
|
-
</BackofficeProvider
|
|
146
|
+
</BackofficeProvider>
|
|
123
147
|
);
|
|
148
|
+
return renderWithProviders(strict ? <StrictMode>{tree}</StrictMode> : tree);
|
|
124
149
|
}
|
|
125
150
|
|
|
126
151
|
function button(name: string): HTMLElement | undefined {
|
|
@@ -130,6 +155,40 @@ function button(name: string): HTMLElement | undefined {
|
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
describe("DoctypeListScreen with DocType meta refused (LC-96)", () => {
|
|
158
|
+
it.each(["wide", "compact"])(
|
|
159
|
+
"owns a bounded vertical scroll region in %s layout",
|
|
160
|
+
async (size) => {
|
|
161
|
+
layout.size = size;
|
|
162
|
+
renderList();
|
|
163
|
+
await waitFor(() => expect(screen.getByText("Bulbasaur")).toBeTruthy(), settle);
|
|
164
|
+
const region = screen.getByTestId("backoffice-list-scroll");
|
|
165
|
+
expect(["auto", "scroll"]).toContain(getComputedStyle(region).overflowY);
|
|
166
|
+
expect(getComputedStyle(region).minHeight).toBe("0px");
|
|
167
|
+
const content = region.firstElementChild?.firstElementChild as HTMLElement;
|
|
168
|
+
expect(getComputedStyle(content).flexBasis).toBe("auto");
|
|
169
|
+
expect(getComputedStyle(content).flexShrink).toBe("0");
|
|
170
|
+
expect(region.contains(screen.getByRole("heading", { name: doctype }))).toBe(false);
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
it("renders headers on wide screens and bounds sampled columns", async () => {
|
|
175
|
+
renderList();
|
|
176
|
+
await waitFor(() => expect(screen.getByText("Bulbasaur")).toBeTruthy(), settle);
|
|
177
|
+
expect(screen.getAllByRole("columnheader").length).toBeGreaterThan(0);
|
|
178
|
+
expect(screen.queryByText("/files/bulbasaur.png")).toBeNull();
|
|
179
|
+
expect(screen.queryByText("overgrow")).toBeNull();
|
|
180
|
+
expect(screen.getByText("Seed monster")).toBeTruthy();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("keeps compact rows in a bounded document list", async () => {
|
|
184
|
+
layout.size = "compact";
|
|
185
|
+
renderList();
|
|
186
|
+
await waitFor(() => expect(screen.getByText("Bulbasaur")).toBeTruthy(), settle);
|
|
187
|
+
expect(screen.queryAllByRole("columnheader")).toEqual([]);
|
|
188
|
+
expect(screen.queryByText("/files/bulbasaur.png")).toBeNull();
|
|
189
|
+
expect(screen.queryByText("overgrow")).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
133
192
|
it("lists the rows Guest can read, laid out from a record's own fields", async () => {
|
|
134
193
|
renderList();
|
|
135
194
|
await waitFor(() => expect(screen.getByText("Bulbasaur")).toBeTruthy(), settle);
|
|
@@ -179,3 +238,23 @@ describe("DoctypeListScreen with DocType meta refused (LC-96)", () => {
|
|
|
179
238
|
await waitFor(() => expect(metaReads).toBe(2), settle);
|
|
180
239
|
});
|
|
181
240
|
});
|
|
241
|
+
|
|
242
|
+
it("requests one count for the list filter state, including strict effect replay", async () => {
|
|
243
|
+
renderList(true);
|
|
244
|
+
await waitFor(() => expect(screen.getByText("Bulbasaur")).toBeTruthy(), settle);
|
|
245
|
+
expect(countReads).toBe(1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("renders rows before a slow count settles", async () => {
|
|
249
|
+
let release!: () => void;
|
|
250
|
+
countGate = new Promise<void>((resolve) => {
|
|
251
|
+
release = resolve;
|
|
252
|
+
});
|
|
253
|
+
try {
|
|
254
|
+
renderList();
|
|
255
|
+
await waitFor(() => expect(screen.getByText("Bulbasaur")).toBeTruthy(), settle);
|
|
256
|
+
expect(countReads).toBe(1);
|
|
257
|
+
} finally {
|
|
258
|
+
release();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
Spinner,
|
|
18
18
|
XStack,
|
|
19
19
|
YStack,
|
|
20
|
-
isWeb,
|
|
21
20
|
notify,
|
|
22
21
|
} from "@multiplatform.one/components";
|
|
23
22
|
import { useLayoutSizeClass, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
@@ -235,7 +234,16 @@ export function DoctypeListScreen({
|
|
|
235
234
|
(recordShape.fields
|
|
236
235
|
? {
|
|
237
236
|
name: doctype,
|
|
238
|
-
fields: recordShape.fields
|
|
237
|
+
fields: [...recordShape.fields]
|
|
238
|
+
.sort(
|
|
239
|
+
(a, b) =>
|
|
240
|
+
Number(b.fieldname === "title" || b.fieldname === "name") -
|
|
241
|
+
Number(a.fieldname === "title" || a.fieldname === "name"),
|
|
242
|
+
)
|
|
243
|
+
.map((field, index) => ({
|
|
244
|
+
...field,
|
|
245
|
+
in_list_view: index < 6 ? (1 as const) : (0 as const),
|
|
246
|
+
})),
|
|
239
247
|
}
|
|
240
248
|
: undefined),
|
|
241
249
|
[meta, recordShape.fields, doctype],
|
|
@@ -981,7 +989,7 @@ export function DoctypeListScreen({
|
|
|
981
989
|
);
|
|
982
990
|
|
|
983
991
|
return (
|
|
984
|
-
<YStack flex={1} {...knobProps.gap} {...knobProps.panelPadding}>
|
|
992
|
+
<YStack flex={1} minHeight={0} {...knobProps.gap} {...knobProps.panelPadding}>
|
|
985
993
|
<PageHeader
|
|
986
994
|
gap="$2"
|
|
987
995
|
heading={
|
|
@@ -1012,7 +1020,7 @@ export function DoctypeListScreen({
|
|
|
1012
1020
|
|
|
1013
1021
|
{(() => {
|
|
1014
1022
|
const listBody = (
|
|
1015
|
-
<XStack
|
|
1023
|
+
<XStack flexGrow={1} flexShrink={0} flexBasis="auto" gap="$2" alignItems="stretch">
|
|
1016
1024
|
{/* Facet sidebar needs bench RPCs (fixture mode hides it) and is
|
|
1017
1025
|
hidden on narrow viewports (< $sm) like desk. MOD-04 keeps it:
|
|
1018
1026
|
facet clicks toggle tuples through the same baseFilters
|
|
@@ -1077,6 +1085,7 @@ export function DoctypeListScreen({
|
|
|
1077
1085
|
refreshToken={refreshGen}
|
|
1078
1086
|
enablePagination={!fixtureMode}
|
|
1079
1087
|
paginationMode="infinite"
|
|
1088
|
+
fetchTotalCount={false}
|
|
1080
1089
|
compact
|
|
1081
1090
|
// ONE FILTER SYSTEM (MOD-04 / LC-16): the list chrome is
|
|
1082
1091
|
// the single filtering authority; tuples reach the table
|
|
@@ -1094,20 +1103,17 @@ export function DoctypeListScreen({
|
|
|
1094
1103
|
onRowClick={handleRowClick}
|
|
1095
1104
|
onStateChange={handleStateChange}
|
|
1096
1105
|
rights={rights}
|
|
1097
|
-
layout="list"
|
|
1106
|
+
layout={compactChrome ? "list" : "table"}
|
|
1098
1107
|
/>
|
|
1099
1108
|
)}
|
|
1100
1109
|
</YStack>
|
|
1101
1110
|
</XStack>
|
|
1102
1111
|
);
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
// stays pinned above it). Wide web keeps the flex layout as-is —
|
|
1109
|
-
// the grid body scrolls internally.
|
|
1110
|
-
return isWeb && !compactChrome ? listBody : <ScrollView flex={1}>{listBody}</ScrollView>;
|
|
1112
|
+
return (
|
|
1113
|
+
<ScrollView flex={1} minHeight={0} testID="backoffice-list-scroll">
|
|
1114
|
+
{listBody}
|
|
1115
|
+
</ScrollView>
|
|
1116
|
+
);
|
|
1111
1117
|
})()}
|
|
1112
1118
|
|
|
1113
1119
|
{showNew && (
|
|
@@ -152,3 +152,15 @@ describe("<DoctypeReportView />", () => {
|
|
|
152
152
|
).toBeTruthy();
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
it("keeps the report toolbar outside a bounded vertical body scrollport", async () => {
|
|
157
|
+
renderReport();
|
|
158
|
+
await waitFor(() => expect(screen.getByText("ACME Corp")).toBeTruthy());
|
|
159
|
+
const region = screen.getByTestId("frappe-report-scroll");
|
|
160
|
+
expect(["auto", "scroll"]).toContain(getComputedStyle(region).overflowY);
|
|
161
|
+
expect(getComputedStyle(region).minHeight).toBe("0px");
|
|
162
|
+
const content = region.firstElementChild?.firstElementChild as HTMLElement;
|
|
163
|
+
expect(getComputedStyle(content).flexBasis).toBe("auto");
|
|
164
|
+
expect(getComputedStyle(content).flexShrink).toBe("0");
|
|
165
|
+
expect(region.querySelector('[aria-label="Columns"]')).toBeNull();
|
|
166
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FrappeProvider } from "@multiplatform.one/frappe-ui";
|
|
3
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
4
|
+
import { cleanup, screen, waitFor } from "@testing-library/react";
|
|
5
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
6
|
+
import { useWorkspaces } from "../shell/useWorkspaces";
|
|
7
|
+
import { useSessionUserInfo } from "../chrome/user-session";
|
|
8
|
+
import { useNotifications } from "../chrome/notifications-data";
|
|
9
|
+
import { clearSessionUserCache } from "../chrome/user-session";
|
|
10
|
+
import { loadSessionUser } from "./sessionUser";
|
|
11
|
+
import { useSessionUserId } from "./useSessionUserId";
|
|
12
|
+
|
|
13
|
+
let run = 0;
|
|
14
|
+
let reads = 0;
|
|
15
|
+
let status = 200;
|
|
16
|
+
function Consumers() {
|
|
17
|
+
const workspaces = useWorkspaces();
|
|
18
|
+
const { info, loading } = useSessionUserInfo();
|
|
19
|
+
useNotifications();
|
|
20
|
+
const id = useSessionUserId();
|
|
21
|
+
return (
|
|
22
|
+
<div data-testid="session">
|
|
23
|
+
{JSON.stringify({ workspace: workspaces.sessionUser, user: info?.user, loading, id })}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
function Tree({ token = "first" }: { token?: string }) {
|
|
28
|
+
return (
|
|
29
|
+
<FrappeProvider
|
|
30
|
+
baseURL={`https://shared-session-${run}.spec.test`}
|
|
31
|
+
auth={{ useToken: true, token, type: "Bearer" }}
|
|
32
|
+
realtime={false}
|
|
33
|
+
>
|
|
34
|
+
<Consumers />
|
|
35
|
+
</FrappeProvider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
run += 1;
|
|
40
|
+
reads = 0;
|
|
41
|
+
status = 200;
|
|
42
|
+
vi.stubGlobal("fetch", async (input: string | URL | Request) => {
|
|
43
|
+
const url = new URL(String(input instanceof Request ? input.url : input));
|
|
44
|
+
let body: unknown = { data: [], message: {} };
|
|
45
|
+
if (url.pathname.endsWith("frappe.auth.get_logged_user")) {
|
|
46
|
+
reads += 1;
|
|
47
|
+
body = status === 200 ? { message: "reader@example.test" } : { exc_type: "PermissionError" };
|
|
48
|
+
return new Response(JSON.stringify(body), {
|
|
49
|
+
status,
|
|
50
|
+
headers: { "content-type": "application/json" },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return new Response(JSON.stringify(body), { headers: { "content-type": "application/json" } });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
cleanup();
|
|
58
|
+
vi.unstubAllGlobals();
|
|
59
|
+
});
|
|
60
|
+
it.each([200, 401, 403])(
|
|
61
|
+
"shares identity across consumers and remounts after %s",
|
|
62
|
+
async (answer) => {
|
|
63
|
+
status = answer;
|
|
64
|
+
const view = renderWithProviders(<Tree />);
|
|
65
|
+
await waitFor(() =>
|
|
66
|
+
expect(screen.getByTestId("session").textContent).toContain('"loading":false'),
|
|
67
|
+
);
|
|
68
|
+
expect(reads).toBe(1);
|
|
69
|
+
view.unmount();
|
|
70
|
+
renderWithProviders(<Tree />);
|
|
71
|
+
await waitFor(() =>
|
|
72
|
+
expect(screen.getByTestId("session").textContent).toContain('"loading":false'),
|
|
73
|
+
);
|
|
74
|
+
expect(reads).toBe(1);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
it("refetches identity when credentials change", async () => {
|
|
78
|
+
const view = renderWithProviders(<Tree />);
|
|
79
|
+
await waitFor(() =>
|
|
80
|
+
expect(screen.getByTestId("session").textContent).toContain('"loading":false'),
|
|
81
|
+
);
|
|
82
|
+
expect(reads).toBe(1);
|
|
83
|
+
view.rerender(<Tree token="second" />);
|
|
84
|
+
await waitFor(() => expect(reads).toBe(2));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("does not retain a failed network request", async () => {
|
|
88
|
+
const request = vi.fn().mockRejectedValue(new TypeError("Network unavailable"));
|
|
89
|
+
vi.stubGlobal("fetch", request);
|
|
90
|
+
const config = { baseURL: `https://network-session-${run}.spec.test` };
|
|
91
|
+
await expect(loadSessionUser(config)).rejects.toThrow("Network unavailable");
|
|
92
|
+
request.mockResolvedValue(
|
|
93
|
+
new Response(JSON.stringify({ message: "reader" }), {
|
|
94
|
+
headers: { "content-type": "application/json" },
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
await expect(loadSessionUser(config)).resolves.toBe("reader");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("clears the shared identity when the user menu clears session caches", async () => {
|
|
101
|
+
const config = { baseURL: `https://logout-session-${run}.spec.test` };
|
|
102
|
+
await loadSessionUser(config);
|
|
103
|
+
clearSessionUserCache();
|
|
104
|
+
await loadSessionUser(config);
|
|
105
|
+
expect(reads).toBe(2);
|
|
106
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { HttpClient, type AuthConfig } from "@multiplatform.one/frappe";
|
|
2
|
+
import { useFrappeConfig, type FrappeConfig } from "@multiplatform.one/frappe-ui";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
const cache = new Map<string, Promise<string>>();
|
|
6
|
+
|
|
7
|
+
export function sessionUserKey(config: Pick<FrappeConfig, "baseURL" | "auth">): string {
|
|
8
|
+
const auth = config.auth;
|
|
9
|
+
return `${config.baseURL}:${auth?.useToken ? "t" : ""}:${auth?.token ?? ""}:${auth?.type ?? ""}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function clearSessionIdentityCache(): void {
|
|
13
|
+
cache.clear();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function loadSessionUser(config: Pick<FrappeConfig, "baseURL" | "auth">): Promise<string> {
|
|
17
|
+
if (!config.baseURL) return Promise.reject(new Error("No session endpoint"));
|
|
18
|
+
const key = sessionUserKey(config);
|
|
19
|
+
let promise = cache.get(key);
|
|
20
|
+
if (!promise) {
|
|
21
|
+
const client = new HttpClient(config.baseURL, config.auth as AuthConfig | undefined);
|
|
22
|
+
promise = client
|
|
23
|
+
.get<{ message?: string }>("/api/method/frappe.auth.get_logged_user")
|
|
24
|
+
.then((response) => response.payload?.message || "Guest")
|
|
25
|
+
.catch((error: unknown) => {
|
|
26
|
+
const status = (error as { status?: number })?.status;
|
|
27
|
+
if (status === 401 || status === 403) return "Guest";
|
|
28
|
+
if (cache.get(key) === promise) cache.delete(key);
|
|
29
|
+
throw error;
|
|
30
|
+
});
|
|
31
|
+
cache.set(key, promise);
|
|
32
|
+
}
|
|
33
|
+
return promise;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useSessionIdentity(): { user?: string; resolved: boolean } {
|
|
37
|
+
const config = useFrappeConfig();
|
|
38
|
+
const key = config?.baseURL && !config.fixtures ? sessionUserKey(config) : undefined;
|
|
39
|
+
const [state, setState] = useState<{ key?: string; user?: string; resolved: boolean }>({
|
|
40
|
+
resolved: false,
|
|
41
|
+
});
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!key || !config) return;
|
|
44
|
+
let cancelled = false;
|
|
45
|
+
loadSessionUser(config)
|
|
46
|
+
.then((user) => {
|
|
47
|
+
if (!cancelled)
|
|
48
|
+
setState({ key, user: user === "Guest" ? undefined : user, resolved: true });
|
|
49
|
+
})
|
|
50
|
+
.catch(() => {
|
|
51
|
+
if (!cancelled) setState({ key, resolved: true });
|
|
52
|
+
});
|
|
53
|
+
return () => {
|
|
54
|
+
cancelled = true;
|
|
55
|
+
};
|
|
56
|
+
}, [key, config]);
|
|
57
|
+
if (!key) return { resolved: true };
|
|
58
|
+
return state.key === key ? state : { resolved: false };
|
|
59
|
+
}
|
|
@@ -1,43 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
// (The shell's useSessionUser in shell/useWorkspaces.ts is the HttpClient
|
|
3
|
-
// variant used by the sidebar; this one rides the FrappeApp auth module.)
|
|
4
|
-
import { useFrappeConfig } from "@multiplatform.one/frappe-ui";
|
|
5
|
-
import { useEffect, useState } from "react";
|
|
6
|
-
import { useFrappeApp } from "./useFrappeApp";
|
|
1
|
+
import { useSessionIdentity } from "./sessionUser";
|
|
7
2
|
|
|
8
|
-
const userCache = new Map<string, Promise<string>>();
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The logged-in user's id (`frappe.auth.get_logged_user`), or undefined
|
|
12
|
-
* while loading / in fixture mode (no session exists against fixtures).
|
|
13
|
-
*/
|
|
14
3
|
export function useSessionUserId(): string | undefined {
|
|
15
|
-
|
|
16
|
-
const app = useFrappeApp();
|
|
17
|
-
const fixtureMode = Boolean(config?.fixtures);
|
|
18
|
-
const [user, setUser] = useState<string | undefined>(undefined);
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
const baseURL = config?.baseURL;
|
|
22
|
-
if (!app || fixtureMode || !baseURL) return;
|
|
23
|
-
let cancelled = false;
|
|
24
|
-
let promise = userCache.get(baseURL);
|
|
25
|
-
if (!promise) {
|
|
26
|
-
promise = app.auth().getLoggedInUser();
|
|
27
|
-
userCache.set(baseURL, promise);
|
|
28
|
-
promise.catch(() => userCache.delete(baseURL));
|
|
29
|
-
}
|
|
30
|
-
promise
|
|
31
|
-
.then((value) => {
|
|
32
|
-
if (!cancelled && value) setUser(value);
|
|
33
|
-
})
|
|
34
|
-
.catch(() => {
|
|
35
|
-
// No session — hearts render unfilled, "me" facets hidden.
|
|
36
|
-
});
|
|
37
|
-
return () => {
|
|
38
|
-
cancelled = true;
|
|
39
|
-
};
|
|
40
|
-
}, [app, fixtureMode, config?.baseURL]);
|
|
41
|
-
|
|
42
|
-
return user;
|
|
4
|
+
return useSessionIdentity().user;
|
|
43
5
|
}
|