diff --git a/e2e/test/scenarios/dashboard/dashboard.cy.spec.js b/e2e/test/scenarios/dashboard/dashboard.cy.spec.js index 611a12f5b040fbf2add0a80eb3aef0e662360533..abff4256578f81be92c1ef114229b247f31822ee 100644 --- a/e2e/test/scenarios/dashboard/dashboard.cy.spec.js +++ b/e2e/test/scenarios/dashboard/dashboard.cy.spec.js @@ -336,6 +336,9 @@ describe("scenarios > dashboard", () => { cy.log("Should revert the title change if editing is cancelled"); cy.findByTestId("dashboard-name-heading").clear().type(newTitle).blur(); cy.findByTestId("edit-bar").button("Cancel").click(); + modal().within(() => { + cy.button("Leave anyway").click(); + }); cy.findByTestId("edit-bar").should("not.exist"); cy.get("@updateDashboardSpy").should("not.have.been.called"); cy.findByDisplayValue(originalDashboardName); diff --git a/e2e/test/scenarios/question/settings.cy.spec.js b/e2e/test/scenarios/question/settings.cy.spec.js index 7e45d08d3a49df2533909c088e36ab3df3797a60..e20b752dec181e98a5274c536618476c3194b94c 100644 --- a/e2e/test/scenarios/question/settings.cy.spec.js +++ b/e2e/test/scenarios/question/settings.cy.spec.js @@ -5,6 +5,7 @@ import { openNavigationSidebar, visitQuestionAdhoc, popover, + modal, sidebar, moveColumnDown, } from "e2e/support/helpers"; @@ -427,6 +428,9 @@ describe("scenarios > question > settings", () => { cy.contains("Orders in a dashboard").click(); // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage cy.findByText("Cancel").click(); + modal().within(() => { + cy.button("Leave anyway").click(); + }); // create a new question to see if the "add to a dashboard" modal is still there openNavigationSidebar(); diff --git a/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.unit.spec.tsx b/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.unit.spec.tsx index 90ea9fc3a032d3a3d540962342504d9dab40eadd..1de6005f847fa3bb5003a857fa233bd74baa4811 100644 --- a/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.unit.spec.tsx +++ b/frontend/src/metabase/dashboard/containers/DashboardApp/DashboardApp.unit.spec.tsx @@ -161,6 +161,8 @@ describe("DashboardApp", function () { screen.queryAllByTestId("loading-spinner"), ); + userEvent.click(screen.getByLabelText("Edit dashboard")); + history.goBack(); expect( @@ -197,6 +199,41 @@ describe("DashboardApp", function () { ), ).toBeInTheDocument(); }); + + it("does not show custom warning modal when leaving with no changes via Cancel button", async () => { + await setup(); + + userEvent.click(screen.getByLabelText("Edit dashboard")); + + userEvent.click(screen.getByRole("button", { name: "Cancel" })); + + expect( + screen.queryByText("Changes were not saved"), + ).not.toBeInTheDocument(); + expect( + screen.queryByText( + "Navigating away from here will cause you to lose any changes you have made.", + ), + ).not.toBeInTheDocument(); + }); + + it("shows custom warning modal when leaving with unsaved changes via Cancel button", async () => { + await setup(); + + userEvent.click(screen.getByLabelText("Edit dashboard")); + userEvent.click(screen.getByTestId("dashboard-name-heading")); + userEvent.type(screen.getByTestId("dashboard-name-heading"), "a"); + userEvent.tab(); // need to click away from the input to trigger the isDirty flag + + userEvent.click(screen.getByRole("button", { name: "Cancel" })); + + expect(screen.getByText("Changes were not saved")).toBeInTheDocument(); + expect( + screen.getByText( + "Navigating away from here will cause you to lose any changes you have made.", + ), + ).toBeInTheDocument(); + }); }); describe("empty dashboard", () => { diff --git a/frontend/src/metabase/dashboard/containers/DashboardHeader.jsx b/frontend/src/metabase/dashboard/containers/DashboardHeader.jsx index 3ed6901f8c86cc3edecca6ab0b25a9c85bd5ab9f..817108b5ddac2a52c92229a90a746ef4ce3cbcac 100644 --- a/frontend/src/metabase/dashboard/containers/DashboardHeader.jsx +++ b/frontend/src/metabase/dashboard/containers/DashboardHeader.jsx @@ -10,6 +10,8 @@ import { trackExportDashboardToPDF } from "metabase/dashboard/analytics"; import { getIsNavbarOpen } from "metabase/selectors/app"; import ActionButton from "metabase/components/ActionButton"; +import ConfirmContent from "metabase/components/ConfirmContent"; +import Modal from "metabase/components/Modal"; import Button from "metabase/core/components/Button"; import { Icon } from "metabase/core/components/Icon"; import Tooltip from "metabase/core/components/Tooltip"; @@ -81,7 +83,7 @@ class DashboardHeader extends Component { } state = { - modal: null, + showCancelWarning: false, }; static propTypes = { @@ -89,6 +91,7 @@ class DashboardHeader extends Component { fetchPulseFormInput: PropTypes.func.isRequired, formInput: PropTypes.object.isRequired, isAdmin: PropTypes.bool, + isDirty: PropTypes.bool, isEditing: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]) .isRequired, isFullscreen: PropTypes.bool.isRequired, @@ -151,6 +154,10 @@ class DashboardHeader extends Component { this.props.onEditingChange(dashboard); } + handleCancelWarningClose = () => { + this.setState({ showCancelWarning: false }); + }; + handleToggleBookmark() { const { createBookmark, deleteBookmark, isBookmarked } = this.props; @@ -206,10 +213,20 @@ class DashboardHeader extends Component { this.onDoneEditing(); } - async onCancel() { + onRequestCancel = () => { + const { isDirty, isEditing } = this.props; + + if (isDirty && isEditing) { + this.setState({ showCancelWarning: true }); + } else { + this.onCancel(); + } + }; + + onCancel = () => { this.onRevert(); this.onDoneEditing(); - } + }; getEditWarning(dashboard) { if (dashboard.embedding_params) { @@ -234,7 +251,7 @@ class DashboardHeader extends Component { data-metabase-event="Dashboard;Cancel Edits" key="cancel" className="Button Button--small mr1" - onClick={() => this.onCancel()} + onClick={this.onRequestCancel} > {t`Cancel`} </Button>, @@ -502,31 +519,47 @@ class DashboardHeader extends Component { setSidebar, isHomepageDashboard, } = this.props; + const { showCancelWarning } = this.state; const hasLastEditInfo = dashboard["last-edit-info"] != null; return ( - <DashboardHeaderComponent - headerClassName="wrapper" - objectType="dashboard" - analyticsContext="Dashboard" - location={this.props.location} - dashboard={dashboard} - isEditing={isEditing} - isBadgeVisible={!isEditing && !isFullscreen && isAdditionalInfoVisible} - isLastEditInfoVisible={hasLastEditInfo && isAdditionalInfoVisible} - isEditingInfo={isEditing} - isNavBarOpen={this.props.isNavBarOpen} - headerButtons={this.getHeaderButtons()} - editWarning={this.getEditWarning(dashboard)} - editingTitle={t`You're editing this dashboard.`.concat( - isHomepageDashboard - ? t` Remember that this dashboard is set as homepage.` - : "", - )} - editingButtons={this.getEditingButtons()} - setDashboardAttribute={setDashboardAttribute} - onLastEditInfoClick={() => setSidebar({ name: SIDEBAR_NAME.info })} - /> + <> + <DashboardHeaderComponent + headerClassName="wrapper" + objectType="dashboard" + analyticsContext="Dashboard" + location={this.props.location} + dashboard={dashboard} + isEditing={isEditing} + isBadgeVisible={ + !isEditing && !isFullscreen && isAdditionalInfoVisible + } + isLastEditInfoVisible={hasLastEditInfo && isAdditionalInfoVisible} + isEditingInfo={isEditing} + isNavBarOpen={this.props.isNavBarOpen} + headerButtons={this.getHeaderButtons()} + editWarning={this.getEditWarning(dashboard)} + editingTitle={t`You're editing this dashboard.`.concat( + isHomepageDashboard + ? t` Remember that this dashboard is set as homepage.` + : "", + )} + editingButtons={this.getEditingButtons()} + setDashboardAttribute={setDashboardAttribute} + onLastEditInfoClick={() => setSidebar({ name: SIDEBAR_NAME.info })} + /> + + <Modal isOpen={showCancelWarning}> + <ConfirmContent + title={t`Changes were not saved`} + message={t`Navigating away from here will cause you to lose any changes you have made.`} + confirmButtonText={t`Leave anyway`} + cancelButtonText={t`Cancel`} + onClose={this.handleCancelWarningClose} + onAction={this.onCancel} + /> + </Modal> + </> ); } }