@bygd/nc-report-ui 0.1.40 → 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.
@@ -1574,6 +1574,39 @@ const ReportingProvider = ({
1574
1574
  }, children);
1575
1575
  };
1576
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
+
1577
1610
  /**
1578
1611
  * useReportingContextOptional - Hook to optionally access reporting context
1579
1612
  * Returns null if used outside of ReportingProvider (useful for optional fallback)
@@ -2817,7 +2850,6 @@ const SortableChip$1 = ({
2817
2850
  // the top-of-page Add form, so it stays visible regardless of scroll position.
2818
2851
  const SortableComputedChip = ({
2819
2852
  id,
2820
- index,
2821
2853
  name,
2822
2854
  value,
2823
2855
  onDelete,
@@ -2870,13 +2902,13 @@ const SortableComputedChip = ({
2870
2902
  setEditValue(value);
2871
2903
  setIsEditing(true);
2872
2904
  };
2873
- const nameError = editName.trim() !== "" && isNameTaken(editName.trim(), index);
2905
+ const nameError = editName.trim() !== "" && isNameTaken(editName.trim(), name);
2874
2906
  const handleSave = () => {
2875
2907
  const trimmedName = editName.trim();
2876
2908
  const trimmedValue = editValue.trim();
2877
2909
  if (!trimmedName || !trimmedValue) return;
2878
- if (isNameTaken(trimmedName, index)) return;
2879
- onUpdate(index, {
2910
+ if (isNameTaken(trimmedName, name)) return;
2911
+ onUpdate(name, {
2880
2912
  name: trimmedName,
2881
2913
  value: trimmedValue
2882
2914
  });
@@ -3074,7 +3106,7 @@ const Dimensions = ({
3074
3106
  savedDimensions = [],
3075
3107
  onSaveDimension,
3076
3108
  onRemoveDimension,
3077
- onReorderDimensions,
3109
+ onUpdateDimensionSortOrder,
3078
3110
  titleOverrides = {},
3079
3111
  onUpdateTitle,
3080
3112
  onResetTitle,
@@ -3084,7 +3116,8 @@ const Dimensions = ({
3084
3116
  onSaveComputedDimension,
3085
3117
  onUpdateComputedDimension,
3086
3118
  onRemoveComputedDimension,
3087
- onReorderComputedDimensions
3119
+ dimensionOrder = [],
3120
+ onReorderDimensionItems
3088
3121
  }) => {
3089
3122
  const reportingContext = useReportingContextOptional();
3090
3123
  const parameters = reportingContext?.parameters;
@@ -3100,49 +3133,56 @@ const Dimensions = ({
3100
3133
  coordinateGetter: sortableKeyboardCoordinates
3101
3134
  }));
3102
3135
 
3103
- // 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
3104
3152
  const handleDragEnd = event => {
3105
3153
  const {
3106
3154
  active,
3107
3155
  over
3108
3156
  } = event;
3109
3157
  if (over && active.id !== over.id) {
3110
- const oldIndex = savedDimensions.findIndex((_, i) => i === active.id);
3111
- const newIndex = savedDimensions.findIndex((_, i) => i === over.id);
3112
- const newOrder = arrayMove(savedDimensions, oldIndex, newIndex);
3113
- 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));
3114
3161
  }
3115
3162
  };
3116
3163
 
3117
3164
  // Handle move up
3118
3165
  const handleMoveUp = index => {
3119
3166
  if (index > 0) {
3120
- const newOrder = arrayMove(savedDimensions, index, index - 1);
3121
- onReorderDimensions(newOrder);
3167
+ onReorderDimensionItems(arrayMove(dimensionOrder, index, index - 1));
3122
3168
  }
3123
3169
  };
3124
3170
 
3125
3171
  // Handle move down
3126
3172
  const handleMoveDown = index => {
3127
- if (index < savedDimensions.length - 1) {
3128
- const newOrder = arrayMove(savedDimensions, index, index + 1);
3129
- onReorderDimensions(newOrder);
3173
+ if (index < dimensionOrder.length - 1) {
3174
+ onReorderDimensionItems(arrayMove(dimensionOrder, index, index + 1));
3130
3175
  }
3131
3176
  };
3132
3177
 
3133
- // Handle sort order change
3134
- const handleSortOrderChange = (index, sortOrder) => {
3135
- const newDimensions = [...savedDimensions];
3136
- newDimensions[index] = {
3137
- ...newDimensions[index],
3138
- sortOrder
3139
- };
3140
- onReorderDimensions(newDimensions);
3178
+ // Handle sort order change (dimensions only - computed dimensions have no order_by)
3179
+ const handleSortOrderChange = (fullPath, sortOrder) => {
3180
+ onUpdateDimensionSortOrder(fullPath, sortOrder);
3141
3181
  };
3142
3182
 
3143
3183
  // Computed Dimensions: { name, value } pairs independent of any provider.
3144
3184
  // Their `value` is a raw SQL literal/expression sent to the API as-is.
3145
- const isComputedNameTaken = (name, excludeIndex = null) => savedComputedDimensions.some((cd, i) => cd.name === name && i !== excludeIndex);
3185
+ const isComputedNameTaken = (name, excludeName = null) => savedComputedDimensions.some(cd => cd.name === name) && name !== excludeName;
3146
3186
  const computedNameError = computedName.trim() !== "" && isComputedNameTaken(computedName.trim());
3147
3187
  const handleAddComputedClick = () => {
3148
3188
  setIsAddingComputed(true);
@@ -3166,27 +3206,6 @@ const Dimensions = ({
3166
3206
  });
3167
3207
  handleCancelComputed();
3168
3208
  };
3169
- const handleComputedDragEnd = event => {
3170
- const {
3171
- active,
3172
- over
3173
- } = event;
3174
- if (over && active.id !== over.id) {
3175
- const oldIndex = savedComputedDimensions.findIndex((_, i) => i === active.id);
3176
- const newIndex = savedComputedDimensions.findIndex((_, i) => i === over.id);
3177
- onReorderComputedDimensions(arrayMove(savedComputedDimensions, oldIndex, newIndex));
3178
- }
3179
- };
3180
- const handleMoveComputedUp = index => {
3181
- if (index > 0) {
3182
- onReorderComputedDimensions(arrayMove(savedComputedDimensions, index, index - 1));
3183
- }
3184
- };
3185
- const handleMoveComputedDown = index => {
3186
- if (index < savedComputedDimensions.length - 1) {
3187
- onReorderComputedDimensions(arrayMove(savedComputedDimensions, index, index + 1));
3188
- }
3189
- };
3190
3209
 
3191
3210
  // Get the current provider based on selection chain
3192
3211
  const getCurrentProvider = () => {
@@ -3629,7 +3648,7 @@ const Dimensions = ({
3629
3648
  boxShadow: "none"
3630
3649
  }
3631
3650
  }
3632
- }, "Save Dimension"))), savedDimensions.length > 0 && /*#__PURE__*/React__default.createElement(Box$1, {
3651
+ }, "Save Dimension"))), orderedItems.length > 0 && /*#__PURE__*/React__default.createElement(Box$1, {
3633
3652
  sx: {
3634
3653
  marginTop: 0
3635
3654
  }
@@ -3652,7 +3671,7 @@ const Dimensions = ({
3652
3671
  collisionDetection: closestCenter,
3653
3672
  onDragEnd: handleDragEnd
3654
3673
  }, /*#__PURE__*/React__default.createElement(SortableContext, {
3655
- items: savedDimensions.map((_, index) => index),
3674
+ items: orderedItems.map((_, index) => index),
3656
3675
  strategy: verticalListSortingStrategy
3657
3676
  }, /*#__PURE__*/React__default.createElement(Box$1, {
3658
3677
  sx: {
@@ -3660,67 +3679,35 @@ const Dimensions = ({
3660
3679
  flexDirection: "column",
3661
3680
  gap: 1
3662
3681
  }
3663
- }, savedDimensions.map((dim, index) => /*#__PURE__*/React__default.createElement(SortableChip$1, {
3664
- key: index,
3682
+ }, orderedItems.map((item, index) => item.kind === "dimension" ? /*#__PURE__*/React__default.createElement(SortableChip$1, {
3683
+ key: `dim-${item.key}`,
3665
3684
  id: index,
3666
- label: dim.dimensionTitle,
3667
- fullLabel: `${formatProviderPath(dim)} → ${dim.dimensionTitle} (${dim.fullPath})`,
3668
- onDelete: () => onRemoveDimension(index),
3685
+ label: item.data.dimensionTitle,
3686
+ fullLabel: `${formatProviderPath(item.data)} → ${item.data.dimensionTitle} (${item.data.fullPath})`,
3687
+ onDelete: () => onRemoveDimension(item.key),
3669
3688
  onMoveUp: () => handleMoveUp(index),
3670
3689
  onMoveDown: () => handleMoveDown(index),
3671
3690
  isFirst: index === 0,
3672
- isLast: index === savedDimensions.length - 1,
3673
- sortOrder: dim.sortOrder || null,
3674
- onSortOrderChange: sortOrder => handleSortOrderChange(index, sortOrder),
3675
- fullPath: dim.fullPath,
3676
- defaultTitle: dim.dimensionTitle,
3677
- 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],
3678
3697
  onUpdateTitle: onUpdateTitle,
3679
3698
  onResetTitle: onResetTitle
3680
- })))))), savedComputedDimensions.length > 0 && /*#__PURE__*/React__default.createElement(Box$1, {
3681
- sx: {
3682
- marginTop: 2
3683
- }
3684
- }, /*#__PURE__*/React__default.createElement(Typography$1, {
3685
- variant: "h6",
3686
- sx: {
3687
- fontSize: "16px",
3688
- fontWeight: 600,
3689
- marginTop: 2,
3690
- color: "rgb(37, 37, 37)"
3691
- }
3692
- }, "Saved Computed Dimensions"), /*#__PURE__*/React__default.createElement(Typography$1, {
3693
- sx: {
3694
- fontSize: "13px",
3695
- color: "#666",
3696
- marginBottom: 2
3697
- }
3698
- }, "Drag to reorder or use arrows"), /*#__PURE__*/React__default.createElement(DndContext, {
3699
- sensors: sensors,
3700
- collisionDetection: closestCenter,
3701
- onDragEnd: handleComputedDragEnd
3702
- }, /*#__PURE__*/React__default.createElement(SortableContext, {
3703
- items: savedComputedDimensions.map((_, index) => index),
3704
- strategy: verticalListSortingStrategy
3705
- }, /*#__PURE__*/React__default.createElement(Box$1, {
3706
- sx: {
3707
- display: "flex",
3708
- flexDirection: "column",
3709
- gap: 1
3710
- }
3711
- }, savedComputedDimensions.map((cd, index) => /*#__PURE__*/React__default.createElement(SortableComputedChip, {
3712
- key: index,
3699
+ }) : /*#__PURE__*/React__default.createElement(SortableComputedChip, {
3700
+ key: `comp-${item.key}`,
3713
3701
  id: index,
3714
- index: index,
3715
- name: cd.name,
3716
- value: cd.value,
3702
+ name: item.data.name,
3703
+ value: item.data.value,
3717
3704
  onUpdate: onUpdateComputedDimension,
3718
3705
  isNameTaken: isComputedNameTaken,
3719
- onDelete: () => onRemoveComputedDimension(index),
3720
- onMoveUp: () => handleMoveComputedUp(index),
3721
- onMoveDown: () => handleMoveComputedDown(index),
3706
+ onDelete: () => onRemoveComputedDimension(item.key),
3707
+ onMoveUp: () => handleMoveUp(index),
3708
+ onMoveDown: () => handleMoveDown(index),
3722
3709
  isFirst: index === 0,
3723
- isLast: index === savedComputedDimensions.length - 1
3710
+ isLast: index === orderedItems.length - 1
3724
3711
  })))))));
3725
3712
  };
3726
3713
 
@@ -5703,9 +5690,8 @@ const formatValue = (value, def) => {
5703
5690
  const isNumericType = type => type === 'integer' || type === 'currency';
5704
5691
  const ReportDataGrid = ({
5705
5692
  reportData,
5706
- dimensions,
5693
+ orderedDimensionItems = [],
5707
5694
  metrics,
5708
- computedDimensions = [],
5709
5695
  loading,
5710
5696
  onPageChange,
5711
5697
  totalRows = 0,
@@ -5725,11 +5711,26 @@ const ReportDataGrid = ({
5725
5711
  const columns = React__default.useMemo(() => {
5726
5712
  const cols = [];
5727
5713
 
5728
- // Add dimension columns
5729
- // 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):
5730
5717
  // - If from base provider: baseAlias.field -> baseAlias_field
5731
5718
  // - If from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
5732
- 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;
5733
5734
  let fieldName;
5734
5735
  let headerName;
5735
5736
 
@@ -5767,18 +5768,6 @@ const ReportDataGrid = ({
5767
5768
  });
5768
5769
  });
5769
5770
 
5770
- // Add computed dimension columns. These have no provider/relations chain,
5771
- // so the API returns each row keyed by the computed dimension's own `name`.
5772
- computedDimensions.forEach(cd => {
5773
- cols.push({
5774
- field: cd.name,
5775
- headerName: cd.name,
5776
- flex: 1,
5777
- minWidth: 150,
5778
- type: 'string'
5779
- });
5780
- });
5781
-
5782
5771
  // Add metric columns
5783
5772
  // The API returns metric keys in two different forms depending on the path depth:
5784
5773
  //
@@ -5838,7 +5827,7 @@ const ReportDataGrid = ({
5838
5827
  });
5839
5828
  });
5840
5829
  return cols;
5841
- }, [dimensions, metrics, computedDimensions, titleOverrides, parameters]);
5830
+ }, [orderedDimensionItems, metrics, titleOverrides, parameters]);
5842
5831
 
5843
5832
  // Transform report data to rows with unique IDs.
5844
5833
  // Keys are normalised by replacing dots with underscores so that MUI DataGrid
@@ -5945,7 +5934,10 @@ const ReportBuilder = ({
5945
5934
  dimensions: [],
5946
5935
  metrics: [],
5947
5936
  filters: {},
5948
- computedDimensions: []
5937
+ computedDimensions: [],
5938
+ // Unified display/execution order across dimensions + computedDimensions.
5939
+ // Entries: { type: 'dimension', key: fullPath } | { type: 'computed', key: name }
5940
+ dimensionOrder: []
5949
5941
  });
5950
5942
  const [titleOverrides, setTitleOverrides] = useState({
5951
5943
  dimensions: {},
@@ -5991,6 +5983,16 @@ const ReportBuilder = ({
5991
5983
  }
5992
5984
  }, [autoRun, reportLoaded]);
5993
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
+
5994
5996
  // Utility function to reconstruct dimension object from fullPath
5995
5997
  const reconstructDimensionFromPath = (fullPath, providersData, rootProvider) => {
5996
5998
  try {
@@ -6120,6 +6122,52 @@ const ReportBuilder = ({
6120
6122
  return null;
6121
6123
  }
6122
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
+ };
6123
6171
  const loadReportDefinition = async id => {
6124
6172
  try {
6125
6173
  console.log("Loading report definition:", id);
@@ -6215,13 +6263,15 @@ const ReportBuilder = ({
6215
6263
  // Computed dimensions are self-contained { name, value } pairs with no
6216
6264
  // provider/relation chain, so they're loaded as-is (no reconstruction needed)
6217
6265
  const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
6266
+ const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
6218
6267
 
6219
6268
  // Set the report state
6220
6269
  setReport({
6221
6270
  dimensions: reconstructedDimensions,
6222
6271
  metrics: reconstructedMetrics,
6223
6272
  filters: loadedFilters,
6224
- computedDimensions
6273
+ computedDimensions,
6274
+ dimensionOrder
6225
6275
  });
6226
6276
 
6227
6277
  // Set title overrides
@@ -6330,13 +6380,15 @@ const ReportBuilder = ({
6330
6380
  // Computed dimensions are self-contained { name, value } pairs with no
6331
6381
  // provider/relation chain, so they're loaded as-is (no reconstruction needed)
6332
6382
  const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
6383
+ const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
6333
6384
 
6334
6385
  // Set the report state
6335
6386
  setReport({
6336
6387
  dimensions: reconstructedDimensions,
6337
6388
  metrics: reconstructedMetrics,
6338
6389
  filters: loadedFilters,
6339
- computedDimensions
6390
+ computedDimensions,
6391
+ dimensionOrder
6340
6392
  });
6341
6393
 
6342
6394
  // Set title overrides
@@ -6369,7 +6421,8 @@ const ReportBuilder = ({
6369
6421
  dimensions: [],
6370
6422
  metrics: [],
6371
6423
  filters: {},
6372
- computedDimensions: []
6424
+ computedDimensions: [],
6425
+ dimensionOrder: []
6373
6426
  });
6374
6427
  // Reset title overrides
6375
6428
  setTitleOverrides({
@@ -6446,51 +6499,68 @@ const ReportBuilder = ({
6446
6499
  setReport(prev => {
6447
6500
  const newReport = {
6448
6501
  ...prev,
6449
- dimensions: [...prev.dimensions, dimensionData]
6502
+ dimensions: [...prev.dimensions, dimensionData],
6503
+ dimensionOrder: [...prev.dimensionOrder, {
6504
+ type: "dimension",
6505
+ key: dimensionData.fullPath
6506
+ }]
6450
6507
  };
6451
6508
  console.log("Dimension saved:", dimensionData);
6452
6509
  console.log("Complete report:", newReport);
6453
6510
  return newReport;
6454
6511
  });
6455
6512
  };
6456
- const handleRemoveDimension = index => {
6513
+ const handleRemoveDimension = fullPath => {
6457
6514
  setReport(prev => ({
6458
6515
  ...prev,
6459
- dimensions: prev.dimensions.filter((_, i) => i !== index)
6516
+ dimensions: prev.dimensions.filter(d => d.fullPath !== fullPath),
6517
+ dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath))
6460
6518
  }));
6461
6519
  };
6462
- const handleReorderDimensions = newOrder => {
6520
+ const handleUpdateDimensionSortOrder = (fullPath, sortOrder) => {
6463
6521
  setReport(prev => ({
6464
6522
  ...prev,
6465
- dimensions: newOrder
6523
+ dimensions: prev.dimensions.map(d => d.fullPath === fullPath ? {
6524
+ ...d,
6525
+ sortOrder
6526
+ } : d)
6466
6527
  }));
6467
6528
  };
6468
- const handleSaveComputedDimension = computedDimensionData => {
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 => {
6469
6534
  setReport(prev => ({
6470
6535
  ...prev,
6471
- computedDimensions: [...prev.computedDimensions, computedDimensionData]
6536
+ dimensionOrder: newDimensionOrder
6472
6537
  }));
6473
6538
  };
6474
- const handleUpdateComputedDimension = (index, computedDimensionData) => {
6475
- setReport(prev => {
6476
- const newComputedDimensions = [...prev.computedDimensions];
6477
- newComputedDimensions[index] = computedDimensionData;
6478
- return {
6479
- ...prev,
6480
- computedDimensions: newComputedDimensions
6481
- };
6482
- });
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
+ }));
6483
6548
  };
6484
- const handleRemoveComputedDimension = index => {
6549
+ const handleUpdateComputedDimension = (name, computedDimensionData) => {
6485
6550
  setReport(prev => ({
6486
6551
  ...prev,
6487
- computedDimensions: prev.computedDimensions.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)
6488
6557
  }));
6489
6558
  };
6490
- const handleReorderComputedDimensions = newOrder => {
6559
+ const handleRemoveComputedDimension = name => {
6491
6560
  setReport(prev => ({
6492
6561
  ...prev,
6493
- computedDimensions: newOrder
6562
+ computedDimensions: prev.computedDimensions.filter(cd => cd.name !== name),
6563
+ dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
6494
6564
  }));
6495
6565
  };
6496
6566
  const handleSaveMetric = metricData => {
@@ -6546,10 +6616,20 @@ const ReportBuilder = ({
6546
6616
  });
6547
6617
  };
6548
6618
 
6549
- // Convert internal report structure to API format
6550
- 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
+
6551
6631
  // Build order_by array - include all dimensions, add desc property only when needed
6552
- const orderBy = report.dimensions.map(dim => {
6632
+ const orderBy = orderedDimensionsOnly.map(dim => {
6553
6633
  const orderItem = {
6554
6634
  name: dim.fullPath
6555
6635
  };
@@ -6605,21 +6685,29 @@ const ReportBuilder = ({
6605
6685
  titles.filters = titleOverrides.filters;
6606
6686
  }
6607
6687
  const queryObj = {
6608
- dimensions: report.dimensions.map(dim => dim.fullPath),
6688
+ dimensions: orderedDimensionsOnly.map(dim => dim.fullPath),
6609
6689
  metrics: report.metrics.map(metric => metric.fullPath),
6610
- order_by: orderBy,
6611
- limit: size,
6612
- offset: page * size
6690
+ order_by: orderBy
6613
6691
  };
6614
6692
 
6615
6693
  // Only add computed_dimensions if there are any
6616
- if (report.computedDimensions && report.computedDimensions.length > 0) {
6617
- queryObj.computed_dimensions = report.computedDimensions.map(cd => ({
6694
+ if (orderedComputedOnly.length > 0) {
6695
+ queryObj.computed_dimensions = orderedComputedOnly.map(cd => ({
6618
6696
  name: cd.name,
6619
6697
  value: cd.value
6620
6698
  }));
6621
6699
  }
6622
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
+
6623
6711
  // Only add titles if there are any overrides
6624
6712
  if (Object.keys(titles).length > 0) {
6625
6713
  queryObj.titles = titles;
@@ -6629,6 +6717,14 @@ const ReportBuilder = ({
6629
6717
  if (filter) {
6630
6718
  queryObj.filter = filter;
6631
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;
6632
6728
 
6633
6729
  // Get parameters from context if available, otherwise use default
6634
6730
  const parameters = reportingContext?.parameters || {
@@ -6722,77 +6818,7 @@ const ReportBuilder = ({
6722
6818
  }
6723
6819
  try {
6724
6820
  setSaving(true);
6725
-
6726
- // Build order_by array - include all dimensions, add desc property only when needed
6727
- const orderBy = report.dimensions.map(dim => {
6728
- const orderItem = {
6729
- name: dim.fullPath
6730
- };
6731
- if (dim.sortOrder === "desc") {
6732
- orderItem.desc = true;
6733
- }
6734
- // For 'asc' or null, just include { name: fullPath } without desc property
6735
- return orderItem;
6736
- });
6737
-
6738
- // Build titles object - only include overridden titles
6739
- const titles = {};
6740
- if (Object.keys(titleOverrides.dimensions).length > 0) {
6741
- titles.dimensions = titleOverrides.dimensions;
6742
- }
6743
- if (Object.keys(titleOverrides.metrics).length > 0) {
6744
- titles.metrics = titleOverrides.metrics;
6745
- }
6746
- if (Object.keys(titleOverrides.filters).length > 0) {
6747
- titles.filters = titleOverrides.filters;
6748
- }
6749
- const queryObj = {
6750
- dimensions: report.dimensions.map(dim => dim.fullPath),
6751
- metrics: report.metrics.map(metric => metric.fullPath),
6752
- order_by: orderBy
6753
- };
6754
-
6755
- // Only add computed_dimensions if there are any
6756
- if (report.computedDimensions && report.computedDimensions.length > 0) {
6757
- queryObj.computed_dimensions = report.computedDimensions.map(cd => ({
6758
- name: cd.name,
6759
- value: cd.value
6760
- }));
6761
- }
6762
-
6763
- // Only add titles if there are any overrides
6764
- if (Object.keys(titles).length > 0) {
6765
- queryObj.titles = titles;
6766
- }
6767
-
6768
- // Add filter if there are any - NEW API format with top-level 'and' operator
6769
- // NEW format: { and: [ { "ft.currency": ["USD", "EUR"] }, { "ft_ba.created_at": {gte: "2025-05-01", lte: "2025-12-31"} } ] }
6770
- if (Object.keys(report.filters).length > 0) {
6771
- const conditions = [];
6772
- Object.entries(report.filters).forEach(([fullPath, filterData]) => {
6773
- if (filterData.values) {
6774
- // Check if values is an object (date range) or array (regular filter)
6775
- if (Array.isArray(filterData.values)) {
6776
- // Regular filter - only add if array has values
6777
- if (filterData.values.length > 0) {
6778
- conditions.push({
6779
- [fullPath]: filterData.values
6780
- });
6781
- }
6782
- } else if (typeof filterData.values === "object") {
6783
- // Date range filter - add the object as-is
6784
- conditions.push({
6785
- [fullPath]: filterData.values
6786
- });
6787
- }
6788
- }
6789
- });
6790
- if (conditions.length > 0) {
6791
- queryObj.filter = {
6792
- and: conditions
6793
- };
6794
- }
6795
- }
6821
+ const queryObj = buildQueryObject();
6796
6822
 
6797
6823
  // Build the doc object
6798
6824
  const docObj = {
@@ -6837,6 +6863,18 @@ const ReportBuilder = ({
6837
6863
  }
6838
6864
  };
6839
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);
6840
6878
  return /*#__PURE__*/React__default.createElement(Box$1, {
6841
6879
  sx: {
6842
6880
  p: 3,
@@ -7115,7 +7153,7 @@ const ReportBuilder = ({
7115
7153
  savedDimensions: report.dimensions,
7116
7154
  onSaveDimension: handleSaveDimension,
7117
7155
  onRemoveDimension: handleRemoveDimension,
7118
- onReorderDimensions: handleReorderDimensions,
7156
+ onUpdateDimensionSortOrder: handleUpdateDimensionSortOrder,
7119
7157
  titleOverrides: titleOverrides.dimensions,
7120
7158
  onUpdateTitle: handleUpdateDimensionTitle,
7121
7159
  onResetTitle: handleResetDimensionTitle,
@@ -7125,7 +7163,8 @@ const ReportBuilder = ({
7125
7163
  onSaveComputedDimension: handleSaveComputedDimension,
7126
7164
  onUpdateComputedDimension: handleUpdateComputedDimension,
7127
7165
  onRemoveComputedDimension: handleRemoveComputedDimension,
7128
- onReorderComputedDimensions: handleReorderComputedDimensions
7166
+ dimensionOrder: report.dimensionOrder,
7167
+ onReorderDimensionItems: handleReorderDimensionItems
7129
7168
  })), /*#__PURE__*/React__default.createElement(TabPanel, {
7130
7169
  value: activeTab,
7131
7170
  index: 1
@@ -7161,9 +7200,8 @@ const ReportBuilder = ({
7161
7200
  index: 3
7162
7201
  }, reportData && /*#__PURE__*/React__default.createElement(ReportDataGrid, {
7163
7202
  reportData: reportData,
7164
- dimensions: report.dimensions,
7203
+ orderedDimensionItems: orderedDimensionItems,
7165
7204
  metrics: report.metrics,
7166
- computedDimensions: report.computedDimensions,
7167
7205
  loading: loading,
7168
7206
  onPageChange: handlePageChange,
7169
7207
  totalRows: totalRows,
@@ -7226,6 +7264,44 @@ const ReportDefinitionsManager = () => {
7226
7264
  });
7227
7265
  };
7228
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
+ };
7229
7305
  const ReportApp = ({
7230
7306
  params,
7231
7307
  api
@@ -7292,7 +7368,13 @@ const ReportApp = ({
7292
7368
  }
7293
7369
  }
7294
7370
  })
7295
- }, /*#__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))));
7296
7378
  };
7297
7379
 
7298
7380
  var index = {