ding-react-admin 1.0.4 → 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.
Files changed (112) hide show
  1. package/README.md +4 -10
  2. package/dist/index.js +1071 -803
  3. package/dist/src/crud/InlineFormSet.d.ts +2 -17
  4. package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
  5. package/dist/src/crud/ResourceForm.d.ts +4 -9
  6. package/dist/src/crud/ResourceForm.d.ts.map +1 -1
  7. package/dist/src/crud/ResourceList.d.ts.map +1 -1
  8. package/dist/src/crud/columns/ImageColumn.d.ts +14 -0
  9. package/dist/src/crud/columns/ImageColumn.d.ts.map +1 -0
  10. package/dist/src/crud/context/InlineFieldsRegistry.d.ts +15 -0
  11. package/dist/src/crud/context/InlineFieldsRegistry.d.ts.map +1 -0
  12. package/dist/src/crud/context/PayloadFieldsContext.d.ts +18 -0
  13. package/dist/src/crud/context/PayloadFieldsContext.d.ts.map +1 -0
  14. package/dist/src/crud/fields/FileField.d.ts +12 -0
  15. package/dist/src/crud/fields/FileField.d.ts.map +1 -0
  16. package/dist/src/crud/fields/ImageField.d.ts +14 -0
  17. package/dist/src/crud/fields/ImageField.d.ts.map +1 -0
  18. package/dist/src/crud/fields/uploadFieldUtils.d.ts +5 -0
  19. package/dist/src/crud/fields/uploadFieldUtils.d.ts.map +1 -0
  20. package/dist/src/crud/fields/useUploadPreviewUrl.d.ts +4 -0
  21. package/dist/src/crud/fields/useUploadPreviewUrl.d.ts.map +1 -0
  22. package/dist/src/crud/index.d.ts +22 -9
  23. package/dist/src/crud/index.d.ts.map +1 -1
  24. package/dist/src/crud/types.d.ts +9 -7
  25. package/dist/src/crud/types.d.ts.map +1 -1
  26. package/dist/src/crud/utils/buildFormPayload.d.ts +6 -0
  27. package/dist/src/crud/utils/buildFormPayload.d.ts.map +1 -0
  28. package/dist/src/crud/utils/buildInlineRowsPayload.d.ts +6 -0
  29. package/dist/src/crud/utils/buildInlineRowsPayload.d.ts.map +1 -0
  30. package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts +5 -0
  31. package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts.map +1 -0
  32. package/dist/src/crud/utils/formErrors.d.ts +1 -1
  33. package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
  34. package/dist/src/crud/utils/getFormValue.d.ts +3 -0
  35. package/dist/src/crud/utils/getFormValue.d.ts.map +1 -0
  36. package/dist/src/crud/utils/hasUploadValues.d.ts +3 -0
  37. package/dist/src/crud/utils/hasUploadValues.d.ts.map +1 -0
  38. package/dist/src/crud/utils/nestedFieldPath.d.ts +3 -0
  39. package/dist/src/crud/utils/nestedFieldPath.d.ts.map +1 -0
  40. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +7 -0
  41. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -0
  42. package/dist/src/crud/utils/setFormValue.d.ts +3 -0
  43. package/dist/src/crud/utils/setFormValue.d.ts.map +1 -0
  44. package/dist/src/crud/utils/toFormData.d.ts +13 -0
  45. package/dist/src/crud/utils/toFormData.d.ts.map +1 -0
  46. package/dist/src/crud/utils/useFormRecord.d.ts +42 -0
  47. package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -0
  48. package/dist/src/data/createRestResourceHandlers.d.ts +5 -5
  49. package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -1
  50. package/dist/src/data/dataProviderTypes.d.ts +6 -5
  51. package/dist/src/data/dataProviderTypes.d.ts.map +1 -1
  52. package/dist/src/data/parseFormErrorHelpers.d.ts +44 -6
  53. package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
  54. package/dist/src/data/resourceHandlers.d.ts +1 -1
  55. package/dist/src/data/resourceHandlers.d.ts.map +1 -1
  56. package/dist/src/index.d.ts +4 -4
  57. package/dist/src/index.d.ts.map +1 -1
  58. package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
  59. package/package.json +1 -1
  60. package/src/crud/InlineFormSet.tsx +28 -85
  61. package/src/crud/ResourceForm.tsx +60 -66
  62. package/src/crud/ResourceFormModal.tsx +25 -25
  63. package/src/crud/ResourceList.tsx +1 -0
  64. package/src/crud/columns/ImageColumn.tsx +58 -0
  65. package/src/crud/columns/TextColumn.tsx +2 -2
  66. package/src/crud/context/InlineFieldsRegistry.tsx +61 -0
  67. package/src/crud/context/PayloadFieldsContext.tsx +62 -0
  68. package/src/crud/fields/FieldWrapper.tsx +87 -87
  69. package/src/crud/fields/FileField.tsx +123 -0
  70. package/src/crud/fields/ImageField.tsx +120 -0
  71. package/src/crud/fields/uploadFieldUtils.ts +21 -0
  72. package/src/crud/fields/useUploadPreviewUrl.ts +20 -0
  73. package/src/crud/index.ts +23 -19
  74. package/src/crud/types.ts +9 -7
  75. package/src/crud/utils/buildFormPayload.ts +24 -0
  76. package/src/crud/utils/buildInlineRowsPayload.ts +37 -0
  77. package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -0
  78. package/src/crud/utils/formErrors.ts +42 -38
  79. package/src/crud/utils/{getByPath.ts → getFormValue.ts} +2 -2
  80. package/src/crud/utils/hasUploadValues.ts +9 -0
  81. package/src/crud/utils/nestedFieldPath.ts +8 -0
  82. package/src/crud/utils/prepareFormSubmitBody.ts +17 -0
  83. package/src/crud/utils/{setByPath.ts → setFormValue.ts} +2 -2
  84. package/src/crud/utils/toFormData.ts +81 -0
  85. package/src/crud/utils/useFormRecord.ts +156 -0
  86. package/src/data/createRestResourceHandlers.ts +19 -13
  87. package/src/data/dataProviderTypes.ts +7 -5
  88. package/src/data/dataUtils.test.ts +118 -6
  89. package/src/data/parseFormErrorHelpers.test.ts +138 -84
  90. package/src/data/parseFormErrorHelpers.ts +311 -224
  91. package/src/data/resourceHandlers.ts +1 -1
  92. package/src/index.ts +23 -11
  93. package/src/layouts/AdminLayout.tsx +2 -1
  94. package/dist/src/crud/context/SubmitFieldsContext.d.ts +0 -18
  95. package/dist/src/crud/context/SubmitFieldsContext.d.ts.map +0 -1
  96. package/dist/src/crud/utils/getByPath.d.ts +0 -3
  97. package/dist/src/crud/utils/getByPath.d.ts.map +0 -1
  98. package/dist/src/crud/utils/inlineArrayName.d.ts +0 -3
  99. package/dist/src/crud/utils/inlineArrayName.d.ts.map +0 -1
  100. package/dist/src/crud/utils/inlineFieldName.d.ts +0 -3
  101. package/dist/src/crud/utils/inlineFieldName.d.ts.map +0 -1
  102. package/dist/src/crud/utils/pickBySources.d.ts +0 -6
  103. package/dist/src/crud/utils/pickBySources.d.ts.map +0 -1
  104. package/dist/src/crud/utils/setByPath.d.ts +0 -3
  105. package/dist/src/crud/utils/setByPath.d.ts.map +0 -1
  106. package/dist/src/crud/utils/useResourceFormData.d.ts +0 -47
  107. package/dist/src/crud/utils/useResourceFormData.d.ts.map +0 -1
  108. package/src/crud/context/SubmitFieldsContext.tsx +0 -62
  109. package/src/crud/utils/inlineArrayName.ts +0 -4
  110. package/src/crud/utils/inlineFieldName.ts +0 -8
  111. package/src/crud/utils/pickBySources.ts +0 -24
  112. package/src/crud/utils/useResourceFormData.ts +0 -231
@@ -1,224 +1,311 @@
1
- import type {
2
- FormValidationErrors,
3
- ParseFormErrorContext,
4
- } from "./dataProviderTypes";
5
-
6
- export function asStringMessages(value: unknown): string[] {
7
- if (typeof value === "string") return [value];
8
- if (Array.isArray(value)) {
9
- const strings = value.filter((x) => typeof x === "string") as string[];
10
- if (strings.length) return strings;
11
- }
12
- return [];
13
- }
14
-
15
- function toFieldValue(msgs: string[]): string | string[] {
16
- return msgs.length === 1 ? msgs[0] : msgs;
17
- }
18
-
19
- /** Read JSON from `throw { body }` or axios-style `{ response: { data } }`. */
20
- export function getErrorBody(error: unknown): Record<string, unknown> | null {
21
- if (!error || typeof error !== "object") return null;
22
- const e = error as Record<string, unknown>;
23
- if (e.body && typeof e.body === "object" && !Array.isArray(e.body)) {
24
- return e.body as Record<string, unknown>;
25
- }
26
- const response = e.response;
27
- if (response && typeof response === "object" && !Array.isArray(response)) {
28
- const data = (response as Record<string, unknown>).data;
29
- if (data && typeof data === "object" && !Array.isArray(data)) {
30
- return data as Record<string, unknown>;
31
- }
32
- }
33
- return null;
34
- }
35
-
36
- export function applyInlineFieldPaths(
37
- fields: Record<string, string | string[]>,
38
- ctx: ParseFormErrorContext,
39
- ): Record<string, string | string[]> {
40
- if (ctx.inlineArrayName == null || ctx.rowIndex == null) return fields;
41
- const rowPrefix = `${ctx.inlineArrayName}.${ctx.rowIndex}.`;
42
- const out: Record<string, string | string[]> = {};
43
- for (const [source, message] of Object.entries(fields)) {
44
- out[rowPrefix + source] = message;
45
- }
46
- return out;
47
- }
48
-
49
- export function finalizeFormErrors(
50
- fields: Record<string, string | string[]>,
51
- global: string[],
52
- ctx: ParseFormErrorContext,
53
- ): FormValidationErrors {
54
- const fieldErrors = applyInlineFieldPaths(fields, ctx);
55
- return {
56
- fields: Object.keys(fieldErrors).length ? fieldErrors : undefined,
57
- global: global.length ? global : undefined,
58
- };
59
- }
60
-
61
- const DRF_GLOBAL_KEYS = new Set(["non_field_errors", "detail"]);
62
-
63
- /**
64
- * Django REST framework validation body:
65
- * `{ "email": ["Invalid"], "non_field_errors": ["…"] }`
66
- */
67
- export function parseDjangoDRFFormErrors(
68
- error: unknown,
69
- ctx: ParseFormErrorContext,
70
- ): FormValidationErrors | null {
71
- const body = getErrorBody(error);
72
- if (!body) return null;
73
-
74
- const fields: Record<string, string | string[]> = {};
75
- const global: string[] = [];
76
-
77
- for (const [key, value] of Object.entries(body)) {
78
- if (DRF_GLOBAL_KEYS.has(key)) {
79
- global.push(...asStringMessages(value));
80
- continue;
81
- }
82
- const msgs = asStringMessages(value);
83
- if (msgs.length) fields[key] = toFieldValue(msgs);
84
- }
85
-
86
- if (!Object.keys(fields).length && !global.length) return null;
87
- return finalizeFormErrors(fields, global, ctx);
88
- }
89
-
90
- export type DotNetFormErrorOptions = {
91
- /** Map API property names to form `source` paths. */
92
- fieldMap?: Record<string, string>;
93
- /** `Email` → `email`. Default true. */
94
- camelCase?: boolean;
95
- /** Include `title` / `message` in global toasts. Default false (often generic). */
96
- includeSummary?: boolean;
97
- };
98
-
99
- /**
100
- * ASP.NET Core `ValidationProblemDetails`:
101
- * `{ "errors": { "Email": ["The Email field is required."] } }`
102
- */
103
- export function parseDotNetFormErrors(
104
- error: unknown,
105
- ctx: ParseFormErrorContext,
106
- options?: DotNetFormErrorOptions,
107
- ): FormValidationErrors | null {
108
- const body = getErrorBody(error);
109
- if (!body) return null;
110
-
111
- const camelCase = options?.camelCase ?? true;
112
- const fieldMap = options?.fieldMap;
113
- const fields: Record<string, string | string[]> = {};
114
- const global: string[] = [];
115
-
116
- if (options?.includeSummary) {
117
- global.push(...asStringMessages(body.title));
118
- global.push(...asStringMessages(body.message));
119
- }
120
-
121
- const errors = body.errors;
122
- if (errors && typeof errors === "object" && !Array.isArray(errors)) {
123
- for (const [key, value] of Object.entries(errors as Record<string, unknown>)) {
124
- const mappedKey =
125
- fieldMap?.[key] ?? (camelCase ? camelCaseField(key) : key);
126
- const msgs = asStringMessages(value);
127
- if (msgs.length) fields[mappedKey] = toFieldValue(msgs);
128
- }
129
- }
130
-
131
- if (!Object.keys(fields).length && !global.length) return null;
132
- return finalizeFormErrors(fields, global, ctx);
133
- }
134
-
135
- export type NodeFormErrorOptions = {
136
- fieldMap?: Record<string, string>;
137
- };
138
-
139
- /**
140
- * Common Node / Express shapes:
141
- * - `{ errors: { email: ["Invalid"] } }`
142
- * - `{ errors: [{ path: "email", msg: "Invalid" }] }` (express-validator)
143
- * - `{ details: [{ message: "…", path: ["email"] }] }` (Joi)
144
- */
145
- export function parseNodeFormErrors(
146
- error: unknown,
147
- ctx: ParseFormErrorContext,
148
- options?: NodeFormErrorOptions,
149
- ): FormValidationErrors | null {
150
- const body = getErrorBody(error);
151
- if (!body) return null;
152
-
153
- const fields: Record<string, string | string[]> = {};
154
- const global: string[] = [];
155
- const fieldMap = options?.fieldMap;
156
-
157
- const errList = body.errors;
158
- if (Array.isArray(errList)) {
159
- for (const item of errList) {
160
- if (!item || typeof item !== "object") continue;
161
- const row = item as Record<string, unknown>;
162
- const path =
163
- (typeof row.path === "string" && row.path) ||
164
- (typeof row.param === "string" && row.param) ||
165
- (typeof row.field === "string" && row.field);
166
- const msg =
167
- asStringMessages(row.msg)[0] ?? asStringMessages(row.message)[0];
168
- if (!msg) continue;
169
- if (path) {
170
- const key = fieldMap?.[path] ?? path;
171
- mergeFieldMessage(fields, key, msg);
172
- } else {
173
- global.push(msg);
174
- }
175
- }
176
- } else if (errList && typeof errList === "object") {
177
- for (const [key, value] of Object.entries(errList as Record<string, unknown>)) {
178
- const mappedKey = fieldMap?.[key] ?? key;
179
- const msgs = asStringMessages(value);
180
- if (msgs.length) fields[mappedKey] = toFieldValue(msgs);
181
- }
182
- }
183
-
184
- const details = body.details;
185
- if (Array.isArray(details)) {
186
- for (const item of details) {
187
- if (!item || typeof item !== "object") continue;
188
- const row = item as Record<string, unknown>;
189
- const pathParts = Array.isArray(row.path) ? row.path : [];
190
- const path = pathParts.map((p) => String(p)).join(".");
191
- const msg = asStringMessages(row.message)[0];
192
- if (!msg) continue;
193
- if (path) {
194
- const key = fieldMap?.[path] ?? path;
195
- fields[key] = msg;
196
- } else {
197
- global.push(msg);
198
- }
199
- }
200
- }
201
-
202
- global.push(...asStringMessages(body.error));
203
-
204
- if (!Object.keys(fields).length && !global.length) return null;
205
- return finalizeFormErrors(fields, global, ctx);
206
- }
207
-
208
- function mergeFieldMessage(
209
- fields: Record<string, string | string[]>,
210
- key: string,
211
- msg: string,
212
- ) {
213
- const existing = fields[key];
214
- if (!existing) {
215
- fields[key] = msg;
216
- return;
217
- }
218
- fields[key] = Array.isArray(existing) ? [...existing, msg] : [existing, msg];
219
- }
220
-
221
- function camelCaseField(key: string): string {
222
- if (!key) return key;
223
- return key.charAt(0).toLowerCase() + key.slice(1);
224
- }
1
+ import type {
2
+ FormValidationErrors,
3
+ ParseFormErrorContext,
4
+ } from "./dataProviderTypes";
5
+
6
+ export function asStringMessages(value: unknown): string[] {
7
+ if (typeof value === "string") return [value];
8
+ if (Array.isArray(value)) {
9
+ const strings = value.filter((x) => typeof x === "string") as string[];
10
+ if (strings.length) return strings;
11
+ }
12
+ return [];
13
+ }
14
+
15
+ function toFieldValue(msgs: string[]): string | string[] {
16
+ return msgs.length === 1 ? msgs[0] : msgs;
17
+ }
18
+
19
+ function isJsonRecord(value: unknown): value is Record<string, unknown> {
20
+ return value !== null && typeof value === "object" && !Array.isArray(value);
21
+ }
22
+
23
+ function isFetchResponse(value: unknown): value is Response {
24
+ if (typeof Response !== "undefined" && value instanceof Response) {
25
+ return true;
26
+ }
27
+ return (
28
+ value !== null &&
29
+ typeof value === "object" &&
30
+ typeof (value as Response).json === "function" &&
31
+ typeof (value as Response).status === "number" &&
32
+ (value as Response).headers != null
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Read a parsed JSON validation body when it is already available on the error.
38
+ *
39
+ * Supported transport shapes (sync only):
40
+ * - `{ body: { email: ["…"] } }` — explicit throw from a handler
41
+ * - `{ response: { data: { email: ["…"] } } }` — axios-style wrappers
42
+ * - `{ data: { email: ["…"] } }` — some HTTP clients attach parsed JSON here
43
+ *
44
+ * For fetch / OpenAPI clients that only expose a `Response`, use `resolveErrorBody`.
45
+ */
46
+ export function getErrorBody(error: unknown): Record<string, unknown> | null {
47
+ if (!error || typeof error !== "object") return null;
48
+ const e = error as Record<string, unknown>;
49
+
50
+ if (isJsonRecord(e.body)) {
51
+ return e.body;
52
+ }
53
+
54
+ if (isJsonRecord(e.data)) {
55
+ return e.data;
56
+ }
57
+
58
+ const response = e.response;
59
+ if (response && typeof response === "object" && !Array.isArray(response)) {
60
+ const data = (response as Record<string, unknown>).data;
61
+ if (isJsonRecord(data)) {
62
+ return data;
63
+ }
64
+ }
65
+
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * Resolve a validation JSON body from any common HTTP client error shape.
71
+ * Falls back to reading `response.json()` for fetch / OpenAPI `ResponseError`.
72
+ */
73
+ export async function resolveErrorBody(
74
+ error: unknown,
75
+ ): Promise<Record<string, unknown> | null> {
76
+ const syncBody = getErrorBody(error);
77
+ if (syncBody) return syncBody;
78
+
79
+ if (!error || typeof error !== "object") return null;
80
+ const response = (error as Record<string, unknown>).response;
81
+ if (!isFetchResponse(response)) return null;
82
+
83
+ const contentType = response.headers.get("content-type");
84
+ if (!contentType || !/application\/json/i.test(contentType)) {
85
+ return null;
86
+ }
87
+
88
+ try {
89
+ const body: unknown = await response.clone().json();
90
+ return isJsonRecord(body) ? body : null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function isNestedRowErrorsArray(value: unknown): boolean {
97
+ if (!Array.isArray(value)) return false;
98
+ return value.some(
99
+ (item) =>
100
+ item &&
101
+ typeof item === "object" &&
102
+ !Array.isArray(item) &&
103
+ Object.values(item as Record<string, unknown>).some(
104
+ (v) => asStringMessages(v).length > 0,
105
+ ),
106
+ );
107
+ }
108
+
109
+ /** Map `{ lines: [{ label: ["…"] }] }` → `{ "lines.0.label": "…" }`. */
110
+ export function flattenNestedArrayErrors(
111
+ arrayKey: string,
112
+ rows: unknown[],
113
+ fields: Record<string, string | string[]>,
114
+ ): void {
115
+ rows.forEach((row, index) => {
116
+ if (!row || typeof row !== "object" || Array.isArray(row)) return;
117
+ for (const [source, value] of Object.entries(
118
+ row as Record<string, unknown>,
119
+ )) {
120
+ const msgs = asStringMessages(value);
121
+ if (msgs.length) {
122
+ fields[`${arrayKey}.${index}.${source}`] = toFieldValue(msgs);
123
+ }
124
+ }
125
+ });
126
+ }
127
+
128
+ export function finalizeFormErrors(
129
+ fields: Record<string, string | string[]>,
130
+ global: string[],
131
+ ): FormValidationErrors {
132
+ return {
133
+ fields: Object.keys(fields).length ? fields : undefined,
134
+ global: global.length ? global : undefined,
135
+ };
136
+ }
137
+
138
+ const DRF_GLOBAL_KEYS = new Set(["non_field_errors", "detail"]);
139
+
140
+ function parseDjangoDRFValidationBody(
141
+ body: Record<string, unknown>,
142
+ ): FormValidationErrors | null {
143
+ const fields: Record<string, string | string[]> = {};
144
+ const global: string[] = [];
145
+
146
+ for (const [key, value] of Object.entries(body)) {
147
+ if (DRF_GLOBAL_KEYS.has(key)) {
148
+ global.push(...asStringMessages(value));
149
+ continue;
150
+ }
151
+ if (isNestedRowErrorsArray(value)) {
152
+ flattenNestedArrayErrors(key, value as unknown[], fields);
153
+ continue;
154
+ }
155
+ const msgs = asStringMessages(value);
156
+ if (msgs.length) fields[key] = toFieldValue(msgs);
157
+ }
158
+
159
+ if (!Object.keys(fields).length && !global.length) return null;
160
+ return finalizeFormErrors(fields, global);
161
+ }
162
+
163
+ /**
164
+ * Django REST framework validation body:
165
+ * `{ "email": ["Invalid"], "non_field_errors": ["…"] }`
166
+ */
167
+ export function parseDjangoDRFFormErrors(
168
+ error: unknown,
169
+ _ctx: ParseFormErrorContext,
170
+ ): FormValidationErrors | null {
171
+ const body = getErrorBody(error);
172
+ if (!body) return null;
173
+ return parseDjangoDRFValidationBody(body);
174
+ }
175
+
176
+ export type DotNetFormErrorOptions = {
177
+ /** Map API property names to form `source` paths. */
178
+ fieldMap?: Record<string, string>;
179
+ /** `Email` `email`. Default true. */
180
+ camelCase?: boolean;
181
+ /** Include `title` / `message` in global toasts. Default false (often generic). */
182
+ includeSummary?: boolean;
183
+ };
184
+
185
+ /**
186
+ * ASP.NET Core `ValidationProblemDetails`:
187
+ * `{ "errors": { "Email": ["The Email field is required."] } }`
188
+ */
189
+ export function parseDotNetFormErrors(
190
+ error: unknown,
191
+ _ctx: ParseFormErrorContext,
192
+ options?: DotNetFormErrorOptions,
193
+ ): FormValidationErrors | null {
194
+ const body = getErrorBody(error);
195
+ if (!body) return null;
196
+
197
+ const camelCase = options?.camelCase ?? true;
198
+ const fieldMap = options?.fieldMap;
199
+ const fields: Record<string, string | string[]> = {};
200
+ const global: string[] = [];
201
+
202
+ if (options?.includeSummary) {
203
+ global.push(...asStringMessages(body.title));
204
+ global.push(...asStringMessages(body.message));
205
+ }
206
+
207
+ const errors = body.errors;
208
+ if (errors && typeof errors === "object" && !Array.isArray(errors)) {
209
+ for (const [key, value] of Object.entries(errors as Record<string, unknown>)) {
210
+ const mappedKey =
211
+ fieldMap?.[key] ?? (camelCase ? camelCaseField(key) : key);
212
+ const msgs = asStringMessages(value);
213
+ if (msgs.length) fields[mappedKey] = toFieldValue(msgs);
214
+ }
215
+ }
216
+
217
+ if (!Object.keys(fields).length && !global.length) return null;
218
+ return finalizeFormErrors(fields, global);
219
+ }
220
+
221
+ export type NodeFormErrorOptions = {
222
+ fieldMap?: Record<string, string>;
223
+ };
224
+
225
+ /**
226
+ * Common Node / Express shapes:
227
+ * - `{ errors: { email: ["Invalid"] } }`
228
+ * - `{ errors: [{ path: "email", msg: "Invalid" }] }` (express-validator)
229
+ * - `{ details: [{ message: "…", path: ["email"] }] }` (Joi)
230
+ */
231
+ export function parseNodeFormErrors(
232
+ error: unknown,
233
+ _ctx: ParseFormErrorContext,
234
+ options?: NodeFormErrorOptions,
235
+ ): FormValidationErrors | null {
236
+ const body = getErrorBody(error);
237
+ if (!body) return null;
238
+
239
+ const fields: Record<string, string | string[]> = {};
240
+ const global: string[] = [];
241
+ const fieldMap = options?.fieldMap;
242
+
243
+ const errList = body.errors;
244
+ if (Array.isArray(errList)) {
245
+ for (const item of errList) {
246
+ if (!item || typeof item !== "object") continue;
247
+ const row = item as Record<string, unknown>;
248
+ const path =
249
+ (typeof row.path === "string" && row.path) ||
250
+ (typeof row.param === "string" && row.param) ||
251
+ (typeof row.field === "string" && row.field);
252
+ const msg =
253
+ asStringMessages(row.msg)[0] ?? asStringMessages(row.message)[0];
254
+ if (!msg) continue;
255
+ if (path) {
256
+ const key = fieldMap?.[path] ?? path;
257
+ mergeFieldMessage(fields, key, msg);
258
+ } else {
259
+ global.push(msg);
260
+ }
261
+ }
262
+ } else if (errList && typeof errList === "object") {
263
+ for (const [key, value] of Object.entries(errList as Record<string, unknown>)) {
264
+ const mappedKey = fieldMap?.[key] ?? key;
265
+ const msgs = asStringMessages(value);
266
+ if (msgs.length) fields[mappedKey] = toFieldValue(msgs);
267
+ }
268
+ }
269
+
270
+ const details = body.details;
271
+ if (Array.isArray(details)) {
272
+ for (const item of details) {
273
+ if (!item || typeof item !== "object") continue;
274
+ const row = item as Record<string, unknown>;
275
+ const pathParts = Array.isArray(row.path) ? row.path : [];
276
+ const path = pathParts.map((p) => String(p)).join(".");
277
+ const msg = asStringMessages(row.message)[0];
278
+ if (!msg) continue;
279
+ if (path) {
280
+ const key = fieldMap?.[path] ?? path;
281
+ fields[key] = msg;
282
+ } else {
283
+ global.push(msg);
284
+ }
285
+ }
286
+ }
287
+
288
+ global.push(...asStringMessages(body.error));
289
+
290
+ if (!Object.keys(fields).length && !global.length) return null;
291
+ return finalizeFormErrors(fields, global);
292
+ }
293
+
294
+ function mergeFieldMessage(
295
+ fields: Record<string, string | string[]>,
296
+ key: string,
297
+ msg: string,
298
+ ) {
299
+ const existing = fields[key];
300
+ if (!existing) {
301
+ fields[key] = msg;
302
+ return;
303
+ }
304
+ fields[key] = Array.isArray(existing) ? [...existing, msg] : [existing, msg];
305
+ }
306
+
307
+ function camelCaseField(key: string): string {
308
+ if (!key) return key;
309
+ return key.charAt(0).toLowerCase() + key.slice(1);
310
+ }
311
+
@@ -29,7 +29,7 @@ export type ResourceHandlers<
29
29
  > = {
30
30
  getList: (params: GetListParams) => Promise<GetListResult<RecordType>>;
31
31
  getOne: (id: Identifier, params?: GetOneParams) => Promise<GetOneResult<RecordType>>;
32
- create: (data: Partial<RecordType>) => Promise<CreateResult<RecordType>>;
32
+ create: (data: Partial<RecordType> | FormData) => Promise<CreateResult<RecordType>>;
33
33
  update: (
34
34
  params: UpdateParams<RecordType>,
35
35
  ) => Promise<UpdateResult<RecordType>>;
package/src/index.ts CHANGED
@@ -37,6 +37,7 @@ export type {
37
37
  UpdateParams,
38
38
  UpdateResult,
39
39
  FormMutation,
40
+ FormMutationBody,
40
41
  FormValidationErrors,
41
42
  ParseFormError,
42
43
  ParseFormErrorContext,
@@ -48,7 +49,8 @@ export {
48
49
  parseDotNetFormErrors,
49
50
  parseNodeFormErrors,
50
51
  getErrorBody,
51
- applyInlineFieldPaths,
52
+ resolveErrorBody,
53
+ flattenNestedArrayErrors,
52
54
  asStringMessages,
53
55
  finalizeFormErrors,
54
56
  } from "./data/parseFormErrorHelpers";
@@ -125,8 +127,6 @@ export {
125
127
  FormTab,
126
128
  FormSteps,
127
129
  FormStep,
128
- saveInlineRows,
129
- loadInlineRows,
130
130
  TextField,
131
131
  NumberField,
132
132
  BooleanField,
@@ -135,6 +135,8 @@ export {
135
135
  PasswordField,
136
136
  ReferenceField,
137
137
  ReferenceManyField,
138
+ ImageField,
139
+ FileField,
138
140
  FieldWrapper,
139
141
  TextColumn,
140
142
  NumberColumn,
@@ -142,6 +144,7 @@ export {
142
144
  DateColumn,
143
145
  ReferenceColumn,
144
146
  ReferenceManyColumn,
147
+ ImageColumn,
145
148
  CustomColumn,
146
149
  TextFilter,
147
150
  NumberFilter,
@@ -153,17 +156,21 @@ export {
153
156
  useListQueryState,
154
157
  useChoices,
155
158
  useAbortableEffect,
156
- getByPath,
157
- pickBySources,
158
- inlineArrayName,
159
- inlineFieldName,
159
+ getFormValue,
160
+ setFormValue,
161
+ buildFormPayload,
162
+ buildInlineRowsPayload,
163
+ buildResourceFormSubmitBody,
164
+ prepareFormSubmitBody,
165
+ toFormData,
166
+ hasUploadValues,
167
+ nestedFieldPath,
160
168
  useResourceListContext,
161
- useSubmitField,
162
- useSectionField,
169
+ useRegisterPayloadField,
170
+ useRegisterSectionField,
163
171
  } from "./crud";
164
172
  export type {
165
173
  ResourceFormProps,
166
- ResourceFormInlineConfig,
167
174
  ResourceFormModalProps,
168
175
  InlineFormSetProps,
169
176
  InlineFormSetStackedProps,
@@ -171,7 +178,6 @@ export type {
171
178
  FormTabProps,
172
179
  FormStepsProps,
173
180
  FormStepProps,
174
- SaveInlineOptions,
175
181
  InlineFormSetLayout,
176
182
  ResourceListProps,
177
183
  BaseSourceProps,
@@ -185,7 +191,13 @@ export type {
185
191
  InlineColumnDef,
186
192
  InlineFormSetBaseProps,
187
193
  InlineRowContext,
194
+ InlineFieldRegistration,
188
195
  FieldWrapperProps,
196
+ ImageFieldProps,
197
+ FileFieldProps,
198
+ UploadFieldValue,
199
+ ToFormDataOptions,
200
+ PrepareFormSubmitBodyOptions,
189
201
  ResourceListBuiltInActions,
190
202
  ResourceListBulkAction,
191
203
  ResourceListBulkActionHelpers,
@@ -321,7 +321,7 @@ export function AdminLayout({
321
321
  />
322
322
  </Drawer>
323
323
  )}
324
- <Layout>
324
+ <Layout style={{ minWidth: 0 }}>
325
325
  <Layout.Header
326
326
  style={{
327
327
  background: token.colorBgContainer,
@@ -371,6 +371,7 @@ export function AdminLayout({
371
371
  <Layout.Content
372
372
  style={{
373
373
  margin: isMobile ? token.marginSM : token.marginLG,
374
+ minWidth: 0,
374
375
  }}
375
376
  >
376
377
  <Outlet />