Add rich text editor to collection pages

This commit is contained in:
dominik-zeglen 2020-11-03 17:49:42 +01:00
parent 06ef285d15
commit b27f28c822
5 changed files with 454 additions and 316 deletions

View file

@ -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";
@ -12,54 +11,21 @@ import VisibilityCard from "@saleor/components/VisibilityCard";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import useDateLocalize from "@saleor/hooks/useDateLocalize";
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 CollectionDetails from "../CollectionDetails/CollectionDetails";
import { CollectionImage } from "../CollectionImage/CollectionImage";
export interface CollectionCreatePageFormData extends MetadataFormData {
backgroundImage: {
url: string;
value: string;
};
backgroundImageAlt: string;
description: RawDraftContentState;
name: string;
slug: string;
publicationDate: string;
isPublished: boolean;
seoDescription: string;
seoTitle: string;
}
import CollectionCreateForm, { CollectionCreateData } from "./form";
export interface CollectionCreatePageProps {
disabled: boolean;
errors: ProductErrorFragment[];
saveButtonBarState: ConfirmButtonTransitionState;
onBack: () => void;
onSubmit: (data: CollectionCreatePageFormData) => void;
onSubmit: (data: CollectionCreateData) => Promise<any[]>;
}
const initialForm: CollectionCreatePageFormData = {
backgroundImage: {
url: null,
value: null
},
backgroundImageAlt: "",
description: convertToRaw(ContentState.createFromText("")),
isPublished: false,
metadata: [],
name: "",
privateMetadata: [],
publicationDate: "",
seoDescription: "",
seoTitle: "",
slug: ""
};
const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
disabled,
errors,
@ -69,16 +35,10 @@ const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
}: CollectionCreatePageProps) => {
const intl = useIntl();
const localizeDate = useDateLocalize();
const {
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
return (
<Form initial={initialForm} onSubmit={onSubmit}>
{({ change, data, hasChanged, submit }) => {
const changeMetadata = makeMetadataChangeHandler(change);
return (
<CollectionCreateForm onSubmit={onSubmit}>
{({ change, data, handlers, hasChanged, submit }) => (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.collections)}
@ -96,6 +56,7 @@ const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
disabled={disabled}
errors={errors}
onChange={change}
onDescriptionChange={handlers.changeDescription}
/>
<CardSpacer />
<CollectionImage
@ -150,7 +111,7 @@ const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
onChange={change}
/>
<CardSpacer />
<Metadata data={data} onChange={changeMetadata} />
<Metadata data={data} onChange={handlers.changeMetadata} />
</div>
<div>
<VisibilityCard
@ -187,9 +148,8 @@ const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
onSave={submit}
/>
</Container>
);
}}
</Form>
)}
</CollectionCreateForm>
);
};
CollectionCreatePage.displayName = "CollectionCreatePage";

View file

@ -0,0 +1,111 @@
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 CollectionCreateFormData extends MetadataFormData {
backgroundImage: {
url: string;
value: string;
};
backgroundImageAlt: string;
name: string;
slug: string;
publicationDate: string;
isPublished: boolean;
seoDescription: string;
seoTitle: string;
}
export interface CollectionCreateData extends CollectionCreateFormData {
description: OutputData;
}
interface CollectionCreateHandlers {
changeMetadata: FormChange;
changeDescription: RichTextEditorChange;
}
export interface UseCollectionCreateFormResult {
change: FormChange;
data: CollectionCreateData;
handlers: CollectionCreateHandlers;
hasChanged: boolean;
submit: () => Promise<boolean>;
}
export interface CollectionCreateFormProps {
children: (props: UseCollectionCreateFormResult) => React.ReactNode;
onSubmit: (data: CollectionCreateData) => Promise<any[]>;
}
function useCollectionCreateForm(
onSubmit: (data: CollectionCreateData) => Promise<any[]>
): UseCollectionCreateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm<CollectionCreateFormData>({
backgroundImage: {
url: null,
value: null
},
backgroundImageAlt: "",
isPublished: false,
metadata: [],
name: "",
privateMetadata: [],
publicationDate: "",
seoDescription: "",
seoTitle: "",
slug: ""
});
const [description, changeDescription] = useRichText({
initial: null,
triggerChange
});
const {
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 = (): CollectionCreateData => ({
...form.data,
description: description.current
});
const submit = () => handleFormSubmit(getData(), onSubmit, setChanged);
return {
change: handleChange,
data: getData(),
handlers: {
changeDescription,
changeMetadata
},
hasChanged: changed,
submit
};
}
const CollectionCreateForm: React.FC<CollectionCreateFormProps> = ({
children,
onSubmit
}) => {
const props = useCollectionCreateForm(onSubmit);
return <form onSubmit={props.submit}>{children(props)}</form>;
};
CollectionCreateForm.displayName = "CollectionCreateForm";
export default CollectionCreateForm;

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 { maybe } from "@saleor/misc";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
import { RawDraftContentState } from "draft-js";
import React from "react";
import { useIntl } from "react-intl";
import { CollectionDetails_collection } from "../../types/CollectionDetails";
export interface CollectionDetailsProps {
collection?: CollectionDetails_collection;
data: {
description: RawDraftContentState;
description: OutputData;
name: string;
};
disabled: boolean;
errors: ProductErrorFragment[];
onChange: (event: React.ChangeEvent<any>) => void;
onDescriptionChange: RichTextEditorChange;
}
const CollectionDetails: React.FC<CollectionDetailsProps> = ({
collection,
disabled,
data,
onChange,
onDescriptionChange,
errors
}) => {
const intl = useIntl();
@ -57,13 +56,13 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
/>
<FormSpacer />
<RichTextEditor
data={data.description}
error={!!formErrors.descriptionJson}
helperText={getProductErrorMessage(formErrors.descriptionJson, intl)}
initial={maybe(() => JSON.parse(collection.descriptionJson))}
label={intl.formatMessage(commonMessages.description)}
name="description"
disabled={disabled}
onChange={onChange}
onChange={onDescriptionChange}
/>
</CardContent>
</Card>

View file

@ -3,12 +3,10 @@ import { CardSpacer } from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import { Container } from "@saleor/components/Container";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer";
import Grid from "@saleor/components/Grid";
import Hr from "@saleor/components/Hr";
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";
@ -16,9 +14,6 @@ import VisibilityCard from "@saleor/components/VisibilityCard";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import useDateLocalize from "@saleor/hooks/useDateLocalize";
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 { useIntl } from "react-intl";
@ -28,18 +23,7 @@ import { CollectionDetails_collection } from "../../types/CollectionDetails";
import CollectionDetails from "../CollectionDetails/CollectionDetails";
import { CollectionImage } from "../CollectionImage/CollectionImage";
import CollectionProducts from "../CollectionProducts/CollectionProducts";
export interface CollectionDetailsPageFormData extends MetadataFormData {
backgroundImageAlt: string;
description: RawDraftContentState;
name: string;
slug: string;
publicationDate: string;
seoDescription: string;
seoTitle: string;
isFeatured: boolean;
isPublished: boolean;
}
import CollectionUpdateForm, { CollectionUpdateData } from "./form";
export interface CollectionDetailsPageProps extends PageListProps, ListActions {
collection: CollectionDetails_collection;
@ -51,7 +35,7 @@ export interface CollectionDetailsPageProps extends PageListProps, ListActions {
onImageDelete: () => void;
onImageUpload: (file: File) => void;
onProductUnassign: (id: string, event: React.MouseEvent<any>) => void;
onSubmit: (data: CollectionDetailsPageFormData) => void;
onSubmit: (data: CollectionUpdateData) => Promise<any>;
}
const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
@ -69,50 +53,14 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
}: CollectionDetailsPageProps) => {
const intl = useIntl();
const localizeDate = useDateLocalize();
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const handleSubmit = (data: CollectionDetailsPageFormData) => {
const metadata = isMetadataModified ? data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified
? data.privateMetadata
: undefined;
return onSubmit({
...data,
isPublished: data.isPublished || !!data.publicationDate,
metadata,
privateMetadata
});
};
return (
<Form
initial={{
backgroundImageAlt: maybe(() => collection.backgroundImage.alt, ""),
description: maybe(() => JSON.parse(collection.descriptionJson)),
isFeatured,
isPublished: maybe(() => collection.isPublished, false),
metadata: collection?.metadata?.map(mapMetadataItemToInput),
name: maybe(() => collection.name, ""),
privateMetadata: collection?.privateMetadata?.map(
mapMetadataItemToInput
),
publicationDate: maybe(() => collection.publicationDate, ""),
seoDescription: maybe(() => collection.seoDescription, ""),
seoTitle: maybe(() => collection.seoTitle, ""),
slug: collection?.slug || ""
}}
onSubmit={handleSubmit}
confirmLeave
<CollectionUpdateForm
collection={collection}
isFeatured={isFeatured}
onSubmit={onSubmit}
>
{({ change, data, hasChanged, submit }) => {
const changeMetadata = makeMetadataChangeHandler(change);
return (
{({ change, data, handlers, hasChanged, submit }) => (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.collections)}
@ -121,11 +69,11 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
<Grid>
<div>
<CollectionDetails
collection={collection}
data={data}
disabled={disabled}
errors={errors}
onChange={change}
onDescriptionChange={handlers.changeDescription}
/>
<CardSpacer />
<CollectionImage
@ -136,7 +84,7 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
onChange={change}
/>
<CardSpacer />
<Metadata data={data} onChange={changeMetadata} />
<Metadata data={data} onChange={handlers.changeMetadata} />
<CardSpacer />
<CollectionProducts
disabled={disabled}
@ -189,7 +137,7 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
<FormSpacer />
<Hr />
<ControlledCheckbox
name={"isFeatured" as keyof CollectionDetailsPageFormData}
name={"isFeatured" as keyof CollectionUpdateData}
label={intl.formatMessage({
defaultMessage: "Feature on Homepage",
description: "switch button"
@ -210,9 +158,8 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
onSave={submit}
/>
</Container>
);
}}
</Form>
)}
</CollectionUpdateForm>
);
};
CollectionDetailsPage.displayName = "CollectionDetailsPage";

View file

@ -0,0 +1,121 @@
import { OutputData } from "@editorjs/editorjs";
import { CollectionDetails_collection } from "@saleor/collections/types/CollectionDetails";
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 CollectionUpdateFormData extends MetadataFormData {
backgroundImageAlt: string;
name: string;
slug: string;
publicationDate: string;
seoDescription: string;
seoTitle: string;
isFeatured: boolean;
isPublished: boolean;
}
export interface CollectionUpdateData extends CollectionUpdateFormData {
description: OutputData;
}
interface CollectionUpdateHandlers {
changeMetadata: FormChange;
changeDescription: RichTextEditorChange;
}
export interface UseCollectionUpdateFormResult {
change: FormChange;
data: CollectionUpdateData;
handlers: CollectionUpdateHandlers;
hasChanged: boolean;
submit: () => Promise<boolean>;
}
export interface CollectionUpdateFormProps {
children: (props: UseCollectionUpdateFormResult) => React.ReactNode;
collection: CollectionDetails_collection;
isFeatured: boolean;
onSubmit: (data: CollectionUpdateData) => Promise<any[]>;
}
function useCollectionUpdateForm(
collection: CollectionDetails_collection,
onSubmit: (data: CollectionUpdateData) => Promise<any[]>,
isFeatured: boolean
): UseCollectionUpdateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm<CollectionUpdateFormData>({
backgroundImageAlt: collection?.backgroundImage?.alt || "",
isFeatured,
isPublished: !!collection?.isPublished,
metadata: collection?.metadata?.map(mapMetadataItemToInput),
name: collection?.name || "",
privateMetadata: collection?.privateMetadata?.map(mapMetadataItemToInput),
publicationDate: collection?.publicationDate || "",
seoDescription: collection?.seoDescription || "",
seoTitle: collection?.seoTitle || "",
slug: collection?.slug || ""
});
const [description, changeDescription] = useRichText({
initial: collection?.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 = (): CollectionUpdateData => ({
...form.data,
description: description.current,
isPublished: form.data.isPublished || !!form.data.publicationDate
});
const getSubmitData = (): CollectionUpdateData => ({
...getData(),
...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified)
});
const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged);
return {
change: handleChange,
data: getData(),
handlers: {
changeDescription,
changeMetadata
},
hasChanged: changed,
submit
};
}
const CollectionUpdateForm: React.FC<CollectionUpdateFormProps> = ({
children,
collection,
isFeatured,
onSubmit
}) => {
const props = useCollectionUpdateForm(collection, onSubmit, isFeatured);
return <form onSubmit={props.submit}>{children(props)}</form>;
};
CollectionUpdateForm.displayName = "CollectionUpdateForm";
export default CollectionUpdateForm;