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
@@ -0,0 +1,22 @@
1
+ import { hasUploadValues } from "./hasUploadValues";
2
+ import { toFormData, type ToFormDataOptions } from "./toFormData";
3
+ import { stripExistingUploadReferencesFromPayload } from "./uploadReferenceUtils";
4
+
5
+ export type PrepareFormSubmitBodyOptions = ToFormDataOptions;
6
+
7
+ /**
8
+ * When the payload contains any `File`/`Blob`, return `FormData`; otherwise the plain object.
9
+ * Unchanged upload URL references are omitted so edit saves do not re-post existing files.
10
+ */
11
+ export function prepareFormSubmitBody(
12
+ payload: Record<string, unknown>,
13
+ options?: PrepareFormSubmitBodyOptions,
14
+ ): Record<string, unknown> | FormData {
15
+ const skipExistingUploadUrls = options?.skipExistingUploadUrls ?? true;
16
+
17
+ if (hasUploadValues(payload)) {
18
+ return toFormData(payload, options);
19
+ }
20
+
21
+ return stripExistingUploadReferencesFromPayload(payload, skipExistingUploadUrls);
22
+ }
@@ -0,0 +1,7 @@
1
+ import { Spin } from "antd";
2
+ import type { ReactNode } from "react";
3
+
4
+ /** Hide Ant Design Select "No data" while options are loading. */
5
+ export function referenceSelectNotFoundContent(loading: boolean): ReactNode {
6
+ return loading ? <Spin size="small" /> : undefined;
7
+ }
@@ -1,18 +1,18 @@
1
- /** Write a value into a record using dot notation, e.g. `invoiceLine.product`. */
2
- export function setByPath(
3
- record: Record<string, unknown>,
4
- path: string,
5
- value: unknown,
6
- ): void {
7
- const parts = path.split(".");
8
- let cur: Record<string, unknown> = record;
9
- for (let i = 0; i < parts.length - 1; i++) {
10
- const part = parts[i];
11
- const next = cur[part];
12
- if (next == null || typeof next !== "object" || Array.isArray(next)) {
13
- cur[part] = {};
14
- }
15
- cur = cur[part] as Record<string, unknown>;
16
- }
17
- cur[parts[parts.length - 1]!] = value;
18
- }
1
+ /** Write a value into an object using dot notation, e.g. `invoiceLine.product`. */
2
+ export function setFormValue(
3
+ record: Record<string, unknown>,
4
+ path: string,
5
+ value: unknown,
6
+ ): void {
7
+ const parts = path.split(".");
8
+ let cur: Record<string, unknown> = record;
9
+ for (let i = 0; i < parts.length - 1; i++) {
10
+ const part = parts[i];
11
+ const next = cur[part];
12
+ if (next == null || typeof next !== "object" || Array.isArray(next)) {
13
+ cur[part] = {};
14
+ }
15
+ cur = cur[part] as Record<string, unknown>;
16
+ }
17
+ cur[parts[parts.length - 1]!] = value;
18
+ }
@@ -1,37 +1,37 @@
1
- import type { SortOrder, SortSpec } from "../../data/dataProviderTypes";
2
-
3
- /** Parse `sort` query (Django-style: `sku,-price`; legacy: `sku:asc,price:desc`). */
4
- export function parseSortParam(raw: string | null): SortSpec[] {
5
- if (!raw) return [];
6
- const specs: SortSpec[] = [];
7
- for (const part of raw.split(",").map((p) => p.trim()).filter(Boolean)) {
8
- if (part.includes(":")) {
9
- const [field, orderRaw] = part.split(":");
10
- if (!field) continue;
11
- const order: SortOrder =
12
- orderRaw?.toLowerCase() === "desc" ? "DESC" : "ASC";
13
- specs.push({ field, order });
14
- continue;
15
- }
16
- if (part.startsWith("-") && part.length > 1) {
17
- specs.push({ field: part.slice(1), order: "DESC" });
18
- } else {
19
- specs.push({ field: part, order: "ASC" });
20
- }
21
- }
22
- return specs;
23
- }
24
-
25
- /** Serialize sort specs to Django-style `sku,-price`. */
26
- export function serializeSort(specs: SortSpec[]): string | null {
27
- if (specs.length === 0) return null;
28
- return specs
29
- .map((s) => (s.order === "DESC" ? `-${s.field}` : s.field))
30
- .join(",");
31
- }
32
-
33
- export function sortPrioritiesFromSpecs(
34
- specs: SortSpec[],
35
- ): Map<string, number> {
36
- return new Map(specs.map((s, i) => [s.field, i + 1]));
37
- }
1
+ import type { SortOrder, SortSpec } from "../../data/dataProviderTypes";
2
+
3
+ /** Parse `sort` query (Django-style: `sku,-price`; legacy: `sku:asc,price:desc`). */
4
+ export function parseSortParam(raw: string | null): SortSpec[] {
5
+ if (!raw) return [];
6
+ const specs: SortSpec[] = [];
7
+ for (const part of raw.split(",").map((p) => p.trim()).filter(Boolean)) {
8
+ if (part.includes(":")) {
9
+ const [field, orderRaw] = part.split(":");
10
+ if (!field) continue;
11
+ const order: SortOrder =
12
+ orderRaw?.toLowerCase() === "desc" ? "DESC" : "ASC";
13
+ specs.push({ field, order });
14
+ continue;
15
+ }
16
+ if (part.startsWith("-") && part.length > 1) {
17
+ specs.push({ field: part.slice(1), order: "DESC" });
18
+ } else {
19
+ specs.push({ field: part, order: "ASC" });
20
+ }
21
+ }
22
+ return specs;
23
+ }
24
+
25
+ /** Serialize sort specs to Django-style `sku,-price`. */
26
+ export function serializeSort(specs: SortSpec[]): string | null {
27
+ if (specs.length === 0) return null;
28
+ return specs
29
+ .map((s) => (s.order === "DESC" ? `-${s.field}` : s.field))
30
+ .join(",");
31
+ }
32
+
33
+ export function sortPrioritiesFromSpecs(
34
+ specs: SortSpec[],
35
+ ): Map<string, number> {
36
+ return new Map(specs.map((s, i) => [s.field, i + 1]));
37
+ }
@@ -0,0 +1,79 @@
1
+ import { isExistingUploadReference } from "./uploadReferenceUtils";
2
+
3
+ export type ToFormDataOptions = {
4
+ /**
5
+ * Omit unchanged upload URL strings (`http://` / `https://` / `/media/`) from the body.
6
+ * Default `true` — edit forms keep existing files without re-sending the URL.
7
+ */
8
+ skipExistingUploadUrls?: boolean;
9
+ };
10
+
11
+ function appendFormDataValue(
12
+ formData: FormData,
13
+ key: string,
14
+ value: unknown,
15
+ options: Required<ToFormDataOptions>,
16
+ ): void {
17
+ if (value === undefined) return;
18
+
19
+ if (value === null) {
20
+ formData.append(key, "");
21
+ return;
22
+ }
23
+
24
+ if (value instanceof Blob) {
25
+ formData.append(key, value);
26
+ return;
27
+ }
28
+
29
+ if (typeof value === "boolean" || typeof value === "number") {
30
+ formData.append(key, String(value));
31
+ return;
32
+ }
33
+
34
+ if (typeof value === "string") {
35
+ if (options.skipExistingUploadUrls && isExistingUploadReference(value)) {
36
+ return;
37
+ }
38
+ formData.append(key, value);
39
+ return;
40
+ }
41
+
42
+ if (Array.isArray(value)) {
43
+ value.forEach((item, index) => {
44
+ appendFormDataValue(formData, `${key}[${index}]`, item, options);
45
+ });
46
+ return;
47
+ }
48
+
49
+ if (typeof value === "object") {
50
+ for (const [childKey, childValue] of Object.entries(
51
+ value as Record<string, unknown>,
52
+ )) {
53
+ appendFormDataValue(formData, `${key}[${childKey}]`, childValue, options);
54
+ }
55
+ return;
56
+ }
57
+
58
+ formData.append(key, String(value));
59
+ }
60
+
61
+ /**
62
+ * Flatten a save payload into `FormData` using bracket notation for nested fields
63
+ * and arrays (e.g. `lines[0][label]`, `address[city]`).
64
+ */
65
+ export function toFormData(
66
+ data: Record<string, unknown>,
67
+ options?: ToFormDataOptions,
68
+ ): FormData {
69
+ const resolved: Required<ToFormDataOptions> = {
70
+ skipExistingUploadUrls: options?.skipExistingUploadUrls ?? true,
71
+ };
72
+ const formData = new FormData();
73
+
74
+ for (const [key, value] of Object.entries(data)) {
75
+ appendFormDataValue(formData, key, value, resolved);
76
+ }
77
+
78
+ return formData;
79
+ }
@@ -0,0 +1,44 @@
1
+ /** Existing server-side upload reference — omit on save unless replaced with a new File. */
2
+ export function isExistingUploadReference(value: string): boolean {
3
+ return /^https?:\/\//i.test(value) || value.startsWith("/media/");
4
+ }
5
+
6
+ export function stripExistingUploadReferences(
7
+ value: unknown,
8
+ skipExistingUploadUrls: boolean,
9
+ ): unknown {
10
+ if (!skipExistingUploadUrls) return value;
11
+
12
+ if (typeof value === "string") {
13
+ return isExistingUploadReference(value) ? undefined : value;
14
+ }
15
+
16
+ if (Array.isArray(value)) {
17
+ return value
18
+ .map((item) => stripExistingUploadReferences(item, skipExistingUploadUrls))
19
+ .filter((item) => item !== undefined);
20
+ }
21
+
22
+ if (value && typeof value === "object" && !(value instanceof Blob)) {
23
+ const result: Record<string, unknown> = {};
24
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
25
+ const stripped = stripExistingUploadReferences(child, skipExistingUploadUrls);
26
+ if (stripped !== undefined) {
27
+ result[key] = stripped;
28
+ }
29
+ }
30
+ return result;
31
+ }
32
+
33
+ return value;
34
+ }
35
+
36
+ export function stripExistingUploadReferencesFromPayload(
37
+ payload: Record<string, unknown>,
38
+ skipExistingUploadUrls = true,
39
+ ): Record<string, unknown> {
40
+ return stripExistingUploadReferences(payload, skipExistingUploadUrls) as Record<
41
+ string,
42
+ unknown
43
+ >;
44
+ }
@@ -1,17 +1,17 @@
1
- import { useEffect, type DependencyList } from "react";
2
-
3
- /**
4
- * Runs an async effect with an `AbortSignal` that aborts on cleanup.
5
- * Use with `params.signal` on DataProvider reads and `isAbortError` in catch blocks.
6
- */
7
- export function useAbortableEffect(
8
- effect: (signal: AbortSignal) => void | Promise<void>,
9
- deps: DependencyList,
10
- ): void {
11
- useEffect(() => {
12
- const controller = new AbortController();
13
- void effect(controller.signal);
14
- return () => controller.abort();
15
- // eslint-disable-next-line react-hooks/exhaustive-deps
16
- }, deps);
17
- }
1
+ import { useEffect, type DependencyList } from "react";
2
+
3
+ /**
4
+ * Runs an async effect with an `AbortSignal` that aborts on cleanup.
5
+ * Use with `params.signal` on DataProvider reads and `isAbortError` in catch blocks.
6
+ */
7
+ export function useAbortableEffect(
8
+ effect: (signal: AbortSignal) => void | Promise<void>,
9
+ deps: DependencyList,
10
+ ): void {
11
+ useEffect(() => {
12
+ const controller = new AbortController();
13
+ void effect(controller.signal);
14
+ return () => controller.abort();
15
+ // eslint-disable-next-line react-hooks/exhaustive-deps
16
+ }, deps);
17
+ }