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 0000000000000000000000000000000000000000..595e62e0f8326d2c81adf23e98291472b8aac8c8 --- /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 1c761e7b880daaa751bce738496e7b4e59097086..7b8377d0418fa389ac19a3f20441275ef3604cee 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 b3383a35aff75d763ffe20ec7a9ea19fd57d950e..671f71f2a9ee554c9598ce6a28df39609979cd17 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 474a2d778ee56ebfe115905df6573693187196dc..59b64e8dedae9cc434cb368e54209164408e15f5 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 728ce44a12b1f7c893230212c201043948c3a081..d4a774168423e53937cbe10229e194d891f59bac 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 0ebbc23a85648f50677489dd1e96024839bfbe0d..693cd2e32c498150769dd2f1572eceb5d0eff939 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 e1ebbc35cef2f02688ac1714e2a285c77f77161d..5d72de849d2c9c100348f8679c9804651ee79406 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 25a55586b4f9253fb4b89ebcae018944a6e8ff43..944071ec100a01f2457dcb2b74a8ea46b9db3c62 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 b0ee948b42e2b053d34a2821bad70ea2ce0fe5e8..0214fd1cfd1c826a8648542da007706cc592d5c2 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 a8fcc752f24d834a76bf90ed19f94afb1310db64..a00b70a88d92450c58a8c2c0abb11daec7af1c73 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 37cb9e3e4371b8998f0442897273f703de89e084..08816a17c65bb0cc3f9f22f22f9a25714dd7895b 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,