ding-react-admin 2.0.0 → 2.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/LICENSE +21 -21
- package/README.md +254 -195
- package/dist/index.css +2 -0
- package/dist/index.js +1772 -1169
- package/dist/src/components/NavMenuLabel.d.ts +8 -0
- package/dist/src/components/NavMenuLabel.d.ts.map +1 -0
- package/dist/src/components/NavMenuSearch.d.ts +9 -0
- package/dist/src/components/NavMenuSearch.d.ts.map +1 -0
- package/dist/src/components/ScrollableArea.d.ts +10 -0
- package/dist/src/components/ScrollableArea.d.ts.map +1 -0
- package/dist/src/components/index.d.ts +3 -0
- package/dist/src/components/index.d.ts.map +1 -1
- package/dist/src/context/AuthProvider.d.ts +2 -0
- package/dist/src/context/AuthProvider.d.ts.map +1 -1
- package/dist/src/crud/FormGlobalErrorsAlert.d.ts +5 -0
- package/dist/src/crud/FormGlobalErrorsAlert.d.ts.map +1 -0
- package/dist/src/crud/ResourceForm.d.ts +1 -4
- package/dist/src/crud/ResourceForm.d.ts.map +1 -1
- package/dist/src/crud/ResourceFormModal.d.ts +7 -1
- package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
- package/dist/src/crud/ResourceRecordForm.d.ts +27 -0
- package/dist/src/crud/ResourceRecordForm.d.ts.map +1 -0
- package/dist/src/crud/fields/ReferenceField.d.ts +12 -2
- package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
- package/dist/src/crud/fields/ReferenceInputActions.d.ts +16 -0
- package/dist/src/crud/fields/ReferenceInputActions.d.ts.map +1 -0
- package/dist/src/crud/fields/ReferenceManyField.d.ts +10 -1
- package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
- package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
- package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
- package/dist/src/crud/index.d.ts +5 -0
- package/dist/src/crud/index.d.ts.map +1 -1
- package/dist/src/crud/types.d.ts +12 -0
- package/dist/src/crud/types.d.ts.map +1 -1
- package/dist/src/crud/utils/choiceSelectionUtils.d.ts +10 -0
- package/dist/src/crud/utils/choiceSelectionUtils.d.ts.map +1 -0
- package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts +2 -0
- package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts.map +1 -0
- package/dist/src/crud/utils/formErrors.d.ts +18 -7
- package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
- package/dist/src/crud/utils/formErrors.test.d.ts +2 -0
- package/dist/src/crud/utils/formErrors.test.d.ts.map +1 -0
- package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +1 -0
- package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -1
- package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts +4 -0
- package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts.map +1 -0
- package/dist/src/crud/utils/toFormData.d.ts +1 -1
- package/dist/src/crud/utils/toFormData.d.ts.map +1 -1
- package/dist/src/crud/utils/uploadReferenceUtils.d.ts +5 -0
- package/dist/src/crud/utils/uploadReferenceUtils.d.ts.map +1 -0
- package/dist/src/crud/utils/useChoices.d.ts +27 -1
- package/dist/src/crud/utils/useChoices.d.ts.map +1 -1
- package/dist/src/crud/utils/useFormRecord.d.ts +9 -7
- package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -1
- package/dist/src/data/parseFormErrorHelpers.d.ts +6 -27
- package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/layouts/AdminLayout.d.ts +1 -1
- package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
- package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -1
- package/dist/src/layouts/navFilter.d.ts +5 -0
- package/dist/src/layouts/navFilter.d.ts.map +1 -0
- package/dist/src/layouts/navMenuItems.d.ts +6 -0
- package/dist/src/layouts/navMenuItems.d.ts.map +1 -0
- package/dist/src/types.d.ts +12 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +79 -59
- package/src/app/AdminApp.tsx +34 -34
- package/src/components/AuthAlternateLink.tsx +23 -23
- package/src/components/DensitySwitch.tsx +31 -31
- package/src/components/NavMenuLabel.tsx +26 -0
- package/src/components/NavMenuSearch.tsx +76 -0
- package/src/components/ScrollableArea.tsx +46 -0
- package/src/components/ThemeSwitch.tsx +28 -28
- package/src/components/ThemeToolbar.tsx +34 -34
- package/src/components/index.ts +7 -4
- package/src/components/navMenu.css +21 -0
- package/src/components/scrollable.css +28 -0
- package/src/context/AppThemeProvider.tsx +128 -128
- package/src/context/AuthProvider.tsx +89 -77
- package/src/context/DataProvider.tsx +30 -30
- package/src/context/PermissionsProvider.tsx +57 -57
- package/src/crud/FilterBar.tsx +36 -36
- package/src/crud/FormGlobalErrorsAlert.tsx +36 -0
- package/src/crud/FormSteps.tsx +135 -135
- package/src/crud/FormTabs.tsx +126 -126
- package/src/crud/InlineFormSet.tsx +162 -162
- package/src/crud/ListActionsBar.tsx +106 -106
- package/src/crud/ResourceForm.tsx +83 -160
- package/src/crud/ResourceFormModal.tsx +75 -133
- package/src/crud/ResourceRecordForm.tsx +166 -0
- package/src/crud/columns/BooleanColumn.tsx +32 -32
- package/src/crud/columns/CustomColumn.tsx +33 -33
- package/src/crud/columns/DateColumn.tsx +28 -28
- package/src/crud/columns/ImageColumn.tsx +58 -58
- package/src/crud/columns/NumberColumn.tsx +31 -31
- package/src/crud/columns/ReferenceColumn.tsx +85 -85
- package/src/crud/columns/ReferenceManyColumn.tsx +67 -67
- package/src/crud/columns/TextColumn.tsx +38 -38
- package/src/crud/context/FilterContext.tsx +71 -71
- package/src/crud/context/FormContext.tsx +39 -39
- package/src/crud/context/FormSectionContext.tsx +22 -22
- package/src/crud/context/InlineFieldsRegistry.tsx +61 -61
- package/src/crud/context/ListContext.tsx +93 -93
- package/src/crud/fields/BooleanField.tsx +38 -38
- package/src/crud/fields/DateField.tsx +53 -53
- package/src/crud/fields/FileField.tsx +123 -123
- package/src/crud/fields/ImageField.tsx +120 -120
- package/src/crud/fields/NumberField.tsx +52 -52
- package/src/crud/fields/PasswordField.tsx +99 -99
- package/src/crud/fields/ReferenceField.tsx +237 -96
- package/src/crud/fields/ReferenceInputActions.tsx +106 -0
- package/src/crud/fields/ReferenceManyField.tsx +200 -76
- package/src/crud/fields/SelectField.tsx +48 -48
- package/src/crud/fields/TextField.tsx +46 -46
- package/src/crud/fields/uploadFieldUtils.ts +21 -21
- package/src/crud/fields/useUploadPreviewUrl.ts +20 -20
- package/src/crud/filters/BooleanFilter.tsx +38 -38
- package/src/crud/filters/DateFilter.tsx +35 -35
- package/src/crud/filters/NumberFilter.tsx +33 -33
- package/src/crud/filters/ReferenceFilter.tsx +192 -173
- package/src/crud/filters/SelectFilter.tsx +44 -44
- package/src/crud/filters/TextFilter.tsx +36 -36
- package/src/crud/index.ts +99 -94
- package/src/crud/types.ts +146 -134
- package/src/crud/utils/buildFormPayload.ts +24 -24
- package/src/crud/utils/buildInlineRowsPayload.ts +37 -37
- package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -29
- package/src/crud/utils/choiceSelectionUtils.test.ts +45 -0
- package/src/crud/utils/choiceSelectionUtils.ts +116 -0
- package/src/crud/utils/formErrors.test.ts +168 -0
- package/src/crud/utils/formErrors.ts +138 -42
- package/src/crud/utils/formSectionErrors.ts +85 -85
- package/src/crud/utils/getFormValue.ts +13 -13
- package/src/crud/utils/hasUploadValues.ts +9 -9
- package/src/crud/utils/nestedFieldPath.ts +8 -8
- package/src/crud/utils/prepareFormSubmitBody.ts +22 -17
- package/src/crud/utils/referenceSelectNotFoundContent.tsx +7 -0
- package/src/crud/utils/setFormValue.ts +18 -18
- package/src/crud/utils/sortQueryParam.ts +37 -37
- package/src/crud/utils/toFormData.ts +79 -81
- package/src/crud/utils/uploadReferenceUtils.ts +44 -0
- package/src/crud/utils/useAbortableEffect.ts +17 -17
- package/src/crud/utils/useChoices.ts +330 -182
- package/src/crud/utils/useFormRecord.ts +179 -156
- package/src/crud/utils/useListQueryState.ts +191 -191
- package/src/data/abortError.test.ts +19 -19
- package/src/data/abortError.ts +10 -10
- package/src/data/createMemoryResourceHandlers.ts +78 -78
- package/src/data/createRestResourceHandlers.ts +63 -63
- package/src/data/dataUtils.test.ts +228 -220
- package/src/data/inMemoryList.ts +97 -97
- package/src/data/parseFormErrorHelpers.test.ts +201 -138
- package/src/data/parseFormErrorHelpers.ts +376 -311
- package/src/data/sortHelpers.ts +48 -48
- package/src/index.ts +211 -207
- package/src/layouts/AdminLayout.tsx +530 -382
- package/src/layouts/AuthPageLayout.tsx +87 -81
- package/src/layouts/navFilter.ts +49 -0
- package/src/layouts/navMenuItems.tsx +44 -0
- package/src/pages/LoginPage.tsx +80 -80
- package/src/pages/PlaceholderPage.tsx +9 -9
- package/src/permissions/resourcePermissions.ts +47 -47
- package/src/router/createAdminRouter.tsx +64 -64
- package/src/router/guards.tsx +63 -63
- package/src/router/routeAccess.ts +89 -89
- package/src/types.ts +10 -0
- package/src/vite-env.d.ts +1 -1
|
@@ -1,311 +1,376 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
FormValidationErrors,
|
|
3
|
-
ParseFormErrorContext,
|
|
4
|
-
} from "./dataProviderTypes";
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return [];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
if (!
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
if (!
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
export
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
1
|
+
import type {
|
|
2
|
+
FormValidationErrors,
|
|
3
|
+
ParseFormErrorContext,
|
|
4
|
+
} from "./dataProviderTypes";
|
|
5
|
+
|
|
6
|
+
export const EXPECTED_VALIDATION_BODY_HINT =
|
|
7
|
+
'Expected HTTP 400 with a JSON object such as `{ "field_name": ["message"] }` or `{ "non_field_errors": ["message"] }`.';
|
|
8
|
+
|
|
9
|
+
const RECEIVED_BODY_MAX_LENGTH = 300;
|
|
10
|
+
|
|
11
|
+
export function asStringMessages(value: unknown): string[] {
|
|
12
|
+
if (typeof value === "string") return [value];
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
const strings = value.filter((x) => typeof x === "string") as string[];
|
|
15
|
+
if (strings.length) return strings;
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toFieldValue(msgs: string[]): string | string[] {
|
|
21
|
+
return msgs.length === 1 ? msgs[0] : msgs;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isJsonRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isFetchResponse(value: unknown): value is Response {
|
|
29
|
+
if (typeof Response !== "undefined" && value instanceof Response) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
value !== null &&
|
|
34
|
+
typeof value === "object" &&
|
|
35
|
+
typeof (value as Response).json === "function" &&
|
|
36
|
+
typeof (value as Response).status === "number" &&
|
|
37
|
+
(value as Response).headers != null
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatReceivedValidationBody(
|
|
42
|
+
body: Record<string, unknown> | null,
|
|
43
|
+
hint?: string,
|
|
44
|
+
): string {
|
|
45
|
+
if (hint) return hint;
|
|
46
|
+
if (body === null) return "(no JSON body)";
|
|
47
|
+
try {
|
|
48
|
+
const text = JSON.stringify(body);
|
|
49
|
+
if (text.length > RECEIVED_BODY_MAX_LENGTH) {
|
|
50
|
+
return `${text.slice(0, RECEIVED_BODY_MAX_LENGTH)}…`;
|
|
51
|
+
}
|
|
52
|
+
return text;
|
|
53
|
+
} catch {
|
|
54
|
+
return String(body);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function describeNonStandardValidationBody(
|
|
59
|
+
body: Record<string, unknown> | null,
|
|
60
|
+
options?: { hint?: string },
|
|
61
|
+
): string {
|
|
62
|
+
const received = formatReceivedValidationBody(body, options?.hint);
|
|
63
|
+
return `Non-standard validation response. ${EXPECTED_VALIDATION_BODY_HINT} Received: ${received}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getValidationErrorStatus(error: unknown): number | null {
|
|
67
|
+
if (!error || typeof error !== "object") return null;
|
|
68
|
+
|
|
69
|
+
const response = (error as Record<string, unknown>).response;
|
|
70
|
+
if (!response || typeof response !== "object") return null;
|
|
71
|
+
|
|
72
|
+
const status = (response as { status?: number }).status;
|
|
73
|
+
if (typeof status === "number" && (status === 400 || status === 422)) {
|
|
74
|
+
return status;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getErrorResponseContentType(error: unknown): string | null {
|
|
81
|
+
if (!error || typeof error !== "object") return null;
|
|
82
|
+
|
|
83
|
+
const response = (error as Record<string, unknown>).response;
|
|
84
|
+
if (!isFetchResponse(response)) return null;
|
|
85
|
+
|
|
86
|
+
return response.headers.get("content-type");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read a parsed JSON validation body when it is already available on the error.
|
|
91
|
+
*
|
|
92
|
+
* Supported transport shapes (sync only):
|
|
93
|
+
* - `{ body: { email: ["…"] } }` — explicit throw from a handler
|
|
94
|
+
* - `{ response: { data: { email: ["…"] } } }` — axios-style wrappers
|
|
95
|
+
* - `{ data: { email: ["…"] } }` — some HTTP clients attach parsed JSON here
|
|
96
|
+
*
|
|
97
|
+
* For fetch / OpenAPI clients that only expose a `Response`, use `resolveErrorBody`.
|
|
98
|
+
*/
|
|
99
|
+
export function getErrorBody(error: unknown): Record<string, unknown> | null {
|
|
100
|
+
if (!error || typeof error !== "object") return null;
|
|
101
|
+
|
|
102
|
+
const e = error as Record<string, unknown>;
|
|
103
|
+
|
|
104
|
+
if (isJsonRecord(e.body)) {
|
|
105
|
+
return e.body;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (isJsonRecord(e.data)) {
|
|
109
|
+
return e.data;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const response = e.response;
|
|
113
|
+
if (response && typeof response === "object" && !Array.isArray(response)) {
|
|
114
|
+
const data = (response as Record<string, unknown>).data;
|
|
115
|
+
if (isJsonRecord(data)) {
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolve a validation JSON body from any common HTTP client error shape.
|
|
125
|
+
* Falls back to reading `response.json()` for fetch / OpenAPI `ResponseError`.
|
|
126
|
+
*/
|
|
127
|
+
function normalizeValidationBody(body: unknown): Record<string, unknown> | null {
|
|
128
|
+
if (isJsonRecord(body)) {
|
|
129
|
+
return body;
|
|
130
|
+
}
|
|
131
|
+
if (Array.isArray(body)) {
|
|
132
|
+
const messages = asStringMessages(body);
|
|
133
|
+
return messages.length ? { non_field_errors: toFieldValue(messages) } : null;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function resolveErrorBody(
|
|
139
|
+
error: unknown,
|
|
140
|
+
): Promise<Record<string, unknown> | null> {
|
|
141
|
+
const syncBody = getErrorBody(error);
|
|
142
|
+
if (syncBody) return syncBody;
|
|
143
|
+
|
|
144
|
+
if (!error || typeof error !== "object") return null;
|
|
145
|
+
|
|
146
|
+
const response = (error as Record<string, unknown>).response;
|
|
147
|
+
if (!isFetchResponse(response)) return null;
|
|
148
|
+
|
|
149
|
+
const contentType = response.headers.get("content-type");
|
|
150
|
+
if (!contentType || !/application\/json/i.test(contentType)) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const body: unknown = await response.clone().json();
|
|
156
|
+
return normalizeValidationBody(body);
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isNestedRowErrorsArray(value: unknown): boolean {
|
|
163
|
+
if (!Array.isArray(value)) return false;
|
|
164
|
+
return value.some(
|
|
165
|
+
(item) =>
|
|
166
|
+
item &&
|
|
167
|
+
typeof item === "object" &&
|
|
168
|
+
!Array.isArray(item) &&
|
|
169
|
+
Object.values(item as Record<string, unknown>).some(
|
|
170
|
+
(v) => asStringMessages(v).length > 0,
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Map `{ lines: [{ label: ["…"] }] }` → `{ "lines.0.label": "…" }`. */
|
|
176
|
+
export function flattenNestedArrayErrors(
|
|
177
|
+
arrayKey: string,
|
|
178
|
+
rows: unknown[],
|
|
179
|
+
fields: Record<string, string | string[]>,
|
|
180
|
+
): void {
|
|
181
|
+
rows.forEach((row, index) => {
|
|
182
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return;
|
|
183
|
+
for (const [source, value] of Object.entries(
|
|
184
|
+
row as Record<string, unknown>,
|
|
185
|
+
)) {
|
|
186
|
+
const msgs = asStringMessages(value);
|
|
187
|
+
if (msgs.length) {
|
|
188
|
+
fields[`${arrayKey}.${index}.${source}`] = toFieldValue(msgs);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function finalizeFormErrors(
|
|
195
|
+
fields: Record<string, string | string[]>,
|
|
196
|
+
global: string[],
|
|
197
|
+
): FormValidationErrors {
|
|
198
|
+
return {
|
|
199
|
+
fields: Object.keys(fields).length ? fields : undefined,
|
|
200
|
+
global: global.length ? global : undefined,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const DRF_GLOBAL_KEYS = new Set(["non_field_errors", "detail"]);
|
|
205
|
+
|
|
206
|
+
function parseDjangoDRFValidationBody(
|
|
207
|
+
body: Record<string, unknown>,
|
|
208
|
+
): FormValidationErrors | null {
|
|
209
|
+
const fields: Record<string, string | string[]> = {};
|
|
210
|
+
const global: string[] = [];
|
|
211
|
+
|
|
212
|
+
for (const [key, value] of Object.entries(body)) {
|
|
213
|
+
if (DRF_GLOBAL_KEYS.has(key)) {
|
|
214
|
+
global.push(...asStringMessages(value));
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (isNestedRowErrorsArray(value)) {
|
|
218
|
+
flattenNestedArrayErrors(key, value as unknown[], fields);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const msgs = asStringMessages(value);
|
|
222
|
+
if (msgs.length) fields[key] = toFieldValue(msgs);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!Object.keys(fields).length && !global.length) return null;
|
|
226
|
+
return finalizeFormErrors(fields, global);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Django REST framework validation body:
|
|
231
|
+
* `{ "email": ["Invalid"], "non_field_errors": ["…"] }`
|
|
232
|
+
*/
|
|
233
|
+
export function parseDjangoDRFFormErrors(
|
|
234
|
+
error: unknown,
|
|
235
|
+
_ctx: ParseFormErrorContext,
|
|
236
|
+
): FormValidationErrors | null {
|
|
237
|
+
const body = getErrorBody(error);
|
|
238
|
+
if (!body) return null;
|
|
239
|
+
return parseDjangoDRFValidationBody(body);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export type DotNetFormErrorOptions = {
|
|
243
|
+
/** Map API property names to form `source` paths. */
|
|
244
|
+
fieldMap?: Record<string, string>;
|
|
245
|
+
/** `Email` → `email`. Default true. */
|
|
246
|
+
camelCase?: boolean;
|
|
247
|
+
/** Include `title` / `message` in global toasts. Default false (often generic). */
|
|
248
|
+
includeSummary?: boolean;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* ASP.NET Core `ValidationProblemDetails`:
|
|
253
|
+
* `{ "errors": { "Email": ["The Email field is required."] } }`
|
|
254
|
+
*/
|
|
255
|
+
export function parseDotNetFormErrors(
|
|
256
|
+
error: unknown,
|
|
257
|
+
_ctx: ParseFormErrorContext,
|
|
258
|
+
options?: DotNetFormErrorOptions,
|
|
259
|
+
): FormValidationErrors | null {
|
|
260
|
+
const body = getErrorBody(error);
|
|
261
|
+
if (!body) return null;
|
|
262
|
+
|
|
263
|
+
const camelCase = options?.camelCase ?? true;
|
|
264
|
+
const fieldMap = options?.fieldMap;
|
|
265
|
+
const fields: Record<string, string | string[]> = {};
|
|
266
|
+
const global: string[] = [];
|
|
267
|
+
|
|
268
|
+
if (options?.includeSummary) {
|
|
269
|
+
global.push(...asStringMessages(body.title));
|
|
270
|
+
global.push(...asStringMessages(body.message));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const errors = body.errors;
|
|
274
|
+
if (errors && typeof errors === "object" && !Array.isArray(errors)) {
|
|
275
|
+
for (const [key, value] of Object.entries(errors as Record<string, unknown>)) {
|
|
276
|
+
const mappedKey =
|
|
277
|
+
fieldMap?.[key] ?? (camelCase ? camelCaseField(key) : key);
|
|
278
|
+
const msgs = asStringMessages(value);
|
|
279
|
+
if (msgs.length) fields[mappedKey] = toFieldValue(msgs);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!Object.keys(fields).length && !global.length) return null;
|
|
284
|
+
return finalizeFormErrors(fields, global);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export type NodeFormErrorOptions = {
|
|
288
|
+
fieldMap?: Record<string, string>;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Common Node / Express shapes:
|
|
293
|
+
* - `{ errors: { email: ["Invalid"] } }`
|
|
294
|
+
* - `{ errors: [{ path: "email", msg: "Invalid" }] }` (express-validator)
|
|
295
|
+
* - `{ details: [{ message: "…", path: ["email"] }] }` (Joi)
|
|
296
|
+
*/
|
|
297
|
+
export function parseNodeFormErrors(
|
|
298
|
+
error: unknown,
|
|
299
|
+
_ctx: ParseFormErrorContext,
|
|
300
|
+
options?: NodeFormErrorOptions,
|
|
301
|
+
): FormValidationErrors | null {
|
|
302
|
+
const body = getErrorBody(error);
|
|
303
|
+
if (!body) return null;
|
|
304
|
+
|
|
305
|
+
const fields: Record<string, string | string[]> = {};
|
|
306
|
+
const global: string[] = [];
|
|
307
|
+
const fieldMap = options?.fieldMap;
|
|
308
|
+
|
|
309
|
+
const errList = body.errors;
|
|
310
|
+
if (Array.isArray(errList)) {
|
|
311
|
+
for (const item of errList) {
|
|
312
|
+
if (!item || typeof item !== "object") continue;
|
|
313
|
+
const row = item as Record<string, unknown>;
|
|
314
|
+
const path =
|
|
315
|
+
(typeof row.path === "string" && row.path) ||
|
|
316
|
+
(typeof row.param === "string" && row.param) ||
|
|
317
|
+
(typeof row.field === "string" && row.field);
|
|
318
|
+
const msg =
|
|
319
|
+
asStringMessages(row.msg)[0] ?? asStringMessages(row.message)[0];
|
|
320
|
+
if (!msg) continue;
|
|
321
|
+
if (path) {
|
|
322
|
+
const key = fieldMap?.[path] ?? path;
|
|
323
|
+
mergeFieldMessage(fields, key, msg);
|
|
324
|
+
} else {
|
|
325
|
+
global.push(msg);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} else if (errList && typeof errList === "object") {
|
|
329
|
+
for (const [key, value] of Object.entries(errList as Record<string, unknown>)) {
|
|
330
|
+
const mappedKey = fieldMap?.[key] ?? key;
|
|
331
|
+
const msgs = asStringMessages(value);
|
|
332
|
+
if (msgs.length) fields[mappedKey] = toFieldValue(msgs);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const details = body.details;
|
|
337
|
+
if (Array.isArray(details)) {
|
|
338
|
+
for (const item of details) {
|
|
339
|
+
if (!item || typeof item !== "object") continue;
|
|
340
|
+
const row = item as Record<string, unknown>;
|
|
341
|
+
const pathParts = Array.isArray(row.path) ? row.path : [];
|
|
342
|
+
const path = pathParts.map((p) => String(p)).join(".");
|
|
343
|
+
const msg = asStringMessages(row.message)[0];
|
|
344
|
+
if (!msg) continue;
|
|
345
|
+
if (path) {
|
|
346
|
+
const key = fieldMap?.[path] ?? path;
|
|
347
|
+
fields[key] = msg;
|
|
348
|
+
} else {
|
|
349
|
+
global.push(msg);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
global.push(...asStringMessages(body.error));
|
|
355
|
+
|
|
356
|
+
if (!Object.keys(fields).length && !global.length) return null;
|
|
357
|
+
return finalizeFormErrors(fields, global);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function mergeFieldMessage(
|
|
361
|
+
fields: Record<string, string | string[]>,
|
|
362
|
+
key: string,
|
|
363
|
+
msg: string,
|
|
364
|
+
) {
|
|
365
|
+
const existing = fields[key];
|
|
366
|
+
if (!existing) {
|
|
367
|
+
fields[key] = msg;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
fields[key] = Array.isArray(existing) ? [...existing, msg] : [existing, msg];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function camelCaseField(key: string): string {
|
|
374
|
+
if (!key) return key;
|
|
375
|
+
return key.charAt(0).toLowerCase() + key.slice(1);
|
|
376
|
+
}
|