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 = { export const UNSAVED_CHANGES_DIALOG = {
leaveWithoutSavingButton: '[data-test-id="leave-without-saving"]', keepEditingButton: '[data-test-id="keep-editing"]',
saveAndContinueButton: '[data-test-id="save-and-continue"]' ignoreChangesButton: '[data-test-id="ignore-changes"]',
}; };

View file

@ -464,10 +464,6 @@
"1div9r": { "1div9r": {
"string": "Search Attribute" "string": "Search Attribute"
}, },
"1eEyJv": {
"context": "ExitFormPrompt continue editing button",
"string": "Continue editing"
},
"1gzck6": { "1gzck6": {
"string": "{firstName} {lastName}" "string": "{firstName} {lastName}"
}, },
@ -2164,10 +2160,6 @@
"context": "option title", "context": "option title",
"string": "Prioritize warehouses by sorting order" "string": "Prioritize warehouses by sorting order"
}, },
"FWWliu": {
"context": "ExitFormPrompt title",
"string": "You have unsaved changes"
},
"FYfoiF": { "FYfoiF": {
"context": "section name", "context": "section name",
"string": "Allocation strategy" "string": "Allocation strategy"
@ -3094,10 +3086,6 @@
"context": "table column header", "context": "table column header",
"string": "Channel threshold" "string": "Channel threshold"
}, },
"MPOj0I": {
"context": "ExitFormPrompt confirm button",
"string": "Save changes"
},
"MPfyne": { "MPfyne": {
"string": "Are you sure you want to send this invoice: {invoiceNumber} to the customer?" "string": "Are you sure you want to send this invoice: {invoiceNumber} to the customer?"
}, },
@ -4086,10 +4074,6 @@
"context": "navigator placeholder", "context": "navigator placeholder",
"string": "Search Customer" "string": "Search Customer"
}, },
"TtZg/K": {
"context": "ExitFormPrompt title",
"string": "Would you like to save changes?"
},
"U1eJIw": { "U1eJIw": {
"context": "order history message", "context": "order history message",
"string": "Products were added to an order" "string": "Products were added to an order"
@ -5187,6 +5171,10 @@
"context": "ProductTypeDeleteWarningDialog single consent label", "context": "ProductTypeDeleteWarningDialog single consent label",
"string": "Yes, I want to delete this product type and assigned products" "string": "Yes, I want to delete this product type and assigned products"
}, },
"blZJmA": {
"context": "ExitFormPrompt ignore changes button",
"string": "Ignore changes"
},
"bp/i0x": { "bp/i0x": {
"context": "header", "context": "header",
"string": "Quantity" "string": "Quantity"
@ -5223,6 +5211,10 @@
"c4gbXr": { "c4gbXr": {
"string": "Draft order successfully finalized" "string": "Draft order successfully finalized"
}, },
"c5fFin": {
"context": "ExitFormPrompt title",
"string": "Leave without saving changes?"
},
"c5pMZ8": { "c5pMZ8": {
"string": "API error" "string": "API error"
}, },
@ -7137,10 +7129,6 @@
"context": "table header column", "context": "table header column",
"string": "Show gross prices in storefront" "string": "Show gross prices in storefront"
}, },
"sn2awN": {
"context": "ExitFormPrompt cancel button",
"string": "Discard changes"
},
"snUby7": { "snUby7": {
"context": "header", "context": "header",
"string": "Unnamed Webhook Details" "string": "Unnamed Webhook Details"
@ -7855,6 +7843,10 @@
"context": "delete shipping method", "context": "delete shipping method",
"string": "Are you sure you want to delete {name}?" "string": "Are you sure you want to delete {name}?"
}, },
"ySqncG": {
"context": "ExitFormPrompt keep editing button",
"string": "Keep editing"
},
"ySqrUU": { "ySqrUU": {
"context": "button", "context": "button",
"string": "Assign variants" "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 { Button, Dialog, DialogContent, makeStyles } from "@material-ui/core";
import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer"; import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer";
import CardTitle from "@saleor/components/CardTitle"; import { DialogHeader } from "@saleor/macaw-ui";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -30,47 +30,37 @@ const useStyles = makeStyles(
); );
interface ExitFormDialogProps { interface ExitFormDialogProps {
onSubmit: () => void;
onClose: () => void; onClose: () => void;
onLeave: () => void; onLeave: () => void;
isOpen: boolean; isOpen: boolean;
isSubmitDisabled: boolean;
} }
const ExitFormDialog: React.FC<ExitFormDialogProps> = ({ const ExitFormDialog: React.FC<ExitFormDialogProps> = ({
onSubmit,
onLeave, onLeave,
onClose, onClose,
isOpen, isOpen,
isSubmitDisabled,
}) => { }) => {
const classes = useStyles(); const classes = useStyles();
const intl = useIntl(); const intl = useIntl();
return ( return (
<Dialog className={classes.container} open={isOpen} onClose={onClose}> <Dialog className={classes.container} open={isOpen} onClose={onClose}>
<CardTitle <DialogHeader onClose={onClose}>
title={intl.formatMessage( {intl.formatMessage(messages.unableToSaveTitle)}
isSubmitDisabled ? messages.unableToSaveTitle : messages.title, </DialogHeader>
)}
/>
<DialogContent className={classes.dialogContent}> <DialogContent className={classes.dialogContent}>
<div className={classes.buttonsContainer}> <div className={classes.buttonsContainer}>
<Button onClick={onLeave} data-test-id="leave-without-saving"> <Button onClick={onClose} data-test-id="keep-editing">
{intl.formatMessage(messages.cancelButton)} {intl.formatMessage(messages.keepEditing)}
</Button> </Button>
<HorizontalSpacer /> <HorizontalSpacer />
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={isSubmitDisabled ? onClose : onSubmit} onClick={onLeave}
data-test-id="save-and-continue" data-test-id="ignore-changes"
> >
{intl.formatMessage( {intl.formatMessage(messages.ignoreChanges)}
isSubmitDisabled
? messages.continueEditingButton
: messages.confirmButton,
)}
</Button> </Button>
</div> </div>
</DialogContent> </DialogContent>

View file

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

View file

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

View file

@ -86,71 +86,6 @@ describe("useExitFormDialog", () => {
expect(result.current.exit.shouldBlockNavigation()).toBe(false); expect(result.current.exit.shouldBlockNavigation()).toBe(false);
expect(result.current.history.location.pathname).toBe(targetPath); 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 () => { it("navigates to full url with querystring", async () => {
// Given // Given
const submitFn = jest.fn(() => Promise.resolve([])); const submitFn = jest.fn(() => Promise.resolve([]));