@bygd/nc-report-ui 0.1.39 → 0.1.41
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.html +21 -0
- package/dist/app/esm/index.js +93493 -0
- package/dist/default/cjs/index.cjs +7468 -1
- package/dist/default/esm/index.js +757 -129
- package/package.json +1 -1
|
@@ -54,6 +54,7 @@ import CheckIcon from '@mui/icons-material/Check';
|
|
|
54
54
|
import CloseIcon from '@mui/icons-material/Close';
|
|
55
55
|
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
|
56
56
|
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
|
|
57
|
+
import FunctionsIcon from '@mui/icons-material/Functions';
|
|
57
58
|
import StorageIcon from '@mui/icons-material/Storage';
|
|
58
59
|
import TrendingUpIcon from '@mui/icons-material/TrendingUp';
|
|
59
60
|
import BoltIcon from '@mui/icons-material/Bolt';
|
|
@@ -1573,6 +1574,39 @@ const ReportingProvider = ({
|
|
|
1573
1574
|
}, children);
|
|
1574
1575
|
};
|
|
1575
1576
|
|
|
1577
|
+
/**
|
|
1578
|
+
* useReportingContext - Hook to access reporting context
|
|
1579
|
+
*
|
|
1580
|
+
* @returns {Object} Context value with parameters, api, and setter functions
|
|
1581
|
+
* @returns {Object} return.parameters - Current parameters object
|
|
1582
|
+
* @returns {Function} return.setParameters - Function to update parameters
|
|
1583
|
+
* @returns {Object} return.api - Current API configuration
|
|
1584
|
+
* @returns {Function} return.setApi - Function to update API configuration
|
|
1585
|
+
* @returns {Function} return.setReportingContext - Function to update both parameters and api
|
|
1586
|
+
*
|
|
1587
|
+
* @example
|
|
1588
|
+
* const { parameters, setParameters, api, setApi, setReportingContext } = useReportingContext();
|
|
1589
|
+
*
|
|
1590
|
+
* // Update parameters only
|
|
1591
|
+
* setParameters({ base_currency: 'USD', merchants: [...] });
|
|
1592
|
+
*
|
|
1593
|
+
* // Update API only
|
|
1594
|
+
* setApi({ token: 'new-token', base_url: 'https://api.example.com' });
|
|
1595
|
+
*
|
|
1596
|
+
* // Update both at once
|
|
1597
|
+
* setReportingContext({
|
|
1598
|
+
* parameters: { base_currency: 'EUR' },
|
|
1599
|
+
* api: { token: 'token', base_url: 'https://api.example.com' }
|
|
1600
|
+
* });
|
|
1601
|
+
*/
|
|
1602
|
+
const useReportingContext = () => {
|
|
1603
|
+
const context = useContext(ReportingContext);
|
|
1604
|
+
if (context === undefined) {
|
|
1605
|
+
throw new Error('useReportingContext must be used within a ReportingProvider');
|
|
1606
|
+
}
|
|
1607
|
+
return context;
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1576
1610
|
/**
|
|
1577
1611
|
* useReportingContextOptional - Hook to optionally access reporting context
|
|
1578
1612
|
* Returns null if used outside of ReportingProvider (useful for optional fallback)
|
|
@@ -2809,68 +2843,368 @@ const SortableChip$1 = ({
|
|
|
2809
2843
|
}
|
|
2810
2844
|
}))));
|
|
2811
2845
|
};
|
|
2846
|
+
|
|
2847
|
+
// Sortable chip for a Computed Dimension: a { name, value } pair independent
|
|
2848
|
+
// of any provider, so unlike SortableChip there's no title override or sort
|
|
2849
|
+
// toggle. Editing happens inline (right where the item is) rather than via
|
|
2850
|
+
// the top-of-page Add form, so it stays visible regardless of scroll position.
|
|
2851
|
+
const SortableComputedChip = ({
|
|
2852
|
+
id,
|
|
2853
|
+
name,
|
|
2854
|
+
value,
|
|
2855
|
+
onDelete,
|
|
2856
|
+
onMoveUp,
|
|
2857
|
+
onMoveDown,
|
|
2858
|
+
isFirst,
|
|
2859
|
+
isLast,
|
|
2860
|
+
onUpdate,
|
|
2861
|
+
isNameTaken
|
|
2862
|
+
}) => {
|
|
2863
|
+
const {
|
|
2864
|
+
attributes,
|
|
2865
|
+
listeners,
|
|
2866
|
+
setNodeRef,
|
|
2867
|
+
transform,
|
|
2868
|
+
transition,
|
|
2869
|
+
isDragging
|
|
2870
|
+
} = useSortable({
|
|
2871
|
+
id
|
|
2872
|
+
});
|
|
2873
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
2874
|
+
const [editName, setEditName] = useState("");
|
|
2875
|
+
const [editValue, setEditValue] = useState("");
|
|
2876
|
+
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
|
+
|
|
2886
|
+
// Handle click outside to cancel
|
|
2887
|
+
useEffect(() => {
|
|
2888
|
+
const handleClickOutside = event => {
|
|
2889
|
+
if (isEditing && containerRef.current && !containerRef.current.contains(event.target)) {
|
|
2890
|
+
handleCancel();
|
|
2891
|
+
}
|
|
2892
|
+
};
|
|
2893
|
+
if (isEditing) {
|
|
2894
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
2895
|
+
return () => {
|
|
2896
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
}, [isEditing]);
|
|
2900
|
+
const handleEditClick = () => {
|
|
2901
|
+
setEditName(name);
|
|
2902
|
+
setEditValue(value);
|
|
2903
|
+
setIsEditing(true);
|
|
2904
|
+
};
|
|
2905
|
+
const nameError = editName.trim() !== "" && isNameTaken(editName.trim(), name);
|
|
2906
|
+
const handleSave = () => {
|
|
2907
|
+
const trimmedName = editName.trim();
|
|
2908
|
+
const trimmedValue = editValue.trim();
|
|
2909
|
+
if (!trimmedName || !trimmedValue) return;
|
|
2910
|
+
if (isNameTaken(trimmedName, name)) return;
|
|
2911
|
+
onUpdate(name, {
|
|
2912
|
+
name: trimmedName,
|
|
2913
|
+
value: trimmedValue
|
|
2914
|
+
});
|
|
2915
|
+
setIsEditing(false);
|
|
2916
|
+
};
|
|
2917
|
+
const handleCancel = () => {
|
|
2918
|
+
setIsEditing(false);
|
|
2919
|
+
setEditName("");
|
|
2920
|
+
setEditValue("");
|
|
2921
|
+
};
|
|
2922
|
+
const handleKeyDown = e => {
|
|
2923
|
+
if (e.key === "Enter") {
|
|
2924
|
+
handleSave();
|
|
2925
|
+
} else if (e.key === "Escape") {
|
|
2926
|
+
handleCancel();
|
|
2927
|
+
}
|
|
2928
|
+
};
|
|
2929
|
+
return /*#__PURE__*/React__default.createElement("div", _extends({
|
|
2930
|
+
ref: setNodeRef,
|
|
2931
|
+
style: style
|
|
2932
|
+
}, attributes), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2933
|
+
ref: containerRef,
|
|
2934
|
+
sx: {
|
|
2935
|
+
display: "flex",
|
|
2936
|
+
alignItems: "center",
|
|
2937
|
+
width: "100%",
|
|
2938
|
+
gap: 1,
|
|
2939
|
+
backgroundColor: "white",
|
|
2940
|
+
border: "1px solid #e0e0e0",
|
|
2941
|
+
borderRadius: 2,
|
|
2942
|
+
padding: 1,
|
|
2943
|
+
"&:hover .hover-icons": {
|
|
2944
|
+
opacity: 1
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, _extends({}, listeners, {
|
|
2948
|
+
sx: {
|
|
2949
|
+
display: "flex",
|
|
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, {
|
|
2962
|
+
icon: /*#__PURE__*/React__default.createElement(FunctionsIcon, {
|
|
2963
|
+
sx: {
|
|
2964
|
+
fontSize: "15px !important"
|
|
2965
|
+
}
|
|
2966
|
+
}),
|
|
2967
|
+
label: name,
|
|
2968
|
+
size: "small",
|
|
2969
|
+
sx: {
|
|
2970
|
+
fontWeight: 500,
|
|
2971
|
+
backgroundColor: "rgba(70, 134, 128, 0.1)",
|
|
2972
|
+
color: "rgb(70, 134, 128)",
|
|
2973
|
+
flexShrink: 0
|
|
2974
|
+
}
|
|
2975
|
+
}), /*#__PURE__*/React__default.createElement(Tooltip$1, {
|
|
2976
|
+
title: value,
|
|
2977
|
+
arrow: true,
|
|
2978
|
+
placement: "top"
|
|
2979
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
2980
|
+
sx: {
|
|
2981
|
+
fontSize: "13px",
|
|
2982
|
+
color: "#666",
|
|
2983
|
+
fontFamily: "monospace",
|
|
2984
|
+
whiteSpace: "nowrap",
|
|
2985
|
+
overflow: "hidden",
|
|
2986
|
+
textOverflow: "ellipsis",
|
|
2987
|
+
minWidth: 0,
|
|
2988
|
+
flex: 1
|
|
2989
|
+
}
|
|
2990
|
+
}, value)), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2991
|
+
className: "hover-icons",
|
|
2992
|
+
sx: {
|
|
2993
|
+
display: "flex",
|
|
2994
|
+
gap: 0.5,
|
|
2995
|
+
opacity: 0,
|
|
2996
|
+
transition: "opacity 0.2s"
|
|
2997
|
+
}
|
|
2998
|
+
}, /*#__PURE__*/React__default.createElement(Tooltip$1, {
|
|
2999
|
+
title: "Edit",
|
|
3000
|
+
arrow: true,
|
|
3001
|
+
placement: "top"
|
|
3002
|
+
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3003
|
+
size: "small",
|
|
3004
|
+
onClick: handleEditClick,
|
|
3005
|
+
"aria-label": "edit computed dimension"
|
|
3006
|
+
}, /*#__PURE__*/React__default.createElement(EditIcon, {
|
|
3007
|
+
fontSize: "small"
|
|
3008
|
+
}))), /*#__PURE__*/React__default.createElement(Tooltip$1, {
|
|
3009
|
+
title: "Delete",
|
|
3010
|
+
arrow: true,
|
|
3011
|
+
placement: "top"
|
|
3012
|
+
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3013
|
+
size: "small",
|
|
3014
|
+
onClick: onDelete,
|
|
3015
|
+
"aria-label": "delete",
|
|
3016
|
+
sx: {
|
|
3017
|
+
color: "#ef5350"
|
|
3018
|
+
}
|
|
3019
|
+
}, /*#__PURE__*/React__default.createElement(GridDeleteIcon, {
|
|
3020
|
+
fontSize: "small"
|
|
3021
|
+
})))), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3022
|
+
sx: {
|
|
3023
|
+
flex: 1
|
|
3024
|
+
}
|
|
3025
|
+
}), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3026
|
+
className: "hover-icons",
|
|
3027
|
+
sx: {
|
|
3028
|
+
display: "flex",
|
|
3029
|
+
gap: 0.5,
|
|
3030
|
+
opacity: 0,
|
|
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, {
|
|
3048
|
+
value: editName,
|
|
3049
|
+
onChange: e => setEditName(e.target.value),
|
|
3050
|
+
onKeyDown: handleKeyDown,
|
|
3051
|
+
size: "small",
|
|
3052
|
+
label: "Name",
|
|
3053
|
+
error: nameError,
|
|
3054
|
+
helperText: nameError ? "Name already in use" : " ",
|
|
3055
|
+
sx: {
|
|
3056
|
+
width: "200px"
|
|
3057
|
+
}
|
|
3058
|
+
}), /*#__PURE__*/React__default.createElement(TextField, {
|
|
3059
|
+
value: editValue,
|
|
3060
|
+
onChange: e => setEditValue(e.target.value),
|
|
3061
|
+
onKeyDown: handleKeyDown,
|
|
3062
|
+
size: "small",
|
|
3063
|
+
label: "Value",
|
|
3064
|
+
helperText: " ",
|
|
3065
|
+
sx: {
|
|
3066
|
+
flex: 1,
|
|
3067
|
+
minWidth: "200px",
|
|
3068
|
+
"& .MuiInputBase-input": {
|
|
3069
|
+
fontFamily: "monospace"
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
}), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3073
|
+
sx: {
|
|
3074
|
+
display: "flex",
|
|
3075
|
+
gap: 0.5,
|
|
3076
|
+
alignSelf: "flex-start",
|
|
3077
|
+
mt: "4px"
|
|
3078
|
+
}
|
|
3079
|
+
}, /*#__PURE__*/React__default.createElement(Tooltip$1, {
|
|
3080
|
+
title: "Save",
|
|
3081
|
+
arrow: true,
|
|
3082
|
+
placement: "top"
|
|
3083
|
+
}, /*#__PURE__*/React__default.createElement("span", null, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3084
|
+
size: "small",
|
|
3085
|
+
onClick: handleSave,
|
|
3086
|
+
color: "primary",
|
|
3087
|
+
"aria-label": "save computed dimension",
|
|
3088
|
+
disabled: !editName.trim() || !editValue.trim() || nameError
|
|
3089
|
+
}, /*#__PURE__*/React__default.createElement(CheckIcon, {
|
|
3090
|
+
fontSize: "small"
|
|
3091
|
+
})))), /*#__PURE__*/React__default.createElement(Tooltip$1, {
|
|
3092
|
+
title: "Cancel",
|
|
3093
|
+
arrow: true,
|
|
3094
|
+
placement: "top"
|
|
3095
|
+
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3096
|
+
size: "small",
|
|
3097
|
+
onClick: handleCancel,
|
|
3098
|
+
"aria-label": "cancel edit"
|
|
3099
|
+
}, /*#__PURE__*/React__default.createElement(CloseIcon, {
|
|
3100
|
+
fontSize: "small"
|
|
3101
|
+
})))))));
|
|
3102
|
+
};
|
|
2812
3103
|
const Dimensions = ({
|
|
2813
3104
|
providersData,
|
|
2814
3105
|
rootProvider,
|
|
2815
3106
|
savedDimensions = [],
|
|
2816
3107
|
onSaveDimension,
|
|
2817
3108
|
onRemoveDimension,
|
|
2818
|
-
|
|
3109
|
+
onUpdateDimensionSortOrder,
|
|
2819
3110
|
titleOverrides = {},
|
|
2820
3111
|
onUpdateTitle,
|
|
2821
3112
|
onResetTitle,
|
|
2822
3113
|
existingMetrics = [],
|
|
2823
|
-
existingFilters = {}
|
|
3114
|
+
existingFilters = {},
|
|
3115
|
+
savedComputedDimensions = [],
|
|
3116
|
+
onSaveComputedDimension,
|
|
3117
|
+
onUpdateComputedDimension,
|
|
3118
|
+
onRemoveComputedDimension,
|
|
3119
|
+
dimensionOrder = [],
|
|
3120
|
+
onReorderDimensionItems
|
|
2824
3121
|
}) => {
|
|
2825
3122
|
const reportingContext = useReportingContextOptional();
|
|
2826
3123
|
const parameters = reportingContext?.parameters;
|
|
2827
3124
|
const [isAdding, setIsAdding] = useState(false);
|
|
2828
3125
|
const [dimensionSelectionChain, setDimensionSelectionChain] = useState([]);
|
|
2829
3126
|
const [selectedDimension, setSelectedDimension] = useState("");
|
|
3127
|
+
const [isAddingComputed, setIsAddingComputed] = useState(false);
|
|
3128
|
+
const [computedName, setComputedName] = useState("");
|
|
3129
|
+
const [computedValue, setComputedValue] = useState("");
|
|
2830
3130
|
|
|
2831
3131
|
// Setup drag and drop sensors
|
|
2832
3132
|
const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
|
|
2833
3133
|
coordinateGetter: sortableKeyboardCoordinates
|
|
2834
3134
|
}));
|
|
2835
3135
|
|
|
2836
|
-
//
|
|
3136
|
+
// Resolve the unified dimensionOrder into the actual dimension / computed
|
|
3137
|
+
// dimension objects, so dimensions and computed dimensions can be shown
|
|
3138
|
+
// (and dragged) as a single list while staying stored in their own arrays.
|
|
3139
|
+
const dimensionByFullPath = Object.fromEntries(savedDimensions.map(d => [d.fullPath, d]));
|
|
3140
|
+
const computedDimensionByName = Object.fromEntries(savedComputedDimensions.map(cd => [cd.name, cd]));
|
|
3141
|
+
const orderedItems = dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
3142
|
+
kind: "dimension",
|
|
3143
|
+
key: entry.key,
|
|
3144
|
+
data: dimensionByFullPath[entry.key]
|
|
3145
|
+
} : {
|
|
3146
|
+
kind: "computed",
|
|
3147
|
+
key: entry.key,
|
|
3148
|
+
data: computedDimensionByName[entry.key]
|
|
3149
|
+
}).filter(item => item.data);
|
|
3150
|
+
|
|
3151
|
+
// Handle drag end for the unified list
|
|
2837
3152
|
const handleDragEnd = event => {
|
|
2838
3153
|
const {
|
|
2839
3154
|
active,
|
|
2840
3155
|
over
|
|
2841
3156
|
} = event;
|
|
2842
3157
|
if (over && active.id !== over.id) {
|
|
2843
|
-
const oldIndex =
|
|
2844
|
-
const newIndex =
|
|
2845
|
-
|
|
2846
|
-
onReorderDimensions(newOrder);
|
|
3158
|
+
const oldIndex = dimensionOrder.findIndex((_, i) => i === active.id);
|
|
3159
|
+
const newIndex = dimensionOrder.findIndex((_, i) => i === over.id);
|
|
3160
|
+
onReorderDimensionItems(arrayMove(dimensionOrder, oldIndex, newIndex));
|
|
2847
3161
|
}
|
|
2848
3162
|
};
|
|
2849
3163
|
|
|
2850
3164
|
// Handle move up
|
|
2851
3165
|
const handleMoveUp = index => {
|
|
2852
3166
|
if (index > 0) {
|
|
2853
|
-
|
|
2854
|
-
onReorderDimensions(newOrder);
|
|
3167
|
+
onReorderDimensionItems(arrayMove(dimensionOrder, index, index - 1));
|
|
2855
3168
|
}
|
|
2856
3169
|
};
|
|
2857
3170
|
|
|
2858
3171
|
// Handle move down
|
|
2859
3172
|
const handleMoveDown = index => {
|
|
2860
|
-
if (index <
|
|
2861
|
-
|
|
2862
|
-
onReorderDimensions(newOrder);
|
|
3173
|
+
if (index < dimensionOrder.length - 1) {
|
|
3174
|
+
onReorderDimensionItems(arrayMove(dimensionOrder, index, index + 1));
|
|
2863
3175
|
}
|
|
2864
3176
|
};
|
|
2865
3177
|
|
|
2866
|
-
// Handle sort order change
|
|
2867
|
-
const handleSortOrderChange = (
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
3178
|
+
// Handle sort order change (dimensions only - computed dimensions have no order_by)
|
|
3179
|
+
const handleSortOrderChange = (fullPath, sortOrder) => {
|
|
3180
|
+
onUpdateDimensionSortOrder(fullPath, sortOrder);
|
|
3181
|
+
};
|
|
3182
|
+
|
|
3183
|
+
// Computed Dimensions: { name, value } pairs independent of any provider.
|
|
3184
|
+
// Their `value` is a raw SQL literal/expression sent to the API as-is.
|
|
3185
|
+
const isComputedNameTaken = (name, excludeName = null) => savedComputedDimensions.some(cd => cd.name === name) && name !== excludeName;
|
|
3186
|
+
const computedNameError = computedName.trim() !== "" && isComputedNameTaken(computedName.trim());
|
|
3187
|
+
const handleAddComputedClick = () => {
|
|
3188
|
+
setIsAddingComputed(true);
|
|
3189
|
+
setIsAdding(false);
|
|
3190
|
+
setComputedName("");
|
|
3191
|
+
setComputedValue("");
|
|
3192
|
+
};
|
|
3193
|
+
const handleCancelComputed = () => {
|
|
3194
|
+
setIsAddingComputed(false);
|
|
3195
|
+
setComputedName("");
|
|
3196
|
+
setComputedValue("");
|
|
3197
|
+
};
|
|
3198
|
+
const handleSaveComputed = () => {
|
|
3199
|
+
const trimmedName = computedName.trim();
|
|
3200
|
+
const trimmedValue = computedValue.trim();
|
|
3201
|
+
if (!trimmedName || !trimmedValue) return;
|
|
3202
|
+
if (isComputedNameTaken(trimmedName)) return;
|
|
3203
|
+
onSaveComputedDimension({
|
|
3204
|
+
name: trimmedName,
|
|
3205
|
+
value: trimmedValue
|
|
3206
|
+
});
|
|
3207
|
+
handleCancelComputed();
|
|
2874
3208
|
};
|
|
2875
3209
|
|
|
2876
3210
|
// Get the current provider based on selection chain
|
|
@@ -2936,6 +3270,7 @@ const Dimensions = ({
|
|
|
2936
3270
|
};
|
|
2937
3271
|
const handleAddClick = () => {
|
|
2938
3272
|
setIsAdding(true);
|
|
3273
|
+
setIsAddingComputed(false);
|
|
2939
3274
|
setDimensionSelectionChain([]);
|
|
2940
3275
|
setSelectedDimension("");
|
|
2941
3276
|
};
|
|
@@ -3064,9 +3399,10 @@ const Dimensions = ({
|
|
|
3064
3399
|
sx: {
|
|
3065
3400
|
display: "flex",
|
|
3066
3401
|
justifyContent: "flex-start",
|
|
3402
|
+
gap: 2,
|
|
3067
3403
|
mb: 2
|
|
3068
3404
|
}
|
|
3069
|
-
}, !isAdding && /*#__PURE__*/React__default.createElement(Button, {
|
|
3405
|
+
}, !isAdding && !isAddingComputed && /*#__PURE__*/React__default.createElement(Button, {
|
|
3070
3406
|
variant: "contained",
|
|
3071
3407
|
startIcon: /*#__PURE__*/React__default.createElement(GridAddIcon, null),
|
|
3072
3408
|
onClick: handleAddClick,
|
|
@@ -3084,7 +3420,117 @@ const Dimensions = ({
|
|
|
3084
3420
|
boxShadow: "none"
|
|
3085
3421
|
}
|
|
3086
3422
|
}
|
|
3087
|
-
}, "Add Dimension")
|
|
3423
|
+
}, "Add Dimension"), !isAdding && !isAddingComputed && /*#__PURE__*/React__default.createElement(Button, {
|
|
3424
|
+
variant: "outlined",
|
|
3425
|
+
startIcon: /*#__PURE__*/React__default.createElement(FunctionsIcon, null),
|
|
3426
|
+
onClick: handleAddComputedClick,
|
|
3427
|
+
sx: {
|
|
3428
|
+
height: "40px",
|
|
3429
|
+
fontFamily: "system-ui",
|
|
3430
|
+
fontSize: "14px",
|
|
3431
|
+
fontWeight: 500,
|
|
3432
|
+
borderRadius: "8px",
|
|
3433
|
+
textTransform: "none",
|
|
3434
|
+
borderColor: "rgb(70, 134, 128)",
|
|
3435
|
+
color: "rgb(70, 134, 128)",
|
|
3436
|
+
boxShadow: "none",
|
|
3437
|
+
"&:hover": {
|
|
3438
|
+
backgroundColor: "rgba(70, 134, 128, 0.04)",
|
|
3439
|
+
borderColor: "rgb(50, 114, 108)",
|
|
3440
|
+
boxShadow: "none"
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
}, "Add Computed Dimension")), isAddingComputed && /*#__PURE__*/React__default.createElement(Paper$1, {
|
|
3444
|
+
elevation: 0,
|
|
3445
|
+
sx: {
|
|
3446
|
+
p: 3,
|
|
3447
|
+
mb: 3,
|
|
3448
|
+
border: "1px solid #e0e0e0",
|
|
3449
|
+
borderRadius: "12px",
|
|
3450
|
+
backgroundColor: "white",
|
|
3451
|
+
boxShadow: "0px 2px 4px rgba(0,0,0,0.02)",
|
|
3452
|
+
fontFamily: "system-ui"
|
|
3453
|
+
}
|
|
3454
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
3455
|
+
sx: {
|
|
3456
|
+
fontSize: "14px",
|
|
3457
|
+
fontWeight: 600,
|
|
3458
|
+
mb: 0.5,
|
|
3459
|
+
color: "rgb(37, 37, 37)"
|
|
3460
|
+
}
|
|
3461
|
+
}, "Define Computed Dimension"), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3462
|
+
sx: {
|
|
3463
|
+
display: "flex",
|
|
3464
|
+
gap: 2,
|
|
3465
|
+
flexWrap: "wrap",
|
|
3466
|
+
mt: 2
|
|
3467
|
+
}
|
|
3468
|
+
}, /*#__PURE__*/React__default.createElement(TextField, {
|
|
3469
|
+
label: "Name",
|
|
3470
|
+
value: computedName,
|
|
3471
|
+
onChange: e => setComputedName(e.target.value),
|
|
3472
|
+
size: "small",
|
|
3473
|
+
sx: {
|
|
3474
|
+
width: "260px"
|
|
3475
|
+
},
|
|
3476
|
+
error: computedNameError,
|
|
3477
|
+
helperText: computedNameError ? "A computed dimension with this name already exists" : " "
|
|
3478
|
+
}), /*#__PURE__*/React__default.createElement(TextField, {
|
|
3479
|
+
label: "Value",
|
|
3480
|
+
value: computedValue,
|
|
3481
|
+
onChange: e => setComputedValue(e.target.value),
|
|
3482
|
+
size: "small",
|
|
3483
|
+
sx: {
|
|
3484
|
+
width: "400px",
|
|
3485
|
+
"& .MuiInputBase-input": {
|
|
3486
|
+
fontFamily: "monospace"
|
|
3487
|
+
}
|
|
3488
|
+
},
|
|
3489
|
+
helperText: " "
|
|
3490
|
+
})), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3491
|
+
sx: {
|
|
3492
|
+
display: "flex",
|
|
3493
|
+
justifyContent: "flex-end",
|
|
3494
|
+
gap: 2,
|
|
3495
|
+
mt: 1
|
|
3496
|
+
}
|
|
3497
|
+
}, /*#__PURE__*/React__default.createElement(Button, {
|
|
3498
|
+
variant: "outlined",
|
|
3499
|
+
onClick: handleCancelComputed,
|
|
3500
|
+
sx: {
|
|
3501
|
+
height: "40px",
|
|
3502
|
+
fontFamily: "system-ui",
|
|
3503
|
+
fontSize: "14px",
|
|
3504
|
+
fontWeight: 500,
|
|
3505
|
+
borderRadius: "8px",
|
|
3506
|
+
boxShadow: "none",
|
|
3507
|
+
borderColor: "#e0e0e0",
|
|
3508
|
+
color: "rgb(37, 37, 37)",
|
|
3509
|
+
textTransform: "none",
|
|
3510
|
+
"&:hover": {
|
|
3511
|
+
backgroundColor: "#f5f5f5",
|
|
3512
|
+
borderColor: "#d0d0d0"
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
}, "Cancel"), /*#__PURE__*/React__default.createElement(Button, {
|
|
3516
|
+
variant: "contained",
|
|
3517
|
+
onClick: handleSaveComputed,
|
|
3518
|
+
disabled: !computedName.trim() || !computedValue.trim() || computedNameError,
|
|
3519
|
+
sx: {
|
|
3520
|
+
height: "40px",
|
|
3521
|
+
fontFamily: "system-ui",
|
|
3522
|
+
fontSize: "14px",
|
|
3523
|
+
fontWeight: 500,
|
|
3524
|
+
borderRadius: "8px",
|
|
3525
|
+
boxShadow: "none",
|
|
3526
|
+
textTransform: "none",
|
|
3527
|
+
backgroundColor: "rgb(70, 134, 128)",
|
|
3528
|
+
"&:hover": {
|
|
3529
|
+
backgroundColor: "rgb(50, 114, 108)",
|
|
3530
|
+
boxShadow: "none"
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
}, "Save Computed Dimension"))), isAdding && /*#__PURE__*/React__default.createElement(Paper$1, {
|
|
3088
3534
|
elevation: 0,
|
|
3089
3535
|
sx: {
|
|
3090
3536
|
p: 3,
|
|
@@ -3202,7 +3648,7 @@ const Dimensions = ({
|
|
|
3202
3648
|
boxShadow: "none"
|
|
3203
3649
|
}
|
|
3204
3650
|
}
|
|
3205
|
-
}, "Save Dimension"))),
|
|
3651
|
+
}, "Save Dimension"))), orderedItems.length > 0 && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3206
3652
|
sx: {
|
|
3207
3653
|
marginTop: 0
|
|
3208
3654
|
}
|
|
@@ -3225,7 +3671,7 @@ const Dimensions = ({
|
|
|
3225
3671
|
collisionDetection: closestCenter,
|
|
3226
3672
|
onDragEnd: handleDragEnd
|
|
3227
3673
|
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
3228
|
-
items:
|
|
3674
|
+
items: orderedItems.map((_, index) => index),
|
|
3229
3675
|
strategy: verticalListSortingStrategy
|
|
3230
3676
|
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3231
3677
|
sx: {
|
|
@@ -3233,23 +3679,35 @@ const Dimensions = ({
|
|
|
3233
3679
|
flexDirection: "column",
|
|
3234
3680
|
gap: 1
|
|
3235
3681
|
}
|
|
3236
|
-
},
|
|
3237
|
-
key:
|
|
3682
|
+
}, orderedItems.map((item, index) => item.kind === "dimension" ? /*#__PURE__*/React__default.createElement(SortableChip$1, {
|
|
3683
|
+
key: `dim-${item.key}`,
|
|
3238
3684
|
id: index,
|
|
3239
|
-
label:
|
|
3240
|
-
fullLabel: `${formatProviderPath(
|
|
3241
|
-
onDelete: () => onRemoveDimension(
|
|
3685
|
+
label: item.data.dimensionTitle,
|
|
3686
|
+
fullLabel: `${formatProviderPath(item.data)} → ${item.data.dimensionTitle} (${item.data.fullPath})`,
|
|
3687
|
+
onDelete: () => onRemoveDimension(item.key),
|
|
3242
3688
|
onMoveUp: () => handleMoveUp(index),
|
|
3243
3689
|
onMoveDown: () => handleMoveDown(index),
|
|
3244
3690
|
isFirst: index === 0,
|
|
3245
|
-
isLast: index ===
|
|
3246
|
-
sortOrder:
|
|
3247
|
-
onSortOrderChange: sortOrder => handleSortOrderChange(
|
|
3248
|
-
fullPath:
|
|
3249
|
-
defaultTitle:
|
|
3250
|
-
customTitle: titleOverrides[
|
|
3691
|
+
isLast: index === orderedItems.length - 1,
|
|
3692
|
+
sortOrder: item.data.sortOrder || null,
|
|
3693
|
+
onSortOrderChange: sortOrder => handleSortOrderChange(item.key, sortOrder),
|
|
3694
|
+
fullPath: item.data.fullPath,
|
|
3695
|
+
defaultTitle: item.data.dimensionTitle,
|
|
3696
|
+
customTitle: titleOverrides[item.data.fullPath],
|
|
3251
3697
|
onUpdateTitle: onUpdateTitle,
|
|
3252
3698
|
onResetTitle: onResetTitle
|
|
3699
|
+
}) : /*#__PURE__*/React__default.createElement(SortableComputedChip, {
|
|
3700
|
+
key: `comp-${item.key}`,
|
|
3701
|
+
id: index,
|
|
3702
|
+
name: item.data.name,
|
|
3703
|
+
value: item.data.value,
|
|
3704
|
+
onUpdate: onUpdateComputedDimension,
|
|
3705
|
+
isNameTaken: isComputedNameTaken,
|
|
3706
|
+
onDelete: () => onRemoveComputedDimension(item.key),
|
|
3707
|
+
onMoveUp: () => handleMoveUp(index),
|
|
3708
|
+
onMoveDown: () => handleMoveDown(index),
|
|
3709
|
+
isFirst: index === 0,
|
|
3710
|
+
isLast: index === orderedItems.length - 1
|
|
3253
3711
|
})))))));
|
|
3254
3712
|
};
|
|
3255
3713
|
|
|
@@ -5232,7 +5690,7 @@ const formatValue = (value, def) => {
|
|
|
5232
5690
|
const isNumericType = type => type === 'integer' || type === 'currency';
|
|
5233
5691
|
const ReportDataGrid = ({
|
|
5234
5692
|
reportData,
|
|
5235
|
-
|
|
5693
|
+
orderedDimensionItems = [],
|
|
5236
5694
|
metrics,
|
|
5237
5695
|
loading,
|
|
5238
5696
|
onPageChange,
|
|
@@ -5253,11 +5711,26 @@ const ReportDataGrid = ({
|
|
|
5253
5711
|
const columns = React__default.useMemo(() => {
|
|
5254
5712
|
const cols = [];
|
|
5255
5713
|
|
|
5256
|
-
// Add dimension columns
|
|
5257
|
-
//
|
|
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):
|
|
5258
5717
|
// - If from base provider: baseAlias.field -> baseAlias_field
|
|
5259
5718
|
// - If from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5260
|
-
dimensions
|
|
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;
|
|
5261
5734
|
let fieldName;
|
|
5262
5735
|
let headerName;
|
|
5263
5736
|
|
|
@@ -5354,7 +5827,7 @@ const ReportDataGrid = ({
|
|
|
5354
5827
|
});
|
|
5355
5828
|
});
|
|
5356
5829
|
return cols;
|
|
5357
|
-
}, [
|
|
5830
|
+
}, [orderedDimensionItems, metrics, titleOverrides, parameters]);
|
|
5358
5831
|
|
|
5359
5832
|
// Transform report data to rows with unique IDs.
|
|
5360
5833
|
// Keys are normalised by replacing dots with underscores so that MUI DataGrid
|
|
@@ -5460,7 +5933,11 @@ const ReportBuilder = ({
|
|
|
5460
5933
|
const [report, setReport] = useState({
|
|
5461
5934
|
dimensions: [],
|
|
5462
5935
|
metrics: [],
|
|
5463
|
-
filters: {}
|
|
5936
|
+
filters: {},
|
|
5937
|
+
computedDimensions: [],
|
|
5938
|
+
// Unified display/execution order across dimensions + computedDimensions.
|
|
5939
|
+
// Entries: { type: 'dimension', key: fullPath } | { type: 'computed', key: name }
|
|
5940
|
+
dimensionOrder: []
|
|
5464
5941
|
});
|
|
5465
5942
|
const [titleOverrides, setTitleOverrides] = useState({
|
|
5466
5943
|
dimensions: {},
|
|
@@ -5501,11 +5978,21 @@ const ReportBuilder = ({
|
|
|
5501
5978
|
|
|
5502
5979
|
// Auto-run report when autoRun is true and report is loaded
|
|
5503
5980
|
useEffect(() => {
|
|
5504
|
-
if (autoRun && reportLoaded && selectedProvider && report.dimensions.length > 0) {
|
|
5981
|
+
if (autoRun && reportLoaded && selectedProvider && (report.dimensions.length > 0 || report.metrics.length > 0 || report.computedDimensions.length > 0)) {
|
|
5505
5982
|
handleRunReport();
|
|
5506
5983
|
}
|
|
5507
5984
|
}, [autoRun, reportLoaded]);
|
|
5508
5985
|
|
|
5986
|
+
// Re-run the currently displayed report when base_currency changes in context
|
|
5987
|
+
const currentBaseCurrency = reportingContext?.parameters?.base_currency;
|
|
5988
|
+
const prevBaseCurrencyRef = useRef(currentBaseCurrency);
|
|
5989
|
+
useEffect(() => {
|
|
5990
|
+
if (reportData && prevBaseCurrencyRef.current !== undefined && prevBaseCurrencyRef.current !== currentBaseCurrency) {
|
|
5991
|
+
runReportWithPagination(currentPage, pageSize);
|
|
5992
|
+
}
|
|
5993
|
+
prevBaseCurrencyRef.current = currentBaseCurrency;
|
|
5994
|
+
}, [currentBaseCurrency]);
|
|
5995
|
+
|
|
5509
5996
|
// Utility function to reconstruct dimension object from fullPath
|
|
5510
5997
|
const reconstructDimensionFromPath = (fullPath, providersData, rootProvider) => {
|
|
5511
5998
|
try {
|
|
@@ -5635,6 +6122,52 @@ const ReportBuilder = ({
|
|
|
5635
6122
|
return null;
|
|
5636
6123
|
}
|
|
5637
6124
|
};
|
|
6125
|
+
|
|
6126
|
+
// Build the unified display/execution order for dimensions + computed
|
|
6127
|
+
// dimensions. Prefers the persisted `ordered_dimensions` field; falls back
|
|
6128
|
+
// to "dimensions first, then computed dimensions" for report definitions
|
|
6129
|
+
// saved before this field existed.
|
|
6130
|
+
const buildDimensionOrder = (rawOrderedDimensions, dimensions, computedDimensions) => {
|
|
6131
|
+
const dimensionOrder = [];
|
|
6132
|
+
const seenDimensionKeys = new Set();
|
|
6133
|
+
const seenComputedKeys = new Set();
|
|
6134
|
+
if (Array.isArray(rawOrderedDimensions)) {
|
|
6135
|
+
rawOrderedDimensions.forEach(entry => {
|
|
6136
|
+
if (entry.type === "dimension" && dimensions.some(d => d.fullPath === entry.ref)) {
|
|
6137
|
+
dimensionOrder.push({
|
|
6138
|
+
type: "dimension",
|
|
6139
|
+
key: entry.ref
|
|
6140
|
+
});
|
|
6141
|
+
seenDimensionKeys.add(entry.ref);
|
|
6142
|
+
} else if (entry.type === "computed" && computedDimensions.some(cd => cd.name === entry.ref)) {
|
|
6143
|
+
dimensionOrder.push({
|
|
6144
|
+
type: "computed",
|
|
6145
|
+
key: entry.ref
|
|
6146
|
+
});
|
|
6147
|
+
seenComputedKeys.add(entry.ref);
|
|
6148
|
+
}
|
|
6149
|
+
});
|
|
6150
|
+
}
|
|
6151
|
+
|
|
6152
|
+
// Append anything not covered by ordered_dimensions (older reports, or drift)
|
|
6153
|
+
dimensions.forEach(d => {
|
|
6154
|
+
if (!seenDimensionKeys.has(d.fullPath)) {
|
|
6155
|
+
dimensionOrder.push({
|
|
6156
|
+
type: "dimension",
|
|
6157
|
+
key: d.fullPath
|
|
6158
|
+
});
|
|
6159
|
+
}
|
|
6160
|
+
});
|
|
6161
|
+
computedDimensions.forEach(cd => {
|
|
6162
|
+
if (!seenComputedKeys.has(cd.name)) {
|
|
6163
|
+
dimensionOrder.push({
|
|
6164
|
+
type: "computed",
|
|
6165
|
+
key: cd.name
|
|
6166
|
+
});
|
|
6167
|
+
}
|
|
6168
|
+
});
|
|
6169
|
+
return dimensionOrder;
|
|
6170
|
+
};
|
|
5638
6171
|
const loadReportDefinition = async id => {
|
|
5639
6172
|
try {
|
|
5640
6173
|
console.log("Loading report definition:", id);
|
|
@@ -5727,11 +6260,18 @@ const ReportBuilder = ({
|
|
|
5727
6260
|
});
|
|
5728
6261
|
}
|
|
5729
6262
|
|
|
6263
|
+
// Computed dimensions are self-contained { name, value } pairs with no
|
|
6264
|
+
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6265
|
+
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6266
|
+
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6267
|
+
|
|
5730
6268
|
// Set the report state
|
|
5731
6269
|
setReport({
|
|
5732
6270
|
dimensions: reconstructedDimensions,
|
|
5733
6271
|
metrics: reconstructedMetrics,
|
|
5734
|
-
filters: loadedFilters
|
|
6272
|
+
filters: loadedFilters,
|
|
6273
|
+
computedDimensions,
|
|
6274
|
+
dimensionOrder
|
|
5735
6275
|
});
|
|
5736
6276
|
|
|
5737
6277
|
// Set title overrides
|
|
@@ -5837,11 +6377,18 @@ const ReportBuilder = ({
|
|
|
5837
6377
|
});
|
|
5838
6378
|
}
|
|
5839
6379
|
|
|
6380
|
+
// Computed dimensions are self-contained { name, value } pairs with no
|
|
6381
|
+
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6382
|
+
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6383
|
+
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6384
|
+
|
|
5840
6385
|
// Set the report state
|
|
5841
6386
|
setReport({
|
|
5842
6387
|
dimensions: reconstructedDimensions,
|
|
5843
6388
|
metrics: reconstructedMetrics,
|
|
5844
|
-
filters: loadedFilters
|
|
6389
|
+
filters: loadedFilters,
|
|
6390
|
+
computedDimensions,
|
|
6391
|
+
dimensionOrder
|
|
5845
6392
|
});
|
|
5846
6393
|
|
|
5847
6394
|
// Set title overrides
|
|
@@ -5873,7 +6420,9 @@ const ReportBuilder = ({
|
|
|
5873
6420
|
setReport({
|
|
5874
6421
|
dimensions: [],
|
|
5875
6422
|
metrics: [],
|
|
5876
|
-
filters: {}
|
|
6423
|
+
filters: {},
|
|
6424
|
+
computedDimensions: [],
|
|
6425
|
+
dimensionOrder: []
|
|
5877
6426
|
});
|
|
5878
6427
|
// Reset title overrides
|
|
5879
6428
|
setTitleOverrides({
|
|
@@ -5950,23 +6499,68 @@ const ReportBuilder = ({
|
|
|
5950
6499
|
setReport(prev => {
|
|
5951
6500
|
const newReport = {
|
|
5952
6501
|
...prev,
|
|
5953
|
-
dimensions: [...prev.dimensions, dimensionData]
|
|
6502
|
+
dimensions: [...prev.dimensions, dimensionData],
|
|
6503
|
+
dimensionOrder: [...prev.dimensionOrder, {
|
|
6504
|
+
type: "dimension",
|
|
6505
|
+
key: dimensionData.fullPath
|
|
6506
|
+
}]
|
|
5954
6507
|
};
|
|
5955
6508
|
console.log("Dimension saved:", dimensionData);
|
|
5956
6509
|
console.log("Complete report:", newReport);
|
|
5957
6510
|
return newReport;
|
|
5958
6511
|
});
|
|
5959
6512
|
};
|
|
5960
|
-
const handleRemoveDimension =
|
|
6513
|
+
const handleRemoveDimension = fullPath => {
|
|
6514
|
+
setReport(prev => ({
|
|
6515
|
+
...prev,
|
|
6516
|
+
dimensions: prev.dimensions.filter(d => d.fullPath !== fullPath),
|
|
6517
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath))
|
|
6518
|
+
}));
|
|
6519
|
+
};
|
|
6520
|
+
const handleUpdateDimensionSortOrder = (fullPath, sortOrder) => {
|
|
6521
|
+
setReport(prev => ({
|
|
6522
|
+
...prev,
|
|
6523
|
+
dimensions: prev.dimensions.map(d => d.fullPath === fullPath ? {
|
|
6524
|
+
...d,
|
|
6525
|
+
sortOrder
|
|
6526
|
+
} : d)
|
|
6527
|
+
}));
|
|
6528
|
+
};
|
|
6529
|
+
|
|
6530
|
+
// Reorders the single unified dimensions + computed-dimensions list;
|
|
6531
|
+
// `dimensions`/`computedDimensions` arrays themselves are unordered from
|
|
6532
|
+
// this perspective, so only `dimensionOrder` needs to change.
|
|
6533
|
+
const handleReorderDimensionItems = newDimensionOrder => {
|
|
6534
|
+
setReport(prev => ({
|
|
6535
|
+
...prev,
|
|
6536
|
+
dimensionOrder: newDimensionOrder
|
|
6537
|
+
}));
|
|
6538
|
+
};
|
|
6539
|
+
const handleSaveComputedDimension = computedDimensionData => {
|
|
6540
|
+
setReport(prev => ({
|
|
6541
|
+
...prev,
|
|
6542
|
+
computedDimensions: [...prev.computedDimensions, computedDimensionData],
|
|
6543
|
+
dimensionOrder: [...prev.dimensionOrder, {
|
|
6544
|
+
type: "computed",
|
|
6545
|
+
key: computedDimensionData.name
|
|
6546
|
+
}]
|
|
6547
|
+
}));
|
|
6548
|
+
};
|
|
6549
|
+
const handleUpdateComputedDimension = (name, computedDimensionData) => {
|
|
5961
6550
|
setReport(prev => ({
|
|
5962
6551
|
...prev,
|
|
5963
|
-
|
|
6552
|
+
computedDimensions: prev.computedDimensions.map(cd => cd.name === name ? computedDimensionData : cd),
|
|
6553
|
+
dimensionOrder: prev.dimensionOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6554
|
+
...entry,
|
|
6555
|
+
key: computedDimensionData.name
|
|
6556
|
+
} : entry)
|
|
5964
6557
|
}));
|
|
5965
6558
|
};
|
|
5966
|
-
const
|
|
6559
|
+
const handleRemoveComputedDimension = name => {
|
|
5967
6560
|
setReport(prev => ({
|
|
5968
6561
|
...prev,
|
|
5969
|
-
|
|
6562
|
+
computedDimensions: prev.computedDimensions.filter(cd => cd.name !== name),
|
|
6563
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
|
|
5970
6564
|
}));
|
|
5971
6565
|
};
|
|
5972
6566
|
const handleSaveMetric = metricData => {
|
|
@@ -6022,10 +6616,20 @@ const ReportBuilder = ({
|
|
|
6022
6616
|
});
|
|
6023
6617
|
};
|
|
6024
6618
|
|
|
6025
|
-
//
|
|
6026
|
-
|
|
6619
|
+
// Builds the query object shared between running (paginated) and saving
|
|
6620
|
+
// (unpaginated) a report. Dimension and computed-dimension order both
|
|
6621
|
+
// derive from the single unified `report.dimensionOrder` list, so the
|
|
6622
|
+
// saved `dimensions`/`computed_dimensions` arrays, `order_by`, and the
|
|
6623
|
+
// persisted `ordered_dimensions` field all stay consistent with whatever
|
|
6624
|
+
// order the user arranged on screen.
|
|
6625
|
+
const buildQueryObject = () => {
|
|
6626
|
+
const dimensionByPath = Object.fromEntries(report.dimensions.map(d => [d.fullPath, d]));
|
|
6627
|
+
const computedByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
6628
|
+
const orderedDimensionsOnly = report.dimensionOrder.filter(entry => entry.type === "dimension").map(entry => dimensionByPath[entry.key]).filter(Boolean);
|
|
6629
|
+
const orderedComputedOnly = report.dimensionOrder.filter(entry => entry.type === "computed").map(entry => computedByName[entry.key]).filter(Boolean);
|
|
6630
|
+
|
|
6027
6631
|
// Build order_by array - include all dimensions, add desc property only when needed
|
|
6028
|
-
const orderBy =
|
|
6632
|
+
const orderBy = orderedDimensionsOnly.map(dim => {
|
|
6029
6633
|
const orderItem = {
|
|
6030
6634
|
name: dim.fullPath
|
|
6031
6635
|
};
|
|
@@ -6081,13 +6685,29 @@ const ReportBuilder = ({
|
|
|
6081
6685
|
titles.filters = titleOverrides.filters;
|
|
6082
6686
|
}
|
|
6083
6687
|
const queryObj = {
|
|
6084
|
-
dimensions:
|
|
6688
|
+
dimensions: orderedDimensionsOnly.map(dim => dim.fullPath),
|
|
6085
6689
|
metrics: report.metrics.map(metric => metric.fullPath),
|
|
6086
|
-
order_by: orderBy
|
|
6087
|
-
limit: size,
|
|
6088
|
-
offset: page * size
|
|
6690
|
+
order_by: orderBy
|
|
6089
6691
|
};
|
|
6090
6692
|
|
|
6693
|
+
// Only add computed_dimensions if there are any
|
|
6694
|
+
if (orderedComputedOnly.length > 0) {
|
|
6695
|
+
queryObj.computed_dimensions = orderedComputedOnly.map(cd => ({
|
|
6696
|
+
name: cd.name,
|
|
6697
|
+
value: cd.value
|
|
6698
|
+
}));
|
|
6699
|
+
}
|
|
6700
|
+
|
|
6701
|
+
// Persist the unified dimension + computed-dimension order so it can be
|
|
6702
|
+
// reconstructed on reload and so executed reports render columns in the
|
|
6703
|
+
// same order they were arranged in the builder.
|
|
6704
|
+
if (report.dimensionOrder.length > 0) {
|
|
6705
|
+
queryObj.ordered_dimensions = report.dimensionOrder.map(entry => ({
|
|
6706
|
+
type: entry.type,
|
|
6707
|
+
ref: entry.key
|
|
6708
|
+
}));
|
|
6709
|
+
}
|
|
6710
|
+
|
|
6091
6711
|
// Only add titles if there are any overrides
|
|
6092
6712
|
if (Object.keys(titles).length > 0) {
|
|
6093
6713
|
queryObj.titles = titles;
|
|
@@ -6097,6 +6717,14 @@ const ReportBuilder = ({
|
|
|
6097
6717
|
if (filter) {
|
|
6098
6718
|
queryObj.filter = filter;
|
|
6099
6719
|
}
|
|
6720
|
+
return queryObj;
|
|
6721
|
+
};
|
|
6722
|
+
|
|
6723
|
+
// Convert internal report structure to API format
|
|
6724
|
+
const convertReportToApiFormat = (page = 0, size = 50) => {
|
|
6725
|
+
const queryObj = buildQueryObject();
|
|
6726
|
+
queryObj.limit = size;
|
|
6727
|
+
queryObj.offset = page * size;
|
|
6100
6728
|
|
|
6101
6729
|
// Get parameters from context if available, otherwise use default
|
|
6102
6730
|
const parameters = reportingContext?.parameters || {
|
|
@@ -6184,75 +6812,13 @@ const ReportBuilder = ({
|
|
|
6184
6812
|
notify.warning("Please enter a report title");
|
|
6185
6813
|
return;
|
|
6186
6814
|
}
|
|
6187
|
-
if (report.dimensions.length === 0 && report.metrics.length === 0) {
|
|
6188
|
-
notify.warning("Please add at least one dimension or
|
|
6815
|
+
if (report.dimensions.length === 0 && report.metrics.length === 0 && report.computedDimensions.length === 0) {
|
|
6816
|
+
notify.warning("Please add at least one dimension, metric, or computed dimension");
|
|
6189
6817
|
return;
|
|
6190
6818
|
}
|
|
6191
6819
|
try {
|
|
6192
6820
|
setSaving(true);
|
|
6193
|
-
|
|
6194
|
-
// Build order_by array - include all dimensions, add desc property only when needed
|
|
6195
|
-
const orderBy = report.dimensions.map(dim => {
|
|
6196
|
-
const orderItem = {
|
|
6197
|
-
name: dim.fullPath
|
|
6198
|
-
};
|
|
6199
|
-
if (dim.sortOrder === "desc") {
|
|
6200
|
-
orderItem.desc = true;
|
|
6201
|
-
}
|
|
6202
|
-
// For 'asc' or null, just include { name: fullPath } without desc property
|
|
6203
|
-
return orderItem;
|
|
6204
|
-
});
|
|
6205
|
-
|
|
6206
|
-
// Build titles object - only include overridden titles
|
|
6207
|
-
const titles = {};
|
|
6208
|
-
if (Object.keys(titleOverrides.dimensions).length > 0) {
|
|
6209
|
-
titles.dimensions = titleOverrides.dimensions;
|
|
6210
|
-
}
|
|
6211
|
-
if (Object.keys(titleOverrides.metrics).length > 0) {
|
|
6212
|
-
titles.metrics = titleOverrides.metrics;
|
|
6213
|
-
}
|
|
6214
|
-
if (Object.keys(titleOverrides.filters).length > 0) {
|
|
6215
|
-
titles.filters = titleOverrides.filters;
|
|
6216
|
-
}
|
|
6217
|
-
const queryObj = {
|
|
6218
|
-
dimensions: report.dimensions.map(dim => dim.fullPath),
|
|
6219
|
-
metrics: report.metrics.map(metric => metric.fullPath),
|
|
6220
|
-
order_by: orderBy
|
|
6221
|
-
};
|
|
6222
|
-
|
|
6223
|
-
// Only add titles if there are any overrides
|
|
6224
|
-
if (Object.keys(titles).length > 0) {
|
|
6225
|
-
queryObj.titles = titles;
|
|
6226
|
-
}
|
|
6227
|
-
|
|
6228
|
-
// Add filter if there are any - NEW API format with top-level 'and' operator
|
|
6229
|
-
// NEW format: { and: [ { "ft.currency": ["USD", "EUR"] }, { "ft_ba.created_at": {gte: "2025-05-01", lte: "2025-12-31"} } ] }
|
|
6230
|
-
if (Object.keys(report.filters).length > 0) {
|
|
6231
|
-
const conditions = [];
|
|
6232
|
-
Object.entries(report.filters).forEach(([fullPath, filterData]) => {
|
|
6233
|
-
if (filterData.values) {
|
|
6234
|
-
// Check if values is an object (date range) or array (regular filter)
|
|
6235
|
-
if (Array.isArray(filterData.values)) {
|
|
6236
|
-
// Regular filter - only add if array has values
|
|
6237
|
-
if (filterData.values.length > 0) {
|
|
6238
|
-
conditions.push({
|
|
6239
|
-
[fullPath]: filterData.values
|
|
6240
|
-
});
|
|
6241
|
-
}
|
|
6242
|
-
} else if (typeof filterData.values === "object") {
|
|
6243
|
-
// Date range filter - add the object as-is
|
|
6244
|
-
conditions.push({
|
|
6245
|
-
[fullPath]: filterData.values
|
|
6246
|
-
});
|
|
6247
|
-
}
|
|
6248
|
-
}
|
|
6249
|
-
});
|
|
6250
|
-
if (conditions.length > 0) {
|
|
6251
|
-
queryObj.filter = {
|
|
6252
|
-
and: conditions
|
|
6253
|
-
};
|
|
6254
|
-
}
|
|
6255
|
-
}
|
|
6821
|
+
const queryObj = buildQueryObject();
|
|
6256
6822
|
|
|
6257
6823
|
// Build the doc object
|
|
6258
6824
|
const docObj = {
|
|
@@ -6296,7 +6862,19 @@ const ReportBuilder = ({
|
|
|
6296
6862
|
setSaving(false);
|
|
6297
6863
|
}
|
|
6298
6864
|
};
|
|
6299
|
-
const canSaveReport = selectedProvider && reportTitle.trim() && (report.dimensions.length > 0 || report.metrics.length > 0);
|
|
6865
|
+
const canSaveReport = selectedProvider && reportTitle.trim() && (report.dimensions.length > 0 || report.metrics.length > 0 || report.computedDimensions.length > 0);
|
|
6866
|
+
|
|
6867
|
+
// Resolve the unified dimensionOrder into actual dimension/computed-dimension
|
|
6868
|
+
// objects so the results grid can render columns in that same order.
|
|
6869
|
+
const dimensionByFullPath = Object.fromEntries(report.dimensions.map(d => [d.fullPath, d]));
|
|
6870
|
+
const computedDimensionByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
6871
|
+
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
6872
|
+
kind: "dimension",
|
|
6873
|
+
data: dimensionByFullPath[entry.key]
|
|
6874
|
+
} : {
|
|
6875
|
+
kind: "computed",
|
|
6876
|
+
data: computedDimensionByName[entry.key]
|
|
6877
|
+
}).filter(item => item.data);
|
|
6300
6878
|
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
6301
6879
|
sx: {
|
|
6302
6880
|
p: 3,
|
|
@@ -6469,7 +7047,7 @@ const ReportBuilder = ({
|
|
|
6469
7047
|
}
|
|
6470
7048
|
}, /*#__PURE__*/React__default.createElement(Tab, {
|
|
6471
7049
|
label: /*#__PURE__*/React__default.createElement(Badge, {
|
|
6472
|
-
badgeContent: report.dimensions.length,
|
|
7050
|
+
badgeContent: report.dimensions.length + report.computedDimensions.length,
|
|
6473
7051
|
sx: {
|
|
6474
7052
|
"& .MuiBadge-badge": {
|
|
6475
7053
|
backgroundColor: "rgb(70, 134, 128)",
|
|
@@ -6479,7 +7057,7 @@ const ReportBuilder = ({
|
|
|
6479
7057
|
}
|
|
6480
7058
|
}, /*#__PURE__*/React__default.createElement("span", {
|
|
6481
7059
|
style: {
|
|
6482
|
-
marginRight: report.dimensions.length > 0 ? "12px" : "0"
|
|
7060
|
+
marginRight: report.dimensions.length + report.computedDimensions.length > 0 ? "12px" : "0"
|
|
6483
7061
|
}
|
|
6484
7062
|
}, "Dimensions")),
|
|
6485
7063
|
id: "report-tab-0",
|
|
@@ -6575,12 +7153,18 @@ const ReportBuilder = ({
|
|
|
6575
7153
|
savedDimensions: report.dimensions,
|
|
6576
7154
|
onSaveDimension: handleSaveDimension,
|
|
6577
7155
|
onRemoveDimension: handleRemoveDimension,
|
|
6578
|
-
|
|
7156
|
+
onUpdateDimensionSortOrder: handleUpdateDimensionSortOrder,
|
|
6579
7157
|
titleOverrides: titleOverrides.dimensions,
|
|
6580
7158
|
onUpdateTitle: handleUpdateDimensionTitle,
|
|
6581
7159
|
onResetTitle: handleResetDimensionTitle,
|
|
6582
7160
|
existingMetrics: report.metrics,
|
|
6583
|
-
existingFilters: report.filters
|
|
7161
|
+
existingFilters: report.filters,
|
|
7162
|
+
savedComputedDimensions: report.computedDimensions,
|
|
7163
|
+
onSaveComputedDimension: handleSaveComputedDimension,
|
|
7164
|
+
onUpdateComputedDimension: handleUpdateComputedDimension,
|
|
7165
|
+
onRemoveComputedDimension: handleRemoveComputedDimension,
|
|
7166
|
+
dimensionOrder: report.dimensionOrder,
|
|
7167
|
+
onReorderDimensionItems: handleReorderDimensionItems
|
|
6584
7168
|
})), /*#__PURE__*/React__default.createElement(TabPanel, {
|
|
6585
7169
|
value: activeTab,
|
|
6586
7170
|
index: 1
|
|
@@ -6616,7 +7200,7 @@ const ReportBuilder = ({
|
|
|
6616
7200
|
index: 3
|
|
6617
7201
|
}, reportData && /*#__PURE__*/React__default.createElement(ReportDataGrid, {
|
|
6618
7202
|
reportData: reportData,
|
|
6619
|
-
|
|
7203
|
+
orderedDimensionItems: orderedDimensionItems,
|
|
6620
7204
|
metrics: report.metrics,
|
|
6621
7205
|
loading: loading,
|
|
6622
7206
|
onPageChange: handlePageChange,
|
|
@@ -6680,6 +7264,44 @@ const ReportDefinitionsManager = () => {
|
|
|
6680
7264
|
});
|
|
6681
7265
|
};
|
|
6682
7266
|
|
|
7267
|
+
// TODO: replace with currencies fetched from the API once that's available.
|
|
7268
|
+
const CURRENCY_OPTIONS = [{
|
|
7269
|
+
key: "USD",
|
|
7270
|
+
value: "USD"
|
|
7271
|
+
}, {
|
|
7272
|
+
key: "EUR",
|
|
7273
|
+
value: "EUR"
|
|
7274
|
+
}, {
|
|
7275
|
+
key: "SEK",
|
|
7276
|
+
value: "SEK"
|
|
7277
|
+
}, {
|
|
7278
|
+
key: "GBP",
|
|
7279
|
+
value: "GBP"
|
|
7280
|
+
}, {
|
|
7281
|
+
key: "DKK",
|
|
7282
|
+
value: "DKK"
|
|
7283
|
+
}, {
|
|
7284
|
+
key: "NOK",
|
|
7285
|
+
value: "NOK"
|
|
7286
|
+
}];
|
|
7287
|
+
const CurrencySelector = () => {
|
|
7288
|
+
const {
|
|
7289
|
+
parameters,
|
|
7290
|
+
setParameters
|
|
7291
|
+
} = useReportingContext();
|
|
7292
|
+
return /*#__PURE__*/React__default.createElement(SingleSelect, {
|
|
7293
|
+
items: CURRENCY_OPTIONS,
|
|
7294
|
+
value: parameters.base_currency,
|
|
7295
|
+
label: "Currency",
|
|
7296
|
+
onChange: e => setParameters({
|
|
7297
|
+
...parameters,
|
|
7298
|
+
base_currency: e.target.value
|
|
7299
|
+
}),
|
|
7300
|
+
sx: {
|
|
7301
|
+
width: 110
|
|
7302
|
+
}
|
|
7303
|
+
});
|
|
7304
|
+
};
|
|
6683
7305
|
const ReportApp = ({
|
|
6684
7306
|
params,
|
|
6685
7307
|
api
|
|
@@ -6746,7 +7368,13 @@ const ReportApp = ({
|
|
|
6746
7368
|
}
|
|
6747
7369
|
}
|
|
6748
7370
|
})
|
|
6749
|
-
}, /*#__PURE__*/React__default.createElement(
|
|
7371
|
+
}, /*#__PURE__*/React__default.createElement(Box, {
|
|
7372
|
+
sx: {
|
|
7373
|
+
display: "flex",
|
|
7374
|
+
justifyContent: "flex-end",
|
|
7375
|
+
p: 1
|
|
7376
|
+
}
|
|
7377
|
+
}, /*#__PURE__*/React__default.createElement(CurrencySelector, null)), /*#__PURE__*/React__default.createElement(ReportDefinitionsManager, null))));
|
|
6750
7378
|
};
|
|
6751
7379
|
|
|
6752
7380
|
var index = {
|