From da86f1ad03a3f7205549a20456740e74cb59a10b Mon Sep 17 00:00:00 2001 From: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com> Date: Sat, 30 Sep 2023 17:20:59 +0530 Subject: [PATCH] refactoring to LiteTextEditor and RichTextEditor --- packages/editor/core/package.json | 2 +- packages/editor/core/src/hooks/useEditor.tsx | 34 -- packages/editor/core/src/index.ts | 17 +- packages/editor/core/src/interfaces/index.ts | 4 - packages/editor/core/src/lib/utils.ts | 16 +- .../editor/core/src/ui/editor-container.tsx | 20 + .../editor/core/src/ui/editor-content.tsx | 19 + .../image/{updated-image.tsx => index.tsx} | 4 +- .../core/src/ui/extensions/index-new.tsx | 88 ----- .../editor/core/src/ui/extensions/index.tsx | 71 +--- .../editor/core/src/ui/hooks/useEditor.tsx | 70 ++++ packages/editor/core/src/ui/index-new.tsx | 150 -------- packages/editor/core/src/ui/index.tsx | 94 ++--- packages/editor/core/src/useEditor.tsx | 35 -- packages/editor/lite-text-editor/package.json | 25 +- packages/editor/lite-text-editor/src/index.ts | 1 + .../editor/lite-text-editor/src/ui/index.tsx | 96 +++++ .../src/ui/menus/fixed-menu/icon.tsx | 13 + .../src/ui/menus/fixed-menu/index.tsx | 115 ++++++ .../src/ui/menus/fixed-menu/tooltip.tsx | 77 ++++ packages/editor/rich-text-editor/package.json | 37 +- .../rich-text-editor/src/extensions.tsx | 66 ---- packages/editor/rich-text-editor/src/index.ts | 1 + .../editor/rich-text-editor/src/index.tsx | 0 .../rich-text-editor/src/slash-command.tsx | 363 ------------------ .../rich-text-editor/src/table/table-cell.ts | 32 -- .../src/table/table-header.ts | 7 - .../rich-text-editor/src/table/table.ts | 9 - .../src/types/upload-image.ts | 1 - .../src/ui/extensions/index.tsx | 20 + .../src/ui/extensions/slash-command.tsx | 5 +- .../editor/rich-text-editor/src/ui/index.tsx | 83 ++++ .../src/ui/menus/bubble-menu/index.tsx | 121 ++++++ .../ui/menus/bubble-menu/link-selector.tsx | 93 +++++ .../ui/menus/bubble-menu/node-selector.tsx | 130 +++++++ .../src/ui/menus/bubble-menu/utils/index.tsx | 11 + .../peek-overview/comment/add-comment.tsx | 7 +- .../comment/comment-detail-card.tsx | 12 +- .../issues/peek-overview/issue-details.tsx | 7 +- space/package.json | 2 +- space/services/file.service.ts | 18 +- turbo.json | 2 +- .../core/modals/gpt-assistant-modal.tsx | 12 +- web/components/issues/comment/add-comment.tsx | 15 +- .../issues/comment/comment-card.tsx | 12 +- web/components/issues/description-form.tsx | 4 +- web/components/issues/draft-issue-form.tsx | 5 +- web/components/issues/form.tsx | 5 +- .../pages/create-update-block-inline.tsx | 8 +- web/components/pages/single-page-block.tsx | 7 +- web/components/web-view/add-comment.tsx | 5 +- .../web-view/issue-web-view-form.tsx | 7 +- web/package.json | 3 +- web/pages/[workspaceSlug]/editor.tsx | 7 +- .../[workspaceSlug]/me/profile/activity.tsx | 7 +- web/pages/m/[workspaceSlug]/editor.tsx | 7 +- yarn.lock | 2 +- 57 files changed, 1006 insertions(+), 1078 deletions(-) delete mode 100644 packages/editor/core/src/hooks/useEditor.tsx delete mode 100644 packages/editor/core/src/interfaces/index.ts create mode 100644 packages/editor/core/src/ui/editor-container.tsx create mode 100644 packages/editor/core/src/ui/editor-content.tsx rename packages/editor/core/src/ui/extensions/image/{updated-image.tsx => index.tsx} (83%) delete mode 100644 packages/editor/core/src/ui/extensions/index-new.tsx create mode 100644 packages/editor/core/src/ui/hooks/useEditor.tsx delete mode 100644 packages/editor/core/src/ui/index-new.tsx delete mode 100644 packages/editor/core/src/useEditor.tsx create mode 100644 packages/editor/lite-text-editor/src/index.ts create mode 100644 packages/editor/lite-text-editor/src/ui/index.tsx create mode 100644 packages/editor/lite-text-editor/src/ui/menus/fixed-menu/icon.tsx create mode 100644 packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx create mode 100644 packages/editor/lite-text-editor/src/ui/menus/fixed-menu/tooltip.tsx delete mode 100644 packages/editor/rich-text-editor/src/extensions.tsx create mode 100644 packages/editor/rich-text-editor/src/index.ts delete mode 100644 packages/editor/rich-text-editor/src/index.tsx delete mode 100644 packages/editor/rich-text-editor/src/slash-command.tsx delete mode 100644 packages/editor/rich-text-editor/src/table/table-cell.ts delete mode 100644 packages/editor/rich-text-editor/src/table/table-header.ts delete mode 100644 packages/editor/rich-text-editor/src/table/table.ts delete mode 100644 packages/editor/rich-text-editor/src/types/upload-image.ts create mode 100644 packages/editor/rich-text-editor/src/ui/extensions/index.tsx rename packages/editor/{core => rich-text-editor}/src/ui/extensions/slash-command.tsx (98%) create mode 100644 packages/editor/rich-text-editor/src/ui/index.tsx create mode 100644 packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx create mode 100644 packages/editor/rich-text-editor/src/ui/menus/bubble-menu/link-selector.tsx create mode 100644 packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx create mode 100644 packages/editor/rich-text-editor/src/ui/menus/bubble-menu/utils/index.tsx diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index a6a61cffb..9cf27203c 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -1,5 +1,5 @@ { - "name": "@plane/editor", + "name": "@plane/editor-core", "version": "0.0.1", "description": "Rich Text Editor that powers Plane", "main": "./dist/index.mjs", diff --git a/packages/editor/core/src/hooks/useEditor.tsx b/packages/editor/core/src/hooks/useEditor.tsx deleted file mode 100644 index 9d121fcff..000000000 --- a/packages/editor/core/src/hooks/useEditor.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { DeleteImage } from "@/types/delete-image"; -import { UploadImage } from "@/types/upload-image"; -import { TiptapExtensions } from "@/ui/extensions"; -import { TiptapEditorProps } from "@/ui/props"; -import { useEditor as useTiptapEditor } from "@tiptap/react"; - -interface ITiptapEditor { - value: string; - uploadFile: UploadImage; - deleteFile: DeleteImage; - onChange?: (json: any, html: string) => void; - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; - setShouldShowAlert?: (showAlert: boolean) => void; - editable?: boolean; - debouncedUpdatesEnabled?: boolean; - debouncedUpdates: ({ onChange, editor }: { onChange?: (json: any, html: string) => void; editor: any }) => void; -} - -export const useEditor = ({ uploadFile, debouncedUpdates, setShouldShowAlert, deleteFile, setIsSubmitting, value, onChange, debouncedUpdatesEnabled, editable }: ITiptapEditor) => useTiptapEditor({ - editable: editable ?? true, - editorProps: TiptapEditorProps(uploadFile, setIsSubmitting), - extensions: TiptapExtensions(uploadFile, deleteFile, setIsSubmitting), - content: (typeof value === "string" && value.trim() !== "") ? value : "

", - onUpdate: async ({ editor }) => { - // for instant feedback loop - setIsSubmitting?.("submitting"); - setShouldShowAlert?.(true); - if (debouncedUpdatesEnabled) { - debouncedUpdates({ onChange, editor }); - } else { - onChange?.(editor.getJSON(), editor.getHTML()); - } - }, -}); diff --git a/packages/editor/core/src/index.ts b/packages/editor/core/src/index.ts index 890841cb9..1fc9b618c 100644 --- a/packages/editor/core/src/index.ts +++ b/packages/editor/core/src/index.ts @@ -1,13 +1,10 @@ import "@/styles/tailwind.css"; import "@/styles/editor.css"; -// export { ImageResizer } from "./ui/extensions/image/image-resize"; -// export { TiptapEditorProps } from "./ui/props"; -// export { TableMenu } from "./ui/menus/table-menu"; -// export { TiptapExtensions } from "./ui/extensions"; -// export { cn } from "./lib/utils"; -// export { FixedMenu } from "./ui/menus/fixed-menu"; -// export { EditorBubbleMenu } from "./ui/menus/bubble-menu"; +export { startImageUpload } from "@/ui/plugins/upload-image"; -export { TiptapEditor, TiptapEditorWithRef } from "@/ui"; - -export { useEditor } from "@/useEditor"; +// export { TiptapEditor, TiptapEditorWithRef } from "@/ui"; +export { useEditor } from "@/ui/hooks/useEditor"; +export { cn } from "@/lib/utils"; +export { getEditorClassNames } from "@/lib/utils"; +export { EditorContainer } from "@/ui/editor-container"; +export { EditorContentWrapper } from "@/ui/editor-content"; diff --git a/packages/editor/core/src/interfaces/index.ts b/packages/editor/core/src/interfaces/index.ts deleted file mode 100644 index 7f5492df7..000000000 --- a/packages/editor/core/src/interfaces/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface EditorHandle { - clearEditor: () => void; - setEditorValue: (content: string) => void; -} diff --git a/packages/editor/core/src/lib/utils.ts b/packages/editor/core/src/lib/utils.ts index 1c985922b..64b5a3db7 100644 --- a/packages/editor/core/src/lib/utils.ts +++ b/packages/editor/core/src/lib/utils.ts @@ -12,4 +12,18 @@ export const findTableAncestor = ( node = node.parentNode; } return node as HTMLTableElement; -}; \ No newline at end of file +}; + +interface EditorClassNames { + noBorder?: boolean; + borderOnFocus?: boolean; + customClassName?: string; +} + +export const getEditorClassNames = ({ noBorder, borderOnFocus, customClassName }: EditorClassNames) => cn( + 'relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md', + noBorder ? '' : 'border border-custom-border-200', + borderOnFocus ? 'focus:border border-custom-border-300' : 'focus:border-0', + customClassName +); + diff --git a/packages/editor/core/src/ui/editor-container.tsx b/packages/editor/core/src/ui/editor-container.tsx new file mode 100644 index 000000000..fca24f962 --- /dev/null +++ b/packages/editor/core/src/ui/editor-container.tsx @@ -0,0 +1,20 @@ +import { Editor } from "@tiptap/react"; +import { ReactNode } from "react"; + +interface EditorContainerProps { + editor: Editor | null; + editorClassNames: string; + children: ReactNode; +} + +export const EditorContainer = ({ editor, editorClassNames, children }: EditorContainerProps) => ( +
{ + editor?.chain().focus().run(); + }} + className={`tiptap-editor-container cursor-text ${editorClassNames}`} + > + {children} +
+); diff --git a/packages/editor/core/src/ui/editor-content.tsx b/packages/editor/core/src/ui/editor-content.tsx new file mode 100644 index 000000000..7b06944d8 --- /dev/null +++ b/packages/editor/core/src/ui/editor-content.tsx @@ -0,0 +1,19 @@ +import { Editor, EditorContent } from "@tiptap/react"; +import { ReactNode } from "react"; +import { ImageResizer } from "@/ui/extensions/image/image-resize"; +import { TableMenu } from "./menus/table-menu"; + +interface EditorContentProps { + editor: Editor | null; + editorContentCustomClassNames: string | undefined; + children?: ReactNode; +} + +export const EditorContentWrapper = ({ editor, editorContentCustomClassNames = '', children }: EditorContentProps) => ( +
+ + + {editor?.isActive("image") && } + {children} +
+); diff --git a/packages/editor/core/src/ui/extensions/image/updated-image.tsx b/packages/editor/core/src/ui/extensions/image/index.tsx similarity index 83% rename from packages/editor/core/src/ui/extensions/image/updated-image.tsx rename to packages/editor/core/src/ui/extensions/image/index.tsx index 9157e8905..ac8d43597 100644 --- a/packages/editor/core/src/ui/extensions/image/updated-image.tsx +++ b/packages/editor/core/src/ui/extensions/image/index.tsx @@ -3,7 +3,7 @@ import TrackImageDeletionPlugin from "@/ui/plugins/delete-image"; import UploadImagesPlugin from "@/ui/plugins/upload-image"; import { DeleteImage } from "@/types/delete-image"; -const UpdatedImage = (deleteImage: DeleteImage) => Image.extend({ +const ImageExtension = (deleteImage: DeleteImage) => Image.extend({ addProseMirrorPlugins() { return [UploadImagesPlugin(), TrackImageDeletionPlugin(deleteImage)]; }, @@ -20,4 +20,4 @@ const UpdatedImage = (deleteImage: DeleteImage) => Image.extend({ }, }); -export default UpdatedImage; +export default ImageExtension; diff --git a/packages/editor/core/src/ui/extensions/index-new.tsx b/packages/editor/core/src/ui/extensions/index-new.tsx deleted file mode 100644 index 27639fe06..000000000 --- a/packages/editor/core/src/ui/extensions/index-new.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import StarterKit from "@tiptap/starter-kit"; -import TiptapLink from "@tiptap/extension-link"; -import TiptapUnderline from "@tiptap/extension-underline"; -import TextStyle from "@tiptap/extension-text-style"; -import { Color } from "@tiptap/extension-color"; -import TaskItem from "@tiptap/extension-task-item"; -import TaskList from "@tiptap/extension-task-list"; -import { Markdown } from "tiptap-markdown"; -import Gapcursor from "@tiptap/extension-gapcursor"; - -import UpdatedImage from "@/ui/extensions/image/updated-image"; - -import { DeleteImage } from "@/types/delete-image"; - -import isValidHttpUrl from "@/ui/menus/bubble-menu/utils" - -export const TiptapExtensions = ( - deleteFile: DeleteImage, -) => [ - StarterKit.configure({ - bulletList: { - HTMLAttributes: { - class: "list-disc list-outside leading-3 -mt-2", - }, - }, - orderedList: { - HTMLAttributes: { - class: "list-decimal list-outside leading-3 -mt-2", - }, - }, - listItem: { - HTMLAttributes: { - class: "leading-normal -mb-2", - }, - }, - blockquote: { - HTMLAttributes: { - class: "border-l-4 border-custom-border-300", - }, - }, - code: { - HTMLAttributes: { - class: - "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", - spellcheck: "false", - }, - }, - codeBlock: false, - horizontalRule: false, - dropcursor: { - color: "rgba(var(--color-text-100))", - width: 2, - }, - gapcursor: false, - }), - Gapcursor, - TiptapLink.configure({ - protocols: ["http", "https"], - validate: (url) => isValidHttpUrl(url), - HTMLAttributes: { - class: - "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", - }, - }), - UpdatedImage(deleteFile).configure({ - HTMLAttributes: { - class: "rounded-lg border border-custom-border-300", - }, - }), - TiptapUnderline, - TextStyle, - Color, - TaskList.configure({ - HTMLAttributes: { - class: "not-prose pl-2", - }, - }), - TaskItem.configure({ - HTMLAttributes: { - class: "flex items-start my-4", - }, - nested: true, - }), - Markdown.configure({ - html: true, - transformCopiedText: true, - }), - ]; diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index 1c1132444..65fb7582d 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -1,40 +1,26 @@ import StarterKit from "@tiptap/starter-kit"; -import HorizontalRule from "@tiptap/extension-horizontal-rule"; import TiptapLink from "@tiptap/extension-link"; -import Placeholder from "@tiptap/extension-placeholder"; import TiptapUnderline from "@tiptap/extension-underline"; import TextStyle from "@tiptap/extension-text-style"; import { Color } from "@tiptap/extension-color"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; import { Markdown } from "tiptap-markdown"; -import Highlight from "@tiptap/extension-highlight"; -import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; -import { InputRule } from "@tiptap/core"; import Gapcursor from "@tiptap/extension-gapcursor"; -import { Table } from "@/ui/extensions/table/table"; -import { TableHeader } from "@/ui/extensions/table/table-header"; -import { TableRow } from "@tiptap/extension-table-row"; -import { CustomTableCell } from "@/ui/extensions/table/table-cell"; -import UpdatedImage from "@/ui/extensions/image/updated-image"; -import SlashCommand from "@/ui/extensions/slash-command"; +import { CustomTableCell } from "./table/table-cell"; +import { Table } from "./table/table"; +import { TableHeader } from "./table/table-header"; +import { TableRow } from "@tiptap/extension-table-row"; + +import ImageExtension from "@/ui/extensions/image"; import { DeleteImage } from "@/types/delete-image"; -import { UploadImage } from "@/types/upload-image"; import isValidHttpUrl from "@/ui/menus/bubble-menu/utils" -import ts from "highlight.js/lib/languages/typescript"; -import { lowlight } from "lowlight/lib/core"; -import "highlight.js/styles/github-dark.css"; - -lowlight.registerLanguage("ts", ts); - export const TiptapExtensions = ( - uploadFile: UploadImage, deleteFile: DeleteImage, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void ) => [ StarterKit.configure({ bulletList: { @@ -72,32 +58,6 @@ export const TiptapExtensions = ( }, gapcursor: false, }), - CodeBlockLowlight.configure({ - lowlight, - }), - HorizontalRule.extend({ - addInputRules() { - return [ - new InputRule({ - find: /^(?:---|—-|___\s|\*\*\*\s)$/, - handler: ({ state, range, commands }) => { - commands.splitBlock(); - - const attributes = {}; - const { tr } = state; - const start = range.from; - const end = range.to; - // @ts-ignore - tr.replaceWith(start - 1, end, this.type.create(attributes)); - }, - }), - ]; - }, - }).configure({ - HTMLAttributes: { - class: "mb-6 border-t border-custom-border-300", - }, - }), Gapcursor, TiptapLink.configure({ protocols: ["http", "https"], @@ -107,31 +67,14 @@ export const TiptapExtensions = ( "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", }, }), - UpdatedImage(deleteFile).configure({ + ImageExtension(deleteFile).configure({ HTMLAttributes: { class: "rounded-lg border border-custom-border-300", }, }), - Placeholder.configure({ - placeholder: ({ node }) => { - if (node.type.name === "heading") { - return `Heading ${node.attrs.level}`; - } - if (node.type.name === "image" || node.type.name === "table") { - return ""; - } - - return "Press '/' for commands..."; - }, - includeChildren: true, - }), - SlashCommand(uploadFile, setIsSubmitting), TiptapUnderline, TextStyle, Color, - Highlight.configure({ - multicolor: true, - }), TaskList.configure({ HTMLAttributes: { class: "not-prose pl-2", diff --git a/packages/editor/core/src/ui/hooks/useEditor.tsx b/packages/editor/core/src/ui/hooks/useEditor.tsx new file mode 100644 index 000000000..cec52d25f --- /dev/null +++ b/packages/editor/core/src/ui/hooks/useEditor.tsx @@ -0,0 +1,70 @@ +import { useEditor as useCustomEditor, Editor, Extension } from "@tiptap/react"; +import { useImperativeHandle, useRef, MutableRefObject, forwardRef } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import { UploadImage } from '@/types/upload-image'; +import { DeleteImage } from '@/types/delete-image'; +import { TiptapEditorProps } from "../props"; +import { TiptapExtensions } from "../extensions"; +import { EditorProps } from '@tiptap/pm/view'; + +const DEBOUNCE_DELAY = 1500; + +interface CustomEditorProps { + editable?: boolean; + uploadFile: UploadImage; + setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; + setShouldShowAlert?: (showAlert: boolean) => void; + value: string; + deleteFile: DeleteImage; + debouncedUpdatesEnabled?: boolean; + onChange?: (json: any, html: string) => void; + extensions?: Extension[]; + editorProps?: EditorProps; + forwardedRef?: any; +} + +export const useEditor = ({ uploadFile, editable, deleteFile, editorProps = {}, value, extensions = [], onChange, setIsSubmitting, debouncedUpdatesEnabled, forwardedRef, setShouldShowAlert, }: CustomEditorProps) => { + const editor = useCustomEditor({ + editable: editable ?? true, + editorProps: { + ...TiptapEditorProps(uploadFile, setIsSubmitting), + ...editorProps, + }, + extensions: [...TiptapExtensions(deleteFile), ...extensions], + content: (typeof value === "string" && value.trim() !== "") ? value : "

", + onUpdate: async ({ editor }) => { + // for instant feedback loop + setIsSubmitting?.("submitting"); + setShouldShowAlert?.(true); + if (debouncedUpdatesEnabled) { + debouncedUpdates({ onChange: onChange, editor }); + } else { + onChange?.(editor.getJSON(), editor.getHTML()); + } + }, + }); + + const editorRef: MutableRefObject = useRef(null); + editorRef.current = editor; + + useImperativeHandle(forwardedRef, () => ({ + clearEditor: () => { + editorRef.current?.commands.clearContent(); + }, + setEditorValue: (content: string) => { + editorRef.current?.commands.setContent(content); + }, + })); + + const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => { + if (onChange) { + onChange(editor.getJSON(), editor.getHTML()); + } + }, DEBOUNCE_DELAY); + + if (!editor) { + return null; + } + + return editor; +}; diff --git a/packages/editor/core/src/ui/index-new.tsx b/packages/editor/core/src/ui/index-new.tsx deleted file mode 100644 index b6e42348b..000000000 --- a/packages/editor/core/src/ui/index-new.tsx +++ /dev/null @@ -1,150 +0,0 @@ -"use client" -import * as React from 'react'; -import { useImperativeHandle, useRef, forwardRef } from "react"; -import { useEditor, EditorContent, Editor, Extension } from "@tiptap/react"; -import { useDebouncedCallback } from "use-debounce"; -import { TableMenu } from '@/ui/menus/table-menu'; -import { TiptapExtensions } from '@/ui/extensions'; -import { EditorBubbleMenu } from '@/ui/menus/bubble-menu'; -import { ImageResizer } from '@/ui/extensions/image/image-resize'; -import { TiptapEditorProps } from '@/ui/props'; -import { UploadImage } from '@/types/upload-image'; -import { DeleteImage } from '@/types/delete-image'; -import { cn } from '@/lib/utils'; -import { FixedMenu } from './menus/fixed-menu'; -import { EditorProps } from '@tiptap/pm/view'; - -interface ITiptapEditor { - value: string; - uploadFile: UploadImage; - deleteFile: DeleteImage; - noBorder?: boolean; - borderOnFocus?: boolean; - customClassName?: string; - editorContentCustomClassNames?: string; - onChange?: (json: any, html: string) => void; - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; - setShouldShowAlert?: (showAlert: boolean) => void; - editable?: boolean; - forwardedRef?: any; - debouncedUpdatesEnabled?: boolean; - accessValue: string; - onAccessChange: (accessKey: string) => void; - commentAccess: { - icon: string; - key: string; - label: "Private" | "Public"; - }[]; - extensions?: Extension[]; - editorProps?: EditorProps; -} - -interface TiptapProps extends ITiptapEditor { - forwardedRef?: React.Ref; -} - -interface EditorHandle { - clearEditor: () => void; - setEditorValue: (content: string) => void; -} - -const DEBOUNCE_DELAY = 1500; - -const TiptapEditor = ({ - onChange, - debouncedUpdatesEnabled, - editable, - setIsSubmitting, - setShouldShowAlert, - editorContentCustomClassNames, - value, - uploadFile, - extensions = [], - editorProps = {}, - deleteFile, - noBorder, - borderOnFocus, - customClassName, - forwardedRef, - accessValue, - onAccessChange, - commentAccess, -}: TiptapProps) => { - const editor = useEditor({ - editable: editable ?? true, - editorProps: { - ...TiptapEditorProps(uploadFile, setIsSubmitting), - ...editorProps, - }, - extensions: [...TiptapExtensions(uploadFile, deleteFile, setIsSubmitting), ...extensions], - content: (typeof value === "string" && value.trim() !== "") ? value : "

", - onUpdate: async ({ editor }) => { - // for instant feedback loop - setIsSubmitting?.("submitting"); - setShouldShowAlert?.(true); - if (debouncedUpdatesEnabled) { - debouncedUpdates({ onChange, editor }); - } else { - onChange?.(editor.getJSON(), editor.getHTML()); - } - }, - }); - - const editorRef: React.MutableRefObject = useRef(null); - editorRef.current = editor; - - useImperativeHandle(forwardedRef, () => ({ - clearEditor: () => { - editorRef.current?.commands.clearContent(); - }, - setEditorValue: (content: string) => { - editorRef.current?.commands.setContent(content); - }, - })); - - const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => { - if (onChange) { - onChange(editor.getJSON(), editor.getHTML()); - } - }, DEBOUNCE_DELAY); - - const editorClassNames = cn( - 'relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md', - noBorder ? '' : 'border border-custom-border-200', - borderOnFocus ? 'focus:border border-custom-border-300' : 'focus:border-0', - customClassName - ); - - if (!editor) return null; - - return ( -
{ - editor?.chain().focus().run(); - }} - className={`tiptap-editor-container cursor-text ${editorClassNames}`} - > -
-
- - - {editor?.isActive("image") && } -
- {editor && editable !== false && - (
- -
) - } -
-
- ); -}; - -const TiptapEditorWithRef = forwardRef((props, ref) => ( - -)); - -TiptapEditorWithRef.displayName = "TiptapEditorWithRef"; - -export { TiptapEditor, TiptapEditorWithRef }; diff --git a/packages/editor/core/src/ui/index.tsx b/packages/editor/core/src/ui/index.tsx index 28808f649..ae2654e1c 100644 --- a/packages/editor/core/src/ui/index.tsx +++ b/packages/editor/core/src/ui/index.tsx @@ -1,17 +1,13 @@ "use client" import * as React from 'react'; -import { useImperativeHandle, useRef, forwardRef } from "react"; -import { useEditor, EditorContent, Editor } from "@tiptap/react"; -import { useDebouncedCallback } from "use-debounce"; -import { TableMenu } from '@/ui/menus/table-menu'; -import { TiptapExtensions } from '@/ui/extensions'; -import { EditorBubbleMenu } from '@/ui/menus/bubble-menu'; -import { ImageResizer } from '@/ui/extensions/image/image-resize'; -import { TiptapEditorProps } from '@/ui/props'; +import { Extension } from "@tiptap/react"; import { UploadImage } from '@/types/upload-image'; import { DeleteImage } from '@/types/delete-image'; -import { cn } from '@/lib/utils'; -import { FixedMenu } from './menus/fixed-menu'; +import { getEditorClassNames } from '@/lib/utils'; +import { EditorProps } from '@tiptap/pm/view'; +import { useEditor } from './hooks/useEditor'; +import { EditorContainer } from '@/ui/editor-container'; +import { EditorContentWrapper } from '@/ui/editor-content'; interface ITiptapEditor { value: string; @@ -34,6 +30,8 @@ interface ITiptapEditor { key: string; label: "Private" | "Public"; }[]; + extensions?: Extension[]; + editorProps?: EditorProps; } interface TiptapProps extends ITiptapEditor { @@ -45,8 +43,6 @@ interface EditorHandle { setEditorValue: (content: string) => void; } -const DEBOUNCE_DELAY = 1500; - const TiptapEditor = ({ onChange, debouncedUpdatesEnabled, @@ -61,79 +57,33 @@ const TiptapEditor = ({ borderOnFocus, customClassName, forwardedRef, - accessValue, - onAccessChange, - commentAccess, }: TiptapProps) => { const editor = useEditor({ - editable: editable ?? true, - editorProps: TiptapEditorProps(uploadFile, setIsSubmitting), - extensions: TiptapExtensions(uploadFile, deleteFile, setIsSubmitting), - content: (typeof value === "string" && value.trim() !== "") ? value : "

", - onUpdate: async ({ editor }) => { - // for instant feedback loop - setIsSubmitting?.("submitting"); - setShouldShowAlert?.(true); - if (debouncedUpdatesEnabled) { - debouncedUpdates({ onChange, editor }); - } else { - onChange?.(editor.getJSON(), editor.getHTML()); - } - }, + onChange, + debouncedUpdatesEnabled, + editable, + setIsSubmitting, + setShouldShowAlert, + value, + uploadFile, + deleteFile, + forwardedRef, }); - const editorRef: React.MutableRefObject = useRef(null); - editorRef.current = editor; - - useImperativeHandle(forwardedRef, () => ({ - clearEditor: () => { - editorRef.current?.commands.clearContent(); - }, - setEditorValue: (content: string) => { - editorRef.current?.commands.setContent(content); - }, - })); - - const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => { - if (onChange) { - onChange(editor.getJSON(), editor.getHTML()); - } - }, DEBOUNCE_DELAY); - - const editorClassNames = cn( - 'relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md', - noBorder ? '' : 'border border-custom-border-200', - borderOnFocus ? 'focus:border border-custom-border-300' : 'focus:border-0', - customClassName - ); + const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); if (!editor) return null; return ( -
{ - editor?.chain().focus().run(); - }} - className={`tiptap-editor-container cursor-text ${editorClassNames}`} - > +
-
- - - {editor?.isActive("image") && } -
- {editor && editable !== false && - (
- -
) - } +
-
+ ); }; -const TiptapEditorWithRef = forwardRef((props, ref) => ( +const TiptapEditorWithRef = React.forwardRef((props, ref) => ( )); diff --git a/packages/editor/core/src/useEditor.tsx b/packages/editor/core/src/useEditor.tsx deleted file mode 100644 index 552c0ca21..000000000 --- a/packages/editor/core/src/useEditor.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { - useEditor as useEditorCore, -} from "@tiptap/react"; -import { findTableAncestor } from "@/lib/utils"; - -export const useEditor = (props: any) => useEditorCore({ - editorProps: { - attributes: { - class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`, - }, - handleDOMEvents: { - keydown: (_view, event) => { - // prevent default event listeners from firing when slash command is active - if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) { - const slashCommand = document.querySelector("#slash-command"); - if (slashCommand) { - return true; - } - } - }, - }, - handlePaste: () => { - if (typeof window !== "undefined") { - const selection: any = window?.getSelection(); - if (selection.rangeCount !== 0) { - const range = selection.getRangeAt(0); - if (findTableAncestor(range.startContainer)) { - return; - } - } - } - }, - }, - ...props, -}); diff --git a/packages/editor/lite-text-editor/package.json b/packages/editor/lite-text-editor/package.json index fe1d7ee01..f2805da99 100644 --- a/packages/editor/lite-text-editor/package.json +++ b/packages/editor/lite-text-editor/package.json @@ -24,28 +24,6 @@ "react": "^18.2.0" }, "dependencies": { - "@blueprintjs/popover2": "^2.0.10", - "@radix-ui/react-slot": "^1.0.2", - "@tiptap/core": "^2.1.7", - "@tiptap/extension-code-block-lowlight": "^2.0.4", - "@tiptap/extension-color": "^2.1.11", - "@tiptap/extension-highlight": "^2.1.7", - "@tiptap/extension-horizontal-rule": "^2.1.7", - "@tiptap/extension-image": "^2.1.7", - "@tiptap/extension-link": "^2.1.7", - "@tiptap/extension-placeholder": "2.0.3", - "@tiptap/extension-table": "^2.1.6", - "@tiptap/extension-table-cell": "^2.1.6", - "@tiptap/extension-table-header": "^2.1.6", - "@tiptap/extension-table-row": "^2.1.6", - "@tiptap/extension-task-item": "^2.1.7", - "@tiptap/extension-task-list": "^2.1.7", - "@tiptap/extension-text-style": "^2.1.11", - "@tiptap/extension-underline": "^2.1.7", - "@tiptap/pm": "^2.1.7", - "@tiptap/react": "^2.1.7", - "@tiptap/starter-kit": "^2.1.10", - "@tiptap/suggestion": "^2.1.7", "@types/node": "18.15.3", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", @@ -64,7 +42,8 @@ "tailwind-merge": "^1.14.0", "tippy.js": "^6.3.7", "tiptap-markdown": "^0.8.2", - "use-debounce": "^9.0.4" + "use-debounce": "^9.0.4", + "@plane/editor-core": "*" }, "devDependencies": { "@types/react": "^18.2.5", diff --git a/packages/editor/lite-text-editor/src/index.ts b/packages/editor/lite-text-editor/src/index.ts new file mode 100644 index 000000000..9238be8b9 --- /dev/null +++ b/packages/editor/lite-text-editor/src/index.ts @@ -0,0 +1 @@ +export { LiteTextEditor, LiteTextEditorWithRef } from "@/ui"; diff --git a/packages/editor/lite-text-editor/src/ui/index.tsx b/packages/editor/lite-text-editor/src/ui/index.tsx new file mode 100644 index 000000000..ce87a39b3 --- /dev/null +++ b/packages/editor/lite-text-editor/src/ui/index.tsx @@ -0,0 +1,96 @@ +"use client" +import * as React from 'react'; +import { EditorContainer, EditorContentWrapper, getEditorClassNames, useEditor } from '@plane/editor-core'; +import { FixedMenu } from './menus/fixed-menu'; + +export type UploadImage = (file: File) => Promise; +export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; + +interface ITiptapEditor { + value: string; + uploadFile: UploadImage; + deleteFile: DeleteImage; + noBorder?: boolean; + borderOnFocus?: boolean; + customClassName?: string; + editorContentCustomClassNames?: string; + onChange?: (json: any, html: string) => void; + setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; + setShouldShowAlert?: (showAlert: boolean) => void; + editable?: boolean; + forwardedRef?: any; + debouncedUpdatesEnabled?: boolean; + commentAccessSpecifier?: { + accessValue: string, + onAccessChange: (accessKey: string) => void, + showAccessSpecifier: boolean, + commentAccess: { + icon: string; + key: string; + label: "Private" | "Public"; + }[] + } +} + +interface TiptapProps extends ITiptapEditor { + forwardedRef?: React.Ref; +} + +interface EditorHandle { + clearEditor: () => void; + setEditorValue: (content: string) => void; +} + +const LiteTextEditor = ({ + onChange, + debouncedUpdatesEnabled, + editable, + setIsSubmitting, + setShouldShowAlert, + editorContentCustomClassNames, + value, + uploadFile, + deleteFile, + noBorder, + borderOnFocus, + customClassName, + forwardedRef, + commentAccessSpecifier, +}: TiptapProps) => { + const editor = useEditor({ + onChange, + debouncedUpdatesEnabled, + editable, + setIsSubmitting, + setShouldShowAlert, + value, + uploadFile, + deleteFile, + forwardedRef, + }); + + const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); + + if (!editor) return null; + + return ( + +
+ + {(editable !== false) && + (
+ +
) + } +
+
+ ); +}; + +const LiteTextEditorWithRef = React.forwardRef((props, ref) => ( + +)); + +LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef"; + +export { LiteTextEditor, LiteTextEditorWithRef }; diff --git a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/icon.tsx b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/icon.tsx new file mode 100644 index 000000000..c0006b3f2 --- /dev/null +++ b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/icon.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +type Props = { + iconName: string; + className?: string; +}; + +export const Icon: React.FC = ({ iconName, className = "" }) => ( + + {iconName} + +); + diff --git a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx new file mode 100644 index 000000000..0cece1d58 --- /dev/null +++ b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx @@ -0,0 +1,115 @@ +import { Editor } from "@tiptap/react"; +import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from "lucide-react"; + +import { cn } from "@plane/editor-core"; +import { Tooltip } from "./tooltip"; +import { Icon } from "./icon"; + +export interface BubbleMenuItem { + name: string; + isActive: () => boolean; + command: () => void; + icon: typeof BoldIcon; +} + +type EditorBubbleMenuProps = { + editor: Editor; + commentAccessSpecifier?: { + accessValue: string, + onAccessChange: (accessKey: string) => void, + showAccessSpecifier: boolean, + commentAccess: { + icon: string; + key: string; + label: "Private" | "Public"; + }[] | undefined; + } +} + +export const FixedMenu = (props: EditorBubbleMenuProps) => { + const items: BubbleMenuItem[] = [ + { + name: "bold", + isActive: () => props.editor?.isActive("bold"), + command: () => props.editor?.chain().focus().toggleBold().run(), + icon: BoldIcon, + }, + { + name: "italic", + isActive: () => props.editor?.isActive("italic"), + command: () => props.editor?.chain().focus().toggleItalic().run(), + icon: ItalicIcon, + }, + { + name: "underline", + isActive: () => props.editor?.isActive("underline"), + command: () => props.editor?.chain().focus().toggleUnderline().run(), + icon: UnderlineIcon, + }, + { + name: "strike", + isActive: () => props.editor?.isActive("strike"), + command: () => props.editor?.chain().focus().toggleStrike().run(), + icon: StrikethroughIcon, + }, + { + name: "code", + isActive: () => props.editor?.isActive("code"), + command: () => props.editor?.chain().focus().toggleCode().run(), + icon: CodeIcon, + }, + ]; + + const handleAccessChange = (accessKey: string) => { + props.commentAccessSpecifier?.onAccessChange(accessKey); + }; + + + return ( +
+
+ {props.commentAccessSpecifier && (
+ {props?.commentAccessSpecifier.commentAccess?.map((access) => ( + + + + ))} +
)} + {items.map((item, index) => ( + + ))} +
+
+ ); +}; diff --git a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/tooltip.tsx b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/tooltip.tsx new file mode 100644 index 000000000..f29d8a491 --- /dev/null +++ b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/tooltip.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; + +// next-themes +import { useTheme } from "next-themes"; +// tooltip2 +import { Tooltip2 } from "@blueprintjs/popover2"; + +type Props = { + tooltipHeading?: string; + tooltipContent: string | React.ReactNode; + position?: + | "top" + | "right" + | "bottom" + | "left" + | "auto" + | "auto-end" + | "auto-start" + | "bottom-left" + | "bottom-right" + | "left-bottom" + | "left-top" + | "right-bottom" + | "right-top" + | "top-left" + | "top-right"; + children: JSX.Element; + disabled?: boolean; + className?: string; + openDelay?: number; + closeDelay?: number; +}; + +export const Tooltip: React.FC = ({ + tooltipHeading, + tooltipContent, + position = "top", + children, + disabled = false, + className = "", + openDelay = 200, + closeDelay, +}) => { + const { theme } = useTheme(); + + return ( + + {tooltipHeading && ( +
+ {tooltipHeading} +
+ )} + {tooltipContent} + + } + position={position} + renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) => + React.cloneElement(children, { ref: eleReference, ...tooltipProps, ...children.props }) + } + /> + ); +}; diff --git a/packages/editor/rich-text-editor/package.json b/packages/editor/rich-text-editor/package.json index 3dc8f2b27..27ca22ac4 100644 --- a/packages/editor/rich-text-editor/package.json +++ b/packages/editor/rich-text-editor/package.json @@ -24,49 +24,18 @@ "react": "^18.2.0" }, "dependencies": { - "@blueprintjs/popover2": "^2.0.10", - "@radix-ui/react-slot": "^1.0.2", - "@tiptap/core": "^2.1.7", - "@tiptap/extension-code-block-lowlight": "^2.0.4", - "@tiptap/extension-color": "^2.1.11", - "@tiptap/extension-highlight": "^2.1.7", - "@tiptap/extension-horizontal-rule": "^2.1.7", - "@tiptap/extension-image": "^2.1.7", - "@tiptap/extension-link": "^2.1.7", - "@tiptap/extension-placeholder": "2.0.3", - "@tiptap/extension-table": "^2.1.6", - "@tiptap/extension-table-cell": "^2.1.6", - "@tiptap/extension-table-header": "^2.1.6", - "@tiptap/extension-table-row": "^2.1.6", - "@tiptap/extension-task-item": "^2.1.7", - "@tiptap/extension-task-list": "^2.1.7", - "@tiptap/extension-text-style": "^2.1.11", - "@tiptap/extension-underline": "^2.1.7", - "@tiptap/pm": "^2.1.7", - "@tiptap/react": "^2.1.7", - "@tiptap/starter-kit": "^2.1.10", - "@tiptap/suggestion": "^2.1.7", - "@types/node": "18.15.3", - "@types/react": "18.0.28", - "@types/react-dom": "18.0.11", "class-variance-authority": "^0.7.0", "clsx": "^1.2.1", - "eslint": "8.36.0", - "eslint-config-next": "13.2.4", - "eventsource-parser": "^0.1.0", "lowlight": "^2.9.0", "lucide-react": "^0.244.0", "next": "12.3.2", - "next-themes": "^0.2.1", "react": "18.2.0", "react-dom": "18.2.0", - "react-markdown": "^8.0.7", - "tailwind-merge": "^1.14.0", - "tippy.js": "^6.3.7", - "tiptap-markdown": "^0.8.2", - "use-debounce": "^9.0.4" + "@plane/editor-core": "*" }, "devDependencies": { + "@types/node": "18.15.3", + "@types/react-dom": "18.0.11", "@types/react": "^18.2.5", "eslint": "^7.32.0", "postcss": "^8.4.29", diff --git a/packages/editor/rich-text-editor/src/extensions.tsx b/packages/editor/rich-text-editor/src/extensions.tsx deleted file mode 100644 index 85087e5e1..000000000 --- a/packages/editor/rich-text-editor/src/extensions.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import HorizontalRule from "@tiptap/extension-horizontal-rule"; -import { TableRow } from "@tiptap/extension-table-row"; -import Placeholder from "@tiptap/extension-placeholder"; -import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; -import { InputRule } from "@tiptap/core"; - -import ts from "highlight.js/lib/languages/typescript"; -import { lowlight } from "lowlight/lib/core"; -import "highlight.js/styles/github-dark.css"; -import { Table } from "./table/table"; -import { TableHeader } from "./table/table-header"; -import { CustomTableCell } from "./table/table-cell"; -import SlashCommand from "./slash-command"; -import { UploadImage } from "./types/upload-image"; - -lowlight.registerLanguage("ts", ts); - -export const TiptapExtensions = ( - uploadFile: UploadImage, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void -) => [ - CodeBlockLowlight.configure({ - lowlight, - }), - HorizontalRule.extend({ - addInputRules() { - return [ - new InputRule({ - find: /^(?:---|—-|___\s|\*\*\*\s)$/, - handler: ({ state, range, commands }) => { - commands.splitBlock(); - - const attributes = {}; - const { tr } = state; - const start = range.from; - const end = range.to; - // @ts-ignore - tr.replaceWith(start - 1, end, this.type.create(attributes)); - }, - }), - ]; - }, - }).configure({ - HTMLAttributes: { - class: "mb-6 border-t border-custom-border-300", - }, - }), - Placeholder.configure({ - placeholder: ({ node }) => { - if (node.type.name === "heading") { - return `Heading ${node.attrs.level}`; - } - if (node.type.name === "image" || node.type.name === "table") { - return ""; - } - - return "Press '/' for commands..."; - }, - includeChildren: true, - }), - SlashCommand(uploadFile, setIsSubmitting), - Table, - TableHeader, - CustomTableCell, - TableRow, - ]; diff --git a/packages/editor/rich-text-editor/src/index.ts b/packages/editor/rich-text-editor/src/index.ts new file mode 100644 index 000000000..eeca9f837 --- /dev/null +++ b/packages/editor/rich-text-editor/src/index.ts @@ -0,0 +1 @@ +export { RichTextEditor, RichTextEditorWithRef } from "@/ui"; diff --git a/packages/editor/rich-text-editor/src/index.tsx b/packages/editor/rich-text-editor/src/index.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/editor/rich-text-editor/src/slash-command.tsx b/packages/editor/rich-text-editor/src/slash-command.tsx deleted file mode 100644 index 844d4c55a..000000000 --- a/packages/editor/rich-text-editor/src/slash-command.tsx +++ /dev/null @@ -1,363 +0,0 @@ -import { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react"; -import { Editor, Range, Extension } from "@tiptap/core"; -import Suggestion from "@tiptap/suggestion"; -import { ReactRenderer } from "@tiptap/react"; -import tippy from "tippy.js"; -import { - Heading1, - Heading2, - Heading3, - List, - ListOrdered, - Text, - TextQuote, - Code, - MinusSquare, - CheckSquare, - ImageIcon, - Table, -} from "lucide-react"; -import { startImageUpload } from "@/ui/plugins/upload-image"; -import { cn } from "@/lib/utils"; -import { UploadImage } from "@/types/upload-image"; - -interface CommandItemProps { - title: string; - description: string; - icon: ReactNode; -} - -interface CommandProps { - editor: Editor; - range: Range; -} - -const Command = Extension.create({ - name: "slash-command", - addOptions() { - return { - suggestion: { - char: "/", - command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => { - props.command({ editor, range }); - }, - }, - }; - }, - addProseMirrorPlugins() { - return [ - Suggestion({ - editor: this.editor, - allow({ editor }) { - return !editor.isActive("table"); - }, - ...this.options.suggestion, - }), - ]; - }, -}); - -const getSuggestionItems = - ( - uploadFile: UploadImage, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void - ) => - ({ query }: { query: string }) => - [ - { - title: "Text", - description: "Just start typing with plain text.", - searchTerms: ["p", "paragraph"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run(); - }, - }, - { - title: "Heading 1", - description: "Big section heading.", - searchTerms: ["title", "big", "large"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run(); - }, - }, - { - title: "Heading 2", - description: "Medium section heading.", - searchTerms: ["subtitle", "medium"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run(); - }, - }, - { - title: "Heading 3", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run(); - }, - }, - { - title: "To-do List", - description: "Track tasks with a to-do list.", - searchTerms: ["todo", "task", "list", "check", "checkbox"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).toggleTaskList().run(); - }, - }, - { - title: "Bullet List", - description: "Create a simple bullet list.", - searchTerms: ["unordered", "point"], - icon: , - command: ({ editor, range }: CommandProps) => { - // @ts-ignore - editor.chain().focus().deleteRange(range).toggleBulletList().run(); - }, - }, - { - title: "Divider", - description: "Visually divide blocks", - searchTerms: ["line", "divider", "horizontal", "rule", "separate"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).setHorizontalRule().run(); - }, - }, - { - title: "Table", - description: "Create a Table", - searchTerms: ["table", "cell", "db", "data", "tabular"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor - .chain() - .focus() - .deleteRange(range) - .insertTable({ rows: 3, cols: 3, withHeaderRow: true }) - .run(); - }, - }, - { - title: "Numbered List", - description: "Create a list with numbering.", - searchTerms: ["ordered"], - icon: , - command: ({ editor, range }: CommandProps) => { - // @ts-ignore - editor.chain().focus().deleteRange(range).toggleOrderedList().run(); - }, - }, - { - title: "Quote", - description: "Capture a quote.", - searchTerms: ["blockquote"], - icon: , - command: ({ editor, range }: CommandProps) => - // @ts-ignore - editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run(), - }, - { - title: "Code", - description: "Capture a code snippet.", - searchTerms: ["codeblock"], - icon: , - command: ({ editor, range }: CommandProps) => - editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), - }, - { - title: "Image", - description: "Upload an image from your computer.", - searchTerms: ["photo", "picture", "media"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).run(); - // upload image - const input = document.createElement("input"); - input.type = "file"; - input.accept = "image/*"; - input.onchange = async () => { - if (input.files?.length) { - const file = input.files[0]; - const pos = editor.view.state.selection.from; - startImageUpload(file, editor.view, pos, uploadFile, setIsSubmitting); - } - }; - input.click(); - }, - }, - ].filter((item) => { - if (typeof query === "string" && query.length > 0) { - const search = query.toLowerCase(); - return ( - item.title.toLowerCase().includes(search) || - item.description.toLowerCase().includes(search) || - (item.searchTerms && item.searchTerms.some((term: string) => term.includes(search))) - ); - } - return true; - }); - -export const updateScrollView = (container: HTMLElement, item: HTMLElement) => { - const containerHeight = container.offsetHeight; - const itemHeight = item ? item.offsetHeight : 0; - - const top = item.offsetTop; - const bottom = top + itemHeight; - - if (top < container.scrollTop) { - container.scrollTop -= container.scrollTop - top + 5; - } else if (bottom > containerHeight + container.scrollTop) { - container.scrollTop += bottom - containerHeight - container.scrollTop + 5; - } -}; - -const CommandList = ({ - items, - command, -}: { - items: CommandItemProps[]; - command: any; - editor: any; - range: any; -}) => { - const [selectedIndex, setSelectedIndex] = useState(0); - - const selectItem = useCallback( - (index: number) => { - const item = items[index]; - if (item) { - command(item); - } - }, - [command, items] - ); - - useEffect(() => { - const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"]; - const onKeyDown = (e: KeyboardEvent) => { - if (navigationKeys.includes(e.key)) { - e.preventDefault(); - if (e.key === "ArrowUp") { - setSelectedIndex((selectedIndex + items.length - 1) % items.length); - return true; - } - if (e.key === "ArrowDown") { - setSelectedIndex((selectedIndex + 1) % items.length); - return true; - } - if (e.key === "Enter") { - selectItem(selectedIndex); - return true; - } - return false; - } - }; - document.addEventListener("keydown", onKeyDown); - return () => { - document.removeEventListener("keydown", onKeyDown); - }; - }, [items, selectedIndex, setSelectedIndex, selectItem]); - - useEffect(() => { - setSelectedIndex(0); - }, [items]); - - const commandListContainer = useRef(null); - - useLayoutEffect(() => { - const container = commandListContainer?.current; - - const item = container?.children[selectedIndex] as HTMLElement; - - if (item && container) updateScrollView(container, item); - }, [selectedIndex]); - - return items.length > 0 ? ( -
- {items.map((item: CommandItemProps, index: number) => ( - - ))} -
- ) : null; -}; - -const renderItems = () => { - let component: ReactRenderer | null = null; - let popup: any | null = null; - - return { - onStart: (props: { editor: Editor; clientRect: DOMRect }) => { - component = new ReactRenderer(CommandList, { - props, - editor: props.editor, - }); - - // @ts-ignore - popup = tippy("body", { - getReferenceClientRect: props.clientRect, - appendTo: () => document.querySelector("#tiptap-container"), - content: component.element, - showOnCreate: true, - interactive: true, - trigger: "manual", - placement: "bottom-start", - }); - }, - onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => { - component?.updateProps(props); - - popup && - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }); - }, - onKeyDown: (props: { event: KeyboardEvent }) => { - if (props.event.key === "Escape") { - popup?.[0].hide(); - - return true; - } - - // @ts-ignore - return component?.ref?.onKeyDown(props); - }, - onExit: () => { - popup?.[0].destroy(); - component?.destroy(); - }, - }; -}; - -export const SlashCommand = ( - uploadFile: UploadImage, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void -) => - Command.configure({ - suggestion: { - items: getSuggestionItems(uploadFile, setIsSubmitting), - render: renderItems, - }, - }); - -export default SlashCommand; diff --git a/packages/editor/rich-text-editor/src/table/table-cell.ts b/packages/editor/rich-text-editor/src/table/table-cell.ts deleted file mode 100644 index 643cb8c64..000000000 --- a/packages/editor/rich-text-editor/src/table/table-cell.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TableCell } from "@tiptap/extension-table-cell"; - -export const CustomTableCell = TableCell.extend({ - addAttributes() { - return { - ...this.parent?.(), - isHeader: { - default: false, - parseHTML: (element) => { - isHeader: element.tagName === "TD"; - }, - renderHTML: (attributes) => { - tag: attributes.isHeader ? "th" : "td"; - }, - }, - }; - }, - renderHTML({ HTMLAttributes }) { - if (HTMLAttributes.isHeader) { - return [ - "th", - { - ...HTMLAttributes, - class: `relative ${HTMLAttributes.class}`, - }, - ["span", { class: "absolute top-0 right-0" }], - 0, - ]; - } - return ["td", HTMLAttributes, 0]; - }, -}); diff --git a/packages/editor/rich-text-editor/src/table/table-header.ts b/packages/editor/rich-text-editor/src/table/table-header.ts deleted file mode 100644 index f23aa93ef..000000000 --- a/packages/editor/rich-text-editor/src/table/table-header.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header"; - -const TableHeader = BaseTableHeader.extend({ - content: "paragraph", -}); - -export { TableHeader }; diff --git a/packages/editor/rich-text-editor/src/table/table.ts b/packages/editor/rich-text-editor/src/table/table.ts deleted file mode 100644 index 9b727bb51..000000000 --- a/packages/editor/rich-text-editor/src/table/table.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Table as BaseTable } from "@tiptap/extension-table"; - -const Table = BaseTable.configure({ - resizable: true, - cellMinWidth: 100, - allowTableNodeSelection: true, -}); - -export { Table }; diff --git a/packages/editor/rich-text-editor/src/types/upload-image.ts b/packages/editor/rich-text-editor/src/types/upload-image.ts deleted file mode 100644 index 3cf1408d2..000000000 --- a/packages/editor/rich-text-editor/src/types/upload-image.ts +++ /dev/null @@ -1 +0,0 @@ -export type UploadImage = (file: File) => Promise; diff --git a/packages/editor/rich-text-editor/src/ui/extensions/index.tsx b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx new file mode 100644 index 000000000..7c394b53e --- /dev/null +++ b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx @@ -0,0 +1,20 @@ +import HorizontalRule from "@tiptap/extension-horizontal-rule"; +import Placeholder from "@tiptap/extension-placeholder"; +import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; +import { lowlight } from "lowlight/lib/core"; +import { InputRule } from "@tiptap/core"; + +import ts from "highlight.js/lib/languages/typescript"; + +import "highlight.js/styles/github-dark.css"; +import SlashCommand from "./slash-command"; +import { UploadImage } from ".."; + +lowlight.registerLanguage("ts", ts); + +export const RichTextEditorExtensions = ( + uploadFile: UploadImage, + setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void +) => [ + SlashCommand(uploadFile, setIsSubmitting), + ]; diff --git a/packages/editor/core/src/ui/extensions/slash-command.tsx b/packages/editor/rich-text-editor/src/ui/extensions/slash-command.tsx similarity index 98% rename from packages/editor/core/src/ui/extensions/slash-command.tsx rename to packages/editor/rich-text-editor/src/ui/extensions/slash-command.tsx index 844d4c55a..8fdb5ddcd 100644 --- a/packages/editor/core/src/ui/extensions/slash-command.tsx +++ b/packages/editor/rich-text-editor/src/ui/extensions/slash-command.tsx @@ -17,9 +17,8 @@ import { ImageIcon, Table, } from "lucide-react"; -import { startImageUpload } from "@/ui/plugins/upload-image"; -import { cn } from "@/lib/utils"; -import { UploadImage } from "@/types/upload-image"; +import { UploadImage } from ".."; +import { cn, startImageUpload } from "@plane/editor-core"; interface CommandItemProps { title: string; diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx new file mode 100644 index 000000000..8784ae17d --- /dev/null +++ b/packages/editor/rich-text-editor/src/ui/index.tsx @@ -0,0 +1,83 @@ +"use client" +import * as React from 'react'; +import { EditorContainer, EditorContentWrapper, getEditorClassNames, useEditor } from '@plane/editor-core'; +import { EditorBubbleMenu } from './menus/bubble-menu'; +import { RichTextEditorExtensions } from './extensions'; + +export type UploadImage = (file: File) => Promise; +export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; + +interface ITiptapEditor { + value: string; + uploadFile: UploadImage; + deleteFile: DeleteImage; + noBorder?: boolean; + borderOnFocus?: boolean; + customClassName?: string; + editorContentCustomClassNames?: string; + onChange?: (json: any, html: string) => void; + setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; + setShouldShowAlert?: (showAlert: boolean) => void; + editable?: boolean; + forwardedRef?: any; + debouncedUpdatesEnabled?: boolean; +} + +interface TiptapProps extends ITiptapEditor { + forwardedRef?: React.Ref; +} + +interface EditorHandle { + clearEditor: () => void; + setEditorValue: (content: string) => void; +} + +const RichTextEditor = ({ + onChange, + debouncedUpdatesEnabled, + editable, + setIsSubmitting, + setShouldShowAlert, + editorContentCustomClassNames, + value, + uploadFile, + deleteFile, + noBorder, + borderOnFocus, + customClassName, + forwardedRef, +}: TiptapProps) => { + const editor = useEditor({ + onChange, + debouncedUpdatesEnabled, + editable, + setIsSubmitting, + setShouldShowAlert, + value, + uploadFile, + deleteFile, + forwardedRef, + extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting) + }); + + const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); + + if (!editor) return null; + + return ( + + {editor && } +
+ +
+
+ ); +}; + +const RichTextEditorWithRef = React.forwardRef((props, ref) => ( + +)); + +RichTextEditorWithRef.displayName = "RichTextEditorWithRef"; + +export { RichTextEditor, RichTextEditorWithRef}; diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx new file mode 100644 index 000000000..b9ce6159d --- /dev/null +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx @@ -0,0 +1,121 @@ +import { BubbleMenu, BubbleMenuProps } from "@tiptap/react"; +import { FC, useState } from "react"; +import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from "lucide-react"; + +import { NodeSelector } from "./node-selector"; +import { LinkSelector } from "./link-selector"; +import { cn } from "@plane/editor-core"; + +export interface BubbleMenuItem { + name: string; + isActive: () => boolean; + command: () => void; + icon: typeof BoldIcon; +} + +type EditorBubbleMenuProps = Omit; + +export const EditorBubbleMenu: FC = (props: any) => { + const items: BubbleMenuItem[] = [ + { + name: "bold", + isActive: () => props.editor?.isActive("bold"), + command: () => props.editor?.chain().focus().toggleBold().run(), + icon: BoldIcon, + }, + { + name: "italic", + isActive: () => props.editor?.isActive("italic"), + command: () => props.editor?.chain().focus().toggleItalic().run(), + icon: ItalicIcon, + }, + { + name: "underline", + isActive: () => props.editor?.isActive("underline"), + command: () => props.editor?.chain().focus().toggleUnderline().run(), + icon: UnderlineIcon, + }, + { + name: "strike", + isActive: () => props.editor?.isActive("strike"), + command: () => props.editor?.chain().focus().toggleStrike().run(), + icon: StrikethroughIcon, + }, + { + name: "code", + isActive: () => props.editor?.isActive("code"), + command: () => props.editor?.chain().focus().toggleCode().run(), + icon: CodeIcon, + }, + ]; + + const bubbleMenuProps: EditorBubbleMenuProps = { + ...props, + shouldShow: ({ editor }) => { + if (!editor.isEditable) { + return false; + } + if (editor.isActive("image")) { + return false; + } + return editor.view.state.selection.content().size > 0; + }, + tippyOptions: { + moveTransition: "transform 0.15s ease-out", + onHidden: () => { + setIsNodeSelectorOpen(false); + setIsLinkSelectorOpen(false); + }, + }, + }; + + const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false); + const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false); + + return ( + + {!props.editor.isActive("table") && ( + { + setIsNodeSelectorOpen(!isNodeSelectorOpen); + setIsLinkSelectorOpen(false); + }} + /> + )} + { + setIsLinkSelectorOpen(!isLinkSelectorOpen); + setIsNodeSelectorOpen(false); + }} + /> +
+ {items.map((item, index) => ( + + ))} +
+
+ ); +}; diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/link-selector.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/link-selector.tsx new file mode 100644 index 000000000..cf347cf3b --- /dev/null +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/link-selector.tsx @@ -0,0 +1,93 @@ +import { Editor } from "@tiptap/core"; +import { Check, Trash } from "lucide-react"; +import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react"; +import isValidHttpUrl from "@/ui/menus/bubble-menu/utils"; +import { cn } from "@plane/editor-core"; + +interface LinkSelectorProps { + editor: Editor; + isOpen: boolean; + setIsOpen: Dispatch>; +} + +export const LinkSelector: FC = ({ editor, isOpen, setIsOpen }) => { + const inputRef = useRef(null); + + const onLinkSubmit = useCallback(() => { + const input = inputRef.current; + const url = input?.value; + if (url && isValidHttpUrl(url)) { + editor.chain().focus().setLink({ href: url }).run(); + setIsOpen(false); + } + }, [editor, inputRef, setIsOpen]); + + useEffect(() => { + inputRef.current && inputRef.current?.focus(); + }); + + return ( +
+ + {isOpen && ( +
{ + if (e.key === "Enter") { + e.preventDefault(); + onLinkSubmit(); + } + }} + > + + {editor.getAttributes("link").href ? ( + + ) : ( + + )} +
+ )} +
+ ); +}; diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx new file mode 100644 index 000000000..e1f8ce211 --- /dev/null +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx @@ -0,0 +1,130 @@ +import { cn } from "@plane/editor-core"; +import { Editor } from "@tiptap/core"; +import { + Check, + ChevronDown, + Heading1, + Heading2, + Heading3, + TextQuote, + ListOrdered, + TextIcon, + Code, + CheckSquare, +} from "lucide-react"; +import { Dispatch, FC, SetStateAction } from "react"; + +import { BubbleMenuItem } from "."; + +interface NodeSelectorProps { + editor: Editor; + isOpen: boolean; + setIsOpen: Dispatch>; +} + +export const NodeSelector: FC = ({ editor, isOpen, setIsOpen }) => { + const items: BubbleMenuItem[] = [ + { + name: "Text", + icon: TextIcon, + command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(), + isActive: () => + editor.isActive("paragraph") && + !editor.isActive("bulletList") && + !editor.isActive("orderedList"), + }, + { + name: "H1", + icon: Heading1, + command: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), + isActive: () => editor.isActive("heading", { level: 1 }), + }, + { + name: "H2", + icon: Heading2, + command: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), + isActive: () => editor.isActive("heading", { level: 2 }), + }, + { + name: "H3", + icon: Heading3, + command: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), + isActive: () => editor.isActive("heading", { level: 3 }), + }, + { + name: "To-do List", + icon: CheckSquare, + command: () => editor.chain().focus().toggleTaskList().run(), + isActive: () => editor.isActive("taskItem"), + }, + { + name: "Bullet List", + icon: ListOrdered, + command: () => editor.chain().focus().toggleBulletList().run(), + isActive: () => editor.isActive("bulletList"), + }, + { + name: "Numbered List", + icon: ListOrdered, + command: () => editor.chain().focus().toggleOrderedList().run(), + isActive: () => editor.isActive("orderedList"), + }, + { + name: "Quote", + icon: TextQuote, + command: () => + editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(), + isActive: () => editor.isActive("blockquote"), + }, + { + name: "Code", + icon: Code, + command: () => editor.chain().focus().toggleCodeBlock().run(), + isActive: () => editor.isActive("codeBlock"), + }, + ]; + + const activeItem = items.filter((item) => item.isActive()).pop() ?? { + name: "Multiple", + }; + + return ( +
+ + + {isOpen && ( +
+ {items.map((item, index) => ( + + ))} +
+ )} +
+ ); +}; diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/utils/index.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/utils/index.tsx new file mode 100644 index 000000000..b5add3f54 --- /dev/null +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/utils/index.tsx @@ -0,0 +1,11 @@ +export default function isValidHttpUrl(string: string): boolean { + let url: URL; + + try { + url = new URL(string); + } catch (_) { + return false; + } + + return url.protocol === "http:" || url.protocol === "https:"; +} diff --git a/space/components/issues/peek-overview/comment/add-comment.tsx b/space/components/issues/peek-overview/comment/add-comment.tsx index 7869667d2..c06634fc6 100644 --- a/space/components/issues/peek-overview/comment/add-comment.tsx +++ b/space/components/issues/peek-overview/comment/add-comment.tsx @@ -11,7 +11,7 @@ import { SecondaryButton } from "components/ui"; // types import { Comment } from "types/issue"; // components -import { TiptapEditorWithRef } from "@plane/editor"; +import { RichTextEditorWithRef } from "@plane/rich-text-editor"; // service import fileService from "@/services/file.service"; @@ -71,10 +71,9 @@ export const AddComment: React.FC = observer((props) => { name="comment_html" control={control} render={({ field: { value, onChange } }) => ( - = observer((props) => { control={control} name="comment_html" render={({ field: { onChange, value } }) => ( - = observer((props) => {
- = ({ issueDetails }) => {

{issueDetails.name}

{issueDetails.description_html !== "" && issueDetails.description_html !== "

" && ( - { - return this.mediaUpload(`/api/workspaces/${workspaceSlug}/file-assets/`, file) + return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, { + headers: { + ...this.getHeaders(), + "Content-Type": "multipart/form-data", + }, + }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; }); } + getUploadFileFunction(workspaceSlug: string): (file: File) => Promise { + return async (file: File) => { + const formData = new FormData(); + formData.append("asset", file); + formData.append("attributes", JSON.stringify({})); + + const data = await this.uploadFile(workspaceSlug, formData); + return data.asset; + }; + } + async deleteImage(assetUrlWithWorkspaceId: string): Promise { return this.delete(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/`) .then((response) => response?.status) diff --git a/turbo.json b/turbo.json index e7a4a08d5..40f67bc05 100644 --- a/turbo.json +++ b/turbo.json @@ -44,7 +44,7 @@ "cache": false, "persistent": true, "dependsOn": [ - "@plane/editor#build" + "@plane/editor-core#build" ] }, "test": { diff --git a/web/components/core/modals/gpt-assistant-modal.tsx b/web/components/core/modals/gpt-assistant-modal.tsx index 8c5895f39..236037531 100644 --- a/web/components/core/modals/gpt-assistant-modal.tsx +++ b/web/components/core/modals/gpt-assistant-modal.tsx @@ -11,7 +11,7 @@ import useUserAuth from "hooks/use-user-auth"; // ui import { Input, PrimaryButton, SecondaryButton } from "components/ui"; // components -import { TiptapEditor, TiptapEditorWithRef } from "@plane/editor"; +import { RichTextEditor, RichTextEditorWithRef } from "@plane/rich-text-editor"; // types import { IIssue, IPageBlock } from "types"; // services @@ -143,10 +143,9 @@ export const GptAssistantModal: React.FC = ({ {((content && content !== "") || (htmlContent && htmlContent !== "

")) && (
Content: - ${content}

`} customClassName="-m-3" noBorder @@ -159,10 +158,9 @@ export const GptAssistantModal: React.FC = ({ {response !== "" && (
Response: - ${response}

`} customClassName="-mx-3 -my-3" noBorder diff --git a/web/components/issues/comment/add-comment.tsx b/web/components/issues/comment/add-comment.tsx index 76a45faa4..4b0e92ba7 100644 --- a/web/components/issues/comment/add-comment.tsx +++ b/web/components/issues/comment/add-comment.tsx @@ -3,9 +3,9 @@ import { useRouter } from "next/router"; // react-hook-form import { useForm, Controller } from "react-hook-form"; // components -import { TiptapEditorWithRef } from "@plane/editor"; +import { LiteTextEditorWithRef } from "@plane/lite-text-editor"; // ui -import { Icon, SecondaryButton, Tooltip } from "components/ui"; +import { SecondaryButton } from "components/ui"; // types import type { IIssueComment } from "types"; // services @@ -74,23 +74,20 @@ export const AddComment: React.FC = ({ ( + render={({ field: { onChange: onAccessChange, value: accessValue } }) => ( ( -

" : commentValue} customClassName="p-3 min-h-[100px] shadow-sm" debouncedUpdatesEnabled={false} onChange={(comment_json: Object, comment_html: string) => onCommentChange(comment_html)} - accessValue={value} - onAccessChange={onChange} - commentAccess={commentAccess} + commentAccessSpecifier={{ accessValue, onAccessChange, showAccessSpecifier, commentAccess }} /> )} /> diff --git a/web/components/issues/comment/comment-card.tsx b/web/components/issues/comment/comment-card.tsx index 19cc0fe7e..147c49bd1 100644 --- a/web/components/issues/comment/comment-card.tsx +++ b/web/components/issues/comment/comment-card.tsx @@ -9,7 +9,7 @@ import useUser from "hooks/use-user"; // ui import { CustomMenu, Icon } from "components/ui"; import { CommentReaction } from "components/issues"; -import { TiptapEditorWithRef } from "@plane/editor"; +import { LiteTextEditorWithRef } from "@plane/lite-text-editor"; // helpers import { timeAgo } from "helpers/date-time.helper"; // types @@ -112,10 +112,9 @@ export const CommentCard: React.FC = ({ onSubmit={handleSubmit(onEnter)} >
- = ({ />
)} - = ({ if (!value) return <>; return ( - = (props) => { return ( = (props) => { return ( = ({ if (!data) return (

"} debouncedUpdatesEnabled={false} @@ -304,9 +303,8 @@ export const CreateUpdateBlockInline: React.FC = ({ return ( 0 diff --git a/web/components/pages/single-page-block.tsx b/web/components/pages/single-page-block.tsx index 592da42c7..356186882 100644 --- a/web/components/pages/single-page-block.tsx +++ b/web/components/pages/single-page-block.tsx @@ -19,7 +19,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { GptAssistantModal } from "components/core"; import { CreateUpdateBlockInline } from "components/pages"; -import { TiptapEditor } from "@plane/editor"; +import { RichTextEditor } from "@plane/rich-text-editor"; // ui import { CustomMenu, TextArea } from "components/ui"; // icons @@ -452,10 +452,9 @@ export const SinglePageBlock: React.FC = ({ {showBlockDetails ? block.description_html.length > 7 && ( - = ({ disabled = false, onSubmit }) => { control={control} render={({ field: { value, onChange } }) => (

" : value} customClassName="p-3 min-h-[100px] shadow-sm" diff --git a/web/components/web-view/issue-web-view-form.tsx b/web/components/web-view/issue-web-view-form.tsx index 138242104..8a791e9ac 100644 --- a/web/components/web-view/issue-web-view-form.tsx +++ b/web/components/web-view/issue-web-view-form.tsx @@ -16,7 +16,7 @@ import useReloadConfirmations from "hooks/use-reload-confirmation"; import { TextArea } from "components/ui"; // components -import { TiptapEditor } from "@plane/editor"; +import { RichTextEditor } from "@plane/rich-text-editor"; import { Label } from "components/web-view"; // types @@ -123,8 +123,8 @@ export const IssueWebViewForm: React.FC = (props) => { if (!value) return <>; return ( - = (props) => { ? "

" : value } - workspaceSlug={workspaceSlug!.toString()} debouncedUpdatesEnabled={true} setShouldShowAlert={setShowAlert} setIsSubmitting={setIsSubmitting} diff --git a/web/package.json b/web/package.json index 8e4629242..23aa28bfa 100644 --- a/web/package.json +++ b/web/package.json @@ -27,7 +27,8 @@ "@nivo/pie": "0.80.0", "@nivo/scatterplot": "0.80.0", "@sentry/nextjs": "^7.36.0", - "@plane/editor": "*", + "@plane/lite-text-editor": "*", + "@plane/rich-text-editor": "*", "@types/lodash.debounce": "^4.0.7", "@types/react-datepicker": "^4.8.0", "axios": "^1.1.3", diff --git a/web/pages/[workspaceSlug]/editor.tsx b/web/pages/[workspaceSlug]/editor.tsx index d825826c2..7b16bdd54 100644 --- a/web/pages/[workspaceSlug]/editor.tsx +++ b/web/pages/[workspaceSlug]/editor.tsx @@ -1,4 +1,4 @@ -import { TiptapEditor } from "@plane/editor"; +import { RichTextEditor } from "@plane/rich-text-editor"; import type { NextPage } from "next"; import { useCallback, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; @@ -135,8 +135,8 @@ const Editor: NextPage = () => { name="description_html" control={control} render={({ field: { value, onChange } }) => ( - { } editable={editable === "true"} noBorder={true} - workspaceSlug={cookies.MOBILE_slug ?? ""} debouncedUpdatesEnabled={true} setShouldShowAlert={setShowAlert} setIsSubmitting={setIsSubmitting} diff --git a/web/pages/[workspaceSlug]/me/profile/activity.tsx b/web/pages/[workspaceSlug]/me/profile/activity.tsx index b6c478a86..68e46723d 100644 --- a/web/pages/[workspaceSlug]/me/profile/activity.tsx +++ b/web/pages/[workspaceSlug]/me/profile/activity.tsx @@ -9,7 +9,7 @@ import userService from "services/user.service"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; // components import { ActivityIcon, ActivityMessage } from "components/core"; -import { TiptapEditor } from "@plane/editor"; +import { RichTextEditor } from "@plane/rich-text-editor"; // icons import { ArrowTopRightOnSquareIcon, ChatBubbleLeftEllipsisIcon } from "@heroicons/react/24/outline"; // ui @@ -98,10 +98,9 @@ const ProfileActivity = () => {

- { name="data_html" control={control} render={({ field: { value, onChange } }) => ( - { } editable={isEditable} noBorder={true} - workspaceSlug={workspaceSlug?.toString() ?? ""} debouncedUpdatesEnabled={true} customClassName="min-h-[150px] shadow-sm" editorContentCustomClassNames="pb-9" diff --git a/yarn.lock b/yarn.lock index b2d3e099b..bdee87350 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2808,7 +2808,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.0.15", "@types/react@18.0.28", "@types/react@18.2.0", "@types/react@^18.0.17": +"@types/react@*", "@types/react@18.0.15", "@types/react@18.0.28", "@types/react@18.2.0", "@types/react@^18.0.17", "@types/react@^18.2.5": version "18.2.0" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.0.tgz#15cda145354accfc09a18d2f2305f9fc099ada21" integrity sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==