@etsoo/toolpad 1.0.0
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/.github/workflows/main.yml +49 -0
- package/.vscode/settings.json +3 -0
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/build/src/Account/Account.d.ts +71 -0
- package/build/src/Account/Account.js +117 -0
- package/build/src/Account/Account.test.d.ts +1 -0
- package/build/src/Account/Account.test.js +35 -0
- package/build/src/Account/AccountPopoverFooter.d.ts +6 -0
- package/build/src/Account/AccountPopoverFooter.js +21 -0
- package/build/src/Account/AccountPopoverHeader.d.ts +6 -0
- package/build/src/Account/AccountPopoverHeader.js +15 -0
- package/build/src/Account/AccountPreview.d.ts +67 -0
- package/build/src/Account/AccountPreview.js +76 -0
- package/build/src/Account/AccountPreview.test.d.ts +1 -0
- package/build/src/Account/AccountPreview.test.js +47 -0
- package/build/src/Account/SignInButton.d.ts +2 -0
- package/build/src/Account/SignInButton.js +26 -0
- package/build/src/Account/SignOutButton.d.ts +3 -0
- package/build/src/Account/SignOutButton.js +28 -0
- package/build/src/Account/index.d.ts +6 -0
- package/build/src/Account/index.js +6 -0
- package/build/src/AppProvider/AppProvider.d.ts +98 -0
- package/build/src/AppProvider/AppProvider.js +4 -0
- package/build/src/AppProvider/AppProvider.test.d.ts +4 -0
- package/build/src/AppProvider/AppProvider.test.js +16 -0
- package/build/src/AppProvider/AppProviderComponent.d.ts +17 -0
- package/build/src/AppProvider/AppProviderComponent.js +120 -0
- package/build/src/AppProvider/AppThemeProvider.d.ts +12 -0
- package/build/src/AppProvider/AppThemeProvider.js +73 -0
- package/build/src/AppProvider/index.d.ts +1 -0
- package/build/src/AppProvider/index.js +1 -0
- package/build/src/DashboardLayout/DashboardLayout.d.ts +83 -0
- package/build/src/DashboardLayout/DashboardLayout.js +286 -0
- package/build/src/DashboardLayout/DashboardLayout.test.d.ts +1 -0
- package/build/src/DashboardLayout/DashboardLayout.test.js +291 -0
- package/build/src/DashboardLayout/DashboardSidebarSubNavigation.d.ts +16 -0
- package/build/src/DashboardLayout/DashboardSidebarSubNavigation.js +149 -0
- package/build/src/DashboardLayout/ThemeSwitcher.d.ts +5 -0
- package/build/src/DashboardLayout/ThemeSwitcher.js +40 -0
- package/build/src/DashboardLayout/ToolbarActions.d.ts +5 -0
- package/build/src/DashboardLayout/ToolbarActions.js +9 -0
- package/build/src/DashboardLayout/index.d.ts +2 -0
- package/build/src/DashboardLayout/index.js +3 -0
- package/build/src/DashboardLayout/utils.d.ts +8 -0
- package/build/src/DashboardLayout/utils.js +16 -0
- package/build/src/PageContainer/PageContainer.d.ts +66 -0
- package/build/src/PageContainer/PageContainer.js +97 -0
- package/build/src/PageContainer/PageContainer.test.d.ts +1 -0
- package/build/src/PageContainer/PageContainer.test.js +127 -0
- package/build/src/PageContainer/PageContainerToolbar.d.ts +16 -0
- package/build/src/PageContainer/PageContainerToolbar.js +24 -0
- package/build/src/PageContainer/PageContainerToolbar.test.d.ts +1 -0
- package/build/src/PageContainer/PageContainerToolbar.test.js +10 -0
- package/build/src/PageContainer/index.d.ts +2 -0
- package/build/src/PageContainer/index.js +2 -0
- package/build/src/index.d.ts +8 -0
- package/build/src/index.js +8 -0
- package/build/src/nextjs/AppProvider.d.ts +6 -0
- package/build/src/nextjs/AppProvider.js +16 -0
- package/build/src/nextjs/AppProvider.test.d.ts +1 -0
- package/build/src/nextjs/AppProvider.test.js +36 -0
- package/build/src/nextjs/AppProviderNextApp.d.ts +5 -0
- package/build/src/nextjs/AppProviderNextApp.js +27 -0
- package/build/src/nextjs/AppProviderNextPages.d.ts +5 -0
- package/build/src/nextjs/AppProviderNextPages.js +37 -0
- package/build/src/nextjs/index.d.ts +1 -0
- package/build/src/nextjs/index.js +1 -0
- package/build/src/persistence/codec.d.ts +47 -0
- package/build/src/persistence/codec.js +57 -0
- package/build/src/persistence/index.d.ts +1 -0
- package/build/src/persistence/index.js +1 -0
- package/build/src/persistence/useStorageState.d.ts +50 -0
- package/build/src/persistence/useStorageState.js +116 -0
- package/build/src/persistence/useStorageState.test.d.ts +1 -0
- package/build/src/persistence/useStorageState.test.js +61 -0
- package/build/src/react-router-dom/AppProvider.d.ts +6 -0
- package/build/src/react-router-dom/AppProvider.js +29 -0
- package/build/src/react-router-dom/AppProvider.test.d.ts +1 -0
- package/build/src/react-router-dom/AppProvider.test.js +12 -0
- package/build/src/react-router-dom/index.d.ts +1 -0
- package/build/src/react-router-dom/index.js +1 -0
- package/build/src/shared/Link.d.ts +8 -0
- package/build/src/shared/Link.js +19 -0
- package/build/src/shared/branding.d.ts +1 -0
- package/build/src/shared/branding.js +6 -0
- package/build/src/shared/components.d.ts +5 -0
- package/build/src/shared/components.js +23 -0
- package/build/src/shared/context.d.ts +12 -0
- package/build/src/shared/context.js +10 -0
- package/build/src/shared/locales/LocaleContext.d.ts +19 -0
- package/build/src/shared/locales/LocaleContext.js +18 -0
- package/build/src/shared/locales/en.d.ts +6 -0
- package/build/src/shared/locales/en.js +8 -0
- package/build/src/shared/navigation.d.ts +16 -0
- package/build/src/shared/navigation.js +129 -0
- package/build/src/useActivePage/index.d.ts +1 -0
- package/build/src/useActivePage/index.js +1 -0
- package/build/src/useActivePage/useActivePage.d.ts +11 -0
- package/build/src/useActivePage/useActivePage.js +48 -0
- package/build/src/useLocalStorageState/index.d.ts +1 -0
- package/build/src/useLocalStorageState/index.js +1 -0
- package/build/src/useLocalStorageState/useLocalStorageState.d.ts +2 -0
- package/build/src/useLocalStorageState/useLocalStorageState.js +14 -0
- package/build/src/useSessionStorageState/index.d.ts +1 -0
- package/build/src/useSessionStorageState/index.js +1 -0
- package/build/src/useSessionStorageState/useSessionStorageState.d.ts +2 -0
- package/build/src/useSessionStorageState/useSessionStorageState.js +14 -0
- package/build/src/utils/collections.d.ts +44 -0
- package/build/src/utils/collections.js +43 -0
- package/build/src/utils/describeConformance.d.ts +7 -0
- package/build/src/utils/describeConformance.js +3 -0
- package/build/src/utils/events.d.ts +27 -0
- package/build/src/utils/events.js +54 -0
- package/build/src/utils/hooks/index.d.ts +2 -0
- package/build/src/utils/hooks/index.js +2 -0
- package/build/src/utils/hooks/useBoolean.d.ts +11 -0
- package/build/src/utils/hooks/useBoolean.js +11 -0
- package/build/src/utils/hooks/useDebounced.d.ts +8 -0
- package/build/src/utils/hooks/useDebounced.js +28 -0
- package/build/src/utils/hooks/useDebouncedHandler.d.ts +12 -0
- package/build/src/utils/hooks/useDebouncedHandler.js +41 -0
- package/build/src/utils/hooks/useLatest.d.ts +6 -0
- package/build/src/utils/hooks/useLatest.js +9 -0
- package/build/src/utils/hooks/usePageTitle.d.ts +4 -0
- package/build/src/utils/hooks/usePageTitle.js +13 -0
- package/build/src/utils/hooks/useSsr.d.ts +4 -0
- package/build/src/utils/hooks/useSsr.js +16 -0
- package/build/src/utils/hooks/useStorageState.d.ts +10 -0
- package/build/src/utils/hooks/useStorageState.js +51 -0
- package/build/src/utils/warnOnce.d.ts +5 -0
- package/build/src/utils/warnOnce.js +11 -0
- package/build/vitest.setup.d.ts +1 -0
- package/build/vitest.setup.js +19 -0
- package/package.json +60 -0
- package/src/Account/Account.test.tsx +56 -0
- package/src/Account/Account.tsx +226 -0
- package/src/Account/AccountPopoverFooter.tsx +33 -0
- package/src/Account/AccountPopoverHeader.tsx +20 -0
- package/src/Account/AccountPreview.test.tsx +79 -0
- package/src/Account/AccountPreview.tsx +189 -0
- package/src/Account/SignInButton.tsx +38 -0
- package/src/Account/SignOutButton.tsx +44 -0
- package/src/Account/index.ts +6 -0
- package/src/AppProvider/AppProvider.test.tsx +25 -0
- package/src/AppProvider/AppProvider.tsx +116 -0
- package/src/AppProvider/AppProviderComponent.tsx +162 -0
- package/src/AppProvider/AppThemeProvider.tsx +177 -0
- package/src/AppProvider/index.ts +1 -0
- package/src/DashboardLayout/DashboardLayout.test.tsx +429 -0
- package/src/DashboardLayout/DashboardLayout.tsx +558 -0
- package/src/DashboardLayout/DashboardSidebarSubNavigation.tsx +301 -0
- package/src/DashboardLayout/ThemeSwitcher.tsx +81 -0
- package/src/DashboardLayout/ToolbarActions.tsx +11 -0
- package/src/DashboardLayout/index.ts +4 -0
- package/src/DashboardLayout/utils.ts +23 -0
- package/src/PageContainer/PageContainer.test.tsx +194 -0
- package/src/PageContainer/PageContainer.tsx +201 -0
- package/src/PageContainer/PageContainerToolbar.test.tsx +11 -0
- package/src/PageContainer/PageContainerToolbar.tsx +31 -0
- package/src/PageContainer/index.ts +2 -0
- package/src/index.ts +15 -0
- package/src/nextjs/AppProvider.test.tsx +48 -0
- package/src/nextjs/AppProvider.tsx +18 -0
- package/src/nextjs/AppProviderNextApp.tsx +37 -0
- package/src/nextjs/AppProviderNextPages.tsx +52 -0
- package/src/nextjs/index.tsx +1 -0
- package/src/persistence/codec.tsx +81 -0
- package/src/persistence/index.ts +1 -0
- package/src/persistence/useStorageState.test.tsx +101 -0
- package/src/persistence/useStorageState.tsx +244 -0
- package/src/react-router-dom/AppProvider.test.tsx +17 -0
- package/src/react-router-dom/AppProvider.tsx +40 -0
- package/src/react-router-dom/index.tsx +1 -0
- package/src/shared/Link.tsx +37 -0
- package/src/shared/branding.ts +7 -0
- package/src/shared/components.tsx +46 -0
- package/src/shared/context.ts +21 -0
- package/src/shared/locales/LocaleContext.tsx +44 -0
- package/src/shared/locales/en.tsx +9 -0
- package/src/shared/navigation.tsx +197 -0
- package/src/useActivePage/index.ts +1 -0
- package/src/useActivePage/useActivePage.ts +66 -0
- package/src/useLocalStorageState/index.ts +1 -0
- package/src/useLocalStorageState/useLocalStorageState.tsx +23 -0
- package/src/useSessionStorageState/index.tsx +1 -0
- package/src/useSessionStorageState/useSessionStorageState.tsx +23 -0
- package/src/utils/collections.ts +125 -0
- package/src/utils/describeConformance.ts +14 -0
- package/src/utils/events.ts +81 -0
- package/src/utils/hooks/index.ts +2 -0
- package/src/utils/hooks/useBoolean.ts +12 -0
- package/src/utils/hooks/useDebounced.ts +39 -0
- package/src/utils/hooks/useDebouncedHandler.ts +71 -0
- package/src/utils/hooks/useLatest.ts +16 -0
- package/src/utils/hooks/usePageTitle.ts +14 -0
- package/src/utils/hooks/useSsr.ts +24 -0
- package/src/utils/hooks/useStorageState.ts +130 -0
- package/src/utils/warnOnce.ts +12 -0
- package/tsconfig.json +19 -0
- package/vite.config.ts +27 -0
- package/vitest.setup.ts +22 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
function subscribe() {
|
|
3
|
+
return () => { };
|
|
4
|
+
}
|
|
5
|
+
function getSnapshot() {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
function getServerSnapshot() {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Returns true when serverside rendering, or when hydrating.
|
|
13
|
+
*/
|
|
14
|
+
export default function useSsr() {
|
|
15
|
+
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
type Initializer<T> = () => T;
|
|
3
|
+
type UseStorageStateHookResult<T> = [
|
|
4
|
+
T,
|
|
5
|
+
React.Dispatch<React.SetStateAction<T>>
|
|
6
|
+
];
|
|
7
|
+
declare function useStorageStateServer(kind: "session" | "local", key: string, initializer: string | Initializer<string>): UseStorageStateHookResult<string>;
|
|
8
|
+
declare function useStorageStateServer(kind: "session" | "local", key: string, initializer?: string | null | Initializer<string | null>): UseStorageStateHookResult<string | null>;
|
|
9
|
+
declare const _default: typeof useStorageStateServer;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Emitter } from "../events";
|
|
3
|
+
// storage events only work across windows, we'll use an event emitter to announce within the window
|
|
4
|
+
const emitter = new Emitter();
|
|
5
|
+
function subscribe(area, key, cb) {
|
|
6
|
+
const storageHandler = (event) => {
|
|
7
|
+
if (event.storageArea === area && event.key === key) {
|
|
8
|
+
cb();
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
window.addEventListener("storage", storageHandler);
|
|
12
|
+
emitter.on(key, cb);
|
|
13
|
+
return () => {
|
|
14
|
+
window.removeEventListener("storage", storageHandler);
|
|
15
|
+
emitter.off(key, cb);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function getSnapshot(area, key) {
|
|
19
|
+
return area.getItem(key);
|
|
20
|
+
}
|
|
21
|
+
function setValue(area, key, value) {
|
|
22
|
+
if (typeof window !== "undefined") {
|
|
23
|
+
if (value === null) {
|
|
24
|
+
area.removeItem(key);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
area.setItem(key, String(value));
|
|
28
|
+
}
|
|
29
|
+
emitter.emit(key, null);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function useStorageStateServer(kind, key, initializer = null) {
|
|
33
|
+
const [initialValue] = React.useState(initializer);
|
|
34
|
+
return [initialValue, () => { }];
|
|
35
|
+
}
|
|
36
|
+
function useStorageStateBrowser(kind, key, initializer = null) {
|
|
37
|
+
const [initialValue] = React.useState(initializer);
|
|
38
|
+
const area = kind === "session" ? window.sessionStorage : window.localStorage;
|
|
39
|
+
const subscribeKey = React.useCallback((cb) => subscribe(area, key, cb), [area, key]);
|
|
40
|
+
const getKeySnapshot = React.useCallback(() => getSnapshot(area, key) ?? initialValue, [area, initialValue, key]);
|
|
41
|
+
const getKeyServerSnapshot = React.useCallback(() => initialValue, [initialValue]);
|
|
42
|
+
const storedValue = React.useSyncExternalStore(subscribeKey, getKeySnapshot, getKeyServerSnapshot);
|
|
43
|
+
const setStoredValue = React.useCallback((value) => {
|
|
44
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
45
|
+
setValue(area, key, valueToStore);
|
|
46
|
+
}, [area, key, storedValue]);
|
|
47
|
+
return [storedValue, setStoredValue];
|
|
48
|
+
}
|
|
49
|
+
export default typeof window === "undefined"
|
|
50
|
+
? useStorageStateServer
|
|
51
|
+
: useStorageStateBrowser;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { afterEach, vi } from "vitest";
|
|
2
|
+
import { cleanup } from "@testing-library/react";
|
|
3
|
+
afterEach(cleanup);
|
|
4
|
+
// Mocks
|
|
5
|
+
if (typeof window !== "undefined" && !window.matchMedia) {
|
|
6
|
+
Object.defineProperty(window, "matchMedia", {
|
|
7
|
+
writable: true,
|
|
8
|
+
value: vi.fn().mockImplementation((query) => ({
|
|
9
|
+
matches: false,
|
|
10
|
+
media: query,
|
|
11
|
+
addListener: vi.fn(), // deprecated
|
|
12
|
+
removeListener: vi.fn(), // deprecated
|
|
13
|
+
onchange: null,
|
|
14
|
+
addEventListener: vi.fn(),
|
|
15
|
+
removeEventListener: vi.fn(),
|
|
16
|
+
dispatchEvent: vi.fn()
|
|
17
|
+
}))
|
|
18
|
+
});
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@etsoo/toolpad",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"author": "ETSOO",
|
|
5
|
+
"description": "Dashboard framework extention based on Toolpad Core",
|
|
6
|
+
"main": "build/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"types": "build/index.d.ts",
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"keywords": [
|
|
11
|
+
"react",
|
|
12
|
+
"mui",
|
|
13
|
+
"toolpad",
|
|
14
|
+
"dashboard",
|
|
15
|
+
"Etsoo"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/ETSOO/toolpad.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/ETSOO/toolpad/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/ETSOO/toolpad#readme",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"test": "vitest",
|
|
29
|
+
"testb": "vitest --browser.enabled"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@babel/runtime": "^7.25.7",
|
|
33
|
+
"@emotion/css": "^11.13.4",
|
|
34
|
+
"@emotion/react": "^11.13.3",
|
|
35
|
+
"@emotion/styled": "^11.13.0",
|
|
36
|
+
"@mui/lab": "6.0.0-beta.12",
|
|
37
|
+
"@mui/utils": "6.1.4",
|
|
38
|
+
"@vitejs/plugin-react": "4.3.3",
|
|
39
|
+
"invariant": "2.2.4",
|
|
40
|
+
"path-to-regexp": "6.3.0",
|
|
41
|
+
"prop-types": "15.8.1",
|
|
42
|
+
"react": "^18.3.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@mui/icons-material": "6.1.4",
|
|
46
|
+
"@mui/material": "6.1.4",
|
|
47
|
+
"@testing-library/jest-dom": "^6.6.2",
|
|
48
|
+
"@testing-library/react": "^16.0.1",
|
|
49
|
+
"@types/invariant": "2.2.37",
|
|
50
|
+
"@types/prop-types": "15.7.13",
|
|
51
|
+
"@types/react": "18.3.11",
|
|
52
|
+
"@types/react-dom": "18.3.1",
|
|
53
|
+
"@vitest/browser": "2.1.2",
|
|
54
|
+
"next": "^14.2.15",
|
|
55
|
+
"next-router-mock": "^0.9.13",
|
|
56
|
+
"playwright": "^1.47.2",
|
|
57
|
+
"react-router-dom": "6.26.2",
|
|
58
|
+
"vitest": "2.1.2"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, test, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Account } from "./Account";
|
|
5
|
+
import describeConformance from "../utils/describeConformance";
|
|
6
|
+
import { AppProvider } from "../AppProvider/AppProviderComponent";
|
|
7
|
+
|
|
8
|
+
describe("AppProvider", () => {
|
|
9
|
+
describeConformance(<Account />, () => ({
|
|
10
|
+
skip: ["themeDefaultProps"]
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
test("renders nothing in button when no authentication", async () => {
|
|
14
|
+
render(<Account />);
|
|
15
|
+
|
|
16
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("renders log in button when no session", async () => {
|
|
20
|
+
const auth = { signIn: vi.fn(), signOut: vi.fn() };
|
|
21
|
+
render(
|
|
22
|
+
<AppProvider authentication={auth}>
|
|
23
|
+
<Account />
|
|
24
|
+
</AppProvider>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const loginButton = screen.getByRole("button", { name: "Sign In" });
|
|
28
|
+
|
|
29
|
+
await userEvent.click(loginButton);
|
|
30
|
+
|
|
31
|
+
expect(auth.signIn).toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("renders content correctly when there is a session", async () => {
|
|
35
|
+
const auth = { signIn: vi.fn(), signOut: vi.fn() };
|
|
36
|
+
const session = { user: { name: "John Doe", email: "john@example.com" } };
|
|
37
|
+
render(
|
|
38
|
+
<AppProvider authentication={auth} session={session}>
|
|
39
|
+
<Account />
|
|
40
|
+
</AppProvider>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const userButton = screen.getByRole("button", { name: "Current User" });
|
|
44
|
+
|
|
45
|
+
await userEvent.click(userButton);
|
|
46
|
+
|
|
47
|
+
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByText("john@example.com")).toBeInTheDocument();
|
|
49
|
+
|
|
50
|
+
const signOutButton = screen.getByRole("button", { name: "Sign Out" });
|
|
51
|
+
|
|
52
|
+
await userEvent.click(signOutButton);
|
|
53
|
+
|
|
54
|
+
expect(auth.signOut).toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import Button from "@mui/material/Button";
|
|
4
|
+
import Popover from "@mui/material/Popover";
|
|
5
|
+
import Divider from "@mui/material/Divider";
|
|
6
|
+
import Stack from "@mui/material/Stack";
|
|
7
|
+
import { SignInButton } from "./SignInButton";
|
|
8
|
+
import { SignOutButton } from "./SignOutButton";
|
|
9
|
+
import { AccountPreview, AccountPreviewProps } from "./AccountPreview";
|
|
10
|
+
import { AccountPopoverHeader } from "./AccountPopoverHeader";
|
|
11
|
+
import { AccountPopoverFooter } from "./AccountPopoverFooter";
|
|
12
|
+
import {
|
|
13
|
+
SessionContext,
|
|
14
|
+
AuthenticationContext
|
|
15
|
+
} from "../AppProvider/AppProvider";
|
|
16
|
+
import { LocaleProvider, useLocaleText } from "../shared/locales/LocaleContext";
|
|
17
|
+
|
|
18
|
+
export interface AccountSlots {
|
|
19
|
+
/**
|
|
20
|
+
* The component used for the account preview
|
|
21
|
+
* @default AccountPreview
|
|
22
|
+
*/
|
|
23
|
+
preview?: React.ElementType;
|
|
24
|
+
/**
|
|
25
|
+
* The component used for the account popover menu
|
|
26
|
+
* @default Popover
|
|
27
|
+
*/
|
|
28
|
+
popover?: React.ElementType;
|
|
29
|
+
/**
|
|
30
|
+
* The component used for the content of account popover
|
|
31
|
+
* @default Stack
|
|
32
|
+
*/
|
|
33
|
+
popoverContent?: React.ElementType;
|
|
34
|
+
/**
|
|
35
|
+
* The component used for the sign in button.
|
|
36
|
+
* @default Button
|
|
37
|
+
*/
|
|
38
|
+
signInButton?: React.ElementType;
|
|
39
|
+
/**
|
|
40
|
+
* The component used for the sign out button.
|
|
41
|
+
* @default Button
|
|
42
|
+
*/
|
|
43
|
+
signOutButton?: React.ElementType;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AccountProps {
|
|
47
|
+
/**
|
|
48
|
+
* The components used for each slot inside.
|
|
49
|
+
*/
|
|
50
|
+
slots?: AccountSlots;
|
|
51
|
+
/**
|
|
52
|
+
* The props used for each slot inside.
|
|
53
|
+
*/
|
|
54
|
+
slotProps?: {
|
|
55
|
+
preview?: AccountPreviewProps;
|
|
56
|
+
popover?: React.ComponentProps<typeof Popover>;
|
|
57
|
+
popoverContent?: React.ComponentProps<typeof Stack>;
|
|
58
|
+
signInButton?: React.ComponentProps<typeof SignInButton>;
|
|
59
|
+
signOutButton?: React.ComponentProps<typeof Button>;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The labels for the account component.
|
|
63
|
+
*/
|
|
64
|
+
localeText?: Partial<ReturnType<typeof useLocaleText>>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
*
|
|
68
|
+
* Demos:
|
|
69
|
+
*
|
|
70
|
+
* - [Account](https://mui.com/toolpad/core/react-account/)
|
|
71
|
+
* - [Dashboard Layout](https://mui.com/toolpad/core/react-dashboard-layout/)
|
|
72
|
+
* - [Sign-in Page](https://mui.com/toolpad/core/react-sign-in-page/)
|
|
73
|
+
*
|
|
74
|
+
* API:
|
|
75
|
+
*
|
|
76
|
+
* - [Account API](https://mui.com/toolpad/core/api/account)
|
|
77
|
+
*/
|
|
78
|
+
function Account(props: AccountProps) {
|
|
79
|
+
const { localeText } = props;
|
|
80
|
+
const { slots, slotProps } = props;
|
|
81
|
+
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
|
|
82
|
+
const session = React.useContext(SessionContext);
|
|
83
|
+
const authentication = React.useContext(AuthenticationContext);
|
|
84
|
+
const open = Boolean(anchorEl);
|
|
85
|
+
|
|
86
|
+
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
87
|
+
setAnchorEl(event.currentTarget);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleClose = () => {
|
|
91
|
+
setAnchorEl(null);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (!authentication) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!session?.user) {
|
|
99
|
+
return (
|
|
100
|
+
<LocaleProvider localeText={localeText}>
|
|
101
|
+
{slots?.signInButton ? (
|
|
102
|
+
<slots.signInButton onClick={authentication.signIn} />
|
|
103
|
+
) : (
|
|
104
|
+
<SignInButton {...slotProps?.signInButton} />
|
|
105
|
+
)}
|
|
106
|
+
</LocaleProvider>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<LocaleProvider localeText={localeText}>
|
|
112
|
+
{slots?.preview ? (
|
|
113
|
+
<slots.preview handleClick={handleClick} open={open} />
|
|
114
|
+
) : (
|
|
115
|
+
<AccountPreview
|
|
116
|
+
variant="condensed"
|
|
117
|
+
handleClick={handleClick}
|
|
118
|
+
open={open}
|
|
119
|
+
{...slotProps?.preview}
|
|
120
|
+
/>
|
|
121
|
+
)}
|
|
122
|
+
{slots?.popover ? (
|
|
123
|
+
<slots.popover {...slotProps?.popover} />
|
|
124
|
+
) : (
|
|
125
|
+
<Popover
|
|
126
|
+
anchorEl={anchorEl}
|
|
127
|
+
id="account-menu"
|
|
128
|
+
open={open}
|
|
129
|
+
onClose={handleClose}
|
|
130
|
+
onClick={handleClose}
|
|
131
|
+
transformOrigin={{ horizontal: "right", vertical: "top" }}
|
|
132
|
+
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
|
|
133
|
+
{...slotProps?.popover}
|
|
134
|
+
slotProps={{
|
|
135
|
+
paper: {
|
|
136
|
+
elevation: 0,
|
|
137
|
+
sx: {
|
|
138
|
+
overflow: "visible",
|
|
139
|
+
filter: "drop-shadow(0px 2px 8px rgba(0,0,0,0.32))",
|
|
140
|
+
mt: 1,
|
|
141
|
+
"&::before": {
|
|
142
|
+
content: '""',
|
|
143
|
+
display: "block",
|
|
144
|
+
position: "absolute",
|
|
145
|
+
top: 0,
|
|
146
|
+
right: 14,
|
|
147
|
+
width: 10,
|
|
148
|
+
height: 10,
|
|
149
|
+
bgcolor: "background.paper",
|
|
150
|
+
transform: "translateY(-50%) rotate(45deg)",
|
|
151
|
+
zIndex: 0
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
...slotProps?.popover?.slotProps
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
{slots?.popoverContent ? (
|
|
159
|
+
<slots.popoverContent {...slotProps?.popoverContent} />
|
|
160
|
+
) : (
|
|
161
|
+
<Stack direction="column" {...slotProps?.popoverContent}>
|
|
162
|
+
<AccountPopoverHeader>
|
|
163
|
+
<AccountPreview variant="expanded" />
|
|
164
|
+
</AccountPopoverHeader>
|
|
165
|
+
<Divider />
|
|
166
|
+
<AccountPopoverFooter>
|
|
167
|
+
<SignOutButton {...slotProps?.signOutButton} />
|
|
168
|
+
</AccountPopoverFooter>
|
|
169
|
+
</Stack>
|
|
170
|
+
)}
|
|
171
|
+
</Popover>
|
|
172
|
+
)}
|
|
173
|
+
</LocaleProvider>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
Account.propTypes /* remove-proptypes */ = {
|
|
178
|
+
// ┌────────────────────────────── Warning ──────────────────────────────┐
|
|
179
|
+
// │ These PropTypes are generated from the TypeScript type definitions. │
|
|
180
|
+
// │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
|
|
181
|
+
// └─────────────────────────────────────────────────────────────────────┘
|
|
182
|
+
/**
|
|
183
|
+
* The labels for the account component.
|
|
184
|
+
*/
|
|
185
|
+
localeText: PropTypes.shape({
|
|
186
|
+
iconButtonAriaLabel: PropTypes.string,
|
|
187
|
+
signInLabel: PropTypes.string,
|
|
188
|
+
signOutLabel: PropTypes.string
|
|
189
|
+
}),
|
|
190
|
+
/**
|
|
191
|
+
* The props used for each slot inside.
|
|
192
|
+
*/
|
|
193
|
+
slotProps: PropTypes.shape({
|
|
194
|
+
popover: PropTypes.object,
|
|
195
|
+
popoverContent: PropTypes.object,
|
|
196
|
+
preview: PropTypes.shape({
|
|
197
|
+
handleClick: PropTypes.func,
|
|
198
|
+
open: PropTypes.bool,
|
|
199
|
+
slotProps: PropTypes.shape({
|
|
200
|
+
avatar: PropTypes.object,
|
|
201
|
+
avatarIconButton: PropTypes.object,
|
|
202
|
+
moreIconButton: PropTypes.object
|
|
203
|
+
}),
|
|
204
|
+
slots: PropTypes.shape({
|
|
205
|
+
avatar: PropTypes.elementType,
|
|
206
|
+
avatarIconButton: PropTypes.elementType,
|
|
207
|
+
moreIconButton: PropTypes.elementType
|
|
208
|
+
}),
|
|
209
|
+
variant: PropTypes.oneOf(["condensed", "expanded"])
|
|
210
|
+
}),
|
|
211
|
+
signInButton: PropTypes.object,
|
|
212
|
+
signOutButton: PropTypes.object
|
|
213
|
+
}),
|
|
214
|
+
/**
|
|
215
|
+
* The components used for each slot inside.
|
|
216
|
+
*/
|
|
217
|
+
slots: PropTypes.shape({
|
|
218
|
+
popover: PropTypes.elementType,
|
|
219
|
+
popoverContent: PropTypes.elementType,
|
|
220
|
+
preview: PropTypes.elementType,
|
|
221
|
+
signInButton: PropTypes.elementType,
|
|
222
|
+
signOutButton: PropTypes.elementType
|
|
223
|
+
})
|
|
224
|
+
} as any;
|
|
225
|
+
|
|
226
|
+
export { Account };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import Box, { BoxProps } from "@mui/material/Box";
|
|
3
|
+
|
|
4
|
+
export interface AccountPopoverFooterProps extends BoxProps {
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export /**
|
|
9
|
+
*
|
|
10
|
+
* Demos:
|
|
11
|
+
*
|
|
12
|
+
* - [Account](https://mui.com/toolpad/core/react-account/)
|
|
13
|
+
*
|
|
14
|
+
* API:
|
|
15
|
+
*
|
|
16
|
+
* - [AccountPopoverFooter API](https://mui.com/toolpad/core/api/account-popover-footer)
|
|
17
|
+
*/ function AccountPopoverFooter(props: AccountPopoverFooterProps) {
|
|
18
|
+
const { children, ...rest } = props;
|
|
19
|
+
return (
|
|
20
|
+
<Box
|
|
21
|
+
{...rest}
|
|
22
|
+
sx={{
|
|
23
|
+
display: "flex",
|
|
24
|
+
flexDirection: "row",
|
|
25
|
+
p: 1,
|
|
26
|
+
justifyContent: "flex-end",
|
|
27
|
+
...rest.sx
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</Box>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import Stack, { StackProps } from "@mui/material/Stack";
|
|
3
|
+
|
|
4
|
+
export interface AccountPopoverHeaderProps extends StackProps {
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export /**
|
|
9
|
+
*
|
|
10
|
+
* Demos:
|
|
11
|
+
*
|
|
12
|
+
* - [Account](https://mui.com/toolpad/core/react-account/)
|
|
13
|
+
*
|
|
14
|
+
* API:
|
|
15
|
+
*
|
|
16
|
+
* - [AccountPopoverHeader API](https://mui.com/toolpad/core/api/account-popover-header)
|
|
17
|
+
*/ function AccountPopoverHeader(props: AccountPopoverHeaderProps) {
|
|
18
|
+
const { children, ...rest } = props;
|
|
19
|
+
return <Stack {...rest}>{children}</Stack>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, test, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { AccountPreview } from "./AccountPreview";
|
|
5
|
+
import { AppProvider } from "../AppProvider/AppProviderComponent";
|
|
6
|
+
|
|
7
|
+
describe("AccountPreview", () => {
|
|
8
|
+
const auth = { signIn: vi.fn(), signOut: vi.fn() };
|
|
9
|
+
const session = {
|
|
10
|
+
user: {
|
|
11
|
+
name: "John Doe",
|
|
12
|
+
email: "john@example.com",
|
|
13
|
+
image: "https://example.com/avatar.jpg"
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
test("renders nothing when no session is provided", () => {
|
|
18
|
+
render(
|
|
19
|
+
<AppProvider authentication={auth}>
|
|
20
|
+
<AccountPreview />
|
|
21
|
+
</AppProvider>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("displays condensed variant by default", () => {
|
|
28
|
+
render(
|
|
29
|
+
<AppProvider authentication={auth} session={session}>
|
|
30
|
+
<AccountPreview />
|
|
31
|
+
</AppProvider>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const avatar = screen.getByRole("img", { name: "John Doe" });
|
|
35
|
+
expect(avatar).toBeInTheDocument();
|
|
36
|
+
expect(screen.queryByText("John Doe")).not.toBeInTheDocument();
|
|
37
|
+
expect(avatar).toHaveAttribute("src", "https://example.com/avatar.jpg");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("displays user name, email, and avatar in expanded variant", () => {
|
|
41
|
+
render(
|
|
42
|
+
<AppProvider authentication={auth} session={session}>
|
|
43
|
+
<AccountPreview variant="expanded" />
|
|
44
|
+
</AppProvider>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByText("john@example.com")).toBeInTheDocument();
|
|
49
|
+
expect(screen.getByRole("img", { name: "John Doe" })).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("calls handleClick when more icon button is clicked in expanded variant", async () => {
|
|
53
|
+
const handleClick = vi.fn();
|
|
54
|
+
render(
|
|
55
|
+
<AppProvider authentication={auth} session={session}>
|
|
56
|
+
<AccountPreview variant="expanded" handleClick={handleClick} />
|
|
57
|
+
</AppProvider>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const moreButton = screen.getByRole("button");
|
|
61
|
+
await userEvent.click(moreButton);
|
|
62
|
+
|
|
63
|
+
expect(handleClick).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("calls handleClick when avatar is clicked in condensed variant", async () => {
|
|
67
|
+
const handleClick = vi.fn();
|
|
68
|
+
render(
|
|
69
|
+
<AppProvider authentication={auth} session={session}>
|
|
70
|
+
<AccountPreview handleClick={handleClick} />
|
|
71
|
+
</AppProvider>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const avatarButton = screen.getByRole("button", { name: "Current User" });
|
|
75
|
+
await userEvent.click(avatarButton);
|
|
76
|
+
|
|
77
|
+
expect(handleClick).toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
});
|