@bygd/nc-report-ui 0.1.42 → 0.1.44
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 +422 -196
- package/dist/default/cjs/index.cjs +417 -195
- package/dist/default/esm/index.js +416 -195
- package/package.json +1 -1
|
@@ -20,6 +20,7 @@ var reactIntersectionObserver = require('react-intersection-observer');
|
|
|
20
20
|
var material = require('@mui/material');
|
|
21
21
|
var CheckBoxOutlineBlankIcon = require('@mui/icons-material/CheckBoxOutlineBlank');
|
|
22
22
|
var CheckBoxIcon = require('@mui/icons-material/CheckBox');
|
|
23
|
+
var CheckCircleIcon = require('@mui/icons-material/CheckCircle');
|
|
23
24
|
var EventEmitter = require('eventemitter3');
|
|
24
25
|
var Grid = require('@mui/material/Grid');
|
|
25
26
|
var IconButton = require('@mui/material/IconButton');
|
|
@@ -105,6 +106,7 @@ var Select__default = /*#__PURE__*/_interopDefault(Select);
|
|
|
105
106
|
var MenuItem__default = /*#__PURE__*/_interopDefault(MenuItem);
|
|
106
107
|
var CheckBoxOutlineBlankIcon__default = /*#__PURE__*/_interopDefault(CheckBoxOutlineBlankIcon);
|
|
107
108
|
var CheckBoxIcon__default = /*#__PURE__*/_interopDefault(CheckBoxIcon);
|
|
109
|
+
var CheckCircleIcon__default = /*#__PURE__*/_interopDefault(CheckCircleIcon);
|
|
108
110
|
var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
|
|
109
111
|
var Grid__default = /*#__PURE__*/_interopDefault(Grid);
|
|
110
112
|
var IconButton__default = /*#__PURE__*/_interopDefault(IconButton);
|
|
@@ -1135,7 +1137,8 @@ function SingleSelect({
|
|
|
1135
1137
|
key,
|
|
1136
1138
|
value,
|
|
1137
1139
|
disabled,
|
|
1138
|
-
icon
|
|
1140
|
+
icon,
|
|
1141
|
+
alreadySelected
|
|
1139
1142
|
} = itm;
|
|
1140
1143
|
return /*#__PURE__*/React__namespace.default.createElement(MenuItem__default.default, {
|
|
1141
1144
|
key: key,
|
|
@@ -1146,7 +1149,12 @@ function SingleSelect({
|
|
|
1146
1149
|
minHeight: "36px",
|
|
1147
1150
|
display: "flex",
|
|
1148
1151
|
alignItems: "center",
|
|
1149
|
-
gap: "6px"
|
|
1152
|
+
gap: "6px",
|
|
1153
|
+
...(alreadySelected && {
|
|
1154
|
+
color: "rgb(70, 134, 128)",
|
|
1155
|
+
backgroundColor: "rgba(70, 134, 128, 0.08)",
|
|
1156
|
+
fontWeight: 600
|
|
1157
|
+
})
|
|
1150
1158
|
}
|
|
1151
1159
|
}, icon && /*#__PURE__*/React__namespace.default.createElement(Box__default.default, {
|
|
1152
1160
|
component: "span",
|
|
@@ -1154,7 +1162,13 @@ function SingleSelect({
|
|
|
1154
1162
|
display: "inline-flex",
|
|
1155
1163
|
alignItems: "center"
|
|
1156
1164
|
}
|
|
1157
|
-
}, icon),
|
|
1165
|
+
}, icon), alreadySelected && /*#__PURE__*/React__namespace.default.createElement(CheckCircleIcon__default.default, {
|
|
1166
|
+
fontSize: "small",
|
|
1167
|
+
sx: {
|
|
1168
|
+
color: "rgb(70, 134, 128)",
|
|
1169
|
+
fontSize: "16px"
|
|
1170
|
+
}
|
|
1171
|
+
}), formatLabel(value));
|
|
1158
1172
|
}))));
|
|
1159
1173
|
}
|
|
1160
1174
|
|
|
@@ -2743,6 +2757,29 @@ const MoveButtons = ({
|
|
|
2743
2757
|
fontSize: "small"
|
|
2744
2758
|
})));
|
|
2745
2759
|
|
|
2760
|
+
// Builds an identity string for a saved dimension/metric instance. Deliberate
|
|
2761
|
+
// duplicates (the same field added more than once) are allowed - the first
|
|
2762
|
+
// occurrence keeps the plain fullPath (so existing saved reports and their
|
|
2763
|
+
// title-override keys stay backward compatible), later occurrences get a
|
|
2764
|
+
// `#n` suffix.
|
|
2765
|
+
const makeInstanceId = (fullPath, occurrenceIndex) => occurrenceIndex === 0 ? fullPath : `${fullPath}#${occurrenceIndex}`;
|
|
2766
|
+
|
|
2767
|
+
// Picks the next free instance id for a newly-added fullPath, given the
|
|
2768
|
+
// items already saved. Scans occurrence numbers from 0 rather than just
|
|
2769
|
+
// counting existing instances with this fullPath, because counting alone
|
|
2770
|
+
// can collide: e.g. add "ft.currency" twice (ids "ft.currency",
|
|
2771
|
+
// "ft.currency#1"), remove the first, then add "ft.currency" again - a
|
|
2772
|
+
// plain count of 1 would regenerate "ft.currency#1", clashing with the
|
|
2773
|
+
// surviving duplicate.
|
|
2774
|
+
const nextInstanceId = (fullPath, existingItems) => {
|
|
2775
|
+
const existingIds = new Set(existingItems.map(item => item.id));
|
|
2776
|
+
let occurrenceIndex = 0;
|
|
2777
|
+
while (existingIds.has(makeInstanceId(fullPath, occurrenceIndex))) {
|
|
2778
|
+
occurrenceIndex += 1;
|
|
2779
|
+
}
|
|
2780
|
+
return makeInstanceId(fullPath, occurrenceIndex);
|
|
2781
|
+
};
|
|
2782
|
+
|
|
2746
2783
|
// Replace {{key}} placeholders in a title with values from the report
|
|
2747
2784
|
// parameters (e.g. "Balance ({{base_currency}})" -> "Balance (EUR)").
|
|
2748
2785
|
// Unknown keys are left as-is so missing parameters remain visible.
|
|
@@ -2769,14 +2806,17 @@ const resolveColumnOrder = (columnOrder, orderedDimensionItems, metrics) => {
|
|
|
2769
2806
|
// Normalise every candidate item to a uniform { kind, key, data } shape
|
|
2770
2807
|
// up front, so callers (e.g. the Column Order tab) can always read
|
|
2771
2808
|
// `.key` regardless of where the item came from.
|
|
2809
|
+
// Keyed by instance id (not fullPath) so that two instances of the same
|
|
2810
|
+
// dimension/metric - added deliberately as duplicates - are treated as
|
|
2811
|
+
// distinct columns rather than collapsing into one.
|
|
2772
2812
|
const normalizedDimensionItems = orderedDimensionItems.map(item => ({
|
|
2773
2813
|
kind: item.kind,
|
|
2774
|
-
key: item.kind === 'computed' ? item.data.name : item.data.
|
|
2814
|
+
key: item.kind === 'computed' ? item.data.name : item.data.id,
|
|
2775
2815
|
data: item.data
|
|
2776
2816
|
}));
|
|
2777
2817
|
const normalizedMetricItems = metrics.map(m => ({
|
|
2778
2818
|
kind: 'metric',
|
|
2779
|
-
key: m.
|
|
2819
|
+
key: m.id,
|
|
2780
2820
|
data: m
|
|
2781
2821
|
}));
|
|
2782
2822
|
const byKey = Object.fromEntries([...normalizedDimensionItems, ...normalizedMetricItems].map(item => [`${item.kind}:${item.key}`, item]));
|
|
@@ -2810,7 +2850,7 @@ const SortableChip$1 = ({
|
|
|
2810
2850
|
isLast,
|
|
2811
2851
|
sortOrder,
|
|
2812
2852
|
onSortOrderChange,
|
|
2813
|
-
|
|
2853
|
+
instanceId,
|
|
2814
2854
|
defaultTitle,
|
|
2815
2855
|
customTitle,
|
|
2816
2856
|
onUpdateTitle,
|
|
@@ -2893,7 +2933,7 @@ const SortableChip$1 = ({
|
|
|
2893
2933
|
handleCancel();
|
|
2894
2934
|
return;
|
|
2895
2935
|
}
|
|
2896
|
-
onUpdateTitle(
|
|
2936
|
+
onUpdateTitle(instanceId, trimmedValue);
|
|
2897
2937
|
setIsEditing(false);
|
|
2898
2938
|
};
|
|
2899
2939
|
const handleCancel = () => {
|
|
@@ -2901,7 +2941,7 @@ const SortableChip$1 = ({
|
|
|
2901
2941
|
setEditValue("");
|
|
2902
2942
|
};
|
|
2903
2943
|
const handleReset = () => {
|
|
2904
|
-
onResetTitle(
|
|
2944
|
+
onResetTitle(instanceId);
|
|
2905
2945
|
setIsEditing(false);
|
|
2906
2946
|
setEditValue("");
|
|
2907
2947
|
};
|
|
@@ -3112,7 +3152,7 @@ const SortableComputedChip = ({
|
|
|
3112
3152
|
const handleSave = () => {
|
|
3113
3153
|
const trimmedName = editName.trim();
|
|
3114
3154
|
const trimmedValue = editValue.trim();
|
|
3115
|
-
if (!trimmedName
|
|
3155
|
+
if (!trimmedName) return;
|
|
3116
3156
|
if (isNameTaken(trimmedName, name)) return;
|
|
3117
3157
|
onUpdate(name, {
|
|
3118
3158
|
name: trimmedName,
|
|
@@ -3132,6 +3172,16 @@ const SortableComputedChip = ({
|
|
|
3132
3172
|
handleCancel();
|
|
3133
3173
|
}
|
|
3134
3174
|
};
|
|
3175
|
+
|
|
3176
|
+
// The value field is multiline, so Enter inserts a newline instead of
|
|
3177
|
+
// saving; only Escape (cancel) and Cmd/Ctrl+Enter (save) are handled here.
|
|
3178
|
+
const handleValueKeyDown = e => {
|
|
3179
|
+
if (e.key === "Escape") {
|
|
3180
|
+
handleCancel();
|
|
3181
|
+
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
3182
|
+
handleSave();
|
|
3183
|
+
}
|
|
3184
|
+
};
|
|
3135
3185
|
return /*#__PURE__*/React__namespace.default.createElement("div", _extends({
|
|
3136
3186
|
ref: setNodeRef,
|
|
3137
3187
|
style: style
|
|
@@ -3235,10 +3285,13 @@ const SortableComputedChip = ({
|
|
|
3235
3285
|
}), /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
3236
3286
|
value: editValue,
|
|
3237
3287
|
onChange: e => setEditValue(e.target.value),
|
|
3238
|
-
onKeyDown:
|
|
3288
|
+
onKeyDown: handleValueKeyDown,
|
|
3239
3289
|
size: "small",
|
|
3240
3290
|
label: "Value",
|
|
3241
|
-
helperText: " ",
|
|
3291
|
+
helperText: "Cmd/Ctrl+Enter to save",
|
|
3292
|
+
multiline: true,
|
|
3293
|
+
minRows: 2,
|
|
3294
|
+
maxRows: 10,
|
|
3242
3295
|
sx: {
|
|
3243
3296
|
flex: 1,
|
|
3244
3297
|
minWidth: "200px",
|
|
@@ -3262,7 +3315,7 @@ const SortableComputedChip = ({
|
|
|
3262
3315
|
onClick: handleSave,
|
|
3263
3316
|
color: "primary",
|
|
3264
3317
|
"aria-label": "save computed dimension",
|
|
3265
|
-
disabled: !editName.trim() ||
|
|
3318
|
+
disabled: !editName.trim() || nameError
|
|
3266
3319
|
}, /*#__PURE__*/React__namespace.default.createElement(CheckIcon__default.default, {
|
|
3267
3320
|
fontSize: "small"
|
|
3268
3321
|
})))), /*#__PURE__*/React__namespace.default.createElement(material.Tooltip, {
|
|
@@ -3311,12 +3364,14 @@ const Dimensions = ({
|
|
|
3311
3364
|
// Resolve the unified dimensionOrder into the actual dimension / computed
|
|
3312
3365
|
// dimension objects, so dimensions and computed dimensions can be shown
|
|
3313
3366
|
// (and dragged) as a single list while staying stored in their own arrays.
|
|
3314
|
-
|
|
3367
|
+
// Keyed by instance id rather than fullPath, so two instances of the same
|
|
3368
|
+
// dimension (added deliberately as a duplicate) stay distinct entries.
|
|
3369
|
+
const dimensionById = Object.fromEntries(savedDimensions.map(d => [d.id, d]));
|
|
3315
3370
|
const computedDimensionByName = Object.fromEntries(savedComputedDimensions.map(cd => [cd.name, cd]));
|
|
3316
3371
|
const orderedItems = dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
3317
3372
|
kind: "dimension",
|
|
3318
3373
|
key: entry.key,
|
|
3319
|
-
data:
|
|
3374
|
+
data: dimensionById[entry.key]
|
|
3320
3375
|
} : {
|
|
3321
3376
|
kind: "computed",
|
|
3322
3377
|
key: entry.key,
|
|
@@ -3330,9 +3385,13 @@ const Dimensions = ({
|
|
|
3330
3385
|
handleMoveDown
|
|
3331
3386
|
} = makeListReorderHandlers(dimensionOrder, onReorderDimensionItems);
|
|
3332
3387
|
|
|
3333
|
-
// Handle sort order change (dimensions only - computed dimensions have no
|
|
3334
|
-
|
|
3335
|
-
|
|
3388
|
+
// Handle sort order change (dimensions only - computed dimensions have no
|
|
3389
|
+
// order_by). Keyed by instance id but applied query-wide: SQL can only
|
|
3390
|
+
// sort by a given column once, so toggling sort on any duplicate instance
|
|
3391
|
+
// of the same field is propagated to all instances sharing its fullPath
|
|
3392
|
+
// (handled in ReportBuilder's onUpdateDimensionSortOrder).
|
|
3393
|
+
const handleSortOrderChange = (id, sortOrder) => {
|
|
3394
|
+
onUpdateDimensionSortOrder(id, sortOrder);
|
|
3336
3395
|
};
|
|
3337
3396
|
|
|
3338
3397
|
// Computed Dimensions: { name, value } pairs independent of any provider.
|
|
@@ -3353,7 +3412,7 @@ const Dimensions = ({
|
|
|
3353
3412
|
const handleSaveComputed = () => {
|
|
3354
3413
|
const trimmedName = computedName.trim();
|
|
3355
3414
|
const trimmedValue = computedValue.trim();
|
|
3356
|
-
if (!trimmedName
|
|
3415
|
+
if (!trimmedName) return;
|
|
3357
3416
|
if (isComputedNameTaken(trimmedName)) return;
|
|
3358
3417
|
onSaveComputedDimension({
|
|
3359
3418
|
name: trimmedName,
|
|
@@ -3395,7 +3454,10 @@ const Dimensions = ({
|
|
|
3395
3454
|
currentProviderKey = selection.targetKey;
|
|
3396
3455
|
});
|
|
3397
3456
|
|
|
3398
|
-
// Create a Set of already selected dimension fullPaths for quick lookup
|
|
3457
|
+
// Create a Set of already selected dimension fullPaths for quick lookup.
|
|
3458
|
+
// Already-selected items stay selectable (so the same dimension can be
|
|
3459
|
+
// added again on purpose) - they're just flagged so the dropdown can
|
|
3460
|
+
// highlight them instead of blocking the click.
|
|
3399
3461
|
const selectedFullPaths = new Set(savedDimensions.map(dim => dim.fullPath));
|
|
3400
3462
|
const items = [];
|
|
3401
3463
|
// Iterate through aliases (e.g., 'ba', 'pc')
|
|
@@ -3407,9 +3469,6 @@ const Dimensions = ({
|
|
|
3407
3469
|
|
|
3408
3470
|
// Construct the fullPath for this dimension
|
|
3409
3471
|
const fullPath = `${aliasPath.join("_")}.${dimKey}`;
|
|
3410
|
-
|
|
3411
|
-
// Check if this dimension is already selected
|
|
3412
|
-
const isAlreadySelected = selectedFullPaths.has(fullPath);
|
|
3413
3472
|
items.push({
|
|
3414
3473
|
// Include provider name to ensure uniqueness across different providers
|
|
3415
3474
|
key: `${currentProvider}_${alias}.${dimKey}`,
|
|
@@ -3417,7 +3476,7 @@ const Dimensions = ({
|
|
|
3417
3476
|
dimensionKey: dimKey,
|
|
3418
3477
|
alias: alias,
|
|
3419
3478
|
dimension: dimension,
|
|
3420
|
-
|
|
3479
|
+
alreadySelected: selectedFullPaths.has(fullPath)
|
|
3421
3480
|
});
|
|
3422
3481
|
});
|
|
3423
3482
|
});
|
|
@@ -3479,7 +3538,10 @@ const Dimensions = ({
|
|
|
3479
3538
|
dimensionKey: selectedItem.dimensionKey,
|
|
3480
3539
|
dimensionTitle: selectedItem.value,
|
|
3481
3540
|
// The constructed path for server
|
|
3482
|
-
fullPath: fullPath
|
|
3541
|
+
fullPath: fullPath,
|
|
3542
|
+
// Unique per-instance identity, so this field can be added more than
|
|
3543
|
+
// once and each occurrence stays independently orderable/removable.
|
|
3544
|
+
id: nextInstanceId(fullPath, savedDimensions)
|
|
3483
3545
|
};
|
|
3484
3546
|
onSaveDimension(dimensionData);
|
|
3485
3547
|
handleCancel();
|
|
@@ -3490,8 +3552,9 @@ const Dimensions = ({
|
|
|
3490
3552
|
const handleAddAll = () => {
|
|
3491
3553
|
const dimensionItems = getDimensionItems();
|
|
3492
3554
|
|
|
3493
|
-
// Filter out already selected dimensions
|
|
3494
|
-
|
|
3555
|
+
// Filter out already selected dimensions - "Add all" only fills in
|
|
3556
|
+
// missing ones; intentional duplicates are added one at a time above.
|
|
3557
|
+
const availableItems = dimensionItems.filter(item => !item.alreadySelected);
|
|
3495
3558
|
if (availableItems.length === 0) return;
|
|
3496
3559
|
|
|
3497
3560
|
// Build the complete relation objects array (same for all dimensions in this provider)
|
|
@@ -3536,7 +3599,10 @@ const Dimensions = ({
|
|
|
3536
3599
|
dimensionKey: item.dimensionKey,
|
|
3537
3600
|
dimensionTitle: item.value,
|
|
3538
3601
|
// The constructed path for server
|
|
3539
|
-
fullPath: fullPath
|
|
3602
|
+
fullPath: fullPath,
|
|
3603
|
+
// "Add all" only ever adds fields that aren't already selected, so
|
|
3604
|
+
// this is always the first (and only) occurrence of this fullPath.
|
|
3605
|
+
id: fullPath
|
|
3540
3606
|
};
|
|
3541
3607
|
onSaveDimension(dimensionData);
|
|
3542
3608
|
});
|
|
@@ -3635,6 +3701,9 @@ const Dimensions = ({
|
|
|
3635
3701
|
value: computedValue,
|
|
3636
3702
|
onChange: e => setComputedValue(e.target.value),
|
|
3637
3703
|
size: "small",
|
|
3704
|
+
multiline: true,
|
|
3705
|
+
minRows: 2,
|
|
3706
|
+
maxRows: 10,
|
|
3638
3707
|
sx: {
|
|
3639
3708
|
width: "400px",
|
|
3640
3709
|
"& .MuiInputBase-input": {
|
|
@@ -3670,7 +3739,7 @@ const Dimensions = ({
|
|
|
3670
3739
|
}, "Cancel"), /*#__PURE__*/React__namespace.default.createElement(material.Button, {
|
|
3671
3740
|
variant: "contained",
|
|
3672
3741
|
onClick: handleSaveComputed,
|
|
3673
|
-
disabled: !computedName.trim() ||
|
|
3742
|
+
disabled: !computedName.trim() || computedNameError,
|
|
3674
3743
|
sx: {
|
|
3675
3744
|
height: "40px",
|
|
3676
3745
|
fontFamily: "system-ui",
|
|
@@ -3746,7 +3815,7 @@ const Dimensions = ({
|
|
|
3746
3815
|
placement: "top"
|
|
3747
3816
|
}, /*#__PURE__*/React__namespace.default.createElement("span", null, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
3748
3817
|
onClick: handleAddAll,
|
|
3749
|
-
disabled: getDimensionItems().filter(item => !item.
|
|
3818
|
+
disabled: getDimensionItems().filter(item => !item.alreadySelected).length === 0,
|
|
3750
3819
|
"aria-label": "add all dimensions",
|
|
3751
3820
|
sx: {
|
|
3752
3821
|
color: "rgb(70, 134, 128)",
|
|
@@ -3837,9 +3906,9 @@ const Dimensions = ({
|
|
|
3837
3906
|
isLast: index === orderedItems.length - 1,
|
|
3838
3907
|
sortOrder: item.data.sortOrder || null,
|
|
3839
3908
|
onSortOrderChange: sortOrder => handleSortOrderChange(item.key, sortOrder),
|
|
3840
|
-
|
|
3909
|
+
instanceId: item.data.id,
|
|
3841
3910
|
defaultTitle: item.data.dimensionTitle,
|
|
3842
|
-
customTitle: titleOverrides[item.data.
|
|
3911
|
+
customTitle: titleOverrides[item.data.id],
|
|
3843
3912
|
onUpdateTitle: onUpdateTitle,
|
|
3844
3913
|
onResetTitle: onResetTitle
|
|
3845
3914
|
}) : /*#__PURE__*/React__namespace.default.createElement(SortableComputedChip, {
|
|
@@ -3912,7 +3981,7 @@ const SortableChip = ({
|
|
|
3912
3981
|
onMoveDown,
|
|
3913
3982
|
isFirst,
|
|
3914
3983
|
isLast,
|
|
3915
|
-
|
|
3984
|
+
instanceId,
|
|
3916
3985
|
defaultTitle,
|
|
3917
3986
|
customTitle,
|
|
3918
3987
|
onUpdateTitle,
|
|
@@ -3965,7 +4034,7 @@ const SortableChip = ({
|
|
|
3965
4034
|
handleCancel();
|
|
3966
4035
|
return;
|
|
3967
4036
|
}
|
|
3968
|
-
onUpdateTitle(
|
|
4037
|
+
onUpdateTitle(instanceId, trimmedValue);
|
|
3969
4038
|
setIsEditing(false);
|
|
3970
4039
|
};
|
|
3971
4040
|
const handleCancel = () => {
|
|
@@ -3973,7 +4042,7 @@ const SortableChip = ({
|
|
|
3973
4042
|
setEditValue('');
|
|
3974
4043
|
};
|
|
3975
4044
|
const handleReset = () => {
|
|
3976
|
-
onResetTitle(
|
|
4045
|
+
onResetTitle(instanceId);
|
|
3977
4046
|
setIsEditing(false);
|
|
3978
4047
|
setEditValue('');
|
|
3979
4048
|
};
|
|
@@ -4194,7 +4263,10 @@ const Metrics = ({
|
|
|
4194
4263
|
currentProviderKey = selection.targetKey;
|
|
4195
4264
|
});
|
|
4196
4265
|
|
|
4197
|
-
// Create a Set of already selected metric fullPaths for quick lookup
|
|
4266
|
+
// Create a Set of already selected metric fullPaths for quick lookup.
|
|
4267
|
+
// Already-selected items stay selectable (so the same metric can be
|
|
4268
|
+
// added again on purpose) - they're just flagged so the dropdown can
|
|
4269
|
+
// highlight them instead of blocking the click.
|
|
4198
4270
|
const selectedFullPaths = new Set(savedMetrics.map(metric => metric.fullPath));
|
|
4199
4271
|
|
|
4200
4272
|
// Metrics are a direct array, not nested by alias
|
|
@@ -4202,15 +4274,12 @@ const Metrics = ({
|
|
|
4202
4274
|
const items = provider.metrics.map((metric, index) => {
|
|
4203
4275
|
// Construct the fullPath for this metric
|
|
4204
4276
|
const fullPath = `${aliasPath.join('_')}.${metric.name}`;
|
|
4205
|
-
|
|
4206
|
-
// Check if this metric is already selected
|
|
4207
|
-
const isAlreadySelected = selectedFullPaths.has(fullPath);
|
|
4208
4277
|
return {
|
|
4209
4278
|
key: `${currentProvider}_${metric.name}_${index}`,
|
|
4210
4279
|
value: metric.title || metric.name,
|
|
4211
4280
|
metricName: metric.name,
|
|
4212
4281
|
metric: metric,
|
|
4213
|
-
|
|
4282
|
+
alreadySelected: selectedFullPaths.has(fullPath),
|
|
4214
4283
|
icon: metric.source ? /*#__PURE__*/React__namespace.default.createElement(MetricSourceIcon, {
|
|
4215
4284
|
source: metric.source
|
|
4216
4285
|
}) : undefined
|
|
@@ -4273,7 +4342,11 @@ const Metrics = ({
|
|
|
4273
4342
|
metricName: selectedItem.metricName,
|
|
4274
4343
|
metricTitle: selectedItem.value,
|
|
4275
4344
|
// The constructed path for server
|
|
4276
|
-
fullPath: fullPath
|
|
4345
|
+
fullPath: fullPath,
|
|
4346
|
+
// Unique per-instance identity, so this field can be added more
|
|
4347
|
+
// than once and each occurrence stays independently
|
|
4348
|
+
// orderable/removable/titleable.
|
|
4349
|
+
id: nextInstanceId(fullPath, savedMetrics)
|
|
4277
4350
|
};
|
|
4278
4351
|
onSaveMetric(metricData);
|
|
4279
4352
|
handleCancel();
|
|
@@ -4284,8 +4357,9 @@ const Metrics = ({
|
|
|
4284
4357
|
const handleAddAll = () => {
|
|
4285
4358
|
const metricItems = getMetricItems();
|
|
4286
4359
|
|
|
4287
|
-
// Filter out already selected metrics
|
|
4288
|
-
|
|
4360
|
+
// Filter out already selected metrics - "Add all" only fills in
|
|
4361
|
+
// missing ones; intentional duplicates are added one at a time above.
|
|
4362
|
+
const availableItems = metricItems.filter(item => !item.alreadySelected);
|
|
4289
4363
|
if (availableItems.length === 0) return;
|
|
4290
4364
|
|
|
4291
4365
|
// Build the complete relation objects array (same for all metrics in this provider)
|
|
@@ -4330,7 +4404,10 @@ const Metrics = ({
|
|
|
4330
4404
|
metricName: item.metricName,
|
|
4331
4405
|
metricTitle: item.value,
|
|
4332
4406
|
// The constructed path for server
|
|
4333
|
-
fullPath: fullPath
|
|
4407
|
+
fullPath: fullPath,
|
|
4408
|
+
// "Add all" only ever adds fields that aren't already
|
|
4409
|
+
// selected, so this is always the first occurrence.
|
|
4410
|
+
id: fullPath
|
|
4334
4411
|
};
|
|
4335
4412
|
onSaveMetric(metricData);
|
|
4336
4413
|
});
|
|
@@ -4429,7 +4506,7 @@ const Metrics = ({
|
|
|
4429
4506
|
placement: "top"
|
|
4430
4507
|
}, /*#__PURE__*/React__namespace.default.createElement("span", null, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
4431
4508
|
onClick: handleAddAll,
|
|
4432
|
-
disabled: getMetricItems().filter(item => !item.
|
|
4509
|
+
disabled: getMetricItems().filter(item => !item.alreadySelected).length === 0,
|
|
4433
4510
|
"aria-label": "add all metrics",
|
|
4434
4511
|
sx: {
|
|
4435
4512
|
color: "rgb(70, 134, 128)",
|
|
@@ -4507,20 +4584,64 @@ const Metrics = ({
|
|
|
4507
4584
|
id: index,
|
|
4508
4585
|
label: metric.metricTitle,
|
|
4509
4586
|
fullLabel: `${formatProviderPath(metric)} → ${metric.metricTitle} (${metric.fullPath})`,
|
|
4510
|
-
onDelete: () => onRemoveMetric(
|
|
4587
|
+
onDelete: () => onRemoveMetric(metric.id),
|
|
4511
4588
|
onMoveUp: () => handleMoveUp(index),
|
|
4512
4589
|
onMoveDown: () => handleMoveDown(index),
|
|
4513
4590
|
isFirst: index === 0,
|
|
4514
4591
|
isLast: index === savedMetrics.length - 1,
|
|
4515
|
-
|
|
4592
|
+
instanceId: metric.id,
|
|
4516
4593
|
defaultTitle: metric.metricTitle,
|
|
4517
|
-
customTitle: titleOverrides[metric.
|
|
4594
|
+
customTitle: titleOverrides[metric.id],
|
|
4518
4595
|
onUpdateTitle: onUpdateTitle,
|
|
4519
4596
|
onResetTitle: onResetTitle,
|
|
4520
4597
|
source: metric.metric?.source
|
|
4521
4598
|
})))));
|
|
4522
4599
|
};
|
|
4523
4600
|
|
|
4601
|
+
// Small text input + button for adding a filter value that isn't present in
|
|
4602
|
+
// the "Select Filter Values" dropdown (e.g. because no existing row has that
|
|
4603
|
+
// value). Kept as an uncontrolled local field so it clears itself after add.
|
|
4604
|
+
const CustomFilterValueInput = ({
|
|
4605
|
+
onAdd
|
|
4606
|
+
}) => {
|
|
4607
|
+
const [value, setValue] = React.useState('');
|
|
4608
|
+
const handleAdd = () => {
|
|
4609
|
+
const trimmed = value.trim();
|
|
4610
|
+
if (!trimmed) return;
|
|
4611
|
+
onAdd(trimmed);
|
|
4612
|
+
setValue('');
|
|
4613
|
+
};
|
|
4614
|
+
return /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
4615
|
+
sx: {
|
|
4616
|
+
display: 'flex',
|
|
4617
|
+
gap: 1,
|
|
4618
|
+
mt: 1
|
|
4619
|
+
}
|
|
4620
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
4621
|
+
size: "small",
|
|
4622
|
+
fullWidth: true,
|
|
4623
|
+
placeholder: "Add a value not in the list above",
|
|
4624
|
+
value: value,
|
|
4625
|
+
onChange: e => setValue(e.target.value),
|
|
4626
|
+
onKeyDown: e => {
|
|
4627
|
+
if (e.key === 'Enter') {
|
|
4628
|
+
e.preventDefault();
|
|
4629
|
+
handleAdd();
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4632
|
+
}), /*#__PURE__*/React__namespace.default.createElement(material.Button, {
|
|
4633
|
+
variant: "outlined",
|
|
4634
|
+
size: "small",
|
|
4635
|
+
onClick: handleAdd,
|
|
4636
|
+
disabled: !value.trim(),
|
|
4637
|
+
sx: {
|
|
4638
|
+
textTransform: 'none',
|
|
4639
|
+
whiteSpace: 'nowrap',
|
|
4640
|
+
borderColor: "rgb(70, 134, 128)",
|
|
4641
|
+
color: "rgb(70, 134, 128)"
|
|
4642
|
+
}
|
|
4643
|
+
}, "Add"));
|
|
4644
|
+
};
|
|
4524
4645
|
const Filters = ({
|
|
4525
4646
|
providersData,
|
|
4526
4647
|
rootProvider,
|
|
@@ -4660,8 +4781,9 @@ const Filters = ({
|
|
|
4660
4781
|
}
|
|
4661
4782
|
} else {
|
|
4662
4783
|
// For regular filters, set selected values and fetch available values
|
|
4663
|
-
|
|
4664
|
-
|
|
4784
|
+
const currentValues = filter.values || [];
|
|
4785
|
+
setSelectedFilterValues(currentValues);
|
|
4786
|
+
await fetchFilterValues(fullPath, '', currentValues);
|
|
4665
4787
|
}
|
|
4666
4788
|
};
|
|
4667
4789
|
const handleSaveEditedFilter = fullPath => {
|
|
@@ -4756,7 +4878,11 @@ const Filters = ({
|
|
|
4756
4878
|
};
|
|
4757
4879
|
|
|
4758
4880
|
// Fetch distinct values for the selected dimension
|
|
4759
|
-
|
|
4881
|
+
// preserveValues lets a caller pass the values that must survive the merge
|
|
4882
|
+
// explicitly, instead of relying on the selectedFilterValues closure - a
|
|
4883
|
+
// setSelectedFilterValues() call right before fetchFilterValues() hasn't
|
|
4884
|
+
// committed yet by the time this function reads that state.
|
|
4885
|
+
const fetchFilterValues = async (fullPath, searchText = '', preserveValues = null) => {
|
|
4760
4886
|
setLoadingFilterValues(true);
|
|
4761
4887
|
try {
|
|
4762
4888
|
// Get parameters from context if available, otherwise use default
|
|
@@ -4829,7 +4955,8 @@ const Filters = ({
|
|
|
4829
4955
|
// Merge with currently selected values to ensure they're always available
|
|
4830
4956
|
// This is important when editing filters with search text - we don't want to lose
|
|
4831
4957
|
// previously selected values that don't match the current search
|
|
4832
|
-
const
|
|
4958
|
+
const valuesToPreserve = preserveValues !== null ? preserveValues : selectedFilterValues;
|
|
4959
|
+
const selectedValueItems = valuesToPreserve.map(key => ({
|
|
4833
4960
|
key: String(key),
|
|
4834
4961
|
value: String(key)
|
|
4835
4962
|
}));
|
|
@@ -5058,6 +5185,21 @@ const Filters = ({
|
|
|
5058
5185
|
}
|
|
5059
5186
|
};
|
|
5060
5187
|
|
|
5188
|
+
// Add a value typed in the "custom value" box directly into the selected
|
|
5189
|
+
// filter values, and inject it into availableFilterValues too, since
|
|
5190
|
+
// CheckboxMultiAutocomplete only renders a chip for keys it can resolve
|
|
5191
|
+
// against its items list.
|
|
5192
|
+
const handleAddCustomFilterValue = rawValue => {
|
|
5193
|
+
const value = rawValue.trim();
|
|
5194
|
+
if (!value) return;
|
|
5195
|
+
if (selectedFilterValues.includes(value)) return;
|
|
5196
|
+
setAvailableFilterValues(prev => prev.some(item => item.key === value) ? prev : [{
|
|
5197
|
+
key: value,
|
|
5198
|
+
value
|
|
5199
|
+
}, ...prev]);
|
|
5200
|
+
setSelectedFilterValues(prev => [...prev, value]);
|
|
5201
|
+
};
|
|
5202
|
+
|
|
5061
5203
|
// Handler for search text input in filter values dropdown (debounced - triggers API call)
|
|
5062
5204
|
const handleFilterSearchChange = searchText => {
|
|
5063
5205
|
// Determine the fullPath based on current mode
|
|
@@ -5277,6 +5419,8 @@ const Filters = ({
|
|
|
5277
5419
|
loading: loadingFilterValues,
|
|
5278
5420
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
5279
5421
|
debounceMs: 1000
|
|
5422
|
+
}), /*#__PURE__*/React__namespace.default.createElement(CustomFilterValueInput, {
|
|
5423
|
+
onAdd: handleAddCustomFilterValue
|
|
5280
5424
|
})));
|
|
5281
5425
|
})(), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5282
5426
|
sx: {
|
|
@@ -5351,7 +5495,12 @@ const Filters = ({
|
|
|
5351
5495
|
mb: 3
|
|
5352
5496
|
}
|
|
5353
5497
|
}, /*#__PURE__*/React__namespace.default.createElement(SingleSelect, {
|
|
5354
|
-
items:
|
|
5498
|
+
items: Array.from(
|
|
5499
|
+
// Dimensions can now be added more than once (same fullPath,
|
|
5500
|
+
// different instance), but a filter targets the underlying
|
|
5501
|
+
// field regardless of how many columns display it - so this
|
|
5502
|
+
// list is deduped by fullPath to avoid showing it twice.
|
|
5503
|
+
new Map(existingDimensions.map(dim => [dim.fullPath, dim])).values()).map(dim => ({
|
|
5355
5504
|
key: dim.fullPath,
|
|
5356
5505
|
value: interpolateTitle(dimensionTitleOverrides[dim.fullPath] || dim.dimensionTitle, parameters)
|
|
5357
5506
|
})).sort((a, b) => a.value.localeCompare(b.value)),
|
|
@@ -5423,6 +5572,8 @@ const Filters = ({
|
|
|
5423
5572
|
loading: loadingFilterValues,
|
|
5424
5573
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
5425
5574
|
debounceMs: 1000
|
|
5575
|
+
}), /*#__PURE__*/React__namespace.default.createElement(CustomFilterValueInput, {
|
|
5576
|
+
onAdd: handleAddCustomFilterValue
|
|
5426
5577
|
})));
|
|
5427
5578
|
})(), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5428
5579
|
sx: {
|
|
@@ -5712,6 +5863,8 @@ const Filters = ({
|
|
|
5712
5863
|
loading: loadingFilterValues,
|
|
5713
5864
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
5714
5865
|
debounceMs: 1000
|
|
5866
|
+
}), /*#__PURE__*/React__namespace.default.createElement(CustomFilterValueInput, {
|
|
5867
|
+
onAdd: handleAddCustomFilterValue
|
|
5715
5868
|
})), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5716
5869
|
sx: {
|
|
5717
5870
|
display: 'flex',
|
|
@@ -5934,7 +6087,7 @@ const ColumnOrder = ({
|
|
|
5934
6087
|
key: `${item.kind}-${item.key}`,
|
|
5935
6088
|
id: index,
|
|
5936
6089
|
label: labelFor(item),
|
|
5937
|
-
fullPath: item.kind === "computed" ? item.data.value : item.
|
|
6090
|
+
fullPath: item.kind === "computed" ? item.data.value : item.data.fullPath,
|
|
5938
6091
|
kind: item.kind,
|
|
5939
6092
|
source: item.kind === "metric" ? item.data.metric?.source : undefined,
|
|
5940
6093
|
onMoveUp: () => handleMoveUp(index),
|
|
@@ -5960,11 +6113,17 @@ const formatValue = (value, def) => {
|
|
|
5960
6113
|
const isNumericType = type => type === 'integer' || type === 'currency';
|
|
5961
6114
|
|
|
5962
6115
|
// Builds a single DataGrid column definition for one resolved column item
|
|
5963
|
-
// (a dimension, computed dimension, or metric).
|
|
6116
|
+
// (a dimension, computed dimension, or metric). The DataGrid `field` is the
|
|
6117
|
+
// item's instance id (so two duplicate instances of the same underlying
|
|
6118
|
+
// dimension/metric still get distinct columns); the actual cell value is
|
|
6119
|
+
// read via `valueGetter` from `dataFieldName`, the key the API response
|
|
6120
|
+
// actually uses - shared by every duplicate instance, since the backend
|
|
6121
|
+
// only ever returns one value per fullPath. dataFieldName rules:
|
|
5964
6122
|
// - Dimensions from base provider: baseAlias.field -> baseAlias_field
|
|
5965
6123
|
// - Dimensions from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5966
6124
|
// - Computed dimensions have no provider/relations chain, so the API returns
|
|
5967
|
-
// each row keyed by the computed dimension's own `name
|
|
6125
|
+
// each row keyed by the computed dimension's own `name` (and can't be
|
|
6126
|
+
// duplicated - names must be unique - so field/dataFieldName coincide).
|
|
5968
6127
|
// - Metrics: API returns metric keys in different forms depending on path depth
|
|
5969
6128
|
// (see comment inline below), normalised to match the row-key normalisation
|
|
5970
6129
|
// done when building `rows`.
|
|
@@ -5982,13 +6141,13 @@ const buildColumn = (item, titleOverrides, parameters) => {
|
|
|
5982
6141
|
if (item.kind === 'metric') {
|
|
5983
6142
|
const metric = item.data;
|
|
5984
6143
|
const metricDef = metric.metric;
|
|
5985
|
-
let
|
|
6144
|
+
let dataFieldName;
|
|
5986
6145
|
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
5987
6146
|
if (dotCount >= 2) {
|
|
5988
6147
|
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
5989
6148
|
// Normalise dots to underscores to match the row key normalisation below.
|
|
5990
6149
|
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
5991
|
-
|
|
6150
|
+
dataFieldName = metric.fullPath.replace(/\./g, '_');
|
|
5992
6151
|
} else if (metric.relations && metric.relations.length > 0) {
|
|
5993
6152
|
// 2-part path from a nested provider: API drops the base provider alias.
|
|
5994
6153
|
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
@@ -5999,19 +6158,24 @@ const buildColumn = (item, titleOverrides, parameters) => {
|
|
|
5999
6158
|
const pathParts = pathWithoutField.split('_');
|
|
6000
6159
|
pathParts.shift(); // remove base provider alias
|
|
6001
6160
|
const pathWithoutBase = pathParts.join('_');
|
|
6002
|
-
|
|
6161
|
+
dataFieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
6003
6162
|
} else {
|
|
6004
6163
|
// 2-part path from the base provider: API returns just the metric name.
|
|
6005
6164
|
// Example: "mc.record_count" -> "record_count"
|
|
6006
|
-
|
|
6165
|
+
dataFieldName = metric.metricName;
|
|
6007
6166
|
}
|
|
6008
|
-
const headerName = interpolateTitle(titleOverrides.metrics[metric.
|
|
6167
|
+
const headerName = interpolateTitle(titleOverrides.metrics[metric.id] || metric.metricTitle || dataFieldName, parameters);
|
|
6009
6168
|
return {
|
|
6010
|
-
field
|
|
6169
|
+
// `field` is the instance id (unique even for duplicate columns
|
|
6170
|
+
// pointing at the same underlying metric); the actual value is
|
|
6171
|
+
// read from the row via valueGetter using dataFieldName, which is
|
|
6172
|
+
// the key the API response actually uses (shared across duplicates).
|
|
6173
|
+
field: metric.id.replace(/[.#]/g, '_'),
|
|
6011
6174
|
headerName: headerName,
|
|
6012
6175
|
flex: 1,
|
|
6013
6176
|
minWidth: 150,
|
|
6014
6177
|
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
6178
|
+
valueGetter: (_value, row) => row[dataFieldName],
|
|
6015
6179
|
renderHeader: () => /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
6016
6180
|
sx: {
|
|
6017
6181
|
display: 'flex',
|
|
@@ -6027,7 +6191,7 @@ const buildColumn = (item, titleOverrides, parameters) => {
|
|
|
6027
6191
|
|
|
6028
6192
|
// Dimension
|
|
6029
6193
|
const dim = item.data;
|
|
6030
|
-
let
|
|
6194
|
+
let dataFieldName;
|
|
6031
6195
|
if (dim.relations && dim.relations.length > 0) {
|
|
6032
6196
|
// From nested provider: drop the base provider alias
|
|
6033
6197
|
// Example: ft_fa_db.currency -> fa_db_currency
|
|
@@ -6039,20 +6203,24 @@ const buildColumn = (item, titleOverrides, parameters) => {
|
|
|
6039
6203
|
pathParts.shift(); // Remove base provider alias
|
|
6040
6204
|
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
6041
6205
|
|
|
6042
|
-
|
|
6206
|
+
dataFieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
6043
6207
|
} else {
|
|
6044
6208
|
// From base provider: keep the full path with underscore
|
|
6045
6209
|
// Example: ba.created_at -> ba_created_at
|
|
6046
|
-
|
|
6210
|
+
dataFieldName = dim.fullPath.replace('.', '_');
|
|
6047
6211
|
}
|
|
6048
|
-
const headerName = interpolateTitle(titleOverrides.dimensions[dim.
|
|
6212
|
+
const headerName = interpolateTitle(titleOverrides.dimensions[dim.id] || dim.dimensionTitle || dataFieldName, parameters);
|
|
6049
6213
|
const dimDef = dim.dimension;
|
|
6050
6214
|
return {
|
|
6051
|
-
|
|
6215
|
+
// Same rationale as the metric branch above: `field` must be unique
|
|
6216
|
+
// per instance, while the value always comes from the one real
|
|
6217
|
+
// underlying row key (dataFieldName), shared by every duplicate.
|
|
6218
|
+
field: dim.id.replace(/[.#]/g, '_'),
|
|
6052
6219
|
headerName: headerName,
|
|
6053
6220
|
flex: 1,
|
|
6054
6221
|
minWidth: 150,
|
|
6055
6222
|
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
6223
|
+
valueGetter: (_value, row) => row[dataFieldName],
|
|
6056
6224
|
valueFormatter: value => formatValue(value, dimDef)
|
|
6057
6225
|
};
|
|
6058
6226
|
};
|
|
@@ -6378,6 +6546,72 @@ const ReportBuilder = ({
|
|
|
6378
6546
|
}
|
|
6379
6547
|
};
|
|
6380
6548
|
|
|
6549
|
+
// Reconstructs saved dimensions as independent, individually-identified
|
|
6550
|
+
// instances so that deliberate duplicates (the same field added more than
|
|
6551
|
+
// once) survive a save/reload round trip. Prefers `ordered_dimensions`
|
|
6552
|
+
// (one ref per instance - the nth duplicate of a path is suffixed
|
|
6553
|
+
// `#n`) when present; falls back to one instance per entry in the
|
|
6554
|
+
// (necessarily duplicate-free) `dimensions` path list for reports saved
|
|
6555
|
+
// before this field existed.
|
|
6556
|
+
const reconstructDimensions = (reportDef, providersData) => {
|
|
6557
|
+
const orderByMap = {};
|
|
6558
|
+
if (reportDef.definition?.doc?.query?.order_by) {
|
|
6559
|
+
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
6560
|
+
orderByMap[orderItem.name] = orderItem.desc === true ? "desc" : "asc";
|
|
6561
|
+
}
|
|
6562
|
+
}
|
|
6563
|
+
const reconstructed = [];
|
|
6564
|
+
const orderedRefs = (reportDef.definition?.doc?.query?.ordered_dimensions || []).filter(entry => entry.type === "dimension").map(entry => entry.ref);
|
|
6565
|
+
if (orderedRefs.length > 0) {
|
|
6566
|
+
orderedRefs.forEach(ref => {
|
|
6567
|
+
const fullPath = ref.split("#")[0];
|
|
6568
|
+
const dim = reconstructDimensionFromPath(fullPath, providersData, reportDef.provider);
|
|
6569
|
+
if (dim) {
|
|
6570
|
+
dim.id = ref;
|
|
6571
|
+
dim.sortOrder = orderByMap[fullPath] || null;
|
|
6572
|
+
reconstructed.push(dim);
|
|
6573
|
+
}
|
|
6574
|
+
});
|
|
6575
|
+
} else if (reportDef.definition?.doc?.query?.dimensions) {
|
|
6576
|
+
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
6577
|
+
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
6578
|
+
if (dim) {
|
|
6579
|
+
dim.id = dimPath;
|
|
6580
|
+
dim.sortOrder = orderByMap[dimPath] || null;
|
|
6581
|
+
reconstructed.push(dim);
|
|
6582
|
+
}
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
return reconstructed;
|
|
6586
|
+
};
|
|
6587
|
+
|
|
6588
|
+
// Mirrors reconstructDimensions for metrics, using the analogous
|
|
6589
|
+
// `ordered_metrics` field (metrics have no other unified-order concept to
|
|
6590
|
+
// fall back on, so this is always written whenever there are metrics).
|
|
6591
|
+
const reconstructMetrics = (reportDef, providersData) => {
|
|
6592
|
+
const reconstructed = [];
|
|
6593
|
+
const orderedRefs = reportDef.definition?.doc?.query?.ordered_metrics || [];
|
|
6594
|
+
if (orderedRefs.length > 0) {
|
|
6595
|
+
orderedRefs.forEach(ref => {
|
|
6596
|
+
const fullPath = ref.split("#")[0];
|
|
6597
|
+
const metric = reconstructMetricFromPath(fullPath, providersData, reportDef.provider);
|
|
6598
|
+
if (metric) {
|
|
6599
|
+
metric.id = ref;
|
|
6600
|
+
reconstructed.push(metric);
|
|
6601
|
+
}
|
|
6602
|
+
});
|
|
6603
|
+
} else if (reportDef.definition?.doc?.query?.metrics) {
|
|
6604
|
+
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
6605
|
+
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
6606
|
+
if (metric) {
|
|
6607
|
+
metric.id = metricPath;
|
|
6608
|
+
reconstructed.push(metric);
|
|
6609
|
+
}
|
|
6610
|
+
}
|
|
6611
|
+
}
|
|
6612
|
+
return reconstructed;
|
|
6613
|
+
};
|
|
6614
|
+
|
|
6381
6615
|
// Build the unified display/execution order for dimensions + computed
|
|
6382
6616
|
// dimensions. Prefers the persisted `ordered_dimensions` field; falls back
|
|
6383
6617
|
// to "dimensions first, then computed dimensions" for report definitions
|
|
@@ -6388,7 +6622,7 @@ const ReportBuilder = ({
|
|
|
6388
6622
|
const seenComputedKeys = new Set();
|
|
6389
6623
|
if (Array.isArray(rawOrderedDimensions)) {
|
|
6390
6624
|
rawOrderedDimensions.forEach(entry => {
|
|
6391
|
-
if (entry.type === "dimension" && dimensions.some(d => d.
|
|
6625
|
+
if (entry.type === "dimension" && dimensions.some(d => d.id === entry.ref)) {
|
|
6392
6626
|
dimensionOrder.push({
|
|
6393
6627
|
type: "dimension",
|
|
6394
6628
|
key: entry.ref
|
|
@@ -6406,10 +6640,10 @@ const ReportBuilder = ({
|
|
|
6406
6640
|
|
|
6407
6641
|
// Append anything not covered by ordered_dimensions (older reports, or drift)
|
|
6408
6642
|
dimensions.forEach(d => {
|
|
6409
|
-
if (!seenDimensionKeys.has(d.
|
|
6643
|
+
if (!seenDimensionKeys.has(d.id)) {
|
|
6410
6644
|
dimensionOrder.push({
|
|
6411
6645
|
type: "dimension",
|
|
6412
|
-
key: d.
|
|
6646
|
+
key: d.id
|
|
6413
6647
|
});
|
|
6414
6648
|
}
|
|
6415
6649
|
});
|
|
@@ -6432,12 +6666,12 @@ const ReportBuilder = ({
|
|
|
6432
6666
|
// "fall back to the default order".
|
|
6433
6667
|
const buildColumnOrder = (rawOrderedColumns, dimensions, computedDimensions, metrics) => {
|
|
6434
6668
|
if (!Array.isArray(rawOrderedColumns)) return [];
|
|
6435
|
-
const
|
|
6669
|
+
const dimensionIds = new Set(dimensions.map(d => d.id));
|
|
6436
6670
|
const computedNames = new Set(computedDimensions.map(cd => cd.name));
|
|
6437
|
-
const
|
|
6671
|
+
const metricIds = new Set(metrics.map(m => m.id));
|
|
6438
6672
|
const columnOrder = [];
|
|
6439
6673
|
rawOrderedColumns.forEach(entry => {
|
|
6440
|
-
if (entry.type === "dimension" &&
|
|
6674
|
+
if (entry.type === "dimension" && dimensionIds.has(entry.ref)) {
|
|
6441
6675
|
columnOrder.push({
|
|
6442
6676
|
type: "dimension",
|
|
6443
6677
|
key: entry.ref
|
|
@@ -6447,7 +6681,7 @@ const ReportBuilder = ({
|
|
|
6447
6681
|
type: "computed",
|
|
6448
6682
|
key: entry.ref
|
|
6449
6683
|
});
|
|
6450
|
-
} else if (entry.type === "metric" &&
|
|
6684
|
+
} else if (entry.type === "metric" && metricIds.has(entry.ref)) {
|
|
6451
6685
|
columnOrder.push({
|
|
6452
6686
|
type: "metric",
|
|
6453
6687
|
key: entry.ref
|
|
@@ -6477,43 +6711,10 @@ const ReportBuilder = ({
|
|
|
6477
6711
|
// Set the title
|
|
6478
6712
|
setReportTitle(reportDef.title || "");
|
|
6479
6713
|
|
|
6480
|
-
// Reconstruct dimensions
|
|
6481
|
-
|
|
6482
|
-
const
|
|
6483
|
-
|
|
6484
|
-
// First, build a map of fullPath -> sortOrder from order_by
|
|
6485
|
-
if (reportDef.definition?.doc?.query?.order_by) {
|
|
6486
|
-
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
6487
|
-
if (orderItem.desc === true) {
|
|
6488
|
-
orderByMap[orderItem.name] = "desc";
|
|
6489
|
-
} else {
|
|
6490
|
-
orderByMap[orderItem.name] = "asc";
|
|
6491
|
-
}
|
|
6492
|
-
}
|
|
6493
|
-
}
|
|
6494
|
-
|
|
6495
|
-
// Now reconstruct dimensions and add sortOrder from the map
|
|
6496
|
-
if (reportDef.definition?.doc?.query?.dimensions) {
|
|
6497
|
-
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
6498
|
-
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
6499
|
-
if (dim) {
|
|
6500
|
-
// Add sortOrder from order_by if it exists
|
|
6501
|
-
dim.sortOrder = orderByMap[dimPath] || null;
|
|
6502
|
-
reconstructedDimensions.push(dim);
|
|
6503
|
-
}
|
|
6504
|
-
}
|
|
6505
|
-
}
|
|
6506
|
-
|
|
6507
|
-
// Reconstruct metrics
|
|
6508
|
-
const reconstructedMetrics = [];
|
|
6509
|
-
if (reportDef.definition?.doc?.query?.metrics) {
|
|
6510
|
-
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
6511
|
-
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
6512
|
-
if (metric) {
|
|
6513
|
-
reconstructedMetrics.push(metric);
|
|
6514
|
-
}
|
|
6515
|
-
}
|
|
6516
|
-
}
|
|
6714
|
+
// Reconstruct dimensions and metrics as independent instances
|
|
6715
|
+
// (preserving deliberate duplicates - see reconstructDimensions/Metrics)
|
|
6716
|
+
const reconstructedDimensions = reconstructDimensions(reportDef, providersData);
|
|
6717
|
+
const reconstructedMetrics = reconstructMetrics(reportDef, providersData);
|
|
6517
6718
|
|
|
6518
6719
|
// Load title overrides if they exist
|
|
6519
6720
|
const loadedTitleOverrides = {
|
|
@@ -6597,43 +6798,10 @@ const ReportBuilder = ({
|
|
|
6597
6798
|
// Set the title (already modified with " (Copy)" suffix)
|
|
6598
6799
|
setReportTitle(reportDef.title || "");
|
|
6599
6800
|
|
|
6600
|
-
// Reconstruct dimensions
|
|
6601
|
-
|
|
6602
|
-
const
|
|
6603
|
-
|
|
6604
|
-
// First, build a map of fullPath -> sortOrder from order_by
|
|
6605
|
-
if (reportDef.definition?.doc?.query?.order_by) {
|
|
6606
|
-
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
6607
|
-
if (orderItem.desc === true) {
|
|
6608
|
-
orderByMap[orderItem.name] = "desc";
|
|
6609
|
-
} else {
|
|
6610
|
-
orderByMap[orderItem.name] = "asc";
|
|
6611
|
-
}
|
|
6612
|
-
}
|
|
6613
|
-
}
|
|
6614
|
-
|
|
6615
|
-
// Now reconstruct dimensions and add sortOrder from the map
|
|
6616
|
-
if (reportDef.definition?.doc?.query?.dimensions) {
|
|
6617
|
-
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
6618
|
-
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
6619
|
-
if (dim) {
|
|
6620
|
-
// Add sortOrder from order_by if it exists
|
|
6621
|
-
dim.sortOrder = orderByMap[dimPath] || null;
|
|
6622
|
-
reconstructedDimensions.push(dim);
|
|
6623
|
-
}
|
|
6624
|
-
}
|
|
6625
|
-
}
|
|
6626
|
-
|
|
6627
|
-
// Reconstruct metrics
|
|
6628
|
-
const reconstructedMetrics = [];
|
|
6629
|
-
if (reportDef.definition?.doc?.query?.metrics) {
|
|
6630
|
-
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
6631
|
-
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
6632
|
-
if (metric) {
|
|
6633
|
-
reconstructedMetrics.push(metric);
|
|
6634
|
-
}
|
|
6635
|
-
}
|
|
6636
|
-
}
|
|
6801
|
+
// Reconstruct dimensions and metrics as independent instances
|
|
6802
|
+
// (preserving deliberate duplicates - see reconstructDimensions/Metrics)
|
|
6803
|
+
const reconstructedDimensions = reconstructDimensions(reportDef, providersData);
|
|
6804
|
+
const reconstructedMetrics = reconstructMetrics(reportDef, providersData);
|
|
6637
6805
|
|
|
6638
6806
|
// Load title overrides if they exist
|
|
6639
6807
|
const loadedTitleOverrides = {
|
|
@@ -6732,42 +6900,45 @@ const ReportBuilder = ({
|
|
|
6732
6900
|
});
|
|
6733
6901
|
console.log("Selected root provider:", event.target.value);
|
|
6734
6902
|
};
|
|
6735
|
-
|
|
6903
|
+
|
|
6904
|
+
// Keyed by instance id, so two instances of the same dimension can carry
|
|
6905
|
+
// independent custom titles.
|
|
6906
|
+
const handleUpdateDimensionTitle = (id, customTitle) => {
|
|
6736
6907
|
setTitleOverrides(prev => ({
|
|
6737
6908
|
...prev,
|
|
6738
6909
|
dimensions: {
|
|
6739
6910
|
...prev.dimensions,
|
|
6740
|
-
[
|
|
6911
|
+
[id]: customTitle
|
|
6741
6912
|
}
|
|
6742
6913
|
}));
|
|
6743
6914
|
};
|
|
6744
|
-
const handleResetDimensionTitle =
|
|
6915
|
+
const handleResetDimensionTitle = id => {
|
|
6745
6916
|
setTitleOverrides(prev => {
|
|
6746
6917
|
const newDimensions = {
|
|
6747
6918
|
...prev.dimensions
|
|
6748
6919
|
};
|
|
6749
|
-
delete newDimensions[
|
|
6920
|
+
delete newDimensions[id];
|
|
6750
6921
|
return {
|
|
6751
6922
|
...prev,
|
|
6752
6923
|
dimensions: newDimensions
|
|
6753
6924
|
};
|
|
6754
6925
|
});
|
|
6755
6926
|
};
|
|
6756
|
-
const handleUpdateMetricTitle = (
|
|
6927
|
+
const handleUpdateMetricTitle = (id, customTitle) => {
|
|
6757
6928
|
setTitleOverrides(prev => ({
|
|
6758
6929
|
...prev,
|
|
6759
6930
|
metrics: {
|
|
6760
6931
|
...prev.metrics,
|
|
6761
|
-
[
|
|
6932
|
+
[id]: customTitle
|
|
6762
6933
|
}
|
|
6763
6934
|
}));
|
|
6764
6935
|
};
|
|
6765
|
-
const handleResetMetricTitle =
|
|
6936
|
+
const handleResetMetricTitle = id => {
|
|
6766
6937
|
setTitleOverrides(prev => {
|
|
6767
6938
|
const newMetrics = {
|
|
6768
6939
|
...prev.metrics
|
|
6769
6940
|
};
|
|
6770
|
-
delete newMetrics[
|
|
6941
|
+
delete newMetrics[id];
|
|
6771
6942
|
return {
|
|
6772
6943
|
...prev,
|
|
6773
6944
|
metrics: newMetrics
|
|
@@ -6802,11 +6973,11 @@ const ReportBuilder = ({
|
|
|
6802
6973
|
dimensions: [...prev.dimensions, dimensionData],
|
|
6803
6974
|
dimensionOrder: [...prev.dimensionOrder, {
|
|
6804
6975
|
type: "dimension",
|
|
6805
|
-
key: dimensionData.
|
|
6976
|
+
key: dimensionData.id
|
|
6806
6977
|
}],
|
|
6807
6978
|
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6808
6979
|
type: "dimension",
|
|
6809
|
-
key: dimensionData.
|
|
6980
|
+
key: dimensionData.id
|
|
6810
6981
|
})
|
|
6811
6982
|
};
|
|
6812
6983
|
console.log("Dimension saved:", dimensionData);
|
|
@@ -6814,22 +6985,30 @@ const ReportBuilder = ({
|
|
|
6814
6985
|
return newReport;
|
|
6815
6986
|
});
|
|
6816
6987
|
};
|
|
6817
|
-
const handleRemoveDimension =
|
|
6988
|
+
const handleRemoveDimension = id => {
|
|
6818
6989
|
setReport(prev => ({
|
|
6819
6990
|
...prev,
|
|
6820
|
-
dimensions: prev.dimensions.filter(d => d.
|
|
6821
|
-
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key ===
|
|
6822
|
-
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "dimension" && entry.key ===
|
|
6991
|
+
dimensions: prev.dimensions.filter(d => d.id !== id),
|
|
6992
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === id)),
|
|
6993
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "dimension" && entry.key === id))
|
|
6823
6994
|
}));
|
|
6824
6995
|
};
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6996
|
+
|
|
6997
|
+
// sortOrder is fundamentally a per-field (query-level) setting - SQL can
|
|
6998
|
+
// only sort by a given column once - so toggling it on any instance of a
|
|
6999
|
+
// duplicated dimension applies to every instance sharing that fullPath.
|
|
7000
|
+
const handleUpdateDimensionSortOrder = (id, sortOrder) => {
|
|
7001
|
+
setReport(prev => {
|
|
7002
|
+
const target = prev.dimensions.find(d => d.id === id);
|
|
7003
|
+
if (!target) return prev;
|
|
7004
|
+
return {
|
|
7005
|
+
...prev,
|
|
7006
|
+
dimensions: prev.dimensions.map(d => d.fullPath === target.fullPath ? {
|
|
7007
|
+
...d,
|
|
7008
|
+
sortOrder
|
|
7009
|
+
} : d)
|
|
7010
|
+
};
|
|
7011
|
+
});
|
|
6833
7012
|
};
|
|
6834
7013
|
|
|
6835
7014
|
// Reorders the single unified dimensions + computed-dimensions list;
|
|
@@ -6884,7 +7063,7 @@ const ReportBuilder = ({
|
|
|
6884
7063
|
metrics: [...prev.metrics, metricData],
|
|
6885
7064
|
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6886
7065
|
type: "metric",
|
|
6887
|
-
key: metricData.
|
|
7066
|
+
key: metricData.id
|
|
6888
7067
|
})
|
|
6889
7068
|
};
|
|
6890
7069
|
console.log("Metric saved:", metricData);
|
|
@@ -6892,15 +7071,12 @@ const ReportBuilder = ({
|
|
|
6892
7071
|
return newReport;
|
|
6893
7072
|
});
|
|
6894
7073
|
};
|
|
6895
|
-
const handleRemoveMetric =
|
|
6896
|
-
setReport(prev => {
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "metric" && entry.key === fullPath))
|
|
6902
|
-
};
|
|
6903
|
-
});
|
|
7074
|
+
const handleRemoveMetric = id => {
|
|
7075
|
+
setReport(prev => ({
|
|
7076
|
+
...prev,
|
|
7077
|
+
metrics: prev.metrics.filter(m => m.id !== id),
|
|
7078
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "metric" && entry.key === id))
|
|
7079
|
+
}));
|
|
6904
7080
|
};
|
|
6905
7081
|
const handleReorderMetrics = newOrder => {
|
|
6906
7082
|
setReport(prev => ({
|
|
@@ -6955,13 +7131,30 @@ const ReportBuilder = ({
|
|
|
6955
7131
|
// persisted `ordered_dimensions` field all stay consistent with whatever
|
|
6956
7132
|
// order the user arranged on screen.
|
|
6957
7133
|
const buildQueryObject = () => {
|
|
6958
|
-
const
|
|
7134
|
+
const dimensionById = Object.fromEntries(report.dimensions.map(d => [d.id, d]));
|
|
6959
7135
|
const computedByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
6960
|
-
|
|
7136
|
+
|
|
7137
|
+
// May contain more than one instance of the same fullPath (deliberate
|
|
7138
|
+
// duplicates), kept in dimensionOrder's display order.
|
|
7139
|
+
const orderedDimensionsOnly = report.dimensionOrder.filter(entry => entry.type === "dimension").map(entry => dimensionById[entry.key]).filter(Boolean);
|
|
6961
7140
|
const orderedComputedOnly = report.dimensionOrder.filter(entry => entry.type === "computed").map(entry => computedByName[entry.key]).filter(Boolean);
|
|
6962
7141
|
|
|
7142
|
+
// The API keys each result row by fullPath, so duplicate instances of
|
|
7143
|
+
// the same field can only ever carry one value/one sort direction at
|
|
7144
|
+
// the query level - dedupe by fullPath (keeping first-occurrence order)
|
|
7145
|
+
// before sending. The builder still shows/orders/titles each instance
|
|
7146
|
+
// independently; they just read from the same underlying query column.
|
|
7147
|
+
const uniqueDimensionsByPath = [];
|
|
7148
|
+
const seenDimensionPaths = new Set();
|
|
7149
|
+
orderedDimensionsOnly.forEach(dim => {
|
|
7150
|
+
if (!seenDimensionPaths.has(dim.fullPath)) {
|
|
7151
|
+
seenDimensionPaths.add(dim.fullPath);
|
|
7152
|
+
uniqueDimensionsByPath.push(dim);
|
|
7153
|
+
}
|
|
7154
|
+
});
|
|
7155
|
+
|
|
6963
7156
|
// Build order_by array - include all dimensions, add desc property only when needed
|
|
6964
|
-
const orderBy =
|
|
7157
|
+
const orderBy = uniqueDimensionsByPath.map(dim => {
|
|
6965
7158
|
const orderItem = {
|
|
6966
7159
|
name: dim.fullPath
|
|
6967
7160
|
};
|
|
@@ -7016,9 +7209,20 @@ const ReportBuilder = ({
|
|
|
7016
7209
|
if (Object.keys(titleOverrides.filters).length > 0) {
|
|
7017
7210
|
titles.filters = titleOverrides.filters;
|
|
7018
7211
|
}
|
|
7212
|
+
|
|
7213
|
+
// Same dedupe rationale as dimensions above: the query only needs each
|
|
7214
|
+
// metric fullPath once, however many display instances reference it.
|
|
7215
|
+
const uniqueMetricPaths = [];
|
|
7216
|
+
const seenMetricPaths = new Set();
|
|
7217
|
+
report.metrics.forEach(metric => {
|
|
7218
|
+
if (!seenMetricPaths.has(metric.fullPath)) {
|
|
7219
|
+
seenMetricPaths.add(metric.fullPath);
|
|
7220
|
+
uniqueMetricPaths.push(metric.fullPath);
|
|
7221
|
+
}
|
|
7222
|
+
});
|
|
7019
7223
|
const queryObj = {
|
|
7020
|
-
dimensions:
|
|
7021
|
-
metrics:
|
|
7224
|
+
dimensions: uniqueDimensionsByPath.map(dim => dim.fullPath),
|
|
7225
|
+
metrics: uniqueMetricPaths,
|
|
7022
7226
|
order_by: orderBy
|
|
7023
7227
|
};
|
|
7024
7228
|
|
|
@@ -7032,7 +7236,9 @@ const ReportBuilder = ({
|
|
|
7032
7236
|
|
|
7033
7237
|
// Persist the unified dimension + computed-dimension order so it can be
|
|
7034
7238
|
// reconstructed on reload and so executed reports render columns in the
|
|
7035
|
-
// same order they were arranged in the builder.
|
|
7239
|
+
// same order they were arranged in the builder. Entries use each
|
|
7240
|
+
// dimension's instance id as `ref`, so deliberate duplicates (the nth
|
|
7241
|
+
// occurrence of a fullPath is suffixed `#n`) round-trip correctly.
|
|
7036
7242
|
if (report.dimensionOrder.length > 0) {
|
|
7037
7243
|
queryObj.ordered_dimensions = report.dimensionOrder.map(entry => ({
|
|
7038
7244
|
type: entry.type,
|
|
@@ -7040,14 +7246,30 @@ const ReportBuilder = ({
|
|
|
7040
7246
|
}));
|
|
7041
7247
|
}
|
|
7042
7248
|
|
|
7043
|
-
// Persist
|
|
7044
|
-
//
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7249
|
+
// Persist metric instance order/identity the same way (metrics have no
|
|
7250
|
+
// other unified-order field to piggyback on).
|
|
7251
|
+
if (report.metrics.length > 0) {
|
|
7252
|
+
queryObj.ordered_metrics = report.metrics.map(metric => metric.id);
|
|
7253
|
+
}
|
|
7254
|
+
|
|
7255
|
+
// Persist the full resolved column order - the explicit Column Order tab
|
|
7256
|
+
// arrangement when defined, otherwise dimensions/computed (in their own
|
|
7257
|
+
// order) followed by metrics (in their own order), same as what the
|
|
7258
|
+
// results grid renders. Always emitted (not just when a custom order
|
|
7259
|
+
// exists) so the API/results grid get an explicit order even for
|
|
7260
|
+
// untouched reports, rather than having to fall back to guessing it.
|
|
7261
|
+
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
7262
|
+
kind: "dimension",
|
|
7263
|
+
data: dimensionById[entry.key]
|
|
7264
|
+
} : {
|
|
7265
|
+
kind: "computed",
|
|
7266
|
+
data: computedByName[entry.key]
|
|
7267
|
+
}).filter(item => item.data);
|
|
7268
|
+
const resolvedColumnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
|
|
7269
|
+
if (resolvedColumnItems.length > 0) {
|
|
7270
|
+
queryObj.ordered_columns = resolvedColumnItems.map(item => ({
|
|
7271
|
+
type: item.kind,
|
|
7272
|
+
ref: item.key
|
|
7051
7273
|
}));
|
|
7052
7274
|
}
|
|
7053
7275
|
|
|
@@ -7209,11 +7431,11 @@ const ReportBuilder = ({
|
|
|
7209
7431
|
|
|
7210
7432
|
// Resolve the unified dimensionOrder into actual dimension/computed-dimension
|
|
7211
7433
|
// objects so the results grid can render columns in that same order.
|
|
7212
|
-
const
|
|
7434
|
+
const dimensionById = Object.fromEntries(report.dimensions.map(d => [d.id, d]));
|
|
7213
7435
|
const computedDimensionByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
7214
7436
|
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
7215
7437
|
kind: "dimension",
|
|
7216
|
-
data:
|
|
7438
|
+
data: dimensionById[entry.key]
|
|
7217
7439
|
} : {
|
|
7218
7440
|
kind: "computed",
|
|
7219
7441
|
data: computedDimensionByName[entry.key]
|