From 7c9f2a93d41d54a1f98b8c0a0ec58ae6712b95c3 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 4 Nov 2020 11:48:49 +0100 Subject: [PATCH] Add rich text editor to pages --- .../components/CollectionDetailsPage/form.tsx | 3 +- src/collections/views/CollectionCreate.tsx | 16 +- .../PageDetailsPage/PageDetailsPage.tsx | 239 +++++++----------- src/pages/components/PageDetailsPage/form.tsx | 118 +++++++++ src/pages/components/PageInfo/PageInfo.tsx | 22 +- src/pages/mutations.ts | 1 + src/pages/views/PageCreate.tsx | 7 +- src/utils/data/getPublicationData.ts | 16 ++ src/utils/richText/useRichText.test.ts | 1 + src/utils/richText/useRichText.ts | 4 +- 10 files changed, 246 insertions(+), 181 deletions(-) create mode 100644 src/pages/components/PageDetailsPage/form.tsx create mode 100644 src/utils/data/getPublicationData.ts diff --git a/src/collections/components/CollectionDetailsPage/form.tsx b/src/collections/components/CollectionDetailsPage/form.tsx index a5dd5417e..00364cf2b 100644 --- a/src/collections/components/CollectionDetailsPage/form.tsx +++ b/src/collections/components/CollectionDetailsPage/form.tsx @@ -3,6 +3,7 @@ import { CollectionDetails_collection } from "@saleor/collections/types/Collecti import { MetadataFormData } from "@saleor/components/Metadata"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import useForm, { FormChange } from "@saleor/hooks/useForm"; +import getPublicationData from "@saleor/utils/data/getPublicationData"; import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import { mapMetadataItemToInput } from "@saleor/utils/maps"; import getMetadata from "@saleor/utils/metadata/getMetadata"; @@ -89,7 +90,7 @@ function useCollectionUpdateForm( const getSubmitData = (): CollectionUpdateData => ({ ...getData(), ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified), - isPublished: form.data.isPublished || !!form.data.publicationDate + ...getPublicationData(form.data) }); const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged); diff --git a/src/collections/views/CollectionCreate.tsx b/src/collections/views/CollectionCreate.tsx index 549a93efc..277c67be9 100644 --- a/src/collections/views/CollectionCreate.tsx +++ b/src/collections/views/CollectionCreate.tsx @@ -2,6 +2,7 @@ import { WindowTitle } from "@saleor/components/WindowTitle"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import { commonMessages } from "@saleor/intl"; +import getPublicationData from "@saleor/utils/data/getPublicationData"; import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; import { useMetadataUpdate, @@ -11,9 +12,8 @@ import React from "react"; import { useIntl } from "react-intl"; import { CollectionCreateInput } from "../../types/globalTypes"; -import CollectionCreatePage, { - CollectionCreatePageFormData -} from "../components/CollectionCreatePage/CollectionCreatePage"; +import CollectionCreatePage from "../components/CollectionCreatePage/CollectionCreatePage"; +import { CollectionCreateData } from "../components/CollectionCreatePage/form"; import { useCollectionCreateMutation } from "../mutations"; import { collectionListUrl, collectionUrl } from "../urls"; @@ -47,15 +47,7 @@ export const CollectionCreate: React.FC = () => { } }); - const getPublicationData = ({ - publicationDate, - isPublished - }: CollectionCreatePageFormData) => ({ - isPublished: !!publicationDate || isPublished, - publicationDate: publicationDate || null - }); - - const handleCreate = async (formData: CollectionCreatePageFormData) => { + const handleCreate = async (formData: CollectionCreateData) => { const result = await createCollection({ variables: { input: { diff --git a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx index 739433f0c..23e40fd48 100644 --- a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx +++ b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx @@ -2,9 +2,8 @@ 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 Grid from "@saleor/components/Grid"; -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"; @@ -13,30 +12,12 @@ import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; import useDateLocalize from "@saleor/hooks/useDateLocalize"; 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 { - ContentState, - convertFromRaw, - convertToRaw, - RawDraftContentState -} from "draft-js"; import React from "react"; import { useIntl } from "react-intl"; -import { maybe } from "../../../misc"; import { PageDetails_page } from "../../types/PageDetails"; import PageInfo from "../PageInfo"; - -export interface PageDetailsPageFormData extends MetadataFormData { - content: RawDraftContentState; - isPublished: boolean; - publicationDate: string; - seoDescription: string; - seoTitle: string; - slug: string; - title: string; -} +import PageForm, { PageData } from "./form"; export interface PageDetailsPageProps { disabled: boolean; @@ -46,7 +27,7 @@ export interface PageDetailsPageProps { saveButtonBarState: ConfirmButtonTransitionState; onBack: () => void; onRemove: () => void; - onSubmit: (data: PageDetailsPageFormData) => SubmitPromise; + onSubmit: (data: PageData) => SubmitPromise; } const PageDetailsPage: React.FC = ({ @@ -60,140 +41,94 @@ const PageDetailsPage: React.FC = ({ }) => { const intl = useIntl(); const localizeDate = useDateLocalize(); - const { - isMetadataModified, - isPrivateMetadataModified, - makeChangeHandler: makeMetadataChangeHandler - } = useMetadataChangeTrigger(); const pageExists = page !== null; - const initialForm: PageDetailsPageFormData = { - content: maybe( - () => JSON.parse(page.contentJson), - convertToRaw(ContentState.createFromText("")) - ), - isPublished: page?.isPublished, - metadata: pageExists ? page?.metadata?.map(mapMetadataItemToInput) : [], - privateMetadata: pageExists - ? page?.privateMetadata?.map(mapMetadataItemToInput) - : [], - publicationDate: page?.publicationDate || "", - seoDescription: page?.seoDescription || "", - seoTitle: page?.seoTitle || "", - slug: page?.slug || "", - title: page?.title || "" - }; - - const handleSubmit = (data: PageDetailsPageFormData) => { - const metadata = isMetadataModified ? data.metadata : undefined; - const privateMetadata = isPrivateMetadataModified - ? data.privateMetadata - : undefined; - - return onSubmit({ - ...data, - isPublished: data.isPublished || !!data.publicationDate, - metadata, - privateMetadata - }); - }; - return ( -
- {({ change, data, hasChanged, submit }) => { - const changeMetadata = makeMetadataChangeHandler(change); - - return ( - - - {intl.formatMessage(sectionNames.pages)} - - page.title) - } - /> - -
- - - - convertFromRaw(data.content) - .getPlainText() - .slice(0, 300), - "" - )} - onChange={change} - slug={data.slug} - slugPlaceholder={data.title} - title={data.seoTitle} - titlePlaceholder={data.title} - helperText={intl.formatMessage({ - defaultMessage: - "Add search engine title and description to make this page easier to find" - })} - /> - - -
-
- - -
-
- -
- ); - }} -
+ + {({ change, data, handlers, hasChanged, submit }) => ( + + + {intl.formatMessage(sectionNames.pages)} + + + +
+ + + + + +
+
+ + +
+
+ +
+ )} +
); }; PageDetailsPage.displayName = "PageDetailsPage"; diff --git a/src/pages/components/PageDetailsPage/form.tsx b/src/pages/components/PageDetailsPage/form.tsx new file mode 100644 index 000000000..9a035ff18 --- /dev/null +++ b/src/pages/components/PageDetailsPage/form.tsx @@ -0,0 +1,118 @@ +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 { PageDetails_page } from "@saleor/pages/types/PageDetails"; +import getPublicationData from "@saleor/utils/data/getPublicationData"; +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 PageFormData extends MetadataFormData { + isPublished: boolean; + publicationDate: string; + seoDescription: string; + seoTitle: string; + slug: string; + title: string; +} +export interface PageData extends PageFormData { + content: OutputData; +} + +interface PageUpdateHandlers { + changeMetadata: FormChange; + changeContent: RichTextEditorChange; +} +export interface UsePageUpdateFormResult { + change: FormChange; + data: PageData; + handlers: PageUpdateHandlers; + hasChanged: boolean; + submit: () => void; +} + +export interface PageFormProps { + children: (props: UsePageUpdateFormResult) => React.ReactNode; + page: PageDetails_page; + onSubmit: (data: PageData) => Promise; +} + +function usePageForm( + page: PageDetails_page, + onSubmit: (data: PageData) => Promise +): UsePageUpdateFormResult { + const [changed, setChanged] = React.useState(false); + const triggerChange = () => setChanged(true); + + const pageExists = page !== null; + + const form = useForm({ + isPublished: page?.isPublished, + metadata: pageExists ? page?.metadata?.map(mapMetadataItemToInput) : [], + privateMetadata: pageExists + ? page?.privateMetadata?.map(mapMetadataItemToInput) + : [], + publicationDate: page?.publicationDate || "", + seoDescription: page?.seoDescription || "", + seoTitle: page?.seoTitle || "", + slug: page?.slug || "", + title: page?.title || "" + }); + const [content, changeContent] = useRichText({ + initial: pageExists ? page?.contentJson : 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 content.current up to date + const getData = (): PageData => ({ + ...form.data, + content: content.current + }); + + const getSubmitData = (): PageData => ({ + ...getData(), + ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified), + ...getPublicationData(form.data) + }); + + const submit = () => + pageExists + ? handleFormSubmit(getSubmitData(), onSubmit, setChanged) + : onSubmit(getSubmitData()); + + return { + change: handleChange, + data: getData(), + handlers: { + changeContent, + changeMetadata + }, + hasChanged: changed, + submit + }; +} + +const PageForm: React.FC = ({ children, page, onSubmit }) => { + const props = usePageForm(page, onSubmit); + + return
{children(props)}
; +}; + +PageForm.displayName = "PageForm"; +export default PageForm; diff --git a/src/pages/components/PageInfo/PageInfo.tsx b/src/pages/components/PageInfo/PageInfo.tsx index 12c2ecdeb..bf211d3e0 100644 --- a/src/pages/components/PageInfo/PageInfo.tsx +++ b/src/pages/components/PageInfo/PageInfo.tsx @@ -4,7 +4,9 @@ import { makeStyles } from "@material-ui/core/styles"; 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 { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; import { commonMessages } from "@saleor/intl"; import { getFormErrors } from "@saleor/utils/errors"; @@ -12,16 +14,14 @@ import getPageErrorMessage from "@saleor/utils/errors/page"; import React from "react"; import { useIntl } from "react-intl"; -import { maybe } from "../../../misc"; -import { PageDetails_page } from "../../types/PageDetails"; -import { PageDetailsPageFormData } from "../PageDetailsPage"; +import { PageData } from "../PageDetailsPage/form"; export interface PageInfoProps { - data: PageDetailsPageFormData; + data: PageData; disabled: boolean; errors: PageErrorFragment[]; - page: PageDetails_page; onChange: (event: React.ChangeEvent) => void; + onContentChange: RichTextEditorChange; } const useStyles = makeStyles( @@ -34,7 +34,7 @@ const useStyles = makeStyles( ); const PageInfo: React.FC = props => { - const { data, disabled, errors, page, onChange } = props; + const { data, disabled, errors, onChange, onContentChange } = props; const classes = useStyles(props); const intl = useIntl(); @@ -56,22 +56,22 @@ const PageInfo: React.FC = props => { defaultMessage: "Title", description: "page title" })} - name={"title" as keyof PageDetailsPageFormData} + name={"title" as keyof PageData} value={data.title} onChange={onChange} /> JSON.parse(page.contentJson))} label={intl.formatMessage({ defaultMessage: "Content", description: "page content" })} - name={"content" as keyof PageDetailsPageFormData} - onChange={onChange} + name={"content" as keyof PageData} + onChange={onContentChange} /> diff --git a/src/pages/mutations.ts b/src/pages/mutations.ts index 9cc38a9c1..e26d9e6fa 100644 --- a/src/pages/mutations.ts +++ b/src/pages/mutations.ts @@ -22,6 +22,7 @@ const pageCreate = gql` pageCreate(input: $input) { errors: pageErrors { ...PageErrorFragment + message } page { ...PageDetailsFragment diff --git a/src/pages/views/PageCreate.tsx b/src/pages/views/PageCreate.tsx index 4c5fb0fb8..628c84ebf 100644 --- a/src/pages/views/PageCreate.tsx +++ b/src/pages/views/PageCreate.tsx @@ -9,9 +9,8 @@ import { import React from "react"; import { useIntl } from "react-intl"; -import PageDetailsPage, { - PageDetailsPageFormData -} from "../components/PageDetailsPage"; +import PageDetailsPage from "../components/PageDetailsPage"; +import { PageData } from "../components/PageDetailsPage/form"; import { TypedPageCreate } from "../mutations"; import { PageCreate as PageCreateData } from "../types/PageCreate"; import { pageListUrl, pageUrl } from "../urls"; @@ -42,7 +41,7 @@ export const PageCreate: React.FC = () => { return ( {(pageCreate, pageCreateOpts) => { - const handleCreate = async (formData: PageDetailsPageFormData) => { + const handleCreate = async (formData: PageData) => { const result = await pageCreate({ variables: { input: { diff --git a/src/utils/data/getPublicationData.ts b/src/utils/data/getPublicationData.ts new file mode 100644 index 000000000..bfbff49f2 --- /dev/null +++ b/src/utils/data/getPublicationData.ts @@ -0,0 +1,16 @@ +interface PublicationData { + publicationDate: string; + isPublished: boolean; +} + +function getPublicationData({ + publicationDate, + isPublished +}: PublicationData): PublicationData { + return { + isPublished: !!publicationDate || isPublished, + publicationDate: publicationDate || null + }; +} + +export default getPublicationData; diff --git a/src/utils/richText/useRichText.test.ts b/src/utils/richText/useRichText.test.ts index 26fc9c782..bedf3d42f 100644 --- a/src/utils/richText/useRichText.test.ts +++ b/src/utils/richText/useRichText.test.ts @@ -39,6 +39,7 @@ describe("useRichText", () => { const triggerChange = jest.fn(); const hook = renderHook(() => useRichText({ + initial: null, triggerChange }) ); diff --git a/src/utils/richText/useRichText.ts b/src/utils/richText/useRichText.ts index fa51fb74a..522e06a08 100644 --- a/src/utils/richText/useRichText.ts +++ b/src/utils/richText/useRichText.ts @@ -1,6 +1,6 @@ import { OutputData } from "@editorjs/editorjs"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; -import { MutableRefObject, useEffect, useRef } from "react"; +import { MutableRefObject, useEffect, useRef, useState } from "react"; function useRichText(opts: { initial: string | null; @@ -9,9 +9,11 @@ function useRichText(opts: { const data = useRef( opts.initial === null ? { blocks: [] } : undefined ); + const [, setLoaded] = useState(false); useEffect(() => { try { data.current = JSON.parse(opts.initial); + setLoaded(true); } catch { data.current = undefined; }