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,126 +1,126 @@
1
- import { Tabs, theme, type TabsProps } from "antd";
2
- import {
3
- Children,
4
- isValidElement,
5
- useCallback,
6
- useMemo,
7
- useState,
8
- type ReactElement,
9
- type ReactNode,
10
- } from "react";
11
- import { useFormContext, useFormState } from "react-hook-form";
12
- import { FormSectionSourcesProvider } from "./context/FormSectionContext";
13
- import {
14
- sectionHasError,
15
- useActivateFirstErrorSection,
16
- useSectionSourceRefs,
17
- } from "./utils/formSectionErrors";
18
-
19
- export type FormTabProps = {
20
- label: ReactNode;
21
- children: ReactNode;
22
- disabled?: boolean;
23
- };
24
-
25
- export function FormTab(_props: FormTabProps) {
26
- return null;
27
- }
28
-
29
- function isFormTab(child: ReactNode): child is ReactElement<FormTabProps> {
30
- return isValidElement(child) && child.type === FormTab;
31
- }
32
-
33
- export type FormTabsProps = Omit<TabsProps, "items"> & {
34
- children: ReactNode;
35
- };
36
-
37
- export function FormTabs({
38
- children,
39
- defaultActiveKey,
40
- activeKey: controlledActiveKey,
41
- onChange,
42
- ...tabsProps
43
- }: FormTabsProps) {
44
- const { token } = theme.useToken();
45
- const tabs = useMemo(
46
- () =>
47
- Children.toArray(children)
48
- .filter(isFormTab)
49
- .map((tab, index) => ({
50
- key: (tab.key as string | undefined) ?? String(index),
51
- label: tab.props.label,
52
- disabled: tab.props.disabled,
53
- children: tab.props.children,
54
- })),
55
- [children],
56
- );
57
-
58
- const sectionRefs = useSectionSourceRefs(tabs.length);
59
- const isControlled = controlledActiveKey !== undefined;
60
- const [internalActiveKey, setInternalActiveKey] = useState(
61
- () => (defaultActiveKey as string | undefined) ?? tabs[0]?.key ?? "0",
62
- );
63
- const activeKey = isControlled ? controlledActiveKey : internalActiveKey;
64
-
65
- const setActiveKey = useCallback(
66
- (key: string) => {
67
- if (!isControlled) {
68
- setInternalActiveKey(key);
69
- }
70
- onChange?.(key);
71
- },
72
- [isControlled, onChange],
73
- );
74
-
75
- const activateByIndex = useCallback(
76
- (index: number) => {
77
- const key = tabs[index]?.key;
78
- if (key != null) {
79
- setActiveKey(key);
80
- }
81
- },
82
- [setActiveKey, tabs],
83
- );
84
-
85
- useActivateFirstErrorSection(sectionRefs, activateByIndex);
86
-
87
- const { control, getFieldState } = useFormContext();
88
- const formState = useFormState({ control });
89
-
90
- const items = useMemo(
91
- () =>
92
- tabs.map((tab, index) => {
93
- const hasError = sectionHasError(
94
- sectionRefs[index].current,
95
- getFieldState,
96
- formState,
97
- );
98
-
99
- return {
100
- key: tab.key,
101
- label: hasError ? (
102
- <span style={{ color: token.colorError }}>{tab.label}</span>
103
- ) : (
104
- tab.label
105
- ),
106
- disabled: tab.disabled,
107
- children: (
108
- <FormSectionSourcesProvider sourcesRef={sectionRefs[index]}>
109
- {tab.children}
110
- </FormSectionSourcesProvider>
111
- ),
112
- };
113
- }),
114
- [formState, getFieldState, sectionRefs, tabs, token.colorError],
115
- );
116
-
117
- return (
118
- <Tabs
119
- destroyOnHidden={false}
120
- items={items}
121
- activeKey={activeKey}
122
- onChange={setActiveKey}
123
- {...tabsProps}
124
- />
125
- );
126
- }
1
+ import { Tabs, theme, type TabsProps } from "antd";
2
+ import {
3
+ Children,
4
+ isValidElement,
5
+ useCallback,
6
+ useMemo,
7
+ useState,
8
+ type ReactElement,
9
+ type ReactNode,
10
+ } from "react";
11
+ import { useFormContext, useFormState } from "react-hook-form";
12
+ import { FormSectionSourcesProvider } from "./context/FormSectionContext";
13
+ import {
14
+ sectionHasError,
15
+ useActivateFirstErrorSection,
16
+ useSectionSourceRefs,
17
+ } from "./utils/formSectionErrors";
18
+
19
+ export type FormTabProps = {
20
+ label: ReactNode;
21
+ children: ReactNode;
22
+ disabled?: boolean;
23
+ };
24
+
25
+ export function FormTab(_props: FormTabProps) {
26
+ return null;
27
+ }
28
+
29
+ function isFormTab(child: ReactNode): child is ReactElement<FormTabProps> {
30
+ return isValidElement(child) && child.type === FormTab;
31
+ }
32
+
33
+ export type FormTabsProps = Omit<TabsProps, "items"> & {
34
+ children: ReactNode;
35
+ };
36
+
37
+ export function FormTabs({
38
+ children,
39
+ defaultActiveKey,
40
+ activeKey: controlledActiveKey,
41
+ onChange,
42
+ ...tabsProps
43
+ }: FormTabsProps) {
44
+ const { token } = theme.useToken();
45
+ const tabs = useMemo(
46
+ () =>
47
+ Children.toArray(children)
48
+ .filter(isFormTab)
49
+ .map((tab, index) => ({
50
+ key: (tab.key as string | undefined) ?? String(index),
51
+ label: tab.props.label,
52
+ disabled: tab.props.disabled,
53
+ children: tab.props.children,
54
+ })),
55
+ [children],
56
+ );
57
+
58
+ const sectionRefs = useSectionSourceRefs(tabs.length);
59
+ const isControlled = controlledActiveKey !== undefined;
60
+ const [internalActiveKey, setInternalActiveKey] = useState(
61
+ () => (defaultActiveKey as string | undefined) ?? tabs[0]?.key ?? "0",
62
+ );
63
+ const activeKey = isControlled ? controlledActiveKey : internalActiveKey;
64
+
65
+ const setActiveKey = useCallback(
66
+ (key: string) => {
67
+ if (!isControlled) {
68
+ setInternalActiveKey(key);
69
+ }
70
+ onChange?.(key);
71
+ },
72
+ [isControlled, onChange],
73
+ );
74
+
75
+ const activateByIndex = useCallback(
76
+ (index: number) => {
77
+ const key = tabs[index]?.key;
78
+ if (key != null) {
79
+ setActiveKey(key);
80
+ }
81
+ },
82
+ [setActiveKey, tabs],
83
+ );
84
+
85
+ useActivateFirstErrorSection(sectionRefs, activateByIndex);
86
+
87
+ const { control, getFieldState } = useFormContext();
88
+ const formState = useFormState({ control });
89
+
90
+ const items = useMemo(
91
+ () =>
92
+ tabs.map((tab, index) => {
93
+ const hasError = sectionHasError(
94
+ sectionRefs[index].current,
95
+ getFieldState,
96
+ formState,
97
+ );
98
+
99
+ return {
100
+ key: tab.key,
101
+ label: hasError ? (
102
+ <span style={{ color: token.colorError }}>{tab.label}</span>
103
+ ) : (
104
+ tab.label
105
+ ),
106
+ disabled: tab.disabled,
107
+ children: (
108
+ <FormSectionSourcesProvider sourcesRef={sectionRefs[index]}>
109
+ {tab.children}
110
+ </FormSectionSourcesProvider>
111
+ ),
112
+ };
113
+ }),
114
+ [formState, getFieldState, sectionRefs, tabs, token.colorError],
115
+ );
116
+
117
+ return (
118
+ <Tabs
119
+ destroyOnHidden={false}
120
+ items={items}
121
+ activeKey={activeKey}
122
+ onChange={setActiveKey}
123
+ {...tabsProps}
124
+ />
125
+ );
126
+ }
@@ -1,219 +1,162 @@
1
- import { Button, Card, Space, Table, Typography } from "antd";
2
- import { useFieldArray, useFormContext, type FieldValues } from "react-hook-form";
3
- import { useMemo, type ReactNode } from "react";
4
- import type { Identifier } from "../data/dataProviderTypes";
5
- import type { DataProvider } from "../data/dataProviderTypes";
6
- import type {
7
- InlineColumnDef,
8
- InlineFormSetBaseProps,
9
- InlineRowContext,
10
- } from "./types";
11
- import { inlineArrayName as buildInlineArrayName } from "./utils/inlineArrayName";
12
- import { inlineFieldName } from "./utils/inlineFieldName";
13
-
14
- export type InlineFormSetLayout = "tabular" | "stacked";
15
-
16
- export type InlineFormSetProps = InlineFormSetBaseProps & {
17
- columns: InlineColumnDef[];
18
- };
19
-
20
- export type InlineFormSetStackedProps = InlineFormSetBaseProps & {
21
- /** Field sources in each row — used when appending an empty row. */
22
- sources: string[];
23
- renderRow: (ctx: InlineRowContext) => ReactNode;
24
- };
25
-
26
- function emptyRow(sources: string[]) {
27
- const row: Record<string, unknown> = {};
28
- for (const source of sources) row[source] = undefined;
29
- return row;
30
- }
31
-
32
- function useInlineRows(arrayName: string, sources: string[]) {
33
- const { control } = useFormContext<FieldValues>();
34
- const { fields, append, remove } = useFieldArray({
35
- control,
36
- name: arrayName,
37
- });
38
-
39
- const appendEmpty = () => append(emptyRow(sources));
40
-
41
- return { fields, remove, appendEmpty };
42
- }
43
-
44
- /** Tabular inline (Django TabularInline) — table with column headers. */
45
- export function InlineFormSet({
46
- resource,
47
- label,
48
- name,
49
- columns,
50
- }: InlineFormSetProps) {
51
- const arrayName = buildInlineArrayName(resource, name);
52
- const sources = useMemo(
53
- () => columns.map((col) => col.source),
54
- [columns],
55
- );
56
- const { fields, remove, appendEmpty } = useInlineRows(arrayName, sources);
57
-
58
- const tableColumns = useMemo(
59
- () =>
60
- columns.map((col) => ({
61
- title: col.label ?? col.source,
62
- key: col.source,
63
- width: col.width,
64
- onHeaderCell: () =>
65
- col.minWidth != null ? { style: { minWidth: col.minWidth } } : {},
66
- onCell: () =>
67
- col.minWidth != null ? { style: { minWidth: col.minWidth } } : {},
68
- render: (_: unknown, __: unknown, index: number) =>
69
- col.cell({
70
- name: inlineFieldName(arrayName, index, col.source),
71
- index,
72
- arrayName,
73
- }),
74
- })),
75
- [columns, arrayName],
76
- );
77
-
78
- return (
79
- <div style={{ marginTop: 24 }}>
80
- <Typography.Title level={5}>{label ?? "Related items"}</Typography.Title>
81
- <Table
82
- size="small"
83
- pagination={false}
84
- scroll={{ x: "max-content" }}
85
- dataSource={fields.map((f) => ({ ...f, key: f.id }))}
86
- columns={[
87
- ...tableColumns,
88
- {
89
- title: "",
90
- key: "__remove",
91
- width: 80,
92
- render: (_: unknown, __: unknown, index: number) => (
93
- <Button
94
- type="link"
95
- danger
96
- size="small"
97
- onClick={() => remove(index)}
98
- >
99
- Remove
100
- </Button>
101
- ),
102
- },
103
- ]}
104
- />
105
- <Button type="dashed" style={{ marginTop: 8 }} onClick={appendEmpty}>
106
- Add row
107
- </Button>
108
- </div>
109
- );
110
- }
111
-
112
- /** Stacked inline (Django StackedInline) — each row in a card with field labels. */
113
- export function InlineFormSetStacked({
114
- resource,
115
- label,
116
- name,
117
- sources,
118
- renderRow,
119
- }: InlineFormSetStackedProps) {
120
- const arrayName = buildInlineArrayName(resource, name);
121
- const { fields, remove, appendEmpty } = useInlineRows(arrayName, sources);
122
-
123
- return (
124
- <div style={{ marginTop: 24 }}>
125
- <Typography.Title level={5}>{label ?? "Related items"}</Typography.Title>
126
- <Space orientation="vertical" size="middle" style={{ width: "100%" }}>
127
- {fields.map((field, index) => (
128
- <Card
129
- key={field.id}
130
- size="small"
131
- title={`Item ${index + 1}`}
132
- extra={
133
- <Button
134
- type="link"
135
- danger
136
- size="small"
137
- onClick={() => remove(index)}
138
- >
139
- Remove
140
- </Button>
141
- }
142
- >
143
- {renderRow({
144
- arrayName,
145
- index,
146
- name: (source) => inlineFieldName(arrayName, index, source),
147
- })}
148
- </Card>
149
- ))}
150
- </Space>
151
- <Button type="dashed" style={{ marginTop: 8 }} onClick={appendEmpty}>
152
- Add item
153
- </Button>
154
- </div>
155
- );
156
- }
157
-
158
- export type SaveInlineOptions = {
159
- resource: string;
160
- foreignKey: string;
161
- parentId: Identifier;
162
- rows: Record<string, unknown>[];
163
- existingIds?: Identifier[];
164
- /** Return true when the error was handled (field errors applied). Stops saving further rows. */
165
- onRowError?: (error: unknown, index: number) => boolean;
166
- };
167
-
168
- export async function saveInlineRows(
169
- dp: DataProvider,
170
- opts: SaveInlineOptions,
171
- ): Promise<boolean> {
172
- const { resource, foreignKey, parentId, rows, existingIds = [], onRowError } =
173
- opts;
174
- const keptIds: Identifier[] = [];
175
-
176
- for (let index = 0; index < rows.length; index++) {
177
- const row = rows[index];
178
- try {
179
- const { id: _id, ...rest } = row;
180
- const data = { ...rest, [foreignKey]: parentId };
181
- const rowId = row.id as Identifier | undefined;
182
- if (rowId != null && existingIds.some((e) => e === rowId)) {
183
- await dp.update(resource, { id: rowId, data });
184
- keptIds.push(rowId);
185
- } else {
186
- const res = await dp.create(resource, data);
187
- const created = res.data as { id: Identifier };
188
- keptIds.push(created.id);
189
- }
190
- } catch (e) {
191
- if (onRowError?.(e, index)) return false;
192
- throw e;
193
- }
194
- }
195
-
196
- for (const oldId of existingIds) {
197
- if (!keptIds.some((k) => k === oldId)) {
198
- await dp.delete(resource, oldId);
199
- }
200
- }
201
-
202
- return true;
203
- }
204
-
205
- export async function loadInlineRows(
206
- dp: DataProvider,
207
- resource: string,
208
- foreignKey: string,
209
- parentId: Identifier,
210
- ) {
211
- const res = await dp.getList(resource, {
212
- filter: { [foreignKey]: Number(parentId) || parentId },
213
- pagination: { page: 1, perPage: 500 },
214
- });
215
- return {
216
- rows: res.data as Record<string, unknown>[],
217
- ids: (res.data as { id: Identifier }[]).map((r) => r.id),
218
- };
219
- }
1
+ import { Button, Card, Space, Table, Typography } from "antd";
2
+ import { useFieldArray, useFormContext, type FieldValues } from "react-hook-form";
3
+ import { useMemo, type ReactNode } from "react";
4
+ import type {
5
+ InlineColumnDef,
6
+ InlineFormSetBaseProps,
7
+ InlineRowContext,
8
+ } from "./types";
9
+ import { nestedFieldPath } from "./utils/nestedFieldPath";
10
+ import { useRegisterInlineField } from "./context/InlineFieldsRegistry";
11
+ import { useRegisterPayloadField } from "./context/PayloadFieldsContext";
12
+
13
+ export type InlineFormSetLayout = "tabular" | "stacked";
14
+
15
+ export type InlineFormSetProps = InlineFormSetBaseProps & {
16
+ columns: InlineColumnDef[];
17
+ };
18
+
19
+ export type InlineFormSetStackedProps = InlineFormSetBaseProps & {
20
+ /** Field sources in each row — used when appending an empty row. */
21
+ sources: string[];
22
+ renderRow: (ctx: InlineRowContext) => ReactNode;
23
+ };
24
+
25
+ function emptyRow(sources: string[]) {
26
+ const row: Record<string, unknown> = {};
27
+ for (const source of sources) row[source] = undefined;
28
+ return row;
29
+ }
30
+
31
+ function useInlineRows(field: string, sources: string[]) {
32
+ const { control } = useFormContext<FieldValues>();
33
+ const { fields, append, remove } = useFieldArray({
34
+ control,
35
+ name: field,
36
+ keyName: "rowKey",
37
+ });
38
+
39
+ const appendEmpty = () => append(emptyRow(sources));
40
+
41
+ return { fields, remove, appendEmpty };
42
+ }
43
+
44
+ /** Tabular inline (Django TabularInline) — table with column headers. */
45
+ export function InlineFormSet({
46
+ field,
47
+ label,
48
+ payloadKey,
49
+ transformRows,
50
+ columns,
51
+ }: InlineFormSetProps) {
52
+ const sources = useMemo(
53
+ () => columns.map((col) => col.source),
54
+ [columns],
55
+ );
56
+ const { fields, remove, appendEmpty } = useInlineRows(field, sources);
57
+
58
+ useRegisterPayloadField(field);
59
+ useRegisterInlineField(field, sources, payloadKey, transformRows);
60
+
61
+ const tableColumns = useMemo(
62
+ () =>
63
+ columns.map((col) => ({
64
+ title: col.label ?? col.source,
65
+ key: col.source,
66
+ width: col.width,
67
+ onHeaderCell: () =>
68
+ col.minWidth != null ? { style: { minWidth: col.minWidth } } : {},
69
+ onCell: () =>
70
+ col.minWidth != null ? { style: { minWidth: col.minWidth } } : {},
71
+ render: (_: unknown, __: unknown, index: number) =>
72
+ col.cell({
73
+ name: nestedFieldPath(field, index, col.source),
74
+ index,
75
+ field,
76
+ }),
77
+ })),
78
+ [columns, field],
79
+ );
80
+
81
+ return (
82
+ <div style={{ marginTop: 24 }}>
83
+ <Typography.Title level={5}>{label ?? "Related items"}</Typography.Title>
84
+ <Table
85
+ size="small"
86
+ pagination={false}
87
+ scroll={{ x: "max-content" }}
88
+ dataSource={fields.map((f) => ({ ...f, key: f.rowKey }))}
89
+ columns={[
90
+ ...tableColumns,
91
+ {
92
+ title: "",
93
+ key: "__remove",
94
+ width: 80,
95
+ render: (_: unknown, __: unknown, index: number) => (
96
+ <Button
97
+ type="link"
98
+ danger
99
+ size="small"
100
+ onClick={() => remove(index)}
101
+ >
102
+ Remove
103
+ </Button>
104
+ ),
105
+ },
106
+ ]}
107
+ />
108
+ <Button type="dashed" style={{ marginTop: 8 }} onClick={appendEmpty}>
109
+ Add row
110
+ </Button>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ /** Stacked inline (Django StackedInline) — each row in a card with field labels. */
116
+ export function InlineFormSetStacked({
117
+ field,
118
+ label,
119
+ payloadKey,
120
+ transformRows,
121
+ sources,
122
+ renderRow,
123
+ }: InlineFormSetStackedProps) {
124
+ const { fields, remove, appendEmpty } = useInlineRows(field, sources);
125
+
126
+ useRegisterPayloadField(field);
127
+ useRegisterInlineField(field, sources, payloadKey, transformRows);
128
+
129
+ return (
130
+ <div style={{ marginTop: 24 }}>
131
+ <Typography.Title level={5}>{label ?? "Related items"}</Typography.Title>
132
+ <Space orientation="vertical" size="middle" style={{ width: "100%" }}>
133
+ {fields.map((rowField, index) => (
134
+ <Card
135
+ key={rowField.rowKey}
136
+ size="small"
137
+ title={`Item ${index + 1}`}
138
+ extra={
139
+ <Button
140
+ type="link"
141
+ danger
142
+ size="small"
143
+ onClick={() => remove(index)}
144
+ >
145
+ Remove
146
+ </Button>
147
+ }
148
+ >
149
+ {renderRow({
150
+ field,
151
+ index,
152
+ name: (source) => nestedFieldPath(field, index, source),
153
+ })}
154
+ </Card>
155
+ ))}
156
+ </Space>
157
+ <Button type="dashed" style={{ marginTop: 8 }} onClick={appendEmpty}>
158
+ Add item
159
+ </Button>
160
+ </div>
161
+ );
162
+ }