diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index c46bcf44b0539f..3ea6f1fa261a1f 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1375,5 +1375,6 @@ "route_to": "Route to", "test_preview_description": "Test your routing form without submitting any data", "test_routing": "Test Routing", - "booking_limit_reached":"Booking Limit for this event type has been reached" + "booking_limit_reached":"Booking Limit for this event type has been reached", + "fill_this_field": "Please fill in this field" } diff --git a/packages/features/ee/workflows/components/AddVariablesDropdown.tsx b/packages/features/ee/workflows/components/AddVariablesDropdown.tsx index 909bd5ea3b87a5..f02c4cf270b62f 100644 --- a/packages/features/ee/workflows/components/AddVariablesDropdown.tsx +++ b/packages/features/ee/workflows/components/AddVariablesDropdown.tsx @@ -2,8 +2,9 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Dropdown, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Icon } from "@calcom/ui"; interface IAddVariablesDropdown { - addVariable: (isEmailSubject: boolean, variable: string) => void; - isEmailSubject: boolean; + addVariable: (variable: string, isEmailSubject?: boolean) => void; + isEmailSubject?: boolean; + isTextEditor?: boolean; } const variables = [ @@ -23,9 +24,21 @@ export const AddVariablesDropdown = (props: IAddVariablesDropdown) => { return ( -
- {t("add_variable")} - +
+ {props.isTextEditor ? ( + <> +
+ {t("add_variable")} + +
+
+
+ + ) : ( +
+ {t("add_variable")} + +
+ )}
@@ -39,7 +52,7 @@ export const AddVariablesDropdown = (props: IAddVariablesDropdown) => { key={variable} type="button" className="w-full px-4 py-2" - onClick={() => props.addVariable(props.isEmailSubject, t(`${variable}_workflow`))}> + onClick={() => props.addVariable(t(`${variable}_workflow`), props.isEmailSubject)}>
{`{${t(`${variable}_workflow`).toUpperCase().replace(/ /g, "_")}}`} diff --git a/packages/features/ee/workflows/components/TextEditor/Editor.tsx b/packages/features/ee/workflows/components/TextEditor/Editor.tsx new file mode 100644 index 00000000000000..dd66260409e278 --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/Editor.tsx @@ -0,0 +1,69 @@ +import { CodeHighlightNode, CodeNode } from "@lexical/code"; +import { AutoLinkNode, LinkNode } from "@lexical/link"; +import { ListItemNode, ListNode } from "@lexical/list"; +import { TRANSFORMERS } from "@lexical/markdown"; +import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin"; +import { LexicalComposer } from "@lexical/react/LexicalComposer"; +import { ContentEditable } from "@lexical/react/LexicalContentEditable"; +import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"; +import { ListPlugin } from "@lexical/react/LexicalListPlugin"; +import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"; +import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; +import { HeadingNode, QuoteNode } from "@lexical/rich-text"; +import { TableCellNode, TableNode, TableRowNode } from "@lexical/table"; +import { UseFormReturn } from "react-hook-form"; + +import { FormValues } from "../../pages/workflow"; +import ExampleTheme from "./ExampleTheme"; +import AutoLinkPlugin from "./plugins/AutoLinkPlugin"; +import ToolbarPlugin from "./plugins/ToolbarPlugin"; +import "./stylesEditor.css"; + +export type TextEditorProps = { + form: UseFormReturn; + stepNumber: number; +}; + +const editorConfig = { + // The editor theme + theme: ExampleTheme, + // Handling of errors during update + onError(error: any) { + throw error; + }, + namespace: "", + // Any custom nodes go here + nodes: [ + HeadingNode, + ListNode, + ListItemNode, + QuoteNode, + CodeNode, + CodeHighlightNode, + TableNode, + TableCellNode, + TableRowNode, + AutoLinkNode, + LinkNode, + ], +}; + +export default function Editor(props: TextEditorProps) { + return ( +
+ +
+ +
+ } placeholder="" /> + + + + + +
+
+
+
+ ); +} diff --git a/packages/features/ee/workflows/components/TextEditor/ExampleTheme.ts b/packages/features/ee/workflows/components/TextEditor/ExampleTheme.ts new file mode 100644 index 00000000000000..c08b06cbc7db0d --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/ExampleTheme.ts @@ -0,0 +1,24 @@ +const exampleTheme = { + placeholder: "editor-placeholder", + paragraph: "editor-paragraph", + heading: { + h1: "editor-heading-h1", + h2: "editor-heading-h2", + }, + list: { + nested: { + listitem: "editor-nested-listitem", + }, + ol: "editor-list-ol", + ul: "editor-list-ul", + listitem: "editor-listitem", + }, + image: "editor-image", + link: "editor-link", + text: { + bold: "editor-text-bold", + italic: "editor-text-italic", + }, +}; + +export default exampleTheme; diff --git a/packages/features/ee/workflows/components/TextEditor/images/icons/link.svg b/packages/features/ee/workflows/components/TextEditor/images/icons/link.svg new file mode 100644 index 00000000000000..fc4594c325da7a --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/images/icons/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/features/ee/workflows/components/TextEditor/images/icons/list-ol.svg b/packages/features/ee/workflows/components/TextEditor/images/icons/list-ol.svg new file mode 100644 index 00000000000000..d728bd74c64759 --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/images/icons/list-ol.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/features/ee/workflows/components/TextEditor/images/icons/list-ul.svg b/packages/features/ee/workflows/components/TextEditor/images/icons/list-ul.svg new file mode 100644 index 00000000000000..00b9841836c1f6 --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/images/icons/list-ul.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/features/ee/workflows/components/TextEditor/images/icons/pencil-fill.svg b/packages/features/ee/workflows/components/TextEditor/images/icons/pencil-fill.svg new file mode 100644 index 00000000000000..b2ba1fb28f2c5f --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/images/icons/pencil-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/features/ee/workflows/components/TextEditor/images/icons/text-paragraph.svg b/packages/features/ee/workflows/components/TextEditor/images/icons/text-paragraph.svg new file mode 100644 index 00000000000000..4b8dadf106d6b2 --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/images/icons/text-paragraph.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/features/ee/workflows/components/TextEditor/images/icons/type-bold.svg b/packages/features/ee/workflows/components/TextEditor/images/icons/type-bold.svg new file mode 100644 index 00000000000000..4546eaaa30e5cb --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/images/icons/type-bold.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/features/ee/workflows/components/TextEditor/images/icons/type-h1.svg b/packages/features/ee/workflows/components/TextEditor/images/icons/type-h1.svg new file mode 100644 index 00000000000000..6c57b9e3cf8bc4 --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/images/icons/type-h1.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/features/ee/workflows/components/TextEditor/images/icons/type-h2.svg b/packages/features/ee/workflows/components/TextEditor/images/icons/type-h2.svg new file mode 100644 index 00000000000000..329d7cdd59cbd7 --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/images/icons/type-h2.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/features/ee/workflows/components/TextEditor/images/icons/type-italic.svg b/packages/features/ee/workflows/components/TextEditor/images/icons/type-italic.svg new file mode 100644 index 00000000000000..0282636dc14756 --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/images/icons/type-italic.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/features/ee/workflows/components/TextEditor/plugins/AutoLinkPlugin.tsx b/packages/features/ee/workflows/components/TextEditor/plugins/AutoLinkPlugin.tsx new file mode 100644 index 00000000000000..95375dc3ceb901 --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/plugins/AutoLinkPlugin.tsx @@ -0,0 +1,36 @@ +import { AutoLinkPlugin } from "@lexical/react/LexicalAutoLinkPlugin"; + +const URL_MATCHER = + /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/; + +const EMAIL_MATCHER = + /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/; + +const MATCHERS = [ + (text: any) => { + const match = URL_MATCHER.exec(text); + return ( + match && { + index: match.index, + length: match[0].length, + text: match[0], + url: match[0], + } + ); + }, + (text: any) => { + const match = EMAIL_MATCHER.exec(text); + return ( + match && { + index: match.index, + length: match[0].length, + text: match[0], + url: `mailto:${match[0]}`, + } + ); + }, +]; + +export default function PlaygroundAutoLinkPlugin() { + return ; +} diff --git a/packages/features/ee/workflows/components/TextEditor/plugins/ToolbarPlugin.tsx b/packages/features/ee/workflows/components/TextEditor/plugins/ToolbarPlugin.tsx new file mode 100644 index 00000000000000..e650e549593b59 --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/plugins/ToolbarPlugin.tsx @@ -0,0 +1,526 @@ +import { $generateHtmlFromNodes, $generateNodesFromDOM } from "@lexical/html"; +import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; +import { + INSERT_ORDERED_LIST_COMMAND, + INSERT_UNORDERED_LIST_COMMAND, + REMOVE_LIST_COMMAND, + $isListNode, + ListNode, +} from "@lexical/list"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { $createHeadingNode, $isHeadingNode } from "@lexical/rich-text"; +import { $wrapNodes, $isAtNodeEnd } from "@lexical/selection"; +import { $getNearestNodeOfType, mergeRegister } from "@lexical/utils"; +import { + SELECTION_CHANGE_COMMAND, + FORMAT_TEXT_COMMAND, + $getSelection, + $isRangeSelection, + $createParagraphNode, + RangeSelection, + NodeSelection, + GridSelection, + $getRoot, + $insertNodes, + LexicalEditor, + EditorState, +} from "lexical"; +import { RefObject, Dispatch, SetStateAction } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; + +import { Icon } from "@calcom/ui"; + +import { AddVariablesDropdown } from "../../AddVariablesDropdown"; +import { TextEditorProps } from "../Editor"; + +const LowPriority = 1; + +const supportedBlockTypes = new Set(["paragraph", "h1", "h2", "ul", "ol"]); + +interface BlockType { + [key: string]: string; +} + +const blockTypeToBlockName: BlockType = { + h1: "Large Heading", + h2: "Small Heading", + ol: "Numbered List", + paragraph: "Normal", + ul: "Bulleted List", +}; + +function positionEditorElement(editor: HTMLInputElement, rect: DOMRect | null) { + if (rect === null) { + editor.style.opacity = "0"; + editor.style.top = "-1000px"; + editor.style.left = "-1000px"; + } else { + editor.style.opacity = "1"; + editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`; + editor.style.left = `${rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2}px`; + } +} + +function FloatingLinkEditor({ editor }: { editor: LexicalEditor }) { + const editorRef = useRef(null); + const mouseDownRef = useRef(false); + const inputRef = useRef(null); + const [linkUrl, setLinkUrl] = useState(""); + const [isEditMode, setEditMode] = useState(false); + const [lastSelection, setLastSelection] = useState( + null + ); + + const updateLinkEditor = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const node = getSelectedNode(selection); + const parent = node.getParent(); + if ($isLinkNode(parent)) { + setLinkUrl(parent.getURL()); + } else if ($isLinkNode(node)) { + setLinkUrl(node.getURL()); + } else { + setLinkUrl(""); + } + } + const editorElem = editorRef.current; + const nativeSelection = window.getSelection(); + const activeElement = document.activeElement; + + if (editorElem === null) { + return; + } + + const rootElement = editor.getRootElement(); + if ( + selection !== null && + !nativeSelection?.isCollapsed && + rootElement !== null && + rootElement.contains(nativeSelection?.anchorNode || null) + ) { + const domRange = nativeSelection?.getRangeAt(0); + let rect: DOMRect | undefined; + if (nativeSelection?.anchorNode === rootElement) { + let inner: Element = rootElement; + while (inner.firstElementChild != null) { + inner = inner.firstElementChild; + } + rect = inner.getBoundingClientRect(); + } else { + rect = domRange?.getBoundingClientRect(); + } + if (!mouseDownRef.current) { + positionEditorElement(editorElem, rect || null); + } + + setLastSelection(selection); + } else if (!activeElement || activeElement.className !== "link-input") { + positionEditorElement(editorElem, null); + setLastSelection(null); + setEditMode(false); + setLinkUrl(""); + } + + return true; + }, [editor]); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({ editorState }: { editorState: EditorState }) => { + editorState.read(() => { + updateLinkEditor(); + }); + }), + + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + () => { + updateLinkEditor(); + return true; + }, + LowPriority + ) + ); + }, [editor, updateLinkEditor]); + + useEffect(() => { + editor.getEditorState().read(() => { + updateLinkEditor(); + }); + }, [editor, updateLinkEditor]); + + useEffect(() => { + if (isEditMode && inputRef.current) { + inputRef.current.focus(); + } + }, [isEditMode]); + + return ( +
+ {isEditMode ? ( + { + setLinkUrl(event.target.value); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + if (lastSelection !== null) { + if (linkUrl !== "") { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl); + } + setEditMode(false); + } + } else if (event.key === "Escape") { + event.preventDefault(); + setEditMode(false); + } + }} + /> + ) : ( + <> +
+ + {linkUrl} + +
event.preventDefault()} + onClick={() => { + setEditMode(true); + }} + /> +
+ + )} +
+ ); +} + +function getSelectedNode(selection: RangeSelection) { + const anchor = selection.anchor; + const focus = selection.focus; + const anchorNode = selection.anchor.getNode(); + const focusNode = selection.focus.getNode(); + if (anchorNode === focusNode) { + return anchorNode; + } + const isBackward = selection.isBackward(); + if (isBackward) { + return $isAtNodeEnd(focus) ? anchorNode : focusNode; + } else { + return $isAtNodeEnd(anchor) ? focusNode : anchorNode; + } +} + +type BlockOptionsDropdownProps = { + editor: LexicalEditor; + blockType: string; + toolbarRef: RefObject; + setShowBlockOptionsDropDown: Dispatch>; +}; + +function BlockOptionsDropdownList({ + editor, + blockType, + toolbarRef, + setShowBlockOptionsDropDown, +}: BlockOptionsDropdownProps) { + const dropDownRef = useRef(null); + + useEffect(() => { + const toolbar = toolbarRef.current; + const dropDown = dropDownRef.current; + + if (toolbar !== null && dropDown) { + const { top, left } = toolbar.getBoundingClientRect(); + dropDown.style.top = `${top + 40}px`; + dropDown.style.left = `${left}px`; + } + }, [dropDownRef, toolbarRef]); + + useEffect(() => { + const dropDown = dropDownRef.current; + const toolbar = toolbarRef.current; + + if (dropDown && toolbar !== null) { + const handle = (event: any) => { + const target = event.target; + + if (!dropDown.contains(target) && !toolbar.contains(target)) { + setShowBlockOptionsDropDown(false); + } + }; + document.addEventListener("click", handle); + + return () => { + document.removeEventListener("click", handle); + }; + } + }, [dropDownRef, setShowBlockOptionsDropDown, toolbarRef]); + + const formatParagraph = () => { + if (blockType !== "paragraph") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createParagraphNode()); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatLargeHeading = () => { + if (blockType !== "h1") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createHeadingNode("h1")); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatSmallHeading = () => { + if (blockType !== "h2") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createHeadingNode("h2")); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatBulletList = () => { + if (blockType !== "ul") { + editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined); + } else { + editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined); + } + setShowBlockOptionsDropDown(false); + }; + + const formatNumberedList = () => { + if (blockType !== "ol") { + editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined); + } else { + editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined); + } + setShowBlockOptionsDropDown(false); + }; + + return ( +
+ + + + + +
+ ); +} + +export default function ToolbarPlugin(props: TextEditorProps) { + const [editor] = useLexicalComposerContext(); + const toolbarRef = useRef(null); + const [blockType, setBlockType] = useState("paragraph"); + const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = useState(false); + const [isLink, setIsLink] = useState(false); + const [isBold, setIsBold] = useState(false); + const [isItalic, setIsItalic] = useState(false); + + const updateToolbar = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const anchorNode = selection.anchor.getNode(); + const element = anchorNode.getKey() === "root" ? anchorNode : anchorNode.getTopLevelElementOrThrow(); + const elementKey = element.getKey(); + const elementDOM = editor.getElementByKey(elementKey); + if (elementDOM !== null) { + if ($isListNode(element)) { + const parentList = $getNearestNodeOfType(anchorNode, ListNode); + const type = parentList ? parentList.getTag() : element.getTag(); + setBlockType(type); + } else { + const type = $isHeadingNode(element) ? element.getTag() : element.getType(); + setBlockType(type); + } + } + setIsBold(selection.hasFormat("bold")); + setIsItalic(selection.hasFormat("italic")); + + const node = getSelectedNode(selection); + const parent = node.getParent(); + if ($isLinkNode(parent) || $isLinkNode(node)) { + setIsLink(true); + } else { + setIsLink(false); + } + } + }, [editor]); + + const addVariable = (variable: string) => { + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + editor.update(() => { + const formatedVariable = `{${variable.toUpperCase().replace(/ /g, "_")}}`; + selection?.insertRawText(formatedVariable); + }); + } + }); + }; + + useEffect(() => { + editor.update(() => { + const parser = new DOMParser(); + const dom = parser.parseFromString( + props.form.getValues(`steps.${props.stepNumber - 1}.reminderBody`) || "", + "text/html" + ); + + const nodes = $generateNodesFromDOM(editor, dom); + + $getRoot().select(); + $insertNodes(nodes); + + editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + const textInHtml = $generateHtmlFromNodes(editor); + props.form.setValue(`steps.${props.stepNumber - 1}.reminderBody`, textInHtml); + props.form.clearErrors(); + }); + }); + }); + }, []); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + updateToolbar(); + }); + }), + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + (_payload, newEditor) => { + updateToolbar(); + return false; + }, + LowPriority + ) + ); + }, [editor, updateToolbar]); + + const insertLink = useCallback(() => { + if (!isLink) { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://"); + } else { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); + } + }, [editor, isLink]); + + return ( +
+ <> + {supportedBlockTypes.has(blockType) && ( + <> + + {showBlockOptionsDropDown && + createPortal( + , + document.body + )} + + )} + + <> + + + + {isLink && createPortal(, document.body)}{" "} + +
+ +
+ +
+ ); +} diff --git a/packages/features/ee/workflows/components/TextEditor/stylesEditor.css b/packages/features/ee/workflows/components/TextEditor/stylesEditor.css new file mode 100644 index 00000000000000..9562eb9329f918 --- /dev/null +++ b/packages/features/ee/workflows/components/TextEditor/stylesEditor.css @@ -0,0 +1,402 @@ + +.editor a { + color: #0000FF; + text-decoration: underline; + font-size: 14px; +} + +.editor li { + padding-left: 1.28571429em; + text-indent: -1.28571429em; +} + +.editor ul { + list-style: disc inside +} + +.editor ol { + list-style: decimal inside +} + +.editor-container { + border-radius: 6px; + position: relative; + line-height: 20px; + font-weight: 400; + text-align: left; + border-color: #D1D5DB; + border-width: 1px; + padding: 1px +} + +.editor-inner { + background: #fff; + position: relative; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + height: 200px; + overflow: scroll; + resize: vertical; +} + +.editor-input { + min-height: 150px; + font-size: 14px; + position: relative; + tab-size: 1; + outline: 0; + padding: 15px 10px; + outline: none +} + +.editor-text-bold { + font-weight: bold; +} + +.editor-text-italic { + font-style: italic; +} + +.editor-link { + color: rgb(33, 111, 219); + text-decoration: none; +} + +.editor-tokenFunction { + color: #dd4a68; +} + +.editor-paragraph { + margin: 0; + margin-bottom: 8px; + position: relative; +} + +.editor-paragraph:last-child { + margin-bottom: 0; +} + +.editor-heading-h1 { + font-size: 25px; + font-weight: 400; + margin-bottom: 20px; + font-weight: bold; +} + + +.editor-heading-h2 { + font-size: 20px; + font-weight: bold; + margin-bottom: 20px; +} + +.editor-list-ul { + margin-bottom: 12px; +} + +.editor-list-ol { + margin-bottom: 12px; +} + + +.editor-listitem { + margin: 0px 32px; +} + +.editor-nested-listitem { + list-style-type: none; +} + +pre::-webkit-scrollbar { + background: transparent; + width: 10px; +} + +pre::-webkit-scrollbar-thumb { + background: #999; +} + +.toolbar { + display: flex; + margin-bottom: 1px; + background: #fff; + padding: 2px; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + vertical-align: middle; +} + +.toolbar button.toolbar-item { + border: 0; + display: flex; + background: none; + border-radius: 10px; + padding: 8px; + cursor: pointer; + vertical-align: middle; + height: 36px; +} + +.toolbar button.toolbar-item.spaced { + margin-right: 2px; +} + +.toolbar button.toolbar-item i.format { + background-size: contain; + display: inline-block; + height: 18px; + width: 18px; + margin-top: 2px; + vertical-align: -0.25em; + display: flex; + opacity: 0.6; +} + +.icon.paragraph { + background-image: url(images/icons/text-paragraph.svg); +} + +.toolbar button.toolbar-item.active { + background-color: rgba(223, 232, 250, 0.3); +} + +.toolbar button.toolbar-item.active i { + opacity: 1; +} + +.toolbar .toolbar-item:hover:not([disabled]) { + background-color: #eee; +} + +.toolbar select.toolbar-item { + border: 0; + display: flex; + background: none; + border-radius: 10px; + padding: 8px; + vertical-align: middle; + -webkit-appearance: none; + -moz-appearance: none; + width: 70px; + font-size: 14px; + color: #777; + text-overflow: ellipsis; +} + +.toolbar .toolbar-item .text { + line-height: 20px; + width: 200px; + vertical-align: middle; + font-size: 14px; + color: #777; + text-overflow: ellipsis; + width: 70px; + overflow: hidden; + height: 20px; + text-align: left; +} + +.toolbar .toolbar-item .icon { + display: flex; + width: 20px; + height: 20px; + user-select: none; + margin-right: 8px; + line-height: 16px; + background-size: contain; +} + +#block-controls button:hover { + background-color: #efefef; +} + +#block-controls button:focus-visible { + border-color: blue; +} + +.dropdown { + z-index: 5; + display: block; + position: absolute; + box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1), + inset 0 0 0 1px rgba(255, 255, 255, 0.5); + border-radius: 8px; + min-width: 100px; + min-height: 40px; + background-color: #fff; +} + +.dropdown .item { + margin: 0 8px 0 8px; + padding: 8px; + color: #050505; + cursor: pointer; + line-height: 16px; + font-size: 15px; + display: flex; + align-content: center; + flex-direction: row; + flex-shrink: 0; + justify-content: space-between; + background-color: #fff; + border-radius: 8px; + border: 0; + min-width: 268px; +} + +.dropdown .item .active { + display: flex; + width: 20px; + height: 20px; + background-size: contain; +} + +.dropdown .item:first-child { + margin-top: 8px; +} + +.dropdown .item:last-child { + margin-bottom: 8px; +} + +.dropdown .item:hover { + background-color: #eee; +} + +.dropdown .item .text { + display: flex; + line-height: 20px; + flex-grow: 1; + width: 200px; +} + +.dropdown .item .icon { + display: flex; + width: 20px; + height: 20px; + user-select: none; + margin-right: 12px; + line-height: 16px; + background-size: contain; +} + +.link-editor { + position: absolute; + z-index: 100; + top: -10000px; + left: -10000px; + margin-top: -6px; + max-width: 300px; + width: 100%; + opacity: 0; + background-color: #fff; + box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3); + border-radius: 8px; + transition: opacity 0.5s; +} + +.link-editor .link-input { + display: block; + width: calc(100% - 24px); + box-sizing: border-box; + margin: 8px 12px; + padding: 8px 12px; + border-radius: 15px; + background-color: #eee; + font-size: 15px; + color: rgb(5, 5, 5); + border: 0; + outline: 0; + position: relative; + font-family: inherit; +} + +.link-editor div.link-edit { + background-image: url(images/icons/pencil-fill.svg); + background-size: 16px; + background-position: center; + background-repeat: no-repeat; + width: 35px; + vertical-align: -0.25em; + position: absolute; + right: 0; + top: 0; + bottom: 0; + cursor: pointer; +} + +.link-editor .link-input a { + color: rgb(33, 111, 219); + text-decoration: none; + display: block; + white-space: nowrap; + overflow: hidden; + margin-right: 30px; + text-overflow: ellipsis; +} + +.link-editor .link-input a:hover { + text-decoration: underline; +} + +.link-editor .button { + width: 20px; + height: 20px; + display: inline-block; + padding: 6px; + border-radius: 8px; + cursor: pointer; + margin: 0 2px; +} + +.link-editor .button.hovered { + width: 20px; + height: 20px; + display: inline-block; + background-color: #eee; +} + +.link-editor .button i, +.actions i { + background-size: contain; + display: inline-block; + height: 20px; + width: 20px; + vertical-align: -0.25em; +} + +.icon.paragraph { + background-image: url(images/icons/text-paragraph.svg); +} + +.icon.large-heading, +.icon.h1 { + background-image: url(images/icons/type-h1.svg); +} + +.icon.small-heading, +.icon.h2 { + background-image: url(images/icons/type-h2.svg); +} + +.icon.bullet-list, +.icon.ul { + background-image: url(images/icons/list-ul.svg); +} + +.icon.numbered-list, +.icon.ol { + background-image: url(images/icons/list-ol.svg); +} + +i.bold { + background-image: url(images/icons/type-bold.svg); +} + +i.italic { + background-image: url(images/icons/type-italic.svg); +} + +i.link { + background-image: url(images/icons/link.svg); +} diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index 3f005e96ee3372..c6d610a1efaac9 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -41,6 +41,7 @@ import { } from "../lib/getOptions"; import { translateVariablesToEnglish } from "../lib/variableTranslations"; import type { FormValues } from "../pages/workflow"; +import Editor from "./TextEditor/Editor"; import { TimeTimeUnitInput } from "./TimeTimeUnitInput"; type WorkflowStepProps = { @@ -107,7 +108,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { const refReminderBody = useRef(null); - const addVariable = (isEmailSubject: boolean, variable: string) => { + const addVariable = (variable: string, isEmailSubject?: boolean) => { if (step) { if (isEmailSubject) { const currentEmailSubject = refEmailSubject?.current?.value || ""; @@ -291,23 +292,32 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { className="text-sm" onChange={(val) => { if (val) { - if (val.value === WorkflowActions.SMS_NUMBER) { - setIsPhoneNumberNeeded(true); - setIsSenderIdNeeded(true); - setIsEmailAddressNeeded(false); - } else if (val.value === WorkflowActions.EMAIL_ADDRESS) { - setIsEmailAddressNeeded(true); - setIsPhoneNumberNeeded(false); - setIsSenderIdNeeded(false); - } else if (val.value === WorkflowActions.SMS_ATTENDEE) { + const oldValue = form.getValues(`steps.${step.stepNumber - 1}.action`); + const wasSMSAction = + oldValue === WorkflowActions.SMS_ATTENDEE || + oldValue === WorkflowActions.SMS_NUMBER; + const isSMSAction = + val.value === WorkflowActions.SMS_ATTENDEE || + val.value === WorkflowActions.SMS_NUMBER; + + if (isSMSAction) { setIsSenderIdNeeded(true); setIsEmailAddressNeeded(false); - setIsPhoneNumberNeeded(false); + setIsPhoneNumberNeeded(val.value === WorkflowActions.SMS_NUMBER); + + if (!wasSMSAction) { + form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, ""); + } } else { - setIsEmailAddressNeeded(false); setIsPhoneNumberNeeded(false); setIsSenderIdNeeded(false); + setIsEmailAddressNeeded(val.value === WorkflowActions.EMAIL_ADDRESS); + + if (wasSMSAction) { + form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, ""); + } } + form.unregister(`steps.${step.stepNumber - 1}.sendTo`); form.clearErrors(`steps.${step.stepNumber - 1}.sendTo`); if ( @@ -424,8 +434,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { {isCustomReminderBodyNeeded && (
{isEmailSubjectNeeded && ( -
-
+
+
@@ -436,7 +446,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { emailSubjectFormRef?.(e); refEmailSubject.current = e; }} - className="my-0" + rows={1} + className="my-0 focus:ring-transparent" required {...restEmailSubjectForm} /> @@ -448,26 +459,41 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { )}
)} -
- -
- -
-
-