Add rich text editor to category pages

This commit is contained in:
dominik-zeglen 2020-11-03 17:17:47 +01:00
parent 28cc74c954
commit 9adde24a48
5 changed files with 391 additions and 268 deletions

View file

@ -2,43 +2,23 @@ import AppHeader from "@saleor/components/AppHeader";
import { CardSpacer } from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Metadata, { MetadataFormData } from "@saleor/components/Metadata";
import Metadata from "@saleor/components/Metadata";
import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import { sectionNames } from "@saleor/intl";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { ContentState, convertToRaw, RawDraftContentState } from "draft-js";
import React from "react";
import { useIntl } from "react-intl";
import CategoryDetailsForm from "../../components/CategoryDetailsForm";
export interface FormData extends MetadataFormData {
description: RawDraftContentState;
name: string;
slug: string;
seoTitle: string;
seoDescription: string;
}
const initialData: FormData = {
description: convertToRaw(ContentState.createFromText("")),
metadata: [],
name: "",
privateMetadata: [],
seoDescription: "",
seoTitle: "",
slug: ""
};
import CategoryCreateForm, { CategoryCreateData } from "./form";
export interface CategoryCreatePageProps {
errors: ProductErrorFragment[];
disabled: boolean;
saveButtonBarState: ConfirmButtonTransitionState;
onSubmit(data: FormData);
onSubmit(data: CategoryCreateData);
onBack();
}
@ -50,16 +30,10 @@ export const CategoryCreatePage: React.FC<CategoryCreatePageProps> = ({
saveButtonBarState
}) => {
const intl = useIntl();
const {
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
return (
<Form onSubmit={onSubmit} initial={initialData} confirmLeave>
{({ data, change, submit, hasChanged }) => {
const changeMetadata = makeMetadataChangeHandler(change);
return (
<CategoryCreateForm onSubmit={onSubmit}>
{({ data, change, handlers, submit, hasChanged }) => (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.categories)}
@ -72,10 +46,11 @@ export const CategoryCreatePage: React.FC<CategoryCreatePageProps> = ({
/>
<div>
<CategoryDetailsForm
disabled={disabled}
data={data}
onChange={change}
disabled={disabled}
errors={errors}
onChange={change}
onDescriptionChange={handlers.changeDescription}
/>
<CardSpacer />
<SeoForm
@ -95,7 +70,7 @@ export const CategoryCreatePage: React.FC<CategoryCreatePageProps> = ({
disabled={disabled}
/>
<CardSpacer />
<Metadata data={data} onChange={changeMetadata} />
<Metadata data={data} onChange={handlers.changeMetadata} />
<SaveButtonBar
onCancel={onBack}
onSave={submit}
@ -104,9 +79,8 @@ export const CategoryCreatePage: React.FC<CategoryCreatePageProps> = ({
/>
</div>
</Container>
);
}}
</Form>
)}
</CategoryCreateForm>
);
};
CategoryCreatePage.displayName = "CategoryCreatePage";

View file

@ -0,0 +1,100 @@
import { OutputData } from "@editorjs/editorjs";
import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import useForm, { FormChange } from "@saleor/hooks/useForm";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import getMetadata from "@saleor/utils/metadata/getMetadata";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import useRichText from "@saleor/utils/richText/useRichText";
import React from "react";
export interface CategoryCreateFormData extends MetadataFormData {
name: string;
seoDescription: string;
seoTitle: string;
slug: string;
}
export interface CategoryCreateData extends CategoryCreateFormData {
description: OutputData;
}
interface CategoryCreateHandlers {
changeMetadata: FormChange;
changeDescription: RichTextEditorChange;
}
export interface UseCategoryCreateFormResult {
change: FormChange;
data: CategoryCreateData;
handlers: CategoryCreateHandlers;
hasChanged: boolean;
submit: () => Promise<boolean>;
}
export interface CategoryCreateFormProps {
children: (props: UseCategoryCreateFormResult) => React.ReactNode;
onSubmit: (data: CategoryCreateData) => Promise<any[]>;
}
function useCategoryCreateForm(
onSubmit: (data: CategoryCreateData) => Promise<any[]>
): UseCategoryCreateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm<CategoryCreateFormData>({
metadata: [],
name: "",
privateMetadata: [],
seoDescription: "",
seoTitle: "",
slug: ""
});
const [description, changeDescription] = useRichText({
initial: null,
triggerChange
});
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const changeMetadata = makeMetadataChangeHandler(handleChange);
// Need to make it function to always have description.current up to date
const getData = (): CategoryCreateData => ({
...form.data,
...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified),
description: description.current
});
const submit = () => handleFormSubmit(getData(), onSubmit, setChanged);
return {
change: handleChange,
data: getData(),
handlers: {
changeDescription,
changeMetadata
},
hasChanged: changed,
submit
};
}
const CategoryCreateForm: React.FC<CategoryCreateFormProps> = ({
children,
onSubmit
}) => {
const props = useCategoryCreateForm(onSubmit);
return <form onSubmit={props.submit}>{children(props)}</form>;
};
CategoryCreateForm.displayName = "CategoryCreateForm";
export default CategoryCreateForm;

View file

@ -1,35 +1,34 @@
import { OutputData } from "@editorjs/editorjs";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField";
import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer";
import RichTextEditor from "@saleor/components/RichTextEditor";
import RichTextEditor, {
RichTextEditorChange
} from "@saleor/components/RichTextEditor";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import { commonMessages } from "@saleor/intl";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
import { RawDraftContentState } from "draft-js";
import React from "react";
import { useIntl } from "react-intl";
import { maybe } from "../../../misc";
import { CategoryDetails_category } from "../../types/CategoryDetails";
interface CategoryDetailsFormProps {
category?: CategoryDetails_category;
data: {
name: string;
description: RawDraftContentState;
description: OutputData;
};
disabled: boolean;
errors: ProductErrorFragment[];
onChange: (event: React.ChangeEvent<any>) => void;
onDescriptionChange: RichTextEditorChange;
}
export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
category,
disabled,
data,
onChange,
onDescriptionChange,
errors
}) => {
const intl = useIntl();
@ -58,15 +57,15 @@ export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
</div>
<FormSpacer />
<RichTextEditor
data={data.description}
disabled={disabled}
error={!!formErrors.descriptionJson}
helperText={getProductErrorMessage(formErrors.descriptionJson, intl)}
label={intl.formatMessage({
defaultMessage: "Category Description"
})}
initial={maybe(() => JSON.parse(category.descriptionJson))}
name="description"
onChange={onChange}
onChange={onDescriptionChange}
/>
</CardContent>
</Card>

View file

@ -5,9 +5,7 @@ import { CardSpacer } from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Metadata from "@saleor/components/Metadata/Metadata";
import { MetadataFormData } from "@saleor/components/Metadata/types";
import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm";
@ -15,9 +13,6 @@ import { Tab, TabContainer } from "@saleor/components/Tab";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { RawDraftContentState } from "draft-js";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -32,15 +27,7 @@ import {
} from "../../types/CategoryDetails";
import CategoryBackground from "../CategoryBackground";
import CategoryProducts from "../CategoryProducts";
export interface FormData extends MetadataFormData {
backgroundImageAlt: string;
description: RawDraftContentState;
name: string;
slug: string;
seoTitle: string;
seoDescription: string;
}
import CategoryUpdateForm, { CategoryUpdateData } from "./form";
export enum CategoryPageTab {
categories = "categories",
@ -62,7 +49,7 @@ export interface CategoryUpdatePageProps
};
saveButtonBarState: ConfirmButtonTransitionState;
onImageDelete: () => void;
onSubmit: (data: FormData) => SubmitPromise;
onSubmit: (data: CategoryUpdateData) => SubmitPromise;
onImageUpload(file: File);
onNextPage();
onPreviousPage();
@ -106,64 +93,21 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
toggleAll
}: CategoryUpdatePageProps) => {
const intl = useIntl();
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const initialData: FormData = category
? {
backgroundImageAlt: maybe(() => category.backgroundImage.alt, ""),
description: maybe(() => JSON.parse(category.descriptionJson)),
metadata: category?.metadata?.map(mapMetadataItemToInput),
name: category.name || "",
privateMetadata: category?.privateMetadata?.map(mapMetadataItemToInput),
seoDescription: category.seoDescription || "",
seoTitle: category.seoTitle || "",
slug: category?.slug || ""
}
: {
backgroundImageAlt: "",
description: "",
metadata: undefined,
name: "",
privateMetadata: undefined,
seoDescription: "",
seoTitle: "",
slug: ""
};
const handleSubmit = (data: FormData) => {
const metadata = isMetadataModified ? data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified
? data.privateMetadata
: undefined;
return onSubmit({
...data,
metadata,
privateMetadata
});
};
return (
<Form onSubmit={handleSubmit} initial={initialData} confirmLeave>
{({ data, change, submit, hasChanged }) => {
const changeMetadata = makeMetadataChangeHandler(change);
return (
<CategoryUpdateForm category={category} onSubmit={onSubmit}>
{({ data, change, handlers, submit, hasChanged }) => (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.categories)}
</AppHeader>
<PageHeader title={category ? category.name : undefined} />
<PageHeader title={category?.name} />
<CategoryDetailsForm
category={category}
data={data}
disabled={disabled}
errors={errors}
onChange={change}
onDescriptionChange={handlers.changeDescription}
/>
<CardSpacer />
<CategoryBackground
@ -191,7 +135,7 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
disabled={disabled}
/>
<CardSpacer />
<Metadata data={data} onChange={changeMetadata} />
<Metadata data={data} onChange={handlers.changeMetadata} />
<CardSpacer />
<TabContainer>
<CategoriesTab
@ -254,7 +198,7 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
)}
{currentTab === CategoryPageTab.products && (
<CategoryProducts
categoryName={maybe(() => category.name)}
categoryName={category?.name}
products={products}
disabled={disabled}
pageInfo={pageInfo}
@ -277,9 +221,8 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
disabled={disabled || !hasChanged}
/>
</Container>
);
}}
</Form>
)}
</CategoryUpdateForm>
);
};
CategoryUpdatePage.displayName = "CategoryUpdatePage";

View file

@ -0,0 +1,107 @@
import { OutputData } from "@editorjs/editorjs";
import { CategoryDetails_category } from "@saleor/categories/types/CategoryDetails";
import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import useForm, { FormChange } from "@saleor/hooks/useForm";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import useRichText from "@saleor/utils/richText/useRichText";
import React from "react";
export interface CategoryUpdateFormData extends MetadataFormData {
backgroundImageAlt: string;
name: string;
slug: string;
seoTitle: string;
seoDescription: string;
}
export interface CategoryUpdateData extends CategoryUpdateFormData {
description: OutputData;
}
interface CategoryUpdateHandlers {
changeMetadata: FormChange;
changeDescription: RichTextEditorChange;
}
export interface UseCategoryUpdateFormResult {
change: FormChange;
data: CategoryUpdateData;
handlers: CategoryUpdateHandlers;
hasChanged: boolean;
submit: () => Promise<boolean>;
}
export interface CategoryUpdateFormProps {
children: (props: UseCategoryUpdateFormResult) => React.ReactNode;
category: CategoryDetails_category;
onSubmit: (data: CategoryUpdateData) => Promise<any[]>;
}
function useCategoryUpdateForm(
category: CategoryDetails_category,
onSubmit: (data: CategoryUpdateData) => Promise<any[]>
): UseCategoryUpdateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm<CategoryUpdateFormData>({
backgroundImageAlt: category?.backgroundImage?.alt || "",
metadata: category?.metadata?.map(mapMetadataItemToInput),
name: category?.name || "",
privateMetadata: category?.privateMetadata?.map(mapMetadataItemToInput),
seoDescription: category?.seoDescription || "",
seoTitle: category?.seoTitle || "",
slug: category?.slug || ""
});
const [description, changeDescription] = useRichText({
initial: category?.descriptionJson,
triggerChange
});
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const changeMetadata = makeMetadataChangeHandler(handleChange);
// Need to make it function to always have description.current up to date
const getData = (): CategoryUpdateData => ({
...form.data,
...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified),
description: description.current
});
const submit = () => handleFormSubmit(getData(), onSubmit, setChanged);
return {
change: handleChange,
data: getData(),
handlers: {
changeDescription,
changeMetadata
},
hasChanged: changed,
submit
};
}
const CategoryUpdateForm: React.FC<CategoryUpdateFormProps> = ({
children,
category,
onSubmit
}) => {
const props = useCategoryUpdateForm(category, onSubmit);
return <form onSubmit={props.submit}>{children(props)}</form>;
};
CategoryUpdateForm.displayName = "CategoryUpdateForm";
export default CategoryUpdateForm;