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
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
import { useCallback, useRef, useState, type ReactNode } from "react";
|
|
9
9
|
import { useDataProvider } from "../context/DataProvider";
|
|
10
10
|
import { FormMetaProvider } from "./context/FormContext";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { PayloadFieldsProvider } from "./context/PayloadFieldsContext";
|
|
12
|
+
import { buildResourceFormSubmitBody } from "./utils/buildResourceFormSubmitBody";
|
|
13
|
+
import { applyApiErrorsToForm } from "./utils/formErrors";
|
|
14
14
|
import { useAbortableEffect } from "./utils/useAbortableEffect";
|
|
15
15
|
import { isAbortError } from "../data/abortError";
|
|
16
16
|
|
|
@@ -36,7 +36,7 @@ export function ResourceFormModal({
|
|
|
36
36
|
const [loading, setLoading] = useState(!isNew);
|
|
37
37
|
|
|
38
38
|
const form = useForm<FieldValues>();
|
|
39
|
-
const
|
|
39
|
+
const payloadFieldsRef = useRef(new Set<string>());
|
|
40
40
|
|
|
41
41
|
const load = useCallback(
|
|
42
42
|
async (signal?: AbortSignal) => {
|
|
@@ -71,20 +71,20 @@ export function ResourceFormModal({
|
|
|
71
71
|
|
|
72
72
|
async function onSubmit(values: FieldValues) {
|
|
73
73
|
try {
|
|
74
|
-
const
|
|
74
|
+
const body = buildResourceFormSubmitBody(
|
|
75
75
|
values as Record<string, unknown>,
|
|
76
|
-
Array.from(
|
|
76
|
+
Array.from(payloadFieldsRef.current),
|
|
77
77
|
);
|
|
78
78
|
if (isNew) {
|
|
79
|
-
await dp.create(resource,
|
|
79
|
+
await dp.create(resource, body);
|
|
80
80
|
message.success("Created");
|
|
81
81
|
} else if (editId) {
|
|
82
|
-
await dp.update(resource, { id: editId, data:
|
|
82
|
+
await dp.update(resource, { id: editId, data: body });
|
|
83
83
|
message.success("Updated");
|
|
84
84
|
}
|
|
85
85
|
onClose();
|
|
86
86
|
} catch (e) {
|
|
87
|
-
const handled =
|
|
87
|
+
const handled = await applyApiErrorsToForm(dp, form, message, e, {
|
|
88
88
|
resource,
|
|
89
89
|
mutation: isNew ? "create" : "update",
|
|
90
90
|
});
|
|
@@ -110,22 +110,22 @@ export function ResourceFormModal({
|
|
|
110
110
|
<Spin />
|
|
111
111
|
) : (
|
|
112
112
|
<FormMetaProvider resource={resource} isNew={isNew}>
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</
|
|
113
|
+
<PayloadFieldsProvider fieldsRef={payloadFieldsRef}>
|
|
114
|
+
<FormProvider {...form}>
|
|
115
|
+
<Form
|
|
116
|
+
layout="vertical"
|
|
117
|
+
onFinish={() => void form.handleSubmit(onSubmit)()}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
<Form.Item style={{ marginTop: 16, marginBottom: 0 }}>
|
|
121
|
+
<Button type="primary" htmlType="submit" style={{ marginRight: 8 }}>
|
|
122
|
+
Save
|
|
123
|
+
</Button>
|
|
124
|
+
<Button onClick={onClose}>Cancel</Button>
|
|
125
|
+
</Form.Item>
|
|
126
|
+
</Form>
|
|
127
|
+
</FormProvider>
|
|
128
|
+
</PayloadFieldsProvider>
|
|
129
129
|
</FormMetaProvider>
|
|
130
130
|
)}
|
|
131
131
|
</Modal>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useMemo, type CSSProperties } from "react";
|
|
2
|
+
import type { BaseSourceProps } from "../types";
|
|
3
|
+
import { useRegisterColumn } from "../context/ListContext";
|
|
4
|
+
|
|
5
|
+
export type ImageColumnProps = BaseSourceProps & {
|
|
6
|
+
sortable?: boolean;
|
|
7
|
+
/** Thumbnail width in pixels. */
|
|
8
|
+
width?: number;
|
|
9
|
+
/** Thumbnail height in pixels. */
|
|
10
|
+
height?: number;
|
|
11
|
+
objectFit?: CSSProperties["objectFit"];
|
|
12
|
+
borderRadius?: number;
|
|
13
|
+
alt?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function ImageColumn({
|
|
17
|
+
source,
|
|
18
|
+
label,
|
|
19
|
+
sortable = false,
|
|
20
|
+
width = 40,
|
|
21
|
+
height = 40,
|
|
22
|
+
objectFit = "cover",
|
|
23
|
+
borderRadius = 4,
|
|
24
|
+
alt = "",
|
|
25
|
+
}: ImageColumnProps) {
|
|
26
|
+
const def = useMemo(
|
|
27
|
+
() => ({
|
|
28
|
+
key: source,
|
|
29
|
+
source,
|
|
30
|
+
label,
|
|
31
|
+
sortable,
|
|
32
|
+
buildColumn: () => ({
|
|
33
|
+
title: label ?? source,
|
|
34
|
+
dataIndex: source,
|
|
35
|
+
key: source,
|
|
36
|
+
sorter: sortable ? true : undefined,
|
|
37
|
+
render: (value: unknown) => {
|
|
38
|
+
if (value == null || value === "") return null;
|
|
39
|
+
return (
|
|
40
|
+
<img
|
|
41
|
+
src={String(value)}
|
|
42
|
+
alt={alt}
|
|
43
|
+
style={{
|
|
44
|
+
width,
|
|
45
|
+
height,
|
|
46
|
+
objectFit,
|
|
47
|
+
borderRadius,
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
}),
|
|
54
|
+
[source, label, sortable, width, height, objectFit, borderRadius, alt],
|
|
55
|
+
);
|
|
56
|
+
useRegisterColumn(def);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import type { BaseSourceProps } from "../types";
|
|
3
|
-
import {
|
|
3
|
+
import { getFormValue } from "../utils/getFormValue";
|
|
4
4
|
import { useRegisterColumn } from "../context/ListContext";
|
|
5
5
|
|
|
6
6
|
export type TextColumnProps = BaseSourceProps & {
|
|
@@ -33,6 +33,6 @@ export function renderDisplayValue(
|
|
|
33
33
|
display?: string | ((record: Record<string, unknown>) => unknown),
|
|
34
34
|
): unknown {
|
|
35
35
|
if (typeof display === "function") return display(record);
|
|
36
|
-
if (display) return
|
|
36
|
+
if (display) return getFormValue(record, display);
|
|
37
37
|
return record[source];
|
|
38
38
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
type RefObject,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
|
|
9
|
+
export type InlineFieldRegistration = {
|
|
10
|
+
field: string;
|
|
11
|
+
sources: string[];
|
|
12
|
+
payloadKey?: string;
|
|
13
|
+
transformRows?: (rows: Record<string, unknown>[]) => unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const InlineFieldsRegistryContext =
|
|
17
|
+
createContext<RefObject<Map<string, InlineFieldRegistration>> | null>(
|
|
18
|
+
null,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export function InlineFieldsRegistryProvider({
|
|
22
|
+
children,
|
|
23
|
+
registryRef,
|
|
24
|
+
}: {
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
registryRef: RefObject<Map<string, InlineFieldRegistration>>;
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<InlineFieldsRegistryContext.Provider value={registryRef}>
|
|
30
|
+
{children}
|
|
31
|
+
</InlineFieldsRegistryContext.Provider>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useInlineFieldsRegistryOptional() {
|
|
36
|
+
return useContext(InlineFieldsRegistryContext);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Register an inline field array (`useFieldArray` name) for save payload cleaning. */
|
|
40
|
+
export function useRegisterInlineField(
|
|
41
|
+
field: string,
|
|
42
|
+
sources: string[],
|
|
43
|
+
payloadKey?: string,
|
|
44
|
+
transformRows?: (rows: Record<string, unknown>[]) => unknown,
|
|
45
|
+
enabled = true,
|
|
46
|
+
) {
|
|
47
|
+
const registryRef = useInlineFieldsRegistryOptional();
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!enabled || !registryRef) return;
|
|
51
|
+
registryRef.current.set(field, {
|
|
52
|
+
field,
|
|
53
|
+
sources,
|
|
54
|
+
payloadKey,
|
|
55
|
+
transformRows,
|
|
56
|
+
});
|
|
57
|
+
return () => {
|
|
58
|
+
registryRef.current.delete(field);
|
|
59
|
+
};
|
|
60
|
+
}, [registryRef, field, sources, payloadKey, transformRows, enabled]);
|
|
61
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
type RefObject,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { useFormSectionSourcesOptional } from "./FormSectionContext";
|
|
9
|
+
|
|
10
|
+
const PayloadFieldsContext =
|
|
11
|
+
createContext<RefObject<Set<string>> | null>(null);
|
|
12
|
+
|
|
13
|
+
/** Tracks which field paths are included in the save payload. */
|
|
14
|
+
export function PayloadFieldsProvider({
|
|
15
|
+
children,
|
|
16
|
+
fieldsRef,
|
|
17
|
+
}: {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
fieldsRef: RefObject<Set<string>>;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<PayloadFieldsContext.Provider value={fieldsRef}>
|
|
23
|
+
{children}
|
|
24
|
+
</PayloadFieldsContext.Provider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function usePayloadFieldsRefOptional() {
|
|
29
|
+
return useContext(PayloadFieldsContext);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a field path for the save payload (`buildFormPayload`).
|
|
34
|
+
* Top-level fields use `source`; inline arrays register their `field` name.
|
|
35
|
+
*/
|
|
36
|
+
export function useRegisterPayloadField(fieldPath: string, enabled = true) {
|
|
37
|
+
const fieldsRef = usePayloadFieldsRefOptional();
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!enabled || !fieldsRef) return;
|
|
41
|
+
fieldsRef.current.add(fieldPath);
|
|
42
|
+
return () => {
|
|
43
|
+
fieldsRef.current.delete(fieldPath);
|
|
44
|
+
};
|
|
45
|
+
}, [fieldsRef, fieldPath, enabled]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Register a field for tab/step error highlighting.
|
|
50
|
+
* Top-level fields only — no-op outside FormTabs / FormSteps.
|
|
51
|
+
*/
|
|
52
|
+
export function useRegisterSectionField(source: string, enabled = true) {
|
|
53
|
+
const sectionRef = useFormSectionSourcesOptional();
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!enabled || !sectionRef) return;
|
|
57
|
+
sectionRef.current.add(source);
|
|
58
|
+
return () => {
|
|
59
|
+
sectionRef.current.delete(source);
|
|
60
|
+
};
|
|
61
|
+
}, [sectionRef, source, enabled]);
|
|
62
|
+
}
|
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
import { Form } from "antd";
|
|
2
|
-
import { Controller, useFormContext, type FieldValues } from "react-hook-form";
|
|
3
|
-
import type { ReactNode } from "react";
|
|
4
|
-
import type { FieldRules } from "../types";
|
|
5
|
-
import { useFormMetaOptional } from "../context/FormContext";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "../context/
|
|
10
|
-
|
|
11
|
-
export type FieldWrapperProps = {
|
|
12
|
-
/** Logical field name — used for submit payload tracking on top-level fields. */
|
|
13
|
-
source: string;
|
|
14
|
-
/** Full RHF path. Defaults to `source`. Inline cells pass e.g. `lines.0.label`. */
|
|
15
|
-
name?: string;
|
|
16
|
-
label?: string;
|
|
17
|
-
required?: boolean;
|
|
18
|
-
rules?: FieldRules;
|
|
19
|
-
/** Tabular inline cells: column header replaces the label. */
|
|
20
|
-
hideLabel?: boolean;
|
|
21
|
-
children: (props: {
|
|
22
|
-
value: unknown;
|
|
23
|
-
onChange: (value: unknown) => void;
|
|
24
|
-
onBlur: () => void;
|
|
25
|
-
disabled?: boolean;
|
|
26
|
-
name: string;
|
|
27
|
-
}) => ReactNode;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export function FieldWrapper({
|
|
31
|
-
source,
|
|
32
|
-
name,
|
|
33
|
-
label,
|
|
34
|
-
required,
|
|
35
|
-
rules,
|
|
36
|
-
hideLabel,
|
|
37
|
-
children,
|
|
38
|
-
}: FieldWrapperProps) {
|
|
39
|
-
const fieldName = name ?? source;
|
|
40
|
-
const isTopLevel = !fieldName.includes(".");
|
|
41
|
-
|
|
42
|
-
// react-hook-form: connect this field to the form from <ResourceForm>'s FormProvider.
|
|
43
|
-
// `control` is passed to <Controller> below so value, onChange, and validation work.
|
|
44
|
-
const { control } = useFormContext<FieldValues>();
|
|
45
|
-
|
|
46
|
-
// ding-react-admin: optional form-wide flags from <FormMetaProvider> (inside ResourceForm).
|
|
47
|
-
// Used here for `disabled` on inputs (e.g. read-only view mode). Returns null outside a form page.
|
|
48
|
-
const meta = useFormMetaOptional();
|
|
49
|
-
const fieldLabel = hideLabel ? undefined : (label ?? source);
|
|
50
|
-
const requiredMessage = label ?? source;
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<Controller
|
|
62
|
-
name={fieldName}
|
|
63
|
-
control={control}
|
|
64
|
-
rules={{
|
|
65
|
-
required: required ? `${requiredMessage} is required` : false,
|
|
66
|
-
...rules,
|
|
67
|
-
}}
|
|
68
|
-
render={({ field, fieldState }) => (
|
|
69
|
-
<Form.Item
|
|
70
|
-
label={fieldLabel}
|
|
71
|
-
validateStatus={fieldState.error ? "error" : undefined}
|
|
72
|
-
help={fieldState.error?.message}
|
|
73
|
-
required={required && !hideLabel}
|
|
74
|
-
style={hideLabel ? { marginBottom: 0 } : undefined}
|
|
75
|
-
>
|
|
76
|
-
{children({
|
|
77
|
-
value: field.value,
|
|
78
|
-
onChange: field.onChange,
|
|
79
|
-
onBlur: field.onBlur,
|
|
80
|
-
disabled: meta?.disabled,
|
|
81
|
-
name: fieldName,
|
|
82
|
-
})}
|
|
83
|
-
</Form.Item>
|
|
84
|
-
)}
|
|
85
|
-
/>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
1
|
+
import { Form } from "antd";
|
|
2
|
+
import { Controller, useFormContext, type FieldValues } from "react-hook-form";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { FieldRules } from "../types";
|
|
5
|
+
import { useFormMetaOptional } from "../context/FormContext";
|
|
6
|
+
import {
|
|
7
|
+
useRegisterPayloadField,
|
|
8
|
+
useRegisterSectionField,
|
|
9
|
+
} from "../context/PayloadFieldsContext";
|
|
10
|
+
|
|
11
|
+
export type FieldWrapperProps = {
|
|
12
|
+
/** Logical field name — used for submit payload tracking on top-level fields. */
|
|
13
|
+
source: string;
|
|
14
|
+
/** Full RHF path. Defaults to `source`. Inline cells pass e.g. `lines.0.label`. */
|
|
15
|
+
name?: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
required?: boolean;
|
|
18
|
+
rules?: FieldRules;
|
|
19
|
+
/** Tabular inline cells: column header replaces the label. */
|
|
20
|
+
hideLabel?: boolean;
|
|
21
|
+
children: (props: {
|
|
22
|
+
value: unknown;
|
|
23
|
+
onChange: (value: unknown) => void;
|
|
24
|
+
onBlur: () => void;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
name: string;
|
|
27
|
+
}) => ReactNode;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function FieldWrapper({
|
|
31
|
+
source,
|
|
32
|
+
name,
|
|
33
|
+
label,
|
|
34
|
+
required,
|
|
35
|
+
rules,
|
|
36
|
+
hideLabel,
|
|
37
|
+
children,
|
|
38
|
+
}: FieldWrapperProps) {
|
|
39
|
+
const fieldName = name ?? source;
|
|
40
|
+
const isTopLevel = !fieldName.includes(".");
|
|
41
|
+
|
|
42
|
+
// react-hook-form: connect this field to the form from <ResourceForm>'s FormProvider.
|
|
43
|
+
// `control` is passed to <Controller> below so value, onChange, and validation work.
|
|
44
|
+
const { control } = useFormContext<FieldValues>();
|
|
45
|
+
|
|
46
|
+
// ding-react-admin: optional form-wide flags from <FormMetaProvider> (inside ResourceForm).
|
|
47
|
+
// Used here for `disabled` on inputs (e.g. read-only view mode). Returns null outside a form page.
|
|
48
|
+
const meta = useFormMetaOptional();
|
|
49
|
+
const fieldLabel = hideLabel ? undefined : (label ?? source);
|
|
50
|
+
const requiredMessage = label ?? source;
|
|
51
|
+
|
|
52
|
+
// Add this field's `source` to the Save payload (ResourceForm → buildFormPayload).
|
|
53
|
+
// `isTopLevel` is false when `name` contains "." (e.g. inline cell `__inline_lines.0.label`) —
|
|
54
|
+
// those values live in the inline array and are saved by saveInlineRows, not the parent PATCH.
|
|
55
|
+
useRegisterPayloadField(source, isTopLevel);
|
|
56
|
+
|
|
57
|
+
// Same top-level check: attach this field to the current FormTab / FormStep for error highlighting.
|
|
58
|
+
useRegisterSectionField(source, isTopLevel);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Controller
|
|
62
|
+
name={fieldName}
|
|
63
|
+
control={control}
|
|
64
|
+
rules={{
|
|
65
|
+
required: required ? `${requiredMessage} is required` : false,
|
|
66
|
+
...rules,
|
|
67
|
+
}}
|
|
68
|
+
render={({ field, fieldState }) => (
|
|
69
|
+
<Form.Item
|
|
70
|
+
label={fieldLabel}
|
|
71
|
+
validateStatus={fieldState.error ? "error" : undefined}
|
|
72
|
+
help={fieldState.error?.message}
|
|
73
|
+
required={required && !hideLabel}
|
|
74
|
+
style={hideLabel ? { marginBottom: 0 } : undefined}
|
|
75
|
+
>
|
|
76
|
+
{children({
|
|
77
|
+
value: field.value,
|
|
78
|
+
onChange: field.onChange,
|
|
79
|
+
onBlur: field.onBlur,
|
|
80
|
+
disabled: meta?.disabled,
|
|
81
|
+
name: fieldName,
|
|
82
|
+
})}
|
|
83
|
+
</Form.Item>
|
|
84
|
+
)}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { DeleteOutlined, PaperClipOutlined, UploadOutlined } from "@ant-design/icons";
|
|
2
|
+
import { Button, Space, Typography } from "antd";
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
import type { BaseSourceProps, FieldRules } from "../types";
|
|
5
|
+
import { FieldWrapper } from "./FieldWrapper";
|
|
6
|
+
import {
|
|
7
|
+
getUploadFileName,
|
|
8
|
+
hasUploadValue,
|
|
9
|
+
type UploadFieldValue,
|
|
10
|
+
} from "./uploadFieldUtils";
|
|
11
|
+
|
|
12
|
+
export type FileFieldProps = BaseSourceProps & {
|
|
13
|
+
name?: string;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
rules?: FieldRules;
|
|
16
|
+
hideLabel?: boolean;
|
|
17
|
+
/** Allow clearing an existing or newly selected file. */
|
|
18
|
+
clearable?: boolean;
|
|
19
|
+
accept?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function FileFieldInput({
|
|
23
|
+
value,
|
|
24
|
+
onChange,
|
|
25
|
+
disabled,
|
|
26
|
+
clearable,
|
|
27
|
+
accept,
|
|
28
|
+
}: {
|
|
29
|
+
value: UploadFieldValue;
|
|
30
|
+
onChange: (value: UploadFieldValue) => void;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
clearable?: boolean;
|
|
33
|
+
accept?: string;
|
|
34
|
+
}) {
|
|
35
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
36
|
+
const fileName = getUploadFileName(value);
|
|
37
|
+
const existingUrl = typeof value === "string" && value.length > 0 ? value : undefined;
|
|
38
|
+
const showClear = clearable && hasUploadValue(value);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
|
42
|
+
{fileName ? (
|
|
43
|
+
<Space>
|
|
44
|
+
<PaperClipOutlined />
|
|
45
|
+
{existingUrl ? (
|
|
46
|
+
<Typography.Link href={existingUrl} target="_blank" rel="noopener noreferrer">
|
|
47
|
+
{fileName}
|
|
48
|
+
</Typography.Link>
|
|
49
|
+
) : (
|
|
50
|
+
<Typography.Text>{fileName}</Typography.Text>
|
|
51
|
+
)}
|
|
52
|
+
</Space>
|
|
53
|
+
) : null}
|
|
54
|
+
<Space wrap>
|
|
55
|
+
<Button
|
|
56
|
+
icon={<UploadOutlined />}
|
|
57
|
+
disabled={disabled}
|
|
58
|
+
onClick={() => inputRef.current?.click()}
|
|
59
|
+
>
|
|
60
|
+
Choose file
|
|
61
|
+
</Button>
|
|
62
|
+
{showClear ? (
|
|
63
|
+
<Button
|
|
64
|
+
icon={<DeleteOutlined />}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
onClick={() => {
|
|
67
|
+
onChange(null);
|
|
68
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
Clear
|
|
72
|
+
</Button>
|
|
73
|
+
) : null}
|
|
74
|
+
</Space>
|
|
75
|
+
<input
|
|
76
|
+
ref={inputRef}
|
|
77
|
+
type="file"
|
|
78
|
+
accept={accept}
|
|
79
|
+
disabled={disabled}
|
|
80
|
+
tabIndex={-1}
|
|
81
|
+
aria-hidden
|
|
82
|
+
style={{ display: "none" }}
|
|
83
|
+
onChange={(e) => {
|
|
84
|
+
const file = e.target.files?.[0];
|
|
85
|
+
onChange(file ?? null);
|
|
86
|
+
e.target.value = "";
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
</Space>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function FileField({
|
|
94
|
+
source,
|
|
95
|
+
name,
|
|
96
|
+
label,
|
|
97
|
+
required,
|
|
98
|
+
rules,
|
|
99
|
+
hideLabel,
|
|
100
|
+
clearable,
|
|
101
|
+
accept,
|
|
102
|
+
}: FileFieldProps) {
|
|
103
|
+
return (
|
|
104
|
+
<FieldWrapper
|
|
105
|
+
source={source}
|
|
106
|
+
name={name}
|
|
107
|
+
label={label}
|
|
108
|
+
required={required}
|
|
109
|
+
rules={rules}
|
|
110
|
+
hideLabel={hideLabel}
|
|
111
|
+
>
|
|
112
|
+
{({ value, onChange, disabled }) => (
|
|
113
|
+
<FileFieldInput
|
|
114
|
+
value={value as UploadFieldValue}
|
|
115
|
+
onChange={onChange}
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
clearable={clearable}
|
|
118
|
+
accept={accept}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
</FieldWrapper>
|
|
122
|
+
);
|
|
123
|
+
}
|