From dab8064e2657942b29591e5d7831f672e7996114 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 28 Oct 2020 16:26:30 +0100 Subject: [PATCH 01/23] Use editorjs instead of draftail --- package-lock.json | 44 +++ package.json | 6 + .../RichTextEditor/RichTextEditor.tsx | 366 ++++++------------ src/icons/StrikethroughIcon.tsx | 12 +- .../stories/components/RichTextEditor.tsx | 104 +---- .../stories/components/fixtures.json | 74 ++++ 6 files changed, 245 insertions(+), 361 deletions(-) create mode 100644 src/storybook/stories/components/fixtures.json diff --git a/package-lock.json b/package-lock.json index 73d557208..bd2e536dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1949,6 +1949,35 @@ } } }, + "@editorjs/editorjs": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.19.0.tgz", + "integrity": "sha512-8PUVaBZx69IrG8dNrE+FZbHSiRTR8ql8L/cmEi1mOdEdTqnOLq5Wv9dgemK00mBWEgNoavMAjtGQpItGknAa8A==", + "requires": { + "codex-notifier": "^1.1.2", + "codex-tooltip": "^1.0.1" + } + }, + "@editorjs/header": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@editorjs/header/-/header-2.6.0.tgz", + "integrity": "sha512-1psNX/irDjJ8Bp1l7DjkYWz7IBtjVIRANk7kPkNoY2CfAeeCFYbJmMlXdqTF2WeAjYv2WMy5ey/aR5fTccgFaw==" + }, + "@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==" + }, + "@editorjs/list": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@editorjs/list/-/list-1.6.0.tgz", + "integrity": "sha512-2oJ3Nj3lDcIKS6GcrHYHzUUabIjg7zlXTYXQWdEWXevbnM0/fq+4psyI/AYtqbaa3jN+bycPBIW4OG3zD+3d5A==" + }, + "@editorjs/quote": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@editorjs/quote/-/quote-2.4.0.tgz", + "integrity": "sha512-IWOBWjL2ngPP63GcIAltyD9kc7OVZFma4kS+T5JRHvKKDspYsnmrxsbRmCPc+coZQzqPxXHkiOZuNMdmGX/Y3w==" + }, "@emotion/cache": { "version": "10.0.19", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.19.tgz", @@ -8778,6 +8807,16 @@ "urlgrey": "0.4.4" } }, + "codex-notifier": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/codex-notifier/-/codex-notifier-1.1.2.tgz", + "integrity": "sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg==" + }, + "codex-tooltip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.1.tgz", + "integrity": "sha512-1xLb1NZbxguNtf02xBRhDphq/EXvMMeEbY0ievjQTHqf8UjXsD41evGk9rqcbjpl+JOjNgtwnp1OaU/X/h6fhQ==" + }, "coffeescript": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", @@ -10306,6 +10345,11 @@ "safer-buffer": "^2.1.0" } }, + "editorjs-inline-tool": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/editorjs-inline-tool/-/editorjs-inline-tool-0.4.0.tgz", + "integrity": "sha512-Ppb4e8IFPjWuNcoNM4tg9bDSo7FgMYAlqP4UhuV5W2JoJBubV5pUcpLrFrSyGTt1HJVEpbrib134zf4wxO+7VA==" + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", diff --git a/package.json b/package.json index 87724860c..a589c198e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,11 @@ "npm": ">=6.11.0" }, "dependencies": { + "@editorjs/editorjs": "^2.19.0", + "@editorjs/header": "^2.6.0", + "@editorjs/image": "^2.6.0", + "@editorjs/list": "^1.6.0", + "@editorjs/quote": "^2.4.0", "@material-ui/core": "^4.5.1", "@material-ui/icons": "^4.5.1", "@material-ui/styles": "^4.5.2", @@ -36,6 +41,7 @@ "draft-js": "^0.10.5", "draftail": "^1.2.1", "draftjs-to-html": "^0.9.1", + "editorjs-inline-tool": "^0.4.0", "fast-array-diff": "^0.2.0", "fuzzaldrin": "^2.1.0", "graphql": "^14.4.2", diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index 68d8164ff..16b9a576c 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -1,298 +1,163 @@ +import EditorJS, { OutputData } from "@editorjs/editorjs"; +import Header from "@editorjs/header"; +import List from "@editorjs/list"; +import Quote from "@editorjs/quote"; import { makeStyles } from "@material-ui/core/styles"; import { fade } from "@material-ui/core/styles/colorManipulator"; import Typography from "@material-ui/core/Typography"; import { CreateCSSProperties } from "@material-ui/styles/withStyles"; -import { ChangeEvent } from "@saleor/hooks/useForm"; +import { FormChange } from "@saleor/hooks/useForm"; +import strikethroughIcon from "@saleor/icons/StrikethroughIcon"; import classNames from "classnames"; -import { RawDraftContentState } from "draft-js"; -import { - BLOCK_TYPE, - DraftailEditor, - ENTITY_TYPE, - INLINE_STYLE -} from "draftail"; -import isEqual from "lodash-es/isEqual"; +import createGenericInlineTool, { + ItalicInlineTool, + UnderlineInlineTool +} from "editorjs-inline-tool"; import React from "react"; -import ErrorBoundary from "react-error-boundary"; -import { FormattedMessage } from "react-intl"; - -import BoldIcon from "../../icons/BoldIcon"; -import HeaderOne from "../../icons/HeaderOne"; -import HeaderThree from "../../icons/HeaderThree"; -import HeaderTwo from "../../icons/HeaderTwo"; -import ItalicIcon from "../../icons/ItalicIcon"; -import LinkIcon from "../../icons/LinkIcon"; -import OrderedListIcon from "../../icons/OrderedListIcon"; -import QuotationIcon from "../../icons/QuotationIcon"; -import StrikethroughIcon from "../../icons/StrikethroughIcon"; -import UnorderedListIcon from "../../icons/UnorderedListIcon"; -import LinkEntity from "./LinkEntity"; -import LinkSource from "./LinkSource"; export interface RichTextEditorProps { disabled: boolean; error: boolean; helperText: string; - initial?: RawDraftContentState; + initial: OutputData; label: string; name: string; - scroll?: boolean; - onChange: (event: React.ChangeEvent) => void; + onChange: FormChange; } const useStyles = makeStyles( theme => { - const editorContainer: CreateCSSProperties = { - border: `1px ${theme.palette.divider} solid`, - borderRadius: 4, - padding: "27px 12px 10px", - position: "relative", - transition: theme.transitions.duration.shortest + "ms" + const hover = { + "&:hover": { + background: fade(theme.palette.primary.main, 0.1) + } }; return { - editorContainer, error: { color: theme.palette.error.main }, helperText: { marginTop: theme.spacing(0.75) }, - input: { - position: "relative" - }, label: { - fontSize: theme.typography.caption.fontSize, - left: 12, + color: theme.palette.text.secondary, position: "absolute", - top: 9 + top: theme.spacing(4), + transition: theme.transitions.duration.short + "ms" }, - linkIcon: { - marginTop: 2 + labelActive: { + color: theme.palette.primary.main }, root: { - "& .DraftEditor": { - "&-editorContainer": { - "& .public-DraftEditor-content": { - lineHeight: 1.62 - }, - "& a": { - color: theme.palette.primary.light - }, - "&:after": { - background: theme.palette.getContrastText( - theme.palette.background.default - ), - bottom: -11, - content: "''", - display: "block", - height: 2, - left: -12, - position: "absolute", - transform: "scaleX(0) scaleY(0)", - width: "calc(100% + 24px)" - }, - position: "relative" - }, - "&-root": { - ...theme.typography.body1 - } + "& .cdx-quote__text": { + minHeight: 24 }, - "& .Draftail": { - "&-Editor": { - "&--focus": { - boxShadow: `inset 0px 0px 0px 2px ${theme.palette.primary.main}` - }, - "&:hover": { - borderColor: theme.palette.primary.main - }, - ...editorContainer - }, - "&-Toolbar": { - "&Button": { - "& svg": { - padding: 2 - }, - "&--active": { - "&:hover": { - background: theme.palette.primary.main - }, - "&:not(:hover)": { - borderRightColor: theme.palette.primary.main - }, - background: theme.palette.primary.main - }, - "&:focus": { - "&:active": { - "&:after": { - background: fade(theme.palette.primary.main, 0.3), - borderRadius: "100%", - content: "''", - display: "block", - height: "100%", - width: "100%" - } - } - }, - "&:hover": { - background: fade(theme.palette.primary.main, 0.3) - }, - background: "none", - border: "none", - borderRight: `1px ${theme.palette.divider} solid`, - color: theme.typography.body1.color, - cursor: "pointer", - display: "inline-flex", - height: 36, - justifyContent: "center", - padding: theme.spacing(1) + 2, - transition: theme.transitions.duration.short + "ms", - width: 36 - }, - "&Group": { - "&:last-of-type": { - "& .Draftail-ToolbarButton": { - "&:last-of-type": { - border: "none" - } - } - }, - display: "flex" - }, - background: theme.palette.background.default, - border: `1px ${theme.palette.divider} solid`, - display: "inline-flex", - flexWrap: "wrap", - marginBottom: theme.spacing(), - marginTop: 10, - [theme.breakpoints.down(460)]: { - width: "min-content" - } - }, - "&-block": { - "&--blockquote": { - borderLeft: `2px solid ${theme.palette.divider}`, - margin: 0, - padding: theme.spacing(1, 2) - } - } + "& .ce-conversion-tool": { + ...hover }, - "&$error": { - "& .Draftail": { - "&-Editor": { - borderColor: theme.palette.error.main - } - } - } + "& .ce-conversion-tool--focused": { + background: `${fade(theme.palette.primary.main, 0.1)} !important` + }, + "& .ce-inline-tool": { + ...hover, + height: 32, + transition: theme.transitions.duration.short + "ms", + width: 32 + }, + "& .ce-inline-toolbar__dropdown": { + ...hover, + height: 32, + marginRight: 0 + }, + "& .ce-inline-toolbar__toggler-and-button-wrapper": { + paddingRight: 0 + }, + "& .codex-editor__redactor": { + marginRight: `${theme.spacing(4)}px !important`, + paddingBottom: "0 !important" + }, + "& a": { + color: theme.palette.primary.light + }, + "&:hover": { + borderColor: theme.palette.primary.main + }, + border: `1px solid ${theme.palette.divider}`, + borderRadius: 8, + boxShadow: `inset 0 0 0 0 ${theme.palette.primary.main}`, + padding: theme.spacing(3, 2), + transition: theme.transitions.duration.short + "ms" }, - scroll: { - "& .DraftEditor": { - "&-editorContainer": { - "& .public-DraftEditor-content": { - lineHeight: 1.62 - } - } - } - }, - smallIcon: { - marginLeft: 10 + rootActive: { + boxShadow: `inset 0px 0px 0 2px ${theme.palette.primary.main}` } }; }, { name: "RichTextEditor" } ); -function handleSave( - value: any, - initial: any, - name: string, - onChange: (event: ChangeEvent) => void -) { - if (value && !isEqual(value, initial)) { - onChange({ - target: { - name, - value +class NewEditor extends EditorJS {} + +const RichTextEditor: React.FC = ({ + error, + helperText, + initial, + label, + name, + onChange +}) => { + const classes = useStyles({}); + + const [isFocused, setFocus] = React.useState(false); + const editor = React.useRef(); + const editorContainer = React.useRef(); + React.useEffect(() => { + editor.current = new NewEditor({ + data: initial, + holder: editorContainer.current, + tools: { + header: { + class: Header, + config: { + defaultLevel: 1, + levels: [1, 2, 3] + } + }, + list: List, + quote: Quote, + strikethrough: createGenericInlineTool({ + sanitize: { + s: true + }, + shortcut: "CMD+S", + tagName: "s", + toolboxIcon: strikethroughIcon + }) } }); - } -} - -const RichTextEditor: React.FC = props => { - const { error, helperText, initial, label, name, scroll, onChange } = props; - - const classes = useStyles(props); + }, []); + React.useEffect(() => () => editor.current.destroy(), []); return ( -
-
- +
+
setFocus(true)} + onBlur={() => setFocus(false)} + > + {label} - ( -
- - - -
- )} - > - 0 ? initial : null - } - onSave={value => handleSave(value, initial, name, onChange)} - blockTypes={[ - { - icon: , - type: BLOCK_TYPE.HEADER_ONE - }, - { icon: , type: BLOCK_TYPE.HEADER_TWO }, - { icon: , type: BLOCK_TYPE.HEADER_THREE }, - { icon: , type: BLOCK_TYPE.BLOCKQUOTE }, - { - icon: , - type: BLOCK_TYPE.UNORDERED_LIST_ITEM - }, - { icon: , type: BLOCK_TYPE.ORDERED_LIST_ITEM } - ]} - inlineStyles={[ - { - icon: , - type: INLINE_STYLE.BOLD - }, - { - icon: , - type: INLINE_STYLE.ITALIC - }, - { - icon: , - type: INLINE_STYLE.STRIKETHROUGH - } - ]} - enableLineBreak - entityTypes={[ - { - attributes: ["url"], - decorator: LinkEntity, - icon: , - source: LinkSource, - type: ENTITY_TYPE.LINK - } - ]} - /> -
{helperText && ( = props => { }; RichTextEditor.displayName = "RichTextEditor"; -RichTextEditor.defaultProps = { - scroll: true -}; export default RichTextEditor; diff --git a/src/icons/StrikethroughIcon.tsx b/src/icons/StrikethroughIcon.tsx index b409b4c00..68957fd06 100644 --- a/src/icons/StrikethroughIcon.tsx +++ b/src/icons/StrikethroughIcon.tsx @@ -1,11 +1,3 @@ -import createSvgIcon from "@material-ui/icons/utils/createSvgIcon"; -import React from "react"; +const Strikethrough = ``; -const HeaderOne = createSvgIcon( - <> - - , - "HeaderOne" -); - -export default HeaderOne; +export default Strikethrough; diff --git a/src/storybook/stories/components/RichTextEditor.tsx b/src/storybook/stories/components/RichTextEditor.tsx index e645acb4f..9cfd5580c 100644 --- a/src/storybook/stories/components/RichTextEditor.tsx +++ b/src/storybook/stories/components/RichTextEditor.tsx @@ -1,108 +1,14 @@ +import { OutputData } from "@editorjs/editorjs"; import RichTextEditor from "@saleor/components/RichTextEditor"; import { storiesOf } from "@storybook/react"; -import { RawDraftContentState } from "draft-js"; import React from "react"; import CardDecorator from "../../CardDecorator"; import Decorator from "../../Decorator"; +import * as fixtures from "./fixtures.json"; + +export const content: OutputData = fixtures.richTextEditor; -export const content: RawDraftContentState = { - blocks: [ - { - data: {}, - depth: 0, - entityRanges: [], - inlineStyleRanges: [{ length: 4, offset: 0, style: "BOLD" }], - key: "rosn", - text: "bold", - type: "unstyled" - }, - { - data: {}, - depth: 0, - entityRanges: [], - inlineStyleRanges: [{ length: 6, offset: 0, style: "ITALIC" }], - key: "6tbch", - text: "italic", - type: "unstyled" - }, - { - data: {}, - depth: 0, - entityRanges: [], - inlineStyleRanges: [{ length: 13, offset: 0, style: "STRIKETHROUGH" }], - key: "1p044", - text: "strikethrough", - type: "unstyled" - }, - { - data: {}, - depth: 0, - entityRanges: [], - inlineStyleRanges: [], - key: "aven6", - text: "h1", - type: "header-one" - }, - { - data: {}, - depth: 0, - entityRanges: [], - inlineStyleRanges: [], - key: "9rabl", - text: "h2", - type: "header-two" - }, - { - data: {}, - depth: 0, - entityRanges: [], - inlineStyleRanges: [], - key: "bv0ac", - text: "h3", - type: "header-three" - }, - { - data: {}, - depth: 0, - entityRanges: [], - inlineStyleRanges: [], - key: "2ip7q", - text: "blockquote", - type: "blockquote" - }, - { - data: {}, - depth: 0, - entityRanges: [], - inlineStyleRanges: [], - key: "8r8ss", - text: "ul", - type: "unordered-list-item" - }, - { - data: {}, - depth: 0, - entityRanges: [], - inlineStyleRanges: [], - key: "911hc", - text: "ol", - type: "ordered-list-item" - }, - { - data: {}, - depth: 0, - entityRanges: [{ key: 0, length: 4, offset: 0 }], - inlineStyleRanges: [], - key: "5aejo", - text: "link", - type: "unstyled" - } - ], - entityMap: { - "0": { data: { url: "#" }, mutability: "MUTABLE", type: "LINK" } - } -}; storiesOf("Generics / Rich text editor", module) .addDecorator(CardDecorator) .addDecorator(Decorator) @@ -110,7 +16,7 @@ storiesOf("Generics / Rich text editor", module) Sed iaculis urna et justo accumsan, eget porta est egestas. Nunc odio libero, pharetra in tristique eget, pellentesque in lectus. Sed sed laoreet orci. Suspendisse dui nibh, iaculis ac dui posuere, placerat elementum dolor. In sit amet aliquet nibh. Maecenas sed felis sed lectus gravida vulputate et a mi. Sed a tristique neque, ut euismod arcu. Donec quis aliquet massa. Curabitur arcu purus, facilisis quis posuere sit amet, pharetra at erat." + } + }, + { + "type": "list", + "data": { + "style": "ordered", + "items": [ + "Maecenas pretium aliquam odio, a iaculis diam dictum ut.", + "Vestibulum pulvinar, quam quis sollicitudin luctus, libero odio laoreet lectus, in tristique ligula dui et ex.
", + "Nam quis nibh sed elit fermentum interdum non eget quam." + ] + } + }, + { + "type": "paragraph", + "data": { + "text": "Nulla sit amet cursus augue, in maximus tellus. Donec sit amet mollis neque, eget commodo odio." + } + }, + { "type": "header", "data": { "text": "Morbi aliquam", "level": 2 } }, + { + "type": "paragraph", + "data": { + "text": "Aliquam posuere nisi et ante malesuada egestas. Phasellus auctor risus a erat aliquam, tempus volutpat arcu rutrum. Duis bibendum id justo ut commodo. Suspendisse imperdiet tincidunt blandit." + } + }, + { "type": "header", "data": { "text": "Nam ipsum purus", "level": 3 } }, + { + "type": "paragraph", + "data": { + "text": "Nam ipsum purus, feugiat ut dapibus at, porttitor eget leo. Phasellus sodales urna quis mi viverra, non mollis magna tristique. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Pellentesque viverra est sit amet nisi hendrerit, pharetra vehicula neque volutpat. Maecenas feugiat a nulla id blandit. Sed sagittis tempus libero non dignissim. In lectus felis, mattis vitae lacinia nec, volutpat eu elit. Proin ultricies lacus id felis placerat mollis. Integer ultricies eros nec mauris interdum, sit amet sodales ipsum elementum. Vivamus quis dapibus turpis, eu dignissim quam." + } + }, + { + "type": "quote", + "data": { + "text": "Nam facilisis augue vel urna tristique rutrum id et tortor.", + "caption": "Morbi erat mi", + "alignment": "left" + } + }, + { + "type": "header", + "data": { "text": "Tempor ac posuere nec", "level": 3 } + }, + { + "type": "paragraph", + "data": { + "text": "Rhoncus ac lectus. Etiam viverra nisl feugiat tempus eleifend. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae." + } + }, + { + "type": "list", + "data": { + "style": "unordered", + "items": [ + "Phasellus nec ipsum non metus vestibulum semper", + "In tincidunt, dui vitae suscipit sodales, lacus justo porttitor nulla
" + ] + } + } + ], + "version": "2.19.0" + } +} From 815941fc821aaf8312063010dca3cec7d5f9a7d8 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 28 Oct 2020 16:30:44 +0100 Subject: [PATCH 02/23] Fix types --- src/components/RichTextEditor/RichTextEditor.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index 16b9a576c..07093b6bd 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -5,21 +5,18 @@ import Quote from "@editorjs/quote"; import { makeStyles } from "@material-ui/core/styles"; import { fade } from "@material-ui/core/styles/colorManipulator"; import Typography from "@material-ui/core/Typography"; -import { CreateCSSProperties } from "@material-ui/styles/withStyles"; import { FormChange } from "@saleor/hooks/useForm"; import strikethroughIcon from "@saleor/icons/StrikethroughIcon"; import classNames from "classnames"; -import createGenericInlineTool, { - ItalicInlineTool, - UnderlineInlineTool -} from "editorjs-inline-tool"; +import createGenericInlineTool from "editorjs-inline-tool"; import React from "react"; export interface RichTextEditorProps { disabled: boolean; error: boolean; helperText: string; - initial: OutputData; + // TODO: Remove any type + initial: OutputData | any; label: string; name: string; onChange: FormChange; @@ -103,9 +100,7 @@ const RichTextEditor: React.FC = ({ error, helperText, initial, - label, - name, - onChange + label }) => { const classes = useStyles({}); From 393b4a586027782853a463e75ddd459b49c6c3dd Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 29 Oct 2020 13:14:05 +0100 Subject: [PATCH 03/23] Fix styles --- src/components/RichTextEditor/RichTextEditor.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index 07093b6bd..0bad6fae4 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -40,7 +40,7 @@ const useStyles = makeStyles( label: { color: theme.palette.text.secondary, position: "absolute", - top: theme.spacing(4), + top: theme.spacing(1), transition: theme.transitions.duration.short + "ms" }, labelActive: { @@ -50,6 +50,10 @@ const useStyles = makeStyles( "& .cdx-quote__text": { minHeight: 24 }, + "& .ce-block__content": { + margin: 0, + maxWidth: "unset" + }, "& .ce-conversion-tool": { ...hover }, @@ -80,10 +84,11 @@ const useStyles = makeStyles( "&:hover": { borderColor: theme.palette.primary.main }, - border: `1px solid ${theme.palette.divider}`, - borderRadius: 8, + border: `1px solid ${fade(theme.palette.text.secondary, 0.4)}`, + borderRadius: 4, boxShadow: `inset 0 0 0 0 ${theme.palette.primary.main}`, padding: theme.spacing(3, 2), + position: "relative", transition: theme.transitions.duration.short + "ms" }, rootActive: { From 88bd52763cf69d8fd38e2a27ea638c2187f90791 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 3 Nov 2020 12:35:36 +0100 Subject: [PATCH 04/23] Add ability to save data --- .../RichTextEditor/RichTextEditor.tsx | 74 +++++++++++-------- .../ProductDetailsForm/ProductDetailsForm.tsx | 20 ++--- .../ProductUpdatePage/ProductUpdatePage.tsx | 19 ++--- .../components/ProductUpdatePage/form.tsx | 56 ++++++++++---- src/products/utils/data.ts | 3 +- 5 files changed, 100 insertions(+), 72 deletions(-) diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index 0bad6fae4..8653a9616 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -5,21 +5,20 @@ import Quote from "@editorjs/quote"; import { makeStyles } from "@material-ui/core/styles"; import { fade } from "@material-ui/core/styles/colorManipulator"; import Typography from "@material-ui/core/Typography"; -import { FormChange } from "@saleor/hooks/useForm"; import strikethroughIcon from "@saleor/icons/StrikethroughIcon"; import classNames from "classnames"; import createGenericInlineTool from "editorjs-inline-tool"; import React from "react"; +export type RichTextEditorChange = (data: OutputData) => void; export interface RichTextEditorProps { + data: OutputData; disabled: boolean; error: boolean; helperText: string; - // TODO: Remove any type - initial: OutputData | any; label: string; name: string; - onChange: FormChange; + onChange: RichTextEditorChange; } const useStyles = makeStyles( @@ -99,45 +98,56 @@ const useStyles = makeStyles( { name: "RichTextEditor" } ); -class NewEditor extends EditorJS {} - const RichTextEditor: React.FC = ({ + data, error, helperText, - initial, - label + label, + onChange }) => { const classes = useStyles({}); const [isFocused, setFocus] = React.useState(false); const editor = React.useRef(); const editorContainer = React.useRef(); - React.useEffect(() => { - editor.current = new NewEditor({ - data: initial, - holder: editorContainer.current, - tools: { - header: { - class: Header, - config: { - defaultLevel: 1, - levels: [1, 2, 3] - } - }, - list: List, - quote: Quote, - strikethrough: createGenericInlineTool({ - sanitize: { - s: true + React.useEffect( + () => { + if (data) { + editor.current = new EditorJS({ + data, + holder: editorContainer.current, + onChange: async api => { + const savedData = await api.saver.save(); + onChange(savedData); }, - shortcut: "CMD+S", - tagName: "s", - toolboxIcon: strikethroughIcon - }) + tools: { + header: { + class: Header, + config: { + defaultLevel: 1, + levels: [1, 2, 3] + } + }, + list: List, + quote: Quote, + strikethrough: createGenericInlineTool({ + sanitize: { + s: true + }, + shortcut: "CMD+S", + tagName: "s", + toolboxIcon: strikethroughIcon + }) + } + }); } - }); - }, []); - React.useEffect(() => () => editor.current.destroy(), []); + + return editor.current?.destroy; + }, + // Rerender editor only if changed from undefined to defined state + [data === undefined] + ); + React.useEffect(() => editor.current?.destroy, []); return (
diff --git a/src/products/components/ProductDetailsForm/ProductDetailsForm.tsx b/src/products/components/ProductDetailsForm/ProductDetailsForm.tsx index aac585345..a533cf97c 100644 --- a/src/products/components/ProductDetailsForm/ProductDetailsForm.tsx +++ b/src/products/components/ProductDetailsForm/ProductDetailsForm.tsx @@ -1,27 +1,27 @@ +import { OutputData } from "@editorjs/editorjs"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import TextField from "@material-ui/core/TextField"; import CardTitle from "@saleor/components/CardTitle"; import FormSpacer from "@saleor/components/FormSpacer"; -import RichTextEditor from "@saleor/components/RichTextEditor"; +import RichTextEditor, { + RichTextEditorChange +} from "@saleor/components/RichTextEditor"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import { commonMessages } from "@saleor/intl"; import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors"; -import { RawDraftContentState } from "draft-js"; import React from "react"; import { useIntl } from "react-intl"; interface ProductDetailsFormProps { data: { - description: RawDraftContentState; + description: OutputData; name: string; }; disabled?: boolean; errors: ProductErrorFragment[]; - // Draftail isn't controlled - it needs only initial input - // because it's autosaving on its own. - // Ref https://github.com/mirumee/saleor/issues/4470 - initialDescription: RawDraftContentState; + + onDescriptionChange: RichTextEditorChange; onChange(event: any); } @@ -29,7 +29,7 @@ export const ProductDetailsForm: React.FC = ({ data, disabled, errors, - initialDescription, + onDescriptionChange, onChange }) => { const intl = useIntl(); @@ -57,13 +57,13 @@ export const ProductDetailsForm: React.FC = ({ /> diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index 3e99347e2..4d51ad9cd 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -1,3 +1,4 @@ +import { OutputData } from "@editorjs/editorjs"; import AppHeader from "@saleor/components/AppHeader"; import AvailabilityCard from "@saleor/components/AvailabilityCard"; import CardSpacer from "@saleor/components/CardSpacer"; @@ -19,7 +20,6 @@ import { maybe } from "@saleor/misc"; import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories"; import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections"; import { FetchMoreProps, ListActions, ReorderAction } from "@saleor/types"; -import { convertFromRaw, RawDraftContentState } from "draft-js"; import React from "react"; import { useIntl } from "react-intl"; @@ -75,11 +75,12 @@ export interface ProductUpdatePageProps extends ListActions { } export interface ProductUpdatePageSubmitData extends ProductUpdatePageFormData { + addStocks: ProductStockInput[]; attributes: ProductAttributeInput[]; collections: string[]; - addStocks: ProductStockInput[]; - updateStocks: ProductStockInput[]; + description: OutputData; removeStocks: string[]; + updateStocks: ProductStockInput[]; } export const ProductUpdatePage: React.FC = ({ @@ -135,10 +136,6 @@ export const ProductUpdatePage: React.FC = ({ product?.taxType.description ); - const initialDescription = maybe(() => - JSON.parse(product.descriptionJson) - ); - const categories = getChoices(categoryChoiceList); const collections = getChoices(collectionChoiceList); const currency = product?.variants[0]?.price.currency; @@ -175,7 +172,7 @@ export const ProductUpdatePage: React.FC = ({ data={data} disabled={disabled} errors={errors} - initialDescription={initialDescription} + onDescriptionChange={handlers.changeDescription} onChange={change} /> @@ -262,11 +259,7 @@ export const ProductUpdatePage: React.FC = ({ title={data.seoTitle} titlePlaceholder={data.name} description={data.seoDescription} - descriptionPlaceholder={maybe(() => - convertFromRaw(data.description) - .getPlainText() - .slice(0, 300) - )} + descriptionPlaceholder={""} // TODO: cast description to string slug={data.slug} slugPlaceholder={data.name} loading={disabled} diff --git a/src/products/components/ProductUpdatePage/form.tsx b/src/products/components/ProductUpdatePage/form.tsx index 009b8b27a..ff4c40393 100644 --- a/src/products/components/ProductUpdatePage/form.tsx +++ b/src/products/components/ProductUpdatePage/form.tsx @@ -1,5 +1,7 @@ +import { OutputData } from "@editorjs/editorjs"; 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 useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm"; import useFormset, { @@ -21,7 +23,6 @@ import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; -import { RawDraftContentState } from "draft-js"; import { diff } from "fast-array-diff"; import React from "react"; @@ -35,7 +36,6 @@ export interface ProductUpdateFormData extends MetadataFormData { changeTaxCode: boolean; chargeTaxes: boolean; collections: string[]; - description: RawDraftContentState; isAvailable: boolean; isAvailableForPurchase: boolean; isPublished: boolean; @@ -52,27 +52,36 @@ export interface ProductUpdateFormData extends MetadataFormData { } export interface ProductUpdateData extends ProductUpdateFormData { attributes: ProductAttributeInput[]; + description: OutputData; stocks: ProductStockInput[]; } export interface ProductUpdateSubmitData extends ProductUpdateFormData { attributes: ProductAttributeInput[]; collections: string[]; + description: OutputData; addStocks: ProductStockInput[]; updateStocks: ProductStockInput[]; removeStocks: string[]; } -type ProductUpdateHandlers = Record< - "changeMetadata" | "selectCategory" | "selectCollection" | "selectTaxRate", - FormChange -> & - Record< - "changeStock" | "selectAttribute" | "selectAttributeMultiple", - FormsetChange - > & - Record<"addStock" | "deleteStock", (id: string) => void>; +interface ProductUpdateHandlers + extends Record< + | "changeMetadata" + | "selectCategory" + | "selectCollection" + | "selectTaxRate", + FormChange + >, + Record< + "changeStock" | "selectAttribute" | "selectAttributeMultiple", + FormsetChange + >, + Record<"addStock" | "deleteStock", (id: string) => void> { + changeDescription: RichTextEditorChange; +} export interface UseProductUpdateFormResult { change: FormChange; + data: ProductUpdateData; handlers: ProductUpdateHandlers; hasChanged: boolean; @@ -155,6 +164,15 @@ function useProductUpdateForm( ); const attributes = useFormset(getAttributeInputFromProduct(product)); const stocks = useFormset(getStockInputFromProduct(product)); + const description = React.useRef(); + + React.useEffect(() => { + try { + description.current = JSON.parse(product.descriptionJson); + } catch { + description.current = undefined; + } + }, [product]); const { isMetadataModified, @@ -209,28 +227,36 @@ function useProductUpdateForm( opts.taxTypes ); const changeMetadata = makeMetadataChangeHandler(handleChange); + const changeDescription: RichTextEditorChange = data => { + triggerChange(); + description.current = data; + }; const data: ProductUpdateData = { ...form.data, attributes: attributes.data, + description: description.current, stocks: stocks.data }; - const submitData: ProductUpdateSubmitData = { + // Need to make it function to always have description.current up to date + const getSubmitData = (): ProductUpdateSubmitData => ({ ...data, ...getAvailabilityData(data), ...getStocksData(product, stocks.data), ...getMetadata(data, isMetadataModified, isPrivateMetadataModified), addStocks: [], - attributes: attributes.data - }; + attributes: attributes.data, + description: description.current + }); - const submit = () => handleFormSubmit(submitData, onSubmit, setChanged); + const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged); return { change: handleChange, data, handlers: { addStock: handleStockAdd, + changeDescription, changeMetadata, changeStock: handleStockChange, deleteStock: handleStockDelete, diff --git a/src/products/utils/data.ts b/src/products/utils/data.ts index f581aecac..f48043e1e 100644 --- a/src/products/utils/data.ts +++ b/src/products/utils/data.ts @@ -1,3 +1,4 @@ +import { OutputData } from "@editorjs/editorjs"; import { MetadataFormData } from "@saleor/components/Metadata/types"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; @@ -175,7 +176,6 @@ export interface ProductUpdatePageFormData extends MetadataFormData { changeTaxCode: boolean; chargeTaxes: boolean; collections: string[]; - description: RawDraftContentState; isAvailable: boolean; isAvailableForPurchase: boolean; isPublished: boolean; @@ -205,7 +205,6 @@ export function getProductUpdatePageFormData( () => product.collections.map(collection => collection.id), [] ), - description: maybe(() => JSON.parse(product.descriptionJson)), isAvailable: !!product?.isAvailable, isAvailableForPurchase: !!product?.isAvailableForPurchase, isPublished: maybe(() => product.isPublished, false), From 437df6fe9a11dacb7f8b098672b385244864ce45 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 3 Nov 2020 14:53:17 +0100 Subject: [PATCH 05/23] wip --- .../RichTextEditor/RichTextEditor.tsx | 8 ++- .../ProductCreatePage/ProductCreatePage.tsx | 9 +-- .../components/ProductCreatePage/form.tsx | 53 ++++++++------ .../components/ProductUpdatePage/form.tsx | 18 ++--- src/utils/richText/useRichText.test.ts | 72 +++++++++++++++++++ src/utils/richText/useRichText.ts | 28 ++++++++ 6 files changed, 146 insertions(+), 42 deletions(-) create mode 100644 src/utils/richText/useRichText.test.ts create mode 100644 src/utils/richText/useRichText.ts diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index 8653a9616..2eb58bd04 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -19,6 +19,7 @@ export interface RichTextEditorProps { label: string; name: string; onChange: RichTextEditorChange; + onReady?: () => void; } const useStyles = makeStyles( @@ -86,7 +87,10 @@ const useStyles = makeStyles( border: `1px solid ${fade(theme.palette.text.secondary, 0.4)}`, borderRadius: 4, boxShadow: `inset 0 0 0 0 ${theme.palette.primary.main}`, + fontSize: theme.typography.body1.fontSize, padding: theme.spacing(3, 2), + paddingBottom: theme.spacing(), + paddingLeft: 10, position: "relative", transition: theme.transitions.duration.short + "ms" }, @@ -103,7 +107,8 @@ const RichTextEditor: React.FC = ({ error, helperText, label, - onChange + onChange, + onReady }) => { const classes = useStyles({}); @@ -120,6 +125,7 @@ const RichTextEditor: React.FC = ({ const savedData = await api.saver.save(); onChange(savedData); }, + onReady, tools: { header: { class: Header, diff --git a/src/products/components/ProductCreatePage/ProductCreatePage.tsx b/src/products/components/ProductCreatePage/ProductCreatePage.tsx index ffebe8145..b288068b4 100644 --- a/src/products/components/ProductCreatePage/ProductCreatePage.tsx +++ b/src/products/components/ProductCreatePage/ProductCreatePage.tsx @@ -86,12 +86,6 @@ export const ProductCreatePage: React.FC = ({ const intl = useIntl(); const localizeDate = useDateLocalize(); - // Ensures that it will not change after component rerenders, because it - // generates different block keys and it causes editor to lose its content. - const initialDescription = React.useRef( - convertToRaw(ContentState.createFromText("")) - ); - // Display values const [selectedCategory, setSelectedCategory] = useStateFromProps( initial?.category || "" @@ -144,8 +138,8 @@ export const ProductCreatePage: React.FC = ({ data={data} disabled={disabled} errors={errors} - initialDescription={initialDescription.current} onChange={change} + onDescriptionChange={handlers.changeDescription} /> {data.attributes.length > 0 && ( @@ -167,6 +161,7 @@ export const ProductCreatePage: React.FC = ({ weightUnit={weightUnit} onChange={change} /> + & - Record< - "changeStock" | "selectAttribute" | "selectAttributeMultiple", - FormsetChange - > & - Record<"addStock" | "deleteStock", (id: string) => void>; +interface ProductCreateHandlers + extends Record< + | "changeMetadata" + | "selectCategory" + | "selectCollection" + | "selectProductType" + | "selectTaxRate", + FormChange + >, + Record< + "changeStock" | "selectAttribute" | "selectAttributeMultiple", + FormsetChange + >, + Record<"addStock" | "deleteStock", (id: string) => void> { + changeDescription: RichTextEditorChange; +} export interface UseProductCreateFormResult { change: FormChange; data: ProductCreateData; @@ -106,7 +111,7 @@ const defaultInitialFormData: ProductCreateFormData & changeTaxCode: false, chargeTaxes: false, collections: [], - description: {} as any, + description: null, isAvailable: false, isAvailableForPurchase: false, isPublished: false, @@ -117,7 +122,7 @@ const defaultInitialFormData: ProductCreateFormData & publicationDate: "", seoDescription: "", seoTitle: "", - sku: null, + sku: "", slug: "", stockQuantity: null, taxCode: null, @@ -152,6 +157,10 @@ function useProductCreateForm( const [productType, setProductType] = useStateFromProps( initialProductType || null ); + const [description, changeDescription] = useRichText({ + initial: null, + triggerChange + }); const { makeChangeHandler: makeMetadataChangeHandler @@ -211,19 +220,21 @@ function useProductCreateForm( ); const changeMetadata = makeMetadataChangeHandler(handleChange); - const data: ProductCreateData = { + const getData = (): ProductCreateData => ({ ...form.data, attributes: attributes.data, + description: description.current, productType, stocks: stocks.data - }; - const submit = () => onSubmit(data); + }); + const submit = () => onSubmit(getData()); return { change: handleChange, - data, + data: getData(), handlers: { addStock: handleStockAdd, + changeDescription, changeMetadata, changeStock: handleStockChange, deleteStock: handleStockDelete, diff --git a/src/products/components/ProductUpdatePage/form.tsx b/src/products/components/ProductUpdatePage/form.tsx index ff4c40393..c1f2e8fa2 100644 --- a/src/products/components/ProductUpdatePage/form.tsx +++ b/src/products/components/ProductUpdatePage/form.tsx @@ -23,6 +23,7 @@ import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; +import useRichText from "@saleor/utils/richText/useRichText"; import { diff } from "fast-array-diff"; import React from "react"; @@ -164,15 +165,10 @@ function useProductUpdateForm( ); const attributes = useFormset(getAttributeInputFromProduct(product)); const stocks = useFormset(getStockInputFromProduct(product)); - const description = React.useRef(); - - React.useEffect(() => { - try { - description.current = JSON.parse(product.descriptionJson); - } catch { - description.current = undefined; - } - }, [product]); + const [description, changeDescription] = useRichText({ + initial: product?.descriptionJson, + triggerChange + }); const { isMetadataModified, @@ -227,10 +223,6 @@ function useProductUpdateForm( opts.taxTypes ); const changeMetadata = makeMetadataChangeHandler(handleChange); - const changeDescription: RichTextEditorChange = data => { - triggerChange(); - description.current = data; - }; const data: ProductUpdateData = { ...form.data, diff --git a/src/utils/richText/useRichText.test.ts b/src/utils/richText/useRichText.test.ts new file mode 100644 index 000000000..26fc9c782 --- /dev/null +++ b/src/utils/richText/useRichText.test.ts @@ -0,0 +1,72 @@ +import { OutputData } from "@editorjs/editorjs"; +import { renderHook } from "@testing-library/react-hooks"; + +import useRichText from "./useRichText"; + +type Fixtures = Record<"short" | "long", OutputData>; +const fixtures: Fixtures = { + long: { + blocks: [ + { + data: { + level: 1, + text: "Some header" + }, + type: "header" + }, + { + data: { + text: "Some text" + }, + type: "paragraph" + } + ] + }, + short: { + blocks: [ + { + data: { + text: "Some text" + }, + type: "paragraph" + } + ] + } +}; + +describe("useRichText", () => { + it("properly saves data in form", () => { + const triggerChange = jest.fn(); + const hook = renderHook(() => + useRichText({ + triggerChange + }) + ); + + const [data, change] = hook.result.current; + expect(data.current).toBe(undefined); + + change(fixtures.short); + + expect(data.current).toMatchObject(fixtures.short); + expect(triggerChange).toHaveBeenCalled(); + }); + + it("properly updates data in form", () => { + const triggerChange = jest.fn(); + const hook = renderHook(() => + useRichText({ + initial: JSON.stringify(fixtures.short), + triggerChange + }) + ); + + const [data, change] = hook.result.current; + expect(data.current).toMatchObject(fixtures.short); + + change(fixtures.long); + + expect(data.current).toMatchObject(fixtures.long); + expect(triggerChange).toHaveBeenCalled(); + }); +}); diff --git a/src/utils/richText/useRichText.ts b/src/utils/richText/useRichText.ts new file mode 100644 index 000000000..58613193b --- /dev/null +++ b/src/utils/richText/useRichText.ts @@ -0,0 +1,28 @@ +import { OutputData } from "@editorjs/editorjs"; +import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; +import { MutableRefObject, useEffect, useRef } from "react"; + +function useRichText(opts: { + initial?: string | null; + triggerChange: () => void; +}): [MutableRefObject, RichTextEditorChange] { + const data = useRef( + opts.initial === null ? { blocks: [] } : undefined + ); + useEffect(() => { + try { + data.current = JSON.parse(opts.initial); + } catch { + data.current = undefined; + } + }, [opts.initial]); + + const change: RichTextEditorChange = newData => { + opts.triggerChange(); + data.current = newData; + }; + + return [data, change]; +} + +export default useRichText; From 28cc74c9548a7f2ab8b6c9a4cdd5f7eda631ec17 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 3 Nov 2020 17:17:37 +0100 Subject: [PATCH 06/23] Add rich text editor to product pages --- src/components/RichTextEditor/RichTextEditor.tsx | 13 +++++++++++++ .../components/ProductUpdatePage/form.tsx | 10 +--------- .../components/ProductVariantPage/form.tsx | 9 ++------- src/utils/metadata/getMetadata.ts | 16 ++++++++++++++++ src/utils/richText/useRichText.ts | 2 +- 5 files changed, 33 insertions(+), 17 deletions(-) create mode 100644 src/utils/metadata/getMetadata.ts diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index 2eb58bd04..0a5a6de76 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -74,6 +74,19 @@ const useStyles = makeStyles( "& .ce-inline-toolbar__toggler-and-button-wrapper": { paddingRight: 0 }, + "& .ce-toolbar__actions": { + right: 0, + top: 0 + }, + "& .ce-toolbar__content": { + maxWidth: "unset" + }, + "& .ce-toolbar__plus": { + left: -9 + }, + "& .ce-toolbox.ce-toolbox--opened": { + left: 16 + }, "& .codex-editor__redactor": { marginRight: `${theme.spacing(4)}px !important`, paddingBottom: "0 !important" diff --git a/src/products/components/ProductUpdatePage/form.tsx b/src/products/components/ProductUpdatePage/form.tsx index c1f2e8fa2..21e59cd0e 100644 --- a/src/products/components/ProductUpdatePage/form.tsx +++ b/src/products/components/ProductUpdatePage/form.tsx @@ -22,6 +22,7 @@ import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/Searc import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; +import getMetadata from "@saleor/utils/metadata/getMetadata"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useRichText from "@saleor/utils/richText/useRichText"; import { diff } from "fast-array-diff"; @@ -143,15 +144,6 @@ const getStocksData = ( }; }; -const getMetadata = ( - data: ProductUpdateFormData, - isMetadataModified: boolean, - isPrivateMetadataModified: boolean -) => ({ - metadata: isMetadataModified ? data.metadata : undefined, - privateMetadata: isPrivateMetadataModified ? data.privateMetadata : undefined -}); - function useProductUpdateForm( product: ProductDetails_product, onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise, diff --git a/src/products/components/ProductVariantPage/form.tsx b/src/products/components/ProductVariantPage/form.tsx index 9d9fc8203..b667c9cb4 100644 --- a/src/products/components/ProductVariantPage/form.tsx +++ b/src/products/components/ProductVariantPage/form.tsx @@ -11,6 +11,7 @@ import { } from "@saleor/products/utils/data"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { mapMetadataItemToInput } from "@saleor/utils/maps"; +import getMetadata from "@saleor/utils/metadata/getMetadata"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import { diff } from "fast-array-diff"; import React from "react"; @@ -117,11 +118,6 @@ function useProductVariantUpdateForm( stocks.remove(id); }; - const metadata = isMetadataModified ? form.data.metadata : undefined; - const privateMetadata = isPrivateMetadataModified - ? form.data.privateMetadata - : undefined; - const dataStocks = stocks.data.map(stock => stock.id); const variantStocks = variant?.stocks.map(stock => stock.warehouse.id) || []; const stockDiff = diff(variantStocks, dataStocks); @@ -140,10 +136,9 @@ function useProductVariantUpdateForm( }; const submitData: ProductVariantUpdateSubmitData = { ...form.data, + ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified), addStocks, attributes: attributes.data, - metadata, - privateMetadata, removeStocks: stockDiff.removed, updateStocks }; diff --git a/src/utils/metadata/getMetadata.ts b/src/utils/metadata/getMetadata.ts new file mode 100644 index 000000000..820f3a766 --- /dev/null +++ b/src/utils/metadata/getMetadata.ts @@ -0,0 +1,16 @@ +import { MetadataFormData } from "@saleor/components/Metadata"; + +function getMetadata( + data: MetadataFormData, + isMetadataModified: boolean, + isPrivateMetadataModified: boolean +) { + return { + metadata: isMetadataModified ? data.metadata : undefined, + privateMetadata: isPrivateMetadataModified + ? data.privateMetadata + : undefined + }; +} + +export default getMetadata; diff --git a/src/utils/richText/useRichText.ts b/src/utils/richText/useRichText.ts index 58613193b..fa51fb74a 100644 --- a/src/utils/richText/useRichText.ts +++ b/src/utils/richText/useRichText.ts @@ -3,7 +3,7 @@ import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import { MutableRefObject, useEffect, useRef } from "react"; function useRichText(opts: { - initial?: string | null; + initial: string | null; triggerChange: () => void; }): [MutableRefObject, RichTextEditorChange] { const data = useRef( From 9adde24a4813f25f3c429870c626de5cb2f66fbb Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 3 Nov 2020 17:17:47 +0100 Subject: [PATCH 07/23] Add rich text editor to category pages --- .../CategoryCreatePage/CategoryCreatePage.tsx | 128 +++----- .../components/CategoryCreatePage/form.tsx | 100 ++++++ .../CategoryDetailsForm.tsx | 19 +- .../CategoryUpdatePage/CategoryUpdatePage.tsx | 305 +++++++----------- .../components/CategoryUpdatePage/form.tsx | 107 ++++++ 5 files changed, 391 insertions(+), 268 deletions(-) create mode 100644 src/categories/components/CategoryCreatePage/form.tsx create mode 100644 src/categories/components/CategoryUpdatePage/form.tsx diff --git a/src/categories/components/CategoryCreatePage/CategoryCreatePage.tsx b/src/categories/components/CategoryCreatePage/CategoryCreatePage.tsx index 0ac329841..91b91d136 100644 --- a/src/categories/components/CategoryCreatePage/CategoryCreatePage.tsx +++ b/src/categories/components/CategoryCreatePage/CategoryCreatePage.tsx @@ -2,43 +2,23 @@ import AppHeader from "@saleor/components/AppHeader"; import { CardSpacer } from "@saleor/components/CardSpacer"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; -import Form from "@saleor/components/Form"; -import Metadata, { MetadataFormData } from "@saleor/components/Metadata"; +import Metadata from "@saleor/components/Metadata"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SeoForm from "@saleor/components/SeoForm"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import { sectionNames } from "@saleor/intl"; -import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; -import { ContentState, convertToRaw, RawDraftContentState } from "draft-js"; import React from "react"; import { useIntl } from "react-intl"; import CategoryDetailsForm from "../../components/CategoryDetailsForm"; - -export interface FormData extends MetadataFormData { - description: RawDraftContentState; - name: string; - slug: string; - seoTitle: string; - seoDescription: string; -} - -const initialData: FormData = { - description: convertToRaw(ContentState.createFromText("")), - metadata: [], - name: "", - privateMetadata: [], - seoDescription: "", - seoTitle: "", - slug: "" -}; +import CategoryCreateForm, { CategoryCreateData } from "./form"; export interface CategoryCreatePageProps { errors: ProductErrorFragment[]; disabled: boolean; saveButtonBarState: ConfirmButtonTransitionState; - onSubmit(data: FormData); + onSubmit(data: CategoryCreateData); onBack(); } @@ -50,63 +30,57 @@ export const CategoryCreatePage: React.FC = ({ saveButtonBarState }) => { const intl = useIntl(); - const { - makeChangeHandler: makeMetadataChangeHandler - } = useMetadataChangeTrigger(); return ( -
- {({ data, change, submit, hasChanged }) => { - const changeMetadata = makeMetadataChangeHandler(change); - - return ( - - - {intl.formatMessage(sectionNames.categories)} - - + {({ data, change, handlers, submit, hasChanged }) => ( + + + {intl.formatMessage(sectionNames.categories)} + + +
+ -
- - - - - - -
- - ); - }} - + + + + + +
+
+ )} + ); }; CategoryCreatePage.displayName = "CategoryCreatePage"; diff --git a/src/categories/components/CategoryCreatePage/form.tsx b/src/categories/components/CategoryCreatePage/form.tsx new file mode 100644 index 000000000..7c1344a0c --- /dev/null +++ b/src/categories/components/CategoryCreatePage/form.tsx @@ -0,0 +1,100 @@ +import { OutputData } from "@editorjs/editorjs"; +import { MetadataFormData } from "@saleor/components/Metadata"; +import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; +import useForm, { FormChange } from "@saleor/hooks/useForm"; +import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; +import getMetadata from "@saleor/utils/metadata/getMetadata"; +import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; +import useRichText from "@saleor/utils/richText/useRichText"; +import React from "react"; + +export interface CategoryCreateFormData extends MetadataFormData { + name: string; + seoDescription: string; + seoTitle: string; + slug: string; +} +export interface CategoryCreateData extends CategoryCreateFormData { + description: OutputData; +} + +interface CategoryCreateHandlers { + changeMetadata: FormChange; + changeDescription: RichTextEditorChange; +} +export interface UseCategoryCreateFormResult { + change: FormChange; + data: CategoryCreateData; + handlers: CategoryCreateHandlers; + hasChanged: boolean; + submit: () => Promise; +} + +export interface CategoryCreateFormProps { + children: (props: UseCategoryCreateFormResult) => React.ReactNode; + onSubmit: (data: CategoryCreateData) => Promise; +} + +function useCategoryCreateForm( + onSubmit: (data: CategoryCreateData) => Promise +): UseCategoryCreateFormResult { + const [changed, setChanged] = React.useState(false); + const triggerChange = () => setChanged(true); + + const form = useForm({ + metadata: [], + name: "", + privateMetadata: [], + seoDescription: "", + seoTitle: "", + slug: "" + }); + const [description, changeDescription] = useRichText({ + initial: null, + triggerChange + }); + + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + + const handleChange: FormChange = (event, cb) => { + form.change(event, cb); + triggerChange(); + }; + const changeMetadata = makeMetadataChangeHandler(handleChange); + + // Need to make it function to always have description.current up to date + const getData = (): CategoryCreateData => ({ + ...form.data, + ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified), + description: description.current + }); + + const submit = () => handleFormSubmit(getData(), onSubmit, setChanged); + + return { + change: handleChange, + data: getData(), + handlers: { + changeDescription, + changeMetadata + }, + hasChanged: changed, + submit + }; +} + +const CategoryCreateForm: React.FC = ({ + children, + onSubmit +}) => { + const props = useCategoryCreateForm(onSubmit); + + return
{children(props)}
; +}; + +CategoryCreateForm.displayName = "CategoryCreateForm"; +export default CategoryCreateForm; diff --git a/src/categories/components/CategoryDetailsForm/CategoryDetailsForm.tsx b/src/categories/components/CategoryDetailsForm/CategoryDetailsForm.tsx index 4779571ba..d1e984b25 100644 --- a/src/categories/components/CategoryDetailsForm/CategoryDetailsForm.tsx +++ b/src/categories/components/CategoryDetailsForm/CategoryDetailsForm.tsx @@ -1,35 +1,34 @@ +import { OutputData } from "@editorjs/editorjs"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import TextField from "@material-ui/core/TextField"; import CardTitle from "@saleor/components/CardTitle"; import FormSpacer from "@saleor/components/FormSpacer"; -import RichTextEditor from "@saleor/components/RichTextEditor"; +import RichTextEditor, { + RichTextEditorChange +} from "@saleor/components/RichTextEditor"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import { commonMessages } from "@saleor/intl"; import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors"; -import { RawDraftContentState } from "draft-js"; import React from "react"; import { useIntl } from "react-intl"; -import { maybe } from "../../../misc"; -import { CategoryDetails_category } from "../../types/CategoryDetails"; - interface CategoryDetailsFormProps { - category?: CategoryDetails_category; data: { name: string; - description: RawDraftContentState; + description: OutputData; }; disabled: boolean; errors: ProductErrorFragment[]; onChange: (event: React.ChangeEvent) => void; + onDescriptionChange: RichTextEditorChange; } export const CategoryDetailsForm: React.FC = ({ - category, disabled, data, onChange, + onDescriptionChange, errors }) => { const intl = useIntl(); @@ -58,15 +57,15 @@ export const CategoryDetailsForm: React.FC = ({
JSON.parse(category.descriptionJson))} name="description" - onChange={onChange} + onChange={onDescriptionChange} /> diff --git a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx index 96c46e1c2..9446f74c3 100644 --- a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx +++ b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx @@ -5,9 +5,7 @@ import { CardSpacer } from "@saleor/components/CardSpacer"; import CardTitle from "@saleor/components/CardTitle"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; -import Form from "@saleor/components/Form"; import Metadata from "@saleor/components/Metadata/Metadata"; -import { MetadataFormData } from "@saleor/components/Metadata/types"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SeoForm from "@saleor/components/SeoForm"; @@ -15,9 +13,6 @@ import { Tab, TabContainer } from "@saleor/components/Tab"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import { SubmitPromise } from "@saleor/hooks/useForm"; import { sectionNames } from "@saleor/intl"; -import { mapMetadataItemToInput } from "@saleor/utils/maps"; -import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; -import { RawDraftContentState } from "draft-js"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -32,15 +27,7 @@ import { } from "../../types/CategoryDetails"; import CategoryBackground from "../CategoryBackground"; import CategoryProducts from "../CategoryProducts"; - -export interface FormData extends MetadataFormData { - backgroundImageAlt: string; - description: RawDraftContentState; - name: string; - slug: string; - seoTitle: string; - seoDescription: string; -} +import CategoryUpdateForm, { CategoryUpdateData } from "./form"; export enum CategoryPageTab { categories = "categories", @@ -62,7 +49,7 @@ export interface CategoryUpdatePageProps }; saveButtonBarState: ConfirmButtonTransitionState; onImageDelete: () => void; - onSubmit: (data: FormData) => SubmitPromise; + onSubmit: (data: CategoryUpdateData) => SubmitPromise; onImageUpload(file: File); onNextPage(); onPreviousPage(); @@ -106,180 +93,136 @@ export const CategoryUpdatePage: React.FC = ({ toggleAll }: CategoryUpdatePageProps) => { const intl = useIntl(); - const { - isMetadataModified, - isPrivateMetadataModified, - makeChangeHandler: makeMetadataChangeHandler - } = useMetadataChangeTrigger(); - - const initialData: FormData = category - ? { - backgroundImageAlt: maybe(() => category.backgroundImage.alt, ""), - description: maybe(() => JSON.parse(category.descriptionJson)), - metadata: category?.metadata?.map(mapMetadataItemToInput), - name: category.name || "", - privateMetadata: category?.privateMetadata?.map(mapMetadataItemToInput), - seoDescription: category.seoDescription || "", - seoTitle: category.seoTitle || "", - slug: category?.slug || "" - } - : { - backgroundImageAlt: "", - description: "", - metadata: undefined, - name: "", - privateMetadata: undefined, - seoDescription: "", - seoTitle: "", - slug: "" - }; - - const handleSubmit = (data: FormData) => { - const metadata = isMetadataModified ? data.metadata : undefined; - const privateMetadata = isPrivateMetadataModified - ? data.privateMetadata - : undefined; - - return onSubmit({ - ...data, - metadata, - privateMetadata - }); - }; return ( -
- {({ data, change, submit, hasChanged }) => { - const changeMetadata = makeMetadataChangeHandler(change); - - return ( - - - {intl.formatMessage(sectionNames.categories)} - - - - - category.backgroundImage)} - onChange={change} - /> - - - - - - - - - - - - - - - {currentTab === CategoryPageTab.categories && ( - - - - - } - /> - undefined} - /> - - )} - {currentTab === CategoryPageTab.products && ( - category.name)} - products={products} + + {({ data, change, handlers, submit, hasChanged }) => ( + + + {intl.formatMessage(sectionNames.categories)} + + + + + category.backgroundImage)} + onChange={change} + /> + + + + + + + + + + + + + + + {currentTab === CategoryPageTab.categories && ( + + + + + } + /> + undefined} /> - )} - + )} + {currentTab === CategoryPageTab.products && ( + - - ); - }} - + )} + +
+ )} + ); }; CategoryUpdatePage.displayName = "CategoryUpdatePage"; diff --git a/src/categories/components/CategoryUpdatePage/form.tsx b/src/categories/components/CategoryUpdatePage/form.tsx new file mode 100644 index 000000000..a98c80c1f --- /dev/null +++ b/src/categories/components/CategoryUpdatePage/form.tsx @@ -0,0 +1,107 @@ +import { OutputData } from "@editorjs/editorjs"; +import { CategoryDetails_category } from "@saleor/categories/types/CategoryDetails"; +import { MetadataFormData } from "@saleor/components/Metadata"; +import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; +import useForm, { FormChange } from "@saleor/hooks/useForm"; +import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; +import { mapMetadataItemToInput } from "@saleor/utils/maps"; +import getMetadata from "@saleor/utils/metadata/getMetadata"; +import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; +import useRichText from "@saleor/utils/richText/useRichText"; +import React from "react"; + +export interface CategoryUpdateFormData extends MetadataFormData { + backgroundImageAlt: string; + name: string; + slug: string; + seoTitle: string; + seoDescription: string; +} +export interface CategoryUpdateData extends CategoryUpdateFormData { + description: OutputData; +} + +interface CategoryUpdateHandlers { + changeMetadata: FormChange; + changeDescription: RichTextEditorChange; +} +export interface UseCategoryUpdateFormResult { + change: FormChange; + data: CategoryUpdateData; + handlers: CategoryUpdateHandlers; + hasChanged: boolean; + submit: () => Promise; +} + +export interface CategoryUpdateFormProps { + children: (props: UseCategoryUpdateFormResult) => React.ReactNode; + category: CategoryDetails_category; + onSubmit: (data: CategoryUpdateData) => Promise; +} + +function useCategoryUpdateForm( + category: CategoryDetails_category, + onSubmit: (data: CategoryUpdateData) => Promise +): UseCategoryUpdateFormResult { + const [changed, setChanged] = React.useState(false); + const triggerChange = () => setChanged(true); + + const form = useForm({ + backgroundImageAlt: category?.backgroundImage?.alt || "", + metadata: category?.metadata?.map(mapMetadataItemToInput), + name: category?.name || "", + privateMetadata: category?.privateMetadata?.map(mapMetadataItemToInput), + seoDescription: category?.seoDescription || "", + seoTitle: category?.seoTitle || "", + slug: category?.slug || "" + }); + const [description, changeDescription] = useRichText({ + initial: category?.descriptionJson, + triggerChange + }); + + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + + const handleChange: FormChange = (event, cb) => { + form.change(event, cb); + triggerChange(); + }; + const changeMetadata = makeMetadataChangeHandler(handleChange); + + // Need to make it function to always have description.current up to date + const getData = (): CategoryUpdateData => ({ + ...form.data, + ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified), + description: description.current + }); + + const submit = () => handleFormSubmit(getData(), onSubmit, setChanged); + + return { + change: handleChange, + data: getData(), + handlers: { + changeDescription, + changeMetadata + }, + hasChanged: changed, + submit + }; +} + +const CategoryUpdateForm: React.FC = ({ + children, + category, + onSubmit +}) => { + const props = useCategoryUpdateForm(category, onSubmit); + + return
{children(props)}
; +}; + +CategoryUpdateForm.displayName = "CategoryUpdateForm"; +export default CategoryUpdateForm; From 06ef285d15bb70c37b4b26d2145725b9994efbd2 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 3 Nov 2020 17:49:11 +0100 Subject: [PATCH 08/23] Fix metadata --- src/categories/components/CategoryCreatePage/form.tsx | 3 --- src/categories/components/CategoryUpdatePage/form.tsx | 6 +++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/categories/components/CategoryCreatePage/form.tsx b/src/categories/components/CategoryCreatePage/form.tsx index 7c1344a0c..0393a005a 100644 --- a/src/categories/components/CategoryCreatePage/form.tsx +++ b/src/categories/components/CategoryCreatePage/form.tsx @@ -55,8 +55,6 @@ function useCategoryCreateForm( }); const { - isMetadataModified, - isPrivateMetadataModified, makeChangeHandler: makeMetadataChangeHandler } = useMetadataChangeTrigger(); @@ -69,7 +67,6 @@ function useCategoryCreateForm( // Need to make it function to always have description.current up to date const getData = (): CategoryCreateData => ({ ...form.data, - ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified), description: description.current }); diff --git a/src/categories/components/CategoryUpdatePage/form.tsx b/src/categories/components/CategoryUpdatePage/form.tsx index a98c80c1f..809b5a851 100644 --- a/src/categories/components/CategoryUpdatePage/form.tsx +++ b/src/categories/components/CategoryUpdatePage/form.tsx @@ -78,8 +78,12 @@ function useCategoryUpdateForm( ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified), description: description.current }); + const getSubmitData = (): CategoryUpdateData => ({ + ...getData(), + ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified) + }); - const submit = () => handleFormSubmit(getData(), onSubmit, setChanged); + const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged); return { change: handleChange, From b27f28c8226814482d709a811dc02c4813319b60 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 3 Nov 2020 17:49:42 +0100 Subject: [PATCH 09/23] Add rich text editor to collection pages --- .../CollectionCreatePage.tsx | 266 ++++++++---------- .../components/CollectionCreatePage/form.tsx | 111 ++++++++ .../CollectionDetails/CollectionDetails.tsx | 19 +- .../CollectionDetailsPage.tsx | 253 +++++++---------- .../components/CollectionDetailsPage/form.tsx | 121 ++++++++ 5 files changed, 454 insertions(+), 316 deletions(-) create mode 100644 src/collections/components/CollectionCreatePage/form.tsx create mode 100644 src/collections/components/CollectionDetailsPage/form.tsx diff --git a/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx b/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx index 777ef78c7..a54235658 100644 --- a/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx +++ b/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx @@ -2,9 +2,8 @@ import AppHeader from "@saleor/components/AppHeader"; import { CardSpacer } from "@saleor/components/CardSpacer"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { Container } from "@saleor/components/Container"; -import Form from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; -import Metadata, { MetadataFormData } from "@saleor/components/Metadata"; +import Metadata from "@saleor/components/Metadata"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SeoForm from "@saleor/components/SeoForm"; @@ -12,54 +11,21 @@ import VisibilityCard from "@saleor/components/VisibilityCard"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import useDateLocalize from "@saleor/hooks/useDateLocalize"; import { sectionNames } from "@saleor/intl"; -import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; -import { ContentState, convertToRaw, RawDraftContentState } from "draft-js"; import React from "react"; import { useIntl } from "react-intl"; import CollectionDetails from "../CollectionDetails/CollectionDetails"; import { CollectionImage } from "../CollectionImage/CollectionImage"; - -export interface CollectionCreatePageFormData extends MetadataFormData { - backgroundImage: { - url: string; - value: string; - }; - backgroundImageAlt: string; - description: RawDraftContentState; - name: string; - slug: string; - publicationDate: string; - isPublished: boolean; - seoDescription: string; - seoTitle: string; -} +import CollectionCreateForm, { CollectionCreateData } from "./form"; export interface CollectionCreatePageProps { disabled: boolean; errors: ProductErrorFragment[]; saveButtonBarState: ConfirmButtonTransitionState; onBack: () => void; - onSubmit: (data: CollectionCreatePageFormData) => void; + onSubmit: (data: CollectionCreateData) => Promise; } -const initialForm: CollectionCreatePageFormData = { - backgroundImage: { - url: null, - value: null - }, - backgroundImageAlt: "", - description: convertToRaw(ContentState.createFromText("")), - isPublished: false, - metadata: [], - name: "", - privateMetadata: [], - publicationDate: "", - seoDescription: "", - seoTitle: "", - slug: "" -}; - const CollectionCreatePage: React.FC = ({ disabled, errors, @@ -69,127 +35,121 @@ const CollectionCreatePage: React.FC = ({ }: CollectionCreatePageProps) => { const intl = useIntl(); const localizeDate = useDateLocalize(); - const { - makeChangeHandler: makeMetadataChangeHandler - } = useMetadataChangeTrigger(); return ( -
- {({ change, data, hasChanged, submit }) => { - const changeMetadata = makeMetadataChangeHandler(change); - - return ( - - - {intl.formatMessage(sectionNames.collections)} - - - -
- - - - change({ - target: { - name: "backgroundImage", - value: { - url: null, - value: null - } + + {({ change, data, handlers, hasChanged, submit }) => ( + + + {intl.formatMessage(sectionNames.collections)} + + + +
+ + + - change({ - target: { - name: "backgroundImage", - value: { - url: URL.createObjectURL(file), - value: file - } + : null + } + onImageDelete={() => + change({ + target: { + name: "backgroundImage", + value: { + url: null, + value: null } - } as any) - } - onChange={change} - data={data} - /> - - - - -
-
- + change({ + target: { + name: "backgroundImage", + value: { + url: URL.createObjectURL(file), + value: file } - ), - visibleLabel: intl.formatMessage({ - defaultMessage: "Visible", - description: "collection label" - }) - }} - onChange={change} - /> -
-
- -
- ); - }} - + } + } as any) + } + onChange={change} + data={data} + /> + + + + +
+
+ +
+
+ +
+ )} + ); }; CollectionCreatePage.displayName = "CollectionCreatePage"; diff --git a/src/collections/components/CollectionCreatePage/form.tsx b/src/collections/components/CollectionCreatePage/form.tsx new file mode 100644 index 000000000..4a49c03cf --- /dev/null +++ b/src/collections/components/CollectionCreatePage/form.tsx @@ -0,0 +1,111 @@ +import { OutputData } from "@editorjs/editorjs"; +import { MetadataFormData } from "@saleor/components/Metadata"; +import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; +import useForm, { FormChange } from "@saleor/hooks/useForm"; +import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; +import getMetadata from "@saleor/utils/metadata/getMetadata"; +import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; +import useRichText from "@saleor/utils/richText/useRichText"; +import React from "react"; + +export interface CollectionCreateFormData extends MetadataFormData { + backgroundImage: { + url: string; + value: string; + }; + backgroundImageAlt: string; + name: string; + slug: string; + publicationDate: string; + isPublished: boolean; + seoDescription: string; + seoTitle: string; +} +export interface CollectionCreateData extends CollectionCreateFormData { + description: OutputData; +} + +interface CollectionCreateHandlers { + changeMetadata: FormChange; + changeDescription: RichTextEditorChange; +} +export interface UseCollectionCreateFormResult { + change: FormChange; + data: CollectionCreateData; + handlers: CollectionCreateHandlers; + hasChanged: boolean; + submit: () => Promise; +} + +export interface CollectionCreateFormProps { + children: (props: UseCollectionCreateFormResult) => React.ReactNode; + onSubmit: (data: CollectionCreateData) => Promise; +} + +function useCollectionCreateForm( + onSubmit: (data: CollectionCreateData) => Promise +): UseCollectionCreateFormResult { + const [changed, setChanged] = React.useState(false); + const triggerChange = () => setChanged(true); + + const form = useForm({ + backgroundImage: { + url: null, + value: null + }, + backgroundImageAlt: "", + isPublished: false, + metadata: [], + name: "", + privateMetadata: [], + publicationDate: "", + seoDescription: "", + seoTitle: "", + slug: "" + }); + const [description, changeDescription] = useRichText({ + initial: null, + triggerChange + }); + + const { + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + + const handleChange: FormChange = (event, cb) => { + form.change(event, cb); + triggerChange(); + }; + const changeMetadata = makeMetadataChangeHandler(handleChange); + + // Need to make it function to always have description.current up to date + const getData = (): CollectionCreateData => ({ + ...form.data, + description: description.current + }); + + const submit = () => handleFormSubmit(getData(), onSubmit, setChanged); + + return { + change: handleChange, + data: getData(), + handlers: { + changeDescription, + changeMetadata + }, + hasChanged: changed, + submit + }; +} + +const CollectionCreateForm: React.FC = ({ + children, + onSubmit +}) => { + const props = useCollectionCreateForm(onSubmit); + + return
{children(props)}
; +}; + +CollectionCreateForm.displayName = "CollectionCreateForm"; +export default CollectionCreateForm; diff --git a/src/collections/components/CollectionDetails/CollectionDetails.tsx b/src/collections/components/CollectionDetails/CollectionDetails.tsx index 2de3ae015..90187bcd0 100644 --- a/src/collections/components/CollectionDetails/CollectionDetails.tsx +++ b/src/collections/components/CollectionDetails/CollectionDetails.tsx @@ -1,35 +1,34 @@ +import { OutputData } from "@editorjs/editorjs"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import TextField from "@material-ui/core/TextField"; import CardTitle from "@saleor/components/CardTitle"; import FormSpacer from "@saleor/components/FormSpacer"; -import RichTextEditor from "@saleor/components/RichTextEditor"; +import RichTextEditor, { + RichTextEditorChange +} from "@saleor/components/RichTextEditor"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import { commonMessages } from "@saleor/intl"; -import { maybe } from "@saleor/misc"; import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors"; -import { RawDraftContentState } from "draft-js"; import React from "react"; import { useIntl } from "react-intl"; -import { CollectionDetails_collection } from "../../types/CollectionDetails"; - export interface CollectionDetailsProps { - collection?: CollectionDetails_collection; data: { - description: RawDraftContentState; + description: OutputData; name: string; }; disabled: boolean; errors: ProductErrorFragment[]; onChange: (event: React.ChangeEvent) => void; + onDescriptionChange: RichTextEditorChange; } const CollectionDetails: React.FC = ({ - collection, disabled, data, onChange, + onDescriptionChange, errors }) => { const intl = useIntl(); @@ -57,13 +56,13 @@ const CollectionDetails: React.FC = ({ /> JSON.parse(collection.descriptionJson))} label={intl.formatMessage(commonMessages.description)} name="description" disabled={disabled} - onChange={onChange} + onChange={onDescriptionChange} /> diff --git a/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx b/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx index 0b37e6e51..4728cec71 100644 --- a/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx +++ b/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.tsx @@ -3,12 +3,10 @@ import { CardSpacer } from "@saleor/components/CardSpacer"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { Container } from "@saleor/components/Container"; import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; -import Form from "@saleor/components/Form"; import FormSpacer from "@saleor/components/FormSpacer"; import Grid from "@saleor/components/Grid"; import Hr from "@saleor/components/Hr"; import Metadata from "@saleor/components/Metadata/Metadata"; -import { MetadataFormData } from "@saleor/components/Metadata/types"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SeoForm from "@saleor/components/SeoForm"; @@ -16,9 +14,6 @@ import VisibilityCard from "@saleor/components/VisibilityCard"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import useDateLocalize from "@saleor/hooks/useDateLocalize"; import { sectionNames } from "@saleor/intl"; -import { mapMetadataItemToInput } from "@saleor/utils/maps"; -import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; -import { RawDraftContentState } from "draft-js"; import React from "react"; import { useIntl } from "react-intl"; @@ -28,18 +23,7 @@ import { CollectionDetails_collection } from "../../types/CollectionDetails"; import CollectionDetails from "../CollectionDetails/CollectionDetails"; import { CollectionImage } from "../CollectionImage/CollectionImage"; import CollectionProducts from "../CollectionProducts/CollectionProducts"; - -export interface CollectionDetailsPageFormData extends MetadataFormData { - backgroundImageAlt: string; - description: RawDraftContentState; - name: string; - slug: string; - publicationDate: string; - seoDescription: string; - seoTitle: string; - isFeatured: boolean; - isPublished: boolean; -} +import CollectionUpdateForm, { CollectionUpdateData } from "./form"; export interface CollectionDetailsPageProps extends PageListProps, ListActions { collection: CollectionDetails_collection; @@ -51,7 +35,7 @@ export interface CollectionDetailsPageProps extends PageListProps, ListActions { onImageDelete: () => void; onImageUpload: (file: File) => void; onProductUnassign: (id: string, event: React.MouseEvent) => void; - onSubmit: (data: CollectionDetailsPageFormData) => void; + onSubmit: (data: CollectionUpdateData) => Promise; } const CollectionDetailsPage: React.FC = ({ @@ -69,150 +53,113 @@ const CollectionDetailsPage: React.FC = ({ }: CollectionDetailsPageProps) => { const intl = useIntl(); const localizeDate = useDateLocalize(); - const { - isMetadataModified, - isPrivateMetadataModified, - makeChangeHandler: makeMetadataChangeHandler - } = useMetadataChangeTrigger(); - - const handleSubmit = (data: CollectionDetailsPageFormData) => { - const metadata = isMetadataModified ? data.metadata : undefined; - const privateMetadata = isPrivateMetadataModified - ? data.privateMetadata - : undefined; - - return onSubmit({ - ...data, - isPublished: data.isPublished || !!data.publicationDate, - metadata, - privateMetadata - }); - }; return ( -
collection.backgroundImage.alt, ""), - description: maybe(() => JSON.parse(collection.descriptionJson)), - isFeatured, - isPublished: maybe(() => collection.isPublished, false), - metadata: collection?.metadata?.map(mapMetadataItemToInput), - name: maybe(() => collection.name, ""), - privateMetadata: collection?.privateMetadata?.map( - mapMetadataItemToInput - ), - publicationDate: maybe(() => collection.publicationDate, ""), - seoDescription: maybe(() => collection.seoDescription, ""), - seoTitle: maybe(() => collection.seoTitle, ""), - slug: collection?.slug || "" - }} - onSubmit={handleSubmit} - confirmLeave + - {({ change, data, hasChanged, submit }) => { - const changeMetadata = makeMetadataChangeHandler(change); - - return ( - - - {intl.formatMessage(sectionNames.collections)} - - collection.name)} /> - + {({ change, data, handlers, hasChanged, submit }) => ( + + + {intl.formatMessage(sectionNames.collections)} + + collection.name)} /> + +
+ + + collection.backgroundImage)} + onImageDelete={onImageDelete} + onImageUpload={onImageUpload} + onChange={change} + /> + + + + + + collection.name)} + onChange={change} + /> +
+
- - - collection.backgroundImage)} - onImageDelete={onImageDelete} - onImageUpload={onImageUpload} - onChange={change} - /> - - - - - - collection.name)} - onChange={change} - /> -
-
-
- + +
+ - -
- -
-
+ disabled={disabled} + /> +
- - - - ); - }} - +
+
+ +
+ )} +
); }; CollectionDetailsPage.displayName = "CollectionDetailsPage"; diff --git a/src/collections/components/CollectionDetailsPage/form.tsx b/src/collections/components/CollectionDetailsPage/form.tsx new file mode 100644 index 000000000..e5cb9bfc7 --- /dev/null +++ b/src/collections/components/CollectionDetailsPage/form.tsx @@ -0,0 +1,121 @@ +import { OutputData } from "@editorjs/editorjs"; +import { CollectionDetails_collection } from "@saleor/collections/types/CollectionDetails"; +import { MetadataFormData } from "@saleor/components/Metadata"; +import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; +import useForm, { FormChange } from "@saleor/hooks/useForm"; +import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; +import { mapMetadataItemToInput } from "@saleor/utils/maps"; +import getMetadata from "@saleor/utils/metadata/getMetadata"; +import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; +import useRichText from "@saleor/utils/richText/useRichText"; +import React from "react"; + +export interface CollectionUpdateFormData extends MetadataFormData { + backgroundImageAlt: string; + name: string; + slug: string; + publicationDate: string; + seoDescription: string; + seoTitle: string; + isFeatured: boolean; + isPublished: boolean; +} +export interface CollectionUpdateData extends CollectionUpdateFormData { + description: OutputData; +} + +interface CollectionUpdateHandlers { + changeMetadata: FormChange; + changeDescription: RichTextEditorChange; +} +export interface UseCollectionUpdateFormResult { + change: FormChange; + data: CollectionUpdateData; + handlers: CollectionUpdateHandlers; + hasChanged: boolean; + submit: () => Promise; +} + +export interface CollectionUpdateFormProps { + children: (props: UseCollectionUpdateFormResult) => React.ReactNode; + collection: CollectionDetails_collection; + isFeatured: boolean; + onSubmit: (data: CollectionUpdateData) => Promise; +} + +function useCollectionUpdateForm( + collection: CollectionDetails_collection, + onSubmit: (data: CollectionUpdateData) => Promise, + isFeatured: boolean +): UseCollectionUpdateFormResult { + const [changed, setChanged] = React.useState(false); + const triggerChange = () => setChanged(true); + + const form = useForm({ + backgroundImageAlt: collection?.backgroundImage?.alt || "", + isFeatured, + isPublished: !!collection?.isPublished, + metadata: collection?.metadata?.map(mapMetadataItemToInput), + name: collection?.name || "", + privateMetadata: collection?.privateMetadata?.map(mapMetadataItemToInput), + publicationDate: collection?.publicationDate || "", + seoDescription: collection?.seoDescription || "", + seoTitle: collection?.seoTitle || "", + slug: collection?.slug || "" + }); + const [description, changeDescription] = useRichText({ + initial: collection?.descriptionJson, + triggerChange + }); + + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + + const handleChange: FormChange = (event, cb) => { + form.change(event, cb); + triggerChange(); + }; + const changeMetadata = makeMetadataChangeHandler(handleChange); + + // Need to make it function to always have description.current up to date + const getData = (): CollectionUpdateData => ({ + ...form.data, + description: description.current, + isPublished: form.data.isPublished || !!form.data.publicationDate + }); + + const getSubmitData = (): CollectionUpdateData => ({ + ...getData(), + ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified) + }); + + const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged); + + return { + change: handleChange, + data: getData(), + handlers: { + changeDescription, + changeMetadata + }, + hasChanged: changed, + submit + }; +} + +const CollectionUpdateForm: React.FC = ({ + children, + collection, + isFeatured, + onSubmit +}) => { + const props = useCollectionUpdateForm(collection, onSubmit, isFeatured); + + return
{children(props)}
; +}; + +CollectionUpdateForm.displayName = "CollectionUpdateForm"; +export default CollectionUpdateForm; From aa3559117dde909d568da27e1300d1a909c5ac4e Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 3 Nov 2020 18:25:35 +0100 Subject: [PATCH 10/23] Fix isPublished flag --- src/collections/components/CollectionDetailsPage/form.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/collections/components/CollectionDetailsPage/form.tsx b/src/collections/components/CollectionDetailsPage/form.tsx index e5cb9bfc7..a5dd5417e 100644 --- a/src/collections/components/CollectionDetailsPage/form.tsx +++ b/src/collections/components/CollectionDetailsPage/form.tsx @@ -83,13 +83,13 @@ function useCollectionUpdateForm( // Need to make it function to always have description.current up to date const getData = (): CollectionUpdateData => ({ ...form.data, - description: description.current, - isPublished: form.data.isPublished || !!form.data.publicationDate + description: description.current }); const getSubmitData = (): CollectionUpdateData => ({ ...getData(), - ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified) + ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified), + isPublished: form.data.isPublished || !!form.data.publicationDate }); const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged); From 7c9f2a93d41d54a1f98b8c0a0ec58ae6712b95c3 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 4 Nov 2020 11:48:49 +0100 Subject: [PATCH 11/23] Add rich text editor to pages --- .../components/CollectionDetailsPage/form.tsx | 3 +- src/collections/views/CollectionCreate.tsx | 16 +- .../PageDetailsPage/PageDetailsPage.tsx | 239 +++++++----------- src/pages/components/PageDetailsPage/form.tsx | 118 +++++++++ src/pages/components/PageInfo/PageInfo.tsx | 22 +- src/pages/mutations.ts | 1 + src/pages/views/PageCreate.tsx | 7 +- src/utils/data/getPublicationData.ts | 16 ++ src/utils/richText/useRichText.test.ts | 1 + src/utils/richText/useRichText.ts | 4 +- 10 files changed, 246 insertions(+), 181 deletions(-) create mode 100644 src/pages/components/PageDetailsPage/form.tsx create mode 100644 src/utils/data/getPublicationData.ts diff --git a/src/collections/components/CollectionDetailsPage/form.tsx b/src/collections/components/CollectionDetailsPage/form.tsx index a5dd5417e..00364cf2b 100644 --- a/src/collections/components/CollectionDetailsPage/form.tsx +++ b/src/collections/components/CollectionDetailsPage/form.tsx @@ -3,6 +3,7 @@ import { CollectionDetails_collection } from "@saleor/collections/types/Collecti import { MetadataFormData } from "@saleor/components/Metadata"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import useForm, { FormChange } from "@saleor/hooks/useForm"; +import getPublicationData from "@saleor/utils/data/getPublicationData"; import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import { mapMetadataItemToInput } from "@saleor/utils/maps"; import getMetadata from "@saleor/utils/metadata/getMetadata"; @@ -89,7 +90,7 @@ function useCollectionUpdateForm( const getSubmitData = (): CollectionUpdateData => ({ ...getData(), ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified), - isPublished: form.data.isPublished || !!form.data.publicationDate + ...getPublicationData(form.data) }); const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged); diff --git a/src/collections/views/CollectionCreate.tsx b/src/collections/views/CollectionCreate.tsx index 549a93efc..277c67be9 100644 --- a/src/collections/views/CollectionCreate.tsx +++ b/src/collections/views/CollectionCreate.tsx @@ -2,6 +2,7 @@ import { WindowTitle } from "@saleor/components/WindowTitle"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import { commonMessages } from "@saleor/intl"; +import getPublicationData from "@saleor/utils/data/getPublicationData"; import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; import { useMetadataUpdate, @@ -11,9 +12,8 @@ import React from "react"; import { useIntl } from "react-intl"; import { CollectionCreateInput } from "../../types/globalTypes"; -import CollectionCreatePage, { - CollectionCreatePageFormData -} from "../components/CollectionCreatePage/CollectionCreatePage"; +import CollectionCreatePage from "../components/CollectionCreatePage/CollectionCreatePage"; +import { CollectionCreateData } from "../components/CollectionCreatePage/form"; import { useCollectionCreateMutation } from "../mutations"; import { collectionListUrl, collectionUrl } from "../urls"; @@ -47,15 +47,7 @@ export const CollectionCreate: React.FC = () => { } }); - const getPublicationData = ({ - publicationDate, - isPublished - }: CollectionCreatePageFormData) => ({ - isPublished: !!publicationDate || isPublished, - publicationDate: publicationDate || null - }); - - const handleCreate = async (formData: CollectionCreatePageFormData) => { + const handleCreate = async (formData: CollectionCreateData) => { const result = await createCollection({ variables: { input: { diff --git a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx index 739433f0c..23e40fd48 100644 --- a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx +++ b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx @@ -2,9 +2,8 @@ import AppHeader from "@saleor/components/AppHeader"; import CardSpacer from "@saleor/components/CardSpacer"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; -import Form from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; -import Metadata, { MetadataFormData } from "@saleor/components/Metadata"; +import Metadata from "@saleor/components/Metadata"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SeoForm from "@saleor/components/SeoForm"; @@ -13,30 +12,12 @@ import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; import useDateLocalize from "@saleor/hooks/useDateLocalize"; import { SubmitPromise } from "@saleor/hooks/useForm"; import { sectionNames } from "@saleor/intl"; -import { mapMetadataItemToInput } from "@saleor/utils/maps"; -import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; -import { - ContentState, - convertFromRaw, - convertToRaw, - RawDraftContentState -} from "draft-js"; import React from "react"; import { useIntl } from "react-intl"; -import { maybe } from "../../../misc"; import { PageDetails_page } from "../../types/PageDetails"; import PageInfo from "../PageInfo"; - -export interface PageDetailsPageFormData extends MetadataFormData { - content: RawDraftContentState; - isPublished: boolean; - publicationDate: string; - seoDescription: string; - seoTitle: string; - slug: string; - title: string; -} +import PageForm, { PageData } from "./form"; export interface PageDetailsPageProps { disabled: boolean; @@ -46,7 +27,7 @@ export interface PageDetailsPageProps { saveButtonBarState: ConfirmButtonTransitionState; onBack: () => void; onRemove: () => void; - onSubmit: (data: PageDetailsPageFormData) => SubmitPromise; + onSubmit: (data: PageData) => SubmitPromise; } const PageDetailsPage: React.FC = ({ @@ -60,140 +41,94 @@ const PageDetailsPage: React.FC = ({ }) => { const intl = useIntl(); const localizeDate = useDateLocalize(); - const { - isMetadataModified, - isPrivateMetadataModified, - makeChangeHandler: makeMetadataChangeHandler - } = useMetadataChangeTrigger(); const pageExists = page !== null; - const initialForm: PageDetailsPageFormData = { - content: maybe( - () => JSON.parse(page.contentJson), - convertToRaw(ContentState.createFromText("")) - ), - isPublished: page?.isPublished, - metadata: pageExists ? page?.metadata?.map(mapMetadataItemToInput) : [], - privateMetadata: pageExists - ? page?.privateMetadata?.map(mapMetadataItemToInput) - : [], - publicationDate: page?.publicationDate || "", - seoDescription: page?.seoDescription || "", - seoTitle: page?.seoTitle || "", - slug: page?.slug || "", - title: page?.title || "" - }; - - const handleSubmit = (data: PageDetailsPageFormData) => { - const metadata = isMetadataModified ? data.metadata : undefined; - const privateMetadata = isPrivateMetadataModified - ? data.privateMetadata - : undefined; - - return onSubmit({ - ...data, - isPublished: data.isPublished || !!data.publicationDate, - metadata, - privateMetadata - }); - }; - return ( -
- {({ change, data, hasChanged, submit }) => { - const changeMetadata = makeMetadataChangeHandler(change); - - return ( - - - {intl.formatMessage(sectionNames.pages)} - - page.title) - } - /> - -
- - - - convertFromRaw(data.content) - .getPlainText() - .slice(0, 300), - "" - )} - onChange={change} - slug={data.slug} - slugPlaceholder={data.title} - title={data.seoTitle} - titlePlaceholder={data.title} - helperText={intl.formatMessage({ - defaultMessage: - "Add search engine title and description to make this page easier to find" - })} - /> - - -
-
- - -
-
- -
- ); - }} -
+ + {({ change, data, handlers, hasChanged, submit }) => ( + + + {intl.formatMessage(sectionNames.pages)} + + + +
+ + + + + +
+
+ + +
+
+ +
+ )} +
); }; PageDetailsPage.displayName = "PageDetailsPage"; diff --git a/src/pages/components/PageDetailsPage/form.tsx b/src/pages/components/PageDetailsPage/form.tsx new file mode 100644 index 000000000..9a035ff18 --- /dev/null +++ b/src/pages/components/PageDetailsPage/form.tsx @@ -0,0 +1,118 @@ +import { OutputData } from "@editorjs/editorjs"; +import { MetadataFormData } from "@saleor/components/Metadata"; +import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; +import useForm, { FormChange } from "@saleor/hooks/useForm"; +import { PageDetails_page } from "@saleor/pages/types/PageDetails"; +import getPublicationData from "@saleor/utils/data/getPublicationData"; +import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; +import { mapMetadataItemToInput } from "@saleor/utils/maps"; +import getMetadata from "@saleor/utils/metadata/getMetadata"; +import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; +import useRichText from "@saleor/utils/richText/useRichText"; +import React from "react"; + +export interface PageFormData extends MetadataFormData { + isPublished: boolean; + publicationDate: string; + seoDescription: string; + seoTitle: string; + slug: string; + title: string; +} +export interface PageData extends PageFormData { + content: OutputData; +} + +interface PageUpdateHandlers { + changeMetadata: FormChange; + changeContent: RichTextEditorChange; +} +export interface UsePageUpdateFormResult { + change: FormChange; + data: PageData; + handlers: PageUpdateHandlers; + hasChanged: boolean; + submit: () => void; +} + +export interface PageFormProps { + children: (props: UsePageUpdateFormResult) => React.ReactNode; + page: PageDetails_page; + onSubmit: (data: PageData) => Promise; +} + +function usePageForm( + page: PageDetails_page, + onSubmit: (data: PageData) => Promise +): UsePageUpdateFormResult { + const [changed, setChanged] = React.useState(false); + const triggerChange = () => setChanged(true); + + const pageExists = page !== null; + + const form = useForm({ + isPublished: page?.isPublished, + metadata: pageExists ? page?.metadata?.map(mapMetadataItemToInput) : [], + privateMetadata: pageExists + ? page?.privateMetadata?.map(mapMetadataItemToInput) + : [], + publicationDate: page?.publicationDate || "", + seoDescription: page?.seoDescription || "", + seoTitle: page?.seoTitle || "", + slug: page?.slug || "", + title: page?.title || "" + }); + const [content, changeContent] = useRichText({ + initial: pageExists ? page?.contentJson : null, + triggerChange + }); + + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + + const handleChange: FormChange = (event, cb) => { + form.change(event, cb); + triggerChange(); + }; + const changeMetadata = makeMetadataChangeHandler(handleChange); + + // Need to make it function to always have content.current up to date + const getData = (): PageData => ({ + ...form.data, + content: content.current + }); + + const getSubmitData = (): PageData => ({ + ...getData(), + ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified), + ...getPublicationData(form.data) + }); + + const submit = () => + pageExists + ? handleFormSubmit(getSubmitData(), onSubmit, setChanged) + : onSubmit(getSubmitData()); + + return { + change: handleChange, + data: getData(), + handlers: { + changeContent, + changeMetadata + }, + hasChanged: changed, + submit + }; +} + +const PageForm: React.FC = ({ children, page, onSubmit }) => { + const props = usePageForm(page, onSubmit); + + return
{children(props)}
; +}; + +PageForm.displayName = "PageForm"; +export default PageForm; diff --git a/src/pages/components/PageInfo/PageInfo.tsx b/src/pages/components/PageInfo/PageInfo.tsx index 12c2ecdeb..bf211d3e0 100644 --- a/src/pages/components/PageInfo/PageInfo.tsx +++ b/src/pages/components/PageInfo/PageInfo.tsx @@ -4,7 +4,9 @@ import { makeStyles } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import CardTitle from "@saleor/components/CardTitle"; import FormSpacer from "@saleor/components/FormSpacer"; -import RichTextEditor from "@saleor/components/RichTextEditor"; +import RichTextEditor, { + RichTextEditorChange +} from "@saleor/components/RichTextEditor"; import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; import { commonMessages } from "@saleor/intl"; import { getFormErrors } from "@saleor/utils/errors"; @@ -12,16 +14,14 @@ import getPageErrorMessage from "@saleor/utils/errors/page"; import React from "react"; import { useIntl } from "react-intl"; -import { maybe } from "../../../misc"; -import { PageDetails_page } from "../../types/PageDetails"; -import { PageDetailsPageFormData } from "../PageDetailsPage"; +import { PageData } from "../PageDetailsPage/form"; export interface PageInfoProps { - data: PageDetailsPageFormData; + data: PageData; disabled: boolean; errors: PageErrorFragment[]; - page: PageDetails_page; onChange: (event: React.ChangeEvent) => void; + onContentChange: RichTextEditorChange; } const useStyles = makeStyles( @@ -34,7 +34,7 @@ const useStyles = makeStyles( ); const PageInfo: React.FC = props => { - const { data, disabled, errors, page, onChange } = props; + const { data, disabled, errors, onChange, onContentChange } = props; const classes = useStyles(props); const intl = useIntl(); @@ -56,22 +56,22 @@ const PageInfo: React.FC = props => { defaultMessage: "Title", description: "page title" })} - name={"title" as keyof PageDetailsPageFormData} + name={"title" as keyof PageData} value={data.title} onChange={onChange} /> JSON.parse(page.contentJson))} label={intl.formatMessage({ defaultMessage: "Content", description: "page content" })} - name={"content" as keyof PageDetailsPageFormData} - onChange={onChange} + name={"content" as keyof PageData} + onChange={onContentChange} /> diff --git a/src/pages/mutations.ts b/src/pages/mutations.ts index 9cc38a9c1..e26d9e6fa 100644 --- a/src/pages/mutations.ts +++ b/src/pages/mutations.ts @@ -22,6 +22,7 @@ const pageCreate = gql` pageCreate(input: $input) { errors: pageErrors { ...PageErrorFragment + message } page { ...PageDetailsFragment diff --git a/src/pages/views/PageCreate.tsx b/src/pages/views/PageCreate.tsx index 4c5fb0fb8..628c84ebf 100644 --- a/src/pages/views/PageCreate.tsx +++ b/src/pages/views/PageCreate.tsx @@ -9,9 +9,8 @@ import { import React from "react"; import { useIntl } from "react-intl"; -import PageDetailsPage, { - PageDetailsPageFormData -} from "../components/PageDetailsPage"; +import PageDetailsPage from "../components/PageDetailsPage"; +import { PageData } from "../components/PageDetailsPage/form"; import { TypedPageCreate } from "../mutations"; import { PageCreate as PageCreateData } from "../types/PageCreate"; import { pageListUrl, pageUrl } from "../urls"; @@ -42,7 +41,7 @@ export const PageCreate: React.FC = () => { return ( {(pageCreate, pageCreateOpts) => { - const handleCreate = async (formData: PageDetailsPageFormData) => { + const handleCreate = async (formData: PageData) => { const result = await pageCreate({ variables: { input: { diff --git a/src/utils/data/getPublicationData.ts b/src/utils/data/getPublicationData.ts new file mode 100644 index 000000000..bfbff49f2 --- /dev/null +++ b/src/utils/data/getPublicationData.ts @@ -0,0 +1,16 @@ +interface PublicationData { + publicationDate: string; + isPublished: boolean; +} + +function getPublicationData({ + publicationDate, + isPublished +}: PublicationData): PublicationData { + return { + isPublished: !!publicationDate || isPublished, + publicationDate: publicationDate || null + }; +} + +export default getPublicationData; diff --git a/src/utils/richText/useRichText.test.ts b/src/utils/richText/useRichText.test.ts index 26fc9c782..bedf3d42f 100644 --- a/src/utils/richText/useRichText.test.ts +++ b/src/utils/richText/useRichText.test.ts @@ -39,6 +39,7 @@ describe("useRichText", () => { const triggerChange = jest.fn(); const hook = renderHook(() => useRichText({ + initial: null, triggerChange }) ); diff --git a/src/utils/richText/useRichText.ts b/src/utils/richText/useRichText.ts index fa51fb74a..522e06a08 100644 --- a/src/utils/richText/useRichText.ts +++ b/src/utils/richText/useRichText.ts @@ -1,6 +1,6 @@ import { OutputData } from "@editorjs/editorjs"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; -import { MutableRefObject, useEffect, useRef } from "react"; +import { MutableRefObject, useEffect, useRef, useState } from "react"; function useRichText(opts: { initial: string | null; @@ -9,9 +9,11 @@ function useRichText(opts: { const data = useRef( opts.initial === null ? { blocks: [] } : undefined ); + const [, setLoaded] = useState(false); useEffect(() => { try { data.current = JSON.parse(opts.initial); + setLoaded(true); } catch { data.current = undefined; } From f4dfbe7e9099dd25b2b496baedaf85513be0693d Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 4 Nov 2020 13:48:58 +0100 Subject: [PATCH 12/23] Fix header styling --- src/components/RichTextEditor/RichTextEditor.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index 0a5a6de76..bebeeb943 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -60,6 +60,10 @@ const useStyles = makeStyles( "& .ce-conversion-tool--focused": { background: `${fade(theme.palette.primary.main, 0.1)} !important` }, + "& .ce-header": { + marginBottom: 0, + paddingBottom: theme.spacing(1) + }, "& .ce-inline-tool": { ...hover, height: 32, From 7c90ec1c72cda145bdf0e88bcb45a9714a70dc9d Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 4 Nov 2020 13:54:37 +0100 Subject: [PATCH 13/23] Fix types --- .../components/CategoryBackground/CategoryBackground.tsx | 4 ++-- src/categories/components/CategoryCreatePage/form.tsx | 1 - src/categories/views/CategoryCreate.tsx | 5 +++-- src/categories/views/CategoryDetails.tsx | 6 +++--- src/collections/components/CollectionCreatePage/form.tsx | 1 - src/collections/views/CollectionDetails.tsx | 9 +++------ src/pages/views/PageDetails.tsx | 9 ++++----- .../components/ProductCreatePage/ProductCreatePage.tsx | 1 - src/products/utils/data.ts | 2 -- 9 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/categories/components/CategoryBackground/CategoryBackground.tsx b/src/categories/components/CategoryBackground/CategoryBackground.tsx index b801afa95..29e4a93f4 100644 --- a/src/categories/components/CategoryBackground/CategoryBackground.tsx +++ b/src/categories/components/CategoryBackground/CategoryBackground.tsx @@ -13,7 +13,7 @@ import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { CategoryDetails_category_backgroundImage } from "../../types/CategoryDetails"; -import { FormData } from "../CategoryUpdatePage"; +import { CategoryUpdateData } from "../CategoryUpdatePage/form"; const useStyles = makeStyles( theme => ({ @@ -42,7 +42,7 @@ const useStyles = makeStyles( ); export interface CategoryBackgroundProps { - data: FormData; + data: CategoryUpdateData; image: CategoryDetails_category_backgroundImage; onChange: (event: React.ChangeEvent) => void; onImageDelete: () => void; diff --git a/src/categories/components/CategoryCreatePage/form.tsx b/src/categories/components/CategoryCreatePage/form.tsx index 0393a005a..8572efc91 100644 --- a/src/categories/components/CategoryCreatePage/form.tsx +++ b/src/categories/components/CategoryCreatePage/form.tsx @@ -3,7 +3,6 @@ import { MetadataFormData } from "@saleor/components/Metadata"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import useForm, { FormChange } from "@saleor/hooks/useForm"; import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; -import getMetadata from "@saleor/utils/metadata/getMetadata"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useRichText from "@saleor/utils/richText/useRichText"; import React from "react"; diff --git a/src/categories/views/CategoryCreate.tsx b/src/categories/views/CategoryCreate.tsx index 761b12e20..c31329714 100644 --- a/src/categories/views/CategoryCreate.tsx +++ b/src/categories/views/CategoryCreate.tsx @@ -9,7 +9,8 @@ import { import React from "react"; import { useIntl } from "react-intl"; -import CategoryCreatePage, { FormData } from "../components/CategoryCreatePage"; +import CategoryCreatePage from "../components/CategoryCreatePage"; +import { CategoryCreateData } from "../components/CategoryCreatePage/form"; import { useCategoryCreateMutation } from "../mutations"; import { CategoryCreate } from "../types/CategoryCreate"; import { categoryListUrl, categoryUrl } from "../urls"; @@ -43,7 +44,7 @@ export const CategoryCreateView: React.FC = ({ onCompleted: handleSuccess }); - const handleCreate = async (formData: FormData) => { + const handleCreate = async (formData: CategoryCreateData) => { const result = await createCategory({ variables: { input: { diff --git a/src/categories/views/CategoryDetails.tsx b/src/categories/views/CategoryDetails.tsx index cd52af91c..5efdaafd6 100644 --- a/src/categories/views/CategoryDetails.tsx +++ b/src/categories/views/CategoryDetails.tsx @@ -27,9 +27,9 @@ import { productAddUrl, productUrl } from "../../products/urls"; import { CategoryInput } from "../../types/globalTypes"; import { CategoryPageTab, - CategoryUpdatePage, - FormData + CategoryUpdatePage } from "../components/CategoryUpdatePage/CategoryUpdatePage"; +import { CategoryUpdateData } from "../components/CategoryUpdatePage/form"; import { useCategoryBulkDeleteMutation, useCategoryDeleteMutation, @@ -175,7 +175,7 @@ export const CategoryDetails: React.FC = ({ params ); - const handleUpdate = async (formData: FormData) => { + const handleUpdate = async (formData: CategoryUpdateData) => { const result = await updateCategory({ variables: { id, diff --git a/src/collections/components/CollectionCreatePage/form.tsx b/src/collections/components/CollectionCreatePage/form.tsx index 4a49c03cf..9cf38191b 100644 --- a/src/collections/components/CollectionCreatePage/form.tsx +++ b/src/collections/components/CollectionCreatePage/form.tsx @@ -3,7 +3,6 @@ import { MetadataFormData } from "@saleor/components/Metadata"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import useForm, { FormChange } from "@saleor/hooks/useForm"; import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; -import getMetadata from "@saleor/utils/metadata/getMetadata"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useRichText from "@saleor/utils/richText/useRichText"; import React from "react"; diff --git a/src/collections/views/CollectionDetails.tsx b/src/collections/views/CollectionDetails.tsx index e1e5a0af5..d9c886825 100644 --- a/src/collections/views/CollectionDetails.tsx +++ b/src/collections/views/CollectionDetails.tsx @@ -25,9 +25,8 @@ import { FormattedMessage, useIntl } from "react-intl"; import { getMutationState, maybe } from "../../misc"; import { productUrl } from "../../products/urls"; import { CollectionInput } from "../../types/globalTypes"; -import CollectionDetailsPage, { - CollectionDetailsPageFormData -} from "../components/CollectionDetailsPage/CollectionDetailsPage"; +import CollectionDetailsPage from "../components/CollectionDetailsPage/CollectionDetailsPage"; +import { CollectionUpdateData } from "../components/CollectionDetailsPage/form"; import { useCollectionAssignProductMutation, useCollectionRemoveMutation, @@ -168,9 +167,7 @@ export const CollectionDetails: React.FC = ({ return ; } - const handleUpdate = async ( - formData: CollectionDetailsPageFormData - ) => { + const handleUpdate = async (formData: CollectionUpdateData) => { const input: CollectionInput = { backgroundImageAlt: formData.backgroundImageAlt, descriptionJson: JSON.stringify(formData.description), diff --git a/src/pages/views/PageDetails.tsx b/src/pages/views/PageDetails.tsx index 7b159d7de..d4fed8722 100644 --- a/src/pages/views/PageDetails.tsx +++ b/src/pages/views/PageDetails.tsx @@ -14,9 +14,8 @@ import { FormattedMessage, useIntl } from "react-intl"; import { getStringOrPlaceholder, maybe } from "../../misc"; import { PageInput } from "../../types/globalTypes"; -import PageDetailsPage, { - PageDetailsPageFormData -} from "../components/PageDetailsPage"; +import PageDetailsPage from "../components/PageDetailsPage"; +import { PageData } from "../components/PageDetailsPage/form"; import { TypedPageRemove, TypedPageUpdate } from "../mutations"; import { TypedPageDetailsQuery } from "../queries"; import { PageRemove } from "../types/PageRemove"; @@ -27,7 +26,7 @@ export interface PageDetailsProps { params: PageUrlQueryParams; } -const createPageInput = (data: PageDetailsPageFormData): PageInput => ({ +const createPageInput = (data: PageData): PageInput => ({ contentJson: JSON.stringify(data.content), isPublished: data.isPublished, publicationDate: data.publicationDate, @@ -63,7 +62,7 @@ export const PageDetails: React.FC = ({ id, params }) => { {(pageUpdate, pageUpdateOpts) => ( {pageDetails => { - const handleUpdate = async (data: PageDetailsPageFormData) => { + const handleUpdate = async (data: PageData) => { const result = await pageUpdate({ variables: { id, diff --git a/src/products/components/ProductCreatePage/ProductCreatePage.tsx b/src/products/components/ProductCreatePage/ProductCreatePage.tsx index b288068b4..66bc08146 100644 --- a/src/products/components/ProductCreatePage/ProductCreatePage.tsx +++ b/src/products/components/ProductCreatePage/ProductCreatePage.tsx @@ -19,7 +19,6 @@ import { SearchCategories_search_edges_node } from "@saleor/searches/types/Searc import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections"; import { SearchProductTypes_search_edges_node } from "@saleor/searches/types/SearchProductTypes"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; -import { ContentState, convertToRaw } from "draft-js"; import React from "react"; import { useIntl } from "react-intl"; diff --git a/src/products/utils/data.ts b/src/products/utils/data.ts index f48043e1e..6bc8bc719 100644 --- a/src/products/utils/data.ts +++ b/src/products/utils/data.ts @@ -1,4 +1,3 @@ -import { OutputData } from "@editorjs/editorjs"; import { MetadataFormData } from "@saleor/components/Metadata/types"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; @@ -13,7 +12,6 @@ import { import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/searches/types/SearchProductTypes"; import { StockInput } from "@saleor/types/globalTypes"; import { mapMetadataItemToInput } from "@saleor/utils/maps"; -import { RawDraftContentState } from "draft-js"; import { ProductAttributeInput } from "../components/ProductAttributes"; import { ProductStockInput } from "../components/ProductStocks"; From 44df6225ade5817e1eb0cb5d48f3d803668392cb Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 4 Nov 2020 13:56:04 +0100 Subject: [PATCH 14/23] Add todo --- src/components/RichTextEditor/RichTextEditor.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index bebeeb943..1703bbcd6 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -22,6 +22,7 @@ export interface RichTextEditorProps { onReady?: () => void; } +// TODO: improve dark mode const useStyles = makeStyles( theme => { const hover = { From dfc063caa196a5b15ee535deb84ab9935826bfed Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 5 Nov 2020 14:56:29 +0100 Subject: [PATCH 15/23] Improve component encapsulation --- src/categories/fixtures.ts | 4 +- src/collections/fixtures.ts | 4 +- src/components/RichTextEditor/ImageEntity.tsx | 104 --------------- src/components/RichTextEditor/ImageSource.tsx | 107 ---------------- src/components/RichTextEditor/LinkEntity.tsx | 121 ------------------ src/components/RichTextEditor/LinkSource.tsx | 98 -------------- .../RichTextEditor/RichTextEditor.stories.tsx | 28 ++++ .../RichTextEditor/RichTextEditor.tsx | 78 ++++++----- .../RichTextEditor}/fixtures.json | 0 src/pages/fixtures.ts | 4 +- src/products/fixtures.ts | 4 +- src/storybook/config.js | 1 - .../stories/components/RichTextEditor.tsx | 25 ---- 13 files changed, 77 insertions(+), 501 deletions(-) delete mode 100644 src/components/RichTextEditor/ImageEntity.tsx delete mode 100644 src/components/RichTextEditor/ImageSource.tsx delete mode 100644 src/components/RichTextEditor/LinkEntity.tsx delete mode 100644 src/components/RichTextEditor/LinkSource.tsx create mode 100644 src/components/RichTextEditor/RichTextEditor.stories.tsx rename src/{storybook/stories/components => components/RichTextEditor}/fixtures.json (100%) delete mode 100644 src/storybook/stories/components/RichTextEditor.tsx diff --git a/src/categories/fixtures.ts b/src/categories/fixtures.ts index ddc277a83..aabbc37e9 100644 --- a/src/categories/fixtures.ts +++ b/src/categories/fixtures.ts @@ -1,8 +1,10 @@ import { CategoryFragment } from "@saleor/fragments/types/CategoryFragment"; -import { content } from "../storybook/stories/components/RichTextEditor"; +import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json"; import { CategoryDetails_category } from "./types/CategoryDetails"; +const content = richTextEditorFixtures.richTextEditor; + export const categories: CategoryFragment[] = [ { __typename: "Category", diff --git a/src/collections/fixtures.ts b/src/collections/fixtures.ts index f604aa199..4e515428f 100644 --- a/src/collections/fixtures.ts +++ b/src/collections/fixtures.ts @@ -1,7 +1,9 @@ -import { content } from "../storybook/stories/components/RichTextEditor"; +import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json"; import { CollectionDetails_collection } from "./types/CollectionDetails"; import { CollectionList_collections_edges_node } from "./types/CollectionList"; +const content = richTextEditorFixtures.richTextEditor; + export const collections: CollectionList_collections_edges_node[] = [ { __typename: "Collection", diff --git a/src/components/RichTextEditor/ImageEntity.tsx b/src/components/RichTextEditor/ImageEntity.tsx deleted file mode 100644 index 2bc502f3f..000000000 --- a/src/components/RichTextEditor/ImageEntity.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import Button from "@material-ui/core/Button"; -import ClickAwayListener from "@material-ui/core/ClickAwayListener"; -import Grow from "@material-ui/core/Grow"; -import IconButton from "@material-ui/core/IconButton"; -import Paper from "@material-ui/core/Paper"; -import Popper from "@material-ui/core/Popper"; -import { makeStyles } from "@material-ui/core/styles"; -import DeleteIcon from "@material-ui/icons/Delete"; -import { ContentState } from "draft-js"; -import React from "react"; -import { FormattedMessage } from "react-intl"; - -interface ImageEntityProps { - children: React.ReactNode; - contentState: ContentState; - entityKey: string; - onEdit: (entityKey: string) => void; - onRemove: (entityKey: string) => void; -} - -const useStyles = makeStyles( - theme => ({ - anchor: { - display: "inline-block" - }, - container: { - alignItems: "center", - display: "flex" - }, - image: { maxWidth: "100%" }, - inline: { - display: "inline-block" - }, - root: { - alignItems: "center", - display: "flex", - minHeight: 72, - padding: theme.spacing(1.5) - } - }), - { name: "ImageEntity" } -); - -const ImageEntity: React.FC = props => { - const { contentState, entityKey, onEdit, onRemove } = props; - const classes = useStyles(props); - - const [isOpened, setOpenStatus] = React.useState(false); - const anchor = React.useRef(); - - const disable = () => setOpenStatus(false); - const toggle = () => setOpenStatus(!isOpened); - - return ( - <> -
- - {({ TransitionProps, placement }) => ( - - - -
- - onRemove(entityKey)}> - - -
-
-
-
- )} -
-
- - - ); -}; -export default ImageEntity; diff --git a/src/components/RichTextEditor/ImageSource.tsx b/src/components/RichTextEditor/ImageSource.tsx deleted file mode 100644 index d3ae28ff9..000000000 --- a/src/components/RichTextEditor/ImageSource.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import TextField from "@material-ui/core/TextField"; -import { buttonMessages } from "@saleor/intl"; -import { AtomicBlockUtils, EditorState, EntityInstance } from "draft-js"; -import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; - -import Form from "../Form"; - -interface ImageSourceProps { - editorState: EditorState; - entity?: EntityInstance; - entityKey?: string; - entityType: { - type: string; - }; - onComplete: (updateState: EditorState) => void; - onClose: () => void; -} - -const ImageSource: React.FC = ({ - editorState, - entity, - entityKey, - entityType, - onComplete, - onClose -}) => { - const intl = useIntl(); - - const initial = entity ? entity.getData().href : ""; - - const handleSubmit = (href: string) => { - if (href) { - const content = editorState.getCurrentContent(); - if (entity) { - const nextContent = content.mergeEntityData(entityKey, { href }); - const nextState = EditorState.push( - editorState, - nextContent, - "apply-entity" - ); - onComplete(nextState); - } else { - const contentWithEntity = content.createEntity( - entityType.type, - "IMMUTABLE", - { href } - ); - const nextState = AtomicBlockUtils.insertAtomicBlock( - editorState, - contentWithEntity.getLastCreatedEntityKey(), - " " - ); - - onComplete(nextState); - } - } else { - onComplete(editorState); - } - }; - - return ( - -
handleSubmit(href)} - > - {({ data, change, submit }) => ( - <> - - - - - - - - - - - - )} -
-
- ); -}; - -export default ImageSource; diff --git a/src/components/RichTextEditor/LinkEntity.tsx b/src/components/RichTextEditor/LinkEntity.tsx deleted file mode 100644 index 86166fe2e..000000000 --- a/src/components/RichTextEditor/LinkEntity.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import Button from "@material-ui/core/Button"; -import ClickAwayListener from "@material-ui/core/ClickAwayListener"; -import Grow from "@material-ui/core/Grow"; -import IconButton from "@material-ui/core/IconButton"; -import Paper from "@material-ui/core/Paper"; -import Popper from "@material-ui/core/Popper"; -import { makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import DeleteIcon from "@material-ui/icons/Delete"; -import { buttonMessages } from "@saleor/intl"; -import { ContentState } from "draft-js"; -import React from "react"; -import { FormattedMessage } from "react-intl"; - -import Link from "../Link"; - -interface LinkEntityProps { - children: React.ReactNode; - contentState: ContentState; - entityKey: string; - onEdit: (entityKey: string) => void; - onRemove: (entityKey: string) => void; -} - -const useStyles = makeStyles( - theme => ({ - anchor: { - display: "inline-block" - }, - container: { - alignItems: "center", - display: "flex" - }, - inline: { - display: "inline-block" - }, - popover: { - zIndex: 1 - }, - root: { - alignItems: "center", - display: "flex", - minHeight: 72, - padding: theme.spacing(1.5, 1.5, 1.5, 3) - }, - separator: { - backgroundColor: theme.palette.grey[300], - display: "inline-block", - height: 30, - marginLeft: theme.spacing(2), - marginRight: theme.spacing(), - width: 1 - } - }), - { name: "LinkEntity" } -); - -const LinkEntity: React.FC = props => { - const { children, contentState, entityKey, onEdit, onRemove } = props; - const classes = useStyles(props); - - const [isOpened, setOpenStatus] = React.useState(false); - const anchor = React.useRef(); - - const disable = () => setOpenStatus(false); - const toggle = () => setOpenStatus(!isOpened); - - return ( - <> -
- - {({ TransitionProps, placement }) => ( - - - -
- - {contentState.getEntity(entityKey).getData().url} - - - - onRemove(entityKey)}> - - -
-
-
-
- )} -
-
- - {children} - - - ); -}; -export default LinkEntity; diff --git a/src/components/RichTextEditor/LinkSource.tsx b/src/components/RichTextEditor/LinkSource.tsx deleted file mode 100644 index b6c55e7a5..000000000 --- a/src/components/RichTextEditor/LinkSource.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import TextField from "@material-ui/core/TextField"; -import { buttonMessages } from "@saleor/intl"; -import { EditorState, EntityInstance, RichUtils } from "draft-js"; -import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; - -import Form from "../Form"; - -interface LinkSourceProps { - editorState: EditorState; - entity?: EntityInstance; - entityType: { - type: string; - }; - onComplete: (updateState: EditorState) => void; - onClose: () => void; -} - -const LinkSource: React.FC = ({ - editorState, - entity, - entityType, - onComplete, - onClose -}) => { - const intl = useIntl(); - const initial = entity ? entity.getData().url : ""; - - const handleSubmit = (url: string) => { - if (url) { - const content = editorState.getCurrentContent(); - const contentWithEntity = content.createEntity( - entityType.type, - "MUTABLE", - { url } - ); - const entityKey = contentWithEntity.getLastCreatedEntityKey(); - const newEditorState = EditorState.set(editorState, { - currentContent: contentWithEntity - }); - const nextState = RichUtils.toggleLink( - newEditorState, - newEditorState.getSelection(), - entityKey - ); - - onComplete(nextState); - } else { - onComplete(editorState); - } - }; - - return ( - -
handleSubmit(url)} - > - {({ data, change, submit }) => ( - <> - - - - - - - - - - - - )} -
-
- ); -}; - -export default LinkSource; diff --git a/src/components/RichTextEditor/RichTextEditor.stories.tsx b/src/components/RichTextEditor/RichTextEditor.stories.tsx new file mode 100644 index 000000000..e250f5496 --- /dev/null +++ b/src/components/RichTextEditor/RichTextEditor.stories.tsx @@ -0,0 +1,28 @@ +import { OutputData } from "@editorjs/editorjs"; +import RichTextEditor from "@saleor/components/RichTextEditor"; +import CardDecorator from "@saleor/storybook/CardDecorator"; +import Decorator from "@saleor/storybook/Decorator"; +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import * as fixtures from "./fixtures.json"; +import { RichTextEditorProps } from "./RichTextEditor"; + +export const data: OutputData = fixtures.richTextEditor; + +const props: RichTextEditorProps = { + data, + disabled: false, + error: false, + helperText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit", + label: "Content", + name: "content", + onChange: () => undefined +}; + +storiesOf("Generics / Rich text editor", module) + .addDecorator(CardDecorator) + .addDecorator(Decorator) + .add("default", () => ) + .add("disabled", () => ) + .add("error", () => ); diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index 1703bbcd6..62a616327 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -2,6 +2,10 @@ import EditorJS, { OutputData } from "@editorjs/editorjs"; import Header from "@editorjs/header"; import List from "@editorjs/list"; import Quote from "@editorjs/quote"; +import FormControl from "@material-ui/core/FormControl"; +import FormHelperText from "@material-ui/core/FormHelperText"; +import InputLabel from "@material-ui/core/InputLabel"; +import OutlinedInput from "@material-ui/core/OutlinedInput"; import { makeStyles } from "@material-ui/core/styles"; import { fade } from "@material-ui/core/styles/colorManipulator"; import Typography from "@material-ui/core/Typography"; @@ -32,21 +36,6 @@ const useStyles = makeStyles( }; return { - error: { - color: theme.palette.error.main - }, - helperText: { - marginTop: theme.spacing(0.75) - }, - label: { - color: theme.palette.text.secondary, - position: "absolute", - top: theme.spacing(1), - transition: theme.transitions.duration.short + "ms" - }, - labelActive: { - color: theme.palette.primary.main - }, root: { "& .cdx-quote__text": { minHeight: 24 @@ -99,13 +88,14 @@ const useStyles = makeStyles( "& a": { color: theme.palette.primary.light }, - "&:hover": { + "&:not($rootDisabled):hover": { borderColor: theme.palette.primary.main }, border: `1px solid ${fade(theme.palette.text.secondary, 0.4)}`, borderRadius: 4, boxShadow: `inset 0 0 0 0 ${theme.palette.primary.main}`, fontSize: theme.typography.body1.fontSize, + minHeight: 56, padding: theme.spacing(3, 2), paddingBottom: theme.spacing(), paddingLeft: 10, @@ -114,6 +104,12 @@ const useStyles = makeStyles( }, rootActive: { boxShadow: `inset 0px 0px 0 2px ${theme.palette.primary.main}` + }, + rootDisabled: { + ...theme.overrides.MuiOutlinedInput.root["&$disabled"]["& fieldset"] + }, + rootError: { + borderColor: theme.palette.error.main } }; }, @@ -122,9 +118,11 @@ const useStyles = makeStyles( const RichTextEditor: React.FC = ({ data, + disabled, error, helperText, label, + name, onChange, onReady }) => { @@ -144,6 +142,7 @@ const RichTextEditor: React.FC = ({ onChange(savedData); }, onReady, + readOnly: disabled, tools: { header: { class: Header, @@ -172,39 +171,36 @@ const RichTextEditor: React.FC = ({ [data === undefined] ); React.useEffect(() => editor.current?.destroy, []); + React.useEffect(() => { + if (editor.current?.readOnly) { + editor.current.readOnly.toggle(disabled); + } + }, [disabled]); return ( -
+ + + {label} +
setFocus(true)} onBlur={() => setFocus(false)} - > - - {label} - -
- {helperText && ( - - {helperText} - - )} -
+ /> + {helperText} + ); }; diff --git a/src/storybook/stories/components/fixtures.json b/src/components/RichTextEditor/fixtures.json similarity index 100% rename from src/storybook/stories/components/fixtures.json rename to src/components/RichTextEditor/fixtures.json diff --git a/src/pages/fixtures.ts b/src/pages/fixtures.ts index 6aba5a7ad..d912d5c09 100644 --- a/src/pages/fixtures.ts +++ b/src/pages/fixtures.ts @@ -1,7 +1,9 @@ -import { content } from "../storybook/stories/components/RichTextEditor"; +import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json"; import { PageDetails_page } from "./types/PageDetails"; import { PageList_pages_edges_node } from "./types/PageList"; +const content = richTextEditorFixtures.richTextEditor; + export const pageList: PageList_pages_edges_node[] = [ { __typename: "Page", diff --git a/src/products/fixtures.ts b/src/products/fixtures.ts index 928217fe1..734843360 100644 --- a/src/products/fixtures.ts +++ b/src/products/fixtures.ts @@ -5,11 +5,13 @@ import { } from "@saleor/types/globalTypes"; import { warehouseList } from "@saleor/warehouses/fixtures"; -import { content } from "../storybook/stories/components/RichTextEditor"; +import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json"; import { ProductDetails_product } from "./types/ProductDetails"; import { ProductList_products_edges_node } from "./types/ProductList"; import { ProductVariantCreateData_product } from "./types/ProductVariantCreateData"; +const content = richTextEditorFixtures.richTextEditor; + export const product: ( placeholderImage: string ) => ProductDetails_product & diff --git a/src/storybook/config.js b/src/storybook/config.js index 28b2fc2ef..d088e83e8 100644 --- a/src/storybook/config.js +++ b/src/storybook/config.js @@ -35,7 +35,6 @@ function loadStories() { require("./stories/components/Percent"); require("./stories/components/PhoneField"); require("./stories/components/PriceField"); - require("./stories/components/RichTextEditor"); require("./stories/components/SaveButtonBar"); require("./stories/components/SaveFilterTabDialog"); require("./stories/components/SingleSelectField"); diff --git a/src/storybook/stories/components/RichTextEditor.tsx b/src/storybook/stories/components/RichTextEditor.tsx deleted file mode 100644 index 9cfd5580c..000000000 --- a/src/storybook/stories/components/RichTextEditor.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { OutputData } from "@editorjs/editorjs"; -import RichTextEditor from "@saleor/components/RichTextEditor"; -import { storiesOf } from "@storybook/react"; -import React from "react"; - -import CardDecorator from "../../CardDecorator"; -import Decorator from "../../Decorator"; -import * as fixtures from "./fixtures.json"; - -export const content: OutputData = fixtures.richTextEditor; - -storiesOf("Generics / Rich text editor", module) - .addDecorator(CardDecorator) - .addDecorator(Decorator) - .add("default", () => ( - undefined} - /> - )); From bc78c03e29176afbb548a65aba57f9154011acf5 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 5 Nov 2020 15:10:46 +0100 Subject: [PATCH 16/23] Improve dark mode --- .../RichTextEditor/RichTextEditor.tsx | 19 +++++++++++++++++-- src/storybook/Decorator.tsx | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index 62a616327..1a0d063f9 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -5,10 +5,8 @@ import Quote from "@editorjs/quote"; import FormControl from "@material-ui/core/FormControl"; import FormHelperText from "@material-ui/core/FormHelperText"; import InputLabel from "@material-ui/core/InputLabel"; -import OutlinedInput from "@material-ui/core/OutlinedInput"; import { makeStyles } from "@material-ui/core/styles"; import { fade } from "@material-ui/core/styles/colorManipulator"; -import Typography from "@material-ui/core/Typography"; import strikethroughIcon from "@saleor/icons/StrikethroughIcon"; import classNames from "classnames"; import createGenericInlineTool from "editorjs-inline-tool"; @@ -40,6 +38,9 @@ const useStyles = makeStyles( "& .cdx-quote__text": { minHeight: 24 }, + "& .ce-block--selected .ce-block__content": { + background: `${fade(theme.palette.primary.main, 0.4)} !important` + }, "& .ce-block__content": { margin: 0, maxWidth: "unset" @@ -50,16 +51,30 @@ const useStyles = makeStyles( "& .ce-conversion-tool--focused": { background: `${fade(theme.palette.primary.main, 0.1)} !important` }, + "& .ce-conversion-tool__icon": { + background: "none" + }, + "& .ce-conversion-toolbar": { + background: theme.palette.background.paper + }, "& .ce-header": { marginBottom: 0, paddingBottom: theme.spacing(1) }, "& .ce-inline-tool": { ...hover, + color: theme.palette.text.primary, height: 32, transition: theme.transitions.duration.short + "ms", width: 32 }, + "& .ce-inline-toolbar": { + "& input": { + background: "none" + }, + background: theme.palette.background.paper, + color: theme.palette.text.primary + }, "& .ce-inline-toolbar__dropdown": { ...hover, height: 32, diff --git a/src/storybook/Decorator.tsx b/src/storybook/Decorator.tsx index 66e548747..46b6ec656 100644 --- a/src/storybook/Decorator.tsx +++ b/src/storybook/Decorator.tsx @@ -17,7 +17,7 @@ export const Decorator = storyFn => ( > - +
Date: Thu, 5 Nov 2020 15:16:39 +0100 Subject: [PATCH 17/23] wip --- src/components/RichTextEditor/RichTextEditor.tsx | 2 +- src/storybook/Decorator.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index 1a0d063f9..b6cba3e1b 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -170,7 +170,7 @@ const RichTextEditor: React.FC = ({ quote: Quote, strikethrough: createGenericInlineTool({ sanitize: { - s: true + s: {} }, shortcut: "CMD+S", tagName: "s", diff --git a/src/storybook/Decorator.tsx b/src/storybook/Decorator.tsx index 46b6ec656..66e548747 100644 --- a/src/storybook/Decorator.tsx +++ b/src/storybook/Decorator.tsx @@ -17,7 +17,7 @@ export const Decorator = storyFn => ( > - +
Date: Thu, 5 Nov 2020 15:31:00 +0100 Subject: [PATCH 18/23] Fix selection styles --- src/Baseline.tsx | 15 +++++++++++---- src/components/RichTextEditor/RichTextEditor.tsx | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Baseline.tsx b/src/Baseline.tsx index 169464771..db200dda8 100644 --- a/src/Baseline.tsx +++ b/src/Baseline.tsx @@ -1,12 +1,19 @@ import CssBaseline from "@material-ui/core/CssBaseline"; -import { createStyles, withStyles } from "@material-ui/core/styles"; +import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; +import { fade } from "@material-ui/core/styles/colorManipulator"; import React from "react"; -const styles = createStyles({ +const styles = createStyles((theme: Theme) => ({ "@global": { - "@import": "url('https://rsms.me/inter/inter.css')" + "@import": "url('https://rsms.me/inter/inter.css')", + + // For some reason @import clause must be put on top + // eslint-disable-next-line sort-keys + "::selection": { + background: fade(theme.palette.primary.main, 0.2) + } } -}); +})); const Baseline = withStyles(styles, { name: "Baseline" diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index b6cba3e1b..e85df71a8 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -39,7 +39,7 @@ const useStyles = makeStyles( minHeight: 24 }, "& .ce-block--selected .ce-block__content": { - background: `${fade(theme.palette.primary.main, 0.4)} !important` + background: `${fade(theme.palette.primary.main, 0.2)} !important` }, "& .ce-block__content": { margin: 0, From 02e93b06d6bfa4897b6d8e6f126b01763253e180 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 5 Nov 2020 17:30:38 +0100 Subject: [PATCH 19/23] Use rich text editor in translations --- .../RichTextEditor/RichTextEditor.tsx | 143 +----------------- .../RichTextEditor/RichTextEditorContent.tsx | 79 ++++++++++ src/components/RichTextEditor/styles.ts | 112 ++++++++++++++ .../stories/pages/PageDetailsPage.tsx | 4 +- .../TranslationFields/TranslationFields.tsx | 3 +- .../TranslationFieldsRich.tsx | 58 +++---- .../TranslationFieldsSave.tsx | 2 +- src/translations/types.ts | 3 +- src/utils/richText/useRichText.ts | 12 +- 9 files changed, 240 insertions(+), 176 deletions(-) create mode 100644 src/components/RichTextEditor/RichTextEditorContent.tsx create mode 100644 src/components/RichTextEditor/styles.ts diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index e85df71a8..f853f205a 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -1,136 +1,23 @@ import EditorJS, { OutputData } from "@editorjs/editorjs"; -import Header from "@editorjs/header"; -import List from "@editorjs/list"; -import Quote from "@editorjs/quote"; import FormControl from "@material-ui/core/FormControl"; import FormHelperText from "@material-ui/core/FormHelperText"; import InputLabel from "@material-ui/core/InputLabel"; -import { makeStyles } from "@material-ui/core/styles"; -import { fade } from "@material-ui/core/styles/colorManipulator"; -import strikethroughIcon from "@saleor/icons/StrikethroughIcon"; import classNames from "classnames"; -import createGenericInlineTool from "editorjs-inline-tool"; import React from "react"; +import { RichTextEditorContentProps, tools } from "./RichTextEditorContent"; +import useStyles from "./styles"; + export type RichTextEditorChange = (data: OutputData) => void; -export interface RichTextEditorProps { - data: OutputData; +export interface RichTextEditorProps extends RichTextEditorContentProps { disabled: boolean; error: boolean; helperText: string; label: string; name: string; onChange: RichTextEditorChange; - onReady?: () => void; } -// TODO: improve dark mode -const useStyles = makeStyles( - theme => { - const hover = { - "&:hover": { - background: fade(theme.palette.primary.main, 0.1) - } - }; - - return { - root: { - "& .cdx-quote__text": { - minHeight: 24 - }, - "& .ce-block--selected .ce-block__content": { - background: `${fade(theme.palette.primary.main, 0.2)} !important` - }, - "& .ce-block__content": { - margin: 0, - maxWidth: "unset" - }, - "& .ce-conversion-tool": { - ...hover - }, - "& .ce-conversion-tool--focused": { - background: `${fade(theme.palette.primary.main, 0.1)} !important` - }, - "& .ce-conversion-tool__icon": { - background: "none" - }, - "& .ce-conversion-toolbar": { - background: theme.palette.background.paper - }, - "& .ce-header": { - marginBottom: 0, - paddingBottom: theme.spacing(1) - }, - "& .ce-inline-tool": { - ...hover, - color: theme.palette.text.primary, - height: 32, - transition: theme.transitions.duration.short + "ms", - width: 32 - }, - "& .ce-inline-toolbar": { - "& input": { - background: "none" - }, - background: theme.palette.background.paper, - color: theme.palette.text.primary - }, - "& .ce-inline-toolbar__dropdown": { - ...hover, - height: 32, - marginRight: 0 - }, - "& .ce-inline-toolbar__toggler-and-button-wrapper": { - paddingRight: 0 - }, - "& .ce-toolbar__actions": { - right: 0, - top: 0 - }, - "& .ce-toolbar__content": { - maxWidth: "unset" - }, - "& .ce-toolbar__plus": { - left: -9 - }, - "& .ce-toolbox.ce-toolbox--opened": { - left: 16 - }, - "& .codex-editor__redactor": { - marginRight: `${theme.spacing(4)}px !important`, - paddingBottom: "0 !important" - }, - "& a": { - color: theme.palette.primary.light - }, - "&:not($rootDisabled):hover": { - borderColor: theme.palette.primary.main - }, - border: `1px solid ${fade(theme.palette.text.secondary, 0.4)}`, - borderRadius: 4, - boxShadow: `inset 0 0 0 0 ${theme.palette.primary.main}`, - fontSize: theme.typography.body1.fontSize, - minHeight: 56, - padding: theme.spacing(3, 2), - paddingBottom: theme.spacing(), - paddingLeft: 10, - position: "relative", - transition: theme.transitions.duration.short + "ms" - }, - rootActive: { - boxShadow: `inset 0px 0px 0 2px ${theme.palette.primary.main}` - }, - rootDisabled: { - ...theme.overrides.MuiOutlinedInput.root["&$disabled"]["& fieldset"] - }, - rootError: { - borderColor: theme.palette.error.main - } - }; - }, - { name: "RichTextEditor" } -); - const RichTextEditor: React.FC = ({ data, disabled, @@ -158,25 +45,7 @@ const RichTextEditor: React.FC = ({ }, onReady, readOnly: disabled, - tools: { - header: { - class: Header, - config: { - defaultLevel: 1, - levels: [1, 2, 3] - } - }, - list: List, - quote: Quote, - strikethrough: createGenericInlineTool({ - sanitize: { - s: {} - }, - shortcut: "CMD+S", - tagName: "s", - toolboxIcon: strikethroughIcon - }) - } + tools }); } @@ -205,7 +74,7 @@ const RichTextEditor: React.FC = ({ {label}
void; +} + +export const tools: Record = { + header: { + class: Header, + config: { + defaultLevel: 1, + levels: [1, 2, 3] + } + }, + list: List, + quote: Quote, + strikethrough: createGenericInlineTool({ + sanitize: { + s: {} + }, + shortcut: "CMD+S", + tagName: "s", + toolboxIcon: strikethroughIcon + }) +}; + +const RichTextEditorContent: React.FC = ({ + className, + data, + onReady +}) => { + const classes = useStyles({}); + + const editor = React.useRef(); + const editorContainer = React.useRef(); + React.useEffect( + () => { + if (data) { + editor.current = new EditorJS({ + data, + holder: editorContainer.current, + onReady, + readOnly: true, + tools + }); + } + + return editor.current?.destroy; + }, + // Rerender editor only if changed from undefined to defined state + [data === undefined] + ); + React.useEffect(() => editor.current?.destroy, []); + + return ( +
+ ); +}; + +RichTextEditorContent.displayName = "RichTextEditorContent"; +export default RichTextEditorContent; diff --git a/src/components/RichTextEditor/styles.ts b/src/components/RichTextEditor/styles.ts new file mode 100644 index 000000000..2436bf2da --- /dev/null +++ b/src/components/RichTextEditor/styles.ts @@ -0,0 +1,112 @@ +import { makeStyles } from "@material-ui/core/styles"; +import { fade } from "@material-ui/core/styles/colorManipulator"; + +const useStyles = makeStyles( + theme => { + const hover = { + "&:hover": { + background: fade(theme.palette.primary.main, 0.1) + } + }; + + return { + editor: { + "& .cdx-quote__text": { + minHeight: 24 + }, + "& .ce-block--selected .ce-block__content": { + background: `${fade(theme.palette.primary.main, 0.2)} !important` + }, + "& .ce-block__content": { + margin: 0, + maxWidth: "unset" + }, + "& .ce-conversion-tool": { + ...hover + }, + "& .ce-conversion-tool--focused": { + background: `${fade(theme.palette.primary.main, 0.1)} !important` + }, + "& .ce-conversion-tool__icon": { + background: "none" + }, + "& .ce-conversion-toolbar": { + background: theme.palette.background.paper + }, + "& .ce-header": { + marginBottom: 0, + paddingBottom: theme.spacing(1) + }, + "& .ce-inline-tool": { + ...hover, + color: theme.palette.text.primary, + height: 32, + transition: theme.transitions.duration.short + "ms", + width: 32 + }, + "& .ce-inline-toolbar": { + "& input": { + background: "none" + }, + background: theme.palette.background.paper, + color: theme.palette.text.primary + }, + "& .ce-inline-toolbar__dropdown": { + ...hover, + height: 32, + marginRight: 0 + }, + "& .ce-inline-toolbar__toggler-and-button-wrapper": { + paddingRight: 0 + }, + "& .ce-toolbar__actions": { + right: 0, + top: 0 + }, + "& .ce-toolbar__content": { + maxWidth: "unset" + }, + "& .ce-toolbar__plus": { + left: -9 + }, + "& .ce-toolbox.ce-toolbox--opened": { + left: 16 + }, + "& .codex-editor__redactor": { + marginRight: `${theme.spacing(4)}px !important`, + paddingBottom: "0 !important" + }, + "& a": { + color: theme.palette.primary.light + }, + "&:not($rootDisabled):hover": { + borderColor: theme.palette.primary.main + } + }, + root: { + border: `1px solid ${fade(theme.palette.text.secondary, 0.4)}`, + borderRadius: 4, + boxShadow: `inset 0 0 0 0 ${theme.palette.primary.main}`, + fontSize: theme.typography.body1.fontSize, + minHeight: 56, + padding: theme.spacing(3, 2), + paddingBottom: theme.spacing(), + paddingLeft: 10, + position: "relative", + transition: theme.transitions.duration.short + "ms" + }, + rootActive: { + boxShadow: `inset 0px 0px 0 2px ${theme.palette.primary.main}` + }, + rootDisabled: { + ...theme.overrides.MuiOutlinedInput.root["&$disabled"]["& fieldset"] + }, + rootError: { + borderColor: theme.palette.error.main + } + }; + }, + { name: "RichTextEditor" } +); + +export default useStyles; diff --git a/src/storybook/stories/pages/PageDetailsPage.tsx b/src/storybook/stories/pages/PageDetailsPage.tsx index 51af7b057..f8814abe9 100644 --- a/src/storybook/stories/pages/PageDetailsPage.tsx +++ b/src/storybook/stories/pages/PageDetailsPage.tsx @@ -1,9 +1,9 @@ +import { PageData } from "@saleor/pages/components/PageDetailsPage/form"; import { PageErrorCode } from "@saleor/types/globalTypes"; import { storiesOf } from "@storybook/react"; import React from "react"; import PageDetailsPage, { - PageDetailsPageFormData, PageDetailsPageProps } from "../../../pages/components/PageDetailsPage"; import { page } from "../../../pages/fixtures"; @@ -36,7 +36,7 @@ storiesOf("Views / Pages / Page details", module) "isPublished", "seoDescription", "seoTitle" - ] as Array).map(field => ({ + ] as Array).map(field => ({ __typename: "PageError", code: PageErrorCode.INVALID, field diff --git a/src/translations/components/TranslationFields/TranslationFields.tsx b/src/translations/components/TranslationFields/TranslationFields.tsx index 1b82272ab..953300a1d 100644 --- a/src/translations/components/TranslationFields/TranslationFields.tsx +++ b/src/translations/components/TranslationFields/TranslationFields.tsx @@ -1,3 +1,4 @@ +import { OutputData } from "@editorjs/editorjs"; import Button from "@material-ui/core/Button"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; @@ -36,7 +37,7 @@ export interface TranslationFieldsProps { saveButtonState: ConfirmButtonTransitionState; onEdit: (field: string) => void; onDiscard: () => void; - onSubmit: (field: string, data: string) => void; + onSubmit: (field: string, data: string | OutputData) => void; } const useStyles = makeStyles( diff --git a/src/translations/components/TranslationFields/TranslationFieldsRich.tsx b/src/translations/components/TranslationFields/TranslationFieldsRich.tsx index 1a940fc45..51dde46b7 100644 --- a/src/translations/components/TranslationFields/TranslationFieldsRich.tsx +++ b/src/translations/components/TranslationFields/TranslationFieldsRich.tsx @@ -1,8 +1,9 @@ +import { OutputData } from "@editorjs/editorjs"; import Typography from "@material-ui/core/Typography"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; -import DraftRenderer from "@saleor/components/DraftRenderer"; -import Form from "@saleor/components/Form"; import RichTextEditor from "@saleor/components/RichTextEditor"; +import RichTextEditorContent from "@saleor/components/RichTextEditor/RichTextEditorContent"; +import useRichText from "@saleor/utils/richText/useRichText"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -14,7 +15,7 @@ interface TranslationFieldsRichProps { initial: string; saveButtonState: ConfirmButtonTransitionState; onDiscard: () => void; - onSubmit: (data: string) => void; + onSubmit: (data: OutputData) => void; } const TranslationFieldsRich: React.FC = ({ @@ -26,40 +27,39 @@ const TranslationFieldsRich: React.FC = ({ onSubmit }) => { const intl = useIntl(); + const [content, change] = useRichText({ + initial, + triggerChange: () => undefined + }); + + const submit = () => onSubmit(content.current); return edit ? ( -
onSubmit(data.translation)} - > - {({ change, submit }) => ( -
- - -
- )} -
+
+ + + ) : initial === null ? ( ) : ( - + ); }; diff --git a/src/translations/components/TranslationFields/TranslationFieldsSave.tsx b/src/translations/components/TranslationFields/TranslationFieldsSave.tsx index ab2a40b02..c925414c5 100644 --- a/src/translations/components/TranslationFields/TranslationFieldsSave.tsx +++ b/src/translations/components/TranslationFields/TranslationFieldsSave.tsx @@ -43,7 +43,7 @@ const TranslationFieldsSave: React.FC = props => { > -
diff --git a/src/translations/types.ts b/src/translations/types.ts index 8c56c4bf1..335268725 100644 --- a/src/translations/types.ts +++ b/src/translations/types.ts @@ -1,3 +1,4 @@ +import { OutputData } from "@editorjs/editorjs"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { ShopInfo_shop_languages } from "@saleor/components/Shop/types/ShopInfo"; @@ -11,5 +12,5 @@ export interface TranslationsEntitiesPageProps { onEdit: (field: string) => void; onDiscard: () => void; onLanguageChange: (lang: string) => void; - onSubmit: (field: string, data: string) => void; + onSubmit: (field: string, data: string | OutputData) => void; } diff --git a/src/utils/richText/useRichText.ts b/src/utils/richText/useRichText.ts index 522e06a08..652e88bc4 100644 --- a/src/utils/richText/useRichText.ts +++ b/src/utils/richText/useRichText.ts @@ -11,11 +11,13 @@ function useRichText(opts: { ); const [, setLoaded] = useState(false); useEffect(() => { - try { - data.current = JSON.parse(opts.initial); - setLoaded(true); - } catch { - data.current = undefined; + if (opts.initial !== null) { + try { + data.current = JSON.parse(opts.initial); + setLoaded(true); + } catch { + data.current = undefined; + } } }, [opts.initial]); From 4d15fa52b87fd2fd4186ff83f0bc849e19a31289 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 5 Nov 2020 17:32:55 +0100 Subject: [PATCH 20/23] Add undo ability --- package-lock.json | 5 +++++ package.json | 1 + src/components/RichTextEditor/RichTextEditor.tsx | 9 ++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index bd2e536dd..99cf8bf48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10350,6 +10350,11 @@ "resolved": "https://registry.npmjs.org/editorjs-inline-tool/-/editorjs-inline-tool-0.4.0.tgz", "integrity": "sha512-Ppb4e8IFPjWuNcoNM4tg9bDSo7FgMYAlqP4UhuV5W2JoJBubV5pUcpLrFrSyGTt1HJVEpbrib134zf4wxO+7VA==" }, + "editorjs-undo": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/editorjs-undo/-/editorjs-undo-0.1.4.tgz", + "integrity": "sha512-YW+5x8tqMXQ+iQD+1OrroqUT1O/erDrysZLJ+8yB+zjls55rwUw9P7hdQCJ0DFm+WDSaRuEXb2p3dQ6VdrkYCg==" + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", diff --git a/package.json b/package.json index a589c198e..f9f472a16 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "draftail": "^1.2.1", "draftjs-to-html": "^0.9.1", "editorjs-inline-tool": "^0.4.0", + "editorjs-undo": "^0.1.4", "fast-array-diff": "^0.2.0", "fuzzaldrin": "^2.1.0", "graphql": "^14.4.2", diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index f853f205a..cc17b8de8 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -3,6 +3,7 @@ import FormControl from "@material-ui/core/FormControl"; import FormHelperText from "@material-ui/core/FormHelperText"; import InputLabel from "@material-ui/core/InputLabel"; import classNames from "classnames"; +import Undo from "editorjs-undo"; import React from "react"; import { RichTextEditorContentProps, tools } from "./RichTextEditorContent"; @@ -43,7 +44,13 @@ const RichTextEditor: React.FC = ({ const savedData = await api.saver.save(); onChange(savedData); }, - onReady, + onReady: () => { + const undo = new Undo({ editor }); + undo.initialize(data); + if (onReady) { + onReady(); + } + }, readOnly: disabled, tools }); From 3de9ae58df67b173fd788638b299f369255541c4 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Fri, 6 Nov 2020 09:34:05 +0100 Subject: [PATCH 21/23] Update tests --- .../__snapshots__/Stories.test.ts.snap | 16146 +--------------- src/utils/richText/useRichText.test.ts | 2 +- 2 files changed, 587 insertions(+), 15561 deletions(-) diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index de4af41f4..60d02047d 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -7655,520 +7655,96 @@ exports[`Storyshots Generics / Rich text editor default 1`] = ` class="MuiCardContent-root-id" >
-
-
- Content -
-
- -
-
-
-
-
-
- - - bold - - -
-
-
-
- - - italic - - -
-
-
-
- - - strikethrough - - -
-
-

-
- - - h1 - - -
-

-

-
- - - h2 - - -
-

-

-
- - - h3 - - -
-

-
-
- - - blockquote - - -
-
-
    -
  • -
    - - - ul - - -
    -
  • -
-
    -
  1. -
    - - - ol - - -
    -
  2. -
-
-
- - - link - - -
-
-
-
-
-
-
-
+ Content + +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit +

+
+
+
+
+`; + +exports[`Storyshots Generics / Rich text editor disabled 1`] = ` +
+
+
+
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit +

+
+
+
+
+`; + +exports[`Storyshots Generics / Rich text editor error 1`] = ` +
+
+
+
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit +

@@ -31726,316 +31302,22 @@ exports[`Storyshots Views / Categories / Create category When loading 1`] = ` class="FormSpacer-spacer-id" />
-
-
- Category Description -
-
- -
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
+ Category Description + +
+

@@ -32309,316 +31591,22 @@ exports[`Storyshots Views / Categories / Create category default 1`] = ` class="FormSpacer-spacer-id" />
-
-
- Category Description -
-
- -
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
+ Category Description + +
+

@@ -32897,321 +31885,24 @@ exports[`Storyshots Views / Categories / Create category form errors 1`] = ` class="FormSpacer-spacer-id" />
-
-
- Category Description -
-
- -
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
+ Category Description +
+

This field is required -

+

@@ -33484,520 +32175,22 @@ exports[`Storyshots Views / Categories / Update category default 1`] = ` class="FormSpacer-spacer-id" />
-
-
- Category Description -
-
- -
-
-
-
-
-
- - - bold - - -
-
-
-
- - - italic - - -
-
-
-
- - - strikethrough - - -
-
-

-
- - - h1 - - -
-

-

-
- - - h2 - - -
-

-

-
- - - h3 - - -
-

-
-
- - - blockquote - - -
-
-
    -
  • -
    - - - ul - - -
    -
  • -
-
    -
  1. -
    - - - ol - - -
    -
  2. -
-
-
- - - link - - -
-
-
-
-
-
-
-
+ Category Description + +
+

@@ -34216,195 +32409,13 @@ Ctrl + K" class="CardTitle-hr-id" /> - - - - - - - - - - - -