wip
This commit is contained in:
parent
88bd52763c
commit
437df6fe9a
6 changed files with 146 additions and 42 deletions
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
extends Record<
|
||||||
| "changeMetadata"
|
| "changeMetadata"
|
||||||
| "selectCategory"
|
| "selectCategory"
|
||||||
| "selectCollection"
|
| "selectCollection"
|
||||||
| "selectProductType"
|
| "selectProductType"
|
||||||
| "selectTaxRate",
|
| "selectTaxRate",
|
||||||
FormChange
|
FormChange
|
||||||
> &
|
>,
|
||||||
Record<
|
Record<
|
||||||
"changeStock" | "selectAttribute" | "selectAttributeMultiple",
|
"changeStock" | "selectAttribute" | "selectAttributeMultiple",
|
||||||
FormsetChange<string>
|
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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
72
src/utils/richText/useRichText.test.ts
Normal file
72
src/utils/richText/useRichText.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
28
src/utils/richText/useRichText.ts
Normal file
28
src/utils/richText/useRichText.ts
Normal 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;
|
Loading…
Reference in a new issue