ding-react-admin 1.0.4 → 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.
Files changed (230) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +254 -201
  3. package/dist/index.css +2 -0
  4. package/dist/index.js +2073 -1202
  5. package/dist/src/components/NavMenuLabel.d.ts +8 -0
  6. package/dist/src/components/NavMenuLabel.d.ts.map +1 -0
  7. package/dist/src/components/NavMenuSearch.d.ts +9 -0
  8. package/dist/src/components/NavMenuSearch.d.ts.map +1 -0
  9. package/dist/src/components/ScrollableArea.d.ts +10 -0
  10. package/dist/src/components/ScrollableArea.d.ts.map +1 -0
  11. package/dist/src/components/index.d.ts +3 -0
  12. package/dist/src/components/index.d.ts.map +1 -1
  13. package/dist/src/context/AuthProvider.d.ts +2 -0
  14. package/dist/src/context/AuthProvider.d.ts.map +1 -1
  15. package/dist/src/crud/FormGlobalErrorsAlert.d.ts +5 -0
  16. package/dist/src/crud/FormGlobalErrorsAlert.d.ts.map +1 -0
  17. package/dist/src/crud/InlineFormSet.d.ts +2 -17
  18. package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
  19. package/dist/src/crud/ResourceForm.d.ts +2 -10
  20. package/dist/src/crud/ResourceForm.d.ts.map +1 -1
  21. package/dist/src/crud/ResourceFormModal.d.ts +7 -1
  22. package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
  23. package/dist/src/crud/ResourceList.d.ts.map +1 -1
  24. package/dist/src/crud/ResourceRecordForm.d.ts +27 -0
  25. package/dist/src/crud/ResourceRecordForm.d.ts.map +1 -0
  26. package/dist/src/crud/columns/ImageColumn.d.ts +14 -0
  27. package/dist/src/crud/columns/ImageColumn.d.ts.map +1 -0
  28. package/dist/src/crud/context/InlineFieldsRegistry.d.ts +15 -0
  29. package/dist/src/crud/context/InlineFieldsRegistry.d.ts.map +1 -0
  30. package/dist/src/crud/context/PayloadFieldsContext.d.ts +18 -0
  31. package/dist/src/crud/context/PayloadFieldsContext.d.ts.map +1 -0
  32. package/dist/src/crud/fields/FileField.d.ts +12 -0
  33. package/dist/src/crud/fields/FileField.d.ts.map +1 -0
  34. package/dist/src/crud/fields/ImageField.d.ts +14 -0
  35. package/dist/src/crud/fields/ImageField.d.ts.map +1 -0
  36. package/dist/src/crud/fields/ReferenceField.d.ts +12 -2
  37. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
  38. package/dist/src/crud/fields/ReferenceInputActions.d.ts +16 -0
  39. package/dist/src/crud/fields/ReferenceInputActions.d.ts.map +1 -0
  40. package/dist/src/crud/fields/ReferenceManyField.d.ts +10 -1
  41. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
  42. package/dist/src/crud/fields/uploadFieldUtils.d.ts +5 -0
  43. package/dist/src/crud/fields/uploadFieldUtils.d.ts.map +1 -0
  44. package/dist/src/crud/fields/useUploadPreviewUrl.d.ts +4 -0
  45. package/dist/src/crud/fields/useUploadPreviewUrl.d.ts.map +1 -0
  46. package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
  47. package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
  48. package/dist/src/crud/index.d.ts +27 -9
  49. package/dist/src/crud/index.d.ts.map +1 -1
  50. package/dist/src/crud/types.d.ts +21 -7
  51. package/dist/src/crud/types.d.ts.map +1 -1
  52. package/dist/src/crud/utils/buildFormPayload.d.ts +6 -0
  53. package/dist/src/crud/utils/buildFormPayload.d.ts.map +1 -0
  54. package/dist/src/crud/utils/buildInlineRowsPayload.d.ts +6 -0
  55. package/dist/src/crud/utils/buildInlineRowsPayload.d.ts.map +1 -0
  56. package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts +5 -0
  57. package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts.map +1 -0
  58. package/dist/src/crud/utils/choiceSelectionUtils.d.ts +10 -0
  59. package/dist/src/crud/utils/choiceSelectionUtils.d.ts.map +1 -0
  60. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts +2 -0
  61. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts.map +1 -0
  62. package/dist/src/crud/utils/formErrors.d.ts +18 -7
  63. package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
  64. package/dist/src/crud/utils/formErrors.test.d.ts +2 -0
  65. package/dist/src/crud/utils/formErrors.test.d.ts.map +1 -0
  66. package/dist/src/crud/utils/getFormValue.d.ts +3 -0
  67. package/dist/src/crud/utils/getFormValue.d.ts.map +1 -0
  68. package/dist/src/crud/utils/hasUploadValues.d.ts +3 -0
  69. package/dist/src/crud/utils/hasUploadValues.d.ts.map +1 -0
  70. package/dist/src/crud/utils/nestedFieldPath.d.ts +3 -0
  71. package/dist/src/crud/utils/nestedFieldPath.d.ts.map +1 -0
  72. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +8 -0
  73. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -0
  74. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts +4 -0
  75. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts.map +1 -0
  76. package/dist/src/crud/utils/setFormValue.d.ts +3 -0
  77. package/dist/src/crud/utils/setFormValue.d.ts.map +1 -0
  78. package/dist/src/crud/utils/toFormData.d.ts +13 -0
  79. package/dist/src/crud/utils/toFormData.d.ts.map +1 -0
  80. package/dist/src/crud/utils/uploadReferenceUtils.d.ts +5 -0
  81. package/dist/src/crud/utils/uploadReferenceUtils.d.ts.map +1 -0
  82. package/dist/src/crud/utils/useChoices.d.ts +27 -1
  83. package/dist/src/crud/utils/useChoices.d.ts.map +1 -1
  84. package/dist/src/crud/utils/useFormRecord.d.ts +44 -0
  85. package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -0
  86. package/dist/src/data/createRestResourceHandlers.d.ts +5 -5
  87. package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -1
  88. package/dist/src/data/dataProviderTypes.d.ts +6 -5
  89. package/dist/src/data/dataProviderTypes.d.ts.map +1 -1
  90. package/dist/src/data/parseFormErrorHelpers.d.ts +23 -6
  91. package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
  92. package/dist/src/data/resourceHandlers.d.ts +1 -1
  93. package/dist/src/data/resourceHandlers.d.ts.map +1 -1
  94. package/dist/src/index.d.ts +4 -4
  95. package/dist/src/index.d.ts.map +1 -1
  96. package/dist/src/layouts/AdminLayout.d.ts +1 -1
  97. package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
  98. package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -1
  99. package/dist/src/layouts/navFilter.d.ts +5 -0
  100. package/dist/src/layouts/navFilter.d.ts.map +1 -0
  101. package/dist/src/layouts/navMenuItems.d.ts +6 -0
  102. package/dist/src/layouts/navMenuItems.d.ts.map +1 -0
  103. package/dist/src/types.d.ts +12 -0
  104. package/dist/src/types.d.ts.map +1 -1
  105. package/package.json +79 -59
  106. package/src/app/AdminApp.tsx +34 -34
  107. package/src/components/AuthAlternateLink.tsx +23 -23
  108. package/src/components/DensitySwitch.tsx +31 -31
  109. package/src/components/NavMenuLabel.tsx +26 -0
  110. package/src/components/NavMenuSearch.tsx +76 -0
  111. package/src/components/ScrollableArea.tsx +46 -0
  112. package/src/components/ThemeSwitch.tsx +28 -28
  113. package/src/components/ThemeToolbar.tsx +34 -34
  114. package/src/components/index.ts +7 -4
  115. package/src/components/navMenu.css +21 -0
  116. package/src/components/scrollable.css +28 -0
  117. package/src/context/AppThemeProvider.tsx +128 -128
  118. package/src/context/AuthProvider.tsx +89 -77
  119. package/src/context/DataProvider.tsx +30 -30
  120. package/src/context/PermissionsProvider.tsx +57 -57
  121. package/src/crud/FilterBar.tsx +36 -36
  122. package/src/crud/FormGlobalErrorsAlert.tsx +36 -0
  123. package/src/crud/FormSteps.tsx +135 -135
  124. package/src/crud/FormTabs.tsx +126 -126
  125. package/src/crud/InlineFormSet.tsx +162 -219
  126. package/src/crud/ListActionsBar.tsx +106 -106
  127. package/src/crud/ResourceForm.tsx +83 -166
  128. package/src/crud/ResourceFormModal.tsx +75 -133
  129. package/src/crud/ResourceList.tsx +1 -0
  130. package/src/crud/ResourceRecordForm.tsx +166 -0
  131. package/src/crud/columns/BooleanColumn.tsx +32 -32
  132. package/src/crud/columns/CustomColumn.tsx +33 -33
  133. package/src/crud/columns/DateColumn.tsx +28 -28
  134. package/src/crud/columns/ImageColumn.tsx +58 -0
  135. package/src/crud/columns/NumberColumn.tsx +31 -31
  136. package/src/crud/columns/ReferenceColumn.tsx +85 -85
  137. package/src/crud/columns/ReferenceManyColumn.tsx +67 -67
  138. package/src/crud/columns/TextColumn.tsx +38 -38
  139. package/src/crud/context/FilterContext.tsx +71 -71
  140. package/src/crud/context/FormContext.tsx +39 -39
  141. package/src/crud/context/FormSectionContext.tsx +22 -22
  142. package/src/crud/context/InlineFieldsRegistry.tsx +61 -0
  143. package/src/crud/context/ListContext.tsx +93 -93
  144. package/src/crud/context/PayloadFieldsContext.tsx +62 -0
  145. package/src/crud/fields/BooleanField.tsx +38 -38
  146. package/src/crud/fields/DateField.tsx +53 -53
  147. package/src/crud/fields/FieldWrapper.tsx +87 -87
  148. package/src/crud/fields/FileField.tsx +123 -0
  149. package/src/crud/fields/ImageField.tsx +120 -0
  150. package/src/crud/fields/NumberField.tsx +52 -52
  151. package/src/crud/fields/PasswordField.tsx +99 -99
  152. package/src/crud/fields/ReferenceField.tsx +237 -96
  153. package/src/crud/fields/ReferenceInputActions.tsx +106 -0
  154. package/src/crud/fields/ReferenceManyField.tsx +200 -76
  155. package/src/crud/fields/SelectField.tsx +48 -48
  156. package/src/crud/fields/TextField.tsx +46 -46
  157. package/src/crud/fields/uploadFieldUtils.ts +21 -0
  158. package/src/crud/fields/useUploadPreviewUrl.ts +20 -0
  159. package/src/crud/filters/BooleanFilter.tsx +38 -38
  160. package/src/crud/filters/DateFilter.tsx +35 -35
  161. package/src/crud/filters/NumberFilter.tsx +33 -33
  162. package/src/crud/filters/ReferenceFilter.tsx +192 -173
  163. package/src/crud/filters/SelectFilter.tsx +44 -44
  164. package/src/crud/filters/TextFilter.tsx +36 -36
  165. package/src/crud/index.ts +99 -90
  166. package/src/crud/types.ts +146 -132
  167. package/src/crud/utils/buildFormPayload.ts +24 -0
  168. package/src/crud/utils/buildInlineRowsPayload.ts +37 -0
  169. package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -0
  170. package/src/crud/utils/choiceSelectionUtils.test.ts +45 -0
  171. package/src/crud/utils/choiceSelectionUtils.ts +116 -0
  172. package/src/crud/utils/formErrors.test.ts +168 -0
  173. package/src/crud/utils/formErrors.ts +138 -38
  174. package/src/crud/utils/formSectionErrors.ts +85 -85
  175. package/src/crud/utils/{getByPath.ts → getFormValue.ts} +13 -13
  176. package/src/crud/utils/hasUploadValues.ts +9 -0
  177. package/src/crud/utils/nestedFieldPath.ts +8 -0
  178. package/src/crud/utils/prepareFormSubmitBody.ts +22 -0
  179. package/src/crud/utils/referenceSelectNotFoundContent.tsx +7 -0
  180. package/src/crud/utils/{setByPath.ts → setFormValue.ts} +18 -18
  181. package/src/crud/utils/sortQueryParam.ts +37 -37
  182. package/src/crud/utils/toFormData.ts +79 -0
  183. package/src/crud/utils/uploadReferenceUtils.ts +44 -0
  184. package/src/crud/utils/useAbortableEffect.ts +17 -17
  185. package/src/crud/utils/useChoices.ts +330 -182
  186. package/src/crud/utils/useFormRecord.ts +179 -0
  187. package/src/crud/utils/useListQueryState.ts +191 -191
  188. package/src/data/abortError.test.ts +19 -19
  189. package/src/data/abortError.ts +10 -10
  190. package/src/data/createMemoryResourceHandlers.ts +78 -78
  191. package/src/data/createRestResourceHandlers.ts +63 -57
  192. package/src/data/dataProviderTypes.ts +7 -5
  193. package/src/data/dataUtils.test.ts +228 -108
  194. package/src/data/inMemoryList.ts +97 -97
  195. package/src/data/parseFormErrorHelpers.test.ts +201 -84
  196. package/src/data/parseFormErrorHelpers.ts +376 -224
  197. package/src/data/resourceHandlers.ts +1 -1
  198. package/src/data/sortHelpers.ts +48 -48
  199. package/src/index.ts +211 -195
  200. package/src/layouts/AdminLayout.tsx +530 -381
  201. package/src/layouts/AuthPageLayout.tsx +87 -81
  202. package/src/layouts/navFilter.ts +49 -0
  203. package/src/layouts/navMenuItems.tsx +44 -0
  204. package/src/pages/LoginPage.tsx +80 -80
  205. package/src/pages/PlaceholderPage.tsx +9 -9
  206. package/src/permissions/resourcePermissions.ts +47 -47
  207. package/src/router/createAdminRouter.tsx +64 -64
  208. package/src/router/guards.tsx +63 -63
  209. package/src/router/routeAccess.ts +89 -89
  210. package/src/types.ts +10 -0
  211. package/src/vite-env.d.ts +1 -1
  212. package/dist/src/crud/context/SubmitFieldsContext.d.ts +0 -18
  213. package/dist/src/crud/context/SubmitFieldsContext.d.ts.map +0 -1
  214. package/dist/src/crud/utils/getByPath.d.ts +0 -3
  215. package/dist/src/crud/utils/getByPath.d.ts.map +0 -1
  216. package/dist/src/crud/utils/inlineArrayName.d.ts +0 -3
  217. package/dist/src/crud/utils/inlineArrayName.d.ts.map +0 -1
  218. package/dist/src/crud/utils/inlineFieldName.d.ts +0 -3
  219. package/dist/src/crud/utils/inlineFieldName.d.ts.map +0 -1
  220. package/dist/src/crud/utils/pickBySources.d.ts +0 -6
  221. package/dist/src/crud/utils/pickBySources.d.ts.map +0 -1
  222. package/dist/src/crud/utils/setByPath.d.ts +0 -3
  223. package/dist/src/crud/utils/setByPath.d.ts.map +0 -1
  224. package/dist/src/crud/utils/useResourceFormData.d.ts +0 -47
  225. package/dist/src/crud/utils/useResourceFormData.d.ts.map +0 -1
  226. package/src/crud/context/SubmitFieldsContext.tsx +0 -62
  227. package/src/crud/utils/inlineArrayName.ts +0 -4
  228. package/src/crud/utils/inlineFieldName.ts +0 -8
  229. package/src/crud/utils/pickBySources.ts +0 -24
  230. package/src/crud/utils/useResourceFormData.ts +0 -231
@@ -1,97 +1,97 @@
1
- import type { GetListParams, Identifier, SortSpec } from "./dataProviderTypes";
2
-
3
- export function rowIdMatches(
4
- rowId: string | number,
5
- id: Identifier,
6
- ): boolean {
7
- return rowId === id || String(rowId) === String(id);
8
- }
9
-
10
- export function getById<T extends { id: string | number }>(
11
- rows: T[],
12
- id: Identifier,
13
- ): T {
14
- const row = rows.find((r) => rowIdMatches(r.id, id));
15
- if (!row) throw new Error("Not found");
16
- return row;
17
- }
18
-
19
- function sortRows<T extends Record<string, unknown>>(
20
- rows: T[],
21
- specs: SortSpec[],
22
- ): T[] {
23
- if (specs.length === 0) return rows;
24
- const mul = (order: "ASC" | "DESC") => (order === "DESC" ? -1 : 1);
25
- return [...rows].sort((a, b) => {
26
- for (const { field, order } of specs) {
27
- const av = a[field];
28
- const bv = b[field];
29
- if (av === bv) continue;
30
- if (av == null) return -1 * mul(order);
31
- if (bv == null) return 1 * mul(order);
32
- if (typeof av === "number" && typeof bv === "number") {
33
- return av < bv ? -mul(order) : mul(order);
34
- }
35
- const cmp = String(av).localeCompare(String(bv));
36
- if (cmp !== 0) return cmp * mul(order);
37
- }
38
- return 0;
39
- });
40
- }
41
-
42
- function matchFilterValue(rowVal: unknown, filterVal: unknown): boolean {
43
- if (filterVal === undefined || filterVal === null || filterVal === "") {
44
- return true;
45
- }
46
- if (Array.isArray(filterVal)) {
47
- if (filterVal.length === 0) return true;
48
- if (Array.isArray(rowVal)) {
49
- return filterVal.some((fv) => rowVal.includes(fv));
50
- }
51
- return filterVal.includes(rowVal);
52
- }
53
- if (Array.isArray(rowVal)) {
54
- return rowVal.includes(filterVal);
55
- }
56
- if (typeof filterVal === "string" && typeof rowVal === "string") {
57
- return rowVal.toLowerCase().includes(filterVal.toLowerCase());
58
- }
59
- return rowVal === filterVal;
60
- }
61
-
62
- export function filterRows<T extends Record<string, unknown>>(
63
- rows: T[],
64
- filter?: Record<string, unknown>,
65
- ): T[] {
66
- if (!filter) return rows;
67
- return rows.filter((row) =>
68
- Object.entries(filter).every(([k, v]) => matchFilterValue(row[k], v)),
69
- );
70
- }
71
-
72
- function paginate<T>(rows: T[], page: number, perPage: number) {
73
- const start = (page - 1) * perPage;
74
- return {
75
- data: rows.slice(start, start + perPage),
76
- total: rows.length,
77
- };
78
- }
79
-
80
- /** Apply CRUD list filters, sort, and pagination to an in-memory row array. */
81
- export function applyInMemoryListParams<T extends Record<string, unknown>>(
82
- rows: T[],
83
- params: GetListParams,
84
- ): { data: T[]; total: number } {
85
- const { pagination, sort, filter } = params;
86
- let out = filterRows(rows, filter);
87
- if (sort) {
88
- const specs = Array.isArray(sort) ? sort : [sort];
89
- if (specs.length > 0 && specs[0]?.field) {
90
- out = sortRows(out, specs);
91
- }
92
- }
93
- if (pagination) {
94
- return paginate(out, pagination.page, pagination.perPage);
95
- }
96
- return { data: out, total: out.length };
97
- }
1
+ import type { GetListParams, Identifier, SortSpec } from "./dataProviderTypes";
2
+
3
+ export function rowIdMatches(
4
+ rowId: string | number,
5
+ id: Identifier,
6
+ ): boolean {
7
+ return rowId === id || String(rowId) === String(id);
8
+ }
9
+
10
+ export function getById<T extends { id: string | number }>(
11
+ rows: T[],
12
+ id: Identifier,
13
+ ): T {
14
+ const row = rows.find((r) => rowIdMatches(r.id, id));
15
+ if (!row) throw new Error("Not found");
16
+ return row;
17
+ }
18
+
19
+ function sortRows<T extends Record<string, unknown>>(
20
+ rows: T[],
21
+ specs: SortSpec[],
22
+ ): T[] {
23
+ if (specs.length === 0) return rows;
24
+ const mul = (order: "ASC" | "DESC") => (order === "DESC" ? -1 : 1);
25
+ return [...rows].sort((a, b) => {
26
+ for (const { field, order } of specs) {
27
+ const av = a[field];
28
+ const bv = b[field];
29
+ if (av === bv) continue;
30
+ if (av == null) return -1 * mul(order);
31
+ if (bv == null) return 1 * mul(order);
32
+ if (typeof av === "number" && typeof bv === "number") {
33
+ return av < bv ? -mul(order) : mul(order);
34
+ }
35
+ const cmp = String(av).localeCompare(String(bv));
36
+ if (cmp !== 0) return cmp * mul(order);
37
+ }
38
+ return 0;
39
+ });
40
+ }
41
+
42
+ function matchFilterValue(rowVal: unknown, filterVal: unknown): boolean {
43
+ if (filterVal === undefined || filterVal === null || filterVal === "") {
44
+ return true;
45
+ }
46
+ if (Array.isArray(filterVal)) {
47
+ if (filterVal.length === 0) return true;
48
+ if (Array.isArray(rowVal)) {
49
+ return filterVal.some((fv) => rowVal.includes(fv));
50
+ }
51
+ return filterVal.includes(rowVal);
52
+ }
53
+ if (Array.isArray(rowVal)) {
54
+ return rowVal.includes(filterVal);
55
+ }
56
+ if (typeof filterVal === "string" && typeof rowVal === "string") {
57
+ return rowVal.toLowerCase().includes(filterVal.toLowerCase());
58
+ }
59
+ return rowVal === filterVal;
60
+ }
61
+
62
+ export function filterRows<T extends Record<string, unknown>>(
63
+ rows: T[],
64
+ filter?: Record<string, unknown>,
65
+ ): T[] {
66
+ if (!filter) return rows;
67
+ return rows.filter((row) =>
68
+ Object.entries(filter).every(([k, v]) => matchFilterValue(row[k], v)),
69
+ );
70
+ }
71
+
72
+ function paginate<T>(rows: T[], page: number, perPage: number) {
73
+ const start = (page - 1) * perPage;
74
+ return {
75
+ data: rows.slice(start, start + perPage),
76
+ total: rows.length,
77
+ };
78
+ }
79
+
80
+ /** Apply CRUD list filters, sort, and pagination to an in-memory row array. */
81
+ export function applyInMemoryListParams<T extends Record<string, unknown>>(
82
+ rows: T[],
83
+ params: GetListParams,
84
+ ): { data: T[]; total: number } {
85
+ const { pagination, sort, filter } = params;
86
+ let out = filterRows(rows, filter);
87
+ if (sort) {
88
+ const specs = Array.isArray(sort) ? sort : [sort];
89
+ if (specs.length > 0 && specs[0]?.field) {
90
+ out = sortRows(out, specs);
91
+ }
92
+ }
93
+ if (pagination) {
94
+ return paginate(out, pagination.page, pagination.perPage);
95
+ }
96
+ return { data: out, total: out.length };
97
+ }
@@ -1,84 +1,201 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- parseDjangoDRFFormErrors,
4
- parseDotNetFormErrors,
5
- parseNodeFormErrors,
6
- } from "./parseFormErrorHelpers";
7
-
8
- const mainCtx = { resource: "users", mutation: "create" as const };
9
- const inlineCtx = {
10
- resource: "invoice-lines",
11
- mutation: "create" as const,
12
- inlineArrayName: "__inline_invoice_lines",
13
- rowIndex: 1,
14
- };
15
-
16
- describe("parseDjangoDRFFormErrors", () => {
17
- it("maps field and non_field_errors", () => {
18
- const parsed = parseDjangoDRFFormErrors(
19
- {
20
- body: {
21
- email: ["Enter a valid email."],
22
- non_field_errors: ["Cannot save"],
23
- },
24
- },
25
- mainCtx,
26
- );
27
- expect(parsed).toEqual({
28
- fields: { email: "Enter a valid email." },
29
- global: ["Cannot save"],
30
- });
31
- });
32
-
33
- it("prefixes inline row fields", () => {
34
- const parsed = parseDjangoDRFFormErrors(
35
- { body: { quantity: ["Must be positive"] } },
36
- inlineCtx,
37
- );
38
- expect(parsed?.fields).toEqual({
39
- "__inline_invoice_lines.1.quantity": "Must be positive",
40
- });
41
- });
42
- });
43
-
44
- describe("parseDotNetFormErrors", () => {
45
- it("maps errors object with camelCase", () => {
46
- const parsed = parseDotNetFormErrors(
47
- {
48
- body: {
49
- errors: { Email: ["The Email field is required."] },
50
- },
51
- },
52
- mainCtx,
53
- );
54
- expect(parsed?.fields).toEqual({
55
- email: "The Email field is required.",
56
- });
57
- });
58
- });
59
-
60
- describe("parseNodeFormErrors", () => {
61
- it("maps express-validator array", () => {
62
- const parsed = parseNodeFormErrors(
63
- {
64
- body: {
65
- errors: [{ path: "email", msg: "Invalid email" }],
66
- },
67
- },
68
- mainCtx,
69
- );
70
- expect(parsed?.fields).toEqual({ email: "Invalid email" });
71
- });
72
-
73
- it("maps Joi details", () => {
74
- const parsed = parseNodeFormErrors(
75
- {
76
- body: {
77
- details: [{ message: "Required", path: ["email"] }],
78
- },
79
- },
80
- mainCtx,
81
- );
82
- expect(parsed?.fields).toEqual({ email: "Required" });
83
- });
84
- });
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ describeNonStandardValidationBody,
5
+ parseDjangoDRFFormErrors,
6
+ parseDotNetFormErrors,
7
+ parseNodeFormErrors,
8
+ flattenNestedArrayErrors,
9
+ resolveErrorBody,
10
+ } from "./parseFormErrorHelpers";
11
+
12
+ const mainCtx = { resource: "users", mutation: "create" as const };
13
+
14
+ describe("parseDjangoDRFFormErrors", () => {
15
+ it("maps field and non_field_errors", () => {
16
+ const parsed = parseDjangoDRFFormErrors(
17
+ {
18
+ body: {
19
+ email: ["Enter a valid email."],
20
+ non_field_errors: ["Cannot save"],
21
+ },
22
+ },
23
+ mainCtx,
24
+ );
25
+
26
+ expect(parsed).toEqual({
27
+ fields: { email: "Enter a valid email." },
28
+ global: ["Cannot save"],
29
+ });
30
+ });
31
+
32
+ it("flattens nested inline row errors", () => {
33
+ const parsed = parseDjangoDRFFormErrors(
34
+ {
35
+ body: {
36
+ lines: [
37
+ { quantity: ["Must be positive"] },
38
+ {},
39
+ { label: ["Required"] },
40
+ ],
41
+ },
42
+ },
43
+ mainCtx,
44
+ );
45
+
46
+ expect(parsed?.fields).toEqual({
47
+ "lines.0.quantity": "Must be positive",
48
+ "lines.2.label": "Required",
49
+ });
50
+ });
51
+
52
+ it("returns null when no field messages can be extracted", () => {
53
+ const parsed = parseDjangoDRFFormErrors(
54
+ { body: { code: 123, nested: { foo: "bar" } } },
55
+ mainCtx,
56
+ );
57
+
58
+ expect(parsed).toBeNull();
59
+ });
60
+ });
61
+
62
+ describe("describeNonStandardValidationBody", () => {
63
+ it("includes expected shape and received JSON", () => {
64
+ const message = describeNonStandardValidationBody({ message: "Oops" });
65
+
66
+ expect(message).toContain("Non-standard validation response.");
67
+ expect(message).toContain('"field_name"');
68
+ expect(message).toContain('Received: {"message":"Oops"}');
69
+ });
70
+
71
+ it("truncates long received bodies", () => {
72
+ const longValue = "x".repeat(400);
73
+ const message = describeNonStandardValidationBody({ detail: longValue });
74
+
75
+ expect(message).toContain("…");
76
+ expect(message).not.toContain(longValue);
77
+ });
78
+
79
+ it("supports explicit hints for non-json responses", () => {
80
+ const message = describeNonStandardValidationBody(null, {
81
+ hint: "non-JSON response (Content-Type: text/plain)",
82
+ });
83
+
84
+ expect(message).toContain(
85
+ "Received: non-JSON response (Content-Type: text/plain)",
86
+ );
87
+ });
88
+ });
89
+
90
+ describe("resolveErrorBody", () => {
91
+ it("reads axios-style response.data", async () => {
92
+ const body = await resolveErrorBody({
93
+ response: { data: { email: ["Invalid"] } },
94
+ });
95
+
96
+ expect(body).toEqual({ email: ["Invalid"] });
97
+ });
98
+
99
+ it("reads fetch ResponseError response.json()", async () => {
100
+ const response = new Response(
101
+ JSON.stringify({ email: ["Enter a valid email address."] }),
102
+ {
103
+ status: 400,
104
+ headers: { "Content-Type": "application/json" },
105
+ },
106
+ );
107
+
108
+ const body = await resolveErrorBody({
109
+ name: "ResponseError",
110
+ message: "Response returned an error code",
111
+ response,
112
+ });
113
+
114
+ expect(body).toEqual({ email: ["Enter a valid email address."] });
115
+ });
116
+
117
+ it("normalizes top-level JSON array bodies", async () => {
118
+ const response = new Response(
119
+ JSON.stringify(["This code is already in use."]),
120
+ {
121
+ status: 400,
122
+ headers: { "Content-Type": "application/json" },
123
+ },
124
+ );
125
+
126
+ const body = await resolveErrorBody({ response });
127
+
128
+ expect(body).toEqual({ non_field_errors: "This code is already in use." });
129
+ });
130
+
131
+ it("returns null for non-json fetch responses", async () => {
132
+ const response = new Response("bad request", {
133
+ status: 400,
134
+ headers: { "Content-Type": "text/plain" },
135
+ });
136
+
137
+ const body = await resolveErrorBody({ response });
138
+
139
+ expect(body).toBeNull();
140
+ });
141
+ });
142
+
143
+ describe("flattenNestedArrayErrors", () => {
144
+ it("maps row errors to RHF paths", () => {
145
+ const fields: Record<string, string | string[]> = {};
146
+ flattenNestedArrayErrors("lines", [
147
+ { label: ["Required"] },
148
+ { quantity: ["Too small", "Invalid"] },
149
+ ], fields);
150
+
151
+ expect(fields).toEqual({
152
+ "lines.0.label": "Required",
153
+ "lines.1.quantity": ["Too small", "Invalid"],
154
+ });
155
+ });
156
+ });
157
+
158
+ describe("parseDotNetFormErrors", () => {
159
+ it("maps errors object with camelCase", () => {
160
+ const parsed = parseDotNetFormErrors(
161
+ {
162
+ body: {
163
+ errors: { Email: ["The Email field is required."] },
164
+ },
165
+ },
166
+ mainCtx,
167
+ );
168
+
169
+ expect(parsed?.fields).toEqual({
170
+ email: "The Email field is required.",
171
+ });
172
+ });
173
+ });
174
+
175
+ describe("parseNodeFormErrors", () => {
176
+ it("maps express-validator array", () => {
177
+ const parsed = parseNodeFormErrors(
178
+ {
179
+ body: {
180
+ errors: [{ path: "email", msg: "Invalid email" }],
181
+ },
182
+ },
183
+ mainCtx,
184
+ );
185
+
186
+ expect(parsed?.fields).toEqual({ email: "Invalid email" });
187
+ });
188
+
189
+ it("maps Joi details", () => {
190
+ const parsed = parseNodeFormErrors(
191
+ {
192
+ body: {
193
+ details: [{ message: "Required", path: ["email"] }],
194
+ },
195
+ },
196
+ mainCtx,
197
+ );
198
+
199
+ expect(parsed?.fields).toEqual({ email: "Required" });
200
+ });
201
+ });