@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,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;
|