@bygd/nc-report-ui 0.1.41 → 0.1.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -77168,6 +77168,140 @@ var ArrowDownwardIcon = createSvgIcon$1(/*#__PURE__*/u("path", {
77168
77168
  d: "m20 12-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8z"
77169
77169
  }));
77170
77170
 
77171
+ // Shared drag-and-drop primitives for the index-identified, arrow-reorderable
77172
+ // lists used across the report builder (Dimensions, Metrics, Column Order).
77173
+ // Previously duplicated per-tab; consolidated here so all three behave and
77174
+ // look identical and only need to be fixed in one place.
77175
+
77176
+ const useSortableListSensors = () => useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
77177
+ coordinateGetter: sortableKeyboardCoordinates
77178
+ }));
77179
+
77180
+ // Builds drag/move handlers for a list where dnd-kit item ids are the array
77181
+ // index. `onReorder` receives the whole reordered list.
77182
+ const makeListReorderHandlers = (list, onReorder) => {
77183
+ const handleDragEnd = ({
77184
+ active,
77185
+ over
77186
+ }) => {
77187
+ if (over && active.id !== over.id) {
77188
+ const oldIndex = list.findIndex((_, i) => i === active.id);
77189
+ const newIndex = list.findIndex((_, i) => i === over.id);
77190
+ onReorder(arrayMove(list, oldIndex, newIndex));
77191
+ }
77192
+ };
77193
+ const handleMoveUp = index => {
77194
+ if (index > 0) {
77195
+ onReorder(arrayMove(list, index, index - 1));
77196
+ }
77197
+ };
77198
+ const handleMoveDown = index => {
77199
+ if (index < list.length - 1) {
77200
+ onReorder(arrayMove(list, index, index + 1));
77201
+ }
77202
+ };
77203
+ return {
77204
+ handleDragEnd,
77205
+ handleMoveUp,
77206
+ handleMoveDown
77207
+ };
77208
+ };
77209
+
77210
+ // Wraps DndContext + SortableContext + the vertical list container.
77211
+ // `itemIds` must line up with the ids passed to each item's useSortable().
77212
+ const SortableListContext = ({
77213
+ sensors,
77214
+ itemIds,
77215
+ onDragEnd,
77216
+ children
77217
+ }) => /*#__PURE__*/gn.createElement(DndContext, {
77218
+ sensors: sensors,
77219
+ collisionDetection: closestCenter,
77220
+ onDragEnd: onDragEnd
77221
+ }, /*#__PURE__*/gn.createElement(SortableContext, {
77222
+ items: itemIds,
77223
+ strategy: verticalListSortingStrategy
77224
+ }, /*#__PURE__*/gn.createElement(Box, {
77225
+ sx: {
77226
+ display: "flex",
77227
+ flexDirection: "column",
77228
+ gap: 1
77229
+ }
77230
+ }, children)));
77231
+
77232
+ // Per-row useSortable() wiring shared by every sortable chip/row component.
77233
+ const useSortableRow = id => {
77234
+ const {
77235
+ attributes,
77236
+ listeners,
77237
+ setNodeRef,
77238
+ transform,
77239
+ transition,
77240
+ isDragging
77241
+ } = useSortable({
77242
+ id
77243
+ });
77244
+ const style = {
77245
+ transform: CSS$1.Transform.toString(transform),
77246
+ transition,
77247
+ opacity: isDragging ? 0.5 : 1,
77248
+ display: "flex",
77249
+ alignItems: "center",
77250
+ width: "100%"
77251
+ };
77252
+ return {
77253
+ attributes,
77254
+ listeners,
77255
+ setNodeRef,
77256
+ style
77257
+ };
77258
+ };
77259
+ const DragHandle = ({
77260
+ listeners
77261
+ }) => /*#__PURE__*/gn.createElement(Box, _extends({}, listeners, {
77262
+ sx: {
77263
+ display: "flex",
77264
+ alignItems: "center",
77265
+ cursor: "grab",
77266
+ "&:active": {
77267
+ cursor: "grabbing"
77268
+ }
77269
+ }
77270
+ }), /*#__PURE__*/gn.createElement(DragIndicatorIcon, {
77271
+ sx: {
77272
+ cursor: "grab",
77273
+ color: "rgba(110, 110, 110, 0.62)"
77274
+ }
77275
+ }));
77276
+ const MoveButtons = ({
77277
+ onMoveUp,
77278
+ onMoveDown,
77279
+ isFirst,
77280
+ isLast
77281
+ }) => /*#__PURE__*/gn.createElement(Box, {
77282
+ className: "hover-icons",
77283
+ sx: {
77284
+ display: "flex",
77285
+ gap: 0.5,
77286
+ opacity: 0,
77287
+ transition: "opacity 0.2s"
77288
+ }
77289
+ }, /*#__PURE__*/gn.createElement(IconButton, {
77290
+ size: "small",
77291
+ onClick: onMoveUp,
77292
+ disabled: isFirst,
77293
+ "aria-label": "move up"
77294
+ }, /*#__PURE__*/gn.createElement(ArrowUpwardIcon, {
77295
+ fontSize: "small"
77296
+ })), /*#__PURE__*/gn.createElement(IconButton, {
77297
+ size: "small",
77298
+ onClick: onMoveDown,
77299
+ disabled: isLast,
77300
+ "aria-label": "move down"
77301
+ }, /*#__PURE__*/gn.createElement(ArrowDownwardIcon, {
77302
+ fontSize: "small"
77303
+ })));
77304
+
77171
77305
  var SortIcon = createSvgIcon$1(/*#__PURE__*/u("path", {
77172
77306
  d: "M3 18h6v-2H3zM3 6v2h18V6zm0 7h12v-2H3z"
77173
77307
  }));
@@ -77203,6 +77337,50 @@ const interpolateTitle = (template, params) => {
77203
77337
  });
77204
77338
  };
77205
77339
 
77340
+ // Resolves the final column display order for the results grid: dimensions,
77341
+ // computed dimensions, and metrics combined into a single list.
77342
+ //
77343
+ // `columnOrder` is the optional, explicitly-arranged order (raw entries
77344
+ // `{ type: 'dimension' | 'computed' | 'metric', key }`) set on the Column
77345
+ // Order tab. When present, its entries come first, in that order. Anything
77346
+ // not covered by it (new fields added since it was last touched, or when
77347
+ // it's empty/undefined because no custom order has ever been defined) is
77348
+ // appended in the pre-existing default order: dimensions/computed (in their
77349
+ // own `dimensionOrder`) first, then metrics (in their own array order) —
77350
+ // this keeps behavior unchanged for reports that never define a column order.
77351
+ const resolveColumnOrder = (columnOrder, orderedDimensionItems, metrics) => {
77352
+ // Normalise every candidate item to a uniform { kind, key, data } shape
77353
+ // up front, so callers (e.g. the Column Order tab) can always read
77354
+ // `.key` regardless of where the item came from.
77355
+ const normalizedDimensionItems = orderedDimensionItems.map(item => ({
77356
+ kind: item.kind,
77357
+ key: item.kind === 'computed' ? item.data.name : item.data.fullPath,
77358
+ data: item.data
77359
+ }));
77360
+ const normalizedMetricItems = metrics.map(m => ({
77361
+ kind: 'metric',
77362
+ key: m.fullPath,
77363
+ data: m
77364
+ }));
77365
+ const byKey = Object.fromEntries([...normalizedDimensionItems, ...normalizedMetricItems].map(item => [`${item.kind}:${item.key}`, item]));
77366
+ const resolved = [];
77367
+ const seen = new Set();
77368
+ (columnOrder || []).forEach(entry => {
77369
+ const seenKey = `${entry.type}:${entry.key}`;
77370
+ if (seen.has(seenKey) || !byKey[seenKey]) return;
77371
+ resolved.push(byKey[seenKey]);
77372
+ seen.add(seenKey);
77373
+ });
77374
+ [...normalizedDimensionItems, ...normalizedMetricItems].forEach(item => {
77375
+ const seenKey = `${item.kind}:${item.key}`;
77376
+ if (!seen.has(seenKey)) {
77377
+ resolved.push(item);
77378
+ seen.add(seenKey);
77379
+ }
77380
+ });
77381
+ return resolved;
77382
+ };
77383
+
77206
77384
  // Sortable Chip Component
77207
77385
  const SortableChip$1 = ({
77208
77386
  id,
@@ -77231,20 +77409,8 @@ const SortableChip$1 = ({
77231
77409
  attributes,
77232
77410
  listeners,
77233
77411
  setNodeRef,
77234
- transform,
77235
- transition,
77236
- isDragging
77237
- } = useSortable({
77238
- id
77239
- });
77240
- const style = {
77241
- transform: CSS$1.Transform.toString(transform),
77242
- transition,
77243
- opacity: isDragging ? 0.5 : 1,
77244
- display: "flex",
77245
- alignItems: "center",
77246
- width: "100%"
77247
- };
77412
+ style
77413
+ } = useSortableRow(id);
77248
77414
 
77249
77415
  // Focus input when entering edit mode
77250
77416
  h(() => {
@@ -77353,21 +77519,9 @@ const SortableChip$1 = ({
77353
77519
  opacity: 1 // show icons on hover
77354
77520
  }
77355
77521
  }
77356
- }, /*#__PURE__*/gn.createElement(Box, _extends({}, listeners, {
77357
- sx: {
77358
- display: "flex",
77359
- alignItems: "center",
77360
- cursor: "grab",
77361
- "&:active": {
77362
- cursor: "grabbing"
77363
- }
77364
- }
77365
- }), /*#__PURE__*/gn.createElement(DragIndicatorIcon, {
77366
- sx: {
77367
- cursor: "grab",
77368
- color: "rgba(110, 110, 110, 0.62)"
77369
- }
77370
- })), !isEditing ? /*#__PURE__*/gn.createElement(gn.Fragment, null, /*#__PURE__*/gn.createElement(Box, {
77522
+ }, /*#__PURE__*/gn.createElement(DragHandle, {
77523
+ listeners: listeners
77524
+ }), !isEditing ? /*#__PURE__*/gn.createElement(gn.Fragment, null, /*#__PURE__*/gn.createElement(Box, {
77371
77525
  sx: {
77372
77526
  minWidth: 0
77373
77527
  }
@@ -77430,29 +77584,12 @@ const SortableChip$1 = ({
77430
77584
  sx: {
77431
77585
  flex: 1
77432
77586
  }
77433
- }), /*#__PURE__*/gn.createElement(Box, {
77434
- className: "hover-icons",
77435
- sx: {
77436
- display: "flex",
77437
- gap: 0.5,
77438
- opacity: 0,
77439
- transition: "opacity 0.2s"
77440
- }
77441
- }, /*#__PURE__*/gn.createElement(IconButton, {
77442
- size: "small",
77443
- onClick: onMoveUp,
77444
- disabled: isFirst,
77445
- "aria-label": "move up"
77446
- }, /*#__PURE__*/gn.createElement(ArrowUpwardIcon, {
77447
- fontSize: "small"
77448
- })), /*#__PURE__*/gn.createElement(IconButton, {
77449
- size: "small",
77450
- onClick: onMoveDown,
77451
- disabled: isLast,
77452
- "aria-label": "move down"
77453
- }, /*#__PURE__*/gn.createElement(ArrowDownwardIcon, {
77454
- fontSize: "small"
77455
- })))) : /*#__PURE__*/gn.createElement(gn.Fragment, null, /*#__PURE__*/gn.createElement(TextField, {
77587
+ }), /*#__PURE__*/gn.createElement(MoveButtons, {
77588
+ onMoveUp: onMoveUp,
77589
+ onMoveDown: onMoveDown,
77590
+ isFirst: isFirst,
77591
+ isLast: isLast
77592
+ })) : /*#__PURE__*/gn.createElement(gn.Fragment, null, /*#__PURE__*/gn.createElement(TextField, {
77456
77593
  inputRef: inputRef,
77457
77594
  value: editValue,
77458
77595
  onChange: e => setEditValue(e.target.value),
@@ -77528,24 +77665,12 @@ const SortableComputedChip = ({
77528
77665
  attributes,
77529
77666
  listeners,
77530
77667
  setNodeRef,
77531
- transform,
77532
- transition,
77533
- isDragging
77534
- } = useSortable({
77535
- id
77536
- });
77668
+ style
77669
+ } = useSortableRow(id);
77537
77670
  const [isEditing, setIsEditing] = d(false);
77538
77671
  const [editName, setEditName] = d("");
77539
77672
  const [editValue, setEditValue] = d("");
77540
77673
  const containerRef = A$1(null);
77541
- const style = {
77542
- transform: CSS$1.Transform.toString(transform),
77543
- transition,
77544
- opacity: isDragging ? 0.5 : 1,
77545
- display: "flex",
77546
- alignItems: "center",
77547
- width: "100%"
77548
- };
77549
77674
 
77550
77675
  // Handle click outside to cancel
77551
77676
  h(() => {
@@ -77608,21 +77733,9 @@ const SortableComputedChip = ({
77608
77733
  opacity: 1
77609
77734
  }
77610
77735
  }
77611
- }, /*#__PURE__*/gn.createElement(Box, _extends({}, listeners, {
77612
- sx: {
77613
- display: "flex",
77614
- alignItems: "center",
77615
- cursor: "grab",
77616
- "&:active": {
77617
- cursor: "grabbing"
77618
- }
77619
- }
77620
- }), /*#__PURE__*/gn.createElement(DragIndicatorIcon, {
77621
- sx: {
77622
- cursor: "grab",
77623
- color: "rgba(110, 110, 110, 0.62)"
77624
- }
77625
- })), !isEditing ? /*#__PURE__*/gn.createElement(gn.Fragment, null, /*#__PURE__*/gn.createElement(Chip, {
77736
+ }, /*#__PURE__*/gn.createElement(DragHandle, {
77737
+ listeners: listeners
77738
+ }), !isEditing ? /*#__PURE__*/gn.createElement(gn.Fragment, null, /*#__PURE__*/gn.createElement(Chip, {
77626
77739
  icon: /*#__PURE__*/gn.createElement(FunctionsIcon, {
77627
77740
  sx: {
77628
77741
  fontSize: "15px !important"
@@ -77686,29 +77799,12 @@ const SortableComputedChip = ({
77686
77799
  sx: {
77687
77800
  flex: 1
77688
77801
  }
77689
- }), /*#__PURE__*/gn.createElement(Box, {
77690
- className: "hover-icons",
77691
- sx: {
77692
- display: "flex",
77693
- gap: 0.5,
77694
- opacity: 0,
77695
- transition: "opacity 0.2s"
77696
- }
77697
- }, /*#__PURE__*/gn.createElement(IconButton, {
77698
- size: "small",
77699
- onClick: onMoveUp,
77700
- disabled: isFirst,
77701
- "aria-label": "move up"
77702
- }, /*#__PURE__*/gn.createElement(ArrowUpwardIcon, {
77703
- fontSize: "small"
77704
- })), /*#__PURE__*/gn.createElement(IconButton, {
77705
- size: "small",
77706
- onClick: onMoveDown,
77707
- disabled: isLast,
77708
- "aria-label": "move down"
77709
- }, /*#__PURE__*/gn.createElement(ArrowDownwardIcon, {
77710
- fontSize: "small"
77711
- })))) : /*#__PURE__*/gn.createElement(gn.Fragment, null, /*#__PURE__*/gn.createElement(TextField, {
77802
+ }), /*#__PURE__*/gn.createElement(MoveButtons, {
77803
+ onMoveUp: onMoveUp,
77804
+ onMoveDown: onMoveDown,
77805
+ isFirst: isFirst,
77806
+ isLast: isLast
77807
+ })) : /*#__PURE__*/gn.createElement(gn.Fragment, null, /*#__PURE__*/gn.createElement(TextField, {
77712
77808
  value: editName,
77713
77809
  onChange: e => setEditName(e.target.value),
77714
77810
  onKeyDown: handleKeyDown,
@@ -77793,9 +77889,7 @@ const Dimensions = ({
77793
77889
  const [computedValue, setComputedValue] = d("");
77794
77890
 
77795
77891
  // Setup drag and drop sensors
77796
- const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
77797
- coordinateGetter: sortableKeyboardCoordinates
77798
- }));
77892
+ const sensors = useSortableListSensors();
77799
77893
 
77800
77894
  // Resolve the unified dimensionOrder into the actual dimension / computed
77801
77895
  // dimension objects, so dimensions and computed dimensions can be shown
@@ -77812,32 +77906,12 @@ const Dimensions = ({
77812
77906
  data: computedDimensionByName[entry.key]
77813
77907
  }).filter(item => item.data);
77814
77908
 
77815
- // Handle drag end for the unified list
77816
- const handleDragEnd = event => {
77817
- const {
77818
- active,
77819
- over
77820
- } = event;
77821
- if (over && active.id !== over.id) {
77822
- const oldIndex = dimensionOrder.findIndex((_, i) => i === active.id);
77823
- const newIndex = dimensionOrder.findIndex((_, i) => i === over.id);
77824
- onReorderDimensionItems(arrayMove(dimensionOrder, oldIndex, newIndex));
77825
- }
77826
- };
77827
-
77828
- // Handle move up
77829
- const handleMoveUp = index => {
77830
- if (index > 0) {
77831
- onReorderDimensionItems(arrayMove(dimensionOrder, index, index - 1));
77832
- }
77833
- };
77834
-
77835
- // Handle move down
77836
- const handleMoveDown = index => {
77837
- if (index < dimensionOrder.length - 1) {
77838
- onReorderDimensionItems(arrayMove(dimensionOrder, index, index + 1));
77839
- }
77840
- };
77909
+ // Handle drag/move for the unified list
77910
+ const {
77911
+ handleDragEnd,
77912
+ handleMoveUp,
77913
+ handleMoveDown
77914
+ } = makeListReorderHandlers(dimensionOrder, onReorderDimensionItems);
77841
77915
 
77842
77916
  // Handle sort order change (dimensions only - computed dimensions have no order_by)
77843
77917
  const handleSortOrderChange = (fullPath, sortOrder) => {
@@ -78330,19 +78404,10 @@ const Dimensions = ({
78330
78404
  color: "#666",
78331
78405
  marginBottom: 2
78332
78406
  }
78333
- }, "Drag to reorder or use arrows"), /*#__PURE__*/gn.createElement(DndContext, {
78407
+ }, "Drag to reorder or use arrows"), /*#__PURE__*/gn.createElement(SortableListContext, {
78334
78408
  sensors: sensors,
78335
- collisionDetection: closestCenter,
78409
+ itemIds: orderedItems.map((_, index) => index),
78336
78410
  onDragEnd: handleDragEnd
78337
- }, /*#__PURE__*/gn.createElement(SortableContext, {
78338
- items: orderedItems.map((_, index) => index),
78339
- strategy: verticalListSortingStrategy
78340
- }, /*#__PURE__*/gn.createElement(Box, {
78341
- sx: {
78342
- display: "flex",
78343
- flexDirection: "column",
78344
- gap: 1
78345
- }
78346
78411
  }, orderedItems.map((item, index) => item.kind === "dimension" ? /*#__PURE__*/gn.createElement(SortableChip$1, {
78347
78412
  key: `dim-${item.key}`,
78348
78413
  id: index,
@@ -78372,7 +78437,7 @@ const Dimensions = ({
78372
78437
  onMoveDown: () => handleMoveDown(index),
78373
78438
  isFirst: index === 0,
78374
78439
  isLast: index === orderedItems.length - 1
78375
- })))))));
78440
+ })))));
78376
78441
  };
78377
78442
 
78378
78443
  var StorageIcon = createSvgIcon$1(/*#__PURE__*/u("path", {
@@ -78459,20 +78524,8 @@ const SortableChip = ({
78459
78524
  attributes,
78460
78525
  listeners,
78461
78526
  setNodeRef,
78462
- transform,
78463
- transition,
78464
- isDragging
78465
- } = useSortable({
78466
- id
78467
- });
78468
- const style = {
78469
- transform: CSS$1.Transform.toString(transform),
78470
- transition,
78471
- opacity: isDragging ? 0.5 : 1,
78472
- display: 'flex',
78473
- alignItems: 'center',
78474
- width: '100%'
78475
- };
78527
+ style
78528
+ } = useSortableRow(id);
78476
78529
 
78477
78530
  // Focus input when entering edit mode
78478
78531
  h(() => {
@@ -78550,21 +78603,9 @@ const SortableChip = ({
78550
78603
  opacity: 1 // show icons on hover
78551
78604
  }
78552
78605
  }
78553
- }, /*#__PURE__*/gn.createElement(Box, _extends({}, listeners, {
78554
- sx: {
78555
- display: "flex",
78556
- alignItems: "center",
78557
- cursor: "grab",
78558
- "&:active": {
78559
- cursor: "grabbing"
78560
- }
78561
- }
78562
- }), /*#__PURE__*/gn.createElement(DragIndicatorIcon, {
78563
- sx: {
78564
- cursor: "grab",
78565
- color: "rgba(110, 110, 110, 0.62)"
78566
- }
78567
- })), !isEditing ? /*#__PURE__*/gn.createElement(gn.Fragment, null, source && /*#__PURE__*/gn.createElement(Box, {
78606
+ }, /*#__PURE__*/gn.createElement(DragHandle, {
78607
+ listeners: listeners
78608
+ }), !isEditing ? /*#__PURE__*/gn.createElement(gn.Fragment, null, source && /*#__PURE__*/gn.createElement(Box, {
78568
78609
  sx: {
78569
78610
  display: 'flex',
78570
78611
  alignItems: 'center',
@@ -78626,29 +78667,12 @@ const SortableChip = ({
78626
78667
  sx: {
78627
78668
  flex: 1
78628
78669
  }
78629
- }), /*#__PURE__*/gn.createElement(Box, {
78630
- className: "hover-icons",
78631
- sx: {
78632
- display: "flex",
78633
- gap: 0.5,
78634
- opacity: 0,
78635
- transition: "opacity 0.2s"
78636
- }
78637
- }, /*#__PURE__*/gn.createElement(IconButton, {
78638
- size: "small",
78639
- onClick: onMoveUp,
78640
- disabled: isFirst,
78641
- "aria-label": "move up"
78642
- }, /*#__PURE__*/gn.createElement(ArrowUpwardIcon, {
78643
- fontSize: "small"
78644
- })), /*#__PURE__*/gn.createElement(IconButton, {
78645
- size: "small",
78646
- onClick: onMoveDown,
78647
- disabled: isLast,
78648
- "aria-label": "move down"
78649
- }, /*#__PURE__*/gn.createElement(ArrowDownwardIcon, {
78650
- fontSize: "small"
78651
- })))) : /*#__PURE__*/gn.createElement(gn.Fragment, null, /*#__PURE__*/gn.createElement(TextField, {
78670
+ }), /*#__PURE__*/gn.createElement(MoveButtons, {
78671
+ onMoveUp: onMoveUp,
78672
+ onMoveDown: onMoveDown,
78673
+ isFirst: isFirst,
78674
+ isLast: isLast
78675
+ })) : /*#__PURE__*/gn.createElement(gn.Fragment, null, /*#__PURE__*/gn.createElement(TextField, {
78652
78676
  inputRef: inputRef,
78653
78677
  value: editValue,
78654
78678
  onChange: e => setEditValue(e.target.value),
@@ -78723,39 +78747,14 @@ const Metrics = ({
78723
78747
  const [selectedMetric, setSelectedMetric] = d('');
78724
78748
 
78725
78749
  // Setup drag and drop sensors
78726
- const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
78727
- coordinateGetter: sortableKeyboardCoordinates
78728
- }));
78729
-
78730
- // Handle drag end
78731
- const handleDragEnd = event => {
78732
- const {
78733
- active,
78734
- over
78735
- } = event;
78736
- if (over && active.id !== over.id) {
78737
- const oldIndex = savedMetrics.findIndex((_, i) => i === active.id);
78738
- const newIndex = savedMetrics.findIndex((_, i) => i === over.id);
78739
- const newOrder = arrayMove(savedMetrics, oldIndex, newIndex);
78740
- onReorderMetrics(newOrder);
78741
- }
78742
- };
78743
-
78744
- // Handle move up
78745
- const handleMoveUp = index => {
78746
- if (index > 0) {
78747
- const newOrder = arrayMove(savedMetrics, index, index - 1);
78748
- onReorderMetrics(newOrder);
78749
- }
78750
- };
78750
+ const sensors = useSortableListSensors();
78751
78751
 
78752
- // Handle move down
78753
- const handleMoveDown = index => {
78754
- if (index < savedMetrics.length - 1) {
78755
- const newOrder = arrayMove(savedMetrics, index, index + 1);
78756
- onReorderMetrics(newOrder);
78757
- }
78758
- };
78752
+ // Handle drag/move
78753
+ const {
78754
+ handleDragEnd,
78755
+ handleMoveUp,
78756
+ handleMoveDown
78757
+ } = makeListReorderHandlers(savedMetrics, onReorderMetrics);
78759
78758
 
78760
78759
  // Get the current provider based on selection chain
78761
78760
  const getCurrentProvider = () => {
@@ -79094,19 +79093,10 @@ const Metrics = ({
79094
79093
  marginBottom: 1,
79095
79094
  color: "rgb(37, 37, 37)"
79096
79095
  }
79097
- }, "Saved Metrics"), /*#__PURE__*/gn.createElement(DndContext, {
79096
+ }, "Saved Metrics"), /*#__PURE__*/gn.createElement(SortableListContext, {
79098
79097
  sensors: sensors,
79099
- collisionDetection: closestCenter,
79098
+ itemIds: savedMetrics.map((_, index) => index),
79100
79099
  onDragEnd: handleDragEnd
79101
- }, /*#__PURE__*/gn.createElement(SortableContext, {
79102
- items: savedMetrics.map((_, index) => index),
79103
- strategy: verticalListSortingStrategy
79104
- }, /*#__PURE__*/gn.createElement(Box, {
79105
- sx: {
79106
- display: 'flex',
79107
- flexDirection: 'column',
79108
- gap: 1
79109
- }
79110
79100
  }, savedMetrics.map((metric, index) => /*#__PURE__*/gn.createElement(SortableChip, {
79111
79101
  key: index,
79112
79102
  id: index,
@@ -79123,7 +79113,7 @@ const Metrics = ({
79123
79113
  onUpdateTitle: onUpdateTitle,
79124
79114
  onResetTitle: onResetTitle,
79125
79115
  source: metric.metric?.source
79126
- })))))));
79116
+ })))));
79127
79117
  };
79128
79118
 
79129
79119
  var FilterAltIcon = createSvgIcon$1(/*#__PURE__*/u("path", {
@@ -91686,6 +91676,209 @@ const Filters = ({
91686
91676
  }))));
91687
91677
  };
91688
91678
 
91679
+ var CalculateIcon = createSvgIcon$1(/*#__PURE__*/u("path", {
91680
+ d: "M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2m-5.97 4.06L14.09 6l1.41 1.41L16.91 6l1.06 1.06-1.41 1.41 1.41 1.41-1.06 1.06-1.41-1.4-1.41 1.41-1.06-1.06 1.41-1.41zm-6.78.66h5v1.5h-5zM11.5 16h-2v2H8v-2H6v-1.5h2v-2h1.5v2h2zm6.5 1.25h-5v-1.5h5zm0-2.5h-5v-1.5h5z"
91681
+ }));
91682
+
91683
+ const KIND_LABEL = {
91684
+ dimension: "Dimension",
91685
+ computed: "Computed",
91686
+ metric: "Metric"
91687
+ };
91688
+ const KindChip = ({
91689
+ kind,
91690
+ source
91691
+ }) => {
91692
+ if (kind === "metric") {
91693
+ return /*#__PURE__*/gn.createElement(Box, {
91694
+ sx: {
91695
+ display: "flex",
91696
+ alignItems: "center",
91697
+ flexShrink: 0
91698
+ }
91699
+ }, /*#__PURE__*/gn.createElement(MetricSourceIcon, {
91700
+ source: source
91701
+ }));
91702
+ }
91703
+ return /*#__PURE__*/gn.createElement(Chip, {
91704
+ icon: kind === "computed" ? /*#__PURE__*/gn.createElement(CalculateIcon, {
91705
+ sx: {
91706
+ fontSize: "15px !important"
91707
+ }
91708
+ }) : undefined,
91709
+ label: KIND_LABEL[kind],
91710
+ size: "small",
91711
+ sx: {
91712
+ fontWeight: 500,
91713
+ backgroundColor: "rgba(70, 134, 128, 0.1)",
91714
+ color: "rgb(70, 134, 128)",
91715
+ flexShrink: 0
91716
+ }
91717
+ });
91718
+ };
91719
+
91720
+ // Read-only sortable row: this tab only reorders columns. Adding, removing,
91721
+ // and renaming stay on the Dimensions/Metrics tabs so there's one place each
91722
+ // field is managed.
91723
+ const SortableColumnRow = ({
91724
+ id,
91725
+ label,
91726
+ fullPath,
91727
+ kind,
91728
+ source,
91729
+ onMoveUp,
91730
+ onMoveDown,
91731
+ isFirst,
91732
+ isLast
91733
+ }) => {
91734
+ const reportingContext = useReportingContextOptional();
91735
+ const parameters = reportingContext?.parameters;
91736
+ const {
91737
+ attributes,
91738
+ listeners,
91739
+ setNodeRef,
91740
+ style
91741
+ } = useSortableRow(id);
91742
+ const displayLabel = interpolateTitle(label, parameters);
91743
+ return /*#__PURE__*/gn.createElement("div", _extends({
91744
+ ref: setNodeRef,
91745
+ style: style
91746
+ }, attributes), /*#__PURE__*/gn.createElement(Box, {
91747
+ sx: {
91748
+ display: "flex",
91749
+ alignItems: "center",
91750
+ width: "100%",
91751
+ gap: 1,
91752
+ backgroundColor: "white",
91753
+ border: "1px solid #e0e0e0",
91754
+ borderRadius: 2,
91755
+ padding: 1,
91756
+ transition: "transform 0.2s ease, box-shadow 0.2s ease",
91757
+ "&:hover .hover-icons": {
91758
+ opacity: 1
91759
+ }
91760
+ }
91761
+ }, /*#__PURE__*/gn.createElement(DragHandle, {
91762
+ listeners: listeners
91763
+ }), /*#__PURE__*/gn.createElement(KindChip, {
91764
+ kind: kind,
91765
+ source: source
91766
+ }), /*#__PURE__*/gn.createElement(Tooltip, {
91767
+ title: fullPath,
91768
+ arrow: true,
91769
+ placement: "top"
91770
+ }, /*#__PURE__*/gn.createElement(Typography, {
91771
+ variant: "h6",
91772
+ sx: {
91773
+ fontWeight: 500,
91774
+ color: "#1a1a1a",
91775
+ fontSize: "14px",
91776
+ whiteSpace: "nowrap",
91777
+ overflow: "hidden",
91778
+ textOverflow: "ellipsis"
91779
+ }
91780
+ }, displayLabel)), /*#__PURE__*/gn.createElement(Box, {
91781
+ sx: {
91782
+ flex: 1
91783
+ }
91784
+ }), /*#__PURE__*/gn.createElement(MoveButtons, {
91785
+ onMoveUp: onMoveUp,
91786
+ onMoveDown: onMoveDown,
91787
+ isFirst: isFirst,
91788
+ isLast: isLast
91789
+ })));
91790
+ };
91791
+ const ColumnOrder = ({
91792
+ orderedDimensionItems = [],
91793
+ metrics = [],
91794
+ columnOrder = [],
91795
+ onReorderColumnItems,
91796
+ titleOverrides = {
91797
+ dimensions: {},
91798
+ metrics: {}
91799
+ }
91800
+ }) => {
91801
+ const isCustomized = columnOrder.length > 0;
91802
+ const resolvedItems = resolveColumnOrder(columnOrder, orderedDimensionItems, metrics);
91803
+ const sensors = useSortableListSensors();
91804
+ const reorderResolved = newResolvedItems => {
91805
+ onReorderColumnItems(newResolvedItems.map(item => ({
91806
+ type: item.kind,
91807
+ key: item.key
91808
+ })));
91809
+ };
91810
+ const {
91811
+ handleDragEnd,
91812
+ handleMoveUp,
91813
+ handleMoveDown
91814
+ } = makeListReorderHandlers(resolvedItems, reorderResolved);
91815
+ const labelFor = item => {
91816
+ if (item.kind === "computed") return item.data.name;
91817
+ if (item.kind === "metric") {
91818
+ return titleOverrides.metrics[item.key] || item.data.metricTitle || item.key;
91819
+ }
91820
+ return titleOverrides.dimensions[item.key] || item.data.dimensionTitle || item.key;
91821
+ };
91822
+ return /*#__PURE__*/gn.createElement("div", null, /*#__PURE__*/gn.createElement(Box, {
91823
+ sx: {
91824
+ display: "flex",
91825
+ justifyContent: "space-between",
91826
+ alignItems: "flex-start",
91827
+ mb: 2,
91828
+ gap: 2
91829
+ }
91830
+ }, /*#__PURE__*/gn.createElement(Typography, {
91831
+ sx: {
91832
+ fontSize: "13px",
91833
+ color: "#666"
91834
+ }
91835
+ }, "Optional: define a single order for all columns (dimensions, computed dimensions and metrics together) in the results grid. Drag to reorder or use the arrows. Newly added dimensions/metrics are appended to the end automatically. Without a custom order, columns follow the Dimensions tab order, then the Metrics tab order."), isCustomized && /*#__PURE__*/gn.createElement(Tooltip, {
91836
+ title: "Discard the custom order and go back to the default (Dimensions, then Metrics)",
91837
+ arrow: true
91838
+ }, /*#__PURE__*/gn.createElement(Button, {
91839
+ variant: "outlined",
91840
+ size: "small",
91841
+ startIcon: /*#__PURE__*/gn.createElement(RestartAltIcon, null),
91842
+ onClick: () => onReorderColumnItems([]),
91843
+ sx: {
91844
+ height: "36px",
91845
+ flexShrink: 0,
91846
+ fontFamily: "system-ui",
91847
+ fontSize: "13px",
91848
+ fontWeight: 500,
91849
+ borderRadius: "8px",
91850
+ boxShadow: "none",
91851
+ textTransform: "none",
91852
+ borderColor: "#e0e0e0",
91853
+ color: "rgb(37, 37, 37)",
91854
+ "&:hover": {
91855
+ backgroundColor: "#f5f5f5",
91856
+ borderColor: "#d0d0d0"
91857
+ }
91858
+ }
91859
+ }, "Reset to Default"))), resolvedItems.length === 0 ? /*#__PURE__*/gn.createElement(Typography, {
91860
+ sx: {
91861
+ fontSize: "13px",
91862
+ color: "#999"
91863
+ }
91864
+ }, "Add dimensions or metrics in the other tabs first.") : /*#__PURE__*/gn.createElement(SortableListContext, {
91865
+ sensors: sensors,
91866
+ itemIds: resolvedItems.map((_, index) => index),
91867
+ onDragEnd: handleDragEnd
91868
+ }, resolvedItems.map((item, index) => /*#__PURE__*/gn.createElement(SortableColumnRow, {
91869
+ key: `${item.kind}-${item.key}`,
91870
+ id: index,
91871
+ label: labelFor(item),
91872
+ fullPath: item.kind === "computed" ? item.data.value : item.key,
91873
+ kind: item.kind,
91874
+ source: item.kind === "metric" ? item.data.metric?.source : undefined,
91875
+ onMoveUp: () => handleMoveUp(index),
91876
+ onMoveDown: () => handleMoveDown(index),
91877
+ isFirst: index === 0,
91878
+ isLast: index === resolvedItems.length - 1
91879
+ }))));
91880
+ };
91881
+
91689
91882
  // Default numeral.js formats applied when a dimension/metric definition has a
91690
91883
  // known type but no explicit `format` set on the provider.
91691
91884
  const DEFAULT_FORMATS_BY_TYPE = {
@@ -91700,10 +91893,107 @@ const formatValue = (value, def) => {
91700
91893
  return def?.prefix ? `${def.prefix} ${formatted}` : formatted;
91701
91894
  };
91702
91895
  const isNumericType = type => type === 'integer' || type === 'currency';
91896
+
91897
+ // Builds a single DataGrid column definition for one resolved column item
91898
+ // (a dimension, computed dimension, or metric). Field naming logic:
91899
+ // - Dimensions from base provider: baseAlias.field -> baseAlias_field
91900
+ // - Dimensions from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
91901
+ // - Computed dimensions have no provider/relations chain, so the API returns
91902
+ // each row keyed by the computed dimension's own `name`.
91903
+ // - Metrics: API returns metric keys in different forms depending on path depth
91904
+ // (see comment inline below), normalised to match the row-key normalisation
91905
+ // done when building `rows`.
91906
+ const buildColumn = (item, titleOverrides, parameters) => {
91907
+ if (item.kind === 'computed') {
91908
+ const cd = item.data;
91909
+ return {
91910
+ field: cd.name,
91911
+ headerName: cd.name,
91912
+ flex: 1,
91913
+ minWidth: 150,
91914
+ type: 'string'
91915
+ };
91916
+ }
91917
+ if (item.kind === 'metric') {
91918
+ const metric = item.data;
91919
+ const metricDef = metric.metric;
91920
+ let fieldName;
91921
+ const dotCount = (metric.fullPath.match(/\./g) || []).length;
91922
+ if (dotCount >= 2) {
91923
+ // 3+-part namespaced path: API returns the full dotted path as the key.
91924
+ // Normalise dots to underscores to match the row key normalisation below.
91925
+ // Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
91926
+ fieldName = metric.fullPath.replace(/\./g, '_');
91927
+ } else if (metric.relations && metric.relations.length > 0) {
91928
+ // 2-part path from a nested provider: API drops the base provider alias.
91929
+ // Example: "mc_fa.total_amount" -> "fa_total_amount"
91930
+ const parts = metric.fullPath.split('.');
91931
+ const pathWithoutField = parts[0]; // e.g., "mc_fa"
91932
+ const field = parts[1]; // e.g., "total_amount"
91933
+
91934
+ const pathParts = pathWithoutField.split('_');
91935
+ pathParts.shift(); // remove base provider alias
91936
+ const pathWithoutBase = pathParts.join('_');
91937
+ fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
91938
+ } else {
91939
+ // 2-part path from the base provider: API returns just the metric name.
91940
+ // Example: "mc.record_count" -> "record_count"
91941
+ fieldName = metric.metricName;
91942
+ }
91943
+ const headerName = interpolateTitle(titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName, parameters);
91944
+ return {
91945
+ field: fieldName,
91946
+ headerName: headerName,
91947
+ flex: 1,
91948
+ minWidth: 150,
91949
+ type: isNumericType(metricDef?.type) ? 'number' : 'string',
91950
+ renderHeader: () => /*#__PURE__*/gn.createElement(Box, {
91951
+ sx: {
91952
+ display: 'flex',
91953
+ alignItems: 'center',
91954
+ gap: 0.5
91955
+ }
91956
+ }, /*#__PURE__*/gn.createElement(MetricSourceIcon, {
91957
+ source: metricDef?.source
91958
+ }), /*#__PURE__*/gn.createElement("span", null, headerName)),
91959
+ valueFormatter: value => formatValue(value, metricDef)
91960
+ };
91961
+ }
91962
+
91963
+ // Dimension
91964
+ const dim = item.data;
91965
+ let fieldName;
91966
+ if (dim.relations && dim.relations.length > 0) {
91967
+ // From nested provider: drop the base provider alias
91968
+ // Example: ft_fa_db.currency -> fa_db_currency
91969
+ const parts = dim.fullPath.split('.');
91970
+ const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
91971
+ const field = parts[1]; // e.g., "currency"
91972
+
91973
+ const pathParts = pathWithoutField.split('_');
91974
+ pathParts.shift(); // Remove base provider alias
91975
+ const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
91976
+
91977
+ fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
91978
+ } else {
91979
+ // From base provider: keep the full path with underscore
91980
+ // Example: ba.created_at -> ba_created_at
91981
+ fieldName = dim.fullPath.replace('.', '_');
91982
+ }
91983
+ const headerName = interpolateTitle(titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName, parameters);
91984
+ const dimDef = dim.dimension;
91985
+ return {
91986
+ field: fieldName,
91987
+ headerName: headerName,
91988
+ flex: 1,
91989
+ minWidth: 150,
91990
+ type: isNumericType(dimDef?.type) ? 'number' : 'string',
91991
+ valueFormatter: value => formatValue(value, dimDef)
91992
+ };
91993
+ };
91703
91994
  const ReportDataGrid = ({
91704
91995
  reportData,
91705
- orderedDimensionItems = [],
91706
- metrics,
91996
+ columnItems = [],
91707
91997
  loading,
91708
91998
  onPageChange,
91709
91999
  totalRows = 0,
@@ -91719,127 +92009,10 @@ const ReportDataGrid = ({
91719
92009
  pageSize: 50
91720
92010
  });
91721
92011
 
91722
- // Generate columns dynamically from dimensions and metrics
91723
- const columns = gn.useMemo(() => {
91724
- const cols = [];
91725
-
91726
- // Add dimension + computed-dimension columns, in the single unified
91727
- // order the user arranged them in on the Dimensions tab.
91728
- // Field naming logic (dimensions):
91729
- // - If from base provider: baseAlias.field -> baseAlias_field
91730
- // - If from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
91731
- // Computed dimensions have no provider/relations chain, so the API
91732
- // returns each row keyed by the computed dimension's own `name`.
91733
- orderedDimensionItems.forEach(item => {
91734
- if (item.kind === 'computed') {
91735
- const cd = item.data;
91736
- cols.push({
91737
- field: cd.name,
91738
- headerName: cd.name,
91739
- flex: 1,
91740
- minWidth: 150,
91741
- type: 'string'
91742
- });
91743
- return;
91744
- }
91745
- const dim = item.data;
91746
- let fieldName;
91747
- let headerName;
91748
-
91749
- // Check if there are relations (nested providers)
91750
- if (dim.relations && dim.relations.length > 0) {
91751
- // From nested provider: drop the base provider alias
91752
- // Example: ft_fa_db.currency -> fa_db_currency
91753
- const parts = dim.fullPath.split('.');
91754
- const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
91755
- const field = parts[1]; // e.g., "currency"
91756
-
91757
- // Remove the base provider alias (first part before first underscore)
91758
- const pathParts = pathWithoutField.split('_');
91759
- pathParts.shift(); // Remove base provider alias
91760
- const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
91761
-
91762
- fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
91763
- } else {
91764
- // From base provider: keep the full path with underscore
91765
- // Example: ba.created_at -> ba_created_at
91766
- fieldName = dim.fullPath.replace('.', '_');
91767
- }
91768
-
91769
- // Check for title override, otherwise use the friendly dimension title from provider
91770
- headerName = titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName;
91771
- headerName = interpolateTitle(headerName, parameters);
91772
- const dimDef = dim.dimension;
91773
- cols.push({
91774
- field: fieldName,
91775
- headerName: headerName,
91776
- flex: 1,
91777
- minWidth: 150,
91778
- type: isNumericType(dimDef?.type) ? 'number' : 'string',
91779
- valueFormatter: value => formatValue(value, dimDef)
91780
- });
91781
- });
91782
-
91783
- // Add metric columns
91784
- // The API returns metric keys in two different forms depending on the path depth:
91785
- //
91786
- // • 2-part path "mc.record_count" → API key: "record_count"
91787
- // • 2-part path "mc_fa.total_amount" (nested) → API key: "fa_total_amount"
91788
- // • 3+-part path "mc.dkpi.activeAgreementsCount"→ API key: "mc.dkpi.activeAgreementsCount" (dots)
91789
- //
91790
- // For 3+-part paths the API key contains dots. MUI DataGrid interprets dots
91791
- // in field names as nested-object accessors, so the rows useMemo normalises
91792
- // those keys (dots → underscores). The column field must match that normalised key.
91793
- metrics.forEach(metric => {
91794
- const metricDef = metric.metric;
91795
- let fieldName;
91796
- let headerName;
91797
- const dotCount = (metric.fullPath.match(/\./g) || []).length;
91798
- if (dotCount >= 2) {
91799
- // 3+-part namespaced path: API returns the full dotted path as the key.
91800
- // Normalise dots to underscores to match the row key normalisation below.
91801
- // Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
91802
- fieldName = metric.fullPath.replace(/\./g, '_');
91803
- } else if (metric.relations && metric.relations.length > 0) {
91804
- // 2-part path from a nested provider: API drops the base provider alias.
91805
- // Example: "mc_fa.total_amount" -> "fa_total_amount"
91806
- const parts = metric.fullPath.split('.');
91807
- const pathWithoutField = parts[0]; // e.g., "mc_fa"
91808
- const field = parts[1]; // e.g., "total_amount"
91809
-
91810
- const pathParts = pathWithoutField.split('_');
91811
- pathParts.shift(); // remove base provider alias
91812
- const pathWithoutBase = pathParts.join('_');
91813
- fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
91814
- } else {
91815
- // 2-part path from the base provider: API returns just the metric name.
91816
- // Example: "mc.record_count" -> "record_count"
91817
- fieldName = metric.metricName;
91818
- }
91819
-
91820
- // Check for title override, otherwise use the friendly metric title from provider
91821
- headerName = titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName;
91822
- headerName = interpolateTitle(headerName, parameters);
91823
- cols.push({
91824
- field: fieldName,
91825
- headerName: headerName,
91826
- flex: 1,
91827
- minWidth: 150,
91828
- type: isNumericType(metricDef?.type) ? 'number' : 'string',
91829
- renderHeader: () => /*#__PURE__*/gn.createElement(Box, {
91830
- sx: {
91831
- display: 'flex',
91832
- alignItems: 'center',
91833
- gap: 0.5
91834
- }
91835
- }, /*#__PURE__*/gn.createElement(MetricSourceIcon, {
91836
- source: metricDef?.source
91837
- }), /*#__PURE__*/gn.createElement("span", null, headerName)),
91838
- valueFormatter: value => formatValue(value, metricDef)
91839
- });
91840
- });
91841
- return cols;
91842
- }, [orderedDimensionItems, metrics, titleOverrides, parameters]);
92012
+ // Generate columns dynamically, in the final resolved column order
92013
+ // (custom Column Order tab arrangement when defined, otherwise
92014
+ // dimensions/computed then metrics).
92015
+ const columns = gn.useMemo(() => columnItems.map(item => buildColumn(item, titleOverrides, parameters)), [columnItems, titleOverrides, parameters]);
91843
92016
 
91844
92017
  // Transform report data to rows with unique IDs.
91845
92018
  // Keys are normalised by replacing dots with underscores so that MUI DataGrid
@@ -91949,7 +92122,12 @@ const ReportBuilder = ({
91949
92122
  computedDimensions: [],
91950
92123
  // Unified display/execution order across dimensions + computedDimensions.
91951
92124
  // Entries: { type: 'dimension', key: fullPath } | { type: 'computed', key: name }
91952
- dimensionOrder: []
92125
+ dimensionOrder: [],
92126
+ // Optional unified display order across dimensions + computedDimensions +
92127
+ // metrics, set on the Column Order tab. Empty means "not defined" — the
92128
+ // results grid falls back to dimensionOrder followed by metrics order.
92129
+ // Entries: { type: 'dimension' | 'computed' | 'metric', key }
92130
+ columnOrder: []
91953
92131
  });
91954
92132
  const [titleOverrides, setTitleOverrides] = d({
91955
92133
  dimensions: {},
@@ -92180,6 +92358,46 @@ const ReportBuilder = ({
92180
92358
  });
92181
92359
  return dimensionOrder;
92182
92360
  };
92361
+
92362
+ // Build the optional unified column order (dimensions + computed
92363
+ // dimensions + metrics) from the persisted `ordered_columns` field.
92364
+ // Unlike buildDimensionOrder, entries that no longer resolve are simply
92365
+ // dropped rather than backfilled — an empty/partial result just means "no
92366
+ // custom order defined (yet)", which resolveColumnOrder treats as
92367
+ // "fall back to the default order".
92368
+ const buildColumnOrder = (rawOrderedColumns, dimensions, computedDimensions, metrics) => {
92369
+ if (!Array.isArray(rawOrderedColumns)) return [];
92370
+ const dimensionPaths = new Set(dimensions.map(d => d.fullPath));
92371
+ const computedNames = new Set(computedDimensions.map(cd => cd.name));
92372
+ const metricPaths = new Set(metrics.map(m => m.fullPath));
92373
+ const columnOrder = [];
92374
+ rawOrderedColumns.forEach(entry => {
92375
+ if (entry.type === "dimension" && dimensionPaths.has(entry.ref)) {
92376
+ columnOrder.push({
92377
+ type: "dimension",
92378
+ key: entry.ref
92379
+ });
92380
+ } else if (entry.type === "computed" && computedNames.has(entry.ref)) {
92381
+ columnOrder.push({
92382
+ type: "computed",
92383
+ key: entry.ref
92384
+ });
92385
+ } else if (entry.type === "metric" && metricPaths.has(entry.ref)) {
92386
+ columnOrder.push({
92387
+ type: "metric",
92388
+ key: entry.ref
92389
+ });
92390
+ }
92391
+ });
92392
+ return columnOrder;
92393
+ };
92394
+
92395
+ // Auto-append a newly added dimension/computed dimension/metric to the end
92396
+ // of columnOrder — but only once a custom order actually exists. While
92397
+ // columnOrder is empty (no custom order defined) it's left untouched, so
92398
+ // the results grid keeps falling back to the default dimensions-then-
92399
+ // metrics order until the user visits the Column Order tab.
92400
+ const appendToColumnOrder = (columnOrder, entry) => columnOrder.length > 0 ? [...columnOrder, entry] : columnOrder;
92183
92401
  const loadReportDefinition = async id => {
92184
92402
  try {
92185
92403
  console.log("Loading report definition:", id);
@@ -92276,6 +92494,7 @@ const ReportBuilder = ({
92276
92494
  // provider/relation chain, so they're loaded as-is (no reconstruction needed)
92277
92495
  const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
92278
92496
  const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
92497
+ const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
92279
92498
 
92280
92499
  // Set the report state
92281
92500
  setReport({
@@ -92283,7 +92502,8 @@ const ReportBuilder = ({
92283
92502
  metrics: reconstructedMetrics,
92284
92503
  filters: loadedFilters,
92285
92504
  computedDimensions,
92286
- dimensionOrder
92505
+ dimensionOrder,
92506
+ columnOrder
92287
92507
  });
92288
92508
 
92289
92509
  // Set title overrides
@@ -92393,6 +92613,7 @@ const ReportBuilder = ({
92393
92613
  // provider/relation chain, so they're loaded as-is (no reconstruction needed)
92394
92614
  const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
92395
92615
  const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
92616
+ const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
92396
92617
 
92397
92618
  // Set the report state
92398
92619
  setReport({
@@ -92400,7 +92621,8 @@ const ReportBuilder = ({
92400
92621
  metrics: reconstructedMetrics,
92401
92622
  filters: loadedFilters,
92402
92623
  computedDimensions,
92403
- dimensionOrder
92624
+ dimensionOrder,
92625
+ columnOrder
92404
92626
  });
92405
92627
 
92406
92628
  // Set title overrides
@@ -92434,7 +92656,8 @@ const ReportBuilder = ({
92434
92656
  metrics: [],
92435
92657
  filters: {},
92436
92658
  computedDimensions: [],
92437
- dimensionOrder: []
92659
+ dimensionOrder: [],
92660
+ columnOrder: []
92438
92661
  });
92439
92662
  // Reset title overrides
92440
92663
  setTitleOverrides({
@@ -92515,7 +92738,11 @@ const ReportBuilder = ({
92515
92738
  dimensionOrder: [...prev.dimensionOrder, {
92516
92739
  type: "dimension",
92517
92740
  key: dimensionData.fullPath
92518
- }]
92741
+ }],
92742
+ columnOrder: appendToColumnOrder(prev.columnOrder, {
92743
+ type: "dimension",
92744
+ key: dimensionData.fullPath
92745
+ })
92519
92746
  };
92520
92747
  console.log("Dimension saved:", dimensionData);
92521
92748
  console.log("Complete report:", newReport);
@@ -92526,7 +92753,8 @@ const ReportBuilder = ({
92526
92753
  setReport(prev => ({
92527
92754
  ...prev,
92528
92755
  dimensions: prev.dimensions.filter(d => d.fullPath !== fullPath),
92529
- dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath))
92756
+ dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath)),
92757
+ columnOrder: prev.columnOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath))
92530
92758
  }));
92531
92759
  };
92532
92760
  const handleUpdateDimensionSortOrder = (fullPath, sortOrder) => {
@@ -92555,7 +92783,11 @@ const ReportBuilder = ({
92555
92783
  dimensionOrder: [...prev.dimensionOrder, {
92556
92784
  type: "computed",
92557
92785
  key: computedDimensionData.name
92558
- }]
92786
+ }],
92787
+ columnOrder: appendToColumnOrder(prev.columnOrder, {
92788
+ type: "computed",
92789
+ key: computedDimensionData.name
92790
+ })
92559
92791
  }));
92560
92792
  };
92561
92793
  const handleUpdateComputedDimension = (name, computedDimensionData) => {
@@ -92565,6 +92797,10 @@ const ReportBuilder = ({
92565
92797
  dimensionOrder: prev.dimensionOrder.map(entry => entry.type === "computed" && entry.key === name ? {
92566
92798
  ...entry,
92567
92799
  key: computedDimensionData.name
92800
+ } : entry),
92801
+ columnOrder: prev.columnOrder.map(entry => entry.type === "computed" && entry.key === name ? {
92802
+ ...entry,
92803
+ key: computedDimensionData.name
92568
92804
  } : entry)
92569
92805
  }));
92570
92806
  };
@@ -92572,14 +92808,19 @@ const ReportBuilder = ({
92572
92808
  setReport(prev => ({
92573
92809
  ...prev,
92574
92810
  computedDimensions: prev.computedDimensions.filter(cd => cd.name !== name),
92575
- dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
92811
+ dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name)),
92812
+ columnOrder: prev.columnOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
92576
92813
  }));
92577
92814
  };
92578
92815
  const handleSaveMetric = metricData => {
92579
92816
  setReport(prev => {
92580
92817
  const newReport = {
92581
92818
  ...prev,
92582
- metrics: [...prev.metrics, metricData]
92819
+ metrics: [...prev.metrics, metricData],
92820
+ columnOrder: appendToColumnOrder(prev.columnOrder, {
92821
+ type: "metric",
92822
+ key: metricData.fullPath
92823
+ })
92583
92824
  };
92584
92825
  console.log("Metric saved:", metricData);
92585
92826
  console.log("Complete report:", newReport);
@@ -92587,15 +92828,29 @@ const ReportBuilder = ({
92587
92828
  });
92588
92829
  };
92589
92830
  const handleRemoveMetric = index => {
92831
+ setReport(prev => {
92832
+ const fullPath = prev.metrics[index]?.fullPath;
92833
+ return {
92834
+ ...prev,
92835
+ metrics: prev.metrics.filter((_, i) => i !== index),
92836
+ columnOrder: prev.columnOrder.filter(entry => !(entry.type === "metric" && entry.key === fullPath))
92837
+ };
92838
+ });
92839
+ };
92840
+ const handleReorderMetrics = newOrder => {
92590
92841
  setReport(prev => ({
92591
92842
  ...prev,
92592
- metrics: prev.metrics.filter((_, i) => i !== index)
92843
+ metrics: newOrder
92593
92844
  }));
92594
92845
  };
92595
- const handleReorderMetrics = newOrder => {
92846
+
92847
+ // Sets the unified column order shown on the Column Order tab. An empty
92848
+ // array (e.g. via "Reset to Default") clears the customization, and the
92849
+ // results grid falls back to dimensionOrder followed by metrics order.
92850
+ const handleReorderColumnItems = newColumnOrder => {
92596
92851
  setReport(prev => ({
92597
92852
  ...prev,
92598
- metrics: newOrder
92853
+ columnOrder: newColumnOrder
92599
92854
  }));
92600
92855
  };
92601
92856
  const handleSaveFilter = (fullPath, filterData) => {
@@ -92720,6 +92975,17 @@ const ReportBuilder = ({
92720
92975
  }));
92721
92976
  }
92722
92977
 
92978
+ // Persist the optional unified column order (Column Order tab) so the
92979
+ // results grid can reproduce it on reload. Absent when no custom order
92980
+ // has been defined, so old/untouched reports keep their current
92981
+ // dimensions-then-metrics column order unchanged.
92982
+ if (report.columnOrder.length > 0) {
92983
+ queryObj.ordered_columns = report.columnOrder.map(entry => ({
92984
+ type: entry.type,
92985
+ ref: entry.key
92986
+ }));
92987
+ }
92988
+
92723
92989
  // Only add titles if there are any overrides
92724
92990
  if (Object.keys(titles).length > 0) {
92725
92991
  queryObj.titles = titles;
@@ -92779,8 +93045,8 @@ const ReportBuilder = ({
92779
93045
  const handleRunReport = async () => {
92780
93046
  setCurrentPage(0);
92781
93047
  await runReportWithPagination(0, pageSize);
92782
- // Switch to Results tab after running report (now index 3 after adding Filters tab)
92783
- setActiveTab(3);
93048
+ // Switch to Results tab after running report (index 4: Dimensions, Metrics, Filters, Column Order, Results)
93049
+ setActiveTab(4);
92784
93050
  };
92785
93051
  const handleDownloadReport = async () => {
92786
93052
  try {
@@ -92887,6 +93153,11 @@ const ReportBuilder = ({
92887
93153
  kind: "computed",
92888
93154
  data: computedDimensionByName[entry.key]
92889
93155
  }).filter(item => item.data);
93156
+
93157
+ // Final column list for the results grid: the explicit Column Order tab
93158
+ // arrangement when defined, otherwise dimensions/computed (in their own
93159
+ // order) followed by metrics (in their own order) — today's behavior.
93160
+ const columnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
92890
93161
  return /*#__PURE__*/gn.createElement(Box, {
92891
93162
  sx: {
92892
93163
  p: 3,
@@ -93141,9 +93412,36 @@ const ReportBuilder = ({
93141
93412
  }
93142
93413
  }
93143
93414
  }), /*#__PURE__*/gn.createElement(Tab, {
93144
- label: reportData ? "Results" : "Results (Run report first)",
93415
+ label: /*#__PURE__*/gn.createElement(Badge, {
93416
+ variant: "dot",
93417
+ invisible: report.columnOrder.length === 0,
93418
+ sx: {
93419
+ "& .MuiBadge-dot": {
93420
+ backgroundColor: "rgb(70, 134, 128)"
93421
+ }
93422
+ }
93423
+ }, /*#__PURE__*/gn.createElement("span", {
93424
+ style: {
93425
+ marginRight: report.columnOrder.length > 0 ? "8px" : "0"
93426
+ }
93427
+ }, "Column Order")),
93145
93428
  id: "report-tab-3",
93146
93429
  "aria-controls": "report-tabpanel-3",
93430
+ sx: {
93431
+ height: "41px",
93432
+ fontFamily: "system-ui",
93433
+ borderRadius: "0.5rem",
93434
+ boxShadow: "none",
93435
+ textTransform: "none",
93436
+ color: "rgb(37, 37, 37)",
93437
+ "&.Mui-selected": {
93438
+ color: "rgb(70, 134, 128)"
93439
+ }
93440
+ }
93441
+ }), /*#__PURE__*/gn.createElement(Tab, {
93442
+ label: reportData ? "Results" : "Results (Run report first)",
93443
+ id: "report-tab-4",
93444
+ "aria-controls": "report-tabpanel-4",
93147
93445
  disabled: !reportData,
93148
93446
  sx: {
93149
93447
  height: "41px",
@@ -93210,10 +93508,18 @@ const ReportBuilder = ({
93210
93508
  })), /*#__PURE__*/gn.createElement(TabPanel, {
93211
93509
  value: activeTab,
93212
93510
  index: 3
93213
- }, reportData && /*#__PURE__*/gn.createElement(ReportDataGrid, {
93214
- reportData: reportData,
93511
+ }, /*#__PURE__*/gn.createElement(ColumnOrder, {
93215
93512
  orderedDimensionItems: orderedDimensionItems,
93216
93513
  metrics: report.metrics,
93514
+ columnOrder: report.columnOrder,
93515
+ onReorderColumnItems: handleReorderColumnItems,
93516
+ titleOverrides: titleOverrides
93517
+ })), /*#__PURE__*/gn.createElement(TabPanel, {
93518
+ value: activeTab,
93519
+ index: 4
93520
+ }, reportData && /*#__PURE__*/gn.createElement(ReportDataGrid, {
93521
+ reportData: reportData,
93522
+ columnItems: columnItems,
93217
93523
  loading: loading,
93218
93524
  onPageChange: handlePageChange,
93219
93525
  totalRows: totalRows,
@@ -93301,6 +93607,12 @@ const CurrencySelector = () => {
93301
93607
  parameters,
93302
93608
  setParameters
93303
93609
  } = useReportingContext();
93610
+
93611
+ // Hidden by default; only shown when a `cur` query parameter is present in the top-level URL.
93612
+ const showCurrencySelector = new URLSearchParams(window.location.search).has("cur");
93613
+ if (!showCurrencySelector) {
93614
+ return null;
93615
+ }
93304
93616
  return /*#__PURE__*/gn.createElement(SingleSelect, {
93305
93617
  items: CURRENCY_OPTIONS,
93306
93618
  value: parameters.base_currency,