ding-react-admin 1.0.2 → 2.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.
- package/README.md +38 -13
- package/dist/index.js +1317 -1060
- package/dist/src/crud/InlineFormSet.d.ts +12 -25
- package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
- package/dist/src/crud/ResourceForm.d.ts +7 -4
- package/dist/src/crud/ResourceForm.d.ts.map +1 -1
- package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
- package/dist/src/crud/ResourceList.d.ts.map +1 -1
- package/dist/src/crud/columns/ImageColumn.d.ts +14 -0
- package/dist/src/crud/columns/ImageColumn.d.ts.map +1 -0
- package/dist/src/crud/context/InlineFieldsRegistry.d.ts +15 -0
- package/dist/src/crud/context/InlineFieldsRegistry.d.ts.map +1 -0
- package/dist/src/crud/context/PayloadFieldsContext.d.ts +18 -0
- package/dist/src/crud/context/PayloadFieldsContext.d.ts.map +1 -0
- package/dist/src/crud/fields/BooleanField.d.ts +3 -1
- package/dist/src/crud/fields/BooleanField.d.ts.map +1 -1
- package/dist/src/crud/fields/DateField.d.ts +3 -1
- package/dist/src/crud/fields/DateField.d.ts.map +1 -1
- package/dist/src/crud/fields/FieldWrapper.d.ts +7 -14
- package/dist/src/crud/fields/FieldWrapper.d.ts.map +1 -1
- package/dist/src/crud/fields/FileField.d.ts +12 -0
- package/dist/src/crud/fields/FileField.d.ts.map +1 -0
- package/dist/src/crud/fields/ImageField.d.ts +14 -0
- package/dist/src/crud/fields/ImageField.d.ts.map +1 -0
- package/dist/src/crud/fields/NumberField.d.ts +5 -3
- package/dist/src/crud/fields/NumberField.d.ts.map +1 -1
- package/dist/src/crud/fields/PasswordField.d.ts +5 -3
- package/dist/src/crud/fields/PasswordField.d.ts.map +1 -1
- package/dist/src/crud/fields/ReferenceField.d.ts +6 -5
- package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
- package/dist/src/crud/fields/ReferenceManyField.d.ts +3 -1
- package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
- package/dist/src/crud/fields/SelectField.d.ts +3 -1
- package/dist/src/crud/fields/SelectField.d.ts.map +1 -1
- package/dist/src/crud/fields/TextField.d.ts +5 -3
- package/dist/src/crud/fields/TextField.d.ts.map +1 -1
- package/dist/src/crud/fields/uploadFieldUtils.d.ts +5 -0
- package/dist/src/crud/fields/uploadFieldUtils.d.ts.map +1 -0
- package/dist/src/crud/fields/useUploadPreviewUrl.d.ts +4 -0
- package/dist/src/crud/fields/useUploadPreviewUrl.d.ts.map +1 -0
- package/dist/src/crud/index.d.ts +26 -9
- package/dist/src/crud/index.d.ts.map +1 -1
- package/dist/src/crud/types.d.ts +23 -20
- package/dist/src/crud/types.d.ts.map +1 -1
- package/dist/src/crud/utils/buildFormPayload.d.ts +6 -0
- package/dist/src/crud/utils/buildFormPayload.d.ts.map +1 -0
- package/dist/src/crud/utils/buildInlineRowsPayload.d.ts +6 -0
- package/dist/src/crud/utils/buildInlineRowsPayload.d.ts.map +1 -0
- package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts +5 -0
- package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts.map +1 -0
- package/dist/src/crud/utils/formErrors.d.ts +1 -1
- package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
- package/dist/src/crud/utils/getFormValue.d.ts +3 -0
- package/dist/src/crud/utils/getFormValue.d.ts.map +1 -0
- package/dist/src/crud/utils/hasUploadValues.d.ts +3 -0
- package/dist/src/crud/utils/hasUploadValues.d.ts.map +1 -0
- package/dist/src/crud/utils/nestedFieldPath.d.ts +3 -0
- package/dist/src/crud/utils/nestedFieldPath.d.ts.map +1 -0
- package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +7 -0
- package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -0
- package/dist/src/crud/utils/setFormValue.d.ts +3 -0
- package/dist/src/crud/utils/setFormValue.d.ts.map +1 -0
- package/dist/src/crud/utils/toFormData.d.ts +13 -0
- package/dist/src/crud/utils/toFormData.d.ts.map +1 -0
- package/dist/src/crud/utils/useAbortableEffect.d.ts +7 -0
- package/dist/src/crud/utils/useAbortableEffect.d.ts.map +1 -0
- package/dist/src/crud/utils/useFormRecord.d.ts +42 -0
- package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -0
- package/dist/src/data/abortError.d.ts +3 -0
- package/dist/src/data/abortError.d.ts.map +1 -0
- package/dist/src/data/abortError.test.d.ts +2 -0
- package/dist/src/data/abortError.test.d.ts.map +1 -0
- package/dist/src/data/createRestResourceHandlers.d.ts +6 -6
- package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -1
- package/dist/src/data/dataProviderTypes.d.ts +13 -6
- package/dist/src/data/dataProviderTypes.d.ts.map +1 -1
- package/dist/src/data/parseFormErrorHelpers.d.ts +44 -6
- package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
- package/dist/src/data/resourceHandlers.d.ts +3 -3
- package/dist/src/data/resourceHandlers.d.ts.map +1 -1
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
- package/package.json +59 -59
- package/src/crud/InlineFormSet.tsx +90 -159
- package/src/crud/ResourceForm.tsx +83 -204
- package/src/crud/ResourceFormModal.tsx +57 -45
- package/src/crud/ResourceList.tsx +36 -26
- package/src/crud/columns/ImageColumn.tsx +58 -0
- package/src/crud/columns/TextColumn.tsx +2 -2
- package/src/crud/context/InlineFieldsRegistry.tsx +61 -0
- package/src/crud/context/PayloadFieldsContext.tsx +62 -0
- package/src/crud/fields/BooleanField.tsx +22 -16
- package/src/crud/fields/DateField.tsx +32 -26
- package/src/crud/fields/FieldWrapper.tsx +87 -109
- package/src/crud/fields/FileField.tsx +123 -0
- package/src/crud/fields/ImageField.tsx +120 -0
- package/src/crud/fields/NumberField.tsx +36 -34
- package/src/crud/fields/PasswordField.tsx +41 -40
- package/src/crud/fields/ReferenceField.tsx +35 -130
- package/src/crud/fields/ReferenceManyField.tsx +31 -25
- package/src/crud/fields/SelectField.tsx +6 -0
- package/src/crud/fields/TextField.tsx +32 -30
- package/src/crud/fields/uploadFieldUtils.ts +21 -0
- package/src/crud/fields/useUploadPreviewUrl.ts +20 -0
- package/src/crud/index.ts +35 -14
- package/src/crud/types.ts +134 -130
- package/src/crud/utils/buildFormPayload.ts +24 -0
- package/src/crud/utils/buildInlineRowsPayload.ts +37 -0
- package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -0
- package/src/crud/utils/formErrors.ts +42 -38
- package/src/crud/utils/{getByPath.ts → getFormValue.ts} +2 -2
- package/src/crud/utils/hasUploadValues.ts +9 -0
- package/src/crud/utils/nestedFieldPath.ts +8 -0
- package/src/crud/utils/prepareFormSubmitBody.ts +17 -0
- package/src/crud/utils/{setByPath.ts → setFormValue.ts} +2 -2
- package/src/crud/utils/toFormData.ts +81 -0
- package/src/crud/utils/useAbortableEffect.ts +17 -0
- package/src/crud/utils/useFormRecord.ts +156 -0
- package/src/data/abortError.test.ts +19 -0
- package/src/data/abortError.ts +10 -0
- package/src/data/createMemoryResourceHandlers.ts +1 -1
- package/src/data/createRestResourceHandlers.ts +22 -16
- package/src/data/dataProviderTypes.ts +15 -5
- package/src/data/dataUtils.test.ts +118 -6
- package/src/data/parseFormErrorHelpers.test.ts +138 -84
- package/src/data/parseFormErrorHelpers.ts +311 -224
- package/src/data/resourceHandlers.ts +5 -4
- package/src/index.ts +34 -9
- package/src/layouts/AdminLayout.tsx +2 -1
- package/dist/src/crud/context/FormFieldsContext.d.ts +0 -14
- package/dist/src/crud/context/FormFieldsContext.d.ts.map +0 -1
- package/dist/src/crud/context/InlineFormContext.d.ts +0 -16
- package/dist/src/crud/context/InlineFormContext.d.ts.map +0 -1
- package/dist/src/crud/fields/useInlineOrFormField.d.ts +0 -10
- package/dist/src/crud/fields/useInlineOrFormField.d.ts.map +0 -1
- package/dist/src/crud/utils/getByPath.d.ts +0 -3
- package/dist/src/crud/utils/getByPath.d.ts.map +0 -1
- package/dist/src/crud/utils/inlineArrayName.d.ts +0 -3
- package/dist/src/crud/utils/inlineArrayName.d.ts.map +0 -1
- package/dist/src/crud/utils/pickBySources.d.ts +0 -6
- package/dist/src/crud/utils/pickBySources.d.ts.map +0 -1
- package/dist/src/crud/utils/setByPath.d.ts +0 -3
- package/dist/src/crud/utils/setByPath.d.ts.map +0 -1
- package/src/crud/context/FormFieldsContext.tsx +0 -76
- package/src/crud/context/InlineFormContext.tsx +0 -71
- package/src/crud/fields/useInlineOrFormField.tsx +0 -91
- package/src/crud/utils/inlineArrayName.ts +0 -4
- package/src/crud/utils/pickBySources.ts +0 -24
|
@@ -1,32 +1,19 @@
|
|
|
1
1
|
import { ArrowLeftOutlined } from "@ant-design/icons";
|
|
2
2
|
import { App, Button, Card, Form, Space, Spin, Typography, theme } from "antd";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
useForm,
|
|
6
|
-
type DefaultValues,
|
|
7
|
-
type FieldValues,
|
|
8
|
-
} from "react-hook-form";
|
|
9
|
-
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
|
3
|
+
import { FormProvider, useForm, type DefaultValues, type FieldValues } from "react-hook-form";
|
|
4
|
+
import { useEffect, useRef, type ReactNode } from "react";
|
|
10
5
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
|
11
6
|
import { useDataProvider } from "../context/DataProvider";
|
|
12
7
|
import { usePermissions } from "../context/PermissionsProvider";
|
|
13
8
|
import type { ResourcePermissions } from "../permissions/resourcePermissions";
|
|
14
9
|
import { checkResourcePermission } from "../permissions/resourcePermissions";
|
|
15
10
|
import { FormMetaProvider } from "./context/FormContext";
|
|
16
|
-
import {
|
|
17
|
-
import { pickBySources } from "./utils/pickBySources";
|
|
11
|
+
import { PayloadFieldsProvider } from "./context/PayloadFieldsContext";
|
|
18
12
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from "./
|
|
23
|
-
import { inlineArrayName } from "./utils/inlineArrayName";
|
|
24
|
-
import { parseAndApplyFormErrors } from "./utils/formErrors";
|
|
25
|
-
|
|
26
|
-
export type ResourceFormInlineConfig = Pick<
|
|
27
|
-
InlineFormSetProps,
|
|
28
|
-
"resource" | "foreignKey" | "name"
|
|
29
|
-
>;
|
|
13
|
+
InlineFieldsRegistryProvider,
|
|
14
|
+
type InlineFieldRegistration,
|
|
15
|
+
} from "./context/InlineFieldsRegistry";
|
|
16
|
+
import { useFormRecordLoad, useFormRecordSave } from "./utils/useFormRecord";
|
|
30
17
|
|
|
31
18
|
export type ResourceFormProps<T extends FieldValues> = {
|
|
32
19
|
resource: string;
|
|
@@ -36,15 +23,16 @@ export type ResourceFormProps<T extends FieldValues> = {
|
|
|
36
23
|
defaultValues?: Partial<T>;
|
|
37
24
|
onSaved?: (record: T) => void;
|
|
38
25
|
stayOnPage?: boolean;
|
|
39
|
-
inlines?: ResourceFormInlineConfig[];
|
|
40
26
|
/** Permission strings for create vs edit. Omit to allow all (demos only). */
|
|
41
27
|
permissions?: Pick<ResourcePermissions, "add" | "change">;
|
|
42
28
|
};
|
|
43
29
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Create/edit page — standard react-hook-form under the hood.
|
|
32
|
+
*
|
|
33
|
+
* Load: one `getOne` → `form.reset(record)` (nested inline arrays included).
|
|
34
|
+
* Save: one `create` or `update` with parent fields + nested inline rows.
|
|
35
|
+
*/
|
|
48
36
|
export function ResourceForm<T extends FieldValues & { id?: unknown }>({
|
|
49
37
|
resource,
|
|
50
38
|
title,
|
|
@@ -53,7 +41,6 @@ export function ResourceForm<T extends FieldValues & { id?: unknown }>({
|
|
|
53
41
|
defaultValues,
|
|
54
42
|
onSaved,
|
|
55
43
|
stayOnPage,
|
|
56
|
-
inlines,
|
|
57
44
|
permissions,
|
|
58
45
|
}: ResourceFormProps<T>) {
|
|
59
46
|
const { id } = useParams();
|
|
@@ -63,83 +50,37 @@ export function ResourceForm<T extends FieldValues & { id?: unknown }>({
|
|
|
63
50
|
const navigate = useNavigate();
|
|
64
51
|
const { message } = App.useApp();
|
|
65
52
|
const { token } = theme.useToken();
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const [existingInlineIds, setExistingInlineIds] = useState<
|
|
69
|
-
Record<string, (string | number)[]>
|
|
70
|
-
>({});
|
|
53
|
+
const payloadFieldsRef = useRef(new Set<string>());
|
|
54
|
+
const inlineRegistryRef = useRef(new Map<string, InlineFieldRegistration>());
|
|
71
55
|
|
|
72
56
|
const form = useForm<T>({
|
|
73
57
|
defaultValues: defaultValues as DefaultValues<T>,
|
|
74
58
|
});
|
|
75
|
-
const fieldSourcesRef = useRef(new Set<string>());
|
|
76
|
-
|
|
77
|
-
const loadInlines = useCallback(
|
|
78
|
-
async (parentId: string | number) => {
|
|
79
|
-
if (!inlines?.length) return;
|
|
80
|
-
const idsMap: Record<string, (string | number)[]> = {};
|
|
81
|
-
for (const cfg of inlines) {
|
|
82
|
-
const arrayName = resolveInlineArrayName(cfg);
|
|
83
|
-
const { rows, ids } = await loadInlineRows(
|
|
84
|
-
dp,
|
|
85
|
-
cfg.resource,
|
|
86
|
-
cfg.foreignKey,
|
|
87
|
-
parentId,
|
|
88
|
-
);
|
|
89
|
-
form.setValue(
|
|
90
|
-
arrayName as never,
|
|
91
|
-
rows as never,
|
|
92
|
-
);
|
|
93
|
-
idsMap[arrayName] = ids;
|
|
94
|
-
}
|
|
95
|
-
setExistingInlineIds(idsMap);
|
|
96
|
-
},
|
|
97
|
-
[dp, inlines, form],
|
|
98
|
-
);
|
|
99
59
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const res = await dp.getOne(resource, id);
|
|
111
|
-
const record = res.data as DefaultValues<T>;
|
|
112
|
-
if (inlines?.length) {
|
|
113
|
-
const merged = { ...record } as DefaultValues<T>;
|
|
114
|
-
const idsMap: Record<string, (string | number)[]> = {};
|
|
115
|
-
for (const cfg of inlines) {
|
|
116
|
-
const arrayName = resolveInlineArrayName(cfg);
|
|
117
|
-
const { rows, ids } = await loadInlineRows(
|
|
118
|
-
dp,
|
|
119
|
-
cfg.resource,
|
|
120
|
-
cfg.foreignKey,
|
|
121
|
-
id,
|
|
122
|
-
);
|
|
123
|
-
(merged as Record<string, unknown>)[arrayName] = rows;
|
|
124
|
-
idsMap[arrayName] = ids;
|
|
125
|
-
}
|
|
126
|
-
form.reset(merged);
|
|
127
|
-
setExistingInlineIds(idsMap);
|
|
128
|
-
setFormVersion((v) => v + 1);
|
|
129
|
-
} else {
|
|
130
|
-
form.reset(record);
|
|
131
|
-
setFormVersion((v) => v + 1);
|
|
132
|
-
}
|
|
133
|
-
} catch (e) {
|
|
134
|
-
message.error(e instanceof Error ? e.message : "Load failed");
|
|
135
|
-
} finally {
|
|
136
|
-
setLoading(false);
|
|
137
|
-
}
|
|
138
|
-
}, [dp, resource, id, isNew, form, message, defaultValues, loadInlines]);
|
|
60
|
+
const { loading, formVersion } = useFormRecordLoad({
|
|
61
|
+
dp,
|
|
62
|
+
resource,
|
|
63
|
+
id,
|
|
64
|
+
isNew,
|
|
65
|
+
form,
|
|
66
|
+
message,
|
|
67
|
+
defaultValues,
|
|
68
|
+
});
|
|
139
69
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
70
|
+
const onSubmit = useFormRecordSave({
|
|
71
|
+
dp,
|
|
72
|
+
resource,
|
|
73
|
+
id,
|
|
74
|
+
isNew,
|
|
75
|
+
form,
|
|
76
|
+
message,
|
|
77
|
+
navigate,
|
|
78
|
+
listPath,
|
|
79
|
+
payloadFieldsRef,
|
|
80
|
+
inlineRegistryRef,
|
|
81
|
+
onSaved,
|
|
82
|
+
stayOnPage,
|
|
83
|
+
});
|
|
143
84
|
|
|
144
85
|
useEffect(() => {
|
|
145
86
|
if (!permissions) return;
|
|
@@ -153,74 +94,6 @@ export function ResourceForm<T extends FieldValues & { id?: unknown }>({
|
|
|
153
94
|
? checkResourcePermission(can, permissions, isNew ? "add" : "change")
|
|
154
95
|
: true;
|
|
155
96
|
|
|
156
|
-
async function onSubmit(values: T) {
|
|
157
|
-
try {
|
|
158
|
-
const raw = values as Record<string, unknown>;
|
|
159
|
-
let payload = pickBySources(raw, Array.from(fieldSourcesRef.current));
|
|
160
|
-
if (inlines?.length) {
|
|
161
|
-
for (const cfg of inlines) {
|
|
162
|
-
delete payload[resolveInlineArrayName(cfg)];
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
let saved: T;
|
|
167
|
-
if (isNew) {
|
|
168
|
-
const res = await dp.create(resource, payload);
|
|
169
|
-
saved = res.data as T;
|
|
170
|
-
message.success("Created");
|
|
171
|
-
} else if (id) {
|
|
172
|
-
const res = await dp.update(resource, { id, data: payload });
|
|
173
|
-
saved = res.data as T;
|
|
174
|
-
message.success("Updated");
|
|
175
|
-
} else {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const parentId = saved.id as string | number;
|
|
180
|
-
if (inlines?.length && parentId != null) {
|
|
181
|
-
for (const cfg of inlines) {
|
|
182
|
-
const arrayName = resolveInlineArrayName(cfg);
|
|
183
|
-
const rows =
|
|
184
|
-
((form.getValues(arrayName as never) as unknown) as
|
|
185
|
-
Record<string, unknown>[] | undefined) ?? [];
|
|
186
|
-
const inlineOk = await saveInlineRows(dp, {
|
|
187
|
-
resource: cfg.resource,
|
|
188
|
-
foreignKey: cfg.foreignKey,
|
|
189
|
-
parentId,
|
|
190
|
-
rows,
|
|
191
|
-
existingIds: existingInlineIds[arrayName] ?? [],
|
|
192
|
-
onRowError: (e, index) => {
|
|
193
|
-
const row = rows[index];
|
|
194
|
-
const rowId = row?.id as string | number | undefined;
|
|
195
|
-
return parseAndApplyFormErrors(dp, form, message, e, {
|
|
196
|
-
resource: cfg.resource,
|
|
197
|
-
mutation:
|
|
198
|
-
rowId != null &&
|
|
199
|
-
(existingInlineIds[arrayName] ?? []).some((id) => id === rowId)
|
|
200
|
-
? "update"
|
|
201
|
-
: "create",
|
|
202
|
-
inlineArrayName: arrayName,
|
|
203
|
-
rowIndex: index,
|
|
204
|
-
});
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
if (!inlineOk) return;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
onSaved?.(saved);
|
|
212
|
-
if (!stayOnPage) navigate(listPath);
|
|
213
|
-
} catch (e) {
|
|
214
|
-
const handled = parseAndApplyFormErrors(dp, form, message, e, {
|
|
215
|
-
resource,
|
|
216
|
-
mutation: isNew ? "create" : "update",
|
|
217
|
-
});
|
|
218
|
-
if (!handled) {
|
|
219
|
-
message.error(e instanceof Error ? e.message : "Save failed");
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
97
|
return (
|
|
225
98
|
<Card
|
|
226
99
|
title={
|
|
@@ -235,46 +108,52 @@ export function ResourceForm<T extends FieldValues & { id?: unknown }>({
|
|
|
235
108
|
}
|
|
236
109
|
>
|
|
237
110
|
<FormMetaProvider resource={resource} isNew={isNew}>
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
111
|
+
<PayloadFieldsProvider fieldsRef={payloadFieldsRef}>
|
|
112
|
+
<InlineFieldsRegistryProvider registryRef={inlineRegistryRef}>
|
|
113
|
+
<div style={{ position: "relative" }}>
|
|
114
|
+
{loading ? (
|
|
115
|
+
<div
|
|
116
|
+
style={{
|
|
117
|
+
position: "absolute",
|
|
118
|
+
inset: 0,
|
|
119
|
+
display: "flex",
|
|
120
|
+
alignItems: "center",
|
|
121
|
+
justifyContent: "center",
|
|
122
|
+
zIndex: 1,
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
<Spin />
|
|
126
|
+
</div>
|
|
127
|
+
) : null}
|
|
128
|
+
<FormProvider {...form} key={formVersion}>
|
|
129
|
+
<Form
|
|
130
|
+
layout="vertical"
|
|
131
|
+
onFinish={() => void form.handleSubmit(onSubmit)()}
|
|
132
|
+
style={{
|
|
133
|
+
opacity: loading ? 0.4 : 1,
|
|
134
|
+
pointerEvents: loading ? "none" : undefined,
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
{children}
|
|
138
|
+
<Form.Item style={{ marginTop: 16 }}>
|
|
139
|
+
<Space>
|
|
140
|
+
<Button
|
|
141
|
+
type="primary"
|
|
142
|
+
htmlType="submit"
|
|
143
|
+
disabled={loading || !canSave}
|
|
144
|
+
>
|
|
145
|
+
Save
|
|
146
|
+
</Button>
|
|
147
|
+
<Link to={listPath}>
|
|
148
|
+
<Button disabled={loading}>Cancel</Button>
|
|
149
|
+
</Link>
|
|
150
|
+
</Space>
|
|
151
|
+
</Form.Item>
|
|
152
|
+
</Form>
|
|
153
|
+
</FormProvider>
|
|
252
154
|
</div>
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
<Form
|
|
256
|
-
layout="vertical"
|
|
257
|
-
onFinish={() => void form.handleSubmit(onSubmit)()}
|
|
258
|
-
style={{
|
|
259
|
-
opacity: loading ? 0.4 : 1,
|
|
260
|
-
pointerEvents: loading ? "none" : undefined,
|
|
261
|
-
}}
|
|
262
|
-
>
|
|
263
|
-
{children}
|
|
264
|
-
<Form.Item style={{ marginTop: 16 }}>
|
|
265
|
-
<Space>
|
|
266
|
-
<Button type="primary" htmlType="submit" disabled={loading || !canSave}>
|
|
267
|
-
Save
|
|
268
|
-
</Button>
|
|
269
|
-
<Link to={listPath}>
|
|
270
|
-
<Button disabled={loading}>Cancel</Button>
|
|
271
|
-
</Link>
|
|
272
|
-
</Space>
|
|
273
|
-
</Form.Item>
|
|
274
|
-
</Form>
|
|
275
|
-
</FormProvider>
|
|
276
|
-
</div>
|
|
277
|
-
</FormFieldsProvider>
|
|
155
|
+
</InlineFieldsRegistryProvider>
|
|
156
|
+
</PayloadFieldsProvider>
|
|
278
157
|
</FormMetaProvider>
|
|
279
158
|
</Card>
|
|
280
159
|
);
|
|
@@ -5,12 +5,14 @@ import {
|
|
|
5
5
|
type DefaultValues,
|
|
6
6
|
type FieldValues,
|
|
7
7
|
} from "react-hook-form";
|
|
8
|
-
import { useCallback,
|
|
8
|
+
import { useCallback, useRef, useState, type ReactNode } from "react";
|
|
9
9
|
import { useDataProvider } from "../context/DataProvider";
|
|
10
10
|
import { FormMetaProvider } from "./context/FormContext";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { PayloadFieldsProvider } from "./context/PayloadFieldsContext";
|
|
12
|
+
import { buildResourceFormSubmitBody } from "./utils/buildResourceFormSubmitBody";
|
|
13
|
+
import { applyApiErrorsToForm } from "./utils/formErrors";
|
|
14
|
+
import { useAbortableEffect } from "./utils/useAbortableEffect";
|
|
15
|
+
import { isAbortError } from "../data/abortError";
|
|
14
16
|
|
|
15
17
|
export type ResourceFormModalProps = {
|
|
16
18
|
resource: string;
|
|
@@ -34,45 +36,55 @@ export function ResourceFormModal({
|
|
|
34
36
|
const [loading, setLoading] = useState(!isNew);
|
|
35
37
|
|
|
36
38
|
const form = useForm<FieldValues>();
|
|
37
|
-
const
|
|
39
|
+
const payloadFieldsRef = useRef(new Set<string>());
|
|
38
40
|
|
|
39
|
-
const load = useCallback(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
);
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
useAbortableEffect(
|
|
65
|
+
(signal) => {
|
|
66
|
+
if (!open) return;
|
|
67
|
+
return load(signal);
|
|
68
|
+
},
|
|
69
|
+
[open, load],
|
|
70
|
+
);
|
|
59
71
|
|
|
60
72
|
async function onSubmit(values: FieldValues) {
|
|
61
73
|
try {
|
|
62
|
-
const
|
|
74
|
+
const body = buildResourceFormSubmitBody(
|
|
63
75
|
values as Record<string, unknown>,
|
|
64
|
-
Array.from(
|
|
76
|
+
Array.from(payloadFieldsRef.current),
|
|
65
77
|
);
|
|
66
78
|
if (isNew) {
|
|
67
|
-
await dp.create(resource,
|
|
79
|
+
await dp.create(resource, body);
|
|
68
80
|
message.success("Created");
|
|
69
81
|
} else if (editId) {
|
|
70
|
-
await dp.update(resource, { id: editId, data:
|
|
82
|
+
await dp.update(resource, { id: editId, data: body });
|
|
71
83
|
message.success("Updated");
|
|
72
84
|
}
|
|
73
85
|
onClose();
|
|
74
86
|
} catch (e) {
|
|
75
|
-
const handled =
|
|
87
|
+
const handled = await applyApiErrorsToForm(dp, form, message, e, {
|
|
76
88
|
resource,
|
|
77
89
|
mutation: isNew ? "create" : "update",
|
|
78
90
|
});
|
|
@@ -98,22 +110,22 @@ export function ResourceFormModal({
|
|
|
98
110
|
<Spin />
|
|
99
111
|
) : (
|
|
100
112
|
<FormMetaProvider resource={resource} isNew={isNew}>
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
</
|
|
113
|
+
<PayloadFieldsProvider fieldsRef={payloadFieldsRef}>
|
|
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
|
+
</PayloadFieldsProvider>
|
|
117
129
|
</FormMetaProvider>
|
|
118
130
|
)}
|
|
119
131
|
</Modal>
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
createContext,
|
|
10
10
|
useCallback,
|
|
11
11
|
useContext,
|
|
12
|
-
useEffect,
|
|
13
12
|
useMemo,
|
|
14
13
|
useState,
|
|
15
14
|
type Key,
|
|
@@ -32,6 +31,8 @@ import type {
|
|
|
32
31
|
ResourceListRowActionsHelpers,
|
|
33
32
|
} from "./types";
|
|
34
33
|
import { useListQueryState } from "./utils/useListQueryState";
|
|
34
|
+
import { useAbortableEffect } from "./utils/useAbortableEffect";
|
|
35
|
+
import { isAbortError } from "../data/abortError";
|
|
35
36
|
|
|
36
37
|
type ResourceListContextValue = {
|
|
37
38
|
filterValues: Record<string, unknown>;
|
|
@@ -263,32 +264,40 @@ function ResourceListTable<T extends Record<string, unknown>>({
|
|
|
263
264
|
[queryActions, queryState.sort.length],
|
|
264
265
|
);
|
|
265
266
|
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
queryState.sort.length ===
|
|
271
|
-
?
|
|
272
|
-
: queryState.sort
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
267
|
+
const listParams = useMemo(() => {
|
|
268
|
+
const sort =
|
|
269
|
+
queryState.sort.length === 0
|
|
270
|
+
? undefined
|
|
271
|
+
: queryState.sort.length === 1
|
|
272
|
+
? queryState.sort[0]
|
|
273
|
+
: queryState.sort;
|
|
274
|
+
return {
|
|
275
|
+
pagination: { page: queryState.page, perPage: queryState.perPage },
|
|
276
|
+
sort,
|
|
277
|
+
filter: queryState.filter,
|
|
278
|
+
};
|
|
279
|
+
}, [queryState]);
|
|
280
|
+
|
|
281
|
+
const load = useCallback(
|
|
282
|
+
async (signal?: AbortSignal) => {
|
|
283
|
+
setLoading(true);
|
|
284
|
+
try {
|
|
285
|
+
const res = await dp.getList(resource, { ...listParams, signal });
|
|
286
|
+
if (signal?.aborted) return;
|
|
287
|
+
setData(res.data as T[]);
|
|
288
|
+
setTotal(res.total);
|
|
289
|
+
} catch (e) {
|
|
290
|
+
if (!isAbortError(e)) {
|
|
291
|
+
message.error(e instanceof Error ? e.message : "Load failed");
|
|
292
|
+
}
|
|
293
|
+
} finally {
|
|
294
|
+
if (!signal?.aborted) setLoading(false);
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
[dp, resource, listParams, message],
|
|
298
|
+
);
|
|
288
299
|
|
|
289
|
-
|
|
290
|
-
void load();
|
|
291
|
-
}, [load]);
|
|
300
|
+
useAbortableEffect((signal) => load(signal), [load]);
|
|
292
301
|
|
|
293
302
|
const bulkActionHelpers = useMemo(
|
|
294
303
|
(): ResourceListBulkActionHelpers => ({
|
|
@@ -517,6 +526,7 @@ function ResourceListTable<T extends Record<string, unknown>>({
|
|
|
517
526
|
loading={loading}
|
|
518
527
|
columns={tableColumns}
|
|
519
528
|
dataSource={data}
|
|
529
|
+
scroll={{ x: "max-content" }}
|
|
520
530
|
rowSelection={
|
|
521
531
|
showBulkActions
|
|
522
532
|
? {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useMemo, type CSSProperties } from "react";
|
|
2
|
+
import type { BaseSourceProps } from "../types";
|
|
3
|
+
import { useRegisterColumn } from "../context/ListContext";
|
|
4
|
+
|
|
5
|
+
export type ImageColumnProps = BaseSourceProps & {
|
|
6
|
+
sortable?: boolean;
|
|
7
|
+
/** Thumbnail width in pixels. */
|
|
8
|
+
width?: number;
|
|
9
|
+
/** Thumbnail height in pixels. */
|
|
10
|
+
height?: number;
|
|
11
|
+
objectFit?: CSSProperties["objectFit"];
|
|
12
|
+
borderRadius?: number;
|
|
13
|
+
alt?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function ImageColumn({
|
|
17
|
+
source,
|
|
18
|
+
label,
|
|
19
|
+
sortable = false,
|
|
20
|
+
width = 40,
|
|
21
|
+
height = 40,
|
|
22
|
+
objectFit = "cover",
|
|
23
|
+
borderRadius = 4,
|
|
24
|
+
alt = "",
|
|
25
|
+
}: ImageColumnProps) {
|
|
26
|
+
const def = useMemo(
|
|
27
|
+
() => ({
|
|
28
|
+
key: source,
|
|
29
|
+
source,
|
|
30
|
+
label,
|
|
31
|
+
sortable,
|
|
32
|
+
buildColumn: () => ({
|
|
33
|
+
title: label ?? source,
|
|
34
|
+
dataIndex: source,
|
|
35
|
+
key: source,
|
|
36
|
+
sorter: sortable ? true : undefined,
|
|
37
|
+
render: (value: unknown) => {
|
|
38
|
+
if (value == null || value === "") return null;
|
|
39
|
+
return (
|
|
40
|
+
<img
|
|
41
|
+
src={String(value)}
|
|
42
|
+
alt={alt}
|
|
43
|
+
style={{
|
|
44
|
+
width,
|
|
45
|
+
height,
|
|
46
|
+
objectFit,
|
|
47
|
+
borderRadius,
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
}),
|
|
54
|
+
[source, label, sortable, width, height, objectFit, borderRadius, alt],
|
|
55
|
+
);
|
|
56
|
+
useRegisterColumn(def);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import type { BaseSourceProps } from "../types";
|
|
3
|
-
import {
|
|
3
|
+
import { getFormValue } from "../utils/getFormValue";
|
|
4
4
|
import { useRegisterColumn } from "../context/ListContext";
|
|
5
5
|
|
|
6
6
|
export type TextColumnProps = BaseSourceProps & {
|
|
@@ -33,6 +33,6 @@ export function renderDisplayValue(
|
|
|
33
33
|
display?: string | ((record: Record<string, unknown>) => unknown),
|
|
34
34
|
): unknown {
|
|
35
35
|
if (typeof display === "function") return display(record);
|
|
36
|
-
if (display) return
|
|
36
|
+
if (display) return getFormValue(record, display);
|
|
37
37
|
return record[source];
|
|
38
38
|
}
|