Add ability to save data

This commit is contained in:
dominik-zeglen 2020-11-03 12:35:36 +01:00
parent 393b4a5860
commit 88bd52763c
5 changed files with 100 additions and 72 deletions

View file

@ -5,21 +5,20 @@ import Quote from "@editorjs/quote";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { fade } from "@material-ui/core/styles/colorManipulator"; import { fade } from "@material-ui/core/styles/colorManipulator";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import { FormChange } from "@saleor/hooks/useForm";
import strikethroughIcon from "@saleor/icons/StrikethroughIcon"; import strikethroughIcon from "@saleor/icons/StrikethroughIcon";
import classNames from "classnames"; import classNames from "classnames";
import createGenericInlineTool from "editorjs-inline-tool"; import createGenericInlineTool from "editorjs-inline-tool";
import React from "react"; import React from "react";
export type RichTextEditorChange = (data: OutputData) => void;
export interface RichTextEditorProps { export interface RichTextEditorProps {
data: OutputData;
disabled: boolean; disabled: boolean;
error: boolean; error: boolean;
helperText: string; helperText: string;
// TODO: Remove any type
initial: OutputData | any;
label: string; label: string;
name: string; name: string;
onChange: FormChange; onChange: RichTextEditorChange;
} }
const useStyles = makeStyles( const useStyles = makeStyles(
@ -99,45 +98,56 @@ const useStyles = makeStyles(
{ name: "RichTextEditor" } { name: "RichTextEditor" }
); );
class NewEditor extends EditorJS {}
const RichTextEditor: React.FC<RichTextEditorProps> = ({ const RichTextEditor: React.FC<RichTextEditorProps> = ({
data,
error, error,
helperText, helperText,
initial, label,
label onChange
}) => { }) => {
const classes = useStyles({}); const classes = useStyles({});
const [isFocused, setFocus] = React.useState(false); const [isFocused, setFocus] = React.useState(false);
const editor = React.useRef<EditorJS>(); const editor = React.useRef<EditorJS>();
const editorContainer = React.useRef<HTMLDivElement>(); const editorContainer = React.useRef<HTMLDivElement>();
React.useEffect(() => { React.useEffect(
editor.current = new NewEditor({ () => {
data: initial, if (data) {
holder: editorContainer.current, editor.current = new EditorJS({
tools: { data,
header: { holder: editorContainer.current,
class: Header, onChange: async api => {
config: { const savedData = await api.saver.save();
defaultLevel: 1, onChange(savedData);
levels: [1, 2, 3]
}
},
list: List,
quote: Quote,
strikethrough: createGenericInlineTool({
sanitize: {
s: true
}, },
shortcut: "CMD+S", tools: {
tagName: "s", header: {
toolboxIcon: strikethroughIcon 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
})
}
});
} }
});
}, []); return editor.current?.destroy;
React.useEffect(() => () => editor.current.destroy(), []); },
// Rerender editor only if changed from undefined to defined state
[data === undefined]
);
React.useEffect(() => editor.current?.destroy, []);
return ( return (
<div> <div>

View file

@ -1,27 +1,27 @@
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";
interface ProductDetailsFormProps { interface ProductDetailsFormProps {
data: { data: {
description: RawDraftContentState; description: OutputData;
name: string; name: string;
}; };
disabled?: boolean; disabled?: boolean;
errors: ProductErrorFragment[]; errors: ProductErrorFragment[];
// Draftail isn't controlled - it needs only initial input
// because it's autosaving on its own. onDescriptionChange: RichTextEditorChange;
// Ref https://github.com/mirumee/saleor/issues/4470
initialDescription: RawDraftContentState;
onChange(event: any); onChange(event: any);
} }
@ -29,7 +29,7 @@ export const ProductDetailsForm: React.FC<ProductDetailsFormProps> = ({
data, data,
disabled, disabled,
errors, errors,
initialDescription, onDescriptionChange,
onChange onChange
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@ -57,13 +57,13 @@ export const ProductDetailsForm: React.FC<ProductDetailsFormProps> = ({
/> />
<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)}
initial={initialDescription}
label={intl.formatMessage(commonMessages.description)} label={intl.formatMessage(commonMessages.description)}
name="description" name="description"
onChange={onChange} onChange={onDescriptionChange}
/> />
</CardContent> </CardContent>
</Card> </Card>

View file

@ -1,3 +1,4 @@
import { OutputData } from "@editorjs/editorjs";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import AvailabilityCard from "@saleor/components/AvailabilityCard"; import AvailabilityCard from "@saleor/components/AvailabilityCard";
import CardSpacer from "@saleor/components/CardSpacer"; 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 { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories";
import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections"; import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections";
import { FetchMoreProps, ListActions, ReorderAction } from "@saleor/types"; import { FetchMoreProps, ListActions, ReorderAction } from "@saleor/types";
import { convertFromRaw, RawDraftContentState } from "draft-js";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -75,11 +75,12 @@ export interface ProductUpdatePageProps extends ListActions {
} }
export interface ProductUpdatePageSubmitData extends ProductUpdatePageFormData { export interface ProductUpdatePageSubmitData extends ProductUpdatePageFormData {
addStocks: ProductStockInput[];
attributes: ProductAttributeInput[]; attributes: ProductAttributeInput[];
collections: string[]; collections: string[];
addStocks: ProductStockInput[]; description: OutputData;
updateStocks: ProductStockInput[];
removeStocks: string[]; removeStocks: string[];
updateStocks: ProductStockInput[];
} }
export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
@ -135,10 +136,6 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
product?.taxType.description product?.taxType.description
); );
const initialDescription = maybe<RawDraftContentState>(() =>
JSON.parse(product.descriptionJson)
);
const categories = getChoices(categoryChoiceList); const categories = getChoices(categoryChoiceList);
const collections = getChoices(collectionChoiceList); const collections = getChoices(collectionChoiceList);
const currency = product?.variants[0]?.price.currency; const currency = product?.variants[0]?.price.currency;
@ -175,7 +172,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
data={data} data={data}
disabled={disabled} disabled={disabled}
errors={errors} errors={errors}
initialDescription={initialDescription} onDescriptionChange={handlers.changeDescription}
onChange={change} onChange={change}
/> />
<CardSpacer /> <CardSpacer />
@ -262,11 +259,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
title={data.seoTitle} title={data.seoTitle}
titlePlaceholder={data.name} titlePlaceholder={data.name}
description={data.seoDescription} description={data.seoDescription}
descriptionPlaceholder={maybe(() => descriptionPlaceholder={""} // TODO: cast description to string
convertFromRaw(data.description)
.getPlainText()
.slice(0, 300)
)}
slug={data.slug} slug={data.slug}
slugPlaceholder={data.name} slugPlaceholder={data.name}
loading={disabled} loading={disabled}

View file

@ -1,5 +1,7 @@
import { OutputData } from "@editorjs/editorjs";
import { MetadataFormData } from "@saleor/components/Metadata"; import { MetadataFormData } from "@saleor/components/Metadata";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm"; import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm";
import useFormset, { import useFormset, {
@ -21,7 +23,6 @@ import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { RawDraftContentState } from "draft-js";
import { diff } from "fast-array-diff"; import { diff } from "fast-array-diff";
import React from "react"; import React from "react";
@ -35,7 +36,6 @@ export interface ProductUpdateFormData extends MetadataFormData {
changeTaxCode: boolean; changeTaxCode: boolean;
chargeTaxes: boolean; chargeTaxes: boolean;
collections: string[]; collections: string[];
description: RawDraftContentState;
isAvailable: boolean; isAvailable: boolean;
isAvailableForPurchase: boolean; isAvailableForPurchase: boolean;
isPublished: boolean; isPublished: boolean;
@ -52,27 +52,36 @@ export interface ProductUpdateFormData extends MetadataFormData {
} }
export interface ProductUpdateData extends ProductUpdateFormData { export interface ProductUpdateData extends ProductUpdateFormData {
attributes: ProductAttributeInput[]; attributes: ProductAttributeInput[];
description: OutputData;
stocks: ProductStockInput[]; stocks: ProductStockInput[];
} }
export interface ProductUpdateSubmitData extends ProductUpdateFormData { export interface ProductUpdateSubmitData extends ProductUpdateFormData {
attributes: ProductAttributeInput[]; attributes: ProductAttributeInput[];
collections: string[]; collections: string[];
description: OutputData;
addStocks: ProductStockInput[]; addStocks: ProductStockInput[];
updateStocks: ProductStockInput[]; updateStocks: ProductStockInput[];
removeStocks: string[]; removeStocks: string[];
} }
type ProductUpdateHandlers = Record< interface ProductUpdateHandlers
"changeMetadata" | "selectCategory" | "selectCollection" | "selectTaxRate", extends Record<
FormChange | "changeMetadata"
> & | "selectCategory"
Record< | "selectCollection"
"changeStock" | "selectAttribute" | "selectAttributeMultiple", | "selectTaxRate",
FormsetChange<string> FormChange
> & >,
Record<"addStock" | "deleteStock", (id: string) => void>; Record<
"changeStock" | "selectAttribute" | "selectAttributeMultiple",
FormsetChange<string>
>,
Record<"addStock" | "deleteStock", (id: string) => void> {
changeDescription: RichTextEditorChange;
}
export interface UseProductUpdateFormResult { export interface UseProductUpdateFormResult {
change: FormChange; change: FormChange;
data: ProductUpdateData; data: ProductUpdateData;
handlers: ProductUpdateHandlers; handlers: ProductUpdateHandlers;
hasChanged: boolean; hasChanged: boolean;
@ -155,6 +164,15 @@ function useProductUpdateForm(
); );
const attributes = useFormset(getAttributeInputFromProduct(product)); const attributes = useFormset(getAttributeInputFromProduct(product));
const stocks = useFormset(getStockInputFromProduct(product)); const stocks = useFormset(getStockInputFromProduct(product));
const description = React.useRef<OutputData>();
React.useEffect(() => {
try {
description.current = JSON.parse(product.descriptionJson);
} catch {
description.current = undefined;
}
}, [product]);
const { const {
isMetadataModified, isMetadataModified,
@ -209,28 +227,36 @@ function useProductUpdateForm(
opts.taxTypes opts.taxTypes
); );
const changeMetadata = makeMetadataChangeHandler(handleChange); const changeMetadata = makeMetadataChangeHandler(handleChange);
const changeDescription: RichTextEditorChange = data => {
triggerChange();
description.current = data;
};
const data: ProductUpdateData = { const data: ProductUpdateData = {
...form.data, ...form.data,
attributes: attributes.data, attributes: attributes.data,
description: description.current,
stocks: stocks.data stocks: stocks.data
}; };
const submitData: ProductUpdateSubmitData = { // Need to make it function to always have description.current up to date
const getSubmitData = (): ProductUpdateSubmitData => ({
...data, ...data,
...getAvailabilityData(data), ...getAvailabilityData(data),
...getStocksData(product, stocks.data), ...getStocksData(product, stocks.data),
...getMetadata(data, isMetadataModified, isPrivateMetadataModified), ...getMetadata(data, isMetadataModified, isPrivateMetadataModified),
addStocks: [], addStocks: [],
attributes: attributes.data attributes: attributes.data,
}; description: description.current
});
const submit = () => handleFormSubmit(submitData, onSubmit, setChanged); const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged);
return { return {
change: handleChange, change: handleChange,
data, data,
handlers: { handlers: {
addStock: handleStockAdd, addStock: handleStockAdd,
changeDescription,
changeMetadata, changeMetadata,
changeStock: handleStockChange, changeStock: handleStockChange,
deleteStock: handleStockDelete, deleteStock: handleStockDelete,

View file

@ -1,3 +1,4 @@
import { OutputData } from "@editorjs/editorjs";
import { MetadataFormData } from "@saleor/components/Metadata/types"; import { MetadataFormData } from "@saleor/components/Metadata/types";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
@ -175,7 +176,6 @@ export interface ProductUpdatePageFormData extends MetadataFormData {
changeTaxCode: boolean; changeTaxCode: boolean;
chargeTaxes: boolean; chargeTaxes: boolean;
collections: string[]; collections: string[];
description: RawDraftContentState;
isAvailable: boolean; isAvailable: boolean;
isAvailableForPurchase: boolean; isAvailableForPurchase: boolean;
isPublished: boolean; isPublished: boolean;
@ -205,7 +205,6 @@ export function getProductUpdatePageFormData(
() => product.collections.map(collection => collection.id), () => product.collections.map(collection => collection.id),
[] []
), ),
description: maybe(() => JSON.parse(product.descriptionJson)),
isAvailable: !!product?.isAvailable, isAvailable: !!product?.isAvailable,
isAvailableForPurchase: !!product?.isAvailableForPurchase, isAvailableForPurchase: !!product?.isAvailableForPurchase,
isPublished: maybe(() => product.isPublished, false), isPublished: maybe(() => product.isPublished, false),