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 = {
|
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"]',
|
||||||
};
|
};
|
||||||
|
|
|
@ -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"
|
||||||
|
|
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 { 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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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([]));
|
||||||
|
|
Loading…
Reference in a new issue