@dashadmin/dash-components 1.3.25 → 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,604 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone SearchableSelectCheckboxes for filters with checkbox support
|
|
3
|
+
* @author Francisco Aranda <francisco.aranda@sudo.cl> <faranda@gmail.com>
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Autocomplete,
|
|
8
|
+
Box,
|
|
9
|
+
Checkbox,
|
|
10
|
+
CircularProgress,
|
|
11
|
+
TextField,
|
|
12
|
+
Typography,
|
|
13
|
+
Chip,
|
|
14
|
+
FormControlLabel,
|
|
15
|
+
List,
|
|
16
|
+
ListItem,
|
|
17
|
+
ListItemButton,
|
|
18
|
+
ListItemIcon,
|
|
19
|
+
ListItemText,
|
|
20
|
+
Paper,
|
|
21
|
+
Popper,
|
|
22
|
+
ClickAwayListener,
|
|
23
|
+
InputAdornment,
|
|
24
|
+
IconButton,
|
|
25
|
+
} from "@mui/material";
|
|
26
|
+
import React, { useEffect, useState, useRef } from "react";
|
|
27
|
+
import { useGetList } from "react-admin";
|
|
28
|
+
import { LoadingIndicator } from "react-admin";
|
|
29
|
+
import SearchIcon from "@mui/icons-material/Search";
|
|
30
|
+
import ClearIcon from "@mui/icons-material/Clear";
|
|
31
|
+
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
32
|
+
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
|
33
|
+
|
|
34
|
+
interface ISearchableSelectCheckboxes {
|
|
35
|
+
name?: string;
|
|
36
|
+
resource?: string;
|
|
37
|
+
selectLabel?: string;
|
|
38
|
+
title?: string;
|
|
39
|
+
renderText: (option: any, caller: string) => any;
|
|
40
|
+
transformData?: (val: any) => any;
|
|
41
|
+
defaultValues?: any;
|
|
42
|
+
value?: any;
|
|
43
|
+
onChange?: (value: any) => void;
|
|
44
|
+
isMultiple?: boolean;
|
|
45
|
+
minSearch?: number;
|
|
46
|
+
searchResults?: number;
|
|
47
|
+
minOptions?: number;
|
|
48
|
+
timerSearch?: number;
|
|
49
|
+
isEmpty?: boolean;
|
|
50
|
+
filter?: { [x: string]: any };
|
|
51
|
+
queryFilter?: string;
|
|
52
|
+
isOptionEqualToValue: (option: any, value: any) => boolean;
|
|
53
|
+
viewAttribute: string;
|
|
54
|
+
valueKeyId?: string;
|
|
55
|
+
transformOption?: (
|
|
56
|
+
option: any,
|
|
57
|
+
parsedOptions: any[],
|
|
58
|
+
caller?: string
|
|
59
|
+
) => boolean;
|
|
60
|
+
disabled?: boolean;
|
|
61
|
+
placeholder?: string;
|
|
62
|
+
emptyText?: string;
|
|
63
|
+
fullWidth?: boolean;
|
|
64
|
+
maxHeight?: number;
|
|
65
|
+
showSelectAll?: boolean;
|
|
66
|
+
showSelectedCount?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const SearchableSelectCheckboxes: React.FC<ISearchableSelectCheckboxes> = ({
|
|
70
|
+
isMultiple = true, // Default to true for checkbox version
|
|
71
|
+
searchResults = 50,
|
|
72
|
+
minSearch = 1,
|
|
73
|
+
minOptions = 10,
|
|
74
|
+
timerSearch = 300,
|
|
75
|
+
resource,
|
|
76
|
+
selectLabel,
|
|
77
|
+
renderText,
|
|
78
|
+
transformData = (val: any) => val,
|
|
79
|
+
queryFilter = "q",
|
|
80
|
+
defaultValues = undefined,
|
|
81
|
+
value,
|
|
82
|
+
onChange,
|
|
83
|
+
isEmpty = false,
|
|
84
|
+
filter,
|
|
85
|
+
isOptionEqualToValue,
|
|
86
|
+
viewAttribute = null,
|
|
87
|
+
valueKeyId = "id",
|
|
88
|
+
transformOption,
|
|
89
|
+
disabled = false,
|
|
90
|
+
placeholder = "",
|
|
91
|
+
emptyText = "",
|
|
92
|
+
fullWidth = true,
|
|
93
|
+
maxHeight = 300,
|
|
94
|
+
showSelectAll = true,
|
|
95
|
+
showSelectedCount = true,
|
|
96
|
+
}) => {
|
|
97
|
+
// Component state
|
|
98
|
+
const [open, setOpen] = useState(false);
|
|
99
|
+
const [fullyLoaded, setFullyLoaded] = useState(false);
|
|
100
|
+
const [parsedOptions, setParsedOptions] = useState<any[]>([]);
|
|
101
|
+
const [q, setQ] = useState("");
|
|
102
|
+
const [selectedOptions, setSelectedOptions] = useState<any[]>([]);
|
|
103
|
+
const [timeOutSearch, setTimeOutSearch] = useState<any>(undefined);
|
|
104
|
+
const anchorRef = useRef<HTMLDivElement>(null);
|
|
105
|
+
const [searchInputValue, setSearchInputValue] = useState("");
|
|
106
|
+
|
|
107
|
+
const _transformOption = (option: any, caller: string) => {
|
|
108
|
+
if (typeof transformOption === "function") {
|
|
109
|
+
return transformOption(option, parsedOptions, caller);
|
|
110
|
+
}
|
|
111
|
+
return option;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Get includes for the API call
|
|
115
|
+
const getIncludes = () => {
|
|
116
|
+
const currentValue = value || defaultValues;
|
|
117
|
+
if (!currentValue) return [];
|
|
118
|
+
|
|
119
|
+
if (Array.isArray(currentValue)) {
|
|
120
|
+
return currentValue.map(item =>
|
|
121
|
+
typeof item === 'object' ? item[valueKeyId] : item
|
|
122
|
+
).filter(Boolean);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const id = typeof currentValue === 'object' ? currentValue[valueKeyId] : currentValue;
|
|
126
|
+
return id ? [id] : [];
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const includes = getIncludes();
|
|
130
|
+
|
|
131
|
+
// Build filter parameters for mixed results
|
|
132
|
+
const buildFilterParams = () => {
|
|
133
|
+
const baseFilter = { ...filter };
|
|
134
|
+
|
|
135
|
+
// Add includes if we have selected values
|
|
136
|
+
if (includes.length > 0) {
|
|
137
|
+
baseFilter.includes = includes;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// If we have a search query, enable mixed results
|
|
141
|
+
if (q && q.length >= minSearch) {
|
|
142
|
+
baseFilter[queryFilter] = q;
|
|
143
|
+
baseFilter.mixed_results = true;
|
|
144
|
+
baseFilter.min_options = minOptions;
|
|
145
|
+
baseFilter.limit = searchResults;
|
|
146
|
+
} else {
|
|
147
|
+
// No search, just get default options with minimum count
|
|
148
|
+
baseFilter.limit = Math.max(minOptions, searchResults);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return baseFilter;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// React Admin useGetList with dynamic filter
|
|
155
|
+
const {
|
|
156
|
+
data: resourceSearchResults,
|
|
157
|
+
isLoading: isResourceSearchLoading,
|
|
158
|
+
error: isResourceSearchErrored,
|
|
159
|
+
} = useGetList(resource, {
|
|
160
|
+
meta: { removeSortFilters: true },
|
|
161
|
+
filter: buildFilterParams(),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Update parsed options when search results change
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (resourceSearchResults) {
|
|
167
|
+
const resultsArray = Array.isArray(resourceSearchResults)
|
|
168
|
+
? resourceSearchResults
|
|
169
|
+
: Object.values(resourceSearchResults);
|
|
170
|
+
|
|
171
|
+
let processedOptions = resultsArray.map((ele, index) => {
|
|
172
|
+
// Determine if this is a search match
|
|
173
|
+
const isSearchMatch = q && q.length >= minSearch ?
|
|
174
|
+
(ele[viewAttribute] && ele[viewAttribute].toLowerCase().includes(q.toLowerCase())) :
|
|
175
|
+
true;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
...ele,
|
|
179
|
+
key: `option-${ele[valueKeyId] || index}`,
|
|
180
|
+
isSearchMatch,
|
|
181
|
+
priority: isSearchMatch ? 1 : 2,
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Sort by priority (search matches first) and then by name
|
|
186
|
+
processedOptions.sort((a, b) => {
|
|
187
|
+
if (a.priority !== b.priority) {
|
|
188
|
+
return a.priority - b.priority;
|
|
189
|
+
}
|
|
190
|
+
const nameA = a[viewAttribute] || a.name || '';
|
|
191
|
+
const nameB = b[viewAttribute] || b.name || '';
|
|
192
|
+
return nameA.localeCompare(nameB);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Limit results
|
|
196
|
+
if (processedOptions.length > searchResults) {
|
|
197
|
+
processedOptions = processedOptions.slice(0, searchResults);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
setParsedOptions(processedOptions);
|
|
201
|
+
}
|
|
202
|
+
}, [resourceSearchResults, valueKeyId, q, minSearch, viewAttribute, searchResults]);
|
|
203
|
+
|
|
204
|
+
// Initialize selected options from value or defaultValues
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const currentValue = value !== undefined ? value : defaultValues;
|
|
207
|
+
|
|
208
|
+
if (currentValue === null || currentValue === undefined) {
|
|
209
|
+
setSelectedOptions([]);
|
|
210
|
+
setFullyLoaded(true);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let initialSelectedOptions: any[] = [];
|
|
215
|
+
|
|
216
|
+
if (Array.isArray(currentValue)) {
|
|
217
|
+
initialSelectedOptions = currentValue;
|
|
218
|
+
} else if (currentValue) {
|
|
219
|
+
initialSelectedOptions = [currentValue];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
setSelectedOptions(initialSelectedOptions);
|
|
223
|
+
setFullyLoaded(true);
|
|
224
|
+
}, [value, defaultValues]);
|
|
225
|
+
|
|
226
|
+
// Update selected options when parsedOptions change and we have IDs but no full objects
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (parsedOptions.length > 0 && selectedOptions.length > 0) {
|
|
229
|
+
const updatedOptions = selectedOptions.map(selectedOption => {
|
|
230
|
+
// If it's already a full object, keep it
|
|
231
|
+
if (typeof selectedOption === 'object' && selectedOption[viewAttribute]) {
|
|
232
|
+
return selectedOption;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// If it's just an ID, find the full object
|
|
236
|
+
const id = typeof selectedOption === 'object' ? selectedOption[valueKeyId] : selectedOption;
|
|
237
|
+
return parsedOptions.find(option => option[valueKeyId] === id) || selectedOption;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
setSelectedOptions(updatedOptions);
|
|
241
|
+
}
|
|
242
|
+
}, [parsedOptions, viewAttribute, valueKeyId]);
|
|
243
|
+
|
|
244
|
+
// Debounced search
|
|
245
|
+
const debounce = (fn: Function, delay = 300) => {
|
|
246
|
+
clearTimeout(timeOutSearch);
|
|
247
|
+
setTimeOutSearch(
|
|
248
|
+
setTimeout(() => {
|
|
249
|
+
fn();
|
|
250
|
+
}, delay)
|
|
251
|
+
);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Handle search input change
|
|
255
|
+
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
256
|
+
const searchValue = event.target.value;
|
|
257
|
+
setSearchInputValue(searchValue);
|
|
258
|
+
|
|
259
|
+
if (searchValue && searchValue !== "" && searchValue.length >= minSearch) {
|
|
260
|
+
debounce(() => setQ(searchValue), timerSearch);
|
|
261
|
+
} else if (searchValue === "") {
|
|
262
|
+
setQ("");
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Clear search
|
|
267
|
+
const handleClearSearch = () => {
|
|
268
|
+
setSearchInputValue("");
|
|
269
|
+
setQ("");
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Check if option is selected
|
|
273
|
+
const isOptionSelected = (option: any) => {
|
|
274
|
+
return selectedOptions.some(selected => {
|
|
275
|
+
const selectedId = typeof selected === 'object' ? selected[valueKeyId] : selected;
|
|
276
|
+
const optionId = typeof option === 'object' ? option[valueKeyId] : option;
|
|
277
|
+
return selectedId === optionId;
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Handle option toggle
|
|
282
|
+
const handleOptionToggle = (option: any) => {
|
|
283
|
+
const isSelected = isOptionSelected(option);
|
|
284
|
+
let newSelectedOptions: any[];
|
|
285
|
+
|
|
286
|
+
if (isSelected) {
|
|
287
|
+
// Remove option
|
|
288
|
+
newSelectedOptions = selectedOptions.filter(selected => {
|
|
289
|
+
const selectedId = typeof selected === 'object' ? selected[valueKeyId] : selected;
|
|
290
|
+
const optionId = typeof option === 'object' ? option[valueKeyId] : option;
|
|
291
|
+
return selectedId !== optionId;
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
// Add option
|
|
295
|
+
newSelectedOptions = [...selectedOptions, option];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
setSelectedOptions(newSelectedOptions);
|
|
299
|
+
|
|
300
|
+
if (onChange) {
|
|
301
|
+
onChange(newSelectedOptions);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Handle select all
|
|
306
|
+
const handleSelectAll = () => {
|
|
307
|
+
const allSelected = parsedOptions.every(option => isOptionSelected(option));
|
|
308
|
+
|
|
309
|
+
if (allSelected) {
|
|
310
|
+
// Deselect all visible options
|
|
311
|
+
const visibleIds = parsedOptions.map(option =>
|
|
312
|
+
typeof option === 'object' ? option[valueKeyId] : option
|
|
313
|
+
);
|
|
314
|
+
const newSelectedOptions = selectedOptions.filter(selected => {
|
|
315
|
+
const selectedId = typeof selected === 'object' ? selected[valueKeyId] : selected;
|
|
316
|
+
return !visibleIds.includes(selectedId);
|
|
317
|
+
});
|
|
318
|
+
setSelectedOptions(newSelectedOptions);
|
|
319
|
+
if (onChange) {
|
|
320
|
+
onChange(newSelectedOptions);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
// Select all visible options
|
|
324
|
+
const newOptions = parsedOptions.filter(option => !isOptionSelected(option));
|
|
325
|
+
const newSelectedOptions = [...selectedOptions, ...newOptions];
|
|
326
|
+
setSelectedOptions(newSelectedOptions);
|
|
327
|
+
if (onChange) {
|
|
328
|
+
onChange(newSelectedOptions);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Get display text for selected items
|
|
334
|
+
const getSelectedDisplayText = () => {
|
|
335
|
+
if (selectedOptions.length === 0) {
|
|
336
|
+
return placeholder || "Seleccionar opciones...";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (showSelectedCount && selectedOptions.length > 2) {
|
|
340
|
+
return `${selectedOptions.length} elementos seleccionados`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return selectedOptions
|
|
344
|
+
.slice(0, 2)
|
|
345
|
+
.map(option => renderText(_transformOption(option, "display"), "display"))
|
|
346
|
+
.join(", ") + (selectedOptions.length > 2 ? `, +${selectedOptions.length - 2} más` : "");
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Check if all visible options are selected
|
|
350
|
+
const allVisibleSelected = parsedOptions.length > 0 && parsedOptions.every(option => isOptionSelected(option));
|
|
351
|
+
const someVisibleSelected = parsedOptions.some(option => isOptionSelected(option));
|
|
352
|
+
|
|
353
|
+
if (!fullyLoaded) return <LoadingIndicator />;
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<Box ref={anchorRef} sx={{ position: 'relative', width: fullWidth ? '100%' : 'auto' }}>
|
|
357
|
+
{/* Main Input Field */}
|
|
358
|
+
<TextField
|
|
359
|
+
fullWidth={fullWidth}
|
|
360
|
+
label={selectLabel}
|
|
361
|
+
value={getSelectedDisplayText()}
|
|
362
|
+
onClick={() => setOpen(!open)}
|
|
363
|
+
disabled={disabled}
|
|
364
|
+
variant="outlined"
|
|
365
|
+
InputProps={{
|
|
366
|
+
readOnly: true,
|
|
367
|
+
endAdornment: (
|
|
368
|
+
<InputAdornment position="end">
|
|
369
|
+
<IconButton
|
|
370
|
+
onClick={(e) => {
|
|
371
|
+
e.stopPropagation();
|
|
372
|
+
setOpen(!open);
|
|
373
|
+
}}
|
|
374
|
+
edge="end"
|
|
375
|
+
size="small"
|
|
376
|
+
>
|
|
377
|
+
{open ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
|
378
|
+
</IconButton>
|
|
379
|
+
</InputAdornment>
|
|
380
|
+
),
|
|
381
|
+
}}
|
|
382
|
+
sx={{
|
|
383
|
+
cursor: 'pointer',
|
|
384
|
+
'& .MuiInputBase-input': {
|
|
385
|
+
cursor: 'pointer',
|
|
386
|
+
}
|
|
387
|
+
}}
|
|
388
|
+
/>
|
|
389
|
+
|
|
390
|
+
{/* Selected Items Chips */}
|
|
391
|
+
{selectedOptions.length > 0 && (
|
|
392
|
+
<Box sx={{ mt: 1, display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
|
393
|
+
{selectedOptions.slice(0, 5).map((option, index) => (
|
|
394
|
+
<Chip
|
|
395
|
+
key={`chip-${option[valueKeyId] || index}`}
|
|
396
|
+
label={renderText(_transformOption(option, "chip"), "chip")}
|
|
397
|
+
size="small"
|
|
398
|
+
onDelete={() => handleOptionToggle(option)}
|
|
399
|
+
disabled={disabled}
|
|
400
|
+
/>
|
|
401
|
+
))}
|
|
402
|
+
{selectedOptions.length > 5 && (
|
|
403
|
+
<Chip
|
|
404
|
+
label={`+${selectedOptions.length - 5} más`}
|
|
405
|
+
size="small"
|
|
406
|
+
variant="outlined"
|
|
407
|
+
/>
|
|
408
|
+
)}
|
|
409
|
+
</Box>
|
|
410
|
+
)}
|
|
411
|
+
|
|
412
|
+
{/* Dropdown Popper */}
|
|
413
|
+
<Popper
|
|
414
|
+
open={open}
|
|
415
|
+
anchorEl={anchorRef.current}
|
|
416
|
+
placement="bottom-start"
|
|
417
|
+
style={{ zIndex: 1300, width: anchorRef.current?.offsetWidth }}
|
|
418
|
+
>
|
|
419
|
+
<ClickAwayListener onClickAway={() => setOpen(false)}>
|
|
420
|
+
<Paper
|
|
421
|
+
elevation={8}
|
|
422
|
+
sx={{
|
|
423
|
+
maxHeight: maxHeight + 100,
|
|
424
|
+
width: '100%',
|
|
425
|
+
overflow: 'hidden',
|
|
426
|
+
display: 'flex',
|
|
427
|
+
flexDirection: 'column',
|
|
428
|
+
}}
|
|
429
|
+
>
|
|
430
|
+
{/* Search Input */}
|
|
431
|
+
<Box sx={{ p: 2, borderBottom: '1px solid #e0e0e0' }}>
|
|
432
|
+
<TextField
|
|
433
|
+
fullWidth
|
|
434
|
+
size="small"
|
|
435
|
+
placeholder="Buscar..."
|
|
436
|
+
value={searchInputValue}
|
|
437
|
+
onChange={handleSearchChange}
|
|
438
|
+
InputProps={{
|
|
439
|
+
startAdornment: (
|
|
440
|
+
<InputAdornment position="start">
|
|
441
|
+
<SearchIcon fontSize="small" />
|
|
442
|
+
</InputAdornment>
|
|
443
|
+
),
|
|
444
|
+
endAdornment: searchInputValue && (
|
|
445
|
+
<InputAdornment position="end">
|
|
446
|
+
<IconButton
|
|
447
|
+
size="small"
|
|
448
|
+
onClick={handleClearSearch}
|
|
449
|
+
>
|
|
450
|
+
<ClearIcon fontSize="small" />
|
|
451
|
+
</IconButton>
|
|
452
|
+
</InputAdornment>
|
|
453
|
+
),
|
|
454
|
+
}}
|
|
455
|
+
/>
|
|
456
|
+
</Box>
|
|
457
|
+
|
|
458
|
+
{/* Select All Option */}
|
|
459
|
+
{showSelectAll && parsedOptions.length > 0 && (
|
|
460
|
+
<Box sx={{ borderBottom: '1px solid #e0e0e0' }}>
|
|
461
|
+
<ListItem disablePadding>
|
|
462
|
+
<ListItemButton onClick={handleSelectAll} dense>
|
|
463
|
+
<ListItemIcon>
|
|
464
|
+
<Checkbox
|
|
465
|
+
checked={allVisibleSelected}
|
|
466
|
+
indeterminate={someVisibleSelected && !allVisibleSelected}
|
|
467
|
+
size="small"
|
|
468
|
+
/>
|
|
469
|
+
</ListItemIcon>
|
|
470
|
+
<ListItemText
|
|
471
|
+
primary={
|
|
472
|
+
<Typography variant="body2" fontWeight="bold">
|
|
473
|
+
{allVisibleSelected ? "Deseleccionar todo" : "Seleccionar todo"}
|
|
474
|
+
{q && ` (${parsedOptions.length} resultados)`}
|
|
475
|
+
</Typography>
|
|
476
|
+
}
|
|
477
|
+
/>
|
|
478
|
+
</ListItemButton>
|
|
479
|
+
</ListItem>
|
|
480
|
+
</Box>
|
|
481
|
+
)}
|
|
482
|
+
|
|
483
|
+
{/* Loading State */}
|
|
484
|
+
{isResourceSearchLoading && (
|
|
485
|
+
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
|
486
|
+
<CircularProgress size={20} />
|
|
487
|
+
<Typography variant="body2" sx={{ ml: 1 }}>
|
|
488
|
+
Cargando opciones...
|
|
489
|
+
</Typography>
|
|
490
|
+
</Box>
|
|
491
|
+
)}
|
|
492
|
+
|
|
493
|
+
{/* Options List */}
|
|
494
|
+
{!isResourceSearchLoading && (
|
|
495
|
+
<List
|
|
496
|
+
sx={{
|
|
497
|
+
maxHeight: maxHeight,
|
|
498
|
+
overflow: 'auto',
|
|
499
|
+
py: 0,
|
|
500
|
+
}}
|
|
501
|
+
>
|
|
502
|
+
{parsedOptions.length === 0 ? (
|
|
503
|
+
<ListItem>
|
|
504
|
+
<ListItemText
|
|
505
|
+
primary={
|
|
506
|
+
<Typography variant="body2" color="text.secondary" textAlign="center">
|
|
507
|
+
{q ? "No se encontraron resultados" : "No hay opciones disponibles"}
|
|
508
|
+
</Typography>
|
|
509
|
+
}
|
|
510
|
+
/>
|
|
511
|
+
</ListItem>
|
|
512
|
+
) : (
|
|
513
|
+
parsedOptions.map((option, index) => {
|
|
514
|
+
const isSelected = isOptionSelected(option);
|
|
515
|
+
const isSearchMatch = option.isSearchMatch;
|
|
516
|
+
const hasSearchQuery = q && q.length >= minSearch;
|
|
517
|
+
|
|
518
|
+
return (
|
|
519
|
+
<ListItem key={option.key || `option-${index}`} disablePadding>
|
|
520
|
+
<ListItemButton
|
|
521
|
+
onClick={() => handleOptionToggle(option)}
|
|
522
|
+
dense
|
|
523
|
+
sx={{
|
|
524
|
+
// Style search matches differently
|
|
525
|
+
backgroundColor: hasSearchQuery && isSearchMatch
|
|
526
|
+
? 'rgba(25, 118, 210, 0.04)'
|
|
527
|
+
: 'transparent',
|
|
528
|
+
borderLeft: hasSearchQuery && isSearchMatch
|
|
529
|
+
? '3px solid #1976d2'
|
|
530
|
+
: 'none',
|
|
531
|
+
paddingLeft: hasSearchQuery && isSearchMatch ? 1 : 2,
|
|
532
|
+
'&:hover': {
|
|
533
|
+
backgroundColor: hasSearchQuery && isSearchMatch
|
|
534
|
+
? 'rgba(25, 118, 210, 0.08)'
|
|
535
|
+
: 'rgba(0, 0, 0, 0.04)',
|
|
536
|
+
},
|
|
537
|
+
}}
|
|
538
|
+
>
|
|
539
|
+
<ListItemIcon>
|
|
540
|
+
<Checkbox
|
|
541
|
+
checked={isSelected}
|
|
542
|
+
size="small"
|
|
543
|
+
tabIndex={-1}
|
|
544
|
+
disableRipple
|
|
545
|
+
/>
|
|
546
|
+
</ListItemIcon>
|
|
547
|
+
<ListItemText
|
|
548
|
+
primary={
|
|
549
|
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
550
|
+
<Typography
|
|
551
|
+
variant="body2"
|
|
552
|
+
sx={{
|
|
553
|
+
fontWeight: hasSearchQuery && isSearchMatch ? 'bold' : 'normal',
|
|
554
|
+
opacity: hasSearchQuery && !isSearchMatch ? 0.7 : 1,
|
|
555
|
+
}}
|
|
556
|
+
>
|
|
557
|
+
{renderText(_transformOption(option, "option"), "option")}
|
|
558
|
+
</Typography>
|
|
559
|
+
{/* Search match indicator */}
|
|
560
|
+
{hasSearchQuery && isSearchMatch && (
|
|
561
|
+
<Typography
|
|
562
|
+
variant="caption"
|
|
563
|
+
sx={{
|
|
564
|
+
color: '#1976d2',
|
|
565
|
+
fontWeight: 'bold',
|
|
566
|
+
fontSize: '0.75rem',
|
|
567
|
+
}}
|
|
568
|
+
>
|
|
569
|
+
✓
|
|
570
|
+
</Typography>
|
|
571
|
+
)}
|
|
572
|
+
</Box>
|
|
573
|
+
}
|
|
574
|
+
/>
|
|
575
|
+
</ListItemButton>
|
|
576
|
+
</ListItem>
|
|
577
|
+
);
|
|
578
|
+
})
|
|
579
|
+
)}
|
|
580
|
+
</List>
|
|
581
|
+
)}
|
|
582
|
+
|
|
583
|
+
{/* Footer with selection count */}
|
|
584
|
+
{showSelectedCount && selectedOptions.length > 0 && (
|
|
585
|
+
<Box
|
|
586
|
+
sx={{
|
|
587
|
+
p: 1,
|
|
588
|
+
borderTop: '1px solid #e0e0e0',
|
|
589
|
+
backgroundColor: '#f5f5f5'
|
|
590
|
+
}}
|
|
591
|
+
>
|
|
592
|
+
<Typography variant="caption" color="text.secondary">
|
|
593
|
+
{selectedOptions.length} elemento{selectedOptions.length !== 1 ? 's' : ''} seleccionado{selectedOptions.length !== 1 ? 's' : ''}
|
|
594
|
+
</Typography>
|
|
595
|
+
</Box>
|
|
596
|
+
)}
|
|
597
|
+
</Paper>
|
|
598
|
+
</ClickAwayListener>
|
|
599
|
+
</Popper>
|
|
600
|
+
</Box>
|
|
601
|
+
);
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
export default SearchableSelectCheckboxes;
|