Add ability to save data
This commit is contained in:
parent
393b4a5860
commit
88bd52763c
5 changed files with 100 additions and 72 deletions
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in a new issue