From 4e64323e787b02be3ad840a9a72abc0e3e486e79 Mon Sep 17 00:00:00 2001 From: Nishant Bangarwa Date: Thu, 24 Sep 2026 00:17:40 +0530 Subject: [PATCH] fix: click-to-filter converts a Contains dimension filter to Select Click to filter from charts, tables, leaderboards, pivots and search calls toggleValue / appendSelectedValues on the DimensionFilterManager, which only mutated selectedValues. commit() rebuilds the expression from inputText while in Contains mode, so the click was silently dropped and no conversion notification fired. Restores the 0.89 behaviour: the filter converts to Select with the clicked values and the "converted to Select" toast is shown. --- .../filters/ExpressionFilterManager.spec.ts | 100 ++++++++++++++++++ .../DimensionFilterManager.svelte.ts | 14 +++ 2 files changed, 114 insertions(+) diff --git a/web-common/src/features/dashboards/filters/ExpressionFilterManager.spec.ts b/web-common/src/features/dashboards/filters/ExpressionFilterManager.spec.ts index 59991a077b62..ee8d13157544 100644 --- a/web-common/src/features/dashboards/filters/ExpressionFilterManager.spec.ts +++ b/web-common/src/features/dashboards/filters/ExpressionFilterManager.spec.ts @@ -23,6 +23,8 @@ import { AD_BIDS_PUBLISHER_DIMENSION, } from "@rilldata/web-common/features/dashboards/stores/test-data/data"; import { compressUrlParams } from "@rilldata/web-common/features/dashboards/url-state/compression.ts"; +import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus.ts"; +import { m } from "@rilldata/web-common/lib/i18n/gen/messages"; import { ExploreStateURLParams } from "@rilldata/web-common/features/dashboards/url-state/url-params.ts"; import { MetricsViewsProvider } from "@rilldata/web-common/features/metrics-views/providers/MetricsViewsProvider.svelte.ts"; import { @@ -870,6 +872,104 @@ describe("dimensionFilterAction", () => { filterManager.sortedFilterManagers.dimensions[0].selectedValues, ).toEqual(["Google"]); }); + + // Click to filter from a chart, table, leaderboard or search while the dimension has a + // Contains filter. The click selects a concrete value, so the filter converts to Select + // the way it did before the filter managers existed, and the user is told about it. + describe("with a Contains filter applied", () => { + function createWithContainsFilter() { + const filterManager = createFilterManager(); + filterManager.setUrlParams( + perMetricsViewParams({ + [AD_BIDS_METRICS_NAME]: `${AD_BIDS_PUBLISHER_DIMENSION} LIKE '%oo%'`, + }), + ); + const manager = filterManager.sortedFilterManagers.dimensions[0]; + expect(manager.mode).toBe(DimensionFilterMode.Contains); + expect(manager.inputText).toBe("oo"); + + const emit = vi.spyOn(eventBus, "emit"); + cleanups.push(() => emit.mockRestore()); + return { filterManager, manager, emit }; + } + + it("toggleValue converts the filter to Select with the clicked value", () => { + const { filterManager, manager, emit } = createWithContainsFilter(); + + filterManager.dimensionFilterAction( + AD_BIDS_PUBLISHER_DIMENSION, + (manager) => manager.toggleValue("Google", false), + ); + + expect(manager.mode).toBe(DimensionFilterMode.Select); + expect(manager.inputText).toBe(""); + expect(manager.selectedValues).toEqual(["Google"]); + expect(manager.expr).toEqual( + createInExpression(AD_BIDS_PUBLISHER_DIMENSION, ["Google"]), + ); + expect(filterManager.topLevelJoiner.param[AD_BIDS_METRICS_NAME]).toBe( + `${AD_BIDS_PUBLISHER_DIMENSION} IN ('Google')`, + ); + expect(emit).toHaveBeenCalledWith( + "notification", + expect.objectContaining({ message: m.filter_converted_to_select() }), + ); + }); + + it("toggleValue keeps exclude when converting", () => { + const filterManager = createFilterManager(); + filterManager.setUrlParams( + perMetricsViewParams({ + [AD_BIDS_METRICS_NAME]: `${AD_BIDS_PUBLISHER_DIMENSION} NLIKE '%oo%'`, + }), + ); + + filterManager.dimensionFilterAction( + AD_BIDS_PUBLISHER_DIMENSION, + (manager) => manager.toggleValue("Google", false), + ); + + expect(filterManager.topLevelJoiner.param[AD_BIDS_METRICS_NAME]).toBe( + `${AD_BIDS_PUBLISHER_DIMENSION} NIN ('Google')`, + ); + }); + + it("appendSelectedValues converts the filter to Select with the added values", () => { + const { filterManager, manager, emit } = createWithContainsFilter(); + + // The callback return type is `any`, hence the cast. + const added = filterManager.dimensionFilterAction( + AD_BIDS_PUBLISHER_DIMENSION, + (manager) => manager.appendSelectedValues(["Google", "Facebook"]), + ) as string[]; + + expect(added).toEqual(["Google", "Facebook"]); + expect(manager.mode).toBe(DimensionFilterMode.Select); + expect(manager.selectedValues).toEqual(["Google", "Facebook"]); + expect(filterManager.topLevelJoiner.param[AD_BIDS_METRICS_NAME]).toBe( + `${AD_BIDS_PUBLISHER_DIMENSION} IN ('Google','Facebook')`, + ); + expect(emit).toHaveBeenCalledWith( + "notification", + expect.objectContaining({ message: m.filter_converted_to_select() }), + ); + }); + + it("removeSelectedValues leaves the Contains filter alone", () => { + const { filterManager, manager, emit } = createWithContainsFilter(); + + filterManager.dimensionFilterAction( + AD_BIDS_PUBLISHER_DIMENSION, + (manager) => manager.removeSelectedValues(["Google"]), + ); + + expect(manager.mode).toBe(DimensionFilterMode.Contains); + expect(filterManager.topLevelJoiner.param[AD_BIDS_METRICS_NAME]).toBe( + `${AD_BIDS_PUBLISHER_DIMENSION} LIKE '%oo%'`, + ); + expect(emit).not.toHaveBeenCalledWith("notification", expect.anything()); + }); + }); }); describe("clear", () => { diff --git a/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilterManager.svelte.ts b/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilterManager.svelte.ts index fb5b9031014a..586713e0bc8a 100644 --- a/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilterManager.svelte.ts +++ b/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilterManager.svelte.ts @@ -141,6 +141,7 @@ export class DimensionFilterManager { } public toggleValue(dimensionValue: string, isExclusiveFilter: boolean) { + this.leaveContainsMode(); const inIdx = this.selectedValues.findIndex((v) => v === dimensionValue); if (inIdx === -1) { @@ -156,6 +157,7 @@ export class DimensionFilterManager { } public appendSelectedValues(dimensionValues: string[]) { + this.leaveContainsMode(); const newValues = dimensionValues.filter( (v) => !this.selectedValues.includes(v), ); @@ -171,6 +173,18 @@ export class DimensionFilterManager { this.commit(); } + /** + * Click to filter (charts, tables, leaderboards, search) selects concrete values, + * which a Contains filter cannot hold. So the filter converts to Select, + * and `commit` reports the conversion. + * Contains mode keeps no selected values, so the clicked value becomes the whole selection. + */ + private leaveContainsMode() { + if (this.mode !== DimensionFilterMode.Contains) return; + this.mode = DimensionFilterMode.Select; + this.inputText = ""; + } + public setInList(values: string[], exclude: boolean) { this.mode = DimensionFilterMode.InList; this.selectedValues = values;