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,85 +1,85 @@
|
|
|
1
|
-
import { useEffect, useRef, type MutableRefObject } from "react";
|
|
2
|
-
import {
|
|
3
|
-
useFormContext,
|
|
4
|
-
useFormState,
|
|
5
|
-
type FieldPath,
|
|
6
|
-
type FieldValues,
|
|
7
|
-
type FormState,
|
|
8
|
-
type UseFormGetFieldState,
|
|
9
|
-
} from "react-hook-form";
|
|
10
|
-
|
|
11
|
-
export function sectionHasError<T extends FieldValues>(
|
|
12
|
-
sources: Set<string>,
|
|
13
|
-
getFieldState: UseFormGetFieldState<T>,
|
|
14
|
-
formState: FormState<T>,
|
|
15
|
-
): boolean {
|
|
16
|
-
for (const source of sources) {
|
|
17
|
-
if (getFieldState(source as FieldPath<T>, formState).invalid) {
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function useSectionSourceRefs(count: number) {
|
|
25
|
-
const refs = useRef<MutableRefObject<Set<string>>[]>([]);
|
|
26
|
-
|
|
27
|
-
while (refs.current.length < count) {
|
|
28
|
-
refs.current.push({ current: new Set() });
|
|
29
|
-
}
|
|
30
|
-
if (refs.current.length > count) {
|
|
31
|
-
refs.current.length = count;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return refs.current;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function useActivateFirstErrorSection(
|
|
38
|
-
sectionRefs: MutableRefObject<Set<string>>[],
|
|
39
|
-
activate: (index: number) => void,
|
|
40
|
-
) {
|
|
41
|
-
const { control, getFieldState, setFocus } = useFormContext<FieldValues>();
|
|
42
|
-
const formState = useFormState({ control });
|
|
43
|
-
const prevSubmitCount = useRef(0);
|
|
44
|
-
const prevErrorCount = useRef(0);
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (formState.submitCount === 0) return;
|
|
48
|
-
|
|
49
|
-
const errorCount = Object.keys(formState.errors).length;
|
|
50
|
-
const submitChanged = formState.submitCount !== prevSubmitCount.current;
|
|
51
|
-
const serverErrorsArrived =
|
|
52
|
-
!submitChanged && errorCount > 0 && prevErrorCount.current === 0;
|
|
53
|
-
|
|
54
|
-
prevSubmitCount.current = formState.submitCount;
|
|
55
|
-
prevErrorCount.current = errorCount;
|
|
56
|
-
|
|
57
|
-
if ((!submitChanged && !serverErrorsArrived) || errorCount === 0) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const first = sectionRefs.findIndex((ref) =>
|
|
62
|
-
sectionHasError(ref.current, getFieldState, formState),
|
|
63
|
-
);
|
|
64
|
-
if (first < 0) return;
|
|
65
|
-
|
|
66
|
-
activate(first);
|
|
67
|
-
|
|
68
|
-
const firstInvalid = [...sectionRefs[first].current].find((source) =>
|
|
69
|
-
getFieldState(source as FieldPath<FieldValues>, formState).invalid,
|
|
70
|
-
);
|
|
71
|
-
if (firstInvalid) {
|
|
72
|
-
requestAnimationFrame(() => {
|
|
73
|
-
void setFocus(firstInvalid as FieldPath<FieldValues>);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}, [
|
|
77
|
-
activate,
|
|
78
|
-
formState,
|
|
79
|
-
formState.errors,
|
|
80
|
-
formState.submitCount,
|
|
81
|
-
getFieldState,
|
|
82
|
-
sectionRefs,
|
|
83
|
-
setFocus,
|
|
84
|
-
]);
|
|
85
|
-
}
|
|
1
|
+
import { useEffect, useRef, type MutableRefObject } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useFormContext,
|
|
4
|
+
useFormState,
|
|
5
|
+
type FieldPath,
|
|
6
|
+
type FieldValues,
|
|
7
|
+
type FormState,
|
|
8
|
+
type UseFormGetFieldState,
|
|
9
|
+
} from "react-hook-form";
|
|
10
|
+
|
|
11
|
+
export function sectionHasError<T extends FieldValues>(
|
|
12
|
+
sources: Set<string>,
|
|
13
|
+
getFieldState: UseFormGetFieldState<T>,
|
|
14
|
+
formState: FormState<T>,
|
|
15
|
+
): boolean {
|
|
16
|
+
for (const source of sources) {
|
|
17
|
+
if (getFieldState(source as FieldPath<T>, formState).invalid) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useSectionSourceRefs(count: number) {
|
|
25
|
+
const refs = useRef<MutableRefObject<Set<string>>[]>([]);
|
|
26
|
+
|
|
27
|
+
while (refs.current.length < count) {
|
|
28
|
+
refs.current.push({ current: new Set() });
|
|
29
|
+
}
|
|
30
|
+
if (refs.current.length > count) {
|
|
31
|
+
refs.current.length = count;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return refs.current;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useActivateFirstErrorSection(
|
|
38
|
+
sectionRefs: MutableRefObject<Set<string>>[],
|
|
39
|
+
activate: (index: number) => void,
|
|
40
|
+
) {
|
|
41
|
+
const { control, getFieldState, setFocus } = useFormContext<FieldValues>();
|
|
42
|
+
const formState = useFormState({ control });
|
|
43
|
+
const prevSubmitCount = useRef(0);
|
|
44
|
+
const prevErrorCount = useRef(0);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (formState.submitCount === 0) return;
|
|
48
|
+
|
|
49
|
+
const errorCount = Object.keys(formState.errors).length;
|
|
50
|
+
const submitChanged = formState.submitCount !== prevSubmitCount.current;
|
|
51
|
+
const serverErrorsArrived =
|
|
52
|
+
!submitChanged && errorCount > 0 && prevErrorCount.current === 0;
|
|
53
|
+
|
|
54
|
+
prevSubmitCount.current = formState.submitCount;
|
|
55
|
+
prevErrorCount.current = errorCount;
|
|
56
|
+
|
|
57
|
+
if ((!submitChanged && !serverErrorsArrived) || errorCount === 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const first = sectionRefs.findIndex((ref) =>
|
|
62
|
+
sectionHasError(ref.current, getFieldState, formState),
|
|
63
|
+
);
|
|
64
|
+
if (first < 0) return;
|
|
65
|
+
|
|
66
|
+
activate(first);
|
|
67
|
+
|
|
68
|
+
const firstInvalid = [...sectionRefs[first].current].find((source) =>
|
|
69
|
+
getFieldState(source as FieldPath<FieldValues>, formState).invalid,
|
|
70
|
+
);
|
|
71
|
+
if (firstInvalid) {
|
|
72
|
+
requestAnimationFrame(() => {
|
|
73
|
+
void setFocus(firstInvalid as FieldPath<FieldValues>);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}, [
|
|
77
|
+
activate,
|
|
78
|
+
formState,
|
|
79
|
+
formState.errors,
|
|
80
|
+
formState.submitCount,
|
|
81
|
+
getFieldState,
|
|
82
|
+
sectionRefs,
|
|
83
|
+
setFocus,
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/** Read a nested value from form values or a record, e.g. `lines.0.label`. */
|
|
2
|
-
export function getFormValue(
|
|
3
|
-
record: Record<string, unknown>,
|
|
4
|
-
path: string,
|
|
5
|
-
): unknown {
|
|
6
|
-
const parts = path.split(".");
|
|
7
|
-
let cur: unknown = record;
|
|
8
|
-
for (const part of parts) {
|
|
9
|
-
if (cur == null || typeof cur !== "object") return undefined;
|
|
10
|
-
cur = (cur as Record<string, unknown>)[part];
|
|
11
|
-
}
|
|
12
|
-
return cur;
|
|
13
|
-
}
|
|
1
|
+
/** Read a nested value from form values or a record, e.g. `lines.0.label`. */
|
|
2
|
+
export function getFormValue(
|
|
3
|
+
record: Record<string, unknown>,
|
|
4
|
+
path: string,
|
|
5
|
+
): unknown {
|
|
6
|
+
const parts = path.split(".");
|
|
7
|
+
let cur: unknown = record;
|
|
8
|
+
for (const part of parts) {
|
|
9
|
+
if (cur == null || typeof cur !== "object") return undefined;
|
|
10
|
+
cur = (cur as Record<string, unknown>)[part];
|
|
11
|
+
}
|
|
12
|
+
return cur;
|
|
13
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/** Whether a save payload contains a `File` or `Blob` anywhere (including inline rows). */
|
|
2
|
-
export function hasUploadValues(value: unknown): boolean {
|
|
3
|
-
if (value instanceof Blob) return true;
|
|
4
|
-
if (Array.isArray(value)) return value.some(hasUploadValues);
|
|
5
|
-
if (value && typeof value === "object") {
|
|
6
|
-
return Object.values(value as Record<string, unknown>).some(hasUploadValues);
|
|
7
|
-
}
|
|
8
|
-
return false;
|
|
9
|
-
}
|
|
1
|
+
/** Whether a save payload contains a `File` or `Blob` anywhere (including inline rows). */
|
|
2
|
+
export function hasUploadValues(value: unknown): boolean {
|
|
3
|
+
if (value instanceof Blob) return true;
|
|
4
|
+
if (Array.isArray(value)) return value.some(hasUploadValues);
|
|
5
|
+
if (value && typeof value === "object") {
|
|
6
|
+
return Object.values(value as Record<string, unknown>).some(hasUploadValues);
|
|
7
|
+
}
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/** RHF path for one cell in a nested field array row, e.g. `lines.0.label`. */
|
|
2
|
-
export function nestedFieldPath(
|
|
3
|
-
field: string,
|
|
4
|
-
index: number,
|
|
5
|
-
source: string,
|
|
6
|
-
): string {
|
|
7
|
-
return `${field}.${index}.${source}`;
|
|
8
|
-
}
|
|
1
|
+
/** RHF path for one cell in a nested field array row, e.g. `lines.0.label`. */
|
|
2
|
+
export function nestedFieldPath(
|
|
3
|
+
field: string,
|
|
4
|
+
index: number,
|
|
5
|
+
source: string,
|
|
6
|
+
): string {
|
|
7
|
+
return `${field}.${index}.${source}`;
|
|
8
|
+
}
|
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
import { hasUploadValues } from "./hasUploadValues";
|
|
2
|
-
import { toFormData, type ToFormDataOptions } from "./toFormData";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 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
|
+
/** 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
|
+
}
|
|
@@ -1,81 +1,79 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (value ===
|
|
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
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return formData;
|
|
81
|
-
}
|
|
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
|
+
}
|