@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
|
@@ -67,6 +67,7 @@ var LocalizationProvider = require('@mui/x-date-pickers/LocalizationProvider');
|
|
|
67
67
|
var DatePicker = require('@mui/x-date-pickers/DatePicker');
|
|
68
68
|
var AdapterDayjs = require('@mui/x-date-pickers/AdapterDayjs');
|
|
69
69
|
var dayjs = require('dayjs');
|
|
70
|
+
var CalculateIcon = require('@mui/icons-material/Calculate');
|
|
70
71
|
var styles = require('@mui/material/styles');
|
|
71
72
|
|
|
72
73
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -144,6 +145,7 @@ var BoltIcon__default = /*#__PURE__*/_interopDefault(BoltIcon);
|
|
|
144
145
|
var FilterAltIcon__default = /*#__PURE__*/_interopDefault(FilterAltIcon);
|
|
145
146
|
var InfoOutlinedIcon__default = /*#__PURE__*/_interopDefault(InfoOutlinedIcon);
|
|
146
147
|
var dayjs__default = /*#__PURE__*/_interopDefault(dayjs);
|
|
148
|
+
var CalculateIcon__default = /*#__PURE__*/_interopDefault(CalculateIcon);
|
|
147
149
|
|
|
148
150
|
function _extends() {
|
|
149
151
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -2607,6 +2609,140 @@ const ProviderSelection = ({
|
|
|
2607
2609
|
}, renderSelectionChain()));
|
|
2608
2610
|
};
|
|
2609
2611
|
|
|
2612
|
+
// Shared drag-and-drop primitives for the index-identified, arrow-reorderable
|
|
2613
|
+
// lists used across the report builder (Dimensions, Metrics, Column Order).
|
|
2614
|
+
// Previously duplicated per-tab; consolidated here so all three behave and
|
|
2615
|
+
// look identical and only need to be fixed in one place.
|
|
2616
|
+
|
|
2617
|
+
const useSortableListSensors = () => core.useSensors(core.useSensor(core.PointerSensor), core.useSensor(core.KeyboardSensor, {
|
|
2618
|
+
coordinateGetter: sortable.sortableKeyboardCoordinates
|
|
2619
|
+
}));
|
|
2620
|
+
|
|
2621
|
+
// Builds drag/move handlers for a list where dnd-kit item ids are the array
|
|
2622
|
+
// index. `onReorder` receives the whole reordered list.
|
|
2623
|
+
const makeListReorderHandlers = (list, onReorder) => {
|
|
2624
|
+
const handleDragEnd = ({
|
|
2625
|
+
active,
|
|
2626
|
+
over
|
|
2627
|
+
}) => {
|
|
2628
|
+
if (over && active.id !== over.id) {
|
|
2629
|
+
const oldIndex = list.findIndex((_, i) => i === active.id);
|
|
2630
|
+
const newIndex = list.findIndex((_, i) => i === over.id);
|
|
2631
|
+
onReorder(sortable.arrayMove(list, oldIndex, newIndex));
|
|
2632
|
+
}
|
|
2633
|
+
};
|
|
2634
|
+
const handleMoveUp = index => {
|
|
2635
|
+
if (index > 0) {
|
|
2636
|
+
onReorder(sortable.arrayMove(list, index, index - 1));
|
|
2637
|
+
}
|
|
2638
|
+
};
|
|
2639
|
+
const handleMoveDown = index => {
|
|
2640
|
+
if (index < list.length - 1) {
|
|
2641
|
+
onReorder(sortable.arrayMove(list, index, index + 1));
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
return {
|
|
2645
|
+
handleDragEnd,
|
|
2646
|
+
handleMoveUp,
|
|
2647
|
+
handleMoveDown
|
|
2648
|
+
};
|
|
2649
|
+
};
|
|
2650
|
+
|
|
2651
|
+
// Wraps DndContext + SortableContext + the vertical list container.
|
|
2652
|
+
// `itemIds` must line up with the ids passed to each item's useSortable().
|
|
2653
|
+
const SortableListContext = ({
|
|
2654
|
+
sensors,
|
|
2655
|
+
itemIds,
|
|
2656
|
+
onDragEnd,
|
|
2657
|
+
children
|
|
2658
|
+
}) => /*#__PURE__*/React__namespace.default.createElement(core.DndContext, {
|
|
2659
|
+
sensors: sensors,
|
|
2660
|
+
collisionDetection: core.closestCenter,
|
|
2661
|
+
onDragEnd: onDragEnd
|
|
2662
|
+
}, /*#__PURE__*/React__namespace.default.createElement(sortable.SortableContext, {
|
|
2663
|
+
items: itemIds,
|
|
2664
|
+
strategy: sortable.verticalListSortingStrategy
|
|
2665
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
2666
|
+
sx: {
|
|
2667
|
+
display: "flex",
|
|
2668
|
+
flexDirection: "column",
|
|
2669
|
+
gap: 1
|
|
2670
|
+
}
|
|
2671
|
+
}, children)));
|
|
2672
|
+
|
|
2673
|
+
// Per-row useSortable() wiring shared by every sortable chip/row component.
|
|
2674
|
+
const useSortableRow = id => {
|
|
2675
|
+
const {
|
|
2676
|
+
attributes,
|
|
2677
|
+
listeners,
|
|
2678
|
+
setNodeRef,
|
|
2679
|
+
transform,
|
|
2680
|
+
transition,
|
|
2681
|
+
isDragging
|
|
2682
|
+
} = sortable.useSortable({
|
|
2683
|
+
id
|
|
2684
|
+
});
|
|
2685
|
+
const style = {
|
|
2686
|
+
transform: utilities.CSS.Transform.toString(transform),
|
|
2687
|
+
transition,
|
|
2688
|
+
opacity: isDragging ? 0.5 : 1,
|
|
2689
|
+
display: "flex",
|
|
2690
|
+
alignItems: "center",
|
|
2691
|
+
width: "100%"
|
|
2692
|
+
};
|
|
2693
|
+
return {
|
|
2694
|
+
attributes,
|
|
2695
|
+
listeners,
|
|
2696
|
+
setNodeRef,
|
|
2697
|
+
style
|
|
2698
|
+
};
|
|
2699
|
+
};
|
|
2700
|
+
const DragHandle = ({
|
|
2701
|
+
listeners
|
|
2702
|
+
}) => /*#__PURE__*/React__namespace.default.createElement(material.Box, _extends({}, listeners, {
|
|
2703
|
+
sx: {
|
|
2704
|
+
display: "flex",
|
|
2705
|
+
alignItems: "center",
|
|
2706
|
+
cursor: "grab",
|
|
2707
|
+
"&:active": {
|
|
2708
|
+
cursor: "grabbing"
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}), /*#__PURE__*/React__namespace.default.createElement(DragIndicatorIcon__default.default, {
|
|
2712
|
+
sx: {
|
|
2713
|
+
cursor: "grab",
|
|
2714
|
+
color: "rgba(110, 110, 110, 0.62)"
|
|
2715
|
+
}
|
|
2716
|
+
}));
|
|
2717
|
+
const MoveButtons = ({
|
|
2718
|
+
onMoveUp,
|
|
2719
|
+
onMoveDown,
|
|
2720
|
+
isFirst,
|
|
2721
|
+
isLast
|
|
2722
|
+
}) => /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
2723
|
+
className: "hover-icons",
|
|
2724
|
+
sx: {
|
|
2725
|
+
display: "flex",
|
|
2726
|
+
gap: 0.5,
|
|
2727
|
+
opacity: 0,
|
|
2728
|
+
transition: "opacity 0.2s"
|
|
2729
|
+
}
|
|
2730
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
2731
|
+
size: "small",
|
|
2732
|
+
onClick: onMoveUp,
|
|
2733
|
+
disabled: isFirst,
|
|
2734
|
+
"aria-label": "move up"
|
|
2735
|
+
}, /*#__PURE__*/React__namespace.default.createElement(ArrowUpwardIcon__default.default, {
|
|
2736
|
+
fontSize: "small"
|
|
2737
|
+
})), /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
2738
|
+
size: "small",
|
|
2739
|
+
onClick: onMoveDown,
|
|
2740
|
+
disabled: isLast,
|
|
2741
|
+
"aria-label": "move down"
|
|
2742
|
+
}, /*#__PURE__*/React__namespace.default.createElement(ArrowDownwardIcon__default.default, {
|
|
2743
|
+
fontSize: "small"
|
|
2744
|
+
})));
|
|
2745
|
+
|
|
2610
2746
|
// Replace {{key}} placeholders in a title with values from the report
|
|
2611
2747
|
// parameters (e.g. "Balance ({{base_currency}})" -> "Balance (EUR)").
|
|
2612
2748
|
// Unknown keys are left as-is so missing parameters remain visible.
|
|
@@ -2618,6 +2754,50 @@ const interpolateTitle = (template, params) => {
|
|
|
2618
2754
|
});
|
|
2619
2755
|
};
|
|
2620
2756
|
|
|
2757
|
+
// Resolves the final column display order for the results grid: dimensions,
|
|
2758
|
+
// computed dimensions, and metrics combined into a single list.
|
|
2759
|
+
//
|
|
2760
|
+
// `columnOrder` is the optional, explicitly-arranged order (raw entries
|
|
2761
|
+
// `{ type: 'dimension' | 'computed' | 'metric', key }`) set on the Column
|
|
2762
|
+
// Order tab. When present, its entries come first, in that order. Anything
|
|
2763
|
+
// not covered by it (new fields added since it was last touched, or when
|
|
2764
|
+
// it's empty/undefined because no custom order has ever been defined) is
|
|
2765
|
+
// appended in the pre-existing default order: dimensions/computed (in their
|
|
2766
|
+
// own `dimensionOrder`) first, then metrics (in their own array order) —
|
|
2767
|
+
// this keeps behavior unchanged for reports that never define a column order.
|
|
2768
|
+
const resolveColumnOrder = (columnOrder, orderedDimensionItems, metrics) => {
|
|
2769
|
+
// Normalise every candidate item to a uniform { kind, key, data } shape
|
|
2770
|
+
// up front, so callers (e.g. the Column Order tab) can always read
|
|
2771
|
+
// `.key` regardless of where the item came from.
|
|
2772
|
+
const normalizedDimensionItems = orderedDimensionItems.map(item => ({
|
|
2773
|
+
kind: item.kind,
|
|
2774
|
+
key: item.kind === 'computed' ? item.data.name : item.data.fullPath,
|
|
2775
|
+
data: item.data
|
|
2776
|
+
}));
|
|
2777
|
+
const normalizedMetricItems = metrics.map(m => ({
|
|
2778
|
+
kind: 'metric',
|
|
2779
|
+
key: m.fullPath,
|
|
2780
|
+
data: m
|
|
2781
|
+
}));
|
|
2782
|
+
const byKey = Object.fromEntries([...normalizedDimensionItems, ...normalizedMetricItems].map(item => [`${item.kind}:${item.key}`, item]));
|
|
2783
|
+
const resolved = [];
|
|
2784
|
+
const seen = new Set();
|
|
2785
|
+
(columnOrder || []).forEach(entry => {
|
|
2786
|
+
const seenKey = `${entry.type}:${entry.key}`;
|
|
2787
|
+
if (seen.has(seenKey) || !byKey[seenKey]) return;
|
|
2788
|
+
resolved.push(byKey[seenKey]);
|
|
2789
|
+
seen.add(seenKey);
|
|
2790
|
+
});
|
|
2791
|
+
[...normalizedDimensionItems, ...normalizedMetricItems].forEach(item => {
|
|
2792
|
+
const seenKey = `${item.kind}:${item.key}`;
|
|
2793
|
+
if (!seen.has(seenKey)) {
|
|
2794
|
+
resolved.push(item);
|
|
2795
|
+
seen.add(seenKey);
|
|
2796
|
+
}
|
|
2797
|
+
});
|
|
2798
|
+
return resolved;
|
|
2799
|
+
};
|
|
2800
|
+
|
|
2621
2801
|
// Sortable Chip Component
|
|
2622
2802
|
const SortableChip$1 = ({
|
|
2623
2803
|
id,
|
|
@@ -2646,20 +2826,8 @@ const SortableChip$1 = ({
|
|
|
2646
2826
|
attributes,
|
|
2647
2827
|
listeners,
|
|
2648
2828
|
setNodeRef,
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
isDragging
|
|
2652
|
-
} = sortable.useSortable({
|
|
2653
|
-
id
|
|
2654
|
-
});
|
|
2655
|
-
const style = {
|
|
2656
|
-
transform: utilities.CSS.Transform.toString(transform),
|
|
2657
|
-
transition,
|
|
2658
|
-
opacity: isDragging ? 0.5 : 1,
|
|
2659
|
-
display: "flex",
|
|
2660
|
-
alignItems: "center",
|
|
2661
|
-
width: "100%"
|
|
2662
|
-
};
|
|
2829
|
+
style
|
|
2830
|
+
} = useSortableRow(id);
|
|
2663
2831
|
|
|
2664
2832
|
// Focus input when entering edit mode
|
|
2665
2833
|
React.useEffect(() => {
|
|
@@ -2768,21 +2936,9 @@ const SortableChip$1 = ({
|
|
|
2768
2936
|
opacity: 1 // show icons on hover
|
|
2769
2937
|
}
|
|
2770
2938
|
}
|
|
2771
|
-
}, /*#__PURE__*/React__namespace.default.createElement(
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
alignItems: "center",
|
|
2775
|
-
cursor: "grab",
|
|
2776
|
-
"&:active": {
|
|
2777
|
-
cursor: "grabbing"
|
|
2778
|
-
}
|
|
2779
|
-
}
|
|
2780
|
-
}), /*#__PURE__*/React__namespace.default.createElement(DragIndicatorIcon__default.default, {
|
|
2781
|
-
sx: {
|
|
2782
|
-
cursor: "grab",
|
|
2783
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
2784
|
-
}
|
|
2785
|
-
})), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
2939
|
+
}, /*#__PURE__*/React__namespace.default.createElement(DragHandle, {
|
|
2940
|
+
listeners: listeners
|
|
2941
|
+
}), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
2786
2942
|
sx: {
|
|
2787
2943
|
minWidth: 0
|
|
2788
2944
|
}
|
|
@@ -2845,29 +3001,12 @@ const SortableChip$1 = ({
|
|
|
2845
3001
|
sx: {
|
|
2846
3002
|
flex: 1
|
|
2847
3003
|
}
|
|
2848
|
-
}), /*#__PURE__*/React__namespace.default.createElement(
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
transition: "opacity 0.2s"
|
|
2855
|
-
}
|
|
2856
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
2857
|
-
size: "small",
|
|
2858
|
-
onClick: onMoveUp,
|
|
2859
|
-
disabled: isFirst,
|
|
2860
|
-
"aria-label": "move up"
|
|
2861
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowUpwardIcon__default.default, {
|
|
2862
|
-
fontSize: "small"
|
|
2863
|
-
})), /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
2864
|
-
size: "small",
|
|
2865
|
-
onClick: onMoveDown,
|
|
2866
|
-
disabled: isLast,
|
|
2867
|
-
"aria-label": "move down"
|
|
2868
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowDownwardIcon__default.default, {
|
|
2869
|
-
fontSize: "small"
|
|
2870
|
-
})))) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
3004
|
+
}), /*#__PURE__*/React__namespace.default.createElement(MoveButtons, {
|
|
3005
|
+
onMoveUp: onMoveUp,
|
|
3006
|
+
onMoveDown: onMoveDown,
|
|
3007
|
+
isFirst: isFirst,
|
|
3008
|
+
isLast: isLast
|
|
3009
|
+
})) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
2871
3010
|
inputRef: inputRef,
|
|
2872
3011
|
value: editValue,
|
|
2873
3012
|
onChange: e => setEditValue(e.target.value),
|
|
@@ -2943,24 +3082,12 @@ const SortableComputedChip = ({
|
|
|
2943
3082
|
attributes,
|
|
2944
3083
|
listeners,
|
|
2945
3084
|
setNodeRef,
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
isDragging
|
|
2949
|
-
} = sortable.useSortable({
|
|
2950
|
-
id
|
|
2951
|
-
});
|
|
3085
|
+
style
|
|
3086
|
+
} = useSortableRow(id);
|
|
2952
3087
|
const [isEditing, setIsEditing] = React.useState(false);
|
|
2953
3088
|
const [editName, setEditName] = React.useState("");
|
|
2954
3089
|
const [editValue, setEditValue] = React.useState("");
|
|
2955
3090
|
const containerRef = React.useRef(null);
|
|
2956
|
-
const style = {
|
|
2957
|
-
transform: utilities.CSS.Transform.toString(transform),
|
|
2958
|
-
transition,
|
|
2959
|
-
opacity: isDragging ? 0.5 : 1,
|
|
2960
|
-
display: "flex",
|
|
2961
|
-
alignItems: "center",
|
|
2962
|
-
width: "100%"
|
|
2963
|
-
};
|
|
2964
3091
|
|
|
2965
3092
|
// Handle click outside to cancel
|
|
2966
3093
|
React.useEffect(() => {
|
|
@@ -3023,21 +3150,9 @@ const SortableComputedChip = ({
|
|
|
3023
3150
|
opacity: 1
|
|
3024
3151
|
}
|
|
3025
3152
|
}
|
|
3026
|
-
}, /*#__PURE__*/React__namespace.default.createElement(
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
alignItems: "center",
|
|
3030
|
-
cursor: "grab",
|
|
3031
|
-
"&:active": {
|
|
3032
|
-
cursor: "grabbing"
|
|
3033
|
-
}
|
|
3034
|
-
}
|
|
3035
|
-
}), /*#__PURE__*/React__namespace.default.createElement(DragIndicatorIcon__default.default, {
|
|
3036
|
-
sx: {
|
|
3037
|
-
cursor: "grab",
|
|
3038
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
3039
|
-
}
|
|
3040
|
-
})), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.Chip, {
|
|
3153
|
+
}, /*#__PURE__*/React__namespace.default.createElement(DragHandle, {
|
|
3154
|
+
listeners: listeners
|
|
3155
|
+
}), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.Chip, {
|
|
3041
3156
|
icon: /*#__PURE__*/React__namespace.default.createElement(FunctionsIcon__default.default, {
|
|
3042
3157
|
sx: {
|
|
3043
3158
|
fontSize: "15px !important"
|
|
@@ -3101,29 +3216,12 @@ const SortableComputedChip = ({
|
|
|
3101
3216
|
sx: {
|
|
3102
3217
|
flex: 1
|
|
3103
3218
|
}
|
|
3104
|
-
}), /*#__PURE__*/React__namespace.default.createElement(
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
transition: "opacity 0.2s"
|
|
3111
|
-
}
|
|
3112
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
3113
|
-
size: "small",
|
|
3114
|
-
onClick: onMoveUp,
|
|
3115
|
-
disabled: isFirst,
|
|
3116
|
-
"aria-label": "move up"
|
|
3117
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowUpwardIcon__default.default, {
|
|
3118
|
-
fontSize: "small"
|
|
3119
|
-
})), /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
3120
|
-
size: "small",
|
|
3121
|
-
onClick: onMoveDown,
|
|
3122
|
-
disabled: isLast,
|
|
3123
|
-
"aria-label": "move down"
|
|
3124
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowDownwardIcon__default.default, {
|
|
3125
|
-
fontSize: "small"
|
|
3126
|
-
})))) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
3219
|
+
}), /*#__PURE__*/React__namespace.default.createElement(MoveButtons, {
|
|
3220
|
+
onMoveUp: onMoveUp,
|
|
3221
|
+
onMoveDown: onMoveDown,
|
|
3222
|
+
isFirst: isFirst,
|
|
3223
|
+
isLast: isLast
|
|
3224
|
+
})) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
3127
3225
|
value: editName,
|
|
3128
3226
|
onChange: e => setEditName(e.target.value),
|
|
3129
3227
|
onKeyDown: handleKeyDown,
|
|
@@ -3208,9 +3306,7 @@ const Dimensions = ({
|
|
|
3208
3306
|
const [computedValue, setComputedValue] = React.useState("");
|
|
3209
3307
|
|
|
3210
3308
|
// Setup drag and drop sensors
|
|
3211
|
-
const sensors =
|
|
3212
|
-
coordinateGetter: sortable.sortableKeyboardCoordinates
|
|
3213
|
-
}));
|
|
3309
|
+
const sensors = useSortableListSensors();
|
|
3214
3310
|
|
|
3215
3311
|
// Resolve the unified dimensionOrder into the actual dimension / computed
|
|
3216
3312
|
// dimension objects, so dimensions and computed dimensions can be shown
|
|
@@ -3227,32 +3323,12 @@ const Dimensions = ({
|
|
|
3227
3323
|
data: computedDimensionByName[entry.key]
|
|
3228
3324
|
}).filter(item => item.data);
|
|
3229
3325
|
|
|
3230
|
-
// Handle drag
|
|
3231
|
-
const
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
if (over && active.id !== over.id) {
|
|
3237
|
-
const oldIndex = dimensionOrder.findIndex((_, i) => i === active.id);
|
|
3238
|
-
const newIndex = dimensionOrder.findIndex((_, i) => i === over.id);
|
|
3239
|
-
onReorderDimensionItems(sortable.arrayMove(dimensionOrder, oldIndex, newIndex));
|
|
3240
|
-
}
|
|
3241
|
-
};
|
|
3242
|
-
|
|
3243
|
-
// Handle move up
|
|
3244
|
-
const handleMoveUp = index => {
|
|
3245
|
-
if (index > 0) {
|
|
3246
|
-
onReorderDimensionItems(sortable.arrayMove(dimensionOrder, index, index - 1));
|
|
3247
|
-
}
|
|
3248
|
-
};
|
|
3249
|
-
|
|
3250
|
-
// Handle move down
|
|
3251
|
-
const handleMoveDown = index => {
|
|
3252
|
-
if (index < dimensionOrder.length - 1) {
|
|
3253
|
-
onReorderDimensionItems(sortable.arrayMove(dimensionOrder, index, index + 1));
|
|
3254
|
-
}
|
|
3255
|
-
};
|
|
3326
|
+
// Handle drag/move for the unified list
|
|
3327
|
+
const {
|
|
3328
|
+
handleDragEnd,
|
|
3329
|
+
handleMoveUp,
|
|
3330
|
+
handleMoveDown
|
|
3331
|
+
} = makeListReorderHandlers(dimensionOrder, onReorderDimensionItems);
|
|
3256
3332
|
|
|
3257
3333
|
// Handle sort order change (dimensions only - computed dimensions have no order_by)
|
|
3258
3334
|
const handleSortOrderChange = (fullPath, sortOrder) => {
|
|
@@ -3745,19 +3821,10 @@ const Dimensions = ({
|
|
|
3745
3821
|
color: "#666",
|
|
3746
3822
|
marginBottom: 2
|
|
3747
3823
|
}
|
|
3748
|
-
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__namespace.default.createElement(
|
|
3824
|
+
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__namespace.default.createElement(SortableListContext, {
|
|
3749
3825
|
sensors: sensors,
|
|
3750
|
-
|
|
3826
|
+
itemIds: orderedItems.map((_, index) => index),
|
|
3751
3827
|
onDragEnd: handleDragEnd
|
|
3752
|
-
}, /*#__PURE__*/React__namespace.default.createElement(sortable.SortableContext, {
|
|
3753
|
-
items: orderedItems.map((_, index) => index),
|
|
3754
|
-
strategy: sortable.verticalListSortingStrategy
|
|
3755
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
3756
|
-
sx: {
|
|
3757
|
-
display: "flex",
|
|
3758
|
-
flexDirection: "column",
|
|
3759
|
-
gap: 1
|
|
3760
|
-
}
|
|
3761
3828
|
}, orderedItems.map((item, index) => item.kind === "dimension" ? /*#__PURE__*/React__namespace.default.createElement(SortableChip$1, {
|
|
3762
3829
|
key: `dim-${item.key}`,
|
|
3763
3830
|
id: index,
|
|
@@ -3787,7 +3854,7 @@ const Dimensions = ({
|
|
|
3787
3854
|
onMoveDown: () => handleMoveDown(index),
|
|
3788
3855
|
isFirst: index === 0,
|
|
3789
3856
|
isLast: index === orderedItems.length - 1
|
|
3790
|
-
})))))
|
|
3857
|
+
})))));
|
|
3791
3858
|
};
|
|
3792
3859
|
|
|
3793
3860
|
const MetricSourceIcon = ({
|
|
@@ -3862,20 +3929,8 @@ const SortableChip = ({
|
|
|
3862
3929
|
attributes,
|
|
3863
3930
|
listeners,
|
|
3864
3931
|
setNodeRef,
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
isDragging
|
|
3868
|
-
} = sortable.useSortable({
|
|
3869
|
-
id
|
|
3870
|
-
});
|
|
3871
|
-
const style = {
|
|
3872
|
-
transform: utilities.CSS.Transform.toString(transform),
|
|
3873
|
-
transition,
|
|
3874
|
-
opacity: isDragging ? 0.5 : 1,
|
|
3875
|
-
display: 'flex',
|
|
3876
|
-
alignItems: 'center',
|
|
3877
|
-
width: '100%'
|
|
3878
|
-
};
|
|
3932
|
+
style
|
|
3933
|
+
} = useSortableRow(id);
|
|
3879
3934
|
|
|
3880
3935
|
// Focus input when entering edit mode
|
|
3881
3936
|
React.useEffect(() => {
|
|
@@ -3953,21 +4008,9 @@ const SortableChip = ({
|
|
|
3953
4008
|
opacity: 1 // show icons on hover
|
|
3954
4009
|
}
|
|
3955
4010
|
}
|
|
3956
|
-
}, /*#__PURE__*/React__namespace.default.createElement(
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
alignItems: "center",
|
|
3960
|
-
cursor: "grab",
|
|
3961
|
-
"&:active": {
|
|
3962
|
-
cursor: "grabbing"
|
|
3963
|
-
}
|
|
3964
|
-
}
|
|
3965
|
-
}), /*#__PURE__*/React__namespace.default.createElement(DragIndicatorIcon__default.default, {
|
|
3966
|
-
sx: {
|
|
3967
|
-
cursor: "grab",
|
|
3968
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
3969
|
-
}
|
|
3970
|
-
})), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, source && /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
4011
|
+
}, /*#__PURE__*/React__namespace.default.createElement(DragHandle, {
|
|
4012
|
+
listeners: listeners
|
|
4013
|
+
}), !isEditing ? /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, source && /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
3971
4014
|
sx: {
|
|
3972
4015
|
display: 'flex',
|
|
3973
4016
|
alignItems: 'center',
|
|
@@ -4029,29 +4072,12 @@ const SortableChip = ({
|
|
|
4029
4072
|
sx: {
|
|
4030
4073
|
flex: 1
|
|
4031
4074
|
}
|
|
4032
|
-
}), /*#__PURE__*/React__namespace.default.createElement(
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
transition: "opacity 0.2s"
|
|
4039
|
-
}
|
|
4040
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
4041
|
-
size: "small",
|
|
4042
|
-
onClick: onMoveUp,
|
|
4043
|
-
disabled: isFirst,
|
|
4044
|
-
"aria-label": "move up"
|
|
4045
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowUpwardIcon__default.default, {
|
|
4046
|
-
fontSize: "small"
|
|
4047
|
-
})), /*#__PURE__*/React__namespace.default.createElement(material.IconButton, {
|
|
4048
|
-
size: "small",
|
|
4049
|
-
onClick: onMoveDown,
|
|
4050
|
-
disabled: isLast,
|
|
4051
|
-
"aria-label": "move down"
|
|
4052
|
-
}, /*#__PURE__*/React__namespace.default.createElement(ArrowDownwardIcon__default.default, {
|
|
4053
|
-
fontSize: "small"
|
|
4054
|
-
})))) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
4075
|
+
}), /*#__PURE__*/React__namespace.default.createElement(MoveButtons, {
|
|
4076
|
+
onMoveUp: onMoveUp,
|
|
4077
|
+
onMoveDown: onMoveDown,
|
|
4078
|
+
isFirst: isFirst,
|
|
4079
|
+
isLast: isLast
|
|
4080
|
+
})) : /*#__PURE__*/React__namespace.default.createElement(React__namespace.default.Fragment, null, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
4055
4081
|
inputRef: inputRef,
|
|
4056
4082
|
value: editValue,
|
|
4057
4083
|
onChange: e => setEditValue(e.target.value),
|
|
@@ -4126,39 +4152,14 @@ const Metrics = ({
|
|
|
4126
4152
|
const [selectedMetric, setSelectedMetric] = React.useState('');
|
|
4127
4153
|
|
|
4128
4154
|
// Setup drag and drop sensors
|
|
4129
|
-
const sensors =
|
|
4130
|
-
coordinateGetter: sortable.sortableKeyboardCoordinates
|
|
4131
|
-
}));
|
|
4132
|
-
|
|
4133
|
-
// Handle drag end
|
|
4134
|
-
const handleDragEnd = event => {
|
|
4135
|
-
const {
|
|
4136
|
-
active,
|
|
4137
|
-
over
|
|
4138
|
-
} = event;
|
|
4139
|
-
if (over && active.id !== over.id) {
|
|
4140
|
-
const oldIndex = savedMetrics.findIndex((_, i) => i === active.id);
|
|
4141
|
-
const newIndex = savedMetrics.findIndex((_, i) => i === over.id);
|
|
4142
|
-
const newOrder = sortable.arrayMove(savedMetrics, oldIndex, newIndex);
|
|
4143
|
-
onReorderMetrics(newOrder);
|
|
4144
|
-
}
|
|
4145
|
-
};
|
|
4155
|
+
const sensors = useSortableListSensors();
|
|
4146
4156
|
|
|
4147
|
-
// Handle move
|
|
4148
|
-
const
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
};
|
|
4154
|
-
|
|
4155
|
-
// Handle move down
|
|
4156
|
-
const handleMoveDown = index => {
|
|
4157
|
-
if (index < savedMetrics.length - 1) {
|
|
4158
|
-
const newOrder = sortable.arrayMove(savedMetrics, index, index + 1);
|
|
4159
|
-
onReorderMetrics(newOrder);
|
|
4160
|
-
}
|
|
4161
|
-
};
|
|
4157
|
+
// Handle drag/move
|
|
4158
|
+
const {
|
|
4159
|
+
handleDragEnd,
|
|
4160
|
+
handleMoveUp,
|
|
4161
|
+
handleMoveDown
|
|
4162
|
+
} = makeListReorderHandlers(savedMetrics, onReorderMetrics);
|
|
4162
4163
|
|
|
4163
4164
|
// Get the current provider based on selection chain
|
|
4164
4165
|
const getCurrentProvider = () => {
|
|
@@ -4497,19 +4498,10 @@ const Metrics = ({
|
|
|
4497
4498
|
marginBottom: 1,
|
|
4498
4499
|
color: "rgb(37, 37, 37)"
|
|
4499
4500
|
}
|
|
4500
|
-
}, "Saved Metrics"), /*#__PURE__*/React__namespace.default.createElement(
|
|
4501
|
+
}, "Saved Metrics"), /*#__PURE__*/React__namespace.default.createElement(SortableListContext, {
|
|
4501
4502
|
sensors: sensors,
|
|
4502
|
-
|
|
4503
|
+
itemIds: savedMetrics.map((_, index) => index),
|
|
4503
4504
|
onDragEnd: handleDragEnd
|
|
4504
|
-
}, /*#__PURE__*/React__namespace.default.createElement(sortable.SortableContext, {
|
|
4505
|
-
items: savedMetrics.map((_, index) => index),
|
|
4506
|
-
strategy: sortable.verticalListSortingStrategy
|
|
4507
|
-
}, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
4508
|
-
sx: {
|
|
4509
|
-
display: 'flex',
|
|
4510
|
-
flexDirection: 'column',
|
|
4511
|
-
gap: 1
|
|
4512
|
-
}
|
|
4513
4505
|
}, savedMetrics.map((metric, index) => /*#__PURE__*/React__namespace.default.createElement(SortableChip, {
|
|
4514
4506
|
key: index,
|
|
4515
4507
|
id: index,
|
|
@@ -4526,7 +4518,7 @@ const Metrics = ({
|
|
|
4526
4518
|
onUpdateTitle: onUpdateTitle,
|
|
4527
4519
|
onResetTitle: onResetTitle,
|
|
4528
4520
|
source: metric.metric?.source
|
|
4529
|
-
})))))
|
|
4521
|
+
})))));
|
|
4530
4522
|
};
|
|
4531
4523
|
|
|
4532
4524
|
const Filters = ({
|
|
@@ -5753,6 +5745,205 @@ const Filters = ({
|
|
|
5753
5745
|
}))));
|
|
5754
5746
|
};
|
|
5755
5747
|
|
|
5748
|
+
const KIND_LABEL = {
|
|
5749
|
+
dimension: "Dimension",
|
|
5750
|
+
computed: "Computed",
|
|
5751
|
+
metric: "Metric"
|
|
5752
|
+
};
|
|
5753
|
+
const KindChip = ({
|
|
5754
|
+
kind,
|
|
5755
|
+
source
|
|
5756
|
+
}) => {
|
|
5757
|
+
if (kind === "metric") {
|
|
5758
|
+
return /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5759
|
+
sx: {
|
|
5760
|
+
display: "flex",
|
|
5761
|
+
alignItems: "center",
|
|
5762
|
+
flexShrink: 0
|
|
5763
|
+
}
|
|
5764
|
+
}, /*#__PURE__*/React__namespace.default.createElement(MetricSourceIcon, {
|
|
5765
|
+
source: source
|
|
5766
|
+
}));
|
|
5767
|
+
}
|
|
5768
|
+
return /*#__PURE__*/React__namespace.default.createElement(material.Chip, {
|
|
5769
|
+
icon: kind === "computed" ? /*#__PURE__*/React__namespace.default.createElement(CalculateIcon__default.default, {
|
|
5770
|
+
sx: {
|
|
5771
|
+
fontSize: "15px !important"
|
|
5772
|
+
}
|
|
5773
|
+
}) : undefined,
|
|
5774
|
+
label: KIND_LABEL[kind],
|
|
5775
|
+
size: "small",
|
|
5776
|
+
sx: {
|
|
5777
|
+
fontWeight: 500,
|
|
5778
|
+
backgroundColor: "rgba(70, 134, 128, 0.1)",
|
|
5779
|
+
color: "rgb(70, 134, 128)",
|
|
5780
|
+
flexShrink: 0
|
|
5781
|
+
}
|
|
5782
|
+
});
|
|
5783
|
+
};
|
|
5784
|
+
|
|
5785
|
+
// Read-only sortable row: this tab only reorders columns. Adding, removing,
|
|
5786
|
+
// and renaming stay on the Dimensions/Metrics tabs so there's one place each
|
|
5787
|
+
// field is managed.
|
|
5788
|
+
const SortableColumnRow = ({
|
|
5789
|
+
id,
|
|
5790
|
+
label,
|
|
5791
|
+
fullPath,
|
|
5792
|
+
kind,
|
|
5793
|
+
source,
|
|
5794
|
+
onMoveUp,
|
|
5795
|
+
onMoveDown,
|
|
5796
|
+
isFirst,
|
|
5797
|
+
isLast
|
|
5798
|
+
}) => {
|
|
5799
|
+
const reportingContext = useReportingContextOptional();
|
|
5800
|
+
const parameters = reportingContext?.parameters;
|
|
5801
|
+
const {
|
|
5802
|
+
attributes,
|
|
5803
|
+
listeners,
|
|
5804
|
+
setNodeRef,
|
|
5805
|
+
style
|
|
5806
|
+
} = useSortableRow(id);
|
|
5807
|
+
const displayLabel = interpolateTitle(label, parameters);
|
|
5808
|
+
return /*#__PURE__*/React__namespace.default.createElement("div", _extends({
|
|
5809
|
+
ref: setNodeRef,
|
|
5810
|
+
style: style
|
|
5811
|
+
}, attributes), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5812
|
+
sx: {
|
|
5813
|
+
display: "flex",
|
|
5814
|
+
alignItems: "center",
|
|
5815
|
+
width: "100%",
|
|
5816
|
+
gap: 1,
|
|
5817
|
+
backgroundColor: "white",
|
|
5818
|
+
border: "1px solid #e0e0e0",
|
|
5819
|
+
borderRadius: 2,
|
|
5820
|
+
padding: 1,
|
|
5821
|
+
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
|
5822
|
+
"&:hover .hover-icons": {
|
|
5823
|
+
opacity: 1
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
}, /*#__PURE__*/React__namespace.default.createElement(DragHandle, {
|
|
5827
|
+
listeners: listeners
|
|
5828
|
+
}), /*#__PURE__*/React__namespace.default.createElement(KindChip, {
|
|
5829
|
+
kind: kind,
|
|
5830
|
+
source: source
|
|
5831
|
+
}), /*#__PURE__*/React__namespace.default.createElement(material.Tooltip, {
|
|
5832
|
+
title: fullPath,
|
|
5833
|
+
arrow: true,
|
|
5834
|
+
placement: "top"
|
|
5835
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.Typography, {
|
|
5836
|
+
variant: "h6",
|
|
5837
|
+
sx: {
|
|
5838
|
+
fontWeight: 500,
|
|
5839
|
+
color: "#1a1a1a",
|
|
5840
|
+
fontSize: "14px",
|
|
5841
|
+
whiteSpace: "nowrap",
|
|
5842
|
+
overflow: "hidden",
|
|
5843
|
+
textOverflow: "ellipsis"
|
|
5844
|
+
}
|
|
5845
|
+
}, displayLabel)), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5846
|
+
sx: {
|
|
5847
|
+
flex: 1
|
|
5848
|
+
}
|
|
5849
|
+
}), /*#__PURE__*/React__namespace.default.createElement(MoveButtons, {
|
|
5850
|
+
onMoveUp: onMoveUp,
|
|
5851
|
+
onMoveDown: onMoveDown,
|
|
5852
|
+
isFirst: isFirst,
|
|
5853
|
+
isLast: isLast
|
|
5854
|
+
})));
|
|
5855
|
+
};
|
|
5856
|
+
const ColumnOrder = ({
|
|
5857
|
+
orderedDimensionItems = [],
|
|
5858
|
+
metrics = [],
|
|
5859
|
+
columnOrder = [],
|
|
5860
|
+
onReorderColumnItems,
|
|
5861
|
+
titleOverrides = {
|
|
5862
|
+
dimensions: {},
|
|
5863
|
+
metrics: {}
|
|
5864
|
+
}
|
|
5865
|
+
}) => {
|
|
5866
|
+
const isCustomized = columnOrder.length > 0;
|
|
5867
|
+
const resolvedItems = resolveColumnOrder(columnOrder, orderedDimensionItems, metrics);
|
|
5868
|
+
const sensors = useSortableListSensors();
|
|
5869
|
+
const reorderResolved = newResolvedItems => {
|
|
5870
|
+
onReorderColumnItems(newResolvedItems.map(item => ({
|
|
5871
|
+
type: item.kind,
|
|
5872
|
+
key: item.key
|
|
5873
|
+
})));
|
|
5874
|
+
};
|
|
5875
|
+
const {
|
|
5876
|
+
handleDragEnd,
|
|
5877
|
+
handleMoveUp,
|
|
5878
|
+
handleMoveDown
|
|
5879
|
+
} = makeListReorderHandlers(resolvedItems, reorderResolved);
|
|
5880
|
+
const labelFor = item => {
|
|
5881
|
+
if (item.kind === "computed") return item.data.name;
|
|
5882
|
+
if (item.kind === "metric") {
|
|
5883
|
+
return titleOverrides.metrics[item.key] || item.data.metricTitle || item.key;
|
|
5884
|
+
}
|
|
5885
|
+
return titleOverrides.dimensions[item.key] || item.data.dimensionTitle || item.key;
|
|
5886
|
+
};
|
|
5887
|
+
return /*#__PURE__*/React__namespace.default.createElement("div", null, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5888
|
+
sx: {
|
|
5889
|
+
display: "flex",
|
|
5890
|
+
justifyContent: "space-between",
|
|
5891
|
+
alignItems: "flex-start",
|
|
5892
|
+
mb: 2,
|
|
5893
|
+
gap: 2
|
|
5894
|
+
}
|
|
5895
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.Typography, {
|
|
5896
|
+
sx: {
|
|
5897
|
+
fontSize: "13px",
|
|
5898
|
+
color: "#666"
|
|
5899
|
+
}
|
|
5900
|
+
}, "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__namespace.default.createElement(material.Tooltip, {
|
|
5901
|
+
title: "Discard the custom order and go back to the default (Dimensions, then Metrics)",
|
|
5902
|
+
arrow: true
|
|
5903
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.Button, {
|
|
5904
|
+
variant: "outlined",
|
|
5905
|
+
size: "small",
|
|
5906
|
+
startIcon: /*#__PURE__*/React__namespace.default.createElement(RestartAltIcon__default.default, null),
|
|
5907
|
+
onClick: () => onReorderColumnItems([]),
|
|
5908
|
+
sx: {
|
|
5909
|
+
height: "36px",
|
|
5910
|
+
flexShrink: 0,
|
|
5911
|
+
fontFamily: "system-ui",
|
|
5912
|
+
fontSize: "13px",
|
|
5913
|
+
fontWeight: 500,
|
|
5914
|
+
borderRadius: "8px",
|
|
5915
|
+
boxShadow: "none",
|
|
5916
|
+
textTransform: "none",
|
|
5917
|
+
borderColor: "#e0e0e0",
|
|
5918
|
+
color: "rgb(37, 37, 37)",
|
|
5919
|
+
"&:hover": {
|
|
5920
|
+
backgroundColor: "#f5f5f5",
|
|
5921
|
+
borderColor: "#d0d0d0"
|
|
5922
|
+
}
|
|
5923
|
+
}
|
|
5924
|
+
}, "Reset to Default"))), resolvedItems.length === 0 ? /*#__PURE__*/React__namespace.default.createElement(material.Typography, {
|
|
5925
|
+
sx: {
|
|
5926
|
+
fontSize: "13px",
|
|
5927
|
+
color: "#999"
|
|
5928
|
+
}
|
|
5929
|
+
}, "Add dimensions or metrics in the other tabs first.") : /*#__PURE__*/React__namespace.default.createElement(SortableListContext, {
|
|
5930
|
+
sensors: sensors,
|
|
5931
|
+
itemIds: resolvedItems.map((_, index) => index),
|
|
5932
|
+
onDragEnd: handleDragEnd
|
|
5933
|
+
}, resolvedItems.map((item, index) => /*#__PURE__*/React__namespace.default.createElement(SortableColumnRow, {
|
|
5934
|
+
key: `${item.kind}-${item.key}`,
|
|
5935
|
+
id: index,
|
|
5936
|
+
label: labelFor(item),
|
|
5937
|
+
fullPath: item.kind === "computed" ? item.data.value : item.key,
|
|
5938
|
+
kind: item.kind,
|
|
5939
|
+
source: item.kind === "metric" ? item.data.metric?.source : undefined,
|
|
5940
|
+
onMoveUp: () => handleMoveUp(index),
|
|
5941
|
+
onMoveDown: () => handleMoveDown(index),
|
|
5942
|
+
isFirst: index === 0,
|
|
5943
|
+
isLast: index === resolvedItems.length - 1
|
|
5944
|
+
}))));
|
|
5945
|
+
};
|
|
5946
|
+
|
|
5756
5947
|
// Default numeral.js formats applied when a dimension/metric definition has a
|
|
5757
5948
|
// known type but no explicit `format` set on the provider.
|
|
5758
5949
|
const DEFAULT_FORMATS_BY_TYPE = {
|
|
@@ -5767,10 +5958,107 @@ const formatValue = (value, def) => {
|
|
|
5767
5958
|
return def?.prefix ? `${def.prefix} ${formatted}` : formatted;
|
|
5768
5959
|
};
|
|
5769
5960
|
const isNumericType = type => type === 'integer' || type === 'currency';
|
|
5961
|
+
|
|
5962
|
+
// Builds a single DataGrid column definition for one resolved column item
|
|
5963
|
+
// (a dimension, computed dimension, or metric). Field naming logic:
|
|
5964
|
+
// - Dimensions from base provider: baseAlias.field -> baseAlias_field
|
|
5965
|
+
// - Dimensions from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5966
|
+
// - Computed dimensions have no provider/relations chain, so the API returns
|
|
5967
|
+
// each row keyed by the computed dimension's own `name`.
|
|
5968
|
+
// - Metrics: API returns metric keys in different forms depending on path depth
|
|
5969
|
+
// (see comment inline below), normalised to match the row-key normalisation
|
|
5970
|
+
// done when building `rows`.
|
|
5971
|
+
const buildColumn = (item, titleOverrides, parameters) => {
|
|
5972
|
+
if (item.kind === 'computed') {
|
|
5973
|
+
const cd = item.data;
|
|
5974
|
+
return {
|
|
5975
|
+
field: cd.name,
|
|
5976
|
+
headerName: cd.name,
|
|
5977
|
+
flex: 1,
|
|
5978
|
+
minWidth: 150,
|
|
5979
|
+
type: 'string'
|
|
5980
|
+
};
|
|
5981
|
+
}
|
|
5982
|
+
if (item.kind === 'metric') {
|
|
5983
|
+
const metric = item.data;
|
|
5984
|
+
const metricDef = metric.metric;
|
|
5985
|
+
let fieldName;
|
|
5986
|
+
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
5987
|
+
if (dotCount >= 2) {
|
|
5988
|
+
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
5989
|
+
// Normalise dots to underscores to match the row key normalisation below.
|
|
5990
|
+
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
5991
|
+
fieldName = metric.fullPath.replace(/\./g, '_');
|
|
5992
|
+
} else if (metric.relations && metric.relations.length > 0) {
|
|
5993
|
+
// 2-part path from a nested provider: API drops the base provider alias.
|
|
5994
|
+
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
5995
|
+
const parts = metric.fullPath.split('.');
|
|
5996
|
+
const pathWithoutField = parts[0]; // e.g., "mc_fa"
|
|
5997
|
+
const field = parts[1]; // e.g., "total_amount"
|
|
5998
|
+
|
|
5999
|
+
const pathParts = pathWithoutField.split('_');
|
|
6000
|
+
pathParts.shift(); // remove base provider alias
|
|
6001
|
+
const pathWithoutBase = pathParts.join('_');
|
|
6002
|
+
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
6003
|
+
} else {
|
|
6004
|
+
// 2-part path from the base provider: API returns just the metric name.
|
|
6005
|
+
// Example: "mc.record_count" -> "record_count"
|
|
6006
|
+
fieldName = metric.metricName;
|
|
6007
|
+
}
|
|
6008
|
+
const headerName = interpolateTitle(titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName, parameters);
|
|
6009
|
+
return {
|
|
6010
|
+
field: fieldName,
|
|
6011
|
+
headerName: headerName,
|
|
6012
|
+
flex: 1,
|
|
6013
|
+
minWidth: 150,
|
|
6014
|
+
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
6015
|
+
renderHeader: () => /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
6016
|
+
sx: {
|
|
6017
|
+
display: 'flex',
|
|
6018
|
+
alignItems: 'center',
|
|
6019
|
+
gap: 0.5
|
|
6020
|
+
}
|
|
6021
|
+
}, /*#__PURE__*/React__namespace.default.createElement(MetricSourceIcon, {
|
|
6022
|
+
source: metricDef?.source
|
|
6023
|
+
}), /*#__PURE__*/React__namespace.default.createElement("span", null, headerName)),
|
|
6024
|
+
valueFormatter: value => formatValue(value, metricDef)
|
|
6025
|
+
};
|
|
6026
|
+
}
|
|
6027
|
+
|
|
6028
|
+
// Dimension
|
|
6029
|
+
const dim = item.data;
|
|
6030
|
+
let fieldName;
|
|
6031
|
+
if (dim.relations && dim.relations.length > 0) {
|
|
6032
|
+
// From nested provider: drop the base provider alias
|
|
6033
|
+
// Example: ft_fa_db.currency -> fa_db_currency
|
|
6034
|
+
const parts = dim.fullPath.split('.');
|
|
6035
|
+
const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
|
|
6036
|
+
const field = parts[1]; // e.g., "currency"
|
|
6037
|
+
|
|
6038
|
+
const pathParts = pathWithoutField.split('_');
|
|
6039
|
+
pathParts.shift(); // Remove base provider alias
|
|
6040
|
+
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
6041
|
+
|
|
6042
|
+
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
6043
|
+
} else {
|
|
6044
|
+
// From base provider: keep the full path with underscore
|
|
6045
|
+
// Example: ba.created_at -> ba_created_at
|
|
6046
|
+
fieldName = dim.fullPath.replace('.', '_');
|
|
6047
|
+
}
|
|
6048
|
+
const headerName = interpolateTitle(titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName, parameters);
|
|
6049
|
+
const dimDef = dim.dimension;
|
|
6050
|
+
return {
|
|
6051
|
+
field: fieldName,
|
|
6052
|
+
headerName: headerName,
|
|
6053
|
+
flex: 1,
|
|
6054
|
+
minWidth: 150,
|
|
6055
|
+
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
6056
|
+
valueFormatter: value => formatValue(value, dimDef)
|
|
6057
|
+
};
|
|
6058
|
+
};
|
|
5770
6059
|
const ReportDataGrid = ({
|
|
5771
6060
|
reportData,
|
|
5772
|
-
|
|
5773
|
-
metrics,
|
|
6061
|
+
columnItems = [],
|
|
5774
6062
|
loading,
|
|
5775
6063
|
onPageChange,
|
|
5776
6064
|
totalRows = 0,
|
|
@@ -5786,127 +6074,10 @@ const ReportDataGrid = ({
|
|
|
5786
6074
|
pageSize: 50
|
|
5787
6075
|
});
|
|
5788
6076
|
|
|
5789
|
-
// Generate columns dynamically
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
// Add dimension + computed-dimension columns, in the single unified
|
|
5794
|
-
// order the user arranged them in on the Dimensions tab.
|
|
5795
|
-
// Field naming logic (dimensions):
|
|
5796
|
-
// - If from base provider: baseAlias.field -> baseAlias_field
|
|
5797
|
-
// - If from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5798
|
-
// Computed dimensions have no provider/relations chain, so the API
|
|
5799
|
-
// returns each row keyed by the computed dimension's own `name`.
|
|
5800
|
-
orderedDimensionItems.forEach(item => {
|
|
5801
|
-
if (item.kind === 'computed') {
|
|
5802
|
-
const cd = item.data;
|
|
5803
|
-
cols.push({
|
|
5804
|
-
field: cd.name,
|
|
5805
|
-
headerName: cd.name,
|
|
5806
|
-
flex: 1,
|
|
5807
|
-
minWidth: 150,
|
|
5808
|
-
type: 'string'
|
|
5809
|
-
});
|
|
5810
|
-
return;
|
|
5811
|
-
}
|
|
5812
|
-
const dim = item.data;
|
|
5813
|
-
let fieldName;
|
|
5814
|
-
let headerName;
|
|
5815
|
-
|
|
5816
|
-
// Check if there are relations (nested providers)
|
|
5817
|
-
if (dim.relations && dim.relations.length > 0) {
|
|
5818
|
-
// From nested provider: drop the base provider alias
|
|
5819
|
-
// Example: ft_fa_db.currency -> fa_db_currency
|
|
5820
|
-
const parts = dim.fullPath.split('.');
|
|
5821
|
-
const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
|
|
5822
|
-
const field = parts[1]; // e.g., "currency"
|
|
5823
|
-
|
|
5824
|
-
// Remove the base provider alias (first part before first underscore)
|
|
5825
|
-
const pathParts = pathWithoutField.split('_');
|
|
5826
|
-
pathParts.shift(); // Remove base provider alias
|
|
5827
|
-
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
5828
|
-
|
|
5829
|
-
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5830
|
-
} else {
|
|
5831
|
-
// From base provider: keep the full path with underscore
|
|
5832
|
-
// Example: ba.created_at -> ba_created_at
|
|
5833
|
-
fieldName = dim.fullPath.replace('.', '_');
|
|
5834
|
-
}
|
|
5835
|
-
|
|
5836
|
-
// Check for title override, otherwise use the friendly dimension title from provider
|
|
5837
|
-
headerName = titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName;
|
|
5838
|
-
headerName = interpolateTitle(headerName, parameters);
|
|
5839
|
-
const dimDef = dim.dimension;
|
|
5840
|
-
cols.push({
|
|
5841
|
-
field: fieldName,
|
|
5842
|
-
headerName: headerName,
|
|
5843
|
-
flex: 1,
|
|
5844
|
-
minWidth: 150,
|
|
5845
|
-
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
5846
|
-
valueFormatter: value => formatValue(value, dimDef)
|
|
5847
|
-
});
|
|
5848
|
-
});
|
|
5849
|
-
|
|
5850
|
-
// Add metric columns
|
|
5851
|
-
// The API returns metric keys in two different forms depending on the path depth:
|
|
5852
|
-
//
|
|
5853
|
-
// • 2-part path "mc.record_count" → API key: "record_count"
|
|
5854
|
-
// • 2-part path "mc_fa.total_amount" (nested) → API key: "fa_total_amount"
|
|
5855
|
-
// • 3+-part path "mc.dkpi.activeAgreementsCount"→ API key: "mc.dkpi.activeAgreementsCount" (dots)
|
|
5856
|
-
//
|
|
5857
|
-
// For 3+-part paths the API key contains dots. MUI DataGrid interprets dots
|
|
5858
|
-
// in field names as nested-object accessors, so the rows useMemo normalises
|
|
5859
|
-
// those keys (dots → underscores). The column field must match that normalised key.
|
|
5860
|
-
metrics.forEach(metric => {
|
|
5861
|
-
const metricDef = metric.metric;
|
|
5862
|
-
let fieldName;
|
|
5863
|
-
let headerName;
|
|
5864
|
-
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
5865
|
-
if (dotCount >= 2) {
|
|
5866
|
-
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
5867
|
-
// Normalise dots to underscores to match the row key normalisation below.
|
|
5868
|
-
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
5869
|
-
fieldName = metric.fullPath.replace(/\./g, '_');
|
|
5870
|
-
} else if (metric.relations && metric.relations.length > 0) {
|
|
5871
|
-
// 2-part path from a nested provider: API drops the base provider alias.
|
|
5872
|
-
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
5873
|
-
const parts = metric.fullPath.split('.');
|
|
5874
|
-
const pathWithoutField = parts[0]; // e.g., "mc_fa"
|
|
5875
|
-
const field = parts[1]; // e.g., "total_amount"
|
|
5876
|
-
|
|
5877
|
-
const pathParts = pathWithoutField.split('_');
|
|
5878
|
-
pathParts.shift(); // remove base provider alias
|
|
5879
|
-
const pathWithoutBase = pathParts.join('_');
|
|
5880
|
-
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5881
|
-
} else {
|
|
5882
|
-
// 2-part path from the base provider: API returns just the metric name.
|
|
5883
|
-
// Example: "mc.record_count" -> "record_count"
|
|
5884
|
-
fieldName = metric.metricName;
|
|
5885
|
-
}
|
|
5886
|
-
|
|
5887
|
-
// Check for title override, otherwise use the friendly metric title from provider
|
|
5888
|
-
headerName = titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName;
|
|
5889
|
-
headerName = interpolateTitle(headerName, parameters);
|
|
5890
|
-
cols.push({
|
|
5891
|
-
field: fieldName,
|
|
5892
|
-
headerName: headerName,
|
|
5893
|
-
flex: 1,
|
|
5894
|
-
minWidth: 150,
|
|
5895
|
-
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
5896
|
-
renderHeader: () => /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5897
|
-
sx: {
|
|
5898
|
-
display: 'flex',
|
|
5899
|
-
alignItems: 'center',
|
|
5900
|
-
gap: 0.5
|
|
5901
|
-
}
|
|
5902
|
-
}, /*#__PURE__*/React__namespace.default.createElement(MetricSourceIcon, {
|
|
5903
|
-
source: metricDef?.source
|
|
5904
|
-
}), /*#__PURE__*/React__namespace.default.createElement("span", null, headerName)),
|
|
5905
|
-
valueFormatter: value => formatValue(value, metricDef)
|
|
5906
|
-
});
|
|
5907
|
-
});
|
|
5908
|
-
return cols;
|
|
5909
|
-
}, [orderedDimensionItems, metrics, titleOverrides, parameters]);
|
|
6077
|
+
// Generate columns dynamically, in the final resolved column order
|
|
6078
|
+
// (custom Column Order tab arrangement when defined, otherwise
|
|
6079
|
+
// dimensions/computed then metrics).
|
|
6080
|
+
const columns = React__namespace.default.useMemo(() => columnItems.map(item => buildColumn(item, titleOverrides, parameters)), [columnItems, titleOverrides, parameters]);
|
|
5910
6081
|
|
|
5911
6082
|
// Transform report data to rows with unique IDs.
|
|
5912
6083
|
// Keys are normalised by replacing dots with underscores so that MUI DataGrid
|
|
@@ -6016,7 +6187,12 @@ const ReportBuilder = ({
|
|
|
6016
6187
|
computedDimensions: [],
|
|
6017
6188
|
// Unified display/execution order across dimensions + computedDimensions.
|
|
6018
6189
|
// Entries: { type: 'dimension', key: fullPath } | { type: 'computed', key: name }
|
|
6019
|
-
dimensionOrder: []
|
|
6190
|
+
dimensionOrder: [],
|
|
6191
|
+
// Optional unified display order across dimensions + computedDimensions +
|
|
6192
|
+
// metrics, set on the Column Order tab. Empty means "not defined" — the
|
|
6193
|
+
// results grid falls back to dimensionOrder followed by metrics order.
|
|
6194
|
+
// Entries: { type: 'dimension' | 'computed' | 'metric', key }
|
|
6195
|
+
columnOrder: []
|
|
6020
6196
|
});
|
|
6021
6197
|
const [titleOverrides, setTitleOverrides] = React.useState({
|
|
6022
6198
|
dimensions: {},
|
|
@@ -6247,6 +6423,46 @@ const ReportBuilder = ({
|
|
|
6247
6423
|
});
|
|
6248
6424
|
return dimensionOrder;
|
|
6249
6425
|
};
|
|
6426
|
+
|
|
6427
|
+
// Build the optional unified column order (dimensions + computed
|
|
6428
|
+
// dimensions + metrics) from the persisted `ordered_columns` field.
|
|
6429
|
+
// Unlike buildDimensionOrder, entries that no longer resolve are simply
|
|
6430
|
+
// dropped rather than backfilled — an empty/partial result just means "no
|
|
6431
|
+
// custom order defined (yet)", which resolveColumnOrder treats as
|
|
6432
|
+
// "fall back to the default order".
|
|
6433
|
+
const buildColumnOrder = (rawOrderedColumns, dimensions, computedDimensions, metrics) => {
|
|
6434
|
+
if (!Array.isArray(rawOrderedColumns)) return [];
|
|
6435
|
+
const dimensionPaths = new Set(dimensions.map(d => d.fullPath));
|
|
6436
|
+
const computedNames = new Set(computedDimensions.map(cd => cd.name));
|
|
6437
|
+
const metricPaths = new Set(metrics.map(m => m.fullPath));
|
|
6438
|
+
const columnOrder = [];
|
|
6439
|
+
rawOrderedColumns.forEach(entry => {
|
|
6440
|
+
if (entry.type === "dimension" && dimensionPaths.has(entry.ref)) {
|
|
6441
|
+
columnOrder.push({
|
|
6442
|
+
type: "dimension",
|
|
6443
|
+
key: entry.ref
|
|
6444
|
+
});
|
|
6445
|
+
} else if (entry.type === "computed" && computedNames.has(entry.ref)) {
|
|
6446
|
+
columnOrder.push({
|
|
6447
|
+
type: "computed",
|
|
6448
|
+
key: entry.ref
|
|
6449
|
+
});
|
|
6450
|
+
} else if (entry.type === "metric" && metricPaths.has(entry.ref)) {
|
|
6451
|
+
columnOrder.push({
|
|
6452
|
+
type: "metric",
|
|
6453
|
+
key: entry.ref
|
|
6454
|
+
});
|
|
6455
|
+
}
|
|
6456
|
+
});
|
|
6457
|
+
return columnOrder;
|
|
6458
|
+
};
|
|
6459
|
+
|
|
6460
|
+
// Auto-append a newly added dimension/computed dimension/metric to the end
|
|
6461
|
+
// of columnOrder — but only once a custom order actually exists. While
|
|
6462
|
+
// columnOrder is empty (no custom order defined) it's left untouched, so
|
|
6463
|
+
// the results grid keeps falling back to the default dimensions-then-
|
|
6464
|
+
// metrics order until the user visits the Column Order tab.
|
|
6465
|
+
const appendToColumnOrder = (columnOrder, entry) => columnOrder.length > 0 ? [...columnOrder, entry] : columnOrder;
|
|
6250
6466
|
const loadReportDefinition = async id => {
|
|
6251
6467
|
try {
|
|
6252
6468
|
console.log("Loading report definition:", id);
|
|
@@ -6343,6 +6559,7 @@ const ReportBuilder = ({
|
|
|
6343
6559
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6344
6560
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6345
6561
|
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6562
|
+
const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
|
|
6346
6563
|
|
|
6347
6564
|
// Set the report state
|
|
6348
6565
|
setReport({
|
|
@@ -6350,7 +6567,8 @@ const ReportBuilder = ({
|
|
|
6350
6567
|
metrics: reconstructedMetrics,
|
|
6351
6568
|
filters: loadedFilters,
|
|
6352
6569
|
computedDimensions,
|
|
6353
|
-
dimensionOrder
|
|
6570
|
+
dimensionOrder,
|
|
6571
|
+
columnOrder
|
|
6354
6572
|
});
|
|
6355
6573
|
|
|
6356
6574
|
// Set title overrides
|
|
@@ -6460,6 +6678,7 @@ const ReportBuilder = ({
|
|
|
6460
6678
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6461
6679
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6462
6680
|
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6681
|
+
const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
|
|
6463
6682
|
|
|
6464
6683
|
// Set the report state
|
|
6465
6684
|
setReport({
|
|
@@ -6467,7 +6686,8 @@ const ReportBuilder = ({
|
|
|
6467
6686
|
metrics: reconstructedMetrics,
|
|
6468
6687
|
filters: loadedFilters,
|
|
6469
6688
|
computedDimensions,
|
|
6470
|
-
dimensionOrder
|
|
6689
|
+
dimensionOrder,
|
|
6690
|
+
columnOrder
|
|
6471
6691
|
});
|
|
6472
6692
|
|
|
6473
6693
|
// Set title overrides
|
|
@@ -6501,7 +6721,8 @@ const ReportBuilder = ({
|
|
|
6501
6721
|
metrics: [],
|
|
6502
6722
|
filters: {},
|
|
6503
6723
|
computedDimensions: [],
|
|
6504
|
-
dimensionOrder: []
|
|
6724
|
+
dimensionOrder: [],
|
|
6725
|
+
columnOrder: []
|
|
6505
6726
|
});
|
|
6506
6727
|
// Reset title overrides
|
|
6507
6728
|
setTitleOverrides({
|
|
@@ -6582,7 +6803,11 @@ const ReportBuilder = ({
|
|
|
6582
6803
|
dimensionOrder: [...prev.dimensionOrder, {
|
|
6583
6804
|
type: "dimension",
|
|
6584
6805
|
key: dimensionData.fullPath
|
|
6585
|
-
}]
|
|
6806
|
+
}],
|
|
6807
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6808
|
+
type: "dimension",
|
|
6809
|
+
key: dimensionData.fullPath
|
|
6810
|
+
})
|
|
6586
6811
|
};
|
|
6587
6812
|
console.log("Dimension saved:", dimensionData);
|
|
6588
6813
|
console.log("Complete report:", newReport);
|
|
@@ -6593,7 +6818,8 @@ const ReportBuilder = ({
|
|
|
6593
6818
|
setReport(prev => ({
|
|
6594
6819
|
...prev,
|
|
6595
6820
|
dimensions: prev.dimensions.filter(d => d.fullPath !== fullPath),
|
|
6596
|
-
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath))
|
|
6821
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath)),
|
|
6822
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath))
|
|
6597
6823
|
}));
|
|
6598
6824
|
};
|
|
6599
6825
|
const handleUpdateDimensionSortOrder = (fullPath, sortOrder) => {
|
|
@@ -6622,7 +6848,11 @@ const ReportBuilder = ({
|
|
|
6622
6848
|
dimensionOrder: [...prev.dimensionOrder, {
|
|
6623
6849
|
type: "computed",
|
|
6624
6850
|
key: computedDimensionData.name
|
|
6625
|
-
}]
|
|
6851
|
+
}],
|
|
6852
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6853
|
+
type: "computed",
|
|
6854
|
+
key: computedDimensionData.name
|
|
6855
|
+
})
|
|
6626
6856
|
}));
|
|
6627
6857
|
};
|
|
6628
6858
|
const handleUpdateComputedDimension = (name, computedDimensionData) => {
|
|
@@ -6632,6 +6862,10 @@ const ReportBuilder = ({
|
|
|
6632
6862
|
dimensionOrder: prev.dimensionOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6633
6863
|
...entry,
|
|
6634
6864
|
key: computedDimensionData.name
|
|
6865
|
+
} : entry),
|
|
6866
|
+
columnOrder: prev.columnOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6867
|
+
...entry,
|
|
6868
|
+
key: computedDimensionData.name
|
|
6635
6869
|
} : entry)
|
|
6636
6870
|
}));
|
|
6637
6871
|
};
|
|
@@ -6639,14 +6873,19 @@ const ReportBuilder = ({
|
|
|
6639
6873
|
setReport(prev => ({
|
|
6640
6874
|
...prev,
|
|
6641
6875
|
computedDimensions: prev.computedDimensions.filter(cd => cd.name !== name),
|
|
6642
|
-
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
|
|
6876
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name)),
|
|
6877
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
|
|
6643
6878
|
}));
|
|
6644
6879
|
};
|
|
6645
6880
|
const handleSaveMetric = metricData => {
|
|
6646
6881
|
setReport(prev => {
|
|
6647
6882
|
const newReport = {
|
|
6648
6883
|
...prev,
|
|
6649
|
-
metrics: [...prev.metrics, metricData]
|
|
6884
|
+
metrics: [...prev.metrics, metricData],
|
|
6885
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6886
|
+
type: "metric",
|
|
6887
|
+
key: metricData.fullPath
|
|
6888
|
+
})
|
|
6650
6889
|
};
|
|
6651
6890
|
console.log("Metric saved:", metricData);
|
|
6652
6891
|
console.log("Complete report:", newReport);
|
|
@@ -6654,15 +6893,29 @@ const ReportBuilder = ({
|
|
|
6654
6893
|
});
|
|
6655
6894
|
};
|
|
6656
6895
|
const handleRemoveMetric = index => {
|
|
6896
|
+
setReport(prev => {
|
|
6897
|
+
const fullPath = prev.metrics[index]?.fullPath;
|
|
6898
|
+
return {
|
|
6899
|
+
...prev,
|
|
6900
|
+
metrics: prev.metrics.filter((_, i) => i !== index),
|
|
6901
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "metric" && entry.key === fullPath))
|
|
6902
|
+
};
|
|
6903
|
+
});
|
|
6904
|
+
};
|
|
6905
|
+
const handleReorderMetrics = newOrder => {
|
|
6657
6906
|
setReport(prev => ({
|
|
6658
6907
|
...prev,
|
|
6659
|
-
metrics:
|
|
6908
|
+
metrics: newOrder
|
|
6660
6909
|
}));
|
|
6661
6910
|
};
|
|
6662
|
-
|
|
6911
|
+
|
|
6912
|
+
// Sets the unified column order shown on the Column Order tab. An empty
|
|
6913
|
+
// array (e.g. via "Reset to Default") clears the customization, and the
|
|
6914
|
+
// results grid falls back to dimensionOrder followed by metrics order.
|
|
6915
|
+
const handleReorderColumnItems = newColumnOrder => {
|
|
6663
6916
|
setReport(prev => ({
|
|
6664
6917
|
...prev,
|
|
6665
|
-
|
|
6918
|
+
columnOrder: newColumnOrder
|
|
6666
6919
|
}));
|
|
6667
6920
|
};
|
|
6668
6921
|
const handleSaveFilter = (fullPath, filterData) => {
|
|
@@ -6787,6 +7040,17 @@ const ReportBuilder = ({
|
|
|
6787
7040
|
}));
|
|
6788
7041
|
}
|
|
6789
7042
|
|
|
7043
|
+
// Persist the optional unified column order (Column Order tab) so the
|
|
7044
|
+
// results grid can reproduce it on reload. Absent when no custom order
|
|
7045
|
+
// has been defined, so old/untouched reports keep their current
|
|
7046
|
+
// dimensions-then-metrics column order unchanged.
|
|
7047
|
+
if (report.columnOrder.length > 0) {
|
|
7048
|
+
queryObj.ordered_columns = report.columnOrder.map(entry => ({
|
|
7049
|
+
type: entry.type,
|
|
7050
|
+
ref: entry.key
|
|
7051
|
+
}));
|
|
7052
|
+
}
|
|
7053
|
+
|
|
6790
7054
|
// Only add titles if there are any overrides
|
|
6791
7055
|
if (Object.keys(titles).length > 0) {
|
|
6792
7056
|
queryObj.titles = titles;
|
|
@@ -6846,8 +7110,8 @@ const ReportBuilder = ({
|
|
|
6846
7110
|
const handleRunReport = async () => {
|
|
6847
7111
|
setCurrentPage(0);
|
|
6848
7112
|
await runReportWithPagination(0, pageSize);
|
|
6849
|
-
// Switch to Results tab after running report (
|
|
6850
|
-
setActiveTab(
|
|
7113
|
+
// Switch to Results tab after running report (index 4: Dimensions, Metrics, Filters, Column Order, Results)
|
|
7114
|
+
setActiveTab(4);
|
|
6851
7115
|
};
|
|
6852
7116
|
const handleDownloadReport = async () => {
|
|
6853
7117
|
try {
|
|
@@ -6954,6 +7218,11 @@ const ReportBuilder = ({
|
|
|
6954
7218
|
kind: "computed",
|
|
6955
7219
|
data: computedDimensionByName[entry.key]
|
|
6956
7220
|
}).filter(item => item.data);
|
|
7221
|
+
|
|
7222
|
+
// Final column list for the results grid: the explicit Column Order tab
|
|
7223
|
+
// arrangement when defined, otherwise dimensions/computed (in their own
|
|
7224
|
+
// order) followed by metrics (in their own order) — today's behavior.
|
|
7225
|
+
const columnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
|
|
6957
7226
|
return /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
6958
7227
|
sx: {
|
|
6959
7228
|
p: 3,
|
|
@@ -7208,9 +7477,36 @@ const ReportBuilder = ({
|
|
|
7208
7477
|
}
|
|
7209
7478
|
}
|
|
7210
7479
|
}), /*#__PURE__*/React__namespace.default.createElement(material.Tab, {
|
|
7211
|
-
label:
|
|
7480
|
+
label: /*#__PURE__*/React__namespace.default.createElement(material.Badge, {
|
|
7481
|
+
variant: "dot",
|
|
7482
|
+
invisible: report.columnOrder.length === 0,
|
|
7483
|
+
sx: {
|
|
7484
|
+
"& .MuiBadge-dot": {
|
|
7485
|
+
backgroundColor: "rgb(70, 134, 128)"
|
|
7486
|
+
}
|
|
7487
|
+
}
|
|
7488
|
+
}, /*#__PURE__*/React__namespace.default.createElement("span", {
|
|
7489
|
+
style: {
|
|
7490
|
+
marginRight: report.columnOrder.length > 0 ? "8px" : "0"
|
|
7491
|
+
}
|
|
7492
|
+
}, "Column Order")),
|
|
7212
7493
|
id: "report-tab-3",
|
|
7213
7494
|
"aria-controls": "report-tabpanel-3",
|
|
7495
|
+
sx: {
|
|
7496
|
+
height: "41px",
|
|
7497
|
+
fontFamily: "system-ui",
|
|
7498
|
+
borderRadius: "0.5rem",
|
|
7499
|
+
boxShadow: "none",
|
|
7500
|
+
textTransform: "none",
|
|
7501
|
+
color: "rgb(37, 37, 37)",
|
|
7502
|
+
"&.Mui-selected": {
|
|
7503
|
+
color: "rgb(70, 134, 128)"
|
|
7504
|
+
}
|
|
7505
|
+
}
|
|
7506
|
+
}), /*#__PURE__*/React__namespace.default.createElement(material.Tab, {
|
|
7507
|
+
label: reportData ? "Results" : "Results (Run report first)",
|
|
7508
|
+
id: "report-tab-4",
|
|
7509
|
+
"aria-controls": "report-tabpanel-4",
|
|
7214
7510
|
disabled: !reportData,
|
|
7215
7511
|
sx: {
|
|
7216
7512
|
height: "41px",
|
|
@@ -7277,10 +7573,18 @@ const ReportBuilder = ({
|
|
|
7277
7573
|
})), /*#__PURE__*/React__namespace.default.createElement(TabPanel, {
|
|
7278
7574
|
value: activeTab,
|
|
7279
7575
|
index: 3
|
|
7280
|
-
},
|
|
7281
|
-
reportData: reportData,
|
|
7576
|
+
}, /*#__PURE__*/React__namespace.default.createElement(ColumnOrder, {
|
|
7282
7577
|
orderedDimensionItems: orderedDimensionItems,
|
|
7283
7578
|
metrics: report.metrics,
|
|
7579
|
+
columnOrder: report.columnOrder,
|
|
7580
|
+
onReorderColumnItems: handleReorderColumnItems,
|
|
7581
|
+
titleOverrides: titleOverrides
|
|
7582
|
+
})), /*#__PURE__*/React__namespace.default.createElement(TabPanel, {
|
|
7583
|
+
value: activeTab,
|
|
7584
|
+
index: 4
|
|
7585
|
+
}, reportData && /*#__PURE__*/React__namespace.default.createElement(ReportDataGrid, {
|
|
7586
|
+
reportData: reportData,
|
|
7587
|
+
columnItems: columnItems,
|
|
7284
7588
|
loading: loading,
|
|
7285
7589
|
onPageChange: handlePageChange,
|
|
7286
7590
|
totalRows: totalRows,
|
|
@@ -7368,6 +7672,12 @@ const CurrencySelector = () => {
|
|
|
7368
7672
|
parameters,
|
|
7369
7673
|
setParameters
|
|
7370
7674
|
} = useReportingContext();
|
|
7675
|
+
|
|
7676
|
+
// Hidden by default; only shown when a `cur` query parameter is present in the top-level URL.
|
|
7677
|
+
const showCurrencySelector = new URLSearchParams(window.location.search).has("cur");
|
|
7678
|
+
if (!showCurrencySelector) {
|
|
7679
|
+
return null;
|
|
7680
|
+
}
|
|
7371
7681
|
return /*#__PURE__*/React__namespace.default.createElement(SingleSelect, {
|
|
7372
7682
|
items: CURRENCY_OPTIONS,
|
|
7373
7683
|
value: parameters.base_currency,
|