ding-react-admin 1.0.2 → 1.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.
- package/README.md +36 -5
- package/dist/index.js +712 -723
- package/dist/src/crud/InlineFormSet.d.ts +13 -11
- package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
- package/dist/src/crud/ResourceForm.d.ts +10 -2
- 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/context/SubmitFieldsContext.d.ts +18 -0
- package/dist/src/crud/context/SubmitFieldsContext.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/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/index.d.ts +8 -4
- package/dist/src/crud/index.d.ts.map +1 -1
- package/dist/src/crud/types.d.ts +21 -20
- package/dist/src/crud/types.d.ts.map +1 -1
- package/dist/src/crud/utils/inlineFieldName.d.ts +3 -0
- package/dist/src/crud/utils/inlineFieldName.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/useResourceFormData.d.ts +47 -0
- package/dist/src/crud/utils/useResourceFormData.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 +2 -2
- package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -1
- package/dist/src/data/dataProviderTypes.d.ts +7 -1
- package/dist/src/data/dataProviderTypes.d.ts.map +1 -1
- package/dist/src/data/resourceHandlers.d.ts +2 -2
- package/dist/src/data/resourceHandlers.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -3
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +59 -59
- package/src/crud/InlineFormSet.tsx +77 -89
- package/src/crud/ResourceForm.tsx +87 -202
- package/src/crud/ResourceFormModal.tsx +37 -25
- package/src/crud/ResourceList.tsx +35 -26
- package/src/crud/context/SubmitFieldsContext.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 +32 -54
- 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/index.ts +19 -2
- package/src/crud/types.ts +132 -130
- package/src/crud/utils/inlineFieldName.ts +8 -0
- package/src/crud/utils/useAbortableEffect.ts +17 -0
- package/src/crud/utils/useResourceFormData.ts +231 -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 +4 -4
- package/src/data/dataProviderTypes.ts +8 -0
- package/src/data/resourceHandlers.ts +4 -3
- package/src/index.ts +14 -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/src/crud/context/FormFieldsContext.tsx +0 -76
- package/src/crud/context/InlineFormContext.tsx +0 -71
- package/src/crud/fields/useInlineOrFormField.tsx +0 -91
|
@@ -1,32 +1,21 @@
|
|
|
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 { SubmitFieldsProvider } from "./context/SubmitFieldsContext";
|
|
18
12
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
type
|
|
22
|
-
} from "./
|
|
23
|
-
import { inlineArrayName } from "./utils/inlineArrayName";
|
|
24
|
-
import { parseAndApplyFormErrors } from "./utils/formErrors";
|
|
13
|
+
useResourceFormLoad,
|
|
14
|
+
useResourceFormSubmit,
|
|
15
|
+
type ResourceFormInlineConfig,
|
|
16
|
+
} from "./utils/useResourceFormData";
|
|
25
17
|
|
|
26
|
-
export type ResourceFormInlineConfig
|
|
27
|
-
InlineFormSetProps,
|
|
28
|
-
"resource" | "foreignKey" | "name"
|
|
29
|
-
>;
|
|
18
|
+
export type { ResourceFormInlineConfig };
|
|
30
19
|
|
|
31
20
|
export type ResourceFormProps<T extends FieldValues> = {
|
|
32
21
|
resource: string;
|
|
@@ -41,10 +30,14 @@ export type ResourceFormProps<T extends FieldValues> = {
|
|
|
41
30
|
permissions?: Pick<ResourcePermissions, "add" | "change">;
|
|
42
31
|
};
|
|
43
32
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Create/edit page shell around react-hook-form.
|
|
35
|
+
*
|
|
36
|
+
* Flow:
|
|
37
|
+
* 1. Load — edit: getOne(parent) + loadInlineRows → form.reset (see useResourceFormLoad)
|
|
38
|
+
* 2. Edit — children render fields; useSubmitField tracks sources for the API payload
|
|
39
|
+
* 3. Save — pickBySources → create/update parent → saveInlineRows (see useResourceFormSubmit)
|
|
40
|
+
*/
|
|
48
41
|
export function ResourceForm<T extends FieldValues & { id?: unknown }>({
|
|
49
42
|
resource,
|
|
50
43
|
title,
|
|
@@ -63,83 +56,38 @@ export function ResourceForm<T extends FieldValues & { id?: unknown }>({
|
|
|
63
56
|
const navigate = useNavigate();
|
|
64
57
|
const { message } = App.useApp();
|
|
65
58
|
const { token } = theme.useToken();
|
|
66
|
-
const
|
|
67
|
-
const [formVersion, setFormVersion] = useState(0);
|
|
68
|
-
const [existingInlineIds, setExistingInlineIds] = useState<
|
|
69
|
-
Record<string, (string | number)[]>
|
|
70
|
-
>({});
|
|
59
|
+
const submitFieldsRef = useRef(new Set<string>());
|
|
71
60
|
|
|
72
61
|
const form = useForm<T>({
|
|
73
62
|
defaultValues: defaultValues as DefaultValues<T>,
|
|
74
63
|
});
|
|
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
64
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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]);
|
|
65
|
+
const { loading, formVersion, existingInlineIds } = useResourceFormLoad({
|
|
66
|
+
dp,
|
|
67
|
+
resource,
|
|
68
|
+
id,
|
|
69
|
+
isNew,
|
|
70
|
+
form,
|
|
71
|
+
message,
|
|
72
|
+
defaultValues,
|
|
73
|
+
inlines,
|
|
74
|
+
});
|
|
139
75
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
76
|
+
const onSubmit = useResourceFormSubmit({
|
|
77
|
+
dp,
|
|
78
|
+
resource,
|
|
79
|
+
id,
|
|
80
|
+
isNew,
|
|
81
|
+
form,
|
|
82
|
+
message,
|
|
83
|
+
navigate,
|
|
84
|
+
listPath,
|
|
85
|
+
submitFieldsRef,
|
|
86
|
+
inlines,
|
|
87
|
+
existingInlineIds,
|
|
88
|
+
onSaved,
|
|
89
|
+
stayOnPage,
|
|
90
|
+
});
|
|
143
91
|
|
|
144
92
|
useEffect(() => {
|
|
145
93
|
if (!permissions) return;
|
|
@@ -153,74 +101,6 @@ export function ResourceForm<T extends FieldValues & { id?: unknown }>({
|
|
|
153
101
|
? checkResourcePermission(can, permissions, isNew ? "add" : "change")
|
|
154
102
|
: true;
|
|
155
103
|
|
|
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
104
|
return (
|
|
225
105
|
<Card
|
|
226
106
|
title={
|
|
@@ -235,46 +115,51 @@ export function ResourceForm<T extends FieldValues & { id?: unknown }>({
|
|
|
235
115
|
}
|
|
236
116
|
>
|
|
237
117
|
<FormMetaProvider resource={resource} isNew={isNew}>
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
<
|
|
266
|
-
<
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
118
|
+
<SubmitFieldsProvider fieldsRef={submitFieldsRef}>
|
|
119
|
+
<div style={{ position: "relative" }}>
|
|
120
|
+
{loading ? (
|
|
121
|
+
<div
|
|
122
|
+
style={{
|
|
123
|
+
position: "absolute",
|
|
124
|
+
inset: 0,
|
|
125
|
+
display: "flex",
|
|
126
|
+
alignItems: "center",
|
|
127
|
+
justifyContent: "center",
|
|
128
|
+
zIndex: 1,
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<Spin />
|
|
132
|
+
</div>
|
|
133
|
+
) : null}
|
|
134
|
+
{/* formVersion remounts RHF after load so inline field arrays bind correctly */}
|
|
135
|
+
<FormProvider {...form} key={formVersion}>
|
|
136
|
+
<Form
|
|
137
|
+
layout="vertical"
|
|
138
|
+
onFinish={() => void form.handleSubmit(onSubmit)()}
|
|
139
|
+
style={{
|
|
140
|
+
opacity: loading ? 0.4 : 1,
|
|
141
|
+
pointerEvents: loading ? "none" : undefined,
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
{children}
|
|
145
|
+
<Form.Item style={{ marginTop: 16 }}>
|
|
146
|
+
<Space>
|
|
147
|
+
<Button
|
|
148
|
+
type="primary"
|
|
149
|
+
htmlType="submit"
|
|
150
|
+
disabled={loading || !canSave}
|
|
151
|
+
>
|
|
152
|
+
Save
|
|
153
|
+
</Button>
|
|
154
|
+
<Link to={listPath}>
|
|
155
|
+
<Button disabled={loading}>Cancel</Button>
|
|
156
|
+
</Link>
|
|
157
|
+
</Space>
|
|
158
|
+
</Form.Item>
|
|
159
|
+
</Form>
|
|
160
|
+
</FormProvider>
|
|
161
|
+
</div>
|
|
162
|
+
</SubmitFieldsProvider>
|
|
278
163
|
</FormMetaProvider>
|
|
279
164
|
</Card>
|
|
280
165
|
);
|
|
@@ -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 {
|
|
11
|
+
import { SubmitFieldsProvider } from "./context/SubmitFieldsContext";
|
|
12
12
|
import { pickBySources } from "./utils/pickBySources";
|
|
13
13
|
import { parseAndApplyFormErrors } 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,34 +36,44 @@ export function ResourceFormModal({
|
|
|
34
36
|
const [loading, setLoading] = useState(!isNew);
|
|
35
37
|
|
|
36
38
|
const form = useForm<FieldValues>();
|
|
37
|
-
const
|
|
39
|
+
const submitFieldsRef = 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
74
|
const payload = pickBySources(
|
|
63
75
|
values as Record<string, unknown>,
|
|
64
|
-
Array.from(
|
|
76
|
+
Array.from(submitFieldsRef.current),
|
|
65
77
|
);
|
|
66
78
|
if (isNew) {
|
|
67
79
|
await dp.create(resource, payload);
|
|
@@ -98,7 +110,7 @@ export function ResourceFormModal({
|
|
|
98
110
|
<Spin />
|
|
99
111
|
) : (
|
|
100
112
|
<FormMetaProvider resource={resource} isNew={isNew}>
|
|
101
|
-
<
|
|
113
|
+
<SubmitFieldsProvider fieldsRef={submitFieldsRef}>
|
|
102
114
|
<FormProvider {...form}>
|
|
103
115
|
<Form
|
|
104
116
|
layout="vertical"
|
|
@@ -113,7 +125,7 @@ export function ResourceFormModal({
|
|
|
113
125
|
</Form.Item>
|
|
114
126
|
</Form>
|
|
115
127
|
</FormProvider>
|
|
116
|
-
</
|
|
128
|
+
</SubmitFieldsProvider>
|
|
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 => ({
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
type MutableRefObject,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { useFormSectionSourcesOptional } from "./FormSectionContext";
|
|
9
|
+
|
|
10
|
+
const SubmitFieldsContext =
|
|
11
|
+
createContext<MutableRefObject<Set<string>> | null>(null);
|
|
12
|
+
|
|
13
|
+
/** Tracks which top-level field sources are included in the save payload. */
|
|
14
|
+
export function SubmitFieldsProvider({
|
|
15
|
+
children,
|
|
16
|
+
fieldsRef,
|
|
17
|
+
}: {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
fieldsRef: MutableRefObject<Set<string>>;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<SubmitFieldsContext.Provider value={fieldsRef}>
|
|
23
|
+
{children}
|
|
24
|
+
</SubmitFieldsContext.Provider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useSubmitFieldsOptional() {
|
|
29
|
+
return useContext(SubmitFieldsContext);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Remember a field's `source` for submit (`pickBySources`).
|
|
34
|
+
* Top-level fields only — skip for inline row cells (nested `name` paths).
|
|
35
|
+
*/
|
|
36
|
+
export function useSubmitField(source: string, enabled = true) {
|
|
37
|
+
const fieldsRef = useSubmitFieldsOptional();
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!enabled || !fieldsRef) return;
|
|
41
|
+
fieldsRef.current.add(source);
|
|
42
|
+
return () => {
|
|
43
|
+
fieldsRef.current.delete(source);
|
|
44
|
+
};
|
|
45
|
+
}, [fieldsRef, source, enabled]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Remember a field's `source` for tab/step error highlighting.
|
|
50
|
+
* Top-level fields only — no-op outside FormTabs / FormSteps.
|
|
51
|
+
*/
|
|
52
|
+
export function useSectionField(source: string, enabled = true) {
|
|
53
|
+
const sectionRef = useFormSectionSourcesOptional();
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!enabled || !sectionRef) return;
|
|
57
|
+
sectionRef.current.add(source);
|
|
58
|
+
return () => {
|
|
59
|
+
sectionRef.current.delete(source);
|
|
60
|
+
};
|
|
61
|
+
}, [sectionRef, source, enabled]);
|
|
62
|
+
}
|
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
import { Switch } from "antd";
|
|
2
2
|
import type { BaseSourceProps, FieldRules } from "../types";
|
|
3
|
-
import {
|
|
3
|
+
import { FieldWrapper } from "./FieldWrapper";
|
|
4
4
|
|
|
5
5
|
export type BooleanFieldProps = BaseSourceProps & {
|
|
6
|
+
name?: string;
|
|
6
7
|
required?: boolean;
|
|
7
8
|
rules?: FieldRules;
|
|
9
|
+
hideLabel?: boolean;
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
export function BooleanField({
|
|
11
13
|
source,
|
|
14
|
+
name,
|
|
12
15
|
label,
|
|
13
16
|
required,
|
|
14
17
|
rules,
|
|
18
|
+
hideLabel,
|
|
15
19
|
}: BooleanFieldProps) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
return (
|
|
21
|
+
<FieldWrapper
|
|
22
|
+
source={source}
|
|
23
|
+
name={name}
|
|
24
|
+
label={label}
|
|
25
|
+
required={required}
|
|
26
|
+
rules={rules}
|
|
27
|
+
hideLabel={hideLabel}
|
|
28
|
+
>
|
|
29
|
+
{({ value, onChange, disabled }) => (
|
|
30
|
+
<Switch
|
|
31
|
+
checked={Boolean(value)}
|
|
32
|
+
onChange={onChange}
|
|
33
|
+
disabled={disabled}
|
|
34
|
+
/>
|
|
35
|
+
)}
|
|
36
|
+
</FieldWrapper>
|
|
28
37
|
);
|
|
29
|
-
|
|
30
|
-
if (field.mode === "inline") return null;
|
|
31
|
-
return field.element;
|
|
32
38
|
}
|