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:
parent
a113c9202f
commit
993a99ff07
49 changed files with 1399 additions and 972 deletions
|
@ -5,6 +5,7 @@ All notable, unreleased changes to this project will be documented in this file.
|
|||
## [Unreleased]
|
||||
- Added links instead of imperative navigation with onClick - #1969 by @taniotanio7
|
||||
- Fixed clearing attribute values - #2047 by @witoszekdev
|
||||
- Fixed EditorJS integration in RichTextEditor input - #2052 by @witoszekdev
|
||||
|
||||
## 3.1
|
||||
### PREVIEW FEATURES
|
||||
|
|
111
package-lock.json
generated
111
package-lock.json
generated
|
@ -2022,35 +2022,35 @@
|
|||
}
|
||||
},
|
||||
"@editorjs/editorjs": {
|
||||
"version": "2.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.22.2.tgz",
|
||||
"integrity": "sha512-rPCv7Z5LZebreQaaL4DZuWzoVGEqwB+P7BF1dsefGQNBmLyeLF412topeW2b6e+g4l1oQ7t75kCOACNTEyYYIA==",
|
||||
"version": "2.24.3",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.24.3.tgz",
|
||||
"integrity": "sha512-VzrWaQ7mggNUAPTDGcqXJNIlBZH3S2IqsIUGA43UM2Q9VFaeS5KuVFVOTrFJvAzF7G+vZTO52ocm+hrDhTwvyw==",
|
||||
"requires": {
|
||||
"codex-notifier": "^1.1.2",
|
||||
"codex-tooltip": "^1.0.2",
|
||||
"codex-tooltip": "^1.0.5",
|
||||
"nanoid": "^3.1.22"
|
||||
}
|
||||
},
|
||||
"@editorjs/embed": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/embed/-/embed-2.4.6.tgz",
|
||||
"integrity": "sha512-c/1TzBBMrU1hpPHRWXlVanBhgyoPZvE2A3qHt23rMVxJVvzu13Zozj0ZOO8pL1XGcubf+Yvalxu7AURn7X3jPw==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/embed/-/embed-2.5.1.tgz",
|
||||
"integrity": "sha512-QoBvC6upo5ubO0toGlt5b1Upcq15MC4eqejxcjcm291Ww5cO+lKhSFY6KNk3ldL9PnWKtORxFCd6pAawtkkijA==",
|
||||
"dev": true
|
||||
},
|
||||
"@editorjs/header": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/header/-/header-2.6.1.tgz",
|
||||
"integrity": "sha512-EsnyVFv5uThpU9tbQ/dUPFCQoa/sBFy2n+9tN3wOXJGx7sjea4fdcacJ2UYhO+7pCgZ+aSgmMOyGLYHUFbchvA=="
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/header/-/header-2.6.2.tgz",
|
||||
"integrity": "sha512-U1dnT+KGjwFmpWneEEyR2Nqp42hn9iKwQDgRHWQM+y6qx82pg+eAyuIf0QWt2Mluu9uPD2CzNfvJ+pxIuwX8Lw=="
|
||||
},
|
||||
"@editorjs/image": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/image/-/image-2.6.0.tgz",
|
||||
"integrity": "sha512-lX4Pz9cW3gGFzlmYLRAsBXTiqUG/MRG7NK4QVU+n/VnUWPU1e791eiIpgRLHfpPj6Maaw5a+GRut90D5EdXtqg=="
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/image/-/image-2.6.2.tgz",
|
||||
"integrity": "sha512-lai6LFJ8m3qRmSjio66o0CX7/75OupC3FQ5JWrV/biRT6GvUHtRNWKaMowKcC2ndXtfs4w6WwRxcXlB4WhUAdg=="
|
||||
},
|
||||
"@editorjs/list": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/list/-/list-1.6.2.tgz",
|
||||
"integrity": "sha512-OxowV0yuE11G01czYM1dEQlz1F37ehX0ak5vAbZ9ncSXrPh0fDRw/fBxTY654FlmrsQ40UFom3owSG++tLvVGw=="
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/list/-/list-1.7.0.tgz",
|
||||
"integrity": "sha512-0k0RKbQqfV32u24UYHHz5mrmSu4wr246qqXBT7xQiS533Bfd4hzki6UGzvy4f275ULzi+egbjI3BXLkpoTh9iQ=="
|
||||
},
|
||||
"@editorjs/paragraph": {
|
||||
"version": "2.8.0",
|
||||
|
@ -5117,6 +5117,22 @@
|
|||
"url-parse": "^1.4.7"
|
||||
}
|
||||
},
|
||||
"@reach/auto-id": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.16.0.tgz",
|
||||
"integrity": "sha512-5ssbeP5bCkM39uVsfQCwBBL+KT8YColdnMN5/Eto6Rj7929ql95R3HZUOkKIvj7mgPtEb60BLQxd1P3o6cjbmg==",
|
||||
"requires": {
|
||||
"@reach/utils": "0.16.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@reach/router": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@reach/router/-/router-1.3.4.tgz",
|
||||
|
@ -5129,6 +5145,22 @@
|
|||
"react-lifecycles-compat": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"@reach/utils": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.16.0.tgz",
|
||||
"integrity": "sha512-PCggBet3qaQmwFNcmQ/GqHSefadAFyNCUekq9RrWoaU9hh/S4iaFgf2MBMdM47eQj5i/Bk0Mm07cP/XPFlkN+Q==",
|
||||
"requires": {
|
||||
"tiny-warning": "^1.0.3",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@react-dnd/asap": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz",
|
||||
|
@ -5144,6 +5176,27 @@
|
|||
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz",
|
||||
"integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg=="
|
||||
},
|
||||
"@react-editor-js/client": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@react-editor-js/client/-/client-2.0.6.tgz",
|
||||
"integrity": "sha512-LMMJLAXAwk1kVMy7fxTRFK6OdouvoseqJbmVUygJb2EcfuT84nC9OAtvGEL4vsVLUcnzEV400+F9t5OKa77FGQ==",
|
||||
"requires": {
|
||||
"@react-editor-js/core": "2.0.6"
|
||||
}
|
||||
},
|
||||
"@react-editor-js/core": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@react-editor-js/core/-/core-2.0.6.tgz",
|
||||
"integrity": "sha512-mvHM2I+gT3AnvFpFhTZI0EFLKD9pRpgXDf286uwv6n6tngwLfnCCmtCbgiGI9ICph2GJvRZfaQubE+MHQ6YV8g=="
|
||||
},
|
||||
"@react-editor-js/server": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@react-editor-js/server/-/server-2.0.6.tgz",
|
||||
"integrity": "sha512-soW/bV5auciYr8gEYISWK4fuIblAcc4bcwPuCKnDBj9W9r/nAxMmNgCG+z9rs9Gnroa0Ko3Hzwzs9d5MdOShzg==",
|
||||
"requires": {
|
||||
"@react-editor-js/core": "2.0.6"
|
||||
}
|
||||
},
|
||||
"@release-it/bumper": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@release-it/bumper/-/bumper-2.0.0.tgz",
|
||||
|
@ -10975,9 +11028,9 @@
|
|||
"integrity": "sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg=="
|
||||
},
|
||||
"codex-tooltip": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.4.tgz",
|
||||
"integrity": "sha512-Ud+N+y8PMIa9xGyKuo2j3q8QlfTzkMWQ5KeRrbCDerwVn7xq45nqPKQCFBXEMV0YI42/OqSMnsxP8MyVAyVhnA=="
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.5.tgz",
|
||||
"integrity": "sha512-IuA8LeyLU5p1B+HyhOsqR6oxyFQ11k3i9e9aXw40CrHFTRO2Y1npNBVU3W1SvhKAbUU7R/YikUBdcYFP0RcJag=="
|
||||
},
|
||||
"collapse-white-space": {
|
||||
"version": "1.0.6",
|
||||
|
@ -13213,11 +13266,6 @@
|
|||
"resolved": "https://registry.npmjs.org/editorjs-inline-tool/-/editorjs-inline-tool-0.4.0.tgz",
|
||||
"integrity": "sha512-Ppb4e8IFPjWuNcoNM4tg9bDSo7FgMYAlqP4UhuV5W2JoJBubV5pUcpLrFrSyGTt1HJVEpbrib134zf4wxO+7VA=="
|
||||
},
|
||||
"editorjs-undo": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/editorjs-undo/-/editorjs-undo-0.1.5.tgz",
|
||||
"integrity": "sha512-+qRmTe7Asn9KrsYHMCtNQi6rCBa+qrinJx/p7/Hj8K62HFzhMEuy7aMRbADd/KqcV2AsPVzgCj3PMHPmb6ZAkA=="
|
||||
},
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
|
@ -22711,9 +22759,9 @@
|
|||
"optional": true
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.1.30",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz",
|
||||
"integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ=="
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
|
@ -25423,6 +25471,15 @@
|
|||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"react-editor-js": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/react-editor-js/-/react-editor-js-2.0.6.tgz",
|
||||
"integrity": "sha512-8u47IbhExiFB2kGNdJYlsX5iVlSzac38A3oJ7bmnTz3Lp7Slys1xreoYdG71+KiOcfX0dEgOIavV4e6TJeB5eg==",
|
||||
"requires": {
|
||||
"@react-editor-js/client": "2.0.6",
|
||||
"@react-editor-js/server": "2.0.6"
|
||||
}
|
||||
},
|
||||
"react-error-boundary": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-1.2.5.tgz",
|
||||
|
|
13
package.json
13
package.json
|
@ -18,16 +18,17 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.4.15",
|
||||
"@editorjs/editorjs": "^2.22.2",
|
||||
"@editorjs/header": "^2.6.1",
|
||||
"@editorjs/image": "^2.6.0",
|
||||
"@editorjs/list": "^1.6.1",
|
||||
"@editorjs/editorjs": "^2.24.3",
|
||||
"@editorjs/header": "^2.6.2",
|
||||
"@editorjs/image": "^2.6.2",
|
||||
"@editorjs/list": "^1.7.0",
|
||||
"@editorjs/paragraph": "^2.8.0",
|
||||
"@editorjs/quote": "^2.4.0",
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.58",
|
||||
"@material-ui/styles": "^4.11.4",
|
||||
"@reach/auto-id": "^0.16.0",
|
||||
"@saleor/macaw-ui": "^0.5.2",
|
||||
"@saleor/sdk": "^0.4.4",
|
||||
"@sentry/react": "^6.0.0",
|
||||
|
@ -44,7 +45,6 @@
|
|||
"cypress-mochawesome-reporter": "^2.3.0",
|
||||
"downshift": "^6.1.7",
|
||||
"editorjs-inline-tool": "^0.4.0",
|
||||
"editorjs-undo": "^0.1.4",
|
||||
"faker": "^5.1.0",
|
||||
"fast-array-diff": "^0.2.0",
|
||||
"fuzzaldrin": "^2.1.0",
|
||||
|
@ -60,6 +60,7 @@
|
|||
"react": "^16.12.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-dropzone": "^11.2.4",
|
||||
"react-editor-js": "^2.0.6",
|
||||
"react-error-boundary": "^1.2.5",
|
||||
"react-gtm-module": "^2.0.11",
|
||||
"react-helmet": "^6.1.0",
|
||||
|
@ -92,7 +93,7 @@
|
|||
"@babel/preset-react": "^7.7.4",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@babel/runtime": "^7.7.6",
|
||||
"@editorjs/embed": "^2.4.6",
|
||||
"@editorjs/embed": "^2.5.1",
|
||||
"@formatjs/cli": "^4.5.0",
|
||||
"@graphql-codegen/add": "^3.1.1",
|
||||
"@graphql-codegen/cli": "^2.1.1",
|
||||
|
|
|
@ -28,6 +28,11 @@ import {
|
|||
mapNodeToChoice,
|
||||
mapPagesToChoices
|
||||
} from "@saleor/utils/maps";
|
||||
import { RichTextContextValues } from "@saleor/utils/richText/context";
|
||||
import {
|
||||
GetRichTextValues,
|
||||
RichTextGetters
|
||||
} from "@saleor/utils/richText/useMultipleRichText";
|
||||
|
||||
import { AttributePageFormData } from "../components/AttributePage";
|
||||
|
||||
|
@ -36,6 +41,11 @@ type AtributesOfFiles = Pick<
|
|||
"file" | "id" | "values" | "contentType"
|
||||
>;
|
||||
|
||||
export interface RichTextProps {
|
||||
richText: RichTextContextValues;
|
||||
attributeRichTextGetters: RichTextGetters<string>;
|
||||
}
|
||||
|
||||
export const ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES = [
|
||||
AttributeInputTypeEnum.DROPDOWN,
|
||||
AttributeInputTypeEnum.MULTISELECT,
|
||||
|
@ -257,6 +267,41 @@ export const mergeAttributeValues = (
|
|||
: attributeValues;
|
||||
};
|
||||
|
||||
export const mergeAttributes = (
|
||||
...attributeLists: AttributeInput[][]
|
||||
): AttributeInput[] =>
|
||||
attributeLists.reduce((prev, attributes) => {
|
||||
const newAttributeIds = new Set(attributes.map(attr => attr.id));
|
||||
return [
|
||||
...prev.filter(attr => !newAttributeIds.has(attr.id)),
|
||||
...attributes
|
||||
];
|
||||
}, []);
|
||||
|
||||
export function getRichTextAttributesFromMap(
|
||||
attributes: AttributeInput[],
|
||||
values: GetRichTextValues
|
||||
): AttributeInput[] {
|
||||
return attributes
|
||||
.filter(({ data }) => data.inputType === AttributeInputTypeEnum.RICH_TEXT)
|
||||
.map(attribute => ({
|
||||
...attribute,
|
||||
value: [JSON.stringify(values[attribute.id])]
|
||||
}));
|
||||
}
|
||||
|
||||
export function getRichTextDataFromAttributes(
|
||||
attributes: AttributeInput[] = []
|
||||
): Record<string, string> {
|
||||
const keyValuePairs = attributes
|
||||
.filter(
|
||||
attribute => attribute.data.inputType === AttributeInputTypeEnum.RICH_TEXT
|
||||
)
|
||||
.map(attribute => [attribute.id, attribute.value[0]]);
|
||||
|
||||
return Object.fromEntries(keyValuePairs);
|
||||
}
|
||||
|
||||
export const getFileValuesToUploadFromAttributes = (
|
||||
attributesWithNewFileValue: FormsetData<null, File>
|
||||
) => attributesWithNewFileValue.filter(fileAttribute => !!fileAttribute.value);
|
||||
|
|
|
@ -53,7 +53,6 @@ export const CategoryCreatePage: React.FC<CategoryCreatePageProps> = ({
|
|||
disabled={disabled}
|
||||
errors={errors}
|
||||
onChange={change}
|
||||
onDescriptionChange={handlers.changeDescription}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<SeoForm
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import { OutputData } from "@editorjs/editorjs";
|
||||
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
|
||||
import { MetadataFormData } from "@saleor/components/Metadata";
|
||||
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
|
||||
import useForm, {
|
||||
CommonUseFormResult,
|
||||
FormChange
|
||||
} from "@saleor/hooks/useForm";
|
||||
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
|
||||
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
|
||||
import {
|
||||
RichTextContext,
|
||||
RichTextContextValues
|
||||
} from "@saleor/utils/richText/context";
|
||||
import useRichText from "@saleor/utils/richText/useRichText";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
|
@ -23,8 +26,8 @@ export interface CategoryCreateData extends CategoryCreateFormData {
|
|||
|
||||
interface CategoryCreateHandlers {
|
||||
changeMetadata: FormChange;
|
||||
changeDescription: RichTextEditorChange;
|
||||
}
|
||||
|
||||
export interface UseCategoryCreateFormResult
|
||||
extends CommonUseFormResult<CategoryCreateData> {
|
||||
handlers: CategoryCreateHandlers;
|
||||
|
@ -48,10 +51,10 @@ const initialData: CategoryCreateFormData = {
|
|||
function useCategoryCreateForm(
|
||||
onSubmit: (data: CategoryCreateData) => Promise<any[]>,
|
||||
disabled: boolean
|
||||
): UseCategoryCreateFormResult {
|
||||
): UseCategoryCreateFormResult & { richText: RichTextContextValues } {
|
||||
const {
|
||||
handleChange,
|
||||
data,
|
||||
data: formData,
|
||||
triggerChange,
|
||||
formId,
|
||||
setIsSubmitDisabled
|
||||
|
@ -66,7 +69,7 @@ function useCategoryCreateForm(
|
|||
formId
|
||||
});
|
||||
|
||||
const [description, changeDescription] = useRichText({
|
||||
const richText = useRichText({
|
||||
initial: null,
|
||||
triggerChange
|
||||
});
|
||||
|
@ -77,13 +80,18 @@ function useCategoryCreateForm(
|
|||
|
||||
const changeMetadata = makeMetadataChangeHandler(handleChange);
|
||||
|
||||
const data: CategoryCreateData = {
|
||||
...formData,
|
||||
description: null
|
||||
};
|
||||
|
||||
// Need to make it function to always have description.current up to date
|
||||
const getData = (): CategoryCreateData => ({
|
||||
...data,
|
||||
description: description.current
|
||||
const getData = async (): Promise<CategoryCreateData> => ({
|
||||
...formData,
|
||||
description: await richText.getValue()
|
||||
});
|
||||
|
||||
const submit = () => handleFormSubmit(getData());
|
||||
const submit = async () => handleFormSubmit(await getData());
|
||||
|
||||
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
|
||||
|
||||
|
@ -91,13 +99,13 @@ function useCategoryCreateForm(
|
|||
|
||||
return {
|
||||
change: handleChange,
|
||||
data: getData(),
|
||||
data,
|
||||
handlers: {
|
||||
changeDescription,
|
||||
changeMetadata
|
||||
},
|
||||
submit,
|
||||
isSaveDisabled: disabled
|
||||
isSaveDisabled: disabled,
|
||||
richText
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -106,9 +114,15 @@ const CategoryCreateForm: React.FC<CategoryCreateFormProps> = ({
|
|||
onSubmit,
|
||||
disabled
|
||||
}) => {
|
||||
const props = useCategoryCreateForm(onSubmit, disabled);
|
||||
const { richText, ...props } = useCategoryCreateForm(onSubmit, disabled);
|
||||
|
||||
return <form onSubmit={props.submit}>{children(props)}</form>;
|
||||
return (
|
||||
<form onSubmit={props.submit}>
|
||||
<RichTextContext.Provider value={richText}>
|
||||
{children(props)}
|
||||
</RichTextContext.Provider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
CategoryCreateForm.displayName = "CategoryCreateForm";
|
||||
|
|
|
@ -2,12 +2,11 @@ import { OutputData } from "@editorjs/editorjs";
|
|||
import { Card, CardContent, TextField } from "@material-ui/core";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import FormSpacer from "@saleor/components/FormSpacer";
|
||||
import RichTextEditor, {
|
||||
RichTextEditorChange
|
||||
} from "@saleor/components/RichTextEditor";
|
||||
import RichTextEditor from "@saleor/components/RichTextEditor";
|
||||
import { ProductErrorFragment } from "@saleor/graphql";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
|
||||
import { useRichTextContext } from "@saleor/utils/richText/context";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
|
@ -19,17 +18,21 @@ interface CategoryDetailsFormProps {
|
|||
disabled: boolean;
|
||||
errors: ProductErrorFragment[];
|
||||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
onDescriptionChange: RichTextEditorChange;
|
||||
}
|
||||
|
||||
export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
|
||||
disabled,
|
||||
data,
|
||||
onChange,
|
||||
onDescriptionChange,
|
||||
errors
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
defaultValue,
|
||||
editorRef,
|
||||
isReadyForMount,
|
||||
handleChange
|
||||
} = useRichTextContext();
|
||||
|
||||
const formErrors = getFormErrors(["name", "description"], errors);
|
||||
|
||||
|
@ -55,8 +58,11 @@ export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
|
|||
/>
|
||||
</div>
|
||||
<FormSpacer />
|
||||
{isReadyForMount && (
|
||||
<RichTextEditor
|
||||
data={data.description}
|
||||
defaultValue={defaultValue}
|
||||
editorRef={editorRef}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
error={!!formErrors.description}
|
||||
helperText={getProductErrorMessage(formErrors.description, intl)}
|
||||
|
@ -65,8 +71,8 @@ export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
|
|||
defaultMessage: "Category Description"
|
||||
})}
|
||||
name="description"
|
||||
onChange={onDescriptionChange}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -110,7 +110,6 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
|
|||
disabled={disabled}
|
||||
errors={errors}
|
||||
onChange={change}
|
||||
onDescriptionChange={handlers.changeDescription}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<CategoryBackground
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { OutputData } from "@editorjs/editorjs";
|
||||
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
|
||||
import { MetadataFormData } from "@saleor/components/Metadata";
|
||||
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
|
||||
import { CategoryDetailsFragment } from "@saleor/graphql";
|
||||
import useForm, {
|
||||
CommonUseFormResult,
|
||||
|
@ -11,6 +10,10 @@ import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
|
|||
import { mapMetadataItemToInput } from "@saleor/utils/maps";
|
||||
import getMetadata from "@saleor/utils/metadata/getMetadata";
|
||||
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
|
||||
import {
|
||||
RichTextContext,
|
||||
RichTextContextValues
|
||||
} from "@saleor/utils/richText/context";
|
||||
import useRichText from "@saleor/utils/richText/useRichText";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
|
@ -27,8 +30,8 @@ export interface CategoryUpdateData extends CategoryUpdateFormData {
|
|||
|
||||
interface CategoryUpdateHandlers {
|
||||
changeMetadata: FormChange;
|
||||
changeDescription: RichTextEditorChange;
|
||||
}
|
||||
|
||||
export interface UseCategoryUpdateFormResult
|
||||
extends CommonUseFormResult<CategoryUpdateData> {
|
||||
handlers: CategoryUpdateHandlers;
|
||||
|
@ -55,10 +58,10 @@ function useCategoryUpdateForm(
|
|||
category: CategoryDetailsFragment,
|
||||
onSubmit: (data: CategoryUpdateData) => Promise<any[]>,
|
||||
disabled: boolean
|
||||
): UseCategoryUpdateFormResult {
|
||||
): UseCategoryUpdateFormResult & { richText: RichTextContextValues } {
|
||||
const {
|
||||
handleChange,
|
||||
data,
|
||||
data: formData,
|
||||
triggerChange,
|
||||
formId,
|
||||
setIsSubmitDisabled
|
||||
|
@ -73,7 +76,7 @@ function useCategoryUpdateForm(
|
|||
formId
|
||||
});
|
||||
|
||||
const [description, changeDescription] = useRichText({
|
||||
const richText = useRichText({
|
||||
initial: category?.description,
|
||||
triggerChange
|
||||
});
|
||||
|
@ -86,18 +89,23 @@ function useCategoryUpdateForm(
|
|||
|
||||
const changeMetadata = makeMetadataChangeHandler(handleChange);
|
||||
|
||||
const data: CategoryUpdateData = {
|
||||
...formData,
|
||||
description: null
|
||||
};
|
||||
|
||||
// Need to make it function to always have description.current up to date
|
||||
const getData = (): CategoryUpdateData => ({
|
||||
...data,
|
||||
description: description.current
|
||||
const getData = async (): Promise<CategoryUpdateData> => ({
|
||||
...formData,
|
||||
description: await richText.getValue()
|
||||
});
|
||||
|
||||
const getSubmitData = (): CategoryUpdateData => ({
|
||||
...getData(),
|
||||
const getSubmitData = async (): Promise<CategoryUpdateData> => ({
|
||||
...(await getData()),
|
||||
...getMetadata(data, isMetadataModified, isPrivateMetadataModified)
|
||||
});
|
||||
|
||||
const submit = () => handleFormSubmit(getSubmitData());
|
||||
const submit = async () => handleFormSubmit(await getSubmitData());
|
||||
|
||||
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
|
||||
|
||||
|
@ -105,13 +113,13 @@ function useCategoryUpdateForm(
|
|||
|
||||
return {
|
||||
change: handleChange,
|
||||
data: getData(),
|
||||
data,
|
||||
handlers: {
|
||||
changeDescription,
|
||||
changeMetadata
|
||||
},
|
||||
submit,
|
||||
isSaveDisabled: disabled
|
||||
isSaveDisabled: disabled,
|
||||
richText
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -121,9 +129,19 @@ const CategoryUpdateForm: React.FC<CategoryUpdateFormProps> = ({
|
|||
onSubmit,
|
||||
disabled
|
||||
}) => {
|
||||
const props = useCategoryUpdateForm(category, onSubmit, disabled);
|
||||
const { richText, ...props } = useCategoryUpdateForm(
|
||||
category,
|
||||
onSubmit,
|
||||
disabled
|
||||
);
|
||||
|
||||
return <form onSubmit={props.submit}>{children(props)}</form>;
|
||||
return (
|
||||
<form onSubmit={props.submit}>
|
||||
<RichTextContext.Provider value={richText}>
|
||||
{children(props)}
|
||||
</RichTextContext.Provider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
CategoryUpdateForm.displayName = "CategoryUpdateForm";
|
||||
|
|
|
@ -77,7 +77,6 @@ const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
|
|||
disabled={disabled}
|
||||
errors={errors}
|
||||
onChange={change}
|
||||
onDescriptionChange={handlers.changeDescription}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<CollectionImage
|
||||
|
|
|
@ -4,7 +4,6 @@ import { createChannelsChangeHandler } from "@saleor/collections/utils";
|
|||
import { COLLECTION_CREATE_FORM_ID } from "@saleor/collections/views/consts";
|
||||
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
|
||||
import { MetadataFormData } from "@saleor/components/Metadata";
|
||||
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
|
||||
import useForm, {
|
||||
CommonUseFormResultWithHandlers,
|
||||
FormChange,
|
||||
|
@ -12,6 +11,10 @@ import useForm, {
|
|||
} from "@saleor/hooks/useForm";
|
||||
import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
|
||||
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
|
||||
import {
|
||||
RichTextContext,
|
||||
RichTextContextValues
|
||||
} from "@saleor/utils/richText/context";
|
||||
import useRichText from "@saleor/utils/richText/useRichText";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
|
@ -33,7 +36,6 @@ export interface CollectionCreateData extends CollectionCreateFormData {
|
|||
|
||||
interface CollectionCreateHandlers {
|
||||
changeMetadata: FormChange;
|
||||
changeDescription: RichTextEditorChange;
|
||||
changeChannels: (
|
||||
id: string,
|
||||
data: Omit<ChannelCollectionData, "name" | "id">
|
||||
|
@ -74,7 +76,7 @@ function useCollectionCreateForm(
|
|||
setChannels: (data: ChannelCollectionData[]) => void,
|
||||
onSubmit: (data: CollectionCreateData) => SubmitPromise,
|
||||
disabled: boolean
|
||||
): UseCollectionCreateFormResult {
|
||||
): UseCollectionCreateFormResult & { richText: RichTextContextValues } {
|
||||
const {
|
||||
handleChange,
|
||||
data: formData,
|
||||
|
@ -95,7 +97,7 @@ function useCollectionCreateForm(
|
|||
formId
|
||||
});
|
||||
|
||||
const [description, changeDescription] = useRichText({
|
||||
const richText = useRichText({
|
||||
initial: null,
|
||||
triggerChange
|
||||
});
|
||||
|
@ -106,10 +108,15 @@ function useCollectionCreateForm(
|
|||
|
||||
const changeMetadata = makeMetadataChangeHandler(handleChange);
|
||||
|
||||
// Need to make it function to always have description.current up to date
|
||||
const getData = (): CollectionCreateData => ({
|
||||
const data: CollectionCreateData = {
|
||||
...formData,
|
||||
description: description.current
|
||||
description: null
|
||||
};
|
||||
|
||||
// Need to make it function to always have description.current up to date
|
||||
const getData = async (): Promise<CollectionCreateData> => ({
|
||||
...formData,
|
||||
description: await richText.getValue()
|
||||
});
|
||||
|
||||
const handleChannelChange = createChannelsChangeHandler(
|
||||
|
@ -118,7 +125,7 @@ function useCollectionCreateForm(
|
|||
triggerChange
|
||||
);
|
||||
|
||||
const submit = () => handleFormSubmit(getData());
|
||||
const submit = async () => handleFormSubmit(await getData());
|
||||
|
||||
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
|
||||
|
||||
|
@ -127,14 +134,14 @@ function useCollectionCreateForm(
|
|||
|
||||
return {
|
||||
change: handleChange,
|
||||
data: getData(),
|
||||
data,
|
||||
handlers: {
|
||||
changeChannels: handleChannelChange,
|
||||
changeDescription,
|
||||
changeMetadata
|
||||
},
|
||||
submit,
|
||||
isSaveDisabled
|
||||
isSaveDisabled,
|
||||
richText
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -145,14 +152,20 @@ const CollectionCreateForm: React.FC<CollectionCreateFormProps> = ({
|
|||
onSubmit,
|
||||
disabled
|
||||
}) => {
|
||||
const props = useCollectionCreateForm(
|
||||
const { richText, ...props } = useCollectionCreateForm(
|
||||
currentChannels,
|
||||
setChannels,
|
||||
onSubmit,
|
||||
disabled
|
||||
);
|
||||
|
||||
return <form onSubmit={props.submit}>{children(props)}</form>;
|
||||
return (
|
||||
<form onSubmit={props.submit}>
|
||||
<RichTextContext.Provider value={richText}>
|
||||
{children(props)}
|
||||
</RichTextContext.Provider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
CollectionCreateForm.displayName = "CollectionCreateForm";
|
||||
|
|
|
@ -2,12 +2,11 @@ import { OutputData } from "@editorjs/editorjs";
|
|||
import { Card, CardContent, TextField } from "@material-ui/core";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import FormSpacer from "@saleor/components/FormSpacer";
|
||||
import RichTextEditor, {
|
||||
RichTextEditorChange
|
||||
} from "@saleor/components/RichTextEditor";
|
||||
import RichTextEditor from "@saleor/components/RichTextEditor";
|
||||
import { CollectionErrorFragment } from "@saleor/graphql";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
|
||||
import { useRichTextContext } from "@saleor/utils/richText/context";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
|
@ -19,18 +18,21 @@ export interface CollectionDetailsProps {
|
|||
disabled: boolean;
|
||||
errors: CollectionErrorFragment[];
|
||||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
onDescriptionChange: RichTextEditorChange;
|
||||
}
|
||||
|
||||
const CollectionDetails: React.FC<CollectionDetailsProps> = ({
|
||||
disabled,
|
||||
data,
|
||||
onChange,
|
||||
onDescriptionChange,
|
||||
errors
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const {
|
||||
defaultValue,
|
||||
editorRef,
|
||||
isReadyForMount,
|
||||
handleChange
|
||||
} = useRichTextContext();
|
||||
const formErrors = getFormErrors(["name", "description"], errors);
|
||||
|
||||
return (
|
||||
|
@ -54,15 +56,18 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
|
|||
fullWidth
|
||||
/>
|
||||
<FormSpacer />
|
||||
{isReadyForMount && (
|
||||
<RichTextEditor
|
||||
data={data.description}
|
||||
defaultValue={defaultValue}
|
||||
editorRef={editorRef}
|
||||
onChange={handleChange}
|
||||
error={!!formErrors.description}
|
||||
helperText={getProductErrorMessage(formErrors.description, intl)}
|
||||
label={intl.formatMessage(commonMessages.description)}
|
||||
name="description"
|
||||
disabled={disabled}
|
||||
onChange={onDescriptionChange}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -88,7 +88,6 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
|
|||
disabled={disabled}
|
||||
errors={errors}
|
||||
onChange={change}
|
||||
onDescriptionChange={handlers.changeDescription}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<CollectionImage
|
||||
|
|
|
@ -4,7 +4,6 @@ import { createChannelsChangeHandler } from "@saleor/collections/utils";
|
|||
import { COLLECTION_DETAILS_FORM_ID } from "@saleor/collections/views/consts";
|
||||
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
|
||||
import { MetadataFormData } from "@saleor/components/Metadata";
|
||||
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
|
||||
import { CollectionDetailsFragment } from "@saleor/graphql";
|
||||
import useForm, {
|
||||
CommonUseFormResultWithHandlers,
|
||||
|
@ -14,6 +13,10 @@ import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit";
|
|||
import { mapMetadataItemToInput } from "@saleor/utils/maps";
|
||||
import getMetadata from "@saleor/utils/metadata/getMetadata";
|
||||
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
|
||||
import {
|
||||
RichTextContext,
|
||||
RichTextContextValues
|
||||
} from "@saleor/utils/richText/context";
|
||||
import useRichText from "@saleor/utils/richText/useRichText";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
|
@ -31,7 +34,6 @@ export interface CollectionUpdateData extends CollectionUpdateFormData {
|
|||
|
||||
interface CollectionUpdateHandlers {
|
||||
changeMetadata: FormChange;
|
||||
changeDescription: RichTextEditorChange;
|
||||
changeChannels: (
|
||||
id: string,
|
||||
data: Omit<ChannelCollectionData, "name" | "id">
|
||||
|
@ -71,7 +73,7 @@ function useCollectionUpdateForm(
|
|||
setChannels: (data: ChannelCollectionData[]) => void,
|
||||
onSubmit: (data: CollectionUpdateData) => Promise<any[]>,
|
||||
disabled: boolean
|
||||
): UseCollectionUpdateFormResult {
|
||||
): UseCollectionUpdateFormResult & { richText: RichTextContextValues } {
|
||||
const {
|
||||
handleChange,
|
||||
data: formData,
|
||||
|
@ -92,7 +94,7 @@ function useCollectionUpdateForm(
|
|||
formId: COLLECTION_DETAILS_FORM_ID
|
||||
});
|
||||
|
||||
const [description, changeDescription] = useRichText({
|
||||
const richText = useRichText({
|
||||
initial: collection?.description,
|
||||
triggerChange
|
||||
});
|
||||
|
@ -105,14 +107,19 @@ function useCollectionUpdateForm(
|
|||
|
||||
const changeMetadata = makeMetadataChangeHandler(handleChange);
|
||||
|
||||
// Need to make it function to always have description.current up to date
|
||||
const getData = (): CollectionUpdateData => ({
|
||||
const data: CollectionUpdateData = {
|
||||
...formData,
|
||||
description: description.current
|
||||
description: null
|
||||
};
|
||||
|
||||
// Need to make it function to always have description.current up to date
|
||||
const getData = async (): Promise<CollectionUpdateData> => ({
|
||||
...formData,
|
||||
description: await richText.getValue()
|
||||
});
|
||||
|
||||
const getSubmitData = (): CollectionUpdateData => ({
|
||||
...getData(),
|
||||
const getSubmitData = async (): Promise<CollectionUpdateData> => ({
|
||||
...(await getData()),
|
||||
...getMetadata(formData, isMetadataModified, isPrivateMetadataModified)
|
||||
});
|
||||
|
||||
|
@ -122,7 +129,7 @@ function useCollectionUpdateForm(
|
|||
triggerChange
|
||||
);
|
||||
|
||||
const submit = () => handleFormSubmit(getSubmitData());
|
||||
const submit = async () => handleFormSubmit(await getSubmitData());
|
||||
|
||||
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
|
||||
|
||||
|
@ -130,13 +137,13 @@ function useCollectionUpdateForm(
|
|||
|
||||
return {
|
||||
change: handleChange,
|
||||
data: getData(),
|
||||
data,
|
||||
handlers: {
|
||||
changeChannels: handleChannelChange,
|
||||
changeDescription,
|
||||
changeMetadata
|
||||
},
|
||||
submit
|
||||
submit,
|
||||
richText
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -148,7 +155,7 @@ const CollectionUpdateForm: React.FC<CollectionUpdateFormProps> = ({
|
|||
onSubmit,
|
||||
disabled
|
||||
}) => {
|
||||
const props = useCollectionUpdateForm(
|
||||
const { richText, ...props } = useCollectionUpdateForm(
|
||||
collection,
|
||||
currentChannels,
|
||||
setChannels,
|
||||
|
@ -156,7 +163,13 @@ const CollectionUpdateForm: React.FC<CollectionUpdateFormProps> = ({
|
|||
disabled
|
||||
);
|
||||
|
||||
return <form onSubmit={props.submit}>{children(props)}</form>;
|
||||
return (
|
||||
<form onSubmit={props.submit}>
|
||||
<RichTextContext.Provider value={richText}>
|
||||
{children(props)}
|
||||
</RichTextContext.Provider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
CollectionUpdateForm.displayName = "CollectionUpdateForm";
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
getMultiChoices,
|
||||
getMultiDisplayValue,
|
||||
getReferenceDisplayValue,
|
||||
getRichTextData,
|
||||
getSingleChoices,
|
||||
getSingleDisplayValue
|
||||
} from "@saleor/components/Attributes/utils";
|
||||
|
@ -43,8 +42,8 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
|
|||
onChange,
|
||||
fetchAttributeValues,
|
||||
fetchMoreAttributeValues,
|
||||
entityId,
|
||||
onAttributeSelectBlur
|
||||
onAttributeSelectBlur,
|
||||
richTextGetters
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles();
|
||||
|
@ -126,18 +125,27 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
|
|||
/>
|
||||
);
|
||||
case AttributeInputTypeEnum.RICH_TEXT:
|
||||
const {
|
||||
getShouldMount,
|
||||
getDefaultValue,
|
||||
getMountEditor,
|
||||
getHandleChange
|
||||
} = richTextGetters;
|
||||
const defaultValue = getDefaultValue(attribute.id);
|
||||
return (
|
||||
<BasicAttributeRow label={attribute.label}>
|
||||
{getShouldMount(attribute.id) && (
|
||||
<RichTextEditor
|
||||
key={entityId} // temporary workaround, TODO: refactor rich text editor
|
||||
defaultValue={defaultValue}
|
||||
editorRef={getMountEditor(attribute.id)}
|
||||
onChange={getHandleChange(attribute.id)}
|
||||
name={`attribute:${attribute.label}`}
|
||||
disabled={disabled}
|
||||
error={!!error}
|
||||
label={intl.formatMessage(attributeRowMessages.valueLabel)}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
onChange={data => onChange(attribute.id, JSON.stringify(data))}
|
||||
data={getRichTextData(attribute)}
|
||||
/>
|
||||
)}
|
||||
</BasicAttributeRow>
|
||||
);
|
||||
case AttributeInputTypeEnum.NUMERIC:
|
||||
|
|
|
@ -20,7 +20,13 @@ const props: AttributesProps = {
|
|||
onReferencesReorder: () => undefined,
|
||||
fetchAttributeValues: () => undefined,
|
||||
fetchMoreAttributeValues: fetchMoreProps,
|
||||
onAttributeSelectBlur: () => undefined
|
||||
onAttributeSelectBlur: () => undefined,
|
||||
richTextGetters: {
|
||||
getDefaultValue: () => undefined,
|
||||
getHandleChange: () => () => undefined,
|
||||
getMountEditor: () => () => undefined,
|
||||
getShouldMount: () => true
|
||||
}
|
||||
};
|
||||
|
||||
storiesOf("Attributes / Attributes", module)
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { FormsetAtomicData } from "@saleor/hooks/useFormset";
|
||||
import { IconButton, makeStyles } from "@saleor/macaw-ui";
|
||||
import { FetchMoreProps } from "@saleor/types";
|
||||
import { RichTextGetters } from "@saleor/utils/richText/useMultipleRichText";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
||||
|
@ -46,7 +47,7 @@ export interface AttributesProps extends AttributeRowHandlers {
|
|||
ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment
|
||||
>;
|
||||
title?: React.ReactNode;
|
||||
entityId?: string;
|
||||
richTextGetters: RichTextGetters<string>;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
|
@ -122,7 +123,7 @@ const Attributes: React.FC<AttributesProps> = ({
|
|||
errors,
|
||||
title,
|
||||
onAttributeSelectBlur,
|
||||
entityId = "_defaultId",
|
||||
richTextGetters,
|
||||
...props
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
@ -170,11 +171,11 @@ const Attributes: React.FC<AttributesProps> = ({
|
|||
<React.Fragment key={attribute.id}>
|
||||
{attributeIndex > 0 && <Hr />}
|
||||
<AttributeRow
|
||||
entityId={entityId}
|
||||
attribute={attribute}
|
||||
attributeValues={attributeValues}
|
||||
error={error}
|
||||
onAttributeSelectBlur={onAttributeSelectBlur}
|
||||
richTextGetters={richTextGetters}
|
||||
{...props}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
} from "@saleor/graphql";
|
||||
import { FormsetChange } from "@saleor/hooks/useFormset";
|
||||
import { FetchMoreProps, ReorderEvent } from "@saleor/types";
|
||||
import { RichTextGetters } from "@saleor/utils/richText/useMultipleRichText";
|
||||
|
||||
import { AttributeInput } from "./Attributes";
|
||||
|
||||
|
@ -31,6 +32,6 @@ export interface AttributeRowProps extends AttributeRowHandlers {
|
|||
disabled: boolean;
|
||||
error: ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment;
|
||||
loading: boolean;
|
||||
entityId: string;
|
||||
onAttributeSelectBlur?: () => void;
|
||||
richTextGetters: RichTextGetters<string>;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface GridProps {
|
|||
children: React.ReactNodeArray | React.ReactNode;
|
||||
className?: string;
|
||||
variant?: GridVariant;
|
||||
richText?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
|
@ -31,13 +32,18 @@ const useStyles = makeStyles(
|
|||
},
|
||||
uniform: {
|
||||
gridTemplateColumns: "1fr 1fr"
|
||||
},
|
||||
richText: {
|
||||
"&& > div": {
|
||||
overflow: "visible"
|
||||
}
|
||||
}
|
||||
}),
|
||||
{ name: "Grid" }
|
||||
);
|
||||
|
||||
export const Grid: React.FC<GridProps> = props => {
|
||||
const { className, children, variant } = props;
|
||||
const { className, children, variant, richText } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
|
||||
|
@ -46,7 +52,8 @@ export const Grid: React.FC<GridProps> = props => {
|
|||
className={classNames(className, classes.root, {
|
||||
[classes.default]: variant === "default",
|
||||
[classes.inverted]: variant === "inverted",
|
||||
[classes.uniform]: variant === "uniform"
|
||||
[classes.uniform]: variant === "uniform",
|
||||
[classes.richText]: richText
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -9,16 +9,16 @@ import * as fixtures from "./fixtures.json";
|
|||
import { RichTextEditorProps } from "./RichTextEditor";
|
||||
import RichTextEditorContent from "./RichTextEditorContent";
|
||||
|
||||
export const data: OutputData = fixtures.richTextEditor;
|
||||
export const defaultValue: OutputData = fixtures.richTextEditor;
|
||||
|
||||
const props: RichTextEditorProps = {
|
||||
data,
|
||||
defaultValue,
|
||||
disabled: false,
|
||||
error: false,
|
||||
helperText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
|
||||
label: "Content",
|
||||
name: "content",
|
||||
onChange: () => undefined
|
||||
editorRef: null
|
||||
};
|
||||
|
||||
storiesOf("Generics / Rich text editor", module)
|
||||
|
@ -27,4 +27,6 @@ storiesOf("Generics / Rich text editor", module)
|
|||
.add("default", () => <RichTextEditor {...props} />)
|
||||
.add("disabled", () => <RichTextEditor {...props} disabled={true} />)
|
||||
.add("error", () => <RichTextEditor {...props} error={true} />)
|
||||
.add("static", () => <RichTextEditorContent {...props} />);
|
||||
.add("static", () => (
|
||||
<RichTextEditorContent {...props} value={defaultValue} />
|
||||
));
|
||||
|
|
|
@ -1,98 +1,73 @@
|
|||
import EditorJS, { LogLevels, OutputData } from "@editorjs/editorjs";
|
||||
import { LogLevels, OutputData } from "@editorjs/editorjs";
|
||||
import { FormControl, FormHelperText, InputLabel } from "@material-ui/core";
|
||||
import { PromiseQueue } from "@saleor/misc";
|
||||
import { useId } from "@reach/auto-id";
|
||||
import { Props as ReactEditorJSProps } from "@react-editor-js/core";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { createReactEditorJS } from "react-editor-js";
|
||||
|
||||
import { RichTextEditorContentProps, tools } from "./RichTextEditorContent";
|
||||
import { tools } from "./consts";
|
||||
import { useHasRendered } from "./hooks";
|
||||
import useStyles from "./styles";
|
||||
import { clean } from "./utils";
|
||||
|
||||
export type RichTextEditorChange = (data: OutputData) => void;
|
||||
export interface RichTextEditorProps extends RichTextEditorContentProps {
|
||||
export type EditorJsProps = Omit<ReactEditorJSProps, "factory">;
|
||||
|
||||
// https://github.com/Jungwoo-An/react-editor-js#how-to-access-editor-js-instance
|
||||
export interface EditorCore {
|
||||
destroy(): Promise<void>;
|
||||
clear(): Promise<void>;
|
||||
save(): Promise<OutputData>;
|
||||
render(data: OutputData): Promise<void>;
|
||||
}
|
||||
|
||||
export interface RichTextEditorProps extends Omit<EditorJsProps, "onChange"> {
|
||||
id?: string;
|
||||
disabled: boolean;
|
||||
error: boolean;
|
||||
helperText: string;
|
||||
label: string;
|
||||
name: string;
|
||||
onChange: RichTextEditorChange;
|
||||
editorRef:
|
||||
| React.RefCallback<EditorCore>
|
||||
| React.MutableRefObject<EditorCore>
|
||||
| null;
|
||||
// onChange with value shouldn't be used due to issues with React and EditorJS integration
|
||||
onChange?: () => void;
|
||||
}
|
||||
|
||||
const ReactEditorJS = createReactEditorJS();
|
||||
|
||||
const RichTextEditor: React.FC<RichTextEditorProps> = ({
|
||||
data,
|
||||
id: defaultId,
|
||||
disabled,
|
||||
error,
|
||||
helperText,
|
||||
label,
|
||||
name,
|
||||
onChange,
|
||||
onReady
|
||||
helperText,
|
||||
editorRef,
|
||||
onInitialize,
|
||||
onReady,
|
||||
...props
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
const id = useId(defaultId);
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
|
||||
const [isFocused, setFocus] = React.useState(false);
|
||||
const editor = React.useRef<EditorJS>();
|
||||
const editorContainer = React.useRef<HTMLDivElement>();
|
||||
const togglePromiseQueue = React.useRef(PromiseQueue()); // used to await subsequent toggle invocations
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (data !== undefined && !editor.current) {
|
||||
editor.current = new EditorJS({
|
||||
data,
|
||||
holder: editorContainer.current,
|
||||
logLevel: "ERROR" as LogLevels,
|
||||
onChange: async api => {
|
||||
const savedData = await api.saver.save();
|
||||
onChange(savedData);
|
||||
},
|
||||
onReady: () => {
|
||||
// FIXME: This throws an error and is not working
|
||||
// const undo = new Undo({ editor });
|
||||
// undo.initialize(data);
|
||||
|
||||
if (onReady) {
|
||||
onReady();
|
||||
}
|
||||
},
|
||||
tools
|
||||
});
|
||||
const handleInitialize = React.useCallback((editor: EditorCore) => {
|
||||
if (onInitialize) {
|
||||
onInitialize(editor);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clean(editor.current);
|
||||
editor.current = null;
|
||||
};
|
||||
},
|
||||
// Rerender editor only if changed from undefined to defined state
|
||||
[data === undefined]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const toggle = async () => {
|
||||
if (!editor.current) {
|
||||
return;
|
||||
if (typeof editorRef === "function") {
|
||||
return editorRef(editor);
|
||||
}
|
||||
|
||||
await editor.current.isReady;
|
||||
if (editor.current?.readOnly) {
|
||||
// readOnly.toggle() by itself does not enqueue the events and will result in a broken output if invocations overlap
|
||||
// Remove this logic when this is fixed in EditorJS
|
||||
togglePromiseQueue.current.add(() =>
|
||||
editor.current.readOnly.toggle(disabled)
|
||||
);
|
||||
|
||||
// Switching to readOnly with empty blocks present causes the editor to freeze
|
||||
// Remove this logic when this is fixed in EditorJS
|
||||
if (!disabled && !data?.blocks?.length) {
|
||||
await togglePromiseQueue.current.queue;
|
||||
editor.current.clear();
|
||||
if (editorRef) {
|
||||
return (editorRef.current = editor);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
toggle();
|
||||
}, [disabled]);
|
||||
// We need to render FormControl first to get id from @reach/auto-id
|
||||
const hasRendered = useHasRendered();
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
|
@ -105,16 +80,28 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
|
|||
<InputLabel focused={true} shrink={true}>
|
||||
{label}
|
||||
</InputLabel>
|
||||
{hasRendered && (
|
||||
<ReactEditorJS
|
||||
// match with the id of holder div
|
||||
holder={id}
|
||||
tools={tools}
|
||||
// LogLeves is undefined at runtime
|
||||
logLevel={"ERROR" as LogLevels.ERROR}
|
||||
onInitialize={handleInitialize}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
id={id}
|
||||
className={classNames(classes.editor, classes.root, {
|
||||
[classes.rootActive]: isFocused,
|
||||
[classes.rootDisabled]: disabled,
|
||||
[classes.rootError]: error
|
||||
})}
|
||||
ref={editorContainer}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
/>
|
||||
</ReactEditorJS>
|
||||
)}
|
||||
<FormHelperText>{helperText}</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
|
|
|
@ -1,104 +1,52 @@
|
|||
import EditorJS, {
|
||||
LogLevels,
|
||||
OutputData,
|
||||
ToolConstructable,
|
||||
ToolSettings
|
||||
} from "@editorjs/editorjs";
|
||||
import Embed from "@editorjs/embed";
|
||||
import Header from "@editorjs/header";
|
||||
import List from "@editorjs/list";
|
||||
import Paragraph from "@editorjs/paragraph";
|
||||
import Quote from "@editorjs/quote";
|
||||
import strikethroughIcon from "@saleor/icons/StrikethroughIcon";
|
||||
import { LogLevels } from "@editorjs/editorjs";
|
||||
import { useId } from "@reach/auto-id";
|
||||
import classNames from "classnames";
|
||||
import createGenericInlineTool from "editorjs-inline-tool";
|
||||
import React from "react";
|
||||
import { createReactEditorJS } from "react-editor-js";
|
||||
|
||||
import { tools } from "./consts";
|
||||
import { useHasRendered } from "./hooks";
|
||||
import { EditorJsProps } from "./RichTextEditor";
|
||||
import useStyles from "./styles";
|
||||
import { clean } from "./utils";
|
||||
|
||||
export interface RichTextEditorContentProps {
|
||||
export interface RichTextEditorContentProps
|
||||
extends Omit<EditorJsProps, "defaultValue"> {
|
||||
id?: string;
|
||||
className?: string;
|
||||
data: OutputData;
|
||||
onReady?: () => void;
|
||||
}
|
||||
|
||||
const inlineToolbar = ["link", "bold", "italic", "strikethrough"];
|
||||
|
||||
export const tools: Record<string, ToolConstructable | ToolSettings> = {
|
||||
embed: Embed,
|
||||
header: {
|
||||
class: Header,
|
||||
config: {
|
||||
defaultLevel: 1,
|
||||
levels: [1, 2, 3]
|
||||
},
|
||||
inlineToolbar
|
||||
},
|
||||
list: {
|
||||
class: List,
|
||||
inlineToolbar
|
||||
},
|
||||
quote: {
|
||||
class: Quote,
|
||||
inlineToolbar
|
||||
},
|
||||
paragraph: {
|
||||
class: Paragraph,
|
||||
inlineToolbar
|
||||
},
|
||||
strikethrough: createGenericInlineTool({
|
||||
sanitize: {
|
||||
s: {}
|
||||
},
|
||||
shortcut: "CMD+S",
|
||||
tagName: "s",
|
||||
toolboxIcon: strikethroughIcon
|
||||
})
|
||||
};
|
||||
const ReactEditorJS = createReactEditorJS();
|
||||
|
||||
const RichTextEditorContent: React.FC<RichTextEditorContentProps> = ({
|
||||
id: defaultId,
|
||||
className,
|
||||
data,
|
||||
onReady
|
||||
value,
|
||||
...props
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
const id = useId(defaultId);
|
||||
|
||||
const editor = React.useRef<EditorJS>();
|
||||
const editorContainer = React.useRef<HTMLDivElement>();
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (data !== undefined && !editor.current) {
|
||||
const editorjs = new EditorJS({
|
||||
data,
|
||||
holder: editorContainer.current,
|
||||
logLevel: "ERROR" as LogLevels,
|
||||
onReady: () => {
|
||||
editor.current = editorjs;
|
||||
// We need to render FormControl first to get id from @reach/auto-id
|
||||
const hasRendered = useHasRendered();
|
||||
|
||||
if (onReady) {
|
||||
onReady();
|
||||
if (!hasRendered) {
|
||||
return <div />;
|
||||
}
|
||||
},
|
||||
readOnly: true,
|
||||
tools
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
clean(editor.current);
|
||||
editor.current = null;
|
||||
};
|
||||
},
|
||||
// Rerender editor only if changed from undefined to defined state
|
||||
[data === undefined]
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactEditorJS
|
||||
holder={id}
|
||||
logLevel={"ERROR" as LogLevels.ERROR}
|
||||
tools={tools}
|
||||
{...props}
|
||||
defaultValue={value}
|
||||
readOnly={true}
|
||||
>
|
||||
<div
|
||||
id={id}
|
||||
className={classNames(classes.editor, classes.rootStatic, className)}
|
||||
ref={editorContainer}
|
||||
/>
|
||||
</ReactEditorJS>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
30
src/components/RichTextEditor/__mocks__/RichTextEditor.tsx
Normal file
30
src/components/RichTextEditor/__mocks__/RichTextEditor.tsx
Normal 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;
|
42
src/components/RichTextEditor/consts.ts
Normal file
42
src/components/RichTextEditor/consts.ts
Normal 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
|
||||
})
|
||||
};
|
11
src/components/RichTextEditor/hooks.ts
Normal file
11
src/components/RichTextEditor/hooks.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { useLayoutEffect, useState } from "react";
|
||||
|
||||
export const useHasRendered = () => {
|
||||
const [hasRendered, setHasRendereed] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setHasRendereed(true);
|
||||
}, []);
|
||||
|
||||
return hasRendered;
|
||||
};
|
|
@ -139,7 +139,14 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
|
|||
onSubmit={onSubmit}
|
||||
disabled={loading}
|
||||
>
|
||||
{({ change, data, handlers, submit, isSaveDisabled }) => (
|
||||
{({
|
||||
change,
|
||||
data,
|
||||
handlers,
|
||||
submit,
|
||||
isSaveDisabled,
|
||||
attributeRichTextGetters
|
||||
}) => (
|
||||
<Container>
|
||||
<Backlink href={pageListUrl()}>
|
||||
{intl.formatMessage(sectionNames.pages)}
|
||||
|
@ -162,7 +169,6 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
|
|||
disabled={loading}
|
||||
errors={errors}
|
||||
onChange={change}
|
||||
onContentChange={handlers.changeContent}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<SeoForm
|
||||
|
@ -199,6 +205,7 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
|
|||
fetchAttributeValues={fetchAttributeValues}
|
||||
fetchMoreAttributeValues={fetchMoreAttributeValues}
|
||||
onAttributeSelectBlur={onAttributeSelectBlur}
|
||||
richTextGetters={attributeRichTextGetters}
|
||||
/>
|
||||
)}
|
||||
<CardSpacer />
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { OutputData } from "@editorjs/editorjs";
|
||||
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
|
||||
import {
|
||||
getAttributesDisplayData,
|
||||
getRichTextAttributesFromMap,
|
||||
getRichTextDataFromAttributes,
|
||||
mergeAttributes,
|
||||
RichTextProps
|
||||
} from "@saleor/attributes/utils/data";
|
||||
import {
|
||||
createAttributeChangeHandler,
|
||||
createAttributeFileChangeHandler,
|
||||
|
@ -12,7 +18,6 @@ import {
|
|||
import { AttributeInput } from "@saleor/components/Attributes";
|
||||
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
|
||||
import { MetadataFormData } from "@saleor/components/Metadata";
|
||||
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
|
||||
import {
|
||||
PageDetailsFragment,
|
||||
SearchPagesQuery,
|
||||
|
@ -39,6 +44,8 @@ import getPublicationData from "@saleor/utils/data/getPublicationData";
|
|||
import { mapMetadataItemToInput } from "@saleor/utils/maps";
|
||||
import getMetadata from "@saleor/utils/metadata/getMetadata";
|
||||
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
|
||||
import { RichTextContext } from "@saleor/utils/richText/context";
|
||||
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
|
||||
import useRichText from "@saleor/utils/richText/useRichText";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
|
@ -64,7 +71,6 @@ export interface PageSubmitData extends PageFormData {
|
|||
|
||||
export interface PageUpdateHandlers {
|
||||
changeMetadata: FormChange;
|
||||
changeContent: RichTextEditorChange;
|
||||
selectPageType: FormChange;
|
||||
selectAttribute: FormsetChange<string>;
|
||||
selectAttributeMulti: FormsetChange<string>;
|
||||
|
@ -75,11 +81,17 @@ export interface PageUpdateHandlers {
|
|||
fetchMoreReferences: FetchMoreProps;
|
||||
}
|
||||
|
||||
export interface UsePageUpdateFormResult
|
||||
extends CommonUseFormResultWithHandlers<PageData, PageUpdateHandlers> {
|
||||
export interface UsePageUpdateFormOutput
|
||||
extends CommonUseFormResultWithHandlers<PageData, PageUpdateHandlers>,
|
||||
RichTextProps {
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
export type UsePageUpdateFormRenderProps = Omit<
|
||||
UsePageUpdateFormOutput,
|
||||
"richText"
|
||||
>;
|
||||
|
||||
export interface UsePageFormOpts {
|
||||
pageTypes?: RelayToFlat<SearchPageTypesQuery["search"]>;
|
||||
referencePages: RelayToFlat<SearchPagesQuery["search"]>;
|
||||
|
@ -94,7 +106,7 @@ export interface UsePageFormOpts {
|
|||
}
|
||||
|
||||
export interface PageFormProps extends UsePageFormOpts {
|
||||
children: (props: UsePageUpdateFormResult) => React.ReactNode;
|
||||
children: (props: UsePageUpdateFormRenderProps) => React.ReactNode;
|
||||
page: PageDetailsFragment;
|
||||
onSubmit: (data: PageData) => SubmitPromise;
|
||||
disabled: boolean;
|
||||
|
@ -117,18 +129,9 @@ function usePageForm(
|
|||
onSubmit: (data: PageData) => SubmitPromise,
|
||||
disabled: boolean,
|
||||
opts: UsePageFormOpts
|
||||
): UsePageUpdateFormResult {
|
||||
): UsePageUpdateFormOutput {
|
||||
const pageExists = page !== null;
|
||||
|
||||
const attributes = useFormset(
|
||||
pageExists
|
||||
? getAttributeInputFromPage(page)
|
||||
: opts.selectedPageType
|
||||
? getAttributeInputFromPageType(opts.selectedPageType)
|
||||
: []
|
||||
);
|
||||
const attributesWithNewFileValue = useFormset<null, File>([]);
|
||||
|
||||
const { handleChange, triggerChange, data: formData, formId } = useForm(
|
||||
getInitialFormData(page),
|
||||
undefined,
|
||||
|
@ -137,11 +140,28 @@ function usePageForm(
|
|||
}
|
||||
);
|
||||
|
||||
const attributes = useFormset(
|
||||
pageExists
|
||||
? getAttributeInputFromPage(page)
|
||||
: opts.selectedPageType
|
||||
? getAttributeInputFromPageType(opts.selectedPageType)
|
||||
: []
|
||||
);
|
||||
|
||||
const {
|
||||
getters: attributeRichTextGetters,
|
||||
getValues: getAttributeRichTextValues
|
||||
} = useMultipleRichText({
|
||||
initial: getRichTextDataFromAttributes(attributes.data),
|
||||
triggerChange
|
||||
});
|
||||
const attributesWithNewFileValue = useFormset<null, File>([]);
|
||||
|
||||
const { setExitDialogSubmitRef, setIsSubmitDisabled } = useExitFormDialog({
|
||||
formId
|
||||
});
|
||||
|
||||
const [content, changeContent] = useRichText({
|
||||
const richText = useRichText({
|
||||
initial: pageExists ? page?.content : null,
|
||||
triggerChange
|
||||
});
|
||||
|
@ -195,8 +215,7 @@ function usePageForm(
|
|||
triggerChange
|
||||
);
|
||||
|
||||
// Need to make it function to always have content.current up to date
|
||||
const getData = (): PageData => ({
|
||||
const data: PageData = {
|
||||
...formData,
|
||||
attributes: getAttributesDisplayData(
|
||||
attributes.data,
|
||||
|
@ -204,14 +223,22 @@ function usePageForm(
|
|||
opts.referencePages,
|
||||
opts.referenceProducts
|
||||
),
|
||||
content: content.current,
|
||||
content: null,
|
||||
pageType: pageExists ? page?.pageType : opts.selectedPageType
|
||||
});
|
||||
};
|
||||
|
||||
const getSubmitData = (): PageSubmitData => ({
|
||||
...getData(),
|
||||
const getSubmitData = async (): Promise<PageSubmitData> => ({
|
||||
...data,
|
||||
...getMetadata(formData, isMetadataModified, isPrivateMetadataModified),
|
||||
...getPublicationData(formData),
|
||||
content: await richText.getValue(),
|
||||
attributes: mergeAttributes(
|
||||
attributes.data,
|
||||
getRichTextAttributesFromMap(
|
||||
attributes.data,
|
||||
await getAttributeRichTextValues()
|
||||
)
|
||||
),
|
||||
attributesWithNewFileValue: attributesWithNewFileValue.data
|
||||
});
|
||||
|
||||
|
@ -230,7 +257,7 @@ function usePageForm(
|
|||
onSubmit: handleSubmit
|
||||
});
|
||||
|
||||
const submit = () => handleFormSubmit(getSubmitData());
|
||||
const submit = async () => handleFormSubmit(await getSubmitData());
|
||||
|
||||
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
|
||||
|
||||
|
@ -241,10 +268,9 @@ function usePageForm(
|
|||
|
||||
return {
|
||||
change: handleChange,
|
||||
data: getData(),
|
||||
data,
|
||||
valid,
|
||||
handlers: {
|
||||
changeContent,
|
||||
changeMetadata,
|
||||
fetchMoreReferences: handleFetchMoreReferences,
|
||||
fetchReferences: handleFetchReferences,
|
||||
|
@ -256,7 +282,9 @@ function usePageForm(
|
|||
selectPageType: handlePageTypeSelect
|
||||
},
|
||||
submit,
|
||||
isSaveDisabled
|
||||
isSaveDisabled,
|
||||
richText,
|
||||
attributeRichTextGetters
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -267,9 +295,15 @@ const PageForm: React.FC<PageFormProps> = ({
|
|||
disabled,
|
||||
...rest
|
||||
}) => {
|
||||
const props = usePageForm(page, onSubmit, disabled, rest);
|
||||
const { richText, ...props } = usePageForm(page, onSubmit, disabled, rest);
|
||||
|
||||
return <form onSubmit={props.submit}>{children(props)}</form>;
|
||||
return (
|
||||
<form onSubmit={props.submit}>
|
||||
<RichTextContext.Provider value={richText}>
|
||||
{children(props)}
|
||||
</RichTextContext.Provider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
PageForm.displayName = "PageForm";
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { Card, CardContent, TextField } from "@material-ui/core";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import FormSpacer from "@saleor/components/FormSpacer";
|
||||
import RichTextEditor, {
|
||||
RichTextEditorChange
|
||||
} from "@saleor/components/RichTextEditor";
|
||||
import RichTextEditor from "@saleor/components/RichTextEditor";
|
||||
import { PageErrorFragment } from "@saleor/graphql";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { getFormErrors } from "@saleor/utils/errors";
|
||||
import getPageErrorMessage from "@saleor/utils/errors/page";
|
||||
import { useRichTextContext } from "@saleor/utils/richText/context";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
|
@ -19,7 +18,6 @@ export interface PageInfoProps {
|
|||
disabled: boolean;
|
||||
errors: PageErrorFragment[];
|
||||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
onContentChange: RichTextEditorChange;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
|
@ -32,11 +30,17 @@ const useStyles = makeStyles(
|
|||
);
|
||||
|
||||
const PageInfo: React.FC<PageInfoProps> = props => {
|
||||
const { data, disabled, errors, onChange, onContentChange } = props;
|
||||
const { data, disabled, errors, onChange } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
|
||||
const {
|
||||
defaultValue,
|
||||
editorRef,
|
||||
isReadyForMount,
|
||||
handleChange
|
||||
} = useRichTextContext();
|
||||
const formErrors = getFormErrors(["title", "content"], errors);
|
||||
|
||||
return (
|
||||
|
@ -60,8 +64,11 @@ const PageInfo: React.FC<PageInfoProps> = props => {
|
|||
onChange={onChange}
|
||||
/>
|
||||
<FormSpacer />
|
||||
{isReadyForMount && (
|
||||
<RichTextEditor
|
||||
data={data.content}
|
||||
defaultValue={defaultValue}
|
||||
editorRef={editorRef}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
error={!!formErrors.content}
|
||||
helperText={getPageErrorMessage(formErrors.content, intl)}
|
||||
|
@ -71,8 +78,8 @@ const PageInfo: React.FC<PageInfoProps> = props => {
|
|||
description: "page content"
|
||||
})}
|
||||
name={"content" as keyof PageData}
|
||||
onChange={onContentChange}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -206,7 +206,15 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
|
|||
assignReferencesAttributeId={assignReferencesAttributeId}
|
||||
loading={loading}
|
||||
>
|
||||
{({ change, data, formErrors, handlers, submit, isSaveDisabled }) => {
|
||||
{({
|
||||
change,
|
||||
data,
|
||||
formErrors,
|
||||
handlers,
|
||||
submit,
|
||||
isSaveDisabled,
|
||||
attributeRichTextGetters
|
||||
}) => {
|
||||
// Comparing explicitly to false because `hasVariants` can be undefined
|
||||
const isSimpleProduct = data.productType?.hasVariants === false;
|
||||
|
||||
|
@ -223,7 +231,6 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
|
|||
disabled={loading}
|
||||
errors={errors}
|
||||
onChange={change}
|
||||
onDescriptionChange={handlers.changeDescription}
|
||||
/>
|
||||
<CardSpacer />
|
||||
{data.attributes.length > 0 && (
|
||||
|
@ -242,6 +249,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
|
|||
fetchAttributeValues={fetchAttributeValues}
|
||||
fetchMoreAttributeValues={fetchMoreAttributeValues}
|
||||
onAttributeSelectBlur={onAttributeSelectBlur}
|
||||
richTextGetters={attributeRichTextGetters}
|
||||
/>
|
||||
)}
|
||||
<CardSpacer />
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { OutputData } from "@editorjs/editorjs";
|
||||
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
|
||||
import {
|
||||
getAttributesDisplayData,
|
||||
getRichTextAttributesFromMap,
|
||||
getRichTextDataFromAttributes,
|
||||
mergeAttributes,
|
||||
RichTextProps
|
||||
} from "@saleor/attributes/utils/data";
|
||||
import {
|
||||
createAttributeChangeHandler,
|
||||
createAttributeFileChangeHandler,
|
||||
|
@ -17,7 +23,6 @@ import {
|
|||
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
|
||||
import { MetadataFormData } from "@saleor/components/Metadata";
|
||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
|
||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||
import {
|
||||
ProductTypeQuery,
|
||||
|
@ -56,6 +61,8 @@ import { FetchMoreProps, RelayToFlat, ReorderEvent } from "@saleor/types";
|
|||
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
|
||||
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
|
||||
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
|
||||
import { RichTextContext } from "@saleor/utils/richText/context";
|
||||
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
|
||||
import useRichText from "@saleor/utils/richText/useRichText";
|
||||
import React, { useEffect } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
@ -119,20 +126,25 @@ export interface ProductCreateHandlers
|
|||
Record<"selectAttributeFile", FormsetChange<File>>,
|
||||
Record<"reorderAttributeValue", FormsetChange<ReorderEvent>>,
|
||||
Record<"addStock" | "deleteStock", (id: string) => void> {
|
||||
changeDescription: RichTextEditorChange;
|
||||
changePreorderEndDate: FormChange;
|
||||
fetchReferences: (value: string) => void;
|
||||
fetchMoreReferences: FetchMoreProps;
|
||||
}
|
||||
export interface UseProductCreateFormResult
|
||||
export interface UseProductCreateFormOutput
|
||||
extends CommonUseFormResultWithHandlers<
|
||||
ProductCreateData,
|
||||
ProductCreateHandlers
|
||||
> {
|
||||
>,
|
||||
RichTextProps {
|
||||
disabled: boolean;
|
||||
formErrors: FormErrors<ProductCreateData>;
|
||||
}
|
||||
|
||||
export type UseProductCreateFormRenderProps = Omit<
|
||||
UseProductCreateFormOutput,
|
||||
"richText"
|
||||
>;
|
||||
|
||||
export interface UseProductCreateFormOpts
|
||||
extends Record<
|
||||
"categories" | "collections" | "taxTypes",
|
||||
|
@ -160,7 +172,7 @@ export interface UseProductCreateFormOpts
|
|||
}
|
||||
|
||||
export interface ProductCreateFormProps extends UseProductCreateFormOpts {
|
||||
children: (props: UseProductCreateFormResult) => React.ReactNode;
|
||||
children: (props: UseProductCreateFormRenderProps) => React.ReactNode;
|
||||
initial?: Partial<ProductCreateFormData>;
|
||||
onSubmit: (data: ProductCreateData) => SubmitPromise;
|
||||
loading: boolean;
|
||||
|
@ -171,7 +183,7 @@ function useProductCreateForm(
|
|||
onSubmit: (data: ProductCreateData) => SubmitPromise,
|
||||
loading: boolean,
|
||||
opts: UseProductCreateFormOpts
|
||||
): UseProductCreateFormResult {
|
||||
): UseProductCreateFormOutput {
|
||||
const intl = useIntl();
|
||||
const defaultInitialFormData: ProductCreateFormData &
|
||||
Record<"productType", string> = {
|
||||
|
@ -224,9 +236,16 @@ function useProductCreateForm(
|
|||
? getAttributeInputFromProductType(opts.selectedProductType)
|
||||
: []
|
||||
);
|
||||
const {
|
||||
getters: attributeRichTextGetters,
|
||||
getValues: getAttributeRichTextValues
|
||||
} = useMultipleRichText({
|
||||
initial: getRichTextDataFromAttributes(attributes.data),
|
||||
triggerChange
|
||||
});
|
||||
const attributesWithNewFileValue = useFormset<null, File>([]);
|
||||
const stocks = useFormset<ProductStockFormsetData>([]);
|
||||
const [description, changeDescription] = useRichText({
|
||||
const richText = useRichText({
|
||||
initial: null,
|
||||
triggerChange
|
||||
});
|
||||
|
@ -329,7 +348,7 @@ function useProductCreateForm(
|
|||
intl.formatMessage(errorMessages.preorderEndDateInFutureErrorText)
|
||||
);
|
||||
|
||||
const getData = (): ProductCreateData => ({
|
||||
const data: ProductCreateData = {
|
||||
...formData,
|
||||
attributes: getAttributesDisplayData(
|
||||
attributes.data,
|
||||
|
@ -338,19 +357,29 @@ function useProductCreateForm(
|
|||
opts.referenceProducts
|
||||
),
|
||||
attributesWithNewFileValue: attributesWithNewFileValue.data,
|
||||
description: description.current,
|
||||
description: null,
|
||||
productType: opts.selectedProductType,
|
||||
stocks: stocks.data
|
||||
});
|
||||
};
|
||||
|
||||
const data = getData();
|
||||
const getData = async (): Promise<ProductCreateData> => ({
|
||||
...data,
|
||||
description: await richText.getValue(),
|
||||
attributes: mergeAttributes(
|
||||
attributes.data,
|
||||
getRichTextAttributesFromMap(
|
||||
attributes.data,
|
||||
await getAttributeRichTextValues()
|
||||
)
|
||||
)
|
||||
});
|
||||
|
||||
const handleFormSubmit = useHandleFormSubmit({
|
||||
formId,
|
||||
onSubmit
|
||||
});
|
||||
|
||||
const submit = () => handleFormSubmit(data);
|
||||
const submit = async () => handleFormSubmit(await getData());
|
||||
|
||||
const { setExitDialogSubmitRef, setIsSubmitDisabled } = useExitFormDialog({
|
||||
formId: PRODUCT_CREATE_FORM_ID
|
||||
|
@ -398,7 +427,6 @@ function useProductCreateForm(
|
|||
addStock: handleStockAdd,
|
||||
changeChannelPrice: handleChannelPriceChange,
|
||||
changeChannels: handleChannelsChange,
|
||||
changeDescription,
|
||||
changeMetadata,
|
||||
changeStock: handleStockChange,
|
||||
changePreorderEndDate: handlePreorderEndDateChange,
|
||||
|
@ -416,7 +444,9 @@ function useProductCreateForm(
|
|||
selectTaxRate: handleTaxTypeSelect
|
||||
},
|
||||
submit,
|
||||
isSaveDisabled
|
||||
isSaveDisabled,
|
||||
richText,
|
||||
attributeRichTextGetters
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -427,9 +457,20 @@ const ProductCreateForm: React.FC<ProductCreateFormProps> = ({
|
|||
loading,
|
||||
...rest
|
||||
}) => {
|
||||
const props = useProductCreateForm(initial || {}, onSubmit, loading, rest);
|
||||
const { richText, ...props } = useProductCreateForm(
|
||||
initial || {},
|
||||
onSubmit,
|
||||
loading,
|
||||
rest
|
||||
);
|
||||
|
||||
return <form onSubmit={props.submit}>{children(props)}</form>;
|
||||
return (
|
||||
<form onSubmit={props.submit}>
|
||||
<RichTextContext.Provider value={richText}>
|
||||
{children(props)}
|
||||
</RichTextContext.Provider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
ProductCreateForm.displayName = "ProductCreateForm";
|
||||
|
|
|
@ -4,12 +4,11 @@ import CardTitle from "@saleor/components/CardTitle";
|
|||
import FormSpacer from "@saleor/components/FormSpacer";
|
||||
import Grid from "@saleor/components/Grid";
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import RichTextEditor, {
|
||||
RichTextEditorChange
|
||||
} from "@saleor/components/RichTextEditor";
|
||||
import RichTextEditor from "@saleor/components/RichTextEditor";
|
||||
import { ProductErrorFragment } from "@saleor/graphql";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
|
||||
import { useRichTextContext } from "@saleor/utils/richText/context";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
|
@ -22,7 +21,6 @@ interface ProductDetailsFormProps {
|
|||
disabled?: boolean;
|
||||
errors: ProductErrorFragment[];
|
||||
|
||||
onDescriptionChange: RichTextEditorChange;
|
||||
onChange(event: any);
|
||||
}
|
||||
|
||||
|
@ -30,13 +28,17 @@ export const ProductDetailsForm: React.FC<ProductDetailsFormProps> = ({
|
|||
data,
|
||||
disabled,
|
||||
errors,
|
||||
onDescriptionChange,
|
||||
onChange
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
editorRef,
|
||||
defaultValue,
|
||||
isReadyForMount,
|
||||
handleChange
|
||||
} = useRichTextContext();
|
||||
|
||||
const formErrors = getFormErrors(["name", "description", "rating"], errors);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle
|
||||
|
@ -58,15 +60,18 @@ export const ProductDetailsForm: React.FC<ProductDetailsFormProps> = ({
|
|||
onChange={onChange}
|
||||
/>
|
||||
<FormSpacer />
|
||||
{isReadyForMount && (
|
||||
<RichTextEditor
|
||||
data={data.description}
|
||||
editorRef={editorRef}
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
error={!!formErrors.description}
|
||||
helperText={getProductErrorMessage(formErrors.description, intl)}
|
||||
label={intl.formatMessage(commonMessages.description)}
|
||||
name="description"
|
||||
onChange={onDescriptionChange}
|
||||
/>
|
||||
)}
|
||||
<FormSpacer />
|
||||
<Hr />
|
||||
<FormSpacer />
|
||||
|
|
|
@ -17,12 +17,15 @@ const channels = createChannelsData(channelsList);
|
|||
|
||||
import * as _useNavigator from "@saleor/hooks/useNavigator";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
const onSubmit = jest.fn();
|
||||
const useNavigator = jest.spyOn(_useNavigator, "default");
|
||||
jest.mock("@saleor/components/RichTextEditor/RichTextEditor");
|
||||
jest.mock("@saleor/utils/richText/useRichText");
|
||||
|
||||
(global as any).document.createRange = () => ({
|
||||
// eslint-disable-next-line
|
||||
|
@ -94,7 +97,7 @@ const selectors = {
|
|||
|
||||
describe("Product details page", () => {
|
||||
useNavigator.mockImplementation();
|
||||
it("can select empty option on attribute", () => {
|
||||
it("can select empty option on attribute", async () => {
|
||||
const component = mount(
|
||||
<MemoryRouter>
|
||||
<Wrapper>
|
||||
|
@ -124,10 +127,15 @@ describe("Product details page", () => {
|
|||
.first()
|
||||
.prop("value")
|
||||
).toEqual("");
|
||||
|
||||
await act(async () => {
|
||||
component
|
||||
.find("form")
|
||||
.first()
|
||||
.simulate("submit");
|
||||
// wait for async function to complete
|
||||
await new Promise(process.nextTick);
|
||||
});
|
||||
expect(onSubmit.mock.calls[0][0].attributes[0].value.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -282,7 +282,15 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
|||
assignReferencesAttributeId={assignReferencesAttributeId}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ change, data, formErrors, handlers, submit, isSaveDisabled }) => (
|
||||
{({
|
||||
change,
|
||||
data,
|
||||
formErrors,
|
||||
handlers,
|
||||
submit,
|
||||
isSaveDisabled,
|
||||
attributeRichTextGetters
|
||||
}) => (
|
||||
<>
|
||||
<Container>
|
||||
<Backlink href={productListUrl()}>
|
||||
|
@ -293,13 +301,12 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
|||
<CardMenu menuItems={extensionMenuItems} data-test-id="menu" />
|
||||
)}
|
||||
</PageHeader>
|
||||
<Grid>
|
||||
<Grid richText>
|
||||
<div>
|
||||
<ProductDetailsForm
|
||||
data={data}
|
||||
disabled={disabled}
|
||||
errors={errors}
|
||||
onDescriptionChange={handlers.changeDescription}
|
||||
onChange={change}
|
||||
/>
|
||||
<CardSpacer />
|
||||
|
@ -331,6 +338,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
|||
fetchAttributeValues={fetchAttributeValues}
|
||||
fetchMoreAttributeValues={fetchMoreAttributeValues}
|
||||
onAttributeSelectBlur={onAttributeSelectBlur}
|
||||
richTextGetters={attributeRichTextGetters}
|
||||
/>
|
||||
)}
|
||||
<CardSpacer />
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { OutputData } from "@editorjs/editorjs";
|
||||
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
|
||||
import {
|
||||
getAttributesDisplayData,
|
||||
getRichTextAttributesFromMap,
|
||||
getRichTextDataFromAttributes,
|
||||
mergeAttributes,
|
||||
RichTextProps
|
||||
} from "@saleor/attributes/utils/data";
|
||||
import {
|
||||
createAttributeChangeHandler,
|
||||
createAttributeFileChangeHandler,
|
||||
|
@ -18,7 +24,6 @@ import { AttributeInput } from "@saleor/components/Attributes";
|
|||
import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog";
|
||||
import { MetadataFormData } from "@saleor/components/Metadata";
|
||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
|
||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||
import {
|
||||
ProductFragment,
|
||||
|
@ -62,6 +67,8 @@ import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAu
|
|||
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
|
||||
import getMetadata from "@saleor/utils/metadata/getMetadata";
|
||||
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
|
||||
import { RichTextContext } from "@saleor/utils/richText/context";
|
||||
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
|
||||
import useRichText from "@saleor/utils/richText/useRichText";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
@ -147,19 +154,25 @@ export interface ProductUpdateHandlers
|
|||
Record<"selectAttributeFile", FormsetChange<File>>,
|
||||
Record<"reorderAttributeValue", FormsetChange<ReorderEvent>>,
|
||||
Record<"addStock" | "deleteStock", (id: string) => void> {
|
||||
changeDescription: RichTextEditorChange;
|
||||
changePreorderEndDate: FormChange;
|
||||
fetchReferences: (value: string) => void;
|
||||
fetchMoreReferences: FetchMoreProps;
|
||||
}
|
||||
export interface UseProductUpdateFormResult
|
||||
|
||||
export interface UseProductUpdateFormOutput
|
||||
extends CommonUseFormResultWithHandlers<
|
||||
ProductUpdateData,
|
||||
ProductUpdateHandlers
|
||||
> {
|
||||
>,
|
||||
RichTextProps {
|
||||
formErrors: FormErrors<ProductUpdateSubmitData>;
|
||||
}
|
||||
|
||||
export type UseProductUpdateFormRenderProps = Omit<
|
||||
UseProductUpdateFormOutput,
|
||||
"richText"
|
||||
>;
|
||||
|
||||
export interface UseProductUpdateFormOpts
|
||||
extends Record<
|
||||
"categories" | "collections" | "taxTypes",
|
||||
|
@ -189,7 +202,7 @@ export interface UseProductUpdateFormOpts
|
|||
}
|
||||
|
||||
export interface ProductUpdateFormProps extends UseProductUpdateFormOpts {
|
||||
children: (props: UseProductUpdateFormResult) => React.ReactNode;
|
||||
children: (props: UseProductUpdateFormRenderProps) => React.ReactNode;
|
||||
product: ProductFragment;
|
||||
onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise;
|
||||
disabled: boolean;
|
||||
|
@ -224,7 +237,7 @@ function useProductUpdateForm(
|
|||
onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise,
|
||||
disabled: boolean,
|
||||
opts: UseProductUpdateFormOpts
|
||||
): UseProductUpdateFormResult {
|
||||
): UseProductUpdateFormOutput {
|
||||
const intl = useIntl();
|
||||
const initial = useMemo(
|
||||
() =>
|
||||
|
@ -257,9 +270,16 @@ function useProductUpdateForm(
|
|||
} = form;
|
||||
|
||||
const attributes = useFormset(getAttributeInputFromProduct(product));
|
||||
const {
|
||||
getters: attributeRichTextGetters,
|
||||
getValues: getAttributeRichTextValues
|
||||
} = useMultipleRichText({
|
||||
initial: getRichTextDataFromAttributes(attributes.data),
|
||||
triggerChange
|
||||
});
|
||||
const attributesWithNewFileValue = useFormset<null, File>([]);
|
||||
const stocks = useFormset(getStockInputFromProduct(product));
|
||||
const [description, changeDescription] = useRichText({
|
||||
const richText = useRichText({
|
||||
initial: product?.description,
|
||||
triggerChange
|
||||
});
|
||||
|
@ -382,18 +402,23 @@ function useProductUpdateForm(
|
|||
opts.referencePages,
|
||||
opts.referenceProducts
|
||||
),
|
||||
description: description.current,
|
||||
description: null,
|
||||
stocks: stocks.data
|
||||
};
|
||||
|
||||
// Need to make it function to always have description.current up to date
|
||||
const getSubmitData = (): ProductUpdateSubmitData => ({
|
||||
const getSubmitData = async (): Promise<ProductUpdateSubmitData> => ({
|
||||
...data,
|
||||
...getStocksData(product, stocks.data),
|
||||
...getMetadata(data, isMetadataModified, isPrivateMetadataModified),
|
||||
attributes: attributes.data,
|
||||
attributes: mergeAttributes(
|
||||
attributes.data,
|
||||
getRichTextAttributesFromMap(
|
||||
attributes.data,
|
||||
await getAttributeRichTextValues()
|
||||
)
|
||||
),
|
||||
attributesWithNewFileValue: attributesWithNewFileValue.data,
|
||||
description: description.current
|
||||
description: await richText.getValue()
|
||||
});
|
||||
|
||||
const handleSubmit = async (data: ProductUpdateSubmitData) => {
|
||||
|
@ -411,7 +436,7 @@ function useProductUpdateForm(
|
|||
onSubmit: handleSubmit
|
||||
});
|
||||
|
||||
const submit = async () => handleFormSubmit(getSubmitData());
|
||||
const submit = async () => handleFormSubmit(await getSubmitData());
|
||||
|
||||
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
|
||||
|
||||
|
@ -458,7 +483,6 @@ function useProductUpdateForm(
|
|||
changeChannelPrice: handleChannelPriceChange,
|
||||
changeChannelPreorder: handleChannelPreorderChange,
|
||||
changeChannels: handleChannelsChange,
|
||||
changeDescription,
|
||||
changeMetadata,
|
||||
changeStock: handleStockChange,
|
||||
changePreorderEndDate: handlePreorderEndDateChange,
|
||||
|
@ -475,7 +499,9 @@ function useProductUpdateForm(
|
|||
selectTaxRate: handleTaxTypeSelect
|
||||
},
|
||||
submit,
|
||||
isSaveDisabled
|
||||
isSaveDisabled,
|
||||
richText,
|
||||
attributeRichTextGetters
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -486,9 +512,20 @@ const ProductUpdateForm: React.FC<ProductUpdateFormProps> = ({
|
|||
disabled,
|
||||
...rest
|
||||
}) => {
|
||||
const props = useProductUpdateForm(product, onSubmit, disabled, rest);
|
||||
const { richText, ...props } = useProductUpdateForm(
|
||||
product,
|
||||
onSubmit,
|
||||
disabled,
|
||||
rest
|
||||
);
|
||||
|
||||
return <form onSubmit={props.submit}>{children(props)}</form>;
|
||||
return (
|
||||
<form onSubmit={props.submit}>
|
||||
<RichTextContext.Provider value={richText}>
|
||||
{children(props)}
|
||||
</RichTextContext.Provider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
ProductUpdateForm.displayName = "ProductUpdateForm";
|
||||
|
|
|
@ -159,7 +159,15 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
|
|||
assignReferencesAttributeId={assignReferencesAttributeId}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ change, data, formErrors, handlers, submit, isSaveDisabled }) => (
|
||||
{({
|
||||
change,
|
||||
data,
|
||||
formErrors,
|
||||
handlers,
|
||||
submit,
|
||||
isSaveDisabled,
|
||||
attributeRichTextGetters
|
||||
}) => (
|
||||
<Container>
|
||||
<Backlink href={productUrl(productId)}>{product?.name}</Backlink>
|
||||
<PageHeader title={header} />
|
||||
|
@ -193,6 +201,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
|
|||
fetchAttributeValues={fetchAttributeValues}
|
||||
fetchMoreAttributeValues={fetchMoreAttributeValues}
|
||||
onAttributeSelectBlur={onAttributeSelectBlur}
|
||||
richTextGetters={attributeRichTextGetters}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<Attributes
|
||||
|
@ -215,6 +224,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
|
|||
fetchAttributeValues={fetchAttributeValues}
|
||||
fetchMoreAttributeValues={fetchMoreAttributeValues}
|
||||
onAttributeSelectBlur={onAttributeSelectBlur}
|
||||
richTextGetters={attributeRichTextGetters}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<ProductVariantCheckoutSettings
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
|
||||
import {
|
||||
getAttributesDisplayData,
|
||||
getRichTextAttributesFromMap,
|
||||
getRichTextDataFromAttributes,
|
||||
mergeAttributes,
|
||||
RichTextProps
|
||||
} from "@saleor/attributes/utils/data";
|
||||
import {
|
||||
createAttributeChangeHandler,
|
||||
createAttributeFileChangeHandler,
|
||||
|
@ -32,6 +38,7 @@ import { getVariantAttributeInputFromProduct } from "@saleor/products/utils/data
|
|||
import { createPreorderEndDateChangeHandler } from "@saleor/products/utils/handlers";
|
||||
import { FetchMoreProps, RelayToFlat, ReorderEvent } from "@saleor/types";
|
||||
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
|
||||
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
|
||||
import React, { useEffect } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
|
@ -80,18 +87,19 @@ export interface ProductVariantCreateHandlers
|
|||
fetchMoreReferences: FetchMoreProps;
|
||||
}
|
||||
|
||||
export interface UseProductVariantCreateFormResult
|
||||
export interface UseProductVariantCreateFormOutput
|
||||
extends CommonUseFormResultWithHandlers<
|
||||
ProductVariantCreateData,
|
||||
ProductVariantCreateHandlers
|
||||
> {
|
||||
>,
|
||||
Omit<RichTextProps, "richText"> {
|
||||
formErrors: FormErrors<ProductVariantCreateData>;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export interface ProductVariantCreateFormProps
|
||||
extends UseProductVariantCreateFormOpts {
|
||||
children: (props: UseProductVariantCreateFormResult) => React.ReactNode;
|
||||
children: (props: UseProductVariantCreateFormOutput) => React.ReactNode;
|
||||
product: ProductVariantCreateDataQuery["product"];
|
||||
onSubmit: (data: ProductVariantCreateData) => void;
|
||||
disabled: boolean;
|
||||
|
@ -116,7 +124,7 @@ function useProductVariantCreateForm(
|
|||
onSubmit: (data: ProductVariantCreateData) => void,
|
||||
disabled: boolean,
|
||||
opts: UseProductVariantCreateFormOpts
|
||||
): UseProductVariantCreateFormResult {
|
||||
): UseProductVariantCreateFormOutput {
|
||||
const intl = useIntl();
|
||||
const attributeInput = getVariantAttributeInputFromProduct(product);
|
||||
|
||||
|
@ -131,6 +139,13 @@ function useProductVariantCreateForm(
|
|||
} = form;
|
||||
|
||||
const attributes = useFormset(attributeInput);
|
||||
const {
|
||||
getters: attributeRichTextGetters,
|
||||
getValues: getAttributeRichTextValues
|
||||
} = useMultipleRichText({
|
||||
initial: getRichTextDataFromAttributes(attributes.data),
|
||||
triggerChange
|
||||
});
|
||||
const attributesWithNewFileValue = useFormset<null, File>([]);
|
||||
const stocks = useFormset<ProductStockFormsetData, string>([]);
|
||||
|
||||
|
@ -218,12 +233,23 @@ function useProductVariantCreateForm(
|
|||
stocks: stocks.data
|
||||
};
|
||||
|
||||
const getSubmitData = async (): Promise<ProductVariantCreateData> => ({
|
||||
...data,
|
||||
attributes: mergeAttributes(
|
||||
attributes.data,
|
||||
getRichTextAttributesFromMap(
|
||||
attributes.data,
|
||||
await getAttributeRichTextValues()
|
||||
)
|
||||
)
|
||||
});
|
||||
|
||||
const handleFormSubmit = useHandleFormSubmit({
|
||||
formId,
|
||||
onSubmit
|
||||
});
|
||||
|
||||
const submit = () => handleFormSubmit(data);
|
||||
const submit = async () => handleFormSubmit(await getSubmitData());
|
||||
|
||||
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
|
||||
|
||||
|
@ -255,7 +281,8 @@ function useProductVariantCreateForm(
|
|||
selectAttributeReference: handleAttributeReferenceChange
|
||||
},
|
||||
submit,
|
||||
isSaveDisabled
|
||||
isSaveDisabled,
|
||||
attributeRichTextGetters
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -224,7 +224,15 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
|
|||
assignReferencesAttributeId={assignReferencesAttributeId}
|
||||
loading={loading}
|
||||
>
|
||||
{({ change, data, formErrors, isSaveDisabled, handlers, submit }) => {
|
||||
{({
|
||||
change,
|
||||
data,
|
||||
formErrors,
|
||||
isSaveDisabled,
|
||||
handlers,
|
||||
submit,
|
||||
attributeRichTextGetters
|
||||
}) => {
|
||||
const nonSelectionAttributes = data.attributes.filter(
|
||||
byAttributeScope(VariantAttributeScope.NOT_VARIANT_SELECTION)
|
||||
);
|
||||
|
@ -250,7 +258,6 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
|
|||
{nonSelectionAttributes.length > 0 && (
|
||||
<>
|
||||
<Attributes
|
||||
entityId={variant?.id}
|
||||
title={intl.formatMessage(
|
||||
messages.nonSelectionAttributes
|
||||
)}
|
||||
|
@ -268,6 +275,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
|
|||
fetchAttributeValues={fetchAttributeValues}
|
||||
fetchMoreAttributeValues={fetchMoreAttributeValues}
|
||||
onAttributeSelectBlur={onAttributeSelectBlur}
|
||||
richTextGetters={attributeRichTextGetters}
|
||||
/>
|
||||
<CardSpacer />
|
||||
</>
|
||||
|
@ -275,7 +283,6 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
|
|||
{selectionAttributes.length > 0 && (
|
||||
<>
|
||||
<Attributes
|
||||
entityId={variant?.id}
|
||||
title={intl.formatMessage(
|
||||
messages.selectionAttributesHeader
|
||||
)}
|
||||
|
@ -293,6 +300,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
|
|||
fetchAttributeValues={fetchAttributeValues}
|
||||
fetchMoreAttributeValues={fetchMoreAttributeValues}
|
||||
onAttributeSelectBlur={onAttributeSelectBlur}
|
||||
richTextGetters={attributeRichTextGetters}
|
||||
/>
|
||||
<CardSpacer />
|
||||
</>
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
|
||||
import {
|
||||
getAttributesDisplayData,
|
||||
getRichTextAttributesFromMap,
|
||||
getRichTextDataFromAttributes,
|
||||
mergeAttributes,
|
||||
RichTextProps
|
||||
} from "@saleor/attributes/utils/data";
|
||||
import {
|
||||
createAttributeChangeHandler,
|
||||
createAttributeFileChangeHandler,
|
||||
|
@ -50,6 +56,7 @@ import { arrayDiff } from "@saleor/utils/arrays";
|
|||
import { mapMetadataItemToInput } from "@saleor/utils/maps";
|
||||
import getMetadata from "@saleor/utils/metadata/getMetadata";
|
||||
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
|
||||
import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText";
|
||||
import React, { useEffect } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
|
@ -121,7 +128,8 @@ export interface UseProductVariantUpdateFormResult
|
|||
extends CommonUseFormResultWithHandlers<
|
||||
ProductVariantUpdateData,
|
||||
ProductVariantUpdateHandlers
|
||||
> {
|
||||
>,
|
||||
Omit<RichTextProps, "richText"> {
|
||||
formErrors: FormErrors<ProductVariantUpdateData>;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
@ -189,6 +197,13 @@ function useProductVariantUpdateForm(
|
|||
});
|
||||
|
||||
const attributes = useFormset(attributeInput);
|
||||
const {
|
||||
getters: attributeRichTextGetters,
|
||||
getValues: getAttributeRichTextValues
|
||||
} = useMultipleRichText({
|
||||
initial: getRichTextDataFromAttributes(attributes.data),
|
||||
triggerChange
|
||||
});
|
||||
const attributesWithNewFileValue = useFormset<null, File>([]);
|
||||
const stocks = useFormset(stockInput);
|
||||
const channels = useFormset(channelsInput);
|
||||
|
@ -302,16 +317,22 @@ function useProductVariantUpdateForm(
|
|||
data.hasPreorderEndDate &&
|
||||
!!form.errors.preorderEndDateTime);
|
||||
|
||||
const submitData: ProductVariantUpdateSubmitData = {
|
||||
const getSubmitData = async (): Promise<ProductVariantUpdateSubmitData> => ({
|
||||
...formData,
|
||||
...getMetadata(formData, isMetadataModified, isPrivateMetadataModified),
|
||||
addStocks,
|
||||
attributes: attributes.data,
|
||||
attributes: mergeAttributes(
|
||||
attributes.data,
|
||||
getRichTextAttributesFromMap(
|
||||
attributes.data,
|
||||
await getAttributeRichTextValues()
|
||||
)
|
||||
),
|
||||
attributesWithNewFileValue: attributesWithNewFileValue.data,
|
||||
channelListings: channels.data,
|
||||
removeStocks: stockDiff.removed,
|
||||
updateStocks
|
||||
};
|
||||
});
|
||||
|
||||
const handleSubmit = async (data: ProductVariantUpdateSubmitData) => {
|
||||
const errors = await onSubmit(data);
|
||||
|
@ -328,7 +349,7 @@ function useProductVariantUpdateForm(
|
|||
onSubmit: handleSubmit
|
||||
});
|
||||
|
||||
const submit = () => handleFormSubmit(submitData);
|
||||
const submit = async () => handleFormSubmit(await getSubmitData());
|
||||
|
||||
useEffect(() => setExitDialogSubmitRef(submit), [submit]);
|
||||
|
||||
|
@ -356,7 +377,8 @@ function useProductVariantUpdateForm(
|
|||
selectAttributeReference: handleAttributeReferenceChange
|
||||
},
|
||||
submit,
|
||||
isSaveDisabled
|
||||
isSaveDisabled,
|
||||
attributeRichTextGetters
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,13 @@ import { OutputData } from "@editorjs/editorjs";
|
|||
import { Card, CardContent, TextField } from "@material-ui/core";
|
||||
import CardSpacer from "@saleor/components/CardSpacer";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import RichTextEditor, {
|
||||
RichTextEditorChange
|
||||
} from "@saleor/components/RichTextEditor";
|
||||
import RichTextEditor from "@saleor/components/RichTextEditor";
|
||||
import { ShippingErrorFragment } from "@saleor/graphql";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { getFormErrors } from "@saleor/utils/errors";
|
||||
import getShippingErrorMessage from "@saleor/utils/errors/shipping";
|
||||
import { useRichTextContext } from "@saleor/utils/richText/context";
|
||||
import React from "react";
|
||||
import { defineMessages, useIntl } from "react-intl";
|
||||
|
||||
|
@ -64,15 +63,21 @@ export interface ShippingRateInfoProps {
|
|||
disabled: boolean;
|
||||
errors: ShippingErrorFragment[];
|
||||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
onDescriptionChange: RichTextEditorChange;
|
||||
}
|
||||
|
||||
const ShippingRateInfo: React.FC<ShippingRateInfoProps> = props => {
|
||||
const { data, disabled, errors, onChange, onDescriptionChange } = props;
|
||||
const { data, disabled, errors, onChange } = props;
|
||||
|
||||
const intl = useIntl();
|
||||
const classes = useStyles(props);
|
||||
|
||||
const {
|
||||
defaultValue,
|
||||
editorRef,
|
||||
isReadyForMount,
|
||||
handleChange
|
||||
} = useRichTextContext();
|
||||
|
||||
const formErrors = getFormErrors(
|
||||
["name", "description", "minDays", "maxDays"],
|
||||
errors
|
||||
|
@ -95,15 +100,18 @@ const ShippingRateInfo: React.FC<ShippingRateInfoProps> = props => {
|
|||
onChange={onChange}
|
||||
/>
|
||||
<CardSpacer />
|
||||
{isReadyForMount && (
|
||||
<RichTextEditor
|
||||
data={data.description}
|
||||
defaultValue={defaultValue}
|
||||
editorRef={editorRef}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
error={!!formErrors.description}
|
||||
helperText={getShippingErrorMessage(formErrors.description, intl)}
|
||||
label={intl.formatMessage(messages.description)}
|
||||
name="description"
|
||||
onChange={onDescriptionChange}
|
||||
/>
|
||||
)}
|
||||
<CardSpacer />
|
||||
<div className={classes.deliveryTimeFields}>
|
||||
<TextField
|
||||
|
|
|
@ -25,6 +25,7 @@ import OrderWeight from "@saleor/shipping/components/OrderWeight";
|
|||
import PricingCard from "@saleor/shipping/components/PricingCard";
|
||||
import ShippingRateInfo from "@saleor/shipping/components/ShippingRateInfo";
|
||||
import { createChannelsChangeHandler } from "@saleor/shipping/handlers";
|
||||
import { RichTextContext } from "@saleor/utils/richText/context";
|
||||
import useRichText from "@saleor/utils/richText/useRichText";
|
||||
import React, { FormEventHandler } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
@ -100,23 +101,27 @@ export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePagePr
|
|||
onSubmit
|
||||
});
|
||||
|
||||
const [description, changeDescription] = useRichText({
|
||||
const richText = useRichText({
|
||||
initial: null,
|
||||
triggerChange
|
||||
});
|
||||
|
||||
// Prevents closing ref in submit functions
|
||||
const getData = () => ({
|
||||
const data: ShippingZoneRateCommonFormData = {
|
||||
...formData,
|
||||
description: description.current
|
||||
});
|
||||
const data = getData();
|
||||
|
||||
const handleFormElementSubmit: FormEventHandler = event => {
|
||||
event.preventDefault();
|
||||
handleFormSubmit(getData());
|
||||
description: null
|
||||
};
|
||||
const handleSubmit = () => handleFormSubmit(getData());
|
||||
|
||||
const getData = async (): Promise<ShippingZoneRateCommonFormData> => ({
|
||||
...formData,
|
||||
description: await richText.getValue()
|
||||
});
|
||||
|
||||
const handleFormElementSubmit: FormEventHandler = async event => {
|
||||
event.preventDefault();
|
||||
handleFormSubmit(await getData());
|
||||
};
|
||||
|
||||
const handleSubmit = async () => handleFormSubmit(await getData());
|
||||
|
||||
const handleChannelsChange = createChannelsChangeHandler(
|
||||
shippingChannels,
|
||||
|
@ -130,6 +135,7 @@ export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePagePr
|
|||
setIsSubmitDisabled(isSaveDisabled);
|
||||
|
||||
return (
|
||||
<RichTextContext.Provider value={richText}>
|
||||
<form onSubmit={handleFormElementSubmit}>
|
||||
<Container>
|
||||
<Backlink href={backUrl}>
|
||||
|
@ -157,7 +163,6 @@ export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePagePr
|
|||
disabled={disabled}
|
||||
errors={errors}
|
||||
onChange={change}
|
||||
onDescriptionChange={changeDescription}
|
||||
/>
|
||||
<CardSpacer />
|
||||
{isPriceVariant ? (
|
||||
|
@ -214,6 +219,7 @@ export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePagePr
|
|||
/>
|
||||
</Container>
|
||||
</form>
|
||||
</RichTextContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import { createChannelsChangeHandler } from "@saleor/shipping/handlers";
|
|||
import { ListActions, ListProps } from "@saleor/types";
|
||||
import { mapEdgesToItems, mapMetadataItemToInput } from "@saleor/utils/maps";
|
||||
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
|
||||
import { RichTextContext } from "@saleor/utils/richText/context";
|
||||
import useRichText from "@saleor/utils/richText/useRichText";
|
||||
import React, { FormEventHandler } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
@ -127,7 +128,7 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
|
|||
onSubmit
|
||||
});
|
||||
|
||||
const [description, changeDescription] = useRichText({
|
||||
const richText = useRichText({
|
||||
initial: rate?.description,
|
||||
triggerChange
|
||||
});
|
||||
|
@ -136,18 +137,22 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
|
|||
makeChangeHandler: makeMetadataChangeHandler
|
||||
} = useMetadataChangeTrigger();
|
||||
|
||||
// Prevents closing ref in submit functions
|
||||
const getData = () => ({
|
||||
const data: ShippingZoneRateUpdateFormData = {
|
||||
...formData,
|
||||
description: description.current
|
||||
});
|
||||
const data = getData();
|
||||
|
||||
const handleFormElementSubmit: FormEventHandler = event => {
|
||||
event.preventDefault();
|
||||
handleFormSubmit(getData());
|
||||
description: null
|
||||
};
|
||||
const handleSubmit = () => handleFormSubmit(getData());
|
||||
|
||||
// Prevents closing ref in submit functions
|
||||
const getData = async (): Promise<ShippingZoneRateUpdateFormData> => ({
|
||||
...data,
|
||||
description: await richText.getValue()
|
||||
});
|
||||
|
||||
const handleFormElementSubmit: FormEventHandler = async event => {
|
||||
event.preventDefault();
|
||||
handleFormSubmit(await getData());
|
||||
};
|
||||
const handleSubmit = async () => handleFormSubmit(await getData());
|
||||
|
||||
const handleChannelsChange = createChannelsChangeHandler(
|
||||
shippingChannels,
|
||||
|
@ -164,6 +169,7 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
|
|||
setIsSubmitDisabled(isSaveDisabled);
|
||||
|
||||
return (
|
||||
<RichTextContext.Provider value={richText}>
|
||||
<form onSubmit={handleFormElementSubmit}>
|
||||
<Container>
|
||||
<Backlink href={backHref}>
|
||||
|
@ -177,7 +183,6 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
|
|||
disabled={disabled}
|
||||
errors={errors}
|
||||
onChange={change}
|
||||
onDescriptionChange={changeDescription}
|
||||
/>
|
||||
<CardSpacer />
|
||||
{isPriceVariant ? (
|
||||
|
@ -247,6 +252,7 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
|
|||
/>
|
||||
</Container>
|
||||
</form>
|
||||
</RichTextContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -376,9 +376,6 @@ exports[`Storyshots Attributes / Attributes default 1`] = `
|
|||
>
|
||||
Value
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -1104,9 +1101,6 @@ exports[`Storyshots Attributes / Attributes disabled 1`] = `
|
|||
>
|
||||
Value
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
|
||||
/>
|
||||
|
@ -2036,9 +2030,6 @@ exports[`Storyshots Attributes / Attributes selected 1`] = `
|
|||
>
|
||||
Value
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -13612,9 +13603,6 @@ exports[`Storyshots Generics / Rich text editor default 1`] = `
|
|||
>
|
||||
Content
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
>
|
||||
|
@ -13647,9 +13635,6 @@ exports[`Storyshots Generics / Rich text editor disabled 1`] = `
|
|||
>
|
||||
Content
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
|
||||
>
|
||||
|
@ -13682,9 +13667,6 @@ exports[`Storyshots Generics / Rich text editor error 1`] = `
|
|||
>
|
||||
Content
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootError-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-error-id"
|
||||
>
|
||||
|
@ -13707,9 +13689,7 @@ exports[`Storyshots Generics / Rich text editor static 1`] = `
|
|||
<div
|
||||
class="MuiCardContent-root-id"
|
||||
>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-rootStatic-id"
|
||||
/>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -21075,9 +21055,6 @@ exports[`Storyshots Shipping / ShippingZoneRatesCreatePage page create price 1`]
|
|||
>
|
||||
Shipping Rate Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -21988,9 +21965,6 @@ exports[`Storyshots Shipping / ShippingZoneRatesCreatePage page create weight 1`
|
|||
>
|
||||
Shipping Rate Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -22911,9 +22885,6 @@ exports[`Storyshots Shipping / ShippingZoneRatesCreatePage page loading 1`] = `
|
|||
>
|
||||
Shipping Rate Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
|
||||
/>
|
||||
|
@ -39305,9 +39276,6 @@ exports[`Storyshots Views / Categories / Create category When loading 1`] = `
|
|||
>
|
||||
Category Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
|
||||
/>
|
||||
|
@ -39620,9 +39588,6 @@ exports[`Storyshots Views / Categories / Create category default 1`] = `
|
|||
>
|
||||
Category Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -39940,9 +39905,6 @@ exports[`Storyshots Views / Categories / Create category form errors 1`] = `
|
|||
>
|
||||
Category Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootError-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-error-id"
|
||||
>
|
||||
|
@ -40256,9 +40218,6 @@ exports[`Storyshots Views / Categories / Update category default 1`] = `
|
|||
>
|
||||
Category Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -41031,9 +40990,6 @@ exports[`Storyshots Views / Categories / Update category form errors 1`] = `
|
|||
>
|
||||
Category Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootError-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-error-id"
|
||||
>
|
||||
|
@ -41800,23 +41756,6 @@ exports[`Storyshots Views / Categories / Update category loading 1`] = `
|
|||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
|
||||
data-test-id="rich-text-editor-description"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
|
||||
data-shrink="true"
|
||||
>
|
||||
Category Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -42439,9 +42378,6 @@ exports[`Storyshots Views / Categories / Update category no background 1`] = `
|
|||
>
|
||||
Category Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -43151,9 +43087,6 @@ exports[`Storyshots Views / Categories / Update category no products 1`] = `
|
|||
>
|
||||
Category Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -43923,9 +43856,6 @@ exports[`Storyshots Views / Categories / Update category no subcategories 1`] =
|
|||
>
|
||||
Category Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -44693,9 +44623,6 @@ exports[`Storyshots Views / Categories / Update category products 1`] = `
|
|||
>
|
||||
Category Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -51307,9 +51234,6 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details d
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -52797,9 +52721,6 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details f
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootError-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-error-id"
|
||||
>
|
||||
|
@ -54281,23 +54202,6 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details l
|
|||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
|
||||
data-test-id="rich-text-editor-description"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
|
||||
data-shrink="true"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -55188,9 +55092,6 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details n
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -57678,9 +57579,6 @@ exports[`Storyshots Views / Collections / Create collection default 1`] = `
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -59581,9 +59479,6 @@ exports[`Storyshots Views / Collections / Create collection form errors 1`] = `
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootError-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-error-id"
|
||||
>
|
||||
|
@ -61482,9 +61377,6 @@ exports[`Storyshots Views / Collections / Create collection loading 1`] = `
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
|
||||
/>
|
||||
|
@ -154092,9 +153984,6 @@ exports[`Storyshots Views / Pages / Page details default 1`] = `
|
|||
>
|
||||
Content
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -154986,9 +154875,6 @@ exports[`Storyshots Views / Pages / Page details form errors 1`] = `
|
|||
>
|
||||
Content
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootError-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-error-id"
|
||||
>
|
||||
|
@ -155882,23 +155768,6 @@ exports[`Storyshots Views / Pages / Page details loading 1`] = `
|
|||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
|
||||
data-test-id="rich-text-editor-content"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
|
||||
data-shrink="true"
|
||||
>
|
||||
Content
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -180985,9 +180854,6 @@ exports[`Storyshots Views / Products / Create product When loading 1`] = `
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
|
||||
/>
|
||||
|
@ -181756,9 +181622,6 @@ exports[`Storyshots Views / Products / Create product default 1`] = `
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -182525,9 +182388,6 @@ exports[`Storyshots Views / Products / Create product form errors 1`] = `
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -188311,7 +188171,7 @@ exports[`Storyshots Views / Products / Product edit form errors 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Grid-root-id Grid-default-id"
|
||||
class="Grid-root-id Grid-default-id Grid-richText-id"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -188384,9 +188244,6 @@ exports[`Storyshots Views / Products / Product edit form errors 1`] = `
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -190214,7 +190071,7 @@ exports[`Storyshots Views / Products / Product edit limits reached 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Grid-root-id Grid-default-id"
|
||||
class="Grid-root-id Grid-default-id Grid-richText-id"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -190282,9 +190139,6 @@ exports[`Storyshots Views / Products / Product edit limits reached 1`] = `
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -192181,7 +192035,7 @@ exports[`Storyshots Views / Products / Product edit no limits 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Grid-root-id Grid-default-id"
|
||||
class="Grid-root-id Grid-default-id Grid-richText-id"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -192249,9 +192103,6 @@ exports[`Storyshots Views / Products / Product edit no limits 1`] = `
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -194069,7 +193920,7 @@ exports[`Storyshots Views / Products / Product edit no product attributes 1`] =
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Grid-root-id Grid-default-id"
|
||||
class="Grid-root-id Grid-default-id Grid-richText-id"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -194137,9 +193988,6 @@ exports[`Storyshots Views / Products / Product edit no product attributes 1`] =
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -195691,7 +195539,7 @@ exports[`Storyshots Views / Products / Product edit no stock and no variants 1`]
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Grid-root-id Grid-default-id"
|
||||
class="Grid-root-id Grid-default-id Grid-richText-id"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -195759,9 +195607,6 @@ exports[`Storyshots Views / Products / Product edit no stock and no variants 1`]
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -197467,7 +197312,7 @@ exports[`Storyshots Views / Products / Product edit no stock, no variants and no
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Grid-root-id Grid-default-id"
|
||||
class="Grid-root-id Grid-default-id Grid-richText-id"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -197535,9 +197380,6 @@ exports[`Storyshots Views / Products / Product edit no stock, no variants and no
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -199243,7 +199085,7 @@ exports[`Storyshots Views / Products / Product edit no variants 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Grid-root-id Grid-default-id"
|
||||
class="Grid-root-id Grid-default-id Grid-richText-id"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -199311,9 +199153,6 @@ exports[`Storyshots Views / Products / Product edit no variants 1`] = `
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -201019,7 +200858,7 @@ exports[`Storyshots Views / Products / Product edit when data is fully loaded 1`
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Grid-root-id Grid-default-id"
|
||||
class="Grid-root-id Grid-default-id Grid-richText-id"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -201087,9 +200926,6 @@ exports[`Storyshots Views / Products / Product edit when data is fully loaded 1`
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -202913,7 +202749,7 @@ exports[`Storyshots Views / Products / Product edit when loading data 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Grid-root-id Grid-default-id"
|
||||
class="Grid-root-id Grid-default-id Grid-richText-id"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -202972,23 +202808,6 @@ exports[`Storyshots Views / Products / Product edit when loading data 1`] = `
|
|||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
|
||||
data-test-id="rich-text-editor-description"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
|
||||
data-shrink="true"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
|
@ -204019,7 +203838,7 @@ exports[`Storyshots Views / Products / Product edit when product has no images 1
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Grid-root-id Grid-default-id"
|
||||
class="Grid-root-id Grid-default-id Grid-richText-id"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -204087,9 +203906,6 @@ exports[`Storyshots Views / Products / Product edit when product has no images 1
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -205736,7 +205552,7 @@ exports[`Storyshots Views / Products / Product edit when product has no variants
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Grid-root-id Grid-default-id"
|
||||
class="Grid-root-id Grid-default-id Grid-richText-id"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -205804,9 +205620,6 @@ exports[`Storyshots Views / Products / Product edit when product has no variants
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -207512,7 +207325,7 @@ exports[`Storyshots Views / Products / Product edit with channels 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Grid-root-id Grid-default-id"
|
||||
class="Grid-root-id Grid-default-id Grid-richText-id"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -207580,9 +207393,6 @@ exports[`Storyshots Views / Products / Product edit with channels 1`] = `
|
|||
>
|
||||
Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -231258,9 +231068,6 @@ exports[`Storyshots Views / Shipping / Shipping rate create price rate 1`] = `
|
|||
>
|
||||
Shipping Rate Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -232536,9 +232343,6 @@ exports[`Storyshots Views / Shipping / Shipping rate create weight rate 1`] = `
|
|||
>
|
||||
Shipping Rate Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -233736,23 +233540,6 @@ exports[`Storyshots Views / Shipping / Shipping rate loading 1`] = `
|
|||
<div
|
||||
class="CardSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiFormControl-root-id MuiFormControl-fullWidth-id"
|
||||
data-test-id="rich-text-editor-description"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-focused-id MuiInputLabel-focused-id"
|
||||
data-shrink="true"
|
||||
>
|
||||
Shipping Rate Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id RichTextEditor-rootDisabled-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id MuiFormHelperText-disabled-id"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="CardSpacer-spacer-id"
|
||||
/>
|
||||
|
@ -234711,9 +234498,6 @@ exports[`Storyshots Views / Shipping / Shipping rate update price rate 1`] = `
|
|||
>
|
||||
Shipping Rate Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
@ -235909,9 +235693,6 @@ exports[`Storyshots Views / Shipping / Shipping rate update weight rate 1`] = `
|
|||
>
|
||||
Shipping Rate Description
|
||||
</label>
|
||||
<div
|
||||
class="RichTextEditor-editor-id RichTextEditor-root-id"
|
||||
/>
|
||||
<p
|
||||
class="MuiFormHelperText-root-id MuiFormHelperText-contained-id"
|
||||
/>
|
||||
|
|
|
@ -34,19 +34,28 @@ const TranslationFieldsRich: React.FC<TranslationFieldsRichProps> = ({
|
|||
|
||||
const { setIsDirty, setExitDialogSubmitRef } = useExitFormDialog();
|
||||
|
||||
const [content, change] = useRichText({
|
||||
const {
|
||||
defaultValue,
|
||||
editorRef,
|
||||
isReadyForMount,
|
||||
handleChange,
|
||||
getValue
|
||||
} = useRichText({
|
||||
initial,
|
||||
triggerChange: () => setIsDirty(true)
|
||||
});
|
||||
|
||||
useEffect(() => setExitDialogSubmitRef(onSubmit), [content]);
|
||||
useEffect(() => setExitDialogSubmitRef(onSubmit), [onSubmit]);
|
||||
|
||||
const submit = () => onSubmit(content.current);
|
||||
const submit = async () => onSubmit(await getValue());
|
||||
|
||||
return edit ? (
|
||||
<form onSubmit={submit}>
|
||||
{isReadyForMount && (
|
||||
<RichTextEditor
|
||||
data={content.current}
|
||||
defaultValue={defaultValue}
|
||||
editorRef={editorRef}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
error={undefined}
|
||||
helperText={undefined}
|
||||
|
@ -56,8 +65,8 @@ const TranslationFieldsRich: React.FC<TranslationFieldsRichProps> = ({
|
|||
})}
|
||||
name="translation"
|
||||
data-test-id="translation-field"
|
||||
onChange={change}
|
||||
/>
|
||||
)}
|
||||
<TranslationFieldsSave
|
||||
saveButtonState={saveButtonState}
|
||||
onDiscard={onDiscard}
|
||||
|
@ -70,7 +79,9 @@ const TranslationFieldsRich: React.FC<TranslationFieldsRichProps> = ({
|
|||
</Typography>
|
||||
) : (
|
||||
<Typography>
|
||||
<RichTextEditorContent key={resetKey} data={JSON.parse(initial)} />
|
||||
{isReadyForMount && (
|
||||
<RichTextEditorContent key={resetKey} value={defaultValue} />
|
||||
)}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
|
56
src/utils/objects/useMap.ts
Normal file
56
src/utils/objects/useMap.ts
Normal 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;
|
14
src/utils/richText/__mocks__/useRichText.ts
Normal file
14
src/utils/richText/__mocks__/useRichText.ts
Normal 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;
|
18
src/utils/richText/context.ts
Normal file
18
src/utils/richText/context.ts
Normal 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;
|
||||
};
|
84
src/utils/richText/useMultipleRichText.ts
Normal file
84
src/utils/richText/useMultipleRichText.ts
Normal 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
|
||||
};
|
||||
};
|
|
@ -3,25 +3,8 @@ import { renderHook } from "@testing-library/react-hooks";
|
|||
|
||||
import useRichText from "./useRichText";
|
||||
|
||||
type Fixtures = Record<"short" | "long", OutputData>;
|
||||
type Fixtures = Record<"short", OutputData>;
|
||||
const fixtures: Fixtures = {
|
||||
long: {
|
||||
blocks: [
|
||||
{
|
||||
data: {
|
||||
level: 1,
|
||||
text: "Some header"
|
||||
},
|
||||
type: "header"
|
||||
},
|
||||
{
|
||||
data: {
|
||||
text: "Some text"
|
||||
},
|
||||
type: "paragraph"
|
||||
}
|
||||
]
|
||||
},
|
||||
short: {
|
||||
blocks: [
|
||||
{
|
||||
|
@ -34,40 +17,65 @@ const fixtures: Fixtures = {
|
|||
}
|
||||
};
|
||||
|
||||
const triggerChange = jest.fn();
|
||||
|
||||
describe("useRichText", () => {
|
||||
it("properly saves data in form", () => {
|
||||
const triggerChange = jest.fn();
|
||||
const hook = renderHook(() =>
|
||||
useRichText({
|
||||
initial: null,
|
||||
triggerChange
|
||||
})
|
||||
it("properly informs RichTextEditor when data is ready to mount", () => {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let initial: string | undefined;
|
||||
const { result, rerender } = renderHook(() =>
|
||||
useRichText({ initial, triggerChange })
|
||||
);
|
||||
|
||||
const [data, change] = hook.result.current;
|
||||
expect(data.current).toMatchObject({ blocks: [] });
|
||||
expect(result.current.isReadyForMount).toBe(false);
|
||||
|
||||
change(fixtures.short);
|
||||
initial = JSON.stringify(fixtures.short); // for JSON.parse()
|
||||
rerender();
|
||||
|
||||
expect(data.current).toMatchObject(fixtures.short);
|
||||
expect(triggerChange).toHaveBeenCalled();
|
||||
expect(result.current.defaultValue).toStrictEqual(fixtures.short);
|
||||
expect(result.current.isReadyForMount).toBe(true);
|
||||
});
|
||||
|
||||
it("properly updates data in form", () => {
|
||||
const triggerChange = jest.fn();
|
||||
const hook = renderHook(() =>
|
||||
useRichText({
|
||||
initial: JSON.stringify(fixtures.short),
|
||||
triggerChange
|
||||
})
|
||||
it("returns undefined when JSON cannot be parsed", () => {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let initial: string | undefined;
|
||||
const { result, rerender } = renderHook(() =>
|
||||
useRichText({ initial, triggerChange })
|
||||
);
|
||||
|
||||
const [data, change] = hook.result.current;
|
||||
expect(data.current).toMatchObject(fixtures.short);
|
||||
expect(result.current.isReadyForMount).toBe(false);
|
||||
|
||||
change(fixtures.long);
|
||||
initial = "this-isnt-valid-json";
|
||||
rerender();
|
||||
|
||||
expect(result.current.defaultValue).toBe(undefined);
|
||||
expect(result.current.isReadyForMount).toBe(false);
|
||||
});
|
||||
|
||||
it("runs editorJS .save() when getValue is called", async () => {
|
||||
const saveFn = jest.fn(async () => fixtures.short);
|
||||
const { result } = renderHook(() =>
|
||||
useRichText({ initial: "", triggerChange })
|
||||
);
|
||||
result.current.editorRef.current = {
|
||||
save: saveFn,
|
||||
destroy: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
render: jest.fn()
|
||||
};
|
||||
|
||||
expect(await result.current.getValue()).toStrictEqual(fixtures.short);
|
||||
expect(saveFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls triggerChange when change is made in the editor", () => {
|
||||
triggerChange.mockClear();
|
||||
const { result } = renderHook(() =>
|
||||
useRichText({ initial: "", triggerChange })
|
||||
);
|
||||
|
||||
result.current.handleChange();
|
||||
|
||||
expect(data.current).toMatchObject(fixtures.long);
|
||||
expect(triggerChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,44 +1,45 @@
|
|||
import { OutputData } from "@editorjs/editorjs";
|
||||
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import { EditorCore } from "@saleor/components/RichTextEditor";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
|
||||
const emptyContent: OutputData = {
|
||||
blocks: []
|
||||
};
|
||||
|
||||
function useRichText(opts: {
|
||||
interface UseRichTextOptions {
|
||||
initial: string | null;
|
||||
triggerChange: () => void;
|
||||
}): [MutableRefObject<OutputData>, RichTextEditorChange] {
|
||||
const data = useRef<OutputData>();
|
||||
const [, setLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (opts.initial === null) {
|
||||
data.current = emptyContent;
|
||||
setLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
data.current = JSON.parse(opts.initial);
|
||||
setLoaded(true);
|
||||
} catch {
|
||||
data.current = undefined;
|
||||
}
|
||||
}, [opts.initial]);
|
||||
export function useRichText({ initial, triggerChange }: UseRichTextOptions) {
|
||||
const editorRef = useRef<EditorCore>(null);
|
||||
const [isReadyForMount, setIsReadyForMount] = useState(false);
|
||||
|
||||
const change: RichTextEditorChange = newData => {
|
||||
if (isEqual(data.current.blocks, newData.blocks)) {
|
||||
return;
|
||||
}
|
||||
|
||||
opts.triggerChange();
|
||||
data.current = newData;
|
||||
const handleChange = () => {
|
||||
triggerChange();
|
||||
};
|
||||
|
||||
return [data, change];
|
||||
const getValue = async () => {
|
||||
if (editorRef.current) {
|
||||
return editorRef.current.save();
|
||||
} else {
|
||||
throw new Error("Editor instance is not available");
|
||||
}
|
||||
};
|
||||
|
||||
const defaultValue = useMemo<OutputData | undefined>(() => {
|
||||
try {
|
||||
const result = JSON.parse(initial);
|
||||
setIsReadyForMount(true);
|
||||
return result;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}, [initial]);
|
||||
|
||||
return {
|
||||
editorRef,
|
||||
handleChange,
|
||||
getValue,
|
||||
defaultValue,
|
||||
isReadyForMount
|
||||
};
|
||||
}
|
||||
|
||||
export default useRichText;
|
||||
|
|
Loading…
Reference in a new issue