diff --git a/live/package.json b/live/package.json index 9616b15130..3dcb8b35ea 100644 --- a/live/package.json +++ b/live/package.json @@ -57,7 +57,7 @@ "concurrently": "^9.0.1", "nodemon": "^3.1.7", "ts-node": "^10.9.2", - "tsup": "^8.4.0", + "tsup": "8.3.0", "typescript": "5.3.3" } } diff --git a/packages/decorators/package.json b/packages/decorators/package.json index 433b5c11a4..198fdc6980 100644 --- a/packages/decorators/package.json +++ b/packages/decorators/package.json @@ -27,7 +27,7 @@ "@types/node": "^20.14.9", "@types/reflect-metadata": "^0.1.0", "@types/ws": "^8.5.10", - "tsup": "8.4.0", + "tsup": "8.3.0", "typescript": "^5.3.3" }, "peerDependencies": { diff --git a/packages/editor/package.json b/packages/editor/package.json index cfbd0861e7..5a899f738d 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -81,7 +81,7 @@ "@types/react": "^18.3.11", "@types/react-dom": "^18.2.18", "postcss": "^8.4.38", - "tsup": "^8.4.0", + "tsup": "8.3.0", "typescript": "5.3.3" }, "keywords": [ diff --git a/packages/editor/src/ce/constants/utility.ts b/packages/editor/src/ce/constants/utility.ts new file mode 100644 index 0000000000..616838a626 --- /dev/null +++ b/packages/editor/src/ce/constants/utility.ts @@ -0,0 +1,14 @@ +import { ExtensionFileSetStorageKey } from "@/plane-editor/types/storage"; + +export const NODE_FILE_MAP: { + [key: string]: { + fileSetName: ExtensionFileSetStorageKey; + }; +} = { + image: { + fileSetName: "deletedImageSet", + }, + imageComponent: { + fileSetName: "deletedImageSet", + }, +}; diff --git a/packages/editor/src/ce/extensions/document-extensions.tsx b/packages/editor/src/ce/extensions/document-extensions.tsx index 445f5e0f80..29072b41c3 100644 --- a/packages/editor/src/ce/extensions/document-extensions.tsx +++ b/packages/editor/src/ce/extensions/document-extensions.tsx @@ -1,5 +1,4 @@ import { HocuspocusProvider } from "@hocuspocus/provider"; -import { Extensions } from "@tiptap/core"; import { AnyExtension } from "@tiptap/core"; import { SlashCommands } from "@/extensions"; // plane editor types diff --git a/packages/editor/src/ce/types/storage.ts b/packages/editor/src/ce/types/storage.ts index 4e106738b5..5f576df509 100644 --- a/packages/editor/src/ce/types/storage.ts +++ b/packages/editor/src/ce/types/storage.ts @@ -1,13 +1,20 @@ -import { HeadingExtensionStorage } from "@/extensions"; -import { CustomImageExtensionStorage } from "@/extensions/custom-image"; -import { CustomLinkStorage } from "@/extensions/custom-link"; -import { MentionExtensionStorage } from "@/extensions/mentions"; -import { ImageExtensionStorage } from "@/plugins/image"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// extensions +import { type HeadingExtensionStorage } from "@/extensions"; +import { type CustomImageExtensionStorage } from "@/extensions/custom-image"; +import { type CustomLinkStorage } from "@/extensions/custom-link"; +import { type ImageExtensionStorage } from "@/extensions/image"; +import { type MentionExtensionStorage } from "@/extensions/mentions"; +import { type UtilityExtensionStorage } from "@/extensions/utility"; export type ExtensionStorageMap = { - imageComponent: CustomImageExtensionStorage; - image: ImageExtensionStorage; - link: CustomLinkStorage; - headingList: HeadingExtensionStorage; - mention: MentionExtensionStorage; + [CORE_EXTENSIONS.CUSTOM_IMAGE]: CustomImageExtensionStorage; + [CORE_EXTENSIONS.IMAGE]: ImageExtensionStorage; + [CORE_EXTENSIONS.CUSTOM_LINK]: CustomLinkStorage; + [CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage; + [CORE_EXTENSIONS.MENTION]: MentionExtensionStorage; + [CORE_EXTENSIONS.UTILITY]: UtilityExtensionStorage; }; + +export type ExtensionFileSetStorageKey = Extract; diff --git a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx index 623ec9508c..d1398ff5ae 100644 --- a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx +++ b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx @@ -7,7 +7,7 @@ import { DocumentContentLoader, PageRenderer } from "@/components/editors"; // constants import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; // extensions -import { IssueWidget } from "@/extensions"; +import { WorkItemEmbedExtension } from "@/extensions"; // helpers import { getEditorClassNames } from "@/helpers/common"; // hooks @@ -39,9 +39,10 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { } = props; const extensions: Extensions = []; + if (embedHandler?.issue) { extensions.push( - IssueWidget({ + WorkItemEmbedExtension({ widgetCallback: embedHandler.issue.widgetCallback, }) ); diff --git a/packages/editor/src/core/components/editors/document/read-only-editor.tsx b/packages/editor/src/core/components/editors/document/read-only-editor.tsx index 54a1f96e2c..2d2e308301 100644 --- a/packages/editor/src/core/components/editors/document/read-only-editor.tsx +++ b/packages/editor/src/core/components/editors/document/read-only-editor.tsx @@ -7,7 +7,7 @@ import { PageRenderer } from "@/components/editors"; // constants import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; // extensions -import { IssueWidget } from "@/extensions"; +import { WorkItemEmbedExtension } from "@/extensions"; // helpers import { getEditorClassNames } from "@/helpers/common"; // hooks @@ -53,7 +53,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => { const extensions: Extensions = []; if (embedHandler?.issue) { extensions.push( - IssueWidget({ + WorkItemEmbedExtension({ widgetCallback: embedHandler.issue.widgetCallback, }) ); diff --git a/packages/editor/src/core/components/editors/editor-container.tsx b/packages/editor/src/core/components/editors/editor-container.tsx index d0811cd410..6daa0719a0 100644 --- a/packages/editor/src/core/components/editors/editor-container.tsx +++ b/packages/editor/src/core/components/editors/editor-container.tsx @@ -4,6 +4,7 @@ import { FC, ReactNode, useRef } from "react"; import { cn } from "@plane/utils"; // constants import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; +import { CORE_EXTENSIONS } from "@/constants/extension"; // types import { TDisplayConfig } from "@/types"; // components @@ -36,12 +37,12 @@ export const EditorContainer: FC = (props) => { if ( currentNode.content.size === 0 && // Check if the current node is empty !( - editor.isActive("orderedList") || - editor.isActive("bulletList") || - editor.isActive("taskItem") || - editor.isActive("table") || - editor.isActive("blockquote") || - editor.isActive("codeBlock") + editor.isActive(CORE_EXTENSIONS.ORDERED_LIST) || + editor.isActive(CORE_EXTENSIONS.BULLET_LIST) || + editor.isActive(CORE_EXTENSIONS.TASK_ITEM) || + editor.isActive(CORE_EXTENSIONS.TABLE) || + editor.isActive(CORE_EXTENSIONS.BLOCKQUOTE) || + editor.isActive(CORE_EXTENSIONS.CODE_BLOCK) ) // Check if it's an empty node within an orderedList, bulletList, taskItem, table, quote or code block ) { return; @@ -53,10 +54,10 @@ export const EditorContainer: FC = (props) => { const lastNode = lastNodePos.node(); // Check if the last node is a not paragraph - if (lastNode && lastNode.type.name !== "paragraph") { + if (lastNode && lastNode.type.name !== CORE_EXTENSIONS.PARAGRAPH) { // If last node is not a paragraph, insert a new paragraph at the end const endPosition = editor?.state.doc.content.size; - editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).run(); + editor?.chain().insertContentAt(endPosition, { type: CORE_EXTENSIONS.PARAGRAPH }).run(); // Focus the newly added paragraph for immediate editing editor diff --git a/packages/editor/src/core/components/editors/link-view-container.tsx b/packages/editor/src/core/components/editors/link-view-container.tsx index 41263a9962..68fa33dde4 100644 --- a/packages/editor/src/core/components/editors/link-view-container.tsx +++ b/packages/editor/src/core/components/editors/link-view-container.tsx @@ -12,7 +12,7 @@ interface LinkViewContainerProps { export const LinkViewContainer: FC = ({ editor, containerRef }) => { const [linkViewProps, setLinkViewProps] = useState(); const [isOpen, setIsOpen] = useState(false); - const [virtualElement, setVirtualElement] = useState(null); + const [virtualElement, setVirtualElement] = useState(null); const editorState = useEditorState({ editor, diff --git a/packages/editor/src/core/components/links/link-edit-view.tsx b/packages/editor/src/core/components/links/link-edit-view.tsx index ad66ce4b45..1e9a62b0e1 100644 --- a/packages/editor/src/core/components/links/link-edit-view.tsx +++ b/packages/editor/src/core/components/links/link-edit-view.tsx @@ -51,7 +51,9 @@ export const LinkEditView = ({ viewProps }: LinkEditViewProps) => { if (!hasSubmitted.current && !linkRemoved && initialUrl === "") { try { removeLink(); - } catch (e) {} + } catch (e) { + console.error("Error removing link", e); + } } }, [linkRemoved, initialUrl] diff --git a/packages/editor/src/core/components/menus/block-menu.tsx b/packages/editor/src/core/components/menus/block-menu.tsx index c143abd009..bd86628cb3 100644 --- a/packages/editor/src/core/components/menus/block-menu.tsx +++ b/packages/editor/src/core/components/menus/block-menu.tsx @@ -1,7 +1,9 @@ -import { useCallback, useEffect, useRef } from "react"; import { Editor } from "@tiptap/react"; -import tippy, { Instance } from "tippy.js"; import { Copy, LucideIcon, Trash2 } from "lucide-react"; +import { useCallback, useEffect, useRef } from "react"; +import tippy, { Instance } from "tippy.js"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; interface BlockMenuProps { editor: Editor; @@ -102,7 +104,8 @@ export const BlockMenu = (props: BlockMenuProps) => { key: "duplicate", label: "Duplicate", isDisabled: - editor.state.selection.content().content.firstChild?.type.name === "image" || editor.isActive("imageComponent"), + editor.state.selection.content().content.firstChild?.type.name === CORE_EXTENSIONS.IMAGE || + editor.isActive(CORE_EXTENSIONS.CUSTOM_IMAGE), onClick: (e) => { e.preventDefault(); e.stopPropagation(); diff --git a/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx b/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx index 1dd47c5bb3..6f582f89c6 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx @@ -1,8 +1,10 @@ import { Editor } from "@tiptap/core"; import { Check, Link, Trash2 } from "lucide-react"; import { Dispatch, FC, SetStateAction, useCallback, useRef, useState } from "react"; -// plane utils +// plane imports import { cn } from "@plane/utils"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // helpers import { isValidHttpUrl } from "@/helpers/common"; import { setLinkEditor, unsetLinkEditor } from "@/helpers/editor-commands"; @@ -43,7 +45,7 @@ export const BubbleMenuLinkSelector: FC = (props) => { "h-full flex items-center gap-1 px-3 text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 active:bg-custom-background-80 rounded transition-colors", { "bg-custom-background-80": isOpen, - "text-custom-text-100": editor.isActive("link"), + "text-custom-text-100": editor.isActive(CORE_EXTENSIONS.CUSTOM_LINK), } )} onClick={(e) => { diff --git a/packages/editor/src/core/components/menus/bubble-menu/node-selector.tsx b/packages/editor/src/core/components/menus/bubble-menu/node-selector.tsx index 7d1378800c..564f7d97ca 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/node-selector.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/node-selector.tsx @@ -1,6 +1,6 @@ -import { Dispatch, FC, SetStateAction } from "react"; import { Editor } from "@tiptap/react"; import { Check, ChevronDown } from "lucide-react"; +import { Dispatch, FC, SetStateAction } from "react"; // plane utils import { cn } from "@plane/utils"; // components diff --git a/packages/editor/src/core/components/menus/bubble-menu/root.tsx b/packages/editor/src/core/components/menus/bubble-menu/root.tsx index 02eb8d4867..30a7c5620b 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/root.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/root.tsx @@ -18,6 +18,7 @@ import { } from "@/components/menus"; // constants import { COLORS_LIST } from "@/constants/common"; +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection"; // local components @@ -90,8 +91,8 @@ export const EditorBubbleMenu: FC = (props: { editor: Edi if ( empty || !editor.isEditable || - editor.isActive("image") || - editor.isActive("imageComponent") || + editor.isActive(CORE_EXTENSIONS.IMAGE) || + editor.isActive(CORE_EXTENSIONS.CUSTOM_IMAGE) || isNodeSelection(selection) || isCellSelection(selection) || isSelecting diff --git a/packages/editor/src/core/components/menus/menu-items.ts b/packages/editor/src/core/components/menus/menu-items.ts index 4268ccb6c4..c3aa4d414a 100644 --- a/packages/editor/src/core/components/menus/menu-items.ts +++ b/packages/editor/src/core/components/menus/menu-items.ts @@ -23,6 +23,8 @@ import { Palette, AlignCenter, } from "lucide-react"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // helpers import { insertHorizontalRule, @@ -35,12 +37,7 @@ import { toggleBold, toggleBulletList, toggleCodeBlock, - toggleHeadingFive, - toggleHeadingFour, - toggleHeadingOne, - toggleHeadingSix, - toggleHeadingThree, - toggleHeadingTwo, + toggleHeading, toggleItalic, toggleOrderedList, toggleStrike, @@ -65,63 +62,49 @@ export type EditorMenuItem = { export const TextItem = (editor: Editor): EditorMenuItem<"text"> => ({ key: "text", name: "Text", - isActive: () => editor.isActive("paragraph"), + isActive: () => editor.isActive(CORE_EXTENSIONS.PARAGRAPH), command: () => setText(editor), icon: CaseSensitive, }); -export const HeadingOneItem = (editor: Editor): EditorMenuItem<"h1"> => ({ - key: "h1", - name: "Heading 1", - isActive: () => editor.isActive("heading", { level: 1 }), - command: () => toggleHeadingOne(editor), - icon: Heading1, +type SupportedHeadingLevels = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + +const HeadingItem = ( + editor: Editor, + level: 1 | 2 | 3 | 4 | 5 | 6, + key: T, + name: string, + icon: LucideIcon +): EditorMenuItem => ({ + key, + name, + isActive: () => editor.isActive(CORE_EXTENSIONS.HEADING, { level }), + command: () => toggleHeading(editor, level), + icon, }); -export const HeadingTwoItem = (editor: Editor): EditorMenuItem<"h2"> => ({ - key: "h2", - name: "Heading 2", - isActive: () => editor.isActive("heading", { level: 2 }), - command: () => toggleHeadingTwo(editor), - icon: Heading2, -}); +export const HeadingOneItem = (editor: Editor): EditorMenuItem<"h1"> => + HeadingItem(editor, 1, "h1", "Heading 1", Heading1); -export const HeadingThreeItem = (editor: Editor): EditorMenuItem<"h3"> => ({ - key: "h3", - name: "Heading 3", - isActive: () => editor.isActive("heading", { level: 3 }), - command: () => toggleHeadingThree(editor), - icon: Heading3, -}); +export const HeadingTwoItem = (editor: Editor): EditorMenuItem<"h2"> => + HeadingItem(editor, 2, "h2", "Heading 2", Heading2); -export const HeadingFourItem = (editor: Editor): EditorMenuItem<"h4"> => ({ - key: "h4", - name: "Heading 4", - isActive: () => editor.isActive("heading", { level: 4 }), - command: () => toggleHeadingFour(editor), - icon: Heading4, -}); +export const HeadingThreeItem = (editor: Editor): EditorMenuItem<"h3"> => + HeadingItem(editor, 3, "h3", "Heading 3", Heading3); -export const HeadingFiveItem = (editor: Editor): EditorMenuItem<"h5"> => ({ - key: "h5", - name: "Heading 5", - isActive: () => editor.isActive("heading", { level: 5 }), - command: () => toggleHeadingFive(editor), - icon: Heading5, -}); +export const HeadingFourItem = (editor: Editor): EditorMenuItem<"h4"> => + HeadingItem(editor, 4, "h4", "Heading 4", Heading4); -export const HeadingSixItem = (editor: Editor): EditorMenuItem<"h6"> => ({ - key: "h6", - name: "Heading 6", - isActive: () => editor.isActive("heading", { level: 6 }), - command: () => toggleHeadingSix(editor), - icon: Heading6, -}); +export const HeadingFiveItem = (editor: Editor): EditorMenuItem<"h5"> => + HeadingItem(editor, 5, "h5", "Heading 5", Heading5); + +export const HeadingSixItem = (editor: Editor): EditorMenuItem<"h6"> => + HeadingItem(editor, 6, "h6", "Heading 6", Heading6); export const BoldItem = (editor: Editor): EditorMenuItem<"bold"> => ({ key: "bold", name: "Bold", - isActive: () => editor?.isActive("bold"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.BOLD), command: () => toggleBold(editor), icon: BoldIcon, }); @@ -129,7 +112,7 @@ export const BoldItem = (editor: Editor): EditorMenuItem<"bold"> => ({ export const ItalicItem = (editor: Editor): EditorMenuItem<"italic"> => ({ key: "italic", name: "Italic", - isActive: () => editor?.isActive("italic"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.ITALIC), command: () => toggleItalic(editor), icon: ItalicIcon, }); @@ -137,7 +120,7 @@ export const ItalicItem = (editor: Editor): EditorMenuItem<"italic"> => ({ export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({ key: "underline", name: "Underline", - isActive: () => editor?.isActive("underline"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.UNDERLINE), command: () => toggleUnderline(editor), icon: UnderlineIcon, }); @@ -145,7 +128,7 @@ export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({ export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough"> => ({ key: "strikethrough", name: "Strikethrough", - isActive: () => editor?.isActive("strike"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.STRIKETHROUGH), command: () => toggleStrike(editor), icon: StrikethroughIcon, }); @@ -153,7 +136,7 @@ export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough export const BulletListItem = (editor: Editor): EditorMenuItem<"bulleted-list"> => ({ key: "bulleted-list", name: "Bulleted list", - isActive: () => editor?.isActive("bulletList"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.BULLET_LIST), command: () => toggleBulletList(editor), icon: ListIcon, }); @@ -161,7 +144,7 @@ export const BulletListItem = (editor: Editor): EditorMenuItem<"bulleted-list"> export const NumberedListItem = (editor: Editor): EditorMenuItem<"numbered-list"> => ({ key: "numbered-list", name: "Numbered list", - isActive: () => editor?.isActive("orderedList"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.ORDERED_LIST), command: () => toggleOrderedList(editor), icon: ListOrderedIcon, }); @@ -169,7 +152,7 @@ export const NumberedListItem = (editor: Editor): EditorMenuItem<"numbered-list" export const TodoListItem = (editor: Editor): EditorMenuItem<"to-do-list"> => ({ key: "to-do-list", name: "To-do list", - isActive: () => editor.isActive("taskItem"), + isActive: () => editor.isActive(CORE_EXTENSIONS.TASK_ITEM), command: () => toggleTaskList(editor), icon: CheckSquare, }); @@ -177,7 +160,7 @@ export const TodoListItem = (editor: Editor): EditorMenuItem<"to-do-list"> => ({ export const QuoteItem = (editor: Editor): EditorMenuItem<"quote"> => ({ key: "quote", name: "Quote", - isActive: () => editor?.isActive("blockquote"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.BLOCKQUOTE), command: () => toggleBlockquote(editor), icon: TextQuote, }); @@ -185,7 +168,7 @@ export const QuoteItem = (editor: Editor): EditorMenuItem<"quote"> => ({ export const CodeItem = (editor: Editor): EditorMenuItem<"code"> => ({ key: "code", name: "Code", - isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.CODE_INLINE) || editor?.isActive(CORE_EXTENSIONS.CODE_BLOCK), command: () => toggleCodeBlock(editor), icon: CodeIcon, }); @@ -193,7 +176,7 @@ export const CodeItem = (editor: Editor): EditorMenuItem<"code"> => ({ export const TableItem = (editor: Editor): EditorMenuItem<"table"> => ({ key: "table", name: "Table", - isActive: () => editor?.isActive("table"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.TABLE), command: () => insertTableCommand(editor), icon: TableIcon, }); @@ -201,7 +184,7 @@ export const TableItem = (editor: Editor): EditorMenuItem<"table"> => ({ export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({ key: "image", name: "Image", - isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.IMAGE) || editor?.isActive(CORE_EXTENSIONS.CUSTOM_IMAGE), command: () => insertImage({ editor, event: "insert", pos: editor.state.selection.from }), icon: ImageIcon, }); @@ -210,7 +193,7 @@ export const HorizontalRuleItem = (editor: Editor) => ({ key: "divider", name: "Divider", - isActive: () => editor?.isActive("horizontalRule"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.HORIZONTAL_RULE), command: () => insertHorizontalRule(editor), icon: MinusSquare, }) as const; @@ -218,7 +201,7 @@ export const HorizontalRuleItem = (editor: Editor) => export const TextColorItem = (editor: Editor): EditorMenuItem<"text-color"> => ({ key: "text-color", name: "Color", - isActive: (props) => editor.isActive("customColor", { color: props?.color }), + isActive: (props) => editor.isActive(CORE_EXTENSIONS.CUSTOM_COLOR, { color: props?.color }), command: (props) => { if (!props) return; toggleTextColor(props.color, editor); @@ -229,7 +212,7 @@ export const TextColorItem = (editor: Editor): EditorMenuItem<"text-color"> => ( export const BackgroundColorItem = (editor: Editor): EditorMenuItem<"background-color"> => ({ key: "background-color", name: "Background color", - isActive: (props) => editor.isActive("customColor", { backgroundColor: props?.color }), + isActive: (props) => editor.isActive(CORE_EXTENSIONS.CUSTOM_COLOR, { backgroundColor: props?.color }), command: (props) => { if (!props) return; toggleBackgroundColor(props.color, editor); diff --git a/packages/editor/src/core/constants/extension.ts b/packages/editor/src/core/constants/extension.ts new file mode 100644 index 0000000000..db070cb7bf --- /dev/null +++ b/packages/editor/src/core/constants/extension.ts @@ -0,0 +1,44 @@ +export enum CORE_EXTENSIONS { + BLOCKQUOTE = "blockquote", + BOLD = "bold", + BULLET_LIST = "bulletList", + CALLOUT = "calloutComponent", + CHARACTER_COUNT = "characterCount", + CODE_BLOCK = "codeBlock", + CODE_INLINE = "code", + CUSTOM_COLOR = "customColor", + CUSTOM_IMAGE = "imageComponent", + CUSTOM_LINK = "link", + DOCUMENT = "doc", + DROP_CURSOR = "dropCursor", + ENTER_KEY = "enterKey", + GAP_CURSOR = "gapCursor", + HARD_BREAK = "hardBreak", + HEADING = "heading", + HEADINGS_LIST = "headingsList", + HISTORY = "history", + HORIZONTAL_RULE = "horizontalRule", + IMAGE = "image", + ITALIC = "italic", + LIST_ITEM = "listItem", + MARKDOWN_CLIPBOARD = "markdownClipboard", + MENTION = "mention", + ORDERED_LIST = "orderedList", + PARAGRAPH = "paragraph", + PLACEHOLDER = "placeholder", + SIDE_MENU = "editorSideMenu", + SLASH_COMMANDS = "slash-command", + STRIKETHROUGH = "strike", + TABLE = "table", + TABLE_CELL = "tableCell", + TABLE_HEADER = "tableHeader", + TABLE_ROW = "tableRow", + TASK_ITEM = "taskItem", + TASK_LIST = "taskList", + TEXT_ALIGN = "textAlign", + TEXT_STYLE = "textStyle", + TYPOGRAPHY = "typography", + UNDERLINE = "underline", + UTILITY = "utility", + WORK_ITEM_EMBED = "issue-embed-component", +} diff --git a/packages/editor/src/core/constants/meta.ts b/packages/editor/src/core/constants/meta.ts new file mode 100644 index 0000000000..66769bb82c --- /dev/null +++ b/packages/editor/src/core/constants/meta.ts @@ -0,0 +1,3 @@ +export enum CORE_EDITOR_META { + SKIP_FILE_DELETION = "skipFileDeletion", +} diff --git a/packages/editor/src/core/extensions/callout/block.tsx b/packages/editor/src/core/extensions/callout/block.tsx index b6c6d7991b..662a5ad397 100644 --- a/packages/editor/src/core/extensions/callout/block.tsx +++ b/packages/editor/src/core/extensions/callout/block.tsx @@ -1,5 +1,5 @@ -import React, { useState } from "react"; import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react"; +import React, { useState } from "react"; // constants import { COLORS_LIST } from "@/constants/common"; // local components diff --git a/packages/editor/src/core/extensions/callout/extension-config.ts b/packages/editor/src/core/extensions/callout/extension-config.ts index 546311509e..e52be72d65 100644 --- a/packages/editor/src/core/extensions/callout/extension-config.ts +++ b/packages/editor/src/core/extensions/callout/extension-config.ts @@ -1,6 +1,8 @@ import { Node, mergeAttributes } from "@tiptap/core"; -import { Node as NodeType } from "@tiptap/pm/model"; import { MarkdownSerializerState } from "@tiptap/pm/markdown"; +import { Node as NodeType } from "@tiptap/pm/model"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // types import { EAttributeNames, TCalloutBlockAttributes } from "./types"; // utils @@ -9,14 +11,14 @@ import { DEFAULT_CALLOUT_BLOCK_ATTRIBUTES } from "./utils"; // Extend Tiptap's Commands interface declare module "@tiptap/core" { interface Commands { - calloutComponent: { + [CORE_EXTENSIONS.CALLOUT]: { insertCallout: () => ReturnType; }; } } export const CustomCalloutExtensionConfig = Node.create({ - name: "calloutComponent", + name: CORE_EXTENSIONS.CALLOUT, group: "block", content: "block+", diff --git a/packages/editor/src/core/extensions/callout/logo-selector.tsx b/packages/editor/src/core/extensions/callout/logo-selector.tsx index 8ea47d50d0..7a552cd16f 100644 --- a/packages/editor/src/core/extensions/callout/logo-selector.tsx +++ b/packages/editor/src/core/extensions/callout/logo-selector.tsx @@ -1,9 +1,6 @@ -// plane helpers -import { convertHexEmojiToDecimal } from "@plane/utils"; -// plane ui +// plane imports import { EmojiIconPicker, EmojiIconPickerTypes, Logo, TEmojiLogoProps } from "@plane/ui"; -// plane utils -import { cn } from "@plane/utils"; +import { cn, convertHexEmojiToDecimal } from "@plane/utils"; // types import { TCalloutBlockAttributes } from "./types"; // utils diff --git a/packages/editor/src/core/extensions/callout/types.ts b/packages/editor/src/core/extensions/callout/types.ts index 17c55d9e5b..8e650d8733 100644 --- a/packages/editor/src/core/extensions/callout/types.ts +++ b/packages/editor/src/core/extensions/callout/types.ts @@ -20,7 +20,7 @@ export type TCalloutBlockEmojiAttributes = { export type TCalloutBlockAttributes = { [EAttributeNames.LOGO_IN_USE]: "emoji" | "icon"; - [EAttributeNames.BACKGROUND]: string; + [EAttributeNames.BACKGROUND]: string | undefined; [EAttributeNames.BLOCK_TYPE]: "callout-component"; } & TCalloutBlockIconAttributes & TCalloutBlockEmojiAttributes; diff --git a/packages/editor/src/core/extensions/callout/utils.ts b/packages/editor/src/core/extensions/callout/utils.ts index 6568a40e3e..3bf07f0a9c 100644 --- a/packages/editor/src/core/extensions/callout/utils.ts +++ b/packages/editor/src/core/extensions/callout/utils.ts @@ -1,7 +1,6 @@ -// plane helpers -import { sanitizeHTML } from "@plane/utils"; -// plane ui +// plane imports import { TEmojiLogoProps } from "@plane/ui"; +import { sanitizeHTML } from "@plane/utils"; // types import { EAttributeNames, @@ -12,11 +11,11 @@ import { export const DEFAULT_CALLOUT_BLOCK_ATTRIBUTES: TCalloutBlockAttributes = { "data-logo-in-use": "emoji", - "data-icon-color": null, - "data-icon-name": null, + "data-icon-color": undefined, + "data-icon-name": undefined, "data-emoji-unicode": "128161", "data-emoji-url": "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png", - "data-background": null, + "data-background": undefined, "data-block-type": "callout-component", }; @@ -32,7 +31,7 @@ export const getStoredLogo = (): TStoredLogoValue => { }; if (typeof window !== "undefined") { - const storedData = sanitizeHTML(localStorage.getItem("editor-calloutComponent-logo")); + const storedData = sanitizeHTML(localStorage.getItem("editor-calloutComponent-logo") ?? ""); if (storedData) { let parsedData: TEmojiLogoProps; try { @@ -69,7 +68,7 @@ export const updateStoredLogo = (value: TEmojiLogoProps): void => { // function to get the stored background color from local storage export const getStoredBackgroundColor = (): string | null => { if (typeof window !== "undefined") { - return sanitizeHTML(localStorage.getItem("editor-calloutComponent-background")); + return sanitizeHTML(localStorage.getItem("editor-calloutComponent-background") ?? ""); } return null; }; diff --git a/packages/editor/src/core/extensions/clipboard.ts b/packages/editor/src/core/extensions/clipboard.ts deleted file mode 100644 index 252f0a113f..0000000000 --- a/packages/editor/src/core/extensions/clipboard.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Extension } from "@tiptap/core"; -import { Fragment, Node } from "@tiptap/pm/model"; -import { Plugin, PluginKey } from "@tiptap/pm/state"; - -export const MarkdownClipboard = Extension.create({ - name: "markdownClipboard", - - addProseMirrorPlugins() { - return [ - new Plugin({ - key: new PluginKey("markdownClipboard"), - props: { - clipboardTextSerializer: (slice) => { - const markdownSerializer = this.editor.storage.markdown.serializer; - const isTableRow = slice.content.firstChild?.type?.name === "tableRow"; - const nodeSelect = slice.openStart === 0 && slice.openEnd === 0; - - if (nodeSelect) { - return markdownSerializer.serialize(slice.content); - } - - const processTableContent = (tableNode: Node | Fragment) => { - let result = ""; - tableNode.content?.forEach?.((tableRowNode: Node | Fragment) => { - tableRowNode.content?.forEach?.((cell: Node) => { - const cellContent = cell.content ? markdownSerializer.serialize(cell.content) : ""; - result += cellContent + "\n"; - }); - }); - return result; - }; - - if (isTableRow) { - const rowsCount = slice.content?.childCount || 0; - const cellsCount = slice.content?.firstChild?.content?.childCount || 0; - if (rowsCount === 1 || cellsCount === 1) { - return processTableContent(slice.content); - } else { - return markdownSerializer.serialize(slice.content); - } - } - - const traverseToParentOfLeaf = ( - node: Node | null, - parent: Fragment | Node, - depth: number - ): Node | Fragment => { - let currentNode = node; - let currentParent = parent; - let currentDepth = depth; - - while (currentNode && currentDepth > 1 && currentNode.content?.firstChild) { - if (currentNode.content?.childCount > 1) { - if (currentNode.content.firstChild?.type?.name === "listItem") { - return currentParent; - } else { - return currentNode.content; - } - } - - currentParent = currentNode; - currentNode = currentNode.content?.firstChild || null; - currentDepth--; - } - - return currentParent; - }; - - if (slice.content.childCount > 1) { - return markdownSerializer.serialize(slice.content); - } else { - const targetNode = traverseToParentOfLeaf(slice.content.firstChild, slice.content, slice.openStart); - - let currentNode = targetNode; - while (currentNode && currentNode.content && currentNode.childCount === 1 && currentNode.firstChild) { - currentNode = currentNode.firstChild; - } - if (currentNode instanceof Node && currentNode.isText) { - return currentNode.text; - } - - return markdownSerializer.serialize(targetNode); - } - }, - }, - }), - ]; - }, -}); diff --git a/packages/editor/src/core/extensions/code-inline/index.tsx b/packages/editor/src/core/extensions/code-inline/index.tsx index 6e023b6ed1..ae320cf6a2 100644 --- a/packages/editor/src/core/extensions/code-inline/index.tsx +++ b/packages/editor/src/core/extensions/code-inline/index.tsx @@ -1,4 +1,6 @@ import { Mark, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface CodeOptions { HTMLAttributes: Record; @@ -6,7 +8,7 @@ export interface CodeOptions { declare module "@tiptap/core" { interface Commands { - code: { + [CORE_EXTENSIONS.CODE_INLINE]: { /** * Set a code mark */ @@ -27,7 +29,7 @@ export const inputRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))$/; const pasteRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))/g; export const CustomCodeInlineExtension = Mark.create({ - name: "code", + name: CORE_EXTENSIONS.CODE_INLINE, addOptions() { return { diff --git a/packages/editor/src/core/extensions/code/code-block-node-view.tsx b/packages/editor/src/core/extensions/code/code-block-node-view.tsx index a06d839908..7626031bc2 100644 --- a/packages/editor/src/core/extensions/code/code-block-node-view.tsx +++ b/packages/editor/src/core/extensions/code/code-block-node-view.tsx @@ -1,11 +1,11 @@ "use client"; -import { useState } from "react"; import { Node as ProseMirrorNode } from "@tiptap/pm/model"; import { NodeViewWrapper, NodeViewContent } from "@tiptap/react"; import ts from "highlight.js/lib/languages/typescript"; import { common, createLowlight } from "lowlight"; import { CopyIcon, CheckIcon } from "lucide-react"; +import { useState } from "react"; // ui import { Tooltip } from "@plane/ui"; // plane utils @@ -27,7 +27,7 @@ export const CodeBlockComponent: React.FC = ({ node }) await navigator.clipboard.writeText(node.textContent); setCopied(true); setTimeout(() => setCopied(false), 1000); - } catch (error) { + } catch { setCopied(false); } e.preventDefault(); diff --git a/packages/editor/src/core/extensions/code/code-block.ts b/packages/editor/src/core/extensions/code/code-block.ts index b2218ee45c..3b07617ca7 100644 --- a/packages/editor/src/core/extensions/code/code-block.ts +++ b/packages/editor/src/core/extensions/code/code-block.ts @@ -1,5 +1,7 @@ import { mergeAttributes, Node, textblockTypeInputRule } from "@tiptap/core"; import { Plugin, PluginKey } from "@tiptap/pm/state"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface CodeBlockOptions { /** @@ -25,7 +27,7 @@ export interface CodeBlockOptions { declare module "@tiptap/core" { interface Commands { - codeBlock: { + [CORE_EXTENSIONS.CODE_BLOCK]: { /** * Set a code block */ @@ -42,7 +44,7 @@ export const backtickInputRegex = /^```([a-z]+)?[\s\n]$/; export const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/; export const CodeBlock = Node.create({ - name: "codeBlock", + name: CORE_EXTENSIONS.CODE_BLOCK, addOptions() { return { @@ -118,7 +120,7 @@ export const CodeBlock = Node.create({ toggleCodeBlock: (attributes) => ({ commands }) => - commands.toggleNode(this.name, "paragraph", attributes), + commands.toggleNode(this.name, CORE_EXTENSIONS.PARAGRAPH, attributes), }; }, @@ -126,7 +128,7 @@ export const CodeBlock = Node.create({ return { "Mod-Alt-c": () => this.editor.commands.toggleCodeBlock(), - // remove code block when at start of document or code block is empty + // remove codeBlock when at start of document or codeBlock is empty Backspace: () => { try { const { empty, $anchor } = this.editor.state.selection; @@ -259,7 +261,7 @@ export const CodeBlock = Node.create({ return false; } - if (this.editor.isActive("code")) { + if (this.editor.isActive(CORE_EXTENSIONS.CODE_INLINE)) { // Check if it's an inline code block event.preventDefault(); const text = event.clipboardData.getData("text/plain"); diff --git a/packages/editor/src/core/extensions/code/lowlight-plugin.ts b/packages/editor/src/core/extensions/code/lowlight-plugin.ts index 5ac30c27ea..0b8ed71ad7 100644 --- a/packages/editor/src/core/extensions/code/lowlight-plugin.ts +++ b/packages/editor/src/core/extensions/code/lowlight-plugin.ts @@ -88,7 +88,7 @@ export function LowlightPlugin({ throw Error("You should provide an instance of lowlight to use the code-block-lowlight extension"); } - const lowlightPlugin: Plugin = new Plugin({ + const lowlightPlugin: Plugin = new Plugin({ key: new PluginKey("lowlight"), state: { diff --git a/packages/editor/src/core/extensions/core-without-props.ts b/packages/editor/src/core/extensions/core-without-props.ts index ed9f5c1a4b..a309c2013a 100644 --- a/packages/editor/src/core/extensions/core-without-props.ts +++ b/packages/editor/src/core/extensions/core-without-props.ts @@ -3,24 +3,24 @@ import TaskList from "@tiptap/extension-task-list"; import TextStyle from "@tiptap/extension-text-style"; import TiptapUnderline from "@tiptap/extension-underline"; import StarterKit from "@tiptap/starter-kit"; -// extensions // helpers import { isValidHttpUrl } from "@/helpers/common"; +// plane editor imports +import { CoreEditorAdditionalExtensionsWithoutProps } from "@/plane-editor/extensions/core/without-props"; +// extensions +import { CustomCalloutExtensionConfig } from "./callout/extension-config"; import { CustomCodeBlockExtensionWithoutProps } from "./code/without-props"; import { CustomCodeInlineExtension } from "./code-inline"; +import { CustomColorExtension } from "./custom-color"; import { CustomLinkExtension } from "./custom-link"; import { CustomHorizontalRule } from "./horizontal-rule"; import { ImageExtensionWithoutProps } from "./image"; import { CustomImageComponentWithoutProps } from "./image/image-component-without-props"; -import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props"; import { CustomMentionExtensionConfig } from "./mentions/extension-config"; import { CustomQuoteExtension } from "./quote"; import { TableHeader, TableCell, TableRow, Table } from "./table"; import { CustomTextAlignExtension } from "./text-align"; -import { CustomCalloutExtensionConfig } from "./callout/extension-config"; -import { CustomColorExtension } from "./custom-color"; -// plane editor extensions -import { CoreEditorAdditionalExtensionsWithoutProps } from "@/plane-editor/extensions/core/without-props"; +import { WorkItemEmbedExtensionConfig } from "./work-item-embed/extension-config"; export const CoreEditorExtensionsWithoutProps = [ StarterKit.configure({ @@ -72,12 +72,12 @@ export const CoreEditorExtensionsWithoutProps = [ "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", }, }), - ImageExtensionWithoutProps().configure({ + ImageExtensionWithoutProps.configure({ HTMLAttributes: { class: "rounded-md", }, }), - CustomImageComponentWithoutProps(), + CustomImageComponentWithoutProps, TiptapUnderline, TextStyle, TaskList.configure({ @@ -104,4 +104,4 @@ export const CoreEditorExtensionsWithoutProps = [ ...CoreEditorAdditionalExtensionsWithoutProps, ]; -export const DocumentEditorExtensionsWithoutProps = [IssueWidgetWithoutProps()]; +export const DocumentEditorExtensionsWithoutProps = [WorkItemEmbedExtensionConfig]; diff --git a/packages/editor/src/core/extensions/custom-code-inline.ts b/packages/editor/src/core/extensions/custom-code-inline.ts deleted file mode 100644 index 3b3cfaab1e..0000000000 --- a/packages/editor/src/core/extensions/custom-code-inline.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Extension } from "@tiptap/core"; -import codemark from "prosemirror-codemark"; - -export const CustomCodeMarkPlugin = Extension.create({ - name: "codemarkPlugin", - addProseMirrorPlugins() { - return codemark({ markType: this.editor.schema.marks.code }); - }, -}); diff --git a/packages/editor/src/core/extensions/custom-color.ts b/packages/editor/src/core/extensions/custom-color.ts index b377099fb5..8b516e8ecd 100644 --- a/packages/editor/src/core/extensions/custom-color.ts +++ b/packages/editor/src/core/extensions/custom-color.ts @@ -1,10 +1,11 @@ import { Mark, mergeAttributes } from "@tiptap/core"; // constants import { COLORS_LIST } from "@/constants/common"; +import { CORE_EXTENSIONS } from "@/constants/extension"; declare module "@tiptap/core" { interface Commands { - color: { + [CORE_EXTENSIONS.CUSTOM_COLOR]: { /** * Set the text color * @param {string} color The color to set @@ -34,7 +35,7 @@ declare module "@tiptap/core" { } export const CustomColorExtension = Mark.create({ - name: "customColor", + name: CORE_EXTENSIONS.CUSTOM_COLOR, addOptions() { return { diff --git a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx index e525bc6da4..f8bfcf4a1f 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx @@ -1,7 +1,10 @@ import { Editor, NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { useEffect, useRef, useState } from "react"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { CustomImageBlock, CustomImageUploader, ImageAttributes } from "@/extensions/custom-image"; +// helpers import { getExtensionStorage } from "@/helpers/get-extension-storage"; export type CustoBaseImageNodeViewProps = { @@ -77,7 +80,7 @@ export const CustomImageNode = (props: CustomImageNodeProps) => { failedToLoadImage={failedToLoadImage} getPos={getPos} loadImageFromFileSystem={setImageFromFileSystem} - maxFileSize={getExtensionStorage(editor, "imageComponent").maxFileSize} + maxFileSize={getExtensionStorage(editor, CORE_EXTENSIONS.CUSTOM_IMAGE).maxFileSize} node={node} setIsUploaded={setIsUploaded} selected={selected} diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index 0a3ee1a1c3..5af4f556d7 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -4,10 +4,12 @@ import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react"; import { cn } from "@plane/utils"; // constants import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { CustoBaseImageNodeViewProps, getImageComponentImageFileMap } from "@/extensions/custom-image"; // hooks import { useUploader, useDropZone, uploadFirstFileAndInsertRemaining } from "@/hooks/use-file-upload"; +import { getExtensionStorage } from "@/helpers/get-extension-storage"; type CustomImageUploaderProps = CustoBaseImageNodeViewProps & { maxFileSize: number; @@ -57,7 +59,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { // control cursor position after upload const nextNode = editor.state.doc.nodeAt(pos + 1); - if (nextNode && nextNode.type.name === "paragraph") { + if (nextNode && nextNode.type.name === CORE_EXTENSIONS.PARAGRAPH) { // If there is a paragraph node after the image component, move the focus to the next node editor.commands.setTextSelection(pos + 1); } else { @@ -75,7 +77,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { // @ts-expect-error - TODO: fix typings, and don't remove await from here for now editorCommand: async (file) => await editor?.commands.uploadImage(imageEntityId, file), handleProgressStatus: (isUploading) => { - editor.storage.imageComponent.uploadInProgress = isUploading; + getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY).uploadInProgress = isUploading; }, loadFileFromFileSystem: loadImageFromFileSystem, maxFileSize, @@ -85,6 +87,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, editor, maxFileSize, + onInvalidFile: (_error, message) => alert(message), pos: getPos(), type: "image", uploader: uploadFile, @@ -123,6 +126,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { editor, filesList, maxFileSize, + onInvalidFile: (_error, message) => alert(message), pos: getPos(), type: "image", uploader: uploadFile, diff --git a/packages/editor/src/core/extensions/custom-image/components/upload-status.tsx b/packages/editor/src/core/extensions/custom-image/components/upload-status.tsx index 8b71713d24..f88c69c6f3 100644 --- a/packages/editor/src/core/extensions/custom-image/components/upload-status.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/upload-status.tsx @@ -1,6 +1,10 @@ import { Editor } from "@tiptap/core"; import { useEditorState } from "@tiptap/react"; import { useEffect, useRef, useState } from "react"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// helpers +import { getExtensionStorage } from "@/helpers/get-extension-storage"; type Props = { editor: Editor; @@ -16,7 +20,7 @@ export const ImageUploadStatus: React.FC = (props) => { // subscribe to image upload status const uploadStatus: number | undefined = useEditorState({ editor, - selector: ({ editor }) => editor.storage.imageComponent?.assetsUploadStatus[nodeId], + selector: ({ editor }) => getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.assetsUploadStatus?.[nodeId], }); useEffect(() => { diff --git a/packages/editor/src/core/extensions/custom-image/custom-image.ts b/packages/editor/src/core/extensions/custom-image/custom-image.ts index 11586bf861..afd02fd099 100644 --- a/packages/editor/src/core/extensions/custom-image/custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/custom-image.ts @@ -1,17 +1,16 @@ import { Editor, mergeAttributes } from "@tiptap/core"; -import { Image } from "@tiptap/extension-image"; +import { Image as BaseImageExtension } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; import { v4 as uuidv4 } from "uuid"; // constants import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { CustomImageNode } from "@/extensions/custom-image"; // helpers import { isFileValid } from "@/helpers/file"; import { getExtensionStorage } from "@/helpers/get-extension-storage"; import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary"; -// plugins -import { TrackImageDeletionPlugin, TrackImageRestorationPlugin } from "@/plugins/image"; // types import { TFileHandler } from "@/types"; @@ -23,23 +22,21 @@ export type InsertImageComponentProps = { declare module "@tiptap/core" { interface Commands { - imageComponent: { + [CORE_EXTENSIONS.CUSTOM_IMAGE]: { insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType; uploadImage: (blockId: string, file: File) => () => Promise | undefined; - updateAssetsUploadStatus?: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void; getImageSource?: (path: string) => () => Promise; restoreImage: (src: string) => () => Promise; }; } } -export const getImageComponentImageFileMap = (editor: Editor) => getExtensionStorage(editor, "imageComponent")?.fileMap; +export const getImageComponentImageFileMap = (editor: Editor) => + getExtensionStorage(editor, CORE_EXTENSIONS.CUSTOM_IMAGE)?.fileMap; export interface CustomImageExtensionStorage { - assetsUploadStatus: TFileHandler["assetsUploadStatus"]; fileMap: Map; deletedImageSet: Map; - uploadInProgress: boolean; maxFileSize: number; } @@ -47,16 +44,14 @@ export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) export const CustomImageExtension = (props: TFileHandler) => { const { - assetsUploadStatus, getAssetSrc, upload, - delete: deleteImageFn, restore: restoreImageFn, validation: { maxFileSize }, } = props; - return Image.extend, CustomImageExtensionStorage>({ - name: "imageComponent", + return BaseImageExtension.extend, CustomImageExtensionStorage>({ + name: CORE_EXTENSIONS.CUSTOM_IMAGE, selectable: true, group: "block", atom: true, @@ -102,41 +97,15 @@ export const CustomImageExtension = (props: TFileHandler) => { }; }, - addProseMirrorPlugins() { - return [ - TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name), - TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name), - ]; - }, - - onCreate(this) { - const imageSources = new Set(); - this.editor.state.doc.descendants((node) => { - if (node.type.name === this.name) { - if (!node.attrs.src?.startsWith("http")) return; - imageSources.add(node.attrs.src); - } - }); - imageSources.forEach(async (src) => { - try { - await restoreImageFn(src); - } catch (error) { - console.error("Error restoring image: ", error); - } - }); - }, - addStorage() { return { fileMap: new Map(), deletedImageSet: new Map(), - uploadInProgress: false, maxFileSize, // escape markdown for images markdown: { serialize() {}, }, - assetsUploadStatus, }; }, @@ -152,6 +121,7 @@ export const CustomImageExtension = (props: TFileHandler) => { acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, file: props.file, maxFileSize, + onError: (_error, message) => alert(message), }) ) { return false; @@ -196,9 +166,6 @@ export const CustomImageExtension = (props: TFileHandler) => { const fileUrl = await upload(blockId, file); return fileUrl; }, - updateAssetsUploadStatus: (updatedStatus) => () => { - this.storage.assetsUploadStatus = updatedStatus; - }, getImageSource: (path) => async () => await getAssetSrc(path), restoreImage: (src) => async () => { await restoreImageFn(src); diff --git a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts index 51b758898b..4a85ffd94c 100644 --- a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts @@ -1,6 +1,8 @@ import { mergeAttributes } from "@tiptap/core"; -import { Image } from "@tiptap/extension-image"; +import { Image as BaseImageExtension } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // components import { CustomImageNode, CustomImageExtensionStorage } from "@/extensions/custom-image"; // types @@ -9,8 +11,8 @@ import { TReadOnlyFileHandler } from "@/types"; export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => { const { getAssetSrc, restore: restoreImageFn } = props; - return Image.extend, CustomImageExtensionStorage>({ - name: "imageComponent", + return BaseImageExtension.extend, CustomImageExtensionStorage>({ + name: CORE_EXTENSIONS.CUSTOM_IMAGE, selectable: false, group: "block", atom: true, @@ -53,13 +55,11 @@ export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => { return { fileMap: new Map(), deletedImageSet: new Map(), - uploadInProgress: false, maxFileSize: 0, // escape markdown for images markdown: { serialize() {}, }, - assetsUploadStatus: {}, }; }, diff --git a/packages/editor/src/core/extensions/custom-link/extension.tsx b/packages/editor/src/core/extensions/custom-link/extension.tsx index 27c1bb598d..182afc9f8f 100644 --- a/packages/editor/src/core/extensions/custom-link/extension.tsx +++ b/packages/editor/src/core/extensions/custom-link/extension.tsx @@ -1,6 +1,9 @@ import { Mark, markPasteRule, mergeAttributes, PasteRuleMatch } from "@tiptap/core"; import { Plugin } from "@tiptap/pm/state"; import { find, registerCustomProtocol, reset } from "linkifyjs"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// local imports import { autolink } from "./helpers/autolink"; import { clickHandler } from "./helpers/clickHandler"; import { pasteHandler } from "./helpers/pasteHandler"; @@ -46,7 +49,7 @@ export interface LinkOptions { declare module "@tiptap/core" { interface Commands { - link: { + [CORE_EXTENSIONS.CUSTOM_LINK]: { /** * Set a link mark */ @@ -79,7 +82,7 @@ export type CustomLinkStorage = { }; export const CustomLinkExtension = Mark.create({ - name: "link", + name: CORE_EXTENSIONS.CUSTOM_LINK, priority: 1000, diff --git a/packages/editor/src/core/extensions/custom-link/helpers/clickHandler.ts b/packages/editor/src/core/extensions/custom-link/helpers/clickHandler.ts index 1b084d1ac5..72906bc942 100644 --- a/packages/editor/src/core/extensions/custom-link/helpers/clickHandler.ts +++ b/packages/editor/src/core/extensions/custom-link/helpers/clickHandler.ts @@ -16,7 +16,7 @@ export function clickHandler(options: ClickHandlerOptions): Plugin { } let a = event.target as HTMLElement; - const els = []; + const els: HTMLElement[] = []; while (a?.nodeName !== "DIV") { els.push(a); diff --git a/packages/editor/src/core/extensions/custom-list-keymap/list-helpers.ts b/packages/editor/src/core/extensions/custom-list-keymap/list-helpers.ts index 7d4cad17e0..547f9f17e1 100644 --- a/packages/editor/src/core/extensions/custom-list-keymap/list-helpers.ts +++ b/packages/editor/src/core/extensions/custom-list-keymap/list-helpers.ts @@ -1,12 +1,14 @@ import { Editor, getNodeType, getNodeAtPosition, isAtEndOfNode, isAtStartOfNode, isNodeActive } from "@tiptap/core"; import { Node, NodeType } from "@tiptap/pm/model"; import { EditorState } from "@tiptap/pm/state"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; const findListItemPos = (typeOrName: string | NodeType, state: EditorState) => { const { $from } = state.selection; const nodeType = getNodeType(typeOrName, state.schema); - let currentNode = null; + let currentNode: Node | null = null; let currentDepth = $from.depth; let currentPos = $from.pos; let targetDepth: number | null = null; @@ -72,7 +74,11 @@ const getPrevListDepth = (typeOrName: string, state: EditorState) => { // Traverse up the document structure from the adjusted position for (let d = resolvedPos.depth; d > 0; d--) { const node = resolvedPos.node(d); - if (node.type.name === "bulletList" || node.type.name === "orderedList" || node.type.name === "taskList") { + if ( + [CORE_EXTENSIONS.BULLET_LIST, CORE_EXTENSIONS.ORDERED_LIST, CORE_EXTENSIONS.TASK_LIST].includes( + node.type.name as CORE_EXTENSIONS + ) + ) { // Increment depth for each list ancestor found depth++; } @@ -309,12 +315,12 @@ const isCurrentParagraphASibling = (state: EditorState): boolean => { // Ensure we're in a paragraph and the parent is a list item. if ( - currentParagraphNode.type.name === "paragraph" && - (listItemNode.type.name === "listItem" || listItemNode.type.name === "taskItem") + currentParagraphNode.type.name === CORE_EXTENSIONS.PARAGRAPH && + [CORE_EXTENSIONS.LIST_ITEM, CORE_EXTENSIONS.TASK_ITEM].includes(listItemNode.type.name as CORE_EXTENSIONS) ) { let paragraphNodesCount = 0; listItemNode.forEach((child) => { - if (child.type.name === "paragraph") { + if (child.type.name === CORE_EXTENSIONS.PARAGRAPH) { paragraphNodesCount++; } }); diff --git a/packages/editor/src/core/extensions/custom-list-keymap/list-keymap.ts b/packages/editor/src/core/extensions/custom-list-keymap/list-keymap.ts index 2a17838fd8..576888f55a 100644 --- a/packages/editor/src/core/extensions/custom-list-keymap/list-keymap.ts +++ b/packages/editor/src/core/extensions/custom-list-keymap/list-keymap.ts @@ -1,4 +1,6 @@ import { Extension } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { handleBackspace, handleDelete } from "@/extensions/custom-list-keymap/list-helpers"; @@ -31,10 +33,10 @@ export const ListKeymap = ({ tabIndex }: { tabIndex?: number }) => addKeyboardShortcuts() { return { Tab: () => { - if (this.editor.isActive("listItem") || this.editor.isActive("taskItem")) { - if (this.editor.commands.sinkListItem("listItem")) { + if (this.editor.isActive(CORE_EXTENSIONS.LIST_ITEM) || this.editor.isActive(CORE_EXTENSIONS.TASK_ITEM)) { + if (this.editor.commands.sinkListItem(CORE_EXTENSIONS.LIST_ITEM)) { return true; - } else if (this.editor.commands.sinkListItem("taskItem")) { + } else if (this.editor.commands.sinkListItem(CORE_EXTENSIONS.TASK_ITEM)) { return true; } return true; @@ -46,9 +48,9 @@ export const ListKeymap = ({ tabIndex }: { tabIndex?: number }) => return true; }, "Shift-Tab": () => { - if (this.editor.commands.liftListItem("listItem")) { + if (this.editor.commands.liftListItem(CORE_EXTENSIONS.LIST_ITEM)) { return true; - } else if (this.editor.commands.liftListItem("taskItem")) { + } else if (this.editor.commands.liftListItem(CORE_EXTENSIONS.TASK_ITEM)) { return true; } // if tabIndex is set, we don't want to handle Tab key diff --git a/packages/editor/src/core/extensions/drop.ts b/packages/editor/src/core/extensions/drop.ts deleted file mode 100644 index 2a5a994f8a..0000000000 --- a/packages/editor/src/core/extensions/drop.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { Extension, Editor } from "@tiptap/core"; -import { Plugin, PluginKey } from "@tiptap/pm/state"; -// constants -import { ACCEPTED_ATTACHMENT_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; -// types -import { TEditorCommands } from "@/types"; - -export const DropHandlerExtension = Extension.create({ - name: "dropHandler", - priority: 1000, - - addProseMirrorPlugins() { - const editor = this.editor; - return [ - new Plugin({ - key: new PluginKey("drop-handler-plugin"), - props: { - handlePaste: (view, event) => { - if ( - editor.isEditable && - event.clipboardData && - event.clipboardData.files && - event.clipboardData.files.length > 0 - ) { - event.preventDefault(); - const files = Array.from(event.clipboardData.files); - const acceptedFiles = files.filter( - (f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type) - ); - - if (acceptedFiles.length) { - const pos = view.state.selection.from; - insertFilesSafely({ - editor, - files: acceptedFiles, - initialPos: pos, - event: "drop", - }); - } - return true; - } - return false; - }, - handleDrop: (view, event, _slice, moved) => { - if ( - editor.isEditable && - !moved && - event.dataTransfer && - event.dataTransfer.files && - event.dataTransfer.files.length > 0 - ) { - event.preventDefault(); - const files = Array.from(event.dataTransfer.files); - const acceptedFiles = files.filter( - (f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type) - ); - - if (acceptedFiles.length) { - const coordinates = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - - if (coordinates) { - const pos = coordinates.pos; - insertFilesSafely({ - editor, - files: acceptedFiles, - initialPos: pos, - event: "drop", - }); - } - return true; - } - } - return false; - }, - }, - }), - ]; - }, -}); - -type InsertFilesSafelyArgs = { - editor: Editor; - event: "insert" | "drop"; - files: File[]; - initialPos: number; - type?: Extract; -}; - -export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => { - const { editor, event, files, initialPos, type } = args; - let pos = initialPos; - - for (const file of files) { - // safe insertion - const docSize = editor.state.doc.content.size; - pos = Math.min(pos, docSize); - - let fileType: "image" | "attachment" | null = null; - - try { - if (type) { - if (["image", "attachment"].includes(type)) fileType = type; - else throw new Error("Wrong file type passed"); - } else { - if (ACCEPTED_IMAGE_MIME_TYPES.includes(file.type)) fileType = "image"; - else if (ACCEPTED_ATTACHMENT_MIME_TYPES.includes(file.type)) fileType = "attachment"; - } - // insert file depending on the type at the current position - if (fileType === "image") { - editor.commands.insertImageComponent({ - file, - pos, - event, - }); - } else if (fileType === "attachment") { - } - } catch (error) { - console.error(`Error while ${event}ing file:`, error); - } - - // Move to the next position - pos += 1; - } -}; diff --git a/packages/editor/src/core/extensions/enter-key-extension.tsx b/packages/editor/src/core/extensions/enter-key.ts similarity index 53% rename from packages/editor/src/core/extensions/enter-key-extension.tsx rename to packages/editor/src/core/extensions/enter-key.ts index d67ceb78b8..65119425fc 100644 --- a/packages/editor/src/core/extensions/enter-key-extension.tsx +++ b/packages/editor/src/core/extensions/enter-key.ts @@ -1,16 +1,19 @@ import { Extension } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// helpers +import { getExtensionStorage } from "@/helpers/get-extension-storage"; export const EnterKeyExtension = (onEnterKeyPress?: () => void) => Extension.create({ - name: "enterKey", + name: CORE_EXTENSIONS.ENTER_KEY, addKeyboardShortcuts(this) { return { Enter: () => { - if (!this.editor.storage.mentionsOpen) { - if (onEnterKeyPress) { - onEnterKeyPress(); - } + const isMentionOpen = getExtensionStorage(this.editor, CORE_EXTENSIONS.MENTION)?.mentionsOpen; + if (!isMentionOpen) { + onEnterKeyPress?.(); return true; } return false; @@ -18,8 +21,8 @@ export const EnterKeyExtension = (onEnterKeyPress?: () => void) => "Shift-Enter": ({ editor }) => editor.commands.first(({ commands }) => [ () => commands.newlineInCode(), - () => commands.splitListItem("listItem"), - () => commands.splitListItem("taskItem"), + () => commands.splitListItem(CORE_EXTENSIONS.LIST_ITEM), + () => commands.splitListItem(CORE_EXTENSIONS.TASK_ITEM), () => commands.createParagraphNear(), () => commands.liftEmptyBlock(), () => commands.splitBlock(), diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.ts similarity index 86% rename from packages/editor/src/core/extensions/extensions.tsx rename to packages/editor/src/core/extensions/extensions.ts index 1ef0a3b157..51969cd5cf 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.ts @@ -7,12 +7,13 @@ import TextStyle from "@tiptap/extension-text-style"; import TiptapUnderline from "@tiptap/extension-underline"; import StarterKit from "@tiptap/starter-kit"; import { Markdown } from "tiptap-markdown"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { CustomCalloutExtension, CustomCodeBlockExtension, CustomCodeInlineExtension, - CustomCodeMarkPlugin, CustomColorExtension, CustomHorizontalRule, CustomImageExtension, @@ -22,17 +23,17 @@ import { CustomQuoteExtension, CustomTextAlignExtension, CustomTypographyExtension, - DropHandlerExtension, ImageExtension, ListKeymap, Table, TableCell, TableHeader, TableRow, - MarkdownClipboard, + UtilityExtension, } from "@/extensions"; // helpers import { isValidHttpUrl } from "@/helpers/common"; +import { getExtensionStorage } from "@/helpers/get-extension-storage"; // plane editor extensions import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions"; // types @@ -49,7 +50,7 @@ type TArguments = { }; export const CoreEditorExtensions = (args: TArguments): Extensions => { - const { disabledExtensions, enableHistory, fileHandler, mentionHandler, placeholder, tabIndex } = args; + const { disabledExtensions, enableHistory, fileHandler, mentionHandler, placeholder, tabIndex, editable } = args; const extensions = [ StarterKit.configure({ @@ -89,7 +90,6 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { ...(enableHistory ? {} : { history: false }), }), CustomQuoteExtension, - DropHandlerExtension, CustomHorizontalRule.configure({ HTMLAttributes: { class: "py-4 border-custom-border-400", @@ -127,7 +127,6 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { class: "", }, }), - CustomCodeMarkPlugin, CustomCodeInlineExtension, Markdown.configure({ html: true, @@ -135,7 +134,6 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { transformPastedText: true, breaks: true, }), - MarkdownClipboard, Table, TableHeader, TableCell, @@ -145,15 +143,17 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { placeholder: ({ editor, node }) => { if (!editor.isEditable) return ""; - if (node.type.name === "heading") return `Heading ${node.attrs.level}`; + if (node.type.name === CORE_EXTENSIONS.HEADING) return `Heading ${node.attrs.level}`; - if (editor.storage.imageComponent?.uploadInProgress) return ""; + const isUploadInProgress = getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress; + + if (isUploadInProgress) return ""; const shouldHidePlaceholder = - editor.isActive("table") || - editor.isActive("codeBlock") || - editor.isActive("image") || - editor.isActive("imageComponent"); + editor.isActive(CORE_EXTENSIONS.TABLE) || + editor.isActive(CORE_EXTENSIONS.CODE_BLOCK) || + editor.isActive(CORE_EXTENSIONS.IMAGE) || + editor.isActive(CORE_EXTENSIONS.CUSTOM_IMAGE); if (shouldHidePlaceholder) return ""; @@ -169,6 +169,10 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { CharacterCount, CustomTextAlignExtension, CustomCalloutExtension, + UtilityExtension({ + isEditable: editable, + fileHandler, + }), CustomColorExtension, ...CoreEditorAdditionalExtensions({ disabledExtensions, diff --git a/packages/editor/src/core/extensions/headers.ts b/packages/editor/src/core/extensions/headings-list.ts similarity index 86% rename from packages/editor/src/core/extensions/headers.ts rename to packages/editor/src/core/extensions/headings-list.ts index 958cf6ca32..51a9aeedc2 100644 --- a/packages/editor/src/core/extensions/headers.ts +++ b/packages/editor/src/core/extensions/headings-list.ts @@ -1,5 +1,7 @@ import { Extension } from "@tiptap/core"; import { Plugin, PluginKey } from "@tiptap/pm/state"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface IMarking { type: "heading"; @@ -12,8 +14,8 @@ export type HeadingExtensionStorage = { headings: IMarking[]; }; -export const HeadingListExtension = Extension.create({ - name: "headingList", +export const HeadingListExtension = Extension.create({ + name: CORE_EXTENSIONS.HEADINGS_LIST, addStorage() { return { diff --git a/packages/editor/src/core/extensions/horizontal-rule.ts b/packages/editor/src/core/extensions/horizontal-rule.ts index b9be1a314d..99a5dacc3e 100644 --- a/packages/editor/src/core/extensions/horizontal-rule.ts +++ b/packages/editor/src/core/extensions/horizontal-rule.ts @@ -1,5 +1,7 @@ import { isNodeSelection, mergeAttributes, Node, nodeInputRule } from "@tiptap/core"; import { NodeSelection, TextSelection } from "@tiptap/pm/state"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface HorizontalRuleOptions { HTMLAttributes: Record; @@ -7,7 +9,7 @@ export interface HorizontalRuleOptions { declare module "@tiptap/core" { interface Commands { - horizontalRule: { + [CORE_EXTENSIONS.HORIZONTAL_RULE]: { /** * Add a horizontal rule */ @@ -17,7 +19,7 @@ declare module "@tiptap/core" { } export const CustomHorizontalRule = Node.create({ - name: "horizontalRule", + name: CORE_EXTENSIONS.HORIZONTAL_RULE, addOptions() { return { diff --git a/packages/editor/src/core/extensions/image/extension.tsx b/packages/editor/src/core/extensions/image/extension.tsx index 6766b4d0c0..12844149cf 100644 --- a/packages/editor/src/core/extensions/image/extension.tsx +++ b/packages/editor/src/core/extensions/image/extension.tsx @@ -1,23 +1,23 @@ -import ImageExt from "@tiptap/extension-image"; +import { Image as BaseImageExtension } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; // extensions import { CustomImageNode } from "@/extensions"; // helpers import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary"; -// plugins -import { ImageExtensionStorage, TrackImageDeletionPlugin, TrackImageRestorationPlugin } from "@/plugins/image"; // types import { TFileHandler } from "@/types"; +export type ImageExtensionStorage = { + deletedImageSet: Map; +}; + export const ImageExtension = (fileHandler: TFileHandler) => { const { getAssetSrc, - delete: deleteImageFn, - restore: restoreImageFn, validation: { maxFileSize }, } = fileHandler; - return ImageExt.extend({ + return BaseImageExtension.extend({ addKeyboardShortcuts() { return { ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name), @@ -25,36 +25,10 @@ export const ImageExtension = (fileHandler: TFileHandler) => { }; }, - addProseMirrorPlugins() { - return [ - TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name), - TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name), - ]; - }, - - onCreate(this) { - const imageSources = new Set(); - this.editor.state.doc.descendants((node) => { - if (node.type.name === this.name) { - if (!node.attrs.src?.startsWith("http")) return; - - imageSources.add(node.attrs.src); - } - }); - imageSources.forEach(async (src) => { - try { - await restoreImageFn(src); - } catch (error) { - console.error("Error restoring image: ", error); - } - }); - }, - // storage to keep track of image states Map addStorage() { return { deletedImageSet: new Map(), - uploadInProgress: false, maxFileSize, }; }, diff --git a/packages/editor/src/core/extensions/image/image-component-without-props.tsx b/packages/editor/src/core/extensions/image/image-component-without-props.tsx index c17bcc5598..bd2c3f16b5 100644 --- a/packages/editor/src/core/extensions/image/image-component-without-props.tsx +++ b/packages/editor/src/core/extensions/image/image-component-without-props.tsx @@ -1,58 +1,56 @@ import { mergeAttributes } from "@tiptap/core"; -import { Image } from "@tiptap/extension-image"; -// extensions -import { ImageExtensionStorage } from "@/plugins/image"; +import { Image as BaseImageExtension } from "@tiptap/extension-image"; +// local imports +import { ImageExtensionStorage } from "./extension"; -export const CustomImageComponentWithoutProps = () => - Image.extend, ImageExtensionStorage>({ - name: "imageComponent", - selectable: true, - group: "block", - atom: true, - draggable: true, +export const CustomImageComponentWithoutProps = BaseImageExtension.extend< + Record, + ImageExtensionStorage +>({ + name: "imageComponent", + selectable: true, + group: "block", + atom: true, + draggable: true, - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - src: { - default: null, - }, - height: { - default: "auto", - }, - ["id"]: { - default: null, - }, - aspectRatio: { - default: null, - }, - }; - }, + addAttributes() { + return { + ...this.parent?.(), + width: { + default: "35%", + }, + src: { + default: null, + }, + height: { + default: "auto", + }, + ["id"]: { + default: null, + }, + aspectRatio: { + default: null, + }, + }; + }, - parseHTML() { - return [ - { - tag: "image-component", - }, - ]; - }, + parseHTML() { + return [ + { + tag: "image-component", + }, + ]; + }, - renderHTML({ HTMLAttributes }) { - return ["image-component", mergeAttributes(HTMLAttributes)]; - }, + renderHTML({ HTMLAttributes }) { + return ["image-component", mergeAttributes(HTMLAttributes)]; + }, - addStorage() { - return { - fileMap: new Map(), - deletedImageSet: new Map(), - uploadInProgress: false, - maxFileSize: 0, - assetsUploadStatus: {}, - }; - }, - }); - -export default CustomImageComponentWithoutProps; + addStorage() { + return { + fileMap: new Map(), + deletedImageSet: new Map(), + maxFileSize: 0, + }; + }, +}); diff --git a/packages/editor/src/core/extensions/image/image-extension-without-props.tsx b/packages/editor/src/core/extensions/image/image-extension-without-props.tsx index bb6c5b4ad8..ba064bef48 100644 --- a/packages/editor/src/core/extensions/image/image-extension-without-props.tsx +++ b/packages/editor/src/core/extensions/image/image-extension-without-props.tsx @@ -1,19 +1,18 @@ -import ImageExt from "@tiptap/extension-image"; +import { Image as BaseImageExtension } from "@tiptap/extension-image"; -export const ImageExtensionWithoutProps = () => - ImageExt.extend({ - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - height: { - default: null, - }, - aspectRatio: { - default: null, - }, - }; - }, - }); +export const ImageExtensionWithoutProps = BaseImageExtension.extend({ + addAttributes() { + return { + ...this.parent?.(), + width: { + default: "35%", + }, + height: { + default: null, + }, + aspectRatio: { + default: null, + }, + }; + }, +}); diff --git a/packages/editor/src/core/extensions/image/read-only-image.tsx b/packages/editor/src/core/extensions/image/read-only-image.tsx index a656078037..271c39fd8d 100644 --- a/packages/editor/src/core/extensions/image/read-only-image.tsx +++ b/packages/editor/src/core/extensions/image/read-only-image.tsx @@ -1,4 +1,4 @@ -import Image from "@tiptap/extension-image"; +import { Image as BaseImageExtension } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; // extensions import { CustomImageNode } from "@/extensions"; @@ -8,7 +8,7 @@ import { TReadOnlyFileHandler } from "@/types"; export const ReadOnlyImageExtension = (props: TReadOnlyFileHandler) => { const { getAssetSrc } = props; - return Image.extend({ + return BaseImageExtension.extend({ addAttributes() { return { ...this.parent?.(), diff --git a/packages/editor/src/core/extensions/index.ts b/packages/editor/src/core/extensions/index.ts index e986075852..3c3232885f 100644 --- a/packages/editor/src/core/extensions/index.ts +++ b/packages/editor/src/core/extensions/index.ts @@ -5,22 +5,20 @@ export * from "./custom-image"; export * from "./custom-link"; export * from "./custom-list-keymap"; export * from "./image"; -export * from "./issue-embed"; export * from "./mentions"; export * from "./slash-commands"; export * from "./table"; export * from "./typography"; +export * from "./work-item-embed"; export * from "./core-without-props"; -export * from "./custom-code-inline"; export * from "./custom-color"; -export * from "./drop"; -export * from "./enter-key-extension"; +export * from "./enter-key"; export * from "./extensions"; -export * from "./headers"; +export * from "./headings-list"; export * from "./horizontal-rule"; export * from "./keymap"; export * from "./quote"; export * from "./read-only-extensions"; export * from "./side-menu"; export * from "./text-align"; -export * from "./clipboard"; +export * from "./utility"; diff --git a/packages/editor/src/core/extensions/issue-embed/index.ts b/packages/editor/src/core/extensions/issue-embed/index.ts deleted file mode 100644 index f47619a034..0000000000 --- a/packages/editor/src/core/extensions/issue-embed/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./widget-node"; -export * from "./issue-embed-without-props"; diff --git a/packages/editor/src/core/extensions/issue-embed/issue-embed-without-props.ts b/packages/editor/src/core/extensions/issue-embed/issue-embed-without-props.ts deleted file mode 100644 index bef366cbab..0000000000 --- a/packages/editor/src/core/extensions/issue-embed/issue-embed-without-props.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { mergeAttributes, Node } from "@tiptap/core"; - -export const IssueWidgetWithoutProps = () => - Node.create({ - name: "issue-embed-component", - group: "block", - atom: true, - selectable: true, - draggable: true, - - addAttributes() { - return { - entity_identifier: { - default: undefined, - }, - project_identifier: { - default: undefined, - }, - workspace_identifier: { - default: undefined, - }, - id: { - default: undefined, - }, - entity_name: { - default: undefined, - }, - }; - }, - - parseHTML() { - return [ - { - tag: "issue-embed-component", - }, - ]; - }, - renderHTML({ HTMLAttributes }) { - return ["issue-embed-component", mergeAttributes(HTMLAttributes)]; - }, - }); diff --git a/packages/editor/src/core/extensions/issue-embed/widget-node.tsx b/packages/editor/src/core/extensions/issue-embed/widget-node.tsx deleted file mode 100644 index a216ab6d92..0000000000 --- a/packages/editor/src/core/extensions/issue-embed/widget-node.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { mergeAttributes, Node } from "@tiptap/core"; -import { ReactNodeViewRenderer, NodeViewWrapper } from "@tiptap/react"; - -type Props = { - widgetCallback: ({ - issueId, - projectId, - workspaceSlug, - }: { - issueId: string; - projectId: string | undefined; - workspaceSlug: string | undefined; - }) => React.ReactNode; -}; - -export const IssueWidget = (props: Props) => - Node.create({ - name: "issue-embed-component", - group: "block", - atom: true, - selectable: true, - draggable: true, - - addAttributes() { - return { - entity_identifier: { - default: undefined, - }, - project_identifier: { - default: undefined, - }, - workspace_identifier: { - default: undefined, - }, - id: { - default: undefined, - }, - entity_name: { - default: undefined, - }, - }; - }, - - addNodeView() { - return ReactNodeViewRenderer((issueProps: any) => ( - - {props.widgetCallback({ - issueId: issueProps.node.attrs.entity_identifier, - projectId: issueProps.node.attrs.project_identifier, - workspaceSlug: issueProps.node.attrs.workspace_identifier, - })} - - )); - }, - - parseHTML() { - return [ - { - tag: "issue-embed-component", - }, - ]; - }, - renderHTML({ HTMLAttributes }) { - return ["issue-embed-component", mergeAttributes(HTMLAttributes)]; - }, - }); diff --git a/packages/editor/src/core/extensions/keymap.tsx b/packages/editor/src/core/extensions/keymap.ts similarity index 92% rename from packages/editor/src/core/extensions/keymap.tsx rename to packages/editor/src/core/extensions/keymap.ts index 81d60e34f6..a4961bb961 100644 --- a/packages/editor/src/core/extensions/keymap.tsx +++ b/packages/editor/src/core/extensions/keymap.ts @@ -2,11 +2,13 @@ import { Extension } from "@tiptap/core"; import { NodeType } from "@tiptap/pm/model"; import { Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; import { canJoin } from "@tiptap/pm/transform"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; declare module "@tiptap/core" { // eslint-disable-next-line no-unused-vars interface Commands { - customkeymap: { + customKeymap: { /** * Select text between node boundaries */ @@ -59,7 +61,7 @@ function autoJoin(tr: Transaction, newTr: Transaction, nodeTypes: NodeType[]) { } export const CustomKeymap = Extension.create({ - name: "CustomKeymap", + name: "customKeymap", addCommands() { return { @@ -87,9 +89,9 @@ export const CustomKeymap = Extension.create({ const newTr = newState.tr; const joinableNodes = [ - newState.schema.nodes["orderedList"], - newState.schema.nodes["taskList"], - newState.schema.nodes["bulletList"], + newState.schema.nodes[CORE_EXTENSIONS.ORDERED_LIST], + newState.schema.nodes[CORE_EXTENSIONS.TASK_LIST], + newState.schema.nodes[CORE_EXTENSIONS.BULLET_LIST], ]; let joined = false; diff --git a/packages/editor/src/core/extensions/mentions/mention-node-view.tsx b/packages/editor/src/core/extensions/mentions/mention-node-view.tsx index 006336fbb6..aac00de884 100644 --- a/packages/editor/src/core/extensions/mentions/mention-node-view.tsx +++ b/packages/editor/src/core/extensions/mentions/mention-node-view.tsx @@ -18,7 +18,7 @@ export const MentionNodeView = (props: Props) => { return ( {(extension.options as TMentionExtensionOptions).renderComponent({ - entity_identifier: attrs[EMentionComponentAttributeNames.ENTITY_IDENTIFIER], + entity_identifier: attrs[EMentionComponentAttributeNames.ENTITY_IDENTIFIER] ?? "", entity_name: attrs[EMentionComponentAttributeNames.ENTITY_NAME] ?? "user_mention", })} diff --git a/packages/editor/src/core/extensions/mentions/mentions-list-dropdown.tsx b/packages/editor/src/core/extensions/mentions/mentions-list-dropdown.tsx index 4f09ed2ae7..da11d0f995 100644 --- a/packages/editor/src/core/extensions/mentions/mentions-list-dropdown.tsx +++ b/packages/editor/src/core/extensions/mentions/mentions-list-dropdown.tsx @@ -1,7 +1,7 @@ "use client"; -import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react"; import { Editor } from "@tiptap/react"; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react"; import { v4 as uuidv4 } from "uuid"; // plane utils import { cn } from "@plane/utils"; @@ -61,7 +61,9 @@ export const MentionsListDropdown = forwardRef((props: MentionsListDropdownProps sections, selectedIndex, }); - setSelectedIndex(newIndex); + if (newIndex) { + setSelectedIndex(newIndex); + } }, })); @@ -79,7 +81,9 @@ export const MentionsListDropdown = forwardRef((props: MentionsListDropdownProps setIsLoading(true); try { const sectionsResponse = await searchCallback?.(query); - setSections(sectionsResponse); + if (sectionsResponse) { + setSections(sectionsResponse); + } } catch (error) { console.error("Failed to fetch suggestions:", error); } finally { diff --git a/packages/editor/src/core/extensions/mentions/utils.ts b/packages/editor/src/core/extensions/mentions/utils.ts index e8e7ed4b7a..5a7550c834 100644 --- a/packages/editor/src/core/extensions/mentions/utils.ts +++ b/packages/editor/src/core/extensions/mentions/utils.ts @@ -1,7 +1,7 @@ import { Editor } from "@tiptap/core"; -import { SuggestionOptions } from "@tiptap/suggestion"; import { ReactRenderer } from "@tiptap/react"; -import tippy from "tippy.js"; +import { SuggestionOptions } from "@tiptap/suggestion"; +import tippy, { Instance } from "tippy.js"; // helpers import { CommandListInstance } from "@/helpers/tippy"; // types @@ -15,7 +15,7 @@ export const renderMentionsDropdown = () => { const { searchCallback } = props; let component: ReactRenderer | null = null; - let popup: any | null = null; + let popup: Instance | null = null; return { onStart: (props: { editor: Editor; clientRect: DOMRect }) => { diff --git a/packages/editor/src/core/extensions/quote.tsx b/packages/editor/src/core/extensions/quote.ts similarity index 85% rename from packages/editor/src/core/extensions/quote.tsx rename to packages/editor/src/core/extensions/quote.ts index 4ae81ffe4f..99a6c10f05 100644 --- a/packages/editor/src/core/extensions/quote.tsx +++ b/packages/editor/src/core/extensions/quote.ts @@ -1,4 +1,6 @@ import Blockquote from "@tiptap/extension-blockquote"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export const CustomQuoteExtension = Blockquote.extend({ addKeyboardShortcuts() { @@ -10,7 +12,7 @@ export const CustomQuoteExtension = Blockquote.extend({ if (!parent) return false; - if (parent.type.name !== "blockquote") { + if (parent.type.name !== CORE_EXTENSIONS.BLOCKQUOTE) { return false; } if ($from.pos !== $to.pos) return false; diff --git a/packages/editor/src/core/extensions/read-only-extensions.tsx b/packages/editor/src/core/extensions/read-only-extensions.ts similarity index 97% rename from packages/editor/src/core/extensions/read-only-extensions.tsx rename to packages/editor/src/core/extensions/read-only-extensions.ts index 3881c548b3..bcfc764115 100644 --- a/packages/editor/src/core/extensions/read-only-extensions.tsx +++ b/packages/editor/src/core/extensions/read-only-extensions.ts @@ -24,7 +24,7 @@ import { CustomTextAlignExtension, CustomCalloutReadOnlyExtension, CustomColorExtension, - MarkdownClipboard, + UtilityExtension, } from "@/extensions"; // helpers import { isValidHttpUrl } from "@/helpers/common"; @@ -117,7 +117,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { html: true, transformCopiedText: false, }), - MarkdownClipboard, Table, TableHeader, TableCell, @@ -127,6 +126,10 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { CustomColorExtension, CustomTextAlignExtension, CustomCalloutReadOnlyExtension, + UtilityExtension({ + isEditable: false, + fileHandler, + }), ...CoreReadOnlyEditorAdditionalExtensions({ disabledExtensions, }), diff --git a/packages/editor/src/core/extensions/side-menu.tsx b/packages/editor/src/core/extensions/side-menu.ts similarity index 97% rename from packages/editor/src/core/extensions/side-menu.tsx rename to packages/editor/src/core/extensions/side-menu.ts index 5f11286b5c..34e3c45e5f 100644 --- a/packages/editor/src/core/extensions/side-menu.tsx +++ b/packages/editor/src/core/extensions/side-menu.ts @@ -1,6 +1,8 @@ import { Extension } from "@tiptap/core"; import { Plugin, PluginKey } from "@tiptap/pm/state"; import { EditorView } from "@tiptap/pm/view"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // plugins import { AIHandlePlugin } from "@/plugins/ai-handle"; import { DragHandlePlugin, nodeDOMAtCoords } from "@/plugins/drag-handle"; @@ -33,7 +35,7 @@ export const SideMenuExtension = (props: Props) => { const { aiEnabled, dragDropEnabled } = props; return Extension.create({ - name: "editorSideMenu", + name: CORE_EXTENSIONS.SIDE_MENU, addProseMirrorPlugins() { return [ SideMenu({ diff --git a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx index 9fcc733aef..fe9ec06a6d 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx @@ -26,22 +26,17 @@ import { toggleBulletList, toggleOrderedList, toggleTaskList, - toggleHeadingOne, - toggleHeadingTwo, - toggleHeadingThree, - toggleHeadingFour, - toggleHeadingFive, - toggleHeadingSix, + toggleHeading, toggleTextColor, toggleBackgroundColor, insertImage, insertCallout, setText, } from "@/helpers/editor-commands"; -// types -import { CommandProps, ISlashCommandItem, TSlashCommandSectionKeys } from "@/types"; // plane editor extensions import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions"; +// types +import { CommandProps, ISlashCommandItem, TSlashCommandSectionKeys } from "@/types"; // local types import { TExtensionProps, TSlashCommandAdditionalOption } from "./root"; @@ -75,7 +70,7 @@ export const getSlashCommandFilteredSections = description: "Big section heading.", searchTerms: ["title", "big", "large"], icon: , - command: ({ editor, range }) => toggleHeadingOne(editor, range), + command: ({ editor, range }) => toggleHeading(editor, 1, range), }, { commandKey: "h2", @@ -84,7 +79,7 @@ export const getSlashCommandFilteredSections = description: "Medium section heading.", searchTerms: ["subtitle", "medium"], icon: , - command: ({ editor, range }) => toggleHeadingTwo(editor, range), + command: ({ editor, range }) => toggleHeading(editor, 2, range), }, { commandKey: "h3", @@ -93,7 +88,7 @@ export const getSlashCommandFilteredSections = description: "Small section heading.", searchTerms: ["subtitle", "small"], icon: , - command: ({ editor, range }) => toggleHeadingThree(editor, range), + command: ({ editor, range }) => toggleHeading(editor, 3, range), }, { commandKey: "h4", @@ -102,7 +97,7 @@ export const getSlashCommandFilteredSections = description: "Small section heading.", searchTerms: ["subtitle", "small"], icon: , - command: ({ editor, range }) => toggleHeadingFour(editor, range), + command: ({ editor, range }) => toggleHeading(editor, 4, range), }, { commandKey: "h5", @@ -111,7 +106,7 @@ export const getSlashCommandFilteredSections = description: "Small section heading.", searchTerms: ["subtitle", "small"], icon: , - command: ({ editor, range }) => toggleHeadingFive(editor, range), + command: ({ editor, range }) => toggleHeading(editor, 5, range), }, { commandKey: "h6", @@ -120,7 +115,7 @@ export const getSlashCommandFilteredSections = description: "Small section heading.", searchTerms: ["subtitle", "small"], icon: , - command: ({ editor, range }) => toggleHeadingSix(editor, range), + command: ({ editor, range }) => toggleHeading(editor, 6, range), }, { commandKey: "to-do-list", diff --git a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx index 4ecd3f8fa2..9d85266f2b 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx @@ -1,15 +1,16 @@ -import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react"; import { Editor } from "@tiptap/core"; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react"; // helpers import { DROPDOWN_NAVIGATION_KEYS, getNextValidIndex } from "@/helpers/tippy"; // components +import { ISlashCommandItem } from "@/types"; import { TSlashCommandSection } from "./command-items-list"; import { CommandMenuItem } from "./command-menu-item"; export type SlashCommandsMenuProps = { editor: Editor; items: TSlashCommandSection[]; - command: any; + command: (item: ISlashCommandItem) => void; }; export const SlashCommandsMenu = forwardRef((props: SlashCommandsMenuProps, ref) => { @@ -103,7 +104,9 @@ export const SlashCommandsMenu = forwardRef((props: SlashCommandsMenuProps, ref) sections, selectedIndex, }); - setSelectedIndex(newIndex); + if (newIndex) { + setSelectedIndex(newIndex); + } }, })); diff --git a/packages/editor/src/core/extensions/slash-commands/root.tsx b/packages/editor/src/core/extensions/slash-commands/root.tsx index c0c078a2dd..828149d502 100644 --- a/packages/editor/src/core/extensions/slash-commands/root.tsx +++ b/packages/editor/src/core/extensions/slash-commands/root.tsx @@ -1,7 +1,9 @@ import { Editor, Range, Extension } from "@tiptap/core"; import { ReactRenderer } from "@tiptap/react"; import Suggestion, { SuggestionOptions } from "@tiptap/suggestion"; -import tippy from "tippy.js"; +import tippy, { Instance } from "tippy.js"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // helpers import { CommandListInstance } from "@/helpers/tippy"; // types @@ -20,7 +22,7 @@ export type TSlashCommandAdditionalOption = ISlashCommandItem & { }; const Command = Extension.create({ - name: "slash-command", + name: CORE_EXTENSIONS.SLASH_COMMANDS, addOptions() { return { suggestion: { @@ -34,11 +36,11 @@ const Command = Extension.create({ const parentNode = selection.$from.node(selection.$from.depth); const blockType = parentNode.type.name; - if (blockType === "codeBlock") { + if (blockType === CORE_EXTENSIONS.CODE_BLOCK) { return false; } - if (editor.isActive("table")) { + if (editor.isActive(CORE_EXTENSIONS.TABLE)) { return false; } @@ -59,7 +61,7 @@ const Command = Extension.create({ const renderItems = () => { let component: ReactRenderer | null = null; - let popup: any | null = null; + let popup: Instance | null = null; return { onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { component = new ReactRenderer(SlashCommandsMenu, { diff --git a/packages/editor/src/core/extensions/table/table-cell/table-cell.ts b/packages/editor/src/core/extensions/table/table-cell.ts similarity index 91% rename from packages/editor/src/core/extensions/table/table-cell/table-cell.ts rename to packages/editor/src/core/extensions/table/table-cell.ts index 403bd3f02c..2ba06845a6 100644 --- a/packages/editor/src/core/extensions/table/table-cell/table-cell.ts +++ b/packages/editor/src/core/extensions/table/table-cell.ts @@ -1,11 +1,12 @@ import { mergeAttributes, Node } from "@tiptap/core"; - +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface TableCellOptions { HTMLAttributes: Record; } export const TableCell = Node.create({ - name: "tableCell", + name: CORE_EXTENSIONS.TABLE_CELL, addOptions() { return { diff --git a/packages/editor/src/core/extensions/table/table-cell/index.ts b/packages/editor/src/core/extensions/table/table-cell/index.ts deleted file mode 100644 index 68a25a9c3d..0000000000 --- a/packages/editor/src/core/extensions/table/table-cell/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TableCell } from "./table-cell"; diff --git a/packages/editor/src/core/extensions/table/table-header/table-header.ts b/packages/editor/src/core/extensions/table/table-header.ts similarity index 90% rename from packages/editor/src/core/extensions/table/table-header/table-header.ts rename to packages/editor/src/core/extensions/table/table-header.ts index bd994f467d..491889eefa 100644 --- a/packages/editor/src/core/extensions/table/table-header/table-header.ts +++ b/packages/editor/src/core/extensions/table/table-header.ts @@ -1,11 +1,12 @@ import { mergeAttributes, Node } from "@tiptap/core"; - +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface TableHeaderOptions { HTMLAttributes: Record; } export const TableHeader = Node.create({ - name: "tableHeader", + name: CORE_EXTENSIONS.TABLE_HEADER, addOptions() { return { diff --git a/packages/editor/src/core/extensions/table/table-header/index.ts b/packages/editor/src/core/extensions/table/table-header/index.ts deleted file mode 100644 index 290f37d0b7..0000000000 --- a/packages/editor/src/core/extensions/table/table-header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TableHeader } from "./table-header"; diff --git a/packages/editor/src/core/extensions/table/table-row/table-row.ts b/packages/editor/src/core/extensions/table/table-row.ts similarity index 88% rename from packages/editor/src/core/extensions/table/table-row/table-row.ts rename to packages/editor/src/core/extensions/table/table-row.ts index f961c05824..48f95a41c9 100644 --- a/packages/editor/src/core/extensions/table/table-row/table-row.ts +++ b/packages/editor/src/core/extensions/table/table-row.ts @@ -1,11 +1,13 @@ import { mergeAttributes, Node } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface TableRowOptions { HTMLAttributes: Record; } export const TableRow = Node.create({ - name: "tableRow", + name: CORE_EXTENSIONS.TABLE_ROW, addOptions() { return { diff --git a/packages/editor/src/core/extensions/table/table-row/index.ts b/packages/editor/src/core/extensions/table/table-row/index.ts deleted file mode 100644 index 24dafb7e01..0000000000 --- a/packages/editor/src/core/extensions/table/table-row/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TableRow } from "./table-row"; diff --git a/packages/editor/src/core/extensions/table/table/table-controls.ts b/packages/editor/src/core/extensions/table/table/table-controls.ts index 0529225796..d499b1b6a7 100644 --- a/packages/editor/src/core/extensions/table/table/table-controls.ts +++ b/packages/editor/src/core/extensions/table/table/table-controls.ts @@ -1,6 +1,8 @@ import { findParentNode } from "@tiptap/core"; -import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state"; +import { Plugin, PluginKey, TextSelection, Transaction } from "@tiptap/pm/state"; import { DecorationSet, Decoration } from "@tiptap/pm/view"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; const key = new PluginKey("tableControls"); @@ -17,16 +19,14 @@ export function tableControls() { }, props: { handleTripleClickOn(view, pos, node, nodePos, event, direct) { - if (node.type.name === 'tableCell') { + if (node.type.name === CORE_EXTENSIONS.TABLE_CELL) { event.preventDefault(); const $pos = view.state.doc.resolve(pos); const line = $pos.parent; const linePos = $pos.start(); const start = linePos; const end = linePos + line.nodeSize - 1; - const tr = view.state.tr.setSelection( - TextSelection.create(view.state.doc, start, end) - ); + const tr = view.state.tr.setSelection(TextSelection.create(view.state.doc, start, end)); view.dispatch(tr); return true; } @@ -52,12 +52,12 @@ export function tableControls() { if (!pos || pos.pos < 0 || pos.pos > view.state.doc.content.size) return; - const table = findParentNode((node) => node.type.name === "table")( - TextSelection.create(view.state.doc, pos.pos) - ); - const cell = findParentNode((node) => node.type.name === "tableCell" || node.type.name === "tableHeader")( + const table = findParentNode((node) => node.type.name === CORE_EXTENSIONS.TABLE)( TextSelection.create(view.state.doc, pos.pos) ); + const cell = findParentNode((node) => + [CORE_EXTENSIONS.TABLE_CELL, CORE_EXTENSIONS.TABLE_HEADER].includes(node.type.name as CORE_EXTENSIONS) + )(TextSelection.create(view.state.doc, pos.pos)); if (!table || !cell) return; @@ -112,7 +112,7 @@ class TableControlsState { }; } - apply(tr: any) { + apply(tr: Transaction) { const actions = tr.getMeta(key); if (actions?.setHoveredTable !== undefined) { diff --git a/packages/editor/src/core/extensions/table/table/table-view.tsx b/packages/editor/src/core/extensions/table/table/table-view.tsx index 2a48021267..f78d964ed4 100644 --- a/packages/editor/src/core/extensions/table/table/table-view.tsx +++ b/packages/editor/src/core/extensions/table/table/table-view.tsx @@ -1,12 +1,12 @@ -import { h } from "jsx-dom-cjs"; -import { Node as ProseMirrorNode, ResolvedPos } from "@tiptap/pm/model"; -import { Decoration, NodeView } from "@tiptap/pm/view"; -import tippy, { Instance, Props } from "tippy.js"; - import { Editor } from "@tiptap/core"; +import { Node as ProseMirrorNode, ResolvedPos } from "@tiptap/pm/model"; import { CellSelection, TableMap, updateColumnsOnResize } from "@tiptap/pm/tables"; - +import { Decoration, NodeView } from "@tiptap/pm/view"; +import { h } from "jsx-dom-cjs"; import { icons } from "src/core/extensions/table/table/icons"; +import tippy, { Instance, Props } from "tippy.js"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; type ToolboxItem = { label: string; @@ -30,10 +30,10 @@ export function updateColumns( if (!row) return; for (let i = 0, col = 0; i < row.childCount; i += 1) { - const { colspan, colwidth } = row.child(i).attrs; + const { colspan, colWidth } = row.child(i).attrs; for (let j = 0; j < colspan; j += 1, col += 1) { - const hasWidth = overrideCol === col ? overrideValue : colwidth && colwidth[j]; + const hasWidth = overrideCol === col ? overrideValue : colWidth && colWidth[j]; const cssWidth = hasWidth ? `${hasWidth}px` : ""; totalWidth += hasWidth || cellMinWidth; @@ -85,7 +85,7 @@ function setCellsBackgroundColor(editor: Editor, color: { backgroundColor: strin return editor .chain() .focus() - .updateAttributes("tableCell", { + .updateAttributes(CORE_EXTENSIONS.TABLE_CELL, { background: color.backgroundColor, textColor: color.textColor, }) @@ -104,12 +104,12 @@ function setTableRowBackgroundColor(editor: Editor, color: { backgroundColor: st // Find the depth of the table row node let rowDepth = hoveredCell.depth; - while (rowDepth > 0 && hoveredCell.node(rowDepth).type.name !== "tableRow") { + while (rowDepth > 0 && hoveredCell.node(rowDepth).type.name !== CORE_EXTENSIONS.TABLE_ROW) { rowDepth--; } // If we couldn't find a tableRow node, we can't set the background color - if (hoveredCell.node(rowDepth).type.name !== "tableRow") { + if (hoveredCell.node(rowDepth).type.name !== CORE_EXTENSIONS.TABLE_ROW) { return false; } diff --git a/packages/editor/src/core/extensions/table/table/table.ts b/packages/editor/src/core/extensions/table/table/table.ts index fd775d211e..4810706b39 100644 --- a/packages/editor/src/core/extensions/table/table/table.ts +++ b/packages/editor/src/core/extensions/table/table/table.ts @@ -19,11 +19,14 @@ import { toggleHeader, toggleHeaderCell, } from "@tiptap/pm/tables"; - -import { tableControls } from "@/extensions/table/table/table-controls"; -import { TableView } from "@/extensions/table/table/table-view"; -import { createTable } from "@/extensions/table/table/utilities/create-table"; -import { deleteTableWhenAllCellsSelected } from "@/extensions/table/table/utilities/delete-table-when-all-cells-selected"; +import { Decoration } from "@tiptap/pm/view"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// local imports +import { tableControls } from "./table-controls"; +import { TableView } from "./table-view"; +import { createTable } from "./utilities/create-table"; +import { deleteTableWhenAllCellsSelected } from "./utilities/delete-table-when-all-cells-selected"; import { insertLineAboveTableAction } from "./utilities/insert-line-above-table-action"; import { insertLineBelowTableAction } from "./utilities/insert-line-below-table-action"; @@ -38,7 +41,7 @@ export interface TableOptions { declare module "@tiptap/core" { interface Commands { - table: { + [CORE_EXTENSIONS.TABLE]: { insertTable: (options?: { rows?: number; cols?: number; @@ -79,7 +82,7 @@ declare module "@tiptap/core" { } export const Table = Node.create({ - name: "table", + name: CORE_EXTENSIONS.TABLE, addOptions() { return { @@ -219,8 +222,8 @@ export const Table = Node.create({ addKeyboardShortcuts() { return { Tab: () => { - if (this.editor.isActive("table")) { - if (this.editor.isActive("listItem") || this.editor.isActive("taskItem")) { + if (this.editor.isActive(CORE_EXTENSIONS.TABLE)) { + if (this.editor.isActive(CORE_EXTENSIONS.LIST_ITEM) || this.editor.isActive(CORE_EXTENSIONS.TASK_ITEM)) { return false; } if (this.editor.commands.goToNextCell()) { @@ -249,7 +252,7 @@ export const Table = Node.create({ return ({ editor, getPos, node, decorations }) => { const { cellMinWidth } = this.options; - return new TableView(node, cellMinWidth, decorations as any, editor, getPos as () => number); + return new TableView(node, cellMinWidth, decorations as Decoration[], editor, getPos as () => number); }; }, diff --git a/packages/editor/src/core/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts b/packages/editor/src/core/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts index 53388fbf23..5c84b8617d 100644 --- a/packages/editor/src/core/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts +++ b/packages/editor/src/core/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts @@ -1,4 +1,6 @@ import { findParentNodeClosestToPos, KeyboardShortcutCommand } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection"; @@ -10,14 +12,17 @@ export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({ edito } let cellCount = 0; - const table = findParentNodeClosestToPos(selection.ranges[0].$from, (node) => node.type.name === "table"); + const table = findParentNodeClosestToPos( + selection.ranges[0].$from, + (node) => node.type.name === CORE_EXTENSIONS.TABLE + ); table?.node.descendants((node) => { - if (node.type.name === "table") { + if (node.type.name === CORE_EXTENSIONS.TABLE) { return false; } - if (["tableCell", "tableHeader"].includes(node.type.name)) { + if ([CORE_EXTENSIONS.TABLE_CELL, CORE_EXTENSIONS.TABLE_HEADER].includes(node.type.name as CORE_EXTENSIONS)) { cellCount += 1; } }); diff --git a/packages/editor/src/core/extensions/table/table/utilities/insert-line-above-table-action.ts b/packages/editor/src/core/extensions/table/table/utilities/insert-line-above-table-action.ts index ca5ed3d7e8..35c2ee3c71 100644 --- a/packages/editor/src/core/extensions/table/table/utilities/insert-line-above-table-action.ts +++ b/packages/editor/src/core/extensions/table/table/utilities/insert-line-above-table-action.ts @@ -1,17 +1,19 @@ import { KeyboardShortcutCommand } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // helpers import { findParentNodeOfType } from "@/helpers/common"; export const insertLineAboveTableAction: KeyboardShortcutCommand = ({ editor }) => { // Check if the current selection or the closest node is a table - if (!editor.isActive("table")) return false; + if (!editor.isActive(CORE_EXTENSIONS.TABLE)) return false; try { // Get the current selection const { selection } = editor.state; // Find the table node and its position - const tableNode = findParentNodeOfType(selection, "table"); + const tableNode = findParentNodeOfType(selection, CORE_EXTENSIONS.TABLE); if (!tableNode) return false; const tablePos = tableNode.pos; @@ -39,7 +41,7 @@ export const insertLineAboveTableAction: KeyboardShortcutCommand = ({ editor }) const prevNode = editor.state.doc.nodeAt(prevNodePos - 1); - if (prevNode && prevNode.type.name === "paragraph") { + if (prevNode && prevNode.type.name === CORE_EXTENSIONS.PARAGRAPH) { // If there's a paragraph before the table, move the cursor to the end of that paragraph const endOfParagraphPos = tablePos - prevNode.nodeSize; editor.chain().setTextSelection(endOfParagraphPos).run(); diff --git a/packages/editor/src/core/extensions/table/table/utilities/insert-line-below-table-action.ts b/packages/editor/src/core/extensions/table/table/utilities/insert-line-below-table-action.ts index 7edca9f30d..6c26e22a2f 100644 --- a/packages/editor/src/core/extensions/table/table/utilities/insert-line-below-table-action.ts +++ b/packages/editor/src/core/extensions/table/table/utilities/insert-line-below-table-action.ts @@ -1,17 +1,19 @@ import { KeyboardShortcutCommand } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // helpers import { findParentNodeOfType } from "@/helpers/common"; export const insertLineBelowTableAction: KeyboardShortcutCommand = ({ editor }) => { // Check if the current selection or the closest node is a table - if (!editor.isActive("table")) return false; + if (!editor.isActive(CORE_EXTENSIONS.TABLE)) return false; try { // Get the current selection const { selection } = editor.state; // Find the table node and its position - const tableNode = findParentNodeOfType(selection, "table"); + const tableNode = findParentNodeOfType(selection, CORE_EXTENSIONS.TABLE); if (!tableNode) return false; const tablePos = tableNode.pos; @@ -31,13 +33,13 @@ export const insertLineBelowTableAction: KeyboardShortcutCommand = ({ editor }) // Check for an existing node immediately after the table const nextNode = editor.state.doc.nodeAt(nextNodePos); - if (nextNode && nextNode.type.name === "paragraph") { + if (nextNode && nextNode.type.name === CORE_EXTENSIONS.PARAGRAPH) { // If the next node is an paragraph, move the cursor there const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1; editor.chain().setTextSelection(endOfParagraphPos).run(); } else if (!nextNode) { // If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there - editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run(); + editor.chain().insertContentAt(nextNodePos, { type: CORE_EXTENSIONS.PARAGRAPH }).run(); editor .chain() .setTextSelection(nextNodePos + 1) diff --git a/packages/editor/src/core/extensions/typography/index.ts b/packages/editor/src/core/extensions/typography/index.ts index 6b736953b5..32ffea6a24 100644 --- a/packages/editor/src/core/extensions/typography/index.ts +++ b/packages/editor/src/core/extensions/typography/index.ts @@ -1,4 +1,6 @@ import { Extension, InputRule } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; import { TypographyOptions, emDash, @@ -23,7 +25,7 @@ import { } from "./inputRules"; export const CustomTypographyExtension = Extension.create({ - name: "typography", + name: CORE_EXTENSIONS.TYPOGRAPHY, addInputRules() { const rules: InputRule[] = []; diff --git a/packages/editor/src/core/extensions/utility.ts b/packages/editor/src/core/extensions/utility.ts new file mode 100644 index 0000000000..1d656de5a8 --- /dev/null +++ b/packages/editor/src/core/extensions/utility.ts @@ -0,0 +1,71 @@ +import { Extension } from "@tiptap/core"; +// prosemirror plugins +import codemark from "prosemirror-codemark"; +// helpers +import { restorePublicImages } from "@/helpers/image-helpers"; +// plugins +import { DropHandlerPlugin } from "@/plugins/drop"; +import { FilePlugins } from "@/plugins/file/root"; +import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard"; +// types +import { TFileHandler, TReadOnlyFileHandler } from "@/types"; + +declare module "@tiptap/core" { + interface Commands { + utility: { + updateAssetsUploadStatus: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void; + }; + } +} + +export interface UtilityExtensionStorage { + assetsUploadStatus: TFileHandler["assetsUploadStatus"]; + uploadInProgress: boolean; +} + +type Props = { + fileHandler: TFileHandler | TReadOnlyFileHandler; + isEditable: boolean; +}; + +export const UtilityExtension = (props: Props) => { + const { fileHandler, isEditable } = props; + const { restore: restoreImageFn } = fileHandler; + + return Extension.create, UtilityExtensionStorage>({ + name: "utility", + priority: 1000, + + addProseMirrorPlugins() { + return [ + ...FilePlugins({ + editor: this.editor, + isEditable, + fileHandler, + }), + ...codemark({ markType: this.editor.schema.marks.code }), + MarkdownClipboardPlugin(this.editor), + DropHandlerPlugin(this.editor), + ]; + }, + + onCreate() { + restorePublicImages(this.editor, restoreImageFn); + }, + + addStorage() { + return { + assetsUploadStatus: isEditable && "assetsUploadStatus" in fileHandler ? fileHandler.assetsUploadStatus : {}, + uploadInProgress: false, + }; + }, + + addCommands() { + return { + updateAssetsUploadStatus: (updatedStatus) => () => { + this.storage.assetsUploadStatus = updatedStatus; + }, + }; + }, + }); +}; diff --git a/packages/editor/src/core/extensions/work-item-embed/extension-config.ts b/packages/editor/src/core/extensions/work-item-embed/extension-config.ts new file mode 100644 index 0000000000..0ea25c770d --- /dev/null +++ b/packages/editor/src/core/extensions/work-item-embed/extension-config.ts @@ -0,0 +1,43 @@ +import { mergeAttributes, Node } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; + +export const WorkItemEmbedExtensionConfig = Node.create({ + name: CORE_EXTENSIONS.WORK_ITEM_EMBED, + group: "block", + atom: true, + selectable: true, + draggable: true, + + addAttributes() { + return { + entity_identifier: { + default: undefined, + }, + project_identifier: { + default: undefined, + }, + workspace_identifier: { + default: undefined, + }, + id: { + default: undefined, + }, + entity_name: { + default: undefined, + }, + }; + }, + + parseHTML() { + return [ + { + tag: "issue-embed-component", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ["issue-embed-component", mergeAttributes(HTMLAttributes)]; + }, +}); diff --git a/packages/editor/src/core/extensions/work-item-embed/extension.tsx b/packages/editor/src/core/extensions/work-item-embed/extension.tsx new file mode 100644 index 0000000000..64e655a408 --- /dev/null +++ b/packages/editor/src/core/extensions/work-item-embed/extension.tsx @@ -0,0 +1,30 @@ +import { ReactNodeViewRenderer, NodeViewWrapper } from "@tiptap/react"; +// local imports +import { WorkItemEmbedExtensionConfig } from "./extension-config"; + +type Props = { + widgetCallback: ({ + issueId, + projectId, + workspaceSlug, + }: { + issueId: string; + projectId: string | undefined; + workspaceSlug: string | undefined; + }) => React.ReactNode; +}; + +export const WorkItemEmbedExtension = (props: Props) => + WorkItemEmbedExtensionConfig.extend({ + addNodeView() { + return ReactNodeViewRenderer((issueProps: any) => ( + + {props.widgetCallback({ + issueId: issueProps.node.attrs.entity_identifier, + projectId: issueProps.node.attrs.project_identifier, + workspaceSlug: issueProps.node.attrs.workspace_identifier, + })} + + )); + }, + }); diff --git a/packages/editor/src/core/extensions/work-item-embed/index.ts b/packages/editor/src/core/extensions/work-item-embed/index.ts new file mode 100644 index 0000000000..2ce32da8ba --- /dev/null +++ b/packages/editor/src/core/extensions/work-item-embed/index.ts @@ -0,0 +1 @@ +export * from "./extension"; diff --git a/packages/editor/src/core/helpers/common.ts b/packages/editor/src/core/helpers/common.ts index 974b111d09..e694e1e853 100644 --- a/packages/editor/src/core/helpers/common.ts +++ b/packages/editor/src/core/helpers/common.ts @@ -1,6 +1,8 @@ import { EditorState, Selection } from "@tiptap/pm/state"; -// plane utils +// plane imports import { cn } from "@plane/utils"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; interface EditorClassNames { noBorder?: boolean; @@ -67,7 +69,7 @@ export const isValidHttpUrl = (string: string): { isValid: boolean; url: string url: string, }; } - } catch (_) { + } catch { // Original string wasn't a valid URL - that's okay, we'll try with https } @@ -79,7 +81,7 @@ export const isValidHttpUrl = (string: string): { isValid: boolean; url: string isValid: true, url: urlWithHttps, }; - } catch (_) { + } catch { return { isValid: false, url: string, @@ -91,7 +93,7 @@ export const getParagraphCount = (editorState: EditorState | undefined) => { if (!editorState) return 0; let paragraphCount = 0; editorState.doc.descendants((node) => { - if (node.type.name === "paragraph" && node.content.size > 0) paragraphCount++; + if (node.type.name === CORE_EXTENSIONS.PARAGRAPH && node.content.size > 0) paragraphCount++; }); return paragraphCount; }; diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index e8c98ada57..5fa15cb08d 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -1,4 +1,6 @@ import { Editor, Range } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { InsertImageComponentProps } from "@/extensions"; import { replaceCodeWithText } from "@/extensions/code/utils/replace-code-block-with-text"; @@ -6,44 +8,14 @@ import { replaceCodeWithText } from "@/extensions/code/utils/replace-code-block- import { findTableAncestor } from "@/helpers/common"; export const setText = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("paragraph").run(); - else editor.chain().focus().setNode("paragraph").run(); + if (range) editor.chain().focus().deleteRange(range).setNode(CORE_EXTENSIONS.PARAGRAPH).run(); + else editor.chain().focus().setNode(CORE_EXTENSIONS.PARAGRAPH).run(); }; -export const toggleHeadingOne = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run(); +export const toggleHeading = (editor: Editor, level: 1 | 2 | 3 | 4 | 5 | 6, range?: Range) => { + if (range) editor.chain().focus().deleteRange(range).setNode(CORE_EXTENSIONS.HEADING, { level }).run(); // @ts-expect-error tiptap types are incorrect - else editor.chain().focus().toggleHeading({ level: 1 }).run(); -}; - -export const toggleHeadingTwo = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run(); - // @ts-expect-error tiptap types are incorrect - else editor.chain().focus().toggleHeading({ level: 2 }).run(); -}; - -export const toggleHeadingThree = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run(); - // @ts-expect-error tiptap types are incorrect - else editor.chain().focus().toggleHeading({ level: 3 }).run(); -}; - -export const toggleHeadingFour = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 4 }).run(); - // @ts-expect-error tiptap types are incorrect - else editor.chain().focus().toggleHeading({ level: 4 }).run(); -}; - -export const toggleHeadingFive = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 5 }).run(); - // @ts-expect-error tiptap types are incorrect - else editor.chain().focus().toggleHeading({ level: 5 }).run(); -}; - -export const toggleHeadingSix = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 6 }).run(); - // @ts-expect-error tiptap types are incorrect - else editor.chain().focus().toggleHeading({ level: 6 }).run(); + else editor.chain().focus().toggleHeading({ level }).run(); }; export const toggleBold = (editor: Editor, range?: Range) => { @@ -68,7 +40,7 @@ export const toggleUnderline = (editor: Editor, range?: Range) => { export const toggleCodeBlock = (editor: Editor, range?: Range) => { try { // if it's a code block, replace it with the code with paragraphs - if (editor.isActive("codeBlock")) { + if (editor.isActive(CORE_EXTENSIONS.CODE_BLOCK)) { replaceCodeWithText(editor); return; } @@ -77,12 +49,12 @@ export const toggleCodeBlock = (editor: Editor, range?: Range) => { const text = editor.state.doc.textBetween(from, to, "\n"); const isMultiline = text.includes("\n"); - // if the selection is not a range i.e. empty, then simply convert it into a code block + // if the selection is not a range i.e. empty, then simply convert it into a codeBlock if (editor.state.selection.empty) { editor.chain().focus().toggleCodeBlock().run(); } else if (isMultiline) { // if the selection is multiline, then also replace the text content with - // a code block + // a codeBlock editor.chain().focus().deleteRange({ from, to }).insertContentAt(from, `\`\`\`\n${text}\n\`\`\``).run(); } else { // if the selection is single line, then simply convert it into inline diff --git a/packages/editor/src/core/helpers/file.ts b/packages/editor/src/core/helpers/file.ts index f2c9968f00..33d3c7d781 100644 --- a/packages/editor/src/core/helpers/file.ts +++ b/packages/editor/src/core/helpers/file.ts @@ -1,24 +1,34 @@ +export enum EFileError { + INVALID_FILE_TYPE = "INVALID_FILE_TYPE", + FILE_SIZE_TOO_LARGE = "FILE_SIZE_TOO_LARGE", + NO_FILE_SELECTED = "NO_FILE_SELECTED", +} + type TArgs = { acceptedMimeTypes: string[]; file: File; maxFileSize: number; + onError: (error: EFileError, message: string) => void; }; export const isFileValid = (args: TArgs): boolean => { - const { acceptedMimeTypes, file, maxFileSize } = args; + const { acceptedMimeTypes, file, maxFileSize, onError } = args; if (!file) { - alert("No file selected. Please select a file to upload."); + onError(EFileError.NO_FILE_SELECTED, "No file selected. Please select a file to upload."); return false; } if (!acceptedMimeTypes.includes(file.type)) { - alert("Invalid file type. Please select a JPEG, JPG, PNG, WEBP or GIF file."); + onError(EFileError.INVALID_FILE_TYPE, "Invalid file type."); return false; } if (file.size > maxFileSize) { - alert(`File size too large. Please select a file smaller than ${maxFileSize / 1024 / 1024}MB.`); + onError( + EFileError.FILE_SIZE_TOO_LARGE, + `File size too large. Please select a file smaller than ${maxFileSize / 1024 / 1024}MB.` + ); return false; } diff --git a/packages/editor/src/core/helpers/image-helpers.ts b/packages/editor/src/core/helpers/image-helpers.ts new file mode 100644 index 0000000000..9fcb877f9d --- /dev/null +++ b/packages/editor/src/core/helpers/image-helpers.ts @@ -0,0 +1,32 @@ +import { Editor } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// types +import { TFileHandler } from "@/types"; + +/** + * Finds all public image nodes in the document and restores them using the provided restore function + * + * Never remove this onCreate hook, it's a hack to restore old public + * images, since they don't give error if they've been deleted as they are + * rendered directly from image source instead of going through the + * apiserver + */ +export const restorePublicImages = (editor: Editor, restoreImageFn: TFileHandler["restore"]) => { + const imageSources = new Set(); + editor.state.doc.descendants((node) => { + if ([CORE_EXTENSIONS.IMAGE, CORE_EXTENSIONS.CUSTOM_IMAGE].includes(node.type.name as CORE_EXTENSIONS)) { + if (!node.attrs.src?.startsWith("http")) return; + + imageSources.add(node.attrs.src); + } + }); + + imageSources.forEach(async (src) => { + try { + await restoreImageFn(src); + } catch (error) { + console.error("Error restoring image: ", error); + } + }); +}; diff --git a/packages/editor/src/core/helpers/insert-empty-paragraph-at-node-boundary.ts b/packages/editor/src/core/helpers/insert-empty-paragraph-at-node-boundary.ts index ffad88d4e7..b9449b494d 100644 --- a/packages/editor/src/core/helpers/insert-empty-paragraph-at-node-boundary.ts +++ b/packages/editor/src/core/helpers/insert-empty-paragraph-at-node-boundary.ts @@ -1,5 +1,7 @@ import { KeyboardShortcutCommand } from "@tiptap/core"; import { Node as ProseMirrorNode } from "@tiptap/pm/model"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; type Direction = "up" | "down"; @@ -39,13 +41,13 @@ export const insertEmptyParagraphAtNodeBoundaries: ( if (insertPosUp === 0) { // If at the very start of the document, insert a new paragraph at the start - editor.chain().insertContentAt(insertPosUp, { type: "paragraph" }).run(); + editor.chain().insertContentAt(insertPosUp, { type: CORE_EXTENSIONS.PARAGRAPH }).run(); editor.chain().setTextSelection(insertPosUp).run(); // Set the cursor to the new paragraph } else { // Otherwise, check the node immediately before the target node const prevNode = doc.nodeAt(insertPosUp - 1); - if (prevNode && prevNode.type.name === "paragraph") { + if (prevNode && prevNode.type.name === CORE_EXTENSIONS.PARAGRAPH) { // If the previous node is a paragraph, move the cursor there editor .chain() @@ -67,13 +69,13 @@ export const insertEmptyParagraphAtNodeBoundaries: ( // Check the node immediately after the target node const nextNode = doc.nodeAt(insertPosDown); - if (nextNode && nextNode.type.name === "paragraph") { + if (nextNode && nextNode.type.name === CORE_EXTENSIONS.PARAGRAPH) { // If the next node is a paragraph, move the cursor to the end of it const endOfParagraphPos = insertPosDown + nextNode.nodeSize - 1; editor.chain().setTextSelection(endOfParagraphPos).run(); } else if (!nextNode) { // If there is no next node (end of document), insert a new paragraph - editor.chain().insertContentAt(insertPosDown, { type: "paragraph" }).run(); + editor.chain().insertContentAt(insertPosDown, { type: CORE_EXTENSIONS.PARAGRAPH }).run(); editor .chain() .setTextSelection(insertPosDown + 1) diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 4abf7d6d1f..8677b29edb 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -1,6 +1,6 @@ -import { useEffect, useMemo, useState } from "react"; import { HocuspocusProvider } from "@hocuspocus/provider"; import Collaboration from "@tiptap/extension-collaboration"; +import { useEffect, useMemo, useState } from "react"; import { IndexeddbPersistence } from "y-indexeddb"; // extensions import { HeadingListExtension, SideMenuExtension } from "@/extensions"; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index cf9d04d83e..a0cd739157 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -6,10 +6,13 @@ import { useImperativeHandle, MutableRefObject, useEffect } from "react"; import * as Y from "yjs"; // components import { getEditorMenuItems } from "@/components/menus"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { CoreEditorExtensions } from "@/extensions"; // helpers import { getParagraphCount } from "@/helpers/common"; +import { getExtensionStorage } from "@/helpers/get-extension-storage"; import { insertContentAtSavedSelection } from "@/helpers/insert-content-at-cursor-position"; import { IMarking, scrollSummary, scrollToNodeViaDOMCoordinates } from "@/helpers/scroll-to-node"; // props @@ -23,6 +26,7 @@ import type { TExtensions, TMentionHandler, } from "@/types"; +import { CORE_EDITOR_META } from "@/constants/meta"; export interface CustomEditorProps { editable: boolean; @@ -111,16 +115,19 @@ export const useEditor = (props: CustomEditorProps) => { // value is null when intentionally passed where syncing is not yet // supported and value is undefined when the data from swr is not populated if (value == null) return; - if (editor && !editor.isDestroyed && !editor.storage.imageComponent?.uploadInProgress) { - try { - editor.commands.setContent(value, false, { preserveWhitespace: "full" }); - if (editor.state.selection) { - const docLength = editor.state.doc.content.size; - const relativePosition = Math.min(editor.state.selection.from, docLength - 1); - editor.commands.setTextSelection(relativePosition); + if (editor) { + const isUploadInProgress = getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress; + if (!editor.isDestroyed && !isUploadInProgress) { + try { + editor.commands.setContent(value, false, { preserveWhitespace: "full" }); + if (editor.state.selection) { + const docLength = editor.state.doc.content.size; + const relativePosition = Math.min(editor.state.selection.from, docLength - 1); + editor.commands.setTextSelection(relativePosition); + } + } catch (error) { + console.error("Error syncing editor content with external value:", error); } - } catch (error) { - console.error("Error syncing editor content with external value:", error); } } }, [editor, value, id]); @@ -143,7 +150,7 @@ export const useEditor = (props: CustomEditorProps) => { }, getCurrentCursorPosition: () => editor?.state.selection.from, clearEditor: (emitUpdate = false) => { - editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run(); + editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); }, setEditorValue: (content: string, emitUpdate = false) => { editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" }); @@ -179,7 +186,10 @@ export const useEditor = (props: CustomEditorProps) => { onHeadingChange: (callback: (headings: IMarking[]) => void) => { // Subscribe to update event emitted from headers extension editor?.on("update", () => { - callback(editor?.storage.headingList.headings); + const headings = getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings; + if (headings) { + callback(headings); + } }); // Return a function to unsubscribe to the continuous transactions of // the editor on unmounting the component that has subscribed to this @@ -188,7 +198,7 @@ export const useEditor = (props: CustomEditorProps) => { editor?.off("update"); }; }, - getHeadings: () => editor?.storage.headingList.headings, + getHeadings: () => (editor ? getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings : []), onStateChange: (callback: () => void) => { // Subscribe to editor state changes editor?.on("transaction", () => { @@ -221,7 +231,8 @@ export const useEditor = (props: CustomEditorProps) => { if (!editor) return; scrollSummary(editor, marking); }, - isEditorReadyToDiscard: () => editor?.storage.imageComponent?.uploadInProgress === false, + isEditorReadyToDiscard: () => + !!editor && getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress === false, setFocusAtPosition: (position: number) => { if (!editor || editor.isDestroyed) { console.error("Editor reference is not available or has been destroyed."); @@ -232,7 +243,7 @@ export const useEditor = (props: CustomEditorProps) => { const safePosition = Math.max(0, Math.min(position, docSize)); editor .chain() - .insertContentAt(safePosition, [{ type: "paragraph" }]) + .insertContentAt(safePosition, [{ type: CORE_EXTENSIONS.PARAGRAPH }]) .focus() .run(); } catch (error) { diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts index b707824f26..e40c159134 100644 --- a/packages/editor/src/core/hooks/use-file-upload.ts +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -1,9 +1,9 @@ import { Editor } from "@tiptap/core"; import { DragEvent, useCallback, useEffect, useState } from "react"; -// extensions -import { insertFilesSafely } from "@/extensions/drop"; +// helpers +import { EFileError, isFileValid } from "@/helpers/file"; // plugins -import { isFileValid } from "@/helpers/file"; +import { insertFilesSafely } from "@/plugins/drop"; // types import { TEditorCommands } from "@/types"; @@ -13,12 +13,20 @@ type TUploaderArgs = { handleProgressStatus?: (isUploading: boolean) => void; loadFileFromFileSystem?: (file: string) => void; maxFileSize: number; + onInvalidFile: (error: EFileError, message: string) => void; onUpload: (url: string, file: File) => void; }; export const useUploader = (args: TUploaderArgs) => { - const { acceptedMimeTypes, editorCommand, handleProgressStatus, loadFileFromFileSystem, maxFileSize, onUpload } = - args; + const { + acceptedMimeTypes, + editorCommand, + handleProgressStatus, + loadFileFromFileSystem, + maxFileSize, + onInvalidFile, + onUpload, + } = args; // states const [isUploading, setIsUploading] = useState(false); @@ -30,6 +38,7 @@ export const useUploader = (args: TUploaderArgs) => { acceptedMimeTypes, file, maxFileSize, + onError: onInvalidFile, }); if (!isValid) { handleProgressStatus?.(false); @@ -75,13 +84,14 @@ type TDropzoneArgs = { acceptedMimeTypes: string[]; editor: Editor; maxFileSize: number; + onInvalidFile: (error: EFileError, message: string) => void; pos: number; type: Extract; uploader: (file: File) => Promise; }; export const useDropZone = (args: TDropzoneArgs) => { - const { acceptedMimeTypes, editor, maxFileSize, pos, type, uploader } = args; + const { acceptedMimeTypes, editor, maxFileSize, onInvalidFile, pos, type, uploader } = args; // states const [isDragging, setIsDragging] = useState(false); const [draggedInside, setDraggedInside] = useState(false); @@ -117,12 +127,13 @@ export const useDropZone = (args: TDropzoneArgs) => { editor, filesList, maxFileSize, + onInvalidFile, pos, type, uploader, }); }, - [acceptedMimeTypes, editor, maxFileSize, pos, type, uploader] + [acceptedMimeTypes, editor, maxFileSize, onInvalidFile, pos, type, uploader] ); const onDragEnter = useCallback(() => setDraggedInside(true), []); const onDragLeave = useCallback(() => setDraggedInside(false), []); @@ -141,6 +152,7 @@ type TMultipleFileArgs = { editor: Editor; filesList: FileList; maxFileSize: number; + onInvalidFile: (error: EFileError, message: string) => void; pos: number; type: Extract; uploader: (file: File) => Promise; @@ -148,7 +160,7 @@ type TMultipleFileArgs = { // Upload the first file and insert the remaining ones for uploading multiple files export const uploadFirstFileAndInsertRemaining = async (args: TMultipleFileArgs) => { - const { acceptedMimeTypes, editor, filesList, maxFileSize, pos, type, uploader } = args; + const { acceptedMimeTypes, editor, filesList, maxFileSize, onInvalidFile, pos, type, uploader } = args; const filteredFiles: File[] = []; for (let i = 0; i < filesList.length; i += 1) { const file = filesList.item(i); @@ -158,6 +170,7 @@ export const uploadFirstFileAndInsertRemaining = async (args: TMultipleFileArgs) acceptedMimeTypes, file, maxFileSize, + onError: onInvalidFile, }) ) { filteredFiles.push(file); diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index b50b56b02d..5bd731d5f1 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -12,6 +12,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; import { CoreReadOnlyEditorProps } from "@/props"; // types import type { EditorReadOnlyRefApi, TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types"; +import { CORE_EDITOR_META } from "@/constants/meta"; interface CustomReadOnlyEditorProps { disabledExtensions: TExtensions[]; @@ -75,7 +76,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { useImperativeHandle(forwardedRef, () => ({ clearEditor: (emitUpdate = false) => { - editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run(); + editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); }, setEditorValue: (content: string, emitUpdate = false) => { editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" }); diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts index f9a60a48c1..aa00fa32d9 100644 --- a/packages/editor/src/core/plugins/drag-handle.ts +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -2,6 +2,8 @@ import { Fragment, Slice, Node, Schema } from "@tiptap/pm/model"; import { NodeSelection } from "@tiptap/pm/state"; // @ts-expect-error __serializeForClipboard's is not exported import { __serializeForClipboard, EditorView } from "@tiptap/pm/view"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions"; @@ -132,7 +134,7 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp let listType = ""; let isDragging = false; let lastClientY = 0; - let scrollAnimationFrame = null; + let scrollAnimationFrame: number | null = null; let isDraggedOutsideWindow: "top" | "bottom" | boolean = false; let isMouseInsideWhileDragging = false; let currentScrollSpeed = 0; @@ -142,8 +144,10 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp }; const handleDragStart = (event: DragEvent, view: EditorView) => { - const { listType: listTypeFromDragStart } = handleNodeSelection(event, view, true, options); - listType = listTypeFromDragStart; + const { listType: listTypeFromDragStart } = handleNodeSelection(event, view, true, options) ?? {}; + if (listTypeFromDragStart) { + listType = listTypeFromDragStart; + } isDragging = true; lastClientY = event.clientY; scroll(); @@ -297,7 +301,7 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp // Traverse up the document tree to find if we're inside a list item for (let i = resolvedPos.depth; i > 0; i--) { - if (resolvedPos.node(i).type.name === "listItem") { + if (resolvedPos.node(i).type.name === CORE_EXTENSIONS.LIST_ITEM) { isDroppedInsideList = true; dropDepth = i; break; @@ -305,7 +309,7 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp } // Handle nested list items and task items - if (droppedNode.type.name === "listItem") { + if (droppedNode.type.name === CORE_EXTENSIONS.LIST_ITEM) { let slice = view.state.selection.content(); let newFragment = slice.content; @@ -348,8 +352,8 @@ function flattenListStructure(fragment: Fragment, schema: Schema): Fragment { (node.content.firstChild.type === schema.nodes.bulletList || node.content.firstChild.type === schema.nodes.orderedList) ) { - const sublist = node.content.firstChild; - const flattened = flattenListStructure(sublist.content, schema); + const subList = node.content.firstChild; + const flattened = flattenListStructure(subList.content, schema); flattened.forEach((subNode) => result.push(subNode)); } } @@ -376,7 +380,7 @@ const handleNodeSelection = ( let draggedNodePos = nodePosAtDOM(node, view, options); if (draggedNodePos == null || draggedNodePos < 0) return; - // Handle blockquotes separately + // Handle blockquote separately if (node.matches("blockquote")) { draggedNodePos = nodePosAtDOMForBlockQuotes(node, view); if (draggedNodePos === null || draggedNodePos === undefined) return; @@ -385,7 +389,10 @@ const handleNodeSelection = ( const $pos = view.state.doc.resolve(draggedNodePos); // If it's a nested list item or task item, move up to the item level - if (($pos.parent.type.name === "listItem" || $pos.parent.type.name === "taskItem") && $pos.depth > 1) { + if ( + [CORE_EXTENSIONS.LIST_ITEM, CORE_EXTENSIONS.TASK_ITEM].includes($pos.parent.type.name as CORE_EXTENSIONS) && + $pos.depth > 1 + ) { draggedNodePos = $pos.before($pos.depth); } } @@ -403,14 +410,16 @@ const handleNodeSelection = ( // Additional logic for drag start if (event instanceof DragEvent && !event.dataTransfer) return; - if (nodeSelection.node.type.name === "listItem" || nodeSelection.node.type.name === "taskItem") { + if ( + [CORE_EXTENSIONS.LIST_ITEM, CORE_EXTENSIONS.TASK_ITEM].includes(nodeSelection.node.type.name as CORE_EXTENSIONS) + ) { listType = node.closest("ol, ul")?.tagName || ""; } const slice = view.state.selection.content(); const { dom, text } = __serializeForClipboard(view, slice); - if (event instanceof DragEvent) { + if (event instanceof DragEvent && event.dataTransfer) { event.dataTransfer.clearData(); event.dataTransfer.setData("text/html", dom.innerHTML); event.dataTransfer.setData("text/plain", text); diff --git a/packages/editor/src/core/plugins/drop.ts b/packages/editor/src/core/plugins/drop.ts new file mode 100644 index 0000000000..a0bb65779f --- /dev/null +++ b/packages/editor/src/core/plugins/drop.ts @@ -0,0 +1,118 @@ +import { Editor } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +// constants +import { ACCEPTED_ATTACHMENT_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; +// types +import { TEditorCommands } from "@/types"; + +export const DropHandlerPlugin = (editor: Editor): Plugin => + new Plugin({ + key: new PluginKey("drop-handler-plugin"), + props: { + handlePaste: (view, event) => { + if ( + editor.isEditable && + event.clipboardData && + event.clipboardData.files && + event.clipboardData.files.length > 0 + ) { + event.preventDefault(); + const files = Array.from(event.clipboardData.files); + const acceptedFiles = files.filter( + (f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type) + ); + + if (acceptedFiles.length) { + const pos = view.state.selection.from; + insertFilesSafely({ + editor, + files: acceptedFiles, + initialPos: pos, + event: "drop", + }); + } + return true; + } + return false; + }, + handleDrop: (view, event, _slice, moved) => { + if ( + editor.isEditable && + !moved && + event.dataTransfer && + event.dataTransfer.files && + event.dataTransfer.files.length > 0 + ) { + event.preventDefault(); + const files = Array.from(event.dataTransfer.files); + const acceptedFiles = files.filter( + (f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type) + ); + + if (acceptedFiles.length) { + const coordinates = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + + if (coordinates) { + const pos = coordinates.pos; + insertFilesSafely({ + editor, + files: acceptedFiles, + initialPos: pos, + event: "drop", + }); + } + return true; + } + } + return false; + }, + }, + }); + +type InsertFilesSafelyArgs = { + editor: Editor; + event: "insert" | "drop"; + files: File[]; + initialPos: number; + type?: Extract; +}; + +export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => { + const { editor, event, files, initialPos, type } = args; + let pos = initialPos; + + for (const file of files) { + // safe insertion + const docSize = editor.state.doc.content.size; + pos = Math.min(pos, docSize); + + let fileType: "image" | "attachment" | null = null; + + try { + if (type) { + if (["image", "attachment"].includes(type)) fileType = type; + else throw new Error("Wrong file type passed"); + } else { + if (ACCEPTED_IMAGE_MIME_TYPES.includes(file.type)) fileType = "image"; + else if (ACCEPTED_ATTACHMENT_MIME_TYPES.includes(file.type)) fileType = "attachment"; + } + // insert file depending on the type at the current position + if (fileType === "image") { + editor.commands.insertImageComponent({ + file, + pos, + event, + }); + } else if (fileType === "attachment") { + } + } catch (error) { + console.error(`Error while ${event}ing file:`, error); + } + + // Move to the next position + pos += 1; + } +}; diff --git a/packages/editor/src/core/plugins/file/delete.ts b/packages/editor/src/core/plugins/file/delete.ts new file mode 100644 index 0000000000..b77841c229 --- /dev/null +++ b/packages/editor/src/core/plugins/file/delete.ts @@ -0,0 +1,67 @@ +import { Editor } from "@tiptap/core"; +import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; +// plane editor imports +import { NODE_FILE_MAP } from "@/plane-editor/constants/utility"; +// types +import { TFileHandler } from "@/types"; +// local imports +import { TFileNode } from "./types"; + +const DELETE_PLUGIN_KEY = new PluginKey("delete-utility"); + +export const TrackFileDeletionPlugin = (editor: Editor, deleteHandler: TFileHandler["delete"]): Plugin => + new Plugin({ + key: DELETE_PLUGIN_KEY, + appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { + const newFileSources: { + [nodeType: string]: Set | undefined; + } = {}; + if (!transactions.some((tr) => tr.docChanged)) return null; + + newState.doc.descendants((node) => { + const nodeType = node.type.name; + const nodeFileSetDetails = NODE_FILE_MAP[nodeType]; + if (nodeFileSetDetails) { + if (newFileSources[nodeType]) { + newFileSources[nodeType].add(node.attrs.src); + } else { + newFileSources[nodeType] = new Set([node.attrs.src]); + } + } + }); + + transactions.forEach((transaction) => { + // if the transaction has meta of skipFileDeletion set to true, then return (like while clearing the editor content programmatically) + if (transaction.getMeta("skipFileDeletion")) return; + + const removedFiles: TFileNode[] = []; + + // iterate through all the nodes in the old state + oldState.doc.descendants((node) => { + const nodeType = node.type.name; + const isAValidNode = NODE_FILE_MAP[nodeType]; + // if the node doesn't match, then return as no point in checking + if (!isAValidNode) return; + // Check if the node has been deleted or replaced + if (!newFileSources[nodeType]?.has(node.attrs.src)) { + removedFiles.push(node as TFileNode); + } + }); + + removedFiles.forEach(async (node) => { + const nodeType = node.type.name; + const src = node.attrs.src; + const nodeFileSetDetails = NODE_FILE_MAP[nodeType]; + if (!nodeFileSetDetails || !src) return; + try { + editor.storage[nodeType][nodeFileSetDetails.fileSetName]?.set(src, true); + await deleteHandler(src); + } catch (error) { + console.error("Error deleting file via delete utility plugin:", error); + } + }); + }); + + return null; + }, + }); diff --git a/packages/editor/src/core/plugins/file/restore.ts b/packages/editor/src/core/plugins/file/restore.ts new file mode 100644 index 0000000000..04a4c295cc --- /dev/null +++ b/packages/editor/src/core/plugins/file/restore.ts @@ -0,0 +1,72 @@ +import { Editor } from "@tiptap/core"; +import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// plane editor imports +import { NODE_FILE_MAP } from "@/plane-editor/constants/utility"; +// types +import { TFileHandler } from "@/types"; +// local imports +import { TFileNode } from "./types"; + +const RESTORE_PLUGIN_KEY = new PluginKey("restore-utility"); + +export const TrackFileRestorationPlugin = (editor: Editor, restoreHandler: TFileHandler["restore"]): Plugin => + new Plugin({ + key: RESTORE_PLUGIN_KEY, + appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { + if (!transactions.some((tr) => tr.docChanged)) return null; + + const oldFileSources: { + [key: string]: Set | undefined; + } = {}; + oldState.doc.descendants((node) => { + const nodeType = node.type.name; + const nodeFileSetDetails = NODE_FILE_MAP[nodeType]; + if (nodeFileSetDetails) { + if (oldFileSources[nodeType]) { + oldFileSources[nodeType].add(node.attrs.src); + } else { + oldFileSources[nodeType] = new Set([node.attrs.src]); + } + } + }); + + transactions.forEach(() => { + const addedFiles: TFileNode[] = []; + + newState.doc.descendants((node, pos) => { + const nodeType = node.type.name; + const isAValidNode = NODE_FILE_MAP[nodeType]; + // if the node doesn't match, then return as no point in checking + if (!isAValidNode) return; + if (pos < 0 || pos > newState.doc.content.size) return; + if (oldFileSources[nodeType]?.has(node.attrs.src)) return; + // if the src is just a id (private bucket), then we don't need to handle restore from here but + // only while it fails to load + if (nodeType === CORE_EXTENSIONS.CUSTOM_IMAGE && !node.attrs.src?.startsWith("http")) return; + addedFiles.push(node as TFileNode); + }); + + addedFiles.forEach(async (node) => { + const nodeType = node.type.name; + const src = node.attrs.src; + const nodeFileSetDetails = NODE_FILE_MAP[nodeType]; + const extensionFileSetStorage = editor.storage[nodeType]?.[nodeFileSetDetails.fileSetName]; + const wasDeleted = extensionFileSetStorage?.get(src); + if (!nodeFileSetDetails || !src) return; + if (wasDeleted === undefined) { + extensionFileSetStorage?.set(src, false); + } else if (wasDeleted === true) { + try { + await restoreHandler(src); + extensionFileSetStorage?.set(src, false); + } catch (error) { + console.error("Error restoring file via restore utility plugin:", error); + } + } + }); + }); + return null; + }, + }); diff --git a/packages/editor/src/core/plugins/file/root.ts b/packages/editor/src/core/plugins/file/root.ts new file mode 100644 index 0000000000..693ac6964b --- /dev/null +++ b/packages/editor/src/core/plugins/file/root.ts @@ -0,0 +1,22 @@ +import { Editor } from "@tiptap/core"; +import { Plugin } from "@tiptap/pm/state"; +// types +import { TFileHandler, TReadOnlyFileHandler } from "@/types"; +// local imports +import { TrackFileDeletionPlugin } from "./delete"; +import { TrackFileRestorationPlugin } from "./restore"; + +type TArgs = { + editor: Editor; + fileHandler: TFileHandler | TReadOnlyFileHandler; + isEditable: boolean; +}; + +export const FilePlugins = (args: TArgs): Plugin[] => { + const { editor, fileHandler, isEditable } = args; + + return [ + ...(isEditable && "delete" in fileHandler ? [TrackFileDeletionPlugin(editor, fileHandler.delete)] : []), + TrackFileRestorationPlugin(editor, fileHandler.restore), + ]; +}; diff --git a/packages/editor/src/core/plugins/file/types.ts b/packages/editor/src/core/plugins/file/types.ts new file mode 100644 index 0000000000..164d12ae7e --- /dev/null +++ b/packages/editor/src/core/plugins/file/types.ts @@ -0,0 +1,8 @@ +import { Node as ProseMirrorNode } from "@tiptap/pm/model"; + +export type TFileNode = ProseMirrorNode & { + attrs: { + src: string; + id: string; + }; +}; diff --git a/packages/editor/src/core/plugins/image/delete-image.ts b/packages/editor/src/core/plugins/image/delete-image.ts deleted file mode 100644 index 459d9fd706..0000000000 --- a/packages/editor/src/core/plugins/image/delete-image.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; -// plugins -import { type ImageNode } from "@/plugins/image"; -// types -import { DeleteImage } from "@/types"; - -export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImage, nodeType: string): Plugin => - new Plugin({ - key: new PluginKey(`delete-${nodeType}`), - appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { - const newImageSources = new Set(); - newState.doc.descendants((node) => { - if (node.type.name === nodeType) { - newImageSources.add(node.attrs.src); - } - }); - - transactions.forEach((transaction) => { - // if the transaction has meta of skipImageDeletion get to true, then return (like while clearing the editor content programatically) - if (transaction.getMeta("skipImageDeletion")) return; - // transaction could be a selection - if (!transaction.docChanged) return; - - const removedImages: ImageNode[] = []; - - // iterate through all the nodes in the old state - oldState.doc.descendants((oldNode) => { - // if the node is not an image, then return as no point in checking - if (oldNode.type.name !== nodeType) return; - - // Check if the node has been deleted or replaced - if (!newImageSources.has(oldNode.attrs.src)) { - removedImages.push(oldNode as ImageNode); - } - }); - - removedImages.forEach(async (node) => { - const src = node.attrs.src; - editor.storage[nodeType].deletedImageSet?.set(src, true); - if (!src) return; - try { - await deleteImage(src); - } catch (error) { - console.error("Error deleting image:", error); - } - }); - }); - - return null; - }, - }); diff --git a/packages/editor/src/core/plugins/image/index.ts b/packages/editor/src/core/plugins/image/index.ts deleted file mode 100644 index c0dc631c53..0000000000 --- a/packages/editor/src/core/plugins/image/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./types"; -export * from "./delete-image"; -export * from "./restore-image"; diff --git a/packages/editor/src/core/plugins/image/restore-image.ts b/packages/editor/src/core/plugins/image/restore-image.ts deleted file mode 100644 index 4eecf01d7e..0000000000 --- a/packages/editor/src/core/plugins/image/restore-image.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; -// plugins -import { ImageNode } from "@/plugins/image"; -// types -import { RestoreImage } from "@/types"; - -export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: RestoreImage, nodeType: string): Plugin => - new Plugin({ - key: new PluginKey(`restore-${nodeType}`), - appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { - const oldImageSources = new Set(); - oldState.doc.descendants((node) => { - if (node.type.name === nodeType) { - oldImageSources.add(node.attrs.src); - } - }); - - transactions.forEach((transaction) => { - if (!transaction.docChanged) return; - - const addedImages: ImageNode[] = []; - - newState.doc.descendants((node, pos) => { - if (node.type.name !== nodeType) return; - if (pos < 0 || pos > newState.doc.content.size) return; - if (oldImageSources.has(node.attrs.src)) return; - // if the src is just a id (private bucket), then we don't need to handle restore from here but - // only while it fails to load - if (!node.attrs.src?.startsWith("http")) return; - addedImages.push(node as ImageNode); - }); - - addedImages.forEach(async (image) => { - const src = image.attrs.src; - const wasDeleted = editor.storage[nodeType].deletedImageSet.get(src); - if (wasDeleted === undefined) { - editor.storage[nodeType].deletedImageSet.set(src, false); - } else if (wasDeleted === true) { - try { - await onNodeRestored(src, restoreImage); - editor.storage[nodeType].deletedImageSet.set(src, false); - } catch (error) { - console.error("Error restoring image: ", error); - } - } - }); - }); - return null; - }, - }); - -async function onNodeRestored(src: string, restoreImage: RestoreImage): Promise { - if (!src) return; - try { - await restoreImage(src); - } catch (error) { - console.error("Error restoring image: ", error); - throw error; - } -} diff --git a/packages/editor/src/core/plugins/image/types/image-node.ts b/packages/editor/src/core/plugins/image/types/image-node.ts deleted file mode 100644 index 67afc8315a..0000000000 --- a/packages/editor/src/core/plugins/image/types/image-node.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Node as ProseMirrorNode } from "@tiptap/pm/model"; - -export interface ImageNode extends ProseMirrorNode { - attrs: { - src: string; - id: string; - }; -} - -export type ImageExtensionStorage = { - deletedImageSet: Map; - uploadInProgress: boolean; -}; diff --git a/packages/editor/src/core/plugins/image/types/index.ts b/packages/editor/src/core/plugins/image/types/index.ts deleted file mode 100644 index 2fddf3bf64..0000000000 --- a/packages/editor/src/core/plugins/image/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./image-node"; diff --git a/packages/editor/src/core/plugins/markdown-clipboard.ts b/packages/editor/src/core/plugins/markdown-clipboard.ts new file mode 100644 index 0000000000..78f649b23d --- /dev/null +++ b/packages/editor/src/core/plugins/markdown-clipboard.ts @@ -0,0 +1,80 @@ +import { Editor } from "@tiptap/core"; +import { Fragment, Node } from "@tiptap/pm/model"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; + +export const MarkdownClipboardPlugin = (editor: Editor): Plugin => + new Plugin({ + key: new PluginKey("markdownClipboard"), + props: { + clipboardTextSerializer: (slice) => { + const markdownSerializer = editor.storage.markdown.serializer; + const isTableRow = slice.content.firstChild?.type?.name === CORE_EXTENSIONS.TABLE_ROW; + const nodeSelect = slice.openStart === 0 && slice.openEnd === 0; + + if (nodeSelect) { + return markdownSerializer.serialize(slice.content); + } + + const processTableContent = (tableNode: Node | Fragment) => { + let result = ""; + tableNode.content?.forEach?.((tableRowNode: Node | Fragment) => { + tableRowNode.content?.forEach?.((cell: Node) => { + const cellContent = cell.content ? markdownSerializer.serialize(cell.content) : ""; + result += cellContent + "\n"; + }); + }); + return result; + }; + + if (isTableRow) { + const rowsCount = slice.content?.childCount || 0; + const cellsCount = slice.content?.firstChild?.content?.childCount || 0; + if (rowsCount === 1 || cellsCount === 1) { + return processTableContent(slice.content); + } else { + return markdownSerializer.serialize(slice.content); + } + } + + const traverseToParentOfLeaf = (node: Node | null, parent: Fragment | Node, depth: number): Node | Fragment => { + let currentNode = node; + let currentParent = parent; + let currentDepth = depth; + + while (currentNode && currentDepth > 1 && currentNode.content?.firstChild) { + if (currentNode.content?.childCount > 1) { + if (currentNode.content.firstChild?.type?.name === CORE_EXTENSIONS.LIST_ITEM) { + return currentParent; + } else { + return currentNode.content; + } + } + + currentParent = currentNode; + currentNode = currentNode.content?.firstChild || null; + currentDepth--; + } + + return currentParent; + }; + + if (slice.content.childCount > 1) { + return markdownSerializer.serialize(slice.content); + } else { + const targetNode = traverseToParentOfLeaf(slice.content.firstChild, slice.content, slice.openStart); + + let currentNode = targetNode; + while (currentNode && currentNode.content && currentNode.childCount === 1 && currentNode.firstChild) { + currentNode = currentNode.firstChild; + } + if (currentNode instanceof Node && currentNode.isText) { + return currentNode.text; + } + + return markdownSerializer.serialize(targetNode); + } + }, + }, + }); diff --git a/packages/editor/src/core/types/collaboration.ts b/packages/editor/src/core/types/collaboration.ts index 82e2f81f9a..5560862320 100644 --- a/packages/editor/src/core/types/collaboration.ts +++ b/packages/editor/src/core/types/collaboration.ts @@ -22,7 +22,7 @@ export type TServerHandler = { type TCollaborativeEditorHookProps = { disabledExtensions: TExtensions[]; - editable?: boolean; + editable: boolean; editorClassName: string; editorProps?: EditorProps; extensions?: Extensions; diff --git a/packages/editor/src/core/types/config.ts b/packages/editor/src/core/types/config.ts index 4c91fec5d1..b72e3dcf65 100644 --- a/packages/editor/src/core/types/config.ts +++ b/packages/editor/src/core/types/config.ts @@ -1,19 +1,17 @@ -import { DeleteImage, RestoreImage, UploadImage } from "@/types"; - export type TReadOnlyFileHandler = { getAssetSrc: (path: string) => Promise; - restore: RestoreImage; + restore: (assetSrc: string) => Promise; }; export type TFileHandler = TReadOnlyFileHandler & { assetsUploadStatus: Record; // blockId => progress percentage cancel: () => void; - delete: DeleteImage; - upload: UploadImage; + delete: (assetSrc: string) => Promise; + upload: (blockId: string, file: File) => Promise; validation: { /** * @description max file size in bytes - * @example enter 5242880( 5* 1024 * 1024) for 5MB + * @example enter 5242880(5 * 1024 * 1024) for 5MB */ maxFileSize: number; }; diff --git a/packages/editor/src/core/types/image.ts b/packages/editor/src/core/types/image.ts deleted file mode 100644 index ca6f76fb1b..0000000000 --- a/packages/editor/src/core/types/image.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; - -export type RestoreImage = (assetUrlWithWorkspaceId: string) => Promise; - -export type UploadImage = (blockId: string, file: File) => Promise; diff --git a/packages/editor/src/core/types/index.ts b/packages/editor/src/core/types/index.ts index e99a74b28e..66cb249425 100644 --- a/packages/editor/src/core/types/index.ts +++ b/packages/editor/src/core/types/index.ts @@ -4,7 +4,6 @@ export * from "./config"; export * from "./editor"; export * from "./embed"; export * from "./extensions"; -export * from "./image"; export * from "./mention"; export * from "./slash-commands-suggestion"; export * from "@/plane-editor/types"; diff --git a/packages/hooks/package.json b/packages/hooks/package.json index d444bedda8..320203e2d8 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -22,7 +22,7 @@ "@plane/eslint-config": "*", "@types/node": "^22.5.4", "@types/react": "^18.3.11", - "tsup": "^8.4.0", + "tsup": "8.3.0", "typescript": "^5.3.3" } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 45550f3697..e0920d49ec 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -71,7 +71,7 @@ "postcss-cli": "^11.0.0", "postcss-nested": "^6.0.1", "storybook": "^8.1.1", - "tsup": "^8.4.0", + "tsup": "8.3.0", "typescript": "5.3.3" } } diff --git a/packages/utils/package.json b/packages/utils/package.json index fc8600077d..07504d9a48 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -29,7 +29,7 @@ "@types/node": "^22.5.4", "@types/react": "^18.3.11", "@types/zxcvbn": "^4.4.5", - "tsup": "^8.4.0", + "tsup": "8.3.0", "typescript": "^5.3.3" } } diff --git a/web/core/store/pages/base-page.ts b/web/core/store/pages/base-page.ts index 32a087c95f..6639e8e84d 100644 --- a/web/core/store/pages/base-page.ts +++ b/web/core/store/pages/base-page.ts @@ -529,7 +529,6 @@ export class BasePage implements TBasePage { }; setEditorRef = (editorRef: EditorRefApi | null) => { - console.log("store editorRef", editorRef); runInAction(() => { this.editorRef = editorRef; }); diff --git a/yarn.lock b/yarn.lock index 22d8754dc6..6fca25f4d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2236,100 +2236,105 @@ resolved "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f" integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg== -"@rollup/rollup-android-arm-eabi@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz#e1d7700735f7e8de561ef7d1fa0362082a180c43" - integrity sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ== +"@rollup/rollup-android-arm-eabi@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz#f39f09f60d4a562de727c960d7b202a2cf797424" + integrity sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw== -"@rollup/rollup-android-arm64@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.35.0.tgz#fa6cdfb1fc9e2c8e227a7f35d524d8f7f90cf4db" - integrity sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA== +"@rollup/rollup-android-arm64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz#d19af7e23760717f1d879d4ca3d2cd247742dff2" + integrity sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA== -"@rollup/rollup-darwin-arm64@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.35.0.tgz#6da5a1ddc4f11d4a7ae85ab443824cb6bf614e30" - integrity sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q== +"@rollup/rollup-darwin-arm64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz#1c3a2fbf205d80641728e05f4a56c909e95218b7" + integrity sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w== -"@rollup/rollup-darwin-x64@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.35.0.tgz#25b74ce2d8d3f9ea8e119b01384d44a1c0a0d3ae" - integrity sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q== +"@rollup/rollup-darwin-x64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz#aa66d2ba1a25e609500e13bef06dc0e71cc0c0d4" + integrity sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg== -"@rollup/rollup-freebsd-arm64@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.35.0.tgz#be3d39e3441df5d6e187c83d158c60656c82e203" - integrity sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ== +"@rollup/rollup-freebsd-arm64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz#df10a7b6316a0ef1028c6ca71a081124c537e30d" + integrity sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg== -"@rollup/rollup-freebsd-x64@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.35.0.tgz#cd932d3ec679711efd65ca25821fb318e25b7ce4" - integrity sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw== +"@rollup/rollup-freebsd-x64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz#a3fdce8a05e95b068cbcb46e4df5185e407d0c35" + integrity sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA== -"@rollup/rollup-linux-arm-gnueabihf@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.35.0.tgz#d300b74c6f805474225632f185daaeae760ac2bb" - integrity sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg== +"@rollup/rollup-linux-arm-gnueabihf@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz#49f766c55383bd0498014a9d76924348c2f3890c" + integrity sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg== -"@rollup/rollup-linux-arm-musleabihf@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.35.0.tgz#2caac622380f314c41934ed1e68ceaf6cc380cc3" - integrity sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A== +"@rollup/rollup-linux-arm-musleabihf@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz#1d4d7d32fc557e17d52e1857817381ea365e2959" + integrity sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA== -"@rollup/rollup-linux-arm64-gnu@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.35.0.tgz#1ec841650b038cc15c194c26326483fd7ebff3e3" - integrity sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A== +"@rollup/rollup-linux-arm64-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz#f4fc317268441e9589edad3be8f62b6c03009bc1" + integrity sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA== -"@rollup/rollup-linux-arm64-musl@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.35.0.tgz#2fc70a446d986e27f6101ea74e81746987f69150" - integrity sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg== +"@rollup/rollup-linux-arm64-musl@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz#63a1f1b0671cb17822dabae827fef0e443aebeb7" + integrity sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg== -"@rollup/rollup-linux-loongarch64-gnu@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.35.0.tgz#561bd045cd9ce9e08c95f42e7a8688af8c93d764" - integrity sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g== +"@rollup/rollup-linux-loongarch64-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz#c659b01cc6c0730b547571fc3973e1e955369f98" + integrity sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw== -"@rollup/rollup-linux-powerpc64le-gnu@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.35.0.tgz#45d849a0b33813f33fe5eba9f99e0ff15ab5caad" - integrity sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA== +"@rollup/rollup-linux-powerpc64le-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz#612e746f9ad7e58480f964d65e0d6c3f4aae69a8" + integrity sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A== -"@rollup/rollup-linux-riscv64-gnu@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.35.0.tgz#78dde3e6fcf5b5733a97d0a67482d768aa1e83a5" - integrity sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g== +"@rollup/rollup-linux-riscv64-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz#4610dbd1dcfbbae32fbc10c20ae7387acb31110c" + integrity sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw== -"@rollup/rollup-linux-s390x-gnu@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.35.0.tgz#2e34835020f9e03dfb411473a5c2a0e8a9c5037b" - integrity sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw== +"@rollup/rollup-linux-riscv64-musl@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz#054911fab40dc83fafc21e470193c058108f19d8" + integrity sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw== -"@rollup/rollup-linux-x64-gnu@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.35.0.tgz#4f9774beddc6f4274df57ac99862eb23040de461" - integrity sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA== +"@rollup/rollup-linux-s390x-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz#98896eca8012547c7f04bd07eaa6896825f9e1a5" + integrity sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g== -"@rollup/rollup-linux-x64-musl@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.35.0.tgz#dfcff2c1aed518b3d23ccffb49afb349d74fb608" - integrity sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg== +"@rollup/rollup-linux-x64-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz#01cf56844a1e636ee80dfb364e72c2b7142ad896" + integrity sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A== -"@rollup/rollup-win32-arm64-msvc@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.35.0.tgz#b0b37e2d77041e3aa772f519291309abf4c03a84" - integrity sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg== +"@rollup/rollup-linux-x64-musl@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz#e67c7531df6dff0b4c241101d4096617fbca87c3" + integrity sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ== -"@rollup/rollup-win32-ia32-msvc@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.35.0.tgz#5b5a40e44a743ddc0e06b8e1b3982f856dc9ce0a" - integrity sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw== +"@rollup/rollup-win32-arm64-msvc@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz#7eeada98444e580674de6989284e4baacd48ea65" + integrity sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ== -"@rollup/rollup-win32-x64-msvc@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.35.0.tgz#05f25dbc9981bee1ae6e713daab10397044a46ca" - integrity sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw== +"@rollup/rollup-win32-ia32-msvc@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz#516c4b54f80587b4a390aaf4940b40870271d35d" + integrity sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg== + +"@rollup/rollup-win32-x64-msvc@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz#848f99b0d9936d92221bb6070baeff4db6947a30" + integrity sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw== "@rtsao/scc@^1.1.0": version "1.1.0" @@ -3323,11 +3328,16 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@1.0.6", "@types/estree@^1.0.0", "@types/estree@^1.0.6": +"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6": version "1.0.6" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== +"@types/estree@1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + "@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": version "5.0.6" resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz#41fec4ea20e9c7b22f024ab88a95c6bb288f51b8" @@ -4604,9 +4614,9 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -bundle-require@^5.1.0: +bundle-require@^5.0.0: version "5.1.0" - resolved "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz#8db66f41950da3d77af1ef3322f4c3e04009faee" + resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-5.1.0.tgz#8db66f41950da3d77af1ef3322f4c3e04009faee" integrity sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA== dependencies: load-tsconfig "^0.2.3" @@ -4761,13 +4771,6 @@ chokidar@^3.3.0, chokidar@^3.5.2, chokidar@^3.5.3, chokidar@^3.6.0: optionalDependencies: fsevents "~2.3.2" -chokidar@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" - integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== - dependencies: - readdirp "^4.0.1" - chownr@^1.1.1: version "1.1.4" resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -4992,10 +4995,10 @@ concurrently@^9.0.1: tree-kill "^1.2.2" yargs "^17.7.2" -consola@^3.4.0: - version "3.4.0" - resolved "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz#4cfc9348fd85ed16a17940b3032765e31061ab88" - integrity sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA== +consola@^3.2.3: + version "3.4.2" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" + integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== constant-case@^3.0.4: version "3.0.4" @@ -5099,7 +5102,7 @@ cross-fetch@^3.1.5: dependencies: node-fetch "^2.7.0" -cross-spawn@^7.0.0, cross-spawn@^7.0.2: +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.6" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -5364,7 +5367,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7, debug@^4.4.0: +debug@4, debug@^4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7: version "4.4.0" resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== @@ -5378,6 +5381,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.5: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + decimal.js-light@^2.4.1: version "2.5.1" resolved "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" @@ -5930,7 +5940,7 @@ esbuild-register@^3.5.0: dependencies: debug "^4.3.4" -esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.25.0: +esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.23.0: version "0.25.0" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== @@ -6266,6 +6276,21 @@ events@^3.2.0, events@^3.3.0: resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +execa@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + expand-template@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" @@ -6400,10 +6425,10 @@ fault@^2.0.0: dependencies: format "^0.2.0" -fdir@^6.4.3: - version "6.4.3" - resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz#011cdacf837eca9b811c89dbb902df714273db72" - integrity sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw== +fdir@^6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" + integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg== fecha@^4.2.0: version "4.2.3" @@ -6737,6 +6762,11 @@ get-stdin@^9.0.0: resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575" integrity sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA== +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-symbol-description@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" @@ -7064,6 +7094,11 @@ https-proxy-agent@^7.0.6: agent-base "^7.1.2" debug "4" +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + hyphen@^1.6.4: version "1.10.6" resolved "https://registry.npmjs.org/hyphen/-/hyphen-1.10.6.tgz#0e779d280e696102b97d7e42f5ca5de2cc97e274" @@ -8286,6 +8321,11 @@ mime@1.6.0: resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + mimic-response@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" @@ -8547,6 +8587,13 @@ normalize.css@^8.0.1: resolved "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3" integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg== +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + nprogress@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1" @@ -8685,6 +8732,13 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + open@^8.0.4: version "8.4.2" resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" @@ -8867,7 +8921,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -9807,11 +9861,6 @@ readable-stream@^4.0.0: process "^0.11.10" string_decoder "^1.3.0" -readdirp@^4.0.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" - integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -10072,32 +10121,33 @@ robust-predicates@^3.0.2: resolved "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== -rollup@^4.34.8: - version "4.35.0" - resolved "https://registry.npmjs.org/rollup/-/rollup-4.35.0.tgz#76c95dba17a579df4c00c3955aed32aa5d4dc66d" - integrity sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg== +rollup@^4.19.0: + version "4.41.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.41.1.tgz#46ddc1b33cf1b0baa99320d3b0b4973dc2253b6a" + integrity sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw== dependencies: - "@types/estree" "1.0.6" + "@types/estree" "1.0.7" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.35.0" - "@rollup/rollup-android-arm64" "4.35.0" - "@rollup/rollup-darwin-arm64" "4.35.0" - "@rollup/rollup-darwin-x64" "4.35.0" - "@rollup/rollup-freebsd-arm64" "4.35.0" - "@rollup/rollup-freebsd-x64" "4.35.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.35.0" - "@rollup/rollup-linux-arm-musleabihf" "4.35.0" - "@rollup/rollup-linux-arm64-gnu" "4.35.0" - "@rollup/rollup-linux-arm64-musl" "4.35.0" - "@rollup/rollup-linux-loongarch64-gnu" "4.35.0" - "@rollup/rollup-linux-powerpc64le-gnu" "4.35.0" - "@rollup/rollup-linux-riscv64-gnu" "4.35.0" - "@rollup/rollup-linux-s390x-gnu" "4.35.0" - "@rollup/rollup-linux-x64-gnu" "4.35.0" - "@rollup/rollup-linux-x64-musl" "4.35.0" - "@rollup/rollup-win32-arm64-msvc" "4.35.0" - "@rollup/rollup-win32-ia32-msvc" "4.35.0" - "@rollup/rollup-win32-x64-msvc" "4.35.0" + "@rollup/rollup-android-arm-eabi" "4.41.1" + "@rollup/rollup-android-arm64" "4.41.1" + "@rollup/rollup-darwin-arm64" "4.41.1" + "@rollup/rollup-darwin-x64" "4.41.1" + "@rollup/rollup-freebsd-arm64" "4.41.1" + "@rollup/rollup-freebsd-x64" "4.41.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.41.1" + "@rollup/rollup-linux-arm-musleabihf" "4.41.1" + "@rollup/rollup-linux-arm64-gnu" "4.41.1" + "@rollup/rollup-linux-arm64-musl" "4.41.1" + "@rollup/rollup-linux-loongarch64-gnu" "4.41.1" + "@rollup/rollup-linux-powerpc64le-gnu" "4.41.1" + "@rollup/rollup-linux-riscv64-gnu" "4.41.1" + "@rollup/rollup-linux-riscv64-musl" "4.41.1" + "@rollup/rollup-linux-s390x-gnu" "4.41.1" + "@rollup/rollup-linux-x64-gnu" "4.41.1" + "@rollup/rollup-linux-x64-musl" "4.41.1" + "@rollup/rollup-win32-arm64-msvc" "4.41.1" + "@rollup/rollup-win32-ia32-msvc" "4.41.1" + "@rollup/rollup-win32-x64-msvc" "4.41.1" fsevents "~2.3.2" rope-sequence@^1.3.0: @@ -10404,6 +10454,11 @@ side-channel@^1.0.6, side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" @@ -10689,6 +10744,11 @@ strip-bom@^3.0.0: resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -10969,17 +11029,12 @@ tinycolor2@^1.4.1: resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== -tinyexec@^0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" - integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== - -tinyglobby@^0.2.11: - version "0.2.12" - resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz#ac941a42e0c5773bd0b5d08f32de82e74a1a61b5" - integrity sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww== +tinyglobby@^0.2.1: + version "0.2.14" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d" + integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ== dependencies: - fdir "^6.4.3" + fdir "^6.4.4" picomatch "^4.0.2" tinyrainbow@^1.2.0: @@ -11152,26 +11207,26 @@ tslib@~2.5.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== -tsup@8.4.0, tsup@^8.4.0: - version "8.4.0" - resolved "https://registry.npmjs.org/tsup/-/tsup-8.4.0.tgz#2fdf537e7abc8f1ccbbbfe4228f16831457d4395" - integrity sha512-b+eZbPCjz10fRryaAA7C8xlIHnf8VnsaRqydheLIqwG/Mcpfk8Z5zp3HayX7GaTygkigHl5cBUs+IhcySiIexQ== +tsup@8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/tsup/-/tsup-8.3.0.tgz#c7dae40b13d11d81fb144c0f90077a99102a572a" + integrity sha512-ALscEeyS03IomcuNdFdc0YWGVIkwH1Ws7nfTbAPuoILvEV2hpGQAY72LIOjglGo4ShWpZfpBqP/jpQVCzqYQag== dependencies: - bundle-require "^5.1.0" + bundle-require "^5.0.0" cac "^6.7.14" - chokidar "^4.0.3" - consola "^3.4.0" - debug "^4.4.0" - esbuild "^0.25.0" + chokidar "^3.6.0" + consola "^3.2.3" + debug "^4.3.5" + esbuild "^0.23.0" + execa "^5.1.1" joycon "^3.1.1" - picocolors "^1.1.1" + picocolors "^1.0.1" postcss-load-config "^6.0.1" resolve-from "^5.0.0" - rollup "^4.34.8" + rollup "^4.19.0" source-map "0.8.0-beta.0" sucrase "^3.35.0" - tinyexec "^0.3.2" - tinyglobby "^0.2.11" + tinyglobby "^0.2.1" tree-kill "^1.2.2" tsutils@^3.21.0: