@bygd/nc-report-ui 0.1.41 → 0.1.42
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 +693 -381
- package/dist/default/cjs/index.cjs +691 -381
- package/dist/default/esm/index.js +691 -382
- package/package.json +1 -1
|
@@ -44,7 +44,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
|
44
44
|
import SaveIcon from '@mui/icons-material/Save';
|
|
45
45
|
import DownloadIcon from '@mui/icons-material/Download';
|
|
46
46
|
import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter } from '@dnd-kit/core';
|
|
47
|
-
import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy,
|
|
47
|
+
import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
|
|
48
48
|
import { CSS } from '@dnd-kit/utilities';
|
|
49
49
|
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
|
50
50
|
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
|
@@ -64,6 +64,7 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
|
64
64
|
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
|
65
65
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
66
66
|
import dayjs from 'dayjs';
|
|
67
|
+
import CalculateIcon from '@mui/icons-material/Calculate';
|
|
67
68
|
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
68
69
|
|
|
69
70
|
function _extends() {
|
|
@@ -2528,6 +2529,140 @@ const ProviderSelection = ({
|
|
|
2528
2529
|
}, renderSelectionChain()));
|
|
2529
2530
|
};
|
|
2530
2531
|
|
|
2532
|
+
// Shared drag-and-drop primitives for the index-identified, arrow-reorderable
|
|
2533
|
+
// lists used across the report builder (Dimensions, Metrics, Column Order).
|
|
2534
|
+
// Previously duplicated per-tab; consolidated here so all three behave and
|
|
2535
|
+
// look identical and only need to be fixed in one place.
|
|
2536
|
+
|
|
2537
|
+
const useSortableListSensors = () => useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
|
|
2538
|
+
coordinateGetter: sortableKeyboardCoordinates
|
|
2539
|
+
}));
|
|
2540
|
+
|
|
2541
|
+
// Builds drag/move handlers for a list where dnd-kit item ids are the array
|
|
2542
|
+
// index. `onReorder` receives the whole reordered list.
|
|
2543
|
+
const makeListReorderHandlers = (list, onReorder) => {
|
|
2544
|
+
const handleDragEnd = ({
|
|
2545
|
+
active,
|
|
2546
|
+
over
|
|
2547
|
+
}) => {
|
|
2548
|
+
if (over && active.id !== over.id) {
|
|
2549
|
+
const oldIndex = list.findIndex((_, i) => i === active.id);
|
|
2550
|
+
const newIndex = list.findIndex((_, i) => i === over.id);
|
|
2551
|
+
onReorder(arrayMove(list, oldIndex, newIndex));
|
|
2552
|
+
}
|
|
2553
|
+
};
|
|
2554
|
+
const handleMoveUp = index => {
|
|
2555
|
+
if (index > 0) {
|
|
2556
|
+
onReorder(arrayMove(list, index, index - 1));
|
|
2557
|
+
}
|
|
2558
|
+
};
|
|
2559
|
+
const handleMoveDown = index => {
|
|
2560
|
+
if (index < list.length - 1) {
|
|
2561
|
+
onReorder(arrayMove(list, index, index + 1));
|
|
2562
|
+
}
|
|
2563
|
+
};
|
|
2564
|
+
return {
|
|
2565
|
+
handleDragEnd,
|
|
2566
|
+
handleMoveUp,
|
|
2567
|
+
handleMoveDown
|
|
2568
|
+
};
|
|
2569
|
+
};
|
|
2570
|
+
|
|
2571
|
+
// Wraps DndContext + SortableContext + the vertical list container.
|
|
2572
|
+
// `itemIds` must line up with the ids passed to each item's useSortable().
|
|
2573
|
+
const SortableListContext = ({
|
|
2574
|
+
sensors,
|
|
2575
|
+
itemIds,
|
|
2576
|
+
onDragEnd,
|
|
2577
|
+
children
|
|
2578
|
+
}) => /*#__PURE__*/React__default.createElement(DndContext, {
|
|
2579
|
+
sensors: sensors,
|
|
2580
|
+
collisionDetection: closestCenter,
|
|
2581
|
+
onDragEnd: onDragEnd
|
|
2582
|
+
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
2583
|
+
items: itemIds,
|
|
2584
|
+
strategy: verticalListSortingStrategy
|
|
2585
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2586
|
+
sx: {
|
|
2587
|
+
display: "flex",
|
|
2588
|
+
flexDirection: "column",
|
|
2589
|
+
gap: 1
|
|
2590
|
+
}
|
|
2591
|
+
}, children)));
|
|
2592
|
+
|
|
2593
|
+
// Per-row useSortable() wiring shared by every sortable chip/row component.
|
|
2594
|
+
const useSortableRow = id => {
|
|
2595
|
+
const {
|
|
2596
|
+
attributes,
|
|
2597
|
+
listeners,
|
|
2598
|
+
setNodeRef,
|
|
2599
|
+
transform,
|
|
2600
|
+
transition,
|
|
2601
|
+
isDragging
|
|
2602
|
+
} = useSortable({
|
|
2603
|
+
id
|
|
2604
|
+
});
|
|
2605
|
+
const style = {
|
|
2606
|
+
transform: CSS.Transform.toString(transform),
|
|
2607
|
+
transition,
|
|
2608
|
+
opacity: isDragging ? 0.5 : 1,
|
|
2609
|
+
display: "flex",
|
|
2610
|
+
alignItems: "center",
|
|
2611
|
+
width: "100%"
|
|
2612
|
+
};
|
|
2613
|
+
return {
|
|
2614
|
+
attributes,
|
|
2615
|
+
listeners,
|
|
2616
|
+
setNodeRef,
|
|
2617
|
+
style
|
|
2618
|
+
};
|
|
2619
|
+
};
|
|
2620
|
+
const DragHandle = ({
|
|
2621
|
+
listeners
|
|
2622
|
+
}) => /*#__PURE__*/React__default.createElement(Box$1, _extends({}, listeners, {
|
|
2623
|
+
sx: {
|
|
2624
|
+
display: "flex",
|
|
2625
|
+
alignItems: "center",
|
|
2626
|
+
cursor: "grab",
|
|
2627
|
+
"&:active": {
|
|
2628
|
+
cursor: "grabbing"
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
2632
|
+
sx: {
|
|
2633
|
+
cursor: "grab",
|
|
2634
|
+
color: "rgba(110, 110, 110, 0.62)"
|
|
2635
|
+
}
|
|
2636
|
+
}));
|
|
2637
|
+
const MoveButtons = ({
|
|
2638
|
+
onMoveUp,
|
|
2639
|
+
onMoveDown,
|
|
2640
|
+
isFirst,
|
|
2641
|
+
isLast
|
|
2642
|
+
}) => /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2643
|
+
className: "hover-icons",
|
|
2644
|
+
sx: {
|
|
2645
|
+
display: "flex",
|
|
2646
|
+
gap: 0.5,
|
|
2647
|
+
opacity: 0,
|
|
2648
|
+
transition: "opacity 0.2s"
|
|
2649
|
+
}
|
|
2650
|
+
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
2651
|
+
size: "small",
|
|
2652
|
+
onClick: onMoveUp,
|
|
2653
|
+
disabled: isFirst,
|
|
2654
|
+
"aria-label": "move up"
|
|
2655
|
+
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
2656
|
+
fontSize: "small"
|
|
2657
|
+
})), /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
2658
|
+
size: "small",
|
|
2659
|
+
onClick: onMoveDown,
|
|
2660
|
+
disabled: isLast,
|
|
2661
|
+
"aria-label": "move down"
|
|
2662
|
+
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
2663
|
+
fontSize: "small"
|
|
2664
|
+
})));
|
|
2665
|
+
|
|
2531
2666
|
// Replace {{key}} placeholders in a title with values from the report
|
|
2532
2667
|
// parameters (e.g. "Balance ({{base_currency}})" -> "Balance (EUR)").
|
|
2533
2668
|
// Unknown keys are left as-is so missing parameters remain visible.
|
|
@@ -2539,6 +2674,50 @@ const interpolateTitle = (template, params) => {
|
|
|
2539
2674
|
});
|
|
2540
2675
|
};
|
|
2541
2676
|
|
|
2677
|
+
// Resolves the final column display order for the results grid: dimensions,
|
|
2678
|
+
// computed dimensions, and metrics combined into a single list.
|
|
2679
|
+
//
|
|
2680
|
+
// `columnOrder` is the optional, explicitly-arranged order (raw entries
|
|
2681
|
+
// `{ type: 'dimension' | 'computed' | 'metric', key }`) set on the Column
|
|
2682
|
+
// Order tab. When present, its entries come first, in that order. Anything
|
|
2683
|
+
// not covered by it (new fields added since it was last touched, or when
|
|
2684
|
+
// it's empty/undefined because no custom order has ever been defined) is
|
|
2685
|
+
// appended in the pre-existing default order: dimensions/computed (in their
|
|
2686
|
+
// own `dimensionOrder`) first, then metrics (in their own array order) —
|
|
2687
|
+
// this keeps behavior unchanged for reports that never define a column order.
|
|
2688
|
+
const resolveColumnOrder = (columnOrder, orderedDimensionItems, metrics) => {
|
|
2689
|
+
// Normalise every candidate item to a uniform { kind, key, data } shape
|
|
2690
|
+
// up front, so callers (e.g. the Column Order tab) can always read
|
|
2691
|
+
// `.key` regardless of where the item came from.
|
|
2692
|
+
const normalizedDimensionItems = orderedDimensionItems.map(item => ({
|
|
2693
|
+
kind: item.kind,
|
|
2694
|
+
key: item.kind === 'computed' ? item.data.name : item.data.fullPath,
|
|
2695
|
+
data: item.data
|
|
2696
|
+
}));
|
|
2697
|
+
const normalizedMetricItems = metrics.map(m => ({
|
|
2698
|
+
kind: 'metric',
|
|
2699
|
+
key: m.fullPath,
|
|
2700
|
+
data: m
|
|
2701
|
+
}));
|
|
2702
|
+
const byKey = Object.fromEntries([...normalizedDimensionItems, ...normalizedMetricItems].map(item => [`${item.kind}:${item.key}`, item]));
|
|
2703
|
+
const resolved = [];
|
|
2704
|
+
const seen = new Set();
|
|
2705
|
+
(columnOrder || []).forEach(entry => {
|
|
2706
|
+
const seenKey = `${entry.type}:${entry.key}`;
|
|
2707
|
+
if (seen.has(seenKey) || !byKey[seenKey]) return;
|
|
2708
|
+
resolved.push(byKey[seenKey]);
|
|
2709
|
+
seen.add(seenKey);
|
|
2710
|
+
});
|
|
2711
|
+
[...normalizedDimensionItems, ...normalizedMetricItems].forEach(item => {
|
|
2712
|
+
const seenKey = `${item.kind}:${item.key}`;
|
|
2713
|
+
if (!seen.has(seenKey)) {
|
|
2714
|
+
resolved.push(item);
|
|
2715
|
+
seen.add(seenKey);
|
|
2716
|
+
}
|
|
2717
|
+
});
|
|
2718
|
+
return resolved;
|
|
2719
|
+
};
|
|
2720
|
+
|
|
2542
2721
|
// Sortable Chip Component
|
|
2543
2722
|
const SortableChip$1 = ({
|
|
2544
2723
|
id,
|
|
@@ -2567,20 +2746,8 @@ const SortableChip$1 = ({
|
|
|
2567
2746
|
attributes,
|
|
2568
2747
|
listeners,
|
|
2569
2748
|
setNodeRef,
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
isDragging
|
|
2573
|
-
} = useSortable({
|
|
2574
|
-
id
|
|
2575
|
-
});
|
|
2576
|
-
const style = {
|
|
2577
|
-
transform: CSS.Transform.toString(transform),
|
|
2578
|
-
transition,
|
|
2579
|
-
opacity: isDragging ? 0.5 : 1,
|
|
2580
|
-
display: "flex",
|
|
2581
|
-
alignItems: "center",
|
|
2582
|
-
width: "100%"
|
|
2583
|
-
};
|
|
2749
|
+
style
|
|
2750
|
+
} = useSortableRow(id);
|
|
2584
2751
|
|
|
2585
2752
|
// Focus input when entering edit mode
|
|
2586
2753
|
useEffect(() => {
|
|
@@ -2689,21 +2856,9 @@ const SortableChip$1 = ({
|
|
|
2689
2856
|
opacity: 1 // show icons on hover
|
|
2690
2857
|
}
|
|
2691
2858
|
}
|
|
2692
|
-
}, /*#__PURE__*/React__default.createElement(
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
alignItems: "center",
|
|
2696
|
-
cursor: "grab",
|
|
2697
|
-
"&:active": {
|
|
2698
|
-
cursor: "grabbing"
|
|
2699
|
-
}
|
|
2700
|
-
}
|
|
2701
|
-
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
2702
|
-
sx: {
|
|
2703
|
-
cursor: "grab",
|
|
2704
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
2705
|
-
}
|
|
2706
|
-
})), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2859
|
+
}, /*#__PURE__*/React__default.createElement(DragHandle, {
|
|
2860
|
+
listeners: listeners
|
|
2861
|
+
}), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2707
2862
|
sx: {
|
|
2708
2863
|
minWidth: 0
|
|
2709
2864
|
}
|
|
@@ -2766,29 +2921,12 @@ const SortableChip$1 = ({
|
|
|
2766
2921
|
sx: {
|
|
2767
2922
|
flex: 1
|
|
2768
2923
|
}
|
|
2769
|
-
}), /*#__PURE__*/React__default.createElement(
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
transition: "opacity 0.2s"
|
|
2776
|
-
}
|
|
2777
|
-
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
2778
|
-
size: "small",
|
|
2779
|
-
onClick: onMoveUp,
|
|
2780
|
-
disabled: isFirst,
|
|
2781
|
-
"aria-label": "move up"
|
|
2782
|
-
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
2783
|
-
fontSize: "small"
|
|
2784
|
-
})), /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
2785
|
-
size: "small",
|
|
2786
|
-
onClick: onMoveDown,
|
|
2787
|
-
disabled: isLast,
|
|
2788
|
-
"aria-label": "move down"
|
|
2789
|
-
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
2790
|
-
fontSize: "small"
|
|
2791
|
-
})))) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
2924
|
+
}), /*#__PURE__*/React__default.createElement(MoveButtons, {
|
|
2925
|
+
onMoveUp: onMoveUp,
|
|
2926
|
+
onMoveDown: onMoveDown,
|
|
2927
|
+
isFirst: isFirst,
|
|
2928
|
+
isLast: isLast
|
|
2929
|
+
})) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
2792
2930
|
inputRef: inputRef,
|
|
2793
2931
|
value: editValue,
|
|
2794
2932
|
onChange: e => setEditValue(e.target.value),
|
|
@@ -2864,24 +3002,12 @@ const SortableComputedChip = ({
|
|
|
2864
3002
|
attributes,
|
|
2865
3003
|
listeners,
|
|
2866
3004
|
setNodeRef,
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
isDragging
|
|
2870
|
-
} = useSortable({
|
|
2871
|
-
id
|
|
2872
|
-
});
|
|
3005
|
+
style
|
|
3006
|
+
} = useSortableRow(id);
|
|
2873
3007
|
const [isEditing, setIsEditing] = useState(false);
|
|
2874
3008
|
const [editName, setEditName] = useState("");
|
|
2875
3009
|
const [editValue, setEditValue] = useState("");
|
|
2876
3010
|
const containerRef = useRef(null);
|
|
2877
|
-
const style = {
|
|
2878
|
-
transform: CSS.Transform.toString(transform),
|
|
2879
|
-
transition,
|
|
2880
|
-
opacity: isDragging ? 0.5 : 1,
|
|
2881
|
-
display: "flex",
|
|
2882
|
-
alignItems: "center",
|
|
2883
|
-
width: "100%"
|
|
2884
|
-
};
|
|
2885
3011
|
|
|
2886
3012
|
// Handle click outside to cancel
|
|
2887
3013
|
useEffect(() => {
|
|
@@ -2944,21 +3070,9 @@ const SortableComputedChip = ({
|
|
|
2944
3070
|
opacity: 1
|
|
2945
3071
|
}
|
|
2946
3072
|
}
|
|
2947
|
-
}, /*#__PURE__*/React__default.createElement(
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
alignItems: "center",
|
|
2951
|
-
cursor: "grab",
|
|
2952
|
-
"&:active": {
|
|
2953
|
-
cursor: "grabbing"
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
2957
|
-
sx: {
|
|
2958
|
-
cursor: "grab",
|
|
2959
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
2960
|
-
}
|
|
2961
|
-
})), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Chip, {
|
|
3073
|
+
}, /*#__PURE__*/React__default.createElement(DragHandle, {
|
|
3074
|
+
listeners: listeners
|
|
3075
|
+
}), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Chip, {
|
|
2962
3076
|
icon: /*#__PURE__*/React__default.createElement(FunctionsIcon, {
|
|
2963
3077
|
sx: {
|
|
2964
3078
|
fontSize: "15px !important"
|
|
@@ -3022,29 +3136,12 @@ const SortableComputedChip = ({
|
|
|
3022
3136
|
sx: {
|
|
3023
3137
|
flex: 1
|
|
3024
3138
|
}
|
|
3025
|
-
}), /*#__PURE__*/React__default.createElement(
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
transition: "opacity 0.2s"
|
|
3032
|
-
}
|
|
3033
|
-
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3034
|
-
size: "small",
|
|
3035
|
-
onClick: onMoveUp,
|
|
3036
|
-
disabled: isFirst,
|
|
3037
|
-
"aria-label": "move up"
|
|
3038
|
-
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
3039
|
-
fontSize: "small"
|
|
3040
|
-
})), /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3041
|
-
size: "small",
|
|
3042
|
-
onClick: onMoveDown,
|
|
3043
|
-
disabled: isLast,
|
|
3044
|
-
"aria-label": "move down"
|
|
3045
|
-
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
3046
|
-
fontSize: "small"
|
|
3047
|
-
})))) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
3139
|
+
}), /*#__PURE__*/React__default.createElement(MoveButtons, {
|
|
3140
|
+
onMoveUp: onMoveUp,
|
|
3141
|
+
onMoveDown: onMoveDown,
|
|
3142
|
+
isFirst: isFirst,
|
|
3143
|
+
isLast: isLast
|
|
3144
|
+
})) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
3048
3145
|
value: editName,
|
|
3049
3146
|
onChange: e => setEditName(e.target.value),
|
|
3050
3147
|
onKeyDown: handleKeyDown,
|
|
@@ -3129,9 +3226,7 @@ const Dimensions = ({
|
|
|
3129
3226
|
const [computedValue, setComputedValue] = useState("");
|
|
3130
3227
|
|
|
3131
3228
|
// Setup drag and drop sensors
|
|
3132
|
-
const sensors =
|
|
3133
|
-
coordinateGetter: sortableKeyboardCoordinates
|
|
3134
|
-
}));
|
|
3229
|
+
const sensors = useSortableListSensors();
|
|
3135
3230
|
|
|
3136
3231
|
// Resolve the unified dimensionOrder into the actual dimension / computed
|
|
3137
3232
|
// dimension objects, so dimensions and computed dimensions can be shown
|
|
@@ -3148,32 +3243,12 @@ const Dimensions = ({
|
|
|
3148
3243
|
data: computedDimensionByName[entry.key]
|
|
3149
3244
|
}).filter(item => item.data);
|
|
3150
3245
|
|
|
3151
|
-
// Handle drag
|
|
3152
|
-
const
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
if (over && active.id !== over.id) {
|
|
3158
|
-
const oldIndex = dimensionOrder.findIndex((_, i) => i === active.id);
|
|
3159
|
-
const newIndex = dimensionOrder.findIndex((_, i) => i === over.id);
|
|
3160
|
-
onReorderDimensionItems(arrayMove(dimensionOrder, oldIndex, newIndex));
|
|
3161
|
-
}
|
|
3162
|
-
};
|
|
3163
|
-
|
|
3164
|
-
// Handle move up
|
|
3165
|
-
const handleMoveUp = index => {
|
|
3166
|
-
if (index > 0) {
|
|
3167
|
-
onReorderDimensionItems(arrayMove(dimensionOrder, index, index - 1));
|
|
3168
|
-
}
|
|
3169
|
-
};
|
|
3170
|
-
|
|
3171
|
-
// Handle move down
|
|
3172
|
-
const handleMoveDown = index => {
|
|
3173
|
-
if (index < dimensionOrder.length - 1) {
|
|
3174
|
-
onReorderDimensionItems(arrayMove(dimensionOrder, index, index + 1));
|
|
3175
|
-
}
|
|
3176
|
-
};
|
|
3246
|
+
// Handle drag/move for the unified list
|
|
3247
|
+
const {
|
|
3248
|
+
handleDragEnd,
|
|
3249
|
+
handleMoveUp,
|
|
3250
|
+
handleMoveDown
|
|
3251
|
+
} = makeListReorderHandlers(dimensionOrder, onReorderDimensionItems);
|
|
3177
3252
|
|
|
3178
3253
|
// Handle sort order change (dimensions only - computed dimensions have no order_by)
|
|
3179
3254
|
const handleSortOrderChange = (fullPath, sortOrder) => {
|
|
@@ -3666,19 +3741,10 @@ const Dimensions = ({
|
|
|
3666
3741
|
color: "#666",
|
|
3667
3742
|
marginBottom: 2
|
|
3668
3743
|
}
|
|
3669
|
-
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__default.createElement(
|
|
3744
|
+
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__default.createElement(SortableListContext, {
|
|
3670
3745
|
sensors: sensors,
|
|
3671
|
-
|
|
3746
|
+
itemIds: orderedItems.map((_, index) => index),
|
|
3672
3747
|
onDragEnd: handleDragEnd
|
|
3673
|
-
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
3674
|
-
items: orderedItems.map((_, index) => index),
|
|
3675
|
-
strategy: verticalListSortingStrategy
|
|
3676
|
-
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3677
|
-
sx: {
|
|
3678
|
-
display: "flex",
|
|
3679
|
-
flexDirection: "column",
|
|
3680
|
-
gap: 1
|
|
3681
|
-
}
|
|
3682
3748
|
}, orderedItems.map((item, index) => item.kind === "dimension" ? /*#__PURE__*/React__default.createElement(SortableChip$1, {
|
|
3683
3749
|
key: `dim-${item.key}`,
|
|
3684
3750
|
id: index,
|
|
@@ -3708,7 +3774,7 @@ const Dimensions = ({
|
|
|
3708
3774
|
onMoveDown: () => handleMoveDown(index),
|
|
3709
3775
|
isFirst: index === 0,
|
|
3710
3776
|
isLast: index === orderedItems.length - 1
|
|
3711
|
-
})))))
|
|
3777
|
+
})))));
|
|
3712
3778
|
};
|
|
3713
3779
|
|
|
3714
3780
|
const MetricSourceIcon = ({
|
|
@@ -3783,20 +3849,8 @@ const SortableChip = ({
|
|
|
3783
3849
|
attributes,
|
|
3784
3850
|
listeners,
|
|
3785
3851
|
setNodeRef,
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
isDragging
|
|
3789
|
-
} = useSortable({
|
|
3790
|
-
id
|
|
3791
|
-
});
|
|
3792
|
-
const style = {
|
|
3793
|
-
transform: CSS.Transform.toString(transform),
|
|
3794
|
-
transition,
|
|
3795
|
-
opacity: isDragging ? 0.5 : 1,
|
|
3796
|
-
display: 'flex',
|
|
3797
|
-
alignItems: 'center',
|
|
3798
|
-
width: '100%'
|
|
3799
|
-
};
|
|
3852
|
+
style
|
|
3853
|
+
} = useSortableRow(id);
|
|
3800
3854
|
|
|
3801
3855
|
// Focus input when entering edit mode
|
|
3802
3856
|
useEffect(() => {
|
|
@@ -3874,21 +3928,9 @@ const SortableChip = ({
|
|
|
3874
3928
|
opacity: 1 // show icons on hover
|
|
3875
3929
|
}
|
|
3876
3930
|
}
|
|
3877
|
-
}, /*#__PURE__*/React__default.createElement(
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
alignItems: "center",
|
|
3881
|
-
cursor: "grab",
|
|
3882
|
-
"&:active": {
|
|
3883
|
-
cursor: "grabbing"
|
|
3884
|
-
}
|
|
3885
|
-
}
|
|
3886
|
-
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
3887
|
-
sx: {
|
|
3888
|
-
cursor: "grab",
|
|
3889
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
3890
|
-
}
|
|
3891
|
-
})), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, source && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3931
|
+
}, /*#__PURE__*/React__default.createElement(DragHandle, {
|
|
3932
|
+
listeners: listeners
|
|
3933
|
+
}), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, source && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3892
3934
|
sx: {
|
|
3893
3935
|
display: 'flex',
|
|
3894
3936
|
alignItems: 'center',
|
|
@@ -3950,29 +3992,12 @@ const SortableChip = ({
|
|
|
3950
3992
|
sx: {
|
|
3951
3993
|
flex: 1
|
|
3952
3994
|
}
|
|
3953
|
-
}), /*#__PURE__*/React__default.createElement(
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
transition: "opacity 0.2s"
|
|
3960
|
-
}
|
|
3961
|
-
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3962
|
-
size: "small",
|
|
3963
|
-
onClick: onMoveUp,
|
|
3964
|
-
disabled: isFirst,
|
|
3965
|
-
"aria-label": "move up"
|
|
3966
|
-
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
3967
|
-
fontSize: "small"
|
|
3968
|
-
})), /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3969
|
-
size: "small",
|
|
3970
|
-
onClick: onMoveDown,
|
|
3971
|
-
disabled: isLast,
|
|
3972
|
-
"aria-label": "move down"
|
|
3973
|
-
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
3974
|
-
fontSize: "small"
|
|
3975
|
-
})))) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
3995
|
+
}), /*#__PURE__*/React__default.createElement(MoveButtons, {
|
|
3996
|
+
onMoveUp: onMoveUp,
|
|
3997
|
+
onMoveDown: onMoveDown,
|
|
3998
|
+
isFirst: isFirst,
|
|
3999
|
+
isLast: isLast
|
|
4000
|
+
})) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
3976
4001
|
inputRef: inputRef,
|
|
3977
4002
|
value: editValue,
|
|
3978
4003
|
onChange: e => setEditValue(e.target.value),
|
|
@@ -4047,39 +4072,14 @@ const Metrics = ({
|
|
|
4047
4072
|
const [selectedMetric, setSelectedMetric] = useState('');
|
|
4048
4073
|
|
|
4049
4074
|
// Setup drag and drop sensors
|
|
4050
|
-
const sensors =
|
|
4051
|
-
coordinateGetter: sortableKeyboardCoordinates
|
|
4052
|
-
}));
|
|
4053
|
-
|
|
4054
|
-
// Handle drag end
|
|
4055
|
-
const handleDragEnd = event => {
|
|
4056
|
-
const {
|
|
4057
|
-
active,
|
|
4058
|
-
over
|
|
4059
|
-
} = event;
|
|
4060
|
-
if (over && active.id !== over.id) {
|
|
4061
|
-
const oldIndex = savedMetrics.findIndex((_, i) => i === active.id);
|
|
4062
|
-
const newIndex = savedMetrics.findIndex((_, i) => i === over.id);
|
|
4063
|
-
const newOrder = arrayMove(savedMetrics, oldIndex, newIndex);
|
|
4064
|
-
onReorderMetrics(newOrder);
|
|
4065
|
-
}
|
|
4066
|
-
};
|
|
4075
|
+
const sensors = useSortableListSensors();
|
|
4067
4076
|
|
|
4068
|
-
// Handle move
|
|
4069
|
-
const
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
};
|
|
4075
|
-
|
|
4076
|
-
// Handle move down
|
|
4077
|
-
const handleMoveDown = index => {
|
|
4078
|
-
if (index < savedMetrics.length - 1) {
|
|
4079
|
-
const newOrder = arrayMove(savedMetrics, index, index + 1);
|
|
4080
|
-
onReorderMetrics(newOrder);
|
|
4081
|
-
}
|
|
4082
|
-
};
|
|
4077
|
+
// Handle drag/move
|
|
4078
|
+
const {
|
|
4079
|
+
handleDragEnd,
|
|
4080
|
+
handleMoveUp,
|
|
4081
|
+
handleMoveDown
|
|
4082
|
+
} = makeListReorderHandlers(savedMetrics, onReorderMetrics);
|
|
4083
4083
|
|
|
4084
4084
|
// Get the current provider based on selection chain
|
|
4085
4085
|
const getCurrentProvider = () => {
|
|
@@ -4418,19 +4418,10 @@ const Metrics = ({
|
|
|
4418
4418
|
marginBottom: 1,
|
|
4419
4419
|
color: "rgb(37, 37, 37)"
|
|
4420
4420
|
}
|
|
4421
|
-
}, "Saved Metrics"), /*#__PURE__*/React__default.createElement(
|
|
4421
|
+
}, "Saved Metrics"), /*#__PURE__*/React__default.createElement(SortableListContext, {
|
|
4422
4422
|
sensors: sensors,
|
|
4423
|
-
|
|
4423
|
+
itemIds: savedMetrics.map((_, index) => index),
|
|
4424
4424
|
onDragEnd: handleDragEnd
|
|
4425
|
-
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
4426
|
-
items: savedMetrics.map((_, index) => index),
|
|
4427
|
-
strategy: verticalListSortingStrategy
|
|
4428
|
-
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
4429
|
-
sx: {
|
|
4430
|
-
display: 'flex',
|
|
4431
|
-
flexDirection: 'column',
|
|
4432
|
-
gap: 1
|
|
4433
|
-
}
|
|
4434
4425
|
}, savedMetrics.map((metric, index) => /*#__PURE__*/React__default.createElement(SortableChip, {
|
|
4435
4426
|
key: index,
|
|
4436
4427
|
id: index,
|
|
@@ -4447,7 +4438,7 @@ const Metrics = ({
|
|
|
4447
4438
|
onUpdateTitle: onUpdateTitle,
|
|
4448
4439
|
onResetTitle: onResetTitle,
|
|
4449
4440
|
source: metric.metric?.source
|
|
4450
|
-
})))))
|
|
4441
|
+
})))));
|
|
4451
4442
|
};
|
|
4452
4443
|
|
|
4453
4444
|
const Filters = ({
|
|
@@ -5674,6 +5665,205 @@ const Filters = ({
|
|
|
5674
5665
|
}))));
|
|
5675
5666
|
};
|
|
5676
5667
|
|
|
5668
|
+
const KIND_LABEL = {
|
|
5669
|
+
dimension: "Dimension",
|
|
5670
|
+
computed: "Computed",
|
|
5671
|
+
metric: "Metric"
|
|
5672
|
+
};
|
|
5673
|
+
const KindChip = ({
|
|
5674
|
+
kind,
|
|
5675
|
+
source
|
|
5676
|
+
}) => {
|
|
5677
|
+
if (kind === "metric") {
|
|
5678
|
+
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5679
|
+
sx: {
|
|
5680
|
+
display: "flex",
|
|
5681
|
+
alignItems: "center",
|
|
5682
|
+
flexShrink: 0
|
|
5683
|
+
}
|
|
5684
|
+
}, /*#__PURE__*/React__default.createElement(MetricSourceIcon, {
|
|
5685
|
+
source: source
|
|
5686
|
+
}));
|
|
5687
|
+
}
|
|
5688
|
+
return /*#__PURE__*/React__default.createElement(Chip, {
|
|
5689
|
+
icon: kind === "computed" ? /*#__PURE__*/React__default.createElement(CalculateIcon, {
|
|
5690
|
+
sx: {
|
|
5691
|
+
fontSize: "15px !important"
|
|
5692
|
+
}
|
|
5693
|
+
}) : undefined,
|
|
5694
|
+
label: KIND_LABEL[kind],
|
|
5695
|
+
size: "small",
|
|
5696
|
+
sx: {
|
|
5697
|
+
fontWeight: 500,
|
|
5698
|
+
backgroundColor: "rgba(70, 134, 128, 0.1)",
|
|
5699
|
+
color: "rgb(70, 134, 128)",
|
|
5700
|
+
flexShrink: 0
|
|
5701
|
+
}
|
|
5702
|
+
});
|
|
5703
|
+
};
|
|
5704
|
+
|
|
5705
|
+
// Read-only sortable row: this tab only reorders columns. Adding, removing,
|
|
5706
|
+
// and renaming stay on the Dimensions/Metrics tabs so there's one place each
|
|
5707
|
+
// field is managed.
|
|
5708
|
+
const SortableColumnRow = ({
|
|
5709
|
+
id,
|
|
5710
|
+
label,
|
|
5711
|
+
fullPath,
|
|
5712
|
+
kind,
|
|
5713
|
+
source,
|
|
5714
|
+
onMoveUp,
|
|
5715
|
+
onMoveDown,
|
|
5716
|
+
isFirst,
|
|
5717
|
+
isLast
|
|
5718
|
+
}) => {
|
|
5719
|
+
const reportingContext = useReportingContextOptional();
|
|
5720
|
+
const parameters = reportingContext?.parameters;
|
|
5721
|
+
const {
|
|
5722
|
+
attributes,
|
|
5723
|
+
listeners,
|
|
5724
|
+
setNodeRef,
|
|
5725
|
+
style
|
|
5726
|
+
} = useSortableRow(id);
|
|
5727
|
+
const displayLabel = interpolateTitle(label, parameters);
|
|
5728
|
+
return /*#__PURE__*/React__default.createElement("div", _extends({
|
|
5729
|
+
ref: setNodeRef,
|
|
5730
|
+
style: style
|
|
5731
|
+
}, attributes), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5732
|
+
sx: {
|
|
5733
|
+
display: "flex",
|
|
5734
|
+
alignItems: "center",
|
|
5735
|
+
width: "100%",
|
|
5736
|
+
gap: 1,
|
|
5737
|
+
backgroundColor: "white",
|
|
5738
|
+
border: "1px solid #e0e0e0",
|
|
5739
|
+
borderRadius: 2,
|
|
5740
|
+
padding: 1,
|
|
5741
|
+
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
|
5742
|
+
"&:hover .hover-icons": {
|
|
5743
|
+
opacity: 1
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
}, /*#__PURE__*/React__default.createElement(DragHandle, {
|
|
5747
|
+
listeners: listeners
|
|
5748
|
+
}), /*#__PURE__*/React__default.createElement(KindChip, {
|
|
5749
|
+
kind: kind,
|
|
5750
|
+
source: source
|
|
5751
|
+
}), /*#__PURE__*/React__default.createElement(Tooltip$1, {
|
|
5752
|
+
title: fullPath,
|
|
5753
|
+
arrow: true,
|
|
5754
|
+
placement: "top"
|
|
5755
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
5756
|
+
variant: "h6",
|
|
5757
|
+
sx: {
|
|
5758
|
+
fontWeight: 500,
|
|
5759
|
+
color: "#1a1a1a",
|
|
5760
|
+
fontSize: "14px",
|
|
5761
|
+
whiteSpace: "nowrap",
|
|
5762
|
+
overflow: "hidden",
|
|
5763
|
+
textOverflow: "ellipsis"
|
|
5764
|
+
}
|
|
5765
|
+
}, displayLabel)), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5766
|
+
sx: {
|
|
5767
|
+
flex: 1
|
|
5768
|
+
}
|
|
5769
|
+
}), /*#__PURE__*/React__default.createElement(MoveButtons, {
|
|
5770
|
+
onMoveUp: onMoveUp,
|
|
5771
|
+
onMoveDown: onMoveDown,
|
|
5772
|
+
isFirst: isFirst,
|
|
5773
|
+
isLast: isLast
|
|
5774
|
+
})));
|
|
5775
|
+
};
|
|
5776
|
+
const ColumnOrder = ({
|
|
5777
|
+
orderedDimensionItems = [],
|
|
5778
|
+
metrics = [],
|
|
5779
|
+
columnOrder = [],
|
|
5780
|
+
onReorderColumnItems,
|
|
5781
|
+
titleOverrides = {
|
|
5782
|
+
dimensions: {},
|
|
5783
|
+
metrics: {}
|
|
5784
|
+
}
|
|
5785
|
+
}) => {
|
|
5786
|
+
const isCustomized = columnOrder.length > 0;
|
|
5787
|
+
const resolvedItems = resolveColumnOrder(columnOrder, orderedDimensionItems, metrics);
|
|
5788
|
+
const sensors = useSortableListSensors();
|
|
5789
|
+
const reorderResolved = newResolvedItems => {
|
|
5790
|
+
onReorderColumnItems(newResolvedItems.map(item => ({
|
|
5791
|
+
type: item.kind,
|
|
5792
|
+
key: item.key
|
|
5793
|
+
})));
|
|
5794
|
+
};
|
|
5795
|
+
const {
|
|
5796
|
+
handleDragEnd,
|
|
5797
|
+
handleMoveUp,
|
|
5798
|
+
handleMoveDown
|
|
5799
|
+
} = makeListReorderHandlers(resolvedItems, reorderResolved);
|
|
5800
|
+
const labelFor = item => {
|
|
5801
|
+
if (item.kind === "computed") return item.data.name;
|
|
5802
|
+
if (item.kind === "metric") {
|
|
5803
|
+
return titleOverrides.metrics[item.key] || item.data.metricTitle || item.key;
|
|
5804
|
+
}
|
|
5805
|
+
return titleOverrides.dimensions[item.key] || item.data.dimensionTitle || item.key;
|
|
5806
|
+
};
|
|
5807
|
+
return /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5808
|
+
sx: {
|
|
5809
|
+
display: "flex",
|
|
5810
|
+
justifyContent: "space-between",
|
|
5811
|
+
alignItems: "flex-start",
|
|
5812
|
+
mb: 2,
|
|
5813
|
+
gap: 2
|
|
5814
|
+
}
|
|
5815
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
5816
|
+
sx: {
|
|
5817
|
+
fontSize: "13px",
|
|
5818
|
+
color: "#666"
|
|
5819
|
+
}
|
|
5820
|
+
}, "Optional: define a single order for all columns (dimensions, computed dimensions and metrics together) in the results grid. Drag to reorder or use the arrows. Newly added dimensions/metrics are appended to the end automatically. Without a custom order, columns follow the Dimensions tab order, then the Metrics tab order."), isCustomized && /*#__PURE__*/React__default.createElement(Tooltip$1, {
|
|
5821
|
+
title: "Discard the custom order and go back to the default (Dimensions, then Metrics)",
|
|
5822
|
+
arrow: true
|
|
5823
|
+
}, /*#__PURE__*/React__default.createElement(Button, {
|
|
5824
|
+
variant: "outlined",
|
|
5825
|
+
size: "small",
|
|
5826
|
+
startIcon: /*#__PURE__*/React__default.createElement(RestartAltIcon, null),
|
|
5827
|
+
onClick: () => onReorderColumnItems([]),
|
|
5828
|
+
sx: {
|
|
5829
|
+
height: "36px",
|
|
5830
|
+
flexShrink: 0,
|
|
5831
|
+
fontFamily: "system-ui",
|
|
5832
|
+
fontSize: "13px",
|
|
5833
|
+
fontWeight: 500,
|
|
5834
|
+
borderRadius: "8px",
|
|
5835
|
+
boxShadow: "none",
|
|
5836
|
+
textTransform: "none",
|
|
5837
|
+
borderColor: "#e0e0e0",
|
|
5838
|
+
color: "rgb(37, 37, 37)",
|
|
5839
|
+
"&:hover": {
|
|
5840
|
+
backgroundColor: "#f5f5f5",
|
|
5841
|
+
borderColor: "#d0d0d0"
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
}, "Reset to Default"))), resolvedItems.length === 0 ? /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
5845
|
+
sx: {
|
|
5846
|
+
fontSize: "13px",
|
|
5847
|
+
color: "#999"
|
|
5848
|
+
}
|
|
5849
|
+
}, "Add dimensions or metrics in the other tabs first.") : /*#__PURE__*/React__default.createElement(SortableListContext, {
|
|
5850
|
+
sensors: sensors,
|
|
5851
|
+
itemIds: resolvedItems.map((_, index) => index),
|
|
5852
|
+
onDragEnd: handleDragEnd
|
|
5853
|
+
}, resolvedItems.map((item, index) => /*#__PURE__*/React__default.createElement(SortableColumnRow, {
|
|
5854
|
+
key: `${item.kind}-${item.key}`,
|
|
5855
|
+
id: index,
|
|
5856
|
+
label: labelFor(item),
|
|
5857
|
+
fullPath: item.kind === "computed" ? item.data.value : item.key,
|
|
5858
|
+
kind: item.kind,
|
|
5859
|
+
source: item.kind === "metric" ? item.data.metric?.source : undefined,
|
|
5860
|
+
onMoveUp: () => handleMoveUp(index),
|
|
5861
|
+
onMoveDown: () => handleMoveDown(index),
|
|
5862
|
+
isFirst: index === 0,
|
|
5863
|
+
isLast: index === resolvedItems.length - 1
|
|
5864
|
+
}))));
|
|
5865
|
+
};
|
|
5866
|
+
|
|
5677
5867
|
// Default numeral.js formats applied when a dimension/metric definition has a
|
|
5678
5868
|
// known type but no explicit `format` set on the provider.
|
|
5679
5869
|
const DEFAULT_FORMATS_BY_TYPE = {
|
|
@@ -5688,10 +5878,107 @@ const formatValue = (value, def) => {
|
|
|
5688
5878
|
return def?.prefix ? `${def.prefix} ${formatted}` : formatted;
|
|
5689
5879
|
};
|
|
5690
5880
|
const isNumericType = type => type === 'integer' || type === 'currency';
|
|
5881
|
+
|
|
5882
|
+
// Builds a single DataGrid column definition for one resolved column item
|
|
5883
|
+
// (a dimension, computed dimension, or metric). Field naming logic:
|
|
5884
|
+
// - Dimensions from base provider: baseAlias.field -> baseAlias_field
|
|
5885
|
+
// - Dimensions from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5886
|
+
// - Computed dimensions have no provider/relations chain, so the API returns
|
|
5887
|
+
// each row keyed by the computed dimension's own `name`.
|
|
5888
|
+
// - Metrics: API returns metric keys in different forms depending on path depth
|
|
5889
|
+
// (see comment inline below), normalised to match the row-key normalisation
|
|
5890
|
+
// done when building `rows`.
|
|
5891
|
+
const buildColumn = (item, titleOverrides, parameters) => {
|
|
5892
|
+
if (item.kind === 'computed') {
|
|
5893
|
+
const cd = item.data;
|
|
5894
|
+
return {
|
|
5895
|
+
field: cd.name,
|
|
5896
|
+
headerName: cd.name,
|
|
5897
|
+
flex: 1,
|
|
5898
|
+
minWidth: 150,
|
|
5899
|
+
type: 'string'
|
|
5900
|
+
};
|
|
5901
|
+
}
|
|
5902
|
+
if (item.kind === 'metric') {
|
|
5903
|
+
const metric = item.data;
|
|
5904
|
+
const metricDef = metric.metric;
|
|
5905
|
+
let fieldName;
|
|
5906
|
+
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
5907
|
+
if (dotCount >= 2) {
|
|
5908
|
+
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
5909
|
+
// Normalise dots to underscores to match the row key normalisation below.
|
|
5910
|
+
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
5911
|
+
fieldName = metric.fullPath.replace(/\./g, '_');
|
|
5912
|
+
} else if (metric.relations && metric.relations.length > 0) {
|
|
5913
|
+
// 2-part path from a nested provider: API drops the base provider alias.
|
|
5914
|
+
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
5915
|
+
const parts = metric.fullPath.split('.');
|
|
5916
|
+
const pathWithoutField = parts[0]; // e.g., "mc_fa"
|
|
5917
|
+
const field = parts[1]; // e.g., "total_amount"
|
|
5918
|
+
|
|
5919
|
+
const pathParts = pathWithoutField.split('_');
|
|
5920
|
+
pathParts.shift(); // remove base provider alias
|
|
5921
|
+
const pathWithoutBase = pathParts.join('_');
|
|
5922
|
+
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5923
|
+
} else {
|
|
5924
|
+
// 2-part path from the base provider: API returns just the metric name.
|
|
5925
|
+
// Example: "mc.record_count" -> "record_count"
|
|
5926
|
+
fieldName = metric.metricName;
|
|
5927
|
+
}
|
|
5928
|
+
const headerName = interpolateTitle(titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName, parameters);
|
|
5929
|
+
return {
|
|
5930
|
+
field: fieldName,
|
|
5931
|
+
headerName: headerName,
|
|
5932
|
+
flex: 1,
|
|
5933
|
+
minWidth: 150,
|
|
5934
|
+
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
5935
|
+
renderHeader: () => /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5936
|
+
sx: {
|
|
5937
|
+
display: 'flex',
|
|
5938
|
+
alignItems: 'center',
|
|
5939
|
+
gap: 0.5
|
|
5940
|
+
}
|
|
5941
|
+
}, /*#__PURE__*/React__default.createElement(MetricSourceIcon, {
|
|
5942
|
+
source: metricDef?.source
|
|
5943
|
+
}), /*#__PURE__*/React__default.createElement("span", null, headerName)),
|
|
5944
|
+
valueFormatter: value => formatValue(value, metricDef)
|
|
5945
|
+
};
|
|
5946
|
+
}
|
|
5947
|
+
|
|
5948
|
+
// Dimension
|
|
5949
|
+
const dim = item.data;
|
|
5950
|
+
let fieldName;
|
|
5951
|
+
if (dim.relations && dim.relations.length > 0) {
|
|
5952
|
+
// From nested provider: drop the base provider alias
|
|
5953
|
+
// Example: ft_fa_db.currency -> fa_db_currency
|
|
5954
|
+
const parts = dim.fullPath.split('.');
|
|
5955
|
+
const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
|
|
5956
|
+
const field = parts[1]; // e.g., "currency"
|
|
5957
|
+
|
|
5958
|
+
const pathParts = pathWithoutField.split('_');
|
|
5959
|
+
pathParts.shift(); // Remove base provider alias
|
|
5960
|
+
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
5961
|
+
|
|
5962
|
+
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5963
|
+
} else {
|
|
5964
|
+
// From base provider: keep the full path with underscore
|
|
5965
|
+
// Example: ba.created_at -> ba_created_at
|
|
5966
|
+
fieldName = dim.fullPath.replace('.', '_');
|
|
5967
|
+
}
|
|
5968
|
+
const headerName = interpolateTitle(titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName, parameters);
|
|
5969
|
+
const dimDef = dim.dimension;
|
|
5970
|
+
return {
|
|
5971
|
+
field: fieldName,
|
|
5972
|
+
headerName: headerName,
|
|
5973
|
+
flex: 1,
|
|
5974
|
+
minWidth: 150,
|
|
5975
|
+
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
5976
|
+
valueFormatter: value => formatValue(value, dimDef)
|
|
5977
|
+
};
|
|
5978
|
+
};
|
|
5691
5979
|
const ReportDataGrid = ({
|
|
5692
5980
|
reportData,
|
|
5693
|
-
|
|
5694
|
-
metrics,
|
|
5981
|
+
columnItems = [],
|
|
5695
5982
|
loading,
|
|
5696
5983
|
onPageChange,
|
|
5697
5984
|
totalRows = 0,
|
|
@@ -5707,127 +5994,10 @@ const ReportDataGrid = ({
|
|
|
5707
5994
|
pageSize: 50
|
|
5708
5995
|
});
|
|
5709
5996
|
|
|
5710
|
-
// Generate columns dynamically
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
// Add dimension + computed-dimension columns, in the single unified
|
|
5715
|
-
// order the user arranged them in on the Dimensions tab.
|
|
5716
|
-
// Field naming logic (dimensions):
|
|
5717
|
-
// - If from base provider: baseAlias.field -> baseAlias_field
|
|
5718
|
-
// - If from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5719
|
-
// Computed dimensions have no provider/relations chain, so the API
|
|
5720
|
-
// returns each row keyed by the computed dimension's own `name`.
|
|
5721
|
-
orderedDimensionItems.forEach(item => {
|
|
5722
|
-
if (item.kind === 'computed') {
|
|
5723
|
-
const cd = item.data;
|
|
5724
|
-
cols.push({
|
|
5725
|
-
field: cd.name,
|
|
5726
|
-
headerName: cd.name,
|
|
5727
|
-
flex: 1,
|
|
5728
|
-
minWidth: 150,
|
|
5729
|
-
type: 'string'
|
|
5730
|
-
});
|
|
5731
|
-
return;
|
|
5732
|
-
}
|
|
5733
|
-
const dim = item.data;
|
|
5734
|
-
let fieldName;
|
|
5735
|
-
let headerName;
|
|
5736
|
-
|
|
5737
|
-
// Check if there are relations (nested providers)
|
|
5738
|
-
if (dim.relations && dim.relations.length > 0) {
|
|
5739
|
-
// From nested provider: drop the base provider alias
|
|
5740
|
-
// Example: ft_fa_db.currency -> fa_db_currency
|
|
5741
|
-
const parts = dim.fullPath.split('.');
|
|
5742
|
-
const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
|
|
5743
|
-
const field = parts[1]; // e.g., "currency"
|
|
5744
|
-
|
|
5745
|
-
// Remove the base provider alias (first part before first underscore)
|
|
5746
|
-
const pathParts = pathWithoutField.split('_');
|
|
5747
|
-
pathParts.shift(); // Remove base provider alias
|
|
5748
|
-
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
5749
|
-
|
|
5750
|
-
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5751
|
-
} else {
|
|
5752
|
-
// From base provider: keep the full path with underscore
|
|
5753
|
-
// Example: ba.created_at -> ba_created_at
|
|
5754
|
-
fieldName = dim.fullPath.replace('.', '_');
|
|
5755
|
-
}
|
|
5756
|
-
|
|
5757
|
-
// Check for title override, otherwise use the friendly dimension title from provider
|
|
5758
|
-
headerName = titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName;
|
|
5759
|
-
headerName = interpolateTitle(headerName, parameters);
|
|
5760
|
-
const dimDef = dim.dimension;
|
|
5761
|
-
cols.push({
|
|
5762
|
-
field: fieldName,
|
|
5763
|
-
headerName: headerName,
|
|
5764
|
-
flex: 1,
|
|
5765
|
-
minWidth: 150,
|
|
5766
|
-
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
5767
|
-
valueFormatter: value => formatValue(value, dimDef)
|
|
5768
|
-
});
|
|
5769
|
-
});
|
|
5770
|
-
|
|
5771
|
-
// Add metric columns
|
|
5772
|
-
// The API returns metric keys in two different forms depending on the path depth:
|
|
5773
|
-
//
|
|
5774
|
-
// • 2-part path "mc.record_count" → API key: "record_count"
|
|
5775
|
-
// • 2-part path "mc_fa.total_amount" (nested) → API key: "fa_total_amount"
|
|
5776
|
-
// • 3+-part path "mc.dkpi.activeAgreementsCount"→ API key: "mc.dkpi.activeAgreementsCount" (dots)
|
|
5777
|
-
//
|
|
5778
|
-
// For 3+-part paths the API key contains dots. MUI DataGrid interprets dots
|
|
5779
|
-
// in field names as nested-object accessors, so the rows useMemo normalises
|
|
5780
|
-
// those keys (dots → underscores). The column field must match that normalised key.
|
|
5781
|
-
metrics.forEach(metric => {
|
|
5782
|
-
const metricDef = metric.metric;
|
|
5783
|
-
let fieldName;
|
|
5784
|
-
let headerName;
|
|
5785
|
-
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
5786
|
-
if (dotCount >= 2) {
|
|
5787
|
-
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
5788
|
-
// Normalise dots to underscores to match the row key normalisation below.
|
|
5789
|
-
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
5790
|
-
fieldName = metric.fullPath.replace(/\./g, '_');
|
|
5791
|
-
} else if (metric.relations && metric.relations.length > 0) {
|
|
5792
|
-
// 2-part path from a nested provider: API drops the base provider alias.
|
|
5793
|
-
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
5794
|
-
const parts = metric.fullPath.split('.');
|
|
5795
|
-
const pathWithoutField = parts[0]; // e.g., "mc_fa"
|
|
5796
|
-
const field = parts[1]; // e.g., "total_amount"
|
|
5797
|
-
|
|
5798
|
-
const pathParts = pathWithoutField.split('_');
|
|
5799
|
-
pathParts.shift(); // remove base provider alias
|
|
5800
|
-
const pathWithoutBase = pathParts.join('_');
|
|
5801
|
-
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5802
|
-
} else {
|
|
5803
|
-
// 2-part path from the base provider: API returns just the metric name.
|
|
5804
|
-
// Example: "mc.record_count" -> "record_count"
|
|
5805
|
-
fieldName = metric.metricName;
|
|
5806
|
-
}
|
|
5807
|
-
|
|
5808
|
-
// Check for title override, otherwise use the friendly metric title from provider
|
|
5809
|
-
headerName = titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName;
|
|
5810
|
-
headerName = interpolateTitle(headerName, parameters);
|
|
5811
|
-
cols.push({
|
|
5812
|
-
field: fieldName,
|
|
5813
|
-
headerName: headerName,
|
|
5814
|
-
flex: 1,
|
|
5815
|
-
minWidth: 150,
|
|
5816
|
-
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
5817
|
-
renderHeader: () => /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5818
|
-
sx: {
|
|
5819
|
-
display: 'flex',
|
|
5820
|
-
alignItems: 'center',
|
|
5821
|
-
gap: 0.5
|
|
5822
|
-
}
|
|
5823
|
-
}, /*#__PURE__*/React__default.createElement(MetricSourceIcon, {
|
|
5824
|
-
source: metricDef?.source
|
|
5825
|
-
}), /*#__PURE__*/React__default.createElement("span", null, headerName)),
|
|
5826
|
-
valueFormatter: value => formatValue(value, metricDef)
|
|
5827
|
-
});
|
|
5828
|
-
});
|
|
5829
|
-
return cols;
|
|
5830
|
-
}, [orderedDimensionItems, metrics, titleOverrides, parameters]);
|
|
5997
|
+
// Generate columns dynamically, in the final resolved column order
|
|
5998
|
+
// (custom Column Order tab arrangement when defined, otherwise
|
|
5999
|
+
// dimensions/computed then metrics).
|
|
6000
|
+
const columns = React__default.useMemo(() => columnItems.map(item => buildColumn(item, titleOverrides, parameters)), [columnItems, titleOverrides, parameters]);
|
|
5831
6001
|
|
|
5832
6002
|
// Transform report data to rows with unique IDs.
|
|
5833
6003
|
// Keys are normalised by replacing dots with underscores so that MUI DataGrid
|
|
@@ -5937,7 +6107,12 @@ const ReportBuilder = ({
|
|
|
5937
6107
|
computedDimensions: [],
|
|
5938
6108
|
// Unified display/execution order across dimensions + computedDimensions.
|
|
5939
6109
|
// Entries: { type: 'dimension', key: fullPath } | { type: 'computed', key: name }
|
|
5940
|
-
dimensionOrder: []
|
|
6110
|
+
dimensionOrder: [],
|
|
6111
|
+
// Optional unified display order across dimensions + computedDimensions +
|
|
6112
|
+
// metrics, set on the Column Order tab. Empty means "not defined" — the
|
|
6113
|
+
// results grid falls back to dimensionOrder followed by metrics order.
|
|
6114
|
+
// Entries: { type: 'dimension' | 'computed' | 'metric', key }
|
|
6115
|
+
columnOrder: []
|
|
5941
6116
|
});
|
|
5942
6117
|
const [titleOverrides, setTitleOverrides] = useState({
|
|
5943
6118
|
dimensions: {},
|
|
@@ -6168,6 +6343,46 @@ const ReportBuilder = ({
|
|
|
6168
6343
|
});
|
|
6169
6344
|
return dimensionOrder;
|
|
6170
6345
|
};
|
|
6346
|
+
|
|
6347
|
+
// Build the optional unified column order (dimensions + computed
|
|
6348
|
+
// dimensions + metrics) from the persisted `ordered_columns` field.
|
|
6349
|
+
// Unlike buildDimensionOrder, entries that no longer resolve are simply
|
|
6350
|
+
// dropped rather than backfilled — an empty/partial result just means "no
|
|
6351
|
+
// custom order defined (yet)", which resolveColumnOrder treats as
|
|
6352
|
+
// "fall back to the default order".
|
|
6353
|
+
const buildColumnOrder = (rawOrderedColumns, dimensions, computedDimensions, metrics) => {
|
|
6354
|
+
if (!Array.isArray(rawOrderedColumns)) return [];
|
|
6355
|
+
const dimensionPaths = new Set(dimensions.map(d => d.fullPath));
|
|
6356
|
+
const computedNames = new Set(computedDimensions.map(cd => cd.name));
|
|
6357
|
+
const metricPaths = new Set(metrics.map(m => m.fullPath));
|
|
6358
|
+
const columnOrder = [];
|
|
6359
|
+
rawOrderedColumns.forEach(entry => {
|
|
6360
|
+
if (entry.type === "dimension" && dimensionPaths.has(entry.ref)) {
|
|
6361
|
+
columnOrder.push({
|
|
6362
|
+
type: "dimension",
|
|
6363
|
+
key: entry.ref
|
|
6364
|
+
});
|
|
6365
|
+
} else if (entry.type === "computed" && computedNames.has(entry.ref)) {
|
|
6366
|
+
columnOrder.push({
|
|
6367
|
+
type: "computed",
|
|
6368
|
+
key: entry.ref
|
|
6369
|
+
});
|
|
6370
|
+
} else if (entry.type === "metric" && metricPaths.has(entry.ref)) {
|
|
6371
|
+
columnOrder.push({
|
|
6372
|
+
type: "metric",
|
|
6373
|
+
key: entry.ref
|
|
6374
|
+
});
|
|
6375
|
+
}
|
|
6376
|
+
});
|
|
6377
|
+
return columnOrder;
|
|
6378
|
+
};
|
|
6379
|
+
|
|
6380
|
+
// Auto-append a newly added dimension/computed dimension/metric to the end
|
|
6381
|
+
// of columnOrder — but only once a custom order actually exists. While
|
|
6382
|
+
// columnOrder is empty (no custom order defined) it's left untouched, so
|
|
6383
|
+
// the results grid keeps falling back to the default dimensions-then-
|
|
6384
|
+
// metrics order until the user visits the Column Order tab.
|
|
6385
|
+
const appendToColumnOrder = (columnOrder, entry) => columnOrder.length > 0 ? [...columnOrder, entry] : columnOrder;
|
|
6171
6386
|
const loadReportDefinition = async id => {
|
|
6172
6387
|
try {
|
|
6173
6388
|
console.log("Loading report definition:", id);
|
|
@@ -6264,6 +6479,7 @@ const ReportBuilder = ({
|
|
|
6264
6479
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6265
6480
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6266
6481
|
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6482
|
+
const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
|
|
6267
6483
|
|
|
6268
6484
|
// Set the report state
|
|
6269
6485
|
setReport({
|
|
@@ -6271,7 +6487,8 @@ const ReportBuilder = ({
|
|
|
6271
6487
|
metrics: reconstructedMetrics,
|
|
6272
6488
|
filters: loadedFilters,
|
|
6273
6489
|
computedDimensions,
|
|
6274
|
-
dimensionOrder
|
|
6490
|
+
dimensionOrder,
|
|
6491
|
+
columnOrder
|
|
6275
6492
|
});
|
|
6276
6493
|
|
|
6277
6494
|
// Set title overrides
|
|
@@ -6381,6 +6598,7 @@ const ReportBuilder = ({
|
|
|
6381
6598
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6382
6599
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6383
6600
|
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6601
|
+
const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
|
|
6384
6602
|
|
|
6385
6603
|
// Set the report state
|
|
6386
6604
|
setReport({
|
|
@@ -6388,7 +6606,8 @@ const ReportBuilder = ({
|
|
|
6388
6606
|
metrics: reconstructedMetrics,
|
|
6389
6607
|
filters: loadedFilters,
|
|
6390
6608
|
computedDimensions,
|
|
6391
|
-
dimensionOrder
|
|
6609
|
+
dimensionOrder,
|
|
6610
|
+
columnOrder
|
|
6392
6611
|
});
|
|
6393
6612
|
|
|
6394
6613
|
// Set title overrides
|
|
@@ -6422,7 +6641,8 @@ const ReportBuilder = ({
|
|
|
6422
6641
|
metrics: [],
|
|
6423
6642
|
filters: {},
|
|
6424
6643
|
computedDimensions: [],
|
|
6425
|
-
dimensionOrder: []
|
|
6644
|
+
dimensionOrder: [],
|
|
6645
|
+
columnOrder: []
|
|
6426
6646
|
});
|
|
6427
6647
|
// Reset title overrides
|
|
6428
6648
|
setTitleOverrides({
|
|
@@ -6503,7 +6723,11 @@ const ReportBuilder = ({
|
|
|
6503
6723
|
dimensionOrder: [...prev.dimensionOrder, {
|
|
6504
6724
|
type: "dimension",
|
|
6505
6725
|
key: dimensionData.fullPath
|
|
6506
|
-
}]
|
|
6726
|
+
}],
|
|
6727
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6728
|
+
type: "dimension",
|
|
6729
|
+
key: dimensionData.fullPath
|
|
6730
|
+
})
|
|
6507
6731
|
};
|
|
6508
6732
|
console.log("Dimension saved:", dimensionData);
|
|
6509
6733
|
console.log("Complete report:", newReport);
|
|
@@ -6514,7 +6738,8 @@ const ReportBuilder = ({
|
|
|
6514
6738
|
setReport(prev => ({
|
|
6515
6739
|
...prev,
|
|
6516
6740
|
dimensions: prev.dimensions.filter(d => d.fullPath !== fullPath),
|
|
6517
|
-
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath))
|
|
6741
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath)),
|
|
6742
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath))
|
|
6518
6743
|
}));
|
|
6519
6744
|
};
|
|
6520
6745
|
const handleUpdateDimensionSortOrder = (fullPath, sortOrder) => {
|
|
@@ -6543,7 +6768,11 @@ const ReportBuilder = ({
|
|
|
6543
6768
|
dimensionOrder: [...prev.dimensionOrder, {
|
|
6544
6769
|
type: "computed",
|
|
6545
6770
|
key: computedDimensionData.name
|
|
6546
|
-
}]
|
|
6771
|
+
}],
|
|
6772
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6773
|
+
type: "computed",
|
|
6774
|
+
key: computedDimensionData.name
|
|
6775
|
+
})
|
|
6547
6776
|
}));
|
|
6548
6777
|
};
|
|
6549
6778
|
const handleUpdateComputedDimension = (name, computedDimensionData) => {
|
|
@@ -6553,6 +6782,10 @@ const ReportBuilder = ({
|
|
|
6553
6782
|
dimensionOrder: prev.dimensionOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6554
6783
|
...entry,
|
|
6555
6784
|
key: computedDimensionData.name
|
|
6785
|
+
} : entry),
|
|
6786
|
+
columnOrder: prev.columnOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6787
|
+
...entry,
|
|
6788
|
+
key: computedDimensionData.name
|
|
6556
6789
|
} : entry)
|
|
6557
6790
|
}));
|
|
6558
6791
|
};
|
|
@@ -6560,14 +6793,19 @@ const ReportBuilder = ({
|
|
|
6560
6793
|
setReport(prev => ({
|
|
6561
6794
|
...prev,
|
|
6562
6795
|
computedDimensions: prev.computedDimensions.filter(cd => cd.name !== name),
|
|
6563
|
-
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
|
|
6796
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name)),
|
|
6797
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
|
|
6564
6798
|
}));
|
|
6565
6799
|
};
|
|
6566
6800
|
const handleSaveMetric = metricData => {
|
|
6567
6801
|
setReport(prev => {
|
|
6568
6802
|
const newReport = {
|
|
6569
6803
|
...prev,
|
|
6570
|
-
metrics: [...prev.metrics, metricData]
|
|
6804
|
+
metrics: [...prev.metrics, metricData],
|
|
6805
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6806
|
+
type: "metric",
|
|
6807
|
+
key: metricData.fullPath
|
|
6808
|
+
})
|
|
6571
6809
|
};
|
|
6572
6810
|
console.log("Metric saved:", metricData);
|
|
6573
6811
|
console.log("Complete report:", newReport);
|
|
@@ -6575,15 +6813,29 @@ const ReportBuilder = ({
|
|
|
6575
6813
|
});
|
|
6576
6814
|
};
|
|
6577
6815
|
const handleRemoveMetric = index => {
|
|
6816
|
+
setReport(prev => {
|
|
6817
|
+
const fullPath = prev.metrics[index]?.fullPath;
|
|
6818
|
+
return {
|
|
6819
|
+
...prev,
|
|
6820
|
+
metrics: prev.metrics.filter((_, i) => i !== index),
|
|
6821
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "metric" && entry.key === fullPath))
|
|
6822
|
+
};
|
|
6823
|
+
});
|
|
6824
|
+
};
|
|
6825
|
+
const handleReorderMetrics = newOrder => {
|
|
6578
6826
|
setReport(prev => ({
|
|
6579
6827
|
...prev,
|
|
6580
|
-
metrics:
|
|
6828
|
+
metrics: newOrder
|
|
6581
6829
|
}));
|
|
6582
6830
|
};
|
|
6583
|
-
|
|
6831
|
+
|
|
6832
|
+
// Sets the unified column order shown on the Column Order tab. An empty
|
|
6833
|
+
// array (e.g. via "Reset to Default") clears the customization, and the
|
|
6834
|
+
// results grid falls back to dimensionOrder followed by metrics order.
|
|
6835
|
+
const handleReorderColumnItems = newColumnOrder => {
|
|
6584
6836
|
setReport(prev => ({
|
|
6585
6837
|
...prev,
|
|
6586
|
-
|
|
6838
|
+
columnOrder: newColumnOrder
|
|
6587
6839
|
}));
|
|
6588
6840
|
};
|
|
6589
6841
|
const handleSaveFilter = (fullPath, filterData) => {
|
|
@@ -6708,6 +6960,17 @@ const ReportBuilder = ({
|
|
|
6708
6960
|
}));
|
|
6709
6961
|
}
|
|
6710
6962
|
|
|
6963
|
+
// Persist the optional unified column order (Column Order tab) so the
|
|
6964
|
+
// results grid can reproduce it on reload. Absent when no custom order
|
|
6965
|
+
// has been defined, so old/untouched reports keep their current
|
|
6966
|
+
// dimensions-then-metrics column order unchanged.
|
|
6967
|
+
if (report.columnOrder.length > 0) {
|
|
6968
|
+
queryObj.ordered_columns = report.columnOrder.map(entry => ({
|
|
6969
|
+
type: entry.type,
|
|
6970
|
+
ref: entry.key
|
|
6971
|
+
}));
|
|
6972
|
+
}
|
|
6973
|
+
|
|
6711
6974
|
// Only add titles if there are any overrides
|
|
6712
6975
|
if (Object.keys(titles).length > 0) {
|
|
6713
6976
|
queryObj.titles = titles;
|
|
@@ -6767,8 +7030,8 @@ const ReportBuilder = ({
|
|
|
6767
7030
|
const handleRunReport = async () => {
|
|
6768
7031
|
setCurrentPage(0);
|
|
6769
7032
|
await runReportWithPagination(0, pageSize);
|
|
6770
|
-
// Switch to Results tab after running report (
|
|
6771
|
-
setActiveTab(
|
|
7033
|
+
// Switch to Results tab after running report (index 4: Dimensions, Metrics, Filters, Column Order, Results)
|
|
7034
|
+
setActiveTab(4);
|
|
6772
7035
|
};
|
|
6773
7036
|
const handleDownloadReport = async () => {
|
|
6774
7037
|
try {
|
|
@@ -6875,6 +7138,11 @@ const ReportBuilder = ({
|
|
|
6875
7138
|
kind: "computed",
|
|
6876
7139
|
data: computedDimensionByName[entry.key]
|
|
6877
7140
|
}).filter(item => item.data);
|
|
7141
|
+
|
|
7142
|
+
// Final column list for the results grid: the explicit Column Order tab
|
|
7143
|
+
// arrangement when defined, otherwise dimensions/computed (in their own
|
|
7144
|
+
// order) followed by metrics (in their own order) — today's behavior.
|
|
7145
|
+
const columnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
|
|
6878
7146
|
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
6879
7147
|
sx: {
|
|
6880
7148
|
p: 3,
|
|
@@ -7129,9 +7397,36 @@ const ReportBuilder = ({
|
|
|
7129
7397
|
}
|
|
7130
7398
|
}
|
|
7131
7399
|
}), /*#__PURE__*/React__default.createElement(Tab, {
|
|
7132
|
-
label:
|
|
7400
|
+
label: /*#__PURE__*/React__default.createElement(Badge, {
|
|
7401
|
+
variant: "dot",
|
|
7402
|
+
invisible: report.columnOrder.length === 0,
|
|
7403
|
+
sx: {
|
|
7404
|
+
"& .MuiBadge-dot": {
|
|
7405
|
+
backgroundColor: "rgb(70, 134, 128)"
|
|
7406
|
+
}
|
|
7407
|
+
}
|
|
7408
|
+
}, /*#__PURE__*/React__default.createElement("span", {
|
|
7409
|
+
style: {
|
|
7410
|
+
marginRight: report.columnOrder.length > 0 ? "8px" : "0"
|
|
7411
|
+
}
|
|
7412
|
+
}, "Column Order")),
|
|
7133
7413
|
id: "report-tab-3",
|
|
7134
7414
|
"aria-controls": "report-tabpanel-3",
|
|
7415
|
+
sx: {
|
|
7416
|
+
height: "41px",
|
|
7417
|
+
fontFamily: "system-ui",
|
|
7418
|
+
borderRadius: "0.5rem",
|
|
7419
|
+
boxShadow: "none",
|
|
7420
|
+
textTransform: "none",
|
|
7421
|
+
color: "rgb(37, 37, 37)",
|
|
7422
|
+
"&.Mui-selected": {
|
|
7423
|
+
color: "rgb(70, 134, 128)"
|
|
7424
|
+
}
|
|
7425
|
+
}
|
|
7426
|
+
}), /*#__PURE__*/React__default.createElement(Tab, {
|
|
7427
|
+
label: reportData ? "Results" : "Results (Run report first)",
|
|
7428
|
+
id: "report-tab-4",
|
|
7429
|
+
"aria-controls": "report-tabpanel-4",
|
|
7135
7430
|
disabled: !reportData,
|
|
7136
7431
|
sx: {
|
|
7137
7432
|
height: "41px",
|
|
@@ -7198,10 +7493,18 @@ const ReportBuilder = ({
|
|
|
7198
7493
|
})), /*#__PURE__*/React__default.createElement(TabPanel, {
|
|
7199
7494
|
value: activeTab,
|
|
7200
7495
|
index: 3
|
|
7201
|
-
},
|
|
7202
|
-
reportData: reportData,
|
|
7496
|
+
}, /*#__PURE__*/React__default.createElement(ColumnOrder, {
|
|
7203
7497
|
orderedDimensionItems: orderedDimensionItems,
|
|
7204
7498
|
metrics: report.metrics,
|
|
7499
|
+
columnOrder: report.columnOrder,
|
|
7500
|
+
onReorderColumnItems: handleReorderColumnItems,
|
|
7501
|
+
titleOverrides: titleOverrides
|
|
7502
|
+
})), /*#__PURE__*/React__default.createElement(TabPanel, {
|
|
7503
|
+
value: activeTab,
|
|
7504
|
+
index: 4
|
|
7505
|
+
}, reportData && /*#__PURE__*/React__default.createElement(ReportDataGrid, {
|
|
7506
|
+
reportData: reportData,
|
|
7507
|
+
columnItems: columnItems,
|
|
7205
7508
|
loading: loading,
|
|
7206
7509
|
onPageChange: handlePageChange,
|
|
7207
7510
|
totalRows: totalRows,
|
|
@@ -7289,6 +7592,12 @@ const CurrencySelector = () => {
|
|
|
7289
7592
|
parameters,
|
|
7290
7593
|
setParameters
|
|
7291
7594
|
} = useReportingContext();
|
|
7595
|
+
|
|
7596
|
+
// Hidden by default; only shown when a `cur` query parameter is present in the top-level URL.
|
|
7597
|
+
const showCurrencySelector = new URLSearchParams(window.location.search).has("cur");
|
|
7598
|
+
if (!showCurrencySelector) {
|
|
7599
|
+
return null;
|
|
7600
|
+
}
|
|
7292
7601
|
return /*#__PURE__*/React__default.createElement(SingleSelect, {
|
|
7293
7602
|
items: CURRENCY_OPTIONS,
|
|
7294
7603
|
value: parameters.base_currency,
|