ding-react-admin 2.0.0 → 2.0.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/LICENSE +21 -21
- package/README.md +254 -195
- package/dist/index.css +2 -0
- package/dist/index.js +1772 -1169
- package/dist/src/components/NavMenuLabel.d.ts +8 -0
- package/dist/src/components/NavMenuLabel.d.ts.map +1 -0
- package/dist/src/components/NavMenuSearch.d.ts +9 -0
- package/dist/src/components/NavMenuSearch.d.ts.map +1 -0
- package/dist/src/components/ScrollableArea.d.ts +10 -0
- package/dist/src/components/ScrollableArea.d.ts.map +1 -0
- package/dist/src/components/index.d.ts +3 -0
- package/dist/src/components/index.d.ts.map +1 -1
- package/dist/src/context/AuthProvider.d.ts +2 -0
- package/dist/src/context/AuthProvider.d.ts.map +1 -1
- package/dist/src/crud/FormGlobalErrorsAlert.d.ts +5 -0
- package/dist/src/crud/FormGlobalErrorsAlert.d.ts.map +1 -0
- package/dist/src/crud/ResourceForm.d.ts +1 -4
- package/dist/src/crud/ResourceForm.d.ts.map +1 -1
- package/dist/src/crud/ResourceFormModal.d.ts +7 -1
- package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
- package/dist/src/crud/ResourceRecordForm.d.ts +27 -0
- package/dist/src/crud/ResourceRecordForm.d.ts.map +1 -0
- package/dist/src/crud/fields/ReferenceField.d.ts +12 -2
- package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
- package/dist/src/crud/fields/ReferenceInputActions.d.ts +16 -0
- package/dist/src/crud/fields/ReferenceInputActions.d.ts.map +1 -0
- package/dist/src/crud/fields/ReferenceManyField.d.ts +10 -1
- package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
- package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
- package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
- package/dist/src/crud/index.d.ts +5 -0
- package/dist/src/crud/index.d.ts.map +1 -1
- package/dist/src/crud/types.d.ts +12 -0
- package/dist/src/crud/types.d.ts.map +1 -1
- package/dist/src/crud/utils/choiceSelectionUtils.d.ts +10 -0
- package/dist/src/crud/utils/choiceSelectionUtils.d.ts.map +1 -0
- package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts +2 -0
- package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts.map +1 -0
- package/dist/src/crud/utils/formErrors.d.ts +18 -7
- package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
- package/dist/src/crud/utils/formErrors.test.d.ts +2 -0
- package/dist/src/crud/utils/formErrors.test.d.ts.map +1 -0
- package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +1 -0
- package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -1
- package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts +4 -0
- package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts.map +1 -0
- package/dist/src/crud/utils/toFormData.d.ts +1 -1
- package/dist/src/crud/utils/toFormData.d.ts.map +1 -1
- package/dist/src/crud/utils/uploadReferenceUtils.d.ts +5 -0
- package/dist/src/crud/utils/uploadReferenceUtils.d.ts.map +1 -0
- package/dist/src/crud/utils/useChoices.d.ts +27 -1
- package/dist/src/crud/utils/useChoices.d.ts.map +1 -1
- package/dist/src/crud/utils/useFormRecord.d.ts +9 -7
- package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -1
- package/dist/src/data/parseFormErrorHelpers.d.ts +6 -27
- package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/layouts/AdminLayout.d.ts +1 -1
- package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
- package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -1
- package/dist/src/layouts/navFilter.d.ts +5 -0
- package/dist/src/layouts/navFilter.d.ts.map +1 -0
- package/dist/src/layouts/navMenuItems.d.ts +6 -0
- package/dist/src/layouts/navMenuItems.d.ts.map +1 -0
- package/dist/src/types.d.ts +12 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +79 -59
- package/src/app/AdminApp.tsx +34 -34
- package/src/components/AuthAlternateLink.tsx +23 -23
- package/src/components/DensitySwitch.tsx +31 -31
- package/src/components/NavMenuLabel.tsx +26 -0
- package/src/components/NavMenuSearch.tsx +76 -0
- package/src/components/ScrollableArea.tsx +46 -0
- package/src/components/ThemeSwitch.tsx +28 -28
- package/src/components/ThemeToolbar.tsx +34 -34
- package/src/components/index.ts +7 -4
- package/src/components/navMenu.css +21 -0
- package/src/components/scrollable.css +28 -0
- package/src/context/AppThemeProvider.tsx +128 -128
- package/src/context/AuthProvider.tsx +89 -77
- package/src/context/DataProvider.tsx +30 -30
- package/src/context/PermissionsProvider.tsx +57 -57
- package/src/crud/FilterBar.tsx +36 -36
- package/src/crud/FormGlobalErrorsAlert.tsx +36 -0
- package/src/crud/FormSteps.tsx +135 -135
- package/src/crud/FormTabs.tsx +126 -126
- package/src/crud/InlineFormSet.tsx +162 -162
- package/src/crud/ListActionsBar.tsx +106 -106
- package/src/crud/ResourceForm.tsx +83 -160
- package/src/crud/ResourceFormModal.tsx +75 -133
- package/src/crud/ResourceRecordForm.tsx +166 -0
- package/src/crud/columns/BooleanColumn.tsx +32 -32
- package/src/crud/columns/CustomColumn.tsx +33 -33
- package/src/crud/columns/DateColumn.tsx +28 -28
- package/src/crud/columns/ImageColumn.tsx +58 -58
- package/src/crud/columns/NumberColumn.tsx +31 -31
- package/src/crud/columns/ReferenceColumn.tsx +85 -85
- package/src/crud/columns/ReferenceManyColumn.tsx +67 -67
- package/src/crud/columns/TextColumn.tsx +38 -38
- package/src/crud/context/FilterContext.tsx +71 -71
- package/src/crud/context/FormContext.tsx +39 -39
- package/src/crud/context/FormSectionContext.tsx +22 -22
- package/src/crud/context/InlineFieldsRegistry.tsx +61 -61
- package/src/crud/context/ListContext.tsx +93 -93
- package/src/crud/fields/BooleanField.tsx +38 -38
- package/src/crud/fields/DateField.tsx +53 -53
- package/src/crud/fields/FileField.tsx +123 -123
- package/src/crud/fields/ImageField.tsx +120 -120
- package/src/crud/fields/NumberField.tsx +52 -52
- package/src/crud/fields/PasswordField.tsx +99 -99
- package/src/crud/fields/ReferenceField.tsx +237 -96
- package/src/crud/fields/ReferenceInputActions.tsx +106 -0
- package/src/crud/fields/ReferenceManyField.tsx +200 -76
- package/src/crud/fields/SelectField.tsx +48 -48
- package/src/crud/fields/TextField.tsx +46 -46
- package/src/crud/fields/uploadFieldUtils.ts +21 -21
- package/src/crud/fields/useUploadPreviewUrl.ts +20 -20
- package/src/crud/filters/BooleanFilter.tsx +38 -38
- package/src/crud/filters/DateFilter.tsx +35 -35
- package/src/crud/filters/NumberFilter.tsx +33 -33
- package/src/crud/filters/ReferenceFilter.tsx +192 -173
- package/src/crud/filters/SelectFilter.tsx +44 -44
- package/src/crud/filters/TextFilter.tsx +36 -36
- package/src/crud/index.ts +99 -94
- package/src/crud/types.ts +146 -134
- package/src/crud/utils/buildFormPayload.ts +24 -24
- package/src/crud/utils/buildInlineRowsPayload.ts +37 -37
- package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -29
- package/src/crud/utils/choiceSelectionUtils.test.ts +45 -0
- package/src/crud/utils/choiceSelectionUtils.ts +116 -0
- package/src/crud/utils/formErrors.test.ts +168 -0
- package/src/crud/utils/formErrors.ts +138 -42
- package/src/crud/utils/formSectionErrors.ts +85 -85
- package/src/crud/utils/getFormValue.ts +13 -13
- package/src/crud/utils/hasUploadValues.ts +9 -9
- package/src/crud/utils/nestedFieldPath.ts +8 -8
- package/src/crud/utils/prepareFormSubmitBody.ts +22 -17
- package/src/crud/utils/referenceSelectNotFoundContent.tsx +7 -0
- package/src/crud/utils/setFormValue.ts +18 -18
- package/src/crud/utils/sortQueryParam.ts +37 -37
- package/src/crud/utils/toFormData.ts +79 -81
- package/src/crud/utils/uploadReferenceUtils.ts +44 -0
- package/src/crud/utils/useAbortableEffect.ts +17 -17
- package/src/crud/utils/useChoices.ts +330 -182
- package/src/crud/utils/useFormRecord.ts +179 -156
- package/src/crud/utils/useListQueryState.ts +191 -191
- package/src/data/abortError.test.ts +19 -19
- package/src/data/abortError.ts +10 -10
- package/src/data/createMemoryResourceHandlers.ts +78 -78
- package/src/data/createRestResourceHandlers.ts +63 -63
- package/src/data/dataUtils.test.ts +228 -220
- package/src/data/inMemoryList.ts +97 -97
- package/src/data/parseFormErrorHelpers.test.ts +201 -138
- package/src/data/parseFormErrorHelpers.ts +376 -311
- package/src/data/sortHelpers.ts +48 -48
- package/src/index.ts +211 -207
- package/src/layouts/AdminLayout.tsx +530 -382
- package/src/layouts/AuthPageLayout.tsx +87 -81
- package/src/layouts/navFilter.ts +49 -0
- package/src/layouts/navMenuItems.tsx +44 -0
- package/src/pages/LoginPage.tsx +80 -80
- package/src/pages/PlaceholderPage.tsx +9 -9
- package/src/permissions/resourcePermissions.ts +47 -47
- package/src/router/createAdminRouter.tsx +64 -64
- package/src/router/guards.tsx +63 -63
- package/src/router/routeAccess.ts +89 -89
- package/src/types.ts +10 -0
- package/src/vite-env.d.ts +1 -1
|
@@ -1,81 +1,87 @@
|
|
|
1
|
-
import { Flex, theme } from "antd";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
1
|
+
import { Flex, theme } from "antd";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { ScrollableArea } from "../components/ScrollableArea";
|
|
4
|
+
import { ThemeToolbar } from "../components/ThemeToolbar";
|
|
5
|
+
|
|
6
|
+
export type AuthPageLayoutProps = {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
/** Brand mark centered above the card. */
|
|
9
|
+
brand?: ReactNode;
|
|
10
|
+
/** Row below the card (e.g. link to register or login). */
|
|
11
|
+
footer?: ReactNode;
|
|
12
|
+
showThemeToolbar?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function AuthPageLayout({
|
|
16
|
+
children,
|
|
17
|
+
brand,
|
|
18
|
+
footer,
|
|
19
|
+
showThemeToolbar = true,
|
|
20
|
+
}: AuthPageLayoutProps) {
|
|
21
|
+
const { token } = theme.useToken();
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Flex
|
|
25
|
+
vertical
|
|
26
|
+
align="stretch"
|
|
27
|
+
style={{
|
|
28
|
+
height: "100dvh",
|
|
29
|
+
maxHeight: "100dvh",
|
|
30
|
+
width: "100%",
|
|
31
|
+
overflow: "hidden",
|
|
32
|
+
background: token.colorBgLayout,
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{showThemeToolbar ? (
|
|
36
|
+
<Flex
|
|
37
|
+
justify="flex-end"
|
|
38
|
+
style={{
|
|
39
|
+
flexShrink: 0,
|
|
40
|
+
width: "100%",
|
|
41
|
+
padding: 16,
|
|
42
|
+
background: token.colorBgLayout,
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<ThemeToolbar />
|
|
46
|
+
</Flex>
|
|
47
|
+
) : null}
|
|
48
|
+
{brand ? (
|
|
49
|
+
<div
|
|
50
|
+
style={{
|
|
51
|
+
flexShrink: 0,
|
|
52
|
+
textAlign: "center",
|
|
53
|
+
padding: "0 24px 16px",
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{brand}
|
|
57
|
+
</div>
|
|
58
|
+
) : null}
|
|
59
|
+
<ScrollableArea
|
|
60
|
+
style={{
|
|
61
|
+
flex: 1,
|
|
62
|
+
minHeight: 0,
|
|
63
|
+
width: "100%",
|
|
64
|
+
background: token.colorBgLayout,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<Flex
|
|
68
|
+
vertical
|
|
69
|
+
align="center"
|
|
70
|
+
justify="flex-start"
|
|
71
|
+
style={{
|
|
72
|
+
width: "100%",
|
|
73
|
+
minHeight: "100%",
|
|
74
|
+
padding: "0 24px 24px",
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
{children}
|
|
78
|
+
{footer ? (
|
|
79
|
+
<div style={{ marginTop: 16, width: "100%", maxWidth: 520 }}>
|
|
80
|
+
{footer}
|
|
81
|
+
</div>
|
|
82
|
+
) : null}
|
|
83
|
+
</Flex>
|
|
84
|
+
</ScrollableArea>
|
|
85
|
+
</Flex>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { NavItem } from "../types";
|
|
2
|
+
|
|
3
|
+
export function getNavItemLabel(item: NavItem): string {
|
|
4
|
+
const { label } = item;
|
|
5
|
+
if (typeof label === "string") return label;
|
|
6
|
+
if (typeof label === "number") return String(label);
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function filterNavItems(items: NavItem[], query: string): NavItem[] {
|
|
11
|
+
const q = query.trim().toLowerCase();
|
|
12
|
+
if (!q) return items;
|
|
13
|
+
|
|
14
|
+
function walk(nodes: NavItem[]): NavItem[] {
|
|
15
|
+
const result: NavItem[] = [];
|
|
16
|
+
for (const node of nodes) {
|
|
17
|
+
const label = getNavItemLabel(node).toLowerCase();
|
|
18
|
+
const selfMatch = label.includes(q);
|
|
19
|
+
|
|
20
|
+
if (node.children?.length) {
|
|
21
|
+
const filteredChildren = walk(node.children);
|
|
22
|
+
if (selfMatch) {
|
|
23
|
+
result.push(node);
|
|
24
|
+
} else if (filteredChildren.length > 0) {
|
|
25
|
+
result.push({ ...node, children: filteredChildren });
|
|
26
|
+
}
|
|
27
|
+
} else if (selfMatch) {
|
|
28
|
+
result.push(node);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return walk(items);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function collectSubmenuKeys(items: NavItem[]): string[] {
|
|
38
|
+
const keys: string[] = [];
|
|
39
|
+
function walk(nodes: NavItem[]) {
|
|
40
|
+
for (const node of nodes) {
|
|
41
|
+
if (node.children?.length) {
|
|
42
|
+
keys.push(node.path);
|
|
43
|
+
walk(node.children);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
walk(items);
|
|
48
|
+
return keys;
|
|
49
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { MenuProps } from "antd";
|
|
2
|
+
import { NavMenuLabel } from "../components/NavMenuLabel";
|
|
3
|
+
import type { NavItem } from "../types";
|
|
4
|
+
import { getNavItemLabel } from "./navFilter";
|
|
5
|
+
|
|
6
|
+
export function navItemsToAntdItems(
|
|
7
|
+
items: NavItem[],
|
|
8
|
+
options?: { showLabelTooltip?: boolean },
|
|
9
|
+
): NonNullable<MenuProps["items"]> {
|
|
10
|
+
const showLabelTooltip = options?.showLabelTooltip !== false;
|
|
11
|
+
|
|
12
|
+
return items.map((item) => {
|
|
13
|
+
const IconComp = item.Icon;
|
|
14
|
+
const icon = IconComp ? <IconComp /> : undefined;
|
|
15
|
+
const title = getNavItemLabel(item);
|
|
16
|
+
const label =
|
|
17
|
+
title && showLabelTooltip ? (
|
|
18
|
+
<NavMenuLabel label={item.label} title={title} />
|
|
19
|
+
) : (
|
|
20
|
+
item.label
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Expanded: NavMenuLabel tooltip only (omit `title` to avoid native DOM tooltip).
|
|
24
|
+
// Collapsed: string title for the sider icon tooltip.
|
|
25
|
+
const itemTitle: { title?: string } =
|
|
26
|
+
!showLabelTooltip && title ? { title } : {};
|
|
27
|
+
|
|
28
|
+
if (item.children?.length) {
|
|
29
|
+
return {
|
|
30
|
+
key: item.path,
|
|
31
|
+
icon,
|
|
32
|
+
label,
|
|
33
|
+
...itemTitle,
|
|
34
|
+
children: navItemsToAntdItems(item.children, options),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
key: item.path,
|
|
39
|
+
icon,
|
|
40
|
+
label,
|
|
41
|
+
...itemTitle,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
package/src/pages/LoginPage.tsx
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import { Button, Card, Form, Input, Typography } from "antd";
|
|
2
|
-
import { useNavigate } from "react-router-dom";
|
|
3
|
-
import { AuthAlternateLink } from "../components/AuthAlternateLink";
|
|
4
|
-
import { useAuth } from "../context/AuthProvider";
|
|
5
|
-
import { AuthPageLayout } from "../layouts/AuthPageLayout";
|
|
6
|
-
import type { LoginPageProps } from "../types";
|
|
7
|
-
|
|
8
|
-
export function LoginPage({
|
|
9
|
-
title = "Sign in",
|
|
10
|
-
description = "Use any username and password to continue.",
|
|
11
|
-
logo,
|
|
12
|
-
brand,
|
|
13
|
-
extraFields,
|
|
14
|
-
showThemeToolbar = true,
|
|
15
|
-
afterLoginPath = "/",
|
|
16
|
-
alternateAuth,
|
|
17
|
-
footer,
|
|
18
|
-
}: LoginPageProps) {
|
|
19
|
-
const { login } = useAuth();
|
|
20
|
-
const navigate = useNavigate();
|
|
21
|
-
const brandNode = brand ?? logo;
|
|
22
|
-
|
|
23
|
-
const resolvedFooter =
|
|
24
|
-
footer ??
|
|
25
|
-
(alternateAuth ? (
|
|
26
|
-
<AuthAlternateLink
|
|
27
|
-
prompt={alternateAuth.prompt ?? "Don't have an account?"}
|
|
28
|
-
linkText={alternateAuth.linkText}
|
|
29
|
-
to={alternateAuth.to}
|
|
30
|
-
/>
|
|
31
|
-
) : null);
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<AuthPageLayout
|
|
35
|
-
brand={brandNode}
|
|
36
|
-
footer={resolvedFooter}
|
|
37
|
-
showThemeToolbar={showThemeToolbar}
|
|
38
|
-
>
|
|
39
|
-
<Card style={{ width: "100%", maxWidth: 360 }} title={title}>
|
|
40
|
-
{description ? (
|
|
41
|
-
<Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
|
|
42
|
-
{description}
|
|
43
|
-
</Typography.Paragraph>
|
|
44
|
-
) : null}
|
|
45
|
-
<Form
|
|
46
|
-
layout="vertical"
|
|
47
|
-
onFinish={async (values: Record<string, unknown>) => {
|
|
48
|
-
await login({
|
|
49
|
-
username: String(values.username ?? ""),
|
|
50
|
-
password: String(values.password ?? ""),
|
|
51
|
-
...values,
|
|
52
|
-
});
|
|
53
|
-
navigate(afterLoginPath, { replace: true });
|
|
54
|
-
}}
|
|
55
|
-
>
|
|
56
|
-
<Form.Item
|
|
57
|
-
name="username"
|
|
58
|
-
label="Username"
|
|
59
|
-
rules={[{ required: true, message: "Required" }]}
|
|
60
|
-
>
|
|
61
|
-
<Input autoComplete="username" />
|
|
62
|
-
</Form.Item>
|
|
63
|
-
<Form.Item
|
|
64
|
-
name="password"
|
|
65
|
-
label="Password"
|
|
66
|
-
rules={[{ required: true, message: "Required" }]}
|
|
67
|
-
>
|
|
68
|
-
<Input.Password autoComplete="current-password" />
|
|
69
|
-
</Form.Item>
|
|
70
|
-
{extraFields}
|
|
71
|
-
<Form.Item style={{ marginBottom: 0 }}>
|
|
72
|
-
<Button type="primary" htmlType="submit" block>
|
|
73
|
-
Log in
|
|
74
|
-
</Button>
|
|
75
|
-
</Form.Item>
|
|
76
|
-
</Form>
|
|
77
|
-
</Card>
|
|
78
|
-
</AuthPageLayout>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
1
|
+
import { Button, Card, Form, Input, Typography } from "antd";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { AuthAlternateLink } from "../components/AuthAlternateLink";
|
|
4
|
+
import { useAuth } from "../context/AuthProvider";
|
|
5
|
+
import { AuthPageLayout } from "../layouts/AuthPageLayout";
|
|
6
|
+
import type { LoginPageProps } from "../types";
|
|
7
|
+
|
|
8
|
+
export function LoginPage({
|
|
9
|
+
title = "Sign in",
|
|
10
|
+
description = "Use any username and password to continue.",
|
|
11
|
+
logo,
|
|
12
|
+
brand,
|
|
13
|
+
extraFields,
|
|
14
|
+
showThemeToolbar = true,
|
|
15
|
+
afterLoginPath = "/",
|
|
16
|
+
alternateAuth,
|
|
17
|
+
footer,
|
|
18
|
+
}: LoginPageProps) {
|
|
19
|
+
const { login } = useAuth();
|
|
20
|
+
const navigate = useNavigate();
|
|
21
|
+
const brandNode = brand ?? logo;
|
|
22
|
+
|
|
23
|
+
const resolvedFooter =
|
|
24
|
+
footer ??
|
|
25
|
+
(alternateAuth ? (
|
|
26
|
+
<AuthAlternateLink
|
|
27
|
+
prompt={alternateAuth.prompt ?? "Don't have an account?"}
|
|
28
|
+
linkText={alternateAuth.linkText}
|
|
29
|
+
to={alternateAuth.to}
|
|
30
|
+
/>
|
|
31
|
+
) : null);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<AuthPageLayout
|
|
35
|
+
brand={brandNode}
|
|
36
|
+
footer={resolvedFooter}
|
|
37
|
+
showThemeToolbar={showThemeToolbar}
|
|
38
|
+
>
|
|
39
|
+
<Card style={{ width: "100%", maxWidth: 360 }} title={title}>
|
|
40
|
+
{description ? (
|
|
41
|
+
<Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
|
|
42
|
+
{description}
|
|
43
|
+
</Typography.Paragraph>
|
|
44
|
+
) : null}
|
|
45
|
+
<Form
|
|
46
|
+
layout="vertical"
|
|
47
|
+
onFinish={async (values: Record<string, unknown>) => {
|
|
48
|
+
await login({
|
|
49
|
+
username: String(values.username ?? ""),
|
|
50
|
+
password: String(values.password ?? ""),
|
|
51
|
+
...values,
|
|
52
|
+
});
|
|
53
|
+
navigate(afterLoginPath, { replace: true });
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<Form.Item
|
|
57
|
+
name="username"
|
|
58
|
+
label="Username"
|
|
59
|
+
rules={[{ required: true, message: "Required" }]}
|
|
60
|
+
>
|
|
61
|
+
<Input autoComplete="username" />
|
|
62
|
+
</Form.Item>
|
|
63
|
+
<Form.Item
|
|
64
|
+
name="password"
|
|
65
|
+
label="Password"
|
|
66
|
+
rules={[{ required: true, message: "Required" }]}
|
|
67
|
+
>
|
|
68
|
+
<Input.Password autoComplete="current-password" />
|
|
69
|
+
</Form.Item>
|
|
70
|
+
{extraFields}
|
|
71
|
+
<Form.Item style={{ marginBottom: 0 }}>
|
|
72
|
+
<Button type="primary" htmlType="submit" block>
|
|
73
|
+
Log in
|
|
74
|
+
</Button>
|
|
75
|
+
</Form.Item>
|
|
76
|
+
</Form>
|
|
77
|
+
</Card>
|
|
78
|
+
</AuthPageLayout>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Typography } from "antd";
|
|
2
|
-
|
|
3
|
-
export function PlaceholderPage({ title }: { title: string }) {
|
|
4
|
-
return (
|
|
5
|
-
<Typography.Title level={3} style={{ marginTop: 0 }}>
|
|
6
|
-
{title}
|
|
7
|
-
</Typography.Title>
|
|
8
|
-
);
|
|
9
|
-
}
|
|
1
|
+
import { Typography } from "antd";
|
|
2
|
+
|
|
3
|
+
export function PlaceholderPage({ title }: { title: string }) {
|
|
4
|
+
return (
|
|
5
|
+
<Typography.Title level={3} style={{ marginTop: 0 }}>
|
|
6
|
+
{title}
|
|
7
|
+
</Typography.Title>
|
|
8
|
+
);
|
|
9
|
+
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import type { PermissionsChecker } from "../context/PermissionsProvider";
|
|
2
|
-
|
|
3
|
-
/** Opaque permission strings per CRUD action (values come from your backend). */
|
|
4
|
-
export type ResourcePermissions = {
|
|
5
|
-
/** List page, nav, route guard */
|
|
6
|
-
list?: string;
|
|
7
|
-
/** getOne — falls back to `list` when omitted */
|
|
8
|
-
read?: string;
|
|
9
|
-
/** New button, create modal, POST */
|
|
10
|
-
add?: string;
|
|
11
|
-
/** Edit / Quick edit, PATCH/PUT */
|
|
12
|
-
change?: string;
|
|
13
|
-
/** Row delete, bulk delete */
|
|
14
|
-
delete?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export type ResourcePermissionSlot = keyof ResourcePermissions;
|
|
18
|
-
|
|
19
|
-
/** When `permissions` is omitted, allow. When provided, missing slot → deny. */
|
|
20
|
-
export function checkResourcePermission(
|
|
21
|
-
can: PermissionsChecker,
|
|
22
|
-
permissions: ResourcePermissions | undefined,
|
|
23
|
-
slot: ResourcePermissionSlot,
|
|
24
|
-
): boolean {
|
|
25
|
-
if (!permissions) return true;
|
|
26
|
-
let perm = permissions[slot];
|
|
27
|
-
if (slot === "read" && !perm) perm = permissions.list;
|
|
28
|
-
if (!perm) return false;
|
|
29
|
-
return can(perm);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function filterNavByPermission<T extends { permission?: string; children?: T[] }>(
|
|
33
|
-
items: T[],
|
|
34
|
-
can: PermissionsChecker,
|
|
35
|
-
): T[] {
|
|
36
|
-
return items
|
|
37
|
-
.map((item) => {
|
|
38
|
-
if (item.children?.length) {
|
|
39
|
-
const children = filterNavByPermission(item.children, can);
|
|
40
|
-
if (children.length === 0) return null;
|
|
41
|
-
return { ...item, children };
|
|
42
|
-
}
|
|
43
|
-
if (item.permission && !can(item.permission)) return null;
|
|
44
|
-
return item;
|
|
45
|
-
})
|
|
46
|
-
.filter((item): item is T => item !== null);
|
|
47
|
-
}
|
|
1
|
+
import type { PermissionsChecker } from "../context/PermissionsProvider";
|
|
2
|
+
|
|
3
|
+
/** Opaque permission strings per CRUD action (values come from your backend). */
|
|
4
|
+
export type ResourcePermissions = {
|
|
5
|
+
/** List page, nav, route guard */
|
|
6
|
+
list?: string;
|
|
7
|
+
/** getOne — falls back to `list` when omitted */
|
|
8
|
+
read?: string;
|
|
9
|
+
/** New button, create modal, POST */
|
|
10
|
+
add?: string;
|
|
11
|
+
/** Edit / Quick edit, PATCH/PUT */
|
|
12
|
+
change?: string;
|
|
13
|
+
/** Row delete, bulk delete */
|
|
14
|
+
delete?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ResourcePermissionSlot = keyof ResourcePermissions;
|
|
18
|
+
|
|
19
|
+
/** When `permissions` is omitted, allow. When provided, missing slot → deny. */
|
|
20
|
+
export function checkResourcePermission(
|
|
21
|
+
can: PermissionsChecker,
|
|
22
|
+
permissions: ResourcePermissions | undefined,
|
|
23
|
+
slot: ResourcePermissionSlot,
|
|
24
|
+
): boolean {
|
|
25
|
+
if (!permissions) return true;
|
|
26
|
+
let perm = permissions[slot];
|
|
27
|
+
if (slot === "read" && !perm) perm = permissions.list;
|
|
28
|
+
if (!perm) return false;
|
|
29
|
+
return can(perm);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function filterNavByPermission<T extends { permission?: string; children?: T[] }>(
|
|
33
|
+
items: T[],
|
|
34
|
+
can: PermissionsChecker,
|
|
35
|
+
): T[] {
|
|
36
|
+
return items
|
|
37
|
+
.map((item) => {
|
|
38
|
+
if (item.children?.length) {
|
|
39
|
+
const children = filterNavByPermission(item.children, can);
|
|
40
|
+
if (children.length === 0) return null;
|
|
41
|
+
return { ...item, children };
|
|
42
|
+
}
|
|
43
|
+
if (item.permission && !can(item.permission)) return null;
|
|
44
|
+
return item;
|
|
45
|
+
})
|
|
46
|
+
.filter((item): item is T => item !== null);
|
|
47
|
+
}
|
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
import { Navigate, createBrowserRouter } from "react-router-dom";
|
|
2
|
-
import { AdminLayout } from "../layouts/AdminLayout";
|
|
3
|
-
import type { CreateAdminRouterOptions } from "../types";
|
|
4
|
-
import { GuestOnly, Protected } from "./guards";
|
|
5
|
-
import {
|
|
6
|
-
deriveAuthPaths,
|
|
7
|
-
partitionAdminRoutes,
|
|
8
|
-
toProtectedRouteObject,
|
|
9
|
-
toRouterSegment,
|
|
10
|
-
} from "./routeAccess";
|
|
11
|
-
|
|
12
|
-
export function createAdminRouter({
|
|
13
|
-
navItems,
|
|
14
|
-
children,
|
|
15
|
-
layoutProps,
|
|
16
|
-
redirects,
|
|
17
|
-
}: CreateAdminRouterOptions) {
|
|
18
|
-
const { loginPath, homePath } = deriveAuthPaths(children, redirects);
|
|
19
|
-
const { guest, public: publicRoutes, protected: protectedRoutes } =
|
|
20
|
-
partitionAdminRoutes(children);
|
|
21
|
-
|
|
22
|
-
const routerRoutes = [];
|
|
23
|
-
|
|
24
|
-
for (const route of guest) {
|
|
25
|
-
if (!("path" in route) || !route.path) continue;
|
|
26
|
-
routerRoutes.push({
|
|
27
|
-
path: toRouterSegment(route.path),
|
|
28
|
-
element: (
|
|
29
|
-
<GuestOnly redirectTo={homePath}>{route.element}</GuestOnly>
|
|
30
|
-
),
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
for (const route of publicRoutes) {
|
|
35
|
-
if (!("path" in route) || !route.path) continue;
|
|
36
|
-
routerRoutes.push({
|
|
37
|
-
path: toRouterSegment(route.path),
|
|
38
|
-
element: route.element,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (protectedRoutes.length > 0) {
|
|
43
|
-
routerRoutes.push({
|
|
44
|
-
path: "/",
|
|
45
|
-
element: (
|
|
46
|
-
<Protected redirectTo={loginPath}>
|
|
47
|
-
<AdminLayout
|
|
48
|
-
navItems={navItems}
|
|
49
|
-
loginPath={loginPath}
|
|
50
|
-
{...layoutProps}
|
|
51
|
-
/>
|
|
52
|
-
</Protected>
|
|
53
|
-
),
|
|
54
|
-
children: protectedRoutes.map(toProtectedRouteObject),
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
routerRoutes.push({
|
|
59
|
-
path: "*",
|
|
60
|
-
element: <Navigate to={homePath} replace />,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
return createBrowserRouter(routerRoutes);
|
|
64
|
-
}
|
|
1
|
+
import { Navigate, createBrowserRouter } from "react-router-dom";
|
|
2
|
+
import { AdminLayout } from "../layouts/AdminLayout";
|
|
3
|
+
import type { CreateAdminRouterOptions } from "../types";
|
|
4
|
+
import { GuestOnly, Protected } from "./guards";
|
|
5
|
+
import {
|
|
6
|
+
deriveAuthPaths,
|
|
7
|
+
partitionAdminRoutes,
|
|
8
|
+
toProtectedRouteObject,
|
|
9
|
+
toRouterSegment,
|
|
10
|
+
} from "./routeAccess";
|
|
11
|
+
|
|
12
|
+
export function createAdminRouter({
|
|
13
|
+
navItems,
|
|
14
|
+
children,
|
|
15
|
+
layoutProps,
|
|
16
|
+
redirects,
|
|
17
|
+
}: CreateAdminRouterOptions) {
|
|
18
|
+
const { loginPath, homePath } = deriveAuthPaths(children, redirects);
|
|
19
|
+
const { guest, public: publicRoutes, protected: protectedRoutes } =
|
|
20
|
+
partitionAdminRoutes(children);
|
|
21
|
+
|
|
22
|
+
const routerRoutes = [];
|
|
23
|
+
|
|
24
|
+
for (const route of guest) {
|
|
25
|
+
if (!("path" in route) || !route.path) continue;
|
|
26
|
+
routerRoutes.push({
|
|
27
|
+
path: toRouterSegment(route.path),
|
|
28
|
+
element: (
|
|
29
|
+
<GuestOnly redirectTo={homePath}>{route.element}</GuestOnly>
|
|
30
|
+
),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const route of publicRoutes) {
|
|
35
|
+
if (!("path" in route) || !route.path) continue;
|
|
36
|
+
routerRoutes.push({
|
|
37
|
+
path: toRouterSegment(route.path),
|
|
38
|
+
element: route.element,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (protectedRoutes.length > 0) {
|
|
43
|
+
routerRoutes.push({
|
|
44
|
+
path: "/",
|
|
45
|
+
element: (
|
|
46
|
+
<Protected redirectTo={loginPath}>
|
|
47
|
+
<AdminLayout
|
|
48
|
+
navItems={navItems}
|
|
49
|
+
loginPath={loginPath}
|
|
50
|
+
{...layoutProps}
|
|
51
|
+
/>
|
|
52
|
+
</Protected>
|
|
53
|
+
),
|
|
54
|
+
children: protectedRoutes.map(toProtectedRouteObject),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
routerRoutes.push({
|
|
59
|
+
path: "*",
|
|
60
|
+
element: <Navigate to={homePath} replace />,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return createBrowserRouter(routerRoutes);
|
|
64
|
+
}
|