Skip to content

Commit

Permalink
Merge pull request #4551 from mirumee/fix/display-menu-errors
Browse files Browse the repository at this point in the history
Display menu item form errors
  • Loading branch information
maarcingebala committed Jul 30, 2019
2 parents 216fbbd + 60a5009 commit 4a75513
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 20 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ All notable, unreleased changes to this project will be documented in this file.
- Input UI changes - #4542 by @benekex2
- Fix rendering user avatar when it's null #4546 by @maarcingebala
- Do not lose focus while typing in product description field - #4549 by @dominik-zeglen

- Update JSON menu representation in mutations - #4524 by @maarcingebala
- Display menu item form errors - #4551 by @dominik-zeglen

## 2.8.0

Expand Down
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"@babel/runtime": "^7.5.4",
"@storybook/addon-storyshots": "^5.1.9",
"@storybook/react": "^5.1.9",
"@testing-library/react-hooks": "^1.1.0",
"@types/draft-js": "^0.10.34",
"@types/i18next": "^8.4.6",
"@types/jest": "^23.3.14",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./useModalDialogErrors";
export * from "./useModalDialogErrors";
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { renderHook } from "@testing-library/react-hooks";

import useModalDialogErrors from "./useModalDialogErrors";

const errors = ["err1", "err2"];

test("Does not render errors after close", () => {
const { result, rerender } = renderHook(
({ errors, open }) => useModalDialogErrors(errors, open),
{
initialProps: {
errors: [] as string[],
open: false
}
}
);

// Open modal
rerender({
errors: [],
open: true
});
expect(result.current.length).toBe(0);

// Throw errors
rerender({
errors,
open: true
});
expect(result.current.length).toBe(2);

// Close modal
rerender({
errors,
open: false
});

// Open modal
rerender({
errors,
open: true
});
expect(result.current.length).toBe(0);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useState } from "react";

import useStateFromProps from "../useStateFromProps";

function useModalDialogErrors<TError>(
errors: TError[],
open: boolean
): TError[] {
const [state, setState] = useStateFromProps(errors);
const [prevOpenState, setPrevOpenstate] = useState(open);

if (open !== prevOpenState) {
setPrevOpenstate(open);
if (!open) {
setState([]);
}
}

return state;
}

export default useModalDialogErrors;
8 changes: 5 additions & 3 deletions saleor/static/dashboard-next/hooks/useStateFromProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ export interface UseStateFromPropsOpts<T> {

function useStateFromProps<T>(
data: T,
opts: UseStateFromPropsOpts<T>
opts?: UseStateFromPropsOpts<T>
): [T, Dispatch<SetStateAction<T>>] {
const [state, setState] = useState(data);
const [prevData, setPrevData] = useState(data);

const { mergeFunc, onRefresh } = opts;
if (!opts) {
opts = {};
}

if (!isEqual(prevData, data)) {
const { mergeFunc, onRefresh } = opts;
const newData =
typeof mergeFunc === "function" ? mergeFunc(prevData, state, data) : data;
setState(newData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import isUrl from "is-url";
import React from "react";

Expand All @@ -12,11 +13,15 @@ import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import FormSpacer from "@saleor/components/FormSpacer";
import { SearchCategories_categories_edges_node } from "../../../containers/SearchCategories/types/SearchCategories";
import { SearchCollections_collections_edges_node } from "../../../containers/SearchCollections/types/SearchCollections";
import { SearchPages_pages_edges_node } from "../../../containers/SearchPages/types/SearchPages";
import i18n from "../../../i18n";
import { getMenuItemByValue, IMenu } from "../../../utils/menu";
import { SearchCategories_categories_edges_node } from "@saleor/containers/SearchCategories/types/SearchCategories";
import { SearchCollections_collections_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections";
import { SearchPages_pages_edges_node } from "@saleor/containers/SearchPages/types/SearchPages";
import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import i18n from "@saleor/i18n";
import { UserError } from "@saleor/types";
import { getErrors, getFieldError } from "@saleor/utils/errors";
import { getMenuItemByValue, IMenu } from "@saleor/utils/menu";

export type MenuItemType = "category" | "collection" | "link" | "page";
export interface MenuItemData {
Expand All @@ -31,6 +36,7 @@ export interface MenuItemDialogFormData extends MenuItemData {
export interface MenuItemDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
disabled: boolean;
errors: UserError[];
initial?: MenuItemDialogFormData;
initialDisplayValue?: string;
loading: boolean;
Expand Down Expand Up @@ -68,6 +74,7 @@ function getDisplayValue(menu: IMenu, value: string): string {
const MenuItemDialog: React.StatelessComponent<MenuItemDialogProps> = ({
confirmButtonState,
disabled,
errors: apiErrors,
initial,
initialDisplayValue,
loading,
Expand All @@ -79,10 +86,11 @@ const MenuItemDialog: React.StatelessComponent<MenuItemDialogProps> = ({
collections,
pages
}) => {
const errors = useModalDialogErrors(apiErrors, open);
const [displayValue, setDisplayValue] = React.useState(
initialDisplayValue || ""
);
const [data, setData] = React.useState<MenuItemDialogFormData>(
const [data, setData] = useStateFromProps<MenuItemDialogFormData>(
initial || defaultInitial
);
const [url, setUrl] = React.useState<string>(undefined);
Expand All @@ -98,6 +106,8 @@ const MenuItemDialog: React.StatelessComponent<MenuItemDialogProps> = ({
setUrl(undefined);
}, [open]);

const mutationErrors = getErrors(errors);

let options: IMenu = [];

if (categories.length > 0) {
Expand Down Expand Up @@ -192,6 +202,10 @@ const MenuItemDialog: React.StatelessComponent<MenuItemDialogProps> = ({

const handleSubmit = () => onSubmit(data);

const idError = ["category", "collection", "page", "url"]
.map(field => getFieldError(errors, field))
.reduce((acc, err) => acc || err);

return (
<Dialog
onClose={onClose}
Expand All @@ -203,9 +217,13 @@ const MenuItemDialog: React.StatelessComponent<MenuItemDialogProps> = ({
}}
>
<DialogTitle>
{i18n.t("Add Item", {
context: "create new menu item"
})}
{!!initial
? i18n.t("Edit Item", {
context: "edit menu item"
})
: i18n.t("Add Item", {
context: "create new menu item"
})}
</DialogTitle>
<DialogContent style={{ overflowY: "visible" }}>
<TextField
Expand All @@ -220,22 +238,31 @@ const MenuItemDialog: React.StatelessComponent<MenuItemDialogProps> = ({
}))
}
name="name"
helperText=""
error={!!getFieldError(errors, "name")}
helperText={getFieldError(errors, "name")}
/>
<FormSpacer />
<AutocompleteSelectMenu
disabled={disabled}
onChange={handleSelectChange}
name="id"
helperText=""
label={i18n.t("Link")}
displayValue={displayValue}
loading={loading}
error={false}
options={options}
error={!!idError}
helperText={idError}
placeholder={i18n.t("Start typing to begin search...")}
onInputChange={handleQueryChange}
/>
{mutationErrors.length > 0 && (
<>
<FormSpacer />
{mutationErrors.map(err => (
<Typography color="error">{err}</Typography>
))}
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,12 @@ const MenuDetails: React.FC<MenuDetailsProps> = ({ id, params }) => {
open={params.action === "add-item"}
categories={categories}
collections={collections}
errors={maybe(
() =>
menuItemCreateOpts.data
.menuItemCreate.errors,
[]
)}
pages={pages}
loading={
categorySearch.result.loading ||
Expand Down Expand Up @@ -355,6 +361,12 @@ const MenuDetails: React.FC<MenuDetailsProps> = ({ id, params }) => {
open={params.action === "edit-item"}
categories={categories}
collections={collections}
errors={maybe(
() =>
menuItemUpdateOpts.data
.menuItemUpdate.errors,
[]
)}
pages={pages}
initial={initialFormData}
initialDisplayValue={getInitialDisplayValue(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function getInitialDisplayValue(item: MenuDetails_menu_items): string {
} else if (item.url) {
return item.url;
} else {
throw unknownTypeError;
return "";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6140,7 +6140,19 @@ exports[`Storyshots Navigation / Menu create loading 1`] = `
/>
`;

exports[`Storyshots Navigation / Menu item create default 1`] = `
exports[`Storyshots Navigation / Menu item default 1`] = `
<div
style="padding:24px"
/>
`;

exports[`Storyshots Navigation / Menu item edit 1`] = `
<div
style="padding:24px"
/>
`;

exports[`Storyshots Navigation / Menu item errors 1`] = `
<div
style="padding:24px"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { storiesOf } from "@storybook/react";
import React from "react";

import { formError } from "@saleor/storybook/misc";
import MenuItemDialog, {
MenuItemDialogProps
} from "../../../navigation/components/MenuItemDialog";
Expand All @@ -22,6 +23,7 @@ const props: MenuItemDialogProps = {
collections: [],
confirmButtonState: "default",
disabled: false,
errors: [],
loading: false,
onClose: () => undefined,
onQueryChange: () => undefined,
Expand All @@ -30,6 +32,22 @@ const props: MenuItemDialogProps = {
pages: []
};

storiesOf("Navigation / Menu item create", module)
storiesOf("Navigation / Menu item", module)
.addDecorator(Decorator)
.add("default", () => <MenuItemDialog {...props} />);
.add("default", () => <MenuItemDialog {...props} />)
.add("edit", () => (
<MenuItemDialog
{...props}
initial={{
...props.categories[0],
type: "category"
}}
initialDisplayValue={props.categories[0].name}
/>
))
.add("errors", () => (
<MenuItemDialog
{...props}
errors={["", "", "name", "category"].map(formError)}
/>
));
14 changes: 14 additions & 0 deletions saleor/static/dashboard-next/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { maybe } from "@saleor/misc";
import { UserError } from "@saleor/types";

export function getFieldError(errors: UserError[], field: string): string {
const err = errors.find(err => err.field === field);

return maybe(() => err.message);
}

export function getErrors(errors: UserError[]): string[] {
return errors
.filter(err => ["", null].includes(err.field))
.map(err => err.message);
}

0 comments on commit 4a75513

Please sign in to comment.