diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index 0bad6fae4..8653a9616 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -5,21 +5,20 @@ import Quote from "@editorjs/quote"; import { makeStyles } from "@material-ui/core/styles"; import { fade } from "@material-ui/core/styles/colorManipulator"; import Typography from "@material-ui/core/Typography"; -import { FormChange } from "@saleor/hooks/useForm"; import strikethroughIcon from "@saleor/icons/StrikethroughIcon"; import classNames from "classnames"; import createGenericInlineTool from "editorjs-inline-tool"; import React from "react"; +export type RichTextEditorChange = (data: OutputData) => void; export interface RichTextEditorProps { + data: OutputData; disabled: boolean; error: boolean; helperText: string; - // TODO: Remove any type - initial: OutputData | any; label: string; name: string; - onChange: FormChange; + onChange: RichTextEditorChange; } const useStyles = makeStyles( @@ -99,45 +98,56 @@ const useStyles = makeStyles( { name: "RichTextEditor" } ); -class NewEditor extends EditorJS {} - const RichTextEditor: React.FC = ({ + data, error, helperText, - initial, - label + label, + onChange }) => { const classes = useStyles({}); const [isFocused, setFocus] = React.useState(false); const editor = React.useRef(); const editorContainer = React.useRef(); - React.useEffect(() => { - editor.current = new NewEditor({ - data: initial, - holder: editorContainer.current, - tools: { - header: { - class: Header, - config: { - defaultLevel: 1, - levels: [1, 2, 3] - } - }, - list: List, - quote: Quote, - strikethrough: createGenericInlineTool({ - sanitize: { - s: true + React.useEffect( + () => { + if (data) { + editor.current = new EditorJS({ + data, + holder: editorContainer.current, + onChange: async api => { + const savedData = await api.saver.save(); + onChange(savedData); }, - shortcut: "CMD+S", - tagName: "s", - toolboxIcon: strikethroughIcon - }) + tools: { + header: { + class: Header, + config: { + defaultLevel: 1, + levels: [1, 2, 3] + } + }, + list: List, + quote: Quote, + strikethrough: createGenericInlineTool({ + sanitize: { + s: true + }, + shortcut: "CMD+S", + tagName: "s", + toolboxIcon: strikethroughIcon + }) + } + }); } - }); - }, []); - React.useEffect(() => () => editor.current.destroy(), []); + + return editor.current?.destroy; + }, + // Rerender editor only if changed from undefined to defined state + [data === undefined] + ); + React.useEffect(() => editor.current?.destroy, []); return (
diff --git a/src/products/components/ProductDetailsForm/ProductDetailsForm.tsx b/src/products/components/ProductDetailsForm/ProductDetailsForm.tsx index aac585345..a533cf97c 100644 --- a/src/products/components/ProductDetailsForm/ProductDetailsForm.tsx +++ b/src/products/components/ProductDetailsForm/ProductDetailsForm.tsx @@ -1,27 +1,27 @@ +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"; interface ProductDetailsFormProps { data: { - description: RawDraftContentState; + description: OutputData; name: string; }; disabled?: boolean; errors: ProductErrorFragment[]; - // Draftail isn't controlled - it needs only initial input - // because it's autosaving on its own. - // Ref https://github.com/mirumee/saleor/issues/4470 - initialDescription: RawDraftContentState; + + onDescriptionChange: RichTextEditorChange; onChange(event: any); } @@ -29,7 +29,7 @@ export const ProductDetailsForm: React.FC = ({ data, disabled, errors, - initialDescription, + onDescriptionChange, onChange }) => { const intl = useIntl(); @@ -57,13 +57,13 @@ export const ProductDetailsForm: React.FC = ({ /> diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index 3e99347e2..4d51ad9cd 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -1,3 +1,4 @@ +import { OutputData } from "@editorjs/editorjs"; import AppHeader from "@saleor/components/AppHeader"; import AvailabilityCard from "@saleor/components/AvailabilityCard"; import CardSpacer from "@saleor/components/CardSpacer"; @@ -19,7 +20,6 @@ import { maybe } from "@saleor/misc"; import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories"; import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections"; import { FetchMoreProps, ListActions, ReorderAction } from "@saleor/types"; -import { convertFromRaw, RawDraftContentState } from "draft-js"; import React from "react"; import { useIntl } from "react-intl"; @@ -75,11 +75,12 @@ export interface ProductUpdatePageProps extends ListActions { } export interface ProductUpdatePageSubmitData extends ProductUpdatePageFormData { + addStocks: ProductStockInput[]; attributes: ProductAttributeInput[]; collections: string[]; - addStocks: ProductStockInput[]; - updateStocks: ProductStockInput[]; + description: OutputData; removeStocks: string[]; + updateStocks: ProductStockInput[]; } export const ProductUpdatePage: React.FC = ({ @@ -135,10 +136,6 @@ export const ProductUpdatePage: React.FC = ({ product?.taxType.description ); - const initialDescription = maybe(() => - JSON.parse(product.descriptionJson) - ); - const categories = getChoices(categoryChoiceList); const collections = getChoices(collectionChoiceList); const currency = product?.variants[0]?.price.currency; @@ -175,7 +172,7 @@ export const ProductUpdatePage: React.FC = ({ data={data} disabled={disabled} errors={errors} - initialDescription={initialDescription} + onDescriptionChange={handlers.changeDescription} onChange={change} /> @@ -262,11 +259,7 @@ export const ProductUpdatePage: React.FC = ({ title={data.seoTitle} titlePlaceholder={data.name} description={data.seoDescription} - descriptionPlaceholder={maybe(() => - convertFromRaw(data.description) - .getPlainText() - .slice(0, 300) - )} + descriptionPlaceholder={""} // TODO: cast description to string slug={data.slug} slugPlaceholder={data.name} loading={disabled} diff --git a/src/products/components/ProductUpdatePage/form.tsx b/src/products/components/ProductUpdatePage/form.tsx index 009b8b27a..ff4c40393 100644 --- a/src/products/components/ProductUpdatePage/form.tsx +++ b/src/products/components/ProductUpdatePage/form.tsx @@ -1,5 +1,7 @@ +import { OutputData } from "@editorjs/editorjs"; import { MetadataFormData } from "@saleor/components/Metadata"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; +import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm"; import useFormset, { @@ -21,7 +23,6 @@ import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; -import { RawDraftContentState } from "draft-js"; import { diff } from "fast-array-diff"; import React from "react"; @@ -35,7 +36,6 @@ export interface ProductUpdateFormData extends MetadataFormData { changeTaxCode: boolean; chargeTaxes: boolean; collections: string[]; - description: RawDraftContentState; isAvailable: boolean; isAvailableForPurchase: boolean; isPublished: boolean; @@ -52,27 +52,36 @@ export interface ProductUpdateFormData extends MetadataFormData { } export interface ProductUpdateData extends ProductUpdateFormData { attributes: ProductAttributeInput[]; + description: OutputData; stocks: ProductStockInput[]; } export interface ProductUpdateSubmitData extends ProductUpdateFormData { attributes: ProductAttributeInput[]; collections: string[]; + description: OutputData; addStocks: ProductStockInput[]; updateStocks: ProductStockInput[]; removeStocks: string[]; } -type ProductUpdateHandlers = Record< - "changeMetadata" | "selectCategory" | "selectCollection" | "selectTaxRate", - FormChange -> & - Record< - "changeStock" | "selectAttribute" | "selectAttributeMultiple", - FormsetChange - > & - Record<"addStock" | "deleteStock", (id: string) => void>; +interface ProductUpdateHandlers + extends Record< + | "changeMetadata" + | "selectCategory" + | "selectCollection" + | "selectTaxRate", + FormChange + >, + Record< + "changeStock" | "selectAttribute" | "selectAttributeMultiple", + FormsetChange + >, + Record<"addStock" | "deleteStock", (id: string) => void> { + changeDescription: RichTextEditorChange; +} export interface UseProductUpdateFormResult { change: FormChange; + data: ProductUpdateData; handlers: ProductUpdateHandlers; hasChanged: boolean; @@ -155,6 +164,15 @@ function useProductUpdateForm( ); const attributes = useFormset(getAttributeInputFromProduct(product)); const stocks = useFormset(getStockInputFromProduct(product)); + const description = React.useRef(); + + React.useEffect(() => { + try { + description.current = JSON.parse(product.descriptionJson); + } catch { + description.current = undefined; + } + }, [product]); const { isMetadataModified, @@ -209,28 +227,36 @@ function useProductUpdateForm( opts.taxTypes ); const changeMetadata = makeMetadataChangeHandler(handleChange); + const changeDescription: RichTextEditorChange = data => { + triggerChange(); + description.current = data; + }; const data: ProductUpdateData = { ...form.data, attributes: attributes.data, + description: description.current, stocks: stocks.data }; - const submitData: ProductUpdateSubmitData = { + // Need to make it function to always have description.current up to date + const getSubmitData = (): ProductUpdateSubmitData => ({ ...data, ...getAvailabilityData(data), ...getStocksData(product, stocks.data), ...getMetadata(data, isMetadataModified, isPrivateMetadataModified), addStocks: [], - attributes: attributes.data - }; + attributes: attributes.data, + description: description.current + }); - const submit = () => handleFormSubmit(submitData, onSubmit, setChanged); + const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged); return { change: handleChange, data, handlers: { addStock: handleStockAdd, + changeDescription, changeMetadata, changeStock: handleStockChange, deleteStock: handleStockDelete, diff --git a/src/products/utils/data.ts b/src/products/utils/data.ts index f581aecac..f48043e1e 100644 --- a/src/products/utils/data.ts +++ b/src/products/utils/data.ts @@ -1,3 +1,4 @@ +import { OutputData } from "@editorjs/editorjs"; import { MetadataFormData } from "@saleor/components/Metadata/types"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; @@ -175,7 +176,6 @@ export interface ProductUpdatePageFormData extends MetadataFormData { changeTaxCode: boolean; chargeTaxes: boolean; collections: string[]; - description: RawDraftContentState; isAvailable: boolean; isAvailableForPurchase: boolean; isPublished: boolean; @@ -205,7 +205,6 @@ export function getProductUpdatePageFormData( () => product.collections.map(collection => collection.id), [] ), - description: maybe(() => JSON.parse(product.descriptionJson)), isAvailable: !!product?.isAvailable, isAvailableForPurchase: !!product?.isAvailableForPurchase, isPublished: maybe(() => product.isPublished, false),