ding-react-admin 1.0.0

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 (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +3067 -0
  5. package/dist/src/app/AdminApp.d.ts +7 -0
  6. package/dist/src/app/AdminApp.d.ts.map +1 -0
  7. package/dist/src/components/AuthAlternateLink.d.ts +7 -0
  8. package/dist/src/components/AuthAlternateLink.d.ts.map +1 -0
  9. package/dist/src/components/DensitySwitch.d.ts +2 -0
  10. package/dist/src/components/DensitySwitch.d.ts.map +1 -0
  11. package/dist/src/components/ThemeSwitch.d.ts +2 -0
  12. package/dist/src/components/ThemeSwitch.d.ts.map +1 -0
  13. package/dist/src/components/ThemeToolbar.d.ts +2 -0
  14. package/dist/src/components/ThemeToolbar.d.ts.map +1 -0
  15. package/dist/src/components/index.d.ts +5 -0
  16. package/dist/src/components/index.d.ts.map +1 -0
  17. package/dist/src/context/AppThemeProvider.d.ts +13 -0
  18. package/dist/src/context/AppThemeProvider.d.ts.map +1 -0
  19. package/dist/src/context/AuthProvider.d.ts +18 -0
  20. package/dist/src/context/AuthProvider.d.ts.map +1 -0
  21. package/dist/src/context/DataProvider.d.ts +10 -0
  22. package/dist/src/context/DataProvider.d.ts.map +1 -0
  23. package/dist/src/context/PermissionsProvider.d.ts +18 -0
  24. package/dist/src/context/PermissionsProvider.d.ts.map +1 -0
  25. package/dist/src/crud/FilterBar.d.ts +6 -0
  26. package/dist/src/crud/FilterBar.d.ts.map +1 -0
  27. package/dist/src/crud/FormSteps.d.ts +20 -0
  28. package/dist/src/crud/FormSteps.d.ts.map +1 -0
  29. package/dist/src/crud/FormTabs.d.ts +13 -0
  30. package/dist/src/crud/FormTabs.d.ts.map +1 -0
  31. package/dist/src/crud/InlineFormSet.d.ts +29 -0
  32. package/dist/src/crud/InlineFormSet.d.ts.map +1 -0
  33. package/dist/src/crud/ListActionsBar.d.ts +15 -0
  34. package/dist/src/crud/ListActionsBar.d.ts.map +1 -0
  35. package/dist/src/crud/ResourceForm.d.ts +21 -0
  36. package/dist/src/crud/ResourceForm.d.ts.map +1 -0
  37. package/dist/src/crud/ResourceFormModal.d.ts +10 -0
  38. package/dist/src/crud/ResourceFormModal.d.ts.map +1 -0
  39. package/dist/src/crud/ResourceList.d.ts +38 -0
  40. package/dist/src/crud/ResourceList.d.ts.map +1 -0
  41. package/dist/src/crud/columns/BooleanColumn.d.ts +6 -0
  42. package/dist/src/crud/columns/BooleanColumn.d.ts.map +1 -0
  43. package/dist/src/crud/columns/CustomColumn.d.ts +9 -0
  44. package/dist/src/crud/columns/CustomColumn.d.ts.map +1 -0
  45. package/dist/src/crud/columns/DateColumn.d.ts +6 -0
  46. package/dist/src/crud/columns/DateColumn.d.ts.map +1 -0
  47. package/dist/src/crud/columns/NumberColumn.d.ts +6 -0
  48. package/dist/src/crud/columns/NumberColumn.d.ts.map +1 -0
  49. package/dist/src/crud/columns/ReferenceColumn.d.ts +6 -0
  50. package/dist/src/crud/columns/ReferenceColumn.d.ts.map +1 -0
  51. package/dist/src/crud/columns/ReferenceManyColumn.d.ts +6 -0
  52. package/dist/src/crud/columns/ReferenceManyColumn.d.ts.map +1 -0
  53. package/dist/src/crud/columns/TextColumn.d.ts +7 -0
  54. package/dist/src/crud/columns/TextColumn.d.ts.map +1 -0
  55. package/dist/src/crud/context/FilterContext.d.ts +17 -0
  56. package/dist/src/crud/context/FilterContext.d.ts.map +1 -0
  57. package/dist/src/crud/context/FormContext.d.ts +16 -0
  58. package/dist/src/crud/context/FormContext.d.ts.map +1 -0
  59. package/dist/src/crud/context/FormFieldsContext.d.ts +14 -0
  60. package/dist/src/crud/context/FormFieldsContext.d.ts.map +1 -0
  61. package/dist/src/crud/context/FormSectionContext.d.ts +7 -0
  62. package/dist/src/crud/context/FormSectionContext.d.ts.map +1 -0
  63. package/dist/src/crud/context/InlineFormContext.d.ts +16 -0
  64. package/dist/src/crud/context/InlineFormContext.d.ts.map +1 -0
  65. package/dist/src/crud/context/ListContext.d.ts +22 -0
  66. package/dist/src/crud/context/ListContext.d.ts.map +1 -0
  67. package/dist/src/crud/fields/BooleanField.d.ts +7 -0
  68. package/dist/src/crud/fields/BooleanField.d.ts.map +1 -0
  69. package/dist/src/crud/fields/DateField.d.ts +8 -0
  70. package/dist/src/crud/fields/DateField.d.ts.map +1 -0
  71. package/dist/src/crud/fields/FieldWrapper.d.ts +29 -0
  72. package/dist/src/crud/fields/FieldWrapper.d.ts.map +1 -0
  73. package/dist/src/crud/fields/NumberField.d.ts +12 -0
  74. package/dist/src/crud/fields/NumberField.d.ts.map +1 -0
  75. package/dist/src/crud/fields/PasswordField.d.ts +11 -0
  76. package/dist/src/crud/fields/PasswordField.d.ts.map +1 -0
  77. package/dist/src/crud/fields/ReferenceField.d.ts +18 -0
  78. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -0
  79. package/dist/src/crud/fields/ReferenceManyField.d.ts +9 -0
  80. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -0
  81. package/dist/src/crud/fields/SelectField.d.ts +10 -0
  82. package/dist/src/crud/fields/SelectField.d.ts.map +1 -0
  83. package/dist/src/crud/fields/TextField.d.ts +10 -0
  84. package/dist/src/crud/fields/TextField.d.ts.map +1 -0
  85. package/dist/src/crud/fields/useInlineOrFormField.d.ts +10 -0
  86. package/dist/src/crud/fields/useInlineOrFormField.d.ts.map +1 -0
  87. package/dist/src/crud/filters/BooleanFilter.d.ts +4 -0
  88. package/dist/src/crud/filters/BooleanFilter.d.ts.map +1 -0
  89. package/dist/src/crud/filters/DateFilter.d.ts +4 -0
  90. package/dist/src/crud/filters/DateFilter.d.ts.map +1 -0
  91. package/dist/src/crud/filters/NumberFilter.d.ts +4 -0
  92. package/dist/src/crud/filters/NumberFilter.d.ts.map +1 -0
  93. package/dist/src/crud/filters/ReferenceFilter.d.ts +13 -0
  94. package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -0
  95. package/dist/src/crud/filters/SelectFilter.d.ts +7 -0
  96. package/dist/src/crud/filters/SelectFilter.d.ts.map +1 -0
  97. package/dist/src/crud/filters/TextFilter.d.ts +6 -0
  98. package/dist/src/crud/filters/TextFilter.d.ts.map +1 -0
  99. package/dist/src/crud/index.d.ts +44 -0
  100. package/dist/src/crud/index.d.ts.map +1 -0
  101. package/dist/src/crud/types.d.ts +99 -0
  102. package/dist/src/crud/types.d.ts.map +1 -0
  103. package/dist/src/crud/utils/formErrors.d.ts +9 -0
  104. package/dist/src/crud/utils/formErrors.d.ts.map +1 -0
  105. package/dist/src/crud/utils/formSectionErrors.d.ts +6 -0
  106. package/dist/src/crud/utils/formSectionErrors.d.ts.map +1 -0
  107. package/dist/src/crud/utils/getByPath.d.ts +3 -0
  108. package/dist/src/crud/utils/getByPath.d.ts.map +1 -0
  109. package/dist/src/crud/utils/inlineArrayName.d.ts +3 -0
  110. package/dist/src/crud/utils/inlineArrayName.d.ts.map +1 -0
  111. package/dist/src/crud/utils/pickBySources.d.ts +6 -0
  112. package/dist/src/crud/utils/pickBySources.d.ts.map +1 -0
  113. package/dist/src/crud/utils/setByPath.d.ts +3 -0
  114. package/dist/src/crud/utils/setByPath.d.ts.map +1 -0
  115. package/dist/src/crud/utils/sortQueryParam.d.ts +7 -0
  116. package/dist/src/crud/utils/sortQueryParam.d.ts.map +1 -0
  117. package/dist/src/crud/utils/useChoices.d.ts +10 -0
  118. package/dist/src/crud/utils/useChoices.d.ts.map +1 -0
  119. package/dist/src/crud/utils/useListQueryState.d.ts +22 -0
  120. package/dist/src/crud/utils/useListQueryState.d.ts.map +1 -0
  121. package/dist/src/data/createMemoryResourceHandlers.d.ts +23 -0
  122. package/dist/src/data/createMemoryResourceHandlers.d.ts.map +1 -0
  123. package/dist/src/data/createRestResourceHandlers.d.ts +22 -0
  124. package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -0
  125. package/dist/src/data/dataProviderTypes.d.ts +67 -0
  126. package/dist/src/data/dataProviderTypes.d.ts.map +1 -0
  127. package/dist/src/data/dataUtils.test.d.ts +2 -0
  128. package/dist/src/data/dataUtils.test.d.ts.map +1 -0
  129. package/dist/src/data/inMemoryList.d.ts +12 -0
  130. package/dist/src/data/inMemoryList.d.ts.map +1 -0
  131. package/dist/src/data/parseFormErrorHelpers.d.ts +35 -0
  132. package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -0
  133. package/dist/src/data/parseFormErrorHelpers.test.d.ts +2 -0
  134. package/dist/src/data/parseFormErrorHelpers.test.d.ts.map +1 -0
  135. package/dist/src/data/resourceHandlers.d.ts +33 -0
  136. package/dist/src/data/resourceHandlers.d.ts.map +1 -0
  137. package/dist/src/data/sortHelpers.d.ts +17 -0
  138. package/dist/src/data/sortHelpers.d.ts.map +1 -0
  139. package/dist/src/index.d.ts +31 -0
  140. package/dist/src/index.d.ts.map +1 -0
  141. package/dist/src/layouts/AdminLayout.d.ts +3 -0
  142. package/dist/src/layouts/AdminLayout.d.ts.map +1 -0
  143. package/dist/src/layouts/AuthPageLayout.d.ts +11 -0
  144. package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -0
  145. package/dist/src/pages/LoginPage.d.ts +3 -0
  146. package/dist/src/pages/LoginPage.d.ts.map +1 -0
  147. package/dist/src/pages/PlaceholderPage.d.ts +4 -0
  148. package/dist/src/pages/PlaceholderPage.d.ts.map +1 -0
  149. package/dist/src/permissions/resourcePermissions.d.ts +22 -0
  150. package/dist/src/permissions/resourcePermissions.d.ts.map +1 -0
  151. package/dist/src/router/createAdminRouter.d.ts +3 -0
  152. package/dist/src/router/createAdminRouter.d.ts.map +1 -0
  153. package/dist/src/router/guards.d.ts +20 -0
  154. package/dist/src/router/guards.d.ts.map +1 -0
  155. package/dist/src/router/routeAccess.d.ts +21 -0
  156. package/dist/src/router/routeAccess.d.ts.map +1 -0
  157. package/dist/src/types.d.ts +123 -0
  158. package/dist/src/types.d.ts.map +1 -0
  159. package/package.json +58 -0
  160. package/src/app/AdminApp.tsx +34 -0
  161. package/src/components/AuthAlternateLink.tsx +23 -0
  162. package/src/components/DensitySwitch.tsx +31 -0
  163. package/src/components/ThemeSwitch.tsx +28 -0
  164. package/src/components/ThemeToolbar.tsx +34 -0
  165. package/src/components/index.ts +4 -0
  166. package/src/context/AppThemeProvider.tsx +128 -0
  167. package/src/context/AuthProvider.tsx +77 -0
  168. package/src/context/DataProvider.tsx +30 -0
  169. package/src/context/PermissionsProvider.tsx +57 -0
  170. package/src/crud/FilterBar.tsx +36 -0
  171. package/src/crud/FormSteps.tsx +135 -0
  172. package/src/crud/FormTabs.tsx +126 -0
  173. package/src/crud/InlineFormSet.tsx +231 -0
  174. package/src/crud/ListActionsBar.tsx +106 -0
  175. package/src/crud/ResourceForm.tsx +281 -0
  176. package/src/crud/ResourceFormModal.tsx +121 -0
  177. package/src/crud/ResourceList.tsx +633 -0
  178. package/src/crud/columns/BooleanColumn.tsx +32 -0
  179. package/src/crud/columns/CustomColumn.tsx +33 -0
  180. package/src/crud/columns/DateColumn.tsx +28 -0
  181. package/src/crud/columns/NumberColumn.tsx +31 -0
  182. package/src/crud/columns/ReferenceColumn.tsx +85 -0
  183. package/src/crud/columns/ReferenceManyColumn.tsx +67 -0
  184. package/src/crud/columns/TextColumn.tsx +38 -0
  185. package/src/crud/context/FilterContext.tsx +71 -0
  186. package/src/crud/context/FormContext.tsx +39 -0
  187. package/src/crud/context/FormFieldsContext.tsx +76 -0
  188. package/src/crud/context/FormSectionContext.tsx +22 -0
  189. package/src/crud/context/InlineFormContext.tsx +71 -0
  190. package/src/crud/context/ListContext.tsx +93 -0
  191. package/src/crud/fields/BooleanField.tsx +32 -0
  192. package/src/crud/fields/DateField.tsx +47 -0
  193. package/src/crud/fields/FieldWrapper.tsx +109 -0
  194. package/src/crud/fields/NumberField.tsx +50 -0
  195. package/src/crud/fields/PasswordField.tsx +98 -0
  196. package/src/crud/fields/ReferenceField.tsx +191 -0
  197. package/src/crud/fields/ReferenceManyField.tsx +70 -0
  198. package/src/crud/fields/SelectField.tsx +42 -0
  199. package/src/crud/fields/TextField.tsx +44 -0
  200. package/src/crud/fields/useInlineOrFormField.tsx +91 -0
  201. package/src/crud/filters/BooleanFilter.tsx +38 -0
  202. package/src/crud/filters/DateFilter.tsx +35 -0
  203. package/src/crud/filters/NumberFilter.tsx +33 -0
  204. package/src/crud/filters/ReferenceFilter.tsx +173 -0
  205. package/src/crud/filters/SelectFilter.tsx +44 -0
  206. package/src/crud/filters/TextFilter.tsx +36 -0
  207. package/src/crud/index.ts +73 -0
  208. package/src/crud/types.ts +130 -0
  209. package/src/crud/utils/formErrors.ts +38 -0
  210. package/src/crud/utils/formSectionErrors.ts +85 -0
  211. package/src/crud/utils/getByPath.ts +13 -0
  212. package/src/crud/utils/inlineArrayName.ts +4 -0
  213. package/src/crud/utils/pickBySources.ts +24 -0
  214. package/src/crud/utils/setByPath.ts +18 -0
  215. package/src/crud/utils/sortQueryParam.ts +37 -0
  216. package/src/crud/utils/useChoices.ts +182 -0
  217. package/src/crud/utils/useListQueryState.ts +191 -0
  218. package/src/data/createMemoryResourceHandlers.ts +78 -0
  219. package/src/data/createRestResourceHandlers.ts +57 -0
  220. package/src/data/dataProviderTypes.ts +101 -0
  221. package/src/data/dataUtils.test.ts +108 -0
  222. package/src/data/inMemoryList.ts +97 -0
  223. package/src/data/parseFormErrorHelpers.test.ts +84 -0
  224. package/src/data/parseFormErrorHelpers.ts +224 -0
  225. package/src/data/resourceHandlers.ts +124 -0
  226. package/src/data/sortHelpers.ts +48 -0
  227. package/src/index.ts +182 -0
  228. package/src/layouts/AdminLayout.tsx +381 -0
  229. package/src/layouts/AuthPageLayout.tsx +81 -0
  230. package/src/pages/LoginPage.tsx +80 -0
  231. package/src/pages/PlaceholderPage.tsx +9 -0
  232. package/src/permissions/resourcePermissions.ts +47 -0
  233. package/src/router/createAdminRouter.tsx +64 -0
  234. package/src/router/guards.tsx +63 -0
  235. package/src/router/routeAccess.ts +89 -0
  236. package/src/types.ts +135 -0
  237. package/src/vite-env.d.ts +1 -0
@@ -0,0 +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
+ }
@@ -0,0 +1,231 @@
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 {
5
+ InlineFormSetProvider,
6
+ useInlineFormSetContext,
7
+ type InlineFormSetLayout,
8
+ } from "./context/InlineFormContext";
9
+ import type { Identifier } from "../data/dataProviderTypes";
10
+ import type { DataProvider } from "../data/dataProviderTypes";
11
+ import { inlineArrayName as buildInlineArrayName } from "./utils/inlineArrayName";
12
+
13
+ export type { InlineFormSetLayout } from "./context/InlineFormContext";
14
+
15
+ export type InlineFormSetProps = {
16
+ resource: string;
17
+ foreignKey: string;
18
+ label?: string;
19
+ children: ReactNode;
20
+ name?: string;
21
+ /** `tabular` (default) — Django TabularInline; `stacked` — Django StackedInline. */
22
+ layout?: InlineFormSetLayout;
23
+ };
24
+
25
+ function useInlineRows(arrayName: string) {
26
+ const ctx = useInlineFormSetContext();
27
+ const { control } = useFormContext<FieldValues>();
28
+ const { fields, append, remove } = useFieldArray({
29
+ control,
30
+ name: arrayName,
31
+ });
32
+
33
+ const appendEmpty = () => {
34
+ if (!ctx) return;
35
+ const empty: Record<string, unknown> = {};
36
+ for (const f of ctx.fields) empty[f.source] = undefined;
37
+ append(empty);
38
+ };
39
+
40
+ return { ctx, fields, remove, appendEmpty };
41
+ }
42
+
43
+ function InlineFormSetTable({
44
+ arrayName,
45
+ label,
46
+ }: {
47
+ arrayName: string;
48
+ label?: string;
49
+ }) {
50
+ const { ctx, fields, remove, appendEmpty } = useInlineRows(arrayName);
51
+
52
+ const columns = useMemo(() => {
53
+ if (!ctx) return [];
54
+ return ctx.fields.map((f) => ({
55
+ title: f.label ?? f.source,
56
+ key: f.source,
57
+ width: f.width,
58
+ onHeaderCell: () =>
59
+ f.minWidth != null
60
+ ? { style: { minWidth: f.minWidth } }
61
+ : {},
62
+ onCell: () =>
63
+ f.minWidth != null ? { style: { minWidth: f.minWidth } } : {},
64
+ render: (_: unknown, __: unknown, index: number) =>
65
+ f.render({ name: `${arrayName}.${index}.${f.source}`, index }),
66
+ }));
67
+ }, [ctx, arrayName]);
68
+
69
+ if (!ctx) return null;
70
+
71
+ return (
72
+ <div style={{ marginTop: 24 }}>
73
+ <Typography.Title level={5}>{label ?? "Related items"}</Typography.Title>
74
+ <Table
75
+ size="small"
76
+ pagination={false}
77
+ scroll={{ x: "max-content" }}
78
+ dataSource={fields.map((f) => ({ ...f, key: f.id }))}
79
+ columns={[
80
+ ...columns,
81
+ {
82
+ title: "",
83
+ key: "__remove",
84
+ width: 80,
85
+ render: (_: unknown, __: unknown, index: number) => (
86
+ <Button
87
+ type="link"
88
+ danger
89
+ size="small"
90
+ onClick={() => remove(index)}
91
+ >
92
+ Remove
93
+ </Button>
94
+ ),
95
+ },
96
+ ]}
97
+ />
98
+ <Button type="dashed" style={{ marginTop: 8 }} onClick={appendEmpty}>
99
+ Add row
100
+ </Button>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ function InlineFormSetStacked({
106
+ arrayName,
107
+ label,
108
+ }: {
109
+ arrayName: string;
110
+ label?: string;
111
+ }) {
112
+ const { ctx, fields, remove, appendEmpty } = useInlineRows(arrayName);
113
+
114
+ if (!ctx) return null;
115
+
116
+ return (
117
+ <div style={{ marginTop: 24 }}>
118
+ <Typography.Title level={5}>{label ?? "Related items"}</Typography.Title>
119
+ <Space orientation="vertical" size="middle" style={{ width: "100%" }}>
120
+ {fields.map((field, index) => (
121
+ <Card
122
+ key={field.id}
123
+ size="small"
124
+ title={`Item ${index + 1}`}
125
+ extra={
126
+ <Button type="link" danger size="small" onClick={() => remove(index)}>
127
+ Remove
128
+ </Button>
129
+ }
130
+ >
131
+ {ctx.fields.map((f) => (
132
+ <div key={f.source}>
133
+ {f.render({
134
+ name: `${arrayName}.${index}.${f.source}`,
135
+ index,
136
+ })}
137
+ </div>
138
+ ))}
139
+ </Card>
140
+ ))}
141
+ </Space>
142
+ <Button type="dashed" style={{ marginTop: 8 }} onClick={appendEmpty}>
143
+ Add item
144
+ </Button>
145
+ </div>
146
+ );
147
+ }
148
+
149
+ export function InlineFormSet({
150
+ resource,
151
+ label,
152
+ children,
153
+ name,
154
+ layout = "tabular",
155
+ }: InlineFormSetProps) {
156
+ const arrayName = buildInlineArrayName(resource, name);
157
+
158
+ return (
159
+ <InlineFormSetProvider arrayName={arrayName} layout={layout}>
160
+ {children}
161
+ {layout === "stacked" ? (
162
+ <InlineFormSetStacked arrayName={arrayName} label={label} />
163
+ ) : (
164
+ <InlineFormSetTable arrayName={arrayName} label={label} />
165
+ )}
166
+ </InlineFormSetProvider>
167
+ );
168
+ }
169
+
170
+ export type SaveInlineOptions = {
171
+ resource: string;
172
+ foreignKey: string;
173
+ parentId: Identifier;
174
+ rows: Record<string, unknown>[];
175
+ existingIds?: Identifier[];
176
+ /** Return true when the error was handled (field errors applied). Stops saving further rows. */
177
+ onRowError?: (error: unknown, index: number) => boolean;
178
+ };
179
+
180
+ export async function saveInlineRows(
181
+ dp: DataProvider,
182
+ opts: SaveInlineOptions,
183
+ ): Promise<boolean> {
184
+ const { resource, foreignKey, parentId, rows, existingIds = [], onRowError } =
185
+ opts;
186
+ const keptIds: Identifier[] = [];
187
+
188
+ for (let index = 0; index < rows.length; index++) {
189
+ const row = rows[index];
190
+ try {
191
+ const { id: _id, ...rest } = row;
192
+ const data = { ...rest, [foreignKey]: parentId };
193
+ const rowId = row.id as Identifier | undefined;
194
+ if (rowId != null && existingIds.some((e) => e === rowId)) {
195
+ await dp.update(resource, { id: rowId, data });
196
+ keptIds.push(rowId);
197
+ } else {
198
+ const res = await dp.create(resource, data);
199
+ const created = res.data as { id: Identifier };
200
+ keptIds.push(created.id);
201
+ }
202
+ } catch (e) {
203
+ if (onRowError?.(e, index)) return false;
204
+ throw e;
205
+ }
206
+ }
207
+
208
+ for (const oldId of existingIds) {
209
+ if (!keptIds.some((k) => k === oldId)) {
210
+ await dp.delete(resource, oldId);
211
+ }
212
+ }
213
+
214
+ return true;
215
+ }
216
+
217
+ export async function loadInlineRows(
218
+ dp: DataProvider,
219
+ resource: string,
220
+ foreignKey: string,
221
+ parentId: Identifier,
222
+ ) {
223
+ const res = await dp.getList(resource, {
224
+ filter: { [foreignKey]: Number(parentId) || parentId },
225
+ pagination: { page: 1, perPage: 500 },
226
+ });
227
+ return {
228
+ rows: res.data as Record<string, unknown>[],
229
+ ids: (res.data as { id: Identifier }[]).map((r) => r.id),
230
+ };
231
+ }
@@ -0,0 +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
+ }