ding-react-admin 1.0.2 → 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.
Files changed (149) hide show
  1. package/README.md +38 -13
  2. package/dist/index.js +1317 -1060
  3. package/dist/src/crud/InlineFormSet.d.ts +12 -25
  4. package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
  5. package/dist/src/crud/ResourceForm.d.ts +7 -4
  6. package/dist/src/crud/ResourceForm.d.ts.map +1 -1
  7. package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
  8. package/dist/src/crud/ResourceList.d.ts.map +1 -1
  9. package/dist/src/crud/columns/ImageColumn.d.ts +14 -0
  10. package/dist/src/crud/columns/ImageColumn.d.ts.map +1 -0
  11. package/dist/src/crud/context/InlineFieldsRegistry.d.ts +15 -0
  12. package/dist/src/crud/context/InlineFieldsRegistry.d.ts.map +1 -0
  13. package/dist/src/crud/context/PayloadFieldsContext.d.ts +18 -0
  14. package/dist/src/crud/context/PayloadFieldsContext.d.ts.map +1 -0
  15. package/dist/src/crud/fields/BooleanField.d.ts +3 -1
  16. package/dist/src/crud/fields/BooleanField.d.ts.map +1 -1
  17. package/dist/src/crud/fields/DateField.d.ts +3 -1
  18. package/dist/src/crud/fields/DateField.d.ts.map +1 -1
  19. package/dist/src/crud/fields/FieldWrapper.d.ts +7 -14
  20. package/dist/src/crud/fields/FieldWrapper.d.ts.map +1 -1
  21. package/dist/src/crud/fields/FileField.d.ts +12 -0
  22. package/dist/src/crud/fields/FileField.d.ts.map +1 -0
  23. package/dist/src/crud/fields/ImageField.d.ts +14 -0
  24. package/dist/src/crud/fields/ImageField.d.ts.map +1 -0
  25. package/dist/src/crud/fields/NumberField.d.ts +5 -3
  26. package/dist/src/crud/fields/NumberField.d.ts.map +1 -1
  27. package/dist/src/crud/fields/PasswordField.d.ts +5 -3
  28. package/dist/src/crud/fields/PasswordField.d.ts.map +1 -1
  29. package/dist/src/crud/fields/ReferenceField.d.ts +6 -5
  30. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
  31. package/dist/src/crud/fields/ReferenceManyField.d.ts +3 -1
  32. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
  33. package/dist/src/crud/fields/SelectField.d.ts +3 -1
  34. package/dist/src/crud/fields/SelectField.d.ts.map +1 -1
  35. package/dist/src/crud/fields/TextField.d.ts +5 -3
  36. package/dist/src/crud/fields/TextField.d.ts.map +1 -1
  37. package/dist/src/crud/fields/uploadFieldUtils.d.ts +5 -0
  38. package/dist/src/crud/fields/uploadFieldUtils.d.ts.map +1 -0
  39. package/dist/src/crud/fields/useUploadPreviewUrl.d.ts +4 -0
  40. package/dist/src/crud/fields/useUploadPreviewUrl.d.ts.map +1 -0
  41. package/dist/src/crud/index.d.ts +26 -9
  42. package/dist/src/crud/index.d.ts.map +1 -1
  43. package/dist/src/crud/types.d.ts +23 -20
  44. package/dist/src/crud/types.d.ts.map +1 -1
  45. package/dist/src/crud/utils/buildFormPayload.d.ts +6 -0
  46. package/dist/src/crud/utils/buildFormPayload.d.ts.map +1 -0
  47. package/dist/src/crud/utils/buildInlineRowsPayload.d.ts +6 -0
  48. package/dist/src/crud/utils/buildInlineRowsPayload.d.ts.map +1 -0
  49. package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts +5 -0
  50. package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts.map +1 -0
  51. package/dist/src/crud/utils/formErrors.d.ts +1 -1
  52. package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
  53. package/dist/src/crud/utils/getFormValue.d.ts +3 -0
  54. package/dist/src/crud/utils/getFormValue.d.ts.map +1 -0
  55. package/dist/src/crud/utils/hasUploadValues.d.ts +3 -0
  56. package/dist/src/crud/utils/hasUploadValues.d.ts.map +1 -0
  57. package/dist/src/crud/utils/nestedFieldPath.d.ts +3 -0
  58. package/dist/src/crud/utils/nestedFieldPath.d.ts.map +1 -0
  59. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +7 -0
  60. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -0
  61. package/dist/src/crud/utils/setFormValue.d.ts +3 -0
  62. package/dist/src/crud/utils/setFormValue.d.ts.map +1 -0
  63. package/dist/src/crud/utils/toFormData.d.ts +13 -0
  64. package/dist/src/crud/utils/toFormData.d.ts.map +1 -0
  65. package/dist/src/crud/utils/useAbortableEffect.d.ts +7 -0
  66. package/dist/src/crud/utils/useAbortableEffect.d.ts.map +1 -0
  67. package/dist/src/crud/utils/useFormRecord.d.ts +42 -0
  68. package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -0
  69. package/dist/src/data/abortError.d.ts +3 -0
  70. package/dist/src/data/abortError.d.ts.map +1 -0
  71. package/dist/src/data/abortError.test.d.ts +2 -0
  72. package/dist/src/data/abortError.test.d.ts.map +1 -0
  73. package/dist/src/data/createRestResourceHandlers.d.ts +6 -6
  74. package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -1
  75. package/dist/src/data/dataProviderTypes.d.ts +13 -6
  76. package/dist/src/data/dataProviderTypes.d.ts.map +1 -1
  77. package/dist/src/data/parseFormErrorHelpers.d.ts +44 -6
  78. package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
  79. package/dist/src/data/resourceHandlers.d.ts +3 -3
  80. package/dist/src/data/resourceHandlers.d.ts.map +1 -1
  81. package/dist/src/index.d.ts +5 -4
  82. package/dist/src/index.d.ts.map +1 -1
  83. package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
  84. package/package.json +59 -59
  85. package/src/crud/InlineFormSet.tsx +90 -159
  86. package/src/crud/ResourceForm.tsx +83 -204
  87. package/src/crud/ResourceFormModal.tsx +57 -45
  88. package/src/crud/ResourceList.tsx +36 -26
  89. package/src/crud/columns/ImageColumn.tsx +58 -0
  90. package/src/crud/columns/TextColumn.tsx +2 -2
  91. package/src/crud/context/InlineFieldsRegistry.tsx +61 -0
  92. package/src/crud/context/PayloadFieldsContext.tsx +62 -0
  93. package/src/crud/fields/BooleanField.tsx +22 -16
  94. package/src/crud/fields/DateField.tsx +32 -26
  95. package/src/crud/fields/FieldWrapper.tsx +87 -109
  96. package/src/crud/fields/FileField.tsx +123 -0
  97. package/src/crud/fields/ImageField.tsx +120 -0
  98. package/src/crud/fields/NumberField.tsx +36 -34
  99. package/src/crud/fields/PasswordField.tsx +41 -40
  100. package/src/crud/fields/ReferenceField.tsx +35 -130
  101. package/src/crud/fields/ReferenceManyField.tsx +31 -25
  102. package/src/crud/fields/SelectField.tsx +6 -0
  103. package/src/crud/fields/TextField.tsx +32 -30
  104. package/src/crud/fields/uploadFieldUtils.ts +21 -0
  105. package/src/crud/fields/useUploadPreviewUrl.ts +20 -0
  106. package/src/crud/index.ts +35 -14
  107. package/src/crud/types.ts +134 -130
  108. package/src/crud/utils/buildFormPayload.ts +24 -0
  109. package/src/crud/utils/buildInlineRowsPayload.ts +37 -0
  110. package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -0
  111. package/src/crud/utils/formErrors.ts +42 -38
  112. package/src/crud/utils/{getByPath.ts → getFormValue.ts} +2 -2
  113. package/src/crud/utils/hasUploadValues.ts +9 -0
  114. package/src/crud/utils/nestedFieldPath.ts +8 -0
  115. package/src/crud/utils/prepareFormSubmitBody.ts +17 -0
  116. package/src/crud/utils/{setByPath.ts → setFormValue.ts} +2 -2
  117. package/src/crud/utils/toFormData.ts +81 -0
  118. package/src/crud/utils/useAbortableEffect.ts +17 -0
  119. package/src/crud/utils/useFormRecord.ts +156 -0
  120. package/src/data/abortError.test.ts +19 -0
  121. package/src/data/abortError.ts +10 -0
  122. package/src/data/createMemoryResourceHandlers.ts +1 -1
  123. package/src/data/createRestResourceHandlers.ts +22 -16
  124. package/src/data/dataProviderTypes.ts +15 -5
  125. package/src/data/dataUtils.test.ts +118 -6
  126. package/src/data/parseFormErrorHelpers.test.ts +138 -84
  127. package/src/data/parseFormErrorHelpers.ts +311 -224
  128. package/src/data/resourceHandlers.ts +5 -4
  129. package/src/index.ts +34 -9
  130. package/src/layouts/AdminLayout.tsx +2 -1
  131. package/dist/src/crud/context/FormFieldsContext.d.ts +0 -14
  132. package/dist/src/crud/context/FormFieldsContext.d.ts.map +0 -1
  133. package/dist/src/crud/context/InlineFormContext.d.ts +0 -16
  134. package/dist/src/crud/context/InlineFormContext.d.ts.map +0 -1
  135. package/dist/src/crud/fields/useInlineOrFormField.d.ts +0 -10
  136. package/dist/src/crud/fields/useInlineOrFormField.d.ts.map +0 -1
  137. package/dist/src/crud/utils/getByPath.d.ts +0 -3
  138. package/dist/src/crud/utils/getByPath.d.ts.map +0 -1
  139. package/dist/src/crud/utils/inlineArrayName.d.ts +0 -3
  140. package/dist/src/crud/utils/inlineArrayName.d.ts.map +0 -1
  141. package/dist/src/crud/utils/pickBySources.d.ts +0 -6
  142. package/dist/src/crud/utils/pickBySources.d.ts.map +0 -1
  143. package/dist/src/crud/utils/setByPath.d.ts +0 -3
  144. package/dist/src/crud/utils/setByPath.d.ts.map +0 -1
  145. package/src/crud/context/FormFieldsContext.tsx +0 -76
  146. package/src/crud/context/InlineFormContext.tsx +0 -71
  147. package/src/crud/fields/useInlineOrFormField.tsx +0 -91
  148. package/src/crud/utils/inlineArrayName.ts +0 -4
  149. package/src/crud/utils/pickBySources.ts +0 -24
@@ -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,32 +1,38 @@
1
1
  import { Switch } from "antd";
2
2
  import type { BaseSourceProps, FieldRules } from "../types";
3
- import { useInlineOrFormField } from "./useInlineOrFormField";
3
+ import { FieldWrapper } from "./FieldWrapper";
4
4
 
5
5
  export type BooleanFieldProps = BaseSourceProps & {
6
+ name?: string;
6
7
  required?: boolean;
7
8
  rules?: FieldRules;
9
+ hideLabel?: boolean;
8
10
  };
9
11
 
10
12
  export function BooleanField({
11
13
  source,
14
+ name,
12
15
  label,
13
16
  required,
14
17
  rules,
18
+ hideLabel,
15
19
  }: BooleanFieldProps) {
16
- const field = useInlineOrFormField(
17
- source,
18
- label,
19
- required,
20
- rules,
21
- ({ value, onChange, disabled }) => (
22
- <Switch
23
- checked={Boolean(value)}
24
- onChange={onChange}
25
- disabled={disabled}
26
- />
27
- ),
20
+ return (
21
+ <FieldWrapper
22
+ source={source}
23
+ name={name}
24
+ label={label}
25
+ required={required}
26
+ rules={rules}
27
+ hideLabel={hideLabel}
28
+ >
29
+ {({ value, onChange, disabled }) => (
30
+ <Switch
31
+ checked={Boolean(value)}
32
+ onChange={onChange}
33
+ disabled={disabled}
34
+ />
35
+ )}
36
+ </FieldWrapper>
28
37
  );
29
-
30
- if (field.mode === "inline") return null;
31
- return field.element;
32
38
  }
@@ -1,47 +1,53 @@
1
1
  import { DatePicker } from "antd";
2
2
  import dayjs from "dayjs";
3
3
  import type { BaseSourceProps, FieldRules } from "../types";
4
- import { useInlineOrFormField } from "./useInlineOrFormField";
4
+ import { FieldWrapper } from "./FieldWrapper";
5
5
 
6
6
  const DATE_FORMAT = "YYYY-MM-DD";
7
7
 
8
8
  export type DateFieldProps = BaseSourceProps & {
9
+ name?: string;
9
10
  required?: boolean;
10
11
  rules?: FieldRules;
11
12
  showTime?: boolean;
13
+ hideLabel?: boolean;
12
14
  };
13
15
 
14
16
  export function DateField({
15
17
  source,
18
+ name,
16
19
  label,
17
20
  required,
18
21
  rules,
19
22
  showTime,
23
+ hideLabel,
20
24
  }: DateFieldProps) {
21
- const field = useInlineOrFormField(
22
- source,
23
- label,
24
- required,
25
- rules,
26
- ({ value, onChange, onBlur, disabled }) => (
27
- <DatePicker
28
- value={value ? dayjs(String(value)) : null}
29
- onChange={(d) =>
30
- onChange(
31
- d
32
- ? d.format(showTime ? `${DATE_FORMAT} HH:mm:ss` : DATE_FORMAT)
33
- : null,
34
- )
35
- }
36
- onBlur={onBlur}
37
- showTime={showTime}
38
- disabled={disabled}
39
- format={showTime ? `${DATE_FORMAT} HH:mm:ss` : DATE_FORMAT}
40
- style={{ width: "100%" }}
41
- />
42
- ),
25
+ return (
26
+ <FieldWrapper
27
+ source={source}
28
+ name={name}
29
+ label={label}
30
+ required={required}
31
+ rules={rules}
32
+ hideLabel={hideLabel}
33
+ >
34
+ {({ value, onChange, onBlur, disabled }) => (
35
+ <DatePicker
36
+ value={value ? dayjs(String(value)) : null}
37
+ onChange={(d) =>
38
+ onChange(
39
+ d
40
+ ? d.format(showTime ? `${DATE_FORMAT} HH:mm:ss` : DATE_FORMAT)
41
+ : null,
42
+ )
43
+ }
44
+ onBlur={onBlur}
45
+ showTime={showTime}
46
+ disabled={disabled}
47
+ format={showTime ? `${DATE_FORMAT} HH:mm:ss` : DATE_FORMAT}
48
+ style={{ width: "100%" }}
49
+ />
50
+ )}
51
+ </FieldWrapper>
43
52
  );
44
-
45
- if (field.mode === "inline") return null;
46
- return field.element;
47
53
  }
@@ -1,109 +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 { useRegisterFormSource } from "../context/FormFieldsContext";
7
-
8
- export type FieldWrapperProps = {
9
- source: string;
10
- label?: string;
11
- required?: boolean;
12
- rules?: FieldRules;
13
- children: (props: {
14
- value: unknown;
15
- onChange: (value: unknown) => void;
16
- onBlur: () => void;
17
- disabled?: boolean;
18
- }) => ReactNode;
19
- };
20
-
21
- export function FieldWrapper({
22
- source,
23
- label,
24
- required,
25
- rules,
26
- children,
27
- }: FieldWrapperProps) {
28
- const { control } = useFormContext<FieldValues>();
29
- const meta = useFormMetaOptional();
30
- const fieldLabel = label ?? source;
31
- useRegisterFormSource(source);
32
-
33
- return (
34
- <Controller
35
- name={source}
36
- control={control}
37
- rules={{
38
- required: required ? `${fieldLabel} is required` : false,
39
- ...rules,
40
- }}
41
- render={({ field, fieldState }) => (
42
- <Form.Item
43
- label={fieldLabel}
44
- validateStatus={fieldState.error ? "error" : undefined}
45
- help={fieldState.error?.message}
46
- required={required}
47
- >
48
- {children({
49
- value: field.value,
50
- onChange: field.onChange,
51
- onBlur: field.onBlur,
52
- disabled: meta?.disabled,
53
- })}
54
- </Form.Item>
55
- )}
56
- />
57
- );
58
- }
59
-
60
- export function InlineFieldWrapper({
61
- name,
62
- label,
63
- required,
64
- rules,
65
- hideLabel,
66
- children,
67
- }: {
68
- name: string;
69
- label?: string;
70
- required?: boolean;
71
- rules?: FieldRules;
72
- /** Tabular inlines use column headers — suppress per-cell labels. */
73
- hideLabel?: boolean;
74
- children: (props: {
75
- value: unknown;
76
- onChange: (value: unknown) => void;
77
- onBlur: () => void;
78
- }) => ReactNode;
79
- }) {
80
- const { control } = useFormContext<FieldValues>();
81
- const fieldLabel = hideLabel ? undefined : label;
82
- const requiredMessage = label ?? "This field";
83
-
84
- return (
85
- <Controller
86
- name={name}
87
- control={control}
88
- rules={{
89
- required: required ? `${requiredMessage} is required` : false,
90
- ...rules,
91
- }}
92
- render={({ field, fieldState }) => (
93
- <Form.Item
94
- label={fieldLabel}
95
- validateStatus={fieldState.error ? "error" : undefined}
96
- help={fieldState.error?.message}
97
- required={required && !hideLabel}
98
- style={{ marginBottom: 0 }}
99
- >
100
- {children({
101
- value: field.value,
102
- onChange: field.onChange,
103
- onBlur: field.onBlur,
104
- })}
105
- </Form.Item>
106
- )}
107
- />
108
- );
109
- }
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
+ }