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,30 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createContext,
|
|
3
|
-
useContext,
|
|
4
|
-
useMemo,
|
|
5
|
-
type ReactNode,
|
|
6
|
-
} from "react";
|
|
7
|
-
import type { DataProvider as DataProviderContract } from "../data/dataProviderTypes";
|
|
8
|
-
|
|
9
|
-
const DataContext = createContext<DataProviderContract | null>(null);
|
|
10
|
-
|
|
11
|
-
export type DataProviderProps = {
|
|
12
|
-
children: ReactNode;
|
|
13
|
-
/** Your API client (REST adapter, in-memory mock, etc.). */
|
|
14
|
-
value: DataProviderContract;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export function DataProvider({ children, value }: DataProviderProps) {
|
|
18
|
-
const memo = useMemo(() => value, [value]);
|
|
19
|
-
return (
|
|
20
|
-
<DataContext.Provider value={memo}>{children}</DataContext.Provider>
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function useDataProvider() {
|
|
25
|
-
const ctx = useContext(DataContext);
|
|
26
|
-
if (!ctx) {
|
|
27
|
-
throw new Error("useDataProvider must be used within DataProvider");
|
|
28
|
-
}
|
|
29
|
-
return ctx;
|
|
30
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useMemo,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
} from "react";
|
|
7
|
+
import type { DataProvider as DataProviderContract } from "../data/dataProviderTypes";
|
|
8
|
+
|
|
9
|
+
const DataContext = createContext<DataProviderContract | null>(null);
|
|
10
|
+
|
|
11
|
+
export type DataProviderProps = {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
/** Your API client (REST adapter, in-memory mock, etc.). */
|
|
14
|
+
value: DataProviderContract;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function DataProvider({ children, value }: DataProviderProps) {
|
|
18
|
+
const memo = useMemo(() => value, [value]);
|
|
19
|
+
return (
|
|
20
|
+
<DataContext.Provider value={memo}>{children}</DataContext.Provider>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useDataProvider() {
|
|
25
|
+
const ctx = useContext(DataContext);
|
|
26
|
+
if (!ctx) {
|
|
27
|
+
throw new Error("useDataProvider must be used within DataProvider");
|
|
28
|
+
}
|
|
29
|
+
return ctx;
|
|
30
|
+
}
|
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createContext,
|
|
3
|
-
useCallback,
|
|
4
|
-
useContext,
|
|
5
|
-
useMemo,
|
|
6
|
-
type ReactNode,
|
|
7
|
-
} from "react";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Check whether the current user has a permission string from your backend.
|
|
11
|
-
* Example: `can("users.create")` or `can("main.add_user")`.
|
|
12
|
-
*/
|
|
13
|
-
export type PermissionsChecker = (permission: string) => boolean;
|
|
14
|
-
|
|
15
|
-
const PermissionsContext = createContext<PermissionsChecker | null>(null);
|
|
16
|
-
|
|
17
|
-
export type PermissionsProviderProps = {
|
|
18
|
-
children: ReactNode;
|
|
19
|
-
/** Return true if the current user has this permission string. */
|
|
20
|
-
can: PermissionsChecker;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export function PermissionsProvider({
|
|
24
|
-
children,
|
|
25
|
-
can,
|
|
26
|
-
}: PermissionsProviderProps) {
|
|
27
|
-
const memo = useMemo(() => can, [can]);
|
|
28
|
-
return (
|
|
29
|
-
<PermissionsContext.Provider value={memo}>
|
|
30
|
-
{children}
|
|
31
|
-
</PermissionsContext.Provider>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function usePermissions(): PermissionsChecker {
|
|
36
|
-
const ctx = useContext(PermissionsContext);
|
|
37
|
-
if (!ctx) {
|
|
38
|
-
throw new Error(
|
|
39
|
-
"usePermissions must be used within PermissionsProvider",
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
return ctx;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Build a `can(permission)` checker from the user's permission strings (e.g. from login). */
|
|
46
|
-
export function createPermissionsChecker(
|
|
47
|
-
getPermissions: () => string[] | undefined,
|
|
48
|
-
): PermissionsChecker {
|
|
49
|
-
return (permission: string) =>
|
|
50
|
-
getPermissions()?.includes(permission) ?? false;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Convenience: `const allowed = useCan("users.create")();` */
|
|
54
|
-
export function useCan(permission: string) {
|
|
55
|
-
const can = usePermissions();
|
|
56
|
-
return useCallback(() => can(permission), [can, permission]);
|
|
57
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check whether the current user has a permission string from your backend.
|
|
11
|
+
* Example: `can("users.create")` or `can("main.add_user")`.
|
|
12
|
+
*/
|
|
13
|
+
export type PermissionsChecker = (permission: string) => boolean;
|
|
14
|
+
|
|
15
|
+
const PermissionsContext = createContext<PermissionsChecker | null>(null);
|
|
16
|
+
|
|
17
|
+
export type PermissionsProviderProps = {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
/** Return true if the current user has this permission string. */
|
|
20
|
+
can: PermissionsChecker;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function PermissionsProvider({
|
|
24
|
+
children,
|
|
25
|
+
can,
|
|
26
|
+
}: PermissionsProviderProps) {
|
|
27
|
+
const memo = useMemo(() => can, [can]);
|
|
28
|
+
return (
|
|
29
|
+
<PermissionsContext.Provider value={memo}>
|
|
30
|
+
{children}
|
|
31
|
+
</PermissionsContext.Provider>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function usePermissions(): PermissionsChecker {
|
|
36
|
+
const ctx = useContext(PermissionsContext);
|
|
37
|
+
if (!ctx) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
"usePermissions must be used within PermissionsProvider",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return ctx;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Build a `can(permission)` checker from the user's permission strings (e.g. from login). */
|
|
46
|
+
export function createPermissionsChecker(
|
|
47
|
+
getPermissions: () => string[] | undefined,
|
|
48
|
+
): PermissionsChecker {
|
|
49
|
+
return (permission: string) =>
|
|
50
|
+
getPermissions()?.includes(permission) ?? false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Convenience: `const allowed = useCan("users.create")();` */
|
|
54
|
+
export function useCan(permission: string) {
|
|
55
|
+
const can = usePermissions();
|
|
56
|
+
return useCallback(() => can(permission), [can, permission]);
|
|
57
|
+
}
|
package/src/crud/FilterBar.tsx
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { Space, Typography } from "antd";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
3
|
-
import { useFilterContext } from "./context/FilterContext";
|
|
4
|
-
|
|
5
|
-
function FilterBarUI() {
|
|
6
|
-
const ctx = useFilterContext();
|
|
7
|
-
if (!ctx || ctx.filters.length === 0) return null;
|
|
8
|
-
|
|
9
|
-
return (
|
|
10
|
-
<Space wrap size="middle" style={{ marginBottom: 16 }}>
|
|
11
|
-
{ctx.filters.map((f) => (
|
|
12
|
-
<Space key={f.key} orientation="vertical" size={2}>
|
|
13
|
-
{f.label ? (
|
|
14
|
-
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
15
|
-
{f.label}
|
|
16
|
-
</Typography.Text>
|
|
17
|
-
) : null}
|
|
18
|
-
{f.render({
|
|
19
|
-
value: ctx.values[f.source],
|
|
20
|
-
onChange: (v) => ctx.setFilterValue(f.source, v),
|
|
21
|
-
})}
|
|
22
|
-
</Space>
|
|
23
|
-
))}
|
|
24
|
-
</Space>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Wrap filter field components; renders filter controls above the table. */
|
|
29
|
-
export function FilterBar({ children }: { children: ReactNode }) {
|
|
30
|
-
return (
|
|
31
|
-
<>
|
|
32
|
-
{children}
|
|
33
|
-
<FilterBarUI />
|
|
34
|
-
</>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
1
|
+
import { Space, Typography } from "antd";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { useFilterContext } from "./context/FilterContext";
|
|
4
|
+
|
|
5
|
+
function FilterBarUI() {
|
|
6
|
+
const ctx = useFilterContext();
|
|
7
|
+
if (!ctx || ctx.filters.length === 0) return null;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Space wrap size="middle" style={{ marginBottom: 16 }}>
|
|
11
|
+
{ctx.filters.map((f) => (
|
|
12
|
+
<Space key={f.key} orientation="vertical" size={2}>
|
|
13
|
+
{f.label ? (
|
|
14
|
+
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
15
|
+
{f.label}
|
|
16
|
+
</Typography.Text>
|
|
17
|
+
) : null}
|
|
18
|
+
{f.render({
|
|
19
|
+
value: ctx.values[f.source],
|
|
20
|
+
onChange: (v) => ctx.setFilterValue(f.source, v),
|
|
21
|
+
})}
|
|
22
|
+
</Space>
|
|
23
|
+
))}
|
|
24
|
+
</Space>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Wrap filter field components; renders filter controls above the table. */
|
|
29
|
+
export function FilterBar({ children }: { children: ReactNode }) {
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
{children}
|
|
33
|
+
<FilterBarUI />
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Alert } from "antd";
|
|
2
|
+
|
|
3
|
+
export type FormGlobalErrorsAlertProps = {
|
|
4
|
+
errors: string[];
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function FormGlobalErrorsAlert({ errors }: FormGlobalErrorsAlertProps) {
|
|
8
|
+
if (!errors.length) return null;
|
|
9
|
+
|
|
10
|
+
if (errors.length === 1) {
|
|
11
|
+
return (
|
|
12
|
+
<Alert
|
|
13
|
+
type="error"
|
|
14
|
+
title={errors[0]}
|
|
15
|
+
showIcon
|
|
16
|
+
style={{ marginBottom: 16 }}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Alert
|
|
23
|
+
type="error"
|
|
24
|
+
title="Could not save"
|
|
25
|
+
showIcon
|
|
26
|
+
style={{ marginBottom: 16 }}
|
|
27
|
+
description={
|
|
28
|
+
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
|
29
|
+
{errors.map((msg) => (
|
|
30
|
+
<li key={msg}>{msg}</li>
|
|
31
|
+
))}
|
|
32
|
+
</ul>
|
|
33
|
+
}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
package/src/crud/FormSteps.tsx
CHANGED
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
import { Button, Space, Steps, type StepsProps } from "antd";
|
|
2
|
-
import {
|
|
3
|
-
Children,
|
|
4
|
-
isValidElement,
|
|
5
|
-
useCallback,
|
|
6
|
-
useMemo,
|
|
7
|
-
useState,
|
|
8
|
-
type CSSProperties,
|
|
9
|
-
type ReactElement,
|
|
10
|
-
type ReactNode,
|
|
11
|
-
} from "react";
|
|
12
|
-
import { useFormContext, useFormState } from "react-hook-form";
|
|
13
|
-
import { FormSectionSourcesProvider } from "./context/FormSectionContext";
|
|
14
|
-
import {
|
|
15
|
-
sectionHasError,
|
|
16
|
-
useActivateFirstErrorSection,
|
|
17
|
-
useSectionSourceRefs,
|
|
18
|
-
} from "./utils/formSectionErrors";
|
|
19
|
-
|
|
20
|
-
export type FormStepProps = {
|
|
21
|
-
title: ReactNode;
|
|
22
|
-
description?: ReactNode;
|
|
23
|
-
children: ReactNode;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export function FormStep(_props: FormStepProps) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function isFormStep(child: ReactNode): child is ReactElement<FormStepProps> {
|
|
31
|
-
return isValidElement(child) && child.type === FormStep;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type FormStepsProps = {
|
|
35
|
-
children: ReactNode;
|
|
36
|
-
initialStep?: number;
|
|
37
|
-
/** Show Previous / Next controls below the active step. Default true. */
|
|
38
|
-
showNavigation?: boolean;
|
|
39
|
-
/** Allow clicking step headers to change the active step. Default false. */
|
|
40
|
-
allowStepSelect?: boolean;
|
|
41
|
-
stepsStyle?: CSSProperties;
|
|
42
|
-
navigationStyle?: CSSProperties;
|
|
43
|
-
} & Partial<Pick<StepsProps, "size" | "direction" | "type" | "status">>;
|
|
44
|
-
|
|
45
|
-
export function FormSteps({
|
|
46
|
-
children,
|
|
47
|
-
initialStep = 0,
|
|
48
|
-
showNavigation = true,
|
|
49
|
-
allowStepSelect = false,
|
|
50
|
-
stepsStyle,
|
|
51
|
-
navigationStyle,
|
|
52
|
-
size,
|
|
53
|
-
direction,
|
|
54
|
-
type,
|
|
55
|
-
status,
|
|
56
|
-
}: FormStepsProps) {
|
|
57
|
-
const steps = useMemo(
|
|
58
|
-
() => Children.toArray(children).filter(isFormStep),
|
|
59
|
-
[children],
|
|
60
|
-
);
|
|
61
|
-
const sectionRefs = useSectionSourceRefs(steps.length);
|
|
62
|
-
const [current, setCurrent] = useState(initialStep);
|
|
63
|
-
const lastIndex = steps.length - 1;
|
|
64
|
-
|
|
65
|
-
useActivateFirstErrorSection(sectionRefs, setCurrent);
|
|
66
|
-
|
|
67
|
-
const { control, getFieldState } = useFormContext();
|
|
68
|
-
const formState = useFormState({ control });
|
|
69
|
-
|
|
70
|
-
const stepItems = useMemo(
|
|
71
|
-
() =>
|
|
72
|
-
steps.map((step, index) => {
|
|
73
|
-
const hasError = sectionHasError(
|
|
74
|
-
sectionRefs[index].current,
|
|
75
|
-
getFieldState,
|
|
76
|
-
formState,
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
title: step.props.title,
|
|
81
|
-
description: step.props.description,
|
|
82
|
-
status: hasError ? ("error" as const) : undefined,
|
|
83
|
-
};
|
|
84
|
-
}),
|
|
85
|
-
[formState, getFieldState, sectionRefs, steps],
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const handleStepSelect = useCallback(
|
|
89
|
-
(next: number) => {
|
|
90
|
-
setCurrent(next);
|
|
91
|
-
},
|
|
92
|
-
[],
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<>
|
|
97
|
-
<Steps
|
|
98
|
-
current={current}
|
|
99
|
-
items={stepItems}
|
|
100
|
-
style={{ marginBottom: 24, ...stepsStyle }}
|
|
101
|
-
onChange={allowStepSelect ? handleStepSelect : undefined}
|
|
102
|
-
size={size}
|
|
103
|
-
direction={direction}
|
|
104
|
-
type={type}
|
|
105
|
-
status={status}
|
|
106
|
-
/>
|
|
107
|
-
|
|
108
|
-
{steps.map((step, index) => (
|
|
109
|
-
<div
|
|
110
|
-
key={(step.key as string | undefined) ?? String(index)}
|
|
111
|
-
style={{ display: current === index ? undefined : "none" }}
|
|
112
|
-
>
|
|
113
|
-
<FormSectionSourcesProvider sourcesRef={sectionRefs[index]}>
|
|
114
|
-
{step.props.children}
|
|
115
|
-
</FormSectionSourcesProvider>
|
|
116
|
-
</div>
|
|
117
|
-
))}
|
|
118
|
-
|
|
119
|
-
{showNavigation && steps.length > 1 ? (
|
|
120
|
-
<Space style={{ marginTop: 16, ...navigationStyle }}>
|
|
121
|
-
<Button disabled={current === 0} onClick={() => setCurrent((s) => s - 1)}>
|
|
122
|
-
Previous
|
|
123
|
-
</Button>
|
|
124
|
-
<Button
|
|
125
|
-
type="primary"
|
|
126
|
-
disabled={current === lastIndex}
|
|
127
|
-
onClick={() => setCurrent((s) => s + 1)}
|
|
128
|
-
>
|
|
129
|
-
Next
|
|
130
|
-
</Button>
|
|
131
|
-
</Space>
|
|
132
|
-
) : null}
|
|
133
|
-
</>
|
|
134
|
-
);
|
|
135
|
-
}
|
|
1
|
+
import { Button, Space, Steps, type StepsProps } from "antd";
|
|
2
|
+
import {
|
|
3
|
+
Children,
|
|
4
|
+
isValidElement,
|
|
5
|
+
useCallback,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
type CSSProperties,
|
|
9
|
+
type ReactElement,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { useFormContext, useFormState } from "react-hook-form";
|
|
13
|
+
import { FormSectionSourcesProvider } from "./context/FormSectionContext";
|
|
14
|
+
import {
|
|
15
|
+
sectionHasError,
|
|
16
|
+
useActivateFirstErrorSection,
|
|
17
|
+
useSectionSourceRefs,
|
|
18
|
+
} from "./utils/formSectionErrors";
|
|
19
|
+
|
|
20
|
+
export type FormStepProps = {
|
|
21
|
+
title: ReactNode;
|
|
22
|
+
description?: ReactNode;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function FormStep(_props: FormStepProps) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isFormStep(child: ReactNode): child is ReactElement<FormStepProps> {
|
|
31
|
+
return isValidElement(child) && child.type === FormStep;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type FormStepsProps = {
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
initialStep?: number;
|
|
37
|
+
/** Show Previous / Next controls below the active step. Default true. */
|
|
38
|
+
showNavigation?: boolean;
|
|
39
|
+
/** Allow clicking step headers to change the active step. Default false. */
|
|
40
|
+
allowStepSelect?: boolean;
|
|
41
|
+
stepsStyle?: CSSProperties;
|
|
42
|
+
navigationStyle?: CSSProperties;
|
|
43
|
+
} & Partial<Pick<StepsProps, "size" | "direction" | "type" | "status">>;
|
|
44
|
+
|
|
45
|
+
export function FormSteps({
|
|
46
|
+
children,
|
|
47
|
+
initialStep = 0,
|
|
48
|
+
showNavigation = true,
|
|
49
|
+
allowStepSelect = false,
|
|
50
|
+
stepsStyle,
|
|
51
|
+
navigationStyle,
|
|
52
|
+
size,
|
|
53
|
+
direction,
|
|
54
|
+
type,
|
|
55
|
+
status,
|
|
56
|
+
}: FormStepsProps) {
|
|
57
|
+
const steps = useMemo(
|
|
58
|
+
() => Children.toArray(children).filter(isFormStep),
|
|
59
|
+
[children],
|
|
60
|
+
);
|
|
61
|
+
const sectionRefs = useSectionSourceRefs(steps.length);
|
|
62
|
+
const [current, setCurrent] = useState(initialStep);
|
|
63
|
+
const lastIndex = steps.length - 1;
|
|
64
|
+
|
|
65
|
+
useActivateFirstErrorSection(sectionRefs, setCurrent);
|
|
66
|
+
|
|
67
|
+
const { control, getFieldState } = useFormContext();
|
|
68
|
+
const formState = useFormState({ control });
|
|
69
|
+
|
|
70
|
+
const stepItems = useMemo(
|
|
71
|
+
() =>
|
|
72
|
+
steps.map((step, index) => {
|
|
73
|
+
const hasError = sectionHasError(
|
|
74
|
+
sectionRefs[index].current,
|
|
75
|
+
getFieldState,
|
|
76
|
+
formState,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
title: step.props.title,
|
|
81
|
+
description: step.props.description,
|
|
82
|
+
status: hasError ? ("error" as const) : undefined,
|
|
83
|
+
};
|
|
84
|
+
}),
|
|
85
|
+
[formState, getFieldState, sectionRefs, steps],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const handleStepSelect = useCallback(
|
|
89
|
+
(next: number) => {
|
|
90
|
+
setCurrent(next);
|
|
91
|
+
},
|
|
92
|
+
[],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<>
|
|
97
|
+
<Steps
|
|
98
|
+
current={current}
|
|
99
|
+
items={stepItems}
|
|
100
|
+
style={{ marginBottom: 24, ...stepsStyle }}
|
|
101
|
+
onChange={allowStepSelect ? handleStepSelect : undefined}
|
|
102
|
+
size={size}
|
|
103
|
+
direction={direction}
|
|
104
|
+
type={type}
|
|
105
|
+
status={status}
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
{steps.map((step, index) => (
|
|
109
|
+
<div
|
|
110
|
+
key={(step.key as string | undefined) ?? String(index)}
|
|
111
|
+
style={{ display: current === index ? undefined : "none" }}
|
|
112
|
+
>
|
|
113
|
+
<FormSectionSourcesProvider sourcesRef={sectionRefs[index]}>
|
|
114
|
+
{step.props.children}
|
|
115
|
+
</FormSectionSourcesProvider>
|
|
116
|
+
</div>
|
|
117
|
+
))}
|
|
118
|
+
|
|
119
|
+
{showNavigation && steps.length > 1 ? (
|
|
120
|
+
<Space style={{ marginTop: 16, ...navigationStyle }}>
|
|
121
|
+
<Button disabled={current === 0} onClick={() => setCurrent((s) => s - 1)}>
|
|
122
|
+
Previous
|
|
123
|
+
</Button>
|
|
124
|
+
<Button
|
|
125
|
+
type="primary"
|
|
126
|
+
disabled={current === lastIndex}
|
|
127
|
+
onClick={() => setCurrent((s) => s + 1)}
|
|
128
|
+
>
|
|
129
|
+
Next
|
|
130
|
+
</Button>
|
|
131
|
+
</Space>
|
|
132
|
+
) : null}
|
|
133
|
+
</>
|
|
134
|
+
);
|
|
135
|
+
}
|