diff --git a/app/theme/client/imports/components/emojiPicker.css b/app/theme/client/imports/components/emojiPicker.css index f8601a798a4ce..9d5c0d5770443 100644 --- a/app/theme/client/imports/components/emojiPicker.css +++ b/app/theme/client/imports/components/emojiPicker.css @@ -22,7 +22,7 @@ .emoji-picker { position: absolute; - z-index: 100; + z-index: 9999; display: none; diff --git a/ee/client/omnichannel/cannedResponses/components/MarkdownTextEditor/InsertPlaceholderDropdown.tsx b/ee/client/omnichannel/cannedResponses/components/MarkdownTextEditor/InsertPlaceholderDropdown.tsx new file mode 100644 index 0000000000000..1f8e809dbbfc0 --- /dev/null +++ b/ee/client/omnichannel/cannedResponses/components/MarkdownTextEditor/InsertPlaceholderDropdown.tsx @@ -0,0 +1,77 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Divider } from '@rocket.chat/fuselage'; +import React, { Dispatch, FC, memo, RefObject, SetStateAction } from 'react'; + +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; + +const InsertPlaceholderDropdown: FC<{ + textAreaRef: RefObject; + setVisible: Dispatch>; +}> = ({ textAreaRef, setVisible }) => { + const t = useTranslation(); + + const clickable = css` + cursor: pointer; + `; + + const setPlaceholder = (name: any): void => { + if (textAreaRef?.current) { + const text = textAreaRef.current.value; + const startPos = textAreaRef.current.selectionStart; + const placeholder = `{{${name}}}`; + + textAreaRef.current.value = text.slice(0, startPos) + placeholder + text.slice(startPos); + + textAreaRef.current.focus(); + textAreaRef.current.setSelectionRange( + startPos + placeholder.length, + startPos + placeholder.length, + ); + + setVisible(false); + } + }; + + return ( + + + {t('Contact')} + + + setPlaceholder('contact.name')}> + + {t('Name')} + + + setPlaceholder('contact.email')}> + + {t('Email')} + + + setPlaceholder('contact.phone')}> + + {t('Phone')} + + + + + + {t('Agent')} + + + setPlaceholder('agent.name')}> + + {t('Name')} + + + setPlaceholder('agent.email')}> + + {t('Email')} + + + + + ); +}; + +export default memo(InsertPlaceholderDropdown); diff --git a/ee/client/omnichannel/cannedResponses/components/MarkdownTextEditor/index.tsx b/ee/client/omnichannel/cannedResponses/components/MarkdownTextEditor/index.tsx new file mode 100644 index 0000000000000..df56057973af4 --- /dev/null +++ b/ee/client/omnichannel/cannedResponses/components/MarkdownTextEditor/index.tsx @@ -0,0 +1,122 @@ +import { Box, Divider, PositionAnimated, Tile } from '@rocket.chat/fuselage'; +import React, { FC, memo, useCallback, useRef, useState } from 'react'; + +import { EmojiPicker } from '../../../../../../app/emoji/client'; +import { Backdrop } from '../../../../../../client/components/Backdrop'; +import { useUserPreference } from '../../../../../../client/contexts/UserContext'; +import TextEditor from '../TextEditor'; +import InsertPlaceholderDropdown from './InsertPlaceholderDropdown'; + +const MarkdownTextEditor: FC = () => { + const useEmojisPreference = useUserPreference('useEmojis'); + + const textAreaRef = useRef(null); + const ref = useRef(null); + + const [visible, setVisible] = useState(false); + + const useMarkdownSyntax = (char: '*' | '_' | '~' | '[]()'): (() => void) => + useCallback(() => { + if (textAreaRef?.current) { + const text = textAreaRef.current.value; + const startPos = textAreaRef.current.selectionStart; + const endPos = textAreaRef.current.selectionEnd; + + if (char === '[]()') { + if (startPos !== endPos) { + textAreaRef.current.value = `${text.slice(0, startPos)}[${text.slice( + startPos, + endPos, + )}]()${text.slice(endPos)}`; + } + } else { + textAreaRef.current.value = `${text.slice(0, startPos)}${char}${text.slice( + startPos, + endPos, + )}${char}${text.slice(endPos)}`; + } + textAreaRef.current.focus(); + + if (char === '[]()') { + if (startPos === endPos) { + textAreaRef.current.setSelectionRange(startPos, endPos); + } else { + textAreaRef.current.setSelectionRange(endPos + 3, endPos + 3); + } + } else { + textAreaRef.current.setSelectionRange(startPos + 1, endPos + 1); + } + } + }, [char]); + + const onClickEmoji = (emoji: string): void => { + if (textAreaRef?.current) { + const text = textAreaRef.current.value; + const startPos = textAreaRef.current.selectionStart; + const emojiValue = `:${emoji}: `; + + textAreaRef.current.value = text.slice(0, startPos) + emojiValue + text.slice(startPos); + + textAreaRef.current.focus(); + textAreaRef.current.setSelectionRange( + startPos + emojiValue.length, + startPos + emojiValue.length, + ); + } + }; + + const openEmojiPicker = (): void => { + if (!useEmojisPreference) { + return; + } + + if (EmojiPicker.isOpened()) { + EmojiPicker.close(); + return; + } + + EmojiPicker.open(textAreaRef.current, (emoji: string): void => { + onClickEmoji(emoji); + }); + }; + + const openPlaceholderSelect = (): void => { + textAreaRef?.current && textAreaRef.current.focus(); + setVisible(!visible); + }; + + return ( + + + + + + + + + + + { + textAreaRef?.current && textAreaRef.current.focus(); + setVisible(false); + }} + /> + + + + + + + + + + ); +}; + +export default memo(MarkdownTextEditor); diff --git a/ee/client/omnichannel/cannedResponses/components/MarkdownTextEditor/markdownTextEditor.stories.js b/ee/client/omnichannel/cannedResponses/components/MarkdownTextEditor/markdownTextEditor.stories.js new file mode 100644 index 0000000000000..87162dc55aec6 --- /dev/null +++ b/ee/client/omnichannel/cannedResponses/components/MarkdownTextEditor/markdownTextEditor.stories.js @@ -0,0 +1,10 @@ +import React from 'react'; + +import MarkdownTextEditor from './index.tsx'; + +export default { + title: 'components/MarkdownTextEditor', + component: MarkdownTextEditor, +}; + +export const deafult = () => ; diff --git a/ee/client/omnichannel/cannedResponses/components/TextEditor/IconButton.tsx b/ee/client/omnichannel/cannedResponses/components/TextEditor/IconButton.tsx new file mode 100644 index 0000000000000..b26a3f00fc869 --- /dev/null +++ b/ee/client/omnichannel/cannedResponses/components/TextEditor/IconButton.tsx @@ -0,0 +1,28 @@ +import { Button, Icon } from '@rocket.chat/fuselage'; +import React, { FC, memo } from 'react'; + +type IconButtonProps = { + name: string; + action: () => void; +}; + +const IconButton: FC = ({ name, action }) => ( + +); +export default memo(IconButton); diff --git a/ee/client/omnichannel/cannedResponses/components/TextEditor/TextButton.tsx b/ee/client/omnichannel/cannedResponses/components/TextEditor/TextButton.tsx new file mode 100644 index 0000000000000..cc5aa7ea4faa4 --- /dev/null +++ b/ee/client/omnichannel/cannedResponses/components/TextEditor/TextButton.tsx @@ -0,0 +1,35 @@ +import { Button } from '@rocket.chat/fuselage'; +import React, { forwardRef, memo } from 'react'; + +import { + TranslationKey, + useTranslation, +} from '../../../../../../client/contexts/TranslationContext'; + +type TextButtonProps = { + text: TranslationKey; + action: () => void; +}; + +const TextButton = forwardRef(function TextButton({ text, action }, ref) { + const t = useTranslation(); + + return ( + + ); +}); +export default memo(TextButton); diff --git a/ee/client/omnichannel/cannedResponses/components/TextEditor/Textarea.tsx b/ee/client/omnichannel/cannedResponses/components/TextEditor/Textarea.tsx new file mode 100644 index 0000000000000..bcd222419a677 --- /dev/null +++ b/ee/client/omnichannel/cannedResponses/components/TextEditor/Textarea.tsx @@ -0,0 +1,20 @@ +import { Box } from '@rocket.chat/fuselage'; +import React, { ComponentProps, forwardRef } from 'react'; + +type TextareaProps = ComponentProps; + +const Textarea = forwardRef(function Textarea(props, ref) { + return ( + + ); +}); + +export default Textarea; diff --git a/ee/client/omnichannel/cannedResponses/components/TextEditor/Toolbox.tsx b/ee/client/omnichannel/cannedResponses/components/TextEditor/Toolbox.tsx new file mode 100644 index 0000000000000..3475202b41779 --- /dev/null +++ b/ee/client/omnichannel/cannedResponses/components/TextEditor/Toolbox.tsx @@ -0,0 +1,18 @@ +import { Box } from '@rocket.chat/fuselage'; +import React, { FC } from 'react'; + +import IconButton from './IconButton'; +import TextButton from './TextButton'; + +const Toolbox: FC = ({ children }) => ( + <> + + {children} + + +); + +export default Object.assign(Toolbox, { + IconButton, + TextButton, +}); diff --git a/ee/client/omnichannel/cannedResponses/components/TextEditor/index.tsx b/ee/client/omnichannel/cannedResponses/components/TextEditor/index.tsx new file mode 100644 index 0000000000000..7352afcc022a9 --- /dev/null +++ b/ee/client/omnichannel/cannedResponses/components/TextEditor/index.tsx @@ -0,0 +1,29 @@ +import { Box } from '@rocket.chat/fuselage'; +import React, { FC } from 'react'; + +import Textarea from './Textarea'; +import Toolbox from './Toolbox'; + +type TextEditorType = { + Toolbox?: FC; + Textarea?: FC; +}; + +const TextEditor: FC = ({ children }) => ( + + {children} + +); + +export default Object.assign(TextEditor, { + Toolbox, + Textarea, +}); diff --git a/ee/client/omnichannel/cannedResponses/components/TextEditor/textEditor.stories.js b/ee/client/omnichannel/cannedResponses/components/TextEditor/textEditor.stories.js new file mode 100644 index 0000000000000..f7ae74e137787 --- /dev/null +++ b/ee/client/omnichannel/cannedResponses/components/TextEditor/textEditor.stories.js @@ -0,0 +1,37 @@ +import { Divider } from '@rocket.chat/fuselage'; +import React, { useRef } from 'react'; + +import TextEditor from './index.tsx'; + +export default { + title: 'components/TextEditor', + component: TextEditor, +}; + +export const deafult = () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const textAreaRef = useRef(); + + const action = () => { + const text = textAreaRef.current.value; + const startPos = textAreaRef.current.selectionStart; + const endPos = textAreaRef.current.selectionEnd; + textAreaRef.current.value = `${text.slice(0, startPos)}*${text.slice( + startPos, + endPos, + )}*${text.slice(endPos)}`; + textAreaRef.current.focus(); + textAreaRef.current.setSelectionRange(startPos + 1, endPos + 1); + }; + + return ( + + + + + + + + + ); +}; diff --git a/package-lock.json b/package-lock.json index 830677962d275..945df8a02f421 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6323,15 +6323,42 @@ } }, "@rocket.chat/fuselage": { - "version": "0.6.3-dev.255", - "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage/-/fuselage-0.6.3-dev.255.tgz", - "integrity": "sha512-zd6lGbR3aJ/Yds/9fmbZmdFyKFZzCHmgaHwLzWoucS4Zpzgd2xIq4H0GAKg7Lx/Isb/e5KnDMNQTiGVkpSMPKg==", + "version": "0.6.3-dev.266", + "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage/-/fuselage-0.6.3-dev.266.tgz", + "integrity": "sha512-YqSOLjU5S88Re1Dc0JadqJLRJRefQEviWNEcNz6sJjT4VlJWvmdvliYLw6CAIbyY/nhHMdOKzhqGu1w+hJlq2w==", "requires": { - "@rocket.chat/css-in-js": "^0.24.0", - "@rocket.chat/fuselage-tokens": "^0.24.0", - "@rocket.chat/memo": "^0.24.0", + "@rocket.chat/css-in-js": "^0.26.0", + "@rocket.chat/fuselage-tokens": "^0.26.0", + "@rocket.chat/memo": "^0.26.0", "invariant": "^2.2.4", "react-keyed-flatten-children": "^1.3.0" + }, + "dependencies": { + "@rocket.chat/css-in-js": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@rocket.chat/css-in-js/-/css-in-js-0.26.0.tgz", + "integrity": "sha512-QYwAGdXcNxGOdd2tYdJqsIf3sG9+8Ji3L433bsBdWaYOTdam2z/Nu0zpdz94mvVegU7+0WV02tc+hFwyztXjIQ==", + "requires": { + "@emotion/hash": "^0.8.0", + "@rocket.chat/memo": "^0.26.0", + "stylis": "^4.0.10" + } + }, + "@rocket.chat/fuselage-tokens": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-tokens/-/fuselage-tokens-0.26.0.tgz", + "integrity": "sha512-ZNZXd1v96Yhjz9kYZvJigBVt6TE4WWSj0B/R3oLPqDntBU3ycgv7DwMYJCbD63l8T3v54TCoa/kMtF55mv2BVw==" + }, + "@rocket.chat/memo": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@rocket.chat/memo/-/memo-0.26.0.tgz", + "integrity": "sha512-DKzQi/KINI3JhSpj8odXP2uWv77uZwRIfoN2kBCppNXhVeXRnsyt5p9L54i1uKRESnymSi0gPmpFy7UOLgJi3Q==" + }, + "stylis": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz", + "integrity": "sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==" + } } }, "@rocket.chat/fuselage-hooks": { @@ -14193,22 +14220,50 @@ } }, "@types/react": { - "version": "16.9.19", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.19.tgz", - "integrity": "sha512-LJV97//H+zqKWMms0kvxaKYJDG05U2TtQB3chRLF8MPNs+MQh/H1aGlyDUxjaHvu08EAGerdX2z4LTBc7ns77A==", + "version": "17.0.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.9.tgz", + "integrity": "sha512-2Cw7FvevpJxQrCb+k5t6GH1KIvmadj5uBbjPaLlJB/nZWUj56e1ZqcD6zsoMFB47MsJUTFl9RJ132A7hb3QFJA==", "dev": true, "requires": { "@types/prop-types": "*", - "csstype": "^2.2.0" + "@types/scheduler": "*", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", + "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==", + "dev": true + } } }, "@types/react-dom": { - "version": "16.9.8", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz", - "integrity": "sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA==", + "version": "16.9.13", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.13.tgz", + "integrity": "sha512-34Hr3XnmUSJbUVDxIw/e7dhQn2BJZhJmlAaPyPwfTQyuVS9mV/CeyghFcXyvkJXxI7notQJz8mF8FeCVvloJrA==", "dev": true, "requires": { - "@types/react": "*" + "@types/react": "^16" + }, + "dependencies": { + "@types/react": { + "version": "16.14.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.8.tgz", + "integrity": "sha512-QN0/Qhmx+l4moe7WJuTxNiTsjBwlBGHqKGvInSQCBdo7Qio0VtOqwsC0Wq7q3PbJlB0cR4Y4CVo1OOe6BOsOmA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "csstype": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", + "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==", + "dev": true + } } }, "@types/react-syntax-highlighter": { @@ -14254,6 +14309,12 @@ "integrity": "sha512-uD0j/AQOa5le7afuK+u+woi8jNKF1vf3DN0H7LCJhft/lNNibUr7VcAesdgtWfEKveZol3ZG1CJqwx2Bhrnl8w==", "dev": true }, + "@types/scheduler": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", + "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==", + "dev": true + }, "@types/semver": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.3.tgz", diff --git a/package.json b/package.json index e3c12447e4b59..70245008d780b 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,8 @@ "@types/moment-timezone": "^0.5.30", "@types/mongodb": "^3.5.26", "@types/node": "^12.20.10", - "@types/react-dom": "^16.9.8", + "@types/react": "^17.0.9", + "@types/react-dom": "^16.9.13", "@types/rewire": "^2.5.28", "@types/semver": "^7.3.3", "@types/toastr": "^2.1.38", @@ -143,7 +144,7 @@ "@rocket.chat/apps-engine": "1.25.0", "@rocket.chat/css-in-js": "^0.25.0", "@rocket.chat/emitter": "^0.25.0", - "@rocket.chat/fuselage": "^0.6.3-dev.255", + "@rocket.chat/fuselage": "^0.6.3-dev.266", "@rocket.chat/fuselage-hooks": "^0.25.0", "@rocket.chat/fuselage-polyfills": "^0.25.0", "@rocket.chat/fuselage-tokens": "^0.25.0", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 3339bc295573f..433c812be8329 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2141,6 +2141,7 @@ "Instructions": "Instructions", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Instructions to your visitor fill the form to send a message", "Insert_Contact_Name": "Insert the Contact Name", + "Insert_Placeholder": "Insert Placeholder", "Insurance": "Insurance", "Integration_added": "Integration has been added", "Integration_Advanced_Settings": "Advanced Settings", diff --git a/packages/rocketchat-i18n/i18n/pt.i18n.json b/packages/rocketchat-i18n/i18n/pt.i18n.json index 8746d53394156..7c95c0756c813 100644 --- a/packages/rocketchat-i18n/i18n/pt.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt.i18n.json @@ -1608,6 +1608,7 @@ "Info": "Informações", "initials_avatar": "Avatar Inicial", "inline_code": "código em linha", + "Insert_Placeholder": "Inserir Substituto", "Install": "Instalar", "Install_Extension": "Instalar Extensão", "Install_FxOs": "Instale o Rocket.Chat no seu Firefox",