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.
Files changed (230) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +254 -201
  3. package/dist/index.css +2 -0
  4. package/dist/index.js +2073 -1202
  5. package/dist/src/components/NavMenuLabel.d.ts +8 -0
  6. package/dist/src/components/NavMenuLabel.d.ts.map +1 -0
  7. package/dist/src/components/NavMenuSearch.d.ts +9 -0
  8. package/dist/src/components/NavMenuSearch.d.ts.map +1 -0
  9. package/dist/src/components/ScrollableArea.d.ts +10 -0
  10. package/dist/src/components/ScrollableArea.d.ts.map +1 -0
  11. package/dist/src/components/index.d.ts +3 -0
  12. package/dist/src/components/index.d.ts.map +1 -1
  13. package/dist/src/context/AuthProvider.d.ts +2 -0
  14. package/dist/src/context/AuthProvider.d.ts.map +1 -1
  15. package/dist/src/crud/FormGlobalErrorsAlert.d.ts +5 -0
  16. package/dist/src/crud/FormGlobalErrorsAlert.d.ts.map +1 -0
  17. package/dist/src/crud/InlineFormSet.d.ts +2 -17
  18. package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
  19. package/dist/src/crud/ResourceForm.d.ts +2 -10
  20. package/dist/src/crud/ResourceForm.d.ts.map +1 -1
  21. package/dist/src/crud/ResourceFormModal.d.ts +7 -1
  22. package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
  23. package/dist/src/crud/ResourceList.d.ts.map +1 -1
  24. package/dist/src/crud/ResourceRecordForm.d.ts +27 -0
  25. package/dist/src/crud/ResourceRecordForm.d.ts.map +1 -0
  26. package/dist/src/crud/columns/ImageColumn.d.ts +14 -0
  27. package/dist/src/crud/columns/ImageColumn.d.ts.map +1 -0
  28. package/dist/src/crud/context/InlineFieldsRegistry.d.ts +15 -0
  29. package/dist/src/crud/context/InlineFieldsRegistry.d.ts.map +1 -0
  30. package/dist/src/crud/context/PayloadFieldsContext.d.ts +18 -0
  31. package/dist/src/crud/context/PayloadFieldsContext.d.ts.map +1 -0
  32. package/dist/src/crud/fields/FileField.d.ts +12 -0
  33. package/dist/src/crud/fields/FileField.d.ts.map +1 -0
  34. package/dist/src/crud/fields/ImageField.d.ts +14 -0
  35. package/dist/src/crud/fields/ImageField.d.ts.map +1 -0
  36. package/dist/src/crud/fields/ReferenceField.d.ts +12 -2
  37. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
  38. package/dist/src/crud/fields/ReferenceInputActions.d.ts +16 -0
  39. package/dist/src/crud/fields/ReferenceInputActions.d.ts.map +1 -0
  40. package/dist/src/crud/fields/ReferenceManyField.d.ts +10 -1
  41. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
  42. package/dist/src/crud/fields/uploadFieldUtils.d.ts +5 -0
  43. package/dist/src/crud/fields/uploadFieldUtils.d.ts.map +1 -0
  44. package/dist/src/crud/fields/useUploadPreviewUrl.d.ts +4 -0
  45. package/dist/src/crud/fields/useUploadPreviewUrl.d.ts.map +1 -0
  46. package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
  47. package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
  48. package/dist/src/crud/index.d.ts +27 -9
  49. package/dist/src/crud/index.d.ts.map +1 -1
  50. package/dist/src/crud/types.d.ts +21 -7
  51. package/dist/src/crud/types.d.ts.map +1 -1
  52. package/dist/src/crud/utils/buildFormPayload.d.ts +6 -0
  53. package/dist/src/crud/utils/buildFormPayload.d.ts.map +1 -0
  54. package/dist/src/crud/utils/buildInlineRowsPayload.d.ts +6 -0
  55. package/dist/src/crud/utils/buildInlineRowsPayload.d.ts.map +1 -0
  56. package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts +5 -0
  57. package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts.map +1 -0
  58. package/dist/src/crud/utils/choiceSelectionUtils.d.ts +10 -0
  59. package/dist/src/crud/utils/choiceSelectionUtils.d.ts.map +1 -0
  60. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts +2 -0
  61. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts.map +1 -0
  62. package/dist/src/crud/utils/formErrors.d.ts +18 -7
  63. package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
  64. package/dist/src/crud/utils/formErrors.test.d.ts +2 -0
  65. package/dist/src/crud/utils/formErrors.test.d.ts.map +1 -0
  66. package/dist/src/crud/utils/getFormValue.d.ts +3 -0
  67. package/dist/src/crud/utils/getFormValue.d.ts.map +1 -0
  68. package/dist/src/crud/utils/hasUploadValues.d.ts +3 -0
  69. package/dist/src/crud/utils/hasUploadValues.d.ts.map +1 -0
  70. package/dist/src/crud/utils/nestedFieldPath.d.ts +3 -0
  71. package/dist/src/crud/utils/nestedFieldPath.d.ts.map +1 -0
  72. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +8 -0
  73. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -0
  74. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts +4 -0
  75. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts.map +1 -0
  76. package/dist/src/crud/utils/setFormValue.d.ts +3 -0
  77. package/dist/src/crud/utils/setFormValue.d.ts.map +1 -0
  78. package/dist/src/crud/utils/toFormData.d.ts +13 -0
  79. package/dist/src/crud/utils/toFormData.d.ts.map +1 -0
  80. package/dist/src/crud/utils/uploadReferenceUtils.d.ts +5 -0
  81. package/dist/src/crud/utils/uploadReferenceUtils.d.ts.map +1 -0
  82. package/dist/src/crud/utils/useChoices.d.ts +27 -1
  83. package/dist/src/crud/utils/useChoices.d.ts.map +1 -1
  84. package/dist/src/crud/utils/useFormRecord.d.ts +44 -0
  85. package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -0
  86. package/dist/src/data/createRestResourceHandlers.d.ts +5 -5
  87. package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -1
  88. package/dist/src/data/dataProviderTypes.d.ts +6 -5
  89. package/dist/src/data/dataProviderTypes.d.ts.map +1 -1
  90. package/dist/src/data/parseFormErrorHelpers.d.ts +23 -6
  91. package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
  92. package/dist/src/data/resourceHandlers.d.ts +1 -1
  93. package/dist/src/data/resourceHandlers.d.ts.map +1 -1
  94. package/dist/src/index.d.ts +4 -4
  95. package/dist/src/index.d.ts.map +1 -1
  96. package/dist/src/layouts/AdminLayout.d.ts +1 -1
  97. package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
  98. package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -1
  99. package/dist/src/layouts/navFilter.d.ts +5 -0
  100. package/dist/src/layouts/navFilter.d.ts.map +1 -0
  101. package/dist/src/layouts/navMenuItems.d.ts +6 -0
  102. package/dist/src/layouts/navMenuItems.d.ts.map +1 -0
  103. package/dist/src/types.d.ts +12 -0
  104. package/dist/src/types.d.ts.map +1 -1
  105. package/package.json +79 -59
  106. package/src/app/AdminApp.tsx +34 -34
  107. package/src/components/AuthAlternateLink.tsx +23 -23
  108. package/src/components/DensitySwitch.tsx +31 -31
  109. package/src/components/NavMenuLabel.tsx +26 -0
  110. package/src/components/NavMenuSearch.tsx +76 -0
  111. package/src/components/ScrollableArea.tsx +46 -0
  112. package/src/components/ThemeSwitch.tsx +28 -28
  113. package/src/components/ThemeToolbar.tsx +34 -34
  114. package/src/components/index.ts +7 -4
  115. package/src/components/navMenu.css +21 -0
  116. package/src/components/scrollable.css +28 -0
  117. package/src/context/AppThemeProvider.tsx +128 -128
  118. package/src/context/AuthProvider.tsx +89 -77
  119. package/src/context/DataProvider.tsx +30 -30
  120. package/src/context/PermissionsProvider.tsx +57 -57
  121. package/src/crud/FilterBar.tsx +36 -36
  122. package/src/crud/FormGlobalErrorsAlert.tsx +36 -0
  123. package/src/crud/FormSteps.tsx +135 -135
  124. package/src/crud/FormTabs.tsx +126 -126
  125. package/src/crud/InlineFormSet.tsx +162 -219
  126. package/src/crud/ListActionsBar.tsx +106 -106
  127. package/src/crud/ResourceForm.tsx +83 -166
  128. package/src/crud/ResourceFormModal.tsx +75 -133
  129. package/src/crud/ResourceList.tsx +1 -0
  130. package/src/crud/ResourceRecordForm.tsx +166 -0
  131. package/src/crud/columns/BooleanColumn.tsx +32 -32
  132. package/src/crud/columns/CustomColumn.tsx +33 -33
  133. package/src/crud/columns/DateColumn.tsx +28 -28
  134. package/src/crud/columns/ImageColumn.tsx +58 -0
  135. package/src/crud/columns/NumberColumn.tsx +31 -31
  136. package/src/crud/columns/ReferenceColumn.tsx +85 -85
  137. package/src/crud/columns/ReferenceManyColumn.tsx +67 -67
  138. package/src/crud/columns/TextColumn.tsx +38 -38
  139. package/src/crud/context/FilterContext.tsx +71 -71
  140. package/src/crud/context/FormContext.tsx +39 -39
  141. package/src/crud/context/FormSectionContext.tsx +22 -22
  142. package/src/crud/context/InlineFieldsRegistry.tsx +61 -0
  143. package/src/crud/context/ListContext.tsx +93 -93
  144. package/src/crud/context/PayloadFieldsContext.tsx +62 -0
  145. package/src/crud/fields/BooleanField.tsx +38 -38
  146. package/src/crud/fields/DateField.tsx +53 -53
  147. package/src/crud/fields/FieldWrapper.tsx +87 -87
  148. package/src/crud/fields/FileField.tsx +123 -0
  149. package/src/crud/fields/ImageField.tsx +120 -0
  150. package/src/crud/fields/NumberField.tsx +52 -52
  151. package/src/crud/fields/PasswordField.tsx +99 -99
  152. package/src/crud/fields/ReferenceField.tsx +237 -96
  153. package/src/crud/fields/ReferenceInputActions.tsx +106 -0
  154. package/src/crud/fields/ReferenceManyField.tsx +200 -76
  155. package/src/crud/fields/SelectField.tsx +48 -48
  156. package/src/crud/fields/TextField.tsx +46 -46
  157. package/src/crud/fields/uploadFieldUtils.ts +21 -0
  158. package/src/crud/fields/useUploadPreviewUrl.ts +20 -0
  159. package/src/crud/filters/BooleanFilter.tsx +38 -38
  160. package/src/crud/filters/DateFilter.tsx +35 -35
  161. package/src/crud/filters/NumberFilter.tsx +33 -33
  162. package/src/crud/filters/ReferenceFilter.tsx +192 -173
  163. package/src/crud/filters/SelectFilter.tsx +44 -44
  164. package/src/crud/filters/TextFilter.tsx +36 -36
  165. package/src/crud/index.ts +99 -90
  166. package/src/crud/types.ts +146 -132
  167. package/src/crud/utils/buildFormPayload.ts +24 -0
  168. package/src/crud/utils/buildInlineRowsPayload.ts +37 -0
  169. package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -0
  170. package/src/crud/utils/choiceSelectionUtils.test.ts +45 -0
  171. package/src/crud/utils/choiceSelectionUtils.ts +116 -0
  172. package/src/crud/utils/formErrors.test.ts +168 -0
  173. package/src/crud/utils/formErrors.ts +138 -38
  174. package/src/crud/utils/formSectionErrors.ts +85 -85
  175. package/src/crud/utils/{getByPath.ts → getFormValue.ts} +13 -13
  176. package/src/crud/utils/hasUploadValues.ts +9 -0
  177. package/src/crud/utils/nestedFieldPath.ts +8 -0
  178. package/src/crud/utils/prepareFormSubmitBody.ts +22 -0
  179. package/src/crud/utils/referenceSelectNotFoundContent.tsx +7 -0
  180. package/src/crud/utils/{setByPath.ts → setFormValue.ts} +18 -18
  181. package/src/crud/utils/sortQueryParam.ts +37 -37
  182. package/src/crud/utils/toFormData.ts +79 -0
  183. package/src/crud/utils/uploadReferenceUtils.ts +44 -0
  184. package/src/crud/utils/useAbortableEffect.ts +17 -17
  185. package/src/crud/utils/useChoices.ts +330 -182
  186. package/src/crud/utils/useFormRecord.ts +179 -0
  187. package/src/crud/utils/useListQueryState.ts +191 -191
  188. package/src/data/abortError.test.ts +19 -19
  189. package/src/data/abortError.ts +10 -10
  190. package/src/data/createMemoryResourceHandlers.ts +78 -78
  191. package/src/data/createRestResourceHandlers.ts +63 -57
  192. package/src/data/dataProviderTypes.ts +7 -5
  193. package/src/data/dataUtils.test.ts +228 -108
  194. package/src/data/inMemoryList.ts +97 -97
  195. package/src/data/parseFormErrorHelpers.test.ts +201 -84
  196. package/src/data/parseFormErrorHelpers.ts +376 -224
  197. package/src/data/resourceHandlers.ts +1 -1
  198. package/src/data/sortHelpers.ts +48 -48
  199. package/src/index.ts +211 -195
  200. package/src/layouts/AdminLayout.tsx +530 -381
  201. package/src/layouts/AuthPageLayout.tsx +87 -81
  202. package/src/layouts/navFilter.ts +49 -0
  203. package/src/layouts/navMenuItems.tsx +44 -0
  204. package/src/pages/LoginPage.tsx +80 -80
  205. package/src/pages/PlaceholderPage.tsx +9 -9
  206. package/src/permissions/resourcePermissions.ts +47 -47
  207. package/src/router/createAdminRouter.tsx +64 -64
  208. package/src/router/guards.tsx +63 -63
  209. package/src/router/routeAccess.ts +89 -89
  210. package/src/types.ts +10 -0
  211. package/src/vite-env.d.ts +1 -1
  212. package/dist/src/crud/context/SubmitFieldsContext.d.ts +0 -18
  213. package/dist/src/crud/context/SubmitFieldsContext.d.ts.map +0 -1
  214. package/dist/src/crud/utils/getByPath.d.ts +0 -3
  215. package/dist/src/crud/utils/getByPath.d.ts.map +0 -1
  216. package/dist/src/crud/utils/inlineArrayName.d.ts +0 -3
  217. package/dist/src/crud/utils/inlineArrayName.d.ts.map +0 -1
  218. package/dist/src/crud/utils/inlineFieldName.d.ts +0 -3
  219. package/dist/src/crud/utils/inlineFieldName.d.ts.map +0 -1
  220. package/dist/src/crud/utils/pickBySources.d.ts +0 -6
  221. package/dist/src/crud/utils/pickBySources.d.ts.map +0 -1
  222. package/dist/src/crud/utils/setByPath.d.ts +0 -3
  223. package/dist/src/crud/utils/setByPath.d.ts.map +0 -1
  224. package/dist/src/crud/utils/useResourceFormData.d.ts +0 -47
  225. package/dist/src/crud/utils/useResourceFormData.d.ts.map +0 -1
  226. package/src/crud/context/SubmitFieldsContext.tsx +0 -62
  227. package/src/crud/utils/inlineArrayName.ts +0 -4
  228. package/src/crud/utils/inlineFieldName.ts +0 -8
  229. package/src/crud/utils/pickBySources.ts +0 -24
  230. package/src/crud/utils/useResourceFormData.ts +0 -231
@@ -1,106 +1,106 @@
1
- import { Button, Select, Space, Typography } from "antd";
2
- import { useCallback, useMemo, useState } from "react";
3
- import type { ResourceListBulkAction } from "./types";
4
-
5
- export type ListActionsBarProps = {
6
- selectedCount: number;
7
- total: number;
8
- allPageSelected: boolean;
9
- allMatchingSelected: boolean;
10
- onSelectAllMatching: () => void;
11
- onClearSelection: () => void;
12
- actions: ResourceListBulkAction[];
13
- onExecute: (
14
- action: ResourceListBulkAction,
15
- selectedIds: (string | number)[],
16
- ) => Promise<void>;
17
- selectedIds: (string | number)[];
18
- running?: boolean;
19
- };
20
-
21
- export function ListActionsBar({
22
- selectedCount,
23
- total,
24
- allPageSelected,
25
- allMatchingSelected,
26
- onSelectAllMatching,
27
- onClearSelection,
28
- actions,
29
- onExecute,
30
- selectedIds,
31
- running = false,
32
- }: ListActionsBarProps) {
33
- const [actionKey, setActionKey] = useState<string | undefined>();
34
-
35
- const options = useMemo(
36
- () => actions.map((a) => ({ value: a.key, label: a.label })),
37
- [actions],
38
- );
39
-
40
- const handleGo = useCallback(async () => {
41
- const action = actions.find((a) => a.key === actionKey);
42
- if (!action || selectedCount === 0) return;
43
- await onExecute(action, selectedIds);
44
- setActionKey(undefined);
45
- }, [actions, actionKey, onExecute, selectedCount, selectedIds]);
46
-
47
- const showSelectAllMatching =
48
- allPageSelected &&
49
- !allMatchingSelected &&
50
- total > selectedCount;
51
-
52
- return (
53
- <Space
54
- wrap
55
- style={{ marginBottom: 16, width: "100%" }}
56
- align="center"
57
- >
58
- <Typography.Text type="secondary">
59
- {selectedCount} of {total} selected
60
- </Typography.Text>
61
- {selectedCount > 0 ? (
62
- <Button type="link" size="small" onClick={onClearSelection} style={{ padding: 0 }}>
63
- Clear selection
64
- </Button>
65
- ) : null}
66
- {showSelectAllMatching ? (
67
- <>
68
- <Typography.Text type="secondary">·</Typography.Text>
69
- <Button
70
- type="link"
71
- size="small"
72
- onClick={onSelectAllMatching}
73
- style={{ padding: 0 }}
74
- >
75
- Select all {total} items matching filter
76
- </Button>
77
- </>
78
- ) : null}
79
- {allMatchingSelected && total > 0 ? (
80
- <>
81
- <Typography.Text type="secondary">·</Typography.Text>
82
- <Typography.Text type="success">
83
- All {total} items selected
84
- </Typography.Text>
85
- </>
86
- ) : null}
87
- <Select
88
- placeholder="Action"
89
- style={{ minWidth: 200 }}
90
- options={options}
91
- value={actionKey}
92
- onChange={setActionKey}
93
- disabled={selectedCount === 0 || running}
94
- allowClear
95
- />
96
- <Button
97
- type="primary"
98
- onClick={() => void handleGo()}
99
- disabled={!actionKey || selectedCount === 0 || running}
100
- loading={running}
101
- >
102
- Go
103
- </Button>
104
- </Space>
105
- );
106
- }
1
+ import { Button, Select, Space, Typography } from "antd";
2
+ import { useCallback, useMemo, useState } from "react";
3
+ import type { ResourceListBulkAction } from "./types";
4
+
5
+ export type ListActionsBarProps = {
6
+ selectedCount: number;
7
+ total: number;
8
+ allPageSelected: boolean;
9
+ allMatchingSelected: boolean;
10
+ onSelectAllMatching: () => void;
11
+ onClearSelection: () => void;
12
+ actions: ResourceListBulkAction[];
13
+ onExecute: (
14
+ action: ResourceListBulkAction,
15
+ selectedIds: (string | number)[],
16
+ ) => Promise<void>;
17
+ selectedIds: (string | number)[];
18
+ running?: boolean;
19
+ };
20
+
21
+ export function ListActionsBar({
22
+ selectedCount,
23
+ total,
24
+ allPageSelected,
25
+ allMatchingSelected,
26
+ onSelectAllMatching,
27
+ onClearSelection,
28
+ actions,
29
+ onExecute,
30
+ selectedIds,
31
+ running = false,
32
+ }: ListActionsBarProps) {
33
+ const [actionKey, setActionKey] = useState<string | undefined>();
34
+
35
+ const options = useMemo(
36
+ () => actions.map((a) => ({ value: a.key, label: a.label })),
37
+ [actions],
38
+ );
39
+
40
+ const handleGo = useCallback(async () => {
41
+ const action = actions.find((a) => a.key === actionKey);
42
+ if (!action || selectedCount === 0) return;
43
+ await onExecute(action, selectedIds);
44
+ setActionKey(undefined);
45
+ }, [actions, actionKey, onExecute, selectedCount, selectedIds]);
46
+
47
+ const showSelectAllMatching =
48
+ allPageSelected &&
49
+ !allMatchingSelected &&
50
+ total > selectedCount;
51
+
52
+ return (
53
+ <Space
54
+ wrap
55
+ style={{ marginBottom: 16, width: "100%" }}
56
+ align="center"
57
+ >
58
+ <Typography.Text type="secondary">
59
+ {selectedCount} of {total} selected
60
+ </Typography.Text>
61
+ {selectedCount > 0 ? (
62
+ <Button type="link" size="small" onClick={onClearSelection} style={{ padding: 0 }}>
63
+ Clear selection
64
+ </Button>
65
+ ) : null}
66
+ {showSelectAllMatching ? (
67
+ <>
68
+ <Typography.Text type="secondary">·</Typography.Text>
69
+ <Button
70
+ type="link"
71
+ size="small"
72
+ onClick={onSelectAllMatching}
73
+ style={{ padding: 0 }}
74
+ >
75
+ Select all {total} items matching filter
76
+ </Button>
77
+ </>
78
+ ) : null}
79
+ {allMatchingSelected && total > 0 ? (
80
+ <>
81
+ <Typography.Text type="secondary">·</Typography.Text>
82
+ <Typography.Text type="success">
83
+ All {total} items selected
84
+ </Typography.Text>
85
+ </>
86
+ ) : null}
87
+ <Select
88
+ placeholder="Action"
89
+ style={{ minWidth: 200 }}
90
+ options={options}
91
+ value={actionKey}
92
+ onChange={setActionKey}
93
+ disabled={selectedCount === 0 || running}
94
+ allowClear
95
+ />
96
+ <Button
97
+ type="primary"
98
+ onClick={() => void handleGo()}
99
+ disabled={!actionKey || selectedCount === 0 || running}
100
+ loading={running}
101
+ >
102
+ Go
103
+ </Button>
104
+ </Space>
105
+ );
106
+ }
@@ -1,166 +1,83 @@
1
- import { ArrowLeftOutlined } from "@ant-design/icons";
2
- import { App, Button, Card, Form, Space, Spin, Typography, theme } from "antd";
3
- import { FormProvider, useForm, type DefaultValues, type FieldValues } from "react-hook-form";
4
- import { useEffect, useRef, type ReactNode } from "react";
5
- import { Link, useNavigate, useParams } from "react-router-dom";
6
- import { useDataProvider } from "../context/DataProvider";
7
- import { usePermissions } from "../context/PermissionsProvider";
8
- import type { ResourcePermissions } from "../permissions/resourcePermissions";
9
- import { checkResourcePermission } from "../permissions/resourcePermissions";
10
- import { FormMetaProvider } from "./context/FormContext";
11
- import { SubmitFieldsProvider } from "./context/SubmitFieldsContext";
12
- import {
13
- useResourceFormLoad,
14
- useResourceFormSubmit,
15
- type ResourceFormInlineConfig,
16
- } from "./utils/useResourceFormData";
17
-
18
- export type { ResourceFormInlineConfig };
19
-
20
- export type ResourceFormProps<T extends FieldValues> = {
21
- resource: string;
22
- title: string;
23
- listPath: string;
24
- children: ReactNode;
25
- defaultValues?: Partial<T>;
26
- onSaved?: (record: T) => void;
27
- stayOnPage?: boolean;
28
- inlines?: ResourceFormInlineConfig[];
29
- /** Permission strings for create vs edit. Omit to allow all (demos only). */
30
- permissions?: Pick<ResourcePermissions, "add" | "change">;
31
- };
32
-
33
- /**
34
- * Create/edit page shell around react-hook-form.
35
- *
36
- * Flow:
37
- * 1. Load edit: getOne(parent) + loadInlineRows → form.reset (see useResourceFormLoad)
38
- * 2. Edit — children render fields; useSubmitField tracks sources for the API payload
39
- * 3. Save — pickBySources → create/update parent → saveInlineRows (see useResourceFormSubmit)
40
- */
41
- export function ResourceForm<T extends FieldValues & { id?: unknown }>({
42
- resource,
43
- title,
44
- listPath,
45
- children,
46
- defaultValues,
47
- onSaved,
48
- stayOnPage,
49
- inlines,
50
- permissions,
51
- }: ResourceFormProps<T>) {
52
- const { id } = useParams();
53
- const isNew = id === "new" || !id;
54
- const dp = useDataProvider();
55
- const can = usePermissions();
56
- const navigate = useNavigate();
57
- const { message } = App.useApp();
58
- const { token } = theme.useToken();
59
- const submitFieldsRef = useRef(new Set<string>());
60
-
61
- const form = useForm<T>({
62
- defaultValues: defaultValues as DefaultValues<T>,
63
- });
64
-
65
- const { loading, formVersion, existingInlineIds } = useResourceFormLoad({
66
- dp,
67
- resource,
68
- id,
69
- isNew,
70
- form,
71
- message,
72
- defaultValues,
73
- inlines,
74
- });
75
-
76
- const onSubmit = useResourceFormSubmit({
77
- dp,
78
- resource,
79
- id,
80
- isNew,
81
- form,
82
- message,
83
- navigate,
84
- listPath,
85
- submitFieldsRef,
86
- inlines,
87
- existingInlineIds,
88
- onSaved,
89
- stayOnPage,
90
- });
91
-
92
- useEffect(() => {
93
- if (!permissions) return;
94
- const slot = isNew ? "add" : "change";
95
- if (!checkResourcePermission(can, permissions, slot)) {
96
- navigate(listPath, { replace: true });
97
- }
98
- }, [permissions, isNew, can, navigate, listPath]);
99
-
100
- const canSave = permissions
101
- ? checkResourcePermission(can, permissions, isNew ? "add" : "change")
102
- : true;
103
-
104
- return (
105
- <Card
106
- title={
107
- <Space>
108
- <Link to={listPath} style={{ color: token.colorText }}>
109
- <ArrowLeftOutlined /> Back
110
- </Link>
111
- <Typography.Title level={5} style={{ margin: 0 }}>
112
- {title}
113
- </Typography.Title>
114
- </Space>
115
- }
116
- >
117
- <FormMetaProvider resource={resource} isNew={isNew}>
118
- <SubmitFieldsProvider fieldsRef={submitFieldsRef}>
119
- <div style={{ position: "relative" }}>
120
- {loading ? (
121
- <div
122
- style={{
123
- position: "absolute",
124
- inset: 0,
125
- display: "flex",
126
- alignItems: "center",
127
- justifyContent: "center",
128
- zIndex: 1,
129
- }}
130
- >
131
- <Spin />
132
- </div>
133
- ) : null}
134
- {/* formVersion remounts RHF after load so inline field arrays bind correctly */}
135
- <FormProvider {...form} key={formVersion}>
136
- <Form
137
- layout="vertical"
138
- onFinish={() => void form.handleSubmit(onSubmit)()}
139
- style={{
140
- opacity: loading ? 0.4 : 1,
141
- pointerEvents: loading ? "none" : undefined,
142
- }}
143
- >
144
- {children}
145
- <Form.Item style={{ marginTop: 16 }}>
146
- <Space>
147
- <Button
148
- type="primary"
149
- htmlType="submit"
150
- disabled={loading || !canSave}
151
- >
152
- Save
153
- </Button>
154
- <Link to={listPath}>
155
- <Button disabled={loading}>Cancel</Button>
156
- </Link>
157
- </Space>
158
- </Form.Item>
159
- </Form>
160
- </FormProvider>
161
- </div>
162
- </SubmitFieldsProvider>
163
- </FormMetaProvider>
164
- </Card>
165
- );
166
- }
1
+ import { ArrowLeftOutlined } from "@ant-design/icons";
2
+ import { Card, Space, Typography, theme } from "antd";
3
+ import { type FieldValues } from "react-hook-form";
4
+ import { useEffect, type ReactNode } from "react";
5
+ import { Link, useNavigate, useParams } from "react-router-dom";
6
+ import { usePermissions } from "../context/PermissionsProvider";
7
+ import type { ResourcePermissions } from "../permissions/resourcePermissions";
8
+ import { checkResourcePermission } from "../permissions/resourcePermissions";
9
+ import { ResourceRecordForm } from "./ResourceRecordForm";
10
+
11
+ export type ResourceFormProps<T extends FieldValues> = {
12
+ resource: string;
13
+ title: string;
14
+ listPath: string;
15
+ children: ReactNode;
16
+ defaultValues?: Partial<T>;
17
+ onSaved?: (record: T) => void;
18
+ stayOnPage?: boolean;
19
+ /** Permission strings for create vs edit. Omit to allow all (demos only). */
20
+ permissions?: Pick<ResourcePermissions, "add" | "change">;
21
+ };
22
+
23
+ /**
24
+ * Create/edit page — Card chrome around {@link ResourceRecordForm}.
25
+ */
26
+ export function ResourceForm<T extends FieldValues & { id?: unknown }>({
27
+ resource,
28
+ title,
29
+ listPath,
30
+ children,
31
+ defaultValues,
32
+ onSaved,
33
+ stayOnPage,
34
+ permissions,
35
+ }: ResourceFormProps<T>) {
36
+ const { id } = useParams();
37
+ const isNew = id === "new" || !id;
38
+ const can = usePermissions();
39
+ const navigate = useNavigate();
40
+ const { token } = theme.useToken();
41
+
42
+ useEffect(() => {
43
+ if (!permissions) return;
44
+ const slot = isNew ? "add" : "change";
45
+ if (!checkResourcePermission(can, permissions, slot)) {
46
+ navigate(listPath, { replace: true });
47
+ }
48
+ }, [permissions, isNew, can, navigate, listPath]);
49
+
50
+ const canSave = permissions
51
+ ? checkResourcePermission(can, permissions, isNew ? "add" : "change")
52
+ : true;
53
+
54
+ return (
55
+ <Card
56
+ title={
57
+ <Space>
58
+ <Link to={listPath} style={{ color: token.colorText }}>
59
+ <ArrowLeftOutlined /> Back
60
+ </Link>
61
+ <Typography.Title level={5} style={{ margin: 0 }}>
62
+ {title}
63
+ </Typography.Title>
64
+ </Space>
65
+ }
66
+ >
67
+ <ResourceRecordForm<T>
68
+ resource={resource}
69
+ id={id}
70
+ defaultValues={defaultValues}
71
+ canSave={canSave}
72
+ cancelHref={listPath}
73
+ onCancel={() => navigate(listPath)}
74
+ onSuccess={(saved) => {
75
+ onSaved?.(saved);
76
+ if (!stayOnPage) navigate(listPath);
77
+ }}
78
+ >
79
+ {children}
80
+ </ResourceRecordForm>
81
+ </Card>
82
+ );
83
+ }
@@ -1,133 +1,75 @@
1
- import { App, Button, Form, Modal, Spin } from "antd";
2
- import {
3
- FormProvider,
4
- useForm,
5
- type DefaultValues,
6
- type FieldValues,
7
- } from "react-hook-form";
8
- import { useCallback, useRef, useState, type ReactNode } from "react";
9
- import { useDataProvider } from "../context/DataProvider";
10
- import { FormMetaProvider } from "./context/FormContext";
11
- import { SubmitFieldsProvider } from "./context/SubmitFieldsContext";
12
- import { pickBySources } from "./utils/pickBySources";
13
- import { parseAndApplyFormErrors } from "./utils/formErrors";
14
- import { useAbortableEffect } from "./utils/useAbortableEffect";
15
- import { isAbortError } from "../data/abortError";
16
-
17
- export type ResourceFormModalProps = {
18
- resource: string;
19
- editId: string | null;
20
- onClose: () => void;
21
- children: ReactNode;
22
- title?: string;
23
- };
24
-
25
- export function ResourceFormModal({
26
- resource,
27
- editId,
28
- onClose,
29
- children,
30
- title,
31
- }: ResourceFormModalProps) {
32
- const isNew = editId === "new" || editId == null;
33
- const open = editId != null;
34
- const dp = useDataProvider();
35
- const { message } = App.useApp();
36
- const [loading, setLoading] = useState(!isNew);
37
-
38
- const form = useForm<FieldValues>();
39
- const submitFieldsRef = useRef(new Set<string>());
40
-
41
- const load = useCallback(
42
- async (signal?: AbortSignal) => {
43
- if (isNew || !editId) {
44
- form.reset({} as DefaultValues<FieldValues>);
45
- setLoading(false);
46
- return;
47
- }
48
- setLoading(true);
49
- try {
50
- const res = await dp.getOne(resource, editId, { signal });
51
- if (signal?.aborted) return;
52
- form.reset(res.data as DefaultValues<FieldValues>);
53
- } catch (e) {
54
- if (!isAbortError(e)) {
55
- message.error(e instanceof Error ? e.message : "Load failed");
56
- }
57
- } finally {
58
- if (!signal?.aborted) setLoading(false);
59
- }
60
- },
61
- [dp, resource, editId, isNew, form, message],
62
- );
63
-
64
- useAbortableEffect(
65
- (signal) => {
66
- if (!open) return;
67
- return load(signal);
68
- },
69
- [open, load],
70
- );
71
-
72
- async function onSubmit(values: FieldValues) {
73
- try {
74
- const payload = pickBySources(
75
- values as Record<string, unknown>,
76
- Array.from(submitFieldsRef.current),
77
- );
78
- if (isNew) {
79
- await dp.create(resource, payload);
80
- message.success("Created");
81
- } else if (editId) {
82
- await dp.update(resource, { id: editId, data: payload });
83
- message.success("Updated");
84
- }
85
- onClose();
86
- } catch (e) {
87
- const handled = parseAndApplyFormErrors(dp, form, message, e, {
88
- resource,
89
- mutation: isNew ? "create" : "update",
90
- });
91
- if (!handled) {
92
- message.error(e instanceof Error ? e.message : "Save failed");
93
- }
94
- }
95
- }
96
-
97
- const modalTitle =
98
- title ?? (isNew ? `New ${resource}` : `Edit ${resource}`);
99
-
100
- return (
101
- <Modal
102
- open={open}
103
- title={modalTitle}
104
- onCancel={onClose}
105
- footer={null}
106
- destroyOnHidden
107
- width={560}
108
- >
109
- {loading ? (
110
- <Spin />
111
- ) : (
112
- <FormMetaProvider resource={resource} isNew={isNew}>
113
- <SubmitFieldsProvider fieldsRef={submitFieldsRef}>
114
- <FormProvider {...form}>
115
- <Form
116
- layout="vertical"
117
- onFinish={() => void form.handleSubmit(onSubmit)()}
118
- >
119
- {children}
120
- <Form.Item style={{ marginTop: 16, marginBottom: 0 }}>
121
- <Button type="primary" htmlType="submit" style={{ marginRight: 8 }}>
122
- Save
123
- </Button>
124
- <Button onClick={onClose}>Cancel</Button>
125
- </Form.Item>
126
- </Form>
127
- </FormProvider>
128
- </SubmitFieldsProvider>
129
- </FormMetaProvider>
130
- )}
131
- </Modal>
132
- );
133
- }
1
+ import { App, Modal } from "antd";
2
+ import { type FieldValues } from "react-hook-form";
3
+ import { type ReactNode } from "react";
4
+ import { usePermissions } from "../context/PermissionsProvider";
5
+ import {
6
+ checkResourcePermission,
7
+ type ResourcePermissions,
8
+ } from "../permissions/resourcePermissions";
9
+ import { ResourceRecordForm } from "./ResourceRecordForm";
10
+
11
+ export type ResourceFormModalProps = {
12
+ resource: string;
13
+ editId: string | null;
14
+ onClose: () => void;
15
+ children: ReactNode;
16
+ title?: string;
17
+ permissions?: Pick<ResourcePermissions, "add" | "change">;
18
+ defaultValues?: Partial<FieldValues>;
19
+ width?: number;
20
+ onSuccess?: (record: FieldValues) => void;
21
+ };
22
+
23
+ export function ResourceFormModal({
24
+ resource,
25
+ editId,
26
+ onClose,
27
+ children,
28
+ title,
29
+ permissions,
30
+ defaultValues,
31
+ width = 560,
32
+ onSuccess,
33
+ }: ResourceFormModalProps) {
34
+ const isNew = editId === "new";
35
+ const open = editId != null;
36
+ const can = usePermissions();
37
+
38
+ const modalTitle =
39
+ title ?? (isNew ? `New ${resource}` : `Edit ${resource}`);
40
+
41
+ const canSave = permissions
42
+ ? checkResourcePermission(can, permissions, isNew ? "add" : "change")
43
+ : true;
44
+
45
+ return (
46
+ <Modal
47
+ open={open}
48
+ title={modalTitle}
49
+ onCancel={onClose}
50
+ footer={null}
51
+ destroyOnHidden
52
+ width={width}
53
+ maskClosable={false}
54
+ >
55
+ {/* Nested App so message/toast works when Modal is portaled to document.body. */}
56
+ <App>
57
+ <ResourceRecordForm<FieldValues>
58
+ resource={resource}
59
+ id={editId ?? undefined}
60
+ enabled={open}
61
+ loadingMode="overlay"
62
+ defaultValues={defaultValues}
63
+ canSave={canSave}
64
+ onCancel={onClose}
65
+ onSuccess={(record) => {
66
+ onSuccess?.(record);
67
+ onClose();
68
+ }}
69
+ >
70
+ {children}
71
+ </ResourceRecordForm>
72
+ </App>
73
+ </Modal>
74
+ );
75
+ }