diff --git a/web-common/src/components/menu/DashboardMetricsDraggableList.svelte b/web-common/src/components/menu/DashboardMetricsDraggableList.svelte
index cee6fafee227..6c623a0d3b01 100644
--- a/web-common/src/components/menu/DashboardMetricsDraggableList.svelte
+++ b/web-common/src/components/menu/DashboardMetricsDraggableList.svelte
@@ -1,7 +1,6 @@
+
+
+ Filter:
+
+ {tagName}
+
+
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+ Clear filter
+
+
+
diff --git a/web-common/src/features/dashboards/leaderboard/LeaderboardControls.svelte b/web-common/src/features/dashboards/leaderboard/LeaderboardControls.svelte
index dd7e18ea7bbe..9702e04f1f74 100644
--- a/web-common/src/features/dashboards/leaderboard/LeaderboardControls.svelte
+++ b/web-common/src/features/dashboards/leaderboard/LeaderboardControls.svelte
@@ -15,6 +15,7 @@
measures: { getMeasureByName, visibleMeasures },
leaderboard: { leaderboardSortByMeasureName, leaderboardMeasureNames },
dimensions: { visibleDimensions, allDimensions },
+ tags: { dimensionTagIndex },
},
actions: {
contextColumn: { setContextColumn },
@@ -78,6 +79,7 @@
onSelectedChange={(items) =>
setDimensionVisibility(items, allDimensionNames)}
allItems={$allDimensions}
+ tagIndex={$dimensionTagIndex}
selectedItems={visibleDimensionsNames}
/>
diff --git a/web-common/src/features/dashboards/pivot/DragList.svelte b/web-common/src/features/dashboards/pivot/DragList.svelte
index 11213ff7882f..31a4b374b59d 100644
--- a/web-common/src/features/dashboards/pivot/DragList.svelte
+++ b/web-common/src/features/dashboards/pivot/DragList.svelte
@@ -26,12 +26,28 @@
} from "@rilldata/web-common/features/dashboards/pivot/time-pill-utils";
import { timePillSelectors } from "./time-pill-store";
- export type Zone = "rows" | "columns" | "Time" | "Measures" | "Dimensions";
+ export type Zone =
+ | "rows"
+ | "columns"
+ | "Time"
+ | "Measures"
+ | "Dimensions"
+ | "tags";
+
+ // When a tag chip is being dragged the drop receivers do a bulk-add instead
+ // of the per-chip splice path. The dimensions and measures arrays are
+ // precomputed at drag-start so each receiver does not have to re-split.
+ export type TagDragPayload = {
+ tagName: string;
+ dimensions: PivotChipData[];
+ measures: PivotChipData[];
+ };
export type DragData = {
source: Zone;
width: number;
chip: PivotChipData;
+ tagPayload?: TagDragPayload;
};
export const dragDataStore = writable(null);
@@ -70,10 +86,14 @@
(i) => i.type !== PivotChipType.Measure,
);
+ $: isTagDrag = !!dragData?.tagPayload;
+
$: isValidDropZone =
isDropLocation &&
dragData &&
- (zone === "columns" || dragChip?.type !== PivotChipType.Measure);
+ (isTagDrag ||
+ zone === "columns" ||
+ dragChip?.type !== PivotChipType.Measure);
// Get available grains from the store
const availableGrainsStore = timePillSelectors.getAvailableGrains("time");
@@ -134,12 +154,33 @@
window.removeEventListener("mousemove", detectDragStart);
}
- function handleDrop() {
+ function handleDrop(e: MouseEvent) {
if (zoneStartedDrag)
$controllerStore?.abort("Drag cancelled - item dropped");
+ // Holding CMD (mac) or Ctrl flips the tag drop from append to replace,
+ // matching the click-side affordance on the tag row.
+ const replace = e.metaKey || e.ctrlKey;
+
if (isValidDropZone) {
- if (dragChip && ghostIndex !== null) {
+ if (dragData?.tagPayload) {
+ // Bulk-add path for tag drops. Skips ghost-index positioning since
+ // we are inserting multiple chips, not a single one. Cross-zone
+ // cleanup on replace happens in the auto-arrange zone or the click
+ // affordances on the tag row — DragList only manages its own zone.
+ const { dimensions, measures } = dragData.tagPayload;
+ const newItems =
+ zone === "rows" ? dimensions : [...dimensions, ...measures];
+ if (newItems.length === 0) {
+ // Pure-measure tag dropped on rows, for instance: nothing to do.
+ } else if (replace) {
+ onUpdate(newItems);
+ } else {
+ const existing = new Set(items.map((c) => c.id));
+ const additions = newItems.filter((c) => !existing.has(c.id));
+ if (additions.length > 0) onUpdate([...items, ...additions]);
+ }
+ } else if (dragChip && ghostIndex !== null) {
const temp = [...items];
let chipToAdd = dragChip;
diff --git a/web-common/src/features/dashboards/pivot/PivotAutoArrangeZone.svelte b/web-common/src/features/dashboards/pivot/PivotAutoArrangeZone.svelte
new file mode 100644
index 000000000000..5ebc37affbe0
--- /dev/null
+++ b/web-common/src/features/dashboards/pivot/PivotAutoArrangeZone.svelte
@@ -0,0 +1,120 @@
+
+
+{#if visible && dragData?.tagPayload}
+
+{/if}
+
+
diff --git a/web-common/src/features/dashboards/pivot/PivotDisplay.svelte b/web-common/src/features/dashboards/pivot/PivotDisplay.svelte
index abbe939868b7..57f1c5a0ffd5 100644
--- a/web-common/src/features/dashboards/pivot/PivotDisplay.svelte
+++ b/web-common/src/features/dashboards/pivot/PivotDisplay.svelte
@@ -26,6 +26,7 @@
validSpecStore,
selectors: {
pivot: { columns, measures, dimensions },
+ tags: { combinedTagIndex, dimensionTagIndex, measureTagIndex },
},
timeRangeSummaryStore,
} = stateManagers;
@@ -100,6 +101,12 @@
pivotState={enrichedPivotState}
measures={$measures}
dimensions={$dimensions}
+ combinedTagIndex={$combinedTagIndex}
+ dimensionTagIndex={$dimensionTagIndex}
+ measureTagIndex={$measureTagIndex}
+ setRows={(rows) => metricsExplorerStore.setPivotRows($exploreName, rows)}
+ setColumns={(columns) =>
+ metricsExplorerStore.setPivotColumns($exploreName, columns)}
{timeControlsForPillActions}
/>
{/if}
diff --git a/web-common/src/features/dashboards/pivot/PivotHeader.svelte b/web-common/src/features/dashboards/pivot/PivotHeader.svelte
index 2277a0034a02..56b09a0b9322 100644
--- a/web-common/src/features/dashboards/pivot/PivotHeader.svelte
+++ b/web-common/src/features/dashboards/pivot/PivotHeader.svelte
@@ -4,6 +4,7 @@
import { splitPivotChips } from "@rilldata/web-common/features/dashboards/pivot/pivot-utils.ts";
import { slide } from "svelte/transition";
import DragList from "./DragList.svelte";
+ import PivotAutoArrangeZone from "./PivotAutoArrangeZone.svelte";
import { lastNestState } from "./PivotToolbar.svelte";
import { PivotChipType, type PivotChipData, type PivotState } from "./types";
@@ -15,6 +16,7 @@
$: splitColumns = splitPivotChips(columns);
$: fullColumns = splitColumns.dimension.concat(splitColumns.measure);
$: isFlat = tableMode === "flat";
+ $: columnsForList = isFlat ? columns : fullColumns;
function updateColumn(items: PivotChipData[]) {
// Reset lastNestState when columns are updated
@@ -32,13 +34,7 @@