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] ## [Unreleased]
- Added links instead of imperative navigation with onClick - #1969 by @taniotanio7 - Added links instead of imperative navigation with onClick - #1969 by @taniotanio7
- Fixed clearing attribute values - #2047 by @witoszekdev - Fixed clearing attribute values - #2047 by @witoszekdev
- Fixed EditorJS integration in RichTextEditor input - #2052 by @witoszekdev
## 3.1 ## 3.1
### PREVIEW FEATURES ### PREVIEW FEATURES

111
package-lock.json generated
View file

@ -2022,35 +2022,35 @@
} }
}, },
"@editorjs/editorjs": { "@editorjs/editorjs": {
"version": "2.22.2", "version": "2.24.3",
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.22.2.tgz", "resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.24.3.tgz",
"integrity": "sha512-rPCv7Z5LZebreQaaL4DZuWzoVGEqwB+P7BF1dsefGQNBmLyeLF412topeW2b6e+g4l1oQ7t75kCOACNTEyYYIA==", "integrity": "sha512-VzrWaQ7mggNUAPTDGcqXJNIlBZH3S2IqsIUGA43UM2Q9VFaeS5KuVFVOTrFJvAzF7G+vZTO52ocm+hrDhTwvyw==",
"requires": { "requires": {
"codex-notifier": "^1.1.2", "codex-notifier": "^1.1.2",
"codex-tooltip": "^1.0.2", "codex-tooltip": "^1.0.5",
"nanoid": "^3.1.22" "nanoid": "^3.1.22"
} }
}, },
"@editorjs/embed": { "@editorjs/embed": {
"version": "2.4.6", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@editorjs/embed/-/embed-2.4.6.tgz", "resolved": "https://registry.npmjs.org/@editorjs/embed/-/embed-2.5.1.tgz",
"integrity": "sha512-c/1TzBBMrU1hpPHRWXlVanBhgyoPZvE2A3qHt23rMVxJVvzu13Zozj0ZOO8pL1XGcubf+Yvalxu7AURn7X3jPw==", "integrity": "sha512-QoBvC6upo5ubO0toGlt5b1Upcq15MC4eqejxcjcm291Ww5cO+lKhSFY6KNk3ldL9PnWKtORxFCd6pAawtkkijA==",
"dev": true "dev": true
}, },
"@editorjs/header": { "@editorjs/header": {
"version": "2.6.1", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/@editorjs/header/-/header-2.6.1.tgz", "resolved": "https://registry.npmjs.org/@editorjs/header/-/header-2.6.2.tgz",
"integrity": "sha512-EsnyVFv5uThpU9tbQ/dUPFCQoa/sBFy2n+9tN3wOXJGx7sjea4fdcacJ2UYhO+7pCgZ+aSgmMOyGLYHUFbchvA==" "integrity": "sha512-U1dnT+KGjwFmpWneEEyR2Nqp42hn9iKwQDgRHWQM+y6qx82pg+eAyuIf0QWt2Mluu9uPD2CzNfvJ+pxIuwX8Lw=="
}, },
"@editorjs/image": { "@editorjs/image": {
"version": "2.6.0", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/@editorjs/image/-/image-2.6.0.tgz", "resolved": "https://registry.npmjs.org/@editorjs/image/-/image-2.6.2.tgz",
"integrity": "sha512-lX4Pz9cW3gGFzlmYLRAsBXTiqUG/MRG7NK4QVU+n/VnUWPU1e791eiIpgRLHfpPj6Maaw5a+GRut90D5EdXtqg==" "integrity": "sha512-lai6LFJ8m3qRmSjio66o0CX7/75OupC3FQ5JWrV/biRT6GvUHtRNWKaMowKcC2ndXtfs4w6WwRxcXlB4WhUAdg=="
}, },
"@editorjs/list": { "@editorjs/list": {
"version": "1.6.2", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@editorjs/list/-/list-1.6.2.tgz", "resolved": "https://registry.npmjs.org/@editorjs/list/-/list-1.7.0.tgz",
"integrity": "sha512-OxowV0yuE11G01czYM1dEQlz1F37ehX0ak5vAbZ9ncSXrPh0fDRw/fBxTY654FlmrsQ40UFom3owSG++tLvVGw==" "integrity": "sha512-0k0RKbQqfV32u24UYHHz5mrmSu4wr246qqXBT7xQiS533Bfd4hzki6UGzvy4f275ULzi+egbjI3BXLkpoTh9iQ=="
}, },
"@editorjs/paragraph": { "@editorjs/paragraph": {
"version": "2.8.0", "version": "2.8.0",
@ -5117,6 +5117,22 @@
"url-parse": "^1.4.7" "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": { "@reach/router": {
"version": "1.3.4", "version": "1.3.4",
"resolved": "https://registry.npmjs.org/@reach/router/-/router-1.3.4.tgz", "resolved": "https://registry.npmjs.org/@reach/router/-/router-1.3.4.tgz",
@ -5129,6 +5145,22 @@
"react-lifecycles-compat": "^3.0.4" "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": { "@react-dnd/asap": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz",
"integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" "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": { "@release-it/bumper": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@release-it/bumper/-/bumper-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@release-it/bumper/-/bumper-2.0.0.tgz",
@ -10975,9 +11028,9 @@
"integrity": "sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg==" "integrity": "sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg=="
}, },
"codex-tooltip": { "codex-tooltip": {
"version": "1.0.4", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.4.tgz", "resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.5.tgz",
"integrity": "sha512-Ud+N+y8PMIa9xGyKuo2j3q8QlfTzkMWQ5KeRrbCDerwVn7xq45nqPKQCFBXEMV0YI42/OqSMnsxP8MyVAyVhnA==" "integrity": "sha512-IuA8LeyLU5p1B+HyhOsqR6oxyFQ11k3i9e9aXw40CrHFTRO2Y1npNBVU3W1SvhKAbUU7R/YikUBdcYFP0RcJag=="
}, },
"collapse-white-space": { "collapse-white-space": {
"version": "1.0.6", "version": "1.0.6",
@ -13213,11 +13266,6 @@
"resolved": "https://registry.npmjs.org/editorjs-inline-tool/-/editorjs-inline-tool-0.4.0.tgz", "resolved": "https://registry.npmjs.org/editorjs-inline-tool/-/editorjs-inline-tool-0.4.0.tgz",
"integrity": "sha512-Ppb4e8IFPjWuNcoNM4tg9bDSo7FgMYAlqP4UhuV5W2JoJBubV5pUcpLrFrSyGTt1HJVEpbrib134zf4wxO+7VA==" "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": { "ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -22711,9 +22759,9 @@
"optional": true "optional": true
}, },
"nanoid": { "nanoid": {
"version": "3.1.30", "version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==" "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
}, },
"nanomatch": { "nanomatch": {
"version": "1.2.13", "version": "1.2.13",
@ -25423,6 +25471,15 @@
"prop-types": "^15.7.2" "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": { "react-error-boundary": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-1.2.5.tgz", "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-1.2.5.tgz",

View file

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

View file

@ -28,6 +28,11 @@ import {
mapNodeToChoice, mapNodeToChoice,
mapPagesToChoices mapPagesToChoices
} from "@saleor/utils/maps"; } from "@saleor/utils/maps";
import { RichTextContextValues } from "@saleor/utils/richText/context";
import {
GetRichTextValues,
RichTextGetters
} from "@saleor/utils/richText/useMultipleRichText";
import { AttributePageFormData } from "../components/AttributePage"; import { AttributePageFormData } from "../components/AttributePage";
@ -36,6 +41,11 @@ type AtributesOfFiles = Pick<
"file" | "id" | "values" | "contentType" "file" | "id" | "values" | "contentType"
>; >;
export interface RichTextProps {
richText: RichTextContextValues;
attributeRichTextGetters: RichTextGetters<string>;
}
export const ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES = [ export const ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES = [
AttributeInputTypeEnum.DROPDOWN, AttributeInputTypeEnum.DROPDOWN,
AttributeInputTypeEnum.MULTISELECT, AttributeInputTypeEnum.MULTISELECT,
@ -257,6 +267,41 @@ export const mergeAttributeValues = (
: attributeValues; : 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 = ( export const getFileValuesToUploadFromAttributes = (
attributesWithNewFileValue: FormsetData<null, File> attributesWithNewFileValue: FormsetData<null, File>
) => attributesWithNewFileValue.filter(fileAttribute => !!fileAttribute.value); ) => attributesWithNewFileValue.filter(fileAttribute => !!fileAttribute.value);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,16 +9,16 @@ import * as fixtures from "./fixtures.json";
import { RichTextEditorProps } from "./RichTextEditor"; import { RichTextEditorProps } from "./RichTextEditor";
import RichTextEditorContent from "./RichTextEditorContent"; import RichTextEditorContent from "./RichTextEditorContent";
export const data: OutputData = fixtures.richTextEditor; export const defaultValue: OutputData = fixtures.richTextEditor;
const props: RichTextEditorProps = { const props: RichTextEditorProps = {
data, defaultValue,
disabled: false, disabled: false,
error: false, error: false,
helperText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit", helperText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
label: "Content", label: "Content",
name: "content", name: "content",
onChange: () => undefined editorRef: null
}; };
storiesOf("Generics / Rich text editor", module) storiesOf("Generics / Rich text editor", module)
@ -27,4 +27,6 @@ storiesOf("Generics / Rich text editor", module)
.add("default", () => <RichTextEditor {...props} />) .add("default", () => <RichTextEditor {...props} />)
.add("disabled", () => <RichTextEditor {...props} disabled={true} />) .add("disabled", () => <RichTextEditor {...props} disabled={true} />)
.add("error", () => <RichTextEditor {...props} error={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 { 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 classNames from "classnames";
import React from "react"; 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 useStyles from "./styles";
import { clean } from "./utils";
export type RichTextEditorChange = (data: OutputData) => void; export type EditorJsProps = Omit<ReactEditorJSProps, "factory">;
export interface RichTextEditorProps extends RichTextEditorContentProps {
// 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; disabled: boolean;
error: boolean; error: boolean;
helperText: string; helperText: string;
label: string; label: string;
name: 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> = ({ const RichTextEditor: React.FC<RichTextEditorProps> = ({
data, id: defaultId,
disabled, disabled,
error, error,
helperText,
label, label,
name, name,
onChange, helperText,
onReady editorRef,
onInitialize,
onReady,
...props
}) => { }) => {
const classes = useStyles({}); const classes = useStyles({});
const id = useId(defaultId);
const [isFocused, setIsFocused] = React.useState(false);
const [isFocused, setFocus] = React.useState(false); const handleInitialize = React.useCallback((editor: EditorCore) => {
const editor = React.useRef<EditorJS>(); if (onInitialize) {
const editorContainer = React.useRef<HTMLDivElement>(); onInitialize(editor);
const togglePromiseQueue = React.useRef(PromiseQueue()); // used to await subsequent toggle invocations }
React.useEffect( if (typeof editorRef === "function") {
() => { return editorRef(editor);
if (data !== undefined && !editor.current) { }
editor.current = new EditorJS({ if (editorRef) {
data, return (editorRef.current = editor);
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) { // We need to render FormControl first to get id from @reach/auto-id
onReady(); const hasRendered = useHasRendered();
}
},
tools
});
}
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;
}
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();
}
}
};
toggle();
}, [disabled]);
return ( return (
<FormControl <FormControl
@ -105,16 +80,28 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
<InputLabel focused={true} shrink={true}> <InputLabel focused={true} shrink={true}>
{label} {label}
</InputLabel> </InputLabel>
<div {hasRendered && (
className={classNames(classes.editor, classes.root, { <ReactEditorJS
[classes.rootActive]: isFocused, // match with the id of holder div
[classes.rootDisabled]: disabled, holder={id}
[classes.rootError]: error tools={tools}
})} // LogLeves is undefined at runtime
ref={editorContainer} logLevel={"ERROR" as LogLevels.ERROR}
onFocus={() => setFocus(true)} onInitialize={handleInitialize}
onBlur={() => setFocus(false)} {...props}
/> >
<div
id={id}
className={classNames(classes.editor, classes.root, {
[classes.rootActive]: isFocused,
[classes.rootDisabled]: disabled,
[classes.rootError]: error
})}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
</ReactEditorJS>
)}
<FormHelperText>{helperText}</FormHelperText> <FormHelperText>{helperText}</FormHelperText>
</FormControl> </FormControl>
); );

View file

@ -1,104 +1,52 @@
import EditorJS, { import { LogLevels } from "@editorjs/editorjs";
LogLevels, import { useId } from "@reach/auto-id";
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 classNames from "classnames"; import classNames from "classnames";
import createGenericInlineTool from "editorjs-inline-tool";
import React from "react"; 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 useStyles from "./styles";
import { clean } from "./utils";
export interface RichTextEditorContentProps { export interface RichTextEditorContentProps
extends Omit<EditorJsProps, "defaultValue"> {
id?: string;
className?: string; className?: string;
data: OutputData;
onReady?: () => void;
} }
const inlineToolbar = ["link", "bold", "italic", "strikethrough"]; const ReactEditorJS = createReactEditorJS();
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 RichTextEditorContent: React.FC<RichTextEditorContentProps> = ({ const RichTextEditorContent: React.FC<RichTextEditorContentProps> = ({
id: defaultId,
className, className,
data, value,
onReady ...props
}) => { }) => {
const classes = useStyles({}); const classes = useStyles({});
const id = useId(defaultId);
const editor = React.useRef<EditorJS>(); // We need to render FormControl first to get id from @reach/auto-id
const editorContainer = React.useRef<HTMLDivElement>(); const hasRendered = useHasRendered();
React.useEffect(
() => {
if (data !== undefined && !editor.current) {
const editorjs = new EditorJS({
data,
holder: editorContainer.current,
logLevel: "ERROR" as LogLevels,
onReady: () => {
editor.current = editorjs;
if (onReady) { if (!hasRendered) {
onReady(); 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 ( return (
<div <ReactEditorJS
className={classNames(classes.editor, classes.rootStatic, className)} holder={id}
ref={editorContainer} logLevel={"ERROR" as LogLevels.ERROR}
/> tools={tools}
{...props}
defaultValue={value}
readOnly={true}
>
<div
id={id}
className={classNames(classes.editor, classes.rootStatic, className)}
/>
</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} onSubmit={onSubmit}
disabled={loading} disabled={loading}
> >
{({ change, data, handlers, submit, isSaveDisabled }) => ( {({
change,
data,
handlers,
submit,
isSaveDisabled,
attributeRichTextGetters
}) => (
<Container> <Container>
<Backlink href={pageListUrl()}> <Backlink href={pageListUrl()}>
{intl.formatMessage(sectionNames.pages)} {intl.formatMessage(sectionNames.pages)}
@ -162,7 +169,6 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
disabled={loading} disabled={loading}
errors={errors} errors={errors}
onChange={change} onChange={change}
onContentChange={handlers.changeContent}
/> />
<CardSpacer /> <CardSpacer />
<SeoForm <SeoForm
@ -199,6 +205,7 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
fetchAttributeValues={fetchAttributeValues} fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues} fetchMoreAttributeValues={fetchMoreAttributeValues}
onAttributeSelectBlur={onAttributeSelectBlur} onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={attributeRichTextGetters}
/> />
)} )}
<CardSpacer /> <CardSpacer />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,12 +17,15 @@ const channels = createChannelsData(channelsList);
import * as _useNavigator from "@saleor/hooks/useNavigator"; import * as _useNavigator from "@saleor/hooks/useNavigator";
import Adapter from "enzyme-adapter-react-16"; import Adapter from "enzyme-adapter-react-16";
import { act } from "react-dom/test-utils";
import { MemoryRouter } from "react-router-dom"; import { MemoryRouter } from "react-router-dom";
configure({ adapter: new Adapter() }); configure({ adapter: new Adapter() });
const onSubmit = jest.fn(); const onSubmit = jest.fn();
const useNavigator = jest.spyOn(_useNavigator, "default"); const useNavigator = jest.spyOn(_useNavigator, "default");
jest.mock("@saleor/components/RichTextEditor/RichTextEditor");
jest.mock("@saleor/utils/richText/useRichText");
(global as any).document.createRange = () => ({ (global as any).document.createRange = () => ({
// eslint-disable-next-line // eslint-disable-next-line
@ -94,7 +97,7 @@ const selectors = {
describe("Product details page", () => { describe("Product details page", () => {
useNavigator.mockImplementation(); useNavigator.mockImplementation();
it("can select empty option on attribute", () => { it("can select empty option on attribute", async () => {
const component = mount( const component = mount(
<MemoryRouter> <MemoryRouter>
<Wrapper> <Wrapper>
@ -124,10 +127,15 @@ describe("Product details page", () => {
.first() .first()
.prop("value") .prop("value")
).toEqual(""); ).toEqual("");
component
.find("form") await act(async () => {
.first() component
.simulate("submit"); .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); 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} assignReferencesAttributeId={assignReferencesAttributeId}
disabled={disabled} disabled={disabled}
> >
{({ change, data, formErrors, handlers, submit, isSaveDisabled }) => ( {({
change,
data,
formErrors,
handlers,
submit,
isSaveDisabled,
attributeRichTextGetters
}) => (
<> <>
<Container> <Container>
<Backlink href={productListUrl()}> <Backlink href={productListUrl()}>
@ -293,13 +301,12 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
<CardMenu menuItems={extensionMenuItems} data-test-id="menu" /> <CardMenu menuItems={extensionMenuItems} data-test-id="menu" />
)} )}
</PageHeader> </PageHeader>
<Grid> <Grid richText>
<div> <div>
<ProductDetailsForm <ProductDetailsForm
data={data} data={data}
disabled={disabled} disabled={disabled}
errors={errors} errors={errors}
onDescriptionChange={handlers.changeDescription}
onChange={change} onChange={change}
/> />
<CardSpacer /> <CardSpacer />
@ -331,6 +338,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
fetchAttributeValues={fetchAttributeValues} fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues} fetchMoreAttributeValues={fetchMoreAttributeValues}
onAttributeSelectBlur={onAttributeSelectBlur} onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={attributeRichTextGetters}
/> />
)} )}
<CardSpacer /> <CardSpacer />

View file

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

View file

@ -159,7 +159,15 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
assignReferencesAttributeId={assignReferencesAttributeId} assignReferencesAttributeId={assignReferencesAttributeId}
disabled={disabled} disabled={disabled}
> >
{({ change, data, formErrors, handlers, submit, isSaveDisabled }) => ( {({
change,
data,
formErrors,
handlers,
submit,
isSaveDisabled,
attributeRichTextGetters
}) => (
<Container> <Container>
<Backlink href={productUrl(productId)}>{product?.name}</Backlink> <Backlink href={productUrl(productId)}>{product?.name}</Backlink>
<PageHeader title={header} /> <PageHeader title={header} />
@ -193,6 +201,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
fetchAttributeValues={fetchAttributeValues} fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues} fetchMoreAttributeValues={fetchMoreAttributeValues}
onAttributeSelectBlur={onAttributeSelectBlur} onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={attributeRichTextGetters}
/> />
<CardSpacer /> <CardSpacer />
<Attributes <Attributes
@ -215,6 +224,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
fetchAttributeValues={fetchAttributeValues} fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues} fetchMoreAttributeValues={fetchMoreAttributeValues}
onAttributeSelectBlur={onAttributeSelectBlur} onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={attributeRichTextGetters}
/> />
<CardSpacer /> <CardSpacer />
<ProductVariantCheckoutSettings <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 { import {
createAttributeChangeHandler, createAttributeChangeHandler,
createAttributeFileChangeHandler, createAttributeFileChangeHandler,
@ -32,6 +38,7 @@ import { getVariantAttributeInputFromProduct } from "@saleor/products/utils/data
import { createPreorderEndDateChangeHandler } from "@saleor/products/utils/handlers"; import { createPreorderEndDateChangeHandler } from "@saleor/products/utils/handlers";
import { FetchMoreProps, RelayToFlat, ReorderEvent } from "@saleor/types"; import { FetchMoreProps, RelayToFlat, ReorderEvent } from "@saleor/types";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -80,18 +87,19 @@ export interface ProductVariantCreateHandlers
fetchMoreReferences: FetchMoreProps; fetchMoreReferences: FetchMoreProps;
} }
export interface UseProductVariantCreateFormResult export interface UseProductVariantCreateFormOutput
extends CommonUseFormResultWithHandlers< extends CommonUseFormResultWithHandlers<
ProductVariantCreateData, ProductVariantCreateData,
ProductVariantCreateHandlers ProductVariantCreateHandlers
> { >,
Omit<RichTextProps, "richText"> {
formErrors: FormErrors<ProductVariantCreateData>; formErrors: FormErrors<ProductVariantCreateData>;
disabled: boolean; disabled: boolean;
} }
export interface ProductVariantCreateFormProps export interface ProductVariantCreateFormProps
extends UseProductVariantCreateFormOpts { extends UseProductVariantCreateFormOpts {
children: (props: UseProductVariantCreateFormResult) => React.ReactNode; children: (props: UseProductVariantCreateFormOutput) => React.ReactNode;
product: ProductVariantCreateDataQuery["product"]; product: ProductVariantCreateDataQuery["product"];
onSubmit: (data: ProductVariantCreateData) => void; onSubmit: (data: ProductVariantCreateData) => void;
disabled: boolean; disabled: boolean;
@ -116,7 +124,7 @@ function useProductVariantCreateForm(
onSubmit: (data: ProductVariantCreateData) => void, onSubmit: (data: ProductVariantCreateData) => void,
disabled: boolean, disabled: boolean,
opts: UseProductVariantCreateFormOpts opts: UseProductVariantCreateFormOpts
): UseProductVariantCreateFormResult { ): UseProductVariantCreateFormOutput {
const intl = useIntl(); const intl = useIntl();
const attributeInput = getVariantAttributeInputFromProduct(product); const attributeInput = getVariantAttributeInputFromProduct(product);
@ -131,6 +139,13 @@ function useProductVariantCreateForm(
} = form; } = form;
const attributes = useFormset(attributeInput); const attributes = useFormset(attributeInput);
const {
getters: attributeRichTextGetters,
getValues: getAttributeRichTextValues
} = useMultipleRichText({
initial: getRichTextDataFromAttributes(attributes.data),
triggerChange
});
const attributesWithNewFileValue = useFormset<null, File>([]); const attributesWithNewFileValue = useFormset<null, File>([]);
const stocks = useFormset<ProductStockFormsetData, string>([]); const stocks = useFormset<ProductStockFormsetData, string>([]);
@ -218,12 +233,23 @@ function useProductVariantCreateForm(
stocks: stocks.data stocks: stocks.data
}; };
const getSubmitData = async (): Promise<ProductVariantCreateData> => ({
...data,
attributes: mergeAttributes(
attributes.data,
getRichTextAttributesFromMap(
attributes.data,
await getAttributeRichTextValues()
)
)
});
const handleFormSubmit = useHandleFormSubmit({ const handleFormSubmit = useHandleFormSubmit({
formId, formId,
onSubmit onSubmit
}); });
const submit = () => handleFormSubmit(data); const submit = async () => handleFormSubmit(await getSubmitData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]); useEffect(() => setExitDialogSubmitRef(submit), [submit]);
@ -255,7 +281,8 @@ function useProductVariantCreateForm(
selectAttributeReference: handleAttributeReferenceChange selectAttributeReference: handleAttributeReferenceChange
}, },
submit, submit,
isSaveDisabled isSaveDisabled,
attributeRichTextGetters
}; };
} }

View file

@ -224,7 +224,15 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
assignReferencesAttributeId={assignReferencesAttributeId} assignReferencesAttributeId={assignReferencesAttributeId}
loading={loading} loading={loading}
> >
{({ change, data, formErrors, isSaveDisabled, handlers, submit }) => { {({
change,
data,
formErrors,
isSaveDisabled,
handlers,
submit,
attributeRichTextGetters
}) => {
const nonSelectionAttributes = data.attributes.filter( const nonSelectionAttributes = data.attributes.filter(
byAttributeScope(VariantAttributeScope.NOT_VARIANT_SELECTION) byAttributeScope(VariantAttributeScope.NOT_VARIANT_SELECTION)
); );
@ -250,7 +258,6 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
{nonSelectionAttributes.length > 0 && ( {nonSelectionAttributes.length > 0 && (
<> <>
<Attributes <Attributes
entityId={variant?.id}
title={intl.formatMessage( title={intl.formatMessage(
messages.nonSelectionAttributes messages.nonSelectionAttributes
)} )}
@ -268,6 +275,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
fetchAttributeValues={fetchAttributeValues} fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues} fetchMoreAttributeValues={fetchMoreAttributeValues}
onAttributeSelectBlur={onAttributeSelectBlur} onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={attributeRichTextGetters}
/> />
<CardSpacer /> <CardSpacer />
</> </>
@ -275,7 +283,6 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
{selectionAttributes.length > 0 && ( {selectionAttributes.length > 0 && (
<> <>
<Attributes <Attributes
entityId={variant?.id}
title={intl.formatMessage( title={intl.formatMessage(
messages.selectionAttributesHeader messages.selectionAttributesHeader
)} )}
@ -293,6 +300,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
fetchAttributeValues={fetchAttributeValues} fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues} fetchMoreAttributeValues={fetchMoreAttributeValues}
onAttributeSelectBlur={onAttributeSelectBlur} onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={attributeRichTextGetters}
/> />
<CardSpacer /> <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 { import {
createAttributeChangeHandler, createAttributeChangeHandler,
createAttributeFileChangeHandler, createAttributeFileChangeHandler,
@ -50,6 +56,7 @@ import { arrayDiff } from "@saleor/utils/arrays";
import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata"; import getMetadata from "@saleor/utils/metadata/getMetadata";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -119,9 +126,10 @@ export interface ProductVariantUpdateHandlers
export interface UseProductVariantUpdateFormResult export interface UseProductVariantUpdateFormResult
extends CommonUseFormResultWithHandlers< extends CommonUseFormResultWithHandlers<
ProductVariantUpdateData, ProductVariantUpdateData,
ProductVariantUpdateHandlers ProductVariantUpdateHandlers
> { >,
Omit<RichTextProps, "richText"> {
formErrors: FormErrors<ProductVariantUpdateData>; formErrors: FormErrors<ProductVariantUpdateData>;
disabled: boolean; disabled: boolean;
} }
@ -189,6 +197,13 @@ function useProductVariantUpdateForm(
}); });
const attributes = useFormset(attributeInput); const attributes = useFormset(attributeInput);
const {
getters: attributeRichTextGetters,
getValues: getAttributeRichTextValues
} = useMultipleRichText({
initial: getRichTextDataFromAttributes(attributes.data),
triggerChange
});
const attributesWithNewFileValue = useFormset<null, File>([]); const attributesWithNewFileValue = useFormset<null, File>([]);
const stocks = useFormset(stockInput); const stocks = useFormset(stockInput);
const channels = useFormset(channelsInput); const channels = useFormset(channelsInput);
@ -302,16 +317,22 @@ function useProductVariantUpdateForm(
data.hasPreorderEndDate && data.hasPreorderEndDate &&
!!form.errors.preorderEndDateTime); !!form.errors.preorderEndDateTime);
const submitData: ProductVariantUpdateSubmitData = { const getSubmitData = async (): Promise<ProductVariantUpdateSubmitData> => ({
...formData, ...formData,
...getMetadata(formData, isMetadataModified, isPrivateMetadataModified), ...getMetadata(formData, isMetadataModified, isPrivateMetadataModified),
addStocks, addStocks,
attributes: attributes.data, attributes: mergeAttributes(
attributes.data,
getRichTextAttributesFromMap(
attributes.data,
await getAttributeRichTextValues()
)
),
attributesWithNewFileValue: attributesWithNewFileValue.data, attributesWithNewFileValue: attributesWithNewFileValue.data,
channelListings: channels.data, channelListings: channels.data,
removeStocks: stockDiff.removed, removeStocks: stockDiff.removed,
updateStocks updateStocks
}; });
const handleSubmit = async (data: ProductVariantUpdateSubmitData) => { const handleSubmit = async (data: ProductVariantUpdateSubmitData) => {
const errors = await onSubmit(data); const errors = await onSubmit(data);
@ -328,7 +349,7 @@ function useProductVariantUpdateForm(
onSubmit: handleSubmit onSubmit: handleSubmit
}); });
const submit = () => handleFormSubmit(submitData); const submit = async () => handleFormSubmit(await getSubmitData());
useEffect(() => setExitDialogSubmitRef(submit), [submit]); useEffect(() => setExitDialogSubmitRef(submit), [submit]);
@ -356,7 +377,8 @@ function useProductVariantUpdateForm(
selectAttributeReference: handleAttributeReferenceChange selectAttributeReference: handleAttributeReferenceChange
}, },
submit, submit,
isSaveDisabled isSaveDisabled,
attributeRichTextGetters
}; };
} }

View file

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

View file

@ -25,6 +25,7 @@ import OrderWeight from "@saleor/shipping/components/OrderWeight";
import PricingCard from "@saleor/shipping/components/PricingCard"; import PricingCard from "@saleor/shipping/components/PricingCard";
import ShippingRateInfo from "@saleor/shipping/components/ShippingRateInfo"; import ShippingRateInfo from "@saleor/shipping/components/ShippingRateInfo";
import { createChannelsChangeHandler } from "@saleor/shipping/handlers"; import { createChannelsChangeHandler } from "@saleor/shipping/handlers";
import { RichTextContext } from "@saleor/utils/richText/context";
import useRichText from "@saleor/utils/richText/useRichText"; import useRichText from "@saleor/utils/richText/useRichText";
import React, { FormEventHandler } from "react"; import React, { FormEventHandler } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -100,23 +101,27 @@ export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePagePr
onSubmit onSubmit
}); });
const [description, changeDescription] = useRichText({ const richText = useRichText({
initial: null, initial: null,
triggerChange triggerChange
}); });
// Prevents closing ref in submit functions const data: ShippingZoneRateCommonFormData = {
const getData = () => ({
...formData, ...formData,
description: description.current description: null
});
const data = getData();
const handleFormElementSubmit: FormEventHandler = event => {
event.preventDefault();
handleFormSubmit(getData());
}; };
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( const handleChannelsChange = createChannelsChangeHandler(
shippingChannels, shippingChannels,
@ -130,90 +135,91 @@ export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePagePr
setIsSubmitDisabled(isSaveDisabled); setIsSubmitDisabled(isSaveDisabled);
return ( return (
<form onSubmit={handleFormElementSubmit}> <RichTextContext.Provider value={richText}>
<Container> <form onSubmit={handleFormElementSubmit}>
<Backlink href={backUrl}> <Container>
<FormattedMessage id="PRlD0A" defaultMessage="Shipping" /> <Backlink href={backUrl}>
</Backlink> <FormattedMessage id="PRlD0A" defaultMessage="Shipping" />
<PageHeader </Backlink>
title={ <PageHeader
isPriceVariant title={
? intl.formatMessage({ isPriceVariant
id: "RXPGi/", ? intl.formatMessage({
defaultMessage: "Price Rate Create", id: "RXPGi/",
description: "page title" defaultMessage: "Price Rate Create",
}) description: "page title"
: intl.formatMessage({ })
id: "NDm2Fe", : intl.formatMessage({
defaultMessage: "Weight Rate Create", id: "NDm2Fe",
description: "page title" defaultMessage: "Weight Rate Create",
}) description: "page title"
} })
/> }
<Grid> />
<div> <Grid>
<ShippingRateInfo <div>
data={data} <ShippingRateInfo
disabled={disabled} data={data}
errors={errors}
onChange={change}
onDescriptionChange={changeDescription}
/>
<CardSpacer />
{isPriceVariant ? (
<OrderValue
channels={data.channelListings}
errors={channelErrors}
orderValueRestricted={data.orderValueRestricted}
disabled={disabled} disabled={disabled}
onChange={change}
onChannelsChange={handleChannelsChange}
/>
) : (
<OrderWeight
orderValueRestricted={data.orderValueRestricted}
disabled={disabled}
minValue={data.minValue}
maxValue={data.maxValue}
onChange={change}
errors={errors} errors={errors}
onChange={change}
/> />
)} <CardSpacer />
<CardSpacer /> {isPriceVariant ? (
<PricingCard <OrderValue
channels={data.channelListings} channels={data.channelListings}
onChange={handleChannelsChange} errors={channelErrors}
disabled={disabled} orderValueRestricted={data.orderValueRestricted}
errors={channelErrors} disabled={disabled}
/> onChange={change}
<CardSpacer /> onChannelsChange={handleChannelsChange}
<ShippingZonePostalCodes />
disabled={disabled} ) : (
onPostalCodeDelete={onPostalCodeUnassign} <OrderWeight
onPostalCodeInclusionChange={onPostalCodeInclusionChange} orderValueRestricted={data.orderValueRestricted}
onPostalCodeRangeAdd={onPostalCodeAssign} disabled={disabled}
postalCodes={postalCodes} minValue={data.minValue}
/> maxValue={data.maxValue}
</div> onChange={change}
<div> errors={errors}
<ChannelsAvailabilityCard />
managePermissions={[PermissionEnum.MANAGE_SHIPPING]} )}
allChannelsCount={allChannelsCount} <CardSpacer />
selectedChannelsCount={shippingChannels?.length} <PricingCard
channelsList={data.channelListings} channels={data.channelListings}
openModal={openChannelsModal} onChange={handleChannelsChange}
/> disabled={disabled}
</div> errors={channelErrors}
</Grid> />
<Savebar <CardSpacer />
disabled={isSaveDisabled} <ShippingZonePostalCodes
onCancel={() => navigate(backUrl)} disabled={disabled}
onDelete={onDelete} onPostalCodeDelete={onPostalCodeUnassign}
onSubmit={handleSubmit} onPostalCodeInclusionChange={onPostalCodeInclusionChange}
state={saveButtonBarState} onPostalCodeRangeAdd={onPostalCodeAssign}
/> postalCodes={postalCodes}
</Container> />
</form> </div>
<div>
<ChannelsAvailabilityCard
managePermissions={[PermissionEnum.MANAGE_SHIPPING]}
allChannelsCount={allChannelsCount}
selectedChannelsCount={shippingChannels?.length}
channelsList={data.channelListings}
openModal={openChannelsModal}
/>
</div>
</Grid>
<Savebar
disabled={isSaveDisabled}
onCancel={() => navigate(backUrl)}
onDelete={onDelete}
onSubmit={handleSubmit}
state={saveButtonBarState}
/>
</Container>
</form>
</RichTextContext.Provider>
); );
}; };

View file

@ -31,6 +31,7 @@ import { createChannelsChangeHandler } from "@saleor/shipping/handlers";
import { ListActions, ListProps } from "@saleor/types"; import { ListActions, ListProps } from "@saleor/types";
import { mapEdgesToItems, mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapEdgesToItems, mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { RichTextContext } from "@saleor/utils/richText/context";
import useRichText from "@saleor/utils/richText/useRichText"; import useRichText from "@saleor/utils/richText/useRichText";
import React, { FormEventHandler } from "react"; import React, { FormEventHandler } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
@ -127,7 +128,7 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
onSubmit onSubmit
}); });
const [description, changeDescription] = useRichText({ const richText = useRichText({
initial: rate?.description, initial: rate?.description,
triggerChange triggerChange
}); });
@ -136,18 +137,22 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
makeChangeHandler: makeMetadataChangeHandler makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger(); } = useMetadataChangeTrigger();
// Prevents closing ref in submit functions const data: ShippingZoneRateUpdateFormData = {
const getData = () => ({
...formData, ...formData,
description: description.current description: null
});
const data = getData();
const handleFormElementSubmit: FormEventHandler = event => {
event.preventDefault();
handleFormSubmit(getData());
}; };
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( const handleChannelsChange = createChannelsChangeHandler(
shippingChannels, shippingChannels,
@ -164,89 +169,90 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
setIsSubmitDisabled(isSaveDisabled); setIsSubmitDisabled(isSaveDisabled);
return ( return (
<form onSubmit={handleFormElementSubmit}> <RichTextContext.Provider value={richText}>
<Container> <form onSubmit={handleFormElementSubmit}>
<Backlink href={backHref}> <Container>
<FormattedMessage id="PRlD0A" defaultMessage="Shipping" /> <Backlink href={backHref}>
</Backlink> <FormattedMessage id="PRlD0A" defaultMessage="Shipping" />
<PageHeader title={rate?.name} /> </Backlink>
<Grid> <PageHeader title={rate?.name} />
<div> <Grid>
<ShippingRateInfo <div>
data={data} <ShippingRateInfo
disabled={disabled} data={data}
errors={errors}
onChange={change}
onDescriptionChange={changeDescription}
/>
<CardSpacer />
{isPriceVariant ? (
<OrderValue
channels={data.channelListings}
errors={channelErrors}
orderValueRestricted={data.orderValueRestricted}
disabled={disabled} disabled={disabled}
onChange={change}
onChannelsChange={handleChannelsChange}
/>
) : (
<OrderWeight
orderValueRestricted={data.orderValueRestricted}
disabled={disabled}
minValue={data.minValue}
maxValue={data.maxValue}
onChange={change}
errors={errors} errors={errors}
onChange={change}
/> />
)} <CardSpacer />
<CardSpacer /> {isPriceVariant ? (
<PricingCard <OrderValue
channels={data.channelListings} channels={data.channelListings}
onChange={handleChannelsChange} errors={channelErrors}
disabled={disabled} orderValueRestricted={data.orderValueRestricted}
errors={channelErrors} disabled={disabled}
/> onChange={change}
<CardSpacer /> onChannelsChange={handleChannelsChange}
<ShippingZonePostalCodes />
disabled={disabled} ) : (
onPostalCodeDelete={onPostalCodeUnassign} <OrderWeight
onPostalCodeInclusionChange={onPostalCodeInclusionChange} orderValueRestricted={data.orderValueRestricted}
onPostalCodeRangeAdd={onPostalCodeAssign} disabled={disabled}
postalCodes={postalCodeRules} minValue={data.minValue}
/> maxValue={data.maxValue}
<CardSpacer /> onChange={change}
<ShippingMethodProducts errors={errors}
products={mapEdgesToItems(rate?.excludedProducts)} />
onProductAssign={onProductAssign} )}
onProductUnassign={onProductUnassign} <CardSpacer />
disabled={disabled} <PricingCard
{...listProps} channels={data.channelListings}
/> onChange={handleChannelsChange}
<CardSpacer /> disabled={disabled}
<Metadata data={data} onChange={changeMetadata} /> errors={channelErrors}
</div> />
<div> <CardSpacer />
<ChannelsAvailabilityCard <ShippingZonePostalCodes
managePermissions={[PermissionEnum.MANAGE_SHIPPING]} disabled={disabled}
allChannelsCount={allChannelsCount} onPostalCodeDelete={onPostalCodeUnassign}
selectedChannelsCount={shippingChannels?.length} onPostalCodeInclusionChange={onPostalCodeInclusionChange}
channelsList={data.channelListings.map(channel => ({ onPostalCodeRangeAdd={onPostalCodeAssign}
id: channel.id, postalCodes={postalCodeRules}
name: channel.name />
}))} <CardSpacer />
openModal={openChannelsModal} <ShippingMethodProducts
/> products={mapEdgesToItems(rate?.excludedProducts)}
</div> onProductAssign={onProductAssign}
</Grid> onProductUnassign={onProductUnassign}
<Savebar disabled={disabled}
disabled={isSaveDisabled} {...listProps}
onCancel={() => navigate(backHref)} />
onDelete={onDelete} <CardSpacer />
onSubmit={handleSubmit} <Metadata data={data} onChange={changeMetadata} />
state={saveButtonBarState} </div>
/> <div>
</Container> <ChannelsAvailabilityCard
</form> managePermissions={[PermissionEnum.MANAGE_SHIPPING]}
allChannelsCount={allChannelsCount}
selectedChannelsCount={shippingChannels?.length}
channelsList={data.channelListings.map(channel => ({
id: channel.id,
name: channel.name
}))}
openModal={openChannelsModal}
/>
</div>
</Grid>
<Savebar
disabled={isSaveDisabled}
onCancel={() => navigate(backHref)}
onDelete={onDelete}
onSubmit={handleSubmit}
state={saveButtonBarState}
/>
</Container>
</form>
</RichTextContext.Provider>
); );
}; };

View file

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

View file

@ -34,30 +34,39 @@ const TranslationFieldsRich: React.FC<TranslationFieldsRichProps> = ({
const { setIsDirty, setExitDialogSubmitRef } = useExitFormDialog(); const { setIsDirty, setExitDialogSubmitRef } = useExitFormDialog();
const [content, change] = useRichText({ const {
defaultValue,
editorRef,
isReadyForMount,
handleChange,
getValue
} = useRichText({
initial, initial,
triggerChange: () => setIsDirty(true) triggerChange: () => setIsDirty(true)
}); });
useEffect(() => setExitDialogSubmitRef(onSubmit), [content]); useEffect(() => setExitDialogSubmitRef(onSubmit), [onSubmit]);
const submit = () => onSubmit(content.current); const submit = async () => onSubmit(await getValue());
return edit ? ( return edit ? (
<form onSubmit={submit}> <form onSubmit={submit}>
<RichTextEditor {isReadyForMount && (
data={content.current} <RichTextEditor
disabled={disabled} defaultValue={defaultValue}
error={undefined} editorRef={editorRef}
helperText={undefined} onChange={handleChange}
label={intl.formatMessage({ disabled={disabled}
id: "/vCXIP", error={undefined}
defaultMessage: "Translation" helperText={undefined}
})} label={intl.formatMessage({
name="translation" id: "/vCXIP",
data-test-id="translation-field" defaultMessage: "Translation"
onChange={change} })}
/> name="translation"
data-test-id="translation-field"
/>
)}
<TranslationFieldsSave <TranslationFieldsSave
saveButtonState={saveButtonState} saveButtonState={saveButtonState}
onDiscard={onDiscard} onDiscard={onDiscard}
@ -70,7 +79,9 @@ const TranslationFieldsRich: React.FC<TranslationFieldsRichProps> = ({
</Typography> </Typography>
) : ( ) : (
<Typography> <Typography>
<RichTextEditorContent key={resetKey} data={JSON.parse(initial)} /> {isReadyForMount && (
<RichTextEditorContent key={resetKey} value={defaultValue} />
)}
</Typography> </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"; import useRichText from "./useRichText";
type Fixtures = Record<"short" | "long", OutputData>; type Fixtures = Record<"short", OutputData>;
const fixtures: Fixtures = { const fixtures: Fixtures = {
long: {
blocks: [
{
data: {
level: 1,
text: "Some header"
},
type: "header"
},
{
data: {
text: "Some text"
},
type: "paragraph"
}
]
},
short: { short: {
blocks: [ blocks: [
{ {
@ -34,40 +17,65 @@ const fixtures: Fixtures = {
} }
}; };
const triggerChange = jest.fn();
describe("useRichText", () => { describe("useRichText", () => {
it("properly saves data in form", () => { it("properly informs RichTextEditor when data is ready to mount", () => {
const triggerChange = jest.fn(); // eslint-disable-next-line prefer-const
const hook = renderHook(() => let initial: string | undefined;
useRichText({ const { result, rerender } = renderHook(() =>
initial: null, useRichText({ initial, triggerChange })
triggerChange
})
); );
const [data, change] = hook.result.current; expect(result.current.isReadyForMount).toBe(false);
expect(data.current).toMatchObject({ blocks: [] });
change(fixtures.short); initial = JSON.stringify(fixtures.short); // for JSON.parse()
rerender();
expect(data.current).toMatchObject(fixtures.short); expect(result.current.defaultValue).toStrictEqual(fixtures.short);
expect(triggerChange).toHaveBeenCalled(); expect(result.current.isReadyForMount).toBe(true);
}); });
it("properly updates data in form", () => { it("returns undefined when JSON cannot be parsed", () => {
const triggerChange = jest.fn(); // eslint-disable-next-line prefer-const
const hook = renderHook(() => let initial: string | undefined;
useRichText({ const { result, rerender } = renderHook(() =>
initial: JSON.stringify(fixtures.short), useRichText({ initial, triggerChange })
triggerChange
})
); );
const [data, change] = hook.result.current; expect(result.current.isReadyForMount).toBe(false);
expect(data.current).toMatchObject(fixtures.short);
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(); expect(triggerChange).toHaveBeenCalled();
}); });
}); });

View file

@ -1,44 +1,45 @@
import { OutputData } from "@editorjs/editorjs"; import { OutputData } from "@editorjs/editorjs";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import { EditorCore } from "@saleor/components/RichTextEditor";
import isEqual from "lodash/isEqual"; import { useMemo, useRef, useState } from "react";
import { MutableRefObject, useEffect, useRef, useState } from "react";
const emptyContent: OutputData = { interface UseRichTextOptions {
blocks: []
};
function useRichText(opts: {
initial: string | null; initial: string | null;
triggerChange: () => void; triggerChange: () => void;
}): [MutableRefObject<OutputData>, RichTextEditorChange] { }
const data = useRef<OutputData>();
const [, setLoaded] = useState(false);
useEffect(() => { export function useRichText({ initial, triggerChange }: UseRichTextOptions) {
if (opts.initial === null) { const editorRef = useRef<EditorCore>(null);
data.current = emptyContent; const [isReadyForMount, setIsReadyForMount] = useState(false);
setLoaded(true);
return;
}
try { const handleChange = () => {
data.current = JSON.parse(opts.initial); triggerChange();
setLoaded(true);
} catch {
data.current = undefined;
}
}, [opts.initial]);
const change: RichTextEditorChange = newData => {
if (isEqual(data.current.blocks, newData.blocks)) {
return;
}
opts.triggerChange();
data.current = newData;
}; };
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; export default useRichText;