@bygd/nc-report-ui 0.1.42 → 0.1.43
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/dist/app/esm/index.js +311 -182
- package/dist/default/cjs/index.cjs +306 -181
- package/dist/default/esm/index.js +305 -181
- package/package.json +1 -1
package/dist/app/esm/index.js
CHANGED
|
@@ -30273,6 +30273,7 @@ function useInView({
|
|
|
30273
30273
|
delay,
|
|
30274
30274
|
trackVisibility,
|
|
30275
30275
|
rootMargin,
|
|
30276
|
+
scrollMargin,
|
|
30276
30277
|
root,
|
|
30277
30278
|
triggerOnce,
|
|
30278
30279
|
skip,
|
|
@@ -30317,8 +30318,8 @@ function useInView({
|
|
|
30317
30318
|
{
|
|
30318
30319
|
root,
|
|
30319
30320
|
rootMargin,
|
|
30321
|
+
scrollMargin,
|
|
30320
30322
|
threshold,
|
|
30321
|
-
// @ts-expect-error
|
|
30322
30323
|
trackVisibility,
|
|
30323
30324
|
delay
|
|
30324
30325
|
},
|
|
@@ -30338,6 +30339,7 @@ function useInView({
|
|
|
30338
30339
|
ref,
|
|
30339
30340
|
root,
|
|
30340
30341
|
rootMargin,
|
|
30342
|
+
scrollMargin,
|
|
30341
30343
|
triggerOnce,
|
|
30342
30344
|
skip,
|
|
30343
30345
|
trackVisibility,
|
|
@@ -43266,6 +43268,10 @@ const ChartFilterMultiSelect = ({
|
|
|
43266
43268
|
})));
|
|
43267
43269
|
};
|
|
43268
43270
|
|
|
43271
|
+
var CheckCircleIcon = createSvgIcon$1(/*#__PURE__*/u("path", {
|
|
43272
|
+
d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m-2 15-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8z"
|
|
43273
|
+
}));
|
|
43274
|
+
|
|
43269
43275
|
const formatLabel = key => {
|
|
43270
43276
|
return key?.replace(/([a-z])([A-Z])/g, "$1 $2") // insert space before capital letters
|
|
43271
43277
|
?.replace(/_/g, " ")?.replace(/-/g, " ") // replace underscores with spaces (optional)
|
|
@@ -43329,7 +43335,8 @@ function SingleSelect({
|
|
|
43329
43335
|
key,
|
|
43330
43336
|
value,
|
|
43331
43337
|
disabled,
|
|
43332
|
-
icon
|
|
43338
|
+
icon,
|
|
43339
|
+
alreadySelected
|
|
43333
43340
|
} = itm;
|
|
43334
43341
|
return /*#__PURE__*/gn.createElement(MenuItem, {
|
|
43335
43342
|
key: key,
|
|
@@ -43340,7 +43347,12 @@ function SingleSelect({
|
|
|
43340
43347
|
minHeight: "36px",
|
|
43341
43348
|
display: "flex",
|
|
43342
43349
|
alignItems: "center",
|
|
43343
|
-
gap: "6px"
|
|
43350
|
+
gap: "6px",
|
|
43351
|
+
...(alreadySelected && {
|
|
43352
|
+
color: "rgb(70, 134, 128)",
|
|
43353
|
+
backgroundColor: "rgba(70, 134, 128, 0.08)",
|
|
43354
|
+
fontWeight: 600
|
|
43355
|
+
})
|
|
43344
43356
|
}
|
|
43345
43357
|
}, icon && /*#__PURE__*/gn.createElement(Box, {
|
|
43346
43358
|
component: "span",
|
|
@@ -43348,7 +43360,13 @@ function SingleSelect({
|
|
|
43348
43360
|
display: "inline-flex",
|
|
43349
43361
|
alignItems: "center"
|
|
43350
43362
|
}
|
|
43351
|
-
}, icon),
|
|
43363
|
+
}, icon), alreadySelected && /*#__PURE__*/gn.createElement(CheckCircleIcon, {
|
|
43364
|
+
fontSize: "small",
|
|
43365
|
+
sx: {
|
|
43366
|
+
color: "rgb(70, 134, 128)",
|
|
43367
|
+
fontSize: "16px"
|
|
43368
|
+
}
|
|
43369
|
+
}), formatLabel(value));
|
|
43352
43370
|
}))));
|
|
43353
43371
|
}
|
|
43354
43372
|
|
|
@@ -77326,6 +77344,29 @@ var FunctionsIcon = createSvgIcon$1(/*#__PURE__*/u("path", {
|
|
|
77326
77344
|
d: "M18 4H6v2l6.5 6L6 18v2h12v-3h-7l5-5-5-5h7z"
|
|
77327
77345
|
}));
|
|
77328
77346
|
|
|
77347
|
+
// Builds an identity string for a saved dimension/metric instance. Deliberate
|
|
77348
|
+
// duplicates (the same field added more than once) are allowed - the first
|
|
77349
|
+
// occurrence keeps the plain fullPath (so existing saved reports and their
|
|
77350
|
+
// title-override keys stay backward compatible), later occurrences get a
|
|
77351
|
+
// `#n` suffix.
|
|
77352
|
+
const makeInstanceId = (fullPath, occurrenceIndex) => occurrenceIndex === 0 ? fullPath : `${fullPath}#${occurrenceIndex}`;
|
|
77353
|
+
|
|
77354
|
+
// Picks the next free instance id for a newly-added fullPath, given the
|
|
77355
|
+
// items already saved. Scans occurrence numbers from 0 rather than just
|
|
77356
|
+
// counting existing instances with this fullPath, because counting alone
|
|
77357
|
+
// can collide: e.g. add "ft.currency" twice (ids "ft.currency",
|
|
77358
|
+
// "ft.currency#1"), remove the first, then add "ft.currency" again - a
|
|
77359
|
+
// plain count of 1 would regenerate "ft.currency#1", clashing with the
|
|
77360
|
+
// surviving duplicate.
|
|
77361
|
+
const nextInstanceId = (fullPath, existingItems) => {
|
|
77362
|
+
const existingIds = new Set(existingItems.map(item => item.id));
|
|
77363
|
+
let occurrenceIndex = 0;
|
|
77364
|
+
while (existingIds.has(makeInstanceId(fullPath, occurrenceIndex))) {
|
|
77365
|
+
occurrenceIndex += 1;
|
|
77366
|
+
}
|
|
77367
|
+
return makeInstanceId(fullPath, occurrenceIndex);
|
|
77368
|
+
};
|
|
77369
|
+
|
|
77329
77370
|
// Replace {{key}} placeholders in a title with values from the report
|
|
77330
77371
|
// parameters (e.g. "Balance ({{base_currency}})" -> "Balance (EUR)").
|
|
77331
77372
|
// Unknown keys are left as-is so missing parameters remain visible.
|
|
@@ -77352,14 +77393,17 @@ const resolveColumnOrder = (columnOrder, orderedDimensionItems, metrics) => {
|
|
|
77352
77393
|
// Normalise every candidate item to a uniform { kind, key, data } shape
|
|
77353
77394
|
// up front, so callers (e.g. the Column Order tab) can always read
|
|
77354
77395
|
// `.key` regardless of where the item came from.
|
|
77396
|
+
// Keyed by instance id (not fullPath) so that two instances of the same
|
|
77397
|
+
// dimension/metric - added deliberately as duplicates - are treated as
|
|
77398
|
+
// distinct columns rather than collapsing into one.
|
|
77355
77399
|
const normalizedDimensionItems = orderedDimensionItems.map(item => ({
|
|
77356
77400
|
kind: item.kind,
|
|
77357
|
-
key: item.kind === 'computed' ? item.data.name : item.data.
|
|
77401
|
+
key: item.kind === 'computed' ? item.data.name : item.data.id,
|
|
77358
77402
|
data: item.data
|
|
77359
77403
|
}));
|
|
77360
77404
|
const normalizedMetricItems = metrics.map(m => ({
|
|
77361
77405
|
kind: 'metric',
|
|
77362
|
-
key: m.
|
|
77406
|
+
key: m.id,
|
|
77363
77407
|
data: m
|
|
77364
77408
|
}));
|
|
77365
77409
|
const byKey = Object.fromEntries([...normalizedDimensionItems, ...normalizedMetricItems].map(item => [`${item.kind}:${item.key}`, item]));
|
|
@@ -77393,7 +77437,7 @@ const SortableChip$1 = ({
|
|
|
77393
77437
|
isLast,
|
|
77394
77438
|
sortOrder,
|
|
77395
77439
|
onSortOrderChange,
|
|
77396
|
-
|
|
77440
|
+
instanceId,
|
|
77397
77441
|
defaultTitle,
|
|
77398
77442
|
customTitle,
|
|
77399
77443
|
onUpdateTitle,
|
|
@@ -77476,7 +77520,7 @@ const SortableChip$1 = ({
|
|
|
77476
77520
|
handleCancel();
|
|
77477
77521
|
return;
|
|
77478
77522
|
}
|
|
77479
|
-
onUpdateTitle(
|
|
77523
|
+
onUpdateTitle(instanceId, trimmedValue);
|
|
77480
77524
|
setIsEditing(false);
|
|
77481
77525
|
};
|
|
77482
77526
|
const handleCancel = () => {
|
|
@@ -77484,7 +77528,7 @@ const SortableChip$1 = ({
|
|
|
77484
77528
|
setEditValue("");
|
|
77485
77529
|
};
|
|
77486
77530
|
const handleReset = () => {
|
|
77487
|
-
onResetTitle(
|
|
77531
|
+
onResetTitle(instanceId);
|
|
77488
77532
|
setIsEditing(false);
|
|
77489
77533
|
setEditValue("");
|
|
77490
77534
|
};
|
|
@@ -77695,7 +77739,7 @@ const SortableComputedChip = ({
|
|
|
77695
77739
|
const handleSave = () => {
|
|
77696
77740
|
const trimmedName = editName.trim();
|
|
77697
77741
|
const trimmedValue = editValue.trim();
|
|
77698
|
-
if (!trimmedName
|
|
77742
|
+
if (!trimmedName) return;
|
|
77699
77743
|
if (isNameTaken(trimmedName, name)) return;
|
|
77700
77744
|
onUpdate(name, {
|
|
77701
77745
|
name: trimmedName,
|
|
@@ -77845,7 +77889,7 @@ const SortableComputedChip = ({
|
|
|
77845
77889
|
onClick: handleSave,
|
|
77846
77890
|
color: "primary",
|
|
77847
77891
|
"aria-label": "save computed dimension",
|
|
77848
|
-
disabled: !editName.trim() ||
|
|
77892
|
+
disabled: !editName.trim() || nameError
|
|
77849
77893
|
}, /*#__PURE__*/gn.createElement(CheckIcon, {
|
|
77850
77894
|
fontSize: "small"
|
|
77851
77895
|
})))), /*#__PURE__*/gn.createElement(Tooltip, {
|
|
@@ -77894,12 +77938,14 @@ const Dimensions = ({
|
|
|
77894
77938
|
// Resolve the unified dimensionOrder into the actual dimension / computed
|
|
77895
77939
|
// dimension objects, so dimensions and computed dimensions can be shown
|
|
77896
77940
|
// (and dragged) as a single list while staying stored in their own arrays.
|
|
77897
|
-
|
|
77941
|
+
// Keyed by instance id rather than fullPath, so two instances of the same
|
|
77942
|
+
// dimension (added deliberately as a duplicate) stay distinct entries.
|
|
77943
|
+
const dimensionById = Object.fromEntries(savedDimensions.map(d => [d.id, d]));
|
|
77898
77944
|
const computedDimensionByName = Object.fromEntries(savedComputedDimensions.map(cd => [cd.name, cd]));
|
|
77899
77945
|
const orderedItems = dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
77900
77946
|
kind: "dimension",
|
|
77901
77947
|
key: entry.key,
|
|
77902
|
-
data:
|
|
77948
|
+
data: dimensionById[entry.key]
|
|
77903
77949
|
} : {
|
|
77904
77950
|
kind: "computed",
|
|
77905
77951
|
key: entry.key,
|
|
@@ -77913,9 +77959,13 @@ const Dimensions = ({
|
|
|
77913
77959
|
handleMoveDown
|
|
77914
77960
|
} = makeListReorderHandlers(dimensionOrder, onReorderDimensionItems);
|
|
77915
77961
|
|
|
77916
|
-
// Handle sort order change (dimensions only - computed dimensions have no
|
|
77917
|
-
|
|
77918
|
-
|
|
77962
|
+
// Handle sort order change (dimensions only - computed dimensions have no
|
|
77963
|
+
// order_by). Keyed by instance id but applied query-wide: SQL can only
|
|
77964
|
+
// sort by a given column once, so toggling sort on any duplicate instance
|
|
77965
|
+
// of the same field is propagated to all instances sharing its fullPath
|
|
77966
|
+
// (handled in ReportBuilder's onUpdateDimensionSortOrder).
|
|
77967
|
+
const handleSortOrderChange = (id, sortOrder) => {
|
|
77968
|
+
onUpdateDimensionSortOrder(id, sortOrder);
|
|
77919
77969
|
};
|
|
77920
77970
|
|
|
77921
77971
|
// Computed Dimensions: { name, value } pairs independent of any provider.
|
|
@@ -77936,7 +77986,7 @@ const Dimensions = ({
|
|
|
77936
77986
|
const handleSaveComputed = () => {
|
|
77937
77987
|
const trimmedName = computedName.trim();
|
|
77938
77988
|
const trimmedValue = computedValue.trim();
|
|
77939
|
-
if (!trimmedName
|
|
77989
|
+
if (!trimmedName) return;
|
|
77940
77990
|
if (isComputedNameTaken(trimmedName)) return;
|
|
77941
77991
|
onSaveComputedDimension({
|
|
77942
77992
|
name: trimmedName,
|
|
@@ -77978,7 +78028,10 @@ const Dimensions = ({
|
|
|
77978
78028
|
currentProviderKey = selection.targetKey;
|
|
77979
78029
|
});
|
|
77980
78030
|
|
|
77981
|
-
// Create a Set of already selected dimension fullPaths for quick lookup
|
|
78031
|
+
// Create a Set of already selected dimension fullPaths for quick lookup.
|
|
78032
|
+
// Already-selected items stay selectable (so the same dimension can be
|
|
78033
|
+
// added again on purpose) - they're just flagged so the dropdown can
|
|
78034
|
+
// highlight them instead of blocking the click.
|
|
77982
78035
|
const selectedFullPaths = new Set(savedDimensions.map(dim => dim.fullPath));
|
|
77983
78036
|
const items = [];
|
|
77984
78037
|
// Iterate through aliases (e.g., 'ba', 'pc')
|
|
@@ -77990,9 +78043,6 @@ const Dimensions = ({
|
|
|
77990
78043
|
|
|
77991
78044
|
// Construct the fullPath for this dimension
|
|
77992
78045
|
const fullPath = `${aliasPath.join("_")}.${dimKey}`;
|
|
77993
|
-
|
|
77994
|
-
// Check if this dimension is already selected
|
|
77995
|
-
const isAlreadySelected = selectedFullPaths.has(fullPath);
|
|
77996
78046
|
items.push({
|
|
77997
78047
|
// Include provider name to ensure uniqueness across different providers
|
|
77998
78048
|
key: `${currentProvider}_${alias}.${dimKey}`,
|
|
@@ -78000,7 +78050,7 @@ const Dimensions = ({
|
|
|
78000
78050
|
dimensionKey: dimKey,
|
|
78001
78051
|
alias: alias,
|
|
78002
78052
|
dimension: dimension,
|
|
78003
|
-
|
|
78053
|
+
alreadySelected: selectedFullPaths.has(fullPath)
|
|
78004
78054
|
});
|
|
78005
78055
|
});
|
|
78006
78056
|
});
|
|
@@ -78062,7 +78112,10 @@ const Dimensions = ({
|
|
|
78062
78112
|
dimensionKey: selectedItem.dimensionKey,
|
|
78063
78113
|
dimensionTitle: selectedItem.value,
|
|
78064
78114
|
// The constructed path for server
|
|
78065
|
-
fullPath: fullPath
|
|
78115
|
+
fullPath: fullPath,
|
|
78116
|
+
// Unique per-instance identity, so this field can be added more than
|
|
78117
|
+
// once and each occurrence stays independently orderable/removable.
|
|
78118
|
+
id: nextInstanceId(fullPath, savedDimensions)
|
|
78066
78119
|
};
|
|
78067
78120
|
onSaveDimension(dimensionData);
|
|
78068
78121
|
handleCancel();
|
|
@@ -78073,8 +78126,9 @@ const Dimensions = ({
|
|
|
78073
78126
|
const handleAddAll = () => {
|
|
78074
78127
|
const dimensionItems = getDimensionItems();
|
|
78075
78128
|
|
|
78076
|
-
// Filter out already selected dimensions
|
|
78077
|
-
|
|
78129
|
+
// Filter out already selected dimensions - "Add all" only fills in
|
|
78130
|
+
// missing ones; intentional duplicates are added one at a time above.
|
|
78131
|
+
const availableItems = dimensionItems.filter(item => !item.alreadySelected);
|
|
78078
78132
|
if (availableItems.length === 0) return;
|
|
78079
78133
|
|
|
78080
78134
|
// Build the complete relation objects array (same for all dimensions in this provider)
|
|
@@ -78119,7 +78173,10 @@ const Dimensions = ({
|
|
|
78119
78173
|
dimensionKey: item.dimensionKey,
|
|
78120
78174
|
dimensionTitle: item.value,
|
|
78121
78175
|
// The constructed path for server
|
|
78122
|
-
fullPath: fullPath
|
|
78176
|
+
fullPath: fullPath,
|
|
78177
|
+
// "Add all" only ever adds fields that aren't already selected, so
|
|
78178
|
+
// this is always the first (and only) occurrence of this fullPath.
|
|
78179
|
+
id: fullPath
|
|
78123
78180
|
};
|
|
78124
78181
|
onSaveDimension(dimensionData);
|
|
78125
78182
|
});
|
|
@@ -78253,7 +78310,7 @@ const Dimensions = ({
|
|
|
78253
78310
|
}, "Cancel"), /*#__PURE__*/gn.createElement(Button, {
|
|
78254
78311
|
variant: "contained",
|
|
78255
78312
|
onClick: handleSaveComputed,
|
|
78256
|
-
disabled: !computedName.trim() ||
|
|
78313
|
+
disabled: !computedName.trim() || computedNameError,
|
|
78257
78314
|
sx: {
|
|
78258
78315
|
height: "40px",
|
|
78259
78316
|
fontFamily: "system-ui",
|
|
@@ -78329,7 +78386,7 @@ const Dimensions = ({
|
|
|
78329
78386
|
placement: "top"
|
|
78330
78387
|
}, /*#__PURE__*/gn.createElement("span", null, /*#__PURE__*/gn.createElement(IconButton, {
|
|
78331
78388
|
onClick: handleAddAll,
|
|
78332
|
-
disabled: getDimensionItems().filter(item => !item.
|
|
78389
|
+
disabled: getDimensionItems().filter(item => !item.alreadySelected).length === 0,
|
|
78333
78390
|
"aria-label": "add all dimensions",
|
|
78334
78391
|
sx: {
|
|
78335
78392
|
color: "rgb(70, 134, 128)",
|
|
@@ -78420,9 +78477,9 @@ const Dimensions = ({
|
|
|
78420
78477
|
isLast: index === orderedItems.length - 1,
|
|
78421
78478
|
sortOrder: item.data.sortOrder || null,
|
|
78422
78479
|
onSortOrderChange: sortOrder => handleSortOrderChange(item.key, sortOrder),
|
|
78423
|
-
|
|
78480
|
+
instanceId: item.data.id,
|
|
78424
78481
|
defaultTitle: item.data.dimensionTitle,
|
|
78425
|
-
customTitle: titleOverrides[item.data.
|
|
78482
|
+
customTitle: titleOverrides[item.data.id],
|
|
78426
78483
|
onUpdateTitle: onUpdateTitle,
|
|
78427
78484
|
onResetTitle: onResetTitle
|
|
78428
78485
|
}) : /*#__PURE__*/gn.createElement(SortableComputedChip, {
|
|
@@ -78507,7 +78564,7 @@ const SortableChip = ({
|
|
|
78507
78564
|
onMoveDown,
|
|
78508
78565
|
isFirst,
|
|
78509
78566
|
isLast,
|
|
78510
|
-
|
|
78567
|
+
instanceId,
|
|
78511
78568
|
defaultTitle,
|
|
78512
78569
|
customTitle,
|
|
78513
78570
|
onUpdateTitle,
|
|
@@ -78560,7 +78617,7 @@ const SortableChip = ({
|
|
|
78560
78617
|
handleCancel();
|
|
78561
78618
|
return;
|
|
78562
78619
|
}
|
|
78563
|
-
onUpdateTitle(
|
|
78620
|
+
onUpdateTitle(instanceId, trimmedValue);
|
|
78564
78621
|
setIsEditing(false);
|
|
78565
78622
|
};
|
|
78566
78623
|
const handleCancel = () => {
|
|
@@ -78568,7 +78625,7 @@ const SortableChip = ({
|
|
|
78568
78625
|
setEditValue('');
|
|
78569
78626
|
};
|
|
78570
78627
|
const handleReset = () => {
|
|
78571
|
-
onResetTitle(
|
|
78628
|
+
onResetTitle(instanceId);
|
|
78572
78629
|
setIsEditing(false);
|
|
78573
78630
|
setEditValue('');
|
|
78574
78631
|
};
|
|
@@ -78789,7 +78846,10 @@ const Metrics = ({
|
|
|
78789
78846
|
currentProviderKey = selection.targetKey;
|
|
78790
78847
|
});
|
|
78791
78848
|
|
|
78792
|
-
// Create a Set of already selected metric fullPaths for quick lookup
|
|
78849
|
+
// Create a Set of already selected metric fullPaths for quick lookup.
|
|
78850
|
+
// Already-selected items stay selectable (so the same metric can be
|
|
78851
|
+
// added again on purpose) - they're just flagged so the dropdown can
|
|
78852
|
+
// highlight them instead of blocking the click.
|
|
78793
78853
|
const selectedFullPaths = new Set(savedMetrics.map(metric => metric.fullPath));
|
|
78794
78854
|
|
|
78795
78855
|
// Metrics are a direct array, not nested by alias
|
|
@@ -78797,15 +78857,12 @@ const Metrics = ({
|
|
|
78797
78857
|
const items = provider.metrics.map((metric, index) => {
|
|
78798
78858
|
// Construct the fullPath for this metric
|
|
78799
78859
|
const fullPath = `${aliasPath.join('_')}.${metric.name}`;
|
|
78800
|
-
|
|
78801
|
-
// Check if this metric is already selected
|
|
78802
|
-
const isAlreadySelected = selectedFullPaths.has(fullPath);
|
|
78803
78860
|
return {
|
|
78804
78861
|
key: `${currentProvider}_${metric.name}_${index}`,
|
|
78805
78862
|
value: metric.title || metric.name,
|
|
78806
78863
|
metricName: metric.name,
|
|
78807
78864
|
metric: metric,
|
|
78808
|
-
|
|
78865
|
+
alreadySelected: selectedFullPaths.has(fullPath),
|
|
78809
78866
|
icon: metric.source ? /*#__PURE__*/gn.createElement(MetricSourceIcon, {
|
|
78810
78867
|
source: metric.source
|
|
78811
78868
|
}) : undefined
|
|
@@ -78868,7 +78925,11 @@ const Metrics = ({
|
|
|
78868
78925
|
metricName: selectedItem.metricName,
|
|
78869
78926
|
metricTitle: selectedItem.value,
|
|
78870
78927
|
// The constructed path for server
|
|
78871
|
-
fullPath: fullPath
|
|
78928
|
+
fullPath: fullPath,
|
|
78929
|
+
// Unique per-instance identity, so this field can be added more
|
|
78930
|
+
// than once and each occurrence stays independently
|
|
78931
|
+
// orderable/removable/titleable.
|
|
78932
|
+
id: nextInstanceId(fullPath, savedMetrics)
|
|
78872
78933
|
};
|
|
78873
78934
|
onSaveMetric(metricData);
|
|
78874
78935
|
handleCancel();
|
|
@@ -78879,8 +78940,9 @@ const Metrics = ({
|
|
|
78879
78940
|
const handleAddAll = () => {
|
|
78880
78941
|
const metricItems = getMetricItems();
|
|
78881
78942
|
|
|
78882
|
-
// Filter out already selected metrics
|
|
78883
|
-
|
|
78943
|
+
// Filter out already selected metrics - "Add all" only fills in
|
|
78944
|
+
// missing ones; intentional duplicates are added one at a time above.
|
|
78945
|
+
const availableItems = metricItems.filter(item => !item.alreadySelected);
|
|
78884
78946
|
if (availableItems.length === 0) return;
|
|
78885
78947
|
|
|
78886
78948
|
// Build the complete relation objects array (same for all metrics in this provider)
|
|
@@ -78925,7 +78987,10 @@ const Metrics = ({
|
|
|
78925
78987
|
metricName: item.metricName,
|
|
78926
78988
|
metricTitle: item.value,
|
|
78927
78989
|
// The constructed path for server
|
|
78928
|
-
fullPath: fullPath
|
|
78990
|
+
fullPath: fullPath,
|
|
78991
|
+
// "Add all" only ever adds fields that aren't already
|
|
78992
|
+
// selected, so this is always the first occurrence.
|
|
78993
|
+
id: fullPath
|
|
78929
78994
|
};
|
|
78930
78995
|
onSaveMetric(metricData);
|
|
78931
78996
|
});
|
|
@@ -79024,7 +79089,7 @@ const Metrics = ({
|
|
|
79024
79089
|
placement: "top"
|
|
79025
79090
|
}, /*#__PURE__*/gn.createElement("span", null, /*#__PURE__*/gn.createElement(IconButton, {
|
|
79026
79091
|
onClick: handleAddAll,
|
|
79027
|
-
disabled: getMetricItems().filter(item => !item.
|
|
79092
|
+
disabled: getMetricItems().filter(item => !item.alreadySelected).length === 0,
|
|
79028
79093
|
"aria-label": "add all metrics",
|
|
79029
79094
|
sx: {
|
|
79030
79095
|
color: "rgb(70, 134, 128)",
|
|
@@ -79102,14 +79167,14 @@ const Metrics = ({
|
|
|
79102
79167
|
id: index,
|
|
79103
79168
|
label: metric.metricTitle,
|
|
79104
79169
|
fullLabel: `${formatProviderPath(metric)} → ${metric.metricTitle} (${metric.fullPath})`,
|
|
79105
|
-
onDelete: () => onRemoveMetric(
|
|
79170
|
+
onDelete: () => onRemoveMetric(metric.id),
|
|
79106
79171
|
onMoveUp: () => handleMoveUp(index),
|
|
79107
79172
|
onMoveDown: () => handleMoveDown(index),
|
|
79108
79173
|
isFirst: index === 0,
|
|
79109
79174
|
isLast: index === savedMetrics.length - 1,
|
|
79110
|
-
|
|
79175
|
+
instanceId: metric.id,
|
|
79111
79176
|
defaultTitle: metric.metricTitle,
|
|
79112
|
-
customTitle: titleOverrides[metric.
|
|
79177
|
+
customTitle: titleOverrides[metric.id],
|
|
79113
79178
|
onUpdateTitle: onUpdateTitle,
|
|
79114
79179
|
onResetTitle: onResetTitle,
|
|
79115
79180
|
source: metric.metric?.source
|
|
@@ -91282,7 +91347,12 @@ const Filters = ({
|
|
|
91282
91347
|
mb: 3
|
|
91283
91348
|
}
|
|
91284
91349
|
}, /*#__PURE__*/gn.createElement(SingleSelect, {
|
|
91285
|
-
items:
|
|
91350
|
+
items: Array.from(
|
|
91351
|
+
// Dimensions can now be added more than once (same fullPath,
|
|
91352
|
+
// different instance), but a filter targets the underlying
|
|
91353
|
+
// field regardless of how many columns display it - so this
|
|
91354
|
+
// list is deduped by fullPath to avoid showing it twice.
|
|
91355
|
+
new Map(existingDimensions.map(dim => [dim.fullPath, dim])).values()).map(dim => ({
|
|
91286
91356
|
key: dim.fullPath,
|
|
91287
91357
|
value: interpolateTitle(dimensionTitleOverrides[dim.fullPath] || dim.dimensionTitle, parameters)
|
|
91288
91358
|
})).sort((a, b) => a.value.localeCompare(b.value)),
|
|
@@ -91869,7 +91939,7 @@ const ColumnOrder = ({
|
|
|
91869
91939
|
key: `${item.kind}-${item.key}`,
|
|
91870
91940
|
id: index,
|
|
91871
91941
|
label: labelFor(item),
|
|
91872
|
-
fullPath: item.kind === "computed" ? item.data.value : item.
|
|
91942
|
+
fullPath: item.kind === "computed" ? item.data.value : item.data.fullPath,
|
|
91873
91943
|
kind: item.kind,
|
|
91874
91944
|
source: item.kind === "metric" ? item.data.metric?.source : undefined,
|
|
91875
91945
|
onMoveUp: () => handleMoveUp(index),
|
|
@@ -91895,11 +91965,17 @@ const formatValue = (value, def) => {
|
|
|
91895
91965
|
const isNumericType = type => type === 'integer' || type === 'currency';
|
|
91896
91966
|
|
|
91897
91967
|
// Builds a single DataGrid column definition for one resolved column item
|
|
91898
|
-
// (a dimension, computed dimension, or metric).
|
|
91968
|
+
// (a dimension, computed dimension, or metric). The DataGrid `field` is the
|
|
91969
|
+
// item's instance id (so two duplicate instances of the same underlying
|
|
91970
|
+
// dimension/metric still get distinct columns); the actual cell value is
|
|
91971
|
+
// read via `valueGetter` from `dataFieldName`, the key the API response
|
|
91972
|
+
// actually uses - shared by every duplicate instance, since the backend
|
|
91973
|
+
// only ever returns one value per fullPath. dataFieldName rules:
|
|
91899
91974
|
// - Dimensions from base provider: baseAlias.field -> baseAlias_field
|
|
91900
91975
|
// - Dimensions from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
91901
91976
|
// - Computed dimensions have no provider/relations chain, so the API returns
|
|
91902
|
-
// each row keyed by the computed dimension's own `name
|
|
91977
|
+
// each row keyed by the computed dimension's own `name` (and can't be
|
|
91978
|
+
// duplicated - names must be unique - so field/dataFieldName coincide).
|
|
91903
91979
|
// - Metrics: API returns metric keys in different forms depending on path depth
|
|
91904
91980
|
// (see comment inline below), normalised to match the row-key normalisation
|
|
91905
91981
|
// done when building `rows`.
|
|
@@ -91917,13 +91993,13 @@ const buildColumn = (item, titleOverrides, parameters) => {
|
|
|
91917
91993
|
if (item.kind === 'metric') {
|
|
91918
91994
|
const metric = item.data;
|
|
91919
91995
|
const metricDef = metric.metric;
|
|
91920
|
-
let
|
|
91996
|
+
let dataFieldName;
|
|
91921
91997
|
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
91922
91998
|
if (dotCount >= 2) {
|
|
91923
91999
|
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
91924
92000
|
// Normalise dots to underscores to match the row key normalisation below.
|
|
91925
92001
|
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
91926
|
-
|
|
92002
|
+
dataFieldName = metric.fullPath.replace(/\./g, '_');
|
|
91927
92003
|
} else if (metric.relations && metric.relations.length > 0) {
|
|
91928
92004
|
// 2-part path from a nested provider: API drops the base provider alias.
|
|
91929
92005
|
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
@@ -91934,19 +92010,24 @@ const buildColumn = (item, titleOverrides, parameters) => {
|
|
|
91934
92010
|
const pathParts = pathWithoutField.split('_');
|
|
91935
92011
|
pathParts.shift(); // remove base provider alias
|
|
91936
92012
|
const pathWithoutBase = pathParts.join('_');
|
|
91937
|
-
|
|
92013
|
+
dataFieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
91938
92014
|
} else {
|
|
91939
92015
|
// 2-part path from the base provider: API returns just the metric name.
|
|
91940
92016
|
// Example: "mc.record_count" -> "record_count"
|
|
91941
|
-
|
|
92017
|
+
dataFieldName = metric.metricName;
|
|
91942
92018
|
}
|
|
91943
|
-
const headerName = interpolateTitle(titleOverrides.metrics[metric.
|
|
92019
|
+
const headerName = interpolateTitle(titleOverrides.metrics[metric.id] || metric.metricTitle || dataFieldName, parameters);
|
|
91944
92020
|
return {
|
|
91945
|
-
field
|
|
92021
|
+
// `field` is the instance id (unique even for duplicate columns
|
|
92022
|
+
// pointing at the same underlying metric); the actual value is
|
|
92023
|
+
// read from the row via valueGetter using dataFieldName, which is
|
|
92024
|
+
// the key the API response actually uses (shared across duplicates).
|
|
92025
|
+
field: metric.id.replace(/[.#]/g, '_'),
|
|
91946
92026
|
headerName: headerName,
|
|
91947
92027
|
flex: 1,
|
|
91948
92028
|
minWidth: 150,
|
|
91949
92029
|
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
92030
|
+
valueGetter: (_value, row) => row[dataFieldName],
|
|
91950
92031
|
renderHeader: () => /*#__PURE__*/gn.createElement(Box, {
|
|
91951
92032
|
sx: {
|
|
91952
92033
|
display: 'flex',
|
|
@@ -91962,7 +92043,7 @@ const buildColumn = (item, titleOverrides, parameters) => {
|
|
|
91962
92043
|
|
|
91963
92044
|
// Dimension
|
|
91964
92045
|
const dim = item.data;
|
|
91965
|
-
let
|
|
92046
|
+
let dataFieldName;
|
|
91966
92047
|
if (dim.relations && dim.relations.length > 0) {
|
|
91967
92048
|
// From nested provider: drop the base provider alias
|
|
91968
92049
|
// Example: ft_fa_db.currency -> fa_db_currency
|
|
@@ -91974,20 +92055,24 @@ const buildColumn = (item, titleOverrides, parameters) => {
|
|
|
91974
92055
|
pathParts.shift(); // Remove base provider alias
|
|
91975
92056
|
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
91976
92057
|
|
|
91977
|
-
|
|
92058
|
+
dataFieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
91978
92059
|
} else {
|
|
91979
92060
|
// From base provider: keep the full path with underscore
|
|
91980
92061
|
// Example: ba.created_at -> ba_created_at
|
|
91981
|
-
|
|
92062
|
+
dataFieldName = dim.fullPath.replace('.', '_');
|
|
91982
92063
|
}
|
|
91983
|
-
const headerName = interpolateTitle(titleOverrides.dimensions[dim.
|
|
92064
|
+
const headerName = interpolateTitle(titleOverrides.dimensions[dim.id] || dim.dimensionTitle || dataFieldName, parameters);
|
|
91984
92065
|
const dimDef = dim.dimension;
|
|
91985
92066
|
return {
|
|
91986
|
-
|
|
92067
|
+
// Same rationale as the metric branch above: `field` must be unique
|
|
92068
|
+
// per instance, while the value always comes from the one real
|
|
92069
|
+
// underlying row key (dataFieldName), shared by every duplicate.
|
|
92070
|
+
field: dim.id.replace(/[.#]/g, '_'),
|
|
91987
92071
|
headerName: headerName,
|
|
91988
92072
|
flex: 1,
|
|
91989
92073
|
minWidth: 150,
|
|
91990
92074
|
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
92075
|
+
valueGetter: (_value, row) => row[dataFieldName],
|
|
91991
92076
|
valueFormatter: value => formatValue(value, dimDef)
|
|
91992
92077
|
};
|
|
91993
92078
|
};
|
|
@@ -92313,6 +92398,72 @@ const ReportBuilder = ({
|
|
|
92313
92398
|
}
|
|
92314
92399
|
};
|
|
92315
92400
|
|
|
92401
|
+
// Reconstructs saved dimensions as independent, individually-identified
|
|
92402
|
+
// instances so that deliberate duplicates (the same field added more than
|
|
92403
|
+
// once) survive a save/reload round trip. Prefers `ordered_dimensions`
|
|
92404
|
+
// (one ref per instance - the nth duplicate of a path is suffixed
|
|
92405
|
+
// `#n`) when present; falls back to one instance per entry in the
|
|
92406
|
+
// (necessarily duplicate-free) `dimensions` path list for reports saved
|
|
92407
|
+
// before this field existed.
|
|
92408
|
+
const reconstructDimensions = (reportDef, providersData) => {
|
|
92409
|
+
const orderByMap = {};
|
|
92410
|
+
if (reportDef.definition?.doc?.query?.order_by) {
|
|
92411
|
+
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
92412
|
+
orderByMap[orderItem.name] = orderItem.desc === true ? "desc" : "asc";
|
|
92413
|
+
}
|
|
92414
|
+
}
|
|
92415
|
+
const reconstructed = [];
|
|
92416
|
+
const orderedRefs = (reportDef.definition?.doc?.query?.ordered_dimensions || []).filter(entry => entry.type === "dimension").map(entry => entry.ref);
|
|
92417
|
+
if (orderedRefs.length > 0) {
|
|
92418
|
+
orderedRefs.forEach(ref => {
|
|
92419
|
+
const fullPath = ref.split("#")[0];
|
|
92420
|
+
const dim = reconstructDimensionFromPath(fullPath, providersData, reportDef.provider);
|
|
92421
|
+
if (dim) {
|
|
92422
|
+
dim.id = ref;
|
|
92423
|
+
dim.sortOrder = orderByMap[fullPath] || null;
|
|
92424
|
+
reconstructed.push(dim);
|
|
92425
|
+
}
|
|
92426
|
+
});
|
|
92427
|
+
} else if (reportDef.definition?.doc?.query?.dimensions) {
|
|
92428
|
+
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
92429
|
+
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
92430
|
+
if (dim) {
|
|
92431
|
+
dim.id = dimPath;
|
|
92432
|
+
dim.sortOrder = orderByMap[dimPath] || null;
|
|
92433
|
+
reconstructed.push(dim);
|
|
92434
|
+
}
|
|
92435
|
+
}
|
|
92436
|
+
}
|
|
92437
|
+
return reconstructed;
|
|
92438
|
+
};
|
|
92439
|
+
|
|
92440
|
+
// Mirrors reconstructDimensions for metrics, using the analogous
|
|
92441
|
+
// `ordered_metrics` field (metrics have no other unified-order concept to
|
|
92442
|
+
// fall back on, so this is always written whenever there are metrics).
|
|
92443
|
+
const reconstructMetrics = (reportDef, providersData) => {
|
|
92444
|
+
const reconstructed = [];
|
|
92445
|
+
const orderedRefs = reportDef.definition?.doc?.query?.ordered_metrics || [];
|
|
92446
|
+
if (orderedRefs.length > 0) {
|
|
92447
|
+
orderedRefs.forEach(ref => {
|
|
92448
|
+
const fullPath = ref.split("#")[0];
|
|
92449
|
+
const metric = reconstructMetricFromPath(fullPath, providersData, reportDef.provider);
|
|
92450
|
+
if (metric) {
|
|
92451
|
+
metric.id = ref;
|
|
92452
|
+
reconstructed.push(metric);
|
|
92453
|
+
}
|
|
92454
|
+
});
|
|
92455
|
+
} else if (reportDef.definition?.doc?.query?.metrics) {
|
|
92456
|
+
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
92457
|
+
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
92458
|
+
if (metric) {
|
|
92459
|
+
metric.id = metricPath;
|
|
92460
|
+
reconstructed.push(metric);
|
|
92461
|
+
}
|
|
92462
|
+
}
|
|
92463
|
+
}
|
|
92464
|
+
return reconstructed;
|
|
92465
|
+
};
|
|
92466
|
+
|
|
92316
92467
|
// Build the unified display/execution order for dimensions + computed
|
|
92317
92468
|
// dimensions. Prefers the persisted `ordered_dimensions` field; falls back
|
|
92318
92469
|
// to "dimensions first, then computed dimensions" for report definitions
|
|
@@ -92323,7 +92474,7 @@ const ReportBuilder = ({
|
|
|
92323
92474
|
const seenComputedKeys = new Set();
|
|
92324
92475
|
if (Array.isArray(rawOrderedDimensions)) {
|
|
92325
92476
|
rawOrderedDimensions.forEach(entry => {
|
|
92326
|
-
if (entry.type === "dimension" && dimensions.some(d => d.
|
|
92477
|
+
if (entry.type === "dimension" && dimensions.some(d => d.id === entry.ref)) {
|
|
92327
92478
|
dimensionOrder.push({
|
|
92328
92479
|
type: "dimension",
|
|
92329
92480
|
key: entry.ref
|
|
@@ -92341,10 +92492,10 @@ const ReportBuilder = ({
|
|
|
92341
92492
|
|
|
92342
92493
|
// Append anything not covered by ordered_dimensions (older reports, or drift)
|
|
92343
92494
|
dimensions.forEach(d => {
|
|
92344
|
-
if (!seenDimensionKeys.has(d.
|
|
92495
|
+
if (!seenDimensionKeys.has(d.id)) {
|
|
92345
92496
|
dimensionOrder.push({
|
|
92346
92497
|
type: "dimension",
|
|
92347
|
-
key: d.
|
|
92498
|
+
key: d.id
|
|
92348
92499
|
});
|
|
92349
92500
|
}
|
|
92350
92501
|
});
|
|
@@ -92367,12 +92518,12 @@ const ReportBuilder = ({
|
|
|
92367
92518
|
// "fall back to the default order".
|
|
92368
92519
|
const buildColumnOrder = (rawOrderedColumns, dimensions, computedDimensions, metrics) => {
|
|
92369
92520
|
if (!Array.isArray(rawOrderedColumns)) return [];
|
|
92370
|
-
const
|
|
92521
|
+
const dimensionIds = new Set(dimensions.map(d => d.id));
|
|
92371
92522
|
const computedNames = new Set(computedDimensions.map(cd => cd.name));
|
|
92372
|
-
const
|
|
92523
|
+
const metricIds = new Set(metrics.map(m => m.id));
|
|
92373
92524
|
const columnOrder = [];
|
|
92374
92525
|
rawOrderedColumns.forEach(entry => {
|
|
92375
|
-
if (entry.type === "dimension" &&
|
|
92526
|
+
if (entry.type === "dimension" && dimensionIds.has(entry.ref)) {
|
|
92376
92527
|
columnOrder.push({
|
|
92377
92528
|
type: "dimension",
|
|
92378
92529
|
key: entry.ref
|
|
@@ -92382,7 +92533,7 @@ const ReportBuilder = ({
|
|
|
92382
92533
|
type: "computed",
|
|
92383
92534
|
key: entry.ref
|
|
92384
92535
|
});
|
|
92385
|
-
} else if (entry.type === "metric" &&
|
|
92536
|
+
} else if (entry.type === "metric" && metricIds.has(entry.ref)) {
|
|
92386
92537
|
columnOrder.push({
|
|
92387
92538
|
type: "metric",
|
|
92388
92539
|
key: entry.ref
|
|
@@ -92412,43 +92563,10 @@ const ReportBuilder = ({
|
|
|
92412
92563
|
// Set the title
|
|
92413
92564
|
setReportTitle(reportDef.title || "");
|
|
92414
92565
|
|
|
92415
|
-
// Reconstruct dimensions
|
|
92416
|
-
|
|
92417
|
-
const
|
|
92418
|
-
|
|
92419
|
-
// First, build a map of fullPath -> sortOrder from order_by
|
|
92420
|
-
if (reportDef.definition?.doc?.query?.order_by) {
|
|
92421
|
-
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
92422
|
-
if (orderItem.desc === true) {
|
|
92423
|
-
orderByMap[orderItem.name] = "desc";
|
|
92424
|
-
} else {
|
|
92425
|
-
orderByMap[orderItem.name] = "asc";
|
|
92426
|
-
}
|
|
92427
|
-
}
|
|
92428
|
-
}
|
|
92429
|
-
|
|
92430
|
-
// Now reconstruct dimensions and add sortOrder from the map
|
|
92431
|
-
if (reportDef.definition?.doc?.query?.dimensions) {
|
|
92432
|
-
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
92433
|
-
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
92434
|
-
if (dim) {
|
|
92435
|
-
// Add sortOrder from order_by if it exists
|
|
92436
|
-
dim.sortOrder = orderByMap[dimPath] || null;
|
|
92437
|
-
reconstructedDimensions.push(dim);
|
|
92438
|
-
}
|
|
92439
|
-
}
|
|
92440
|
-
}
|
|
92441
|
-
|
|
92442
|
-
// Reconstruct metrics
|
|
92443
|
-
const reconstructedMetrics = [];
|
|
92444
|
-
if (reportDef.definition?.doc?.query?.metrics) {
|
|
92445
|
-
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
92446
|
-
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
92447
|
-
if (metric) {
|
|
92448
|
-
reconstructedMetrics.push(metric);
|
|
92449
|
-
}
|
|
92450
|
-
}
|
|
92451
|
-
}
|
|
92566
|
+
// Reconstruct dimensions and metrics as independent instances
|
|
92567
|
+
// (preserving deliberate duplicates - see reconstructDimensions/Metrics)
|
|
92568
|
+
const reconstructedDimensions = reconstructDimensions(reportDef, providersData);
|
|
92569
|
+
const reconstructedMetrics = reconstructMetrics(reportDef, providersData);
|
|
92452
92570
|
|
|
92453
92571
|
// Load title overrides if they exist
|
|
92454
92572
|
const loadedTitleOverrides = {
|
|
@@ -92532,43 +92650,10 @@ const ReportBuilder = ({
|
|
|
92532
92650
|
// Set the title (already modified with " (Copy)" suffix)
|
|
92533
92651
|
setReportTitle(reportDef.title || "");
|
|
92534
92652
|
|
|
92535
|
-
// Reconstruct dimensions
|
|
92536
|
-
|
|
92537
|
-
const
|
|
92538
|
-
|
|
92539
|
-
// First, build a map of fullPath -> sortOrder from order_by
|
|
92540
|
-
if (reportDef.definition?.doc?.query?.order_by) {
|
|
92541
|
-
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
92542
|
-
if (orderItem.desc === true) {
|
|
92543
|
-
orderByMap[orderItem.name] = "desc";
|
|
92544
|
-
} else {
|
|
92545
|
-
orderByMap[orderItem.name] = "asc";
|
|
92546
|
-
}
|
|
92547
|
-
}
|
|
92548
|
-
}
|
|
92549
|
-
|
|
92550
|
-
// Now reconstruct dimensions and add sortOrder from the map
|
|
92551
|
-
if (reportDef.definition?.doc?.query?.dimensions) {
|
|
92552
|
-
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
92553
|
-
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
92554
|
-
if (dim) {
|
|
92555
|
-
// Add sortOrder from order_by if it exists
|
|
92556
|
-
dim.sortOrder = orderByMap[dimPath] || null;
|
|
92557
|
-
reconstructedDimensions.push(dim);
|
|
92558
|
-
}
|
|
92559
|
-
}
|
|
92560
|
-
}
|
|
92561
|
-
|
|
92562
|
-
// Reconstruct metrics
|
|
92563
|
-
const reconstructedMetrics = [];
|
|
92564
|
-
if (reportDef.definition?.doc?.query?.metrics) {
|
|
92565
|
-
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
92566
|
-
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
92567
|
-
if (metric) {
|
|
92568
|
-
reconstructedMetrics.push(metric);
|
|
92569
|
-
}
|
|
92570
|
-
}
|
|
92571
|
-
}
|
|
92653
|
+
// Reconstruct dimensions and metrics as independent instances
|
|
92654
|
+
// (preserving deliberate duplicates - see reconstructDimensions/Metrics)
|
|
92655
|
+
const reconstructedDimensions = reconstructDimensions(reportDef, providersData);
|
|
92656
|
+
const reconstructedMetrics = reconstructMetrics(reportDef, providersData);
|
|
92572
92657
|
|
|
92573
92658
|
// Load title overrides if they exist
|
|
92574
92659
|
const loadedTitleOverrides = {
|
|
@@ -92667,42 +92752,45 @@ const ReportBuilder = ({
|
|
|
92667
92752
|
});
|
|
92668
92753
|
console.log("Selected root provider:", event.target.value);
|
|
92669
92754
|
};
|
|
92670
|
-
|
|
92755
|
+
|
|
92756
|
+
// Keyed by instance id, so two instances of the same dimension can carry
|
|
92757
|
+
// independent custom titles.
|
|
92758
|
+
const handleUpdateDimensionTitle = (id, customTitle) => {
|
|
92671
92759
|
setTitleOverrides(prev => ({
|
|
92672
92760
|
...prev,
|
|
92673
92761
|
dimensions: {
|
|
92674
92762
|
...prev.dimensions,
|
|
92675
|
-
[
|
|
92763
|
+
[id]: customTitle
|
|
92676
92764
|
}
|
|
92677
92765
|
}));
|
|
92678
92766
|
};
|
|
92679
|
-
const handleResetDimensionTitle =
|
|
92767
|
+
const handleResetDimensionTitle = id => {
|
|
92680
92768
|
setTitleOverrides(prev => {
|
|
92681
92769
|
const newDimensions = {
|
|
92682
92770
|
...prev.dimensions
|
|
92683
92771
|
};
|
|
92684
|
-
delete newDimensions[
|
|
92772
|
+
delete newDimensions[id];
|
|
92685
92773
|
return {
|
|
92686
92774
|
...prev,
|
|
92687
92775
|
dimensions: newDimensions
|
|
92688
92776
|
};
|
|
92689
92777
|
});
|
|
92690
92778
|
};
|
|
92691
|
-
const handleUpdateMetricTitle = (
|
|
92779
|
+
const handleUpdateMetricTitle = (id, customTitle) => {
|
|
92692
92780
|
setTitleOverrides(prev => ({
|
|
92693
92781
|
...prev,
|
|
92694
92782
|
metrics: {
|
|
92695
92783
|
...prev.metrics,
|
|
92696
|
-
[
|
|
92784
|
+
[id]: customTitle
|
|
92697
92785
|
}
|
|
92698
92786
|
}));
|
|
92699
92787
|
};
|
|
92700
|
-
const handleResetMetricTitle =
|
|
92788
|
+
const handleResetMetricTitle = id => {
|
|
92701
92789
|
setTitleOverrides(prev => {
|
|
92702
92790
|
const newMetrics = {
|
|
92703
92791
|
...prev.metrics
|
|
92704
92792
|
};
|
|
92705
|
-
delete newMetrics[
|
|
92793
|
+
delete newMetrics[id];
|
|
92706
92794
|
return {
|
|
92707
92795
|
...prev,
|
|
92708
92796
|
metrics: newMetrics
|
|
@@ -92737,11 +92825,11 @@ const ReportBuilder = ({
|
|
|
92737
92825
|
dimensions: [...prev.dimensions, dimensionData],
|
|
92738
92826
|
dimensionOrder: [...prev.dimensionOrder, {
|
|
92739
92827
|
type: "dimension",
|
|
92740
|
-
key: dimensionData.
|
|
92828
|
+
key: dimensionData.id
|
|
92741
92829
|
}],
|
|
92742
92830
|
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
92743
92831
|
type: "dimension",
|
|
92744
|
-
key: dimensionData.
|
|
92832
|
+
key: dimensionData.id
|
|
92745
92833
|
})
|
|
92746
92834
|
};
|
|
92747
92835
|
console.log("Dimension saved:", dimensionData);
|
|
@@ -92749,22 +92837,30 @@ const ReportBuilder = ({
|
|
|
92749
92837
|
return newReport;
|
|
92750
92838
|
});
|
|
92751
92839
|
};
|
|
92752
|
-
const handleRemoveDimension =
|
|
92840
|
+
const handleRemoveDimension = id => {
|
|
92753
92841
|
setReport(prev => ({
|
|
92754
92842
|
...prev,
|
|
92755
|
-
dimensions: prev.dimensions.filter(d => d.
|
|
92756
|
-
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key ===
|
|
92757
|
-
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "dimension" && entry.key ===
|
|
92843
|
+
dimensions: prev.dimensions.filter(d => d.id !== id),
|
|
92844
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === id)),
|
|
92845
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "dimension" && entry.key === id))
|
|
92758
92846
|
}));
|
|
92759
92847
|
};
|
|
92760
|
-
|
|
92761
|
-
|
|
92762
|
-
|
|
92763
|
-
|
|
92764
|
-
|
|
92765
|
-
|
|
92766
|
-
|
|
92767
|
-
|
|
92848
|
+
|
|
92849
|
+
// sortOrder is fundamentally a per-field (query-level) setting - SQL can
|
|
92850
|
+
// only sort by a given column once - so toggling it on any instance of a
|
|
92851
|
+
// duplicated dimension applies to every instance sharing that fullPath.
|
|
92852
|
+
const handleUpdateDimensionSortOrder = (id, sortOrder) => {
|
|
92853
|
+
setReport(prev => {
|
|
92854
|
+
const target = prev.dimensions.find(d => d.id === id);
|
|
92855
|
+
if (!target) return prev;
|
|
92856
|
+
return {
|
|
92857
|
+
...prev,
|
|
92858
|
+
dimensions: prev.dimensions.map(d => d.fullPath === target.fullPath ? {
|
|
92859
|
+
...d,
|
|
92860
|
+
sortOrder
|
|
92861
|
+
} : d)
|
|
92862
|
+
};
|
|
92863
|
+
});
|
|
92768
92864
|
};
|
|
92769
92865
|
|
|
92770
92866
|
// Reorders the single unified dimensions + computed-dimensions list;
|
|
@@ -92819,7 +92915,7 @@ const ReportBuilder = ({
|
|
|
92819
92915
|
metrics: [...prev.metrics, metricData],
|
|
92820
92916
|
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
92821
92917
|
type: "metric",
|
|
92822
|
-
key: metricData.
|
|
92918
|
+
key: metricData.id
|
|
92823
92919
|
})
|
|
92824
92920
|
};
|
|
92825
92921
|
console.log("Metric saved:", metricData);
|
|
@@ -92827,15 +92923,12 @@ const ReportBuilder = ({
|
|
|
92827
92923
|
return newReport;
|
|
92828
92924
|
});
|
|
92829
92925
|
};
|
|
92830
|
-
const handleRemoveMetric =
|
|
92831
|
-
setReport(prev => {
|
|
92832
|
-
|
|
92833
|
-
|
|
92834
|
-
|
|
92835
|
-
|
|
92836
|
-
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "metric" && entry.key === fullPath))
|
|
92837
|
-
};
|
|
92838
|
-
});
|
|
92926
|
+
const handleRemoveMetric = id => {
|
|
92927
|
+
setReport(prev => ({
|
|
92928
|
+
...prev,
|
|
92929
|
+
metrics: prev.metrics.filter(m => m.id !== id),
|
|
92930
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "metric" && entry.key === id))
|
|
92931
|
+
}));
|
|
92839
92932
|
};
|
|
92840
92933
|
const handleReorderMetrics = newOrder => {
|
|
92841
92934
|
setReport(prev => ({
|
|
@@ -92890,13 +92983,30 @@ const ReportBuilder = ({
|
|
|
92890
92983
|
// persisted `ordered_dimensions` field all stay consistent with whatever
|
|
92891
92984
|
// order the user arranged on screen.
|
|
92892
92985
|
const buildQueryObject = () => {
|
|
92893
|
-
const
|
|
92986
|
+
const dimensionById = Object.fromEntries(report.dimensions.map(d => [d.id, d]));
|
|
92894
92987
|
const computedByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
92895
|
-
|
|
92988
|
+
|
|
92989
|
+
// May contain more than one instance of the same fullPath (deliberate
|
|
92990
|
+
// duplicates), kept in dimensionOrder's display order.
|
|
92991
|
+
const orderedDimensionsOnly = report.dimensionOrder.filter(entry => entry.type === "dimension").map(entry => dimensionById[entry.key]).filter(Boolean);
|
|
92896
92992
|
const orderedComputedOnly = report.dimensionOrder.filter(entry => entry.type === "computed").map(entry => computedByName[entry.key]).filter(Boolean);
|
|
92897
92993
|
|
|
92994
|
+
// The API keys each result row by fullPath, so duplicate instances of
|
|
92995
|
+
// the same field can only ever carry one value/one sort direction at
|
|
92996
|
+
// the query level - dedupe by fullPath (keeping first-occurrence order)
|
|
92997
|
+
// before sending. The builder still shows/orders/titles each instance
|
|
92998
|
+
// independently; they just read from the same underlying query column.
|
|
92999
|
+
const uniqueDimensionsByPath = [];
|
|
93000
|
+
const seenDimensionPaths = new Set();
|
|
93001
|
+
orderedDimensionsOnly.forEach(dim => {
|
|
93002
|
+
if (!seenDimensionPaths.has(dim.fullPath)) {
|
|
93003
|
+
seenDimensionPaths.add(dim.fullPath);
|
|
93004
|
+
uniqueDimensionsByPath.push(dim);
|
|
93005
|
+
}
|
|
93006
|
+
});
|
|
93007
|
+
|
|
92898
93008
|
// Build order_by array - include all dimensions, add desc property only when needed
|
|
92899
|
-
const orderBy =
|
|
93009
|
+
const orderBy = uniqueDimensionsByPath.map(dim => {
|
|
92900
93010
|
const orderItem = {
|
|
92901
93011
|
name: dim.fullPath
|
|
92902
93012
|
};
|
|
@@ -92951,9 +93061,20 @@ const ReportBuilder = ({
|
|
|
92951
93061
|
if (Object.keys(titleOverrides.filters).length > 0) {
|
|
92952
93062
|
titles.filters = titleOverrides.filters;
|
|
92953
93063
|
}
|
|
93064
|
+
|
|
93065
|
+
// Same dedupe rationale as dimensions above: the query only needs each
|
|
93066
|
+
// metric fullPath once, however many display instances reference it.
|
|
93067
|
+
const uniqueMetricPaths = [];
|
|
93068
|
+
const seenMetricPaths = new Set();
|
|
93069
|
+
report.metrics.forEach(metric => {
|
|
93070
|
+
if (!seenMetricPaths.has(metric.fullPath)) {
|
|
93071
|
+
seenMetricPaths.add(metric.fullPath);
|
|
93072
|
+
uniqueMetricPaths.push(metric.fullPath);
|
|
93073
|
+
}
|
|
93074
|
+
});
|
|
92954
93075
|
const queryObj = {
|
|
92955
|
-
dimensions:
|
|
92956
|
-
metrics:
|
|
93076
|
+
dimensions: uniqueDimensionsByPath.map(dim => dim.fullPath),
|
|
93077
|
+
metrics: uniqueMetricPaths,
|
|
92957
93078
|
order_by: orderBy
|
|
92958
93079
|
};
|
|
92959
93080
|
|
|
@@ -92967,7 +93088,9 @@ const ReportBuilder = ({
|
|
|
92967
93088
|
|
|
92968
93089
|
// Persist the unified dimension + computed-dimension order so it can be
|
|
92969
93090
|
// reconstructed on reload and so executed reports render columns in the
|
|
92970
|
-
// same order they were arranged in the builder.
|
|
93091
|
+
// same order they were arranged in the builder. Entries use each
|
|
93092
|
+
// dimension's instance id as `ref`, so deliberate duplicates (the nth
|
|
93093
|
+
// occurrence of a fullPath is suffixed `#n`) round-trip correctly.
|
|
92971
93094
|
if (report.dimensionOrder.length > 0) {
|
|
92972
93095
|
queryObj.ordered_dimensions = report.dimensionOrder.map(entry => ({
|
|
92973
93096
|
type: entry.type,
|
|
@@ -92975,6 +93098,12 @@ const ReportBuilder = ({
|
|
|
92975
93098
|
}));
|
|
92976
93099
|
}
|
|
92977
93100
|
|
|
93101
|
+
// Persist metric instance order/identity the same way (metrics have no
|
|
93102
|
+
// other unified-order field to piggyback on).
|
|
93103
|
+
if (report.metrics.length > 0) {
|
|
93104
|
+
queryObj.ordered_metrics = report.metrics.map(metric => metric.id);
|
|
93105
|
+
}
|
|
93106
|
+
|
|
92978
93107
|
// Persist the optional unified column order (Column Order tab) so the
|
|
92979
93108
|
// results grid can reproduce it on reload. Absent when no custom order
|
|
92980
93109
|
// has been defined, so old/untouched reports keep their current
|
|
@@ -93144,11 +93273,11 @@ const ReportBuilder = ({
|
|
|
93144
93273
|
|
|
93145
93274
|
// Resolve the unified dimensionOrder into actual dimension/computed-dimension
|
|
93146
93275
|
// objects so the results grid can render columns in that same order.
|
|
93147
|
-
const
|
|
93276
|
+
const dimensionById = Object.fromEntries(report.dimensions.map(d => [d.id, d]));
|
|
93148
93277
|
const computedDimensionByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
93149
93278
|
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
93150
93279
|
kind: "dimension",
|
|
93151
|
-
data:
|
|
93280
|
+
data: dimensionById[entry.key]
|
|
93152
93281
|
} : {
|
|
93153
93282
|
kind: "computed",
|
|
93154
93283
|
data: computedDimensionByName[entry.key]
|