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.
Files changed (169) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +254 -195
  3. package/dist/index.css +2 -0
  4. package/dist/index.js +1772 -1169
  5. package/dist/src/components/NavMenuLabel.d.ts +8 -0
  6. package/dist/src/components/NavMenuLabel.d.ts.map +1 -0
  7. package/dist/src/components/NavMenuSearch.d.ts +9 -0
  8. package/dist/src/components/NavMenuSearch.d.ts.map +1 -0
  9. package/dist/src/components/ScrollableArea.d.ts +10 -0
  10. package/dist/src/components/ScrollableArea.d.ts.map +1 -0
  11. package/dist/src/components/index.d.ts +3 -0
  12. package/dist/src/components/index.d.ts.map +1 -1
  13. package/dist/src/context/AuthProvider.d.ts +2 -0
  14. package/dist/src/context/AuthProvider.d.ts.map +1 -1
  15. package/dist/src/crud/FormGlobalErrorsAlert.d.ts +5 -0
  16. package/dist/src/crud/FormGlobalErrorsAlert.d.ts.map +1 -0
  17. package/dist/src/crud/ResourceForm.d.ts +1 -4
  18. package/dist/src/crud/ResourceForm.d.ts.map +1 -1
  19. package/dist/src/crud/ResourceFormModal.d.ts +7 -1
  20. package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
  21. package/dist/src/crud/ResourceRecordForm.d.ts +27 -0
  22. package/dist/src/crud/ResourceRecordForm.d.ts.map +1 -0
  23. package/dist/src/crud/fields/ReferenceField.d.ts +12 -2
  24. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
  25. package/dist/src/crud/fields/ReferenceInputActions.d.ts +16 -0
  26. package/dist/src/crud/fields/ReferenceInputActions.d.ts.map +1 -0
  27. package/dist/src/crud/fields/ReferenceManyField.d.ts +10 -1
  28. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
  29. package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
  30. package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
  31. package/dist/src/crud/index.d.ts +5 -0
  32. package/dist/src/crud/index.d.ts.map +1 -1
  33. package/dist/src/crud/types.d.ts +12 -0
  34. package/dist/src/crud/types.d.ts.map +1 -1
  35. package/dist/src/crud/utils/choiceSelectionUtils.d.ts +10 -0
  36. package/dist/src/crud/utils/choiceSelectionUtils.d.ts.map +1 -0
  37. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts +2 -0
  38. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts.map +1 -0
  39. package/dist/src/crud/utils/formErrors.d.ts +18 -7
  40. package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
  41. package/dist/src/crud/utils/formErrors.test.d.ts +2 -0
  42. package/dist/src/crud/utils/formErrors.test.d.ts.map +1 -0
  43. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +1 -0
  44. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -1
  45. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts +4 -0
  46. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts.map +1 -0
  47. package/dist/src/crud/utils/toFormData.d.ts +1 -1
  48. package/dist/src/crud/utils/toFormData.d.ts.map +1 -1
  49. package/dist/src/crud/utils/uploadReferenceUtils.d.ts +5 -0
  50. package/dist/src/crud/utils/uploadReferenceUtils.d.ts.map +1 -0
  51. package/dist/src/crud/utils/useChoices.d.ts +27 -1
  52. package/dist/src/crud/utils/useChoices.d.ts.map +1 -1
  53. package/dist/src/crud/utils/useFormRecord.d.ts +9 -7
  54. package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -1
  55. package/dist/src/data/parseFormErrorHelpers.d.ts +6 -27
  56. package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
  57. package/dist/src/index.d.ts +2 -2
  58. package/dist/src/index.d.ts.map +1 -1
  59. package/dist/src/layouts/AdminLayout.d.ts +1 -1
  60. package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
  61. package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -1
  62. package/dist/src/layouts/navFilter.d.ts +5 -0
  63. package/dist/src/layouts/navFilter.d.ts.map +1 -0
  64. package/dist/src/layouts/navMenuItems.d.ts +6 -0
  65. package/dist/src/layouts/navMenuItems.d.ts.map +1 -0
  66. package/dist/src/types.d.ts +12 -0
  67. package/dist/src/types.d.ts.map +1 -1
  68. package/package.json +79 -59
  69. package/src/app/AdminApp.tsx +34 -34
  70. package/src/components/AuthAlternateLink.tsx +23 -23
  71. package/src/components/DensitySwitch.tsx +31 -31
  72. package/src/components/NavMenuLabel.tsx +26 -0
  73. package/src/components/NavMenuSearch.tsx +76 -0
  74. package/src/components/ScrollableArea.tsx +46 -0
  75. package/src/components/ThemeSwitch.tsx +28 -28
  76. package/src/components/ThemeToolbar.tsx +34 -34
  77. package/src/components/index.ts +7 -4
  78. package/src/components/navMenu.css +21 -0
  79. package/src/components/scrollable.css +28 -0
  80. package/src/context/AppThemeProvider.tsx +128 -128
  81. package/src/context/AuthProvider.tsx +89 -77
  82. package/src/context/DataProvider.tsx +30 -30
  83. package/src/context/PermissionsProvider.tsx +57 -57
  84. package/src/crud/FilterBar.tsx +36 -36
  85. package/src/crud/FormGlobalErrorsAlert.tsx +36 -0
  86. package/src/crud/FormSteps.tsx +135 -135
  87. package/src/crud/FormTabs.tsx +126 -126
  88. package/src/crud/InlineFormSet.tsx +162 -162
  89. package/src/crud/ListActionsBar.tsx +106 -106
  90. package/src/crud/ResourceForm.tsx +83 -160
  91. package/src/crud/ResourceFormModal.tsx +75 -133
  92. package/src/crud/ResourceRecordForm.tsx +166 -0
  93. package/src/crud/columns/BooleanColumn.tsx +32 -32
  94. package/src/crud/columns/CustomColumn.tsx +33 -33
  95. package/src/crud/columns/DateColumn.tsx +28 -28
  96. package/src/crud/columns/ImageColumn.tsx +58 -58
  97. package/src/crud/columns/NumberColumn.tsx +31 -31
  98. package/src/crud/columns/ReferenceColumn.tsx +85 -85
  99. package/src/crud/columns/ReferenceManyColumn.tsx +67 -67
  100. package/src/crud/columns/TextColumn.tsx +38 -38
  101. package/src/crud/context/FilterContext.tsx +71 -71
  102. package/src/crud/context/FormContext.tsx +39 -39
  103. package/src/crud/context/FormSectionContext.tsx +22 -22
  104. package/src/crud/context/InlineFieldsRegistry.tsx +61 -61
  105. package/src/crud/context/ListContext.tsx +93 -93
  106. package/src/crud/fields/BooleanField.tsx +38 -38
  107. package/src/crud/fields/DateField.tsx +53 -53
  108. package/src/crud/fields/FileField.tsx +123 -123
  109. package/src/crud/fields/ImageField.tsx +120 -120
  110. package/src/crud/fields/NumberField.tsx +52 -52
  111. package/src/crud/fields/PasswordField.tsx +99 -99
  112. package/src/crud/fields/ReferenceField.tsx +237 -96
  113. package/src/crud/fields/ReferenceInputActions.tsx +106 -0
  114. package/src/crud/fields/ReferenceManyField.tsx +200 -76
  115. package/src/crud/fields/SelectField.tsx +48 -48
  116. package/src/crud/fields/TextField.tsx +46 -46
  117. package/src/crud/fields/uploadFieldUtils.ts +21 -21
  118. package/src/crud/fields/useUploadPreviewUrl.ts +20 -20
  119. package/src/crud/filters/BooleanFilter.tsx +38 -38
  120. package/src/crud/filters/DateFilter.tsx +35 -35
  121. package/src/crud/filters/NumberFilter.tsx +33 -33
  122. package/src/crud/filters/ReferenceFilter.tsx +192 -173
  123. package/src/crud/filters/SelectFilter.tsx +44 -44
  124. package/src/crud/filters/TextFilter.tsx +36 -36
  125. package/src/crud/index.ts +99 -94
  126. package/src/crud/types.ts +146 -134
  127. package/src/crud/utils/buildFormPayload.ts +24 -24
  128. package/src/crud/utils/buildInlineRowsPayload.ts +37 -37
  129. package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -29
  130. package/src/crud/utils/choiceSelectionUtils.test.ts +45 -0
  131. package/src/crud/utils/choiceSelectionUtils.ts +116 -0
  132. package/src/crud/utils/formErrors.test.ts +168 -0
  133. package/src/crud/utils/formErrors.ts +138 -42
  134. package/src/crud/utils/formSectionErrors.ts +85 -85
  135. package/src/crud/utils/getFormValue.ts +13 -13
  136. package/src/crud/utils/hasUploadValues.ts +9 -9
  137. package/src/crud/utils/nestedFieldPath.ts +8 -8
  138. package/src/crud/utils/prepareFormSubmitBody.ts +22 -17
  139. package/src/crud/utils/referenceSelectNotFoundContent.tsx +7 -0
  140. package/src/crud/utils/setFormValue.ts +18 -18
  141. package/src/crud/utils/sortQueryParam.ts +37 -37
  142. package/src/crud/utils/toFormData.ts +79 -81
  143. package/src/crud/utils/uploadReferenceUtils.ts +44 -0
  144. package/src/crud/utils/useAbortableEffect.ts +17 -17
  145. package/src/crud/utils/useChoices.ts +330 -182
  146. package/src/crud/utils/useFormRecord.ts +179 -156
  147. package/src/crud/utils/useListQueryState.ts +191 -191
  148. package/src/data/abortError.test.ts +19 -19
  149. package/src/data/abortError.ts +10 -10
  150. package/src/data/createMemoryResourceHandlers.ts +78 -78
  151. package/src/data/createRestResourceHandlers.ts +63 -63
  152. package/src/data/dataUtils.test.ts +228 -220
  153. package/src/data/inMemoryList.ts +97 -97
  154. package/src/data/parseFormErrorHelpers.test.ts +201 -138
  155. package/src/data/parseFormErrorHelpers.ts +376 -311
  156. package/src/data/sortHelpers.ts +48 -48
  157. package/src/index.ts +211 -207
  158. package/src/layouts/AdminLayout.tsx +530 -382
  159. package/src/layouts/AuthPageLayout.tsx +87 -81
  160. package/src/layouts/navFilter.ts +49 -0
  161. package/src/layouts/navMenuItems.tsx +44 -0
  162. package/src/pages/LoginPage.tsx +80 -80
  163. package/src/pages/PlaceholderPage.tsx +9 -9
  164. package/src/permissions/resourcePermissions.ts +47 -47
  165. package/src/router/createAdminRouter.tsx +64 -64
  166. package/src/router/guards.tsx +63 -63
  167. package/src/router/routeAccess.ts +89 -89
  168. package/src/types.ts +10 -0
  169. package/src/vite-env.d.ts +1 -1
@@ -1,99 +1,99 @@
1
- import { Input } from "antd";
2
- import { useWatch } from "react-hook-form";
3
- import type { BaseSourceProps, FieldRules } from "../types";
4
- import { FieldWrapper } from "./FieldWrapper";
5
-
6
- export type PasswordFieldProps = BaseSourceProps & {
7
- name?: string;
8
- required?: boolean;
9
- rules?: FieldRules;
10
- /** When set, renders a second field and validates it matches `source`. */
11
- confirmSource?: string;
12
- confirmLabel?: string;
13
- autoComplete?: string;
14
- hideLabel?: boolean;
15
- };
16
-
17
- function PasswordInputField({
18
- source,
19
- name,
20
- label,
21
- required,
22
- rules,
23
- autoComplete,
24
- hideLabel,
25
- }: PasswordFieldProps) {
26
- return (
27
- <FieldWrapper
28
- source={source}
29
- name={name}
30
- label={label}
31
- required={required}
32
- rules={rules}
33
- hideLabel={hideLabel}
34
- >
35
- {({ value, onChange, onBlur, disabled }) => (
36
- <Input.Password
37
- value={value as string | undefined}
38
- onChange={(e) => onChange(e.target.value)}
39
- onBlur={onBlur}
40
- disabled={disabled}
41
- autoComplete={autoComplete}
42
- />
43
- )}
44
- </FieldWrapper>
45
- );
46
- }
47
-
48
- export function PasswordField({
49
- source,
50
- name,
51
- label,
52
- required,
53
- rules,
54
- confirmSource,
55
- confirmLabel = "Confirm password",
56
- autoComplete = "new-password",
57
- hideLabel,
58
- }: PasswordFieldProps) {
59
- const password = useWatch({ name: name ?? source, disabled: !confirmSource });
60
-
61
- if (!confirmSource) {
62
- return (
63
- <PasswordInputField
64
- source={source}
65
- name={name}
66
- label={label}
67
- required={required}
68
- rules={rules}
69
- autoComplete={autoComplete}
70
- hideLabel={hideLabel}
71
- />
72
- );
73
- }
74
-
75
- return (
76
- <>
77
- <PasswordInputField
78
- source={source}
79
- name={name}
80
- label={label}
81
- required={required}
82
- rules={rules}
83
- autoComplete={autoComplete}
84
- hideLabel={hideLabel}
85
- />
86
- <PasswordInputField
87
- source={confirmSource}
88
- label={confirmLabel}
89
- required={required}
90
- autoComplete={autoComplete}
91
- hideLabel={hideLabel}
92
- rules={{
93
- validate: (value: string) =>
94
- !password || value === password || "Passwords do not match",
95
- }}
96
- />
97
- </>
98
- );
99
- }
1
+ import { Input } from "antd";
2
+ import { useWatch } from "react-hook-form";
3
+ import type { BaseSourceProps, FieldRules } from "../types";
4
+ import { FieldWrapper } from "./FieldWrapper";
5
+
6
+ export type PasswordFieldProps = BaseSourceProps & {
7
+ name?: string;
8
+ required?: boolean;
9
+ rules?: FieldRules;
10
+ /** When set, renders a second field and validates it matches `source`. */
11
+ confirmSource?: string;
12
+ confirmLabel?: string;
13
+ autoComplete?: string;
14
+ hideLabel?: boolean;
15
+ };
16
+
17
+ function PasswordInputField({
18
+ source,
19
+ name,
20
+ label,
21
+ required,
22
+ rules,
23
+ autoComplete,
24
+ hideLabel,
25
+ }: PasswordFieldProps) {
26
+ return (
27
+ <FieldWrapper
28
+ source={source}
29
+ name={name}
30
+ label={label}
31
+ required={required}
32
+ rules={rules}
33
+ hideLabel={hideLabel}
34
+ >
35
+ {({ value, onChange, onBlur, disabled }) => (
36
+ <Input.Password
37
+ value={value as string | undefined}
38
+ onChange={(e) => onChange(e.target.value)}
39
+ onBlur={onBlur}
40
+ disabled={disabled}
41
+ autoComplete={autoComplete}
42
+ />
43
+ )}
44
+ </FieldWrapper>
45
+ );
46
+ }
47
+
48
+ export function PasswordField({
49
+ source,
50
+ name,
51
+ label,
52
+ required,
53
+ rules,
54
+ confirmSource,
55
+ confirmLabel = "Confirm password",
56
+ autoComplete = "new-password",
57
+ hideLabel,
58
+ }: PasswordFieldProps) {
59
+ const password = useWatch({ name: name ?? source, disabled: !confirmSource });
60
+
61
+ if (!confirmSource) {
62
+ return (
63
+ <PasswordInputField
64
+ source={source}
65
+ name={name}
66
+ label={label}
67
+ required={required}
68
+ rules={rules}
69
+ autoComplete={autoComplete}
70
+ hideLabel={hideLabel}
71
+ />
72
+ );
73
+ }
74
+
75
+ return (
76
+ <>
77
+ <PasswordInputField
78
+ source={source}
79
+ name={name}
80
+ label={label}
81
+ required={required}
82
+ rules={rules}
83
+ autoComplete={autoComplete}
84
+ hideLabel={hideLabel}
85
+ />
86
+ <PasswordInputField
87
+ source={confirmSource}
88
+ label={confirmLabel}
89
+ required={required}
90
+ autoComplete={autoComplete}
91
+ hideLabel={hideLabel}
92
+ rules={{
93
+ validate: (value: string) =>
94
+ !password || value === password || "Passwords do not match",
95
+ }}
96
+ />
97
+ </>
98
+ );
99
+ }
@@ -1,96 +1,237 @@
1
- import { Select } from "antd";
2
- import type { CSSProperties } from "react";
3
- import { useMemo, useState } from "react";
4
- import type {
5
- BaseSourceProps,
6
- ChoiceOption,
7
- FieldRules,
8
- ReferenceProps,
9
- } from "../types";
10
- import { useChoices } from "../utils/useChoices";
11
- import { FieldWrapper } from "./FieldWrapper";
12
-
13
- export type ReferenceFieldProps = BaseSourceProps &
14
- ReferenceProps & {
15
- name?: string;
16
- required?: boolean;
17
- rules?: FieldRules;
18
- search?: boolean;
19
- allowClear?: boolean;
20
- disabled?: boolean;
21
- hideLabel?: boolean;
22
- /** Applied to the underlying Select / input control. */
23
- inputStyle?: CSSProperties;
24
- /** Called after the value changes (e.g. to update dependent fields). */
25
- onValueChange?: (
26
- value: unknown,
27
- option: ChoiceOption | undefined,
28
- meta: { name: string },
29
- ) => void;
30
- };
31
-
32
- export function ReferenceField({
33
- source,
34
- name,
35
- label,
36
- reference,
37
- choices,
38
- optionLabel = "name",
39
- optionValue = "id",
40
- required,
41
- rules,
42
- search,
43
- allowClear,
44
- disabled: disabledProp,
45
- hideLabel,
46
- inputStyle,
47
- onValueChange,
48
- }: ReferenceFieldProps) {
49
- const [searchText, setSearchText] = useState<string | undefined>();
50
- const { options, loading, optionForValue } = useChoices(
51
- choices,
52
- reference,
53
- optionLabel,
54
- optionValue,
55
- search ? searchText : undefined,
56
- );
57
-
58
- const selectOptions = useMemo(
59
- () =>
60
- options.map((o) => ({
61
- label: o.label,
62
- value: o.value as string | number,
63
- })),
64
- [options],
65
- );
66
-
67
- return (
68
- <FieldWrapper
69
- source={source}
70
- name={name}
71
- label={label}
72
- required={required}
73
- rules={rules}
74
- hideLabel={hideLabel}
75
- >
76
- {({ value, onChange, disabled, name: fieldName }) => (
77
- <Select
78
- value={value as string | number | undefined}
79
- onChange={(next) => {
80
- onChange(next);
81
- onValueChange?.(next, optionForValue(next), { name: fieldName });
82
- }}
83
- options={selectOptions}
84
- loading={loading && options.length === 0}
85
- showSearch={search}
86
- filterOption={search ? false : undefined}
87
- onSearch={search ? setSearchText : undefined}
88
- allowClear={allowClear}
89
- disabled={disabled || disabledProp}
90
- optionFilterProp="label"
91
- style={{ width: "100%", minWidth: 160, ...inputStyle }}
92
- />
93
- )}
94
- </FieldWrapper>
95
- );
96
- }
1
+ import { Select } from "antd";
2
+ import type { CSSProperties } from "react";
3
+ import { useMemo, useState } from "react";
4
+ import { useWatch } from "react-hook-form";
5
+ import type {
6
+ BaseSourceProps,
7
+ ChoiceOption,
8
+ FieldRules,
9
+ ReferenceProps,
10
+ } from "../types";
11
+ import { valueAsId, resolveOptionLabel } from "../utils/choiceSelectionUtils";
12
+ import { referenceSelectNotFoundContent } from "../utils/referenceSelectNotFoundContent";
13
+ import { useChoices } from "../utils/useChoices";
14
+ import { FieldWrapper } from "./FieldWrapper";
15
+ import { ReferenceInputActions } from "./ReferenceInputActions";
16
+ import type { ResourcePermissions } from "../../permissions/resourcePermissions";
17
+ import type { ReactNode } from "react";
18
+
19
+ export type ReferenceFieldProps = BaseSourceProps &
20
+ ReferenceProps & {
21
+ name?: string;
22
+ required?: boolean;
23
+ rules?: FieldRules;
24
+ search?: boolean;
25
+ allowClear?: boolean;
26
+ disabled?: boolean;
27
+ hideLabel?: boolean;
28
+ /** Applied to the underlying Select / input control. */
29
+ inputStyle?: CSSProperties;
30
+ /** Called after the value changes (e.g. to update dependent fields). */
31
+ onValueChange?: (
32
+ value: unknown,
33
+ option: ChoiceOption | undefined,
34
+ meta: { name: string },
35
+ ) => void;
36
+ /** Form fields shown in the add/edit modal. Omit to hide action buttons. */
37
+ referenceForm?: ReactNode;
38
+ /** Permissions for add/edit buttons (`add` and `change` slots). */
39
+ referencePermissions?: ResourcePermissions;
40
+ referenceTitle?: string;
41
+ referenceDefaultValues?: Record<string, unknown>;
42
+ referenceModalWidth?: number;
43
+ /** When false, hide add/edit buttons. Default true when `referenceForm` is set. */
44
+ referenceActions?: boolean;
45
+ };
46
+
47
+ type ReferenceFieldSelectProps = Omit<
48
+ ReferenceFieldProps,
49
+ "source" | "label" | "required" | "rules" | "hideLabel" | "name"
50
+ > & {
51
+ value: unknown;
52
+ onChange: (value: unknown) => void;
53
+ disabled?: boolean;
54
+ fieldName: string;
55
+ selectedRecords?: Record<string, unknown> | Record<string, unknown>[];
56
+ };
57
+
58
+ function ReferenceFieldSelect({
59
+ reference,
60
+ choices,
61
+ optionLabel = "name",
62
+ optionValue = "id",
63
+ search,
64
+ allowClear,
65
+ disabled,
66
+ inputStyle,
67
+ onValueChange,
68
+ lazy = true,
69
+ fetchSelected = true,
70
+ value,
71
+ onChange,
72
+ fieldName,
73
+ selectedRecords,
74
+ referenceForm,
75
+ referencePermissions,
76
+ referenceTitle,
77
+ referenceDefaultValues,
78
+ referenceModalWidth,
79
+ referenceActions = true,
80
+ }: ReferenceFieldSelectProps) {
81
+ const [searchText, setSearchText] = useState<string | undefined>();
82
+ const [dropdownOpen, setDropdownOpen] = useState(false);
83
+ const active = dropdownOpen || Boolean(searchText);
84
+ const selectValue = valueAsId(value, optionValue);
85
+
86
+ const { options, loading, optionForValue, reload } = useChoices(
87
+ choices,
88
+ reference,
89
+ optionLabel,
90
+ optionValue,
91
+ search ? searchText : undefined,
92
+ {
93
+ lazy,
94
+ active,
95
+ selectedValues: value,
96
+ selectedRecords,
97
+ fetchSelected,
98
+ },
99
+ );
100
+
101
+ const selectOptions = useMemo(
102
+ () =>
103
+ options.map((o) => ({
104
+ label: o.label,
105
+ value: o.value as string | number,
106
+ })),
107
+ [options],
108
+ );
109
+
110
+ const applyRecord = (record: Record<string, unknown>) => {
111
+ const id = record[optionValue];
112
+ onChange(id);
113
+ onValueChange?.(
114
+ id,
115
+ {
116
+ label: resolveOptionLabel(record, optionLabel),
117
+ value: id,
118
+ record,
119
+ },
120
+ { name: fieldName },
121
+ );
122
+ void reload();
123
+ };
124
+
125
+ const select = (
126
+ <Select
127
+ value={selectValue}
128
+ onChange={(next) => {
129
+ onChange(next);
130
+ onValueChange?.(next, optionForValue(next), { name: fieldName });
131
+ }}
132
+ options={selectOptions}
133
+ loading={loading}
134
+ notFoundContent={referenceSelectNotFoundContent(loading)}
135
+ showSearch={search}
136
+ filterOption={search ? false : undefined}
137
+ onSearch={search ? setSearchText : undefined}
138
+ onDropdownVisibleChange={(open) => {
139
+ setDropdownOpen(open);
140
+ if (!open) setSearchText(undefined);
141
+ }}
142
+ allowClear={allowClear}
143
+ disabled={disabled}
144
+ optionFilterProp="label"
145
+ style={{ width: "100%", minWidth: 160, ...inputStyle }}
146
+ />
147
+ );
148
+
149
+ if (!referenceActions) return select;
150
+
151
+ return (
152
+ <div style={{ display: "flex", gap: 8, width: "100%", alignItems: "flex-start" }}>
153
+ <div style={{ flex: 1, minWidth: 0 }}>{select}</div>
154
+ <ReferenceInputActions
155
+ reference={reference}
156
+ referenceForm={referenceForm}
157
+ referencePermissions={referencePermissions}
158
+ referenceTitle={referenceTitle}
159
+ referenceDefaultValues={referenceDefaultValues}
160
+ referenceModalWidth={referenceModalWidth}
161
+ selectedId={selectValue}
162
+ disabled={disabled}
163
+ onCreated={applyRecord}
164
+ onUpdated={() => void reload()}
165
+ />
166
+ </div>
167
+ );
168
+ }
169
+
170
+ export function ReferenceField({
171
+ source,
172
+ name,
173
+ label,
174
+ reference,
175
+ choices,
176
+ optionLabel = "name",
177
+ optionValue = "id",
178
+ required,
179
+ rules,
180
+ search,
181
+ allowClear,
182
+ disabled: disabledProp,
183
+ hideLabel,
184
+ inputStyle,
185
+ onValueChange,
186
+ lazy = true,
187
+ recordSource,
188
+ fetchSelected = true,
189
+ referenceForm,
190
+ referencePermissions,
191
+ referenceTitle,
192
+ referenceDefaultValues,
193
+ referenceModalWidth,
194
+ referenceActions = true,
195
+ }: ReferenceFieldProps) {
196
+ const embeddedRecord = useWatch({
197
+ name: recordSource ?? "",
198
+ disabled: !recordSource,
199
+ }) as Record<string, unknown> | undefined;
200
+
201
+ return (
202
+ <FieldWrapper
203
+ source={source}
204
+ name={name}
205
+ label={label}
206
+ required={required}
207
+ rules={rules}
208
+ hideLabel={hideLabel}
209
+ >
210
+ {({ value, onChange, disabled, name: fieldName }) => (
211
+ <ReferenceFieldSelect
212
+ reference={reference}
213
+ choices={choices}
214
+ optionLabel={optionLabel}
215
+ optionValue={optionValue}
216
+ search={search}
217
+ allowClear={allowClear}
218
+ disabled={disabled || disabledProp}
219
+ inputStyle={inputStyle}
220
+ onValueChange={onValueChange}
221
+ lazy={lazy}
222
+ fetchSelected={fetchSelected}
223
+ value={value}
224
+ onChange={onChange}
225
+ fieldName={fieldName}
226
+ selectedRecords={recordSource ? embeddedRecord : undefined}
227
+ referenceForm={referenceForm}
228
+ referencePermissions={referencePermissions}
229
+ referenceTitle={referenceTitle}
230
+ referenceDefaultValues={referenceDefaultValues}
231
+ referenceModalWidth={referenceModalWidth}
232
+ referenceActions={referenceActions}
233
+ />
234
+ )}
235
+ </FieldWrapper>
236
+ );
237
+ }
@@ -0,0 +1,106 @@
1
+ import { EditOutlined, PlusOutlined } from "@ant-design/icons";
2
+ import { Button, Space, Tooltip } from "antd";
3
+ import { useState, type ReactNode } from "react";
4
+ import { usePermissions } from "../../context/PermissionsProvider";
5
+ import {
6
+ checkResourcePermission,
7
+ type ResourcePermissions,
8
+ } from "../../permissions/resourcePermissions";
9
+ import { ResourceFormModal } from "../ResourceFormModal";
10
+
11
+ export type ReferenceInputActionsProps = {
12
+ reference?: string;
13
+ referenceForm?: ReactNode;
14
+ referencePermissions?: ResourcePermissions;
15
+ referenceTitle?: string;
16
+ referenceDefaultValues?: Record<string, unknown>;
17
+ referenceModalWidth?: number;
18
+ selectedId?: unknown;
19
+ disabled?: boolean;
20
+ onCreated?: (record: Record<string, unknown>) => void;
21
+ onUpdated?: (record: Record<string, unknown>) => void;
22
+ };
23
+
24
+ export function ReferenceInputActions({
25
+ reference,
26
+ referenceForm,
27
+ referencePermissions,
28
+ referenceTitle,
29
+ referenceDefaultValues,
30
+ referenceModalWidth,
31
+ selectedId,
32
+ disabled,
33
+ onCreated,
34
+ onUpdated,
35
+ }: ReferenceInputActionsProps) {
36
+ const can = usePermissions();
37
+ const title = referenceTitle ?? reference;
38
+
39
+ const canAdd =
40
+ Boolean(reference && referenceForm) &&
41
+ checkResourcePermission(can, referencePermissions, "add");
42
+ const canEdit =
43
+ Boolean(
44
+ reference && referenceForm && selectedId != null && selectedId !== "",
45
+ ) && checkResourcePermission(can, referencePermissions, "change");
46
+
47
+ const [editId, setEditId] = useState<string | null>(null);
48
+
49
+ if (!canAdd && !canEdit) return null;
50
+
51
+ const closeModal = () => setEditId(null);
52
+
53
+ return (
54
+ <>
55
+ <Space size={4}>
56
+ {canAdd ? (
57
+ <Tooltip title={`Add ${title ?? "record"}`}>
58
+ <Button
59
+ type="default"
60
+ icon={<PlusOutlined />}
61
+ disabled={disabled}
62
+ aria-label={`Add ${title ?? "record"}`}
63
+ onClick={() => setEditId("new")}
64
+ />
65
+ </Tooltip>
66
+ ) : null}
67
+ {canEdit ? (
68
+ <Tooltip title={`Edit ${title ?? "record"}`}>
69
+ <Button
70
+ type="default"
71
+ icon={<EditOutlined />}
72
+ disabled={disabled}
73
+ aria-label={`Edit ${title ?? "record"}`}
74
+ onClick={() => setEditId(String(selectedId))}
75
+ />
76
+ </Tooltip>
77
+ ) : null}
78
+ </Space>
79
+ {reference && referenceForm && editId != null ? (
80
+ <ResourceFormModal
81
+ resource={reference}
82
+ editId={editId}
83
+ onClose={closeModal}
84
+ title={
85
+ editId === "new"
86
+ ? `New ${title ?? reference}`
87
+ : `Edit ${title ?? reference}`
88
+ }
89
+ permissions={referencePermissions}
90
+ defaultValues={editId === "new" ? referenceDefaultValues : undefined}
91
+ width={referenceModalWidth}
92
+ onSuccess={(record) => {
93
+ const row = record as Record<string, unknown>;
94
+ if (editId === "new") {
95
+ onCreated?.(row);
96
+ } else {
97
+ onUpdated?.(row);
98
+ }
99
+ }}
100
+ >
101
+ {referenceForm}
102
+ </ResourceFormModal>
103
+ ) : null}
104
+ </>
105
+ );
106
+ }