@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,1029 @@
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
+ Chip,
11
+ InputAdornment,
12
+ FormControl,
13
+ InputLabel,
14
+ Select,
15
+ MenuItem,
16
+ Grid,
17
+ Pagination,
18
+ Card,
19
+ Link,
20
+ } from '@mui/material';
21
+ import AddIcon from '@mui/icons-material/Add';
22
+ import EditIcon from '@mui/icons-material/Edit';
23
+ import PreviewIcon from '@mui/icons-material/Preview';
24
+ import SearchIcon from '@mui/icons-material/Search';
25
+ import ClearIcon from '@mui/icons-material/Clear';
26
+ import FilterListIcon from '@mui/icons-material/FilterList';
27
+ import DownloadIcon from '@mui/icons-material/Download';
28
+ import UploadIcon from '@mui/icons-material/Upload';
29
+ import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
30
+ import { ColorMapping, KeyValuePair } from './interfaces/interfaces';
31
+ import { extractAvailableModes, getContrastColor, getModeIcon, parseColorKey } from './helpers/functions';
32
+ import ImageColorExtractor from './components/ImageColorExtractor';
33
+ import ColorEditDialog from './components/ColorEditDialog';
34
+ import { updateDomCssVariables } from 'dash-utils';
35
+ import { AuthPersistenceService } from 'dash-auth';
36
+
37
+
38
+ // Feature flags for pagination behavior
39
+ const JSON_COLOR_SELECTOR_PAGINATION_ENABLED: boolean = false;
40
+ const JSON_COLOR_SELECTOR_PAGINATION_SHOW_ALL: boolean = false;
41
+
42
+ // Default color mappings (can be modified later)
43
+ const DEFAULT_COLOR_MAPPINGS: ColorMapping = {
44
+ "primary-color": "Primary Color",
45
+ "secondary-color": "Secondary Color",
46
+ "highlight-color": "Highlight Color",
47
+ "contrast-color": "Contrast Color",
48
+ };
49
+
50
+
51
+ // Local Color Palette Item Component with new design
52
+ const LocalColorPaletteItem: React.FC<{
53
+ pair: KeyValuePair;
54
+ onEdit: (pair: KeyValuePair) => void;
55
+ onDelete: (id: string) => void;
56
+ }> = ({ pair, onEdit, onDelete }) => {
57
+ const textColor = getContrastColor(pair.value);
58
+ const { baseChips, mode } = parseColorKey(pair.key);
59
+ const modeIcon = getModeIcon(mode);
60
+
61
+ return (
62
+ <Card
63
+ onClick={() => onEdit(pair)}
64
+ elevation={1}
65
+ sx={{
66
+ width: '100%',
67
+ display: 'flex',
68
+ alignItems: 'stretch', // Ensure stretch for full height
69
+ cursor: 'pointer',
70
+ transition: 'all 0.2s ease-in-out',
71
+ '&:hover': {
72
+ transform: 'translateY(-2px)',
73
+ boxShadow: 3,
74
+ },
75
+ borderRadius: 2,
76
+ overflow: 'hidden',
77
+ position: 'relative',
78
+ height: 80, // Fixed height for consistency
79
+ backgroundColor: 'background.paper',
80
+ }}
81
+ >
82
+ {/* Left Color Box */}
83
+ <Box
84
+ sx={{
85
+ width: 80, // Fixed square width
86
+ backgroundColor: pair.value,
87
+ flexShrink: 0,
88
+ display: 'flex',
89
+ alignItems: 'center',
90
+ justifyContent: 'center',
91
+ position: 'relative',
92
+ }}
93
+ >
94
+ {/* Hover Edit Icon Overlay */}
95
+ <Box
96
+ sx={{
97
+ position: 'absolute',
98
+ top: 0,
99
+ left: 0,
100
+ right: 0,
101
+ bottom: 0,
102
+ backgroundColor: 'rgba(0,0,0,0.3)',
103
+ opacity: 0,
104
+ transition: 'opacity 0.2s',
105
+ '&:hover': { opacity: 1 },
106
+ display: 'flex',
107
+ alignItems: 'center',
108
+ justifyContent: 'center',
109
+ }}
110
+ >
111
+ <EditIcon sx={{ color: '#fff' }} />
112
+ </Box>
113
+ </Box>
114
+
115
+ {/* Right Details Section */}
116
+ <Box sx={{
117
+ flexGrow: 1,
118
+ p: 1.5,
119
+ display: 'flex',
120
+ flexDirection: 'column',
121
+ justifyContent: 'space-between',
122
+ overflow: 'hidden'
123
+ }}>
124
+ <Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 0.5 }}>
125
+ <Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 'bold', color: 'text.secondary' }}>
126
+ {pair.value}
127
+ </Typography>
128
+ {mode && (
129
+ <Chip
130
+ label={mode}
131
+ size="small"
132
+ icon={<span style={{ marginLeft: 6, fontSize: '0.8rem' }}>{modeIcon}</span>}
133
+ sx={{
134
+ height: 18,
135
+ fontSize: '0.65rem',
136
+ '& .MuiChip-label': { padding: '0 6px' },
137
+ }}
138
+ />
139
+ )}
140
+ </Box>
141
+
142
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
143
+ {baseChips.map((chip, index) => (
144
+ <Chip
145
+ key={index}
146
+ label={chip}
147
+ size="small"
148
+ variant="outlined"
149
+ sx={{
150
+ height: 20,
151
+ fontSize: '0.7rem',
152
+ maxWidth: '100%',
153
+ '& .MuiChip-label': {
154
+ padding: '0 6px',
155
+ overflow: 'hidden',
156
+ textOverflow: 'ellipsis',
157
+ },
158
+ }}
159
+ />
160
+ ))}
161
+ </Box>
162
+ </Box>
163
+ </Card>
164
+ );
165
+ };
166
+
167
+ export const JsonEdit: React.FC<IDashAutoAdminCustomFieldComponent> = (props) => {
168
+ const { method, attribute, resourceConfig } = props;
169
+
170
+ const record = useRecordContext();
171
+ const { setValue, getValues } = useFormContext();
172
+
173
+ const attributePath = attribute.attribute;
174
+ const isNestedSetting = attributePath.startsWith('settings.');
175
+ const settingsPath = isNestedSetting ? attributePath.split('.').slice(1).join('.') : attributePath;
176
+
177
+ const [pairs, setPairs] = useState<KeyValuePair[]>([]);
178
+ const [searchTerm, setSearchTerm] = useState<string>('');
179
+ const [selectedMode, setSelectedMode] = useState<string>('');
180
+ const [isLoading, setIsLoading] = useState<boolean>(true);
181
+ const [isRendering, setIsRendering] = useState<boolean>(true);
182
+ const [editDialogOpen, setEditDialogOpen] = useState<boolean>(false);
183
+ const [editingPair, setEditingPair] = useState<KeyValuePair | null>(null);
184
+ const [extractedColors, setExtractedColors] = useState<number[][]>([]); // Add this state
185
+
186
+ // Pagination state
187
+ const [page, setPage] = useState<number>(1);
188
+ const [pageSize] = useState<number>(50);
189
+ const [showAll, setShowAll] = useState<boolean>(!JSON_COLOR_SELECTOR_PAGINATION_ENABLED);
190
+
191
+ // File input ref for CSS import
192
+ const fileInputRef = useRef<HTMLInputElement>(null);
193
+
194
+ // Throttling refs
195
+ const throttleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
196
+ const pendingUpdatesRef = useRef<Map<string, { key: string; color: string }>>(new Map());
197
+
198
+ // Dispatch loading events
199
+ const dispatchLoadingEvent = useCallback((loading: boolean) => {
200
+ window.dispatchEvent(new MessageEvent('auto-admin-loading-state', { data: loading }));
201
+ }, []);
202
+
203
+ const handleColorsExtracted = useCallback((colors: number[][]) => {
204
+ setExtractedColors(colors);
205
+ }, []);
206
+
207
+ // Extract available modes from pairs
208
+ const availableModes = useMemo(() => {
209
+ return extractAvailableModes(pairs);
210
+ }, [pairs]);
211
+
212
+ // Filter pairs based on search term and selected mode
213
+ const filteredPairs = useMemo(() => {
214
+ let filtered = pairs;
215
+
216
+ // Filter by search term
217
+ if (searchTerm.trim()) {
218
+ filtered = filtered.filter(pair =>
219
+ pair.key.toLowerCase().includes(searchTerm.toLowerCase())
220
+ );
221
+ }
222
+
223
+ // Filter by selected mode
224
+ if (selectedMode) {
225
+ filtered = filtered.filter(pair => {
226
+ const { mode } = parseColorKey(pair.key);
227
+ return mode === selectedMode;
228
+ });
229
+ }
230
+
231
+ return filtered;
232
+ }, [pairs, searchTerm, selectedMode]);
233
+
234
+ // Export colors as CSS variables
235
+ const handleExportCSS = useCallback(() => {
236
+ try {
237
+ const cssContent = generateCSSContent(pairs);
238
+ const blob = new Blob([cssContent], { type: 'text/css' });
239
+ const url = URL.createObjectURL(blob);
240
+
241
+ const link = document.createElement('a');
242
+ link.href = url;
243
+ link.download = 'color-variables.css';
244
+ document.body.appendChild(link);
245
+ link.click();
246
+ document.body.removeChild(link);
247
+ URL.revokeObjectURL(url);
248
+
249
+ console.log('CSS variables exported successfully');
250
+ } catch (error) {
251
+ console.error('Error exporting CSS variables:', error);
252
+ }
253
+ }, [pairs]);
254
+
255
+ // Import colors from CSS file
256
+ const handleImportCSS = useCallback(() => {
257
+ fileInputRef.current?.click();
258
+ }, []);
259
+
260
+ // Handle file selection for CSS import
261
+ const handleFileSelect = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
262
+ const file = event.target.files?.[0];
263
+ if (!file) return;
264
+
265
+ const reader = new FileReader();
266
+ reader.onload = (e) => {
267
+ try {
268
+ const cssContent = e.target?.result as string;
269
+ const importedPairs = parseCSSContent(cssContent);
270
+
271
+ if (importedPairs.length > 0) {
272
+ // Merge with existing pairs, with imported pairs taking precedence
273
+ const mergedPairs = mergePairs(pairs, importedPairs);
274
+ setPairs(mergedPairs);
275
+ updateFormValue(mergedPairs);
276
+ console.log(`Imported ${importedPairs.length} color variables from CSS`);
277
+ } else {
278
+ console.warn('No valid CSS variables found in the imported file');
279
+ }
280
+ } catch (error) {
281
+ console.error('Error importing CSS file:', error);
282
+ }
283
+ };
284
+
285
+ reader.readAsText(file);
286
+ // Reset file input
287
+ event.target.value = '';
288
+ }, [pairs]);
289
+
290
+ // Generate CSS content from pairs
291
+ const generateCSSContent = (colorPairs: KeyValuePair[]): string => {
292
+ const cssLines: string[] = [
293
+ '/* Color Variables - Generated by JsonColorSelector */',
294
+ '/* Import this file or copy these variables to your CSS */',
295
+ '',
296
+ ':root {'
297
+ ];
298
+
299
+ // Group by mode for better organization
300
+ const groupedPairs = colorPairs.reduce((acc, pair) => {
301
+ if (!pair.key.trim()) return acc;
302
+
303
+ const { mode } = parseColorKey(pair.key);
304
+ const groupKey = mode || 'default';
305
+
306
+ if (!acc[groupKey]) {
307
+ acc[groupKey] = [];
308
+ }
309
+ acc[groupKey].push(pair);
310
+ return acc;
311
+ }, {} as Record<string, KeyValuePair[]>);
312
+
313
+ // Add CSS variables grouped by mode
314
+ Object.entries(groupedPairs).forEach(([mode, modePairs]) => {
315
+ if (modePairs.length > 0) {
316
+ cssLines.push(` /* ${mode} mode colors */`);
317
+ modePairs.forEach(pair => {
318
+ const cssVarName = pair.key.startsWith('--') ? pair.key : `--${pair.key}`;
319
+ cssLines.push(` ${cssVarName}: ${pair.value};`);
320
+ });
321
+ cssLines.push('');
322
+ }
323
+ });
324
+
325
+ cssLines.push('}');
326
+ cssLines.push('');
327
+ cssLines.push('/* Usage example: */');
328
+ cssLines.push('/* .my-element { background-color: var(--primary-color); } */');
329
+
330
+ return cssLines.join('\n');
331
+ };
332
+
333
+ // Parse CSS content to extract color variables
334
+ const parseCSSContent = (cssContent: string): KeyValuePair[] => {
335
+ const pairs: KeyValuePair[] = [];
336
+
337
+ // Remove comments and normalize whitespace
338
+ const cleanCSS = cssContent
339
+ .replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
340
+ .replace(/\s+/g, ' ') // Normalize whitespace
341
+ .trim();
342
+
343
+ // Match CSS custom properties (variables)
344
+ const variableRegex = /--([^:]+):\s*([^;]+);/g;
345
+ let match;
346
+
347
+ while ((match = variableRegex.exec(cleanCSS)) !== null) {
348
+ const [, name, value] = match;
349
+ const cleanName = name.trim();
350
+ const cleanValue = value.trim();
351
+
352
+ // Only import if it looks like a color value
353
+ if (isColorValue(cleanValue)) {
354
+ pairs.push({
355
+ key: cleanName,
356
+ value: cleanValue,
357
+ id: generateId()
358
+ });
359
+ }
360
+ }
361
+
362
+ return pairs;
363
+ };
364
+
365
+ // Check if a value looks like a color
366
+ const isColorValue = (value: string): boolean => {
367
+ const colorPatterns = [
368
+ /^#[0-9a-fA-F]{3,8}$/, // Hex colors
369
+ /^rgb\(/i, // RGB colors
370
+ /^rgba\(/i, // RGBA colors
371
+ /^hsl\(/i, // HSL colors
372
+ /^hsla\(/i, // HSLA colors
373
+ /^(red|blue|green|yellow|purple|orange|pink|brown|black|white|gray|grey)$/i // Named colors
374
+ ];
375
+
376
+ return colorPatterns.some(pattern => pattern.test(value.trim()));
377
+ };
378
+
379
+ // Merge imported pairs with existing pairs
380
+ const mergePairs = (existingPairs: KeyValuePair[], importedPairs: KeyValuePair[]): KeyValuePair[] => {
381
+ const merged = [...existingPairs];
382
+
383
+ importedPairs.forEach(importedPair => {
384
+ const existingIndex = merged.findIndex(pair => pair.key === importedPair.key);
385
+ if (existingIndex >= 0) {
386
+ // Update existing pair
387
+ merged[existingIndex] = { ...merged[existingIndex], value: importedPair.value };
388
+ } else {
389
+ // Add new pair
390
+ merged.push(importedPair);
391
+ }
392
+ });
393
+
394
+ return merged;
395
+ };
396
+
397
+ const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
398
+ setSearchTerm(event.target.value);
399
+ setPage(1);
400
+ };
401
+
402
+ const handleClearSearch = () => {
403
+ setSearchTerm('');
404
+ setPage(1);
405
+ };
406
+
407
+ const handleModeChange = (event: any) => {
408
+ setSelectedMode(event.target.value);
409
+ setPage(1);
410
+ };
411
+
412
+ const handleClearMode = () => {
413
+ setSelectedMode('');
414
+ setPage(1);
415
+ };
416
+
417
+ // Initial data loading effect
418
+ useEffect(() => {
419
+ const loadInitialData = async () => {
420
+ setIsLoading(true);
421
+ dispatchLoadingEvent(true);
422
+
423
+ try {
424
+ let initialValue: Record<string, any> = {};
425
+ let defaultValues: Record<string, any> = {};
426
+
427
+ try {
428
+ defaultValues = attribute?.default_value || {};
429
+ } catch (e) {
430
+ defaultValues = {};
431
+ }
432
+
433
+ if (isNestedSetting && record?.settings) {
434
+ const settings = record.settings || {};
435
+ initialValue = { ...defaultValues, ...getNestedValue(settings, settingsPath) };
436
+ } else if (record?.[attributePath]) {
437
+ initialValue = { ...defaultValues, ...record[attributePath] };
438
+ } else {
439
+ initialValue = defaultValues;
440
+ }
441
+
442
+ const initialPairs = Object.entries(initialValue).map(([key, value]) => ({
443
+ key,
444
+ value: String(value),
445
+ id: generateId()
446
+ }));
447
+
448
+ setPairs(initialPairs);
449
+
450
+ // Simulate processing time for large datasets
451
+ if (initialPairs.length > 50) {
452
+ await new Promise(resolve => setTimeout(resolve, 100));
453
+ }
454
+
455
+ } catch (error) {
456
+ console.error('Error loading initial data:', error);
457
+ } finally {
458
+ setIsLoading(false);
459
+ }
460
+ };
461
+
462
+ loadInitialData();
463
+ }, [record, attributePath]);
464
+
465
+ // Rendering completion effect
466
+ useEffect(() => {
467
+ if (!isLoading && pairs.length >= 0) {
468
+ setIsRendering(true);
469
+
470
+ requestAnimationFrame(() => {
471
+ requestAnimationFrame(() => {
472
+ setIsRendering(false);
473
+ dispatchLoadingEvent(false);
474
+ });
475
+ });
476
+ }
477
+ }, [isLoading, pairs.length, dispatchLoadingEvent]);
478
+
479
+ // Cleanup throttle timeout on unmount
480
+ useEffect(() => {
481
+ return () => {
482
+ if (throttleTimeoutRef.current) {
483
+ clearTimeout(throttleTimeoutRef.current);
484
+ }
485
+ dispatchLoadingEvent(false);
486
+ };
487
+ }, [dispatchLoadingEvent]);
488
+
489
+ const getNestedValue = (obj: any, path: string): any => {
490
+ if (!path) return obj;
491
+ const keys = path.split('.');
492
+ return keys.reduce((o, key) => (o && o[key] !== undefined ? o[key] : {}), obj);
493
+ };
494
+
495
+ const setNestedValue = (obj: any, path: string, value: any): any => {
496
+ if (!path) return value;
497
+
498
+ const keys = path.split('.');
499
+ const lastKey = keys.pop();
500
+ const lastObj = keys.reduce((o, key) => {
501
+ if (o[key] === undefined) o[key] = {};
502
+ return o[key];
503
+ }, obj);
504
+
505
+ if (lastKey) {
506
+ lastObj[lastKey] = value;
507
+ }
508
+ return obj;
509
+ };
510
+
511
+ const generateId = (): string => {
512
+ return Math.random().toString(36).substring(2, 11);
513
+ };
514
+
515
+ const addPair = () => {
516
+ const newPair = { key: '', value: '#000000', id: generateId() };
517
+ setEditingPair(newPair);
518
+ setEditDialogOpen(true);
519
+ };
520
+
521
+ const handleEditPair = (pair: KeyValuePair) => {
522
+ setEditingPair(pair);
523
+ setEditDialogOpen(true);
524
+ };
525
+
526
+ const handleSavePair = (editedPair: KeyValuePair) => {
527
+ const existingIndex = pairs.findIndex(p => p.id === editedPair.id);
528
+ let updatedPairs: KeyValuePair[];
529
+
530
+ if (existingIndex >= 0) {
531
+ // Update existing pair
532
+ updatedPairs = pairs.map(p => p.id === editedPair.id ? editedPair : p);
533
+ } else {
534
+ // Add new pair
535
+ updatedPairs = [...pairs, editedPair];
536
+ }
537
+
538
+ setPairs(updatedPairs);
539
+ updateFormValue(updatedPairs);
540
+ updateSingleDomColor(editedPair.key, editedPair.value);
541
+ };
542
+
543
+ const handleDeletePair = (id: string) => {
544
+ const updatedPairs = pairs.filter(pair => pair.id !== id);
545
+ setPairs(updatedPairs);
546
+ updateFormValue(updatedPairs);
547
+ };
548
+
549
+ // Handle colors update from ImageColorExtractor
550
+ const handleColorsUpdate = (newPairs: KeyValuePair[]) => {
551
+ setPairs(newPairs);
552
+ updateFormValue(newPairs);
553
+ };
554
+
555
+ // Update only a single DOM color property
556
+ const updateSingleDomColor = useCallback((key: string, color: string) => {
557
+ if (!key.trim()) return;
558
+
559
+ requestAnimationFrame(() => {
560
+ const currentTheme = document.documentElement.getAttribute('data-theme') || '';
561
+ const themeSuffix = currentTheme ? `--${currentTheme}` : '';
562
+
563
+ if (themeSuffix && key.endsWith(themeSuffix)) {
564
+ const baseKey = key.slice(0, -themeSuffix.length);
565
+ document.documentElement.style.setProperty(`--${baseKey}`, color);
566
+ document.documentElement.style.setProperty(`--${baseKey}${themeSuffix}`, color);
567
+ } else {
568
+ // Fallback: update the variable directly if it doesn't match the current theme suffix
569
+ // This handles global variables or keys being edited that don't adhere to the specific suffix
570
+ const varName = key.startsWith('--') ? key : `--${key}`;
571
+ document.documentElement.style.setProperty(varName, color);
572
+ }
573
+ });
574
+ }, []);
575
+
576
+ // Batch update for preview only - processes all colors at once
577
+ const updateDomColors = (colors: Record<string, any>) => {
578
+ const currentTheme = document.documentElement.getAttribute('data-theme');
579
+ const themeSuffix = `--${currentTheme}`;
580
+
581
+ requestAnimationFrame(() => {
582
+ Object.entries(colors).forEach(([key, value]) => {
583
+ if (key.endsWith(themeSuffix)) {
584
+ const baseKey = key.slice(0, -themeSuffix.length);
585
+ document.documentElement.style.setProperty(`--${baseKey}`, String(value));
586
+ }
587
+ });
588
+ });
589
+ };
590
+
591
+ const handlePreview = useCallback((m: string) => {
592
+ const colorsObj = pairs.reduce((acc, pair) => {
593
+ if (pair.key.trim()) {
594
+ acc[pair.key] = pair.value;
595
+ }
596
+ return acc;
597
+ }, {} as Record<string, any>);
598
+
599
+ // Update CSS variables first
600
+ updateDomCssVariables(m, colorsObj,getTenantSettingsValues());
601
+ }, [pairs]); // Make sure pairs is in the dependency array
602
+
603
+
604
+ const syncPairsFromForm = useCallback(() => {
605
+ try {
606
+ let currentFormValue: Record<string, any> = {};
607
+
608
+ if (isNestedSetting) {
609
+ const currentSettings = getValues('settings') || {};
610
+ currentFormValue = getNestedValue(currentSettings, settingsPath) || {};
611
+ } else {
612
+ currentFormValue = getValues(attributePath) || {};
613
+ }
614
+
615
+ return currentFormValue;
616
+ } catch (error) {
617
+ console.error('Error syncing pairs from form values:', error);
618
+ return {};
619
+ }
620
+ }, [getValues, isNestedSetting, settingsPath, attributePath]);
621
+
622
+
623
+ const getTenantSettingsValues = () => {
624
+ const persistedTenantSettings = AuthPersistenceService.getTenantSettings();
625
+ if (persistedTenantSettings) {
626
+ return persistedTenantSettings?.values || {};
627
+ }
628
+ return {}
629
+ };
630
+
631
+
632
+ useEffect(() => {
633
+ const handleThemeSwitch = (event: CustomEvent<{ mode: string }>) => {
634
+ console.log('Theme switch event received:', event.detail.mode);
635
+
636
+ // Get current colors directly from form instead of relying on pairs state
637
+ const currentColors = syncPairsFromForm();
638
+
639
+ console.log('Current colors from form:', currentColors); // Debug log
640
+
641
+ // Update CSS variables with current form values
642
+ updateDomCssVariables(event.detail.mode, currentColors,getTenantSettingsValues());
643
+ };
644
+
645
+ window.addEventListener('dash-theme-mode-switched', handleThemeSwitch as EventListener);
646
+
647
+ return () => {
648
+ window.removeEventListener('dash-theme-mode-switched', handleThemeSwitch as EventListener);
649
+ };
650
+ }, [syncPairsFromForm]);
651
+
652
+
653
+ const updateFormValue = (currentPairs: KeyValuePair[]) => {
654
+ const obj = currentPairs.reduce((acc, pair) => {
655
+ if (pair.key.trim()) {
656
+ acc[pair.key] = pair.value;
657
+ }
658
+ return acc;
659
+ }, {} as Record<string, any>);
660
+
661
+ if (isNestedSetting) {
662
+ const currentSettings = getValues('settings') || {};
663
+ const updatedSettings = setNestedValue({ ...currentSettings }, settingsPath, obj);
664
+ setValue('settings', updatedSettings, { shouldDirty: true });
665
+ } else {
666
+ setValue(attributePath, obj, { shouldDirty: true });
667
+ }
668
+ };
669
+
670
+ // Show loading state while component is initializing or rendering
671
+ if (isLoading || isRendering) {
672
+ return (
673
+ <Box sx={{ mt: 1, mb: 2, display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 200 }}>
674
+ <Typography variant="body2" color="textSecondary">
675
+ {isLoading ? 'Loading color settings...' : 'Rendering colors...'}
676
+ </Typography>
677
+ </Box>
678
+ );
679
+ }
680
+
681
+ return (
682
+ <Box sx={{ mt: 1, mb: 2 }}>
683
+
684
+ <Typography variant="subtitle1" gutterBottom>
685
+ {attribute.label || 'Color Palette'}
686
+ </Typography>
687
+
688
+ {/* Hidden file input for CSS import */}
689
+ <input
690
+ type="file"
691
+ ref={fileInputRef}
692
+ accept=".css"
693
+ style={{ display: 'none' }}
694
+ onChange={handleFileSelect}
695
+ />
696
+
697
+ {/* Image Color Extractor - Updated */}
698
+ <ImageColorExtractor
699
+ onColorsExtracted={handleColorsExtracted}
700
+ onColorsUpdate={handleColorsUpdate}
701
+ existingPairs={pairs}
702
+ />
703
+
704
+ {/* Search and Filter Controls */}
705
+ <Box sx={{ mb: 3, display: 'flex', gap: 2, alignItems: 'flex-end' }}>
706
+ {/* Search Field */}
707
+ <TextField
708
+ fullWidth
709
+ size="small"
710
+ placeholder="Search colors by name..."
711
+ value={searchTerm}
712
+ onChange={handleSearchChange}
713
+ InputProps={{
714
+ startAdornment: (
715
+ <InputAdornment position="start">
716
+ <SearchIcon />
717
+ </InputAdornment>
718
+ ),
719
+ endAdornment: searchTerm && (
720
+ <InputAdornment position="end">
721
+ <IconButton
722
+ size="small"
723
+ onClick={handleClearSearch}
724
+ edge="end"
725
+ >
726
+ <ClearIcon />
727
+ </IconButton>
728
+ </InputAdornment>
729
+ ),
730
+ }}
731
+ />
732
+
733
+ {/* Mode Selector */}
734
+ <FormControl size="small" sx={{ minWidth: 150 }}>
735
+ <InputLabel>Mode</InputLabel>
736
+ <Select
737
+ value={selectedMode}
738
+ label="Mode"
739
+ onChange={handleModeChange}
740
+ startAdornment={
741
+ <InputAdornment position="start">
742
+ <FilterListIcon fontSize="small" />
743
+ </InputAdornment>
744
+ }
745
+ endAdornment={selectedMode && (
746
+ <InputAdornment position="end">
747
+ <IconButton
748
+ size="small"
749
+ onClick={handleClearMode}
750
+ edge="end"
751
+ sx={{ mr: 1 }}
752
+ >
753
+ <ClearIcon fontSize="small" />
754
+ </IconButton>
755
+ </InputAdornment>
756
+ )}
757
+ >
758
+ <MenuItem value="">
759
+ <em>All Modes</em>
760
+ </MenuItem>
761
+ {availableModes.map((mode) => (
762
+ <MenuItem key={mode} value={mode}>
763
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
764
+ <span>{getModeIcon(mode)}</span>
765
+ <span>{mode}</span>
766
+ </Box>
767
+ </MenuItem>
768
+ ))}
769
+ </Select>
770
+ </FormControl>
771
+ </Box>
772
+
773
+ {/* Header Actions */}
774
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3, flexWrap: 'wrap', gap: 1 }}>
775
+ <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
776
+ <Button
777
+ startIcon={<PreviewIcon />}
778
+ variant="contained"
779
+ size="small"
780
+ onClick={() => handlePreview(document.documentElement.getAttribute('data-theme'))}
781
+ color="primary"
782
+ >
783
+ Preview Colors
784
+ </Button>
785
+ <Button
786
+ startIcon={<DownloadIcon />}
787
+ variant="outlined"
788
+ size="small"
789
+ onClick={handleExportCSS}
790
+ color="secondary"
791
+ disabled={pairs.length === 0}
792
+ >
793
+ Export CSS
794
+ </Button>
795
+ <Button
796
+ startIcon={<UploadIcon />}
797
+ variant="outlined"
798
+ size="small"
799
+ onClick={handleImportCSS}
800
+ color="secondary"
801
+ >
802
+ Import CSS
803
+ </Button>
804
+ </Box>
805
+ <Button
806
+ startIcon={<AddIcon />}
807
+ variant="outlined"
808
+ size="small"
809
+ onClick={addPair}
810
+ >
811
+ Add Color
812
+ </Button>
813
+ </Box>
814
+
815
+ {/* Results count when filtering */}
816
+ {(searchTerm || selectedMode) && (
817
+ <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
818
+ <Chip
819
+ label={`${filteredPairs.length} of ${pairs.length} colors`}
820
+ size="small"
821
+ variant="outlined"
822
+ />
823
+ {selectedMode && (
824
+ <Chip
825
+ label={`Mode: ${selectedMode}`}
826
+ size="small"
827
+ variant="filled"
828
+ color="primary"
829
+ onDelete={handleClearMode}
830
+ avatar={<span style={{ paddingLeft: 6 }}>{getModeIcon(selectedMode)}</span>}
831
+ />
832
+ )}
833
+ {searchTerm && (
834
+ <Chip
835
+ label={`Search: "${searchTerm}"`}
836
+ size="small"
837
+ variant="filled"
838
+ color="secondary"
839
+ onDelete={handleClearSearch}
840
+ />
841
+ )}
842
+ </Box>
843
+ )}
844
+
845
+ {/* Color Payload Grid with Pagination */}
846
+ <Box sx={{ flexGrow: 1, my: 2 }}>
847
+ {isRendering ? (
848
+ <Box sx={{ display: 'flex', justifyContent: 'center', p: 4, alignItems: 'center', flexDirection: 'column', gap: 2 }}>
849
+ <Typography>Rendering Grid...</Typography>
850
+ </Box>
851
+ ) : filteredPairs.length > 0 ? (
852
+ <>
853
+ <Box sx={{
854
+ display: 'grid',
855
+ gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
856
+ gap: 2,
857
+ width: '100%'
858
+ }}>
859
+ {(showAll
860
+ ? filteredPairs
861
+ : filteredPairs.slice((page - 1) * pageSize, page * pageSize)
862
+ ).map((pair) => (
863
+ <Box key={pair.id} sx={{ minWidth: 0 }}>
864
+ <LocalColorPaletteItem
865
+ pair={pair}
866
+ onEdit={handleEditPair}
867
+ onDelete={handleDeletePair}
868
+ />
869
+ </Box>
870
+ ))}
871
+ </Box>
872
+
873
+ {/* Pagination Controls - only when pagination is enabled and not showing all */}
874
+ {JSON_COLOR_SELECTOR_PAGINATION_ENABLED && !showAll && filteredPairs.length > pageSize && (
875
+ <Box sx={{ mt: 4, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}>
876
+ <Pagination
877
+ count={Math.ceil(filteredPairs.length / pageSize)}
878
+ page={page}
879
+ onChange={(_, value) => setPage(value)}
880
+ color="primary"
881
+ size="large"
882
+ showFirstButton
883
+ showLastButton
884
+ />
885
+
886
+ {/* Show All button */}
887
+ {JSON_COLOR_SELECTOR_PAGINATION_SHOW_ALL && (
888
+ <Link
889
+ component="button"
890
+ variant="body2"
891
+ underline="always"
892
+ onClick={() => setShowAll(true)}
893
+ sx={{ mt: 0.5, fontSize: '0.8rem', cursor: 'pointer' }}
894
+ >
895
+ Show all
896
+ </Link>
897
+ )}
898
+ </Box>
899
+ )}
900
+
901
+ <Typography variant="caption" sx={{ display: 'block', textAlign: 'center', mt: 2, color: 'text.secondary' }}>
902
+ {showAll
903
+ ? `Showing all ${filteredPairs.length} colors`
904
+ : `Showing ${Math.min((page - 1) * pageSize + 1, filteredPairs.length)} - ${Math.min(page * pageSize, filteredPairs.length)} of ${filteredPairs.length} colors`
905
+ }
906
+ </Typography>
907
+
908
+ {/* Show paginated link when showing all and pagination is enabled */}
909
+ {JSON_COLOR_SELECTOR_PAGINATION_ENABLED && showAll && filteredPairs.length > pageSize && (
910
+ <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
911
+ <Link
912
+ component="button"
913
+ variant="body2"
914
+ underline="always"
915
+ onClick={() => { setShowAll(false); setPage(1); }}
916
+ sx={{ fontSize: '0.8rem', cursor: 'pointer' }}
917
+ >
918
+ Show paginated
919
+ </Link>
920
+ </Box>
921
+ )}
922
+ </>
923
+ ) : (
924
+ <Box sx={{
925
+ p: 4,
926
+ textAlign: 'center',
927
+ bgcolor: 'background.paper',
928
+ borderRadius: 1,
929
+ border: '1px dashed #ccc'
930
+ }}>
931
+ <Typography color="textSecondary">
932
+ No colors found using current filters.
933
+ </Typography>
934
+ <Box sx={{ mt: 1 }}>
935
+ <Link
936
+ component="button"
937
+ variant="body2"
938
+ onClick={() => {
939
+ setSearchTerm('');
940
+ setSelectedMode('');
941
+ setPage(1);
942
+ }}
943
+ >
944
+ Clear filters
945
+ </Link>
946
+ </Box>
947
+ </Box>
948
+ )}
949
+ </Box>
950
+
951
+
952
+ {/* Show message when no colors exist */}
953
+ {pairs.length === 0 && (
954
+ <Box sx={{ textAlign: 'center', py: 4 }}>
955
+ <Typography variant="body2" color="textSecondary" gutterBottom>
956
+ No colors in your palette yet
957
+ </Typography>
958
+ <Button
959
+ startIcon={<AddIcon />}
960
+ variant="contained"
961
+ onClick={addPair}
962
+ >
963
+ Add Your First Color
964
+ </Button>
965
+ </Box>
966
+ )}
967
+
968
+ {/* Edit Dialog - Updated with extracted colors */}
969
+ <ColorEditDialog
970
+ open={editDialogOpen}
971
+ pair={editingPair}
972
+ onClose={() => {
973
+ setEditDialogOpen(false);
974
+ setEditingPair(null);
975
+ }}
976
+ onSave={handleSavePair}
977
+ onDelete={handleDeletePair}
978
+ existingKeys={pairs.map(p => p.key)}
979
+ updateSingleDomColor={updateSingleDomColor}
980
+ extractedColors={extractedColors} // Pass extracted colors
981
+ />
982
+ </Box>
983
+ );
984
+ };
985
+
986
+
987
+ const JsonColorSelector = ({ method, attribute, resourceConfig }: IDashAutoAdminCustomFieldComponent) => {
988
+ const [componentLoading, setComponentLoading] = useState(true);
989
+
990
+ // Dispatch loading event when component mounts
991
+ useEffect(() => {
992
+ window.dispatchEvent(new MessageEvent('auto-admin-loading-state', { data: true }));
993
+
994
+ // Set a small timeout to allow component to initialize
995
+ const timer = setTimeout(() => {
996
+ setComponentLoading(false);
997
+ }, 50);
998
+
999
+ return () => {
1000
+ clearTimeout(timer);
1001
+ // Ensure loading state is cleared on unmount
1002
+ window.dispatchEvent(new MessageEvent('auto-admin-loading-state', { data: false }));
1003
+ };
1004
+ }, []);
1005
+
1006
+ // Show initial loading state
1007
+ if (componentLoading) {
1008
+ return (
1009
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 100 }}>
1010
+ <Typography variant="body2" color="textSecondary">
1011
+ Initializing color palette...
1012
+ </Typography>
1013
+ </Box>
1014
+ );
1015
+ }
1016
+
1017
+ switch (method) {
1018
+ case 'edit':
1019
+ return <JsonEdit attribute={attribute} method={method} resourceConfig={resourceConfig} />;
1020
+ case 'create':
1021
+ return <JsonEdit attribute={attribute} method={method} resourceConfig={resourceConfig} />;
1022
+ case 'view':
1023
+ case 'list':
1024
+ default:
1025
+ return <>Not implemented</>;
1026
+ }
1027
+ };
1028
+
1029
+ export default JsonColorSelector;