From dc4614151c882ec4c2a14a00dfad362b1c8b584a Mon Sep 17 00:00:00 2001 From: Uladzimir Havenchyk <125459446+uladzimirdev@users.noreply.github.com> Date: Mon, 10 Jun 2024 22:19:17 +0300 Subject: [PATCH] [Dashboard] Replace automatic wiring of the cards with opt-in variant (#43827) --- .../dashcard-replace-question.cy.spec.js | 5 +- .../dashboard-filters-auto-wiring.cy.spec.js | 344 +++++++++++++----- .../dashboard-filters/parameters.cy.spec.js | 24 +- e2e/test/scenarios/dashboard/tabs.cy.spec.js | 7 +- ...dashboard-filters-reproductions.cy.spec.js | 13 +- frontend/src/metabase-types/store/state.ts | 7 +- frontend/src/metabase-types/store/undo.ts | 13 + .../actions/auto-wire-parameters/actions.ts | 102 ++++-- .../actions/auto-wire-parameters/toasts.ts | 151 ++++++-- .../actions/auto-wire-parameters/utils.ts | 68 ++-- .../metabase/dashboard/actions/cards-typed.ts | 13 +- .../dashboard/actions/cards.unit.spec.ts | 64 +++- .../metabase/dashboard/actions/parameters.ts | 22 +- 13 files changed, 592 insertions(+), 241 deletions(-) create mode 100644 frontend/src/metabase-types/store/undo.ts diff --git a/e2e/test/scenarios/dashboard-cards/dashcard-replace-question.cy.spec.js b/e2e/test/scenarios/dashboard-cards/dashcard-replace-question.cy.spec.js index 6bdfd7579c6..4d88e82b1b5 100644 --- a/e2e/test/scenarios/dashboard-cards/dashcard-replace-question.cy.spec.js +++ b/e2e/test/scenarios/dashboard-cards/dashcard-replace-question.cy.spec.js @@ -17,6 +17,7 @@ import { expectGoodSnowplowEvent, expectNoBadSnowplowEvents, entityPickerModal, + undoToastList, } from "e2e/support/helpers"; import { createMockDashboardCard, @@ -192,8 +193,8 @@ describeWithSnowplow("scenarios > dashboard cards > replace question", () => { nextQuestionName: "Next question", }); - // There're two toasts: "Undo replace" and "Undo parameters auto-wiring" - cy.findAllByTestId("toast-undo").eq(0).button("Undo").click(); + // There're two toasts: "Undo replace" and "Auto-connect" + undoToastList().eq(0).button("Undo").click(); // Ensure we kept viz settings and parameter mapping changes from before findTargetDashcard().within(() => { diff --git a/e2e/test/scenarios/dashboard-filters/dashboard-filters-auto-wiring.cy.spec.js b/e2e/test/scenarios/dashboard-filters/dashboard-filters-auto-wiring.cy.spec.js index dc03290a008..c12b32a2633 100644 --- a/e2e/test/scenarios/dashboard-filters/dashboard-filters-auto-wiring.cy.spec.js +++ b/e2e/test/scenarios/dashboard-filters/dashboard-filters-auto-wiring.cy.spec.js @@ -16,12 +16,16 @@ import { goToTab, createNewTab, undoToast, + undoToastList, setFilter, visitQuestion, modal, dashboardParametersContainer, openQuestionActions, entityPickerModal, + findDashCardAction, + removeDashboardCard, + sidebar, } from "e2e/support/helpers"; const { ORDERS_ID, PRODUCTS_ID, REVIEWS_ID } = SAMPLE_DATABASE; @@ -50,8 +54,8 @@ describe("dashboard filters auto-wiring", () => { cy.intercept("GET", "/api/dashboard/**").as("dashboard"); }); - describe("when wiring parameter to all cards for a filter", () => { - it("should automatically wire parameters to cards with matching fields", () => { + describe("parameter mapping", () => { + it("should wire parameters to cards with matching fields", () => { createDashboardWithCards({ cards }).then(dashboardId => { visitDashboard(dashboardId); }); @@ -66,18 +70,23 @@ describe("dashboard filters auto-wiring", () => { cy.findByText("User.Name").should("exist"); }); - getDashboardCard(1).within(() => { - cy.findByText("User.Name").should("exist"); - }); + getDashboardCard(1).findByText("User.Name").should("not.exist"); undoToast() - .findByText( - "This filter has been auto-connected with questions with the same field.", + .should( + "contain", + "Auto-connect this filter to all questions containing “User.Nameâ€?", ) - .should("be.visible"); + .should("not.contain", "in the current tab") + .findByText("Auto-connect") + .click(); + undoToast().should( + "contain", + "The filter was auto-connected to all questions containing “User.Nameâ€.", + ); }); - it("should not automatically wire parameters to cards that already have a parameter, despite matching fields", () => { + it("should not wire parameters to cards that already have a parameter, despite matching fields", () => { createDashboardWithCards({ cards }).then(dashboardId => { visitDashboard(dashboardId); }); @@ -93,10 +102,12 @@ describe("dashboard filters auto-wiring", () => { }); undoToast() - .findByText( - "This filter has been auto-connected with questions with the same field.", + .should( + "contain", + "Auto-connect this filter to all questions containing “User.Nameâ€?", ) - .should("be.visible"); + .findByRole("button", { name: "Auto-connect" }) + .click(); getDashboardCard(1).within(() => { cy.findByLabelText("close icon").click(); @@ -112,10 +123,10 @@ describe("dashboard filters auto-wiring", () => { cy.findByText("User.Address").should("exist"); }); - undoToast().should("not.exist"); + undoToast().should("contain", "Undo"); }); - it("should not automatically wire parameters to cards that don't have a matching field", () => { + it("should not suggest to wire parameters to cards that don't have a matching field", () => { cy.createQuestion({ name: "Products Table", query: { "source-table": PRODUCTS_ID, limit: 1 }, @@ -148,48 +159,10 @@ describe("dashboard filters auto-wiring", () => { selectDashboardFilter(getDashboardCard(0), "Name"); - getDashboardCard(0).within(() => { - cy.findByText("User.Name").should("exist"); - }); - - getDashboardCard(1).within(() => { - cy.findByText("Select…").should("exist"); - }); - undoToast().should("not.exist"); }); - it("should autowire parameters to cards in different tabs", () => { - createDashboardWithCards({ cards }).then(dashboardId => { - visitDashboardAndCreateTab({ - dashboardId, - save: false, - }); - }); - - setFilter("Text or Category", "Is"); - - addCardToDashboard(); - goToFilterMapping(); - - selectDashboardFilter(getDashboardCard(0), "Name"); - - getDashboardCard(0).findByText("User.Name").should("exist"); - - goToTab("Tab 1"); - - for (let i = 0; i < cards.length; i++) { - getDashboardCard(i).findByText("User.Name").should("exist"); - } - - undoToast() - .findByText( - "This filter has been auto-connected with questions with the same field.", - ) - .should("be.visible"); - }); - - it("should undo parameter wiring when 'Undo auto-connection' is clicked", () => { + it("should undo parameter wiring when 'Undo' is clicked", () => { createDashboardWithCards({ cards }).then(dashboardId => { visitDashboard(dashboardId); }); @@ -202,13 +175,15 @@ describe("dashboard filters auto-wiring", () => { selectDashboardFilter(getDashboardCard(0), "Name"); + undoToast().findByRole("button", { name: "Auto-connect" }).click(); + getDashboardCard(0).findByText("User.Name").should("exist"); for (let i = 0; i < cards.length; i++) { getDashboardCard(i).findByText("User.Name").should("exist"); } - undoToast().findByText("Undo auto-connection").click(); + undoToast().findByRole("button", { name: "Undo" }).click(); getDashboardCard(0).findByText("User.Name").should("exist"); for (let i = 1; i < cards.length; i++) { @@ -216,8 +191,8 @@ describe("dashboard filters auto-wiring", () => { } }); - it("in case of two autowiring undo toast, the second one should last the default timeout of 5s", () => { - // The autowiring undo toasts use the same id, a bug in the undo logic caused the second toast to be dismissed by the + it("in case of two auto-wiring undo toast, the second one should last the default timeout of 12s", () => { + // The auto-wiring undo toasts use the same id, a bug in the undo logic caused the second toast to be dismissed by the // timeout set by the first. See https://github.com/metabase/metabase/pull/35461#pullrequestreview-1731776862 const cardTemplate = { card_id: ORDERS_BY_YEAR_QUESTION_ID, @@ -254,25 +229,59 @@ describe("dashboard filters auto-wiring", () => { selectDashboardFilter(getDashboardCard(0), "Name"); removeFilterFromDashCard(0); - removeFilterFromDashCard(1); cy.tick(2000); selectDashboardFilter(getDashboardCard(0), "Name"); - // since we waited 2 seconds earlier, if the toast is still visible after this other delay of 4s, - // it means the first timeout of 5s was cleared correctly - cy.tick(4000); + // since we waited 2s earlier, if the toast is still visible after this other delay of 11s, + // it means the first timeout of 12s was cleared correctly + cy.tick(11000); undoToast().should("exist"); cy.tick(2000); - undoToast().should("not.exist"); }); + + describe("multiple tabs", () => { + it("should not wire parameters to cards in different tabs", () => { + createDashboardWithCards({ cards }).then(dashboardId => { + visitDashboardAndCreateTab({ + dashboardId, + save: false, + }); + }); + + setFilter("Text or Category", "Is"); + + addCardToDashboard(); + goToFilterMapping(); + + selectDashboardFilter(getDashboardCard(0), "Name"); + + getDashboardCard(0).findByText("User.Name").should("exist"); + + undoToast().should("not.exist"); + + goToTab("Tab 1"); + + for (let i = 0; i < cards.length; i++) { + getDashboardCard(i).findByText("User.Name").should("not.exist"); + } + + selectDashboardFilter(getDashboardCard(0), "Name"); + + cy.log("verify prefix 'in the current tab'"); + undoToast().should( + "contain", + "Auto-connect this filter to all questions containing “User.Nameâ€, in the current tab?", + ); + }); + }); }); - describe("wiring parameters when adding a card", () => { - it("should automatically wire a parameters to cards that are added to the dashboard", () => { + describe("add a card", () => { + it("should wire parameters to cards that are added to the dashboard", () => { createDashboardWithCards({ cards }).then(dashboardId => { visitDashboard(dashboardId); }); @@ -282,26 +291,41 @@ describe("dashboard filters auto-wiring", () => { setFilter("Text or Category", "Is"); selectDashboardFilter(getDashboardCard(0), "Name"); + undoToast().findByRole("button", { name: "Auto-connect" }).click(); for (let i = 0; i < cards.length; i++) { getDashboardCard(i).findByText("User.Name").should("exist"); } addCardToDashboard(); + + cy.log("verify toast text and enable auto-connect"); + + undoToastList() + .eq(1) + .should("contain", "Auto-connect “Orders Model†to “Textâ€?") + .findByRole("button", { name: "Auto-connect" }) + .click(); + + cy.log("verify toast text after auto-connect"); + + undoToastList() + .eq(1) + .should("contain", "“Orders Model†was auto-connected to “Textâ€."); + goToFilterMapping(); for (let i = 0; i < cards.length + 1; i++) { getDashboardCard(i).findByText("User.Name").should("exist"); } - undoToast() - .findByText( - "Orders Model has been auto-connected with filters with the same field.", - ) + undoToastList() + .eq(1) + .findByText("“Orders Model†was auto-connected to “Textâ€.") .should("be.visible"); }); - it("should automatically wire parameters to cards that are added to the dashboard in a different tab", () => { + it("should undo parameter wiring when 'Undo' is clicked", () => { createDashboardWithCards({ cards }).then(dashboardId => { visitDashboard(dashboardId); }); @@ -311,24 +335,94 @@ describe("dashboard filters auto-wiring", () => { setFilter("Text or Category", "Is"); selectDashboardFilter(getDashboardCard(0), "Name"); + undoToast().findByRole("button", { name: "Auto-connect" }).click(); + for (let i = 0; i < cards.length; i++) { getDashboardCard(i).findByText("User.Name").should("exist"); } - createNewTab(); addCardToDashboard(); goToFilterMapping(); + undoToastList() + .eq(1) + .should("contain", "Auto-connect “Orders Model†to “Textâ€?") + .findByRole("button", { name: "Auto-connect" }) + .click(); + + for (let i = 0; i < cards.length + 1; i++) { + getDashboardCard(i).findByText("User.Name").should("exist"); + } + + cy.log("verify undo functionality"); + undoToastList().eq(1).findByText("Undo").click(); + getDashboardCard(0).findByText("User.Name").should("exist"); + getDashboardCard(1).findByText("User.Name").should("exist"); + getDashboardCard(2).findByText("Select…").should("exist"); + }); - undoToast() - .findByText( - "Orders Model has been auto-connected with filters with the same field.", - ) - .should("be.visible"); + describe("multiple tabs", () => { + it("should not wire parameters to cards that are added to the dashboard in a different tab", () => { + createDashboardWithCards({ cards }).then(dashboardId => { + visitDashboard(dashboardId); + }); + + editDashboard(); + + setFilter("Number", "Equal to"); + setFilter("Text or Category", "Is"); + + selectDashboardFilter(getDashboardCard(0), "Name"); + + undoToast() + .should( + "contain", + "Auto-connect this filter to all questions containing", + ) + .findByRole("button", { name: "Auto-connect" }) + .click(); + + for (let i = 0; i < cards.length; i++) { + getDashboardCard(i).findByText("User.Name").should("exist"); + } + + createNewTab(); + addCardToDashboard(); + goToFilterMapping(); + + getDashboardCard(0).findByText("User.Name").should("not.exist"); + + cy.log( + "verify that no new toast with suggestion to auto-wire appeared", + ); + + undoToastList() + .should("have.length", 1) + .should( + "contain", + "The filter was auto-connected to all questions containing “User.Nameâ€", + ); + + selectDashboardFilter(getDashboardCard(0), "Name"); + goToFilterMapping("Equal to"); + selectDashboardFilter(getDashboardCard(0), "Total"); + + addCardToDashboard(); + + undoToastList().eq(1).findByText("Auto-connect").click(); + + cy.log("verify that toast shows number of filters that were connected"); + + undoToastList() + .eq(1) + .should("contain", "“Orders Model†was auto-connected to 2 filters."); + }); }); + }); - it("should undo parameter wiring when 'Undo auto-connection' is clicked", () => { + describe("replace a card", () => { + it("should show auto-wire suggestion toast when a card is replaced", () => { createDashboardWithCards({ cards }).then(dashboardId => { visitDashboard(dashboardId); }); @@ -339,22 +433,23 @@ describe("dashboard filters auto-wiring", () => { selectDashboardFilter(getDashboardCard(0), "Name"); - for (let i = 0; i < cards.length; i++) { - getDashboardCard(i).findByText("User.Name").should("exist"); - } + undoToast().findByText("Auto-connect").click(); - addCardToDashboard(); goToFilterMapping(); - for (let i = 0; i < cards.length + 1; i++) { - getDashboardCard(i).findByText("User.Name").should("exist"); - } + findDashCardAction(getDashboardCard(1), "Replace").click(); - undoToast().findByText("Undo auto-connection").click(); + modal().findByText("Orders, Count").click(); - getDashboardCard(0).findByText("User.Name").should("exist"); - getDashboardCard(1).findByText("User.Name").should("exist"); - getDashboardCard(2).findByText("Select…").should("exist"); + undoToastList() + .eq(2) + .should("contain", "Auto-connect “Orders, Count†to “Textâ€?") + .button("Auto-connect") + .click(); + + undoToastList() + .eq(2) + .should("contain", "“Orders, Count†was auto-connected to “Textâ€."); }); }); @@ -398,7 +493,7 @@ describe("dashboard filters auto-wiring", () => { }); }); - it("should autowire and filter cards with foreign keys when added to the dashboard via the sidebar", () => { + it("should auto-wire and filter cards with foreign keys when added to the dashboard via the sidebar", () => { visitDashboard("@dashboardId"); editDashboard(); setFilter("ID"); @@ -410,6 +505,18 @@ describe("dashboard filters auto-wiring", () => { goToFilterMapping("ID"); + undoToastList() + .findByText("Auto-connect “Orders Question†to “IDâ€?") + .closest("[data-testid='toast-undo']") + .findByRole("button", { name: "Auto-connect" }) + .click(); + + undoToastList() + .findByText("Auto-connect “Reviews Question†to “IDâ€?") + .closest("[data-testid='toast-undo']") + .findByRole("button", { name: "Auto-connect" }) + .click(); + getDashboardCard(0).findByText("Product.ID").should("exist"); getDashboardCard(1).findByText("Product.ID").should("exist"); getDashboardCard(2).findByText("Product.ID").should("exist"); @@ -438,7 +545,7 @@ describe("dashboard filters auto-wiring", () => { }); }); - it("should autowire and filter cards with foreign keys when added to the dashboard via the query builder", () => { + it("should auto-wire and filter cards with foreign keys when added to the dashboard via the query builder", () => { visitDashboard("@dashboardId"); editDashboard(); setFilter("ID"); @@ -488,7 +595,58 @@ describe("dashboard filters auto-wiring", () => { }); }); }); + + describe("dismiss toasts", () => { + it("should dismiss auto-wire toasts on filter removal", () => { + createDashboardWithCards({ cards }).then(dashboardId => { + visitDashboard(dashboardId); + }); + + editDashboard(); + setFilter("Text or Category", "Is"); + + selectDashboardFilter(getDashboardCard(0), "Name"); + + undoToast().findByRole("button", { name: "Auto-connect" }).click(); + + addCardToDashboard(); + + undoToastList() + .contains("Auto-connect “Orders Model†to “Textâ€?") + .should("be.visible"); + + removeFilterFromDashboard(); + + undoToast().should("not.exist"); + }); + + it("should dismiss auto-wire toasts on card removal", () => { + createDashboardWithCards({ cards }).then(dashboardId => { + visitDashboard(dashboardId); + }); + + editDashboard(); + setFilter("Text or Category", "Is"); + + selectDashboardFilter(getDashboardCard(0), "Name"); + + undoToast().findByRole("button", { name: "Auto-connect" }).click(); + + addCardToDashboard(); + + undoToastList() + .contains("Auto-connect “Orders Model†to “Textâ€?") + .should("be.visible"); + + removeDashboardCard(2); + + undoToastList() + .should("have.length", 1) + .should("contain", "Removed card"); + }); + }); }); + function createDashboardWithCards({ dashboardName = "my dash", cards = [], @@ -514,6 +672,12 @@ function addCardToDashboard(dashcardNames = "Orders Model") { } } +function removeFilterFromDashboard(filterName = "Text") { + goToFilterMapping(filterName); + + sidebar().findByRole("button", { name: "Remove" }).click(); +} + function goToFilterMapping(name = "Text") { cy.findByTestId("edit-dashboard-parameters-widget-container") .findByText(name) @@ -550,7 +714,9 @@ function addQuestionFromQueryBuilder({ cy.button("Select").click(); }); - undoToast().should("be.visible"); + undoToast().findByRole("button", { name: "Auto-connect" }).click(); + undoToast().should("contain", "Undo"); + if (saveDashboardAfterAdd) { saveDashboard(); } diff --git a/e2e/test/scenarios/dashboard-filters/parameters.cy.spec.js b/e2e/test/scenarios/dashboard-filters/parameters.cy.spec.js index fa2bcab214b..9ad1472e636 100644 --- a/e2e/test/scenarios/dashboard-filters/parameters.cy.spec.js +++ b/e2e/test/scenarios/dashboard-filters/parameters.cy.spec.js @@ -72,19 +72,9 @@ describe("scenarios > dashboard > parameters", () => { // (this doesn't make sense to do, but it illustrates the feature) selectDashboardFilter(getDashboardCard(0), "Name"); - getDashboardCard(1).within(() => { - cy.findByLabelText("close icon").click(); - }); selectDashboardFilter(getDashboardCard(1), "Category"); - // finish editing filter and save dashboard - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.contains("Save").click(); - - // wait for saving to finish - cy.wait("@dashboard"); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.contains("You're editing this dashboard.").should("not.exist"); + saveDashboard(); // confirm that typing searches both fields filterWidget().contains("Text").click(); @@ -638,28 +628,34 @@ describe("scenarios > dashboard > parameters", () => { .click(); selectDashboardFilter(getDashboardCard(0), "Created At"); + selectDashboardFilter(getDashboardCard(1), "Created At"); + saveDashboard(); cy.get("@dashcardRequestSpy").should("have.callCount", 2); }); it("should fetch dashcard data when parameter mapping is removed", () => { - // Connect filter to 1 card only + cy.log("Connect filter to 1 card only"); + editDashboard(); cy.findByTestId("edit-dashboard-parameters-widget-container") .findByText("Date Filter") .click(); selectDashboardFilter(getDashboardCard(0), "Created At"); - disconnectDashboardFilter(getDashboardCard(1)); + saveDashboard(); cy.get("@dashcardRequestSpy").should("have.callCount", 1); - // Disconnect filter from the 1st card + cy.log("Disconnect filter from the 1st card"); + editDashboard(); + cy.findByTestId("edit-dashboard-parameters-widget-container") .findByText("Date Filter") .click(); + disconnectDashboardFilter(getDashboardCard(0)); saveDashboard(); diff --git a/e2e/test/scenarios/dashboard/tabs.cy.spec.js b/e2e/test/scenarios/dashboard/tabs.cy.spec.js index 0e3ab068018..9f7c339ed59 100644 --- a/e2e/test/scenarios/dashboard/tabs.cy.spec.js +++ b/e2e/test/scenarios/dashboard/tabs.cy.spec.js @@ -672,7 +672,6 @@ describe("scenarios > dashboard > tabs", () => { setFilter("Time", "Relative Date"); - // Auto-connection happens here selectDashboardFilter(getDashboardCard(0), "Created At"); saveDashboard(); @@ -692,11 +691,11 @@ describe("scenarios > dashboard > tabs", () => { cy.findAllByTestId("table-row").should("exist"); }); - // Loader in the 1st tab + // we do not auto-wire automatically in different tabs anymore, so first tab + // should not show a loader and re-run query goToTab("Tab 1"); getDashboardCard(0).within(() => { - cy.findByTestId("loading-spinner").should("exist"); - cy.wait("@saveCard"); + cy.findByTestId("loading-spinner").should("not.exist"); cy.findAllByTestId("table-row").should("exist"); }); }); diff --git a/e2e/test/scenarios/filters-reproductions/dashboard-filters-reproductions.cy.spec.js b/e2e/test/scenarios/filters-reproductions/dashboard-filters-reproductions.cy.spec.js index 783001f5364..c1a5828b40d 100644 --- a/e2e/test/scenarios/filters-reproductions/dashboard-filters-reproductions.cy.spec.js +++ b/e2e/test/scenarios/filters-reproductions/dashboard-filters-reproductions.cy.spec.js @@ -211,6 +211,9 @@ describe("issue 8030 + 32444", () => { setFilter("Text or Category", "Is"); selectDashboardFilter(cy.findAllByTestId("dashcard").first(), "Title"); + + undoToast().findByRole("button", { name: "Auto-connect" }).click(); + cy.findAllByTestId("dashcard") .eq(1) .findByLabelText("Disconnect") @@ -923,21 +926,19 @@ describe("issue 19494", () => { connectFilterToCard({ filterName: "Card 1 Filter", cardPosition: 0 }); setDefaultFilter("Doohickey"); - undoToast().findByText("Undo auto-connection").click(); connectFilterToCard({ filterName: "Card 2 Filter", cardPosition: -1 }); setDefaultFilter("Gizmo"); - undoToast().findByText("Undo auto-connection").click(); saveDashboard(); checkAppliedFilter("Card 1 Filter", "Doohickey"); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("148.23"); + + getDashboardCard(0).should("contain", "148.23"); checkAppliedFilter("Card 2 Filter", "Gizmo"); - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("110.93"); + + getDashboardCard(1).should("contain", "110.93"); }); }); diff --git a/frontend/src/metabase-types/store/state.ts b/frontend/src/metabase-types/store/state.ts index db05f67a500..e82937ac8a2 100644 --- a/frontend/src/metabase-types/store/state.ts +++ b/frontend/src/metabase-types/store/state.ts @@ -14,9 +14,11 @@ import type { QueryBuilderState } from "./qb"; import type { RequestsState } from "./requests"; import type { SettingsState } from "./settings"; import type { SetupState } from "./setup"; +import type { UndoState } from "./undo"; import type { FileUploadState } from "./upload"; -type modalName = null | "collection" | "dashboard" | "action"; +type ModalName = null | "collection" | "dashboard" | "action"; + export interface State { admin: AdminState; app: AppState; @@ -33,7 +35,8 @@ export interface State { settings: SettingsState; setup: SetupState; upload: FileUploadState; - modal: modalName; + modal: ModalName; + undo: UndoState; } export type Dispatch<T = any> = (action: T) => unknown | Promise<unknown>; diff --git a/frontend/src/metabase-types/store/undo.ts b/frontend/src/metabase-types/store/undo.ts new file mode 100644 index 00000000000..67e139159cf --- /dev/null +++ b/frontend/src/metabase-types/store/undo.ts @@ -0,0 +1,13 @@ +// TODO: convert redux/undo and UndoListing.jsx to TS and update type +export type UndoState = { + id: string | number; + type?: string; + action?: () => void; + actions?: (() => void)[]; + icon?: string; + toastColor?: string; + actionLabel?: string; + canDismiss?: boolean; + dismissIconColor?: string; + _domId?: string | number; +}[]; diff --git a/frontend/src/metabase/dashboard/actions/auto-wire-parameters/actions.ts b/frontend/src/metabase/dashboard/actions/auto-wire-parameters/actions.ts index 49e84278fbd..7132347fde8 100644 --- a/frontend/src/metabase/dashboard/actions/auto-wire-parameters/actions.ts +++ b/frontend/src/metabase/dashboard/actions/auto-wire-parameters/actions.ts @@ -1,7 +1,3 @@ -import { - setDashCardAttributes, - setMultipleDashCardAttributes, -} from "metabase/dashboard/actions"; import { closeAutoWireParameterToast, showAddedCardAutoWireParametersToast, @@ -10,6 +6,7 @@ import { import { getAllDashboardCardsWithUnmappedParameters, getAutoWiredMappingsForDashcards, + getMatchingParameterOption, getParameterMappings, } from "metabase/dashboard/actions/auto-wire-parameters/utils"; import { getExistingDashCards } from "metabase/dashboard/actions/utils"; @@ -18,62 +15,62 @@ import { getDashCardById, getParameters, getQuestions, + getDashboard, + getSelectedTabId, + getTabs, } from "metabase/dashboard/selectors"; import { isQuestionDashCard } from "metabase/dashboard/utils"; import { getParameterMappingOptions } from "metabase/parameters/utils/mapping-options"; -import { getMetadata } from "metabase/selectors/metadata"; -import Question from "metabase-lib/v1/Question"; import type { QuestionDashboardCard, DashCardId, ParameterId, ParameterTarget, + DashboardTabId, + DashboardParameterMapping, } from "metabase-types/api"; import type { Dispatch, GetState, StoreDashcard } from "metabase-types/store"; -export function autoWireDashcardsWithMatchingParameters( +export function showAutoWireToast( parameter_id: ParameterId, dashcard: QuestionDashboardCard, target: ParameterTarget, + selectedTabId: DashboardTabId, ) { return function (dispatch: Dispatch, getState: GetState) { - const metadata = getMetadata(getState()); - const dashboard_state = getState().dashboard; + const dashboardState = getState().dashboard; const questions = getQuestions(getState()); const parameter = getParameters(getState()).find( ({ id }) => id === parameter_id, ); - if (!dashboard_state.dashboardId || !parameter) { + if (!dashboardState.dashboardId || !parameter) { return; } const dashcardsToAutoApply = getAllDashboardCardsWithUnmappedParameters({ - dashboardState: dashboard_state, - dashboardId: dashboard_state.dashboardId, + dashboards: dashboardState.dashboards, + dashcards: dashboardState.dashcards, + dashboardId: dashboardState.dashboardId, parameterId: parameter_id, + selectedTabId, + // exclude current dashcard as it's being updated in another action excludeDashcardIds: [dashcard.id], }); const dashcardAttributes = getAutoWiredMappingsForDashcards( parameter, - dashcard, dashcardsToAutoApply, target, - metadata, questions, ); - if (dashcardAttributes.length === 0) { + const shouldShowToast = dashcardAttributes.length > 0; + + if (!shouldShowToast) { return; } - dispatch( - setMultipleDashCardAttributes({ - dashcards: dashcardAttributes, - }), - ); - const originalDashcardAttributes = dashcardsToAutoApply.map(dashcard => ({ id: dashcard.id, attributes: { @@ -81,15 +78,31 @@ export function autoWireDashcardsWithMatchingParameters( }, })); + const mappingOption = getMatchingParameterOption( + parameter, + dashcard, + target, + questions, + ); + + const tabs = getTabs(getState()); + + if (!mappingOption) { + return; + } + dispatch( showAutoWireParametersToast({ - dashcardAttributes: originalDashcardAttributes, + dashcardAttributes, + originalDashcardAttributes, + columnName: formatMappingOption(mappingOption), + hasMultipleTabs: tabs.length > 1, }), ); }; } -export function autoWireParametersToNewCard({ +export function showAutoWireToastNewCard({ dashcard_id, }: { dashcard_id: DashCardId; @@ -97,7 +110,6 @@ export function autoWireParametersToNewCard({ return function (dispatch: Dispatch, getState: GetState) { dispatch(closeAutoWireParameterToast()); - const metadata = getMetadata(getState()); const dashboardState = getState().dashboard; const dashboardId = dashboardState.dashboardId; @@ -105,13 +117,20 @@ export function autoWireParametersToNewCard({ return; } + const dashboard = getDashboard(getState()); + if (!dashboard || !dashboard.parameters) { + return; + } + const questions = getQuestions(getState()); const parameters = getParameters(getState()); + const selectedTabId = getSelectedTabId(getState()); const dashcards = getExistingDashCards( dashboardState.dashboards, dashboardState.dashcards, dashboardId, + selectedTabId, ); const targetDashcard: StoreDashcard = getDashCardById( @@ -123,11 +142,9 @@ export function autoWireParametersToNewCard({ return; } - const targetQuestion = - questions[targetDashcard.card.id] ?? - new Question(targetDashcard.card, metadata); + const targetQuestion = questions[targetDashcard.card.id]; - const parametersToAutoApply = []; + const parametersMappingsToApply: DashboardParameterMapping[] = []; const processedParameterIds = new Set(); for (const parameter of parameters) { @@ -153,7 +170,7 @@ export function autoWireParametersToNewCard({ targetDashcard.card_id && !processedParameterIds.has(parameter.id) ) { - parametersToAutoApply.push( + parametersMappingsToApply.push( ...getParameterMappings( targetDashcard, parameter.id, @@ -167,24 +184,35 @@ export function autoWireParametersToNewCard({ } } - if (parametersToAutoApply.length === 0) { + if (parametersMappingsToApply.length === 0) { return; } - dispatch( - setDashCardAttributes({ - id: dashcard_id, - attributes: { - parameter_mappings: parametersToAutoApply, - }, - }), + const parametersToMap = dashboard.parameters.filter(p => + processedParameterIds.has(p.id), ); dispatch( showAddedCardAutoWireParametersToast({ targetDashcard, dashcard_id, + parametersMappingsToApply, + parametersToMap, }), ); }; } + +function formatMappingOption({ + name, + sectionName, +}: { + name: string; + sectionName?: string; +}) { + if (sectionName == null) { + // for native question variables or field literals we just display the name + return name; + } + return `${sectionName}.${name}`; +} diff --git a/frontend/src/metabase/dashboard/actions/auto-wire-parameters/toasts.ts b/frontend/src/metabase/dashboard/actions/auto-wire-parameters/toasts.ts index 1f03177f9a8..bbadcf1ae5f 100644 --- a/frontend/src/metabase/dashboard/actions/auto-wire-parameters/toasts.ts +++ b/frontend/src/metabase/dashboard/actions/auto-wire-parameters/toasts.ts @@ -7,64 +7,165 @@ import { setMultipleDashCardAttributes, } from "metabase/dashboard/actions"; import { addUndo, dismissUndo } from "metabase/redux/undo"; -import type { QuestionDashboardCard, DashCardId } from "metabase-types/api"; -import type { Dispatch } from "metabase-types/store"; +import type { + QuestionDashboardCard, + DashCardId, + DashboardParameterMapping, + Parameter, +} from "metabase-types/api"; +import type { Dispatch, GetState } from "metabase-types/store"; export const AUTO_WIRE_TOAST_ID = _.uniqueId(); +const AUTO_WIRE_UNDO_TOAST_ID = _.uniqueId(); export const showAutoWireParametersToast = ({ dashcardAttributes, + originalDashcardAttributes, + columnName, + hasMultipleTabs, }: { dashcardAttributes: SetMultipleDashCardAttributesOpts; + originalDashcardAttributes: SetMultipleDashCardAttributesOpts; + columnName: string; + hasMultipleTabs: boolean; }) => (dispatch: Dispatch) => { + const message = hasMultipleTabs + ? t`Auto-connect this filter to all questions containing “${columnName}â€, in the current tab?` + : t`Auto-connect this filter to all questions containing “${columnName}â€?`; + dispatch( addUndo({ id: AUTO_WIRE_TOAST_ID, - message: t`This filter has been auto-connected with questions with the same field.`, - actionLabel: t`Undo auto-connection`, - undo: true, + icon: null, + message, + actionLabel: t`Auto-connect`, + timeout: 12000, action: () => { - dispatch( - setMultipleDashCardAttributes({ - dashcards: dashcardAttributes, - }), - ); + connectAll(); + showUndoToast(); }, }), ); + + function connectAll() { + dispatch( + setMultipleDashCardAttributes({ + dashcards: dashcardAttributes, + }), + ); + } + + function revertConnectAll() { + dispatch( + setMultipleDashCardAttributes({ + dashcards: originalDashcardAttributes, + }), + ); + } + + function showUndoToast() { + dispatch( + addUndo({ + id: AUTO_WIRE_UNDO_TOAST_ID, + message: t`The filter was auto-connected to all questions containing “${columnName}â€.`, + actionLabel: t`Undo`, + timeout: 12000, + type: "filterAutoConnect", + action: revertConnectAll, + }), + ); + } }; export const showAddedCardAutoWireParametersToast = ({ targetDashcard, dashcard_id, + parametersMappingsToApply, + parametersToMap, }: { targetDashcard: QuestionDashboardCard; dashcard_id: DashCardId; + parametersMappingsToApply: DashboardParameterMapping[]; + parametersToMap: Parameter[]; }) => (dispatch: Dispatch) => { + const shouldShowParameterName = parametersMappingsToApply.length === 1; + const message = shouldShowParameterName + ? t`Auto-connect “${targetDashcard.card.name}†to “${parametersToMap[0].name}â€?` + : t`Auto-connect “${targetDashcard.card.name}†to ${parametersMappingsToApply.length} filters with the same field?`; + + const toastId = _.uniqueId(); + dispatch( addUndo({ - id: AUTO_WIRE_TOAST_ID, - message: t`${targetDashcard.card.name} has been auto-connected with filters with the same field.`, - actionLabel: t`Undo auto-connection`, - undo: true, + id: toastId, + icon: null, + type: "filterAutoConnect", + message, + actionLabel: t`Auto-connect`, + timeout: 12000, action: () => { - dispatch( - setDashCardAttributes({ - id: dashcard_id, - attributes: { - parameter_mappings: [], - }, - }), - ); + closeAutoWireParameterToast(toastId); + autoWireParametersToNewCard(); + showUndoToast(); }, }), ); + + function autoWireParametersToNewCard() { + dispatch( + setDashCardAttributes({ + id: dashcard_id, + attributes: { + parameter_mappings: parametersMappingsToApply, + }, + }), + ); + } + + function revertAutoWireParametersToNewCard() { + dispatch( + setDashCardAttributes({ + id: dashcard_id, + attributes: { + parameter_mappings: [], + }, + }), + ); + } + + function showUndoToast() { + const message = shouldShowParameterName + ? t`“${targetDashcard.card.name}†was auto-connected to “${parametersToMap[0].name}â€.` + : t`“${targetDashcard.card.name}†was auto-connected to ${parametersToMap.length} filters.`; + + dispatch( + addUndo({ + message, + timeout: 12000, + type: "filterAutoConnect", + action: revertAutoWireParametersToNewCard, + }), + ); + } + }; + +export const closeAutoWireParameterToast = + (toastId: string = AUTO_WIRE_TOAST_ID) => + (dispatch: Dispatch) => { + dispatch(dismissUndo(toastId, false)); }; -export const closeAutoWireParameterToast = () => (dispatch: Dispatch) => { - dispatch(dismissUndo(AUTO_WIRE_TOAST_ID, false)); -}; +export const closeAddCardAutoWireToasts = + () => (dispatch: Dispatch, getState: GetState) => { + const undos = getState().undo; + + for (const undo of undos) { + if (undo.type === "filterAutoConnect") { + dispatch(dismissUndo(undo.id, false)); + } + } + }; diff --git a/frontend/src/metabase/dashboard/actions/auto-wire-parameters/utils.ts b/frontend/src/metabase/dashboard/actions/auto-wire-parameters/utils.ts index 0834c1e9b2e..c15a857bbf4 100644 --- a/frontend/src/metabase/dashboard/actions/auto-wire-parameters/utils.ts +++ b/frontend/src/metabase/dashboard/actions/auto-wire-parameters/utils.ts @@ -7,9 +7,11 @@ import { isQuestionDashCard, isVirtualDashCard, } from "metabase/dashboard/utils"; -import { getParameterMappingOptions } from "metabase/parameters/utils/mapping-options"; +import { + getParameterMappingOptions, + type ParameterMappingOption, +} from "metabase/parameters/utils/mapping-options"; import type Question from "metabase-lib/v1/Question"; -import type Metadata from "metabase-lib/v1/metadata/Metadata"; import type { CardId, QuestionDashboardCard, @@ -18,28 +20,36 @@ import type { ParameterId, ParameterTarget, Parameter, + DashboardTabId, + DashboardCard, } from "metabase-types/api"; import type { DashboardState } from "metabase-types/store"; import type { SetMultipleDashCardAttributesOpts } from "../core"; export function getAllDashboardCardsWithUnmappedParameters({ - dashboardState, + dashboards, + dashcards, dashboardId, parameterId, + selectedTabId, excludeDashcardIds = [], }: { - dashboardState: DashboardState; + dashboards: DashboardState["dashboards"]; + dashcards: DashboardState["dashcards"]; dashboardId: DashboardId; parameterId: ParameterId; + selectedTabId: DashboardTabId; excludeDashcardIds?: DashCardId[]; }): QuestionDashboardCard[] { - const dashCards = getExistingDashCards( - dashboardState.dashboards, - dashboardState.dashcards, + const existingDashcards = getExistingDashCards( + dashboards, + dashcards, dashboardId, + selectedTabId, ); - return dashCards.filter( + + return existingDashcards.filter( (dashcard): dashcard is QuestionDashboardCard => isQuestionDashCard(dashcard) && !excludeDashcardIds.includes(dashcard.id) && @@ -53,12 +63,8 @@ export function getMatchingParameterOption( parameter: Parameter, targetDashcard: QuestionDashboardCard, targetDimension: ParameterTarget, - sourceDashcard: QuestionDashboardCard, - metadata: Metadata, questions: Record<CardId, Question>, -): { - target: ParameterTarget; -} | null { +): ParameterMappingOption | null | undefined { if (!targetDashcard) { return null; } @@ -84,10 +90,8 @@ export function getMatchingParameterOption( export function getAutoWiredMappingsForDashcards( parameter: Parameter, - sourceDashcard: QuestionDashboardCard, targetDashcards: QuestionDashboardCard[], target: ParameterTarget, - metadata: Metadata, questions: Record<CardId, Question>, ): SetMultipleDashCardAttributesOpts { if (targetDashcards.length === 0) { @@ -97,14 +101,10 @@ export function getAutoWiredMappingsForDashcards( const targetDashcardMappings: SetMultipleDashCardAttributesOpts = []; for (const targetDashcard of targetDashcards) { - const selectedMappingOption: { - target: ParameterTarget; - } | null = getMatchingParameterOption( + const selectedMappingOption = getMatchingParameterOption( parameter, targetDashcard, target, - sourceDashcard, - metadata, questions, ); @@ -124,22 +124,24 @@ export function getAutoWiredMappingsForDashcards( } return targetDashcardMappings; } - -export function getParameterMappings( - dashcard: QuestionDashboardCard, +export function getParameterMappings<DC extends DashboardCard>( + dashcard: DC, parameter_id: ParameterId, card_id: CardId, target: ParameterTarget | null, -) { +): NonNullable<DC["parameter_mappings"]> { const isVirtual = isVirtualDashCard(dashcard); const isAction = isActionDashCard(dashcard); - let parameter_mappings = dashcard.parameter_mappings || []; + let parameter_mappings: NonNullable<DC["parameter_mappings"]> = + dashcard.parameter_mappings ?? []; // allow mapping the same parameter to multiple action targets if (!isAction) { parameter_mappings = parameter_mappings.filter( - m => m.card_id !== card_id || m.parameter_id !== parameter_id, + m => + ("card_id" in m && m.card_id !== card_id) || + m.parameter_id !== parameter_id, ); } @@ -151,11 +153,15 @@ export function getParameterMappings( m => !_.isEqual(m.target, target), ); } - parameter_mappings = parameter_mappings.concat({ - parameter_id, - card_id, - target, - }); + + return [ + ...parameter_mappings, + { + parameter_id, + card_id, + target, + }, + ]; } return parameter_mappings; diff --git a/frontend/src/metabase/dashboard/actions/cards-typed.ts b/frontend/src/metabase/dashboard/actions/cards-typed.ts index 8b589d31ba4..a29f9e522f0 100644 --- a/frontend/src/metabase/dashboard/actions/cards-typed.ts +++ b/frontend/src/metabase/dashboard/actions/cards-typed.ts @@ -32,7 +32,8 @@ import { isVirtualDashCard, } from "../utils"; -import { autoWireParametersToNewCard } from "./auto-wire-parameters/actions"; +import { showAutoWireToastNewCard } from "./auto-wire-parameters/actions"; +import { closeAddCardAutoWireToasts } from "./auto-wire-parameters/toasts"; import { ADD_CARD_TO_DASH, ADD_MANY_CARDS_TO_DASH, @@ -53,7 +54,7 @@ type NewDashboardCard = Omit< "entity_id" | "created_at" | "updated_at" >; -type AddDashCardOpts = NewDashCardOpts & { +export type AddDashCardOpts = NewDashCardOpts & { dashcardOverrides: Partial<NewDashboardCard> & { card: Card | VirtualCard; }; @@ -136,7 +137,7 @@ export const addSectionToDashboard = trackSectionAdded(dashId, sectionLayout.id); }; -type AddCardToDashboardOpts = NewDashCardOpts & { +export type AddCardToDashboardOpts = NewDashCardOpts & { cardId: CardId; }; @@ -159,7 +160,7 @@ export const addCardToDashboard = dispatch(fetchCardData(card, dashcard, { reload: true, clearCache: true })); await dispatch(loadMetadataForCard(card)); - dispatch(autoWireParametersToNewCard({ dashcard_id: dashcardId })); + dispatch(showAutoWireToastNewCard({ dashcard_id: dashcardId })); }; export const addHeadingDashCardToDashboard = @@ -234,7 +235,7 @@ export const replaceCard = dispatch(fetchCardData(card, dashcard, { reload: true, clearCache: true })); await dispatch(loadMetadataForCard(card)); - dispatch(autoWireParametersToNewCard({ dashcard_id: dashcardId })); + dispatch(showAutoWireToastNewCard({ dashcard_id: dashcardId })); dashboardId && trackQuestionReplaced(dashboardId); }; @@ -249,6 +250,8 @@ export const removeCardFromDashboard = createThunkAction( cardId: DashboardCard["card_id"]; }) => dispatch => { + dispatch(closeAddCardAutoWireToasts()); + // @ts-expect-error — data-fetching.js actions must be converted to TypeScript dispatch(cancelFetchCardData(cardId, dashcardId)); return { dashcardId }; diff --git a/frontend/src/metabase/dashboard/actions/cards.unit.spec.ts b/frontend/src/metabase/dashboard/actions/cards.unit.spec.ts index d4a1854db44..1cedb3bf504 100644 --- a/frontend/src/metabase/dashboard/actions/cards.unit.spec.ts +++ b/frontend/src/metabase/dashboard/actions/cards.unit.spec.ts @@ -9,7 +9,6 @@ import { setupCardQueryMetadataEndpoint, } from "__support__/server-mocks"; import { Api } from "metabase/api"; -import { checkNotNull } from "metabase/lib/types"; import { mainReducers } from "metabase/reducers-main"; import { CardApi } from "metabase/services"; import type { @@ -45,9 +44,14 @@ import { import type { SectionLayout } from "../sections"; import { layoutOptions } from "../sections"; -import { getDashCardById, getDashcards } from "../selectors"; +import { getDashboardById, getDashCardById, getDashcards } from "../selectors"; -import { addSectionToDashboard, replaceCard } from "./cards-typed"; +import type { AddCardToDashboardOpts } from "./cards-typed"; +import { + addCardToDashboard, + addSectionToDashboard, + replaceCard, +} from "./cards-typed"; const DATE_PARAMETER = createMockParameter({ id: "1", @@ -264,17 +268,8 @@ describe("dashboard/actions/cards", () => { ); }); - it("should auto-wire parameters", async () => { + it("should not auto-wire parameters", async () => { const nextCardId = ORDERS_LINE_CHART_CARD.id; - const otherCardParameterMappings = checkNotNull( - PIE_CHART_DASHCARD.parameter_mappings, - ); - const expectedParameterMappings = otherCardParameterMappings.map( - mapping => ({ - ...mapping, - card_id: nextCardId, - }), - ); const { nextDashCard } = await runReplaceCardAction({ dashcardId: TABLE_DASHCARD.id, @@ -282,9 +277,21 @@ describe("dashboard/actions/cards", () => { dashcards: [...DASHCARDS, PIE_CHART_DASHCARD], }); - expect(nextDashCard.parameter_mappings).toEqual( - expectedParameterMappings, - ); + expect(nextDashCard.parameter_mappings).toEqual([]); + }); + }); + + describe("addCardToDashboard", () => { + it("should not auto-wire parameters", async () => { + const { nextDashCard } = await runAddCardToDashboard({ + dashId: DASHBOARD.id, + cardId: ORDERS_LINE_CHART_CARD.id, + tabId: null, + // for auto-wiring + dashcards: [...DASHCARDS, PIE_CHART_DASHCARD], + }); + + expect(nextDashCard.parameter_mappings).toEqual([]); }); }); }); @@ -338,3 +345,28 @@ async function runReplaceCardAction({ cardQueryEndpointSpy, }; } + +async function runAddCardToDashboard({ + dashId, + tabId, + cardId, + ...opts +}: SetupOpts & AddCardToDashboardOpts) { + const { store } = setup(opts); + + await addCardToDashboard({ + dashId, + tabId, + cardId, + })(store.dispatch, store.getState); + const nextState = store.getState(); + + const tempDashCardId = + getDashboardById(nextState, dashId).dashcards.find( + dashcardId => dashcardId < 0, + ) ?? -1; + + return { + nextDashCard: getDashCardById(nextState, tempDashCardId), + }; +} diff --git a/frontend/src/metabase/dashboard/actions/parameters.ts b/frontend/src/metabase/dashboard/actions/parameters.ts index b0fb9bdea61..74aa2331bfb 100644 --- a/frontend/src/metabase/dashboard/actions/parameters.ts +++ b/frontend/src/metabase/dashboard/actions/parameters.ts @@ -2,8 +2,11 @@ import { assoc } from "icepick"; import { t } from "ttag"; import _ from "underscore"; -import { autoWireDashcardsWithMatchingParameters } from "metabase/dashboard/actions/auto-wire-parameters/actions"; -import { closeAutoWireParameterToast } from "metabase/dashboard/actions/auto-wire-parameters/toasts"; +import { showAutoWireToast } from "metabase/dashboard/actions/auto-wire-parameters/actions"; +import { + closeAutoWireParameterToast, + closeAddCardAutoWireToasts, +} from "metabase/dashboard/actions/auto-wire-parameters/toasts"; import { getParameterMappings } from "metabase/dashboard/actions/auto-wire-parameters/utils"; import { updateDashboard } from "metabase/dashboard/actions/save"; import { SIDEBAR_NAME } from "metabase/dashboard/constants"; @@ -28,7 +31,6 @@ import type { ParameterId, ParameterMappingOptions, ParameterTarget, - QuestionDashboardCard, ValuesQueryType, ValuesSourceConfig, ValuesSourceType, @@ -52,6 +54,7 @@ import { getParameters, getParameterValues, getParameterMappingsBeforeEditing, + getSelectedTabId, } from "../selectors"; import { isQuestionDashCard } from "../utils"; @@ -154,6 +157,8 @@ export const REMOVE_PARAMETER = "metabase/dashboard/REMOVE_PARAMETER"; export const removeParameter = createThunkAction( REMOVE_PARAMETER, (parameterId: ParameterId) => (dispatch, getState) => { + dispatch(closeAddCardAutoWireToasts()); + updateParameters(dispatch, getState, parameters => parameters.filter(p => p.id !== parameterId), ); @@ -178,12 +183,10 @@ export const setParameterMapping = createThunkAction( const dashcard = getDashCardById(getState(), dashcardId); if (target !== null && isQuestionDashCard(dashcard)) { + const selectedTabId = getSelectedTabId(getState()); + dispatch( - autoWireDashcardsWithMatchingParameters( - parameterId, - dashcard, - target, - ), + showAutoWireToast(parameterId, dashcard, target, selectedTabId), ); } @@ -192,8 +195,7 @@ export const setParameterMapping = createThunkAction( id: dashcardId, attributes: { parameter_mappings: getParameterMappings( - // TODO remove type casting when getParameterMappings is fixed - dashcard as QuestionDashboardCard, + dashcard, parameterId, cardId, target, -- GitLab