Remove submitting from exit dirty form modal (#2820)

* Remove submit from exit dirty form util

* Extract messages

* Change data-test-id attributes

* Add a unit test

* Change given when then to AAA

* Mock packages

* Change button click to user events
This commit is contained in:
Michał Droń 2022-12-16 15:54:41 +01:00 committed by GitHub
parent bba95a8fb4
commit e90a2fd757
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 86 additions and 166 deletions

View file

@ -1,4 +1,4 @@
export const UNSAVED_CHANGES_DIALOG = {
leaveWithoutSavingButton: '[data-test-id="leave-without-saving"]',
saveAndContinueButton: '[data-test-id="save-and-continue"]'
keepEditingButton: '[data-test-id="keep-editing"]',
ignoreChangesButton: '[data-test-id="ignore-changes"]',
};

View file

@ -464,10 +464,6 @@
"1div9r": {
"string": "Search Attribute"
},
"1eEyJv": {
"context": "ExitFormPrompt continue editing button",
"string": "Continue editing"
},
"1gzck6": {
"string": "{firstName} {lastName}"
},
@ -2164,10 +2160,6 @@
"context": "option title",
"string": "Prioritize warehouses by sorting order"
},
"FWWliu": {
"context": "ExitFormPrompt title",
"string": "You have unsaved changes"
},
"FYfoiF": {
"context": "section name",
"string": "Allocation strategy"
@ -3094,10 +3086,6 @@
"context": "table column header",
"string": "Channel threshold"
},
"MPOj0I": {
"context": "ExitFormPrompt confirm button",
"string": "Save changes"
},
"MPfyne": {
"string": "Are you sure you want to send this invoice: {invoiceNumber} to the customer?"
},
@ -4086,10 +4074,6 @@
"context": "navigator placeholder",
"string": "Search Customer"
},
"TtZg/K": {
"context": "ExitFormPrompt title",
"string": "Would you like to save changes?"
},
"U1eJIw": {
"context": "order history message",
"string": "Products were added to an order"
@ -5187,6 +5171,10 @@
"context": "ProductTypeDeleteWarningDialog single consent label",
"string": "Yes, I want to delete this product type and assigned products"
},
"blZJmA": {
"context": "ExitFormPrompt ignore changes button",
"string": "Ignore changes"
},
"bp/i0x": {
"context": "header",
"string": "Quantity"
@ -5223,6 +5211,10 @@
"c4gbXr": {
"string": "Draft order successfully finalized"
},
"c5fFin": {
"context": "ExitFormPrompt title",
"string": "Leave without saving changes?"
},
"c5pMZ8": {
"string": "API error"
},
@ -7137,10 +7129,6 @@
"context": "table header column",
"string": "Show gross prices in storefront"
},
"sn2awN": {
"context": "ExitFormPrompt cancel button",
"string": "Discard changes"
},
"snUby7": {
"context": "header",
"string": "Unnamed Webhook Details"
@ -7855,6 +7843,10 @@
"context": "delete shipping method",
"string": "Are you sure you want to delete {name}?"
},
"ySqncG": {
"context": "ExitFormPrompt keep editing button",
"string": "Keep editing"
},
"ySqrUU": {
"context": "button",
"string": "Assign variants"

View file

@ -0,0 +1,53 @@
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import ExitFormDialog from "./ExitFormDialog";
jest.mock("react-intl", () => ({
useIntl: jest.fn(() => ({
formatMessage: jest.fn(x => x.defaultMessage),
})),
defineMessages: jest.fn(x => x),
}));
jest.mock("@saleor/macaw-ui", () => ({
useStyles: jest.fn(() => () => ({})),
makeStyles: jest.fn(() => () => ({})),
DialogHeader: jest.fn(() => () => <></>),
}));
describe("ExitFormDialog", () => {
it("closes when ignore changes is clicked", async () => {
// Arrange
const props = {
onClose: jest.fn(),
onLeave: jest.fn(),
isOpen: true,
};
const user = userEvent.setup();
// Act
const { getByTestId } = render(<ExitFormDialog {...props} />);
await user.click(getByTestId("ignore-changes"));
// Assert
expect(props.onLeave).toHaveBeenCalled();
});
it("closes when keep editing is clicked", async () => {
// Arrange
const props = {
onClose: jest.fn(),
onLeave: jest.fn(),
isOpen: true,
};
const user = userEvent.setup();
// Act
const { getByTestId } = render(<ExitFormDialog {...props} />);
await user.click(getByTestId("keep-editing"));
// Assert
expect(props.onClose).toHaveBeenCalled();
});
});

View file

@ -1,6 +1,6 @@
import { Button, Dialog, DialogContent, makeStyles } from "@material-ui/core";
import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer";
import CardTitle from "@saleor/components/CardTitle";
import { DialogHeader } from "@saleor/macaw-ui";
import React from "react";
import { useIntl } from "react-intl";
@ -30,47 +30,37 @@ const useStyles = makeStyles(
);
interface ExitFormDialogProps {
onSubmit: () => void;
onClose: () => void;
onLeave: () => void;
isOpen: boolean;
isSubmitDisabled: boolean;
}
const ExitFormDialog: React.FC<ExitFormDialogProps> = ({
onSubmit,
onLeave,
onClose,
isOpen,
isSubmitDisabled,
}) => {
const classes = useStyles();
const intl = useIntl();
return (
<Dialog className={classes.container} open={isOpen} onClose={onClose}>
<CardTitle
title={intl.formatMessage(
isSubmitDisabled ? messages.unableToSaveTitle : messages.title,
)}
/>
<DialogHeader onClose={onClose}>
{intl.formatMessage(messages.unableToSaveTitle)}
</DialogHeader>
<DialogContent className={classes.dialogContent}>
<div className={classes.buttonsContainer}>
<Button onClick={onLeave} data-test-id="leave-without-saving">
{intl.formatMessage(messages.cancelButton)}
<Button onClick={onClose} data-test-id="keep-editing">
{intl.formatMessage(messages.keepEditing)}
</Button>
<HorizontalSpacer />
<Button
variant="contained"
color="primary"
onClick={isSubmitDisabled ? onClose : onSubmit}
data-test-id="save-and-continue"
onClick={onLeave}
data-test-id="ignore-changes"
>
{intl.formatMessage(
isSubmitDisabled
? messages.continueEditingButton
: messages.confirmButton,
)}
{intl.formatMessage(messages.ignoreChanges)}
</Button>
</div>
</DialogContent>

View file

@ -13,7 +13,6 @@ export interface ExitFormDialogData {
setEnableExitDialog: (value: boolean) => void;
shouldBlockNavigation: () => boolean;
setIsSubmitting: (value: boolean) => void;
submit: () => SubmitPromise;
leave: () => void;
setIsSubmitDisabled: (value: boolean) => void;
}
@ -41,7 +40,6 @@ export const ExitFormDialogContext = React.createContext<ExitFormDialogData>({
setExitDialogSubmitRef: () => undefined,
shouldBlockNavigation: () => false,
setIsSubmitting: () => undefined,
submit: () => Promise.resolve([]),
leave: () => undefined,
setIsSubmitDisabled: () => undefined,
});
@ -51,7 +49,6 @@ const defaultValues = {
showDialog: false,
blockNav: true,
navAction: null,
submit: null,
enableExitDialog: false,
isSubmitting: false,
formsData: {},
@ -203,37 +200,6 @@ export function useExitFormDialogProvider() {
setStateDefaultValues();
};
const getDirtyFormsSubmitFn = () =>
getFormsDataValuesArray()
.filter(({ isDirty }) => isDirty)
.map(({ submitFn }) => submitFn);
const hasAnySubmitFn = () =>
getFormsDataValuesArray().some(({ submitFn }) => !!submitFn);
const handleSubmit = async () => {
if (!hasAnySubmitFn()) {
return;
}
setShowDialog(false);
setIsSubmitting(true);
const errors = await Promise.all(
getDirtyFormsSubmitFn().map(submitFn => submitFn()),
);
setIsSubmitting(false);
const isError = errors.flat().some(errors => errors);
if (!isError) {
continueNavigation();
}
setDefaultNavAction();
};
const handleLeave = () => {
continueNavigation();
};
@ -253,7 +219,6 @@ export function useExitFormDialogProvider() {
setEnableExitDialog,
setExitDialogSubmitRef: setSubmitRef,
setIsSubmitting,
submit: handleSubmit,
setIsSubmitDisabled,
leave: handleLeave,
};
@ -261,7 +226,6 @@ export function useExitFormDialogProvider() {
return {
providerData,
showDialog,
handleSubmit,
handleLeave,
handleClose,
shouldBlockNav,
@ -273,11 +237,9 @@ const ExitFormDialogProvider = ({ children }) => {
const {
handleClose,
handleLeave,
handleSubmit,
providerData,
showDialog,
shouldBlockNav,
isSubmitDisabled,
} = useExitFormDialogProvider();
useBeforeUnload(e => {
@ -293,10 +255,8 @@ const ExitFormDialogProvider = ({ children }) => {
<ExitFormDialogContext.Provider value={providerData}>
<ExitFormDialog
isOpen={showDialog}
onSubmit={handleSubmit}
onLeave={handleLeave}
onClose={handleClose}
isSubmitDisabled={isSubmitDisabled.current}
/>
{children}
</ExitFormDialogContext.Provider>

View file

@ -1,29 +1,19 @@
import { defineMessages } from "react-intl";
export const exitFormPromptMessages = defineMessages({
title: {
id: "TtZg/K",
defaultMessage: "Would you like to save changes?",
description: "ExitFormPrompt title",
},
unableToSaveTitle: {
id: "FWWliu",
defaultMessage: "You have unsaved changes",
id: "c5fFin",
defaultMessage: "Leave without saving changes?",
description: "ExitFormPrompt title",
},
cancelButton: {
id: "sn2awN",
defaultMessage: "Discard changes",
description: "ExitFormPrompt cancel button",
keepEditing: {
id: "ySqncG",
defaultMessage: "Keep editing",
description: "ExitFormPrompt keep editing button",
},
confirmButton: {
id: "MPOj0I",
defaultMessage: "Save changes",
description: "ExitFormPrompt confirm button",
},
continueEditingButton: {
id: "1eEyJv",
defaultMessage: "Continue editing",
description: "ExitFormPrompt continue editing button",
ignoreChanges: {
id: "blZJmA",
defaultMessage: "Ignore changes",
description: "ExitFormPrompt ignore changes button",
},
});

View file

@ -86,71 +86,6 @@ describe("useExitFormDialog", () => {
expect(result.current.exit.shouldBlockNavigation()).toBe(false);
expect(result.current.history.location.pathname).toBe(targetPath);
});
it("blocks navigation if an error occured", async () => {
// Given
const submitFn = jest.fn(() =>
Promise.resolve([{ field: "field", code: "code" }]),
);
const { result } = setup(submitFn);
// When
act(() => {
result.current.form.change({
target: { name: "field", value: "something" },
});
result.current.history.push(targetPath);
});
await act(() => result.current.exit.submit());
// Then
expect(result.current.history.location.pathname).toBe(initialPath);
});
it("allows navigation if an error occured, but confirmation is not needed", async () => {
// Given
const submitFn = jest.fn(() =>
Promise.resolve([{ field: "field", code: "code" }]),
);
const { result } = setup(submitFn, false);
// When
act(() => {
result.current.form.change({
target: { name: "field", value: "something" },
});
result.current.history.push(targetPath);
});
await act(() => result.current.exit.submit());
// Then
expect(result.current.history.location.pathname).toBe(targetPath);
});
it("blocks navigation if an error occured and user tries to leave anyway", async () => {
// Given
const submitFn = jest.fn(() =>
Promise.resolve([{ field: "field", code: "code" }]),
);
const { result } = setup(submitFn);
// When
act(() => {
result.current.form.change({
target: { name: "field", value: "something" },
});
result.current.history.push(targetPath);
});
await act(() => result.current.exit.submit());
act(() => {
result.current.history.push(targetPath);
});
// Then
expect(result.current.exit.shouldBlockNavigation()).toBe(true);
expect(result.current.history.location.pathname).toBe(initialPath);
});
it("navigates to full url with querystring", async () => {
// Given
const submitFn = jest.fn(() => Promise.resolve([]));