This commit is contained in:
dominik-zeglen 2020-11-03 14:53:17 +01:00
parent 88bd52763c
commit 437df6fe9a
6 changed files with 146 additions and 42 deletions

View file

@ -19,6 +19,7 @@ export interface RichTextEditorProps {
label: string; label: string;
name: string; name: string;
onChange: RichTextEditorChange; onChange: RichTextEditorChange;
onReady?: () => void;
} }
const useStyles = makeStyles( const useStyles = makeStyles(
@ -86,7 +87,10 @@ const useStyles = makeStyles(
border: `1px solid ${fade(theme.palette.text.secondary, 0.4)}`, border: `1px solid ${fade(theme.palette.text.secondary, 0.4)}`,
borderRadius: 4, borderRadius: 4,
boxShadow: `inset 0 0 0 0 ${theme.palette.primary.main}`, boxShadow: `inset 0 0 0 0 ${theme.palette.primary.main}`,
fontSize: theme.typography.body1.fontSize,
padding: theme.spacing(3, 2), padding: theme.spacing(3, 2),
paddingBottom: theme.spacing(),
paddingLeft: 10,
position: "relative", position: "relative",
transition: theme.transitions.duration.short + "ms" transition: theme.transitions.duration.short + "ms"
}, },
@ -103,7 +107,8 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
error, error,
helperText, helperText,
label, label,
onChange onChange,
onReady
}) => { }) => {
const classes = useStyles({}); const classes = useStyles({});
@ -120,6 +125,7 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
const savedData = await api.saver.save(); const savedData = await api.saver.save();
onChange(savedData); onChange(savedData);
}, },
onReady,
tools: { tools: {
header: { header: {
class: Header, class: Header,

View file

@ -86,12 +86,6 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
const intl = useIntl(); const intl = useIntl();
const localizeDate = useDateLocalize(); const localizeDate = useDateLocalize();
// Ensures that it will not change after component rerenders, because it
// generates different block keys and it causes editor to lose its content.
const initialDescription = React.useRef(
convertToRaw(ContentState.createFromText(""))
);
// Display values // Display values
const [selectedCategory, setSelectedCategory] = useStateFromProps( const [selectedCategory, setSelectedCategory] = useStateFromProps(
initial?.category || "" initial?.category || ""
@ -144,8 +138,8 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
data={data} data={data}
disabled={disabled} disabled={disabled}
errors={errors} errors={errors}
initialDescription={initialDescription.current}
onChange={change} onChange={change}
onDescriptionChange={handlers.changeDescription}
/> />
<CardSpacer /> <CardSpacer />
{data.attributes.length > 0 && ( {data.attributes.length > 0 && (
@ -167,6 +161,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
weightUnit={weightUnit} weightUnit={weightUnit}
onChange={change} onChange={change}
/> />
<CardSpacer />
<ProductPricing <ProductPricing
currency={currency} currency={currency}
data={data} data={data}

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 } from "@saleor/hooks/useForm"; import useForm, { FormChange } from "@saleor/hooks/useForm";
import useFormset, { FormsetChange } from "@saleor/hooks/useFormset"; import useFormset, { FormsetChange } from "@saleor/hooks/useFormset";
@ -17,7 +19,7 @@ import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/Searc
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 useRichText from "@saleor/utils/richText/useRichText";
import React from "react"; import React from "react";
import { SearchProductTypes_search_edges_node } from "../../../searches/types/SearchProductTypes"; import { SearchProductTypes_search_edges_node } from "../../../searches/types/SearchProductTypes";
@ -34,7 +36,7 @@ export interface ProductCreateFormData extends MetadataFormData {
changeTaxCode: boolean; changeTaxCode: boolean;
chargeTaxes: boolean; chargeTaxes: boolean;
collections: string[]; collections: string[];
description: RawDraftContentState; description: OutputData;
isAvailable: boolean; isAvailable: boolean;
isAvailableForPurchase: boolean; isAvailableForPurchase: boolean;
isPublished: boolean; isPublished: boolean;
@ -56,19 +58,22 @@ export interface ProductCreateData extends ProductCreateFormData {
stocks: ProductStockInput[]; stocks: ProductStockInput[];
} }
type ProductCreateHandlers = Record< interface ProductCreateHandlers
| "changeMetadata" extends Record<
| "selectCategory" | "changeMetadata"
| "selectCollection" | "selectCategory"
| "selectProductType" | "selectCollection"
| "selectTaxRate", | "selectProductType"
FormChange | "selectTaxRate",
> & FormChange
Record< >,
"changeStock" | "selectAttribute" | "selectAttributeMultiple", Record<
FormsetChange<string> "changeStock" | "selectAttribute" | "selectAttributeMultiple",
> & FormsetChange<string>
Record<"addStock" | "deleteStock", (id: string) => void>; >,
Record<"addStock" | "deleteStock", (id: string) => void> {
changeDescription: RichTextEditorChange;
}
export interface UseProductCreateFormResult { export interface UseProductCreateFormResult {
change: FormChange; change: FormChange;
data: ProductCreateData; data: ProductCreateData;
@ -106,7 +111,7 @@ const defaultInitialFormData: ProductCreateFormData &
changeTaxCode: false, changeTaxCode: false,
chargeTaxes: false, chargeTaxes: false,
collections: [], collections: [],
description: {} as any, description: null,
isAvailable: false, isAvailable: false,
isAvailableForPurchase: false, isAvailableForPurchase: false,
isPublished: false, isPublished: false,
@ -117,7 +122,7 @@ const defaultInitialFormData: ProductCreateFormData &
publicationDate: "", publicationDate: "",
seoDescription: "", seoDescription: "",
seoTitle: "", seoTitle: "",
sku: null, sku: "",
slug: "", slug: "",
stockQuantity: null, stockQuantity: null,
taxCode: null, taxCode: null,
@ -152,6 +157,10 @@ function useProductCreateForm(
const [productType, setProductType] = useStateFromProps<ProductType>( const [productType, setProductType] = useStateFromProps<ProductType>(
initialProductType || null initialProductType || null
); );
const [description, changeDescription] = useRichText({
initial: null,
triggerChange
});
const { const {
makeChangeHandler: makeMetadataChangeHandler makeChangeHandler: makeMetadataChangeHandler
@ -211,19 +220,21 @@ function useProductCreateForm(
); );
const changeMetadata = makeMetadataChangeHandler(handleChange); const changeMetadata = makeMetadataChangeHandler(handleChange);
const data: ProductCreateData = { const getData = (): ProductCreateData => ({
...form.data, ...form.data,
attributes: attributes.data, attributes: attributes.data,
description: description.current,
productType, productType,
stocks: stocks.data stocks: stocks.data
}; });
const submit = () => onSubmit(data); const submit = () => onSubmit(getData());
return { return {
change: handleChange, change: handleChange,
data, data: getData(),
handlers: { handlers: {
addStock: handleStockAdd, addStock: handleStockAdd,
changeDescription,
changeMetadata, changeMetadata,
changeStock: handleStockChange, changeStock: handleStockChange,
deleteStock: handleStockDelete, deleteStock: handleStockDelete,

View file

@ -23,6 +23,7 @@ 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 useRichText from "@saleor/utils/richText/useRichText";
import { diff } from "fast-array-diff"; import { diff } from "fast-array-diff";
import React from "react"; import React from "react";
@ -164,15 +165,10 @@ 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>(); const [description, changeDescription] = useRichText({
initial: product?.descriptionJson,
React.useEffect(() => { triggerChange
try { });
description.current = JSON.parse(product.descriptionJson);
} catch {
description.current = undefined;
}
}, [product]);
const { const {
isMetadataModified, isMetadataModified,
@ -227,10 +223,6 @@ 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,

View file

@ -0,0 +1,72 @@
import { OutputData } from "@editorjs/editorjs";
import { renderHook } from "@testing-library/react-hooks";
import useRichText from "./useRichText";
type Fixtures = Record<"short" | "long", OutputData>;
const fixtures: Fixtures = {
long: {
blocks: [
{
data: {
level: 1,
text: "Some header"
},
type: "header"
},
{
data: {
text: "Some text"
},
type: "paragraph"
}
]
},
short: {
blocks: [
{
data: {
text: "Some text"
},
type: "paragraph"
}
]
}
};
describe("useRichText", () => {
it("properly saves data in form", () => {
const triggerChange = jest.fn();
const hook = renderHook(() =>
useRichText({
triggerChange
})
);
const [data, change] = hook.result.current;
expect(data.current).toBe(undefined);
change(fixtures.short);
expect(data.current).toMatchObject(fixtures.short);
expect(triggerChange).toHaveBeenCalled();
});
it("properly updates data in form", () => {
const triggerChange = jest.fn();
const hook = renderHook(() =>
useRichText({
initial: JSON.stringify(fixtures.short),
triggerChange
})
);
const [data, change] = hook.result.current;
expect(data.current).toMatchObject(fixtures.short);
change(fixtures.long);
expect(data.current).toMatchObject(fixtures.long);
expect(triggerChange).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,28 @@
import { OutputData } from "@editorjs/editorjs";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import { MutableRefObject, useEffect, useRef } from "react";
function useRichText(opts: {
initial?: string | null;
triggerChange: () => void;
}): [MutableRefObject<OutputData>, RichTextEditorChange] {
const data = useRef<OutputData>(
opts.initial === null ? { blocks: [] } : undefined
);
useEffect(() => {
try {
data.current = JSON.parse(opts.initial);
} catch {
data.current = undefined;
}
}, [opts.initial]);
const change: RichTextEditorChange = newData => {
opts.triggerChange();
data.current = newData;
};
return [data, change];
}
export default useRichText;