@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.
@@ -1653,6 +1653,39 @@ const ReportingProvider = ({
1653
1653
  }, children);
1654
1654
  };
1655
1655
 
1656
+ /**
1657
+ * useReportingContext - Hook to access reporting context
1658
+ *
1659
+ * @returns {Object} Context value with parameters, api, and setter functions
1660
+ * @returns {Object} return.parameters - Current parameters object
1661
+ * @returns {Function} return.setParameters - Function to update parameters
1662
+ * @returns {Object} return.api - Current API configuration
1663
+ * @returns {Function} return.setApi - Function to update API configuration
1664
+ * @returns {Function} return.setReportingContext - Function to update both parameters and api
1665
+ *
1666
+ * @example
1667
+ * const { parameters, setParameters, api, setApi, setReportingContext } = useReportingContext();
1668
+ *
1669
+ * // Update parameters only
1670
+ * setParameters({ base_currency: 'USD', merchants: [...] });
1671
+ *
1672
+ * // Update API only
1673
+ * setApi({ token: 'new-token', base_url: 'https://api.example.com' });
1674
+ *
1675
+ * // Update both at once
1676
+ * setReportingContext({
1677
+ * parameters: { base_currency: 'EUR' },
1678
+ * api: { token: 'token', base_url: 'https://api.example.com' }
1679
+ * });
1680
+ */
1681
+ const useReportingContext = () => {
1682
+ const context = React.useContext(ReportingContext);
1683
+ if (context === undefined) {
1684
+ throw new Error('useReportingContext must be used within a ReportingProvider');
1685
+ }
1686
+ return context;
1687
+ };
1688
+
1656
1689
  /**
1657
1690
  * useReportingContextOptional - Hook to optionally access reporting context
1658
1691
  * Returns null if used outside of ReportingProvider (useful for optional fallback)
@@ -2896,7 +2929,6 @@ const SortableChip$1 = ({
2896
2929
  // the top-of-page Add form, so it stays visible regardless of scroll position.
2897
2930
  const SortableComputedChip = ({
2898
2931
  id,
2899
- index,
2900
2932
  name,
2901
2933
  value,
2902
2934
  onDelete,
@@ -2949,13 +2981,13 @@ const SortableComputedChip = ({
2949
2981
  setEditValue(value);
2950
2982
  setIsEditing(true);
2951
2983
  };
2952
- const nameError = editName.trim() !== "" && isNameTaken(editName.trim(), index);
2984
+ const nameError = editName.trim() !== "" && isNameTaken(editName.trim(), name);
2953
2985
  const handleSave = () => {
2954
2986
  const trimmedName = editName.trim();
2955
2987
  const trimmedValue = editValue.trim();
2956
2988
  if (!trimmedName || !trimmedValue) return;
2957
- if (isNameTaken(trimmedName, index)) return;
2958
- onUpdate(index, {
2989
+ if (isNameTaken(trimmedName, name)) return;
2990
+ onUpdate(name, {
2959
2991
  name: trimmedName,
2960
2992
  value: trimmedValue
2961
2993
  });
@@ -3153,7 +3185,7 @@ const Dimensions = ({
3153
3185
  savedDimensions = [],
3154
3186
  onSaveDimension,
3155
3187
  onRemoveDimension,
3156
- onReorderDimensions,
3188
+ onUpdateDimensionSortOrder,
3157
3189
  titleOverrides = {},
3158
3190
  onUpdateTitle,
3159
3191
  onResetTitle,
@@ -3163,7 +3195,8 @@ const Dimensions = ({
3163
3195
  onSaveComputedDimension,
3164
3196
  onUpdateComputedDimension,
3165
3197
  onRemoveComputedDimension,
3166
- onReorderComputedDimensions
3198
+ dimensionOrder = [],
3199
+ onReorderDimensionItems
3167
3200
  }) => {
3168
3201
  const reportingContext = useReportingContextOptional();
3169
3202
  const parameters = reportingContext?.parameters;
@@ -3179,49 +3212,56 @@ const Dimensions = ({
3179
3212
  coordinateGetter: sortable.sortableKeyboardCoordinates
3180
3213
  }));
3181
3214
 
3182
- // Handle drag end
3215
+ // Resolve the unified dimensionOrder into the actual dimension / computed
3216
+ // dimension objects, so dimensions and computed dimensions can be shown
3217
+ // (and dragged) as a single list while staying stored in their own arrays.
3218
+ const dimensionByFullPath = Object.fromEntries(savedDimensions.map(d => [d.fullPath, d]));
3219
+ const computedDimensionByName = Object.fromEntries(savedComputedDimensions.map(cd => [cd.name, cd]));
3220
+ const orderedItems = dimensionOrder.map(entry => entry.type === "dimension" ? {
3221
+ kind: "dimension",
3222
+ key: entry.key,
3223
+ data: dimensionByFullPath[entry.key]
3224
+ } : {
3225
+ kind: "computed",
3226
+ key: entry.key,
3227
+ data: computedDimensionByName[entry.key]
3228
+ }).filter(item => item.data);
3229
+
3230
+ // Handle drag end for the unified list
3183
3231
  const handleDragEnd = event => {
3184
3232
  const {
3185
3233
  active,
3186
3234
  over
3187
3235
  } = event;
3188
3236
  if (over && active.id !== over.id) {
3189
- const oldIndex = savedDimensions.findIndex((_, i) => i === active.id);
3190
- const newIndex = savedDimensions.findIndex((_, i) => i === over.id);
3191
- const newOrder = sortable.arrayMove(savedDimensions, oldIndex, newIndex);
3192
- onReorderDimensions(newOrder);
3237
+ const oldIndex = dimensionOrder.findIndex((_, i) => i === active.id);
3238
+ const newIndex = dimensionOrder.findIndex((_, i) => i === over.id);
3239
+ onReorderDimensionItems(sortable.arrayMove(dimensionOrder, oldIndex, newIndex));
3193
3240
  }
3194
3241
  };
3195
3242
 
3196
3243
  // Handle move up
3197
3244
  const handleMoveUp = index => {
3198
3245
  if (index > 0) {
3199
- const newOrder = sortable.arrayMove(savedDimensions, index, index - 1);
3200
- onReorderDimensions(newOrder);
3246
+ onReorderDimensionItems(sortable.arrayMove(dimensionOrder, index, index - 1));
3201
3247
  }
3202
3248
  };
3203
3249
 
3204
3250
  // Handle move down
3205
3251
  const handleMoveDown = index => {
3206
- if (index < savedDimensions.length - 1) {
3207
- const newOrder = sortable.arrayMove(savedDimensions, index, index + 1);
3208
- onReorderDimensions(newOrder);
3252
+ if (index < dimensionOrder.length - 1) {
3253
+ onReorderDimensionItems(sortable.arrayMove(dimensionOrder, index, index + 1));
3209
3254
  }
3210
3255
  };
3211
3256
 
3212
- // Handle sort order change
3213
- const handleSortOrderChange = (index, sortOrder) => {
3214
- const newDimensions = [...savedDimensions];
3215
- newDimensions[index] = {
3216
- ...newDimensions[index],
3217
- sortOrder
3218
- };
3219
- onReorderDimensions(newDimensions);
3257
+ // Handle sort order change (dimensions only - computed dimensions have no order_by)
3258
+ const handleSortOrderChange = (fullPath, sortOrder) => {
3259
+ onUpdateDimensionSortOrder(fullPath, sortOrder);
3220
3260
  };
3221
3261
 
3222
3262
  // Computed Dimensions: { name, value } pairs independent of any provider.
3223
3263
  // Their `value` is a raw SQL literal/expression sent to the API as-is.
3224
- const isComputedNameTaken = (name, excludeIndex = null) => savedComputedDimensions.some((cd, i) => cd.name === name && i !== excludeIndex);
3264
+ const isComputedNameTaken = (name, excludeName = null) => savedComputedDimensions.some(cd => cd.name === name) && name !== excludeName;
3225
3265
  const computedNameError = computedName.trim() !== "" && isComputedNameTaken(computedName.trim());
3226
3266
  const handleAddComputedClick = () => {
3227
3267
  setIsAddingComputed(true);
@@ -3245,27 +3285,6 @@ const Dimensions = ({
3245
3285
  });
3246
3286
  handleCancelComputed();
3247
3287
  };
3248
- const handleComputedDragEnd = event => {
3249
- const {
3250
- active,
3251
- over
3252
- } = event;
3253
- if (over && active.id !== over.id) {
3254
- const oldIndex = savedComputedDimensions.findIndex((_, i) => i === active.id);
3255
- const newIndex = savedComputedDimensions.findIndex((_, i) => i === over.id);
3256
- onReorderComputedDimensions(sortable.arrayMove(savedComputedDimensions, oldIndex, newIndex));
3257
- }
3258
- };
3259
- const handleMoveComputedUp = index => {
3260
- if (index > 0) {
3261
- onReorderComputedDimensions(sortable.arrayMove(savedComputedDimensions, index, index - 1));
3262
- }
3263
- };
3264
- const handleMoveComputedDown = index => {
3265
- if (index < savedComputedDimensions.length - 1) {
3266
- onReorderComputedDimensions(sortable.arrayMove(savedComputedDimensions, index, index + 1));
3267
- }
3268
- };
3269
3288
 
3270
3289
  // Get the current provider based on selection chain
3271
3290
  const getCurrentProvider = () => {
@@ -3708,7 +3727,7 @@ const Dimensions = ({
3708
3727
  boxShadow: "none"
3709
3728
  }
3710
3729
  }
3711
- }, "Save Dimension"))), savedDimensions.length > 0 && /*#__PURE__*/React__namespace.default.createElement(material.Box, {
3730
+ }, "Save Dimension"))), orderedItems.length > 0 && /*#__PURE__*/React__namespace.default.createElement(material.Box, {
3712
3731
  sx: {
3713
3732
  marginTop: 0
3714
3733
  }
@@ -3731,7 +3750,7 @@ const Dimensions = ({
3731
3750
  collisionDetection: core.closestCenter,
3732
3751
  onDragEnd: handleDragEnd
3733
3752
  }, /*#__PURE__*/React__namespace.default.createElement(sortable.SortableContext, {
3734
- items: savedDimensions.map((_, index) => index),
3753
+ items: orderedItems.map((_, index) => index),
3735
3754
  strategy: sortable.verticalListSortingStrategy
3736
3755
  }, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
3737
3756
  sx: {
@@ -3739,67 +3758,35 @@ const Dimensions = ({
3739
3758
  flexDirection: "column",
3740
3759
  gap: 1
3741
3760
  }
3742
- }, savedDimensions.map((dim, index) => /*#__PURE__*/React__namespace.default.createElement(SortableChip$1, {
3743
- key: index,
3761
+ }, orderedItems.map((item, index) => item.kind === "dimension" ? /*#__PURE__*/React__namespace.default.createElement(SortableChip$1, {
3762
+ key: `dim-${item.key}`,
3744
3763
  id: index,
3745
- label: dim.dimensionTitle,
3746
- fullLabel: `${formatProviderPath(dim)} → ${dim.dimensionTitle} (${dim.fullPath})`,
3747
- onDelete: () => onRemoveDimension(index),
3764
+ label: item.data.dimensionTitle,
3765
+ fullLabel: `${formatProviderPath(item.data)} → ${item.data.dimensionTitle} (${item.data.fullPath})`,
3766
+ onDelete: () => onRemoveDimension(item.key),
3748
3767
  onMoveUp: () => handleMoveUp(index),
3749
3768
  onMoveDown: () => handleMoveDown(index),
3750
3769
  isFirst: index === 0,
3751
- isLast: index === savedDimensions.length - 1,
3752
- sortOrder: dim.sortOrder || null,
3753
- onSortOrderChange: sortOrder => handleSortOrderChange(index, sortOrder),
3754
- fullPath: dim.fullPath,
3755
- defaultTitle: dim.dimensionTitle,
3756
- customTitle: titleOverrides[dim.fullPath],
3770
+ isLast: index === orderedItems.length - 1,
3771
+ sortOrder: item.data.sortOrder || null,
3772
+ onSortOrderChange: sortOrder => handleSortOrderChange(item.key, sortOrder),
3773
+ fullPath: item.data.fullPath,
3774
+ defaultTitle: item.data.dimensionTitle,
3775
+ customTitle: titleOverrides[item.data.fullPath],
3757
3776
  onUpdateTitle: onUpdateTitle,
3758
3777
  onResetTitle: onResetTitle
3759
- })))))), savedComputedDimensions.length > 0 && /*#__PURE__*/React__namespace.default.createElement(material.Box, {
3760
- sx: {
3761
- marginTop: 2
3762
- }
3763
- }, /*#__PURE__*/React__namespace.default.createElement(material.Typography, {
3764
- variant: "h6",
3765
- sx: {
3766
- fontSize: "16px",
3767
- fontWeight: 600,
3768
- marginTop: 2,
3769
- color: "rgb(37, 37, 37)"
3770
- }
3771
- }, "Saved Computed Dimensions"), /*#__PURE__*/React__namespace.default.createElement(material.Typography, {
3772
- sx: {
3773
- fontSize: "13px",
3774
- color: "#666",
3775
- marginBottom: 2
3776
- }
3777
- }, "Drag to reorder or use arrows"), /*#__PURE__*/React__namespace.default.createElement(core.DndContext, {
3778
- sensors: sensors,
3779
- collisionDetection: core.closestCenter,
3780
- onDragEnd: handleComputedDragEnd
3781
- }, /*#__PURE__*/React__namespace.default.createElement(sortable.SortableContext, {
3782
- items: savedComputedDimensions.map((_, index) => index),
3783
- strategy: sortable.verticalListSortingStrategy
3784
- }, /*#__PURE__*/React__namespace.default.createElement(material.Box, {
3785
- sx: {
3786
- display: "flex",
3787
- flexDirection: "column",
3788
- gap: 1
3789
- }
3790
- }, savedComputedDimensions.map((cd, index) => /*#__PURE__*/React__namespace.default.createElement(SortableComputedChip, {
3791
- key: index,
3778
+ }) : /*#__PURE__*/React__namespace.default.createElement(SortableComputedChip, {
3779
+ key: `comp-${item.key}`,
3792
3780
  id: index,
3793
- index: index,
3794
- name: cd.name,
3795
- value: cd.value,
3781
+ name: item.data.name,
3782
+ value: item.data.value,
3796
3783
  onUpdate: onUpdateComputedDimension,
3797
3784
  isNameTaken: isComputedNameTaken,
3798
- onDelete: () => onRemoveComputedDimension(index),
3799
- onMoveUp: () => handleMoveComputedUp(index),
3800
- onMoveDown: () => handleMoveComputedDown(index),
3785
+ onDelete: () => onRemoveComputedDimension(item.key),
3786
+ onMoveUp: () => handleMoveUp(index),
3787
+ onMoveDown: () => handleMoveDown(index),
3801
3788
  isFirst: index === 0,
3802
- isLast: index === savedComputedDimensions.length - 1
3789
+ isLast: index === orderedItems.length - 1
3803
3790
  })))))));
3804
3791
  };
3805
3792
 
@@ -5782,9 +5769,8 @@ const formatValue = (value, def) => {
5782
5769
  const isNumericType = type => type === 'integer' || type === 'currency';
5783
5770
  const ReportDataGrid = ({
5784
5771
  reportData,
5785
- dimensions,
5772
+ orderedDimensionItems = [],
5786
5773
  metrics,
5787
- computedDimensions = [],
5788
5774
  loading,
5789
5775
  onPageChange,
5790
5776
  totalRows = 0,
@@ -5804,11 +5790,26 @@ const ReportDataGrid = ({
5804
5790
  const columns = React__namespace.default.useMemo(() => {
5805
5791
  const cols = [];
5806
5792
 
5807
- // Add dimension columns
5808
- // Field naming logic:
5793
+ // Add dimension + computed-dimension columns, in the single unified
5794
+ // order the user arranged them in on the Dimensions tab.
5795
+ // Field naming logic (dimensions):
5809
5796
  // - If from base provider: baseAlias.field -> baseAlias_field
5810
5797
  // - If from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
5811
- dimensions.forEach(dim => {
5798
+ // Computed dimensions have no provider/relations chain, so the API
5799
+ // returns each row keyed by the computed dimension's own `name`.
5800
+ orderedDimensionItems.forEach(item => {
5801
+ if (item.kind === 'computed') {
5802
+ const cd = item.data;
5803
+ cols.push({
5804
+ field: cd.name,
5805
+ headerName: cd.name,
5806
+ flex: 1,
5807
+ minWidth: 150,
5808
+ type: 'string'
5809
+ });
5810
+ return;
5811
+ }
5812
+ const dim = item.data;
5812
5813
  let fieldName;
5813
5814
  let headerName;
5814
5815
 
@@ -5846,18 +5847,6 @@ const ReportDataGrid = ({
5846
5847
  });
5847
5848
  });
5848
5849
 
5849
- // Add computed dimension columns. These have no provider/relations chain,
5850
- // so the API returns each row keyed by the computed dimension's own `name`.
5851
- computedDimensions.forEach(cd => {
5852
- cols.push({
5853
- field: cd.name,
5854
- headerName: cd.name,
5855
- flex: 1,
5856
- minWidth: 150,
5857
- type: 'string'
5858
- });
5859
- });
5860
-
5861
5850
  // Add metric columns
5862
5851
  // The API returns metric keys in two different forms depending on the path depth:
5863
5852
  //
@@ -5917,7 +5906,7 @@ const ReportDataGrid = ({
5917
5906
  });
5918
5907
  });
5919
5908
  return cols;
5920
- }, [dimensions, metrics, computedDimensions, titleOverrides, parameters]);
5909
+ }, [orderedDimensionItems, metrics, titleOverrides, parameters]);
5921
5910
 
5922
5911
  // Transform report data to rows with unique IDs.
5923
5912
  // Keys are normalised by replacing dots with underscores so that MUI DataGrid
@@ -6024,7 +6013,10 @@ const ReportBuilder = ({
6024
6013
  dimensions: [],
6025
6014
  metrics: [],
6026
6015
  filters: {},
6027
- computedDimensions: []
6016
+ computedDimensions: [],
6017
+ // Unified display/execution order across dimensions + computedDimensions.
6018
+ // Entries: { type: 'dimension', key: fullPath } | { type: 'computed', key: name }
6019
+ dimensionOrder: []
6028
6020
  });
6029
6021
  const [titleOverrides, setTitleOverrides] = React.useState({
6030
6022
  dimensions: {},
@@ -6070,6 +6062,16 @@ const ReportBuilder = ({
6070
6062
  }
6071
6063
  }, [autoRun, reportLoaded]);
6072
6064
 
6065
+ // Re-run the currently displayed report when base_currency changes in context
6066
+ const currentBaseCurrency = reportingContext?.parameters?.base_currency;
6067
+ const prevBaseCurrencyRef = React.useRef(currentBaseCurrency);
6068
+ React.useEffect(() => {
6069
+ if (reportData && prevBaseCurrencyRef.current !== undefined && prevBaseCurrencyRef.current !== currentBaseCurrency) {
6070
+ runReportWithPagination(currentPage, pageSize);
6071
+ }
6072
+ prevBaseCurrencyRef.current = currentBaseCurrency;
6073
+ }, [currentBaseCurrency]);
6074
+
6073
6075
  // Utility function to reconstruct dimension object from fullPath
6074
6076
  const reconstructDimensionFromPath = (fullPath, providersData, rootProvider) => {
6075
6077
  try {
@@ -6199,6 +6201,52 @@ const ReportBuilder = ({
6199
6201
  return null;
6200
6202
  }
6201
6203
  };
6204
+
6205
+ // Build the unified display/execution order for dimensions + computed
6206
+ // dimensions. Prefers the persisted `ordered_dimensions` field; falls back
6207
+ // to "dimensions first, then computed dimensions" for report definitions
6208
+ // saved before this field existed.
6209
+ const buildDimensionOrder = (rawOrderedDimensions, dimensions, computedDimensions) => {
6210
+ const dimensionOrder = [];
6211
+ const seenDimensionKeys = new Set();
6212
+ const seenComputedKeys = new Set();
6213
+ if (Array.isArray(rawOrderedDimensions)) {
6214
+ rawOrderedDimensions.forEach(entry => {
6215
+ if (entry.type === "dimension" && dimensions.some(d => d.fullPath === entry.ref)) {
6216
+ dimensionOrder.push({
6217
+ type: "dimension",
6218
+ key: entry.ref
6219
+ });
6220
+ seenDimensionKeys.add(entry.ref);
6221
+ } else if (entry.type === "computed" && computedDimensions.some(cd => cd.name === entry.ref)) {
6222
+ dimensionOrder.push({
6223
+ type: "computed",
6224
+ key: entry.ref
6225
+ });
6226
+ seenComputedKeys.add(entry.ref);
6227
+ }
6228
+ });
6229
+ }
6230
+
6231
+ // Append anything not covered by ordered_dimensions (older reports, or drift)
6232
+ dimensions.forEach(d => {
6233
+ if (!seenDimensionKeys.has(d.fullPath)) {
6234
+ dimensionOrder.push({
6235
+ type: "dimension",
6236
+ key: d.fullPath
6237
+ });
6238
+ }
6239
+ });
6240
+ computedDimensions.forEach(cd => {
6241
+ if (!seenComputedKeys.has(cd.name)) {
6242
+ dimensionOrder.push({
6243
+ type: "computed",
6244
+ key: cd.name
6245
+ });
6246
+ }
6247
+ });
6248
+ return dimensionOrder;
6249
+ };
6202
6250
  const loadReportDefinition = async id => {
6203
6251
  try {
6204
6252
  console.log("Loading report definition:", id);
@@ -6294,13 +6342,15 @@ const ReportBuilder = ({
6294
6342
  // Computed dimensions are self-contained { name, value } pairs with no
6295
6343
  // provider/relation chain, so they're loaded as-is (no reconstruction needed)
6296
6344
  const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
6345
+ const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
6297
6346
 
6298
6347
  // Set the report state
6299
6348
  setReport({
6300
6349
  dimensions: reconstructedDimensions,
6301
6350
  metrics: reconstructedMetrics,
6302
6351
  filters: loadedFilters,
6303
- computedDimensions
6352
+ computedDimensions,
6353
+ dimensionOrder
6304
6354
  });
6305
6355
 
6306
6356
  // Set title overrides
@@ -6409,13 +6459,15 @@ const ReportBuilder = ({
6409
6459
  // Computed dimensions are self-contained { name, value } pairs with no
6410
6460
  // provider/relation chain, so they're loaded as-is (no reconstruction needed)
6411
6461
  const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
6462
+ const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
6412
6463
 
6413
6464
  // Set the report state
6414
6465
  setReport({
6415
6466
  dimensions: reconstructedDimensions,
6416
6467
  metrics: reconstructedMetrics,
6417
6468
  filters: loadedFilters,
6418
- computedDimensions
6469
+ computedDimensions,
6470
+ dimensionOrder
6419
6471
  });
6420
6472
 
6421
6473
  // Set title overrides
@@ -6448,7 +6500,8 @@ const ReportBuilder = ({
6448
6500
  dimensions: [],
6449
6501
  metrics: [],
6450
6502
  filters: {},
6451
- computedDimensions: []
6503
+ computedDimensions: [],
6504
+ dimensionOrder: []
6452
6505
  });
6453
6506
  // Reset title overrides
6454
6507
  setTitleOverrides({
@@ -6525,51 +6578,68 @@ const ReportBuilder = ({
6525
6578
  setReport(prev => {
6526
6579
  const newReport = {
6527
6580
  ...prev,
6528
- dimensions: [...prev.dimensions, dimensionData]
6581
+ dimensions: [...prev.dimensions, dimensionData],
6582
+ dimensionOrder: [...prev.dimensionOrder, {
6583
+ type: "dimension",
6584
+ key: dimensionData.fullPath
6585
+ }]
6529
6586
  };
6530
6587
  console.log("Dimension saved:", dimensionData);
6531
6588
  console.log("Complete report:", newReport);
6532
6589
  return newReport;
6533
6590
  });
6534
6591
  };
6535
- const handleRemoveDimension = index => {
6592
+ const handleRemoveDimension = fullPath => {
6536
6593
  setReport(prev => ({
6537
6594
  ...prev,
6538
- dimensions: prev.dimensions.filter((_, i) => i !== index)
6595
+ dimensions: prev.dimensions.filter(d => d.fullPath !== fullPath),
6596
+ dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath))
6539
6597
  }));
6540
6598
  };
6541
- const handleReorderDimensions = newOrder => {
6599
+ const handleUpdateDimensionSortOrder = (fullPath, sortOrder) => {
6542
6600
  setReport(prev => ({
6543
6601
  ...prev,
6544
- dimensions: newOrder
6602
+ dimensions: prev.dimensions.map(d => d.fullPath === fullPath ? {
6603
+ ...d,
6604
+ sortOrder
6605
+ } : d)
6545
6606
  }));
6546
6607
  };
6547
- const handleSaveComputedDimension = computedDimensionData => {
6608
+
6609
+ // Reorders the single unified dimensions + computed-dimensions list;
6610
+ // `dimensions`/`computedDimensions` arrays themselves are unordered from
6611
+ // this perspective, so only `dimensionOrder` needs to change.
6612
+ const handleReorderDimensionItems = newDimensionOrder => {
6548
6613
  setReport(prev => ({
6549
6614
  ...prev,
6550
- computedDimensions: [...prev.computedDimensions, computedDimensionData]
6615
+ dimensionOrder: newDimensionOrder
6551
6616
  }));
6552
6617
  };
6553
- const handleUpdateComputedDimension = (index, computedDimensionData) => {
6554
- setReport(prev => {
6555
- const newComputedDimensions = [...prev.computedDimensions];
6556
- newComputedDimensions[index] = computedDimensionData;
6557
- return {
6558
- ...prev,
6559
- computedDimensions: newComputedDimensions
6560
- };
6561
- });
6618
+ const handleSaveComputedDimension = computedDimensionData => {
6619
+ setReport(prev => ({
6620
+ ...prev,
6621
+ computedDimensions: [...prev.computedDimensions, computedDimensionData],
6622
+ dimensionOrder: [...prev.dimensionOrder, {
6623
+ type: "computed",
6624
+ key: computedDimensionData.name
6625
+ }]
6626
+ }));
6562
6627
  };
6563
- const handleRemoveComputedDimension = index => {
6628
+ const handleUpdateComputedDimension = (name, computedDimensionData) => {
6564
6629
  setReport(prev => ({
6565
6630
  ...prev,
6566
- computedDimensions: prev.computedDimensions.filter((_, i) => i !== index)
6631
+ computedDimensions: prev.computedDimensions.map(cd => cd.name === name ? computedDimensionData : cd),
6632
+ dimensionOrder: prev.dimensionOrder.map(entry => entry.type === "computed" && entry.key === name ? {
6633
+ ...entry,
6634
+ key: computedDimensionData.name
6635
+ } : entry)
6567
6636
  }));
6568
6637
  };
6569
- const handleReorderComputedDimensions = newOrder => {
6638
+ const handleRemoveComputedDimension = name => {
6570
6639
  setReport(prev => ({
6571
6640
  ...prev,
6572
- computedDimensions: newOrder
6641
+ computedDimensions: prev.computedDimensions.filter(cd => cd.name !== name),
6642
+ dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
6573
6643
  }));
6574
6644
  };
6575
6645
  const handleSaveMetric = metricData => {
@@ -6625,10 +6695,20 @@ const ReportBuilder = ({
6625
6695
  });
6626
6696
  };
6627
6697
 
6628
- // Convert internal report structure to API format
6629
- const convertReportToApiFormat = (page = 0, size = 50) => {
6698
+ // Builds the query object shared between running (paginated) and saving
6699
+ // (unpaginated) a report. Dimension and computed-dimension order both
6700
+ // derive from the single unified `report.dimensionOrder` list, so the
6701
+ // saved `dimensions`/`computed_dimensions` arrays, `order_by`, and the
6702
+ // persisted `ordered_dimensions` field all stay consistent with whatever
6703
+ // order the user arranged on screen.
6704
+ const buildQueryObject = () => {
6705
+ const dimensionByPath = Object.fromEntries(report.dimensions.map(d => [d.fullPath, d]));
6706
+ const computedByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
6707
+ const orderedDimensionsOnly = report.dimensionOrder.filter(entry => entry.type === "dimension").map(entry => dimensionByPath[entry.key]).filter(Boolean);
6708
+ const orderedComputedOnly = report.dimensionOrder.filter(entry => entry.type === "computed").map(entry => computedByName[entry.key]).filter(Boolean);
6709
+
6630
6710
  // Build order_by array - include all dimensions, add desc property only when needed
6631
- const orderBy = report.dimensions.map(dim => {
6711
+ const orderBy = orderedDimensionsOnly.map(dim => {
6632
6712
  const orderItem = {
6633
6713
  name: dim.fullPath
6634
6714
  };
@@ -6684,21 +6764,29 @@ const ReportBuilder = ({
6684
6764
  titles.filters = titleOverrides.filters;
6685
6765
  }
6686
6766
  const queryObj = {
6687
- dimensions: report.dimensions.map(dim => dim.fullPath),
6767
+ dimensions: orderedDimensionsOnly.map(dim => dim.fullPath),
6688
6768
  metrics: report.metrics.map(metric => metric.fullPath),
6689
- order_by: orderBy,
6690
- limit: size,
6691
- offset: page * size
6769
+ order_by: orderBy
6692
6770
  };
6693
6771
 
6694
6772
  // Only add computed_dimensions if there are any
6695
- if (report.computedDimensions && report.computedDimensions.length > 0) {
6696
- queryObj.computed_dimensions = report.computedDimensions.map(cd => ({
6773
+ if (orderedComputedOnly.length > 0) {
6774
+ queryObj.computed_dimensions = orderedComputedOnly.map(cd => ({
6697
6775
  name: cd.name,
6698
6776
  value: cd.value
6699
6777
  }));
6700
6778
  }
6701
6779
 
6780
+ // Persist the unified dimension + computed-dimension order so it can be
6781
+ // reconstructed on reload and so executed reports render columns in the
6782
+ // same order they were arranged in the builder.
6783
+ if (report.dimensionOrder.length > 0) {
6784
+ queryObj.ordered_dimensions = report.dimensionOrder.map(entry => ({
6785
+ type: entry.type,
6786
+ ref: entry.key
6787
+ }));
6788
+ }
6789
+
6702
6790
  // Only add titles if there are any overrides
6703
6791
  if (Object.keys(titles).length > 0) {
6704
6792
  queryObj.titles = titles;
@@ -6708,6 +6796,14 @@ const ReportBuilder = ({
6708
6796
  if (filter) {
6709
6797
  queryObj.filter = filter;
6710
6798
  }
6799
+ return queryObj;
6800
+ };
6801
+
6802
+ // Convert internal report structure to API format
6803
+ const convertReportToApiFormat = (page = 0, size = 50) => {
6804
+ const queryObj = buildQueryObject();
6805
+ queryObj.limit = size;
6806
+ queryObj.offset = page * size;
6711
6807
 
6712
6808
  // Get parameters from context if available, otherwise use default
6713
6809
  const parameters = reportingContext?.parameters || {
@@ -6801,77 +6897,7 @@ const ReportBuilder = ({
6801
6897
  }
6802
6898
  try {
6803
6899
  setSaving(true);
6804
-
6805
- // Build order_by array - include all dimensions, add desc property only when needed
6806
- const orderBy = report.dimensions.map(dim => {
6807
- const orderItem = {
6808
- name: dim.fullPath
6809
- };
6810
- if (dim.sortOrder === "desc") {
6811
- orderItem.desc = true;
6812
- }
6813
- // For 'asc' or null, just include { name: fullPath } without desc property
6814
- return orderItem;
6815
- });
6816
-
6817
- // Build titles object - only include overridden titles
6818
- const titles = {};
6819
- if (Object.keys(titleOverrides.dimensions).length > 0) {
6820
- titles.dimensions = titleOverrides.dimensions;
6821
- }
6822
- if (Object.keys(titleOverrides.metrics).length > 0) {
6823
- titles.metrics = titleOverrides.metrics;
6824
- }
6825
- if (Object.keys(titleOverrides.filters).length > 0) {
6826
- titles.filters = titleOverrides.filters;
6827
- }
6828
- const queryObj = {
6829
- dimensions: report.dimensions.map(dim => dim.fullPath),
6830
- metrics: report.metrics.map(metric => metric.fullPath),
6831
- order_by: orderBy
6832
- };
6833
-
6834
- // Only add computed_dimensions if there are any
6835
- if (report.computedDimensions && report.computedDimensions.length > 0) {
6836
- queryObj.computed_dimensions = report.computedDimensions.map(cd => ({
6837
- name: cd.name,
6838
- value: cd.value
6839
- }));
6840
- }
6841
-
6842
- // Only add titles if there are any overrides
6843
- if (Object.keys(titles).length > 0) {
6844
- queryObj.titles = titles;
6845
- }
6846
-
6847
- // Add filter if there are any - NEW API format with top-level 'and' operator
6848
- // NEW format: { and: [ { "ft.currency": ["USD", "EUR"] }, { "ft_ba.created_at": {gte: "2025-05-01", lte: "2025-12-31"} } ] }
6849
- if (Object.keys(report.filters).length > 0) {
6850
- const conditions = [];
6851
- Object.entries(report.filters).forEach(([fullPath, filterData]) => {
6852
- if (filterData.values) {
6853
- // Check if values is an object (date range) or array (regular filter)
6854
- if (Array.isArray(filterData.values)) {
6855
- // Regular filter - only add if array has values
6856
- if (filterData.values.length > 0) {
6857
- conditions.push({
6858
- [fullPath]: filterData.values
6859
- });
6860
- }
6861
- } else if (typeof filterData.values === "object") {
6862
- // Date range filter - add the object as-is
6863
- conditions.push({
6864
- [fullPath]: filterData.values
6865
- });
6866
- }
6867
- }
6868
- });
6869
- if (conditions.length > 0) {
6870
- queryObj.filter = {
6871
- and: conditions
6872
- };
6873
- }
6874
- }
6900
+ const queryObj = buildQueryObject();
6875
6901
 
6876
6902
  // Build the doc object
6877
6903
  const docObj = {
@@ -6916,6 +6942,18 @@ const ReportBuilder = ({
6916
6942
  }
6917
6943
  };
6918
6944
  const canSaveReport = selectedProvider && reportTitle.trim() && (report.dimensions.length > 0 || report.metrics.length > 0 || report.computedDimensions.length > 0);
6945
+
6946
+ // Resolve the unified dimensionOrder into actual dimension/computed-dimension
6947
+ // objects so the results grid can render columns in that same order.
6948
+ const dimensionByFullPath = Object.fromEntries(report.dimensions.map(d => [d.fullPath, d]));
6949
+ const computedDimensionByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
6950
+ const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
6951
+ kind: "dimension",
6952
+ data: dimensionByFullPath[entry.key]
6953
+ } : {
6954
+ kind: "computed",
6955
+ data: computedDimensionByName[entry.key]
6956
+ }).filter(item => item.data);
6919
6957
  return /*#__PURE__*/React__namespace.default.createElement(material.Box, {
6920
6958
  sx: {
6921
6959
  p: 3,
@@ -7194,7 +7232,7 @@ const ReportBuilder = ({
7194
7232
  savedDimensions: report.dimensions,
7195
7233
  onSaveDimension: handleSaveDimension,
7196
7234
  onRemoveDimension: handleRemoveDimension,
7197
- onReorderDimensions: handleReorderDimensions,
7235
+ onUpdateDimensionSortOrder: handleUpdateDimensionSortOrder,
7198
7236
  titleOverrides: titleOverrides.dimensions,
7199
7237
  onUpdateTitle: handleUpdateDimensionTitle,
7200
7238
  onResetTitle: handleResetDimensionTitle,
@@ -7204,7 +7242,8 @@ const ReportBuilder = ({
7204
7242
  onSaveComputedDimension: handleSaveComputedDimension,
7205
7243
  onUpdateComputedDimension: handleUpdateComputedDimension,
7206
7244
  onRemoveComputedDimension: handleRemoveComputedDimension,
7207
- onReorderComputedDimensions: handleReorderComputedDimensions
7245
+ dimensionOrder: report.dimensionOrder,
7246
+ onReorderDimensionItems: handleReorderDimensionItems
7208
7247
  })), /*#__PURE__*/React__namespace.default.createElement(TabPanel, {
7209
7248
  value: activeTab,
7210
7249
  index: 1
@@ -7240,9 +7279,8 @@ const ReportBuilder = ({
7240
7279
  index: 3
7241
7280
  }, reportData && /*#__PURE__*/React__namespace.default.createElement(ReportDataGrid, {
7242
7281
  reportData: reportData,
7243
- dimensions: report.dimensions,
7282
+ orderedDimensionItems: orderedDimensionItems,
7244
7283
  metrics: report.metrics,
7245
- computedDimensions: report.computedDimensions,
7246
7284
  loading: loading,
7247
7285
  onPageChange: handlePageChange,
7248
7286
  totalRows: totalRows,
@@ -7305,6 +7343,44 @@ const ReportDefinitionsManager = () => {
7305
7343
  });
7306
7344
  };
7307
7345
 
7346
+ // TODO: replace with currencies fetched from the API once that's available.
7347
+ const CURRENCY_OPTIONS = [{
7348
+ key: "USD",
7349
+ value: "USD"
7350
+ }, {
7351
+ key: "EUR",
7352
+ value: "EUR"
7353
+ }, {
7354
+ key: "SEK",
7355
+ value: "SEK"
7356
+ }, {
7357
+ key: "GBP",
7358
+ value: "GBP"
7359
+ }, {
7360
+ key: "DKK",
7361
+ value: "DKK"
7362
+ }, {
7363
+ key: "NOK",
7364
+ value: "NOK"
7365
+ }];
7366
+ const CurrencySelector = () => {
7367
+ const {
7368
+ parameters,
7369
+ setParameters
7370
+ } = useReportingContext();
7371
+ return /*#__PURE__*/React__namespace.default.createElement(SingleSelect, {
7372
+ items: CURRENCY_OPTIONS,
7373
+ value: parameters.base_currency,
7374
+ label: "Currency",
7375
+ onChange: e => setParameters({
7376
+ ...parameters,
7377
+ base_currency: e.target.value
7378
+ }),
7379
+ sx: {
7380
+ width: 110
7381
+ }
7382
+ });
7383
+ };
7308
7384
  const ReportApp = ({
7309
7385
  params,
7310
7386
  api
@@ -7371,7 +7447,13 @@ const ReportApp = ({
7371
7447
  }
7372
7448
  }
7373
7449
  })
7374
- }, /*#__PURE__*/React__namespace.default.createElement(ReportDefinitionsManager, null))));
7450
+ }, /*#__PURE__*/React__namespace.default.createElement(Box__default.default, {
7451
+ sx: {
7452
+ display: "flex",
7453
+ justifyContent: "flex-end",
7454
+ p: 1
7455
+ }
7456
+ }, /*#__PURE__*/React__namespace.default.createElement(CurrencySelector, null)), /*#__PURE__*/React__namespace.default.createElement(ReportDefinitionsManager, null))));
7375
7457
  };
7376
7458
 
7377
7459
  var index = {