Fix EditorJS inputs (#2052)

* Bump Editor.js version

* Refactor RichTextEditor to use react-editor-js wrapper

* fixup! Bump Editor.js version

* Rewrite RichTextEditor to use uncontrolled input

* Fix RichTextEditorContent not rendering any content due to missing id

* Fix RichTextEditorContent not working on initial render

* Remove editorjs-undo

* Refactor usage of RichTextEditor to get its data only during submit

* Add useMultipleRichText hook for managing rich text attributes

* fixup! Refactor usage of RichTextEditor to get its data only during submit

* Rewrite Attributes usage to use EditorJS .save() on submit

* Refactor RichTextContext into separate file

* Rewrite tests for useRichText

* Add PR changes to the changelog

* Update snaphosts

* Fix failing tests for components that use RichTextEditor

* Remove duplicated getSubmitData function
This commit is contained in:
Jonatan Witoszek 2022-05-26 10:06:46 +02:00 committed by GitHub
parent a113c9202f
commit 993a99ff07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1399 additions and 972 deletions

View file

@ -5,6 +5,7 @@ All notable, unreleased changes to this project will be documented in this file.
## [Unreleased]
- Added links instead of imperative navigation with onClick - #1969 by @taniotanio7
- Fixed clearing attribute values - #2047 by @witoszekdev
- Fixed EditorJS integration in RichTextEditor input - #2052 by @witoszekdev
## 3.1
### PREVIEW FEATURES

111
package-lock.json generated
View file

@ -2022,35 +2022,35 @@
}
},
"@editorjs/editorjs": {
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.22.2.tgz",
"integrity": "sha512-rPCv7Z5LZebreQaaL4DZuWzoVGEqwB+P7BF1dsefGQNBmLyeLF412topeW2b6e+g4l1oQ7t75kCOACNTEyYYIA==",
"version": "2.24.3",
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.24.3.tgz",
"integrity": "sha512-VzrWaQ7mggNUAPTDGcqXJNIlBZH3S2IqsIUGA43UM2Q9VFaeS5KuVFVOTrFJvAzF7G+vZTO52ocm+hrDhTwvyw==",
"requires": {
"codex-notifier": "^1.1.2",
"codex-tooltip": "^1.0.2",
"codex-tooltip": "^1.0.5",
"nanoid": "^3.1.22"
}
},
"@editorjs/embed": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@editorjs/embed/-/embed-2.4.6.tgz",
"integrity": "sha512-c/1TzBBMrU1hpPHRWXlVanBhgyoPZvE2A3qHt23rMVxJVvzu13Zozj0ZOO8pL1XGcubf+Yvalxu7AURn7X3jPw==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@editorjs/embed/-/embed-2.5.1.tgz",
"integrity": "sha512-QoBvC6upo5ubO0toGlt5b1Upcq15MC4eqejxcjcm291Ww5cO+lKhSFY6KNk3ldL9PnWKtORxFCd6pAawtkkijA==",
"dev": true
},
"@editorjs/header": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@editorjs/header/-/header-2.6.1.tgz",
"integrity": "sha512-EsnyVFv5uThpU9tbQ/dUPFCQoa/sBFy2n+9tN3wOXJGx7sjea4fdcacJ2UYhO+7pCgZ+aSgmMOyGLYHUFbchvA=="
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@editorjs/header/-/header-2.6.2.tgz",
"integrity": "sha512-U1dnT+KGjwFmpWneEEyR2Nqp42hn9iKwQDgRHWQM+y6qx82pg+eAyuIf0QWt2Mluu9uPD2CzNfvJ+pxIuwX8Lw=="
},
"@editorjs/image": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@editorjs/image/-/image-2.6.0.tgz",
"integrity": "sha512-lX4Pz9cW3gGFzlmYLRAsBXTiqUG/MRG7NK4QVU+n/VnUWPU1e791eiIpgRLHfpPj6Maaw5a+GRut90D5EdXtqg=="
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@editorjs/image/-/image-2.6.2.tgz",
"integrity": "sha512-lai6LFJ8m3qRmSjio66o0CX7/75OupC3FQ5JWrV/biRT6GvUHtRNWKaMowKcC2ndXtfs4w6WwRxcXlB4WhUAdg=="
},
"@editorjs/list": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@editorjs/list/-/list-1.6.2.tgz",
"integrity": "sha512-OxowV0yuE11G01czYM1dEQlz1F37ehX0ak5vAbZ9ncSXrPh0fDRw/fBxTY654FlmrsQ40UFom3owSG++tLvVGw=="
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@editorjs/list/-/list-1.7.0.tgz",
"integrity": "sha512-0k0RKbQqfV32u24UYHHz5mrmSu4wr246qqXBT7xQiS533Bfd4hzki6UGzvy4f275ULzi+egbjI3BXLkpoTh9iQ=="
},
"@editorjs/paragraph": {
"version": "2.8.0",
@ -5117,6 +5117,22 @@
"url-parse": "^1.4.7"
}
},
"@reach/auto-id": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.16.0.tgz",
"integrity": "sha512-5ssbeP5bCkM39uVsfQCwBBL+KT8YColdnMN5/Eto6Rj7929ql95R3HZUOkKIvj7mgPtEb60BLQxd1P3o6cjbmg==",
"requires": {
"@reach/utils": "0.16.0",
"tslib": "^2.3.0"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"@reach/router": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@reach/router/-/router-1.3.4.tgz",
@ -5129,6 +5145,22 @@
"react-lifecycles-compat": "^3.0.4"
}
},
"@reach/utils": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.16.0.tgz",
"integrity": "sha512-PCggBet3qaQmwFNcmQ/GqHSefadAFyNCUekq9RrWoaU9hh/S4iaFgf2MBMdM47eQj5i/Bk0Mm07cP/XPFlkN+Q==",
"requires": {
"tiny-warning": "^1.0.3",
"tslib": "^2.3.0"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"@react-dnd/asap": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz",
@ -5144,6 +5176,27 @@
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz",
"integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg=="
},
"@react-editor-js/client": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@react-editor-js/client/-/client-2.0.6.tgz",
"integrity": "sha512-LMMJLAXAwk1kVMy7fxTRFK6OdouvoseqJbmVUygJb2EcfuT84nC9OAtvGEL4vsVLUcnzEV400+F9t5OKa77FGQ==",
"requires": {
"@react-editor-js/core": "2.0.6"
}
},
"@react-editor-js/core": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@react-editor-js/core/-/core-2.0.6.tgz",
"integrity": "sha512-mvHM2I+gT3AnvFpFhTZI0EFLKD9pRpgXDf286uwv6n6tngwLfnCCmtCbgiGI9ICph2GJvRZfaQubE+MHQ6YV8g=="
},
"@react-editor-js/server": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@react-editor-js/server/-/server-2.0.6.tgz",
"integrity": "sha512-soW/bV5auciYr8gEYISWK4fuIblAcc4bcwPuCKnDBj9W9r/nAxMmNgCG+z9rs9Gnroa0Ko3Hzwzs9d5MdOShzg==",
"requires": {
"@react-editor-js/core": "2.0.6"
}
},
"@release-it/bumper": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@release-it/bumper/-/bumper-2.0.0.tgz",
@ -10975,9 +11028,9 @@
"integrity": "sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg=="
},
"codex-tooltip": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.4.tgz",
"integrity": "sha512-Ud+N+y8PMIa9xGyKuo2j3q8QlfTzkMWQ5KeRrbCDerwVn7xq45nqPKQCFBXEMV0YI42/OqSMnsxP8MyVAyVhnA=="
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.5.tgz",
"integrity": "sha512-IuA8LeyLU5p1B+HyhOsqR6oxyFQ11k3i9e9aXw40CrHFTRO2Y1npNBVU3W1SvhKAbUU7R/YikUBdcYFP0RcJag=="
},
"collapse-white-space": {
"version": "1.0.6",
@ -13213,11 +13266,6 @@
"resolved": "https://registry.npmjs.org/editorjs-inline-tool/-/editorjs-inline-tool-0.4.0.tgz",
"integrity": "sha512-Ppb4e8IFPjWuNcoNM4tg9bDSo7FgMYAlqP4UhuV5W2JoJBubV5pUcpLrFrSyGTt1HJVEpbrib134zf4wxO+7VA=="
},
"editorjs-undo": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/editorjs-undo/-/editorjs-undo-0.1.5.tgz",
"integrity": "sha512-+qRmTe7Asn9KrsYHMCtNQi6rCBa+qrinJx/p7/Hj8K62HFzhMEuy7aMRbADd/KqcV2AsPVzgCj3PMHPmb6ZAkA=="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -22711,9 +22759,9 @@
"optional": true
},
"nanoid": {
"version": "3.1.30",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz",
"integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ=="
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
},
"nanomatch": {
"version": "1.2.13",
@ -25423,6 +25471,15 @@
"prop-types": "^15.7.2"
}
},
"react-editor-js": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/react-editor-js/-/react-editor-js-2.0.6.tgz",
"integrity": "sha512-8u47IbhExiFB2kGNdJYlsX5iVlSzac38A3oJ7bmnTz3Lp7Slys1xreoYdG71+KiOcfX0dEgOIavV4e6TJeB5eg==",
"requires": {
"@react-editor-js/client": "2.0.6",
"@react-editor-js/server": "2.0.6"
}
},
"react-error-boundary": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-1.2.5.tgz",

View file

@ -18,16 +18,17 @@
},
"dependencies": {
"@apollo/client": "^3.4.15",
"@editorjs/editorjs": "^2.22.2",
"@editorjs/header": "^2.6.1",
"@editorjs/image": "^2.6.0",
"@editorjs/list": "^1.6.1",
"@editorjs/editorjs": "^2.24.3",
"@editorjs/header": "^2.6.2",
"@editorjs/image": "^2.6.2",
"@editorjs/list": "^1.7.0",
"@editorjs/paragraph": "^2.8.0",
"@editorjs/quote": "^2.4.0",
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.58",
"@material-ui/styles": "^4.11.4",
"@reach/auto-id": "^0.16.0",
"@saleor/macaw-ui": "^0.5.2",
"@saleor/sdk": "^0.4.4",
"@sentry/react": "^6.0.0",
@ -44,7 +45,6 @@
"cypress-mochawesome-reporter": "^2.3.0",
"downshift": "^6.1.7",
"editorjs-inline-tool": "^0.4.0",
"editorjs-undo": "^0.1.4",
"faker": "^5.1.0",
"fast-array-diff": "^0.2.0",
"fuzzaldrin": "^2.1.0",
@ -60,6 +60,7 @@
"react": "^16.12.0",
"react-dom": "^16.9.0",
"react-dropzone": "^11.2.4",
"react-editor-js": "^2.0.6",
"react-error-boundary": "^1.2.5",
"react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0",
@ -92,7 +93,7 @@
"@babel/preset-react": "^7.7.4",
"@babel/preset-typescript": "^7.13.0",
"@babel/runtime": "^7.7.6",
"@editorjs/embed": "^2.4.6",
"@editorjs/embed": "^2.5.1",
"@formatjs/cli": "^4.5.0",
"@graphql-codegen/add": "^3.1.1",
"@graphql-codegen/cli": "^2.1.1",

View file

@ -28,6 +28,11 @@ import {
mapNodeToChoice,
mapPagesToChoices
} from "@saleor/utils/maps";
import { RichTextContextValues } from "@saleor/utils/richText/context";
import {
GetRichTextValues,
RichTextGetters
} from "@saleor/utils/richText/useMultipleRichText";
import { AttributePageFormData } from "../components/AttributePage";
@ -36,6 +41,11 @@ type AtributesOfFiles = Pick<
"file" | "id" | "values" | "contentType"
>;
export interface RichTextProps {
richText: RichTextContextValues;
attributeRichTextGetters: RichTextGetters<string>;
}
export const ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES = [
AttributeInputTypeEnum.DROPDOWN,
AttributeInputTypeEnum.MULTISELECT,
@ -257,6 +267,41 @@ export const mergeAttributeValues = (
: attributeValues;
};
export const mergeAttributes = (
...attributeLists: AttributeInput[][]
): AttributeInput[] =>
attributeLists.reduce((prev, attributes) => {
const newAttributeIds = new Set(attributes.map(attr => attr.id));
return [
...prev.filter(attr => !newAttributeIds.has(attr.id)),
...attributes
];
}, []);
export function getRichTextAttributesFromMap(
attributes: AttributeInput[],
values: GetRichTextValues
): AttributeInput[] {
return attributes
.filter(({ data }) => data.inputType === AttributeInputTypeEnum.RICH_TEXT)
.map(attribute => ({
...attribute,
value: [JSON.stringify(values[attribute.id])]
}));
}
export function getRichTextDataFromAttributes(
attributes: AttributeInput[] = []
): Record<string, string> {
const keyValuePairs = attributes
.filter(
attribute => attribute.data.inputType === AttributeInputTypeEnum.RICH_TEXT
)
.map(attribute => [attribute.id, attribute.value[0]]);
return Object.fromEntries(keyValuePairs);
}
export const getFileValuesToUploadFromAttributes = (
attributesWithNewFileValue: FormsetData<null, File>
) => attributesWithNewFileValue.filter(fileAttribute => !!fileAttribute.value);

View file

@ -53,7 +53,6 @@ export const CategoryCreatePage: React.FC<CategoryCreatePageProps> = ({
disabled={disabled}
errors={errors}
onChange={change}
onDescriptionChange={handlers.changeDescription}
/>
<CardSpacer />
<SeoForm

View file

@ -1,13 +1,16 @@
import { OutputData } from "@editorjs/editorjs";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import useForm, {
CommonUseFormResult,
FormChange
} from "@saleor/hooks/useForm";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import {
RichTextContext,
RichTextContextValues
} from "@saleor/utils/richText/context";
import useRichText from "@saleor/utils/richText/useRichText";
import React, { useEffect } from "react";
@ -23,8 +26,8 @@ export interface CategoryCreateData extends CategoryCreateFormData {
interface CategoryCreateHandlers {
changeMetadata: FormChange;
changeDescription: RichTextEditorChange;
}
export interface UseCategoryCreateFormResult
extends CommonUseFormResult<CategoryCreateData> {
handlers: CategoryCreateHandlers;
@ -48,10 +51,10 @@ const initialData: CategoryCreateFormData = {
function useCategoryCreateForm(
onSubmit: (data: CategoryCreateData) => Promise<any[]>,
disabled: boolean
): UseCategoryCreateFormResult {
): UseCategoryCreateFormResult & { richText: RichTextContextValues } {
const {
handleChange,
data,
data: formData,
triggerChange,
formId,
setIsSubmitDisabled
@ -66,7 +69,7 @@ function useCategoryCreateForm(
formId
});
const [description, changeDescription] = useRichText({
const richText = useRichText({
initial: null,
triggerChange
});
@ -77,13 +80,18 @@ function useCategoryCreateForm(
const changeMetadata = makeMetadataChangeHandler(handleChange);
const data: CategoryCreateData = {
...formData,
description: null
};
// Need to make it function to always have description.current up to date
const getData = (): CategoryCreateData => ({
...data,
description: description.current
const getData = async (): Promise<CategoryCreateData> => ({
...formData,
description: await richText.getValue()
});
const submit = () => handleFormSubmit(getData());
const submit = async () => handleFormSubmit(await getData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
@ -91,13 +99,13 @@ function useCategoryCreateForm(
return {
change: handleChange,
data: getData(),
data,
handlers: {
changeDescription,
changeMetadata
},
submit,
isSaveDisabled: disabled
isSaveDisabled: disabled,
richText
};
}
@ -106,9 +114,15 @@ const CategoryCreateForm: React.FC<CategoryCreateFormProps> = ({
onSubmit,
disabled
}) => {
const props = useCategoryCreateForm(onSubmit, disabled);
const { richText, ...props } = useCategoryCreateForm(onSubmit, disabled);
return <form onSubmit={props.submit}>{children(props)}</form>;
return (
<form onSubmit={props.submit}>
<RichTextContext.Provider value={richText}>
{children(props)}
</RichTextContext.Provider>
</form>
);
};
CategoryCreateForm.displayName = "CategoryCreateForm";

View file

@ -2,12 +2,11 @@ import { OutputData } from "@editorjs/editorjs";
import { Card, CardContent, TextField } from "@material-ui/core";
import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer";
import RichTextEditor, {
RichTextEditorChange
} from "@saleor/components/RichTextEditor";
import RichTextEditor from "@saleor/components/RichTextEditor";
import { ProductErrorFragment } from "@saleor/graphql";
import { commonMessages } from "@saleor/intl";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
import { useRichTextContext } from "@saleor/utils/richText/context";
import React from "react";
import { useIntl } from "react-intl";
@ -19,17 +18,21 @@ interface CategoryDetailsFormProps {
disabled: boolean;
errors: ProductErrorFragment[];
onChange: (event: React.ChangeEvent<any>) => void;
onDescriptionChange: RichTextEditorChange;
}
export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
disabled,
data,
onChange,
onDescriptionChange,
errors
}) => {
const intl = useIntl();
const {
defaultValue,
editorRef,
isReadyForMount,
handleChange
} = useRichTextContext();
const formErrors = getFormErrors(["name", "description"], errors);
@ -55,8 +58,11 @@ export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
/>
</div>
<FormSpacer />
{isReadyForMount && (
<RichTextEditor
data={data.description}
defaultValue={defaultValue}
editorRef={editorRef}
onChange={handleChange}
disabled={disabled}
error={!!formErrors.description}
helperText={getProductErrorMessage(formErrors.description, intl)}
@ -65,8 +71,8 @@ export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
defaultMessage: "Category Description"
})}
name="description"
onChange={onDescriptionChange}
/>
)}
</CardContent>
</Card>
);

View file

@ -110,7 +110,6 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
disabled={disabled}
errors={errors}
onChange={change}
onDescriptionChange={handlers.changeDescription}
/>
<CardSpacer />
<CategoryBackground

View file

@ -1,7 +1,6 @@
import { OutputData } from "@editorjs/editorjs";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import { CategoryDetailsFragment } from "@saleor/graphql";
import useForm, {
CommonUseFormResult,
@ -11,6 +10,10 @@ import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import {
RichTextContext,
RichTextContextValues
} from "@saleor/utils/richText/context";
import useRichText from "@saleor/utils/richText/useRichText";
import React, { useEffect } from "react";
@ -27,8 +30,8 @@ export interface CategoryUpdateData extends CategoryUpdateFormData {
interface CategoryUpdateHandlers {
changeMetadata: FormChange;
changeDescription: RichTextEditorChange;
}
export interface UseCategoryUpdateFormResult
extends CommonUseFormResult<CategoryUpdateData> {
handlers: CategoryUpdateHandlers;
@ -55,10 +58,10 @@ function useCategoryUpdateForm(
category: CategoryDetailsFragment,
onSubmit: (data: CategoryUpdateData) => Promise<any[]>,
disabled: boolean
): UseCategoryUpdateFormResult {
): UseCategoryUpdateFormResult & { richText: RichTextContextValues } {
const {
handleChange,
data,
data: formData,
triggerChange,
formId,
setIsSubmitDisabled
@ -73,7 +76,7 @@ function useCategoryUpdateForm(
formId
});
const [description, changeDescription] = useRichText({
const richText = useRichText({
initial: category?.description,
triggerChange
});
@ -86,18 +89,23 @@ function useCategoryUpdateForm(
const changeMetadata = makeMetadataChangeHandler(handleChange);
const data: CategoryUpdateData = {
...formData,
description: null
};
// Need to make it function to always have description.current up to date
const getData = (): CategoryUpdateData => ({
...data,
description: description.current
const getData = async (): Promise<CategoryUpdateData> => ({
...formData,
description: await richText.getValue()
});
const getSubmitData = (): CategoryUpdateData => ({
...getData(),
const getSubmitData = async (): Promise<CategoryUpdateData> => ({
...(await getData()),
...getMetadata(data, isMetadataModified, isPrivateMetadataModified)
});
const submit = () => handleFormSubmit(getSubmitData());
const submit = async () => handleFormSubmit(await getSubmitData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
@ -105,13 +113,13 @@ function useCategoryUpdateForm(
return {
change: handleChange,
data: getData(),
data,
handlers: {
changeDescription,
changeMetadata
},
submit,
isSaveDisabled: disabled
isSaveDisabled: disabled,
richText
};
}
@ -121,9 +129,19 @@ const CategoryUpdateForm: React.FC<CategoryUpdateFormProps> = ({
onSubmit,
disabled
}) => {
const props = useCategoryUpdateForm(category, onSubmit, disabled);
const { richText, ...props } = useCategoryUpdateForm(
category,
onSubmit,
disabled
);
return <form onSubmit={props.submit}>{children(props)}</form>;
return (
<form onSubmit={props.submit}>
<RichTextContext.Provider value={richText}>
{children(props)}
</RichTextContext.Provider>
</form>
);
};
CategoryUpdateForm.displayName = "CategoryUpdateForm";

View file

@ -77,7 +77,6 @@ const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
disabled={disabled}
errors={errors}
onChange={change}
onDescriptionChange={handlers.changeDescription}
/>
<CardSpacer />
<CollectionImage

View file

@ -4,7 +4,6 @@ import { createChannelsChangeHandler } from "@saleor/collections/utils";
import { COLLECTION_CREATE_FORM_ID } from "@saleor/collections/views/consts";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import useForm, {
CommonUseFormResultWithHandlers,
FormChange,
@ -12,6 +11,10 @@ import useForm, {
} from "@saleor/hooks/useForm";
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import {
RichTextContext,
RichTextContextValues
} from "@saleor/utils/richText/context";
import useRichText from "@saleor/utils/richText/useRichText";
import React, { useEffect } from "react";
@ -33,7 +36,6 @@ export interface CollectionCreateData extends CollectionCreateFormData {
interface CollectionCreateHandlers {
changeMetadata: FormChange;
changeDescription: RichTextEditorChange;
changeChannels: (
id: string,
data: Omit<ChannelCollectionData, "name" | "id">
@ -74,7 +76,7 @@ function useCollectionCreateForm(
setChannels: (data: ChannelCollectionData[]) => void,
onSubmit: (data: CollectionCreateData) => SubmitPromise,
disabled: boolean
): UseCollectionCreateFormResult {
): UseCollectionCreateFormResult & { richText: RichTextContextValues } {
const {
handleChange,
data: formData,
@ -95,7 +97,7 @@ function useCollectionCreateForm(
formId
});
const [description, changeDescription] = useRichText({
const richText = useRichText({
initial: null,
triggerChange
});
@ -106,10 +108,15 @@ function useCollectionCreateForm(
const changeMetadata = makeMetadataChangeHandler(handleChange);
// Need to make it function to always have description.current up to date
const getData = (): CollectionCreateData => ({
const data: CollectionCreateData = {
...formData,
description: description.current
description: null
};
// Need to make it function to always have description.current up to date
const getData = async (): Promise<CollectionCreateData> => ({
...formData,
description: await richText.getValue()
});
const handleChannelChange = createChannelsChangeHandler(
@ -118,7 +125,7 @@ function useCollectionCreateForm(
triggerChange
);
const submit = () => handleFormSubmit(getData());
const submit = async () => handleFormSubmit(await getData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
@ -127,14 +134,14 @@ function useCollectionCreateForm(
return {
change: handleChange,
data: getData(),
data,
handlers: {
changeChannels: handleChannelChange,
changeDescription,
changeMetadata
},
submit,
isSaveDisabled
isSaveDisabled,
richText
};
}
@ -145,14 +152,20 @@ const CollectionCreateForm: React.FC<CollectionCreateFormProps> = ({
onSubmit,
disabled
}) => {
const props = useCollectionCreateForm(
const { richText, ...props } = useCollectionCreateForm(
currentChannels,
setChannels,
onSubmit,
disabled
);
return <form onSubmit={props.submit}>{children(props)}</form>;
return (
<form onSubmit={props.submit}>
<RichTextContext.Provider value={richText}>
{children(props)}
</RichTextContext.Provider>
</form>
);
};
CollectionCreateForm.displayName = "CollectionCreateForm";

View file

@ -2,12 +2,11 @@ import { OutputData } from "@editorjs/editorjs";
import { Card, CardContent, TextField } from "@material-ui/core";
import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer";
import RichTextEditor, {
RichTextEditorChange
} from "@saleor/components/RichTextEditor";
import RichTextEditor from "@saleor/components/RichTextEditor";
import { CollectionErrorFragment } from "@saleor/graphql";
import { commonMessages } from "@saleor/intl";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
import { useRichTextContext } from "@saleor/utils/richText/context";
import React from "react";
import { useIntl } from "react-intl";
@ -19,18 +18,21 @@ export interface CollectionDetailsProps {
disabled: boolean;
errors: CollectionErrorFragment[];
onChange: (event: React.ChangeEvent<any>) => void;
onDescriptionChange: RichTextEditorChange;
}
const CollectionDetails: React.FC<CollectionDetailsProps> = ({
disabled,
data,
onChange,
onDescriptionChange,
errors
}) => {
const intl = useIntl();
const {
defaultValue,
editorRef,
isReadyForMount,
handleChange
} = useRichTextContext();
const formErrors = getFormErrors(["name", "description"], errors);
return (
@ -54,15 +56,18 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
fullWidth
/>
<FormSpacer />
{isReadyForMount && (
<RichTextEditor
data={data.description}
defaultValue={defaultValue}
editorRef={editorRef}
onChange={handleChange}
error={!!formErrors.description}
helperText={getProductErrorMessage(formErrors.description, intl)}
label={intl.formatMessage(commonMessages.description)}
name="description"
disabled={disabled}
onChange={onDescriptionChange}
/>
)}
</CardContent>
</Card>
);

View file

@ -88,7 +88,6 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
disabled={disabled}
errors={errors}
onChange={change}
onDescriptionChange={handlers.changeDescription}
/>
<CardSpacer />
<CollectionImage

View file

@ -4,7 +4,6 @@ import { createChannelsChangeHandler } from "@saleor/collections/utils";
import { COLLECTION_DETAILS_FORM_ID } from "@saleor/collections/views/consts";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import { CollectionDetailsFragment } from "@saleor/graphql";
import useForm, {
CommonUseFormResultWithHandlers,
@ -14,6 +13,10 @@ import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import {
RichTextContext,
RichTextContextValues
} from "@saleor/utils/richText/context";
import useRichText from "@saleor/utils/richText/useRichText";
import React, { useEffect } from "react";
@ -31,7 +34,6 @@ export interface CollectionUpdateData extends CollectionUpdateFormData {
interface CollectionUpdateHandlers {
changeMetadata: FormChange;
changeDescription: RichTextEditorChange;
changeChannels: (
id: string,
data: Omit<ChannelCollectionData, "name" | "id">
@ -71,7 +73,7 @@ function useCollectionUpdateForm(
setChannels: (data: ChannelCollectionData[]) => void,
onSubmit: (data: CollectionUpdateData) => Promise<any[]>,
disabled: boolean
): UseCollectionUpdateFormResult {
): UseCollectionUpdateFormResult & { richText: RichTextContextValues } {
const {
handleChange,
data: formData,
@ -92,7 +94,7 @@ function useCollectionUpdateForm(
formId: COLLECTION_DETAILS_FORM_ID
});
const [description, changeDescription] = useRichText({
const richText = useRichText({
initial: collection?.description,
triggerChange
});
@ -105,14 +107,19 @@ function useCollectionUpdateForm(
const changeMetadata = makeMetadataChangeHandler(handleChange);
// Need to make it function to always have description.current up to date
const getData = (): CollectionUpdateData => ({
const data: CollectionUpdateData = {
...formData,
description: description.current
description: null
};
// Need to make it function to always have description.current up to date
const getData = async (): Promise<CollectionUpdateData> => ({
...formData,
description: await richText.getValue()
});
const getSubmitData = (): CollectionUpdateData => ({
...getData(),
const getSubmitData = async (): Promise<CollectionUpdateData> => ({
...(await getData()),
...getMetadata(formData, isMetadataModified, isPrivateMetadataModified)
});
@ -122,7 +129,7 @@ function useCollectionUpdateForm(
triggerChange
);
const submit = () => handleFormSubmit(getSubmitData());
const submit = async () => handleFormSubmit(await getSubmitData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
@ -130,13 +137,13 @@ function useCollectionUpdateForm(
return {
change: handleChange,
data: getData(),
data,
handlers: {
changeChannels: handleChannelChange,
changeDescription,
changeMetadata
},
submit
submit,
richText
};
}
@ -148,7 +155,7 @@ const CollectionUpdateForm: React.FC<CollectionUpdateFormProps> = ({
onSubmit,
disabled
}) => {
const props = useCollectionUpdateForm(
const { richText, ...props } = useCollectionUpdateForm(
collection,
currentChannels,
setChannels,
@ -156,7 +163,13 @@ const CollectionUpdateForm: React.FC<CollectionUpdateFormProps> = ({
disabled
);
return <form onSubmit={props.submit}>{children(props)}</form>;
return (
<form onSubmit={props.submit}>
<RichTextContext.Provider value={richText}>
{children(props)}
</RichTextContext.Provider>
</form>
);
};
CollectionUpdateForm.displayName = "CollectionUpdateForm";

View file

@ -10,7 +10,6 @@ import {
getMultiChoices,
getMultiDisplayValue,
getReferenceDisplayValue,
getRichTextData,
getSingleChoices,
getSingleDisplayValue
} from "@saleor/components/Attributes/utils";
@ -43,8 +42,8 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
onChange,
fetchAttributeValues,
fetchMoreAttributeValues,
entityId,
onAttributeSelectBlur
onAttributeSelectBlur,
richTextGetters
}) => {
const intl = useIntl();
const classes = useStyles();
@ -126,18 +125,27 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
/>
);
case AttributeInputTypeEnum.RICH_TEXT:
const {
getShouldMount,
getDefaultValue,
getMountEditor,
getHandleChange
} = richTextGetters;
const defaultValue = getDefaultValue(attribute.id);
return (
<BasicAttributeRow label={attribute.label}>
{getShouldMount(attribute.id) && (
<RichTextEditor
key={entityId} // temporary workaround, TODO: refactor rich text editor
defaultValue={defaultValue}
editorRef={getMountEditor(attribute.id)}
onChange={getHandleChange(attribute.id)}
name={`attribute:${attribute.label}`}
disabled={disabled}
error={!!error}
label={intl.formatMessage(attributeRowMessages.valueLabel)}
helperText={getErrorMessage(error, intl)}
onChange={data => onChange(attribute.id, JSON.stringify(data))}
data={getRichTextData(attribute)}
/>
)}
</BasicAttributeRow>
);
case AttributeInputTypeEnum.NUMERIC:

View file

@ -20,7 +20,13 @@ const props: AttributesProps = {
onReferencesReorder: () => undefined,
fetchAttributeValues: () => undefined,
fetchMoreAttributeValues: fetchMoreProps,
onAttributeSelectBlur: () => undefined
onAttributeSelectBlur: () => undefined,
richTextGetters: {
getDefaultValue: () => undefined,
getHandleChange: () => () => undefined,
getMountEditor: () => () => undefined,
getShouldMount: () => true
}
};
storiesOf("Attributes / Attributes", module)

View file

@ -15,6 +15,7 @@ import {
import { FormsetAtomicData } from "@saleor/hooks/useFormset";
import { IconButton, makeStyles } from "@saleor/macaw-ui";
import { FetchMoreProps } from "@saleor/types";
import { RichTextGetters } from "@saleor/utils/richText/useMultipleRichText";
import classNames from "classnames";
import React from "react";
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
@ -46,7 +47,7 @@ export interface AttributesProps extends AttributeRowHandlers {
ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment
>;
title?: React.ReactNode;
entityId?: string;
richTextGetters: RichTextGetters<string>;
}
const useStyles = makeStyles(
@ -122,7 +123,7 @@ const Attributes: React.FC<AttributesProps> = ({
errors,
title,
onAttributeSelectBlur,
entityId = "_defaultId",
richTextGetters,
...props
}) => {
const intl = useIntl();
@ -170,11 +171,11 @@ const Attributes: React.FC<AttributesProps> = ({
<React.Fragment key={attribute.id}>
{attributeIndex > 0 && <Hr />}
<AttributeRow
entityId={entityId}
attribute={attribute}
attributeValues={attributeValues}
error={error}
onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={richTextGetters}
{...props}
/>
</React.Fragment>

View file

@ -5,6 +5,7 @@ import {
} from "@saleor/graphql";
import { FormsetChange } from "@saleor/hooks/useFormset";
import { FetchMoreProps, ReorderEvent } from "@saleor/types";
import { RichTextGetters } from "@saleor/utils/richText/useMultipleRichText";
import { AttributeInput } from "./Attributes";
@ -31,6 +32,6 @@ export interface AttributeRowProps extends AttributeRowHandlers {
disabled: boolean;
error: ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment;
loading: boolean;
entityId: string;
onAttributeSelectBlur?: () => void;
richTextGetters: RichTextGetters<string>;
}

View file

@ -7,6 +7,7 @@ export interface GridProps {
children: React.ReactNodeArray | React.ReactNode;
className?: string;
variant?: GridVariant;
richText?: boolean;
}
const useStyles = makeStyles(
@ -31,13 +32,18 @@ const useStyles = makeStyles(
},
uniform: {
gridTemplateColumns: "1fr 1fr"
},
richText: {
"&& > div": {
overflow: "visible"
}
}
}),
{ name: "Grid" }
);
export const Grid: React.FC<GridProps> = props => {
const { className, children, variant } = props;
const { className, children, variant, richText } = props;
const classes = useStyles(props);
@ -46,7 +52,8 @@ export const Grid: React.FC<GridProps> = props => {
className={classNames(className, classes.root, {
[classes.default]: variant === "default",
[classes.inverted]: variant === "inverted",
[classes.uniform]: variant === "uniform"
[classes.uniform]: variant === "uniform",
[classes.richText]: richText
})}
>
{children}

View file

@ -9,16 +9,16 @@ import * as fixtures from "./fixtures.json";
import { RichTextEditorProps } from "./RichTextEditor";
import RichTextEditorContent from "./RichTextEditorContent";
export const data: OutputData = fixtures.richTextEditor;
export const defaultValue: OutputData = fixtures.richTextEditor;
const props: RichTextEditorProps = {
data,
defaultValue,
disabled: false,
error: false,
helperText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
label: "Content",
name: "content",
onChange: () => undefined
editorRef: null
};
storiesOf("Generics / Rich text editor", module)
@ -27,4 +27,6 @@ storiesOf("Generics / Rich text editor", module)
.add("default", () => <RichTextEditor {...props} />)
.add("disabled", () => <RichTextEditor {...props} disabled={true} />)
.add("error", () => <RichTextEditor {...props} error={true} />)
.add("static", () => <RichTextEditorContent {...props} />);
.add("static", () => (
<RichTextEditorContent {...props} value={defaultValue} />
));

View file

@ -1,98 +1,73 @@
import EditorJS, { LogLevels, OutputData } from "@editorjs/editorjs";
import { LogLevels, OutputData } from "@editorjs/editorjs";
import { FormControl, FormHelperText, InputLabel } from "@material-ui/core";
import { PromiseQueue } from "@saleor/misc";
import { useId } from "@reach/auto-id";
import { Props as ReactEditorJSProps } from "@react-editor-js/core";
import classNames from "classnames";
import React from "react";
import { createReactEditorJS } from "react-editor-js";
import { RichTextEditorContentProps, tools } from "./RichTextEditorContent";
import { tools } from "./consts";
import { useHasRendered } from "./hooks";
import useStyles from "./styles";
import { clean } from "./utils";
export type RichTextEditorChange = (data: OutputData) => void;
export interface RichTextEditorProps extends RichTextEditorContentProps {
export type EditorJsProps = Omit<ReactEditorJSProps, "factory">;
// https://github.com/Jungwoo-An/react-editor-js#how-to-access-editor-js-instance
export interface EditorCore {
destroy(): Promise<void>;
clear(): Promise<void>;
save(): Promise<OutputData>;
render(data: OutputData): Promise<void>;
}
export interface RichTextEditorProps extends Omit<EditorJsProps, "onChange"> {
id?: string;
disabled: boolean;
error: boolean;
helperText: string;
label: string;
name: string;
onChange: RichTextEditorChange;
editorRef:
| React.RefCallback<EditorCore>
| React.MutableRefObject<EditorCore>
| null;
// onChange with value shouldn't be used due to issues with React and EditorJS integration
onChange?: () => void;
}
const ReactEditorJS = createReactEditorJS();
const RichTextEditor: React.FC<RichTextEditorProps> = ({
data,
id: defaultId,
disabled,
error,
helperText,
label,
name,
onChange,
onReady
helperText,
editorRef,
onInitialize,
onReady,
...props
}) => {
const classes = useStyles({});
const id = useId(defaultId);
const [isFocused, setIsFocused] = React.useState(false);
const [isFocused, setFocus] = React.useState(false);
const editor = React.useRef<EditorJS>();
const editorContainer = React.useRef<HTMLDivElement>();
const togglePromiseQueue = React.useRef(PromiseQueue()); // used to await subsequent toggle invocations
React.useEffect(
() => {
if (data !== undefined && !editor.current) {
editor.current = new EditorJS({
data,
holder: editorContainer.current,
logLevel: "ERROR" as LogLevels,
onChange: async api => {
const savedData = await api.saver.save();
onChange(savedData);
},
onReady: () => {
// FIXME: This throws an error and is not working
// const undo = new Undo({ editor });
// undo.initialize(data);
if (onReady) {
onReady();
}
},
tools
});
const handleInitialize = React.useCallback((editor: EditorCore) => {
if (onInitialize) {
onInitialize(editor);
}
return () => {
clean(editor.current);
editor.current = null;
};
},
// Rerender editor only if changed from undefined to defined state
[data === undefined]
);
React.useEffect(() => {
const toggle = async () => {
if (!editor.current) {
return;
if (typeof editorRef === "function") {
return editorRef(editor);
}
await editor.current.isReady;
if (editor.current?.readOnly) {
// readOnly.toggle() by itself does not enqueue the events and will result in a broken output if invocations overlap
// Remove this logic when this is fixed in EditorJS
togglePromiseQueue.current.add(() =>
editor.current.readOnly.toggle(disabled)
);
// Switching to readOnly with empty blocks present causes the editor to freeze
// Remove this logic when this is fixed in EditorJS
if (!disabled && !data?.blocks?.length) {
await togglePromiseQueue.current.queue;
editor.current.clear();
if (editorRef) {
return (editorRef.current = editor);
}
}
};
}, []);
toggle();
}, [disabled]);
// We need to render FormControl first to get id from @reach/auto-id
const hasRendered = useHasRendered();
return (
<FormControl
@ -105,16 +80,28 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
<InputLabel focused={true} shrink={true}>
{label}
</InputLabel>
{hasRendered && (
<ReactEditorJS
// match with the id of holder div
holder={id}
tools={tools}
// LogLeves is undefined at runtime
logLevel={"ERROR" as LogLevels.ERROR}
onInitialize={handleInitialize}
{...props}
>
<div
id={id}
className={classNames(classes.editor, classes.root, {
[classes.rootActive]: isFocused,
[classes.rootDisabled]: disabled,
[classes.rootError]: error
})}
ref={editorContainer}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
</ReactEditorJS>
)}
<FormHelperText>{helperText}</FormHelperText>
</FormControl>
);

View file

@ -1,104 +1,52 @@
import EditorJS, {
LogLevels,
OutputData,
ToolConstructable,
ToolSettings
} from "@editorjs/editorjs";
import Embed from "@editorjs/embed";
import Header from "@editorjs/header";
import List from "@editorjs/list";
import Paragraph from "@editorjs/paragraph";
import Quote from "@editorjs/quote";
import strikethroughIcon from "@saleor/icons/StrikethroughIcon";
import { LogLevels } from "@editorjs/editorjs";
import { useId } from "@reach/auto-id";
import classNames from "classnames";
import createGenericInlineTool from "editorjs-inline-tool";
import React from "react";
import { createReactEditorJS } from "react-editor-js";
import { tools } from "./consts";
import { useHasRendered } from "./hooks";
import { EditorJsProps } from "./RichTextEditor";
import useStyles from "./styles";
import { clean } from "./utils";
export interface RichTextEditorContentProps {
export interface RichTextEditorContentProps
extends Omit<EditorJsProps, "defaultValue"> {
id?: string;
className?: string;
data: OutputData;
onReady?: () => void;
}
const inlineToolbar = ["link", "bold", "italic", "strikethrough"];
export const tools: Record<string, ToolConstructable | ToolSettings> = {
embed: Embed,
header: {
class: Header,
config: {
defaultLevel: 1,
levels: [1, 2, 3]
},
inlineToolbar
},
list: {
class: List,
inlineToolbar
},
quote: {
class: Quote,
inlineToolbar
},
paragraph: {
class: Paragraph,
inlineToolbar
},
strikethrough: createGenericInlineTool({
sanitize: {
s: {}
},
shortcut: "CMD+S",
tagName: "s",
toolboxIcon: strikethroughIcon
})
};
const ReactEditorJS = createReactEditorJS();
const RichTextEditorContent: React.FC<RichTextEditorContentProps> = ({
id: defaultId,
className,
data,
onReady
value,
...props
}) => {
const classes = useStyles({});
const id = useId(defaultId);
const editor = React.useRef<EditorJS>();
const editorContainer = React.useRef<HTMLDivElement>();
React.useEffect(
() => {
if (data !== undefined && !editor.current) {
const editorjs = new EditorJS({
data,
holder: editorContainer.current,
logLevel: "ERROR" as LogLevels,
onReady: () => {
editor.current = editorjs;
// We need to render FormControl first to get id from @reach/auto-id
const hasRendered = useHasRendered();
if (onReady) {
onReady();
if (!hasRendered) {
return <div />;
}
},
readOnly: true,
tools
});
}
return () => {
clean(editor.current);
editor.current = null;
};
},
// Rerender editor only if changed from undefined to defined state
[data === undefined]
);
return (
<ReactEditorJS
holder={id}
logLevel={"ERROR" as LogLevels.ERROR}
tools={tools}
{...props}
defaultValue={value}
readOnly={true}
>
<div
id={id}
className={classNames(classes.editor, classes.rootStatic, className)}
ref={editorContainer}
/>
</ReactEditorJS>
);
};

View file

@ -0,0 +1,30 @@
import { FormControl, FormHelperText, InputLabel } from "@material-ui/core";
import React from "react";
import { RichTextEditorProps } from "../RichTextEditor";
export const HOLDER = "TEST_HOLDER";
const RichTextEditor: React.FC<RichTextEditorProps> = ({
disabled,
error,
label,
name,
helperText
}) => (
<FormControl
data-test-id={"rich-text-editor-" + name}
disabled={disabled}
error={error}
fullWidth
variant="outlined"
>
<InputLabel focused={true} shrink={true}>
{label}
</InputLabel>
<FormHelperText>{helperText}</FormHelperText>
</FormControl>
);
export default RichTextEditor;

View file

@ -0,0 +1,42 @@
import { ToolConstructable, ToolSettings } from "@editorjs/editorjs";
import Embed from "@editorjs/embed";
import Header from "@editorjs/header";
import List from "@editorjs/list";
import Paragraph from "@editorjs/paragraph";
import Quote from "@editorjs/quote";
import strikethroughIcon from "@saleor/icons/StrikethroughIcon";
import createGenericInlineTool from "editorjs-inline-tool";
const inlineToolbar = ["link", "bold", "italic", "strikethrough"];
export const tools: Record<string, ToolConstructable | ToolSettings> = {
embed: Embed,
header: {
class: Header,
config: {
defaultLevel: 1,
levels: [1, 2, 3]
},
inlineToolbar
},
list: {
class: List,
inlineToolbar
},
quote: {
class: Quote,
inlineToolbar
},
paragraph: {
class: Paragraph,
inlineToolbar
},
strikethrough: createGenericInlineTool({
sanitize: {
s: {}
},
shortcut: "CMD+S",
tagName: "s",
toolboxIcon: strikethroughIcon
})
};

View file

@ -0,0 +1,11 @@
import { useLayoutEffect, useState } from "react";
export const useHasRendered = () => {
const [hasRendered, setHasRendereed] = useState(false);
useLayoutEffect(() => {
setHasRendereed(true);
}, []);
return hasRendered;
};

View file

@ -139,7 +139,14 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
onSubmit={onSubmit}
disabled={loading}
>
{({ change, data, handlers, submit, isSaveDisabled }) => (
{({
change,
data,
handlers,
submit,
isSaveDisabled,
attributeRichTextGetters
}) => (
<Container>
<Backlink href={pageListUrl()}>
{intl.formatMessage(sectionNames.pages)}
@ -162,7 +169,6 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
disabled={loading}
errors={errors}
onChange={change}
onContentChange={handlers.changeContent}
/>
<CardSpacer />
<SeoForm
@ -199,6 +205,7 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues}
onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={attributeRichTextGetters}
/>
)}
<CardSpacer />

View file

@ -1,5 +1,11 @@
import { OutputData } from "@editorjs/editorjs";
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
import {
getAttributesDisplayData,
getRichTextAttributesFromMap,
getRichTextDataFromAttributes,
mergeAttributes,
RichTextProps
} from "@saleor/attributes/utils/data";
import {
createAttributeChangeHandler,
createAttributeFileChangeHandler,
@ -12,7 +18,6 @@ import {
import { AttributeInput } from "@saleor/components/Attributes";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import {
PageDetailsFragment,
SearchPagesQuery,
@ -39,6 +44,8 @@ import getPublicationData from "@saleor/utils/data/getPublicationData";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { RichTextContext } from "@saleor/utils/richText/context";
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
import useRichText from "@saleor/utils/richText/useRichText";
import React, { useEffect } from "react";
@ -64,7 +71,6 @@ export interface PageSubmitData extends PageFormData {
export interface PageUpdateHandlers {
changeMetadata: FormChange;
changeContent: RichTextEditorChange;
selectPageType: FormChange;
selectAttribute: FormsetChange<string>;
selectAttributeMulti: FormsetChange<string>;
@ -75,11 +81,17 @@ export interface PageUpdateHandlers {
fetchMoreReferences: FetchMoreProps;
}
export interface UsePageUpdateFormResult
extends CommonUseFormResultWithHandlers<PageData, PageUpdateHandlers> {
export interface UsePageUpdateFormOutput
extends CommonUseFormResultWithHandlers<PageData, PageUpdateHandlers>,
RichTextProps {
valid: boolean;
}
export type UsePageUpdateFormRenderProps = Omit<
UsePageUpdateFormOutput,
"richText"
>;
export interface UsePageFormOpts {
pageTypes?: RelayToFlat<SearchPageTypesQuery["search"]>;
referencePages: RelayToFlat<SearchPagesQuery["search"]>;
@ -94,7 +106,7 @@ export interface UsePageFormOpts {
}
export interface PageFormProps extends UsePageFormOpts {
children: (props: UsePageUpdateFormResult) => React.ReactNode;
children: (props: UsePageUpdateFormRenderProps) => React.ReactNode;
page: PageDetailsFragment;
onSubmit: (data: PageData) => SubmitPromise;
disabled: boolean;
@ -117,18 +129,9 @@ function usePageForm(
onSubmit: (data: PageData) => SubmitPromise,
disabled: boolean,
opts: UsePageFormOpts
): UsePageUpdateFormResult {
): UsePageUpdateFormOutput {
const pageExists = page !== null;
const attributes = useFormset(
pageExists
? getAttributeInputFromPage(page)
: opts.selectedPageType
? getAttributeInputFromPageType(opts.selectedPageType)
: []
);
const attributesWithNewFileValue = useFormset<null, File>([]);
const { handleChange, triggerChange, data: formData, formId } = useForm(
getInitialFormData(page),
undefined,
@ -137,11 +140,28 @@ function usePageForm(
}
);
const attributes = useFormset(
pageExists
? getAttributeInputFromPage(page)
: opts.selectedPageType
? getAttributeInputFromPageType(opts.selectedPageType)
: []
);
const {
getters: attributeRichTextGetters,
getValues: getAttributeRichTextValues
} = useMultipleRichText({
initial: getRichTextDataFromAttributes(attributes.data),
triggerChange
});
const attributesWithNewFileValue = useFormset<null, File>([]);
const { setExitDialogSubmitRef, setIsSubmitDisabled } = useExitFormDialog({
formId
});
const [content, changeContent] = useRichText({
const richText = useRichText({
initial: pageExists ? page?.content : null,
triggerChange
});
@ -195,8 +215,7 @@ function usePageForm(
triggerChange
);
// Need to make it function to always have content.current up to date
const getData = (): PageData => ({
const data: PageData = {
...formData,
attributes: getAttributesDisplayData(
attributes.data,
@ -204,14 +223,22 @@ function usePageForm(
opts.referencePages,
opts.referenceProducts
),
content: content.current,
content: null,
pageType: pageExists ? page?.pageType : opts.selectedPageType
});
};
const getSubmitData = (): PageSubmitData => ({
...getData(),
const getSubmitData = async (): Promise<PageSubmitData> => ({
...data,
...getMetadata(formData, isMetadataModified, isPrivateMetadataModified),
...getPublicationData(formData),
content: await richText.getValue(),
attributes: mergeAttributes(
attributes.data,
getRichTextAttributesFromMap(
attributes.data,
await getAttributeRichTextValues()
)
),
attributesWithNewFileValue: attributesWithNewFileValue.data
});
@ -230,7 +257,7 @@ function usePageForm(
onSubmit: handleSubmit
});
const submit = () => handleFormSubmit(getSubmitData());
const submit = async () => handleFormSubmit(await getSubmitData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
@ -241,10 +268,9 @@ function usePageForm(
return {
change: handleChange,
data: getData(),
data,
valid,
handlers: {
changeContent,
changeMetadata,
fetchMoreReferences: handleFetchMoreReferences,
fetchReferences: handleFetchReferences,
@ -256,7 +282,9 @@ function usePageForm(
selectPageType: handlePageTypeSelect
},
submit,
isSaveDisabled
isSaveDisabled,
richText,
attributeRichTextGetters
};
}
@ -267,9 +295,15 @@ const PageForm: React.FC<PageFormProps> = ({
disabled,
...rest
}) => {
const props = usePageForm(page, onSubmit, disabled, rest);
const { richText, ...props } = usePageForm(page, onSubmit, disabled, rest);
return <form onSubmit={props.submit}>{children(props)}</form>;
return (
<form onSubmit={props.submit}>
<RichTextContext.Provider value={richText}>
{children(props)}
</RichTextContext.Provider>
</form>
);
};
PageForm.displayName = "PageForm";

View file

@ -1,14 +1,13 @@
import { Card, CardContent, TextField } from "@material-ui/core";
import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer";
import RichTextEditor, {
RichTextEditorChange
} from "@saleor/components/RichTextEditor";
import RichTextEditor from "@saleor/components/RichTextEditor";
import { PageErrorFragment } from "@saleor/graphql";
import { commonMessages } from "@saleor/intl";
import { makeStyles } from "@saleor/macaw-ui";
import { getFormErrors } from "@saleor/utils/errors";
import getPageErrorMessage from "@saleor/utils/errors/page";
import { useRichTextContext } from "@saleor/utils/richText/context";
import React from "react";
import { useIntl } from "react-intl";
@ -19,7 +18,6 @@ export interface PageInfoProps {
disabled: boolean;
errors: PageErrorFragment[];
onChange: (event: React.ChangeEvent<any>) => void;
onContentChange: RichTextEditorChange;
}
const useStyles = makeStyles(
@ -32,11 +30,17 @@ const useStyles = makeStyles(
);
const PageInfo: React.FC<PageInfoProps> = props => {
const { data, disabled, errors, onChange, onContentChange } = props;
const { data, disabled, errors, onChange } = props;
const classes = useStyles(props);
const intl = useIntl();
const {
defaultValue,
editorRef,
isReadyForMount,
handleChange
} = useRichTextContext();
const formErrors = getFormErrors(["title", "content"], errors);
return (
@ -60,8 +64,11 @@ const PageInfo: React.FC<PageInfoProps> = props => {
onChange={onChange}
/>
<FormSpacer />
{isReadyForMount && (
<RichTextEditor
data={data.content}
defaultValue={defaultValue}
editorRef={editorRef}
onChange={handleChange}
disabled={disabled}
error={!!formErrors.content}
helperText={getPageErrorMessage(formErrors.content, intl)}
@ -71,8 +78,8 @@ const PageInfo: React.FC<PageInfoProps> = props => {
description: "page content"
})}
name={"content" as keyof PageData}
onChange={onContentChange}
/>
)}
</CardContent>
</Card>
);

View file

@ -206,7 +206,15 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
assignReferencesAttributeId={assignReferencesAttributeId}
loading={loading}
>
{({ change, data, formErrors, handlers, submit, isSaveDisabled }) => {
{({
change,
data,
formErrors,
handlers,
submit,
isSaveDisabled,
attributeRichTextGetters
}) => {
// Comparing explicitly to false because `hasVariants` can be undefined
const isSimpleProduct = data.productType?.hasVariants === false;
@ -223,7 +231,6 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
disabled={loading}
errors={errors}
onChange={change}
onDescriptionChange={handlers.changeDescription}
/>
<CardSpacer />
{data.attributes.length > 0 && (
@ -242,6 +249,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues}
onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={attributeRichTextGetters}
/>
)}
<CardSpacer />

View file

@ -1,5 +1,11 @@
import { OutputData } from "@editorjs/editorjs";
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
import {
getAttributesDisplayData,
getRichTextAttributesFromMap,
getRichTextDataFromAttributes,
mergeAttributes,
RichTextProps
} from "@saleor/attributes/utils/data";
import {
createAttributeChangeHandler,
createAttributeFileChangeHandler,
@ -17,7 +23,6 @@ import {
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
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 {
ProductTypeQuery,
@ -56,6 +61,8 @@ import { FetchMoreProps, RelayToFlat, ReorderEvent } from "@saleor/types";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { RichTextContext } from "@saleor/utils/richText/context";
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
import useRichText from "@saleor/utils/richText/useRichText";
import React, { useEffect } from "react";
import { useIntl } from "react-intl";
@ -119,20 +126,25 @@ export interface ProductCreateHandlers
Record<"selectAttributeFile", FormsetChange<File>>,
Record<"reorderAttributeValue", FormsetChange<ReorderEvent>>,
Record<"addStock" | "deleteStock", (id: string) => void> {
changeDescription: RichTextEditorChange;
changePreorderEndDate: FormChange;
fetchReferences: (value: string) => void;
fetchMoreReferences: FetchMoreProps;
}
export interface UseProductCreateFormResult
export interface UseProductCreateFormOutput
extends CommonUseFormResultWithHandlers<
ProductCreateData,
ProductCreateHandlers
> {
>,
RichTextProps {
disabled: boolean;
formErrors: FormErrors<ProductCreateData>;
}
export type UseProductCreateFormRenderProps = Omit<
UseProductCreateFormOutput,
"richText"
>;
export interface UseProductCreateFormOpts
extends Record<
"categories" | "collections" | "taxTypes",
@ -160,7 +172,7 @@ export interface UseProductCreateFormOpts
}
export interface ProductCreateFormProps extends UseProductCreateFormOpts {
children: (props: UseProductCreateFormResult) => React.ReactNode;
children: (props: UseProductCreateFormRenderProps) => React.ReactNode;
initial?: Partial<ProductCreateFormData>;
onSubmit: (data: ProductCreateData) => SubmitPromise;
loading: boolean;
@ -171,7 +183,7 @@ function useProductCreateForm(
onSubmit: (data: ProductCreateData) => SubmitPromise,
loading: boolean,
opts: UseProductCreateFormOpts
): UseProductCreateFormResult {
): UseProductCreateFormOutput {
const intl = useIntl();
const defaultInitialFormData: ProductCreateFormData &
Record<"productType", string> = {
@ -224,9 +236,16 @@ function useProductCreateForm(
? getAttributeInputFromProductType(opts.selectedProductType)
: []
);
const {
getters: attributeRichTextGetters,
getValues: getAttributeRichTextValues
} = useMultipleRichText({
initial: getRichTextDataFromAttributes(attributes.data),
triggerChange
});
const attributesWithNewFileValue = useFormset<null, File>([]);
const stocks = useFormset<ProductStockFormsetData>([]);
const [description, changeDescription] = useRichText({
const richText = useRichText({
initial: null,
triggerChange
});
@ -329,7 +348,7 @@ function useProductCreateForm(
intl.formatMessage(errorMessages.preorderEndDateInFutureErrorText)
);
const getData = (): ProductCreateData => ({
const data: ProductCreateData = {
...formData,
attributes: getAttributesDisplayData(
attributes.data,
@ -338,19 +357,29 @@ function useProductCreateForm(
opts.referenceProducts
),
attributesWithNewFileValue: attributesWithNewFileValue.data,
description: description.current,
description: null,
productType: opts.selectedProductType,
stocks: stocks.data
});
};
const data = getData();
const getData = async (): Promise<ProductCreateData> => ({
...data,
description: await richText.getValue(),
attributes: mergeAttributes(
attributes.data,
getRichTextAttributesFromMap(
attributes.data,
await getAttributeRichTextValues()
)
)
});
const handleFormSubmit = useHandleFormSubmit({
formId,
onSubmit
});
const submit = () => handleFormSubmit(data);
const submit = async () => handleFormSubmit(await getData());
const { setExitDialogSubmitRef, setIsSubmitDisabled } = useExitFormDialog({
formId: PRODUCT_CREATE_FORM_ID
@ -398,7 +427,6 @@ function useProductCreateForm(
addStock: handleStockAdd,
changeChannelPrice: handleChannelPriceChange,
changeChannels: handleChannelsChange,
changeDescription,
changeMetadata,
changeStock: handleStockChange,
changePreorderEndDate: handlePreorderEndDateChange,
@ -416,7 +444,9 @@ function useProductCreateForm(
selectTaxRate: handleTaxTypeSelect
},
submit,
isSaveDisabled
isSaveDisabled,
richText,
attributeRichTextGetters
};
}
@ -427,9 +457,20 @@ const ProductCreateForm: React.FC<ProductCreateFormProps> = ({
loading,
...rest
}) => {
const props = useProductCreateForm(initial || {}, onSubmit, loading, rest);
const { richText, ...props } = useProductCreateForm(
initial || {},
onSubmit,
loading,
rest
);
return <form onSubmit={props.submit}>{children(props)}</form>;
return (
<form onSubmit={props.submit}>
<RichTextContext.Provider value={richText}>
{children(props)}
</RichTextContext.Provider>
</form>
);
};
ProductCreateForm.displayName = "ProductCreateForm";

View file

@ -4,12 +4,11 @@ import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer";
import Grid from "@saleor/components/Grid";
import Hr from "@saleor/components/Hr";
import RichTextEditor, {
RichTextEditorChange
} from "@saleor/components/RichTextEditor";
import RichTextEditor from "@saleor/components/RichTextEditor";
import { ProductErrorFragment } from "@saleor/graphql";
import { commonMessages } from "@saleor/intl";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
import { useRichTextContext } from "@saleor/utils/richText/context";
import React from "react";
import { useIntl } from "react-intl";
@ -22,7 +21,6 @@ interface ProductDetailsFormProps {
disabled?: boolean;
errors: ProductErrorFragment[];
onDescriptionChange: RichTextEditorChange;
onChange(event: any);
}
@ -30,13 +28,17 @@ export const ProductDetailsForm: React.FC<ProductDetailsFormProps> = ({
data,
disabled,
errors,
onDescriptionChange,
onChange
}) => {
const intl = useIntl();
const {
editorRef,
defaultValue,
isReadyForMount,
handleChange
} = useRichTextContext();
const formErrors = getFormErrors(["name", "description", "rating"], errors);
return (
<Card>
<CardTitle
@ -58,15 +60,18 @@ export const ProductDetailsForm: React.FC<ProductDetailsFormProps> = ({
onChange={onChange}
/>
<FormSpacer />
{isReadyForMount && (
<RichTextEditor
data={data.description}
editorRef={editorRef}
defaultValue={defaultValue}
onChange={handleChange}
disabled={disabled}
error={!!formErrors.description}
helperText={getProductErrorMessage(formErrors.description, intl)}
label={intl.formatMessage(commonMessages.description)}
name="description"
onChange={onDescriptionChange}
/>
)}
<FormSpacer />
<Hr />
<FormSpacer />

View file

@ -17,12 +17,15 @@ const channels = createChannelsData(channelsList);
import * as _useNavigator from "@saleor/hooks/useNavigator";
import Adapter from "enzyme-adapter-react-16";
import { act } from "react-dom/test-utils";
import { MemoryRouter } from "react-router-dom";
configure({ adapter: new Adapter() });
const onSubmit = jest.fn();
const useNavigator = jest.spyOn(_useNavigator, "default");
jest.mock("@saleor/components/RichTextEditor/RichTextEditor");
jest.mock("@saleor/utils/richText/useRichText");
(global as any).document.createRange = () => ({
// eslint-disable-next-line
@ -94,7 +97,7 @@ const selectors = {
describe("Product details page", () => {
useNavigator.mockImplementation();
it("can select empty option on attribute", () => {
it("can select empty option on attribute", async () => {
const component = mount(
<MemoryRouter>
<Wrapper>
@ -124,10 +127,15 @@ describe("Product details page", () => {
.first()
.prop("value")
).toEqual("");
await act(async () => {
component
.find("form")
.first()
.simulate("submit");
// wait for async function to complete
await new Promise(process.nextTick);
});
expect(onSubmit.mock.calls[0][0].attributes[0].value.length).toEqual(0);
});
});

View file

@ -282,7 +282,15 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
assignReferencesAttributeId={assignReferencesAttributeId}
disabled={disabled}
>
{({ change, data, formErrors, handlers, submit, isSaveDisabled }) => (
{({
change,
data,
formErrors,
handlers,
submit,
isSaveDisabled,
attributeRichTextGetters
}) => (
<>
<Container>
<Backlink href={productListUrl()}>
@ -293,13 +301,12 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
<CardMenu menuItems={extensionMenuItems} data-test-id="menu" />
)}
</PageHeader>
<Grid>
<Grid richText>
<div>
<ProductDetailsForm
data={data}
disabled={disabled}
errors={errors}
onDescriptionChange={handlers.changeDescription}
onChange={change}
/>
<CardSpacer />
@ -331,6 +338,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues}
onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={attributeRichTextGetters}
/>
)}
<CardSpacer />

View file

@ -1,5 +1,11 @@
import { OutputData } from "@editorjs/editorjs";
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
import {
getAttributesDisplayData,
getRichTextAttributesFromMap,
getRichTextDataFromAttributes,
mergeAttributes,
RichTextProps
} from "@saleor/attributes/utils/data";
import {
createAttributeChangeHandler,
createAttributeFileChangeHandler,
@ -18,7 +24,6 @@ import { AttributeInput } from "@saleor/components/Attributes";
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
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 {
ProductFragment,
@ -62,6 +67,8 @@ import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAu
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import getMetadata from "@saleor/utils/metadata/getMetadata";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { RichTextContext } from "@saleor/utils/richText/context";
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
import useRichText from "@saleor/utils/richText/useRichText";
import React, { useEffect, useMemo } from "react";
import { useIntl } from "react-intl";
@ -147,19 +154,25 @@ export interface ProductUpdateHandlers
Record<"selectAttributeFile", FormsetChange<File>>,
Record<"reorderAttributeValue", FormsetChange<ReorderEvent>>,
Record<"addStock" | "deleteStock", (id: string) => void> {
changeDescription: RichTextEditorChange;
changePreorderEndDate: FormChange;
fetchReferences: (value: string) => void;
fetchMoreReferences: FetchMoreProps;
}
export interface UseProductUpdateFormResult
export interface UseProductUpdateFormOutput
extends CommonUseFormResultWithHandlers<
ProductUpdateData,
ProductUpdateHandlers
> {
>,
RichTextProps {
formErrors: FormErrors<ProductUpdateSubmitData>;
}
export type UseProductUpdateFormRenderProps = Omit<
UseProductUpdateFormOutput,
"richText"
>;
export interface UseProductUpdateFormOpts
extends Record<
"categories" | "collections" | "taxTypes",
@ -189,7 +202,7 @@ export interface UseProductUpdateFormOpts
}
export interface ProductUpdateFormProps extends UseProductUpdateFormOpts {
children: (props: UseProductUpdateFormResult) => React.ReactNode;
children: (props: UseProductUpdateFormRenderProps) => React.ReactNode;
product: ProductFragment;
onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise;
disabled: boolean;
@ -224,7 +237,7 @@ function useProductUpdateForm(
onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise,
disabled: boolean,
opts: UseProductUpdateFormOpts
): UseProductUpdateFormResult {
): UseProductUpdateFormOutput {
const intl = useIntl();
const initial = useMemo(
() =>
@ -257,9 +270,16 @@ function useProductUpdateForm(
} = form;
const attributes = useFormset(getAttributeInputFromProduct(product));
const {
getters: attributeRichTextGetters,
getValues: getAttributeRichTextValues
} = useMultipleRichText({
initial: getRichTextDataFromAttributes(attributes.data),
triggerChange
});
const attributesWithNewFileValue = useFormset<null, File>([]);
const stocks = useFormset(getStockInputFromProduct(product));
const [description, changeDescription] = useRichText({
const richText = useRichText({
initial: product?.description,
triggerChange
});
@ -382,18 +402,23 @@ function useProductUpdateForm(
opts.referencePages,
opts.referenceProducts
),
description: description.current,
description: null,
stocks: stocks.data
};
// Need to make it function to always have description.current up to date
const getSubmitData = (): ProductUpdateSubmitData => ({
const getSubmitData = async (): Promise<ProductUpdateSubmitData> => ({
...data,
...getStocksData(product, stocks.data),
...getMetadata(data, isMetadataModified, isPrivateMetadataModified),
attributes: attributes.data,
attributes: mergeAttributes(
attributes.data,
getRichTextAttributesFromMap(
attributes.data,
await getAttributeRichTextValues()
)
),
attributesWithNewFileValue: attributesWithNewFileValue.data,
description: description.current
description: await richText.getValue()
});
const handleSubmit = async (data: ProductUpdateSubmitData) => {
@ -411,7 +436,7 @@ function useProductUpdateForm(
onSubmit: handleSubmit
});
const submit = async () => handleFormSubmit(getSubmitData());
const submit = async () => handleFormSubmit(await getSubmitData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
@ -458,7 +483,6 @@ function useProductUpdateForm(
changeChannelPrice: handleChannelPriceChange,
changeChannelPreorder: handleChannelPreorderChange,
changeChannels: handleChannelsChange,
changeDescription,
changeMetadata,
changeStock: handleStockChange,
changePreorderEndDate: handlePreorderEndDateChange,
@ -475,7 +499,9 @@ function useProductUpdateForm(
selectTaxRate: handleTaxTypeSelect
},
submit,
isSaveDisabled
isSaveDisabled,
richText,
attributeRichTextGetters
};
}
@ -486,9 +512,20 @@ const ProductUpdateForm: React.FC<ProductUpdateFormProps> = ({
disabled,
...rest
}) => {
const props = useProductUpdateForm(product, onSubmit, disabled, rest);
const { richText, ...props } = useProductUpdateForm(
product,
onSubmit,
disabled,
rest
);
return <form onSubmit={props.submit}>{children(props)}</form>;
return (
<form onSubmit={props.submit}>
<RichTextContext.Provider value={richText}>
{children(props)}
</RichTextContext.Provider>
</form>
);
};
ProductUpdateForm.displayName = "ProductUpdateForm";

View file

@ -159,7 +159,15 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
assignReferencesAttributeId={assignReferencesAttributeId}
disabled={disabled}
>
{({ change, data, formErrors, handlers, submit, isSaveDisabled }) => (
{({
change,
data,
formErrors,
handlers,
submit,
isSaveDisabled,
attributeRichTextGetters
}) => (
<Container>
<Backlink href={productUrl(productId)}>{product?.name}</Backlink>
<PageHeader title={header} />
@ -193,6 +201,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues}
onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={attributeRichTextGetters}
/>
<CardSpacer />
<Attributes
@ -215,6 +224,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues}
onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={attributeRichTextGetters}
/>
<CardSpacer />
<ProductVariantCheckoutSettings

View file

@ -1,4 +1,10 @@
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
import {
getAttributesDisplayData,
getRichTextAttributesFromMap,
getRichTextDataFromAttributes,
mergeAttributes,
RichTextProps
} from "@saleor/attributes/utils/data";
import {
createAttributeChangeHandler,
createAttributeFileChangeHandler,
@ -32,6 +38,7 @@ import { getVariantAttributeInputFromProduct } from "@saleor/products/utils/data
import { createPreorderEndDateChangeHandler } from "@saleor/products/utils/handlers";
import { FetchMoreProps, RelayToFlat, ReorderEvent } from "@saleor/types";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
import React, { useEffect } from "react";
import { useIntl } from "react-intl";
@ -80,18 +87,19 @@ export interface ProductVariantCreateHandlers
fetchMoreReferences: FetchMoreProps;
}
export interface UseProductVariantCreateFormResult
export interface UseProductVariantCreateFormOutput
extends CommonUseFormResultWithHandlers<
ProductVariantCreateData,
ProductVariantCreateHandlers
> {
>,
Omit<RichTextProps, "richText"> {
formErrors: FormErrors<ProductVariantCreateData>;
disabled: boolean;
}
export interface ProductVariantCreateFormProps
extends UseProductVariantCreateFormOpts {
children: (props: UseProductVariantCreateFormResult) => React.ReactNode;
children: (props: UseProductVariantCreateFormOutput) => React.ReactNode;
product: ProductVariantCreateDataQuery["product"];
onSubmit: (data: ProductVariantCreateData) => void;
disabled: boolean;
@ -116,7 +124,7 @@ function useProductVariantCreateForm(
onSubmit: (data: ProductVariantCreateData) => void,
disabled: boolean,
opts: UseProductVariantCreateFormOpts
): UseProductVariantCreateFormResult {
): UseProductVariantCreateFormOutput {
const intl = useIntl();
const attributeInput = getVariantAttributeInputFromProduct(product);
@ -131,6 +139,13 @@ function useProductVariantCreateForm(
} = form;
const attributes = useFormset(attributeInput);
const {
getters: attributeRichTextGetters,
getValues: getAttributeRichTextValues
} = useMultipleRichText({
initial: getRichTextDataFromAttributes(attributes.data),
triggerChange
});
const attributesWithNewFileValue = useFormset<null, File>([]);
const stocks = useFormset<ProductStockFormsetData, string>([]);
@ -218,12 +233,23 @@ function useProductVariantCreateForm(
stocks: stocks.data
};
const getSubmitData = async (): Promise<ProductVariantCreateData> => ({
...data,
attributes: mergeAttributes(
attributes.data,
getRichTextAttributesFromMap(
attributes.data,
await getAttributeRichTextValues()
)
)
});
const handleFormSubmit = useHandleFormSubmit({
formId,
onSubmit
});
const submit = () => handleFormSubmit(data);
const submit = async () => handleFormSubmit(await getSubmitData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
@ -255,7 +281,8 @@ function useProductVariantCreateForm(
selectAttributeReference: handleAttributeReferenceChange
},
submit,
isSaveDisabled
isSaveDisabled,
attributeRichTextGetters
};
}

View file

@ -224,7 +224,15 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
assignReferencesAttributeId={assignReferencesAttributeId}
loading={loading}
>
{({ change, data, formErrors, isSaveDisabled, handlers, submit }) => {
{({
change,
data,
formErrors,
isSaveDisabled,
handlers,
submit,
attributeRichTextGetters
}) => {
const nonSelectionAttributes = data.attributes.filter(
byAttributeScope(VariantAttributeScope.NOT_VARIANT_SELECTION)
);
@ -250,7 +258,6 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
{nonSelectionAttributes.length > 0 && (
<>
<Attributes
entityId={variant?.id}
title={intl.formatMessage(
messages.nonSelectionAttributes
)}
@ -268,6 +275,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues}
onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={attributeRichTextGetters}
/>
<CardSpacer />
</>
@ -275,7 +283,6 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
{selectionAttributes.length > 0 && (
<>
<Attributes
entityId={variant?.id}
title={intl.formatMessage(
messages.selectionAttributesHeader
)}
@ -293,6 +300,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues}
onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={attributeRichTextGetters}
/>
<CardSpacer />
</>

View file

@ -1,4 +1,10 @@
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
import {
getAttributesDisplayData,
getRichTextAttributesFromMap,
getRichTextDataFromAttributes,
mergeAttributes,
RichTextProps
} from "@saleor/attributes/utils/data";
import {
createAttributeChangeHandler,
createAttributeFileChangeHandler,
@ -50,6 +56,7 @@ import { arrayDiff } from "@saleor/utils/arrays";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
import React, { useEffect } from "react";
import { useIntl } from "react-intl";
@ -121,7 +128,8 @@ export interface UseProductVariantUpdateFormResult
extends CommonUseFormResultWithHandlers<
ProductVariantUpdateData,
ProductVariantUpdateHandlers
> {
>,
Omit<RichTextProps, "richText"> {
formErrors: FormErrors<ProductVariantUpdateData>;
disabled: boolean;
}
@ -189,6 +197,13 @@ function useProductVariantUpdateForm(
});
const attributes = useFormset(attributeInput);
const {
getters: attributeRichTextGetters,
getValues: getAttributeRichTextValues
} = useMultipleRichText({
initial: getRichTextDataFromAttributes(attributes.data),
triggerChange
});
const attributesWithNewFileValue = useFormset<null, File>([]);
const stocks = useFormset(stockInput);
const channels = useFormset(channelsInput);
@ -302,16 +317,22 @@ function useProductVariantUpdateForm(
data.hasPreorderEndDate &&
!!form.errors.preorderEndDateTime);
const submitData: ProductVariantUpdateSubmitData = {
const getSubmitData = async (): Promise<ProductVariantUpdateSubmitData> => ({
...formData,
...getMetadata(formData, isMetadataModified, isPrivateMetadataModified),
addStocks,
attributes: attributes.data,
attributes: mergeAttributes(
attributes.data,
getRichTextAttributesFromMap(
attributes.data,
await getAttributeRichTextValues()
)
),
attributesWithNewFileValue: attributesWithNewFileValue.data,
channelListings: channels.data,
removeStocks: stockDiff.removed,
updateStocks
};
});
const handleSubmit = async (data: ProductVariantUpdateSubmitData) => {
const errors = await onSubmit(data);
@ -328,7 +349,7 @@ function useProductVariantUpdateForm(
onSubmit: handleSubmit
});
const submit = () => handleFormSubmit(submitData);
const submit = async () => handleFormSubmit(await getSubmitData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
@ -356,7 +377,8 @@ function useProductVariantUpdateForm(
selectAttributeReference: handleAttributeReferenceChange
},
submit,
isSaveDisabled
isSaveDisabled,
attributeRichTextGetters
};
}

View file

@ -2,14 +2,13 @@ import { OutputData } from "@editorjs/editorjs";
import { Card, CardContent, TextField } from "@material-ui/core";
import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle";
import RichTextEditor, {
RichTextEditorChange
} from "@saleor/components/RichTextEditor";
import RichTextEditor from "@saleor/components/RichTextEditor";
import { ShippingErrorFragment } from "@saleor/graphql";
import { commonMessages } from "@saleor/intl";
import { makeStyles } from "@saleor/macaw-ui";
import { getFormErrors } from "@saleor/utils/errors";
import getShippingErrorMessage from "@saleor/utils/errors/shipping";
import { useRichTextContext } from "@saleor/utils/richText/context";
import React from "react";
import { defineMessages, useIntl } from "react-intl";
@ -64,15 +63,21 @@ export interface ShippingRateInfoProps {
disabled: boolean;
errors: ShippingErrorFragment[];
onChange: (event: React.ChangeEvent<any>) => void;
onDescriptionChange: RichTextEditorChange;
}
const ShippingRateInfo: React.FC<ShippingRateInfoProps> = props => {
const { data, disabled, errors, onChange, onDescriptionChange } = props;
const { data, disabled, errors, onChange } = props;
const intl = useIntl();
const classes = useStyles(props);
const {
defaultValue,
editorRef,
isReadyForMount,
handleChange
} = useRichTextContext();
const formErrors = getFormErrors(
["name", "description", "minDays", "maxDays"],
errors
@ -95,15 +100,18 @@ const ShippingRateInfo: React.FC<ShippingRateInfoProps> = props => {
onChange={onChange}
/>
<CardSpacer />
{isReadyForMount && (
<RichTextEditor
data={data.description}
defaultValue={defaultValue}
editorRef={editorRef}
onChange={handleChange}
disabled={disabled}
error={!!formErrors.description}
helperText={getShippingErrorMessage(formErrors.description, intl)}
label={intl.formatMessage(messages.description)}
name="description"
onChange={onDescriptionChange}
/>
)}
<CardSpacer />
<div className={classes.deliveryTimeFields}>
<TextField

View file

@ -25,6 +25,7 @@ import OrderWeight from "@saleor/shipping/components/OrderWeight";
import PricingCard from "@saleor/shipping/components/PricingCard";
import ShippingRateInfo from "@saleor/shipping/components/ShippingRateInfo";
import { createChannelsChangeHandler } from "@saleor/shipping/handlers";
import { RichTextContext } from "@saleor/utils/richText/context";
import useRichText from "@saleor/utils/richText/useRichText";
import React, { FormEventHandler } from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -100,23 +101,27 @@ export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePagePr
onSubmit
});
const [description, changeDescription] = useRichText({
const richText = useRichText({
initial: null,
triggerChange
});
// Prevents closing ref in submit functions
const getData = () => ({
const data: ShippingZoneRateCommonFormData = {
...formData,
description: description.current
});
const data = getData();
const handleFormElementSubmit: FormEventHandler = event => {
event.preventDefault();
handleFormSubmit(getData());
description: null
};
const handleSubmit = () => handleFormSubmit(getData());
const getData = async (): Promise<ShippingZoneRateCommonFormData> => ({
...formData,
description: await richText.getValue()
});
const handleFormElementSubmit: FormEventHandler = async event => {
event.preventDefault();
handleFormSubmit(await getData());
};
const handleSubmit = async () => handleFormSubmit(await getData());
const handleChannelsChange = createChannelsChangeHandler(
shippingChannels,
@ -130,6 +135,7 @@ export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePagePr
setIsSubmitDisabled(isSaveDisabled);
return (
<RichTextContext.Provider value={richText}>
<form onSubmit={handleFormElementSubmit}>
<Container>
<Backlink href={backUrl}>
@ -157,7 +163,6 @@ export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePagePr
disabled={disabled}
errors={errors}
onChange={change}
onDescriptionChange={changeDescription}
/>
<CardSpacer />
{isPriceVariant ? (
@ -214,6 +219,7 @@ export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePagePr
/>
</Container>
</form>
</RichTextContext.Provider>
);
};

View file

@ -31,6 +31,7 @@ import { createChannelsChangeHandler } from "@saleor/shipping/handlers";
import { ListActions, ListProps } from "@saleor/types";
import { mapEdgesToItems, mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { RichTextContext } from "@saleor/utils/richText/context";
import useRichText from "@saleor/utils/richText/useRichText";
import React, { FormEventHandler } from "react";
import { FormattedMessage } from "react-intl";
@ -127,7 +128,7 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
onSubmit
});
const [description, changeDescription] = useRichText({
const richText = useRichText({
initial: rate?.description,
triggerChange
});
@ -136,18 +137,22 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
// Prevents closing ref in submit functions
const getData = () => ({
const data: ShippingZoneRateUpdateFormData = {
...formData,
description: description.current
});
const data = getData();
const handleFormElementSubmit: FormEventHandler = event => {
event.preventDefault();
handleFormSubmit(getData());
description: null
};
const handleSubmit = () => handleFormSubmit(getData());
// Prevents closing ref in submit functions
const getData = async (): Promise<ShippingZoneRateUpdateFormData> => ({
...data,
description: await richText.getValue()
});
const handleFormElementSubmit: FormEventHandler = async event => {
event.preventDefault();
handleFormSubmit(await getData());
};
const handleSubmit = async () => handleFormSubmit(await getData());
const handleChannelsChange = createChannelsChangeHandler(
shippingChannels,
@ -164,6 +169,7 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
setIsSubmitDisabled(isSaveDisabled);
return (
<RichTextContext.Provider value={richText}>
<form onSubmit={handleFormElementSubmit}>
<Container>
<Backlink href={backHref}>
@ -177,7 +183,6 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
disabled={disabled}
errors={errors}
onChange={change}
onDescriptionChange={changeDescription}
/>
<CardSpacer />
{isPriceVariant ? (
@ -247,6 +252,7 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
/>
</Container>
</form>
</RichTextContext.Provider>
);
};

View file

@ -376,9 +376,6 @@ exports[`Storyshots Attributes / Attributes default 1`] = `
>
Value
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -1104,9 +1101,6 @@ exports[`Storyshots Attributes / Attributes disabled 1`] = `
>
Value
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
@ -2036,9 +2030,6 @@ exports[`Storyshots Attributes / Attributes selected 1`] = `
>
Value
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -13612,9 +13603,6 @@ exports[`Storyshots Generics / Rich text editor default 1`] = `
>
Content
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
>
@ -13647,9 +13635,6 @@ exports[`Storyshots Generics / Rich text editor disabled 1`] = `
>
Content
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
>
@ -13682,9 +13667,6 @@ exports[`Storyshots Generics / Rich text editor error 1`] = `
>
Content
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootError-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-error-id"
>
@ -13707,9 +13689,7 @@ exports[`Storyshots Generics / Rich text editor static 1`] = `
<div
class="MuiCardContent-root-id"
>
<div
class="RichTextEditor-editor-id RichTextEditor-rootStatic-id"
/>
<div />
</div>
</div>
</div>
@ -21075,9 +21055,6 @@ exports[`Storyshots Shipping / ShippingZoneRatesCreatePage page create price 1`]
>
Shipping Rate Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -21988,9 +21965,6 @@ exports[`Storyshots Shipping / ShippingZoneRatesCreatePage page create weight 1`
>
Shipping Rate Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -22911,9 +22885,6 @@ exports[`Storyshots Shipping / ShippingZoneRatesCreatePage page loading 1`] = `
>
Shipping Rate Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
@ -39305,9 +39276,6 @@ exports[`Storyshots Views / Categories / Create category When loading 1`] = `
>
Category Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
@ -39620,9 +39588,6 @@ exports[`Storyshots Views / Categories / Create category default 1`] = `
>
Category Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -39940,9 +39905,6 @@ exports[`Storyshots Views / Categories / Create category form errors 1`] = `
>
Category Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootError-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-error-id"
>
@ -40256,9 +40218,6 @@ exports[`Storyshots Views / Categories / Update category default 1`] = `
>
Category Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -41031,9 +40990,6 @@ exports[`Storyshots Views / Categories / Update category form errors 1`] = `
>
Category Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootError-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-error-id"
>
@ -41800,23 +41756,6 @@ exports[`Storyshots Views / Categories / Update category loading 1`] = `
<div
class="FormSpacer-spacer-id"
/>
<div
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
data-test-id="rich-text-editor-description"
>
<label
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
data-shrink="true"
>
Category Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
</div>
</div>
</div>
<div
@ -42439,9 +42378,6 @@ exports[`Storyshots Views / Categories / Update category no background 1`] = `
>
Category Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -43151,9 +43087,6 @@ exports[`Storyshots Views / Categories / Update category no products 1`] = `
>
Category Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -43923,9 +43856,6 @@ exports[`Storyshots Views / Categories / Update category no subcategories 1`] =
>
Category Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -44693,9 +44623,6 @@ exports[`Storyshots Views / Categories / Update category products 1`] = `
>
Category Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -51307,9 +51234,6 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details d
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -52797,9 +52721,6 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details f
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootError-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-error-id"
>
@ -54281,23 +54202,6 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details l
<div
class="FormSpacer-spacer-id"
/>
<div
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
data-test-id="rich-text-editor-description"
>
<label
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
data-shrink="true"
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
</div>
</div>
</div>
<div
@ -55188,9 +55092,6 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details n
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -57678,9 +57579,6 @@ exports[`Storyshots Views / Collections / Create collection default 1`] = `
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -59581,9 +59479,6 @@ exports[`Storyshots Views / Collections / Create collection form errors 1`] = `
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootError-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-error-id"
>
@ -61482,9 +61377,6 @@ exports[`Storyshots Views / Collections / Create collection loading 1`] = `
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
@ -154092,9 +153984,6 @@ exports[`Storyshots Views / Pages / Page details default 1`] = `
>
Content
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -154986,9 +154875,6 @@ exports[`Storyshots Views / Pages / Page details form errors 1`] = `
>
Content
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootError-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-error-id"
>
@ -155882,23 +155768,6 @@ exports[`Storyshots Views / Pages / Page details loading 1`] = `
<div
class="FormSpacer-spacer-id"
/>
<div
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
data-test-id="rich-text-editor-content"
>
<label
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
data-shrink="true"
>
Content
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
</div>
</div>
</div>
<div
@ -180985,9 +180854,6 @@ exports[`Storyshots Views / Products / Create product When loading 1`] = `
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
@ -181756,9 +181622,6 @@ exports[`Storyshots Views / Products / Create product default 1`] = `
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -182525,9 +182388,6 @@ exports[`Storyshots Views / Products / Create product form errors 1`] = `
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -188311,7 +188171,7 @@ exports[`Storyshots Views / Products / Product edit form errors 1`] = `
</div>
</div>
<div
class="Grid-root-id Grid-default-id"
class="Grid-root-id Grid-default-id Grid-richText-id"
>
<div>
<div
@ -188384,9 +188244,6 @@ exports[`Storyshots Views / Products / Product edit form errors 1`] = `
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -190214,7 +190071,7 @@ exports[`Storyshots Views / Products / Product edit limits reached 1`] = `
</div>
</div>
<div
class="Grid-root-id Grid-default-id"
class="Grid-root-id Grid-default-id Grid-richText-id"
>
<div>
<div
@ -190282,9 +190139,6 @@ exports[`Storyshots Views / Products / Product edit limits reached 1`] = `
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -192181,7 +192035,7 @@ exports[`Storyshots Views / Products / Product edit no limits 1`] = `
</div>
</div>
<div
class="Grid-root-id Grid-default-id"
class="Grid-root-id Grid-default-id Grid-richText-id"
>
<div>
<div
@ -192249,9 +192103,6 @@ exports[`Storyshots Views / Products / Product edit no limits 1`] = `
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -194069,7 +193920,7 @@ exports[`Storyshots Views / Products / Product edit no product attributes 1`] =
</div>
</div>
<div
class="Grid-root-id Grid-default-id"
class="Grid-root-id Grid-default-id Grid-richText-id"
>
<div>
<div
@ -194137,9 +193988,6 @@ exports[`Storyshots Views / Products / Product edit no product attributes 1`] =
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -195691,7 +195539,7 @@ exports[`Storyshots Views / Products / Product edit no stock and no variants 1`]
</div>
</div>
<div
class="Grid-root-id Grid-default-id"
class="Grid-root-id Grid-default-id Grid-richText-id"
>
<div>
<div
@ -195759,9 +195607,6 @@ exports[`Storyshots Views / Products / Product edit no stock and no variants 1`]
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -197467,7 +197312,7 @@ exports[`Storyshots Views / Products / Product edit no stock, no variants and no
</div>
</div>
<div
class="Grid-root-id Grid-default-id"
class="Grid-root-id Grid-default-id Grid-richText-id"
>
<div>
<div
@ -197535,9 +197380,6 @@ exports[`Storyshots Views / Products / Product edit no stock, no variants and no
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -199243,7 +199085,7 @@ exports[`Storyshots Views / Products / Product edit no variants 1`] = `
</div>
</div>
<div
class="Grid-root-id Grid-default-id"
class="Grid-root-id Grid-default-id Grid-richText-id"
>
<div>
<div
@ -199311,9 +199153,6 @@ exports[`Storyshots Views / Products / Product edit no variants 1`] = `
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -201019,7 +200858,7 @@ exports[`Storyshots Views / Products / Product edit when data is fully loaded 1`
</div>
</div>
<div
class="Grid-root-id Grid-default-id"
class="Grid-root-id Grid-default-id Grid-richText-id"
>
<div>
<div
@ -201087,9 +200926,6 @@ exports[`Storyshots Views / Products / Product edit when data is fully loaded 1`
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -202913,7 +202749,7 @@ exports[`Storyshots Views / Products / Product edit when loading data 1`] = `
</div>
</div>
<div
class="Grid-root-id Grid-default-id"
class="Grid-root-id Grid-default-id Grid-richText-id"
>
<div>
<div
@ -202972,23 +202808,6 @@ exports[`Storyshots Views / Products / Product edit when loading data 1`] = `
<div
class="FormSpacer-spacer-id"
/>
<div
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
data-test-id="rich-text-editor-description"
>
<label
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
data-shrink="true"
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
</div>
<div
class="FormSpacer-spacer-id"
/>
@ -204019,7 +203838,7 @@ exports[`Storyshots Views / Products / Product edit when product has no images 1
</div>
</div>
<div
class="Grid-root-id Grid-default-id"
class="Grid-root-id Grid-default-id Grid-richText-id"
>
<div>
<div
@ -204087,9 +203906,6 @@ exports[`Storyshots Views / Products / Product edit when product has no images 1
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -205736,7 +205552,7 @@ exports[`Storyshots Views / Products / Product edit when product has no variants
</div>
</div>
<div
class="Grid-root-id Grid-default-id"
class="Grid-root-id Grid-default-id Grid-richText-id"
>
<div>
<div
@ -205804,9 +205620,6 @@ exports[`Storyshots Views / Products / Product edit when product has no variants
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -207512,7 +207325,7 @@ exports[`Storyshots Views / Products / Product edit with channels 1`] = `
</div>
</div>
<div
class="Grid-root-id Grid-default-id"
class="Grid-root-id Grid-default-id Grid-richText-id"
>
<div>
<div
@ -207580,9 +207393,6 @@ exports[`Storyshots Views / Products / Product edit with channels 1`] = `
>
Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -231258,9 +231068,6 @@ exports[`Storyshots Views / Shipping / Shipping rate create price rate 1`] = `
>
Shipping Rate Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -232536,9 +232343,6 @@ exports[`Storyshots Views / Shipping / Shipping rate create weight rate 1`] = `
>
Shipping Rate Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -233736,23 +233540,6 @@ exports[`Storyshots Views / Shipping / Shipping rate loading 1`] = `
<div
class="CardSpacer-spacer-id"
/>
<div
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
data-test-id="rich-text-editor-description"
>
<label
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
data-shrink="true"
>
Shipping Rate Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
/>
</div>
<div
class="CardSpacer-spacer-id"
/>
@ -234711,9 +234498,6 @@ exports[`Storyshots Views / Shipping / Shipping rate update price rate 1`] = `
>
Shipping Rate Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>
@ -235909,9 +235693,6 @@ exports[`Storyshots Views / Shipping / Shipping rate update weight rate 1`] = `
>
Shipping Rate Description
</label>
<div
class="RichTextEditor-editor-id RichTextEditor-root-id"
/>
<p
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
/>

View file

@ -34,19 +34,28 @@ const TranslationFieldsRich: React.FC<TranslationFieldsRichProps> = ({
const { setIsDirty, setExitDialogSubmitRef } = useExitFormDialog();
const [content, change] = useRichText({
const {
defaultValue,
editorRef,
isReadyForMount,
handleChange,
getValue
} = useRichText({
initial,
triggerChange: () => setIsDirty(true)
});
useEffect(() => setExitDialogSubmitRef(onSubmit), [content]);
useEffect(() => setExitDialogSubmitRef(onSubmit), [onSubmit]);
const submit = () => onSubmit(content.current);
const submit = async () => onSubmit(await getValue());
return edit ? (
<form onSubmit={submit}>
{isReadyForMount && (
<RichTextEditor
data={content.current}
defaultValue={defaultValue}
editorRef={editorRef}
onChange={handleChange}
disabled={disabled}
error={undefined}
helperText={undefined}
@ -56,8 +65,8 @@ const TranslationFieldsRich: React.FC<TranslationFieldsRichProps> = ({
})}
name="translation"
data-test-id="translation-field"
onChange={change}
/>
)}
<TranslationFieldsSave
saveButtonState={saveButtonState}
onDiscard={onDiscard}
@ -70,7 +79,9 @@ const TranslationFieldsRich: React.FC<TranslationFieldsRichProps> = ({
</Typography>
) : (
<Typography>
<RichTextEditorContent key={resetKey} data={JSON.parse(initial)} />
{isReadyForMount && (
<RichTextEditorContent key={resetKey} value={defaultValue} />
)}
</Typography>
);
};

View file

@ -0,0 +1,56 @@
import { useCallback, useState } from "react";
export type MapOrEntries<K, V> = Map<K, V> | Array<[K, V]>;
// Public interface
export interface Actions<K, V> {
set: (key: K, value: V) => void;
setAll: (entries: MapOrEntries<K, V>) => void;
remove: (key: K) => void;
reset: Map<K, V>["clear"];
}
// We hide some setters from the returned map to disable autocompletion
type Return<K, V> = [
Omit<Map<K, V>, "set" | "clear" | "delete">,
Actions<K, V>
];
function useMap<K, V>(
initialState: MapOrEntries<K, V> = new Map()
): Return<K, V> {
const [map, setMap] = useState(() => new Map(initialState));
const actions: Actions<K, V> = {
set: useCallback((key, value) => {
setMap(prev => {
if (prev.get(key) === value) {
return prev;
}
const copy = new Map(prev);
copy.set(key, value);
return copy;
});
}, []),
setAll: useCallback(entries => {
setMap(() => new Map(entries));
}, []),
remove: useCallback(key => {
setMap(prev => {
const copy = new Map(prev);
copy.delete(key);
return copy;
});
}, []),
reset: useCallback(() => {
setMap(() => new Map());
}, [])
};
return [map, actions];
}
export default useMap;

View file

@ -0,0 +1,14 @@
import { useRichText } from "../useRichText";
const useRichTextMocked = ({
initial,
triggerChange
}): ReturnType<typeof useRichText> => ({
editorRef: { current: null },
defaultValue: initial ? JSON.parse(initial) : undefined,
getValue: async () => ({ blocks: [] }),
handleChange: triggerChange,
isReadyForMount: true
});
export default useRichTextMocked;

View file

@ -0,0 +1,18 @@
import { createContext, useContext } from "react";
import { useRichText } from "./useRichText";
export type RichTextContextValues = ReturnType<typeof useRichText>;
export const RichTextContext = createContext<RichTextContextValues | null>(
null
);
export const useRichTextContext = () => {
const value = useContext(RichTextContext);
if (!value) {
throw new Error("useRichTextContext used outside of RichTextContext");
}
return value;
};

View file

@ -0,0 +1,84 @@
import { OutputData } from "@editorjs/editorjs";
import { EditorCore } from "@saleor/components/RichTextEditor";
import { useCallback, useRef } from "react";
import useMap from "../objects/useMap";
export type RefsMap<TKey extends string> = Record<TKey, EditorCore | null>;
export interface RichTextGetters<TKey extends string> {
getShouldMount: (id: TKey) => boolean;
getDefaultValue: (id: TKey) => OutputData;
getHandleChange: (id: TKey) => () => void;
getMountEditor: (id: TKey) => (editor: EditorCore) => void;
}
export type GetRichTextValues = Record<string, OutputData>;
export interface RichTextMultipleOptions<TKey extends string> {
initial: Record<TKey, string>;
triggerChange: () => void;
}
export const useMultipleRichText = <TKey extends string>({
initial,
triggerChange
}: RichTextMultipleOptions<TKey>) => {
const editorRefs = useRef<RefsMap<TKey>>({} as RefsMap<TKey>);
const [shouldMountMap, { set: setShouldMountById }] = useMap();
const getMountEditor = useCallback(
(id: TKey) => (ref: EditorCore | null) => {
editorRefs.current = {
...editorRefs.current,
[id]: ref
};
},
[]
);
const getHandleChange = (_: TKey) => () => triggerChange();
const getDefaultValue = useCallback(
(id: TKey) => {
try {
const result = JSON.parse(initial[id]);
setShouldMountById(id, true);
return result;
} catch (e) {
return undefined;
}
},
[initial]
);
const getShouldMount = useCallback(
(id: TKey) => shouldMountMap.get(id) ?? false,
[shouldMountMap]
);
const getValues = async () => {
const availableRefs = Object.entries(editorRefs.current).filter(
([, value]) => value !== null
) as Array<[string, EditorCore]>;
const results = await Promise.all(
availableRefs.map(async ([key, ref]) => {
const value = await ref.save();
return [key, value] as [string, OutputData];
})
);
return Object.fromEntries(results) as Record<string, OutputData>;
};
return {
getters: {
getShouldMount,
getDefaultValue,
getHandleChange,
getMountEditor
} as RichTextGetters<TKey>,
getValues
};
};

View file

@ -3,25 +3,8 @@ import { renderHook } from "@testing-library/react-hooks";
import useRichText from "./useRichText";
type Fixtures = Record<"short" | "long", OutputData>;
type Fixtures = Record<"short", OutputData>;
const fixtures: Fixtures = {
long: {
blocks: [
{
data: {
level: 1,
text: "Some header"
},
type: "header"
},
{
data: {
text: "Some text"
},
type: "paragraph"
}
]
},
short: {
blocks: [
{
@ -34,40 +17,65 @@ const fixtures: Fixtures = {
}
};
const triggerChange = jest.fn();
describe("useRichText", () => {
it("properly saves data in form", () => {
const triggerChange = jest.fn();
const hook = renderHook(() =>
useRichText({
initial: null,
triggerChange
})
it("properly informs RichTextEditor when data is ready to mount", () => {
// eslint-disable-next-line prefer-const
let initial: string | undefined;
const { result, rerender } = renderHook(() =>
useRichText({ initial, triggerChange })
);
const [data, change] = hook.result.current;
expect(data.current).toMatchObject({ blocks: [] });
expect(result.current.isReadyForMount).toBe(false);
change(fixtures.short);
initial = JSON.stringify(fixtures.short); // for JSON.parse()
rerender();
expect(data.current).toMatchObject(fixtures.short);
expect(triggerChange).toHaveBeenCalled();
expect(result.current.defaultValue).toStrictEqual(fixtures.short);
expect(result.current.isReadyForMount).toBe(true);
});
it("properly updates data in form", () => {
const triggerChange = jest.fn();
const hook = renderHook(() =>
useRichText({
initial: JSON.stringify(fixtures.short),
triggerChange
})
it("returns undefined when JSON cannot be parsed", () => {
// eslint-disable-next-line prefer-const
let initial: string | undefined;
const { result, rerender } = renderHook(() =>
useRichText({ initial, triggerChange })
);
const [data, change] = hook.result.current;
expect(data.current).toMatchObject(fixtures.short);
expect(result.current.isReadyForMount).toBe(false);
change(fixtures.long);
initial = "this-isnt-valid-json";
rerender();
expect(result.current.defaultValue).toBe(undefined);
expect(result.current.isReadyForMount).toBe(false);
});
it("runs editorJS .save() when getValue is called", async () => {
const saveFn = jest.fn(async () => fixtures.short);
const { result } = renderHook(() =>
useRichText({ initial: "", triggerChange })
);
result.current.editorRef.current = {
save: saveFn,
destroy: jest.fn(),
clear: jest.fn(),
render: jest.fn()
};
expect(await result.current.getValue()).toStrictEqual(fixtures.short);
expect(saveFn).toHaveBeenCalled();
});
it("calls triggerChange when change is made in the editor", () => {
triggerChange.mockClear();
const { result } = renderHook(() =>
useRichText({ initial: "", triggerChange })
);
result.current.handleChange();
expect(data.current).toMatchObject(fixtures.long);
expect(triggerChange).toHaveBeenCalled();
});
});

View file

@ -1,44 +1,45 @@
import { OutputData } from "@editorjs/editorjs";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import isEqual from "lodash/isEqual";
import { MutableRefObject, useEffect, useRef, useState } from "react";
import { EditorCore } from "@saleor/components/RichTextEditor";
import { useMemo, useRef, useState } from "react";
const emptyContent: OutputData = {
blocks: []
};
function useRichText(opts: {
interface UseRichTextOptions {
initial: string | null;
triggerChange: () => void;
}): [MutableRefObject<OutputData>, RichTextEditorChange] {
const data = useRef<OutputData>();
const [, setLoaded] = useState(false);
useEffect(() => {
if (opts.initial === null) {
data.current = emptyContent;
setLoaded(true);
return;
}
try {
data.current = JSON.parse(opts.initial);
setLoaded(true);
} catch {
data.current = undefined;
}
}, [opts.initial]);
export function useRichText({ initial, triggerChange }: UseRichTextOptions) {
const editorRef = useRef<EditorCore>(null);
const [isReadyForMount, setIsReadyForMount] = useState(false);
const change: RichTextEditorChange = newData => {
if (isEqual(data.current.blocks, newData.blocks)) {
return;
}
opts.triggerChange();
data.current = newData;
const handleChange = () => {
triggerChange();
};
return [data, change];
const getValue = async () => {
if (editorRef.current) {
return editorRef.current.save();
} else {
throw new Error("Editor instance is not available");
}
};
const defaultValue = useMemo<OutputData | undefined>(() => {
try {
const result = JSON.parse(initial);
setIsReadyForMount(true);
return result;
} catch (e) {
return undefined;
}
}, [initial]);
return {
editorRef,
handleChange,
getValue,
defaultValue,
isReadyForMount
};
}
export default useRichText;