From 914bce893593e430a1ce6e2dddab340d41d34e5a Mon Sep 17 00:00:00 2001
From: Uladzimir Havenchyk <125459446+uladzimirdev@users.noreply.github.com>
Date: Mon, 9 Oct 2023 14:08:18 +0300
Subject: [PATCH] [Dashboard] Do not lose filters state on navigating back from
 Question (#34399)

---
 ...ack-navigation-preserve-filters.cy.spec.js | 120 ++++++++++++++++++
 .../dashboard/actions/data-fetching.js        |   2 +-
 .../components/Dashboard/Dashboard.jsx        |   2 +-
 .../containers/DashboardApp/DashboardApp.jsx  |   4 +-
 .../metabase/dashboard/hoc/DashboardData.jsx  |   4 +-
 frontend/src/metabase/dashboard/reducers.js   |   7 +-
 .../metabase/dashboard/reducers.unit.spec.js  |  44 ++++++-
 .../metabase/dashboard/selectors/selectors.js |   2 +-
 .../FilterApplyButton/FilterApplyButton.tsx   |   3 +-
 .../components/FilterApplyButton/index.ts     |   3 +-
 .../components/EmbedFrame/EmbedFrame.tsx      |   2 +-
 11 files changed, 177 insertions(+), 16 deletions(-)
 create mode 100644 e2e/test/scenarios/dashboard/reproductions/34382-dashboard-back-navigation-preserve-filters.cy.spec.js

diff --git a/e2e/test/scenarios/dashboard/reproductions/34382-dashboard-back-navigation-preserve-filters.cy.spec.js b/e2e/test/scenarios/dashboard/reproductions/34382-dashboard-back-navigation-preserve-filters.cy.spec.js
new file mode 100644
index 00000000000..595e62e0f83
--- /dev/null
+++ b/e2e/test/scenarios/dashboard/reproductions/34382-dashboard-back-navigation-preserve-filters.cy.spec.js
@@ -0,0 +1,120 @@
+import {
+  dashboardParametersContainer,
+  getDashboardCard,
+  restore,
+  visitDashboard,
+  filterWidget,
+  queryBuilderHeader,
+  popover,
+} from "e2e/support/helpers";
+import { PRODUCTS, PRODUCTS_ID } from "metabase-types/api/mocks/presets";
+
+describe("issue 34382", () => {
+  beforeEach(() => {
+    restore();
+    cy.signInAsAdmin();
+
+    cy.intercept("POST", "/api/dashboard/*/dashcard/*/card/*/query").as(
+      "dashcardQuery",
+    );
+  });
+
+  it("should preserve filter value when navigating between the dashboard and the query builder with auto-apply disabled (metabase#34382)", () => {
+    createDashboardWithCards();
+    cy.get("@dashboardId").then(visitDashboard);
+
+    addFilterValue("Gizmo");
+    applyFilter();
+
+    cy.log("Navigate to Products question");
+    getDashboardCard().findByText("Products").click();
+
+    cy.log("Navigate back to dashboard");
+    queryBuilderHeader()
+      .findByLabelText("Back to Products in a dashboard")
+      .click();
+
+    cy.location("search").should("eq", "?category=Gizmo");
+    filterWidget().contains("Gizmo");
+
+    getDashboardCard().within(() => {
+      // only products with category "Gizmo" are filtered
+      cy.findAllByTestId("table-row").should("have.length", 8);
+      cy.findAllByText("Gizmo").should("have.length", 8);
+    });
+  });
+});
+
+const createDashboardWithCards = () => {
+  const getParameterMapping = ({ card_id }, parameters) => ({
+    parameter_mappings: parameters.map(parameter => ({
+      card_id,
+      parameter_id: parameter.id,
+      target: ["dimension", ["field", PRODUCTS.CATEGORY, null]],
+    })),
+  });
+
+  const questionDetails = {
+    name: "Products",
+    query: { "source-table": PRODUCTS_ID },
+  };
+
+  const questionDashcardDetails = {
+    row: 0,
+    col: 0,
+    size_x: 8,
+    size_y: 8,
+    visualization_settings: {},
+  };
+  const filterDetails = {
+    name: "Product Category",
+    slug: "category",
+    id: "96917421",
+    type: "category",
+  };
+
+  const dashboardDetails = {
+    name: "Products in a dashboard",
+    auto_apply_filters: false,
+    parameters: [filterDetails],
+  };
+
+  cy.createDashboard(dashboardDetails).then(
+    ({ body: { id: dashboard_id } }) => {
+      cy.createQuestion(questionDetails).then(
+        ({ body: { id: question_id } }) => {
+          cy.request("PUT", `/api/dashboard/${dashboard_id}/cards`, {
+            cards: [
+              {
+                id: -1,
+                card_id: question_id,
+                ...questionDashcardDetails,
+                ...getParameterMapping({ card_id: question_id }, [
+                  filterDetails,
+                ]),
+              },
+            ],
+          });
+        },
+      );
+
+      cy.wrap(dashboard_id).as("dashboardId");
+    },
+  );
+};
+
+function addFilterValue(value) {
+  filterWidget().click();
+  popover().within(() => {
+    cy.findByText(value).click();
+    cy.findByRole("button", { name: "Add filter" }).click();
+  });
+}
+
+function applyFilter() {
+  dashboardParametersContainer()
+    .findByRole("button", { name: "Apply" })
+    .click();
+
+  cy.wait("@dashcardQuery");
+}
diff --git a/frontend/src/metabase/dashboard/actions/data-fetching.js b/frontend/src/metabase/dashboard/actions/data-fetching.js
index 1c761e7b880..7b8377d0418 100644
--- a/frontend/src/metabase/dashboard/actions/data-fetching.js
+++ b/frontend/src/metabase/dashboard/actions/data-fetching.js
@@ -268,7 +268,7 @@ export const fetchCardData = createThunkAction(
         },
       });
 
-      // If the dataset_query was filtered then we don't have permisison to view this card, so
+      // If the dataset_query was filtered then we don't have permission to view this card, so
       // shortcircuit and return a fake 403
       if (!card.dataset_query) {
         return {
diff --git a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.jsx b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.jsx
index b3383a35aff..671f71f2a9e 100644
--- a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.jsx
+++ b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.jsx
@@ -7,7 +7,7 @@ import { getMainElement } from "metabase/lib/dom";
 
 import DashboardHeader from "metabase/dashboard/containers/DashboardHeader";
 import SyncedParametersList from "metabase/parameters/components/SyncedParametersList/SyncedParametersList";
-import FilterApplyButton from "metabase/parameters/components/FilterApplyButton";
+import { FilterApplyButton } from "metabase/parameters/components/FilterApplyButton";
 import { getVisibleParameters } from "metabase/parameters/utils/ui";
 import DashboardControls from "metabase/dashboard/hoc/DashboardControls";
 import { getValuePopulatedParameters } from "metabase-lib/parameters/utils/parameter-values";
diff --git a/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.jsx b/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.jsx
index 474a2d778ee..59b64e8deda 100644
--- a/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.jsx
@@ -61,7 +61,7 @@ import {
   getSlowCards,
   getIsAutoApplyFilters,
   getSelectedTabId,
-  getisNavigatingBackToDashboard,
+  getIsNavigatingBackToDashboard,
 } from "../../selectors";
 import { DASHBOARD_SLOW_TIMEOUT } from "../../constants";
 
@@ -106,7 +106,7 @@ const mapStateToProps = state => {
     embedOptions: getEmbedOptions(state),
     selectedTabId: getSelectedTabId(state),
     isAutoApplyFilters: getIsAutoApplyFilters(state),
-    isNavigatingBackToDashboard: getisNavigatingBackToDashboard(state),
+    isNavigatingBackToDashboard: getIsNavigatingBackToDashboard(state),
   };
 };
 
diff --git a/frontend/src/metabase/dashboard/hoc/DashboardData.jsx b/frontend/src/metabase/dashboard/hoc/DashboardData.jsx
index 728ce44a12b..d4a77416842 100644
--- a/frontend/src/metabase/dashboard/hoc/DashboardData.jsx
+++ b/frontend/src/metabase/dashboard/hoc/DashboardData.jsx
@@ -12,7 +12,7 @@ import {
   getSlowCards,
   getParameters,
   getParameterValues,
-  getisNavigatingBackToDashboard,
+  getIsNavigatingBackToDashboard,
   getSelectedTabId,
 } from "metabase/dashboard/selectors";
 
@@ -26,7 +26,7 @@ const mapStateToProps = (state, props) => {
     slowCards: getSlowCards(state, props),
     parameters: getParameters(state, props),
     parameterValues: getParameterValues(state, props),
-    isNavigatingBackToDashboard: getisNavigatingBackToDashboard(state),
+    isNavigatingBackToDashboard: getIsNavigatingBackToDashboard(state),
   };
 };
 
diff --git a/frontend/src/metabase/dashboard/reducers.js b/frontend/src/metabase/dashboard/reducers.js
index 0ebbc23a856..693cd2e32c4 100644
--- a/frontend/src/metabase/dashboard/reducers.js
+++ b/frontend/src/metabase/dashboard/reducers.js
@@ -338,7 +338,11 @@ const parameterValues = handleActions(
 
 const draftParameterValues = handleActions(
   {
-    [INITIALIZE]: { next: () => ({}) },
+    [INITIALIZE]: {
+      next: (state, { payload: { clearCache = true } = {} }) => {
+        return clearCache ? {} : state;
+      },
+    },
     [FETCH_DASHBOARD]: {
       next: (
         state,
@@ -358,7 +362,6 @@ const draftParameterValues = handleActions(
     [REMOVE_PARAMETER]: {
       next: (state, { payload: { id } }) => dissoc(state, id),
     },
-    [RESET]: { next: () => ({}) },
   },
   {},
 );
diff --git a/frontend/src/metabase/dashboard/reducers.unit.spec.js b/frontend/src/metabase/dashboard/reducers.unit.spec.js
index e1ebbc35cef..5d72de849d2 100644
--- a/frontend/src/metabase/dashboard/reducers.unit.spec.js
+++ b/frontend/src/metabase/dashboard/reducers.unit.spec.js
@@ -102,10 +102,50 @@ describe("dashboard reducers", () => {
         ),
       ).toEqual({ ...initState, isEditing: null });
     });
+
+    it("should return unchanged state if `clearCache: false` passed", () => {
+      expect(
+        reducer(
+          {
+            ...initState,
+            draftParameterValues: {
+              "60bca071": ["Gadget", "Doohickey", "Gizmo"],
+            },
+          },
+          {
+            type: INITIALIZE,
+            payload: {
+              clearCache: false,
+            },
+          },
+        ),
+      ).toEqual({
+        ...initState,
+        draftParameterValues: {
+          "60bca071": ["Gadget", "Doohickey", "Gizmo"],
+        },
+      });
+    });
+
+    it("should reset state if `clearCache`: false` is not passed", () => {
+      expect(
+        reducer(
+          {
+            ...initState,
+            draftParameterValues: {
+              "60bca071": ["Gadget", "Doohickey", "Gizmo"],
+            },
+          },
+          {
+            type: INITIALIZE,
+          },
+        ),
+      ).toEqual(initState);
+    });
   });
 
   describe("SET_EDITING_DASHBOARD", () => {
-    it("should clear sideabr state when entering edit mode", () => {
+    it("should clear sidebar state when entering edit mode", () => {
       const state = {
         ...initState,
         sidebar: { name: "foo", props: { abc: 123 } },
@@ -118,7 +158,7 @@ describe("dashboard reducers", () => {
       ).toEqual({ ...state, isEditing: true, sidebar: { props: {} } });
     });
 
-    it("should clear sideabr state when leaving edit mode", () => {
+    it("should clear sidebar state when leaving edit mode", () => {
       const state = {
         ...initState,
         sidebar: { name: "foo", props: { abc: 123 } },
diff --git a/frontend/src/metabase/dashboard/selectors/selectors.js b/frontend/src/metabase/dashboard/selectors/selectors.js
index 25a55586b4f..944071ec100 100644
--- a/frontend/src/metabase/dashboard/selectors/selectors.js
+++ b/frontend/src/metabase/dashboard/selectors/selectors.js
@@ -201,7 +201,7 @@ export const getCanShowAutoApplyFiltersToast = createSelector(
 export const getDocumentTitle = state =>
   state.dashboard.loadingControls.documentTitle;
 
-export const getisNavigatingBackToDashboard = state =>
+export const getIsNavigatingBackToDashboard = state =>
   state.dashboard.isNavigatingBackToDashboard;
 
 export const getIsBookmarked = (state, props) =>
diff --git a/frontend/src/metabase/parameters/components/FilterApplyButton/FilterApplyButton.tsx b/frontend/src/metabase/parameters/components/FilterApplyButton/FilterApplyButton.tsx
index b0ee948b42e..0214fd1cfd1 100644
--- a/frontend/src/metabase/parameters/components/FilterApplyButton/FilterApplyButton.tsx
+++ b/frontend/src/metabase/parameters/components/FilterApplyButton/FilterApplyButton.tsx
@@ -8,8 +8,7 @@ import {
 import { applyDraftParameterValues } from "metabase/dashboard/actions";
 import { ApplyButton } from "./FilterApplyButton.styled";
 
-// eslint-disable-next-line import/no-default-export -- deprecated usage
-export default function FilterApplyButton() {
+export function FilterApplyButton() {
   const isAutoApplyFilters = useSelector(getIsAutoApplyFilters);
   const hasUnappliedParameterValues = useSelector(
     getHasUnappliedParameterValues,
diff --git a/frontend/src/metabase/parameters/components/FilterApplyButton/index.ts b/frontend/src/metabase/parameters/components/FilterApplyButton/index.ts
index a8fcc752f24..a00b70a88d9 100644
--- a/frontend/src/metabase/parameters/components/FilterApplyButton/index.ts
+++ b/frontend/src/metabase/parameters/components/FilterApplyButton/index.ts
@@ -1,2 +1 @@
-// eslint-disable-next-line import/no-default-export -- deprecated usage
-export { default } from "./FilterApplyButton";
+export { FilterApplyButton } from "./FilterApplyButton";
diff --git a/frontend/src/metabase/public/components/EmbedFrame/EmbedFrame.tsx b/frontend/src/metabase/public/components/EmbedFrame/EmbedFrame.tsx
index 37cb9e3e437..08816a17c65 100644
--- a/frontend/src/metabase/public/components/EmbedFrame/EmbedFrame.tsx
+++ b/frontend/src/metabase/public/components/EmbedFrame/EmbedFrame.tsx
@@ -14,7 +14,7 @@ import { isWithinIframe, initializeIframeResizer } from "metabase/lib/dom";
 import { parseHashOptions } from "metabase/lib/browser";
 
 import SyncedParametersList from "metabase/parameters/components/SyncedParametersList/SyncedParametersList";
-import FilterApplyButton from "metabase/parameters/components/FilterApplyButton";
+import { FilterApplyButton } from "metabase/parameters/components/FilterApplyButton";
 
 import type {
   Dashboard,
-- 
GitLab