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 { CardSpacer } from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form"; import Metadata from "@saleor/components/Metadata";
import Metadata, { MetadataFormData } from "@saleor/components/Metadata";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm"; import SeoForm from "@saleor/components/SeoForm";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { ContentState, convertToRaw, RawDraftContentState } from "draft-js";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import CategoryDetailsForm from "../../components/CategoryDetailsForm"; import CategoryDetailsForm from "../../components/CategoryDetailsForm";
import CategoryCreateForm, { CategoryCreateData } from "./form";
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: ""
};
export interface CategoryCreatePageProps { export interface CategoryCreatePageProps {
errors: ProductErrorFragment[]; errors: ProductErrorFragment[];
disabled: boolean; disabled: boolean;
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
onSubmit(data: FormData); onSubmit(data: CategoryCreateData);
onBack(); onBack();
} }
@ -50,16 +30,10 @@ export const CategoryCreatePage: React.FC<CategoryCreatePageProps> = ({
saveButtonBarState saveButtonBarState
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const {
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
return (
<Form onSubmit={onSubmit} initial={initialData} confirmLeave>
{({ data, change, submit, hasChanged }) => {
const changeMetadata = makeMetadataChangeHandler(change);
return ( return (
<CategoryCreateForm onSubmit={onSubmit}>
{({ data, change, handlers, submit, hasChanged }) => (
<Container> <Container>
<AppHeader onBack={onBack}> <AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.categories)} {intl.formatMessage(sectionNames.categories)}
@ -72,10 +46,11 @@ export const CategoryCreatePage: React.FC<CategoryCreatePageProps> = ({
/> />
<div> <div>
<CategoryDetailsForm <CategoryDetailsForm
disabled={disabled}
data={data} data={data}
onChange={change} disabled={disabled}
errors={errors} errors={errors}
onChange={change}
onDescriptionChange={handlers.changeDescription}
/> />
<CardSpacer /> <CardSpacer />
<SeoForm <SeoForm
@ -95,7 +70,7 @@ export const CategoryCreatePage: React.FC<CategoryCreatePageProps> = ({
disabled={disabled} disabled={disabled}
/> />
<CardSpacer /> <CardSpacer />
<Metadata data={data} onChange={changeMetadata} /> <Metadata data={data} onChange={handlers.changeMetadata} />
<SaveButtonBar <SaveButtonBar
onCancel={onBack} onCancel={onBack}
onSave={submit} onSave={submit}
@ -104,9 +79,8 @@ export const CategoryCreatePage: React.FC<CategoryCreatePageProps> = ({
/> />
</div> </div>
</Container> </Container>
); )}
}} </CategoryCreateForm>
</Form>
); );
}; };
CategoryCreatePage.displayName = "CategoryCreatePage"; 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 Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent"; import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer"; 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 { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors"; import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
import { RawDraftContentState } from "draft-js";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { maybe } from "../../../misc";
import { CategoryDetails_category } from "../../types/CategoryDetails";
interface CategoryDetailsFormProps { interface CategoryDetailsFormProps {
category?: CategoryDetails_category;
data: { data: {
name: string; name: string;
description: RawDraftContentState; description: OutputData;
}; };
disabled: boolean; disabled: boolean;
errors: ProductErrorFragment[]; errors: ProductErrorFragment[];
onChange: (event: React.ChangeEvent<any>) => void; onChange: (event: React.ChangeEvent<any>) => void;
onDescriptionChange: RichTextEditorChange;
} }
export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({ export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
category,
disabled, disabled,
data, data,
onChange, onChange,
onDescriptionChange,
errors errors
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@ -58,15 +57,15 @@ export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
</div> </div>
<FormSpacer /> <FormSpacer />
<RichTextEditor <RichTextEditor
data={data.description}
disabled={disabled} disabled={disabled}
error={!!formErrors.descriptionJson} error={!!formErrors.descriptionJson}
helperText={getProductErrorMessage(formErrors.descriptionJson, intl)} helperText={getProductErrorMessage(formErrors.descriptionJson, intl)}
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "Category Description" defaultMessage: "Category Description"
})} })}
initial={maybe(() => JSON.parse(category.descriptionJson))}
name="description" name="description"
onChange={onChange} onChange={onDescriptionChange}
/> />
</CardContent> </CardContent>
</Card> </Card>

View file

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