ding-react-admin 1.0.4 → 2.0.0
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/README.md +4 -10
- package/dist/index.js +1071 -803
- 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 +4 -9
- package/dist/src/crud/ResourceForm.d.ts.map +1 -1
- package/dist/src/crud/ResourceList.d.ts.map +1 -1
- 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/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/index.d.ts +22 -9
- package/dist/src/crud/index.d.ts.map +1 -1
- package/dist/src/crud/types.d.ts +9 -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/formErrors.d.ts +1 -1
- package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
- 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 +7 -0
- package/dist/src/crud/utils/prepareFormSubmitBody.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/useFormRecord.d.ts +42 -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 +44 -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.map +1 -1
- package/package.json +1 -1
- package/src/crud/InlineFormSet.tsx +28 -85
- package/src/crud/ResourceForm.tsx +60 -66
- package/src/crud/ResourceFormModal.tsx +25 -25
- package/src/crud/ResourceList.tsx +1 -0
- package/src/crud/columns/ImageColumn.tsx +58 -0
- package/src/crud/columns/TextColumn.tsx +2 -2
- package/src/crud/context/InlineFieldsRegistry.tsx +61 -0
- package/src/crud/context/PayloadFieldsContext.tsx +62 -0
- 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/uploadFieldUtils.ts +21 -0
- package/src/crud/fields/useUploadPreviewUrl.ts +20 -0
- package/src/crud/index.ts +23 -19
- package/src/crud/types.ts +9 -7
- 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/formErrors.ts +42 -38
- package/src/crud/utils/{getByPath.ts → getFormValue.ts} +2 -2
- package/src/crud/utils/hasUploadValues.ts +9 -0
- package/src/crud/utils/nestedFieldPath.ts +8 -0
- package/src/crud/utils/prepareFormSubmitBody.ts +17 -0
- package/src/crud/utils/{setByPath.ts → setFormValue.ts} +2 -2
- package/src/crud/utils/toFormData.ts +81 -0
- package/src/crud/utils/useFormRecord.ts +156 -0
- package/src/data/createRestResourceHandlers.ts +19 -13
- package/src/data/dataProviderTypes.ts +7 -5
- package/src/data/dataUtils.test.ts +118 -6
- package/src/data/parseFormErrorHelpers.test.ts +138 -84
- package/src/data/parseFormErrorHelpers.ts +311 -224
- package/src/data/resourceHandlers.ts +1 -1
- package/src/index.ts +23 -11
- package/src/layouts/AdminLayout.tsx +2 -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,120 @@
|
|
|
1
|
+
import { DeleteOutlined, UploadOutlined } from "@ant-design/icons";
|
|
2
|
+
import { Button, Image, Space } from "antd";
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
import type { BaseSourceProps, FieldRules } from "../types";
|
|
5
|
+
import { FieldWrapper } from "./FieldWrapper";
|
|
6
|
+
import { hasUploadValue, type UploadFieldValue } from "./uploadFieldUtils";
|
|
7
|
+
import { useUploadPreviewUrl } from "./useUploadPreviewUrl";
|
|
8
|
+
|
|
9
|
+
export type ImageFieldProps = BaseSourceProps & {
|
|
10
|
+
name?: string;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
rules?: FieldRules;
|
|
13
|
+
hideLabel?: boolean;
|
|
14
|
+
/** Allow clearing an existing or newly selected image. */
|
|
15
|
+
clearable?: boolean;
|
|
16
|
+
accept?: string;
|
|
17
|
+
/** Max preview width in pixels. */
|
|
18
|
+
previewWidth?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function ImageFieldInput({
|
|
22
|
+
value,
|
|
23
|
+
onChange,
|
|
24
|
+
disabled,
|
|
25
|
+
clearable,
|
|
26
|
+
accept = "image/*",
|
|
27
|
+
previewWidth = 200,
|
|
28
|
+
}: {
|
|
29
|
+
value: UploadFieldValue;
|
|
30
|
+
onChange: (value: UploadFieldValue) => void;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
clearable?: boolean;
|
|
33
|
+
accept?: string;
|
|
34
|
+
previewWidth?: number;
|
|
35
|
+
}) {
|
|
36
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
37
|
+
const previewUrl = useUploadPreviewUrl(value);
|
|
38
|
+
const showClear = clearable && hasUploadValue(value);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
|
42
|
+
{previewUrl ? (
|
|
43
|
+
<Image
|
|
44
|
+
src={previewUrl}
|
|
45
|
+
alt=""
|
|
46
|
+
style={{ maxWidth: previewWidth, maxHeight: previewWidth, objectFit: "contain" }}
|
|
47
|
+
/>
|
|
48
|
+
) : null}
|
|
49
|
+
<Space wrap>
|
|
50
|
+
<Button
|
|
51
|
+
icon={<UploadOutlined />}
|
|
52
|
+
disabled={disabled}
|
|
53
|
+
onClick={() => inputRef.current?.click()}
|
|
54
|
+
>
|
|
55
|
+
Choose image
|
|
56
|
+
</Button>
|
|
57
|
+
{showClear ? (
|
|
58
|
+
<Button
|
|
59
|
+
icon={<DeleteOutlined />}
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
onClick={() => {
|
|
62
|
+
onChange(null);
|
|
63
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
Clear
|
|
67
|
+
</Button>
|
|
68
|
+
) : null}
|
|
69
|
+
</Space>
|
|
70
|
+
<input
|
|
71
|
+
ref={inputRef}
|
|
72
|
+
type="file"
|
|
73
|
+
accept={accept}
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
tabIndex={-1}
|
|
76
|
+
aria-hidden
|
|
77
|
+
style={{ display: "none" }}
|
|
78
|
+
onChange={(e) => {
|
|
79
|
+
const file = e.target.files?.[0];
|
|
80
|
+
onChange(file ?? null);
|
|
81
|
+
e.target.value = "";
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
</Space>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function ImageField({
|
|
89
|
+
source,
|
|
90
|
+
name,
|
|
91
|
+
label,
|
|
92
|
+
required,
|
|
93
|
+
rules,
|
|
94
|
+
hideLabel,
|
|
95
|
+
clearable,
|
|
96
|
+
accept,
|
|
97
|
+
previewWidth,
|
|
98
|
+
}: ImageFieldProps) {
|
|
99
|
+
return (
|
|
100
|
+
<FieldWrapper
|
|
101
|
+
source={source}
|
|
102
|
+
name={name}
|
|
103
|
+
label={label}
|
|
104
|
+
required={required}
|
|
105
|
+
rules={rules}
|
|
106
|
+
hideLabel={hideLabel}
|
|
107
|
+
>
|
|
108
|
+
{({ value, onChange, disabled }) => (
|
|
109
|
+
<ImageFieldInput
|
|
110
|
+
value={value as UploadFieldValue}
|
|
111
|
+
onChange={onChange}
|
|
112
|
+
disabled={disabled}
|
|
113
|
+
clearable={clearable}
|
|
114
|
+
accept={accept}
|
|
115
|
+
previewWidth={previewWidth}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
</FieldWrapper>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Form value for file/image fields — backend-agnostic. */
|
|
2
|
+
export type UploadFieldValue = File | string | null | undefined;
|
|
3
|
+
|
|
4
|
+
export function hasUploadValue(value: UploadFieldValue): boolean {
|
|
5
|
+
if (value instanceof File) return true;
|
|
6
|
+
return typeof value === "string" && value.length > 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getUploadFileName(value: UploadFieldValue): string | undefined {
|
|
10
|
+
if (value instanceof File) return value.name;
|
|
11
|
+
if (typeof value === "string" && value.length > 0) {
|
|
12
|
+
try {
|
|
13
|
+
const pathname = new URL(value, "http://local").pathname;
|
|
14
|
+
const name = pathname.split("/").filter(Boolean).pop();
|
|
15
|
+
return name || value;
|
|
16
|
+
} catch {
|
|
17
|
+
return value.split("/").filter(Boolean).pop() || value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import type { UploadFieldValue } from "./uploadFieldUtils";
|
|
3
|
+
|
|
4
|
+
/** Preview URL for uploads — `URL.createObjectURL` for `File`, direct URL for existing strings. */
|
|
5
|
+
export function useUploadPreviewUrl(value: UploadFieldValue): string | undefined {
|
|
6
|
+
const [objectUrl, setObjectUrl] = useState<string | undefined>();
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (value instanceof File) {
|
|
10
|
+
const url = URL.createObjectURL(value);
|
|
11
|
+
setObjectUrl(url);
|
|
12
|
+
return () => URL.revokeObjectURL(url);
|
|
13
|
+
}
|
|
14
|
+
setObjectUrl(undefined);
|
|
15
|
+
}, [value]);
|
|
16
|
+
|
|
17
|
+
if (value instanceof File) return objectUrl;
|
|
18
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
package/src/crud/index.ts
CHANGED
|
@@ -2,22 +2,13 @@ export { ResourceList, useResourceListContext } from "./ResourceList";
|
|
|
2
2
|
export type { ResourceListProps } from "./ResourceList";
|
|
3
3
|
export { FilterBar } from "./FilterBar";
|
|
4
4
|
export { ResourceForm } from "./ResourceForm";
|
|
5
|
-
export type {
|
|
6
|
-
ResourceFormProps,
|
|
7
|
-
ResourceFormInlineConfig,
|
|
8
|
-
} from "./ResourceForm";
|
|
5
|
+
export type { ResourceFormProps } from "./ResourceForm";
|
|
9
6
|
export { ResourceFormModal } from "./ResourceFormModal";
|
|
10
7
|
export type { ResourceFormModalProps } from "./ResourceFormModal";
|
|
11
|
-
export {
|
|
12
|
-
InlineFormSet,
|
|
13
|
-
InlineFormSetStacked,
|
|
14
|
-
saveInlineRows,
|
|
15
|
-
loadInlineRows,
|
|
16
|
-
} from "./InlineFormSet";
|
|
8
|
+
export { InlineFormSet, InlineFormSetStacked } from "./InlineFormSet";
|
|
17
9
|
export type {
|
|
18
10
|
InlineFormSetProps,
|
|
19
11
|
InlineFormSetStackedProps,
|
|
20
|
-
SaveInlineOptions,
|
|
21
12
|
InlineFormSetLayout,
|
|
22
13
|
} from "./InlineFormSet";
|
|
23
14
|
export { FormTabs, FormTab } from "./FormTabs";
|
|
@@ -33,8 +24,13 @@ export { SelectField } from "./fields/SelectField";
|
|
|
33
24
|
export { PasswordField } from "./fields/PasswordField";
|
|
34
25
|
export { ReferenceField } from "./fields/ReferenceField";
|
|
35
26
|
export { ReferenceManyField } from "./fields/ReferenceManyField";
|
|
27
|
+
export { ImageField } from "./fields/ImageField";
|
|
28
|
+
export { FileField } from "./fields/FileField";
|
|
36
29
|
export { FieldWrapper } from "./fields/FieldWrapper";
|
|
37
30
|
export type { FieldWrapperProps } from "./fields/FieldWrapper";
|
|
31
|
+
export type { ImageFieldProps } from "./fields/ImageField";
|
|
32
|
+
export type { FileFieldProps } from "./fields/FileField";
|
|
33
|
+
export type { UploadFieldValue } from "./fields/uploadFieldUtils";
|
|
38
34
|
|
|
39
35
|
export { TextColumn } from "./columns/TextColumn";
|
|
40
36
|
export { NumberColumn } from "./columns/NumberColumn";
|
|
@@ -42,6 +38,7 @@ export { BooleanColumn } from "./columns/BooleanColumn";
|
|
|
42
38
|
export { DateColumn } from "./columns/DateColumn";
|
|
43
39
|
export { ReferenceColumn } from "./columns/ReferenceColumn";
|
|
44
40
|
export { ReferenceManyColumn } from "./columns/ReferenceManyColumn";
|
|
41
|
+
export { ImageColumn } from "./columns/ImageColumn";
|
|
45
42
|
export { CustomColumn } from "./columns/CustomColumn";
|
|
46
43
|
|
|
47
44
|
export { TextFilter } from "./filters/TextFilter";
|
|
@@ -61,15 +58,22 @@ export type {
|
|
|
61
58
|
ListQueryActions,
|
|
62
59
|
} from "./utils/useListQueryState";
|
|
63
60
|
export { useChoices } from "./utils/useChoices";
|
|
64
|
-
export {
|
|
65
|
-
export {
|
|
66
|
-
export {
|
|
67
|
-
export {
|
|
68
|
-
export {
|
|
61
|
+
export { getFormValue } from "./utils/getFormValue";
|
|
62
|
+
export { setFormValue } from "./utils/setFormValue";
|
|
63
|
+
export { buildFormPayload } from "./utils/buildFormPayload";
|
|
64
|
+
export { buildInlineRowsPayload } from "./utils/buildInlineRowsPayload";
|
|
65
|
+
export { buildResourceFormSubmitBody } from "./utils/buildResourceFormSubmitBody";
|
|
66
|
+
export { prepareFormSubmitBody } from "./utils/prepareFormSubmitBody";
|
|
67
|
+
export { toFormData } from "./utils/toFormData";
|
|
68
|
+
export { hasUploadValues } from "./utils/hasUploadValues";
|
|
69
|
+
export type { ToFormDataOptions } from "./utils/toFormData";
|
|
70
|
+
export type { PrepareFormSubmitBodyOptions } from "./utils/prepareFormSubmitBody";
|
|
71
|
+
export { nestedFieldPath } from "./utils/nestedFieldPath";
|
|
69
72
|
export {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
} from "./context/
|
|
73
|
+
useRegisterPayloadField,
|
|
74
|
+
useRegisterSectionField,
|
|
75
|
+
} from "./context/PayloadFieldsContext";
|
|
76
|
+
export type { InlineFieldRegistration } from "./context/InlineFieldsRegistry";
|
|
73
77
|
|
|
74
78
|
export type {
|
|
75
79
|
BaseSourceProps,
|
package/src/crud/types.ts
CHANGED
|
@@ -57,18 +57,20 @@ export type FilterDefinition = {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
export type InlineFormSetBaseProps = {
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
/** RHF `useFieldArray` name and default API nested key, e.g. `"lines"`. */
|
|
61
|
+
field: string;
|
|
62
62
|
label?: string;
|
|
63
|
-
|
|
63
|
+
/** API key when different from `field`. */
|
|
64
|
+
payloadKey?: string;
|
|
65
|
+
transformRows?: (rows: Record<string, unknown>[]) => unknown;
|
|
64
66
|
};
|
|
65
67
|
|
|
66
68
|
/** Passed to each tabular inline column's `cell` renderer. */
|
|
67
69
|
export type InlineCellContext = {
|
|
68
|
-
/** Full RHF path, e.g. `
|
|
70
|
+
/** Full RHF path, e.g. `lines.0.label`. */
|
|
69
71
|
name: string;
|
|
70
72
|
index: number;
|
|
71
|
-
|
|
73
|
+
field: string;
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
export type InlineColumnDef = {
|
|
@@ -81,9 +83,9 @@ export type InlineColumnDef = {
|
|
|
81
83
|
|
|
82
84
|
/** Passed to `InlineFormSetStacked`'s `renderRow` for one related row. */
|
|
83
85
|
export type InlineRowContext = {
|
|
84
|
-
|
|
86
|
+
field: string;
|
|
85
87
|
index: number;
|
|
86
|
-
/** RHF path helper — `name("label")` → `
|
|
88
|
+
/** RHF path helper — `name("label")` → `field.index.label`. */
|
|
87
89
|
name: (source: string) => string;
|
|
88
90
|
};
|
|
89
91
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getFormValue } from "./getFormValue";
|
|
2
|
+
import { setFormValue } from "./setFormValue";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build the API save body from form values using registered field paths.
|
|
6
|
+
* Flat `email` → `{ email }`; nested `address.city` → `{ address: { city } }`.
|
|
7
|
+
*/
|
|
8
|
+
export function buildFormPayload(
|
|
9
|
+
values: Record<string, unknown>,
|
|
10
|
+
fieldPaths: string[],
|
|
11
|
+
): Record<string, unknown> {
|
|
12
|
+
if (fieldPaths.length === 0) {
|
|
13
|
+
return { ...values };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const result: Record<string, unknown> = {};
|
|
17
|
+
for (const path of fieldPaths) {
|
|
18
|
+
const value = getFormValue(values, path);
|
|
19
|
+
if (value !== undefined) {
|
|
20
|
+
setFormValue(result, path, value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type BuildInlineRowsPayloadOptions = {
|
|
2
|
+
transformRows?: (rows: Record<string, unknown>[]) => unknown;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
/** Clean inline field-array rows for the API (strip RHF `rowKey`, keep record `id`). */
|
|
6
|
+
export function buildInlineRowsPayload(
|
|
7
|
+
rows: unknown,
|
|
8
|
+
sources: string[],
|
|
9
|
+
options?: BuildInlineRowsPayloadOptions,
|
|
10
|
+
): unknown {
|
|
11
|
+
if (!Array.isArray(rows)) return [];
|
|
12
|
+
|
|
13
|
+
const cleaned = rows.map((row) => {
|
|
14
|
+
if (!row || typeof row !== "object") return {};
|
|
15
|
+
const record = row as Record<string, unknown>;
|
|
16
|
+
const out: Record<string, unknown> = {};
|
|
17
|
+
|
|
18
|
+
for (const source of sources) {
|
|
19
|
+
const value = record[source];
|
|
20
|
+
if (value !== undefined) {
|
|
21
|
+
out[source] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const id = record.id;
|
|
26
|
+
if (id !== undefined && id !== null) {
|
|
27
|
+
out.id = id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return out;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (options?.transformRows) {
|
|
34
|
+
return options.transformRows(cleaned);
|
|
35
|
+
}
|
|
36
|
+
return cleaned;
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { InlineFieldRegistration } from "../context/InlineFieldsRegistry";
|
|
2
|
+
import { buildFormPayload } from "./buildFormPayload";
|
|
3
|
+
import { buildInlineRowsPayload } from "./buildInlineRowsPayload";
|
|
4
|
+
import {
|
|
5
|
+
prepareFormSubmitBody,
|
|
6
|
+
type PrepareFormSubmitBodyOptions,
|
|
7
|
+
} from "./prepareFormSubmitBody";
|
|
8
|
+
|
|
9
|
+
/** Build the create/update body from form values, auto-switching to multipart when needed. */
|
|
10
|
+
export function buildResourceFormSubmitBody(
|
|
11
|
+
raw: Record<string, unknown>,
|
|
12
|
+
payloadFieldPaths: string[],
|
|
13
|
+
inlineRegistry?: Iterable<InlineFieldRegistration>,
|
|
14
|
+
options?: PrepareFormSubmitBodyOptions,
|
|
15
|
+
): Record<string, unknown> | FormData {
|
|
16
|
+
const payload = buildFormPayload(raw, payloadFieldPaths);
|
|
17
|
+
|
|
18
|
+
if (inlineRegistry) {
|
|
19
|
+
for (const inline of inlineRegistry) {
|
|
20
|
+
const rows = raw[inline.field];
|
|
21
|
+
const key = inline.payloadKey ?? inline.field;
|
|
22
|
+
payload[key] = buildInlineRowsPayload(rows, inline.sources, {
|
|
23
|
+
transformRows: inline.transformRows,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return prepareFormSubmitBody(payload, options);
|
|
29
|
+
}
|
|
@@ -1,38 +1,42 @@
|
|
|
1
|
-
import type { MessageInstance } from "antd/es/message/interface";
|
|
2
|
-
import type { FieldPath, FieldValues, UseFormReturn } from "react-hook-form";
|
|
3
|
-
import type { DataProvider, ParseFormErrorContext } from "../../data/dataProviderTypes";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
if (!
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
1
|
+
import type { MessageInstance } from "antd/es/message/interface";
|
|
2
|
+
import type { FieldPath, FieldValues, UseFormReturn } from "react-hook-form";
|
|
3
|
+
import type { DataProvider, ParseFormErrorContext } from "../../data/dataProviderTypes";
|
|
4
|
+
import { resolveErrorBody } from "../../data/parseFormErrorHelpers";
|
|
5
|
+
|
|
6
|
+
function toMessages(value: string | string[] | undefined): string[] {
|
|
7
|
+
if (!value) return [];
|
|
8
|
+
return Array.isArray(value) ? value : [value];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function applyFormErrors<T extends FieldValues>(
|
|
12
|
+
form: UseFormReturn<T>,
|
|
13
|
+
parsed: { fields?: Record<string, string | string[]>; global?: string | string[] },
|
|
14
|
+
message: MessageInstance,
|
|
15
|
+
) {
|
|
16
|
+
for (const msg of toMessages(parsed.global)) {
|
|
17
|
+
message.error(msg);
|
|
18
|
+
}
|
|
19
|
+
for (const [path, msg] of Object.entries(parsed.fields ?? {})) {
|
|
20
|
+
const text = Array.isArray(msg) ? msg.join(", ") : msg;
|
|
21
|
+
form.setError(path as FieldPath<T>, { type: "server", message: text });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function applyApiErrorsToForm<T extends FieldValues>(
|
|
26
|
+
dp: DataProvider,
|
|
27
|
+
form: UseFormReturn<T>,
|
|
28
|
+
message: MessageInstance,
|
|
29
|
+
error: unknown,
|
|
30
|
+
context: ParseFormErrorContext,
|
|
31
|
+
): Promise<boolean> {
|
|
32
|
+
const body = await resolveErrorBody(error);
|
|
33
|
+
const normalizedError = body != null ? { body } : error;
|
|
34
|
+
const parsed = dp.parseFormError?.(normalizedError, context);
|
|
35
|
+
if (!parsed) return false;
|
|
36
|
+
const hasFields = Object.keys(parsed.fields ?? {}).length > 0;
|
|
37
|
+
const hasGlobal = toMessages(parsed.global).length > 0;
|
|
38
|
+
if (!hasFields && !hasGlobal) return false;
|
|
39
|
+
applyFormErrors(form, parsed, message);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** Read nested value from
|
|
2
|
-
export function
|
|
1
|
+
/** Read a nested value from form values or a record, e.g. `lines.0.label`. */
|
|
2
|
+
export function getFormValue(
|
|
3
3
|
record: Record<string, unknown>,
|
|
4
4
|
path: string,
|
|
5
5
|
): unknown {
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { hasUploadValues } from "./hasUploadValues";
|
|
2
|
+
import { toFormData, type ToFormDataOptions } from "./toFormData";
|
|
3
|
+
|
|
4
|
+
export type PrepareFormSubmitBodyOptions = ToFormDataOptions;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* When the payload contains any `File`/`Blob`, return `FormData`; otherwise the plain object.
|
|
8
|
+
*/
|
|
9
|
+
export function prepareFormSubmitBody(
|
|
10
|
+
payload: Record<string, unknown>,
|
|
11
|
+
options?: PrepareFormSubmitBodyOptions,
|
|
12
|
+
): Record<string, unknown> | FormData {
|
|
13
|
+
if (hasUploadValues(payload)) {
|
|
14
|
+
return toFormData(payload, options);
|
|
15
|
+
}
|
|
16
|
+
return payload;
|
|
17
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** Write a value into
|
|
2
|
-
export function
|
|
1
|
+
/** Write a value into an object using dot notation, e.g. `invoiceLine.product`. */
|
|
2
|
+
export function setFormValue(
|
|
3
3
|
record: Record<string, unknown>,
|
|
4
4
|
path: string,
|
|
5
5
|
value: unknown,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export type ToFormDataOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Omit unchanged upload URL strings (`http://` / `https://`) from the body.
|
|
4
|
+
* Default `true` — edit forms keep existing files without re-sending the URL.
|
|
5
|
+
*/
|
|
6
|
+
skipExistingUploadUrls?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function isExistingUploadUrl(value: string): boolean {
|
|
10
|
+
return /^https?:\/\//i.test(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function appendFormDataValue(
|
|
14
|
+
formData: FormData,
|
|
15
|
+
key: string,
|
|
16
|
+
value: unknown,
|
|
17
|
+
options: Required<ToFormDataOptions>,
|
|
18
|
+
): void {
|
|
19
|
+
if (value === undefined) return;
|
|
20
|
+
|
|
21
|
+
if (value === null) {
|
|
22
|
+
formData.append(key, "");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (value instanceof Blob) {
|
|
27
|
+
formData.append(key, value);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof value === "boolean" || typeof value === "number") {
|
|
32
|
+
formData.append(key, String(value));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof value === "string") {
|
|
37
|
+
if (options.skipExistingUploadUrls && isExistingUploadUrl(value)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
formData.append(key, value);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
value.forEach((item, index) => {
|
|
46
|
+
appendFormDataValue(formData, `${key}[${index}]`, item, options);
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof value === "object") {
|
|
52
|
+
for (const [childKey, childValue] of Object.entries(
|
|
53
|
+
value as Record<string, unknown>,
|
|
54
|
+
)) {
|
|
55
|
+
appendFormDataValue(formData, `${key}[${childKey}]`, childValue, options);
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
formData.append(key, String(value));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Flatten a save payload into `FormData` using bracket notation for nested fields
|
|
65
|
+
* and arrays (e.g. `lines[0][label]`, `address[city]`).
|
|
66
|
+
*/
|
|
67
|
+
export function toFormData(
|
|
68
|
+
data: Record<string, unknown>,
|
|
69
|
+
options?: ToFormDataOptions,
|
|
70
|
+
): FormData {
|
|
71
|
+
const resolved: Required<ToFormDataOptions> = {
|
|
72
|
+
skipExistingUploadUrls: options?.skipExistingUploadUrls ?? true,
|
|
73
|
+
};
|
|
74
|
+
const formData = new FormData();
|
|
75
|
+
|
|
76
|
+
for (const [key, value] of Object.entries(data)) {
|
|
77
|
+
appendFormDataValue(formData, key, value, resolved);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return formData;
|
|
81
|
+
}
|