@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.
@@ -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
- onReorderDimensions,
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
- // Handle drag end
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 = savedDimensions.findIndex((_, i) => i === active.id);
2844
- const newIndex = savedDimensions.findIndex((_, i) => i === over.id);
2845
- const newOrder = arrayMove(savedDimensions, oldIndex, newIndex);
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
- const newOrder = arrayMove(savedDimensions, index, index - 1);
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 < savedDimensions.length - 1) {
2861
- const newOrder = arrayMove(savedDimensions, index, index + 1);
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 = (index, sortOrder) => {
2868
- const newDimensions = [...savedDimensions];
2869
- newDimensions[index] = {
2870
- ...newDimensions[index],
2871
- sortOrder
2872
- };
2873
- onReorderDimensions(newDimensions);
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")), isAdding && /*#__PURE__*/React__default.createElement(Paper$1, {
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"))), savedDimensions.length > 0 && /*#__PURE__*/React__default.createElement(Box$1, {
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: savedDimensions.map((_, index) => index),
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
- }, savedDimensions.map((dim, index) => /*#__PURE__*/React__default.createElement(SortableChip$1, {
3237
- key: index,
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: dim.dimensionTitle,
3240
- fullLabel: `${formatProviderPath(dim)} → ${dim.dimensionTitle} (${dim.fullPath})`,
3241
- onDelete: () => onRemoveDimension(index),
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 === savedDimensions.length - 1,
3246
- sortOrder: dim.sortOrder || null,
3247
- onSortOrderChange: sortOrder => handleSortOrderChange(index, sortOrder),
3248
- fullPath: dim.fullPath,
3249
- defaultTitle: dim.dimensionTitle,
3250
- customTitle: titleOverrides[dim.fullPath],
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
- dimensions,
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
- // Field naming logic:
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.forEach(dim => {
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
- }, [dimensions, metrics, titleOverrides, parameters]);
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 = index => {
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
- dimensions: prev.dimensions.filter((_, i) => i !== index)
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 handleReorderDimensions = newOrder => {
6559
+ const handleRemoveComputedDimension = name => {
5967
6560
  setReport(prev => ({
5968
6561
  ...prev,
5969
- dimensions: newOrder
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
- // Convert internal report structure to API format
6026
- const convertReportToApiFormat = (page = 0, size = 50) => {
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 = report.dimensions.map(dim => {
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: report.dimensions.map(dim => dim.fullPath),
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 metric");
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
- onReorderDimensions: handleReorderDimensions,
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
- dimensions: report.dimensions,
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(ReportDefinitionsManager, null))));
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 = {