@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
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,
|
|
@@ -77715,6 +77759,16 @@ const SortableComputedChip = ({
|
|
|
77715
77759
|
handleCancel();
|
|
77716
77760
|
}
|
|
77717
77761
|
};
|
|
77762
|
+
|
|
77763
|
+
// The value field is multiline, so Enter inserts a newline instead of
|
|
77764
|
+
// saving; only Escape (cancel) and Cmd/Ctrl+Enter (save) are handled here.
|
|
77765
|
+
const handleValueKeyDown = e => {
|
|
77766
|
+
if (e.key === "Escape") {
|
|
77767
|
+
handleCancel();
|
|
77768
|
+
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
77769
|
+
handleSave();
|
|
77770
|
+
}
|
|
77771
|
+
};
|
|
77718
77772
|
return /*#__PURE__*/gn.createElement("div", _extends({
|
|
77719
77773
|
ref: setNodeRef,
|
|
77720
77774
|
style: style
|
|
@@ -77818,10 +77872,13 @@ const SortableComputedChip = ({
|
|
|
77818
77872
|
}), /*#__PURE__*/gn.createElement(TextField, {
|
|
77819
77873
|
value: editValue,
|
|
77820
77874
|
onChange: e => setEditValue(e.target.value),
|
|
77821
|
-
onKeyDown:
|
|
77875
|
+
onKeyDown: handleValueKeyDown,
|
|
77822
77876
|
size: "small",
|
|
77823
77877
|
label: "Value",
|
|
77824
|
-
helperText: " ",
|
|
77878
|
+
helperText: "Cmd/Ctrl+Enter to save",
|
|
77879
|
+
multiline: true,
|
|
77880
|
+
minRows: 2,
|
|
77881
|
+
maxRows: 10,
|
|
77825
77882
|
sx: {
|
|
77826
77883
|
flex: 1,
|
|
77827
77884
|
minWidth: "200px",
|
|
@@ -77845,7 +77902,7 @@ const SortableComputedChip = ({
|
|
|
77845
77902
|
onClick: handleSave,
|
|
77846
77903
|
color: "primary",
|
|
77847
77904
|
"aria-label": "save computed dimension",
|
|
77848
|
-
disabled: !editName.trim() ||
|
|
77905
|
+
disabled: !editName.trim() || nameError
|
|
77849
77906
|
}, /*#__PURE__*/gn.createElement(CheckIcon, {
|
|
77850
77907
|
fontSize: "small"
|
|
77851
77908
|
})))), /*#__PURE__*/gn.createElement(Tooltip, {
|
|
@@ -77894,12 +77951,14 @@ const Dimensions = ({
|
|
|
77894
77951
|
// Resolve the unified dimensionOrder into the actual dimension / computed
|
|
77895
77952
|
// dimension objects, so dimensions and computed dimensions can be shown
|
|
77896
77953
|
// (and dragged) as a single list while staying stored in their own arrays.
|
|
77897
|
-
|
|
77954
|
+
// Keyed by instance id rather than fullPath, so two instances of the same
|
|
77955
|
+
// dimension (added deliberately as a duplicate) stay distinct entries.
|
|
77956
|
+
const dimensionById = Object.fromEntries(savedDimensions.map(d => [d.id, d]));
|
|
77898
77957
|
const computedDimensionByName = Object.fromEntries(savedComputedDimensions.map(cd => [cd.name, cd]));
|
|
77899
77958
|
const orderedItems = dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
77900
77959
|
kind: "dimension",
|
|
77901
77960
|
key: entry.key,
|
|
77902
|
-
data:
|
|
77961
|
+
data: dimensionById[entry.key]
|
|
77903
77962
|
} : {
|
|
77904
77963
|
kind: "computed",
|
|
77905
77964
|
key: entry.key,
|
|
@@ -77913,9 +77972,13 @@ const Dimensions = ({
|
|
|
77913
77972
|
handleMoveDown
|
|
77914
77973
|
} = makeListReorderHandlers(dimensionOrder, onReorderDimensionItems);
|
|
77915
77974
|
|
|
77916
|
-
// Handle sort order change (dimensions only - computed dimensions have no
|
|
77917
|
-
|
|
77918
|
-
|
|
77975
|
+
// Handle sort order change (dimensions only - computed dimensions have no
|
|
77976
|
+
// order_by). Keyed by instance id but applied query-wide: SQL can only
|
|
77977
|
+
// sort by a given column once, so toggling sort on any duplicate instance
|
|
77978
|
+
// of the same field is propagated to all instances sharing its fullPath
|
|
77979
|
+
// (handled in ReportBuilder's onUpdateDimensionSortOrder).
|
|
77980
|
+
const handleSortOrderChange = (id, sortOrder) => {
|
|
77981
|
+
onUpdateDimensionSortOrder(id, sortOrder);
|
|
77919
77982
|
};
|
|
77920
77983
|
|
|
77921
77984
|
// Computed Dimensions: { name, value } pairs independent of any provider.
|
|
@@ -77936,7 +77999,7 @@ const Dimensions = ({
|
|
|
77936
77999
|
const handleSaveComputed = () => {
|
|
77937
78000
|
const trimmedName = computedName.trim();
|
|
77938
78001
|
const trimmedValue = computedValue.trim();
|
|
77939
|
-
if (!trimmedName
|
|
78002
|
+
if (!trimmedName) return;
|
|
77940
78003
|
if (isComputedNameTaken(trimmedName)) return;
|
|
77941
78004
|
onSaveComputedDimension({
|
|
77942
78005
|
name: trimmedName,
|
|
@@ -77978,7 +78041,10 @@ const Dimensions = ({
|
|
|
77978
78041
|
currentProviderKey = selection.targetKey;
|
|
77979
78042
|
});
|
|
77980
78043
|
|
|
77981
|
-
// Create a Set of already selected dimension fullPaths for quick lookup
|
|
78044
|
+
// Create a Set of already selected dimension fullPaths for quick lookup.
|
|
78045
|
+
// Already-selected items stay selectable (so the same dimension can be
|
|
78046
|
+
// added again on purpose) - they're just flagged so the dropdown can
|
|
78047
|
+
// highlight them instead of blocking the click.
|
|
77982
78048
|
const selectedFullPaths = new Set(savedDimensions.map(dim => dim.fullPath));
|
|
77983
78049
|
const items = [];
|
|
77984
78050
|
// Iterate through aliases (e.g., 'ba', 'pc')
|
|
@@ -77990,9 +78056,6 @@ const Dimensions = ({
|
|
|
77990
78056
|
|
|
77991
78057
|
// Construct the fullPath for this dimension
|
|
77992
78058
|
const fullPath = `${aliasPath.join("_")}.${dimKey}`;
|
|
77993
|
-
|
|
77994
|
-
// Check if this dimension is already selected
|
|
77995
|
-
const isAlreadySelected = selectedFullPaths.has(fullPath);
|
|
77996
78059
|
items.push({
|
|
77997
78060
|
// Include provider name to ensure uniqueness across different providers
|
|
77998
78061
|
key: `${currentProvider}_${alias}.${dimKey}`,
|
|
@@ -78000,7 +78063,7 @@ const Dimensions = ({
|
|
|
78000
78063
|
dimensionKey: dimKey,
|
|
78001
78064
|
alias: alias,
|
|
78002
78065
|
dimension: dimension,
|
|
78003
|
-
|
|
78066
|
+
alreadySelected: selectedFullPaths.has(fullPath)
|
|
78004
78067
|
});
|
|
78005
78068
|
});
|
|
78006
78069
|
});
|
|
@@ -78062,7 +78125,10 @@ const Dimensions = ({
|
|
|
78062
78125
|
dimensionKey: selectedItem.dimensionKey,
|
|
78063
78126
|
dimensionTitle: selectedItem.value,
|
|
78064
78127
|
// The constructed path for server
|
|
78065
|
-
fullPath: fullPath
|
|
78128
|
+
fullPath: fullPath,
|
|
78129
|
+
// Unique per-instance identity, so this field can be added more than
|
|
78130
|
+
// once and each occurrence stays independently orderable/removable.
|
|
78131
|
+
id: nextInstanceId(fullPath, savedDimensions)
|
|
78066
78132
|
};
|
|
78067
78133
|
onSaveDimension(dimensionData);
|
|
78068
78134
|
handleCancel();
|
|
@@ -78073,8 +78139,9 @@ const Dimensions = ({
|
|
|
78073
78139
|
const handleAddAll = () => {
|
|
78074
78140
|
const dimensionItems = getDimensionItems();
|
|
78075
78141
|
|
|
78076
|
-
// Filter out already selected dimensions
|
|
78077
|
-
|
|
78142
|
+
// Filter out already selected dimensions - "Add all" only fills in
|
|
78143
|
+
// missing ones; intentional duplicates are added one at a time above.
|
|
78144
|
+
const availableItems = dimensionItems.filter(item => !item.alreadySelected);
|
|
78078
78145
|
if (availableItems.length === 0) return;
|
|
78079
78146
|
|
|
78080
78147
|
// Build the complete relation objects array (same for all dimensions in this provider)
|
|
@@ -78119,7 +78186,10 @@ const Dimensions = ({
|
|
|
78119
78186
|
dimensionKey: item.dimensionKey,
|
|
78120
78187
|
dimensionTitle: item.value,
|
|
78121
78188
|
// The constructed path for server
|
|
78122
|
-
fullPath: fullPath
|
|
78189
|
+
fullPath: fullPath,
|
|
78190
|
+
// "Add all" only ever adds fields that aren't already selected, so
|
|
78191
|
+
// this is always the first (and only) occurrence of this fullPath.
|
|
78192
|
+
id: fullPath
|
|
78123
78193
|
};
|
|
78124
78194
|
onSaveDimension(dimensionData);
|
|
78125
78195
|
});
|
|
@@ -78218,6 +78288,9 @@ const Dimensions = ({
|
|
|
78218
78288
|
value: computedValue,
|
|
78219
78289
|
onChange: e => setComputedValue(e.target.value),
|
|
78220
78290
|
size: "small",
|
|
78291
|
+
multiline: true,
|
|
78292
|
+
minRows: 2,
|
|
78293
|
+
maxRows: 10,
|
|
78221
78294
|
sx: {
|
|
78222
78295
|
width: "400px",
|
|
78223
78296
|
"& .MuiInputBase-input": {
|
|
@@ -78253,7 +78326,7 @@ const Dimensions = ({
|
|
|
78253
78326
|
}, "Cancel"), /*#__PURE__*/gn.createElement(Button, {
|
|
78254
78327
|
variant: "contained",
|
|
78255
78328
|
onClick: handleSaveComputed,
|
|
78256
|
-
disabled: !computedName.trim() ||
|
|
78329
|
+
disabled: !computedName.trim() || computedNameError,
|
|
78257
78330
|
sx: {
|
|
78258
78331
|
height: "40px",
|
|
78259
78332
|
fontFamily: "system-ui",
|
|
@@ -78329,7 +78402,7 @@ const Dimensions = ({
|
|
|
78329
78402
|
placement: "top"
|
|
78330
78403
|
}, /*#__PURE__*/gn.createElement("span", null, /*#__PURE__*/gn.createElement(IconButton, {
|
|
78331
78404
|
onClick: handleAddAll,
|
|
78332
|
-
disabled: getDimensionItems().filter(item => !item.
|
|
78405
|
+
disabled: getDimensionItems().filter(item => !item.alreadySelected).length === 0,
|
|
78333
78406
|
"aria-label": "add all dimensions",
|
|
78334
78407
|
sx: {
|
|
78335
78408
|
color: "rgb(70, 134, 128)",
|
|
@@ -78420,9 +78493,9 @@ const Dimensions = ({
|
|
|
78420
78493
|
isLast: index === orderedItems.length - 1,
|
|
78421
78494
|
sortOrder: item.data.sortOrder || null,
|
|
78422
78495
|
onSortOrderChange: sortOrder => handleSortOrderChange(item.key, sortOrder),
|
|
78423
|
-
|
|
78496
|
+
instanceId: item.data.id,
|
|
78424
78497
|
defaultTitle: item.data.dimensionTitle,
|
|
78425
|
-
customTitle: titleOverrides[item.data.
|
|
78498
|
+
customTitle: titleOverrides[item.data.id],
|
|
78426
78499
|
onUpdateTitle: onUpdateTitle,
|
|
78427
78500
|
onResetTitle: onResetTitle
|
|
78428
78501
|
}) : /*#__PURE__*/gn.createElement(SortableComputedChip, {
|
|
@@ -78507,7 +78580,7 @@ const SortableChip = ({
|
|
|
78507
78580
|
onMoveDown,
|
|
78508
78581
|
isFirst,
|
|
78509
78582
|
isLast,
|
|
78510
|
-
|
|
78583
|
+
instanceId,
|
|
78511
78584
|
defaultTitle,
|
|
78512
78585
|
customTitle,
|
|
78513
78586
|
onUpdateTitle,
|
|
@@ -78560,7 +78633,7 @@ const SortableChip = ({
|
|
|
78560
78633
|
handleCancel();
|
|
78561
78634
|
return;
|
|
78562
78635
|
}
|
|
78563
|
-
onUpdateTitle(
|
|
78636
|
+
onUpdateTitle(instanceId, trimmedValue);
|
|
78564
78637
|
setIsEditing(false);
|
|
78565
78638
|
};
|
|
78566
78639
|
const handleCancel = () => {
|
|
@@ -78568,7 +78641,7 @@ const SortableChip = ({
|
|
|
78568
78641
|
setEditValue('');
|
|
78569
78642
|
};
|
|
78570
78643
|
const handleReset = () => {
|
|
78571
|
-
onResetTitle(
|
|
78644
|
+
onResetTitle(instanceId);
|
|
78572
78645
|
setIsEditing(false);
|
|
78573
78646
|
setEditValue('');
|
|
78574
78647
|
};
|
|
@@ -78789,7 +78862,10 @@ const Metrics = ({
|
|
|
78789
78862
|
currentProviderKey = selection.targetKey;
|
|
78790
78863
|
});
|
|
78791
78864
|
|
|
78792
|
-
// Create a Set of already selected metric fullPaths for quick lookup
|
|
78865
|
+
// Create a Set of already selected metric fullPaths for quick lookup.
|
|
78866
|
+
// Already-selected items stay selectable (so the same metric can be
|
|
78867
|
+
// added again on purpose) - they're just flagged so the dropdown can
|
|
78868
|
+
// highlight them instead of blocking the click.
|
|
78793
78869
|
const selectedFullPaths = new Set(savedMetrics.map(metric => metric.fullPath));
|
|
78794
78870
|
|
|
78795
78871
|
// Metrics are a direct array, not nested by alias
|
|
@@ -78797,15 +78873,12 @@ const Metrics = ({
|
|
|
78797
78873
|
const items = provider.metrics.map((metric, index) => {
|
|
78798
78874
|
// Construct the fullPath for this metric
|
|
78799
78875
|
const fullPath = `${aliasPath.join('_')}.${metric.name}`;
|
|
78800
|
-
|
|
78801
|
-
// Check if this metric is already selected
|
|
78802
|
-
const isAlreadySelected = selectedFullPaths.has(fullPath);
|
|
78803
78876
|
return {
|
|
78804
78877
|
key: `${currentProvider}_${metric.name}_${index}`,
|
|
78805
78878
|
value: metric.title || metric.name,
|
|
78806
78879
|
metricName: metric.name,
|
|
78807
78880
|
metric: metric,
|
|
78808
|
-
|
|
78881
|
+
alreadySelected: selectedFullPaths.has(fullPath),
|
|
78809
78882
|
icon: metric.source ? /*#__PURE__*/gn.createElement(MetricSourceIcon, {
|
|
78810
78883
|
source: metric.source
|
|
78811
78884
|
}) : undefined
|
|
@@ -78868,7 +78941,11 @@ const Metrics = ({
|
|
|
78868
78941
|
metricName: selectedItem.metricName,
|
|
78869
78942
|
metricTitle: selectedItem.value,
|
|
78870
78943
|
// The constructed path for server
|
|
78871
|
-
fullPath: fullPath
|
|
78944
|
+
fullPath: fullPath,
|
|
78945
|
+
// Unique per-instance identity, so this field can be added more
|
|
78946
|
+
// than once and each occurrence stays independently
|
|
78947
|
+
// orderable/removable/titleable.
|
|
78948
|
+
id: nextInstanceId(fullPath, savedMetrics)
|
|
78872
78949
|
};
|
|
78873
78950
|
onSaveMetric(metricData);
|
|
78874
78951
|
handleCancel();
|
|
@@ -78879,8 +78956,9 @@ const Metrics = ({
|
|
|
78879
78956
|
const handleAddAll = () => {
|
|
78880
78957
|
const metricItems = getMetricItems();
|
|
78881
78958
|
|
|
78882
|
-
// Filter out already selected metrics
|
|
78883
|
-
|
|
78959
|
+
// Filter out already selected metrics - "Add all" only fills in
|
|
78960
|
+
// missing ones; intentional duplicates are added one at a time above.
|
|
78961
|
+
const availableItems = metricItems.filter(item => !item.alreadySelected);
|
|
78884
78962
|
if (availableItems.length === 0) return;
|
|
78885
78963
|
|
|
78886
78964
|
// Build the complete relation objects array (same for all metrics in this provider)
|
|
@@ -78925,7 +79003,10 @@ const Metrics = ({
|
|
|
78925
79003
|
metricName: item.metricName,
|
|
78926
79004
|
metricTitle: item.value,
|
|
78927
79005
|
// The constructed path for server
|
|
78928
|
-
fullPath: fullPath
|
|
79006
|
+
fullPath: fullPath,
|
|
79007
|
+
// "Add all" only ever adds fields that aren't already
|
|
79008
|
+
// selected, so this is always the first occurrence.
|
|
79009
|
+
id: fullPath
|
|
78929
79010
|
};
|
|
78930
79011
|
onSaveMetric(metricData);
|
|
78931
79012
|
});
|
|
@@ -79024,7 +79105,7 @@ const Metrics = ({
|
|
|
79024
79105
|
placement: "top"
|
|
79025
79106
|
}, /*#__PURE__*/gn.createElement("span", null, /*#__PURE__*/gn.createElement(IconButton, {
|
|
79026
79107
|
onClick: handleAddAll,
|
|
79027
|
-
disabled: getMetricItems().filter(item => !item.
|
|
79108
|
+
disabled: getMetricItems().filter(item => !item.alreadySelected).length === 0,
|
|
79028
79109
|
"aria-label": "add all metrics",
|
|
79029
79110
|
sx: {
|
|
79030
79111
|
color: "rgb(70, 134, 128)",
|
|
@@ -79102,14 +79183,14 @@ const Metrics = ({
|
|
|
79102
79183
|
id: index,
|
|
79103
79184
|
label: metric.metricTitle,
|
|
79104
79185
|
fullLabel: `${formatProviderPath(metric)} → ${metric.metricTitle} (${metric.fullPath})`,
|
|
79105
|
-
onDelete: () => onRemoveMetric(
|
|
79186
|
+
onDelete: () => onRemoveMetric(metric.id),
|
|
79106
79187
|
onMoveUp: () => handleMoveUp(index),
|
|
79107
79188
|
onMoveDown: () => handleMoveDown(index),
|
|
79108
79189
|
isFirst: index === 0,
|
|
79109
79190
|
isLast: index === savedMetrics.length - 1,
|
|
79110
|
-
|
|
79191
|
+
instanceId: metric.id,
|
|
79111
79192
|
defaultTitle: metric.metricTitle,
|
|
79112
|
-
customTitle: titleOverrides[metric.
|
|
79193
|
+
customTitle: titleOverrides[metric.id],
|
|
79113
79194
|
onUpdateTitle: onUpdateTitle,
|
|
79114
79195
|
onResetTitle: onResetTitle,
|
|
79115
79196
|
source: metric.metric?.source
|
|
@@ -90452,6 +90533,50 @@ class AdapterDayjs {
|
|
|
90452
90533
|
};
|
|
90453
90534
|
}
|
|
90454
90535
|
|
|
90536
|
+
// Small text input + button for adding a filter value that isn't present in
|
|
90537
|
+
// the "Select Filter Values" dropdown (e.g. because no existing row has that
|
|
90538
|
+
// value). Kept as an uncontrolled local field so it clears itself after add.
|
|
90539
|
+
const CustomFilterValueInput = ({
|
|
90540
|
+
onAdd
|
|
90541
|
+
}) => {
|
|
90542
|
+
const [value, setValue] = d('');
|
|
90543
|
+
const handleAdd = () => {
|
|
90544
|
+
const trimmed = value.trim();
|
|
90545
|
+
if (!trimmed) return;
|
|
90546
|
+
onAdd(trimmed);
|
|
90547
|
+
setValue('');
|
|
90548
|
+
};
|
|
90549
|
+
return /*#__PURE__*/gn.createElement(Box, {
|
|
90550
|
+
sx: {
|
|
90551
|
+
display: 'flex',
|
|
90552
|
+
gap: 1,
|
|
90553
|
+
mt: 1
|
|
90554
|
+
}
|
|
90555
|
+
}, /*#__PURE__*/gn.createElement(TextField, {
|
|
90556
|
+
size: "small",
|
|
90557
|
+
fullWidth: true,
|
|
90558
|
+
placeholder: "Add a value not in the list above",
|
|
90559
|
+
value: value,
|
|
90560
|
+
onChange: e => setValue(e.target.value),
|
|
90561
|
+
onKeyDown: e => {
|
|
90562
|
+
if (e.key === 'Enter') {
|
|
90563
|
+
e.preventDefault();
|
|
90564
|
+
handleAdd();
|
|
90565
|
+
}
|
|
90566
|
+
}
|
|
90567
|
+
}), /*#__PURE__*/gn.createElement(Button, {
|
|
90568
|
+
variant: "outlined",
|
|
90569
|
+
size: "small",
|
|
90570
|
+
onClick: handleAdd,
|
|
90571
|
+
disabled: !value.trim(),
|
|
90572
|
+
sx: {
|
|
90573
|
+
textTransform: 'none',
|
|
90574
|
+
whiteSpace: 'nowrap',
|
|
90575
|
+
borderColor: "rgb(70, 134, 128)",
|
|
90576
|
+
color: "rgb(70, 134, 128)"
|
|
90577
|
+
}
|
|
90578
|
+
}, "Add"));
|
|
90579
|
+
};
|
|
90455
90580
|
const Filters = ({
|
|
90456
90581
|
providersData,
|
|
90457
90582
|
rootProvider,
|
|
@@ -90591,8 +90716,9 @@ const Filters = ({
|
|
|
90591
90716
|
}
|
|
90592
90717
|
} else {
|
|
90593
90718
|
// For regular filters, set selected values and fetch available values
|
|
90594
|
-
|
|
90595
|
-
|
|
90719
|
+
const currentValues = filter.values || [];
|
|
90720
|
+
setSelectedFilterValues(currentValues);
|
|
90721
|
+
await fetchFilterValues(fullPath, '', currentValues);
|
|
90596
90722
|
}
|
|
90597
90723
|
};
|
|
90598
90724
|
const handleSaveEditedFilter = fullPath => {
|
|
@@ -90687,7 +90813,11 @@ const Filters = ({
|
|
|
90687
90813
|
};
|
|
90688
90814
|
|
|
90689
90815
|
// Fetch distinct values for the selected dimension
|
|
90690
|
-
|
|
90816
|
+
// preserveValues lets a caller pass the values that must survive the merge
|
|
90817
|
+
// explicitly, instead of relying on the selectedFilterValues closure - a
|
|
90818
|
+
// setSelectedFilterValues() call right before fetchFilterValues() hasn't
|
|
90819
|
+
// committed yet by the time this function reads that state.
|
|
90820
|
+
const fetchFilterValues = async (fullPath, searchText = '', preserveValues = null) => {
|
|
90691
90821
|
setLoadingFilterValues(true);
|
|
90692
90822
|
try {
|
|
90693
90823
|
// Get parameters from context if available, otherwise use default
|
|
@@ -90760,7 +90890,8 @@ const Filters = ({
|
|
|
90760
90890
|
// Merge with currently selected values to ensure they're always available
|
|
90761
90891
|
// This is important when editing filters with search text - we don't want to lose
|
|
90762
90892
|
// previously selected values that don't match the current search
|
|
90763
|
-
const
|
|
90893
|
+
const valuesToPreserve = preserveValues !== null ? preserveValues : selectedFilterValues;
|
|
90894
|
+
const selectedValueItems = valuesToPreserve.map(key => ({
|
|
90764
90895
|
key: String(key),
|
|
90765
90896
|
value: String(key)
|
|
90766
90897
|
}));
|
|
@@ -90989,6 +91120,21 @@ const Filters = ({
|
|
|
90989
91120
|
}
|
|
90990
91121
|
};
|
|
90991
91122
|
|
|
91123
|
+
// Add a value typed in the "custom value" box directly into the selected
|
|
91124
|
+
// filter values, and inject it into availableFilterValues too, since
|
|
91125
|
+
// CheckboxMultiAutocomplete only renders a chip for keys it can resolve
|
|
91126
|
+
// against its items list.
|
|
91127
|
+
const handleAddCustomFilterValue = rawValue => {
|
|
91128
|
+
const value = rawValue.trim();
|
|
91129
|
+
if (!value) return;
|
|
91130
|
+
if (selectedFilterValues.includes(value)) return;
|
|
91131
|
+
setAvailableFilterValues(prev => prev.some(item => item.key === value) ? prev : [{
|
|
91132
|
+
key: value,
|
|
91133
|
+
value
|
|
91134
|
+
}, ...prev]);
|
|
91135
|
+
setSelectedFilterValues(prev => [...prev, value]);
|
|
91136
|
+
};
|
|
91137
|
+
|
|
90992
91138
|
// Handler for search text input in filter values dropdown (debounced - triggers API call)
|
|
90993
91139
|
const handleFilterSearchChange = searchText => {
|
|
90994
91140
|
// Determine the fullPath based on current mode
|
|
@@ -91208,6 +91354,8 @@ const Filters = ({
|
|
|
91208
91354
|
loading: loadingFilterValues,
|
|
91209
91355
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
91210
91356
|
debounceMs: 1000
|
|
91357
|
+
}), /*#__PURE__*/gn.createElement(CustomFilterValueInput, {
|
|
91358
|
+
onAdd: handleAddCustomFilterValue
|
|
91211
91359
|
})));
|
|
91212
91360
|
})(), /*#__PURE__*/gn.createElement(Box, {
|
|
91213
91361
|
sx: {
|
|
@@ -91282,7 +91430,12 @@ const Filters = ({
|
|
|
91282
91430
|
mb: 3
|
|
91283
91431
|
}
|
|
91284
91432
|
}, /*#__PURE__*/gn.createElement(SingleSelect, {
|
|
91285
|
-
items:
|
|
91433
|
+
items: Array.from(
|
|
91434
|
+
// Dimensions can now be added more than once (same fullPath,
|
|
91435
|
+
// different instance), but a filter targets the underlying
|
|
91436
|
+
// field regardless of how many columns display it - so this
|
|
91437
|
+
// list is deduped by fullPath to avoid showing it twice.
|
|
91438
|
+
new Map(existingDimensions.map(dim => [dim.fullPath, dim])).values()).map(dim => ({
|
|
91286
91439
|
key: dim.fullPath,
|
|
91287
91440
|
value: interpolateTitle(dimensionTitleOverrides[dim.fullPath] || dim.dimensionTitle, parameters)
|
|
91288
91441
|
})).sort((a, b) => a.value.localeCompare(b.value)),
|
|
@@ -91354,6 +91507,8 @@ const Filters = ({
|
|
|
91354
91507
|
loading: loadingFilterValues,
|
|
91355
91508
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
91356
91509
|
debounceMs: 1000
|
|
91510
|
+
}), /*#__PURE__*/gn.createElement(CustomFilterValueInput, {
|
|
91511
|
+
onAdd: handleAddCustomFilterValue
|
|
91357
91512
|
})));
|
|
91358
91513
|
})(), /*#__PURE__*/gn.createElement(Box, {
|
|
91359
91514
|
sx: {
|
|
@@ -91643,6 +91798,8 @@ const Filters = ({
|
|
|
91643
91798
|
loading: loadingFilterValues,
|
|
91644
91799
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
91645
91800
|
debounceMs: 1000
|
|
91801
|
+
}), /*#__PURE__*/gn.createElement(CustomFilterValueInput, {
|
|
91802
|
+
onAdd: handleAddCustomFilterValue
|
|
91646
91803
|
})), /*#__PURE__*/gn.createElement(Box, {
|
|
91647
91804
|
sx: {
|
|
91648
91805
|
display: 'flex',
|
|
@@ -91869,7 +92026,7 @@ const ColumnOrder = ({
|
|
|
91869
92026
|
key: `${item.kind}-${item.key}`,
|
|
91870
92027
|
id: index,
|
|
91871
92028
|
label: labelFor(item),
|
|
91872
|
-
fullPath: item.kind === "computed" ? item.data.value : item.
|
|
92029
|
+
fullPath: item.kind === "computed" ? item.data.value : item.data.fullPath,
|
|
91873
92030
|
kind: item.kind,
|
|
91874
92031
|
source: item.kind === "metric" ? item.data.metric?.source : undefined,
|
|
91875
92032
|
onMoveUp: () => handleMoveUp(index),
|
|
@@ -91895,11 +92052,17 @@ const formatValue = (value, def) => {
|
|
|
91895
92052
|
const isNumericType = type => type === 'integer' || type === 'currency';
|
|
91896
92053
|
|
|
91897
92054
|
// Builds a single DataGrid column definition for one resolved column item
|
|
91898
|
-
// (a dimension, computed dimension, or metric).
|
|
92055
|
+
// (a dimension, computed dimension, or metric). The DataGrid `field` is the
|
|
92056
|
+
// item's instance id (so two duplicate instances of the same underlying
|
|
92057
|
+
// dimension/metric still get distinct columns); the actual cell value is
|
|
92058
|
+
// read via `valueGetter` from `dataFieldName`, the key the API response
|
|
92059
|
+
// actually uses - shared by every duplicate instance, since the backend
|
|
92060
|
+
// only ever returns one value per fullPath. dataFieldName rules:
|
|
91899
92061
|
// - Dimensions from base provider: baseAlias.field -> baseAlias_field
|
|
91900
92062
|
// - Dimensions from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
91901
92063
|
// - Computed dimensions have no provider/relations chain, so the API returns
|
|
91902
|
-
// each row keyed by the computed dimension's own `name
|
|
92064
|
+
// each row keyed by the computed dimension's own `name` (and can't be
|
|
92065
|
+
// duplicated - names must be unique - so field/dataFieldName coincide).
|
|
91903
92066
|
// - Metrics: API returns metric keys in different forms depending on path depth
|
|
91904
92067
|
// (see comment inline below), normalised to match the row-key normalisation
|
|
91905
92068
|
// done when building `rows`.
|
|
@@ -91917,13 +92080,13 @@ const buildColumn = (item, titleOverrides, parameters) => {
|
|
|
91917
92080
|
if (item.kind === 'metric') {
|
|
91918
92081
|
const metric = item.data;
|
|
91919
92082
|
const metricDef = metric.metric;
|
|
91920
|
-
let
|
|
92083
|
+
let dataFieldName;
|
|
91921
92084
|
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
91922
92085
|
if (dotCount >= 2) {
|
|
91923
92086
|
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
91924
92087
|
// Normalise dots to underscores to match the row key normalisation below.
|
|
91925
92088
|
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
91926
|
-
|
|
92089
|
+
dataFieldName = metric.fullPath.replace(/\./g, '_');
|
|
91927
92090
|
} else if (metric.relations && metric.relations.length > 0) {
|
|
91928
92091
|
// 2-part path from a nested provider: API drops the base provider alias.
|
|
91929
92092
|
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
@@ -91934,19 +92097,24 @@ const buildColumn = (item, titleOverrides, parameters) => {
|
|
|
91934
92097
|
const pathParts = pathWithoutField.split('_');
|
|
91935
92098
|
pathParts.shift(); // remove base provider alias
|
|
91936
92099
|
const pathWithoutBase = pathParts.join('_');
|
|
91937
|
-
|
|
92100
|
+
dataFieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
91938
92101
|
} else {
|
|
91939
92102
|
// 2-part path from the base provider: API returns just the metric name.
|
|
91940
92103
|
// Example: "mc.record_count" -> "record_count"
|
|
91941
|
-
|
|
92104
|
+
dataFieldName = metric.metricName;
|
|
91942
92105
|
}
|
|
91943
|
-
const headerName = interpolateTitle(titleOverrides.metrics[metric.
|
|
92106
|
+
const headerName = interpolateTitle(titleOverrides.metrics[metric.id] || metric.metricTitle || dataFieldName, parameters);
|
|
91944
92107
|
return {
|
|
91945
|
-
field
|
|
92108
|
+
// `field` is the instance id (unique even for duplicate columns
|
|
92109
|
+
// pointing at the same underlying metric); the actual value is
|
|
92110
|
+
// read from the row via valueGetter using dataFieldName, which is
|
|
92111
|
+
// the key the API response actually uses (shared across duplicates).
|
|
92112
|
+
field: metric.id.replace(/[.#]/g, '_'),
|
|
91946
92113
|
headerName: headerName,
|
|
91947
92114
|
flex: 1,
|
|
91948
92115
|
minWidth: 150,
|
|
91949
92116
|
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
92117
|
+
valueGetter: (_value, row) => row[dataFieldName],
|
|
91950
92118
|
renderHeader: () => /*#__PURE__*/gn.createElement(Box, {
|
|
91951
92119
|
sx: {
|
|
91952
92120
|
display: 'flex',
|
|
@@ -91962,7 +92130,7 @@ const buildColumn = (item, titleOverrides, parameters) => {
|
|
|
91962
92130
|
|
|
91963
92131
|
// Dimension
|
|
91964
92132
|
const dim = item.data;
|
|
91965
|
-
let
|
|
92133
|
+
let dataFieldName;
|
|
91966
92134
|
if (dim.relations && dim.relations.length > 0) {
|
|
91967
92135
|
// From nested provider: drop the base provider alias
|
|
91968
92136
|
// Example: ft_fa_db.currency -> fa_db_currency
|
|
@@ -91974,20 +92142,24 @@ const buildColumn = (item, titleOverrides, parameters) => {
|
|
|
91974
92142
|
pathParts.shift(); // Remove base provider alias
|
|
91975
92143
|
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
91976
92144
|
|
|
91977
|
-
|
|
92145
|
+
dataFieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
91978
92146
|
} else {
|
|
91979
92147
|
// From base provider: keep the full path with underscore
|
|
91980
92148
|
// Example: ba.created_at -> ba_created_at
|
|
91981
|
-
|
|
92149
|
+
dataFieldName = dim.fullPath.replace('.', '_');
|
|
91982
92150
|
}
|
|
91983
|
-
const headerName = interpolateTitle(titleOverrides.dimensions[dim.
|
|
92151
|
+
const headerName = interpolateTitle(titleOverrides.dimensions[dim.id] || dim.dimensionTitle || dataFieldName, parameters);
|
|
91984
92152
|
const dimDef = dim.dimension;
|
|
91985
92153
|
return {
|
|
91986
|
-
|
|
92154
|
+
// Same rationale as the metric branch above: `field` must be unique
|
|
92155
|
+
// per instance, while the value always comes from the one real
|
|
92156
|
+
// underlying row key (dataFieldName), shared by every duplicate.
|
|
92157
|
+
field: dim.id.replace(/[.#]/g, '_'),
|
|
91987
92158
|
headerName: headerName,
|
|
91988
92159
|
flex: 1,
|
|
91989
92160
|
minWidth: 150,
|
|
91990
92161
|
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
92162
|
+
valueGetter: (_value, row) => row[dataFieldName],
|
|
91991
92163
|
valueFormatter: value => formatValue(value, dimDef)
|
|
91992
92164
|
};
|
|
91993
92165
|
};
|
|
@@ -92313,6 +92485,72 @@ const ReportBuilder = ({
|
|
|
92313
92485
|
}
|
|
92314
92486
|
};
|
|
92315
92487
|
|
|
92488
|
+
// Reconstructs saved dimensions as independent, individually-identified
|
|
92489
|
+
// instances so that deliberate duplicates (the same field added more than
|
|
92490
|
+
// once) survive a save/reload round trip. Prefers `ordered_dimensions`
|
|
92491
|
+
// (one ref per instance - the nth duplicate of a path is suffixed
|
|
92492
|
+
// `#n`) when present; falls back to one instance per entry in the
|
|
92493
|
+
// (necessarily duplicate-free) `dimensions` path list for reports saved
|
|
92494
|
+
// before this field existed.
|
|
92495
|
+
const reconstructDimensions = (reportDef, providersData) => {
|
|
92496
|
+
const orderByMap = {};
|
|
92497
|
+
if (reportDef.definition?.doc?.query?.order_by) {
|
|
92498
|
+
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
92499
|
+
orderByMap[orderItem.name] = orderItem.desc === true ? "desc" : "asc";
|
|
92500
|
+
}
|
|
92501
|
+
}
|
|
92502
|
+
const reconstructed = [];
|
|
92503
|
+
const orderedRefs = (reportDef.definition?.doc?.query?.ordered_dimensions || []).filter(entry => entry.type === "dimension").map(entry => entry.ref);
|
|
92504
|
+
if (orderedRefs.length > 0) {
|
|
92505
|
+
orderedRefs.forEach(ref => {
|
|
92506
|
+
const fullPath = ref.split("#")[0];
|
|
92507
|
+
const dim = reconstructDimensionFromPath(fullPath, providersData, reportDef.provider);
|
|
92508
|
+
if (dim) {
|
|
92509
|
+
dim.id = ref;
|
|
92510
|
+
dim.sortOrder = orderByMap[fullPath] || null;
|
|
92511
|
+
reconstructed.push(dim);
|
|
92512
|
+
}
|
|
92513
|
+
});
|
|
92514
|
+
} else if (reportDef.definition?.doc?.query?.dimensions) {
|
|
92515
|
+
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
92516
|
+
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
92517
|
+
if (dim) {
|
|
92518
|
+
dim.id = dimPath;
|
|
92519
|
+
dim.sortOrder = orderByMap[dimPath] || null;
|
|
92520
|
+
reconstructed.push(dim);
|
|
92521
|
+
}
|
|
92522
|
+
}
|
|
92523
|
+
}
|
|
92524
|
+
return reconstructed;
|
|
92525
|
+
};
|
|
92526
|
+
|
|
92527
|
+
// Mirrors reconstructDimensions for metrics, using the analogous
|
|
92528
|
+
// `ordered_metrics` field (metrics have no other unified-order concept to
|
|
92529
|
+
// fall back on, so this is always written whenever there are metrics).
|
|
92530
|
+
const reconstructMetrics = (reportDef, providersData) => {
|
|
92531
|
+
const reconstructed = [];
|
|
92532
|
+
const orderedRefs = reportDef.definition?.doc?.query?.ordered_metrics || [];
|
|
92533
|
+
if (orderedRefs.length > 0) {
|
|
92534
|
+
orderedRefs.forEach(ref => {
|
|
92535
|
+
const fullPath = ref.split("#")[0];
|
|
92536
|
+
const metric = reconstructMetricFromPath(fullPath, providersData, reportDef.provider);
|
|
92537
|
+
if (metric) {
|
|
92538
|
+
metric.id = ref;
|
|
92539
|
+
reconstructed.push(metric);
|
|
92540
|
+
}
|
|
92541
|
+
});
|
|
92542
|
+
} else if (reportDef.definition?.doc?.query?.metrics) {
|
|
92543
|
+
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
92544
|
+
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
92545
|
+
if (metric) {
|
|
92546
|
+
metric.id = metricPath;
|
|
92547
|
+
reconstructed.push(metric);
|
|
92548
|
+
}
|
|
92549
|
+
}
|
|
92550
|
+
}
|
|
92551
|
+
return reconstructed;
|
|
92552
|
+
};
|
|
92553
|
+
|
|
92316
92554
|
// Build the unified display/execution order for dimensions + computed
|
|
92317
92555
|
// dimensions. Prefers the persisted `ordered_dimensions` field; falls back
|
|
92318
92556
|
// to "dimensions first, then computed dimensions" for report definitions
|
|
@@ -92323,7 +92561,7 @@ const ReportBuilder = ({
|
|
|
92323
92561
|
const seenComputedKeys = new Set();
|
|
92324
92562
|
if (Array.isArray(rawOrderedDimensions)) {
|
|
92325
92563
|
rawOrderedDimensions.forEach(entry => {
|
|
92326
|
-
if (entry.type === "dimension" && dimensions.some(d => d.
|
|
92564
|
+
if (entry.type === "dimension" && dimensions.some(d => d.id === entry.ref)) {
|
|
92327
92565
|
dimensionOrder.push({
|
|
92328
92566
|
type: "dimension",
|
|
92329
92567
|
key: entry.ref
|
|
@@ -92341,10 +92579,10 @@ const ReportBuilder = ({
|
|
|
92341
92579
|
|
|
92342
92580
|
// Append anything not covered by ordered_dimensions (older reports, or drift)
|
|
92343
92581
|
dimensions.forEach(d => {
|
|
92344
|
-
if (!seenDimensionKeys.has(d.
|
|
92582
|
+
if (!seenDimensionKeys.has(d.id)) {
|
|
92345
92583
|
dimensionOrder.push({
|
|
92346
92584
|
type: "dimension",
|
|
92347
|
-
key: d.
|
|
92585
|
+
key: d.id
|
|
92348
92586
|
});
|
|
92349
92587
|
}
|
|
92350
92588
|
});
|
|
@@ -92367,12 +92605,12 @@ const ReportBuilder = ({
|
|
|
92367
92605
|
// "fall back to the default order".
|
|
92368
92606
|
const buildColumnOrder = (rawOrderedColumns, dimensions, computedDimensions, metrics) => {
|
|
92369
92607
|
if (!Array.isArray(rawOrderedColumns)) return [];
|
|
92370
|
-
const
|
|
92608
|
+
const dimensionIds = new Set(dimensions.map(d => d.id));
|
|
92371
92609
|
const computedNames = new Set(computedDimensions.map(cd => cd.name));
|
|
92372
|
-
const
|
|
92610
|
+
const metricIds = new Set(metrics.map(m => m.id));
|
|
92373
92611
|
const columnOrder = [];
|
|
92374
92612
|
rawOrderedColumns.forEach(entry => {
|
|
92375
|
-
if (entry.type === "dimension" &&
|
|
92613
|
+
if (entry.type === "dimension" && dimensionIds.has(entry.ref)) {
|
|
92376
92614
|
columnOrder.push({
|
|
92377
92615
|
type: "dimension",
|
|
92378
92616
|
key: entry.ref
|
|
@@ -92382,7 +92620,7 @@ const ReportBuilder = ({
|
|
|
92382
92620
|
type: "computed",
|
|
92383
92621
|
key: entry.ref
|
|
92384
92622
|
});
|
|
92385
|
-
} else if (entry.type === "metric" &&
|
|
92623
|
+
} else if (entry.type === "metric" && metricIds.has(entry.ref)) {
|
|
92386
92624
|
columnOrder.push({
|
|
92387
92625
|
type: "metric",
|
|
92388
92626
|
key: entry.ref
|
|
@@ -92412,43 +92650,10 @@ const ReportBuilder = ({
|
|
|
92412
92650
|
// Set the title
|
|
92413
92651
|
setReportTitle(reportDef.title || "");
|
|
92414
92652
|
|
|
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
|
-
}
|
|
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);
|
|
92452
92657
|
|
|
92453
92658
|
// Load title overrides if they exist
|
|
92454
92659
|
const loadedTitleOverrides = {
|
|
@@ -92532,43 +92737,10 @@ const ReportBuilder = ({
|
|
|
92532
92737
|
// Set the title (already modified with " (Copy)" suffix)
|
|
92533
92738
|
setReportTitle(reportDef.title || "");
|
|
92534
92739
|
|
|
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
|
-
}
|
|
92740
|
+
// Reconstruct dimensions and metrics as independent instances
|
|
92741
|
+
// (preserving deliberate duplicates - see reconstructDimensions/Metrics)
|
|
92742
|
+
const reconstructedDimensions = reconstructDimensions(reportDef, providersData);
|
|
92743
|
+
const reconstructedMetrics = reconstructMetrics(reportDef, providersData);
|
|
92572
92744
|
|
|
92573
92745
|
// Load title overrides if they exist
|
|
92574
92746
|
const loadedTitleOverrides = {
|
|
@@ -92667,42 +92839,45 @@ const ReportBuilder = ({
|
|
|
92667
92839
|
});
|
|
92668
92840
|
console.log("Selected root provider:", event.target.value);
|
|
92669
92841
|
};
|
|
92670
|
-
|
|
92842
|
+
|
|
92843
|
+
// Keyed by instance id, so two instances of the same dimension can carry
|
|
92844
|
+
// independent custom titles.
|
|
92845
|
+
const handleUpdateDimensionTitle = (id, customTitle) => {
|
|
92671
92846
|
setTitleOverrides(prev => ({
|
|
92672
92847
|
...prev,
|
|
92673
92848
|
dimensions: {
|
|
92674
92849
|
...prev.dimensions,
|
|
92675
|
-
[
|
|
92850
|
+
[id]: customTitle
|
|
92676
92851
|
}
|
|
92677
92852
|
}));
|
|
92678
92853
|
};
|
|
92679
|
-
const handleResetDimensionTitle =
|
|
92854
|
+
const handleResetDimensionTitle = id => {
|
|
92680
92855
|
setTitleOverrides(prev => {
|
|
92681
92856
|
const newDimensions = {
|
|
92682
92857
|
...prev.dimensions
|
|
92683
92858
|
};
|
|
92684
|
-
delete newDimensions[
|
|
92859
|
+
delete newDimensions[id];
|
|
92685
92860
|
return {
|
|
92686
92861
|
...prev,
|
|
92687
92862
|
dimensions: newDimensions
|
|
92688
92863
|
};
|
|
92689
92864
|
});
|
|
92690
92865
|
};
|
|
92691
|
-
const handleUpdateMetricTitle = (
|
|
92866
|
+
const handleUpdateMetricTitle = (id, customTitle) => {
|
|
92692
92867
|
setTitleOverrides(prev => ({
|
|
92693
92868
|
...prev,
|
|
92694
92869
|
metrics: {
|
|
92695
92870
|
...prev.metrics,
|
|
92696
|
-
[
|
|
92871
|
+
[id]: customTitle
|
|
92697
92872
|
}
|
|
92698
92873
|
}));
|
|
92699
92874
|
};
|
|
92700
|
-
const handleResetMetricTitle =
|
|
92875
|
+
const handleResetMetricTitle = id => {
|
|
92701
92876
|
setTitleOverrides(prev => {
|
|
92702
92877
|
const newMetrics = {
|
|
92703
92878
|
...prev.metrics
|
|
92704
92879
|
};
|
|
92705
|
-
delete newMetrics[
|
|
92880
|
+
delete newMetrics[id];
|
|
92706
92881
|
return {
|
|
92707
92882
|
...prev,
|
|
92708
92883
|
metrics: newMetrics
|
|
@@ -92737,11 +92912,11 @@ const ReportBuilder = ({
|
|
|
92737
92912
|
dimensions: [...prev.dimensions, dimensionData],
|
|
92738
92913
|
dimensionOrder: [...prev.dimensionOrder, {
|
|
92739
92914
|
type: "dimension",
|
|
92740
|
-
key: dimensionData.
|
|
92915
|
+
key: dimensionData.id
|
|
92741
92916
|
}],
|
|
92742
92917
|
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
92743
92918
|
type: "dimension",
|
|
92744
|
-
key: dimensionData.
|
|
92919
|
+
key: dimensionData.id
|
|
92745
92920
|
})
|
|
92746
92921
|
};
|
|
92747
92922
|
console.log("Dimension saved:", dimensionData);
|
|
@@ -92749,22 +92924,30 @@ const ReportBuilder = ({
|
|
|
92749
92924
|
return newReport;
|
|
92750
92925
|
});
|
|
92751
92926
|
};
|
|
92752
|
-
const handleRemoveDimension =
|
|
92927
|
+
const handleRemoveDimension = id => {
|
|
92753
92928
|
setReport(prev => ({
|
|
92754
92929
|
...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 ===
|
|
92930
|
+
dimensions: prev.dimensions.filter(d => d.id !== id),
|
|
92931
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === id)),
|
|
92932
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "dimension" && entry.key === id))
|
|
92758
92933
|
}));
|
|
92759
92934
|
};
|
|
92760
|
-
|
|
92761
|
-
|
|
92762
|
-
|
|
92763
|
-
|
|
92764
|
-
|
|
92765
|
-
|
|
92766
|
-
|
|
92767
|
-
|
|
92935
|
+
|
|
92936
|
+
// sortOrder is fundamentally a per-field (query-level) setting - SQL can
|
|
92937
|
+
// only sort by a given column once - so toggling it on any instance of a
|
|
92938
|
+
// duplicated dimension applies to every instance sharing that fullPath.
|
|
92939
|
+
const handleUpdateDimensionSortOrder = (id, sortOrder) => {
|
|
92940
|
+
setReport(prev => {
|
|
92941
|
+
const target = prev.dimensions.find(d => d.id === id);
|
|
92942
|
+
if (!target) return prev;
|
|
92943
|
+
return {
|
|
92944
|
+
...prev,
|
|
92945
|
+
dimensions: prev.dimensions.map(d => d.fullPath === target.fullPath ? {
|
|
92946
|
+
...d,
|
|
92947
|
+
sortOrder
|
|
92948
|
+
} : d)
|
|
92949
|
+
};
|
|
92950
|
+
});
|
|
92768
92951
|
};
|
|
92769
92952
|
|
|
92770
92953
|
// Reorders the single unified dimensions + computed-dimensions list;
|
|
@@ -92819,7 +93002,7 @@ const ReportBuilder = ({
|
|
|
92819
93002
|
metrics: [...prev.metrics, metricData],
|
|
92820
93003
|
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
92821
93004
|
type: "metric",
|
|
92822
|
-
key: metricData.
|
|
93005
|
+
key: metricData.id
|
|
92823
93006
|
})
|
|
92824
93007
|
};
|
|
92825
93008
|
console.log("Metric saved:", metricData);
|
|
@@ -92827,15 +93010,12 @@ const ReportBuilder = ({
|
|
|
92827
93010
|
return newReport;
|
|
92828
93011
|
});
|
|
92829
93012
|
};
|
|
92830
|
-
const handleRemoveMetric =
|
|
92831
|
-
setReport(prev => {
|
|
92832
|
-
|
|
92833
|
-
|
|
92834
|
-
|
|
92835
|
-
|
|
92836
|
-
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "metric" && entry.key === fullPath))
|
|
92837
|
-
};
|
|
92838
|
-
});
|
|
93013
|
+
const handleRemoveMetric = id => {
|
|
93014
|
+
setReport(prev => ({
|
|
93015
|
+
...prev,
|
|
93016
|
+
metrics: prev.metrics.filter(m => m.id !== id),
|
|
93017
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "metric" && entry.key === id))
|
|
93018
|
+
}));
|
|
92839
93019
|
};
|
|
92840
93020
|
const handleReorderMetrics = newOrder => {
|
|
92841
93021
|
setReport(prev => ({
|
|
@@ -92890,13 +93070,30 @@ const ReportBuilder = ({
|
|
|
92890
93070
|
// persisted `ordered_dimensions` field all stay consistent with whatever
|
|
92891
93071
|
// order the user arranged on screen.
|
|
92892
93072
|
const buildQueryObject = () => {
|
|
92893
|
-
const
|
|
93073
|
+
const dimensionById = Object.fromEntries(report.dimensions.map(d => [d.id, d]));
|
|
92894
93074
|
const computedByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
92895
|
-
|
|
93075
|
+
|
|
93076
|
+
// May contain more than one instance of the same fullPath (deliberate
|
|
93077
|
+
// duplicates), kept in dimensionOrder's display order.
|
|
93078
|
+
const orderedDimensionsOnly = report.dimensionOrder.filter(entry => entry.type === "dimension").map(entry => dimensionById[entry.key]).filter(Boolean);
|
|
92896
93079
|
const orderedComputedOnly = report.dimensionOrder.filter(entry => entry.type === "computed").map(entry => computedByName[entry.key]).filter(Boolean);
|
|
92897
93080
|
|
|
93081
|
+
// The API keys each result row by fullPath, so duplicate instances of
|
|
93082
|
+
// the same field can only ever carry one value/one sort direction at
|
|
93083
|
+
// the query level - dedupe by fullPath (keeping first-occurrence order)
|
|
93084
|
+
// before sending. The builder still shows/orders/titles each instance
|
|
93085
|
+
// independently; they just read from the same underlying query column.
|
|
93086
|
+
const uniqueDimensionsByPath = [];
|
|
93087
|
+
const seenDimensionPaths = new Set();
|
|
93088
|
+
orderedDimensionsOnly.forEach(dim => {
|
|
93089
|
+
if (!seenDimensionPaths.has(dim.fullPath)) {
|
|
93090
|
+
seenDimensionPaths.add(dim.fullPath);
|
|
93091
|
+
uniqueDimensionsByPath.push(dim);
|
|
93092
|
+
}
|
|
93093
|
+
});
|
|
93094
|
+
|
|
92898
93095
|
// Build order_by array - include all dimensions, add desc property only when needed
|
|
92899
|
-
const orderBy =
|
|
93096
|
+
const orderBy = uniqueDimensionsByPath.map(dim => {
|
|
92900
93097
|
const orderItem = {
|
|
92901
93098
|
name: dim.fullPath
|
|
92902
93099
|
};
|
|
@@ -92951,9 +93148,20 @@ const ReportBuilder = ({
|
|
|
92951
93148
|
if (Object.keys(titleOverrides.filters).length > 0) {
|
|
92952
93149
|
titles.filters = titleOverrides.filters;
|
|
92953
93150
|
}
|
|
93151
|
+
|
|
93152
|
+
// Same dedupe rationale as dimensions above: the query only needs each
|
|
93153
|
+
// metric fullPath once, however many display instances reference it.
|
|
93154
|
+
const uniqueMetricPaths = [];
|
|
93155
|
+
const seenMetricPaths = new Set();
|
|
93156
|
+
report.metrics.forEach(metric => {
|
|
93157
|
+
if (!seenMetricPaths.has(metric.fullPath)) {
|
|
93158
|
+
seenMetricPaths.add(metric.fullPath);
|
|
93159
|
+
uniqueMetricPaths.push(metric.fullPath);
|
|
93160
|
+
}
|
|
93161
|
+
});
|
|
92954
93162
|
const queryObj = {
|
|
92955
|
-
dimensions:
|
|
92956
|
-
metrics:
|
|
93163
|
+
dimensions: uniqueDimensionsByPath.map(dim => dim.fullPath),
|
|
93164
|
+
metrics: uniqueMetricPaths,
|
|
92957
93165
|
order_by: orderBy
|
|
92958
93166
|
};
|
|
92959
93167
|
|
|
@@ -92967,7 +93175,9 @@ const ReportBuilder = ({
|
|
|
92967
93175
|
|
|
92968
93176
|
// Persist the unified dimension + computed-dimension order so it can be
|
|
92969
93177
|
// reconstructed on reload and so executed reports render columns in the
|
|
92970
|
-
// same order they were arranged in the builder.
|
|
93178
|
+
// same order they were arranged in the builder. Entries use each
|
|
93179
|
+
// dimension's instance id as `ref`, so deliberate duplicates (the nth
|
|
93180
|
+
// occurrence of a fullPath is suffixed `#n`) round-trip correctly.
|
|
92971
93181
|
if (report.dimensionOrder.length > 0) {
|
|
92972
93182
|
queryObj.ordered_dimensions = report.dimensionOrder.map(entry => ({
|
|
92973
93183
|
type: entry.type,
|
|
@@ -92975,14 +93185,30 @@ const ReportBuilder = ({
|
|
|
92975
93185
|
}));
|
|
92976
93186
|
}
|
|
92977
93187
|
|
|
92978
|
-
// Persist
|
|
92979
|
-
//
|
|
92980
|
-
|
|
92981
|
-
|
|
92982
|
-
|
|
92983
|
-
|
|
92984
|
-
|
|
92985
|
-
|
|
93188
|
+
// Persist metric instance order/identity the same way (metrics have no
|
|
93189
|
+
// other unified-order field to piggyback on).
|
|
93190
|
+
if (report.metrics.length > 0) {
|
|
93191
|
+
queryObj.ordered_metrics = report.metrics.map(metric => metric.id);
|
|
93192
|
+
}
|
|
93193
|
+
|
|
93194
|
+
// Persist the full resolved column order - the explicit Column Order tab
|
|
93195
|
+
// arrangement when defined, otherwise dimensions/computed (in their own
|
|
93196
|
+
// order) followed by metrics (in their own order), same as what the
|
|
93197
|
+
// results grid renders. Always emitted (not just when a custom order
|
|
93198
|
+
// exists) so the API/results grid get an explicit order even for
|
|
93199
|
+
// untouched reports, rather than having to fall back to guessing it.
|
|
93200
|
+
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
93201
|
+
kind: "dimension",
|
|
93202
|
+
data: dimensionById[entry.key]
|
|
93203
|
+
} : {
|
|
93204
|
+
kind: "computed",
|
|
93205
|
+
data: computedByName[entry.key]
|
|
93206
|
+
}).filter(item => item.data);
|
|
93207
|
+
const resolvedColumnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
|
|
93208
|
+
if (resolvedColumnItems.length > 0) {
|
|
93209
|
+
queryObj.ordered_columns = resolvedColumnItems.map(item => ({
|
|
93210
|
+
type: item.kind,
|
|
93211
|
+
ref: item.key
|
|
92986
93212
|
}));
|
|
92987
93213
|
}
|
|
92988
93214
|
|
|
@@ -93144,11 +93370,11 @@ const ReportBuilder = ({
|
|
|
93144
93370
|
|
|
93145
93371
|
// Resolve the unified dimensionOrder into actual dimension/computed-dimension
|
|
93146
93372
|
// objects so the results grid can render columns in that same order.
|
|
93147
|
-
const
|
|
93373
|
+
const dimensionById = Object.fromEntries(report.dimensions.map(d => [d.id, d]));
|
|
93148
93374
|
const computedDimensionByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
93149
93375
|
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
93150
93376
|
kind: "dimension",
|
|
93151
|
-
data:
|
|
93377
|
+
data: dimensionById[entry.key]
|
|
93152
93378
|
} : {
|
|
93153
93379
|
kind: "computed",
|
|
93154
93380
|
data: computedDimensionByName[entry.key]
|