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.
- package/LICENSE +21 -21
- package/README.md +254 -201
- package/dist/index.css +2 -0
- package/dist/index.js +2073 -1202
- 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/InlineFormSet.d.ts +2 -17
- package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
- package/dist/src/crud/ResourceForm.d.ts +2 -10
- 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/ResourceList.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/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/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/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/filters/ReferenceFilter.d.ts +1 -1
- package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
- package/dist/src/crud/index.d.ts +27 -9
- package/dist/src/crud/index.d.ts.map +1 -1
- package/dist/src/crud/types.d.ts +21 -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/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/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 +8 -0
- package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -0
- 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/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/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 +44 -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 +23 -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 +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 -219
- package/src/crud/ListActionsBar.tsx +106 -106
- package/src/crud/ResourceForm.tsx +83 -166
- package/src/crud/ResourceFormModal.tsx +75 -133
- package/src/crud/ResourceList.tsx +1 -0
- 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 -0
- 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 -0
- package/src/crud/context/ListContext.tsx +93 -93
- package/src/crud/context/PayloadFieldsContext.tsx +62 -0
- package/src/crud/fields/BooleanField.tsx +38 -38
- package/src/crud/fields/DateField.tsx +53 -53
- 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/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 -0
- package/src/crud/fields/useUploadPreviewUrl.ts +20 -0
- 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 -90
- package/src/crud/types.ts +146 -132
- 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/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 -38
- package/src/crud/utils/formSectionErrors.ts +85 -85
- package/src/crud/utils/{getByPath.ts → getFormValue.ts} +13 -13
- package/src/crud/utils/hasUploadValues.ts +9 -0
- package/src/crud/utils/nestedFieldPath.ts +8 -0
- package/src/crud/utils/prepareFormSubmitBody.ts +22 -0
- package/src/crud/utils/referenceSelectNotFoundContent.tsx +7 -0
- package/src/crud/utils/{setByPath.ts → setFormValue.ts} +18 -18
- package/src/crud/utils/sortQueryParam.ts +37 -37
- package/src/crud/utils/toFormData.ts +79 -0
- 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 -0
- 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 -57
- package/src/data/dataProviderTypes.ts +7 -5
- package/src/data/dataUtils.test.ts +228 -108
- package/src/data/inMemoryList.ts +97 -97
- package/src/data/parseFormErrorHelpers.test.ts +201 -84
- package/src/data/parseFormErrorHelpers.ts +376 -224
- package/src/data/resourceHandlers.ts +1 -1
- package/src/data/sortHelpers.ts +48 -48
- package/src/index.ts +211 -195
- package/src/layouts/AdminLayout.tsx +530 -381
- 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
- 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
|
@@ -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
|
|
2
|
-
export function
|
|
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
|
+
}
|