bananas-commerce-admin 0.1.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/README.md +25 -0
- package/dist/cjs/Admin.js +47 -0
- package/dist/cjs/App.js +49 -0
- package/dist/cjs/api.js +225 -0
- package/dist/cjs/components/Branding.js +41 -0
- package/dist/cjs/components/Hamburger.js +40 -0
- package/dist/cjs/components/Link.js +21 -0
- package/dist/cjs/components/Logo.js +25 -0
- package/dist/cjs/components/NavBar.js +101 -0
- package/dist/cjs/components/NavBarItem.js +42 -0
- package/dist/cjs/components/NavBarRoutes.js +47 -0
- package/dist/cjs/components/ProgressBar.js +14 -0
- package/dist/cjs/components/User.js +71 -0
- package/dist/cjs/containers/Content.js +16 -0
- package/dist/cjs/containers/ErrorScreen.js +35 -0
- package/dist/cjs/containers/LoadingScreen.js +84 -0
- package/dist/cjs/containers/PageErrorBoundary.js +43 -0
- package/dist/cjs/containers/PageLoader.js +123 -0
- package/dist/cjs/contexts/ApiContext.js +105 -0
- package/dist/cjs/contexts/I18nContext.js +109 -0
- package/dist/cjs/contexts/RouterContext.js +99 -0
- package/dist/cjs/contexts/UserContext.js +144 -0
- package/dist/cjs/extensions/bananas/components/PasswordChangeForm.js +85 -0
- package/dist/cjs/extensions/bananas/index.js +54 -0
- package/dist/cjs/extensions/bananas/pages/me/list.js +20 -0
- package/dist/cjs/extensions/pos/components/PurchaseRow.js +32 -0
- package/dist/cjs/extensions/pos/components/ReceiptCard.js +86 -0
- package/dist/cjs/extensions/pos/components/ReceiptLine.js +29 -0
- package/dist/cjs/extensions/pos/index.js +22 -0
- package/dist/cjs/extensions/pos/pages/purchase/detail.js +13 -0
- package/dist/cjs/extensions/pos/pages/purchase/list.js +34 -0
- package/dist/cjs/extensions/pos/types/purchase.js +2 -0
- package/dist/cjs/extensions/pos/types/receipt.js +2 -0
- package/dist/cjs/forms/LoginForm.js +63 -0
- package/dist/cjs/hooks/useAsyncError.js +15 -0
- package/dist/cjs/hooks/useLocalStorage.js +47 -0
- package/dist/cjs/index.js +40 -0
- package/dist/cjs/pages/DashboardPage.js +10 -0
- package/dist/cjs/pages/LoginPage.js +31 -0
- package/dist/cjs/router/Router.js +35 -0
- package/dist/cjs/router/routes.js +57 -0
- package/dist/cjs/types/index.js +2 -0
- package/dist/cjs/util/get_cookie.js +10 -0
- package/dist/cjs/util/index.js +62 -0
- package/dist/cjs/util/select_styles.js +38 -0
- package/dist/esm/Admin.js +42 -0
- package/dist/esm/App.js +44 -0
- package/dist/esm/api.js +219 -0
- package/dist/esm/components/Branding.js +36 -0
- package/dist/esm/components/Hamburger.js +35 -0
- package/dist/esm/components/Link.js +16 -0
- package/dist/esm/components/Logo.js +20 -0
- package/dist/esm/components/NavBar.js +73 -0
- package/dist/esm/components/NavBarItem.js +37 -0
- package/dist/esm/components/NavBarRoutes.js +42 -0
- package/dist/esm/components/ProgressBar.js +9 -0
- package/dist/esm/components/User.js +66 -0
- package/dist/esm/containers/Content.js +11 -0
- package/dist/esm/containers/ErrorScreen.js +30 -0
- package/dist/esm/containers/LoadingScreen.js +79 -0
- package/dist/esm/containers/PageErrorBoundary.js +38 -0
- package/dist/esm/containers/PageLoader.js +117 -0
- package/dist/esm/contexts/ApiContext.js +77 -0
- package/dist/esm/contexts/I18nContext.js +77 -0
- package/dist/esm/contexts/RouterContext.js +71 -0
- package/dist/esm/contexts/UserContext.js +113 -0
- package/dist/esm/extensions/bananas/components/PasswordChangeForm.js +80 -0
- package/dist/esm/extensions/bananas/index.js +48 -0
- package/dist/esm/extensions/bananas/pages/me/list.js +15 -0
- package/dist/esm/extensions/pos/components/PurchaseRow.js +25 -0
- package/dist/esm/extensions/pos/components/ReceiptCard.js +56 -0
- package/dist/esm/extensions/pos/components/ReceiptLine.js +22 -0
- package/dist/esm/extensions/pos/index.js +16 -0
- package/dist/esm/extensions/pos/pages/purchase/detail.js +8 -0
- package/dist/esm/extensions/pos/pages/purchase/list.js +29 -0
- package/dist/esm/extensions/pos/types/purchase.js +1 -0
- package/dist/esm/extensions/pos/types/receipt.js +1 -0
- package/dist/esm/forms/LoginForm.js +58 -0
- package/dist/esm/hooks/useAsyncError.js +9 -0
- package/dist/esm/hooks/useLocalStorage.js +41 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/pages/DashboardPage.js +5 -0
- package/dist/esm/pages/LoginPage.js +26 -0
- package/dist/esm/router/Router.js +28 -0
- package/dist/esm/router/routes.js +49 -0
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/util/get_cookie.js +6 -0
- package/dist/esm/util/index.js +54 -0
- package/dist/esm/util/select_styles.js +34 -0
- package/dist/types/Admin.d.ts +13 -0
- package/dist/types/App.d.ts +13 -0
- package/dist/types/api.d.ts +20 -0
- package/dist/types/components/Branding.d.ts +14 -0
- package/dist/types/components/Hamburger.d.ts +8 -0
- package/dist/types/components/Link.d.ts +9 -0
- package/dist/types/components/Logo.d.ts +7 -0
- package/dist/types/components/NavBar.d.ts +11 -0
- package/dist/types/components/NavBarItem.d.ts +12 -0
- package/dist/types/components/NavBarRoutes.d.ts +7 -0
- package/dist/types/components/ProgressBar.d.ts +7 -0
- package/dist/types/components/User.d.ts +824 -0
- package/dist/types/containers/Content.d.ts +3 -0
- package/dist/types/containers/ErrorScreen.d.ts +7 -0
- package/dist/types/containers/LoadingScreen.d.ts +50 -0
- package/dist/types/containers/PageErrorBoundary.d.ts +16 -0
- package/dist/types/containers/PageLoader.d.ts +13 -0
- package/dist/types/contexts/ApiContext.d.ts +12 -0
- package/dist/types/contexts/I18nContext.d.ts +10 -0
- package/dist/types/contexts/RouterContext.d.ts +24 -0
- package/dist/types/contexts/UserContext.d.ts +20 -0
- package/dist/types/extensions/bananas/components/PasswordChangeForm.d.ts +3 -0
- package/dist/types/extensions/bananas/index.d.ts +2 -0
- package/dist/types/extensions/bananas/pages/me/list.d.ts +6 -0
- package/dist/types/extensions/pos/components/PurchaseRow.d.ts +7 -0
- package/dist/types/extensions/pos/components/ReceiptCard.d.ts +7 -0
- package/dist/types/extensions/pos/components/ReceiptLine.d.ts +7 -0
- package/dist/types/extensions/pos/index.d.ts +2 -0
- package/dist/types/extensions/pos/pages/purchase/detail.d.ts +7 -0
- package/dist/types/extensions/pos/pages/purchase/list.d.ts +9 -0
- package/dist/types/extensions/pos/types/purchase.d.ts +18 -0
- package/dist/types/extensions/pos/types/receipt.d.ts +34 -0
- package/dist/types/forms/LoginForm.d.ts +3 -0
- package/dist/types/hooks/useAsyncError.d.ts +1 -0
- package/dist/types/hooks/useLocalStorage.d.ts +2 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/pages/DashboardPage.d.ts +5 -0
- package/dist/types/pages/LoginPage.d.ts +9 -0
- package/dist/types/router/Router.d.ts +13 -0
- package/dist/types/router/routes.d.ts +20 -0
- package/dist/types/types/index.d.ts +6 -0
- package/dist/types/util/get_cookie.d.ts +1 -0
- package/dist/types/util/index.d.ts +8 -0
- package/dist/types/util/select_styles.d.ts +3 -0
- package/example/Dockerfile +27 -0
- package/example/docker-compose.yml +7 -0
- package/example/index.html +13 -0
- package/example/index.tsx +21 -0
- package/example/package-lock.json +13167 -0
- package/example/package.json +52 -0
- package/example/pages/.gitkeep +0 -0
- package/example/webpack.config.js +67 -0
- package/package.json +52 -0
- package/src/Admin.tsx +94 -0
- package/src/App.tsx +43 -0
- package/src/api.ts +202 -0
- package/src/components/Branding.tsx +79 -0
- package/src/components/Hamburger.tsx +41 -0
- package/src/components/Link.tsx +23 -0
- package/src/components/Logo.tsx +57 -0
- package/src/components/NavBar.tsx +115 -0
- package/src/components/NavBarItem.tsx +73 -0
- package/src/components/NavBarRoutes.tsx +67 -0
- package/src/components/ProgressBar.tsx +30 -0
- package/src/components/User.tsx +97 -0
- package/src/containers/Content.tsx +18 -0
- package/src/containers/ErrorScreen.tsx +64 -0
- package/src/containers/LoadingScreen.tsx +135 -0
- package/src/containers/PageErrorBoundary.tsx +32 -0
- package/src/containers/PageLoader.tsx +64 -0
- package/src/contexts/ApiContext.tsx +55 -0
- package/src/contexts/I18nContext.tsx +55 -0
- package/src/contexts/RouterContext.tsx +127 -0
- package/src/contexts/UserContext.tsx +99 -0
- package/src/extensions/bananas/components/PasswordChangeForm.tsx +138 -0
- package/src/extensions/bananas/index.ts +14 -0
- package/src/extensions/bananas/pages/me/list.tsx +31 -0
- package/src/extensions/pos/components/PurchaseRow.tsx +42 -0
- package/src/extensions/pos/components/ReceiptCard.tsx +101 -0
- package/src/extensions/pos/components/ReceiptLine.tsx +51 -0
- package/src/extensions/pos/index.tsx +20 -0
- package/src/extensions/pos/pages/purchase/detail.tsx +22 -0
- package/src/extensions/pos/pages/purchase/list.tsx +56 -0
- package/src/extensions/pos/types/purchase.ts +20 -0
- package/src/extensions/pos/types/receipt.ts +36 -0
- package/src/forms/LoginForm.tsx +99 -0
- package/src/hooks/useAsyncError.ts +13 -0
- package/src/hooks/useLocalStorage.ts +42 -0
- package/src/index.ts +18 -0
- package/src/pages/DashboardPage.tsx +9 -0
- package/src/pages/LoginPage.tsx +50 -0
- package/src/router/Router.tsx +63 -0
- package/src/router/routes.ts +67 -0
- package/src/types/index.ts +4 -0
- package/src/types/swagger-client.d.ts +1 -0
- package/src/util/get_cookie.ts +6 -0
- package/src/util/index.ts +63 -0
- package/src/util/select_styles.ts +29 -0
- package/tsconfig.json +38 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import { useParams, useSearchParams } from "react-router-dom";
|
|
4
|
+
import { useApi } from "../contexts/ApiContext";
|
|
5
|
+
|
|
6
|
+
import { RouteInfo } from "../contexts/RouterContext";
|
|
7
|
+
import useAsyncError from "../hooks/useAsyncError";
|
|
8
|
+
import { PageComponent } from "../types";
|
|
9
|
+
import LoadingScreen from "./LoadingScreen";
|
|
10
|
+
|
|
11
|
+
export class PageLoadFailedError extends Error {
|
|
12
|
+
readonly response: Response;
|
|
13
|
+
|
|
14
|
+
constructor(response: Response, message?: string) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.response = response;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PageLoaderProps {
|
|
21
|
+
route: RouteInfo;
|
|
22
|
+
page: PageComponent | Promise<PageComponent>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PageLoader: React.FC<PageLoaderProps> = ({ route, page }) => {
|
|
26
|
+
const Page = React.lazy(async () => ({
|
|
27
|
+
default: await Promise.resolve(page),
|
|
28
|
+
}));
|
|
29
|
+
const api = useApi();
|
|
30
|
+
const params = useParams();
|
|
31
|
+
const [searchParams] = useSearchParams();
|
|
32
|
+
const [data, setData] = React.useState(null);
|
|
33
|
+
const throwError = useAsyncError();
|
|
34
|
+
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
if (data === null) {
|
|
37
|
+
api.operations[route.id].call({
|
|
38
|
+
params: params,
|
|
39
|
+
query: Object.fromEntries(searchParams.entries()),
|
|
40
|
+
})
|
|
41
|
+
.then(async (response) => {
|
|
42
|
+
if (response.ok) {
|
|
43
|
+
setData(await response.json());
|
|
44
|
+
} else {
|
|
45
|
+
throwError(
|
|
46
|
+
new PageLoadFailedError(
|
|
47
|
+
response,
|
|
48
|
+
`Page data load failed with ${response.status} ${response.statusText}`,
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
.catch(throwError);
|
|
54
|
+
}
|
|
55
|
+
}, [api, params, searchParams, data]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Suspense fallback={<LoadingScreen />}>
|
|
59
|
+
<Page data={data} />
|
|
60
|
+
</Suspense>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default PageLoader;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useSnackbar } from "notistack";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { ApiClient } from "../api";
|
|
4
|
+
import { getCookie } from "../util/get_cookie";
|
|
5
|
+
|
|
6
|
+
export interface ApiContextProviderProps {
|
|
7
|
+
api: ApiClient | string | URL | {
|
|
8
|
+
schema: string | URL;
|
|
9
|
+
server?: string | URL;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ApiContext = React.createContext<ApiClient>(
|
|
14
|
+
undefined as unknown as ApiClient,
|
|
15
|
+
);
|
|
16
|
+
export const useApi = () => React.useContext(ApiContext);
|
|
17
|
+
|
|
18
|
+
export const ApiContextProvider: React.FC<
|
|
19
|
+
React.PropsWithChildren<ApiContextProviderProps>
|
|
20
|
+
> = (
|
|
21
|
+
{ children, api: init },
|
|
22
|
+
) => {
|
|
23
|
+
const [api, setApi] = React.useState<ApiClient>();
|
|
24
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
25
|
+
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
if (api === undefined) {
|
|
28
|
+
(init instanceof ApiClient
|
|
29
|
+
? Promise.resolve(init)
|
|
30
|
+
: typeof init === "string" ||
|
|
31
|
+
init instanceof URL
|
|
32
|
+
? ApiClient.load(init)
|
|
33
|
+
: ApiClient.load(init.schema, init.server)).then(async (api) => {
|
|
34
|
+
setApi(api);
|
|
35
|
+
|
|
36
|
+
if (getCookie("csrftoken") === undefined) {
|
|
37
|
+
await api.operations["bananas.csrf:list"].call();
|
|
38
|
+
}
|
|
39
|
+
}).catch((e) => {
|
|
40
|
+
enqueueSnackbar("Failed to load schema, view console for more info", {
|
|
41
|
+
variant: "error",
|
|
42
|
+
});
|
|
43
|
+
throw e;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}, [api]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<ApiContext.Provider value={api!}>
|
|
50
|
+
{children}
|
|
51
|
+
</ApiContext.Provider>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default ApiContext;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import useLocalStorage from "../hooks/useLocalStorage";
|
|
3
|
+
import { useApi } from "./ApiContext";
|
|
4
|
+
|
|
5
|
+
export function interpolateString(
|
|
6
|
+
string: string,
|
|
7
|
+
params: Record<string, string> | string[],
|
|
8
|
+
): string {
|
|
9
|
+
return Array.isArray(params)
|
|
10
|
+
? params.reduce((s, value) => s.replace(/%[sd]|\{\}/, value), string)
|
|
11
|
+
: Object.entries(params).reduce(
|
|
12
|
+
(s, [key, value]) =>
|
|
13
|
+
s.replace(new RegExp(`%\\(${key}\\)[sd]|\\{${key}\\}`, "g"), value),
|
|
14
|
+
string,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface I18nContext {
|
|
19
|
+
i18n: any;
|
|
20
|
+
t: (key: string, params?: Record<string, string> | string[]) => string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const I18nContext = React.createContext<I18nContext>(
|
|
24
|
+
undefined as unknown as I18nContext,
|
|
25
|
+
);
|
|
26
|
+
export const useI18n = () => React.useContext(I18nContext);
|
|
27
|
+
|
|
28
|
+
export const I18nContextProvider: React.FC<React.PropsWithChildren<{}>> = (
|
|
29
|
+
{ children },
|
|
30
|
+
) => {
|
|
31
|
+
const api = useApi();
|
|
32
|
+
const [i18n, setI18n] = useLocalStorage("i18n", undefined);
|
|
33
|
+
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
if (api && i18n === undefined) {
|
|
36
|
+
api.operations["bananas.i18n:list"].call().then(async (response) => {
|
|
37
|
+
const i18n = (await response.json()).catalog;
|
|
38
|
+
setI18n(i18n);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}, [api, i18n]);
|
|
42
|
+
|
|
43
|
+
const t = (key: string, params?: Record<string, string> | string[]) => {
|
|
44
|
+
const value = i18n?.[key] ?? key;
|
|
45
|
+
return params ? interpolateString(value, params) : value;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<I18nContext.Provider value={{ i18n, t }}>
|
|
50
|
+
{children}
|
|
51
|
+
</I18nContext.Provider>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default I18nContext;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import {
|
|
4
|
+
getPage,
|
|
5
|
+
getPath,
|
|
6
|
+
getTitle,
|
|
7
|
+
isNavigation,
|
|
8
|
+
parseOperationId,
|
|
9
|
+
} from "../router/routes";
|
|
10
|
+
import { useApi } from "./ApiContext";
|
|
11
|
+
|
|
12
|
+
export interface RouteInfo {
|
|
13
|
+
id: string;
|
|
14
|
+
app: string;
|
|
15
|
+
view: string;
|
|
16
|
+
action: string;
|
|
17
|
+
title: string;
|
|
18
|
+
navigation: boolean;
|
|
19
|
+
path: string;
|
|
20
|
+
page: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RouterContext {
|
|
24
|
+
routes: RouteInfo[];
|
|
25
|
+
getRoute(reverse: string): RouteInfo | undefined;
|
|
26
|
+
navigate(
|
|
27
|
+
route: number | string | RouteInfo,
|
|
28
|
+
options?: {
|
|
29
|
+
params?: Record<
|
|
30
|
+
string,
|
|
31
|
+
string | number | boolean
|
|
32
|
+
>;
|
|
33
|
+
query?: URLSearchParams;
|
|
34
|
+
replace?: boolean;
|
|
35
|
+
},
|
|
36
|
+
): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const RouterContext = React.createContext<RouterContext>(
|
|
40
|
+
{
|
|
41
|
+
routes: [],
|
|
42
|
+
getRoute: (reverse) => void reverse,
|
|
43
|
+
navigate: (route, options) => void [route, options],
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
export const useRouter = () => React.useContext(RouterContext);
|
|
47
|
+
|
|
48
|
+
export const RouterContextProvider: React.FC<React.PropsWithChildren<{}>> = (
|
|
49
|
+
{ children },
|
|
50
|
+
) => {
|
|
51
|
+
const routes: RouteInfo[] = [];
|
|
52
|
+
const api = useApi();
|
|
53
|
+
const routerNavigate = useNavigate();
|
|
54
|
+
|
|
55
|
+
for (const operation of Object.values(api.operations)) {
|
|
56
|
+
const parsedOperationId = parseOperationId(operation.id);
|
|
57
|
+
if (!parsedOperationId) {
|
|
58
|
+
throw new TypeError(`Could not parse operation id ${operation.id}`);
|
|
59
|
+
}
|
|
60
|
+
const { app, view, action } = parsedOperationId;
|
|
61
|
+
const title = getTitle(view, operation.summary);
|
|
62
|
+
const navigation = isNavigation(operation.tags);
|
|
63
|
+
const path = getPath(operation.endpoint, operation.method, action);
|
|
64
|
+
const page = getPage(path, action);
|
|
65
|
+
routes.push({
|
|
66
|
+
id: operation.id,
|
|
67
|
+
app,
|
|
68
|
+
view,
|
|
69
|
+
action,
|
|
70
|
+
title,
|
|
71
|
+
navigation,
|
|
72
|
+
path,
|
|
73
|
+
page,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const getRoute = (reverse: string) => {
|
|
78
|
+
return routes.find(({ id }) => reverse === id)!;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const navigate = (
|
|
82
|
+
route: number | string | RouteInfo,
|
|
83
|
+
options?: {
|
|
84
|
+
params?: Record<
|
|
85
|
+
string,
|
|
86
|
+
string | number | boolean
|
|
87
|
+
>;
|
|
88
|
+
query?: URLSearchParams;
|
|
89
|
+
replace?: boolean;
|
|
90
|
+
},
|
|
91
|
+
) => {
|
|
92
|
+
// Relative history, e.g. go back or forward x steps
|
|
93
|
+
if (typeof route === "number") {
|
|
94
|
+
routerNavigate(route);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Direct operation id routes, requires reversing the operation id
|
|
99
|
+
if (typeof route === "string") {
|
|
100
|
+
const routeInfo = getRoute(route);
|
|
101
|
+
|
|
102
|
+
if (routeInfo === undefined) {
|
|
103
|
+
throw new Error(`Could not find route with reverse: ${route}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
route = routeInfo;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let path = route.path;
|
|
110
|
+
for (const [key, value] of Object.entries(options?.params ?? {})) {
|
|
111
|
+
path = path.replace(`:${key}`, encodeURIComponent(value));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
routerNavigate({
|
|
115
|
+
pathname: path,
|
|
116
|
+
search: new URLSearchParams(options?.query).toString(),
|
|
117
|
+
}, { replace: options?.replace });
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<RouterContext.Provider value={{ routes, getRoute, navigate }}>
|
|
122
|
+
{children}
|
|
123
|
+
</RouterContext.Provider>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default RouterContext;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import useLocalStorage from "../hooks/useLocalStorage";
|
|
3
|
+
import { useApi } from "./ApiContext";
|
|
4
|
+
|
|
5
|
+
interface User {
|
|
6
|
+
id: string;
|
|
7
|
+
email: string;
|
|
8
|
+
is_superuser: boolean;
|
|
9
|
+
groups: string[];
|
|
10
|
+
full_name: string;
|
|
11
|
+
username: string;
|
|
12
|
+
permissions: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UserContext {
|
|
16
|
+
user: User | null;
|
|
17
|
+
login: (username: string, password: string) => Promise<User | null>;
|
|
18
|
+
logout: () => Promise<void>;
|
|
19
|
+
changePassword: (
|
|
20
|
+
oldPassword: string,
|
|
21
|
+
newPassword1: string,
|
|
22
|
+
newPassword2: string,
|
|
23
|
+
) => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const UserContext = React.createContext<UserContext>(
|
|
27
|
+
undefined as unknown as UserContext,
|
|
28
|
+
);
|
|
29
|
+
export const useUser = () => React.useContext(UserContext);
|
|
30
|
+
|
|
31
|
+
export const UserContextProvider: React.FC<React.PropsWithChildren<{}>> = (
|
|
32
|
+
{ children },
|
|
33
|
+
) => {
|
|
34
|
+
const api = useApi();
|
|
35
|
+
const [user, setUser] = useLocalStorage("user", null);
|
|
36
|
+
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
if (api !== undefined) {
|
|
39
|
+
api.isAuthenticated().then((authenticated) => {
|
|
40
|
+
if (!authenticated) {
|
|
41
|
+
setUser(null);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}, [user, api]);
|
|
46
|
+
|
|
47
|
+
const login = async (username: string, password: string) => {
|
|
48
|
+
const response = await api?.operations["bananas.login:create"].call({
|
|
49
|
+
body: { username, password },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (response !== undefined && response.ok) {
|
|
53
|
+
const user = await response.json();
|
|
54
|
+
setUser(user);
|
|
55
|
+
return user;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setUser(null);
|
|
59
|
+
return null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const changePassword = async (
|
|
63
|
+
oldPassword: string,
|
|
64
|
+
newPassword1: string,
|
|
65
|
+
newPassword2: string,
|
|
66
|
+
) => {
|
|
67
|
+
const response = await api?.operations["bananas.change_password:create"]
|
|
68
|
+
.call({
|
|
69
|
+
body: {
|
|
70
|
+
old_password: oldPassword,
|
|
71
|
+
new_password1: newPassword1,
|
|
72
|
+
new_password2: newPassword2,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error("Failed to change password");
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const logout = async () => {
|
|
82
|
+
const response = await api?.operations["bananas.logout:create"].call();
|
|
83
|
+
|
|
84
|
+
if (response !== undefined && response.ok) {
|
|
85
|
+
setUser(null);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw new Error("Could not log out");
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<UserContext.Provider value={{ user, login, logout, changePassword }}>
|
|
94
|
+
{children}
|
|
95
|
+
</UserContext.Provider>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default UserContext;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useSnackbar } from "notistack";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
import Box from "@mui/material/Box";
|
|
5
|
+
import FormControl from "@mui/material/FormControl";
|
|
6
|
+
import FormGroup from "@mui/material/FormGroup";
|
|
7
|
+
import FormLabel from "@mui/material/FormLabel";
|
|
8
|
+
import { useTheme } from "@mui/material/styles";
|
|
9
|
+
import TextField from "@mui/material/TextField";
|
|
10
|
+
import Typography from "@mui/material/Typography";
|
|
11
|
+
|
|
12
|
+
import { useApi } from "../../../contexts/ApiContext";
|
|
13
|
+
import { useI18n } from "../../../contexts/I18nContext";
|
|
14
|
+
import { useUser } from "../../../contexts/UserContext";
|
|
15
|
+
import LoadingButton from "@mui/lab/LoadingButton";
|
|
16
|
+
|
|
17
|
+
const PasswordChangeForm: React.FC = () => {
|
|
18
|
+
const theme = useTheme();
|
|
19
|
+
const { t } = useI18n();
|
|
20
|
+
const api = useApi();
|
|
21
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
22
|
+
const [loading, setLoading] = React.useState(false);
|
|
23
|
+
|
|
24
|
+
const operation = api.operations["bananas.change_password:create"];
|
|
25
|
+
const { properties } = operation.request.body.schema;
|
|
26
|
+
const [fields, setFields] = React.useState<Record<string, string>>({
|
|
27
|
+
"old_password": "",
|
|
28
|
+
"new_password1": "",
|
|
29
|
+
"new_password2": "",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const { changePassword } = useUser();
|
|
33
|
+
|
|
34
|
+
const onSubmit = (event: React.FormEvent) => {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
setLoading(true);
|
|
37
|
+
|
|
38
|
+
changePassword(
|
|
39
|
+
fields.old_password,
|
|
40
|
+
fields.new_password1,
|
|
41
|
+
fields.new_password2,
|
|
42
|
+
).then(() => {
|
|
43
|
+
enqueueSnackbar(t("Password changed successfully."), {
|
|
44
|
+
variant: "success",
|
|
45
|
+
});
|
|
46
|
+
setFields({
|
|
47
|
+
"old_password": "",
|
|
48
|
+
"new_password1": "",
|
|
49
|
+
"new_password2": "",
|
|
50
|
+
});
|
|
51
|
+
}).catch(() => {
|
|
52
|
+
enqueueSnackbar(t("Incorrect authentication credentials."), {
|
|
53
|
+
variant: "error",
|
|
54
|
+
});
|
|
55
|
+
}).finally(() => setLoading(false));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const onChange = (
|
|
59
|
+
event: React.ChangeEvent<{ name: string; value: string }>,
|
|
60
|
+
) => {
|
|
61
|
+
setFields({
|
|
62
|
+
...fields,
|
|
63
|
+
[event.target.name]: event.target.value,
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Box
|
|
69
|
+
component="form"
|
|
70
|
+
onSubmit={onSubmit}
|
|
71
|
+
sx={{
|
|
72
|
+
maxWidth: 350,
|
|
73
|
+
}}
|
|
74
|
+
data-testid="change-password-form"
|
|
75
|
+
>
|
|
76
|
+
<FormLabel
|
|
77
|
+
component="legend"
|
|
78
|
+
sx={{
|
|
79
|
+
marginBottom: theme.spacing(2),
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{operation.summary}
|
|
83
|
+
</FormLabel>
|
|
84
|
+
<Typography>
|
|
85
|
+
{t(
|
|
86
|
+
"Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly.",
|
|
87
|
+
)}
|
|
88
|
+
</Typography>
|
|
89
|
+
<FormControl fullWidth component="fieldset">
|
|
90
|
+
<FormGroup>
|
|
91
|
+
{["old_password", "new_password1", "new_password2"].map((field) => (
|
|
92
|
+
<TextField
|
|
93
|
+
key={field}
|
|
94
|
+
autoComplete={field}
|
|
95
|
+
sx={{
|
|
96
|
+
marginTop: theme.spacing(3),
|
|
97
|
+
}}
|
|
98
|
+
label={properties[field].title}
|
|
99
|
+
inputProps={{ "aria-label": properties[field].title }}
|
|
100
|
+
name={field}
|
|
101
|
+
value={fields[field]}
|
|
102
|
+
type="password"
|
|
103
|
+
onChange={onChange}
|
|
104
|
+
fullWidth
|
|
105
|
+
required
|
|
106
|
+
color="secondary"
|
|
107
|
+
/>
|
|
108
|
+
))}
|
|
109
|
+
</FormGroup>
|
|
110
|
+
</FormControl>
|
|
111
|
+
|
|
112
|
+
<FormControl
|
|
113
|
+
fullWidth
|
|
114
|
+
margin="normal"
|
|
115
|
+
sx={{
|
|
116
|
+
marginTop: theme.spacing(3),
|
|
117
|
+
marginBottom: 0,
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<LoadingButton
|
|
121
|
+
sx={{
|
|
122
|
+
margin: "auto",
|
|
123
|
+
}}
|
|
124
|
+
variant="outlined"
|
|
125
|
+
type="submit"
|
|
126
|
+
color="secondary"
|
|
127
|
+
fullWidth
|
|
128
|
+
aria-label="login"
|
|
129
|
+
loading={loading}
|
|
130
|
+
>
|
|
131
|
+
{t("Change my password")}
|
|
132
|
+
</LoadingButton>
|
|
133
|
+
</FormControl>
|
|
134
|
+
</Box>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export default PasswordChangeForm;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { RouterExtension } from "../../router/Router";
|
|
2
|
+
import { PageComponent } from "../../types";
|
|
3
|
+
import MePage from "./pages/me/list";
|
|
4
|
+
|
|
5
|
+
export const bananasRouterExtension: RouterExtension = {
|
|
6
|
+
app: "bananas",
|
|
7
|
+
pages: async (route) => {
|
|
8
|
+
if (route.view === "me" && route.action === "list") {
|
|
9
|
+
return MePage as PageComponent;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
throw new Error("Unknown bananas page");
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Paper from "@mui/material/Paper";
|
|
2
|
+
import { useTheme } from "@mui/material/styles";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import Content from "../../../../containers/Content";
|
|
5
|
+
import { useUser } from "../../../../contexts/UserContext";
|
|
6
|
+
import PasswordChangeForm from "../../components/PasswordChangeForm";
|
|
7
|
+
|
|
8
|
+
interface MePageProps {
|
|
9
|
+
editable?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const MePage: React.FC<MePageProps> = ({}) => {
|
|
13
|
+
const theme = useTheme();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Content>
|
|
17
|
+
<Paper
|
|
18
|
+
sx={{
|
|
19
|
+
padding: theme.spacing(3),
|
|
20
|
+
alignSelf: "flex-start",
|
|
21
|
+
}}
|
|
22
|
+
elevation={1}
|
|
23
|
+
square
|
|
24
|
+
>
|
|
25
|
+
<PasswordChangeForm />
|
|
26
|
+
</Paper>
|
|
27
|
+
</Content>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default MePage;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { TableCell, TableRow } from "@mui/material";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { useRouter } from "../../../contexts/RouterContext";
|
|
4
|
+
import { Purchase } from "../types/purchase";
|
|
5
|
+
|
|
6
|
+
interface PurchaseRowProps {
|
|
7
|
+
purchase: Purchase;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const PurchaseRow: React.FC<PurchaseRowProps> = ({
|
|
11
|
+
purchase,
|
|
12
|
+
}) => {
|
|
13
|
+
const { navigate } = useRouter();
|
|
14
|
+
|
|
15
|
+
const onClick = () => {
|
|
16
|
+
navigate(`pos.purchase:detail`, {
|
|
17
|
+
params: {
|
|
18
|
+
purchase_number: purchase.number,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const purchaseNumber = purchase.number
|
|
24
|
+
? `#${purchase.number.toString().padStart(4, "0")}`
|
|
25
|
+
: "-";
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<TableRow onClick={onClick} hover>
|
|
29
|
+
<TableCell>{purchaseNumber}</TableCell>
|
|
30
|
+
<TableCell>
|
|
31
|
+
{purchase.email}
|
|
32
|
+
</TableCell>
|
|
33
|
+
<TableCell></TableCell>
|
|
34
|
+
<TableCell>1337 SEK</TableCell>
|
|
35
|
+
<TableCell>Betald</TableCell>
|
|
36
|
+
<TableCell>
|
|
37
|
+
{new Date(purchase.date_initiated).toLocaleString()}
|
|
38
|
+
</TableCell>
|
|
39
|
+
<TableCell>-</TableCell>
|
|
40
|
+
</TableRow>
|
|
41
|
+
);
|
|
42
|
+
};
|