ding-react-admin 2.0.0 → 2.0.5

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 (169) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +254 -195
  3. package/dist/index.css +1 -0
  4. package/dist/index.js +1773 -1170
  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/ResourceForm.d.ts +1 -4
  18. package/dist/src/crud/ResourceForm.d.ts.map +1 -1
  19. package/dist/src/crud/ResourceFormModal.d.ts +7 -1
  20. package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
  21. package/dist/src/crud/ResourceRecordForm.d.ts +27 -0
  22. package/dist/src/crud/ResourceRecordForm.d.ts.map +1 -0
  23. package/dist/src/crud/fields/ReferenceField.d.ts +12 -2
  24. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
  25. package/dist/src/crud/fields/ReferenceInputActions.d.ts +16 -0
  26. package/dist/src/crud/fields/ReferenceInputActions.d.ts.map +1 -0
  27. package/dist/src/crud/fields/ReferenceManyField.d.ts +10 -1
  28. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
  29. package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
  30. package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
  31. package/dist/src/crud/index.d.ts +5 -0
  32. package/dist/src/crud/index.d.ts.map +1 -1
  33. package/dist/src/crud/types.d.ts +12 -0
  34. package/dist/src/crud/types.d.ts.map +1 -1
  35. package/dist/src/crud/utils/choiceSelectionUtils.d.ts +10 -0
  36. package/dist/src/crud/utils/choiceSelectionUtils.d.ts.map +1 -0
  37. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts +2 -0
  38. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts.map +1 -0
  39. package/dist/src/crud/utils/formErrors.d.ts +18 -7
  40. package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
  41. package/dist/src/crud/utils/formErrors.test.d.ts +2 -0
  42. package/dist/src/crud/utils/formErrors.test.d.ts.map +1 -0
  43. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +1 -0
  44. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -1
  45. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts +4 -0
  46. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts.map +1 -0
  47. package/dist/src/crud/utils/toFormData.d.ts +1 -1
  48. package/dist/src/crud/utils/toFormData.d.ts.map +1 -1
  49. package/dist/src/crud/utils/uploadReferenceUtils.d.ts +5 -0
  50. package/dist/src/crud/utils/uploadReferenceUtils.d.ts.map +1 -0
  51. package/dist/src/crud/utils/useChoices.d.ts +27 -1
  52. package/dist/src/crud/utils/useChoices.d.ts.map +1 -1
  53. package/dist/src/crud/utils/useFormRecord.d.ts +9 -7
  54. package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -1
  55. package/dist/src/data/parseFormErrorHelpers.d.ts +6 -27
  56. package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
  57. package/dist/src/index.d.ts +2 -2
  58. package/dist/src/index.d.ts.map +1 -1
  59. package/dist/src/layouts/AdminLayout.d.ts +1 -1
  60. package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
  61. package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -1
  62. package/dist/src/layouts/navFilter.d.ts +5 -0
  63. package/dist/src/layouts/navFilter.d.ts.map +1 -0
  64. package/dist/src/layouts/navMenuItems.d.ts +6 -0
  65. package/dist/src/layouts/navMenuItems.d.ts.map +1 -0
  66. package/dist/src/types.d.ts +12 -0
  67. package/dist/src/types.d.ts.map +1 -1
  68. package/package.json +85 -59
  69. package/src/app/AdminApp.tsx +34 -34
  70. package/src/components/AuthAlternateLink.tsx +23 -23
  71. package/src/components/DensitySwitch.tsx +31 -31
  72. package/src/components/NavMenuLabel.tsx +26 -0
  73. package/src/components/NavMenuSearch.tsx +76 -0
  74. package/src/components/ScrollableArea.tsx +46 -0
  75. package/src/components/ThemeSwitch.tsx +28 -28
  76. package/src/components/ThemeToolbar.tsx +34 -34
  77. package/src/components/index.ts +7 -4
  78. package/src/components/navMenu.css +21 -0
  79. package/src/components/scrollable.css +28 -0
  80. package/src/context/AppThemeProvider.tsx +128 -128
  81. package/src/context/AuthProvider.tsx +89 -77
  82. package/src/context/DataProvider.tsx +30 -30
  83. package/src/context/PermissionsProvider.tsx +57 -57
  84. package/src/crud/FilterBar.tsx +36 -36
  85. package/src/crud/FormGlobalErrorsAlert.tsx +36 -0
  86. package/src/crud/FormSteps.tsx +135 -135
  87. package/src/crud/FormTabs.tsx +126 -126
  88. package/src/crud/InlineFormSet.tsx +162 -162
  89. package/src/crud/ListActionsBar.tsx +106 -106
  90. package/src/crud/ResourceForm.tsx +83 -160
  91. package/src/crud/ResourceFormModal.tsx +75 -133
  92. package/src/crud/ResourceRecordForm.tsx +166 -0
  93. package/src/crud/columns/BooleanColumn.tsx +32 -32
  94. package/src/crud/columns/CustomColumn.tsx +33 -33
  95. package/src/crud/columns/DateColumn.tsx +28 -28
  96. package/src/crud/columns/ImageColumn.tsx +58 -58
  97. package/src/crud/columns/NumberColumn.tsx +31 -31
  98. package/src/crud/columns/ReferenceColumn.tsx +85 -85
  99. package/src/crud/columns/ReferenceManyColumn.tsx +67 -67
  100. package/src/crud/columns/TextColumn.tsx +38 -38
  101. package/src/crud/context/FilterContext.tsx +71 -71
  102. package/src/crud/context/FormContext.tsx +39 -39
  103. package/src/crud/context/FormSectionContext.tsx +22 -22
  104. package/src/crud/context/InlineFieldsRegistry.tsx +61 -61
  105. package/src/crud/context/ListContext.tsx +93 -93
  106. package/src/crud/fields/BooleanField.tsx +38 -38
  107. package/src/crud/fields/DateField.tsx +53 -53
  108. package/src/crud/fields/FileField.tsx +123 -123
  109. package/src/crud/fields/ImageField.tsx +120 -120
  110. package/src/crud/fields/NumberField.tsx +52 -52
  111. package/src/crud/fields/PasswordField.tsx +99 -99
  112. package/src/crud/fields/ReferenceField.tsx +237 -96
  113. package/src/crud/fields/ReferenceInputActions.tsx +106 -0
  114. package/src/crud/fields/ReferenceManyField.tsx +200 -76
  115. package/src/crud/fields/SelectField.tsx +48 -48
  116. package/src/crud/fields/TextField.tsx +46 -46
  117. package/src/crud/fields/uploadFieldUtils.ts +21 -21
  118. package/src/crud/fields/useUploadPreviewUrl.ts +20 -20
  119. package/src/crud/filters/BooleanFilter.tsx +38 -38
  120. package/src/crud/filters/DateFilter.tsx +35 -35
  121. package/src/crud/filters/NumberFilter.tsx +33 -33
  122. package/src/crud/filters/ReferenceFilter.tsx +192 -173
  123. package/src/crud/filters/SelectFilter.tsx +44 -44
  124. package/src/crud/filters/TextFilter.tsx +36 -36
  125. package/src/crud/index.ts +99 -94
  126. package/src/crud/types.ts +146 -134
  127. package/src/crud/utils/buildFormPayload.ts +24 -24
  128. package/src/crud/utils/buildInlineRowsPayload.ts +37 -37
  129. package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -29
  130. package/src/crud/utils/choiceSelectionUtils.test.ts +45 -0
  131. package/src/crud/utils/choiceSelectionUtils.ts +116 -0
  132. package/src/crud/utils/formErrors.test.ts +168 -0
  133. package/src/crud/utils/formErrors.ts +138 -42
  134. package/src/crud/utils/formSectionErrors.ts +85 -85
  135. package/src/crud/utils/getFormValue.ts +13 -13
  136. package/src/crud/utils/hasUploadValues.ts +9 -9
  137. package/src/crud/utils/nestedFieldPath.ts +8 -8
  138. package/src/crud/utils/prepareFormSubmitBody.ts +22 -17
  139. package/src/crud/utils/referenceSelectNotFoundContent.tsx +7 -0
  140. package/src/crud/utils/setFormValue.ts +18 -18
  141. package/src/crud/utils/sortQueryParam.ts +37 -37
  142. package/src/crud/utils/toFormData.ts +79 -81
  143. package/src/crud/utils/uploadReferenceUtils.ts +44 -0
  144. package/src/crud/utils/useAbortableEffect.ts +17 -17
  145. package/src/crud/utils/useChoices.ts +330 -182
  146. package/src/crud/utils/useFormRecord.ts +179 -156
  147. package/src/crud/utils/useListQueryState.ts +191 -191
  148. package/src/data/abortError.test.ts +19 -19
  149. package/src/data/abortError.ts +10 -10
  150. package/src/data/createMemoryResourceHandlers.ts +78 -78
  151. package/src/data/createRestResourceHandlers.ts +63 -63
  152. package/src/data/dataUtils.test.ts +228 -220
  153. package/src/data/inMemoryList.ts +97 -97
  154. package/src/data/parseFormErrorHelpers.test.ts +201 -138
  155. package/src/data/parseFormErrorHelpers.ts +376 -311
  156. package/src/data/sortHelpers.ts +48 -48
  157. package/src/index.ts +211 -207
  158. package/src/layouts/AdminLayout.tsx +530 -382
  159. package/src/layouts/AuthPageLayout.tsx +87 -81
  160. package/src/layouts/navFilter.ts +49 -0
  161. package/src/layouts/navMenuItems.tsx +44 -0
  162. package/src/pages/LoginPage.tsx +80 -80
  163. package/src/pages/PlaceholderPage.tsx +9 -9
  164. package/src/permissions/resourcePermissions.ts +47 -47
  165. package/src/router/createAdminRouter.tsx +64 -64
  166. package/src/router/guards.tsx +63 -63
  167. package/src/router/routeAccess.ts +89 -89
  168. package/src/types.ts +10 -0
  169. package/src/vite-env.d.ts +1 -1
@@ -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,162 +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 {
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
- }
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
+ }