ding-react-admin 1.0.4 → 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 -201
- package/dist/index.css +2 -0
- package/dist/index.js +2073 -1202
- 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/InlineFormSet.d.ts +2 -17
- package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
- package/dist/src/crud/ResourceForm.d.ts +2 -10
- 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/ResourceList.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/columns/ImageColumn.d.ts +14 -0
- package/dist/src/crud/columns/ImageColumn.d.ts.map +1 -0
- package/dist/src/crud/context/InlineFieldsRegistry.d.ts +15 -0
- package/dist/src/crud/context/InlineFieldsRegistry.d.ts.map +1 -0
- package/dist/src/crud/context/PayloadFieldsContext.d.ts +18 -0
- package/dist/src/crud/context/PayloadFieldsContext.d.ts.map +1 -0
- package/dist/src/crud/fields/FileField.d.ts +12 -0
- package/dist/src/crud/fields/FileField.d.ts.map +1 -0
- package/dist/src/crud/fields/ImageField.d.ts +14 -0
- package/dist/src/crud/fields/ImageField.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/fields/uploadFieldUtils.d.ts +5 -0
- package/dist/src/crud/fields/uploadFieldUtils.d.ts.map +1 -0
- package/dist/src/crud/fields/useUploadPreviewUrl.d.ts +4 -0
- package/dist/src/crud/fields/useUploadPreviewUrl.d.ts.map +1 -0
- 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 +27 -9
- package/dist/src/crud/index.d.ts.map +1 -1
- package/dist/src/crud/types.d.ts +21 -7
- package/dist/src/crud/types.d.ts.map +1 -1
- package/dist/src/crud/utils/buildFormPayload.d.ts +6 -0
- package/dist/src/crud/utils/buildFormPayload.d.ts.map +1 -0
- package/dist/src/crud/utils/buildInlineRowsPayload.d.ts +6 -0
- package/dist/src/crud/utils/buildInlineRowsPayload.d.ts.map +1 -0
- package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts +5 -0
- package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts.map +1 -0
- 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/getFormValue.d.ts +3 -0
- package/dist/src/crud/utils/getFormValue.d.ts.map +1 -0
- package/dist/src/crud/utils/hasUploadValues.d.ts +3 -0
- package/dist/src/crud/utils/hasUploadValues.d.ts.map +1 -0
- package/dist/src/crud/utils/nestedFieldPath.d.ts +3 -0
- package/dist/src/crud/utils/nestedFieldPath.d.ts.map +1 -0
- package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +8 -0
- package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -0
- 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/setFormValue.d.ts +3 -0
- package/dist/src/crud/utils/setFormValue.d.ts.map +1 -0
- package/dist/src/crud/utils/toFormData.d.ts +13 -0
- package/dist/src/crud/utils/toFormData.d.ts.map +1 -0
- 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 +44 -0
- package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -0
- package/dist/src/data/createRestResourceHandlers.d.ts +5 -5
- package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -1
- package/dist/src/data/dataProviderTypes.d.ts +6 -5
- package/dist/src/data/dataProviderTypes.d.ts.map +1 -1
- package/dist/src/data/parseFormErrorHelpers.d.ts +23 -6
- package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
- package/dist/src/data/resourceHandlers.d.ts +1 -1
- package/dist/src/data/resourceHandlers.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -4
- 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 -219
- package/src/crud/ListActionsBar.tsx +106 -106
- package/src/crud/ResourceForm.tsx +83 -166
- package/src/crud/ResourceFormModal.tsx +75 -133
- package/src/crud/ResourceList.tsx +1 -0
- 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 -0
- 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 -0
- package/src/crud/context/ListContext.tsx +93 -93
- package/src/crud/context/PayloadFieldsContext.tsx +62 -0
- package/src/crud/fields/BooleanField.tsx +38 -38
- package/src/crud/fields/DateField.tsx +53 -53
- package/src/crud/fields/FieldWrapper.tsx +87 -87
- package/src/crud/fields/FileField.tsx +123 -0
- package/src/crud/fields/ImageField.tsx +120 -0
- 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 -0
- package/src/crud/fields/useUploadPreviewUrl.ts +20 -0
- 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 -90
- package/src/crud/types.ts +146 -132
- package/src/crud/utils/buildFormPayload.ts +24 -0
- package/src/crud/utils/buildInlineRowsPayload.ts +37 -0
- package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -0
- 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 -38
- package/src/crud/utils/formSectionErrors.ts +85 -85
- package/src/crud/utils/{getByPath.ts → getFormValue.ts} +13 -13
- package/src/crud/utils/hasUploadValues.ts +9 -0
- package/src/crud/utils/nestedFieldPath.ts +8 -0
- package/src/crud/utils/prepareFormSubmitBody.ts +22 -0
- package/src/crud/utils/referenceSelectNotFoundContent.tsx +7 -0
- package/src/crud/utils/{setByPath.ts → setFormValue.ts} +18 -18
- package/src/crud/utils/sortQueryParam.ts +37 -37
- package/src/crud/utils/toFormData.ts +79 -0
- 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 -0
- 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 -57
- package/src/data/dataProviderTypes.ts +7 -5
- package/src/data/dataUtils.test.ts +228 -108
- package/src/data/inMemoryList.ts +97 -97
- package/src/data/parseFormErrorHelpers.test.ts +201 -84
- package/src/data/parseFormErrorHelpers.ts +376 -224
- package/src/data/resourceHandlers.ts +1 -1
- package/src/data/sortHelpers.ts +48 -48
- package/src/index.ts +211 -195
- package/src/layouts/AdminLayout.tsx +530 -381
- 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
- package/dist/src/crud/context/SubmitFieldsContext.d.ts +0 -18
- package/dist/src/crud/context/SubmitFieldsContext.d.ts.map +0 -1
- package/dist/src/crud/utils/getByPath.d.ts +0 -3
- package/dist/src/crud/utils/getByPath.d.ts.map +0 -1
- package/dist/src/crud/utils/inlineArrayName.d.ts +0 -3
- package/dist/src/crud/utils/inlineArrayName.d.ts.map +0 -1
- package/dist/src/crud/utils/inlineFieldName.d.ts +0 -3
- package/dist/src/crud/utils/inlineFieldName.d.ts.map +0 -1
- package/dist/src/crud/utils/pickBySources.d.ts +0 -6
- package/dist/src/crud/utils/pickBySources.d.ts.map +0 -1
- package/dist/src/crud/utils/setByPath.d.ts +0 -3
- package/dist/src/crud/utils/setByPath.d.ts.map +0 -1
- package/dist/src/crud/utils/useResourceFormData.d.ts +0 -47
- package/dist/src/crud/utils/useResourceFormData.d.ts.map +0 -1
- package/src/crud/context/SubmitFieldsContext.tsx +0 -62
- package/src/crud/utils/inlineArrayName.ts +0 -4
- package/src/crud/utils/inlineFieldName.ts +0 -8
- package/src/crud/utils/pickBySources.ts +0 -24
- package/src/crud/utils/useResourceFormData.ts +0 -231
|
@@ -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
|
+
}
|