From 9adde24a4813f25f3c429870c626de5cb2f66fbb Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 3 Nov 2020 17:17:47 +0100 Subject: [PATCH] Add rich text editor to category pages --- .../CategoryCreatePage/CategoryCreatePage.tsx | 128 +++----- .../components/CategoryCreatePage/form.tsx | 100 ++++++ .../CategoryDetailsForm.tsx | 19 +- .../CategoryUpdatePage/CategoryUpdatePage.tsx | 305 +++++++----------- .../components/CategoryUpdatePage/form.tsx | 107 ++++++ 5 files changed, 391 insertions(+), 268 deletions(-) create mode 100644 src/categories/components/CategoryCreatePage/form.tsx create mode 100644 src/categories/components/CategoryUpdatePage/form.tsx diff --git a/src/categories/components/CategoryCreatePage/CategoryCreatePage.tsx b/src/categories/components/CategoryCreatePage/CategoryCreatePage.tsx index 0ac329841..91b91d136 100644 --- a/src/categories/components/CategoryCreatePage/CategoryCreatePage.tsx +++ b/src/categories/components/CategoryCreatePage/CategoryCreatePage.tsx @@ -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,63 +30,57 @@ export const CategoryCreatePage: React.FC = ({ saveButtonBarState }) => { const intl = useIntl(); - const { - makeChangeHandler: makeMetadataChangeHandler - } = useMetadataChangeTrigger(); return ( -
- {({ data, change, submit, hasChanged }) => { - const changeMetadata = makeMetadataChangeHandler(change); - - return ( - - - {intl.formatMessage(sectionNames.categories)} - - + {({ data, change, handlers, submit, hasChanged }) => ( + + + {intl.formatMessage(sectionNames.categories)} + + +
+ -
- - - - - - -
- - ); - }} - + + + + + +
+
+ )} + ); }; CategoryCreatePage.displayName = "CategoryCreatePage"; diff --git a/src/categories/components/CategoryCreatePage/form.tsx b/src/categories/components/CategoryCreatePage/form.tsx new file mode 100644 index 000000000..7c1344a0c --- /dev/null +++ b/src/categories/components/CategoryCreatePage/form.tsx @@ -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; +} + +export interface CategoryCreateFormProps { + children: (props: UseCategoryCreateFormResult) => React.ReactNode; + onSubmit: (data: CategoryCreateData) => Promise; +} + +function useCategoryCreateForm( + onSubmit: (data: CategoryCreateData) => Promise +): UseCategoryCreateFormResult { + const [changed, setChanged] = React.useState(false); + const triggerChange = () => setChanged(true); + + const form = useForm({ + 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 = ({ + children, + onSubmit +}) => { + const props = useCategoryCreateForm(onSubmit); + + return
{children(props)}
; +}; + +CategoryCreateForm.displayName = "CategoryCreateForm"; +export default CategoryCreateForm; diff --git a/src/categories/components/CategoryDetailsForm/CategoryDetailsForm.tsx b/src/categories/components/CategoryDetailsForm/CategoryDetailsForm.tsx index 4779571ba..d1e984b25 100644 --- a/src/categories/components/CategoryDetailsForm/CategoryDetailsForm.tsx +++ b/src/categories/components/CategoryDetailsForm/CategoryDetailsForm.tsx @@ -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) => void; + onDescriptionChange: RichTextEditorChange; } export const CategoryDetailsForm: React.FC = ({ - category, disabled, data, onChange, + onDescriptionChange, errors }) => { const intl = useIntl(); @@ -58,15 +57,15 @@ export const CategoryDetailsForm: React.FC = ({ JSON.parse(category.descriptionJson))} name="description" - onChange={onChange} + onChange={onDescriptionChange} /> diff --git a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx index 96c46e1c2..9446f74c3 100644 --- a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx +++ b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx @@ -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,180 +93,136 @@ export const CategoryUpdatePage: React.FC = ({ 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 ( -
- {({ data, change, submit, hasChanged }) => { - const changeMetadata = makeMetadataChangeHandler(change); - - return ( - - - {intl.formatMessage(sectionNames.categories)} - - - - - category.backgroundImage)} - onChange={change} - /> - - - - - - - - - - - - - - - {currentTab === CategoryPageTab.categories && ( - - - - - } - /> - undefined} - /> - - )} - {currentTab === CategoryPageTab.products && ( - category.name)} - products={products} + + {({ data, change, handlers, submit, hasChanged }) => ( + + + {intl.formatMessage(sectionNames.categories)} + + + + + category.backgroundImage)} + onChange={change} + /> + + + + + + + + + + + + + + + {currentTab === CategoryPageTab.categories && ( + + + + + } + /> + undefined} /> - )} - + )} + {currentTab === CategoryPageTab.products && ( + - - ); - }} - + )} + +
+ )} + ); }; CategoryUpdatePage.displayName = "CategoryUpdatePage"; diff --git a/src/categories/components/CategoryUpdatePage/form.tsx b/src/categories/components/CategoryUpdatePage/form.tsx new file mode 100644 index 000000000..a98c80c1f --- /dev/null +++ b/src/categories/components/CategoryUpdatePage/form.tsx @@ -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; +} + +export interface CategoryUpdateFormProps { + children: (props: UseCategoryUpdateFormResult) => React.ReactNode; + category: CategoryDetails_category; + onSubmit: (data: CategoryUpdateData) => Promise; +} + +function useCategoryUpdateForm( + category: CategoryDetails_category, + onSubmit: (data: CategoryUpdateData) => Promise +): UseCategoryUpdateFormResult { + const [changed, setChanged] = React.useState(false); + const triggerChange = () => setChanged(true); + + const form = useForm({ + 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 = ({ + children, + category, + onSubmit +}) => { + const props = useCategoryUpdateForm(category, onSubmit); + + return
{children(props)}
; +}; + +CategoryUpdateForm.displayName = "CategoryUpdateForm"; +export default CategoryUpdateForm;