@visns-studio/visns-components 6.1.7 → 6.2.0
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 +161 -1
- package/package.json +1 -1
- package/src/components/Fetch.jsx +49 -19
- package/src/components/TableFilter.jsx +59 -45
- package/src/components/generic/GenericReport.jsx +835 -68
- package/src/components/generic/SectionGroupedReport.jsx +199 -226
- package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +81 -0
- package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +337 -0
- package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +483 -0
- package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +202 -0
- package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +157 -0
- package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +352 -0
- package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +154 -0
- package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +153 -0
- package/src/components/generic/reportSemantics.js +971 -0
- package/src/components/generic/useSemanticModel.js +99 -0
- package/src/components/styles/ReportSemantic.module.scss +444 -0
- package/src/index.js +4 -0
|
@@ -38,6 +38,31 @@ import debugLog from '../utils/debugLog';
|
|
|
38
38
|
import { saveAs } from 'file-saver';
|
|
39
39
|
import * as XLSX from 'xlsx';
|
|
40
40
|
import Swal from 'sweetalert2';
|
|
41
|
+
import useSemanticModel from './useSemanticModel';
|
|
42
|
+
import {
|
|
43
|
+
DEFAULT_SEMANTIC_MODEL_URL,
|
|
44
|
+
appendToFilterGroup,
|
|
45
|
+
buildDefinition,
|
|
46
|
+
collectParameters,
|
|
47
|
+
createFilterCondition,
|
|
48
|
+
createFilterGroup,
|
|
49
|
+
createSelection,
|
|
50
|
+
defaultSelectionLabel,
|
|
51
|
+
emptyParameterValue,
|
|
52
|
+
getEntityLabel,
|
|
53
|
+
isSemanticDefinition,
|
|
54
|
+
parseDefinition,
|
|
55
|
+
parametersSatisfied,
|
|
56
|
+
relationPathsFromDefinition,
|
|
57
|
+
resolveFieldPath,
|
|
58
|
+
updateFilterNode,
|
|
59
|
+
} from './reportSemantics';
|
|
60
|
+
import SemanticEntityStep from './reportSemanticSteps/SemanticEntityStep';
|
|
61
|
+
import SemanticRelationsStep from './reportSemanticSteps/SemanticRelationsStep';
|
|
62
|
+
import SemanticFieldsStep from './reportSemanticSteps/SemanticFieldsStep';
|
|
63
|
+
import SemanticFiltersStep from './reportSemanticSteps/SemanticFiltersStep';
|
|
64
|
+
import SemanticGroupingStep from './reportSemanticSteps/SemanticGroupingStep';
|
|
65
|
+
import SemanticPreviewStep from './reportSemanticSteps/SemanticPreviewStep';
|
|
41
66
|
import styles from '../styles/GenericReport.module.scss';
|
|
42
67
|
import '../styles/SweetAlert.module.css';
|
|
43
68
|
import './groupedReport.css';
|
|
@@ -144,32 +169,6 @@ const getUniqueTableDisplayName = (tableName, allTables, definition) => {
|
|
|
144
169
|
return `${baseDisplayName} (${tableName})`;
|
|
145
170
|
};
|
|
146
171
|
|
|
147
|
-
// Get structured table display name data for better styling
|
|
148
|
-
const getStructuredTableDisplayName = (tableName, allTables, definition) => {
|
|
149
|
-
const baseDisplayName = getTableDisplayName(tableName, definition);
|
|
150
|
-
|
|
151
|
-
// Find all tables with the same display name
|
|
152
|
-
const conflictingTables = allTables.filter((table) => {
|
|
153
|
-
const otherTableName = typeof table === 'string' ? table : table.name;
|
|
154
|
-
const otherDisplayName = getTableDisplayName(
|
|
155
|
-
otherTableName,
|
|
156
|
-
definition
|
|
157
|
-
);
|
|
158
|
-
return (
|
|
159
|
-
otherDisplayName === baseDisplayName && otherTableName !== tableName
|
|
160
|
-
);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
main: baseDisplayName,
|
|
165
|
-
disambiguation: conflictingTables.length > 0 ? tableName : null,
|
|
166
|
-
full:
|
|
167
|
-
conflictingTables.length > 0
|
|
168
|
-
? `${baseDisplayName} (${tableName})`
|
|
169
|
-
: baseDisplayName,
|
|
170
|
-
};
|
|
171
|
-
};
|
|
172
|
-
|
|
173
172
|
// Get table description from definition
|
|
174
173
|
const getTableDescription = (tableName, definition) => {
|
|
175
174
|
if (definition && definition.tables) {
|
|
@@ -1013,6 +1012,110 @@ const wizardSteps = [
|
|
|
1013
1012
|
},
|
|
1014
1013
|
];
|
|
1015
1014
|
|
|
1015
|
+
/**
|
|
1016
|
+
* Wizard steps used when the semantic model is available. Same six ids and the
|
|
1017
|
+
* same order as `wizardSteps` — only the wording changes, because in this mode
|
|
1018
|
+
* the user never sees a table, a column or a join.
|
|
1019
|
+
*/
|
|
1020
|
+
const semanticWizardSteps = [
|
|
1021
|
+
{
|
|
1022
|
+
id: 'table',
|
|
1023
|
+
title: 'Choose Your Subject',
|
|
1024
|
+
icon: Data,
|
|
1025
|
+
description: 'What is this report about?',
|
|
1026
|
+
guidance: {
|
|
1027
|
+
summary: 'Pick the main thing you want to report on',
|
|
1028
|
+
howTo: [
|
|
1029
|
+
'Each card is a kind of record your system keeps',
|
|
1030
|
+
'Read the description to check it is the one you mean',
|
|
1031
|
+
'Click it to make it the subject of your report',
|
|
1032
|
+
'You can bring in connected information at the next step',
|
|
1033
|
+
],
|
|
1034
|
+
tip: 'Pick the thing you want one row per — for example one row per client.',
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
id: 'relationships',
|
|
1039
|
+
title: 'Add Related Data',
|
|
1040
|
+
icon: LinkChain,
|
|
1041
|
+
description: 'Bring in connected information (optional)',
|
|
1042
|
+
guidance: {
|
|
1043
|
+
summary: 'Add areas connected to your subject',
|
|
1044
|
+
howTo: [
|
|
1045
|
+
'Each card is an area of information already connected for you',
|
|
1046
|
+
'Click one to make its details available in the next step',
|
|
1047
|
+
'Areas marked "many per record" can repeat rows',
|
|
1048
|
+
'Skip this step if the main subject has everything you need',
|
|
1049
|
+
],
|
|
1050
|
+
tip: 'Summarising a "many per record" area (count, total) keeps one row per record.',
|
|
1051
|
+
},
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
id: 'columns',
|
|
1055
|
+
title: 'Choose Information',
|
|
1056
|
+
icon: File,
|
|
1057
|
+
description: 'Pick the details you want to see',
|
|
1058
|
+
guidance: {
|
|
1059
|
+
summary: 'Tick the details to include as columns',
|
|
1060
|
+
howTo: [
|
|
1061
|
+
'Details are grouped by the area they come from',
|
|
1062
|
+
'Within each area they are sorted into kinds of information',
|
|
1063
|
+
'Tick anything you want to see in the report',
|
|
1064
|
+
'Use "Show as" to total, count or average a detail instead',
|
|
1065
|
+
],
|
|
1066
|
+
tip: 'Start with how you identify a record, then add the numbers you care about.',
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
id: 'filters',
|
|
1071
|
+
title: 'Filter Results',
|
|
1072
|
+
icon: Filter,
|
|
1073
|
+
description: 'Narrow down to specific records (optional)',
|
|
1074
|
+
guidance: {
|
|
1075
|
+
summary: 'Keep only the records you care about',
|
|
1076
|
+
howTo: [
|
|
1077
|
+
'Add a condition, then choose a detail and how to compare it',
|
|
1078
|
+
'Switch a group between "all of these" and "any of these"',
|
|
1079
|
+
'Nest a group to mix the two, e.g. active AND (email OR phone)',
|
|
1080
|
+
'Tick "ask me each time" to be prompted for the value at run time',
|
|
1081
|
+
],
|
|
1082
|
+
tip: 'Anything you might change week to week is better asked at run time than fixed here.',
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
id: 'grouping',
|
|
1087
|
+
title: 'Group & Order',
|
|
1088
|
+
icon: Data,
|
|
1089
|
+
description: 'Organise the results (optional)',
|
|
1090
|
+
guidance: {
|
|
1091
|
+
summary: 'Split the report into sections and set the row order',
|
|
1092
|
+
howTo: [
|
|
1093
|
+
'Tick a column to break the report into sections by its value',
|
|
1094
|
+
'Add a sort to control the order rows appear in',
|
|
1095
|
+
'Only columns you chose to show can be used here',
|
|
1096
|
+
'Skip this step for a plain list',
|
|
1097
|
+
],
|
|
1098
|
+
tip: 'Grouping suits reports like "clients by adviser".',
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
id: 'preview',
|
|
1103
|
+
title: 'Preview & Save',
|
|
1104
|
+
icon: EyeOpen,
|
|
1105
|
+
description: 'Review your report and save it for reuse',
|
|
1106
|
+
guidance: {
|
|
1107
|
+
summary: 'Run, review, save and export',
|
|
1108
|
+
howTo: [
|
|
1109
|
+
'Answer any run-time questions you set up',
|
|
1110
|
+
'Click "Run report" to see your data',
|
|
1111
|
+
'Save the report so you or your team can run it again',
|
|
1112
|
+
'Export the results as Excel, CSV or PDF',
|
|
1113
|
+
],
|
|
1114
|
+
tip: 'A saved report always runs against live data — save it once, run it whenever.',
|
|
1115
|
+
},
|
|
1116
|
+
},
|
|
1117
|
+
];
|
|
1118
|
+
|
|
1016
1119
|
// Icon mapping for business templates (moved from hardcoded templates)
|
|
1017
1120
|
const getTemplateIcon = (iconName) => {
|
|
1018
1121
|
const iconMap = {
|
|
@@ -1175,6 +1278,451 @@ const GenericReportImproved = ({
|
|
|
1175
1278
|
exportUrl = '/ajax/reportBuilder/export',
|
|
1176
1279
|
} = setting;
|
|
1177
1280
|
|
|
1281
|
+
// `semanticModelUrl: null` (or false) opts a host app out of the probe
|
|
1282
|
+
// entirely; omitting the key uses the contract default.
|
|
1283
|
+
const semanticModelUrl =
|
|
1284
|
+
'semanticModelUrl' in setting
|
|
1285
|
+
? setting.semanticModelUrl
|
|
1286
|
+
: DEFAULT_SEMANTIC_MODEL_URL;
|
|
1287
|
+
|
|
1288
|
+
const {
|
|
1289
|
+
model: semanticModel,
|
|
1290
|
+
status: semanticStatus,
|
|
1291
|
+
isSemantic,
|
|
1292
|
+
} = useSemanticModel(
|
|
1293
|
+
semanticModelUrl || DEFAULT_SEMANTIC_MODEL_URL,
|
|
1294
|
+
!!semanticModelUrl
|
|
1295
|
+
);
|
|
1296
|
+
|
|
1297
|
+
// ---- Semantic mode state -------------------------------------------
|
|
1298
|
+
// All of it is inert while `isSemantic` is false, which is what keeps
|
|
1299
|
+
// legacy mode byte-for-byte unchanged.
|
|
1300
|
+
const [semanticEntity, setSemanticEntity] = useState('');
|
|
1301
|
+
const [semanticRelations, setSemanticRelations] = useState([]);
|
|
1302
|
+
const [semanticSelections, setSemanticSelections] = useState([]);
|
|
1303
|
+
const [semanticFilterTree, setSemanticFilterTree] = useState(() =>
|
|
1304
|
+
createFilterGroup('and', [])
|
|
1305
|
+
);
|
|
1306
|
+
const [semanticParameters, setSemanticParameters] = useState([]);
|
|
1307
|
+
const [semanticParameterValues, setSemanticParameterValues] = useState({});
|
|
1308
|
+
const [semanticGroupBy, setSemanticGroupBy] = useState([]);
|
|
1309
|
+
const [semanticSort, setSemanticSort] = useState([]);
|
|
1310
|
+
const [semanticRows, setSemanticRows] = useState([]);
|
|
1311
|
+
const [semanticTotal, setSemanticTotal] = useState(0);
|
|
1312
|
+
const [semanticPage, setSemanticPage] = useState(1);
|
|
1313
|
+
const [semanticPageSize, setSemanticPageSize] = useState(25);
|
|
1314
|
+
const [semanticError, setSemanticError] = useState('');
|
|
1315
|
+
const [semanticHasRun, setSemanticHasRun] = useState(false);
|
|
1316
|
+
|
|
1317
|
+
const activeWizardSteps = isSemantic ? semanticWizardSteps : wizardSteps;
|
|
1318
|
+
|
|
1319
|
+
/* ---- Semantic mode handlers ---------------------------------------- */
|
|
1320
|
+
|
|
1321
|
+
const resetSemanticState = useCallback(() => {
|
|
1322
|
+
setSemanticEntity('');
|
|
1323
|
+
setSemanticRelations([]);
|
|
1324
|
+
setSemanticSelections([]);
|
|
1325
|
+
setSemanticFilterTree(createFilterGroup('and', []));
|
|
1326
|
+
setSemanticParameters([]);
|
|
1327
|
+
setSemanticParameterValues({});
|
|
1328
|
+
setSemanticGroupBy([]);
|
|
1329
|
+
setSemanticSort([]);
|
|
1330
|
+
setSemanticRows([]);
|
|
1331
|
+
setSemanticTotal(0);
|
|
1332
|
+
setSemanticPage(1);
|
|
1333
|
+
setSemanticError('');
|
|
1334
|
+
setSemanticHasRun(false);
|
|
1335
|
+
}, []);
|
|
1336
|
+
|
|
1337
|
+
const handleSemanticSelectEntity = (entityId) => {
|
|
1338
|
+
if (entityId === semanticEntity) return;
|
|
1339
|
+
// A different subject invalidates every path that was built on the
|
|
1340
|
+
// previous one, so start the semantic side of the wizard over.
|
|
1341
|
+
resetSemanticState();
|
|
1342
|
+
setSemanticEntity(entityId);
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
const handleSemanticToggleRelation = (relationPath) => {
|
|
1346
|
+
const isRemoving = semanticRelations.includes(relationPath);
|
|
1347
|
+
|
|
1348
|
+
if (!isRemoving) {
|
|
1349
|
+
setSemanticRelations((previous) => [...previous, relationPath]);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Removing a relation also removes anything nested under it and every
|
|
1354
|
+
// selection, filter, grouping and sort that reached through it.
|
|
1355
|
+
const prefix = `${relationPath}.`;
|
|
1356
|
+
const isOrphaned = (path) =>
|
|
1357
|
+
path === relationPath || String(path).startsWith(prefix);
|
|
1358
|
+
|
|
1359
|
+
setSemanticRelations((previous) =>
|
|
1360
|
+
previous.filter((path) => !isOrphaned(path))
|
|
1361
|
+
);
|
|
1362
|
+
setSemanticSelections((previous) =>
|
|
1363
|
+
previous.filter((selection) => !isOrphaned(selection.path))
|
|
1364
|
+
);
|
|
1365
|
+
setSemanticGroupBy((previous) =>
|
|
1366
|
+
previous.filter((path) => !isOrphaned(path))
|
|
1367
|
+
);
|
|
1368
|
+
setSemanticSort((previous) =>
|
|
1369
|
+
previous.filter((entry) => !isOrphaned(entry.field))
|
|
1370
|
+
);
|
|
1371
|
+
setSemanticFilterTree((previous) => {
|
|
1372
|
+
const prune = (node) => {
|
|
1373
|
+
if (node.kind === 'group') {
|
|
1374
|
+
return {
|
|
1375
|
+
...node,
|
|
1376
|
+
items: (node.items || [])
|
|
1377
|
+
.map(prune)
|
|
1378
|
+
.filter((child) => child !== null),
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
return isOrphaned(node.field) ? null : node;
|
|
1382
|
+
};
|
|
1383
|
+
return prune(previous);
|
|
1384
|
+
});
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
const handleSemanticToggleField = (path) => {
|
|
1388
|
+
setSemanticSelections((previous) => {
|
|
1389
|
+
const exists = previous.some(
|
|
1390
|
+
(selection) => selection.path === path
|
|
1391
|
+
);
|
|
1392
|
+
if (exists) {
|
|
1393
|
+
return previous.filter(
|
|
1394
|
+
(selection) => selection.path !== path
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
const selection = createSelection(
|
|
1398
|
+
semanticModel,
|
|
1399
|
+
semanticEntity,
|
|
1400
|
+
path
|
|
1401
|
+
);
|
|
1402
|
+
return selection ? [...previous, selection] : previous;
|
|
1403
|
+
});
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
const handleSemanticChangeAggregate = (path, agg) => {
|
|
1407
|
+
setSemanticSelections((previous) =>
|
|
1408
|
+
previous.map((selection) => {
|
|
1409
|
+
if (selection.path !== path) return selection;
|
|
1410
|
+
const resolved = resolveFieldPath(
|
|
1411
|
+
semanticModel,
|
|
1412
|
+
semanticEntity,
|
|
1413
|
+
path
|
|
1414
|
+
);
|
|
1415
|
+
if (!resolved) return selection;
|
|
1416
|
+
return {
|
|
1417
|
+
...selection,
|
|
1418
|
+
agg: agg || '',
|
|
1419
|
+
label: defaultSelectionLabel(resolved, agg),
|
|
1420
|
+
};
|
|
1421
|
+
})
|
|
1422
|
+
);
|
|
1423
|
+
|
|
1424
|
+
// An aggregate cannot also be the grouping key.
|
|
1425
|
+
if (agg) {
|
|
1426
|
+
setSemanticGroupBy((previous) =>
|
|
1427
|
+
previous.filter((entry) => entry !== path)
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
const handleSemanticRemoveSelection = (path, agg) => {
|
|
1433
|
+
setSemanticSelections((previous) =>
|
|
1434
|
+
previous.filter(
|
|
1435
|
+
(selection) =>
|
|
1436
|
+
!(
|
|
1437
|
+
selection.path === path &&
|
|
1438
|
+
(selection.agg || '') === (agg || '')
|
|
1439
|
+
)
|
|
1440
|
+
)
|
|
1441
|
+
);
|
|
1442
|
+
setSemanticGroupBy((previous) =>
|
|
1443
|
+
previous.filter((entry) => entry !== path)
|
|
1444
|
+
);
|
|
1445
|
+
setSemanticSort((previous) =>
|
|
1446
|
+
previous.filter((entry) => entry.field !== path)
|
|
1447
|
+
);
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
const handleSemanticMoveSelection = (from, to) => {
|
|
1451
|
+
setSemanticSelections((previous) => {
|
|
1452
|
+
if (to < 0 || to >= previous.length) return previous;
|
|
1453
|
+
const next = [...previous];
|
|
1454
|
+
const [moved] = next.splice(from, 1);
|
|
1455
|
+
next.splice(to, 0, moved);
|
|
1456
|
+
return next;
|
|
1457
|
+
});
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
const handleSemanticChangeFilterNode = (nodeId, patch) => {
|
|
1461
|
+
setSemanticFilterTree((previous) =>
|
|
1462
|
+
updateFilterNode(previous, nodeId, (node) => ({
|
|
1463
|
+
...node,
|
|
1464
|
+
...patch,
|
|
1465
|
+
}))
|
|
1466
|
+
);
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
const handleSemanticRemoveFilterNode = (nodeId) => {
|
|
1470
|
+
setSemanticFilterTree((previous) =>
|
|
1471
|
+
updateFilterNode(previous, nodeId, () => null)
|
|
1472
|
+
);
|
|
1473
|
+
};
|
|
1474
|
+
|
|
1475
|
+
const handleSemanticAddFilterCondition = (groupId) => {
|
|
1476
|
+
setSemanticFilterTree((previous) =>
|
|
1477
|
+
appendToFilterGroup(previous, groupId, createFilterCondition())
|
|
1478
|
+
);
|
|
1479
|
+
};
|
|
1480
|
+
|
|
1481
|
+
const handleSemanticAddFilterGroup = (groupId) => {
|
|
1482
|
+
setSemanticFilterTree((previous) =>
|
|
1483
|
+
appendToFilterGroup(
|
|
1484
|
+
previous,
|
|
1485
|
+
groupId,
|
|
1486
|
+
createFilterGroup('or', [createFilterCondition()])
|
|
1487
|
+
)
|
|
1488
|
+
);
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
const handleSemanticChangeParameter = (parameterId, patch) => {
|
|
1492
|
+
setSemanticParameters((previous) =>
|
|
1493
|
+
previous.map((parameter) =>
|
|
1494
|
+
parameter.id === parameterId
|
|
1495
|
+
? { ...parameter, ...patch }
|
|
1496
|
+
: parameter
|
|
1497
|
+
)
|
|
1498
|
+
);
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
const handleSemanticChangeParameterValue = (parameterId, value) => {
|
|
1502
|
+
setSemanticParameterValues((previous) => ({
|
|
1503
|
+
...previous,
|
|
1504
|
+
[parameterId]: value,
|
|
1505
|
+
}));
|
|
1506
|
+
};
|
|
1507
|
+
|
|
1508
|
+
const handleSemanticToggleGroupBy = (path) => {
|
|
1509
|
+
setSemanticGroupBy((previous) =>
|
|
1510
|
+
previous.includes(path)
|
|
1511
|
+
? previous.filter((entry) => entry !== path)
|
|
1512
|
+
: [...previous, path]
|
|
1513
|
+
);
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
const handleSemanticAddSort = (field, dir) => {
|
|
1517
|
+
setSemanticSort((previous) => [...previous, { field, dir }]);
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
const handleSemanticChangeSort = (index, patch) => {
|
|
1521
|
+
setSemanticSort((previous) =>
|
|
1522
|
+
previous.map((entry, entryIndex) =>
|
|
1523
|
+
entryIndex === index ? { ...entry, ...patch } : entry
|
|
1524
|
+
)
|
|
1525
|
+
);
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
const handleSemanticRemoveSort = (index) => {
|
|
1529
|
+
setSemanticSort((previous) =>
|
|
1530
|
+
previous.filter((entry, entryIndex) => entryIndex !== index)
|
|
1531
|
+
);
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
// Parameter definitions are derived from the filter tree: ticking "ask me
|
|
1535
|
+
// each time" creates one, unticking removes it. Edited labels survive.
|
|
1536
|
+
useEffect(() => {
|
|
1537
|
+
if (!isSemantic || !semanticEntity) return;
|
|
1538
|
+
setSemanticParameters((previous) => {
|
|
1539
|
+
const next = collectParameters(
|
|
1540
|
+
semanticModel,
|
|
1541
|
+
semanticEntity,
|
|
1542
|
+
semanticFilterTree,
|
|
1543
|
+
previous
|
|
1544
|
+
);
|
|
1545
|
+
const unchanged =
|
|
1546
|
+
next.length === previous.length &&
|
|
1547
|
+
next.every(
|
|
1548
|
+
(parameter, index) =>
|
|
1549
|
+
parameter.id === previous[index].id &&
|
|
1550
|
+
parameter.label === previous[index].label &&
|
|
1551
|
+
parameter.type === previous[index].type &&
|
|
1552
|
+
parameter.required === previous[index].required
|
|
1553
|
+
);
|
|
1554
|
+
return unchanged ? previous : next;
|
|
1555
|
+
});
|
|
1556
|
+
}, [isSemantic, semanticModel, semanticEntity, semanticFilterTree]);
|
|
1557
|
+
|
|
1558
|
+
// Seed a blank answer for each parameter so the inputs stay controlled.
|
|
1559
|
+
useEffect(() => {
|
|
1560
|
+
if (!isSemantic) return;
|
|
1561
|
+
setSemanticParameterValues((previous) => {
|
|
1562
|
+
const next = {};
|
|
1563
|
+
semanticParameters.forEach((parameter) => {
|
|
1564
|
+
next[parameter.id] =
|
|
1565
|
+
previous[parameter.id] ??
|
|
1566
|
+
emptyParameterValue(parameter.type);
|
|
1567
|
+
});
|
|
1568
|
+
return next;
|
|
1569
|
+
});
|
|
1570
|
+
}, [isSemantic, semanticParameters]);
|
|
1571
|
+
|
|
1572
|
+
/** The v2 document this wizard currently describes. */
|
|
1573
|
+
const semanticDefinition = useMemo(() => {
|
|
1574
|
+
if (!isSemantic || !semanticEntity) return null;
|
|
1575
|
+
return buildDefinition({
|
|
1576
|
+
model: semanticModel,
|
|
1577
|
+
entity: semanticEntity,
|
|
1578
|
+
selections: semanticSelections,
|
|
1579
|
+
filterTree: semanticFilterTree,
|
|
1580
|
+
parameters: semanticParameters,
|
|
1581
|
+
groupBy: semanticGroupBy,
|
|
1582
|
+
sort: semanticSort,
|
|
1583
|
+
});
|
|
1584
|
+
}, [
|
|
1585
|
+
isSemantic,
|
|
1586
|
+
semanticModel,
|
|
1587
|
+
semanticEntity,
|
|
1588
|
+
semanticSelections,
|
|
1589
|
+
semanticFilterTree,
|
|
1590
|
+
semanticParameters,
|
|
1591
|
+
semanticGroupBy,
|
|
1592
|
+
semanticSort,
|
|
1593
|
+
]);
|
|
1594
|
+
|
|
1595
|
+
/**
|
|
1596
|
+
* The server's own words for a rejected definition.
|
|
1597
|
+
*
|
|
1598
|
+
* A bad definition comes back as HTTP 422 with
|
|
1599
|
+
* `{success:false, message, errors:[{path, message}]}` — but a non-2xx
|
|
1600
|
+
* response makes CustomFetch reject rather than resolve, so the body never
|
|
1601
|
+
* reaches the `success === false` branch below. "Unknown field [x] on
|
|
1602
|
+
* [clients]" is exactly what the user needs to fix the report, so dig it
|
|
1603
|
+
* off the error before falling back to a generic line.
|
|
1604
|
+
*/
|
|
1605
|
+
const semanticErrorMessage = (error, fallback) => {
|
|
1606
|
+
const data = error?.response?.data;
|
|
1607
|
+
if (typeof data?.message === 'string' && data.message) {
|
|
1608
|
+
return data.message;
|
|
1609
|
+
}
|
|
1610
|
+
const first = Array.isArray(data?.errors) ? data.errors[0] : null;
|
|
1611
|
+
if (typeof first?.message === 'string' && first.message) {
|
|
1612
|
+
return first.message;
|
|
1613
|
+
}
|
|
1614
|
+
return fallback;
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1617
|
+
/**
|
|
1618
|
+
* POST executeUrl {definition, parameters, limit, offset}
|
|
1619
|
+
* -> {success, data: [rows], total}
|
|
1620
|
+
*/
|
|
1621
|
+
const runSemanticReport = async ({ page, size } = {}) => {
|
|
1622
|
+
if (!semanticDefinition || semanticSelections.length === 0) return;
|
|
1623
|
+
|
|
1624
|
+
const pageToLoad = page || semanticPage;
|
|
1625
|
+
const sizeToLoad = size || semanticPageSize;
|
|
1626
|
+
|
|
1627
|
+
if (!parametersSatisfied(semanticParameters, semanticParameterValues)) {
|
|
1628
|
+
setSemanticError(
|
|
1629
|
+
'Answer every required question before running this report.'
|
|
1630
|
+
);
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
setIsLoading(true);
|
|
1635
|
+
setSemanticError('');
|
|
1636
|
+
|
|
1637
|
+
try {
|
|
1638
|
+
const result = await CustomFetch(
|
|
1639
|
+
customUrls.executeUrl || executeUrl,
|
|
1640
|
+
'POST',
|
|
1641
|
+
{
|
|
1642
|
+
definition: semanticDefinition,
|
|
1643
|
+
parameters: semanticParameterValues,
|
|
1644
|
+
limit: sizeToLoad,
|
|
1645
|
+
offset: (pageToLoad - 1) * sizeToLoad,
|
|
1646
|
+
},
|
|
1647
|
+
null,
|
|
1648
|
+
() => {},
|
|
1649
|
+
{ cache: false }
|
|
1650
|
+
);
|
|
1651
|
+
|
|
1652
|
+
const body = result?.data;
|
|
1653
|
+
|
|
1654
|
+
if (body?.success === false) {
|
|
1655
|
+
setSemanticRows([]);
|
|
1656
|
+
setSemanticTotal(0);
|
|
1657
|
+
setSemanticError(
|
|
1658
|
+
body.message || 'The report could not be run.'
|
|
1659
|
+
);
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
1664
|
+
setSemanticRows(rows);
|
|
1665
|
+
setSemanticTotal(
|
|
1666
|
+
typeof body?.total === 'number' ? body.total : rows.length
|
|
1667
|
+
);
|
|
1668
|
+
setSemanticPage(pageToLoad);
|
|
1669
|
+
setSemanticPageSize(sizeToLoad);
|
|
1670
|
+
setSemanticHasRun(true);
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
debugLog('Semantic report execution failed:', error);
|
|
1673
|
+
setSemanticRows([]);
|
|
1674
|
+
setSemanticTotal(0);
|
|
1675
|
+
setSemanticError(
|
|
1676
|
+
semanticErrorMessage(
|
|
1677
|
+
error,
|
|
1678
|
+
'We could not run this report. Please try again.'
|
|
1679
|
+
)
|
|
1680
|
+
);
|
|
1681
|
+
} finally {
|
|
1682
|
+
setIsLoading(false);
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
|
|
1686
|
+
/** POST exportUrl {definition, parameters, format} -> blob */
|
|
1687
|
+
const exportSemanticReport = async (format) => {
|
|
1688
|
+
if (!semanticDefinition) return;
|
|
1689
|
+
|
|
1690
|
+
try {
|
|
1691
|
+
const result = await Download(
|
|
1692
|
+
customUrls.exportUrl || exportUrl,
|
|
1693
|
+
'POST',
|
|
1694
|
+
{
|
|
1695
|
+
definition: semanticDefinition,
|
|
1696
|
+
parameters: semanticParameterValues,
|
|
1697
|
+
format,
|
|
1698
|
+
}
|
|
1699
|
+
);
|
|
1700
|
+
|
|
1701
|
+
if (result?.data) {
|
|
1702
|
+
const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss');
|
|
1703
|
+
const baseName = (
|
|
1704
|
+
reportName ||
|
|
1705
|
+
getEntityLabel(semanticModel, semanticEntity) ||
|
|
1706
|
+
'report'
|
|
1707
|
+
).replace(/[^a-zA-Z0-9]/g, '_');
|
|
1708
|
+
saveAs(result.data, `${baseName}_${timestamp}.${format}`);
|
|
1709
|
+
toast.success(`Report exported as ${format.toUpperCase()}`);
|
|
1710
|
+
} else {
|
|
1711
|
+
toast.error(
|
|
1712
|
+
`Failed to export report as ${format.toUpperCase()}`
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
debugLog('Semantic export failed:', error);
|
|
1717
|
+
toast.error(
|
|
1718
|
+
semanticErrorMessage(
|
|
1719
|
+
error,
|
|
1720
|
+
`Failed to export report as ${format.toUpperCase()}`
|
|
1721
|
+
)
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1178
1726
|
// Simple icon helper for SweetAlert2 - returns HTML strings with SVG icons
|
|
1179
1727
|
const getIcon = (iconName, color = '#6b7280', size = 16) => {
|
|
1180
1728
|
const icons = {
|
|
@@ -1436,7 +1984,14 @@ const GenericReportImproved = ({
|
|
|
1436
1984
|
|
|
1437
1985
|
// Initialize component
|
|
1438
1986
|
useEffect(() => {
|
|
1439
|
-
|
|
1987
|
+
// Wait for the semantic probe to settle before hitting the legacy
|
|
1988
|
+
// table endpoint: a semantic-only backend has no /getTables to call,
|
|
1989
|
+
// and asking anyway would surface an error the user cannot act on.
|
|
1990
|
+
if (semanticStatus === 'loading') return;
|
|
1991
|
+
if (semanticStatus !== 'ready') {
|
|
1992
|
+
fetchDatabaseTables();
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1440
1995
|
fetchSavedReports();
|
|
1441
1996
|
|
|
1442
1997
|
// Show help on first load
|
|
@@ -1447,7 +2002,7 @@ const GenericReportImproved = ({
|
|
|
1447
2002
|
localStorage.setItem('reportBuilder_hasSeenHelp', 'true');
|
|
1448
2003
|
}, 500);
|
|
1449
2004
|
}
|
|
1450
|
-
}, [tableUrl, reportsUrl]); //
|
|
2005
|
+
}, [tableUrl, reportsUrl, semanticStatus]); // Re-fetch when URLs change
|
|
1451
2006
|
|
|
1452
2007
|
// Fetch columns when table is selected (like advanced mode)
|
|
1453
2008
|
useEffect(() => {
|
|
@@ -1522,6 +2077,7 @@ const GenericReportImproved = ({
|
|
|
1522
2077
|
// Auto-execute report when user reaches step 5 (preview step)
|
|
1523
2078
|
useEffect(() => {
|
|
1524
2079
|
if (
|
|
2080
|
+
!isSemantic && // Semantic mode has its own run trigger below
|
|
1525
2081
|
currentWizardStep === 5 && // Step 6 is index 5 (preview step)
|
|
1526
2082
|
selectedTable &&
|
|
1527
2083
|
selectedColumns.length > 0 &&
|
|
@@ -1532,6 +2088,7 @@ const GenericReportImproved = ({
|
|
|
1532
2088
|
executeReport();
|
|
1533
2089
|
}
|
|
1534
2090
|
}, [
|
|
2091
|
+
isSemantic,
|
|
1535
2092
|
currentWizardStep,
|
|
1536
2093
|
selectedTable,
|
|
1537
2094
|
selectedColumns.length,
|
|
@@ -1539,6 +2096,40 @@ const GenericReportImproved = ({
|
|
|
1539
2096
|
hasAutoExecuted,
|
|
1540
2097
|
]);
|
|
1541
2098
|
|
|
2099
|
+
// Semantic mode: run once on arriving at the preview step, but only when
|
|
2100
|
+
// there is nothing to ask the user first — otherwise wait for their answers.
|
|
2101
|
+
useEffect(() => {
|
|
2102
|
+
if (!isSemantic) return;
|
|
2103
|
+
if (currentWizardStep !== 5) return;
|
|
2104
|
+
if (isLoading || semanticHasRun) return;
|
|
2105
|
+
if (semanticSelections.length === 0) return;
|
|
2106
|
+
if (!parametersSatisfied(semanticParameters, semanticParameterValues)) {
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
runSemanticReport({ page: 1 });
|
|
2110
|
+
}, [
|
|
2111
|
+
isSemantic,
|
|
2112
|
+
currentWizardStep,
|
|
2113
|
+
isLoading,
|
|
2114
|
+
semanticHasRun,
|
|
2115
|
+
semanticSelections.length,
|
|
2116
|
+
semanticParameters,
|
|
2117
|
+
semanticParameterValues,
|
|
2118
|
+
]);
|
|
2119
|
+
|
|
2120
|
+
// Any change to the report design invalidates the results already on screen.
|
|
2121
|
+
useEffect(() => {
|
|
2122
|
+
if (!isSemantic) return;
|
|
2123
|
+
setSemanticHasRun(false);
|
|
2124
|
+
}, [
|
|
2125
|
+
isSemantic,
|
|
2126
|
+
semanticEntity,
|
|
2127
|
+
semanticSelections,
|
|
2128
|
+
semanticFilterTree,
|
|
2129
|
+
semanticGroupBy,
|
|
2130
|
+
semanticSort,
|
|
2131
|
+
]);
|
|
2132
|
+
|
|
1542
2133
|
// Escape closes whichever modal is open
|
|
1543
2134
|
useEffect(() => {
|
|
1544
2135
|
if (!showCalculatedFieldModal && !showRowExclusionModal) return undefined;
|
|
@@ -1724,6 +2315,9 @@ const GenericReportImproved = ({
|
|
|
1724
2315
|
// Reset UI state
|
|
1725
2316
|
setShowHiddenFields(false);
|
|
1726
2317
|
|
|
2318
|
+
// Reset semantic mode state (inert in legacy mode)
|
|
2319
|
+
resetSemanticState();
|
|
2320
|
+
|
|
1727
2321
|
toast.info(
|
|
1728
2322
|
'Report builder reset. You can start creating a new report.'
|
|
1729
2323
|
);
|
|
@@ -1731,7 +2325,7 @@ const GenericReportImproved = ({
|
|
|
1731
2325
|
|
|
1732
2326
|
// Wizard navigation
|
|
1733
2327
|
const goToNextStep = () => {
|
|
1734
|
-
if (currentWizardStep <
|
|
2328
|
+
if (currentWizardStep < activeWizardSteps.length - 1) {
|
|
1735
2329
|
setCurrentWizardStep(currentWizardStep + 1);
|
|
1736
2330
|
}
|
|
1737
2331
|
};
|
|
@@ -1744,6 +2338,25 @@ const GenericReportImproved = ({
|
|
|
1744
2338
|
|
|
1745
2339
|
// Check if current step is complete
|
|
1746
2340
|
const isStepComplete = (stepId) => {
|
|
2341
|
+
if (isSemantic) {
|
|
2342
|
+
switch (stepId) {
|
|
2343
|
+
case 'table':
|
|
2344
|
+
return !!semanticEntity;
|
|
2345
|
+
case 'relationships':
|
|
2346
|
+
return true; // Optional step
|
|
2347
|
+
case 'columns':
|
|
2348
|
+
return semanticSelections.length > 0;
|
|
2349
|
+
case 'filters':
|
|
2350
|
+
return true; // Optional step
|
|
2351
|
+
case 'grouping':
|
|
2352
|
+
return true; // Optional step
|
|
2353
|
+
case 'preview':
|
|
2354
|
+
return semanticHasRun;
|
|
2355
|
+
default:
|
|
2356
|
+
return false;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
|
|
1747
2360
|
switch (stepId) {
|
|
1748
2361
|
case 'table':
|
|
1749
2362
|
return !!selectedTable;
|
|
@@ -1813,15 +2426,15 @@ const GenericReportImproved = ({
|
|
|
1813
2426
|
return;
|
|
1814
2427
|
}
|
|
1815
2428
|
|
|
1816
|
-
// Filter out hidden tables
|
|
2429
|
+
// Filter out hidden tables only. Snake_case names are ordinary
|
|
2430
|
+
// table names — dropping them silently hid most of the schema.
|
|
1817
2431
|
let filteredTables = tableData
|
|
1818
2432
|
.filter((table) => {
|
|
1819
2433
|
const tableName =
|
|
1820
2434
|
typeof table === 'string' ? table : table?.name;
|
|
1821
2435
|
return (
|
|
1822
2436
|
typeof tableName === 'string' &&
|
|
1823
|
-
!hiddenTables.includes(tableName)
|
|
1824
|
-
!tableName.includes('_')
|
|
2437
|
+
!hiddenTables.includes(tableName)
|
|
1825
2438
|
);
|
|
1826
2439
|
})
|
|
1827
2440
|
.map((table) => {
|
|
@@ -2988,8 +3601,8 @@ const GenericReportImproved = ({
|
|
|
2988
3601
|
}
|
|
2989
3602
|
}
|
|
2990
3603
|
|
|
2991
|
-
if (currentWizardStep <
|
|
2992
|
-
setCurrentWizardStep(
|
|
3604
|
+
if (currentWizardStep < activeWizardSteps.length - 1) {
|
|
3605
|
+
setCurrentWizardStep(activeWizardSteps.length - 1);
|
|
2993
3606
|
}
|
|
2994
3607
|
|
|
2995
3608
|
// Report executed successfully
|
|
@@ -3033,6 +3646,40 @@ const GenericReportImproved = ({
|
|
|
3033
3646
|
return;
|
|
3034
3647
|
}
|
|
3035
3648
|
|
|
3649
|
+
// A v2 document restores the semantic wizard. A v1 document (it has
|
|
3650
|
+
// `mainTable`) always falls through to the legacy path below, even
|
|
3651
|
+
// when the semantic model happens to be available.
|
|
3652
|
+
if (isSemanticDefinition(config)) {
|
|
3653
|
+
if (!isSemantic) {
|
|
3654
|
+
toast.error(
|
|
3655
|
+
'This report needs the newer report engine, which is not available here.'
|
|
3656
|
+
);
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
const parsed = parseDefinition(config);
|
|
3661
|
+
resetSemanticState();
|
|
3662
|
+
setSemanticEntity(parsed.entity);
|
|
3663
|
+
setSemanticSelections(parsed.selections);
|
|
3664
|
+
setSemanticFilterTree(parsed.filterTree);
|
|
3665
|
+
setSemanticParameters(parsed.parameters);
|
|
3666
|
+
setSemanticGroupBy(parsed.groupBy);
|
|
3667
|
+
setSemanticSort(parsed.sort);
|
|
3668
|
+
// Re-derive which related areas the saved paths reach through,
|
|
3669
|
+
// so the relations step shows them as already added.
|
|
3670
|
+
setSemanticRelations(
|
|
3671
|
+
relationPathsFromDefinition(parsed, semanticModel)
|
|
3672
|
+
);
|
|
3673
|
+
|
|
3674
|
+
setReportName(report.label || '');
|
|
3675
|
+
setIsPublic(!!report.is_public);
|
|
3676
|
+
setLoadedReportId(report.id);
|
|
3677
|
+
setShowTemplates(false);
|
|
3678
|
+
setCurrentWizardStep(0);
|
|
3679
|
+
toast.success(`Loaded report: ${report.label}`);
|
|
3680
|
+
return;
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3036
3683
|
if (config.mainTable) {
|
|
3037
3684
|
setSelectedTable(config.mainTable);
|
|
3038
3685
|
setAvailableTables([config.mainTable]);
|
|
@@ -3440,9 +4087,21 @@ const GenericReportImproved = ({
|
|
|
3440
4087
|
<h3>Your Saved Reports ({savedReports.length})</h3>
|
|
3441
4088
|
<div className={styles.reportsGrid}>
|
|
3442
4089
|
{savedReports.map((report) => {
|
|
3443
|
-
const
|
|
3444
|
-
|
|
3445
|
-
|
|
4090
|
+
const reportDetail = parseReportDetail(
|
|
4091
|
+
report.detail
|
|
4092
|
+
);
|
|
4093
|
+
// v1 reports show the table name; v2 ones
|
|
4094
|
+
// show the entity's business label.
|
|
4095
|
+
const reportSubject = isSemanticDefinition(
|
|
4096
|
+
reportDetail
|
|
4097
|
+
)
|
|
4098
|
+
? getEntityLabel(
|
|
4099
|
+
semanticModel,
|
|
4100
|
+
reportDetail.entity
|
|
4101
|
+
)
|
|
4102
|
+
: formatName(
|
|
4103
|
+
reportDetail?.mainTable || ''
|
|
4104
|
+
);
|
|
3446
4105
|
return (
|
|
3447
4106
|
<div
|
|
3448
4107
|
key={report.id}
|
|
@@ -3497,7 +4156,7 @@ const GenericReportImproved = ({
|
|
|
3497
4156
|
</span>
|
|
3498
4157
|
</div>
|
|
3499
4158
|
</div>
|
|
3500
|
-
{
|
|
4159
|
+
{reportSubject && (
|
|
3501
4160
|
<div
|
|
3502
4161
|
className={
|
|
3503
4162
|
styles.reportTableInfo
|
|
@@ -3517,9 +4176,7 @@ const GenericReportImproved = ({
|
|
|
3517
4176
|
'middle',
|
|
3518
4177
|
}}
|
|
3519
4178
|
/>
|
|
3520
|
-
{
|
|
3521
|
-
reportMainTable
|
|
3522
|
-
)}
|
|
4179
|
+
{reportSubject}
|
|
3523
4180
|
</span>
|
|
3524
4181
|
</div>
|
|
3525
4182
|
)}
|
|
@@ -3571,14 +4228,14 @@ const GenericReportImproved = ({
|
|
|
3571
4228
|
|
|
3572
4229
|
// Render wizard mode
|
|
3573
4230
|
const renderWizardMode = () => {
|
|
3574
|
-
const currentStep =
|
|
4231
|
+
const currentStep = activeWizardSteps[currentWizardStep];
|
|
3575
4232
|
const StepIcon = currentStep.icon;
|
|
3576
4233
|
|
|
3577
4234
|
return (
|
|
3578
4235
|
<div className={styles.wizardMode}>
|
|
3579
4236
|
{/* Progress bar */}
|
|
3580
4237
|
<div className={styles.wizardProgress}>
|
|
3581
|
-
{
|
|
4238
|
+
{activeWizardSteps.map((step, index) => {
|
|
3582
4239
|
const StepIcon = step.icon;
|
|
3583
4240
|
const isActive = index === currentWizardStep;
|
|
3584
4241
|
const isComplete =
|
|
@@ -3696,7 +4353,7 @@ const GenericReportImproved = ({
|
|
|
3696
4353
|
</button>
|
|
3697
4354
|
)}
|
|
3698
4355
|
|
|
3699
|
-
{currentWizardStep ===
|
|
4356
|
+
{currentWizardStep === activeWizardSteps.length - 1 ? (
|
|
3700
4357
|
<button
|
|
3701
4358
|
type="button"
|
|
3702
4359
|
className={`${styles.btn} ${styles.btnStartAgain}`}
|
|
@@ -3720,8 +4377,107 @@ const GenericReportImproved = ({
|
|
|
3720
4377
|
);
|
|
3721
4378
|
};
|
|
3722
4379
|
|
|
4380
|
+
// Render the semantic-mode body for a step. Returns null in legacy mode so
|
|
4381
|
+
// the caller falls through to the original renderers untouched.
|
|
4382
|
+
const renderSemanticStepContent = (stepId) => {
|
|
4383
|
+
switch (stepId) {
|
|
4384
|
+
case 'table':
|
|
4385
|
+
return (
|
|
4386
|
+
<SemanticEntityStep
|
|
4387
|
+
model={semanticModel}
|
|
4388
|
+
selectedEntity={semanticEntity}
|
|
4389
|
+
onSelect={handleSemanticSelectEntity}
|
|
4390
|
+
/>
|
|
4391
|
+
);
|
|
4392
|
+
case 'relationships':
|
|
4393
|
+
return (
|
|
4394
|
+
<SemanticRelationsStep
|
|
4395
|
+
model={semanticModel}
|
|
4396
|
+
entityId={semanticEntity}
|
|
4397
|
+
addedRelations={semanticRelations}
|
|
4398
|
+
onToggle={handleSemanticToggleRelation}
|
|
4399
|
+
/>
|
|
4400
|
+
);
|
|
4401
|
+
case 'columns':
|
|
4402
|
+
return (
|
|
4403
|
+
<SemanticFieldsStep
|
|
4404
|
+
model={semanticModel}
|
|
4405
|
+
entityId={semanticEntity}
|
|
4406
|
+
addedRelations={semanticRelations}
|
|
4407
|
+
selections={semanticSelections}
|
|
4408
|
+
onToggleField={handleSemanticToggleField}
|
|
4409
|
+
onChangeAggregate={handleSemanticChangeAggregate}
|
|
4410
|
+
onRemoveSelection={handleSemanticRemoveSelection}
|
|
4411
|
+
onMoveSelection={handleSemanticMoveSelection}
|
|
4412
|
+
/>
|
|
4413
|
+
);
|
|
4414
|
+
case 'filters':
|
|
4415
|
+
return (
|
|
4416
|
+
<SemanticFiltersStep
|
|
4417
|
+
model={semanticModel}
|
|
4418
|
+
entityId={semanticEntity}
|
|
4419
|
+
addedRelations={semanticRelations}
|
|
4420
|
+
filterTree={semanticFilterTree}
|
|
4421
|
+
parameters={semanticParameters}
|
|
4422
|
+
onChangeNode={handleSemanticChangeFilterNode}
|
|
4423
|
+
onRemoveNode={handleSemanticRemoveFilterNode}
|
|
4424
|
+
onAddCondition={handleSemanticAddFilterCondition}
|
|
4425
|
+
onAddGroup={handleSemanticAddFilterGroup}
|
|
4426
|
+
onChangeParameter={handleSemanticChangeParameter}
|
|
4427
|
+
/>
|
|
4428
|
+
);
|
|
4429
|
+
case 'grouping':
|
|
4430
|
+
return (
|
|
4431
|
+
<SemanticGroupingStep
|
|
4432
|
+
model={semanticModel}
|
|
4433
|
+
entityId={semanticEntity}
|
|
4434
|
+
selections={semanticSelections}
|
|
4435
|
+
groupBy={semanticGroupBy}
|
|
4436
|
+
sort={semanticSort}
|
|
4437
|
+
onToggleGroupBy={handleSemanticToggleGroupBy}
|
|
4438
|
+
onAddSort={handleSemanticAddSort}
|
|
4439
|
+
onChangeSort={handleSemanticChangeSort}
|
|
4440
|
+
onRemoveSort={handleSemanticRemoveSort}
|
|
4441
|
+
/>
|
|
4442
|
+
);
|
|
4443
|
+
case 'preview':
|
|
4444
|
+
return (
|
|
4445
|
+
<SemanticPreviewStep
|
|
4446
|
+
model={semanticModel}
|
|
4447
|
+
entityId={semanticEntity}
|
|
4448
|
+
selections={semanticSelections}
|
|
4449
|
+
filterTree={semanticFilterTree}
|
|
4450
|
+
parameters={semanticParameters}
|
|
4451
|
+
parameterValues={semanticParameterValues}
|
|
4452
|
+
groupBy={semanticGroupBy}
|
|
4453
|
+
rows={semanticRows}
|
|
4454
|
+
total={semanticTotal}
|
|
4455
|
+
page={semanticPage}
|
|
4456
|
+
pageSize={semanticPageSize}
|
|
4457
|
+
isLoading={isLoading}
|
|
4458
|
+
error={semanticError}
|
|
4459
|
+
onChangeParameterValue={handleSemanticChangeParameterValue}
|
|
4460
|
+
onRun={runSemanticReport}
|
|
4461
|
+
onChangePage={(page) => runSemanticReport({ page })}
|
|
4462
|
+
onChangePageSize={(size) =>
|
|
4463
|
+
runSemanticReport({ page: 1, size })
|
|
4464
|
+
}
|
|
4465
|
+
onExport={exportSemanticReport}
|
|
4466
|
+
onSave={() => saveReport(false)}
|
|
4467
|
+
canSave={semanticSelections.length > 0}
|
|
4468
|
+
/>
|
|
4469
|
+
);
|
|
4470
|
+
default:
|
|
4471
|
+
return null;
|
|
4472
|
+
}
|
|
4473
|
+
};
|
|
4474
|
+
|
|
3723
4475
|
// Render wizard step content
|
|
3724
4476
|
const renderWizardStepContent = (stepId) => {
|
|
4477
|
+
if (isSemantic) {
|
|
4478
|
+
return renderSemanticStepContent(stepId);
|
|
4479
|
+
}
|
|
4480
|
+
|
|
3725
4481
|
switch (stepId) {
|
|
3726
4482
|
case 'table':
|
|
3727
4483
|
return renderTableSelection();
|
|
@@ -7341,10 +8097,15 @@ const GenericReportImproved = ({
|
|
|
7341
8097
|
|
|
7342
8098
|
// Save report configuration (with save/save-as functionality)
|
|
7343
8099
|
const saveReport = async (saveAs = false) => {
|
|
8100
|
+
// In semantic mode the subject is an entity label, not a table name.
|
|
8101
|
+
const subjectName = isSemantic
|
|
8102
|
+
? getEntityLabel(semanticModel, semanticEntity)
|
|
8103
|
+
: formatName(selectedTable);
|
|
8104
|
+
|
|
7344
8105
|
if (!reportName.trim()) {
|
|
7345
|
-
const defaultName = `${
|
|
7346
|
-
|
|
7347
|
-
)}
|
|
8106
|
+
const defaultName = `${subjectName} Report - ${moment().format(
|
|
8107
|
+
'YYYY-MM-DD'
|
|
8108
|
+
)}`;
|
|
7348
8109
|
setReportName(defaultName);
|
|
7349
8110
|
}
|
|
7350
8111
|
|
|
@@ -7377,9 +8138,7 @@ const GenericReportImproved = ({
|
|
|
7377
8138
|
saveAs
|
|
7378
8139
|
? `${reportName} (Copy)`
|
|
7379
8140
|
: reportName ||
|
|
7380
|
-
`${
|
|
7381
|
-
selectedTable
|
|
7382
|
-
)} Report - ${moment().format(
|
|
8141
|
+
`${subjectName} Report - ${moment().format(
|
|
7383
8142
|
'YYYY-MM-DD'
|
|
7384
8143
|
)}`
|
|
7385
8144
|
)}"
|
|
@@ -7416,24 +8175,32 @@ const GenericReportImproved = ({
|
|
|
7416
8175
|
|
|
7417
8176
|
if (result.isConfirmed) {
|
|
7418
8177
|
try {
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
8178
|
+
// Semantic reports save the v2 definition document verbatim;
|
|
8179
|
+
// legacy reports keep their existing v1 shape.
|
|
8180
|
+
const reportConfig = isSemantic
|
|
8181
|
+
? {
|
|
8182
|
+
label: result.value.name,
|
|
8183
|
+
is_public: result.value.isPublic,
|
|
8184
|
+
detail: semanticDefinition,
|
|
8185
|
+
}
|
|
8186
|
+
: {
|
|
8187
|
+
label: result.value.name,
|
|
8188
|
+
is_public: result.value.isPublic,
|
|
8189
|
+
detail: {
|
|
8190
|
+
mainTable: selectedTable,
|
|
8191
|
+
columns: selectedColumns,
|
|
8192
|
+
joins: joins,
|
|
8193
|
+
filters: filterCriteria,
|
|
8194
|
+
sorting: sortCriteria,
|
|
8195
|
+
grouping: groupingConfig.enabled
|
|
8196
|
+
? groupingConfig
|
|
8197
|
+
: null,
|
|
8198
|
+
isGrouped: groupingConfig.enabled,
|
|
8199
|
+
customCalculatedFields: customCalculatedFields,
|
|
8200
|
+
customUrls: customUrls,
|
|
8201
|
+
unique: uniqueFieldConfig,
|
|
8202
|
+
},
|
|
8203
|
+
};
|
|
7437
8204
|
|
|
7438
8205
|
let saveResult;
|
|
7439
8206
|
if (isExistingReport) {
|