@dashadmin/dash-components 1.3.25 → 1.3.28

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 (112) hide show
  1. package/README.md +35 -0
  2. package/dist/components/AuditLog/AuditLog.js +345 -1
  3. package/dist/components/AuditLog/index.js +4 -1
  4. package/dist/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.js +630 -3
  5. package/dist/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.js +53 -1
  6. package/dist/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.js +647 -3
  7. package/dist/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.js +53 -1
  8. package/dist/components/Json/Json.js +235 -1
  9. package/dist/components/JsonColorSelector/JsonColorSelector.js +1172 -1
  10. package/dist/components/JsonColorSelector/JsonColorSelectorEnhanced.js +863 -2
  11. package/dist/components/JsonColorSelector/JsonCssVarValues.js +738 -1
  12. package/dist/components/JsonColorSelector/components/ColorEditDialog.js +426 -1
  13. package/dist/components/JsonColorSelector/components/ColorPaletteItem.js +75 -1
  14. package/dist/components/JsonColorSelector/components/ImageColorExtractor.js +283 -1
  15. package/dist/components/JsonColorSelector/helpers/functions.js +101 -1
  16. package/dist/components/JsonColorSelector/useAsyncColorThief.js +105 -1
  17. package/dist/components/ListActive/ListActive.js +126 -1
  18. package/dist/components/ListActive/ListBoolean.js +124 -1
  19. package/dist/components/NotificationPreferences/NotificationPreferences.js +317 -1
  20. package/dist/components/NotificationPreferences/index.js +7 -1
  21. package/dist/components/SearchableSelects/RASearchableSelect.js +236 -1
  22. package/dist/components/SearchableSelects/RASearchableSelectCheckboxes.js +479 -1
  23. package/dist/components/SearchableSelects/RASearchableSelectChips.js +465 -1
  24. package/dist/components/SearchableSelects/RASearchableSelectChipsRecordContext.js +295 -1
  25. package/dist/components/SearchableSelects/SearchableSelect.js +202 -1
  26. package/dist/components/SearchableSelects/SearchableSelectCheckboxes.js +290 -1
  27. package/dist/components/SortableDataGrid/SortableDataGrid.js +72 -1
  28. package/dist/components/SortableDataGrid/SortableDataGridBody.js +167 -1
  29. package/dist/components/SortableDataGrid/SortableDataGridHeader.js +109 -1
  30. package/dist/components/SortableDataGrid/SortableDataGridRow.js +283 -1
  31. package/dist/components/Upload/SingleImageUploader.js +40 -1
  32. package/dist/components/custom/PackageCopyMethod.js +78 -1
  33. package/dist/components/custom/PackageRadioButtonGroup.js +45 -1
  34. package/dist/components/dialog/Basic.js +8 -1
  35. package/dist/components/dialog/Correct.js +14 -1
  36. package/dist/components/dialog/Error.js +14 -1
  37. package/dist/components/dialog/Info.js +14 -1
  38. package/dist/components/dialog/InfoV2.js +21 -1
  39. package/dist/components/dialog/NotFound.js +14 -1
  40. package/dist/components/dialog/QuickSearch.js +117 -1
  41. package/dist/components/notifications/NotificationsCenter.js +597 -1
  42. package/dist/components/notifications/index.js +24 -1
  43. package/dist/components/tables/PaginationComponent.js +36 -1
  44. package/dist/components/theme/AppLoadingFallback.js +38 -2
  45. package/dist/components/theme/ErrorBoundary.js +52 -1
  46. package/dist/components/theme/InitialLoader.js +13 -1
  47. package/dist/components/theme/NotFound.js +136 -1
  48. package/dist/components/theme/NotResults.js +38 -1
  49. package/dist/hooks/useDraggable.js +6 -1
  50. package/dist/hooks/useNotifications.js +8 -1
  51. package/dist/hooks/useQuickSearch.js +26 -1
  52. package/dist/index.js +66 -5567
  53. package/dist/utils/setNativeValue.js +17 -1
  54. package/package.json +148 -132
  55. package/src/components/AuditLog/AuditLog.tsx +476 -0
  56. package/src/components/AuditLog/index.ts +2 -0
  57. package/src/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.tsx +917 -0
  58. package/src/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.tsx +39 -0
  59. package/src/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.tsx +916 -0
  60. package/src/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.tsx +44 -0
  61. package/src/components/Json/Json.tsx +312 -0
  62. package/src/components/JsonColorSelector/JsonColorSelector.tsx +1435 -0
  63. package/src/components/JsonColorSelector/JsonColorSelectorEnhanced.tsx +1029 -0
  64. package/src/components/JsonColorSelector/JsonCssVarValues.tsx +863 -0
  65. package/src/components/JsonColorSelector/components/ColorEditDialog.tsx +491 -0
  66. package/src/components/JsonColorSelector/components/ColorPaletteItem.tsx +80 -0
  67. package/src/components/JsonColorSelector/components/ImageColorExtractor.tsx +328 -0
  68. package/src/components/JsonColorSelector/helpers/functions.tsx +123 -0
  69. package/src/components/JsonColorSelector/interfaces/interfaces.tsx +22 -0
  70. package/src/components/JsonColorSelector/useAsyncColorThief.tsx +97 -0
  71. package/src/components/ListActive/ListActive.tsx +106 -0
  72. package/src/components/ListActive/ListBoolean.tsx +96 -0
  73. package/src/components/NotificationPreferences/NotificationPreferences.tsx +463 -0
  74. package/src/components/NotificationPreferences/index.tsx +2 -0
  75. package/src/components/SearchableSelects/RASearchableSelect.tsx +300 -0
  76. package/src/components/SearchableSelects/RASearchableSelectCheckboxes.tsx +604 -0
  77. package/src/components/SearchableSelects/RASearchableSelectChips.tsx +558 -0
  78. package/src/components/SearchableSelects/RASearchableSelectChipsRecordContext.tsx +362 -0
  79. package/src/components/SearchableSelects/SearchableSelect.tsx +321 -0
  80. package/src/components/SearchableSelects/SearchableSelectCheckboxes.tsx +355 -0
  81. package/src/components/SortableDataGrid/SortableDataGrid.tsx +56 -0
  82. package/src/components/SortableDataGrid/SortableDataGridBody.tsx +175 -0
  83. package/src/components/SortableDataGrid/SortableDataGridHeader.tsx +171 -0
  84. package/src/components/SortableDataGrid/SortableDataGridRow.tsx +247 -0
  85. package/src/components/Upload/SingleImageUploader.tsx +55 -0
  86. package/src/components/custom/PackageCopyMethod.tsx +109 -0
  87. package/src/components/custom/PackageRadioButtonGroup.tsx +47 -0
  88. package/src/components/dialog/Basic.tsx +11 -0
  89. package/src/components/dialog/Correct.tsx +16 -0
  90. package/src/components/dialog/Error.tsx +15 -0
  91. package/src/components/dialog/Info.tsx +15 -0
  92. package/src/components/dialog/InfoV2.tsx +35 -0
  93. package/src/components/dialog/NotFound.tsx +18 -0
  94. package/src/components/dialog/QuickSearch.tsx +115 -0
  95. package/src/components/notifications/NotificationsCenter.tsx +792 -0
  96. package/src/components/notifications/index.ts +42 -0
  97. package/src/components/tables/PaginationComponent.tsx +17 -0
  98. package/src/components/theme/AppLoadingFallback.tsx +42 -0
  99. package/src/components/theme/ErrorBoundary.tsx +57 -0
  100. package/src/components/theme/InitialLoader.tsx +16 -0
  101. package/src/components/theme/NotFound.tsx +124 -0
  102. package/src/components/theme/NotResults.tsx +54 -0
  103. package/src/hooks/useDraggable.ts +11 -0
  104. package/src/hooks/useNotifications.ts +14 -0
  105. package/src/hooks/useQuickSearch.tsx +29 -0
  106. package/src/index.tsx +77 -0
  107. package/src/utils/setNativeValue.tsx +23 -0
  108. package/dist/color-thief-CTwGFhOB.js +0 -272
  109. /package/{dist → src}/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayAutoSearch.tsx.wip +0 -0
  110. /package/{dist → src}/components/JsonColorSelector/JsonColorSelector.tsx.bkup +0 -0
  111. /package/{dist → src}/components/SortableDataGrid/README.md +0 -0
  112. /package/{dist → src}/styles/index.less +0 -0
@@ -0,0 +1,1435 @@
1
+ import { IDashAutoAdminCustomFieldComponent } from 'dash-auto-admin';
2
+ import { useRecordContext } from 'react-admin';
3
+ import { useFormContext } from 'react-hook-form';
4
+ import {
5
+ Box,
6
+ Button,
7
+ IconButton,
8
+ TextField,
9
+ Typography,
10
+ Dialog,
11
+ DialogTitle,
12
+ DialogContent,
13
+ DialogActions,
14
+ Card,
15
+ CardContent,
16
+ Chip,
17
+ InputAdornment,
18
+ FormControl,
19
+ InputLabel,
20
+ Select,
21
+ MenuItem,
22
+ Tabs,
23
+ Tab,
24
+ useColorScheme,
25
+ Grid,
26
+ Pagination,
27
+ Link,
28
+ } from '@mui/material';
29
+ import DeleteIcon from '@mui/icons-material/Delete';
30
+ import AddIcon from '@mui/icons-material/Add';
31
+ import EditIcon from '@mui/icons-material/Edit';
32
+ import PreviewIcon from '@mui/icons-material/Preview';
33
+ import SearchIcon from '@mui/icons-material/Search';
34
+ import ClearIcon from '@mui/icons-material/Clear';
35
+ import FilterListIcon from '@mui/icons-material/FilterList';
36
+ import { ChromePicker, RGBColor } from 'react-color';
37
+ import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
38
+ import { updateDomCssVariables } from 'dash-utils';
39
+ import { AuthPersistenceService } from 'dash-auth';
40
+
41
+ interface KeyValuePair {
42
+ key: string;
43
+ value: string;
44
+ id: string;
45
+ }
46
+
47
+ interface ColorFormat {
48
+ hex: string;
49
+ rgb: RGBColor;
50
+ hsl: { h: number; s: number; l: number; a?: number };
51
+ }
52
+
53
+ type ColorFormatType = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla';
54
+
55
+ // Helper function to determine text color based on background
56
+ const getContrastColor = (color: string): string => {
57
+ // Handle different color formats
58
+ let r: number, g: number, b: number, a: number = 1;
59
+
60
+ if (color.startsWith('#')) {
61
+ // Hex color
62
+ const hex = color.replace('#', '');
63
+ if (hex.length === 8) {
64
+ // RGBA hex
65
+ r = parseInt(hex.substr(0, 2), 16);
66
+ g = parseInt(hex.substr(2, 2), 16);
67
+ b = parseInt(hex.substr(4, 2), 16);
68
+ a = parseInt(hex.substr(6, 2), 16) / 255;
69
+ } else {
70
+ // RGB hex
71
+ r = parseInt(hex.substr(0, 2), 16);
72
+ g = parseInt(hex.substr(2, 2), 16);
73
+ b = parseInt(hex.substr(4, 2), 16);
74
+ }
75
+ } else if (color.startsWith('rgb')) {
76
+ // RGB/RGBA color
77
+ const match = color.match(/rgba?\(([^)]+)\)/);
78
+ if (match) {
79
+ const values = match[1].split(',').map(v => parseFloat(v.trim()));
80
+ [r, g, b, a = 1] = values;
81
+ } else {
82
+ return '#000000';
83
+ }
84
+ } else if (color.startsWith('hsl')) {
85
+ // For HSL, we'll use a simple approach
86
+ return '#000000';
87
+ } else {
88
+ return '#000000';
89
+ }
90
+
91
+ // Calculate luminance considering alpha
92
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
93
+ const effectiveLuminance = luminance * a + (1 - a); // Blend with white background
94
+
95
+ return effectiveLuminance > 0.5 ? '#000000' : '#ffffff';
96
+ };
97
+
98
+ // Helper function to convert color to different formats
99
+ const convertColor = (color: ColorFormat, format: ColorFormatType): string => {
100
+ switch (format) {
101
+ case 'hex':
102
+ return color.hex;
103
+ case 'rgb':
104
+ return `rgb(${Math.round(color.rgb.r)}, ${Math.round(color.rgb.g)}, ${Math.round(color.rgb.b)})`;
105
+ case 'rgba':
106
+ return `rgba(${Math.round(color.rgb.r)}, ${Math.round(color.rgb.g)}, ${Math.round(color.rgb.b)}, ${color.rgb.a || 1})`;
107
+ case 'hsl':
108
+ return `hsl(${Math.round(color.hsl.h)}, ${Math.round(color.hsl.s * 100)}%, ${Math.round(color.hsl.l * 100)}%)`;
109
+ case 'hsla':
110
+ return `hsla(${Math.round(color.hsl.h)}, ${Math.round(color.hsl.s * 100)}%, ${Math.round(color.hsl.l * 100)}%, ${color.hsl.a || 1})`;
111
+ default:
112
+ return color.hex;
113
+ }
114
+ };
115
+
116
+ // Helper function to parse color key and create chips
117
+ const parseColorKey = (key: string) => {
118
+ const parts = key.split('--');
119
+ const baseName = parts[0];
120
+ const mode = parts[1];
121
+
122
+ // Split base name by hyphens for individual chips
123
+ const baseChips = baseName.split('-').filter(part => part.length > 0);
124
+
125
+ return { baseChips, mode };
126
+ };
127
+
128
+ // Helper function to extract available modes from color keys
129
+ const extractAvailableModes = (pairs: KeyValuePair[]): string[] => {
130
+ const modes = new Set<string>();
131
+
132
+ pairs.forEach(pair => {
133
+ const { mode } = parseColorKey(pair.key);
134
+ if (mode) {
135
+ modes.add(mode);
136
+ }
137
+ });
138
+
139
+ return Array.from(modes).sort();
140
+ };
141
+
142
+ // Helper function to get mode icon
143
+ const getModeIcon = (mode: string | undefined): string => {
144
+ if (!mode) return '';
145
+
146
+ switch (mode.toLowerCase()) {
147
+ case 'light':
148
+ return '☀️'; // Sun icon
149
+ case 'dark':
150
+ return '🌙'; // Moon icon
151
+ default:
152
+ return mode; // Plain text for other modes
153
+ }
154
+ };
155
+
156
+ // Color palette item component
157
+ const ColorPaletteItem: React.FC<{
158
+ pair: KeyValuePair;
159
+ onEdit: (pair: KeyValuePair) => void;
160
+ onDelete: (id: string) => void;
161
+ }> = ({ pair, onEdit, onDelete }) => {
162
+ const textColor = getContrastColor(pair.value);
163
+ const { baseChips, mode } = parseColorKey(pair.key);
164
+ const modeIcon = getModeIcon(mode);
165
+
166
+ return (
167
+ <Card
168
+ onClick={() => onEdit(pair)}
169
+ elevation={1}
170
+ sx={{
171
+ width: '100%',
172
+ display: 'flex',
173
+ alignItems: 'stretch', // Ensure stretch for full height
174
+ cursor: 'pointer',
175
+ transition: 'all 0.2s ease-in-out',
176
+ '&:hover': {
177
+ transform: 'translateY(-2px)',
178
+ boxShadow: 3,
179
+ },
180
+ borderRadius: 2,
181
+ overflow: 'hidden',
182
+ position: 'relative',
183
+ height: 80, // Fixed height for consistency
184
+ backgroundColor: 'background.paper',
185
+ }}
186
+ >
187
+ {/* Left Color Box */}
188
+ <Box
189
+ sx={{
190
+ width: 80, // Fixed square width
191
+ backgroundColor: pair.value,
192
+ flexShrink: 0,
193
+ display: 'flex',
194
+ alignItems: 'center',
195
+ justifyContent: 'center',
196
+ position: 'relative',
197
+ }}
198
+ >
199
+ {/* Hover Edit Icon Overlay */}
200
+ <Box
201
+ sx={{
202
+ position: 'absolute',
203
+ top: 0,
204
+ left: 0,
205
+ right: 0,
206
+ bottom: 0,
207
+ backgroundColor: 'rgba(0,0,0,0.3)',
208
+ opacity: 0,
209
+ transition: 'opacity 0.2s',
210
+ '&:hover': { opacity: 1 },
211
+ display: 'flex',
212
+ alignItems: 'center',
213
+ justifyContent: 'center',
214
+ }}
215
+ >
216
+ <EditIcon sx={{ color: '#fff' }} />
217
+ </Box>
218
+ </Box>
219
+
220
+ {/* Right Details Section */}
221
+ <Box sx={{
222
+ flexGrow: 1,
223
+ p: 1.5,
224
+ display: 'flex',
225
+ flexDirection: 'column',
226
+ justifyContent: 'space-between', // Space key and chips
227
+ overflow: 'hidden'
228
+ }}>
229
+ <Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 0.5 }}>
230
+ <Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 'bold', color: 'text.secondary' }}>
231
+ {pair.value}
232
+ </Typography>
233
+ {mode && (
234
+ <Chip
235
+ label={mode}
236
+ size="small"
237
+ icon={<span style={{ marginLeft: 6, fontSize: '0.8rem' }}>{modeIcon}</span>}
238
+ sx={{
239
+ height: 18,
240
+ fontSize: '0.65rem',
241
+ '& .MuiChip-label': { padding: '0 6px' },
242
+ }}
243
+ />
244
+ )}
245
+ </Box>
246
+
247
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
248
+ {baseChips.map((chip, index) => (
249
+ <Chip
250
+ key={index}
251
+ label={chip}
252
+ size="small"
253
+ variant="outlined"
254
+ sx={{
255
+ height: 20,
256
+ fontSize: '0.7rem',
257
+ maxWidth: '100%',
258
+ '& .MuiChip-label': {
259
+ padding: '0 6px',
260
+ overflow: 'hidden',
261
+ textOverflow: 'ellipsis',
262
+ },
263
+ }}
264
+ />
265
+ ))}
266
+ </Box>
267
+ </Box>
268
+ </Card>
269
+ );
270
+ };
271
+
272
+ // Color edit dialog component
273
+ const ColorEditDialog: React.FC<{
274
+ open: boolean;
275
+ pair: KeyValuePair | null;
276
+ onClose: () => void;
277
+ onSave: (pair: KeyValuePair) => void;
278
+ onDelete: (id: string) => void;
279
+ existingKeys: string[];
280
+ updateSingleDomColor: (key: string, color: string) => void;
281
+ }> = ({ open, pair, onClose, onSave, onDelete, existingKeys,updateSingleDomColor }) => {
282
+ const [editedPair, setEditedPair] = useState<KeyValuePair | null>(null);
283
+ const [colorFormat, setColorFormat] = useState<ColorFormatType>('hex');
284
+ const [keyError, setKeyError] = useState<string>('');
285
+ const [tabValue, setTabValue] = useState(0);
286
+
287
+ useEffect(() => {
288
+ if (pair) {
289
+ setEditedPair({ ...pair });
290
+ // Detect color format
291
+ if (pair.value.startsWith('rgba(')) {
292
+ setColorFormat('rgba');
293
+ } else if (pair.value.startsWith('rgb(')) {
294
+ setColorFormat('rgb');
295
+ } else if (pair.value.startsWith('hsla(')) {
296
+ setColorFormat('hsla');
297
+ } else if (pair.value.startsWith('hsl(')) {
298
+ setColorFormat('hsl');
299
+ } else {
300
+ setColorFormat('hex');
301
+ }
302
+ }
303
+ setKeyError('');
304
+ }, [pair]);
305
+
306
+ const handleKeyChange = (newKey: string) => {
307
+ if (!editedPair) return;
308
+
309
+ const isDuplicate = existingKeys.some(key => key === newKey && newKey !== pair?.key);
310
+ setKeyError(isDuplicate ? 'Key already exists' : '');
311
+
312
+ setEditedPair({ ...editedPair, key: newKey });
313
+ };
314
+
315
+ const handleColorChange = (color: any) => {
316
+ if (!editedPair) return;
317
+
318
+ const newValue = convertColor(color, colorFormat);
319
+ setEditedPair({ ...editedPair, value: newValue });
320
+
321
+ updateSingleDomColor(editedPair.key, newValue);
322
+
323
+ };
324
+
325
+ const handleSave = () => {
326
+ if (!editedPair || keyError || !editedPair.key.trim()) return;
327
+ onSave(editedPair);
328
+ onClose();
329
+ };
330
+
331
+ const handleDelete = () => {
332
+ if (!editedPair) return;
333
+ onDelete(editedPair.id);
334
+ onClose();
335
+ };
336
+
337
+ if (!editedPair) return null;
338
+
339
+ return (
340
+ <Dialog
341
+ open={open}
342
+ onClose={onClose}
343
+ maxWidth="sm"
344
+ fullWidth
345
+ PaperProps={{
346
+ sx: { minHeight: 600 }
347
+ }}
348
+ >
349
+ <DialogTitle>
350
+ {pair?.key ? `Edit Color: ${pair.key}` : 'Add New Color'}
351
+ </DialogTitle>
352
+ <DialogContent>
353
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, pt: 1 }}>
354
+ {/* Color Name */}
355
+ <TextField
356
+ fullWidth
357
+ label="Color Name"
358
+ value={editedPair.key}
359
+ onChange={(e) => handleKeyChange(e.target.value)}
360
+ error={!!keyError}
361
+ helperText={keyError}
362
+ placeholder="e.g., primary-color--light, background-dark--dark"
363
+ />
364
+
365
+ {/* Color Format Selector */}
366
+ <FormControl fullWidth>
367
+ <InputLabel>Color Format</InputLabel>
368
+ <Select
369
+ value={colorFormat}
370
+ label="Color Format"
371
+ onChange={(e) => setColorFormat(e.target.value as ColorFormatType)}
372
+ >
373
+ <MenuItem value="hex">HEX</MenuItem>
374
+ <MenuItem value="rgb">RGB</MenuItem>
375
+ <MenuItem value="rgba">RGBA (with alpha)</MenuItem>
376
+ <MenuItem value="hsl">HSL</MenuItem>
377
+ <MenuItem value="hsla">HSLA (with alpha)</MenuItem>
378
+ </Select>
379
+ </FormControl>
380
+
381
+ {/* Color Picker Tabs */}
382
+ <Box>
383
+ <Tabs value={tabValue} onChange={(_, newValue) => setTabValue(newValue)}>
384
+ <Tab label="Color Picker" />
385
+ <Tab label="Manual Input" />
386
+ </Tabs>
387
+
388
+ {tabValue === 0 && (
389
+ <Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
390
+ <ChromePicker
391
+ color={editedPair.value}
392
+ onChange={handleColorChange}
393
+ disableAlpha={!['rgba', 'hsla'].includes(colorFormat)}
394
+ />
395
+ </Box>
396
+ )}
397
+
398
+ {tabValue === 1 && (
399
+ <Box sx={{ mt: 2 }}>
400
+ <TextField
401
+ fullWidth
402
+ label={`Color Value (${colorFormat.toUpperCase()})`}
403
+ value={editedPair.value}
404
+ onChange={(e) => setEditedPair({ ...editedPair, value: e.target.value })}
405
+ placeholder={
406
+ colorFormat === 'hex' ? '#ff0000' :
407
+ colorFormat === 'rgb' ? 'rgb(255, 0, 0)' :
408
+ colorFormat === 'rgba' ? 'rgba(255, 0, 0, 1)' :
409
+ colorFormat === 'hsl' ? 'hsl(0, 100%, 50%)' :
410
+ 'hsla(0, 100%, 50%, 1)'
411
+ }
412
+ InputProps={{
413
+ startAdornment: (
414
+ <InputAdornment position="start">
415
+ <Box
416
+ sx={{
417
+ width: 20,
418
+ height: 20,
419
+ backgroundColor: editedPair.value,
420
+ border: '1px solid #ccc',
421
+ borderRadius: 1,
422
+ }}
423
+ />
424
+ </InputAdornment>
425
+ ),
426
+ }}
427
+ />
428
+ </Box>
429
+ )}
430
+ </Box>
431
+
432
+ {/* Color Preview */}
433
+ <Box>
434
+ <Typography variant="subtitle2" gutterBottom>
435
+ Preview
436
+ </Typography>
437
+ <Box
438
+ sx={{
439
+ width: '100%',
440
+ height: 80,
441
+ backgroundColor: editedPair.value,
442
+ border: '1px solid #ccc',
443
+ borderRadius: 1,
444
+ display: 'flex',
445
+ alignItems: 'center',
446
+ justifyContent: 'center',
447
+ position: 'relative',
448
+ }}
449
+ >
450
+ <Typography
451
+ variant="body2"
452
+ sx={{
453
+ color: getContrastColor(editedPair.value),
454
+ fontWeight: 'bold',
455
+ backgroundColor: 'rgba(0,0,0,0.1)',
456
+ padding: '4px 8px',
457
+ borderRadius: 1,
458
+ }}
459
+ >
460
+ {editedPair.key || 'Color Preview'}
461
+ </Typography>
462
+ </Box>
463
+ </Box>
464
+ </Box>
465
+ </DialogContent>
466
+ <DialogActions>
467
+ {pair?.key && (
468
+ <Button
469
+ onClick={handleDelete}
470
+ color="error"
471
+ startIcon={<DeleteIcon />}
472
+ >
473
+ Delete
474
+ </Button>
475
+ )}
476
+ <Box sx={{ flexGrow: 1 }} />
477
+ <Button onClick={onClose}>
478
+ Cancel
479
+ </Button>
480
+ <Button
481
+ onClick={handleSave}
482
+ variant="contained"
483
+ disabled={!!keyError || !editedPair.key.trim()}
484
+ >
485
+ Save
486
+ </Button>
487
+ </DialogActions>
488
+ </Dialog>
489
+ );
490
+ };
491
+
492
+ export const JsonEdit: React.FC<IDashAutoAdminCustomFieldComponent> = (props) => {
493
+ const { method, attribute, resourceConfig } = props;
494
+
495
+ const record = useRecordContext();
496
+ const { setValue, getValues } = useFormContext();
497
+
498
+ const attributePath = attribute.attribute;
499
+ const isNestedSetting = attributePath.startsWith('settings.');
500
+ const settingsPath = isNestedSetting ? attributePath.split('.').slice(1).join('.') : attributePath;
501
+
502
+ const [pairs, setPairs] = useState<KeyValuePair[]>([]);
503
+ const [searchTerm, setSearchTerm] = useState<string>('');
504
+ const [selectedMode, setSelectedMode] = useState<string>('');
505
+ const [isLoading, setIsLoading] = useState<boolean>(true);
506
+ const [isRendering, setIsRendering] = useState<boolean>(true);
507
+ const [editDialogOpen, setEditDialogOpen] = useState<boolean>(false);
508
+ const [editingPair, setEditingPair] = useState<KeyValuePair | null>(null);
509
+
510
+ // Pagination state
511
+ const [page, setPage] = useState<number>(1);
512
+ const [pageSize] = useState<number>(24);
513
+
514
+ // Throttling refs
515
+ const throttleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
516
+ const pendingUpdatesRef = useRef<Map<string, { key: string; color: string }>>(new Map());
517
+
518
+ // Dispatch loading events
519
+ const dispatchLoadingEvent = useCallback((loading: boolean) => {
520
+ window.dispatchEvent(new MessageEvent('auto-admin-loading-state', { data: loading }));
521
+ }, []);
522
+
523
+ // Extract available modes from pairs
524
+ const availableModes = useMemo(() => {
525
+ return extractAvailableModes(pairs);
526
+ }, [pairs]);
527
+
528
+ // Filter pairs based on search term and selected mode
529
+ const filteredPairs = useMemo(() => {
530
+ let filtered = pairs;
531
+
532
+ // Filter by search term
533
+ if (searchTerm.trim()) {
534
+ filtered = filtered.filter(pair =>
535
+ pair.key.toLowerCase().includes(searchTerm.toLowerCase())
536
+ );
537
+ }
538
+
539
+ // Filter by selected mode
540
+ if (selectedMode) {
541
+ filtered = filtered.filter(pair => {
542
+ const { mode } = parseColorKey(pair.key);
543
+ return mode === selectedMode;
544
+ });
545
+ }
546
+
547
+ return filtered;
548
+ }, [pairs, searchTerm, selectedMode]);
549
+
550
+ const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
551
+ setSearchTerm(event.target.value);
552
+ };
553
+
554
+ const handleClearSearch = () => {
555
+ setSearchTerm('');
556
+ };
557
+
558
+ const handleModeChange = (event: any) => {
559
+ setSelectedMode(event.target.value);
560
+ };
561
+
562
+ const handleClearMode = () => {
563
+ setSelectedMode('');
564
+ };
565
+
566
+ // Initial data loading effect
567
+ useEffect(() => {
568
+ const loadInitialData = async () => {
569
+ setIsLoading(true);
570
+ dispatchLoadingEvent(true);
571
+
572
+ try {
573
+ let initialValue: Record<string, any> = {};
574
+ let defaultValues: Record<string, any> = {};
575
+
576
+ try {
577
+ defaultValues = attribute?.default_value || {};
578
+ } catch (e) {
579
+ defaultValues = {};
580
+ }
581
+
582
+ if (isNestedSetting && record?.settings) {
583
+ const settings = record.settings || {};
584
+ initialValue = { ...defaultValues, ...getNestedValue(settings, settingsPath) };
585
+ } else if (record?.[attributePath]) {
586
+ initialValue = { ...defaultValues, ...record[attributePath] };
587
+ } else {
588
+ initialValue = defaultValues;
589
+ }
590
+
591
+ const initialPairs = Object.entries(initialValue).map(([key, value]) => ({
592
+ key,
593
+ value: String(value),
594
+ id: generateId()
595
+ }));
596
+
597
+ setPairs(initialPairs);
598
+
599
+ // Simulate processing time for large datasets
600
+ if (initialPairs.length > 50) {
601
+ await new Promise(resolve => setTimeout(resolve, 100));
602
+ }
603
+
604
+ } catch (error) {
605
+ console.error('Error loading initial data:', error);
606
+ } finally {
607
+ setIsLoading(false);
608
+ }
609
+ };
610
+
611
+ loadInitialData();
612
+ }, [record, attributePath]);
613
+
614
+ // Rendering completion effect
615
+ useEffect(() => {
616
+ if (!isLoading && pairs.length >= 0) {
617
+ setIsRendering(true);
618
+
619
+ requestAnimationFrame(() => {
620
+ requestAnimationFrame(() => {
621
+ setIsRendering(false);
622
+ dispatchLoadingEvent(false);
623
+ });
624
+ });
625
+ }
626
+ }, [isLoading, pairs.length, dispatchLoadingEvent]);
627
+
628
+ // Cleanup throttle timeout on unmount
629
+ useEffect(() => {
630
+ return () => {
631
+ if (throttleTimeoutRef.current) {
632
+ clearTimeout(throttleTimeoutRef.current);
633
+ }
634
+ dispatchLoadingEvent(false);
635
+ };
636
+ }, [dispatchLoadingEvent]);
637
+
638
+ const getNestedValue = (obj: any, path: string): any => {
639
+ if (!path) return obj;
640
+ const keys = path.split('.');
641
+ return keys.reduce((o, key) => (o && o[key] !== undefined ? o[key] : {}), obj);
642
+ };
643
+
644
+ const setNestedValue = (obj: any, path: string, value: any): any => {
645
+ if (!path) return value;
646
+
647
+ const keys = path.split('.');
648
+ const lastKey = keys.pop();
649
+ const lastObj = keys.reduce((o, key) => {
650
+ if (o[key] === undefined) o[key] = {};
651
+ return o[key];
652
+ }, obj);
653
+
654
+ if (lastKey) {
655
+ lastObj[lastKey] = value;
656
+ }
657
+ return obj;
658
+ };
659
+
660
+ const generateId = (): string => {
661
+ return Math.random().toString(36).substring(2, 11);
662
+ };
663
+
664
+ const addPair = () => {
665
+ const newPair = { key: '', value: '#000000', id: generateId() };
666
+ setEditingPair(newPair);
667
+ setEditDialogOpen(true);
668
+ };
669
+
670
+ const handleEditPair = (pair: KeyValuePair) => {
671
+ setEditingPair(pair);
672
+ setEditDialogOpen(true);
673
+ };
674
+
675
+ const handleSavePair = (editedPair: KeyValuePair) => {
676
+ const existingIndex = pairs.findIndex(p => p.id === editedPair.id);
677
+ let updatedPairs: KeyValuePair[];
678
+
679
+ if (existingIndex >= 0) {
680
+ // Update existing pair
681
+ updatedPairs = pairs.map(p => p.id === editedPair.id ? editedPair : p);
682
+ } else {
683
+ // Add new pair
684
+ updatedPairs = [...pairs, editedPair];
685
+ }
686
+
687
+ setPairs(updatedPairs);
688
+ updateFormValue(updatedPairs);
689
+ updateSingleDomColor(editedPair.key, editedPair.value);
690
+ };
691
+
692
+ const handleDeletePair = (id: string) => {
693
+ const updatedPairs = pairs.filter(pair => pair.id !== id);
694
+ setPairs(updatedPairs);
695
+ updateFormValue(updatedPairs);
696
+ };
697
+
698
+ // Update only a single DOM color property
699
+ const updateSingleDomColor = (key: string, color: string) => {
700
+ if (!key.trim()) return;
701
+
702
+ const currentTheme = document.documentElement.getAttribute('data-theme');
703
+ const themeSuffix = `--${currentTheme}`;
704
+
705
+ if (key.endsWith(themeSuffix)) {
706
+ const baseKey = key.slice(0, -themeSuffix.length);
707
+ document.documentElement.style.setProperty(`--${baseKey}`, color);
708
+ }
709
+ };
710
+
711
+ // Batch update for preview only - processes all colors at once
712
+ /*const updateDomColors = (colors: Record<string, any>) => {
713
+ const currentTheme = document.documentElement.getAttribute('data-theme');
714
+ const themeSuffix = `--${currentTheme}`;
715
+
716
+ requestAnimationFrame(() => {
717
+ Object.entries(colors).forEach(([key, value]) => {
718
+ if (key.endsWith(themeSuffix)) {
719
+ const baseKey = key.slice(0, -themeSuffix.length);
720
+ document.documentElement.style.setProperty(`--${baseKey}`, String(value));
721
+ }
722
+ });
723
+ });
724
+ };*/
725
+
726
+
727
+ const getTenantSettingsValues = () => {
728
+ const persistedTenantSettings = AuthPersistenceService.getTenantSettings();
729
+ if (persistedTenantSettings) {
730
+ return persistedTenantSettings?.values || {};
731
+ }
732
+ return {}
733
+ };
734
+
735
+
736
+ const handlePreview = (m:string) => {
737
+ const colorsObj = pairs.reduce((acc, pair) => {
738
+ if (pair.key.trim()) {
739
+ acc[pair.key] = pair.value;
740
+ }
741
+ return acc;
742
+ }, {} as Record<string, any>);
743
+
744
+ //updateAllDomColors();
745
+ //updateDomColors(colorsObj);
746
+ updateDomCssVariables(m,colorsObj,getTenantSettingsValues());
747
+
748
+ };
749
+
750
+ const updateFormValue = (currentPairs: KeyValuePair[]) => {
751
+ const obj = currentPairs.reduce((acc, pair) => {
752
+ if (pair.key.trim()) {
753
+ acc[pair.key] = pair.value;
754
+ }
755
+ return acc;
756
+ }, {} as Record<string, any>);
757
+
758
+ if (isNestedSetting) {
759
+ const currentSettings = getValues('settings') || {};
760
+ const updatedSettings = setNestedValue({...currentSettings}, settingsPath, obj);
761
+ setValue('settings', updatedSettings, { shouldDirty: true });
762
+ } else {
763
+ setValue(attributePath, obj, { shouldDirty: true });
764
+ }
765
+ };
766
+
767
+ // Show loading state while component is initializing or rendering
768
+ if (isLoading || isRendering) {
769
+ return (
770
+ <Box sx={{ mt: 1, mb: 2, display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 200 }}>
771
+ <Typography variant="body2" color="textSecondary">
772
+ {isLoading ? 'Loading color settings...' : 'Rendering colors...'}
773
+ </Typography>
774
+ </Box>
775
+ );
776
+ }
777
+
778
+ return (
779
+ <Box sx={{ mt: 1, mb: 2 }}>
780
+ <Typography variant="subtitle1" gutterBottom>
781
+ {attribute.label || 'Color Palette'}
782
+ </Typography>
783
+
784
+ {/* Header Actions */}
785
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
786
+ <Button
787
+ startIcon={<PreviewIcon />}
788
+ variant="contained"
789
+ size="small"
790
+ onClick={() => handlePreview(document.documentElement.getAttribute('data-theme'))}
791
+ color="primary"
792
+ >
793
+ Preview Colors TEST
794
+ </Button>
795
+ <Button
796
+ startIcon={<AddIcon />}
797
+ variant="outlined"
798
+ size="small"
799
+ onClick={addPair}
800
+ >
801
+ Add Color
802
+ </Button>
803
+ </Box>
804
+
805
+ {/* Search and Filter Controls */}
806
+ <Box sx={{ mb: 3, display: 'flex', gap: 2, alignItems: 'flex-end' }}>
807
+ {/* Search Field */}
808
+ <TextField
809
+ fullWidth
810
+ size="small"
811
+ placeholder="Search colors by name..."
812
+ value={searchTerm}
813
+ onChange={handleSearchChange}
814
+ InputProps={{
815
+ startAdornment: (
816
+ <InputAdornment position="start">
817
+ <SearchIcon />
818
+ </InputAdornment>
819
+ ),
820
+ endAdornment: searchTerm && (
821
+ <InputAdornment position="end">
822
+ <IconButton
823
+ size="small"
824
+ onClick={handleClearSearch}
825
+ edge="end"
826
+ >
827
+ <ClearIcon />
828
+ </IconButton>
829
+ </InputAdornment>
830
+ ),
831
+ }}
832
+ />
833
+
834
+ {/* Mode Selector */}
835
+ <FormControl size="small" sx={{ minWidth: 150 }}>
836
+ <InputLabel>Mode</InputLabel>
837
+ <Select
838
+ value={selectedMode}
839
+ label="Mode"
840
+ onChange={handleModeChange}
841
+ startAdornment={
842
+ <InputAdornment position="start">
843
+ <FilterListIcon fontSize="small" />
844
+ </InputAdornment>
845
+ }
846
+ endAdornment={selectedMode && (
847
+ <InputAdornment position="end">
848
+ <IconButton
849
+ size="small"
850
+ onClick={handleClearMode}
851
+ edge="end"
852
+ sx={{ mr: 1 }}
853
+ >
854
+ <ClearIcon fontSize="small" />
855
+ </IconButton>
856
+ </InputAdornment>
857
+ )}
858
+ >
859
+ <MenuItem value="">
860
+ <em>All Modes</em>
861
+ </MenuItem>
862
+ {availableModes.map((mode) => (
863
+ <MenuItem key={mode} value={mode}>
864
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
865
+ <span>{getModeIcon(mode)}</span>
866
+ <span>{mode}</span>
867
+ </Box>
868
+ </MenuItem>
869
+ ))}
870
+ </Select>
871
+ </FormControl>
872
+ </Box>
873
+
874
+ {/* Results count when filtering */}
875
+ {(searchTerm || selectedMode) && (
876
+ <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
877
+ <Chip
878
+ label={`${filteredPairs.length} of ${pairs.length} colors`}
879
+ size="small"
880
+ variant="outlined"
881
+ />
882
+ {selectedMode && (
883
+ <Chip
884
+ label={`Mode: ${selectedMode}`}
885
+ size="small"
886
+ variant="filled"
887
+ color="primary"
888
+ onDelete={handleClearMode}
889
+ avatar={<span style={{ paddingLeft: 6 }}>{getModeIcon(selectedMode)}</span>}
890
+ />
891
+ )}
892
+ {searchTerm && (
893
+ <Chip
894
+ label={`Search: "${searchTerm}"`}
895
+ size="small"
896
+ variant="filled"
897
+ color="secondary"
898
+ onDelete={handleClearSearch}
899
+ />
900
+ )}
901
+ </Box>
902
+ )}
903
+
904
+ {/* Color Grid with Pagination */}
905
+ {isRendering ? (
906
+ <Box sx={{ display: 'flex', justifyContent: 'center', p: 4, alignItems: 'center', flexDirection: 'column', gap: 2 }}>
907
+ <Typography>Rendering Grid...</Typography>
908
+ </Box>
909
+ ) : filteredPairs.length > 0 ? (
910
+ <>
911
+ <Box sx={{
912
+ display: 'grid',
913
+ gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
914
+ gap: 2,
915
+ width: '100%'
916
+ }}>
917
+ {filteredPairs
918
+ .slice((page - 1) * pageSize, page * pageSize)
919
+ .map((pair) => (
920
+ <Box key={pair.id} sx={{ minWidth: 0 }}>
921
+ <ColorPaletteItem
922
+ pair={pair}
923
+ onEdit={handleEditPair}
924
+ onDelete={handleDeletePair}
925
+ />
926
+ </Box>
927
+ ))}
928
+ </Box>
929
+
930
+ {/* Pagination Controls */}
931
+ {filteredPairs.length > pageSize && (
932
+ <Box sx={{ mt: 4, display: 'flex', justifyContent: 'center' }}>
933
+ <Pagination
934
+ count={Math.ceil(filteredPairs.length / pageSize)}
935
+ page={page}
936
+ onChange={(_, value) => setPage(value)}
937
+ color="primary"
938
+ size="large"
939
+ showFirstButton
940
+ showLastButton
941
+ />
942
+ </Box>
943
+ )}
944
+
945
+ <Typography variant="caption" sx={{ display: 'block', textAlign: 'center', mt: 2, color: 'text.secondary' }}>
946
+ Showing {Math.min((page - 1) * pageSize + 1, filteredPairs.length)} - {Math.min(page * pageSize, filteredPairs.length)} of {filteredPairs.length} colors
947
+ </Typography>
948
+ </>
949
+ ) : (
950
+ <Box sx={{
951
+ p: 4,
952
+ textAlign: 'center',
953
+ bgcolor: 'background.paper',
954
+ borderRadius: 1,
955
+ border: '1px dashed #ccc'
956
+ }}>
957
+ <Typography color="textSecondary">
958
+ No colors found using current filters.
959
+ </Typography>
960
+ <Box sx={{ mt: 1 }}>
961
+ <Link
962
+ component="button"
963
+ variant="body2"
964
+ onClick={() => {
965
+ setSearchTerm('');
966
+ setSelectedMode('');
967
+ setPage(1);
968
+ }}
969
+ >
970
+ Clear filters
971
+ </Link>
972
+ </Box>
973
+ </Box>
974
+ )}
975
+
976
+ {/* Show message when no colors exist */}
977
+ {pairs.length === 0 && (
978
+ <Box sx={{ textAlign: 'center', py: 4 }}>
979
+ <Typography variant="body2" color="textSecondary" gutterBottom>
980
+ No colors in your palette yet
981
+ </Typography>
982
+ <Button
983
+ startIcon={<AddIcon />}
984
+ variant="contained"
985
+ onClick={addPair}
986
+ >
987
+ Add Your First Color
988
+ </Button>
989
+ </Box>
990
+ )}
991
+
992
+ {/* Edit Dialog */}
993
+ <ColorEditDialog
994
+ open={editDialogOpen}
995
+ pair={editingPair}
996
+ onClose={() => {
997
+ setEditDialogOpen(false);
998
+ setEditingPair(null);
999
+ }}
1000
+ onSave={handleSavePair}
1001
+ onDelete={handleDeletePair}
1002
+ existingKeys={pairs.map(p => p.key)}
1003
+ updateSingleDomColor={updateSingleDomColor}
1004
+ />
1005
+ </Box>
1006
+ );
1007
+ };
1008
+
1009
+ export const JsonView: React.FC<IDashAutoAdminCustomFieldComponent> = (props) => {
1010
+ const { attribute } = props;
1011
+ const record = useRecordContext();
1012
+
1013
+ const attributePath = attribute.attribute;
1014
+ const isNestedSetting = attributePath.startsWith('settings.');
1015
+ const settingsPath = isNestedSetting ? attributePath.split('.').slice(1).join('.') : attributePath;
1016
+
1017
+ const [searchTerm, setSearchTerm] = useState<string>('');
1018
+ const [selectedMode, setSelectedMode] = useState<string>('');
1019
+ const [isLoading, setIsLoading] = useState<boolean>(true);
1020
+ const [isRendering, setIsRendering] = useState<boolean>(true);
1021
+ const [displayValue, setDisplayValue] = useState<Record<string, any>>({});
1022
+
1023
+ // Dispatch loading events
1024
+ const dispatchLoadingEvent = useCallback((loading: boolean) => {
1025
+ window.dispatchEvent(new MessageEvent('auto-admin-loading-state', { data: loading }));
1026
+ }, []);
1027
+
1028
+ // Convert displayValue to pairs for mode extraction
1029
+ const displayPairs = useMemo(() => {
1030
+ return Object.entries(displayValue).map(([key, value]) => ({
1031
+ key,
1032
+ value: String(value),
1033
+ id: key
1034
+ }));
1035
+ }, [displayValue]);
1036
+
1037
+ // Extract available modes from display pairs
1038
+ const availableModes = useMemo(() => {
1039
+ return extractAvailableModes(displayPairs);
1040
+ }, [displayPairs]);
1041
+
1042
+ // Initial data loading effect
1043
+ useEffect(() => {
1044
+ const loadViewData = async () => {
1045
+ setIsLoading(true);
1046
+ dispatchLoadingEvent(true);
1047
+
1048
+ try {
1049
+ let viewValue: Record<string, any> = {};
1050
+ let defaultValues: Record<string, any> = {};
1051
+
1052
+ try {
1053
+ defaultValues = attribute?.default_value || {};
1054
+ } catch (e) {
1055
+ defaultValues = {};
1056
+ }
1057
+
1058
+ if (isNestedSetting && record?.settings) {
1059
+ const settings = record.settings || {};
1060
+ if (settingsPath) {
1061
+ const keys = settingsPath.split('.');
1062
+ viewValue = { ...defaultValues, ...keys.reduce((o, key) => (o && o[key] !== undefined ? o[key] : {}), settings) };
1063
+ } else {
1064
+ viewValue = { ...defaultValues, ...settings };
1065
+ }
1066
+ } else if (record?.[attributePath]) {
1067
+ viewValue = { ...defaultValues, ...record[attributePath] };
1068
+ } else {
1069
+ viewValue = defaultValues;
1070
+ }
1071
+
1072
+ setDisplayValue(viewValue);
1073
+
1074
+ // Simulate processing time for large datasets
1075
+ if (Object.keys(viewValue).length > 50) {
1076
+ await new Promise(resolve => setTimeout(resolve, 100));
1077
+ }
1078
+
1079
+ } catch (error) {
1080
+ console.error('Error loading view data:', error);
1081
+ } finally {
1082
+ setIsLoading(false);
1083
+ }
1084
+ };
1085
+
1086
+ loadViewData();
1087
+ }, [record, attributePath, attribute, isNestedSetting, settingsPath]);
1088
+
1089
+ // Rendering completion effect
1090
+ useEffect(() => {
1091
+ if (!isLoading && Object.keys(displayValue).length >= 0) {
1092
+ setIsRendering(true);
1093
+
1094
+ requestAnimationFrame(() => {
1095
+ requestAnimationFrame(() => {
1096
+ setIsRendering(false);
1097
+ dispatchLoadingEvent(false);
1098
+ });
1099
+ });
1100
+ }
1101
+ }, [isLoading, displayValue, dispatchLoadingEvent]);
1102
+
1103
+ // Cleanup effect
1104
+ useEffect(() => {
1105
+ return () => {
1106
+ dispatchLoadingEvent(false);
1107
+ };
1108
+ }, [dispatchLoadingEvent]);
1109
+
1110
+ // Filter display values based on search term and selected mode
1111
+ const filteredDisplayValue = useMemo(() => {
1112
+ let filtered = Object.entries(displayValue);
1113
+
1114
+ // Filter by search term
1115
+ if (searchTerm.trim()) {
1116
+ filtered = filtered.filter(([key]) =>
1117
+ key.toLowerCase().includes(searchTerm.toLowerCase())
1118
+ );
1119
+ }
1120
+
1121
+ // Filter by selected mode
1122
+ if (selectedMode) {
1123
+ filtered = filtered.filter(([key]) => {
1124
+ const { mode } = parseColorKey(key);
1125
+ return mode === selectedMode;
1126
+ });
1127
+ }
1128
+
1129
+ return filtered.reduce((acc, [key, value]) => {
1130
+ acc[key] = value;
1131
+ return acc;
1132
+ }, {} as Record<string, any>);
1133
+ }, [displayValue, searchTerm, selectedMode]);
1134
+
1135
+ const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
1136
+ setSearchTerm(event.target.value);
1137
+ };
1138
+
1139
+ const handleClearSearch = () => {
1140
+ setSearchTerm('');
1141
+ };
1142
+
1143
+ const handleModeChange = (event: any) => {
1144
+ setSelectedMode(event.target.value);
1145
+ };
1146
+
1147
+ const handleClearMode = () => {
1148
+ setSelectedMode('');
1149
+ };
1150
+
1151
+ // Show loading state while component is initializing or rendering
1152
+ if (isLoading || isRendering) {
1153
+ return (
1154
+ <Box sx={{ mt: 1, mb: 2, display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 200 }}>
1155
+ <Typography variant="body2" color="textSecondary">
1156
+ {isLoading ? 'Loading color settings...' : 'Rendering colors...'}
1157
+ </Typography>
1158
+ </Box>
1159
+ );
1160
+ }
1161
+
1162
+ return (
1163
+ <Box sx={{ mt: 1, mb: 2 }}>
1164
+ <Typography variant="subtitle1" gutterBottom>
1165
+ {attribute.label || 'Color Palette'}
1166
+ </Typography>
1167
+
1168
+ {Object.keys(displayValue).length > 0 && (
1169
+ <>
1170
+ {/* Search and Filter Controls for View Mode */}
1171
+ <Box sx={{ mb: 3, display: 'flex', gap: 2, alignItems: 'flex-end' }}>
1172
+ {/* Search Field */}
1173
+ <TextField
1174
+ fullWidth
1175
+ size="small"
1176
+ placeholder="Search colors by name..."
1177
+ value={searchTerm}
1178
+ onChange={handleSearchChange}
1179
+ InputProps={{
1180
+ startAdornment: (
1181
+ <InputAdornment position="start">
1182
+ <SearchIcon />
1183
+ </InputAdornment>
1184
+ ),
1185
+ endAdornment: searchTerm && (
1186
+ <InputAdornment position="end">
1187
+ <IconButton
1188
+ size="small"
1189
+ onClick={handleClearSearch}
1190
+ edge="end"
1191
+ >
1192
+ <ClearIcon />
1193
+ </IconButton>
1194
+ </InputAdornment>
1195
+ ),
1196
+ }}
1197
+ />
1198
+
1199
+ {/* Mode Selector */}
1200
+ <FormControl size="small" sx={{ minWidth: 150 }}>
1201
+ <InputLabel>Mode</InputLabel>
1202
+ <Select
1203
+ value={selectedMode}
1204
+ label="Mode"
1205
+ onChange={handleModeChange}
1206
+ startAdornment={
1207
+ <InputAdornment position="start">
1208
+ <FilterListIcon fontSize="small" />
1209
+ </InputAdornment>
1210
+ }
1211
+ endAdornment={selectedMode && (
1212
+ <InputAdornment position="end">
1213
+ <IconButton
1214
+ size="small"
1215
+ onClick={handleClearMode}
1216
+ edge="end"
1217
+ sx={{ mr: 1 }}
1218
+ >
1219
+ <ClearIcon fontSize="small" />
1220
+ </IconButton>
1221
+ </InputAdornment>
1222
+ )}
1223
+ >
1224
+ <MenuItem value="">
1225
+ <em>All Modes</em>
1226
+ </MenuItem>
1227
+ {availableModes.map((mode) => (
1228
+ <MenuItem key={mode} value={mode}>
1229
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
1230
+ <span>{getModeIcon(mode)}</span>
1231
+ <span>{mode}</span>
1232
+ </Box>
1233
+ </MenuItem>
1234
+ ))}
1235
+ </Select>
1236
+ </FormControl>
1237
+ </Box>
1238
+
1239
+ {/* Results count when filtering */}
1240
+ {(searchTerm || selectedMode) && (
1241
+ <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
1242
+ <Chip
1243
+ label={`${Object.keys(filteredDisplayValue).length} of ${Object.keys(displayValue).length} colors`}
1244
+ size="small"
1245
+ variant="outlined"
1246
+ />
1247
+ {selectedMode && (
1248
+ <Chip
1249
+ label={`Mode: ${getModeIcon(selectedMode)} ${selectedMode}`}
1250
+ size="small"
1251
+ variant="filled"
1252
+ color="primary"
1253
+ onDelete={handleClearMode}
1254
+ />
1255
+ )}
1256
+ {searchTerm && (
1257
+ <Chip
1258
+ label={`Search: "${searchTerm}"`}
1259
+ size="small"
1260
+ variant="filled"
1261
+ color="secondary"
1262
+ onDelete={handleClearSearch}
1263
+ />
1264
+ )}
1265
+ </Box>
1266
+ )}
1267
+
1268
+ {Object.keys(filteredDisplayValue).length > 0 ? (
1269
+ <Box sx={{
1270
+ display: 'grid',
1271
+ gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', // Double the width
1272
+ gap: 0,
1273
+ border: '1px solid #e0e0e0',
1274
+ }}>
1275
+ {Object.entries(filteredDisplayValue).map(([key, value]) => {
1276
+ const { baseChips, mode } = parseColorKey(key);
1277
+ const modeIcon = getModeIcon(mode);
1278
+
1279
+ return (
1280
+ <Box
1281
+ key={key}
1282
+ sx={{
1283
+ aspectRatio: 'unset', // Remove aspect ratio
1284
+ height: '30px', // Set fixed height
1285
+ transition: 'all 0.2s ease-in-out',
1286
+ '&:hover': {
1287
+ transform: 'translateY(-2px)',
1288
+ boxShadow: 3,
1289
+ },
1290
+ position: 'relative',
1291
+ overflow: 'hidden',
1292
+ backgroundColor: value as string,
1293
+ display: 'flex',
1294
+ flexDirection: 'row', // Change to row
1295
+ alignItems: 'center',
1296
+ justifyContent: 'flex-start', // Align to start
1297
+ border: '1px solid rgba(255,255,255,0.1)',
1298
+ padding: '0 8px', // Horizontal padding only
1299
+ gap: 0.5,
1300
+ }}
1301
+ >
1302
+ {/* Base name chips */}
1303
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.25, justifyContent: 'center' }}>
1304
+ {baseChips.map((chip, index) => (
1305
+ <Chip
1306
+ key={index}
1307
+ label={chip}
1308
+ size="small"
1309
+ sx={{
1310
+ height: 16,
1311
+ fontSize: '0.6rem',
1312
+ backgroundColor: 'rgba(255,255,255,0.1)',
1313
+ color: '#fff',
1314
+ '& .MuiChip-label': {
1315
+ padding: '0 4px',
1316
+ },
1317
+ }}
1318
+ />
1319
+ ))}
1320
+ </Box>
1321
+
1322
+ {/* Mode chip/icon */}
1323
+ {mode && (
1324
+ <Chip
1325
+ label={modeIcon}
1326
+ size="small"
1327
+ sx={{
1328
+ height: 16,
1329
+ fontSize: '0.6rem',
1330
+ backgroundColor: 'rgba(0,0,0,0.7)',
1331
+ color: '#fff',
1332
+ '& .MuiChip-label': {
1333
+ padding: '0 4px',
1334
+ },
1335
+ }}
1336
+ />
1337
+ )}
1338
+
1339
+ {/* Color value - smaller and at bottom */}
1340
+ <Typography
1341
+ variant="caption"
1342
+ sx={{
1343
+ color: getContrastColor(value as string),
1344
+ backgroundColor: 'rgba(0,0,0,0.1)',
1345
+ padding: '1px 4px',
1346
+ borderRadius: 0.5,
1347
+ fontSize: '0.5rem',
1348
+ fontFamily: 'monospace',
1349
+ textAlign: 'center',
1350
+ mt: 'auto',
1351
+ }}
1352
+ >
1353
+ {value as string}
1354
+ </Typography>
1355
+ </Box>
1356
+ );
1357
+ })}
1358
+ </Box>
1359
+ ) : (
1360
+ <Box sx={{ textAlign: 'center', py: 4 }}>
1361
+ <Typography variant="body2" color="textSecondary">
1362
+ No colors found matching your filters
1363
+ </Typography>
1364
+ <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center', gap: 1 }}>
1365
+ {searchTerm && (
1366
+ <Button size="small" onClick={handleClearSearch}>
1367
+ Clear search
1368
+ </Button>
1369
+ )}
1370
+ {selectedMode && (
1371
+ <Button size="small" onClick={handleClearMode}>
1372
+ Clear mode filter
1373
+ </Button>
1374
+ )}
1375
+ </Box>
1376
+ </Box>
1377
+ )}
1378
+ </>
1379
+ )}
1380
+
1381
+ {Object.keys(displayValue).length === 0 && (
1382
+ <Box sx={{ textAlign: 'center', py: 4 }}>
1383
+ <Typography variant="body2" color="textSecondary">
1384
+ No colors configured in this palette
1385
+ </Typography>
1386
+ </Box>
1387
+ )}
1388
+ </Box>
1389
+ );
1390
+ };
1391
+
1392
+ const JsonColorSelector = ({ method, attribute, resourceConfig }: IDashAutoAdminCustomFieldComponent) => {
1393
+ const [componentLoading, setComponentLoading] = useState(true);
1394
+
1395
+
1396
+ // Dispatch loading event when component mounts
1397
+ useEffect(() => {
1398
+ window.dispatchEvent(new MessageEvent('auto-admin-loading-state', { data: true }));
1399
+
1400
+ // Set a small timeout to allow component to initialize
1401
+ const timer = setTimeout(() => {
1402
+ setComponentLoading(false);
1403
+ }, 50);
1404
+
1405
+ return () => {
1406
+ clearTimeout(timer);
1407
+ // Ensure loading state is cleared on unmount
1408
+ window.dispatchEvent(new MessageEvent('auto-admin-loading-state', { data: false }));
1409
+ };
1410
+ }, []);
1411
+
1412
+ // Show initial loading state
1413
+ if (componentLoading) {
1414
+ return (
1415
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 100 }}>
1416
+ <Typography variant="body2" color="textSecondary">
1417
+ Initializing color palette...
1418
+ </Typography>
1419
+ </Box>
1420
+ );
1421
+ }
1422
+
1423
+ switch (method) {
1424
+ case 'edit':
1425
+ return <JsonEdit attribute={attribute} method={method} resourceConfig={resourceConfig} />;
1426
+ case 'create':
1427
+ return <JsonEdit attribute={attribute} method={method} resourceConfig={resourceConfig} />;
1428
+ case 'view':
1429
+ return <JsonView attribute={attribute} method={method} resourceConfig={resourceConfig} />;
1430
+ default:
1431
+ return null;
1432
+ }
1433
+ };
1434
+
1435
+ export default JsonColorSelector;