@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.
- package/README.md +35 -0
- package/dist/components/AuditLog/AuditLog.js +345 -1
- package/dist/components/AuditLog/index.js +4 -1
- package/dist/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.js +630 -3
- package/dist/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.js +53 -1
- package/dist/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.js +647 -3
- package/dist/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.js +53 -1
- package/dist/components/Json/Json.js +235 -1
- package/dist/components/JsonColorSelector/JsonColorSelector.js +1172 -1
- package/dist/components/JsonColorSelector/JsonColorSelectorEnhanced.js +863 -2
- package/dist/components/JsonColorSelector/JsonCssVarValues.js +738 -1
- package/dist/components/JsonColorSelector/components/ColorEditDialog.js +426 -1
- package/dist/components/JsonColorSelector/components/ColorPaletteItem.js +75 -1
- package/dist/components/JsonColorSelector/components/ImageColorExtractor.js +283 -1
- package/dist/components/JsonColorSelector/helpers/functions.js +101 -1
- package/dist/components/JsonColorSelector/useAsyncColorThief.js +105 -1
- package/dist/components/ListActive/ListActive.js +126 -1
- package/dist/components/ListActive/ListBoolean.js +124 -1
- package/dist/components/NotificationPreferences/NotificationPreferences.js +317 -1
- package/dist/components/NotificationPreferences/index.js +7 -1
- package/dist/components/SearchableSelects/RASearchableSelect.js +236 -1
- package/dist/components/SearchableSelects/RASearchableSelectCheckboxes.js +479 -1
- package/dist/components/SearchableSelects/RASearchableSelectChips.js +465 -1
- package/dist/components/SearchableSelects/RASearchableSelectChipsRecordContext.js +295 -1
- package/dist/components/SearchableSelects/SearchableSelect.js +202 -1
- package/dist/components/SearchableSelects/SearchableSelectCheckboxes.js +290 -1
- package/dist/components/SortableDataGrid/SortableDataGrid.js +72 -1
- package/dist/components/SortableDataGrid/SortableDataGridBody.js +167 -1
- package/dist/components/SortableDataGrid/SortableDataGridHeader.js +109 -1
- package/dist/components/SortableDataGrid/SortableDataGridRow.js +283 -1
- package/dist/components/Upload/SingleImageUploader.js +40 -1
- package/dist/components/custom/PackageCopyMethod.js +78 -1
- package/dist/components/custom/PackageRadioButtonGroup.js +45 -1
- package/dist/components/dialog/Basic.js +8 -1
- package/dist/components/dialog/Correct.js +14 -1
- package/dist/components/dialog/Error.js +14 -1
- package/dist/components/dialog/Info.js +14 -1
- package/dist/components/dialog/InfoV2.js +21 -1
- package/dist/components/dialog/NotFound.js +14 -1
- package/dist/components/dialog/QuickSearch.js +117 -1
- package/dist/components/notifications/NotificationsCenter.js +597 -1
- package/dist/components/notifications/index.js +24 -1
- package/dist/components/tables/PaginationComponent.js +36 -1
- package/dist/components/theme/AppLoadingFallback.js +38 -2
- package/dist/components/theme/ErrorBoundary.js +52 -1
- package/dist/components/theme/InitialLoader.js +13 -1
- package/dist/components/theme/NotFound.js +136 -1
- package/dist/components/theme/NotResults.js +38 -1
- package/dist/hooks/useDraggable.js +6 -1
- package/dist/hooks/useNotifications.js +8 -1
- package/dist/hooks/useQuickSearch.js +26 -1
- package/dist/index.js +66 -5567
- package/dist/utils/setNativeValue.js +17 -1
- package/package.json +148 -132
- package/src/components/AuditLog/AuditLog.tsx +476 -0
- package/src/components/AuditLog/index.ts +2 -0
- package/src/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.tsx +917 -0
- package/src/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.tsx +39 -0
- package/src/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.tsx +916 -0
- package/src/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.tsx +44 -0
- package/src/components/Json/Json.tsx +312 -0
- package/src/components/JsonColorSelector/JsonColorSelector.tsx +1435 -0
- package/src/components/JsonColorSelector/JsonColorSelectorEnhanced.tsx +1029 -0
- package/src/components/JsonColorSelector/JsonCssVarValues.tsx +863 -0
- package/src/components/JsonColorSelector/components/ColorEditDialog.tsx +491 -0
- package/src/components/JsonColorSelector/components/ColorPaletteItem.tsx +80 -0
- package/src/components/JsonColorSelector/components/ImageColorExtractor.tsx +328 -0
- package/src/components/JsonColorSelector/helpers/functions.tsx +123 -0
- package/src/components/JsonColorSelector/interfaces/interfaces.tsx +22 -0
- package/src/components/JsonColorSelector/useAsyncColorThief.tsx +97 -0
- package/src/components/ListActive/ListActive.tsx +106 -0
- package/src/components/ListActive/ListBoolean.tsx +96 -0
- package/src/components/NotificationPreferences/NotificationPreferences.tsx +463 -0
- package/src/components/NotificationPreferences/index.tsx +2 -0
- package/src/components/SearchableSelects/RASearchableSelect.tsx +300 -0
- package/src/components/SearchableSelects/RASearchableSelectCheckboxes.tsx +604 -0
- package/src/components/SearchableSelects/RASearchableSelectChips.tsx +558 -0
- package/src/components/SearchableSelects/RASearchableSelectChipsRecordContext.tsx +362 -0
- package/src/components/SearchableSelects/SearchableSelect.tsx +321 -0
- package/src/components/SearchableSelects/SearchableSelectCheckboxes.tsx +355 -0
- package/src/components/SortableDataGrid/SortableDataGrid.tsx +56 -0
- package/src/components/SortableDataGrid/SortableDataGridBody.tsx +175 -0
- package/src/components/SortableDataGrid/SortableDataGridHeader.tsx +171 -0
- package/src/components/SortableDataGrid/SortableDataGridRow.tsx +247 -0
- package/src/components/Upload/SingleImageUploader.tsx +55 -0
- package/src/components/custom/PackageCopyMethod.tsx +109 -0
- package/src/components/custom/PackageRadioButtonGroup.tsx +47 -0
- package/src/components/dialog/Basic.tsx +11 -0
- package/src/components/dialog/Correct.tsx +16 -0
- package/src/components/dialog/Error.tsx +15 -0
- package/src/components/dialog/Info.tsx +15 -0
- package/src/components/dialog/InfoV2.tsx +35 -0
- package/src/components/dialog/NotFound.tsx +18 -0
- package/src/components/dialog/QuickSearch.tsx +115 -0
- package/src/components/notifications/NotificationsCenter.tsx +792 -0
- package/src/components/notifications/index.ts +42 -0
- package/src/components/tables/PaginationComponent.tsx +17 -0
- package/src/components/theme/AppLoadingFallback.tsx +42 -0
- package/src/components/theme/ErrorBoundary.tsx +57 -0
- package/src/components/theme/InitialLoader.tsx +16 -0
- package/src/components/theme/NotFound.tsx +124 -0
- package/src/components/theme/NotResults.tsx +54 -0
- package/src/hooks/useDraggable.ts +11 -0
- package/src/hooks/useNotifications.ts +14 -0
- package/src/hooks/useQuickSearch.tsx +29 -0
- package/src/index.tsx +77 -0
- package/src/utils/setNativeValue.tsx +23 -0
- package/dist/color-thief-CTwGFhOB.js +0 -272
- /package/{dist → src}/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayAutoSearch.tsx.wip +0 -0
- /package/{dist → src}/components/JsonColorSelector/JsonColorSelector.tsx.bkup +0 -0
- /package/{dist → src}/components/SortableDataGrid/README.md +0 -0
- /package/{dist → src}/styles/index.less +0 -0
|
@@ -0,0 +1,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;
|