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 { 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<RichTextEditorProps> = ({
data,
error,
helperText,
initial,
label
label,
onChange
}) => {
const classes = useStyles({});
const [isFocused, setFocus] = React.useState(false);
const editor = React.useRef<EditorJS>();
const editorContainer = React.useRef<HTMLDivElement>();
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 (
<div>

View file

@ -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<ProductDetailsFormProps> = ({
data,
disabled,
errors,
initialDescription,
onDescriptionChange,
onChange
}) => {
const intl = useIntl();
@ -57,13 +57,13 @@ export const ProductDetailsForm: React.FC<ProductDetailsFormProps> = ({
/>
<FormSpacer />
<RichTextEditor
data={data.description}
disabled={disabled}
error={!!formErrors.descriptionJson}
helperText={getProductErrorMessage(formErrors.descriptionJson, intl)}
initial={initialDescription}
label={intl.formatMessage(commonMessages.description)}
name="description"
onChange={onChange}
onChange={onDescriptionChange}
/>
</CardContent>
</Card>

View file

@ -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<ProductUpdatePageProps> = ({
@ -135,10 +136,6 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
product?.taxType.description
);
const initialDescription = maybe<RawDraftContentState>(() =>
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<ProductUpdatePageProps> = ({
data={data}
disabled={disabled}
errors={errors}
initialDescription={initialDescription}
onDescriptionChange={handlers.changeDescription}
onChange={change}
/>
<CardSpacer />
@ -262,11 +259,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
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}

View file

@ -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<string>
> &
Record<"addStock" | "deleteStock", (id: string) => void>;
interface ProductUpdateHandlers
extends Record<
| "changeMetadata"
| "selectCategory"
| "selectCollection"
| "selectTaxRate",
FormChange
>,
Record<
"changeStock" | "selectAttribute" | "selectAttributeMultiple",
FormsetChange<string>
>,
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<OutputData>();
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,

View file

@ -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),