@dashadmin/dash-components 1.3.24 → 1.3.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/components/AuditLog/AuditLog.js +345 -1
  2. package/dist/components/AuditLog/index.js +4 -1
  3. package/dist/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.js +630 -3
  4. package/dist/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.js +53 -1
  5. package/dist/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.js +647 -3
  6. package/dist/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.js +53 -1
  7. package/dist/components/Json/Json.js +235 -1
  8. package/dist/components/JsonColorSelector/JsonColorSelector.js +1172 -1
  9. package/dist/components/JsonColorSelector/JsonColorSelectorEnhanced.js +863 -2
  10. package/dist/components/JsonColorSelector/JsonCssVarValues.js +738 -1
  11. package/dist/components/JsonColorSelector/components/ColorEditDialog.js +426 -1
  12. package/dist/components/JsonColorSelector/components/ColorPaletteItem.js +75 -1
  13. package/dist/components/JsonColorSelector/components/ImageColorExtractor.js +283 -1
  14. package/dist/components/JsonColorSelector/helpers/functions.js +101 -1
  15. package/dist/components/JsonColorSelector/useAsyncColorThief.js +105 -1
  16. package/dist/components/ListActive/ListActive.js +126 -1
  17. package/dist/components/ListActive/ListBoolean.js +124 -1
  18. package/dist/components/NotificationPreferences/NotificationPreferences.js +317 -1
  19. package/dist/components/NotificationPreferences/index.js +7 -1
  20. package/dist/components/SearchableSelects/RASearchableSelect.js +236 -1
  21. package/dist/components/SearchableSelects/RASearchableSelectCheckboxes.js +479 -1
  22. package/dist/components/SearchableSelects/RASearchableSelectChips.js +465 -1
  23. package/dist/components/SearchableSelects/RASearchableSelectChipsRecordContext.js +295 -1
  24. package/dist/components/SearchableSelects/SearchableSelect.js +202 -1
  25. package/dist/components/SearchableSelects/SearchableSelectCheckboxes.js +290 -1
  26. package/dist/components/SortableDataGrid/SortableDataGrid.js +72 -1
  27. package/dist/components/SortableDataGrid/SortableDataGridBody.js +167 -1
  28. package/dist/components/SortableDataGrid/SortableDataGridHeader.js +109 -1
  29. package/dist/components/SortableDataGrid/SortableDataGridRow.js +283 -1
  30. package/dist/components/Upload/SingleImageUploader.js +40 -1
  31. package/dist/components/custom/PackageCopyMethod.js +78 -1
  32. package/dist/components/custom/PackageRadioButtonGroup.js +45 -1
  33. package/dist/components/dialog/Basic.js +8 -1
  34. package/dist/components/dialog/Correct.js +14 -1
  35. package/dist/components/dialog/Error.js +14 -1
  36. package/dist/components/dialog/Info.js +14 -1
  37. package/dist/components/dialog/InfoV2.js +21 -1
  38. package/dist/components/dialog/NotFound.js +14 -1
  39. package/dist/components/dialog/QuickSearch.js +117 -1
  40. package/dist/components/notifications/NotificationsCenter.js +597 -1
  41. package/dist/components/notifications/index.js +24 -1
  42. package/dist/components/tables/PaginationComponent.js +36 -1
  43. package/dist/components/theme/AppLoadingFallback.js +38 -2
  44. package/dist/components/theme/ErrorBoundary.js +52 -1
  45. package/dist/components/theme/InitialLoader.js +13 -1
  46. package/dist/components/theme/NotFound.js +136 -1
  47. package/dist/components/theme/NotResults.js +38 -1
  48. package/dist/hooks/useDraggable.js +6 -1
  49. package/dist/hooks/useNotifications.js +8 -1
  50. package/dist/hooks/useQuickSearch.js +26 -1
  51. package/dist/index.js +66 -5567
  52. package/dist/utils/setNativeValue.js +17 -1
  53. package/package.json +149 -132
  54. package/src/components/AuditLog/AuditLog.tsx +476 -0
  55. package/src/components/AuditLog/index.ts +2 -0
  56. package/src/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.tsx +917 -0
  57. package/src/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.tsx +39 -0
  58. package/src/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.tsx +916 -0
  59. package/src/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.tsx +44 -0
  60. package/src/components/Json/Json.tsx +312 -0
  61. package/src/components/JsonColorSelector/JsonColorSelector.tsx +1435 -0
  62. package/src/components/JsonColorSelector/JsonColorSelectorEnhanced.tsx +1029 -0
  63. package/src/components/JsonColorSelector/JsonCssVarValues.tsx +863 -0
  64. package/src/components/JsonColorSelector/components/ColorEditDialog.tsx +491 -0
  65. package/src/components/JsonColorSelector/components/ColorPaletteItem.tsx +80 -0
  66. package/src/components/JsonColorSelector/components/ImageColorExtractor.tsx +328 -0
  67. package/src/components/JsonColorSelector/helpers/functions.tsx +123 -0
  68. package/src/components/JsonColorSelector/interfaces/interfaces.tsx +22 -0
  69. package/src/components/JsonColorSelector/useAsyncColorThief.tsx +97 -0
  70. package/src/components/ListActive/ListActive.tsx +106 -0
  71. package/src/components/ListActive/ListBoolean.tsx +96 -0
  72. package/src/components/NotificationPreferences/NotificationPreferences.tsx +463 -0
  73. package/src/components/NotificationPreferences/index.tsx +2 -0
  74. package/src/components/SearchableSelects/RASearchableSelect.tsx +300 -0
  75. package/src/components/SearchableSelects/RASearchableSelectCheckboxes.tsx +604 -0
  76. package/src/components/SearchableSelects/RASearchableSelectChips.tsx +558 -0
  77. package/src/components/SearchableSelects/RASearchableSelectChipsRecordContext.tsx +362 -0
  78. package/src/components/SearchableSelects/SearchableSelect.tsx +321 -0
  79. package/src/components/SearchableSelects/SearchableSelectCheckboxes.tsx +355 -0
  80. package/src/components/SortableDataGrid/SortableDataGrid.tsx +56 -0
  81. package/src/components/SortableDataGrid/SortableDataGridBody.tsx +175 -0
  82. package/src/components/SortableDataGrid/SortableDataGridHeader.tsx +171 -0
  83. package/src/components/SortableDataGrid/SortableDataGridRow.tsx +247 -0
  84. package/src/components/Upload/SingleImageUploader.tsx +55 -0
  85. package/src/components/custom/PackageCopyMethod.tsx +109 -0
  86. package/src/components/custom/PackageRadioButtonGroup.tsx +47 -0
  87. package/src/components/dialog/Basic.tsx +11 -0
  88. package/src/components/dialog/Correct.tsx +16 -0
  89. package/src/components/dialog/Error.tsx +15 -0
  90. package/src/components/dialog/Info.tsx +15 -0
  91. package/src/components/dialog/InfoV2.tsx +35 -0
  92. package/src/components/dialog/NotFound.tsx +18 -0
  93. package/src/components/dialog/QuickSearch.tsx +115 -0
  94. package/src/components/notifications/NotificationsCenter.tsx +792 -0
  95. package/src/components/notifications/index.ts +42 -0
  96. package/src/components/tables/PaginationComponent.tsx +17 -0
  97. package/src/components/theme/AppLoadingFallback.tsx +42 -0
  98. package/src/components/theme/ErrorBoundary.tsx +57 -0
  99. package/src/components/theme/InitialLoader.tsx +16 -0
  100. package/src/components/theme/NotFound.tsx +124 -0
  101. package/src/components/theme/NotResults.tsx +54 -0
  102. package/src/hooks/useDraggable.ts +11 -0
  103. package/src/hooks/useNotifications.ts +14 -0
  104. package/src/hooks/useQuickSearch.tsx +29 -0
  105. package/src/index.tsx +77 -0
  106. package/src/utils/setNativeValue.tsx +23 -0
  107. package/dist/color-thief-CTwGFhOB.js +0 -272
  108. /package/{dist → src}/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayAutoSearch.tsx.wip +0 -0
  109. /package/{dist → src}/components/JsonColorSelector/JsonColorSelector.tsx.bkup +0 -0
  110. /package/{dist → src}/components/SortableDataGrid/README.md +0 -0
  111. /package/{dist → src}/styles/index.less +0 -0
@@ -0,0 +1,863 @@
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
+ Card,
11
+ CardContent,
12
+ Chip,
13
+ InputAdornment,
14
+ } from '@mui/material';
15
+ import DeleteIcon from '@mui/icons-material/Delete';
16
+ import AddIcon from '@mui/icons-material/Add';
17
+ import SearchIcon from '@mui/icons-material/Search';
18
+ import ClearIcon from '@mui/icons-material/Clear';
19
+ import { useState, useEffect, useCallback, useMemo } from 'react';
20
+
21
+ interface KeyValuePair {
22
+ key: string;
23
+ value: string;
24
+ id: string;
25
+ }
26
+
27
+ type ValueType = 'string' | 'number' | 'boolean' | 'json';
28
+
29
+ // Helper function to detect value type
30
+ const detectValueType = (value: string): ValueType => {
31
+ if (value === 'true' || value === 'false') return 'boolean';
32
+ if (!isNaN(Number(value)) && value.trim() !== '') return 'number';
33
+ if ((value.startsWith('{') && value.endsWith('}')) ||
34
+ (value.startsWith('[') && value.endsWith(']'))) return 'json';
35
+ return 'string';
36
+ };
37
+
38
+ // Helper function to format value for display
39
+ const formatValueForDisplay = (value: string, maxLength: number = 30): string => {
40
+ if (value.length <= maxLength) return value;
41
+ return value.substring(0, maxLength) + '...';
42
+ };
43
+
44
+ // Inline editable value item component
45
+ const ValueItem: React.FC<{
46
+ pair: KeyValuePair;
47
+ onUpdate: (pair: KeyValuePair) => void;
48
+ onDelete: (id: string) => void;
49
+ existingKeys: string[];
50
+ }> = ({ pair, onUpdate, onDelete, existingKeys }) => {
51
+ const [isEditingKey, setIsEditingKey] = useState(false);
52
+ const [isEditingValue, setIsEditingValue] = useState(false);
53
+ const [editKey, setEditKey] = useState(pair.key);
54
+ const [editValue, setEditValue] = useState(pair.value);
55
+ const [keyError, setKeyError] = useState('');
56
+ const [valueError, setValueError] = useState('');
57
+
58
+ const valueType = detectValueType(pair.value);
59
+
60
+ // Color coding for different value types
61
+ const getTypeColor = (type: ValueType) => {
62
+ switch (type) {
63
+ case 'string': return '#2196f3'; // Blue
64
+ case 'number': return '#4caf50'; // Green
65
+ case 'boolean': return '#ff9800'; // Orange
66
+ case 'json': return '#9c27b0'; // Purple
67
+ default: return '#757575'; // Grey
68
+ }
69
+ };
70
+
71
+ const validateKey = (key: string) => {
72
+ const isDuplicate = existingKeys.some(k => k === key && key !== pair.key);
73
+ setKeyError(isDuplicate ? 'Key already exists' : '');
74
+ return !isDuplicate && key.trim() !== '';
75
+ };
76
+
77
+ const validateValue = (value: string, type: ValueType) => {
78
+ let error = '';
79
+ if (type === 'number' && isNaN(Number(value)) && value.trim() !== '') {
80
+ error = 'Invalid number format';
81
+ } else if (type === 'boolean' && !['true', 'false'].includes(value.toLowerCase())) {
82
+ error = 'Boolean must be "true" or "false"';
83
+ } else if (type === 'json') {
84
+ try {
85
+ JSON.parse(value);
86
+ } catch (e) {
87
+ error = 'Invalid JSON format';
88
+ }
89
+ }
90
+ setValueError(error);
91
+ return !error;
92
+ };
93
+
94
+ const handleKeySubmit = () => {
95
+ if (validateKey(editKey)) {
96
+ onUpdate({ ...pair, key: editKey });
97
+ setIsEditingKey(false);
98
+ }
99
+ };
100
+
101
+ const handleValueSubmit = () => {
102
+ const currentType = detectValueType(editValue);
103
+ if (validateValue(editValue, currentType)) {
104
+ onUpdate({ ...pair, value: editValue });
105
+ setIsEditingValue(false);
106
+ }
107
+ };
108
+
109
+ const handleKeyCancel = () => {
110
+ setEditKey(pair.key);
111
+ setKeyError('');
112
+ setIsEditingKey(false);
113
+ };
114
+
115
+ const handleValueCancel = () => {
116
+ setEditValue(pair.value);
117
+ setValueError('');
118
+ setIsEditingValue(false);
119
+ };
120
+
121
+ return (
122
+ <Card
123
+ variant="outlined"
124
+ sx={{
125
+ mb: 1,
126
+ //border: `2px solid ${getTypeColor(valueType)}`,
127
+ '&:hover': {
128
+ boxShadow: 2,
129
+ },
130
+ }}
131
+ >
132
+ <CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}>
133
+ <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
134
+ {/* Key Section */}
135
+ <Box sx={{ flex: 1, minWidth: 0 }}>
136
+ <Typography variant="caption" color="textSecondary" sx={{ display: 'block', mb: 0.5 }}>
137
+ Key
138
+ </Typography>
139
+ {isEditingKey ? (
140
+ <TextField
141
+ fullWidth
142
+ size="small"
143
+ value={editKey}
144
+ onChange={(e) => {
145
+ setEditKey(e.target.value);
146
+ validateKey(e.target.value);
147
+ }}
148
+ error={!!keyError}
149
+ helperText={keyError}
150
+ onKeyDown={(e) => {
151
+ if (e.key === 'Enter') handleKeySubmit();
152
+ if (e.key === 'Escape') handleKeyCancel();
153
+ }}
154
+ onBlur={handleKeySubmit}
155
+ autoFocus
156
+ />
157
+ ) : (
158
+ <Box
159
+ onClick={() => setIsEditingKey(true)}
160
+ sx={{
161
+ cursor: 'pointer',
162
+ p: 1,
163
+ borderRadius: 1,
164
+ backgroundColor: 'rgba(0,0,0,0.04)',
165
+ '&:hover': {
166
+ backgroundColor: 'rgba(0,0,0,0.08)',
167
+ },
168
+ minHeight: 24,
169
+ display: 'flex',
170
+ alignItems: 'center',
171
+ }}
172
+ >
173
+ <Typography
174
+ variant="body2"
175
+ sx={{
176
+ fontFamily: 'monospace',
177
+ wordBreak: 'break-all',
178
+ }}
179
+ >
180
+ {pair.key || 'Click to add key'}
181
+ </Typography>
182
+ </Box>
183
+ )}
184
+ </Box>
185
+
186
+ {/* Value Section */}
187
+ <Box sx={{ flex: 1, minWidth: 0 }}>
188
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
189
+ <Typography variant="caption" color="textSecondary">
190
+ Value
191
+ </Typography>
192
+ <Chip
193
+ label={valueType.toUpperCase()}
194
+ size="small"
195
+ sx={{
196
+ height: 16,
197
+ fontSize: '0.6rem',
198
+ backgroundColor: getTypeColor(valueType),
199
+ color: '#fff',
200
+ '& .MuiChip-label': {
201
+ padding: '0 4px',
202
+ },
203
+ }}
204
+ />
205
+ </Box>
206
+ {isEditingValue ? (
207
+ <TextField
208
+ fullWidth
209
+ size="small"
210
+ value={editValue}
211
+ onChange={(e) => {
212
+ setEditValue(e.target.value);
213
+ const currentType = detectValueType(e.target.value);
214
+ validateValue(e.target.value, currentType);
215
+ }}
216
+ error={!!valueError}
217
+ helperText={valueError}
218
+ multiline={valueType === 'json'}
219
+ rows={valueType === 'json' ? 3 : 1}
220
+ onKeyDown={(e) => {
221
+ if (e.key === 'Enter' && !e.shiftKey && valueType !== 'json') {
222
+ e.preventDefault();
223
+ handleValueSubmit();
224
+ }
225
+ if (e.key === 'Escape') handleValueCancel();
226
+ }}
227
+ onBlur={handleValueSubmit}
228
+ autoFocus
229
+ />
230
+ ) : (
231
+ <Box
232
+ onClick={() => setIsEditingValue(true)}
233
+ sx={{
234
+ cursor: 'pointer',
235
+ p: 1,
236
+ borderRadius: 1,
237
+ backgroundColor: 'rgba(0,0,0,0.04)',
238
+ '&:hover': {
239
+ backgroundColor: 'rgba(0,0,0,0.08)',
240
+ },
241
+ minHeight: 24,
242
+ display: 'flex',
243
+ alignItems: 'center',
244
+ }}
245
+ >
246
+ <Typography
247
+ variant="body2"
248
+ sx={{
249
+ fontFamily: 'monospace',
250
+ wordBreak: 'break-all',
251
+ }}
252
+ title={pair.value} // Show full value on hover
253
+ >
254
+ {formatValueForDisplay(pair.value) || 'Click to add value'}
255
+ </Typography>
256
+ </Box>
257
+ )}
258
+ </Box>
259
+
260
+ {/* Actions */}
261
+ <Box sx={{ display: 'flex', alignItems: 'center' }}>
262
+ <IconButton
263
+ size="small"
264
+ onClick={() => onDelete(pair.id)}
265
+ color="error"
266
+ >
267
+ <DeleteIcon fontSize="small" />
268
+ </IconButton>
269
+ </Box>
270
+ </Box>
271
+ </CardContent>
272
+ </Card>
273
+ );
274
+ };
275
+
276
+ export const JsonEdit: React.FC<IDashAutoAdminCustomFieldComponent> = (props) => {
277
+ const { attribute } = props;
278
+
279
+ const record = useRecordContext();
280
+ const { setValue, getValues } = useFormContext();
281
+
282
+ const attributePath = attribute.attribute;
283
+ const isNestedSetting = attributePath.startsWith('settings.');
284
+ const settingsPath = isNestedSetting ? attributePath.split('.').slice(1).join('.') : attributePath;
285
+
286
+ const [pairs, setPairs] = useState<KeyValuePair[]>([]);
287
+ const [searchTerm, setSearchTerm] = useState<string>('');
288
+ const [isLoading, setIsLoading] = useState<boolean>(true);
289
+
290
+ // Filter pairs based on search term
291
+ const filteredPairs = useMemo(() => {
292
+ if (!searchTerm.trim()) return pairs;
293
+
294
+ return pairs.filter(pair =>
295
+ pair.key.toLowerCase().includes(searchTerm.toLowerCase()) ||
296
+ pair.value.toLowerCase().includes(searchTerm.toLowerCase())
297
+ );
298
+ }, [pairs, searchTerm]);
299
+
300
+ const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
301
+ setSearchTerm(event.target.value);
302
+ };
303
+
304
+ const handleClearSearch = () => {
305
+ setSearchTerm('');
306
+ };
307
+
308
+ // Initial data loading effect
309
+ useEffect(() => {
310
+ const loadInitialData = async () => {
311
+ setIsLoading(true);
312
+
313
+ try {
314
+ let initialValue: Record<string, any> = {};
315
+ let defaultValues: Record<string, any> = {};
316
+
317
+ try {
318
+ defaultValues = attribute?.default_value || {};
319
+ } catch (e) {
320
+ defaultValues = {};
321
+ }
322
+
323
+ if (isNestedSetting && record?.settings) {
324
+ const settings = record.settings || {};
325
+ initialValue = { ...defaultValues, ...getNestedValue(settings, settingsPath) };
326
+ } else if (record?.[attributePath]) {
327
+ initialValue = { ...defaultValues, ...record[attributePath] };
328
+ } else {
329
+ initialValue = defaultValues;
330
+ }
331
+
332
+ const initialPairs = Object.entries(initialValue).map(([key, value]) => ({
333
+ key,
334
+ value: String(value),
335
+ id: generateId()
336
+ }));
337
+
338
+ setPairs(initialPairs);
339
+
340
+ } catch (error) {
341
+ console.error('Error loading initial data:', error);
342
+ } finally {
343
+ setIsLoading(false);
344
+ }
345
+ };
346
+
347
+ loadInitialData();
348
+ }, [record, attributePath]);
349
+
350
+ const getNestedValue = (obj: any, path: string): any => {
351
+ if (!path) return obj;
352
+ const keys = path.split('.');
353
+ return keys.reduce((o, key) => (o && o[key] !== undefined ? o[key] : {}), obj);
354
+ };
355
+
356
+ const setNestedValue = (obj: any, path: string, value: any): any => {
357
+ if (!path) return value;
358
+
359
+ const keys = path.split('.');
360
+ const lastKey = keys.pop();
361
+ const lastObj = keys.reduce((o, key) => {
362
+ if (o[key] === undefined) o[key] = {};
363
+ return o[key];
364
+ }, obj);
365
+
366
+ if (lastKey) {
367
+ lastObj[lastKey] = value;
368
+ }
369
+ return obj;
370
+ };
371
+
372
+ const generateId = (): string => {
373
+ return Math.random().toString(36).substring(2, 11);
374
+ };
375
+
376
+ const addPair = () => {
377
+ const newPair = { key: '', value: '', id: generateId() };
378
+ const updatedPairs = [...pairs, newPair];
379
+ setPairs(updatedPairs);
380
+ updateFormValue(updatedPairs);
381
+ };
382
+
383
+ const handleUpdatePair = (updatedPair: KeyValuePair) => {
384
+ const updatedPairs = pairs.map(p => p.id === updatedPair.id ? updatedPair : p);
385
+ setPairs(updatedPairs);
386
+ updateFormValue(updatedPairs);
387
+ };
388
+
389
+ const handleDeletePair = (id: string) => {
390
+ const updatedPairs = pairs.filter(pair => pair.id !== id);
391
+ setPairs(updatedPairs);
392
+ updateFormValue(updatedPairs);
393
+ };
394
+
395
+ const updateFormValue = (currentPairs: KeyValuePair[]) => {
396
+ const obj = currentPairs.reduce((acc, pair) => {
397
+ if (pair.key.trim()) {
398
+ // Parse value based on detected type
399
+ const valueType = detectValueType(pair.value);
400
+ let parsedValue: any = pair.value;
401
+
402
+ try {
403
+ switch (valueType) {
404
+ case 'number':
405
+ parsedValue = Number(pair.value);
406
+ break;
407
+ case 'boolean':
408
+ parsedValue = pair.value.toLowerCase() === 'true';
409
+ break;
410
+ case 'json':
411
+ parsedValue
412
+ parsedValue = JSON.parse(pair.value);
413
+ break;
414
+ case 'string':
415
+ default:
416
+ parsedValue = pair.value;
417
+ break;
418
+ }
419
+ } catch (e) {
420
+ // If parsing fails, use string value
421
+ parsedValue = pair.value;
422
+ }
423
+
424
+ acc[pair.key] = parsedValue;
425
+ }
426
+ return acc;
427
+ }, {} as Record<string, any>);
428
+
429
+ if (isNestedSetting) {
430
+ const currentSettings = getValues('settings') || {};
431
+ const updatedSettings = setNestedValue({...currentSettings}, settingsPath, obj);
432
+ setValue('settings', updatedSettings, { shouldDirty: true });
433
+ } else {
434
+ setValue(attributePath, obj, { shouldDirty: true });
435
+ }
436
+ };
437
+
438
+ // Show loading state while component is initializing
439
+ if (isLoading) {
440
+ return (
441
+ <Box sx={{ mt: 1, mb: 2, display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 200 }}>
442
+ <Typography variant="body2" color="textSecondary">
443
+ Loading values...
444
+ </Typography>
445
+ </Box>
446
+ );
447
+ }
448
+
449
+ return (
450
+ <Box sx={{ mt: 1, mb: 2 }}>
451
+ <Typography variant="subtitle1" gutterBottom>
452
+ {attribute.label || 'Key-Value Pairs'}
453
+ </Typography>
454
+
455
+ {/* Header Actions */}
456
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
457
+ <Button
458
+ startIcon={<AddIcon />}
459
+ variant="outlined"
460
+ size="small"
461
+ onClick={addPair}
462
+ >
463
+ Add Pair
464
+ </Button>
465
+ </Box>
466
+
467
+ {/* Search Control */}
468
+ {pairs.length > 0 && (
469
+ <Box sx={{ mb: 3 }}>
470
+ <TextField
471
+ fullWidth
472
+ size="small"
473
+ placeholder="Search by key or value..."
474
+ value={searchTerm}
475
+ onChange={handleSearchChange}
476
+ InputProps={{
477
+ startAdornment: (
478
+ <InputAdornment position="start">
479
+ <SearchIcon />
480
+ </InputAdornment>
481
+ ),
482
+ endAdornment: searchTerm && (
483
+ <InputAdornment position="end">
484
+ <IconButton
485
+ size="small"
486
+ onClick={handleClearSearch}
487
+ edge="end"
488
+ >
489
+ <ClearIcon />
490
+ </IconButton>
491
+ </InputAdornment>
492
+ ),
493
+ }}
494
+ />
495
+ </Box>
496
+ )}
497
+
498
+ {/* Results count when filtering */}
499
+ {searchTerm && (
500
+ <Box sx={{ mb: 2 }}>
501
+ <Chip
502
+ label={`${filteredPairs.length} of ${pairs.length} pairs`}
503
+ size="small"
504
+ variant="outlined"
505
+ onDelete={handleClearSearch}
506
+ />
507
+ </Box>
508
+ )}
509
+
510
+ {/* Values List */}
511
+ <Box sx={{ mb: 2 }}>
512
+ {filteredPairs.map((pair) => (
513
+ <ValueItem
514
+ key={pair.id}
515
+ pair={pair}
516
+ onUpdate={handleUpdatePair}
517
+ onDelete={handleDeletePair}
518
+ existingKeys={pairs.map(p => p.key)}
519
+ />
520
+ ))}
521
+ </Box>
522
+
523
+ {/* Show message when no results found */}
524
+ {searchTerm && filteredPairs.length === 0 && (
525
+ <Box sx={{ textAlign: 'center', py: 4 }}>
526
+ <Typography variant="body2" color="textSecondary">
527
+ No pairs found matching your search
528
+ </Typography>
529
+ <Button size="small" onClick={handleClearSearch} sx={{ mt: 1 }}>
530
+ Clear search
531
+ </Button>
532
+ </Box>
533
+ )}
534
+
535
+ {/* Show message when no values exist */}
536
+ {pairs.length === 0 && (
537
+ <Box sx={{ textAlign: 'center', py: 4 }}>
538
+ <Typography variant="body2" color="textSecondary" gutterBottom>
539
+ No key-value pairs configured yet
540
+ </Typography>
541
+ <Button
542
+ startIcon={<AddIcon />}
543
+ variant="contained"
544
+ onClick={addPair}
545
+ >
546
+ Add Your First Pair
547
+ </Button>
548
+ </Box>
549
+ )}
550
+ </Box>
551
+ );
552
+ };
553
+
554
+ export const JsonView: React.FC<IDashAutoAdminCustomFieldComponent> = (props) => {
555
+ const { attribute } = props;
556
+ const record = useRecordContext();
557
+
558
+ const attributePath = attribute.attribute;
559
+ const isNestedSetting = attributePath.startsWith('settings.');
560
+ const settingsPath = isNestedSetting ? attributePath.split('.').slice(1).join('.') : attributePath;
561
+
562
+ const [searchTerm, setSearchTerm] = useState<string>('');
563
+ const [isLoading, setIsLoading] = useState<boolean>(true);
564
+ const [displayValue, setDisplayValue] = useState<Record<string, any>>({});
565
+
566
+ // Convert displayValue to pairs for filtering
567
+ const displayPairs = useMemo(() => {
568
+ return Object.entries(displayValue).map(([key, value]) => ({
569
+ key,
570
+ value: String(value),
571
+ id: key
572
+ }));
573
+ }, [displayValue]);
574
+
575
+ // Initial data loading effect
576
+ useEffect(() => {
577
+ const loadViewData = async () => {
578
+ setIsLoading(true);
579
+
580
+ try {
581
+ let viewValue: Record<string, any> = {};
582
+ let defaultValues: Record<string, any> = {};
583
+
584
+ try {
585
+ defaultValues = attribute?.default_value || {};
586
+ } catch (e) {
587
+ defaultValues = {};
588
+ }
589
+
590
+ if (isNestedSetting && record?.settings) {
591
+ const settings = record.settings || {};
592
+ if (settingsPath) {
593
+ const keys = settingsPath.split('.');
594
+ viewValue = { ...defaultValues, ...keys.reduce((o, key) => (o && o[key] !== undefined ? o[key] : {}), settings) };
595
+ } else {
596
+ viewValue = { ...defaultValues, ...settings };
597
+ }
598
+ } else if (record?.[attributePath]) {
599
+ viewValue = { ...defaultValues, ...record[attributePath] };
600
+ } else {
601
+ viewValue = defaultValues;
602
+ }
603
+
604
+ setDisplayValue(viewValue);
605
+
606
+ } catch (error) {
607
+ console.error('Error loading view data:', error);
608
+ } finally {
609
+ setIsLoading(false);
610
+ }
611
+ };
612
+
613
+ loadViewData();
614
+ }, [record, attributePath, attribute, isNestedSetting, settingsPath]);
615
+
616
+ // Filter display values based on search term
617
+ const filteredDisplayValue = useMemo(() => {
618
+ if (!searchTerm.trim()) return displayValue;
619
+
620
+ return Object.entries(displayValue)
621
+ .filter(([key, value]) =>
622
+ key.toLowerCase().includes(searchTerm.toLowerCase()) ||
623
+ String(value).toLowerCase().includes(searchTerm.toLowerCase())
624
+ )
625
+ .reduce((acc, [key, value]) => {
626
+ acc[key] = value;
627
+ return acc;
628
+ }, {} as Record<string, any>);
629
+ }, [displayValue, searchTerm]);
630
+
631
+ const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
632
+ setSearchTerm(event.target.value);
633
+ };
634
+
635
+ const handleClearSearch = () => {
636
+ setSearchTerm('');
637
+ };
638
+
639
+ // Show loading state while component is initializing
640
+ if (isLoading) {
641
+ return (
642
+ <Box sx={{ mt: 1, mb: 2, display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 200 }}>
643
+ <Typography variant="body2" color="textSecondary">
644
+ Loading values...
645
+ </Typography>
646
+ </Box>
647
+ );
648
+ }
649
+
650
+ return (
651
+ <Box sx={{ mt: 1, mb: 2 }}>
652
+ <Typography variant="subtitle1" gutterBottom>
653
+ {attribute.label || 'Key-Value Pairs'}
654
+ </Typography>
655
+
656
+ {Object.keys(displayValue).length > 0 && (
657
+ <>
658
+ {/* Search Control for View Mode */}
659
+ <Box sx={{ mb: 3 }}>
660
+ <TextField
661
+ fullWidth
662
+ size="small"
663
+ placeholder="Search by key or value..."
664
+ value={searchTerm}
665
+ onChange={handleSearchChange}
666
+ InputProps={{
667
+ startAdornment: (
668
+ <InputAdornment position="start">
669
+ <SearchIcon />
670
+ </InputAdornment>
671
+ ),
672
+ endAdornment: searchTerm && (
673
+ <InputAdornment position="end">
674
+ <IconButton
675
+ size="small"
676
+ onClick={handleClearSearch}
677
+ edge="end"
678
+ >
679
+ <ClearIcon />
680
+ </IconButton>
681
+ </InputAdornment>
682
+ ),
683
+ }}
684
+ />
685
+ </Box>
686
+
687
+ {/* Results count when filtering */}
688
+ {searchTerm && (
689
+ <Box sx={{ mb: 2 }}>
690
+ <Chip
691
+ label={`${Object.keys(filteredDisplayValue).length} of ${Object.keys(displayValue).length} pairs`}
692
+ size="small"
693
+ variant="outlined"
694
+ onDelete={handleClearSearch}
695
+ />
696
+ </Box>
697
+ )}
698
+
699
+ {Object.keys(filteredDisplayValue).length > 0 ? (
700
+ <Box sx={{ mb: 2 }}>
701
+ {Object.entries(filteredDisplayValue).map(([key, value]) => {
702
+ const valueType = detectValueType(String(value));
703
+
704
+ // Color coding for different value types
705
+ const getTypeColor = (type: ValueType) => {
706
+ switch (type) {
707
+ case 'string': return '#2196f3'; // Blue
708
+ case 'number': return '#4caf50'; // Green
709
+ case 'boolean': return '#ff9800'; // Orange
710
+ case 'json': return '#9c27b0'; // Purple
711
+ default: return '#757575'; // Grey
712
+ }
713
+ };
714
+
715
+ return (
716
+ <Card
717
+ key={key}
718
+ variant="outlined"
719
+ sx={{
720
+ mb: 1,
721
+ border: `2px solid ${getTypeColor(valueType)}`,
722
+ }}
723
+ >
724
+ <CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}>
725
+ <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
726
+ {/* Key Section */}
727
+ <Box sx={{ flex: 1, minWidth: 0 }}>
728
+ <Typography variant="caption" color="textSecondary" sx={{ display: 'block', mb: 0.5 }}>
729
+ Key
730
+ </Typography>
731
+ <Box
732
+ sx={{
733
+ p: 1,
734
+ borderRadius: 1,
735
+ backgroundColor: 'rgba(0,0,0,0.04)',
736
+ minHeight: 24,
737
+ display: 'flex',
738
+ alignItems: 'center',
739
+ }}
740
+ >
741
+ <Typography
742
+ variant="body2"
743
+ sx={{
744
+ fontFamily: 'monospace',
745
+ wordBreak: 'break-all',
746
+ }}
747
+ >
748
+ {key}
749
+ </Typography>
750
+ </Box>
751
+ </Box>
752
+
753
+ {/* Value Section */}
754
+ <Box sx={{ flex: 1, minWidth: 0 }}>
755
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
756
+ <Typography variant="caption" color="textSecondary">
757
+ Value
758
+ </Typography>
759
+ <Chip
760
+ label={valueType.toUpperCase()}
761
+ size="small"
762
+ sx={{
763
+ height: 16,
764
+ fontSize: '0.6rem',
765
+ backgroundColor: getTypeColor(valueType),
766
+ color: '#fff',
767
+ '& .MuiChip-label': {
768
+ padding: '0 4px',
769
+ },
770
+ }}
771
+ />
772
+ </Box>
773
+ <Box
774
+ sx={{
775
+ p: 1,
776
+ borderRadius: 1,
777
+ backgroundColor: 'rgba(0,0,0,0.04)',
778
+ minHeight: 24,
779
+ display: 'flex',
780
+ alignItems: 'center',
781
+ }}
782
+ >
783
+ <Typography
784
+ variant="body2"
785
+ sx={{
786
+ fontFamily: 'monospace',
787
+ wordBreak: 'break-all',
788
+ }}
789
+ title={String(value)} // Show full value on hover
790
+ >
791
+ {formatValueForDisplay(String(value))}
792
+ </Typography>
793
+ </Box>
794
+ </Box>
795
+ </Box>
796
+ </CardContent>
797
+ </Card>
798
+ );
799
+ })}
800
+ </Box>
801
+ ) : (
802
+ <Box sx={{ textAlign: 'center', py: 4 }}>
803
+ <Typography variant="body2" color="textSecondary">
804
+ No pairs found matching your search
805
+ </Typography>
806
+ <Button size="small" onClick={handleClearSearch} sx={{ mt: 1 }}>
807
+ Clear search
808
+ </Button>
809
+ </Box>
810
+ )}
811
+ </>
812
+ )}
813
+
814
+ {Object.keys(displayValue).length === 0 && (
815
+ <Box sx={{ textAlign: 'center', py: 4 }}>
816
+ <Typography variant="body2" color="textSecondary">
817
+ No key-value pairs configured
818
+ </Typography>
819
+ </Box>
820
+ )}
821
+ </Box>
822
+ );
823
+ };
824
+
825
+ const JsonCssVarValues = ({ method, attribute, resourceConfig }: IDashAutoAdminCustomFieldComponent) => {
826
+ const [componentLoading, setComponentLoading] = useState(true);
827
+
828
+ // Set a small timeout to allow component to initialize
829
+ useEffect(() => {
830
+ const timer = setTimeout(() => {
831
+ setComponentLoading(false);
832
+ }, 50);
833
+
834
+ return () => {
835
+ clearTimeout(timer);
836
+ };
837
+ }, [
838
+ ]);
839
+
840
+ // Show initial loading state
841
+ if (componentLoading) {
842
+ return (
843
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 100 }}>
844
+ <Typography variant="body2" color="textSecondary">
845
+ Initializing key-value editor...
846
+ </Typography>
847
+ </Box>
848
+ );
849
+ }
850
+
851
+ switch (method) {
852
+ case 'edit':
853
+ return <JsonEdit attribute={attribute} method={method} resourceConfig={resourceConfig} />;
854
+ case 'create':
855
+ return <JsonEdit attribute={attribute} method={method} resourceConfig={resourceConfig} />;
856
+ case 'view':
857
+ return <JsonView attribute={attribute} method={method} resourceConfig={resourceConfig} />;
858
+ default:
859
+ return null;
860
+ }
861
+ };
862
+
863
+ export default JsonCssVarValues;