Skip to content
Snippets Groups Projects
Unverified Commit 4fab2910 authored by Kamil Mielnik's avatar Kamil Mielnik Committed by GitHub
Browse files

Show custom warning message when leaving editing model via Cancel button (#34487)

* Show a warning modal when clicking "Cancel" while editing a model with unsaved changes

* Add tests for cancel button when editing metadata

* Add tests for cancel button when editing query definition

* Update e2e test

* Update e2e tests

* Update code after merge

* Remove todo

* Inverse hasFieldWithoutDisplayName logic to everyFieldHasDisplayName
parent 3a4e6b2b
No related branches found
No related tags found
No related merge requests found
import {
modal,
restore,
rightSidebar,
visualize,
......@@ -81,6 +82,9 @@ describe("scenarios > models metadata", () => {
setColumnType("No special type", "Cost");
cy.button("Cancel").click();
modal().within(() => {
cy.button("Leave anyway").click();
});
cy.findByTestId("TableInteractive-root").findByText("Subtotal");
});
......
import {
modal,
restore,
runNativeQuery,
summarize,
......@@ -89,6 +90,9 @@ describe("scenarios > models query editor", () => {
.and("not.contain", "109.22");
cy.button("Cancel").click();
modal().within(() => {
cy.button("Leave anyway").click();
});
cy.wait("@cardQuery");
cy.url()
......@@ -187,6 +191,9 @@ describe("scenarios > models query editor", () => {
.and("not.contain", "109.22");
cy.button("Cancel").click();
modal().within(() => {
cy.button("Leave anyway").click();
});
cy.wait("@cardQuery");
cy.get(".cellData").should("contain", "37.65").and("contain", "109.22");
......
......@@ -10,6 +10,8 @@ import ActionButton from "metabase/components/ActionButton";
import Button from "metabase/core/components/Button";
import DebouncedFrame from "metabase/components/DebouncedFrame";
import Confirm from "metabase/components/Confirm";
import { LeaveConfirmationModalContent } from "metabase/components/LeaveConfirmationModal";
import Modal from "metabase/components/Modal";
import QueryVisualization from "metabase/query_builder/components/QueryVisualization";
import ViewSidebar from "metabase/query_builder/components/view/ViewSidebar";
......@@ -200,6 +202,8 @@ function DatasetEditor(props) {
modelIndexes = [],
} = props;
const isDirty = isModelQueryDirty || isMetadataDirty;
const [showCancelEditWarning, setShowCancelEditWarning] = useState(false);
const fields = useMemo(
() => getSortedModelFields(dataset, resultsMetadata?.columns),
[dataset, resultsMetadata],
......@@ -299,14 +303,22 @@ function DatasetEditor(props) {
[initialEditorHeight, setDatasetEditorTab],
);
const handleCancelCreate = useCallback(() => {
onCancelCreateNewModel();
}, [onCancelCreateNewModel]);
const handleCancelEdit = useCallback(() => {
const handleCancelEdit = () => {
onCancelDatasetChanges();
setQueryBuilderMode("view");
}, [setQueryBuilderMode, onCancelDatasetChanges]);
};
const handleCancelEditWarningClose = () => {
setShowCancelEditWarning(false);
};
const handleRequestCancelEdit = () => {
if (isDirty) {
setShowCancelEditWarning(true);
} else {
handleCancelEdit();
}
};
const handleSave = useCallback(async () => {
const canBeDataset = checkCanBeModel(dataset);
......@@ -392,11 +404,9 @@ function DatasetEditor(props) {
if (dataset.query().isEmpty()) {
return false;
}
const hasFieldWithoutDisplayName = fields.some(f => !f.display_name);
return (
!hasFieldWithoutDisplayName && (isModelQueryDirty || isMetadataDirty)
);
}, [dataset, fields, isModelQueryDirty, isMetadataDirty]);
const everyFieldHasDisplayName = fields.every(field => field.display_name);
return everyFieldHasDisplayName && isDirty;
}, [dataset, fields, isDirty]);
const sidebar = getSidebar(
{ ...props, modelIndexes },
......@@ -435,12 +445,12 @@ function DatasetEditor(props) {
<Button
key="cancel"
small
onClick={handleCancelEdit}
onClick={handleRequestCancelEdit}
>{t`Cancel`}</Button>
) : (
<Confirm
key="cancel"
action={handleCancelCreate}
action={onCancelCreateNewModel}
title={t`Discard changes?`}
message={t`Your model won't be created.`}
confirmButtonText={t`Discard`}
......@@ -505,6 +515,13 @@ function DatasetEditor(props) {
{sidebar}
</ViewSidebar>
</Root>
<Modal isOpen={showCancelEditWarning}>
<LeaveConfirmationModalContent
onAction={handleCancelEdit}
onClose={handleCancelEditWarningClose}
/>
</Modal>
</>
);
}
......
......@@ -606,6 +606,63 @@ describe("QueryBuilder", () => {
).not.toBeInTheDocument();
});
it("shows custom warning modal when leaving edited query via Cancel button", async () => {
const { history } = await setup({
card: TEST_MODEL_CARD,
initialRoute: "/home",
});
history.push(`/model/${TEST_MODEL_CARD.id}/query`);
await waitForLoaderToBeRemoved();
const rowLimitInput = await within(
screen.getByTestId("step-limit-0-0"),
).findByPlaceholderText("Enter a limit");
userEvent.click(rowLimitInput);
userEvent.type(rowLimitInput, "0");
await waitFor(() => {
expect(rowLimitInput).toHaveValue(10);
});
userEvent.tab();
userEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(screen.getByTestId("leave-confirmation")).toBeInTheDocument();
});
it("does not show custom warning modal when leaving unedited query via Cancel button", async () => {
const { history } = await setup({
card: TEST_MODEL_CARD,
initialRoute: "/home",
});
history.push(`/model/${TEST_MODEL_CARD.id}/query`);
await waitForLoaderToBeRemoved();
const rowLimitInput = await within(
screen.getByTestId("step-limit-0-0"),
).findByPlaceholderText("Enter a limit");
userEvent.click(rowLimitInput);
userEvent.type(rowLimitInput, "0");
userEvent.tab();
userEvent.click(rowLimitInput);
userEvent.type(rowLimitInput, "{backspace}");
userEvent.tab();
userEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(
screen.queryByTestId("leave-confirmation"),
).not.toBeInTheDocument();
});
it("does not show custom warning modal when saving edited query", async () => {
const { history } = await setup({
card: TEST_MODEL_CARD,
......@@ -729,6 +786,85 @@ describe("QueryBuilder", () => {
).not.toBeInTheDocument();
});
it("does not show custom warning modal when leaving with no changes via Cancel button", async () => {
await setup({
card: TEST_MODEL_CARD,
dataset: TEST_MODEL_DATASET,
initialRoute: `/model/${TEST_MODEL_CARD.id}/metadata`,
});
await waitForLoaderToBeRemoved();
const columnDisplayName = await screen.findByTitle("Display name");
userEvent.click(columnDisplayName);
userEvent.type(columnDisplayName, "X");
await waitFor(() => {
expect(columnDisplayName).toHaveValue(
`${TEST_MODEL_DATASET_COLUMN.display_name}X`,
);
});
userEvent.tab();
userEvent.click(columnDisplayName);
userEvent.type(columnDisplayName, "{backspace}");
await waitFor(() => {
expect(columnDisplayName).toHaveValue(
TEST_MODEL_DATASET_COLUMN.display_name,
);
});
userEvent.tab();
await waitFor(() => {
expect(
screen.getByRole("button", { name: "Save changes" }),
).toBeDisabled();
});
userEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(
screen.queryByTestId("leave-confirmation"),
).not.toBeInTheDocument();
});
it("shows custom warning modal when leaving with unsaved changes via Cancel button", async () => {
await setup({
card: TEST_MODEL_CARD,
dataset: TEST_MODEL_DATASET,
initialRoute: `/model/${TEST_MODEL_CARD.id}/metadata`,
});
await waitForLoaderToBeRemoved();
const columnDisplayName = await screen.findByTitle("Display name");
userEvent.click(columnDisplayName);
userEvent.type(columnDisplayName, "X");
await waitFor(() => {
expect(columnDisplayName).toHaveValue(
`${TEST_MODEL_DATASET_COLUMN.display_name}X`,
);
});
userEvent.tab();
await waitFor(() => {
expect(
screen.getByRole("button", { name: "Save changes" }),
).toBeEnabled();
});
userEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(screen.getByTestId("leave-confirmation")).toBeInTheDocument();
});
it("does not show custom warning modal when saving edited metadata", async () => {
const { history } = await setup({
card: TEST_MODEL_CARD,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment