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.
Files changed (86) hide show
  1. package/README.md +36 -5
  2. package/dist/index.js +712 -723
  3. package/dist/src/crud/InlineFormSet.d.ts +13 -11
  4. package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
  5. package/dist/src/crud/ResourceForm.d.ts +10 -2
  6. package/dist/src/crud/ResourceForm.d.ts.map +1 -1
  7. package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
  8. package/dist/src/crud/ResourceList.d.ts.map +1 -1
  9. package/dist/src/crud/context/SubmitFieldsContext.d.ts +18 -0
  10. package/dist/src/crud/context/SubmitFieldsContext.d.ts.map +1 -0
  11. package/dist/src/crud/fields/BooleanField.d.ts +3 -1
  12. package/dist/src/crud/fields/BooleanField.d.ts.map +1 -1
  13. package/dist/src/crud/fields/DateField.d.ts +3 -1
  14. package/dist/src/crud/fields/DateField.d.ts.map +1 -1
  15. package/dist/src/crud/fields/FieldWrapper.d.ts +7 -14
  16. package/dist/src/crud/fields/FieldWrapper.d.ts.map +1 -1
  17. package/dist/src/crud/fields/NumberField.d.ts +5 -3
  18. package/dist/src/crud/fields/NumberField.d.ts.map +1 -1
  19. package/dist/src/crud/fields/PasswordField.d.ts +5 -3
  20. package/dist/src/crud/fields/PasswordField.d.ts.map +1 -1
  21. package/dist/src/crud/fields/ReferenceField.d.ts +6 -5
  22. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
  23. package/dist/src/crud/fields/ReferenceManyField.d.ts +3 -1
  24. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
  25. package/dist/src/crud/fields/SelectField.d.ts +3 -1
  26. package/dist/src/crud/fields/SelectField.d.ts.map +1 -1
  27. package/dist/src/crud/fields/TextField.d.ts +5 -3
  28. package/dist/src/crud/fields/TextField.d.ts.map +1 -1
  29. package/dist/src/crud/index.d.ts +8 -4
  30. package/dist/src/crud/index.d.ts.map +1 -1
  31. package/dist/src/crud/types.d.ts +21 -20
  32. package/dist/src/crud/types.d.ts.map +1 -1
  33. package/dist/src/crud/utils/inlineFieldName.d.ts +3 -0
  34. package/dist/src/crud/utils/inlineFieldName.d.ts.map +1 -0
  35. package/dist/src/crud/utils/useAbortableEffect.d.ts +7 -0
  36. package/dist/src/crud/utils/useAbortableEffect.d.ts.map +1 -0
  37. package/dist/src/crud/utils/useResourceFormData.d.ts +47 -0
  38. package/dist/src/crud/utils/useResourceFormData.d.ts.map +1 -0
  39. package/dist/src/data/abortError.d.ts +3 -0
  40. package/dist/src/data/abortError.d.ts.map +1 -0
  41. package/dist/src/data/abortError.test.d.ts +2 -0
  42. package/dist/src/data/abortError.test.d.ts.map +1 -0
  43. package/dist/src/data/createRestResourceHandlers.d.ts +2 -2
  44. package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -1
  45. package/dist/src/data/dataProviderTypes.d.ts +7 -1
  46. package/dist/src/data/dataProviderTypes.d.ts.map +1 -1
  47. package/dist/src/data/resourceHandlers.d.ts +2 -2
  48. package/dist/src/data/resourceHandlers.d.ts.map +1 -1
  49. package/dist/src/index.d.ts +4 -3
  50. package/dist/src/index.d.ts.map +1 -1
  51. package/package.json +59 -59
  52. package/src/crud/InlineFormSet.tsx +77 -89
  53. package/src/crud/ResourceForm.tsx +87 -202
  54. package/src/crud/ResourceFormModal.tsx +37 -25
  55. package/src/crud/ResourceList.tsx +35 -26
  56. package/src/crud/context/SubmitFieldsContext.tsx +62 -0
  57. package/src/crud/fields/BooleanField.tsx +22 -16
  58. package/src/crud/fields/DateField.tsx +32 -26
  59. package/src/crud/fields/FieldWrapper.tsx +32 -54
  60. package/src/crud/fields/NumberField.tsx +36 -34
  61. package/src/crud/fields/PasswordField.tsx +41 -40
  62. package/src/crud/fields/ReferenceField.tsx +35 -130
  63. package/src/crud/fields/ReferenceManyField.tsx +31 -25
  64. package/src/crud/fields/SelectField.tsx +6 -0
  65. package/src/crud/fields/TextField.tsx +32 -30
  66. package/src/crud/index.ts +19 -2
  67. package/src/crud/types.ts +132 -130
  68. package/src/crud/utils/inlineFieldName.ts +8 -0
  69. package/src/crud/utils/useAbortableEffect.ts +17 -0
  70. package/src/crud/utils/useResourceFormData.ts +231 -0
  71. package/src/data/abortError.test.ts +19 -0
  72. package/src/data/abortError.ts +10 -0
  73. package/src/data/createMemoryResourceHandlers.ts +1 -1
  74. package/src/data/createRestResourceHandlers.ts +4 -4
  75. package/src/data/dataProviderTypes.ts +8 -0
  76. package/src/data/resourceHandlers.ts +4 -3
  77. package/src/index.ts +14 -1
  78. package/dist/src/crud/context/FormFieldsContext.d.ts +0 -14
  79. package/dist/src/crud/context/FormFieldsContext.d.ts.map +0 -1
  80. package/dist/src/crud/context/InlineFormContext.d.ts +0 -16
  81. package/dist/src/crud/context/InlineFormContext.d.ts.map +0 -1
  82. package/dist/src/crud/fields/useInlineOrFormField.d.ts +0 -10
  83. package/dist/src/crud/fields/useInlineOrFormField.d.ts.map +0 -1
  84. package/src/crud/context/FormFieldsContext.tsx +0 -76
  85. package/src/crud/context/InlineFormContext.tsx +0 -71
  86. 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
- FormProvider,
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 { FormFieldsProvider } from "./context/FormFieldsContext";
17
- import { pickBySources } from "./utils/pickBySources";
11
+ import { SubmitFieldsProvider } from "./context/SubmitFieldsContext";
18
12
  import {
19
- loadInlineRows,
20
- saveInlineRows,
21
- type InlineFormSetProps,
22
- } from "./InlineFormSet";
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 = Pick<
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
- function resolveInlineArrayName(config: ResourceFormInlineConfig) {
45
- return inlineArrayName(config.resource, config.name);
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 [loading, setLoading] = useState(!isNew);
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 load = useCallback(async () => {
101
- if (isNew || !id) {
102
- if (defaultValues) {
103
- form.reset({ ...defaultValues } as DefaultValues<T>);
104
- }
105
- setLoading(false);
106
- return;
107
- }
108
- setLoading(true);
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]);
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
- useEffect(() => {
141
- void load();
142
- }, [load]);
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
- <FormFieldsProvider sourcesRef={fieldSourcesRef}>
239
- <div style={{ position: "relative" }}>
240
- {loading ? (
241
- <div
242
- style={{
243
- position: "absolute",
244
- inset: 0,
245
- display: "flex",
246
- alignItems: "center",
247
- justifyContent: "center",
248
- zIndex: 1,
249
- }}
250
- >
251
- <Spin />
252
- </div>
253
- ) : null}
254
- <FormProvider {...form} key={formVersion}>
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>
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, useEffect, useRef, useState, type ReactNode } from "react";
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 { FormFieldsProvider } from "./context/FormFieldsContext";
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 fieldSourcesRef = useRef(new Set<string>());
39
+ const submitFieldsRef = useRef(new Set<string>());
38
40
 
39
- const load = useCallback(async () => {
40
- if (isNew || !editId) {
41
- form.reset({} as DefaultValues<FieldValues>);
42
- setLoading(false);
43
- return;
44
- }
45
- setLoading(true);
46
- try {
47
- const res = await dp.getOne(resource, editId);
48
- form.reset(res.data as DefaultValues<FieldValues>);
49
- } catch (e) {
50
- message.error(e instanceof Error ? e.message : "Load failed");
51
- } finally {
52
- setLoading(false);
53
- }
54
- }, [dp, resource, editId, isNew, form, message]);
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
- useEffect(() => {
57
- if (open) void load();
58
- }, [open, load]);
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(fieldSourcesRef.current),
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
- <FormFieldsProvider sourcesRef={fieldSourcesRef}>
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
- </FormFieldsProvider>
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 load = useCallback(async () => {
267
- setLoading(true);
268
- try {
269
- const sort =
270
- queryState.sort.length === 0
271
- ? undefined
272
- : queryState.sort.length === 1
273
- ? queryState.sort[0]
274
- : queryState.sort;
275
- const res = await dp.getList(resource, {
276
- pagination: { page: queryState.page, perPage: queryState.perPage },
277
- sort,
278
- filter: queryState.filter,
279
- });
280
- setData(res.data as T[]);
281
- setTotal(res.total);
282
- } catch (e) {
283
- message.error(e instanceof Error ? e.message : "Load failed");
284
- } finally {
285
- setLoading(false);
286
- }
287
- }, [dp, resource, queryState, message]);
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
- useEffect(() => {
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 { useInlineOrFormField } from "./useInlineOrFormField";
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
- const field = useInlineOrFormField(
17
- source,
18
- label,
19
- required,
20
- rules,
21
- ({ value, onChange, disabled }) => (
22
- <Switch
23
- checked={Boolean(value)}
24
- onChange={onChange}
25
- disabled={disabled}
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
  }