@visns-studio/visns-components 5.6.2 → 5.6.4
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/package.json
CHANGED
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
85
85
|
},
|
|
86
86
|
"name": "@visns-studio/visns-components",
|
|
87
|
-
"version": "5.6.
|
|
87
|
+
"version": "5.6.4",
|
|
88
88
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
89
89
|
"main": "src/index.js",
|
|
90
90
|
"files": [
|
|
@@ -35,28 +35,64 @@ const CustomDownload = (
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
method: method,
|
|
40
|
-
data: formData,
|
|
41
|
-
headers: headers,
|
|
42
|
-
url: url,
|
|
43
|
-
responseType: 'blob',
|
|
44
|
-
};
|
|
45
|
-
|
|
38
|
+
// First make a request to check if the request is valid
|
|
46
39
|
return trackPromise(
|
|
47
40
|
new Promise((resolve, reject) => {
|
|
48
|
-
|
|
41
|
+
// First, make a request without responseType: 'blob' to check for errors
|
|
42
|
+
axios({
|
|
43
|
+
method: method,
|
|
44
|
+
data: formData,
|
|
45
|
+
headers: headers,
|
|
46
|
+
url: url,
|
|
47
|
+
})
|
|
49
48
|
.then((response) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
// If the response is JSON and contains an error, handle it
|
|
50
|
+
if (response.data && response.data.success === false) {
|
|
51
|
+
let errorMessage =
|
|
52
|
+
response.data.message || 'Unknown error';
|
|
53
|
+
if (response.data.error) {
|
|
54
|
+
errorMessage += ': ' + response.data.error;
|
|
55
|
+
}
|
|
54
56
|
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
+
if (errorCallback) {
|
|
58
|
+
errorCallback(errorMessage);
|
|
59
|
+
} else {
|
|
60
|
+
toast.error(<div>{parse(errorMessage)}</div>);
|
|
57
61
|
}
|
|
62
|
+
|
|
63
|
+
// Resolve with the error response
|
|
64
|
+
resolve(response);
|
|
65
|
+
} else {
|
|
66
|
+
// If no error, make the actual blob request
|
|
67
|
+
axios({
|
|
68
|
+
method: method,
|
|
69
|
+
data: formData,
|
|
70
|
+
headers: headers,
|
|
71
|
+
url: url,
|
|
72
|
+
responseType: 'blob',
|
|
73
|
+
})
|
|
74
|
+
.then((blobResponse) => {
|
|
75
|
+
if (successCallback) {
|
|
76
|
+
successCallback(blobResponse.data);
|
|
77
|
+
}
|
|
78
|
+
resolve(blobResponse);
|
|
79
|
+
})
|
|
80
|
+
.catch((blobError) => {
|
|
81
|
+
let errorMessage =
|
|
82
|
+
blobError.message ||
|
|
83
|
+
'Error downloading file';
|
|
84
|
+
|
|
85
|
+
if (errorCallback) {
|
|
86
|
+
errorCallback(errorMessage);
|
|
87
|
+
} else {
|
|
88
|
+
toast.error(
|
|
89
|
+
<div>{parse(errorMessage)}</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
reject(blobError);
|
|
94
|
+
});
|
|
58
95
|
}
|
|
59
|
-
resolve(response);
|
|
60
96
|
})
|
|
61
97
|
.catch((error) => {
|
|
62
98
|
let errorMessage = '';
|
|
@@ -177,6 +177,96 @@ const isJsonValue = (value) => {
|
|
|
177
177
|
return typeof value === 'object' && value !== null;
|
|
178
178
|
};
|
|
179
179
|
|
|
180
|
+
// Helper function to validate filter criteria
|
|
181
|
+
const validateFilterCriteria = (filterCriteria) => {
|
|
182
|
+
// If it's already a flat array (legacy format)
|
|
183
|
+
if (Array.isArray(filterCriteria)) {
|
|
184
|
+
// Check each filter in the array
|
|
185
|
+
for (let i = 0; i < filterCriteria.length; i++) {
|
|
186
|
+
const filter = filterCriteria[i];
|
|
187
|
+
|
|
188
|
+
// Skip validation for IS NULL and IS NOT NULL operators which don't need a value
|
|
189
|
+
if (
|
|
190
|
+
filter.operator === 'IS NULL' ||
|
|
191
|
+
filter.operator === 'IS NOT NULL'
|
|
192
|
+
) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check if any required field is missing
|
|
197
|
+
if (
|
|
198
|
+
!filter.table ||
|
|
199
|
+
!filter.column ||
|
|
200
|
+
!filter.operator ||
|
|
201
|
+
filter.value === undefined ||
|
|
202
|
+
filter.value === null ||
|
|
203
|
+
filter.value === ''
|
|
204
|
+
) {
|
|
205
|
+
return {
|
|
206
|
+
valid: false,
|
|
207
|
+
message: `Filter #${
|
|
208
|
+
i + 1
|
|
209
|
+
} is missing required fields (table, column, operator, or value)`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { valid: true };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// If it's the new structure with groups and operators
|
|
217
|
+
if (filterCriteria.groups && Array.isArray(filterCriteria.groups)) {
|
|
218
|
+
// Process each group
|
|
219
|
+
for (
|
|
220
|
+
let groupIndex = 0;
|
|
221
|
+
groupIndex < filterCriteria.groups.length;
|
|
222
|
+
groupIndex++
|
|
223
|
+
) {
|
|
224
|
+
const group = filterCriteria.groups[groupIndex];
|
|
225
|
+
|
|
226
|
+
// Only validate non-empty groups
|
|
227
|
+
if (group.filters && group.filters.length > 0) {
|
|
228
|
+
// Check all filters in this group
|
|
229
|
+
for (
|
|
230
|
+
let filterIndex = 0;
|
|
231
|
+
filterIndex < group.filters.length;
|
|
232
|
+
filterIndex++
|
|
233
|
+
) {
|
|
234
|
+
const filter = group.filters[filterIndex];
|
|
235
|
+
|
|
236
|
+
// Skip validation for IS NULL and IS NOT NULL operators which don't need a value
|
|
237
|
+
if (
|
|
238
|
+
filter.operator === 'IS NULL' ||
|
|
239
|
+
filter.operator === 'IS NOT NULL'
|
|
240
|
+
) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check if any required field is missing
|
|
245
|
+
if (
|
|
246
|
+
!filter.table ||
|
|
247
|
+
!filter.column ||
|
|
248
|
+
!filter.operator ||
|
|
249
|
+
filter.value === undefined ||
|
|
250
|
+
filter.value === null ||
|
|
251
|
+
filter.value === ''
|
|
252
|
+
) {
|
|
253
|
+
return {
|
|
254
|
+
valid: false,
|
|
255
|
+
message: `Filter #${filterIndex + 1} in group #${
|
|
256
|
+
groupIndex + 1
|
|
257
|
+
} is missing required fields (table, column, operator, or value)`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { valid: true };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// If it's neither a flat array nor the new structure, consider it valid (empty)
|
|
267
|
+
return { valid: true };
|
|
268
|
+
};
|
|
269
|
+
|
|
180
270
|
// Helper function to convert the enhanced filter structure to a flat array for the backend
|
|
181
271
|
const flattenFilterCriteria = (filterCriteria) => {
|
|
182
272
|
// If it's already a flat array (legacy format), return it as is
|
|
@@ -474,11 +564,17 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
474
564
|
const [gridColumns, setGridColumns] = useState([]);
|
|
475
565
|
const [isLoading, setIsLoading] = useState(false);
|
|
476
566
|
|
|
567
|
+
// State for JSON field keys
|
|
568
|
+
const [jsonFieldKeys, setJsonFieldKeys] = useState({});
|
|
569
|
+
const [loadingJsonKeys, setLoadingJsonKeys] = useState({});
|
|
570
|
+
const [showJsonKeySelector, setShowJsonKeySelector] = useState({});
|
|
571
|
+
|
|
477
572
|
// Extract additional settings with defaults
|
|
478
573
|
const {
|
|
479
574
|
reportsUrl = '/ajax/reportBuilder/reports',
|
|
480
575
|
executeUrl = '/ajax/reportBuilder/execute',
|
|
481
576
|
suggestedJoinsUrl = '/ajax/reportBuilder/getSuggestedJoins',
|
|
577
|
+
jsonFieldKeysUrl = '/ajax/reportBuilder/getJsonFieldKeys',
|
|
482
578
|
} = setting;
|
|
483
579
|
|
|
484
580
|
// Fetch all database tables and saved reports on component mount
|
|
@@ -744,9 +840,40 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
744
840
|
setSelectedColumns(updatedColumns);
|
|
745
841
|
};
|
|
746
842
|
|
|
843
|
+
// Start editing a column name
|
|
844
|
+
const handleStartEditColumn = (index) => {
|
|
845
|
+
const column = selectedColumns[index];
|
|
846
|
+
setEditingColumnIndex(index);
|
|
847
|
+
setEditingColumnName(column.alias || column.displayName);
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
// Save the edited column name
|
|
851
|
+
const handleSaveColumnName = () => {
|
|
852
|
+
if (editingColumnIndex !== null) {
|
|
853
|
+
const updatedColumns = [...selectedColumns];
|
|
854
|
+
updatedColumns[editingColumnIndex] = {
|
|
855
|
+
...updatedColumns[editingColumnIndex],
|
|
856
|
+
alias: editingColumnName,
|
|
857
|
+
};
|
|
858
|
+
setSelectedColumns(updatedColumns);
|
|
859
|
+
setEditingColumnIndex(null);
|
|
860
|
+
setEditingColumnName('');
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
// Cancel column name editing
|
|
865
|
+
const handleCancelEditColumn = () => {
|
|
866
|
+
setEditingColumnIndex(null);
|
|
867
|
+
setEditingColumnName('');
|
|
868
|
+
};
|
|
869
|
+
|
|
747
870
|
// State for drag and drop
|
|
748
871
|
const [draggedItemIndex, setDraggedItemIndex] = useState(null);
|
|
749
872
|
|
|
873
|
+
// State for column renaming
|
|
874
|
+
const [editingColumnIndex, setEditingColumnIndex] = useState(null);
|
|
875
|
+
const [editingColumnName, setEditingColumnName] = useState('');
|
|
876
|
+
|
|
750
877
|
// Handle drag start for column reordering
|
|
751
878
|
const handleDragStart = (e, index) => {
|
|
752
879
|
setDraggedItemIndex(index);
|
|
@@ -1051,6 +1178,118 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1051
1178
|
setShowFilteringSection(true);
|
|
1052
1179
|
};
|
|
1053
1180
|
|
|
1181
|
+
// Fetch JSON field keys for a specific table and column
|
|
1182
|
+
const fetchJsonFieldKeys = async (tableName, columnName) => {
|
|
1183
|
+
const cacheKey = `${tableName}.${columnName}`;
|
|
1184
|
+
|
|
1185
|
+
// Check if we're already loading this data
|
|
1186
|
+
if (loadingJsonKeys[cacheKey]) {
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Check if we already have this data cached
|
|
1191
|
+
if (jsonFieldKeys[cacheKey]) {
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Set loading state
|
|
1196
|
+
setLoadingJsonKeys((prev) => ({
|
|
1197
|
+
...prev,
|
|
1198
|
+
[cacheKey]: true,
|
|
1199
|
+
}));
|
|
1200
|
+
|
|
1201
|
+
try {
|
|
1202
|
+
const res = await CustomFetch(jsonFieldKeysUrl, 'POST', {
|
|
1203
|
+
table: tableName,
|
|
1204
|
+
column: columnName,
|
|
1205
|
+
limit: 100, // Analyze up to 100 records
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
if (res.data?.success && res.data?.data?.keys) {
|
|
1209
|
+
// Store the keys in state
|
|
1210
|
+
setJsonFieldKeys((prev) => ({
|
|
1211
|
+
...prev,
|
|
1212
|
+
[cacheKey]: res.data.data.keys,
|
|
1213
|
+
}));
|
|
1214
|
+
} else {
|
|
1215
|
+
console.warn(
|
|
1216
|
+
`No JSON keys found for ${tableName}.${columnName} or invalid response format`
|
|
1217
|
+
);
|
|
1218
|
+
// Set empty array for this field
|
|
1219
|
+
setJsonFieldKeys((prev) => ({
|
|
1220
|
+
...prev,
|
|
1221
|
+
[cacheKey]: [],
|
|
1222
|
+
}));
|
|
1223
|
+
}
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
console.error(
|
|
1226
|
+
`Error fetching JSON field keys for ${tableName}.${columnName}:`,
|
|
1227
|
+
error
|
|
1228
|
+
);
|
|
1229
|
+
toast.error(
|
|
1230
|
+
`Failed to load JSON field keys for ${formatName(
|
|
1231
|
+
tableName
|
|
1232
|
+
)}.${formatName(columnName)}`
|
|
1233
|
+
);
|
|
1234
|
+
// Set empty array for this field on error
|
|
1235
|
+
setJsonFieldKeys((prev) => ({
|
|
1236
|
+
...prev,
|
|
1237
|
+
[cacheKey]: [],
|
|
1238
|
+
}));
|
|
1239
|
+
} finally {
|
|
1240
|
+
// Update loading state
|
|
1241
|
+
setLoadingJsonKeys((prev) => ({
|
|
1242
|
+
...prev,
|
|
1243
|
+
[cacheKey]: false,
|
|
1244
|
+
}));
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
// Check if a column is a JSON type
|
|
1249
|
+
const isJsonColumn = (tableName, columnName) => {
|
|
1250
|
+
// First check the main table
|
|
1251
|
+
if (tableName === selectedTable) {
|
|
1252
|
+
const column = tableColumns.find((col) => col.name === columnName);
|
|
1253
|
+
return (
|
|
1254
|
+
column && column.type && column.type.toLowerCase() === 'json'
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Then check joined tables
|
|
1259
|
+
for (const join of joins) {
|
|
1260
|
+
if (join.targetTable === tableName && join.availableColumns) {
|
|
1261
|
+
const column = join.availableColumns.find(
|
|
1262
|
+
(col) => col.name === columnName
|
|
1263
|
+
);
|
|
1264
|
+
return (
|
|
1265
|
+
column &&
|
|
1266
|
+
column.type &&
|
|
1267
|
+
column.type.toLowerCase() === 'json'
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
return false;
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
// Toggle JSON key selector visibility
|
|
1276
|
+
const toggleJsonKeySelector = (groupIndex, filterIndex) => {
|
|
1277
|
+
const key = `${groupIndex}-${filterIndex}`;
|
|
1278
|
+
setShowJsonKeySelector((prev) => ({
|
|
1279
|
+
...prev,
|
|
1280
|
+
[key]: !prev[key],
|
|
1281
|
+
}));
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
// Handle JSON key selection
|
|
1285
|
+
const handleJsonKeySelect = (groupIndex, filterIndex, key) => {
|
|
1286
|
+
// Update the filter value with the selected JSON key
|
|
1287
|
+
handleFilterCriterionChange(groupIndex, filterIndex, 'jsonKey', key);
|
|
1288
|
+
|
|
1289
|
+
// Hide the selector
|
|
1290
|
+
toggleJsonKeySelector(groupIndex, filterIndex);
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1054
1293
|
// Update a filter criterion
|
|
1055
1294
|
const handleFilterCriterionChange = (
|
|
1056
1295
|
groupIndex,
|
|
@@ -1067,6 +1306,34 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1067
1306
|
updatedFilterCriteria.groups[groupIndex].filters[filterIndex][field] =
|
|
1068
1307
|
value;
|
|
1069
1308
|
|
|
1309
|
+
// If we're changing the table or column, check if it's a JSON field
|
|
1310
|
+
if (field === 'table' || field === 'column') {
|
|
1311
|
+
const filter =
|
|
1312
|
+
updatedFilterCriteria.groups[groupIndex].filters[filterIndex];
|
|
1313
|
+
const tableName = field === 'table' ? value : filter.table;
|
|
1314
|
+
const columnName = field === 'column' ? value : filter.column;
|
|
1315
|
+
|
|
1316
|
+
// If both table and column are set, check if it's a JSON field
|
|
1317
|
+
if (tableName && columnName) {
|
|
1318
|
+
const isJson = isJsonColumn(tableName, columnName);
|
|
1319
|
+
|
|
1320
|
+
// If it's a JSON field, fetch the keys
|
|
1321
|
+
if (isJson) {
|
|
1322
|
+
fetchJsonFieldKeys(tableName, columnName);
|
|
1323
|
+
|
|
1324
|
+
// Add a jsonKey field if it doesn't exist
|
|
1325
|
+
if (!filter.jsonKey) {
|
|
1326
|
+
filter.jsonKey = '';
|
|
1327
|
+
}
|
|
1328
|
+
} else {
|
|
1329
|
+
// If it's not a JSON field, remove the jsonKey field if it exists
|
|
1330
|
+
if (filter.jsonKey) {
|
|
1331
|
+
delete filter.jsonKey;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1070
1337
|
setFilterCriteria(updatedFilterCriteria);
|
|
1071
1338
|
};
|
|
1072
1339
|
|
|
@@ -1190,7 +1457,7 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1190
1457
|
}
|
|
1191
1458
|
};
|
|
1192
1459
|
|
|
1193
|
-
// Export report as CSV or
|
|
1460
|
+
// Export report as CSV, Excel or PDF
|
|
1194
1461
|
const handleExportReport = async (format = 'csv') => {
|
|
1195
1462
|
if (selectedColumns.length === 0) {
|
|
1196
1463
|
toast.error('Please select at least one column for your report');
|
|
@@ -1202,6 +1469,15 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1202
1469
|
return;
|
|
1203
1470
|
}
|
|
1204
1471
|
|
|
1472
|
+
// Validate filter criteria
|
|
1473
|
+
if (filterCriteria) {
|
|
1474
|
+
const validationResult = validateFilterCriteria(filterCriteria);
|
|
1475
|
+
if (!validationResult.valid) {
|
|
1476
|
+
toast.error(validationResult.message);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1205
1481
|
setIsLoading(true);
|
|
1206
1482
|
const toastId = toast.loading(
|
|
1207
1483
|
`Exporting report as ${format.toUpperCase()}...`
|
|
@@ -1222,8 +1498,8 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1222
1498
|
sourceColumn: join.sourceColumn,
|
|
1223
1499
|
targetColumn: join.targetColumn,
|
|
1224
1500
|
})),
|
|
1225
|
-
filters:
|
|
1226
|
-
sorting:
|
|
1501
|
+
filters: flattenFilterCriteria(filterCriteria),
|
|
1502
|
+
sorting: sortCriteria,
|
|
1227
1503
|
};
|
|
1228
1504
|
|
|
1229
1505
|
// Prepare export request data
|
|
@@ -1249,16 +1525,66 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1249
1525
|
const date = moment().format('YYYYMMDD');
|
|
1250
1526
|
const fileName = `${date}_${reportName}.${format}`;
|
|
1251
1527
|
|
|
1252
|
-
//
|
|
1253
|
-
|
|
1528
|
+
// For PDF (HTML) format, open in a new window for printing
|
|
1529
|
+
if (format === 'pdf') {
|
|
1530
|
+
// Convert blob to URL
|
|
1531
|
+
const blob = new Blob([res.data], { type: 'text/html' });
|
|
1532
|
+
const url = URL.createObjectURL(blob);
|
|
1533
|
+
|
|
1534
|
+
// Open in a new window
|
|
1535
|
+
const printWindow = window.open(url, '_blank');
|
|
1536
|
+
|
|
1537
|
+
// Add print instructions
|
|
1538
|
+
if (printWindow) {
|
|
1539
|
+
printWindow.onload = function () {
|
|
1540
|
+
// Add print button at the top
|
|
1541
|
+
const printButton =
|
|
1542
|
+
printWindow.document.createElement('button');
|
|
1543
|
+
printButton.innerHTML = 'Print PDF';
|
|
1544
|
+
printButton.style.position = 'fixed';
|
|
1545
|
+
printButton.style.top = '10px';
|
|
1546
|
+
printButton.style.right = '10px';
|
|
1547
|
+
printButton.style.zIndex = '9999';
|
|
1548
|
+
printButton.style.padding = '8px 16px';
|
|
1549
|
+
printButton.style.backgroundColor = '#4CAF50';
|
|
1550
|
+
printButton.style.color = 'white';
|
|
1551
|
+
printButton.style.border = 'none';
|
|
1552
|
+
printButton.style.borderRadius = '4px';
|
|
1553
|
+
printButton.style.cursor = 'pointer';
|
|
1554
|
+
|
|
1555
|
+
printButton.onclick = function () {
|
|
1556
|
+
// Hide the button before printing
|
|
1557
|
+
this.style.display = 'none';
|
|
1558
|
+
printWindow.print();
|
|
1559
|
+
this.style.display = 'block';
|
|
1560
|
+
};
|
|
1254
1561
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1562
|
+
printWindow.document.body.insertBefore(
|
|
1563
|
+
printButton,
|
|
1564
|
+
printWindow.document.body.firstChild
|
|
1565
|
+
);
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
toast.update(toastId, {
|
|
1570
|
+
render: `Report opened in a new window. Use browser's print function to save as PDF.`,
|
|
1571
|
+
type: 'success',
|
|
1572
|
+
isLoading: false,
|
|
1573
|
+
autoClose: 5000,
|
|
1574
|
+
closeButton: true,
|
|
1575
|
+
});
|
|
1576
|
+
} else {
|
|
1577
|
+
// Use FileSaver.js to save the file for CSV and Excel
|
|
1578
|
+
saveAs(res.data, fileName);
|
|
1579
|
+
|
|
1580
|
+
toast.update(toastId, {
|
|
1581
|
+
render: `Report exported successfully as ${format.toUpperCase()}`,
|
|
1582
|
+
type: 'success',
|
|
1583
|
+
isLoading: false,
|
|
1584
|
+
autoClose: 5000,
|
|
1585
|
+
closeButton: true,
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1262
1588
|
} else {
|
|
1263
1589
|
throw new Error('Invalid response format');
|
|
1264
1590
|
}
|
|
@@ -1520,6 +1846,15 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1520
1846
|
return;
|
|
1521
1847
|
}
|
|
1522
1848
|
|
|
1849
|
+
// Validate filter criteria
|
|
1850
|
+
if (filterCriteria) {
|
|
1851
|
+
const validationResult = validateFilterCriteria(filterCriteria);
|
|
1852
|
+
if (!validationResult.valid) {
|
|
1853
|
+
toast.error(validationResult.message);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1523
1858
|
setIsLoading(true);
|
|
1524
1859
|
toast.info('Executing query...');
|
|
1525
1860
|
|
|
@@ -1699,6 +2034,15 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1699
2034
|
return;
|
|
1700
2035
|
}
|
|
1701
2036
|
|
|
2037
|
+
// Validate filter criteria
|
|
2038
|
+
if (filterCriteria) {
|
|
2039
|
+
const validationResult = validateFilterCriteria(filterCriteria);
|
|
2040
|
+
if (!validationResult.valid) {
|
|
2041
|
+
toast.error(validationResult.message);
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
1702
2046
|
setIsLoading(true);
|
|
1703
2047
|
|
|
1704
2048
|
try {
|
|
@@ -2102,7 +2446,12 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
2102
2446
|
)}
|
|
2103
2447
|
</div>
|
|
2104
2448
|
|
|
2105
|
-
<
|
|
2449
|
+
<div className={styles.columnSectionHeader}>
|
|
2450
|
+
<h3>Selected Columns</h3>
|
|
2451
|
+
<span className={styles.instructionText}>
|
|
2452
|
+
Click the pencil icon (✎) to rename columns
|
|
2453
|
+
</span>
|
|
2454
|
+
</div>
|
|
2106
2455
|
{selectedColumns.length > 0 ? (
|
|
2107
2456
|
<div className={styles.selectedColumns}>
|
|
2108
2457
|
{selectedColumns.map((column, index) => (
|
|
@@ -2130,10 +2479,86 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
2130
2479
|
>
|
|
2131
2480
|
⋮⋮
|
|
2132
2481
|
</div>
|
|
2133
|
-
<span>
|
|
2482
|
+
<span>
|
|
2483
|
+
{editingColumnIndex ===
|
|
2484
|
+
index ? (
|
|
2485
|
+
<div
|
|
2486
|
+
className={
|
|
2487
|
+
styles.inlineEditContainer
|
|
2488
|
+
}
|
|
2489
|
+
>
|
|
2490
|
+
<input
|
|
2491
|
+
type="text"
|
|
2492
|
+
className={
|
|
2493
|
+
styles.columnNameInput
|
|
2494
|
+
}
|
|
2495
|
+
value={
|
|
2496
|
+
editingColumnName
|
|
2497
|
+
}
|
|
2498
|
+
onChange={(e) =>
|
|
2499
|
+
setEditingColumnName(
|
|
2500
|
+
e.target
|
|
2501
|
+
.value
|
|
2502
|
+
)
|
|
2503
|
+
}
|
|
2504
|
+
autoFocus
|
|
2505
|
+
onKeyDown={(e) => {
|
|
2506
|
+
if (
|
|
2507
|
+
e.key ===
|
|
2508
|
+
'Enter'
|
|
2509
|
+
) {
|
|
2510
|
+
handleSaveColumnName();
|
|
2511
|
+
} else if (
|
|
2512
|
+
e.key ===
|
|
2513
|
+
'Escape'
|
|
2514
|
+
) {
|
|
2515
|
+
handleCancelEditColumn();
|
|
2516
|
+
}
|
|
2517
|
+
}}
|
|
2518
|
+
/>
|
|
2519
|
+
<button
|
|
2520
|
+
className={
|
|
2521
|
+
styles.saveBtn
|
|
2522
|
+
}
|
|
2523
|
+
onClick={
|
|
2524
|
+
handleSaveColumnName
|
|
2525
|
+
}
|
|
2526
|
+
title="Save"
|
|
2527
|
+
>
|
|
2528
|
+
✓
|
|
2529
|
+
</button>
|
|
2530
|
+
<button
|
|
2531
|
+
className={
|
|
2532
|
+
styles.cancelBtn
|
|
2533
|
+
}
|
|
2534
|
+
onClick={
|
|
2535
|
+
handleCancelEditColumn
|
|
2536
|
+
}
|
|
2537
|
+
title="Cancel"
|
|
2538
|
+
>
|
|
2539
|
+
✕
|
|
2540
|
+
</button>
|
|
2541
|
+
</div>
|
|
2542
|
+
) : (
|
|
2543
|
+
column.alias ||
|
|
2544
|
+
column.displayName
|
|
2545
|
+
)}
|
|
2546
|
+
</span>
|
|
2134
2547
|
<div
|
|
2135
2548
|
className={styles.columnActions}
|
|
2136
2549
|
>
|
|
2550
|
+
<button
|
|
2551
|
+
className={styles.editBtn}
|
|
2552
|
+
onClick={() =>
|
|
2553
|
+
handleStartEditColumn(
|
|
2554
|
+
index
|
|
2555
|
+
)
|
|
2556
|
+
}
|
|
2557
|
+
aria-label="Edit column name"
|
|
2558
|
+
title="Edit name"
|
|
2559
|
+
>
|
|
2560
|
+
✎
|
|
2561
|
+
</button>
|
|
2137
2562
|
<button
|
|
2138
2563
|
className={styles.removeBtn}
|
|
2139
2564
|
onClick={() =>
|
|
@@ -3615,31 +4040,218 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
3615
4040
|
styles.joinField
|
|
3616
4041
|
}
|
|
3617
4042
|
>
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
4043
|
+
{/* Check if this is a JSON field */}
|
|
4044
|
+
{criterion.table &&
|
|
4045
|
+
criterion.column &&
|
|
4046
|
+
isJsonColumn(
|
|
4047
|
+
criterion.table,
|
|
4048
|
+
criterion.column
|
|
4049
|
+
) ? (
|
|
4050
|
+
<>
|
|
4051
|
+
<label>
|
|
4052
|
+
JSON
|
|
4053
|
+
Path:
|
|
4054
|
+
</label>
|
|
4055
|
+
<div
|
|
4056
|
+
className={
|
|
4057
|
+
styles.jsonKeyInputContainer
|
|
4058
|
+
}
|
|
4059
|
+
>
|
|
4060
|
+
<input
|
|
4061
|
+
type="text"
|
|
4062
|
+
className={
|
|
4063
|
+
styles.formControl
|
|
4064
|
+
}
|
|
4065
|
+
value={
|
|
4066
|
+
criterion.jsonKey ||
|
|
4067
|
+
''
|
|
4068
|
+
}
|
|
4069
|
+
onChange={(
|
|
4070
|
+
e
|
|
4071
|
+
) =>
|
|
4072
|
+
handleFilterCriterionChange(
|
|
4073
|
+
groupIndex,
|
|
4074
|
+
filterIndex,
|
|
4075
|
+
'jsonKey',
|
|
4076
|
+
e
|
|
4077
|
+
.target
|
|
4078
|
+
.value
|
|
4079
|
+
)
|
|
4080
|
+
}
|
|
4081
|
+
placeholder="Enter JSON path (e.g., data.name)"
|
|
4082
|
+
/>
|
|
4083
|
+
<button
|
|
4084
|
+
type="button"
|
|
4085
|
+
className={
|
|
4086
|
+
styles.jsonKeyButton
|
|
4087
|
+
}
|
|
4088
|
+
onClick={() =>
|
|
4089
|
+
toggleJsonKeySelector(
|
|
4090
|
+
groupIndex,
|
|
4091
|
+
filterIndex
|
|
4092
|
+
)
|
|
4093
|
+
}
|
|
4094
|
+
title="Select from available JSON keys"
|
|
4095
|
+
>
|
|
4096
|
+
▼
|
|
4097
|
+
</button>
|
|
4098
|
+
</div>
|
|
4099
|
+
|
|
4100
|
+
{/* JSON key selector dropdown */}
|
|
4101
|
+
{showJsonKeySelector[
|
|
4102
|
+
`${groupIndex}-${filterIndex}`
|
|
4103
|
+
] && (
|
|
4104
|
+
<div
|
|
4105
|
+
className={
|
|
4106
|
+
styles.jsonKeyDropdown
|
|
4107
|
+
}
|
|
4108
|
+
>
|
|
4109
|
+
{loadingJsonKeys[
|
|
4110
|
+
`${criterion.table}.${criterion.column}`
|
|
4111
|
+
] ? (
|
|
4112
|
+
<div
|
|
4113
|
+
className={
|
|
4114
|
+
styles.jsonKeyLoading
|
|
4115
|
+
}
|
|
4116
|
+
>
|
|
4117
|
+
Loading
|
|
4118
|
+
available
|
|
4119
|
+
JSON
|
|
4120
|
+
keys...
|
|
4121
|
+
</div>
|
|
4122
|
+
) : jsonFieldKeys[
|
|
4123
|
+
`${criterion.table}.${criterion.column}`
|
|
4124
|
+
]
|
|
4125
|
+
?.length >
|
|
4126
|
+
0 ? (
|
|
4127
|
+
<div
|
|
4128
|
+
className={
|
|
4129
|
+
styles.jsonKeyList
|
|
4130
|
+
}
|
|
4131
|
+
>
|
|
4132
|
+
{jsonFieldKeys[
|
|
4133
|
+
`${criterion.table}.${criterion.column}`
|
|
4134
|
+
].map(
|
|
4135
|
+
(
|
|
4136
|
+
keyInfo,
|
|
4137
|
+
idx
|
|
4138
|
+
) => (
|
|
4139
|
+
<div
|
|
4140
|
+
key={
|
|
4141
|
+
idx
|
|
4142
|
+
}
|
|
4143
|
+
className={
|
|
4144
|
+
styles.jsonKeyItem
|
|
4145
|
+
}
|
|
4146
|
+
onClick={() =>
|
|
4147
|
+
handleJsonKeySelect(
|
|
4148
|
+
groupIndex,
|
|
4149
|
+
filterIndex,
|
|
4150
|
+
keyInfo.key
|
|
4151
|
+
)
|
|
4152
|
+
}
|
|
4153
|
+
title={`Used in ${keyInfo.percentage}% of records. Example: ${keyInfo.example}`}
|
|
4154
|
+
>
|
|
4155
|
+
<span
|
|
4156
|
+
className={
|
|
4157
|
+
styles.jsonKeyName
|
|
4158
|
+
}
|
|
4159
|
+
>
|
|
4160
|
+
{
|
|
4161
|
+
keyInfo.key
|
|
4162
|
+
}
|
|
4163
|
+
</span>
|
|
4164
|
+
<span
|
|
4165
|
+
className={
|
|
4166
|
+
styles.jsonKeyFrequency
|
|
4167
|
+
}
|
|
4168
|
+
>
|
|
4169
|
+
{
|
|
4170
|
+
keyInfo.percentage
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
%
|
|
4174
|
+
</span>
|
|
4175
|
+
</div>
|
|
4176
|
+
)
|
|
4177
|
+
)}
|
|
4178
|
+
</div>
|
|
4179
|
+
) : (
|
|
4180
|
+
<div
|
|
4181
|
+
className={
|
|
4182
|
+
styles.jsonKeyEmpty
|
|
4183
|
+
}
|
|
4184
|
+
>
|
|
4185
|
+
No
|
|
4186
|
+
JSON
|
|
4187
|
+
keys
|
|
4188
|
+
found.
|
|
4189
|
+
Enter
|
|
4190
|
+
a
|
|
4191
|
+
path
|
|
4192
|
+
manually.
|
|
4193
|
+
</div>
|
|
4194
|
+
)}
|
|
4195
|
+
</div>
|
|
4196
|
+
)}
|
|
4197
|
+
|
|
4198
|
+
<label>
|
|
4199
|
+
Value:
|
|
4200
|
+
</label>
|
|
4201
|
+
<input
|
|
4202
|
+
type="text"
|
|
4203
|
+
className={
|
|
4204
|
+
styles.formControl
|
|
4205
|
+
}
|
|
4206
|
+
value={
|
|
4207
|
+
criterion.value ||
|
|
4208
|
+
''
|
|
4209
|
+
}
|
|
4210
|
+
onChange={(
|
|
4211
|
+
e
|
|
4212
|
+
) =>
|
|
4213
|
+
handleFilterCriterionChange(
|
|
4214
|
+
groupIndex,
|
|
4215
|
+
filterIndex,
|
|
4216
|
+
'value',
|
|
4217
|
+
e
|
|
4218
|
+
.target
|
|
4219
|
+
.value
|
|
4220
|
+
)
|
|
4221
|
+
}
|
|
4222
|
+
placeholder="Enter filter value"
|
|
4223
|
+
/>
|
|
4224
|
+
</>
|
|
4225
|
+
) : (
|
|
4226
|
+
<>
|
|
4227
|
+
<label>
|
|
4228
|
+
Value:
|
|
4229
|
+
</label>
|
|
4230
|
+
<input
|
|
4231
|
+
type="text"
|
|
4232
|
+
className={
|
|
4233
|
+
styles.formControl
|
|
4234
|
+
}
|
|
4235
|
+
value={
|
|
4236
|
+
criterion.value ||
|
|
4237
|
+
''
|
|
4238
|
+
}
|
|
4239
|
+
onChange={(
|
|
4240
|
+
e
|
|
4241
|
+
) =>
|
|
4242
|
+
handleFilterCriterionChange(
|
|
4243
|
+
groupIndex,
|
|
4244
|
+
filterIndex,
|
|
4245
|
+
'value',
|
|
4246
|
+
e
|
|
4247
|
+
.target
|
|
4248
|
+
.value
|
|
4249
|
+
)
|
|
4250
|
+
}
|
|
4251
|
+
placeholder="Enter filter value"
|
|
4252
|
+
/>
|
|
4253
|
+
</>
|
|
4254
|
+
)}
|
|
3643
4255
|
</div>
|
|
3644
4256
|
)}
|
|
3645
4257
|
</div>
|
|
@@ -3701,6 +4313,17 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
3701
4313
|
>
|
|
3702
4314
|
Export as Excel
|
|
3703
4315
|
</button>
|
|
4316
|
+
<button
|
|
4317
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
4318
|
+
onClick={() => handleExportReport('pdf')}
|
|
4319
|
+
disabled={
|
|
4320
|
+
isLoading ||
|
|
4321
|
+
selectedColumns.length === 0 ||
|
|
4322
|
+
!reportName
|
|
4323
|
+
}
|
|
4324
|
+
>
|
|
4325
|
+
Export as PDF
|
|
4326
|
+
</button>
|
|
3704
4327
|
<button
|
|
3705
4328
|
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
3706
4329
|
onClick={handleSaveReport}
|
|
@@ -911,6 +911,55 @@
|
|
|
911
911
|
text-overflow: ellipsis;
|
|
912
912
|
white-space: nowrap;
|
|
913
913
|
}
|
|
914
|
+
|
|
915
|
+
.inlineEditContainer {
|
|
916
|
+
display: flex;
|
|
917
|
+
flex: 1;
|
|
918
|
+
align-items: center;
|
|
919
|
+
gap: 4px;
|
|
920
|
+
width: 100%;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.columnNameInput {
|
|
924
|
+
flex: 1;
|
|
925
|
+
padding: 4px 8px;
|
|
926
|
+
border: 1px solid var(--primary-color, #2563eb);
|
|
927
|
+
border-radius: 4px;
|
|
928
|
+
font-size: 0.9em;
|
|
929
|
+
color: var(--paragraph-color, #4b5563);
|
|
930
|
+
min-width: 50px;
|
|
931
|
+
max-width: 200px;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.saveBtn,
|
|
935
|
+
.cancelBtn {
|
|
936
|
+
background: none;
|
|
937
|
+
border: none;
|
|
938
|
+
cursor: pointer;
|
|
939
|
+
padding: 2px 4px;
|
|
940
|
+
border-radius: 4px;
|
|
941
|
+
display: flex;
|
|
942
|
+
align-items: center;
|
|
943
|
+
justify-content: center;
|
|
944
|
+
transition: all 0.2s ease;
|
|
945
|
+
font-size: 0.9em;
|
|
946
|
+
min-width: 20px;
|
|
947
|
+
min-height: 20px;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.saveBtn {
|
|
951
|
+
color: var(--success-color, #10b981);
|
|
952
|
+
&:hover {
|
|
953
|
+
background: rgba(16, 185, 129, 0.1);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.cancelBtn {
|
|
958
|
+
color: var(--danger-color, #ef4444);
|
|
959
|
+
&:hover {
|
|
960
|
+
background: rgba(239, 68, 68, 0.1);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
914
963
|
}
|
|
915
964
|
|
|
916
965
|
.selectedColumnItem:hover {
|
|
@@ -949,6 +998,25 @@
|
|
|
949
998
|
flex-shrink: 0;
|
|
950
999
|
}
|
|
951
1000
|
|
|
1001
|
+
.editBtn {
|
|
1002
|
+
background: none;
|
|
1003
|
+
border: none;
|
|
1004
|
+
color: var(--primary-color, #2563eb);
|
|
1005
|
+
cursor: pointer;
|
|
1006
|
+
font-size: 1em;
|
|
1007
|
+
display: flex;
|
|
1008
|
+
align-items: center;
|
|
1009
|
+
justify-content: center;
|
|
1010
|
+
padding: 4px;
|
|
1011
|
+
border-radius: 4px;
|
|
1012
|
+
min-width: 24px;
|
|
1013
|
+
min-height: 24px;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.editBtn:hover {
|
|
1017
|
+
background: rgba(var(--primary-rgb, 37, 99, 235), 0.1);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
952
1020
|
.removeBtn {
|
|
953
1021
|
background: none;
|
|
954
1022
|
border: none;
|
|
@@ -1034,6 +1102,25 @@
|
|
|
1034
1102
|
margin: 0;
|
|
1035
1103
|
}
|
|
1036
1104
|
|
|
1105
|
+
.columnSectionHeader {
|
|
1106
|
+
display: flex;
|
|
1107
|
+
align-items: center;
|
|
1108
|
+
margin-bottom: 10px;
|
|
1109
|
+
flex-wrap: wrap;
|
|
1110
|
+
gap: 8px;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.columnSectionHeader h3 {
|
|
1114
|
+
margin: 0;
|
|
1115
|
+
margin-right: 8px;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
.instructionText {
|
|
1119
|
+
font-size: 0.8em;
|
|
1120
|
+
color: var(--muted-color, #6b7280);
|
|
1121
|
+
font-style: italic;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1037
1124
|
.btnSmall {
|
|
1038
1125
|
padding: 4px 8px;
|
|
1039
1126
|
font-size: 0.8em;
|
|
@@ -1157,6 +1244,94 @@
|
|
|
1157
1244
|
margin: 16px 0;
|
|
1158
1245
|
}
|
|
1159
1246
|
|
|
1247
|
+
.jsonKeyInputContainer {
|
|
1248
|
+
position: relative;
|
|
1249
|
+
display: flex;
|
|
1250
|
+
align-items: center;
|
|
1251
|
+
width: 100%;
|
|
1252
|
+
|
|
1253
|
+
.jsonKeyButton {
|
|
1254
|
+
position: absolute;
|
|
1255
|
+
right: 0;
|
|
1256
|
+
top: 0;
|
|
1257
|
+
height: 100%;
|
|
1258
|
+
background: none;
|
|
1259
|
+
border: none;
|
|
1260
|
+
border-left: 1px solid var(--border-color, #d1d5db);
|
|
1261
|
+
padding: 0 10px;
|
|
1262
|
+
color: var(--paragraph-color, #4b5563);
|
|
1263
|
+
cursor: pointer;
|
|
1264
|
+
font-size: 0.8em;
|
|
1265
|
+
transition: all 0.2s ease;
|
|
1266
|
+
|
|
1267
|
+
&:hover {
|
|
1268
|
+
background-color: #f3f4f6;
|
|
1269
|
+
color: var(--primary-color, #2563eb);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
input {
|
|
1274
|
+
padding-right: 30px;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
.jsonKeyDropdown {
|
|
1279
|
+
position: relative;
|
|
1280
|
+
width: 100%;
|
|
1281
|
+
max-height: 200px;
|
|
1282
|
+
overflow-y: auto;
|
|
1283
|
+
background-color: white;
|
|
1284
|
+
border: 1px solid var(--border-color, #d1d5db);
|
|
1285
|
+
border-radius: 6px;
|
|
1286
|
+
margin-top: 4px;
|
|
1287
|
+
margin-bottom: 12px;
|
|
1288
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
1289
|
+
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
1290
|
+
z-index: 100;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
.jsonKeyList {
|
|
1294
|
+
padding: 4px 0;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
.jsonKeyItem {
|
|
1298
|
+
display: flex;
|
|
1299
|
+
justify-content: space-between;
|
|
1300
|
+
align-items: center;
|
|
1301
|
+
padding: 8px 12px;
|
|
1302
|
+
cursor: pointer;
|
|
1303
|
+
transition: all 0.2s ease;
|
|
1304
|
+
font-size: 0.85em;
|
|
1305
|
+
|
|
1306
|
+
&:hover {
|
|
1307
|
+
background-color: #f3f4f6;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
.jsonKeyName {
|
|
1311
|
+
flex: 1;
|
|
1312
|
+
overflow: hidden;
|
|
1313
|
+
text-overflow: ellipsis;
|
|
1314
|
+
white-space: nowrap;
|
|
1315
|
+
color: var(--primary-color, #2563eb);
|
|
1316
|
+
font-weight: 500;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
.jsonKeyFrequency {
|
|
1320
|
+
color: var(--muted-color, #6b7280);
|
|
1321
|
+
font-size: 0.85em;
|
|
1322
|
+
margin-left: 8px;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
.jsonKeyLoading,
|
|
1327
|
+
.jsonKeyEmpty {
|
|
1328
|
+
padding: 12px;
|
|
1329
|
+
text-align: center;
|
|
1330
|
+
color: var(--muted-color, #6b7280);
|
|
1331
|
+
font-size: 0.85em;
|
|
1332
|
+
font-style: italic;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1160
1335
|
.jsonPreview {
|
|
1161
1336
|
max-width: 100%;
|
|
1162
1337
|
max-height: 150px;
|