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.
- package/README.md +4 -10
- package/dist/index.js +1071 -803
- package/dist/src/crud/InlineFormSet.d.ts +2 -17
- package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
- package/dist/src/crud/ResourceForm.d.ts +4 -9
- package/dist/src/crud/ResourceForm.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/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/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 +22 -9
- package/dist/src/crud/index.d.ts.map +1 -1
- package/dist/src/crud/types.d.ts +9 -7
- 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/useFormRecord.d.ts +42 -0
- package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -0
- package/dist/src/data/createRestResourceHandlers.d.ts +5 -5
- package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -1
- package/dist/src/data/dataProviderTypes.d.ts +6 -5
- 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 +1 -1
- package/dist/src/data/resourceHandlers.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/crud/InlineFormSet.tsx +28 -85
- package/src/crud/ResourceForm.tsx +60 -66
- package/src/crud/ResourceFormModal.tsx +25 -25
- package/src/crud/ResourceList.tsx +1 -0
- 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/FieldWrapper.tsx +87 -87
- package/src/crud/fields/FileField.tsx +123 -0
- package/src/crud/fields/ImageField.tsx +120 -0
- package/src/crud/fields/uploadFieldUtils.ts +21 -0
- package/src/crud/fields/useUploadPreviewUrl.ts +20 -0
- package/src/crud/index.ts +23 -19
- package/src/crud/types.ts +9 -7
- 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/useFormRecord.ts +156 -0
- package/src/data/createRestResourceHandlers.ts +19 -13
- package/src/data/dataProviderTypes.ts +7 -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 +1 -1
- package/src/index.ts +23 -11
- package/src/layouts/AdminLayout.tsx +2 -1
- package/dist/src/crud/context/SubmitFieldsContext.d.ts +0 -18
- package/dist/src/crud/context/SubmitFieldsContext.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/inlineFieldName.d.ts +0 -3
- package/dist/src/crud/utils/inlineFieldName.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/dist/src/crud/utils/useResourceFormData.d.ts +0 -47
- package/dist/src/crud/utils/useResourceFormData.d.ts.map +0 -1
- package/src/crud/context/SubmitFieldsContext.tsx +0 -62
- package/src/crud/utils/inlineArrayName.ts +0 -4
- package/src/crud/utils/inlineFieldName.ts +0 -8
- package/src/crud/utils/pickBySources.ts +0 -24
- 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
getFormValue,
|
|
160
|
+
setFormValue,
|
|
161
|
+
buildFormPayload,
|
|
162
|
+
buildInlineRowsPayload,
|
|
163
|
+
buildResourceFormSubmitBody,
|
|
164
|
+
prepareFormSubmitBody,
|
|
165
|
+
toFormData,
|
|
166
|
+
hasUploadValues,
|
|
167
|
+
nestedFieldPath,
|
|
160
168
|
useResourceListContext,
|
|
161
|
-
|
|
162
|
-
|
|
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 />
|