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:
parent
bba95a8fb4
commit
e90a2fd757
7 changed files with 86 additions and 166 deletions
|
@ -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"]',
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
53
src/components/Form/ExitFormDialog.test.tsx
Normal file
53
src/components/Form/ExitFormDialog.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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([]));
|
||||
|
|
Loading…
Reference in a new issue