@dashadmin/dash-components 1.3.24 → 1.3.26
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/dist/components/AuditLog/AuditLog.js +345 -1
- package/dist/components/AuditLog/index.js +4 -1
- package/dist/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.js +630 -3
- package/dist/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.js +53 -1
- package/dist/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.js +647 -3
- package/dist/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.js +53 -1
- package/dist/components/Json/Json.js +235 -1
- package/dist/components/JsonColorSelector/JsonColorSelector.js +1172 -1
- package/dist/components/JsonColorSelector/JsonColorSelectorEnhanced.js +863 -2
- package/dist/components/JsonColorSelector/JsonCssVarValues.js +738 -1
- package/dist/components/JsonColorSelector/components/ColorEditDialog.js +426 -1
- package/dist/components/JsonColorSelector/components/ColorPaletteItem.js +75 -1
- package/dist/components/JsonColorSelector/components/ImageColorExtractor.js +283 -1
- package/dist/components/JsonColorSelector/helpers/functions.js +101 -1
- package/dist/components/JsonColorSelector/useAsyncColorThief.js +105 -1
- package/dist/components/ListActive/ListActive.js +126 -1
- package/dist/components/ListActive/ListBoolean.js +124 -1
- package/dist/components/NotificationPreferences/NotificationPreferences.js +317 -1
- package/dist/components/NotificationPreferences/index.js +7 -1
- package/dist/components/SearchableSelects/RASearchableSelect.js +236 -1
- package/dist/components/SearchableSelects/RASearchableSelectCheckboxes.js +479 -1
- package/dist/components/SearchableSelects/RASearchableSelectChips.js +465 -1
- package/dist/components/SearchableSelects/RASearchableSelectChipsRecordContext.js +295 -1
- package/dist/components/SearchableSelects/SearchableSelect.js +202 -1
- package/dist/components/SearchableSelects/SearchableSelectCheckboxes.js +290 -1
- package/dist/components/SortableDataGrid/SortableDataGrid.js +72 -1
- package/dist/components/SortableDataGrid/SortableDataGridBody.js +167 -1
- package/dist/components/SortableDataGrid/SortableDataGridHeader.js +109 -1
- package/dist/components/SortableDataGrid/SortableDataGridRow.js +283 -1
- package/dist/components/Upload/SingleImageUploader.js +40 -1
- package/dist/components/custom/PackageCopyMethod.js +78 -1
- package/dist/components/custom/PackageRadioButtonGroup.js +45 -1
- package/dist/components/dialog/Basic.js +8 -1
- package/dist/components/dialog/Correct.js +14 -1
- package/dist/components/dialog/Error.js +14 -1
- package/dist/components/dialog/Info.js +14 -1
- package/dist/components/dialog/InfoV2.js +21 -1
- package/dist/components/dialog/NotFound.js +14 -1
- package/dist/components/dialog/QuickSearch.js +117 -1
- package/dist/components/notifications/NotificationsCenter.js +597 -1
- package/dist/components/notifications/index.js +24 -1
- package/dist/components/tables/PaginationComponent.js +36 -1
- package/dist/components/theme/AppLoadingFallback.js +38 -2
- package/dist/components/theme/ErrorBoundary.js +52 -1
- package/dist/components/theme/InitialLoader.js +13 -1
- package/dist/components/theme/NotFound.js +136 -1
- package/dist/components/theme/NotResults.js +38 -1
- package/dist/hooks/useDraggable.js +6 -1
- package/dist/hooks/useNotifications.js +8 -1
- package/dist/hooks/useQuickSearch.js +26 -1
- package/dist/index.js +66 -5567
- package/dist/utils/setNativeValue.js +17 -1
- package/package.json +149 -132
- package/src/components/AuditLog/AuditLog.tsx +476 -0
- package/src/components/AuditLog/index.ts +2 -0
- package/src/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.tsx +917 -0
- package/src/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.tsx +39 -0
- package/src/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.tsx +916 -0
- package/src/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.tsx +44 -0
- package/src/components/Json/Json.tsx +312 -0
- package/src/components/JsonColorSelector/JsonColorSelector.tsx +1435 -0
- package/src/components/JsonColorSelector/JsonColorSelectorEnhanced.tsx +1029 -0
- package/src/components/JsonColorSelector/JsonCssVarValues.tsx +863 -0
- package/src/components/JsonColorSelector/components/ColorEditDialog.tsx +491 -0
- package/src/components/JsonColorSelector/components/ColorPaletteItem.tsx +80 -0
- package/src/components/JsonColorSelector/components/ImageColorExtractor.tsx +328 -0
- package/src/components/JsonColorSelector/helpers/functions.tsx +123 -0
- package/src/components/JsonColorSelector/interfaces/interfaces.tsx +22 -0
- package/src/components/JsonColorSelector/useAsyncColorThief.tsx +97 -0
- package/src/components/ListActive/ListActive.tsx +106 -0
- package/src/components/ListActive/ListBoolean.tsx +96 -0
- package/src/components/NotificationPreferences/NotificationPreferences.tsx +463 -0
- package/src/components/NotificationPreferences/index.tsx +2 -0
- package/src/components/SearchableSelects/RASearchableSelect.tsx +300 -0
- package/src/components/SearchableSelects/RASearchableSelectCheckboxes.tsx +604 -0
- package/src/components/SearchableSelects/RASearchableSelectChips.tsx +558 -0
- package/src/components/SearchableSelects/RASearchableSelectChipsRecordContext.tsx +362 -0
- package/src/components/SearchableSelects/SearchableSelect.tsx +321 -0
- package/src/components/SearchableSelects/SearchableSelectCheckboxes.tsx +355 -0
- package/src/components/SortableDataGrid/SortableDataGrid.tsx +56 -0
- package/src/components/SortableDataGrid/SortableDataGridBody.tsx +175 -0
- package/src/components/SortableDataGrid/SortableDataGridHeader.tsx +171 -0
- package/src/components/SortableDataGrid/SortableDataGridRow.tsx +247 -0
- package/src/components/Upload/SingleImageUploader.tsx +55 -0
- package/src/components/custom/PackageCopyMethod.tsx +109 -0
- package/src/components/custom/PackageRadioButtonGroup.tsx +47 -0
- package/src/components/dialog/Basic.tsx +11 -0
- package/src/components/dialog/Correct.tsx +16 -0
- package/src/components/dialog/Error.tsx +15 -0
- package/src/components/dialog/Info.tsx +15 -0
- package/src/components/dialog/InfoV2.tsx +35 -0
- package/src/components/dialog/NotFound.tsx +18 -0
- package/src/components/dialog/QuickSearch.tsx +115 -0
- package/src/components/notifications/NotificationsCenter.tsx +792 -0
- package/src/components/notifications/index.ts +42 -0
- package/src/components/tables/PaginationComponent.tsx +17 -0
- package/src/components/theme/AppLoadingFallback.tsx +42 -0
- package/src/components/theme/ErrorBoundary.tsx +57 -0
- package/src/components/theme/InitialLoader.tsx +16 -0
- package/src/components/theme/NotFound.tsx +124 -0
- package/src/components/theme/NotResults.tsx +54 -0
- package/src/hooks/useDraggable.ts +11 -0
- package/src/hooks/useNotifications.ts +14 -0
- package/src/hooks/useQuickSearch.tsx +29 -0
- package/src/index.tsx +77 -0
- package/src/utils/setNativeValue.tsx +23 -0
- package/dist/color-thief-CTwGFhOB.js +0 -272
- /package/{dist → src}/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayAutoSearch.tsx.wip +0 -0
- /package/{dist → src}/components/JsonColorSelector/JsonColorSelector.tsx.bkup +0 -0
- /package/{dist → src}/components/SortableDataGrid/README.md +0 -0
- /package/{dist → src}/styles/index.less +0 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISearchableSelect for AutoAdmin for ReactAdmin
|
|
3
|
+
* @author Franisco Aranda <faranda@gmail.com>
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Autocomplete,
|
|
8
|
+
Box,
|
|
9
|
+
Chip,
|
|
10
|
+
CircularProgress,
|
|
11
|
+
TextField,
|
|
12
|
+
} from "@mui/material";
|
|
13
|
+
import React, { useEffect, useState } from "react";
|
|
14
|
+
import { useFormContext } from "react-hook-form";
|
|
15
|
+
import { useGetList } from "react-admin";
|
|
16
|
+
|
|
17
|
+
import { useRecordContext } from "react-admin";
|
|
18
|
+
import { isArray } from "lodash";
|
|
19
|
+
import { LoadingIndicator } from "react-admin";
|
|
20
|
+
import { IDashAutoAdminCustomFieldComponent } from "dash-auto-admin";
|
|
21
|
+
|
|
22
|
+
/* NOTES: use defaultValues = null, to infer record attribute */
|
|
23
|
+
interface ISearchableSelectChipsControlRecordContext {
|
|
24
|
+
name?: string;
|
|
25
|
+
resource?: string;
|
|
26
|
+
selectLabel?: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
renderText: (option: any, caller: string) => any;
|
|
29
|
+
transformData?: (val: any) => any;
|
|
30
|
+
defaultValues?: any;
|
|
31
|
+
isMultiple?: boolean;
|
|
32
|
+
minSearch?: number;
|
|
33
|
+
searchResults?: number;
|
|
34
|
+
timerSearch?: number;
|
|
35
|
+
isEmpty?: boolean;
|
|
36
|
+
filter?: { [x: string]: any };
|
|
37
|
+
queryFilter?: string;
|
|
38
|
+
isOptionEqualToValue: (option: any, value: any) => boolean;
|
|
39
|
+
viewAttribute: string;
|
|
40
|
+
valueKeyId?: string;
|
|
41
|
+
useBaseAttributeName?: boolean;
|
|
42
|
+
useListAttributeForController?: boolean;
|
|
43
|
+
transformOption?: (
|
|
44
|
+
option: any,
|
|
45
|
+
parsedOptions: any[],
|
|
46
|
+
caller?: string
|
|
47
|
+
) => boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const SearchableSelectChipsControlRecordContextEdit: React.FC<
|
|
51
|
+
ISearchableSelectChipsControlRecordContext & IDashAutoAdminCustomFieldComponent
|
|
52
|
+
> = ({
|
|
53
|
+
method,
|
|
54
|
+
attribute,
|
|
55
|
+
isMultiple = false,
|
|
56
|
+
searchResults = 50,
|
|
57
|
+
minSearch = 3,
|
|
58
|
+
timerSearch = 500,
|
|
59
|
+
resource,
|
|
60
|
+
selectLabel,
|
|
61
|
+
renderText,
|
|
62
|
+
transformData = (val: any) => val,
|
|
63
|
+
queryFilter = "q",
|
|
64
|
+
defaultValues = undefined,
|
|
65
|
+
isEmpty = false,
|
|
66
|
+
filter,
|
|
67
|
+
isOptionEqualToValue = undefined,
|
|
68
|
+
viewAttribute = null,
|
|
69
|
+
valueKeyId = "id",
|
|
70
|
+
useBaseAttributeName = false,
|
|
71
|
+
useListAttributeForController = false,
|
|
72
|
+
transformOption,
|
|
73
|
+
}) => {
|
|
74
|
+
const { setValue, watch } = useFormContext();
|
|
75
|
+
const record = useRecordContext();
|
|
76
|
+
|
|
77
|
+
// Get the correct attribute names
|
|
78
|
+
const saveAttributeName = attribute.attribute; // e.g., 'category_ids' - what gets saved
|
|
79
|
+
const displayAttributeName = attribute.listAttribute || attribute.attribute; // e.g., 'categories' - what gets displayed
|
|
80
|
+
|
|
81
|
+
console.log('Attribute config:', {
|
|
82
|
+
saveAttributeName,
|
|
83
|
+
displayAttributeName,
|
|
84
|
+
record: record ? {
|
|
85
|
+
[saveAttributeName]: record[saveAttributeName],
|
|
86
|
+
[displayAttributeName]: record[displayAttributeName]
|
|
87
|
+
} : null
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Component state
|
|
91
|
+
const [open, setOpen] = useState(false);
|
|
92
|
+
const [fullyLoaded, setFullyLoaded] = useState(false);
|
|
93
|
+
const [parsedOptions, setParsedOptions] = useState<any[]>([]);
|
|
94
|
+
const [q, setQ] = useState("");
|
|
95
|
+
const [selectedOptions, setSelectedOptions] = useState<any[]>([]);
|
|
96
|
+
const [timeOutSearch, setTimeOutSearch] = useState<any>(undefined);
|
|
97
|
+
|
|
98
|
+
// Watch current form value (the IDs that get saved)
|
|
99
|
+
const currentFormValue = watch(saveAttributeName);
|
|
100
|
+
|
|
101
|
+
const _transformOption = (option: any, caller: string) => {
|
|
102
|
+
if (typeof transformOption === "function") {
|
|
103
|
+
return transformOption(option, parsedOptions, caller);
|
|
104
|
+
}
|
|
105
|
+
return option;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Get includes for the API call - use the save attribute (IDs)
|
|
109
|
+
const getIncludes = () => {
|
|
110
|
+
let recordValue = record?.[saveAttributeName]; // category_ids
|
|
111
|
+
|
|
112
|
+
if (!recordValue) return [];
|
|
113
|
+
|
|
114
|
+
if (!Array.isArray(recordValue)) {
|
|
115
|
+
recordValue = [recordValue];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return recordValue.filter(Boolean);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const includes = getIncludes();
|
|
122
|
+
|
|
123
|
+
// React Admin useGetList
|
|
124
|
+
const {
|
|
125
|
+
data: resourceSearchResults,
|
|
126
|
+
total: totalSearchResults,
|
|
127
|
+
isLoading: isResourceSearchLoading,
|
|
128
|
+
error: isResourceSearchErrored,
|
|
129
|
+
} = useGetList(resource, {
|
|
130
|
+
meta: { removeSortFilters: true },
|
|
131
|
+
filter: { [queryFilter]: q, ...filter, includes: includes },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Update parsed options when search results change
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (resourceSearchResults) {
|
|
137
|
+
const resultsArray = Array.isArray(resourceSearchResults)
|
|
138
|
+
? resourceSearchResults
|
|
139
|
+
: Object.values(resourceSearchResults);
|
|
140
|
+
|
|
141
|
+
const _parsedOptions = resultsArray.map((ele, index) => ({
|
|
142
|
+
...ele,
|
|
143
|
+
key: `option-${ele[valueKeyId] || index}`,
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
setParsedOptions(_parsedOptions);
|
|
147
|
+
}
|
|
148
|
+
}, [resourceSearchResults, valueKeyId]);
|
|
149
|
+
|
|
150
|
+
// Initialize default values
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (fullyLoaded) return;
|
|
153
|
+
|
|
154
|
+
console.log('Initializing default values...');
|
|
155
|
+
|
|
156
|
+
let initialSelectedOptions: any[] = [];
|
|
157
|
+
let initialFormValue: any = isMultiple ? [] : null;
|
|
158
|
+
|
|
159
|
+
if (defaultValues === null && record) {
|
|
160
|
+
// Try to get full objects from listAttribute first (e.g., 'categories')
|
|
161
|
+
if (record[displayAttributeName] && Array.isArray(record[displayAttributeName])) {
|
|
162
|
+
initialSelectedOptions = record[displayAttributeName];
|
|
163
|
+
initialFormValue = record[displayAttributeName].map((item: any) => item[valueKeyId]);
|
|
164
|
+
}
|
|
165
|
+
// Fallback to IDs from saveAttribute (e.g., 'category_ids')
|
|
166
|
+
else if (record[saveAttributeName]) {
|
|
167
|
+
const ids = Array.isArray(record[saveAttributeName])
|
|
168
|
+
? record[saveAttributeName]
|
|
169
|
+
: [record[saveAttributeName]];
|
|
170
|
+
initialFormValue = isMultiple ? ids : ids[0];
|
|
171
|
+
// selectedOptions will be set when parsedOptions are loaded
|
|
172
|
+
}
|
|
173
|
+
} else if (defaultValues !== undefined && defaultValues !== null) {
|
|
174
|
+
if (Array.isArray(defaultValues)) {
|
|
175
|
+
initialSelectedOptions = defaultValues;
|
|
176
|
+
initialFormValue = defaultValues.map((item: any) =>
|
|
177
|
+
typeof item === 'object' ? item[valueKeyId] : item
|
|
178
|
+
);
|
|
179
|
+
} else {
|
|
180
|
+
initialSelectedOptions = [defaultValues];
|
|
181
|
+
initialFormValue = typeof defaultValues === 'object' ? defaultValues[valueKeyId] : defaultValues;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log('Initial values:', {
|
|
186
|
+
initialSelectedOptions,
|
|
187
|
+
initialFormValue
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
setSelectedOptions(initialSelectedOptions);
|
|
191
|
+
setValue(saveAttributeName, initialFormValue);
|
|
192
|
+
setFullyLoaded(true);
|
|
193
|
+
}, [record, defaultValues, fullyLoaded, saveAttributeName, displayAttributeName, valueKeyId, isMultiple]);
|
|
194
|
+
|
|
195
|
+
// Update selected options when parsedOptions change and we have form values but no selected options
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (parsedOptions.length > 0 && selectedOptions.length === 0 && currentFormValue) {
|
|
198
|
+
const formValueArray = Array.isArray(currentFormValue) ? currentFormValue : [currentFormValue];
|
|
199
|
+
const matchedOptions = formValueArray.map(id =>
|
|
200
|
+
parsedOptions.find(option => option[valueKeyId] === id)
|
|
201
|
+
).filter(Boolean);
|
|
202
|
+
|
|
203
|
+
if (matchedOptions.length > 0) {
|
|
204
|
+
console.log('Setting selected options from parsed options:', matchedOptions);
|
|
205
|
+
setSelectedOptions(matchedOptions);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}, [parsedOptions, selectedOptions, currentFormValue, valueKeyId]);
|
|
209
|
+
|
|
210
|
+
// Debounced search
|
|
211
|
+
const debounce = (fn: Function, delay = 500) => {
|
|
212
|
+
clearTimeout(timeOutSearch);
|
|
213
|
+
setTimeOutSearch(
|
|
214
|
+
setTimeout(() => {
|
|
215
|
+
fn();
|
|
216
|
+
}, delay)
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Update field value
|
|
221
|
+
const updateFieldValue = (newSelectedOptions: any[]) => {
|
|
222
|
+
console.log('Updating field value:', newSelectedOptions);
|
|
223
|
+
|
|
224
|
+
setSelectedOptions(newSelectedOptions || []);
|
|
225
|
+
|
|
226
|
+
// Save the IDs to the form
|
|
227
|
+
const idsToSave = (newSelectedOptions || []).map(option => option[valueKeyId]);
|
|
228
|
+
const valueToSave = isMultiple ? idsToSave : (idsToSave[0] || null);
|
|
229
|
+
|
|
230
|
+
console.log('Saving to form:', { attribute: saveAttributeName, value: valueToSave });
|
|
231
|
+
setValue(saveAttributeName, valueToSave);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (!fullyLoaded) return <LoadingIndicator />;
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<>
|
|
238
|
+
<Autocomplete
|
|
239
|
+
key={`autocomplete-${saveAttributeName}`}
|
|
240
|
+
multiple={isMultiple}
|
|
241
|
+
loading={isResourceSearchLoading}
|
|
242
|
+
fullWidth
|
|
243
|
+
freeSolo={false}
|
|
244
|
+
options={parsedOptions}
|
|
245
|
+
open={open}
|
|
246
|
+
onOpen={() => setOpen(true)}
|
|
247
|
+
onClose={() => setOpen(false)}
|
|
248
|
+
value={isMultiple ? selectedOptions : (selectedOptions[0] || null)}
|
|
249
|
+
isOptionEqualToValue={isOptionEqualToValue}
|
|
250
|
+
autoHighlight
|
|
251
|
+
getOptionLabel={(option: any) => {
|
|
252
|
+
if (!option) return '';
|
|
253
|
+
return renderText(_transformOption(option, "optionlabel"), "optionlabel") || '';
|
|
254
|
+
}}
|
|
255
|
+
renderOption={(props, option: any) => {
|
|
256
|
+
const { key, ...newProps } = props as any;
|
|
257
|
+
return (
|
|
258
|
+
<Box
|
|
259
|
+
key={option.key || `option-${option[valueKeyId]}`}
|
|
260
|
+
component="li"
|
|
261
|
+
sx={{ "& > img": { mr: 2, flexShrink: 0 } }}
|
|
262
|
+
{...newProps}
|
|
263
|
+
>
|
|
264
|
+
{renderText(_transformOption(option, "option"), "option")}
|
|
265
|
+
</Box>
|
|
266
|
+
);
|
|
267
|
+
}}
|
|
268
|
+
renderTags={(tagValue, getTagProps) => {
|
|
269
|
+
return tagValue.map((option, index) => (
|
|
270
|
+
<Chip
|
|
271
|
+
key={`chip-${option[valueKeyId] || index}`}
|
|
272
|
+
label={renderText(_transformOption(option, "chip"), "chip")}
|
|
273
|
+
{...getTagProps({ index })}
|
|
274
|
+
/>
|
|
275
|
+
));
|
|
276
|
+
}}
|
|
277
|
+
onChange={(event: any, rawValue) => {
|
|
278
|
+
const newValue = isMultiple ? rawValue : (rawValue ? [rawValue] : []);
|
|
279
|
+
updateFieldValue(newValue);
|
|
280
|
+
}}
|
|
281
|
+
renderInput={(params) => (
|
|
282
|
+
<TextField
|
|
283
|
+
{...params}
|
|
284
|
+
label={selectLabel}
|
|
285
|
+
variant="outlined"
|
|
286
|
+
onChange={(ev) => {
|
|
287
|
+
const searchValue = ev.target.value;
|
|
288
|
+
if (
|
|
289
|
+
searchValue &&
|
|
290
|
+
searchValue !== "" &&
|
|
291
|
+
searchValue.length >= minSearch
|
|
292
|
+
) {
|
|
293
|
+
debounce(() => setQ(searchValue), timerSearch);
|
|
294
|
+
}
|
|
295
|
+
}}
|
|
296
|
+
InputProps={{
|
|
297
|
+
...params.InputProps,
|
|
298
|
+
autoComplete: "new-password",
|
|
299
|
+
endAdornment: (
|
|
300
|
+
<>
|
|
301
|
+
{isResourceSearchLoading && (
|
|
302
|
+
<CircularProgress color="inherit" size={20} />
|
|
303
|
+
)}
|
|
304
|
+
{params.InputProps.endAdornment}
|
|
305
|
+
</>
|
|
306
|
+
),
|
|
307
|
+
}}
|
|
308
|
+
/>
|
|
309
|
+
)}
|
|
310
|
+
/>
|
|
311
|
+
</>
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
export const SearchableSelectChipsControlRecordContextView: React.FC<IDashAutoAdminCustomFieldComponent> = ({attribute,
|
|
316
|
+
resourceConfig }) => {
|
|
317
|
+
const record = useRecordContext<any>();
|
|
318
|
+
const values = Array.isArray(record[attribute.listAttribute || attribute.attribute]) ? record[attribute.listAttribute || attribute.attribute] : [record[attribute.listAttribute || attribute.attribute]];
|
|
319
|
+
return (<>
|
|
320
|
+
{record && (
|
|
321
|
+
<Box sx={{ width: "100%" }}>
|
|
322
|
+
{values.map((item: any) => (
|
|
323
|
+
<Chip
|
|
324
|
+
key={item.id}
|
|
325
|
+
label={item.name}
|
|
326
|
+
sx={{ margin: 0.5 }}
|
|
327
|
+
/>
|
|
328
|
+
))}
|
|
329
|
+
</Box>
|
|
330
|
+
)}
|
|
331
|
+
</>);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
const SearchableSelectChipsControlRecordContext: React.FC<
|
|
336
|
+
IDashAutoAdminCustomFieldComponent & ISearchableSelectChipsControlRecordContext
|
|
337
|
+
> = ({ method, attribute, resourceConfig,...props }) => {
|
|
338
|
+
switch (method) {
|
|
339
|
+
case "edit":
|
|
340
|
+
case "create":
|
|
341
|
+
return (
|
|
342
|
+
<SearchableSelectChipsControlRecordContextEdit
|
|
343
|
+
resourceConfig={resourceConfig}
|
|
344
|
+
attribute={attribute}
|
|
345
|
+
method={method}
|
|
346
|
+
{...props}
|
|
347
|
+
/>
|
|
348
|
+
);
|
|
349
|
+
case "view":
|
|
350
|
+
case "list":
|
|
351
|
+
return <SearchableSelectChipsControlRecordContextView
|
|
352
|
+
resourceConfig={resourceConfig}
|
|
353
|
+
attribute={attribute}
|
|
354
|
+
method={method}
|
|
355
|
+
{...props}
|
|
356
|
+
/>
|
|
357
|
+
default:
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
export default SearchableSelectChipsControlRecordContext;
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Auto Admin - SearchableSelect
|
|
3
|
+
*
|
|
4
|
+
* @author Franisco Aranda <francisco.aranda@dash.cl> <faranda@gmail.com>
|
|
5
|
+
* @author Omar Muñoz <omar.munoz@dash.cl>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Autocomplete,
|
|
10
|
+
AutocompleteProps,
|
|
11
|
+
Box,
|
|
12
|
+
CircularProgress,
|
|
13
|
+
TextField,
|
|
14
|
+
} from '@mui/material';
|
|
15
|
+
import React, { JSX, useEffect, useState } from 'react';
|
|
16
|
+
import { useController } from 'react-hook-form';
|
|
17
|
+
import { useGetList } from 'react-admin';
|
|
18
|
+
import { isArray } from 'lodash';
|
|
19
|
+
|
|
20
|
+
export interface ISearchableSelect<
|
|
21
|
+
T,
|
|
22
|
+
Multiple extends boolean | undefined,
|
|
23
|
+
DisableClearable extends boolean | undefined,
|
|
24
|
+
FreeSolo extends boolean | undefined = undefined,
|
|
25
|
+
> extends Omit<AutocompleteProps<T | string, Multiple, DisableClearable, FreeSolo>, 'renderInput' | 'options'> {
|
|
26
|
+
/** Options are ingored because they are calculated within this component */
|
|
27
|
+
/** renderInput is ignored because is implemented privately within this component */
|
|
28
|
+
|
|
29
|
+
name: string;
|
|
30
|
+
/** url path to the resource, a getList or getForSelect e.g: 'system/user' */
|
|
31
|
+
resource: string;
|
|
32
|
+
/** label for the select */
|
|
33
|
+
selectLabel: string;
|
|
34
|
+
/** Optional function to parse the item and return a string or JSX.Element to show in the selector list*/
|
|
35
|
+
renderText?: (object: any) => string | JSX.Element;
|
|
36
|
+
/** Optional function will transform the item at the moment of field.onChange */
|
|
37
|
+
transformData?: (val: any) => any;
|
|
38
|
+
/** Optional defaultValue, or defaultValues array */
|
|
39
|
+
defaultValues?: any;
|
|
40
|
+
/** Specify if the component must handle multiple options */
|
|
41
|
+
isMultiple?: boolean;
|
|
42
|
+
/** @deprecated pagination, must be sent on filters */
|
|
43
|
+
//pagination?: boolean,
|
|
44
|
+
/** Min character input to perform search */
|
|
45
|
+
minSearch?: number;
|
|
46
|
+
/** @deprecated searchResults, must be sent on filters as perPage */
|
|
47
|
+
//searchResults?: number;
|
|
48
|
+
/** Search after x ms */
|
|
49
|
+
timerSearch?: number;
|
|
50
|
+
/** isEmpty - TODO: this flow is not tested. */
|
|
51
|
+
isEmpty?: boolean;
|
|
52
|
+
/** filter - optional parameters for the axios query */
|
|
53
|
+
filter?: { [x: string]: any };
|
|
54
|
+
/** name of the query parameter, usually just 'q' or 'search' */
|
|
55
|
+
queryFilter?: string;
|
|
56
|
+
/**
|
|
57
|
+
* The default function will compare the raw values equality
|
|
58
|
+
* the edge case is when a transformData is implemented, then the isOptionEqualToValue will have to compare to the transformation output.
|
|
59
|
+
* e.g: isOptionEqualToValue={(option, value) => value === option.id }
|
|
60
|
+
transformData={(data) => data?.id || null}
|
|
61
|
+
*/
|
|
62
|
+
isOptionEqualToValue?: (option, value) => boolean;
|
|
63
|
+
/**
|
|
64
|
+
* if not specified it will render the raw value for options
|
|
65
|
+
* if the response is not just a string or number array, you will have to specify which attribute from the option object you want to render
|
|
66
|
+
*/
|
|
67
|
+
renderAttribute?: string;
|
|
68
|
+
/** elementKeyID, Advanced usage, by default id, specify the attribute from the option object representing the key or id */
|
|
69
|
+
elementKeyId?: string;
|
|
70
|
+
/** onChange */
|
|
71
|
+
onChange?: (value: any) => void;
|
|
72
|
+
/* renderInputTextFieldProps - Partial<TextFieldProps> */
|
|
73
|
+
renderInputTextFieldProps?: any; // It should be: Partial<TextFieldProps>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const SearchableSelect = function WrappedAutoComplete<
|
|
77
|
+
T,
|
|
78
|
+
Multiple extends boolean | undefined = undefined,
|
|
79
|
+
DisableClearable extends boolean | undefined = undefined,
|
|
80
|
+
FreeSolo extends boolean | undefined = undefined,
|
|
81
|
+
>({ ...props }: ISearchableSelect<T, Multiple, DisableClearable, FreeSolo>) {
|
|
82
|
+
const {
|
|
83
|
+
isMultiple = false,
|
|
84
|
+
//pagination = true,
|
|
85
|
+
//searchResults = 50,
|
|
86
|
+
minSearch = 3,
|
|
87
|
+
timerSearch = 500,
|
|
88
|
+
resource,
|
|
89
|
+
selectLabel,
|
|
90
|
+
renderText,
|
|
91
|
+
transformData,
|
|
92
|
+
queryFilter = 'q',
|
|
93
|
+
defaultValues = undefined,
|
|
94
|
+
elementKeyId = 'id',
|
|
95
|
+
isEmpty = false,
|
|
96
|
+
filter,
|
|
97
|
+
isOptionEqualToValue = (option, value) => value === option,
|
|
98
|
+
renderAttribute = null,
|
|
99
|
+
name,
|
|
100
|
+
onChange,
|
|
101
|
+
renderInputTextFieldProps,
|
|
102
|
+
//..._rest
|
|
103
|
+
} = props;
|
|
104
|
+
//const element_key = "system_marketplace_category_id";
|
|
105
|
+
const [open, setOpen] = useState(false);
|
|
106
|
+
/* parsedOptions se setea null, esto para que el control no aparezca hasta que se obtenga la data */
|
|
107
|
+
const [parsedOptions, setParsedOptions] = useState(null);
|
|
108
|
+
const [q, setQ] = useState('');
|
|
109
|
+
|
|
110
|
+
const field = useController({
|
|
111
|
+
name: name,
|
|
112
|
+
defaultValue: defaultValues ? defaultValues : null,
|
|
113
|
+
});
|
|
114
|
+
const includes =
|
|
115
|
+
defaultValues && isArray(defaultValues)
|
|
116
|
+
? defaultValues.map((element) =>
|
|
117
|
+
element[elementKeyId]
|
|
118
|
+
? element[elementKeyId]
|
|
119
|
+
: element)
|
|
120
|
+
: defaultValues;
|
|
121
|
+
|
|
122
|
+
const {
|
|
123
|
+
data: resourceData,
|
|
124
|
+
isLoading,
|
|
125
|
+
} = useGetList(
|
|
126
|
+
resource,
|
|
127
|
+
{
|
|
128
|
+
filter: { [queryFilter]: q, ...filter, includes: includes },
|
|
129
|
+
},
|
|
130
|
+
{ refetchOnWindowFocus: false },
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (resourceData && !isLoading) {
|
|
135
|
+
//Le agrega el key, para evitar el warning de react.
|
|
136
|
+
//setParsedOptions([...parsedOptions,...resourceData.map(ele => { return {...ele,key:"option"+ele.id}})])
|
|
137
|
+
if (parsedOptions)
|
|
138
|
+
setParsedOptions([
|
|
139
|
+
...parsedOptions,
|
|
140
|
+
...resourceData.map((ele) => {
|
|
141
|
+
return { ...ele, key: 'option' + ele.id };
|
|
142
|
+
}),
|
|
143
|
+
]);
|
|
144
|
+
setParsedOptions(
|
|
145
|
+
resourceData.map((ele) => {
|
|
146
|
+
return { ...ele, key: 'option' + ele.id };
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
} /*else {
|
|
150
|
+
// Setea la data a un arreglo vacío, ya que si es null, se ocultará el componente.
|
|
151
|
+
//setParsedOptions([]);
|
|
152
|
+
}*/
|
|
153
|
+
return () => setParsedOptions([]);
|
|
154
|
+
// TODO! reset callback, empty parsedOptions.
|
|
155
|
+
}, [resourceData, isLoading]);
|
|
156
|
+
|
|
157
|
+
const [timeOutSearch, setTimeOutSearch] = useState(undefined);
|
|
158
|
+
|
|
159
|
+
const debounce = (fn: Function, delay = 500) => {
|
|
160
|
+
clearTimeout(timeOutSearch);
|
|
161
|
+
setTimeOutSearch(
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
fn();
|
|
164
|
+
}, delay),
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const calculateDefaultValue = () => {
|
|
169
|
+
let value = null;
|
|
170
|
+
if (isMultiple) value = [];
|
|
171
|
+
if (isMultiple && field.field.value && Array.isArray(field.field.value))
|
|
172
|
+
value = field.field.value;
|
|
173
|
+
if (isMultiple && field.field.value && !Array.isArray(field.field.value))
|
|
174
|
+
value = [field.field.value];
|
|
175
|
+
if (!isMultiple && field.field.value) value = field.field.value;
|
|
176
|
+
return value;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const defaultValue = calculateDefaultValue();
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<>
|
|
183
|
+
{isLoading && !parsedOptions ? (
|
|
184
|
+
<CircularProgress color='inherit' size={20} />
|
|
185
|
+
) : (
|
|
186
|
+
<></>
|
|
187
|
+
)}
|
|
188
|
+
{parsedOptions && (
|
|
189
|
+
<Autocomplete
|
|
190
|
+
key={'autocomplete-' + name}
|
|
191
|
+
multiple={isMultiple}
|
|
192
|
+
loading={isLoading && open}
|
|
193
|
+
//sx={{ width: 373 }}
|
|
194
|
+
fullWidth
|
|
195
|
+
freeSolo={true}
|
|
196
|
+
//options={!resourceData ? [] : !defaultValues ? resourceData : resourceData}
|
|
197
|
+
//options={resourceData ? resourceData.map(ele => { return {...ele,key:ele.id}} ) : []}
|
|
198
|
+
options={parsedOptions}
|
|
199
|
+
open={open}
|
|
200
|
+
onOpen={() => setOpen(true)}
|
|
201
|
+
onClose={() => setOpen(false)}
|
|
202
|
+
/*defaultValue={field.field?.value && field.field.value.map(item => parsedOptions.find(
|
|
203
|
+
(element) => {
|
|
204
|
+
|
|
205
|
+
return element.id === item.id
|
|
206
|
+
}))}*/
|
|
207
|
+
//value={isOptionEqualToValue && parsedOptions ? parsedOptions.find(ele => isOptionEqualToValue(ele,field.field.value))[renderAttribute] : field.field.value}
|
|
208
|
+
|
|
209
|
+
defaultValue={defaultValue}
|
|
210
|
+
//value={field.field.value}
|
|
211
|
+
|
|
212
|
+
isOptionEqualToValue={isOptionEqualToValue}
|
|
213
|
+
autoHighlight
|
|
214
|
+
//{...(!isEmpty && { value: field.field.value })}
|
|
215
|
+
//{...(isEmpty && { renderTags: (value, getTagProps) => <></> })}
|
|
216
|
+
// 8
|
|
217
|
+
|
|
218
|
+
getOptionLabel={(option: any) => {
|
|
219
|
+
const _option = !Array.isArray(option) // si es un numero como 'id', busca la opción en los valores con isOptionEqualToValue, sino devuelve el objeto original.
|
|
220
|
+
? parsedOptions &&
|
|
221
|
+
parsedOptions.find((ele) =>
|
|
222
|
+
isOptionEqualToValue(ele, field.field.value))
|
|
223
|
+
: option;
|
|
224
|
+
if (!_option) return q; //es posbile que no se haya encontrado en parsedOptions o que parsedOptions aún no esté definido.
|
|
225
|
+
const r =
|
|
226
|
+
renderAttribute && !renderText
|
|
227
|
+
? _option[renderAttribute]
|
|
228
|
+
: renderText(_option);
|
|
229
|
+
/*console.log(
|
|
230
|
+
"getOptionLabel",
|
|
231
|
+
parsedOptions.find((ele) =>
|
|
232
|
+
isOptionEqualToValue(ele, field.field.value)
|
|
233
|
+
),
|
|
234
|
+
r
|
|
235
|
+
);*/
|
|
236
|
+
if (!r) return q;
|
|
237
|
+
return r;
|
|
238
|
+
}}
|
|
239
|
+
//getOptionLabel={(option: any) => option}
|
|
240
|
+
|
|
241
|
+
renderOption={(p, option: any) => {
|
|
242
|
+
const { key, ...newProps } = p as any;
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<Box
|
|
246
|
+
key={option.key}
|
|
247
|
+
component='li'
|
|
248
|
+
sx={{ '& > img': { mr: 2, flexShrink: 0 } }}
|
|
249
|
+
{...newProps}
|
|
250
|
+
>
|
|
251
|
+
{renderAttribute && !renderText
|
|
252
|
+
? option[renderAttribute]
|
|
253
|
+
: renderText(option)}
|
|
254
|
+
</Box>
|
|
255
|
+
);
|
|
256
|
+
}}
|
|
257
|
+
onChange={(event: any, rawValue) => {
|
|
258
|
+
//console.log("onChange", event, rawValue);
|
|
259
|
+
let newValue = rawValue;
|
|
260
|
+
|
|
261
|
+
if (Array.isArray(rawValue)) {
|
|
262
|
+
newValue = isEmpty
|
|
263
|
+
? [...rawValue, ...field.field.value]
|
|
264
|
+
: [...new Map(newValue.map((v) => [v.id, v])).values()];
|
|
265
|
+
}
|
|
266
|
+
if (transformData) {
|
|
267
|
+
field.field.onChange(transformData(newValue));
|
|
268
|
+
} else {
|
|
269
|
+
field.field.onChange(newValue);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (onChange) { onChange(newValue); }
|
|
273
|
+
}}
|
|
274
|
+
renderInput={(params) => {
|
|
275
|
+
//console.log("renderInput", params.id);
|
|
276
|
+
return (
|
|
277
|
+
<TextField
|
|
278
|
+
key={params.id}
|
|
279
|
+
{...params}
|
|
280
|
+
label={selectLabel}
|
|
281
|
+
variant='outlined'
|
|
282
|
+
onChange={(ev) => {
|
|
283
|
+
// dont fire API if the user delete or not entered anything
|
|
284
|
+
const searchValue = ev.target.value;
|
|
285
|
+
//onSearch={(searchValue) => {}
|
|
286
|
+
|
|
287
|
+
if (
|
|
288
|
+
searchValue &&
|
|
289
|
+
searchValue !== '' &&
|
|
290
|
+
searchValue.length >= minSearch
|
|
291
|
+
)
|
|
292
|
+
debounce(() => setQ(searchValue), timerSearch);
|
|
293
|
+
|
|
294
|
+
/*if (ev.target.value !== "" || ev.target.value !== null) {
|
|
295
|
+
setQ(ev.target.value);
|
|
296
|
+
}*/
|
|
297
|
+
}}
|
|
298
|
+
//onBlur={() => setQ('')}
|
|
299
|
+
InputProps={{
|
|
300
|
+
...params.InputProps,
|
|
301
|
+
autoComplete: 'new-password',
|
|
302
|
+
endAdornment: (
|
|
303
|
+
<>
|
|
304
|
+
{isLoading ? (
|
|
305
|
+
<CircularProgress color='inherit' size={20} />
|
|
306
|
+
) : null}
|
|
307
|
+
{params.InputProps.endAdornment}
|
|
308
|
+
</>
|
|
309
|
+
),
|
|
310
|
+
}}
|
|
311
|
+
{...renderInputTextFieldProps}
|
|
312
|
+
/>
|
|
313
|
+
);
|
|
314
|
+
}}
|
|
315
|
+
/>
|
|
316
|
+
)}
|
|
317
|
+
</>
|
|
318
|
+
);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export default SearchableSelect;
|