@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.
Files changed (111) hide show
  1. package/dist/components/AuditLog/AuditLog.js +345 -1
  2. package/dist/components/AuditLog/index.js +4 -1
  3. package/dist/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.js +630 -3
  4. package/dist/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.js +53 -1
  5. package/dist/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.js +647 -3
  6. package/dist/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.js +53 -1
  7. package/dist/components/Json/Json.js +235 -1
  8. package/dist/components/JsonColorSelector/JsonColorSelector.js +1172 -1
  9. package/dist/components/JsonColorSelector/JsonColorSelectorEnhanced.js +863 -2
  10. package/dist/components/JsonColorSelector/JsonCssVarValues.js +738 -1
  11. package/dist/components/JsonColorSelector/components/ColorEditDialog.js +426 -1
  12. package/dist/components/JsonColorSelector/components/ColorPaletteItem.js +75 -1
  13. package/dist/components/JsonColorSelector/components/ImageColorExtractor.js +283 -1
  14. package/dist/components/JsonColorSelector/helpers/functions.js +101 -1
  15. package/dist/components/JsonColorSelector/useAsyncColorThief.js +105 -1
  16. package/dist/components/ListActive/ListActive.js +126 -1
  17. package/dist/components/ListActive/ListBoolean.js +124 -1
  18. package/dist/components/NotificationPreferences/NotificationPreferences.js +317 -1
  19. package/dist/components/NotificationPreferences/index.js +7 -1
  20. package/dist/components/SearchableSelects/RASearchableSelect.js +236 -1
  21. package/dist/components/SearchableSelects/RASearchableSelectCheckboxes.js +479 -1
  22. package/dist/components/SearchableSelects/RASearchableSelectChips.js +465 -1
  23. package/dist/components/SearchableSelects/RASearchableSelectChipsRecordContext.js +295 -1
  24. package/dist/components/SearchableSelects/SearchableSelect.js +202 -1
  25. package/dist/components/SearchableSelects/SearchableSelectCheckboxes.js +290 -1
  26. package/dist/components/SortableDataGrid/SortableDataGrid.js +72 -1
  27. package/dist/components/SortableDataGrid/SortableDataGridBody.js +167 -1
  28. package/dist/components/SortableDataGrid/SortableDataGridHeader.js +109 -1
  29. package/dist/components/SortableDataGrid/SortableDataGridRow.js +283 -1
  30. package/dist/components/Upload/SingleImageUploader.js +40 -1
  31. package/dist/components/custom/PackageCopyMethod.js +78 -1
  32. package/dist/components/custom/PackageRadioButtonGroup.js +45 -1
  33. package/dist/components/dialog/Basic.js +8 -1
  34. package/dist/components/dialog/Correct.js +14 -1
  35. package/dist/components/dialog/Error.js +14 -1
  36. package/dist/components/dialog/Info.js +14 -1
  37. package/dist/components/dialog/InfoV2.js +21 -1
  38. package/dist/components/dialog/NotFound.js +14 -1
  39. package/dist/components/dialog/QuickSearch.js +117 -1
  40. package/dist/components/notifications/NotificationsCenter.js +597 -1
  41. package/dist/components/notifications/index.js +24 -1
  42. package/dist/components/tables/PaginationComponent.js +36 -1
  43. package/dist/components/theme/AppLoadingFallback.js +38 -2
  44. package/dist/components/theme/ErrorBoundary.js +52 -1
  45. package/dist/components/theme/InitialLoader.js +13 -1
  46. package/dist/components/theme/NotFound.js +136 -1
  47. package/dist/components/theme/NotResults.js +38 -1
  48. package/dist/hooks/useDraggable.js +6 -1
  49. package/dist/hooks/useNotifications.js +8 -1
  50. package/dist/hooks/useQuickSearch.js +26 -1
  51. package/dist/index.js +66 -5567
  52. package/dist/utils/setNativeValue.js +17 -1
  53. package/package.json +149 -132
  54. package/src/components/AuditLog/AuditLog.tsx +476 -0
  55. package/src/components/AuditLog/index.ts +2 -0
  56. package/src/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.tsx +917 -0
  57. package/src/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.tsx +39 -0
  58. package/src/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.tsx +916 -0
  59. package/src/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.tsx +44 -0
  60. package/src/components/Json/Json.tsx +312 -0
  61. package/src/components/JsonColorSelector/JsonColorSelector.tsx +1435 -0
  62. package/src/components/JsonColorSelector/JsonColorSelectorEnhanced.tsx +1029 -0
  63. package/src/components/JsonColorSelector/JsonCssVarValues.tsx +863 -0
  64. package/src/components/JsonColorSelector/components/ColorEditDialog.tsx +491 -0
  65. package/src/components/JsonColorSelector/components/ColorPaletteItem.tsx +80 -0
  66. package/src/components/JsonColorSelector/components/ImageColorExtractor.tsx +328 -0
  67. package/src/components/JsonColorSelector/helpers/functions.tsx +123 -0
  68. package/src/components/JsonColorSelector/interfaces/interfaces.tsx +22 -0
  69. package/src/components/JsonColorSelector/useAsyncColorThief.tsx +97 -0
  70. package/src/components/ListActive/ListActive.tsx +106 -0
  71. package/src/components/ListActive/ListBoolean.tsx +96 -0
  72. package/src/components/NotificationPreferences/NotificationPreferences.tsx +463 -0
  73. package/src/components/NotificationPreferences/index.tsx +2 -0
  74. package/src/components/SearchableSelects/RASearchableSelect.tsx +300 -0
  75. package/src/components/SearchableSelects/RASearchableSelectCheckboxes.tsx +604 -0
  76. package/src/components/SearchableSelects/RASearchableSelectChips.tsx +558 -0
  77. package/src/components/SearchableSelects/RASearchableSelectChipsRecordContext.tsx +362 -0
  78. package/src/components/SearchableSelects/SearchableSelect.tsx +321 -0
  79. package/src/components/SearchableSelects/SearchableSelectCheckboxes.tsx +355 -0
  80. package/src/components/SortableDataGrid/SortableDataGrid.tsx +56 -0
  81. package/src/components/SortableDataGrid/SortableDataGridBody.tsx +175 -0
  82. package/src/components/SortableDataGrid/SortableDataGridHeader.tsx +171 -0
  83. package/src/components/SortableDataGrid/SortableDataGridRow.tsx +247 -0
  84. package/src/components/Upload/SingleImageUploader.tsx +55 -0
  85. package/src/components/custom/PackageCopyMethod.tsx +109 -0
  86. package/src/components/custom/PackageRadioButtonGroup.tsx +47 -0
  87. package/src/components/dialog/Basic.tsx +11 -0
  88. package/src/components/dialog/Correct.tsx +16 -0
  89. package/src/components/dialog/Error.tsx +15 -0
  90. package/src/components/dialog/Info.tsx +15 -0
  91. package/src/components/dialog/InfoV2.tsx +35 -0
  92. package/src/components/dialog/NotFound.tsx +18 -0
  93. package/src/components/dialog/QuickSearch.tsx +115 -0
  94. package/src/components/notifications/NotificationsCenter.tsx +792 -0
  95. package/src/components/notifications/index.ts +42 -0
  96. package/src/components/tables/PaginationComponent.tsx +17 -0
  97. package/src/components/theme/AppLoadingFallback.tsx +42 -0
  98. package/src/components/theme/ErrorBoundary.tsx +57 -0
  99. package/src/components/theme/InitialLoader.tsx +16 -0
  100. package/src/components/theme/NotFound.tsx +124 -0
  101. package/src/components/theme/NotResults.tsx +54 -0
  102. package/src/hooks/useDraggable.ts +11 -0
  103. package/src/hooks/useNotifications.ts +14 -0
  104. package/src/hooks/useQuickSearch.tsx +29 -0
  105. package/src/index.tsx +77 -0
  106. package/src/utils/setNativeValue.tsx +23 -0
  107. package/dist/color-thief-CTwGFhOB.js +0 -272
  108. /package/{dist → src}/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayAutoSearch.tsx.wip +0 -0
  109. /package/{dist → src}/components/JsonColorSelector/JsonColorSelector.tsx.bkup +0 -0
  110. /package/{dist → src}/components/SortableDataGrid/README.md +0 -0
  111. /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;