diff --git a/packages/editor/src/core/components/menus/menu-items.ts b/packages/editor/src/core/components/menus/menu-items.ts index d5c2316b9..60db11704 100644 --- a/packages/editor/src/core/components/menus/menu-items.ts +++ b/packages/editor/src/core/components/menus/menu-items.ts @@ -1,3 +1,5 @@ +import { Selection } from "@tiptap/pm/state"; +import { Editor } from "@tiptap/react"; import { BoldIcon, Heading1, @@ -19,8 +21,6 @@ import { CaseSensitive, LucideIcon, } from "lucide-react"; -import { Selection } from "@tiptap/pm/state"; -import { Editor } from "@tiptap/react"; // helpers import { insertImageCommand, @@ -43,168 +43,151 @@ import { toggleUnderline, } from "@/helpers/editor-commands"; // types -import { UploadImage } from "@/types"; +import { TEditorCommands, UploadImage } from "@/types"; export interface EditorMenuItem { - key: string; + key: TEditorCommands; name: string; isActive: () => boolean; command: () => void; icon: LucideIcon; } -export const TextItem = (editor: Editor) => - ({ - key: "text", - name: "Text", - isActive: () => editor.isActive("paragraph"), - command: () => setText(editor), - icon: CaseSensitive, - }) as const satisfies EditorMenuItem; +export const TextItem = (editor: Editor): EditorMenuItem => ({ + key: "text", + name: "Text", + isActive: () => editor.isActive("paragraph"), + command: () => setText(editor), + icon: CaseSensitive, +}); -export const HeadingOneItem = (editor: Editor) => - ({ - key: "h1", - name: "Heading 1", - isActive: () => editor.isActive("heading", { level: 1 }), - command: () => toggleHeadingOne(editor), - icon: Heading1, - }) as const satisfies EditorMenuItem; +export const HeadingOneItem = (editor: Editor): EditorMenuItem => ({ + key: "h1", + name: "Heading 1", + isActive: () => editor.isActive("heading", { level: 1 }), + command: () => toggleHeadingOne(editor), + icon: Heading1, +}); -export const HeadingTwoItem = (editor: Editor) => - ({ - key: "h2", - name: "Heading 2", - isActive: () => editor.isActive("heading", { level: 2 }), - command: () => toggleHeadingTwo(editor), - icon: Heading2, - }) as const satisfies EditorMenuItem; +export const HeadingTwoItem = (editor: Editor): EditorMenuItem => ({ + key: "h2", + name: "Heading 2", + isActive: () => editor.isActive("heading", { level: 2 }), + command: () => toggleHeadingTwo(editor), + icon: Heading2, +}); -export const HeadingThreeItem = (editor: Editor) => - ({ - key: "h3", - name: "Heading 3", - isActive: () => editor.isActive("heading", { level: 3 }), - command: () => toggleHeadingThree(editor), - icon: Heading3, - }) as const satisfies EditorMenuItem; +export const HeadingThreeItem = (editor: Editor): EditorMenuItem => ({ + key: "h3", + name: "Heading 3", + isActive: () => editor.isActive("heading", { level: 3 }), + command: () => toggleHeadingThree(editor), + icon: Heading3, +}); -export const HeadingFourItem = (editor: Editor) => - ({ - key: "h4", - name: "Heading 4", - isActive: () => editor.isActive("heading", { level: 4 }), - command: () => toggleHeadingFour(editor), - icon: Heading4, - }) as const satisfies EditorMenuItem; +export const HeadingFourItem = (editor: Editor): EditorMenuItem => ({ + key: "h4", + name: "Heading 4", + isActive: () => editor.isActive("heading", { level: 4 }), + command: () => toggleHeadingFour(editor), + icon: Heading4, +}); -export const HeadingFiveItem = (editor: Editor) => - ({ - key: "h5", - name: "Heading 5", - isActive: () => editor.isActive("heading", { level: 5 }), - command: () => toggleHeadingFive(editor), - icon: Heading5, - }) as const satisfies EditorMenuItem; +export const HeadingFiveItem = (editor: Editor): EditorMenuItem => ({ + key: "h5", + name: "Heading 5", + isActive: () => editor.isActive("heading", { level: 5 }), + command: () => toggleHeadingFive(editor), + icon: Heading5, +}); -export const HeadingSixItem = (editor: Editor) => - ({ - key: "h6", - name: "Heading 6", - isActive: () => editor.isActive("heading", { level: 6 }), - command: () => toggleHeadingSix(editor), - icon: Heading6, - }) as const satisfies EditorMenuItem; +export const HeadingSixItem = (editor: Editor): EditorMenuItem => ({ + key: "h6", + name: "Heading 6", + isActive: () => editor.isActive("heading", { level: 6 }), + command: () => toggleHeadingSix(editor), + icon: Heading6, +}); -export const BoldItem = (editor: Editor) => - ({ - key: "bold", - name: "Bold", - isActive: () => editor?.isActive("bold"), - command: () => toggleBold(editor), - icon: BoldIcon, - }) as const satisfies EditorMenuItem; +export const BoldItem = (editor: Editor): EditorMenuItem => ({ + key: "bold", + name: "Bold", + isActive: () => editor?.isActive("bold"), + command: () => toggleBold(editor), + icon: BoldIcon, +}); -export const ItalicItem = (editor: Editor) => - ({ - key: "italic", - name: "Italic", - isActive: () => editor?.isActive("italic"), - command: () => toggleItalic(editor), - icon: ItalicIcon, - }) as const satisfies EditorMenuItem; +export const ItalicItem = (editor: Editor): EditorMenuItem => ({ + key: "italic", + name: "Italic", + isActive: () => editor?.isActive("italic"), + command: () => toggleItalic(editor), + icon: ItalicIcon, +}); -export const UnderLineItem = (editor: Editor) => - ({ - key: "underline", - name: "Underline", - isActive: () => editor?.isActive("underline"), - command: () => toggleUnderline(editor), - icon: UnderlineIcon, - }) as const satisfies EditorMenuItem; +export const UnderLineItem = (editor: Editor): EditorMenuItem => ({ + key: "underline", + name: "Underline", + isActive: () => editor?.isActive("underline"), + command: () => toggleUnderline(editor), + icon: UnderlineIcon, +}); -export const StrikeThroughItem = (editor: Editor) => - ({ - key: "strikethrough", - name: "Strikethrough", - isActive: () => editor?.isActive("strike"), - command: () => toggleStrike(editor), - icon: StrikethroughIcon, - }) as const satisfies EditorMenuItem; +export const StrikeThroughItem = (editor: Editor): EditorMenuItem => ({ + key: "strikethrough", + name: "Strikethrough", + isActive: () => editor?.isActive("strike"), + command: () => toggleStrike(editor), + icon: StrikethroughIcon, +}); -export const BulletListItem = (editor: Editor) => - ({ - key: "bulleted-list", - name: "Bulleted list", - isActive: () => editor?.isActive("bulletList"), - command: () => toggleBulletList(editor), - icon: ListIcon, - }) as const satisfies EditorMenuItem; +export const BulletListItem = (editor: Editor): EditorMenuItem => ({ + key: "bulleted-list", + name: "Bulleted list", + isActive: () => editor?.isActive("bulletList"), + command: () => toggleBulletList(editor), + icon: ListIcon, +}); -export const NumberedListItem = (editor: Editor) => - ({ - key: "numbered-list", - name: "Numbered list", - isActive: () => editor?.isActive("orderedList"), - command: () => toggleOrderedList(editor), - icon: ListOrderedIcon, - }) as const satisfies EditorMenuItem; +export const NumberedListItem = (editor: Editor): EditorMenuItem => ({ + key: "numbered-list", + name: "Numbered list", + isActive: () => editor?.isActive("orderedList"), + command: () => toggleOrderedList(editor), + icon: ListOrderedIcon, +}); -export const TodoListItem = (editor: Editor) => - ({ - key: "to-do-list", - name: "To-do list", - isActive: () => editor.isActive("taskItem"), - command: () => toggleTaskList(editor), - icon: CheckSquare, - }) as const satisfies EditorMenuItem; +export const TodoListItem = (editor: Editor): EditorMenuItem => ({ + key: "to-do-list", + name: "To-do list", + isActive: () => editor.isActive("taskItem"), + command: () => toggleTaskList(editor), + icon: CheckSquare, +}); -export const QuoteItem = (editor: Editor) => - ({ - key: "quote", - name: "Quote", - isActive: () => editor?.isActive("blockquote"), - command: () => toggleBlockquote(editor), - icon: QuoteIcon, - }) as const satisfies EditorMenuItem; +export const QuoteItem = (editor: Editor): EditorMenuItem => ({ + key: "quote", + name: "Quote", + isActive: () => editor?.isActive("blockquote"), + command: () => toggleBlockquote(editor), + icon: QuoteIcon, +}); -export const CodeItem = (editor: Editor) => - ({ - key: "code", - name: "Code", - isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"), - command: () => toggleCodeBlock(editor), - icon: CodeIcon, - }) as const satisfies EditorMenuItem; +export const CodeItem = (editor: Editor): EditorMenuItem => ({ + key: "code", + name: "Code", + isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"), + command: () => toggleCodeBlock(editor), + icon: CodeIcon, +}); -export const TableItem = (editor: Editor) => - ({ - key: "table", - name: "Table", - isActive: () => editor?.isActive("table"), - command: () => insertTableCommand(editor), - icon: TableIcon, - }) as const satisfies EditorMenuItem; +export const TableItem = (editor: Editor): EditorMenuItem => ({ + key: "table", + name: "Table", + isActive: () => editor?.isActive("table"), + command: () => insertTableCommand(editor), + icon: TableIcon, +}); export const ImageItem = (editor: Editor, uploadFile: UploadImage) => ({ @@ -240,6 +223,3 @@ export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImag ImageItem(editor, uploadFile), ]; } - -export type EditorMenuItemNames = - ReturnType extends (infer U)[] ? (U extends { key: infer N } ? N : never) : never; diff --git a/packages/editor/src/core/extensions/slash-commands.tsx b/packages/editor/src/core/extensions/slash-commands.tsx index 38fc0231b..b58becc16 100644 --- a/packages/editor/src/core/extensions/slash-commands.tsx +++ b/packages/editor/src/core/extensions/slash-commands.tsx @@ -9,6 +9,9 @@ import { Heading1, Heading2, Heading3, + Heading4, + Heading5, + Heading6, ImageIcon, List, ListOrdered, @@ -91,7 +94,7 @@ const getSuggestionItems = title: "Text", description: "Just start typing with plain text.", searchTerms: ["p", "paragraph"], - icon: , + icon: , command: ({ editor, range }: CommandProps) => { if (range) { editor.chain().focus().deleteRange(range).clearNodes().run(); @@ -100,61 +103,91 @@ const getSuggestionItems = }, }, { - key: "heading_1", + key: "h1", title: "Heading 1", description: "Big section heading.", searchTerms: ["title", "big", "large"], - icon: , + icon: , command: ({ editor, range }: CommandProps) => { toggleHeadingOne(editor, range); }, }, { - key: "heading_2", + key: "h2", title: "Heading 2", description: "Medium section heading.", searchTerms: ["subtitle", "medium"], - icon: , + icon: , command: ({ editor, range }: CommandProps) => { toggleHeadingTwo(editor, range); }, }, { - key: "heading_3", + key: "h3", title: "Heading 3", description: "Small section heading.", searchTerms: ["subtitle", "small"], - icon: , + icon: , command: ({ editor, range }: CommandProps) => { toggleHeadingThree(editor, range); }, }, { - key: "todo_list", + key: "h4", + title: "Heading 4", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleHeadingThree(editor, range); + }, + }, + { + key: "h5", + title: "Heading 5", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleHeadingThree(editor, range); + }, + }, + { + key: "h6", + title: "Heading 6", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleHeadingThree(editor, range); + }, + }, + { + key: "to-do-list", title: "To do", description: "Track tasks with a to-do list.", searchTerms: ["todo", "task", "list", "check", "checkbox"], - icon: , + icon: , command: ({ editor, range }: CommandProps) => { toggleTaskList(editor, range); }, }, { - key: "bullet_list", + key: "bulleted-list", title: "Bullet list", description: "Create a simple bullet list.", searchTerms: ["unordered", "point"], - icon: , + icon: , command: ({ editor, range }: CommandProps) => { toggleBulletList(editor, range); }, }, { - key: "numbered_list", + key: "numbered-list", title: "Numbered list", description: "Create a list with numbering.", searchTerms: ["ordered"], - icon: , + icon: , command: ({ editor, range }: CommandProps) => { toggleOrderedList(editor, range); }, @@ -164,25 +197,25 @@ const getSuggestionItems = title: "Table", description: "Create a table", searchTerms: ["table", "cell", "db", "data", "tabular"], - icon: , + icon:
, command: ({ editor, range }: CommandProps) => { insertTableCommand(editor, range); }, }, { - key: "quote_block", + key: "quote", title: "Quote", description: "Capture a quote.", searchTerms: ["blockquote"], - icon: , + icon: , command: ({ editor, range }: CommandProps) => toggleBlockquote(editor, range), }, { - key: "code_block", + key: "code", title: "Code", description: "Capture a code snippet.", searchTerms: ["codeblock"], - icon: , + icon: , command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), }, { @@ -190,7 +223,7 @@ const getSuggestionItems = title: "Image", description: "Upload an image from your computer.", searchTerms: ["img", "photo", "picture", "media"], - icon: , + icon: , command: ({ editor, range }: CommandProps) => { insertImageCommand(editor, uploadFile, null, range); }, @@ -200,7 +233,7 @@ const getSuggestionItems = title: "Divider", description: "Visually divide blocks.", searchTerms: ["line", "divider", "horizontal", "rule", "separate"], - icon: , + icon: , command: ({ editor, range }: CommandProps) => { editor.chain().focus().deleteRange(range).setHorizontalRule().run(); }, diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 35b547970..41d962af0 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -3,7 +3,7 @@ import { Selection } from "@tiptap/pm/state"; import { EditorProps } from "@tiptap/pm/view"; import { useEditor as useCustomEditor, Editor } from "@tiptap/react"; // components -import { EditorMenuItemNames, getEditorMenuItems } from "@/components/menus"; +import { getEditorMenuItems } from "@/components/menus"; // extensions import { CoreEditorExtensions } from "@/extensions"; // helpers @@ -14,7 +14,15 @@ import { CollaborationProvider } from "@/plane-editor/providers"; // props import { CoreEditorProps } from "@/props"; // types -import { DeleteImage, EditorRefApi, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types"; +import { + DeleteImage, + EditorRefApi, + IMentionHighlight, + IMentionSuggestion, + RestoreImage, + TEditorCommands, + UploadImage, +} from "@/types"; export type TFileHandler = { cancel: () => void; @@ -147,12 +155,12 @@ export const useEditor = ({ insertContentAtSavedSelection(editorRef, content, savedSelection); } }, - executeMenuItemCommand: (itemName: EditorMenuItemNames) => { + executeMenuItemCommand: (itemKey: TEditorCommands) => { const editorItems = getEditorMenuItems(editorRef.current, fileHandler.upload); - const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.key === itemName); + const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey); - const item = getEditorMenuItem(itemName); + const item = getEditorMenuItem(itemKey); if (item) { if (item.key === "image") { item.command(savedSelectionRef.current); @@ -160,13 +168,13 @@ export const useEditor = ({ item.command(); } } else { - console.warn(`No command found for item: ${itemName}`); + console.warn(`No command found for item: ${itemKey}`); } }, - isMenuItemActive: (itemName: EditorMenuItemNames): boolean => { + isMenuItemActive: (itemName: TEditorCommands): boolean => { const editorItems = getEditorMenuItems(editorRef.current, fileHandler.upload); - const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.key === itemName); + const getEditorMenuItem = (itemName: TEditorCommands) => editorItems.find((item) => item.key === itemName); const item = getEditorMenuItem(itemName); return item ? item.isActive() : false; }, diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 84e522b55..6fa243f8a 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -1,11 +1,9 @@ -// components -import { EditorMenuItemNames } from "@/components/menus"; // helpers import { IMarking } from "@/helpers/scroll-to-node"; // hooks import { TFileHandler } from "@/hooks/use-editor"; // types -import { IMentionHighlight, IMentionSuggestion } from "@/types"; +import { IMentionHighlight, IMentionSuggestion, TEditorCommands } from "@/types"; export type EditorReadOnlyRefApi = { getMarkDown: () => string; @@ -17,8 +15,8 @@ export type EditorReadOnlyRefApi = { export interface EditorRefApi extends EditorReadOnlyRefApi { setEditorValueAtCursorPosition: (content: string) => void; - executeMenuItemCommand: (itemName: EditorMenuItemNames) => void; - isMenuItemActive: (itemName: EditorMenuItemNames) => boolean; + executeMenuItemCommand: (itemKey: TEditorCommands) => void; + isMenuItemActive: (itemKey: TEditorCommands) => boolean; onStateChange: (callback: () => void) => () => void; setFocusAtPosition: (position: number) => void; isEditorReadyToDiscard: () => boolean; diff --git a/packages/editor/src/core/types/slash-commands-suggestion.ts b/packages/editor/src/core/types/slash-commands-suggestion.ts index 34e451098..f7696f45d 100644 --- a/packages/editor/src/core/types/slash-commands-suggestion.ts +++ b/packages/editor/src/core/types/slash-commands-suggestion.ts @@ -1,13 +1,34 @@ import { ReactNode } from "react"; import { Editor, Range } from "@tiptap/core"; +export type TEditorCommands = + | "text" + | "h1" + | "h2" + | "h3" + | "h4" + | "h5" + | "h6" + | "bold" + | "italic" + | "underline" + | "strikethrough" + | "bulleted-list" + | "numbered-list" + | "to-do-list" + | "quote" + | "code" + | "table" + | "image" + | "divider"; + export type CommandProps = { editor: Editor; range: Range; }; export type ISlashCommandItem = { - key: string; + key: TEditorCommands; title: string; description: string; searchTerms: string[]; diff --git a/space/core/components/editor/toolbar.tsx b/space/core/components/editor/toolbar.tsx index d97c04d5f..a16cd0e20 100644 --- a/space/core/components/editor/toolbar.tsx +++ b/space/core/components/editor/toolbar.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback } from "react"; // editor -import { EditorMenuItemNames, EditorRefApi } from "@plane/editor"; +import { EditorRefApi, TEditorCommands } from "@plane/editor"; // ui import { Button, Tooltip } from "@plane/ui"; // constants @@ -11,7 +11,7 @@ import { TOOLBAR_ITEMS } from "@/constants/editor"; import { cn } from "@/helpers/common.helper"; type Props = { - executeCommand: (commandName: EditorMenuItemNames) => void; + executeCommand: (commandKey: TEditorCommands) => void; handleSubmit: () => void; isCommentEmpty: boolean; isSubmitting: boolean; diff --git a/space/core/constants/editor.ts b/space/core/constants/editor.ts index 698faf8cb..4c6bef2cc 100644 --- a/space/core/constants/editor.ts +++ b/space/core/constants/editor.ts @@ -19,12 +19,12 @@ import { Underline, } from "lucide-react"; // editor -import { EditorMenuItemNames } from "@plane/editor"; +import { TEditorCommands } from "@plane/editor"; type TEditorTypes = "lite" | "document"; export type ToolbarMenuItem = { - key: EditorMenuItemNames; + key: TEditorCommands; name: string; icon: LucideIcon; shortcut?: string[]; diff --git a/web/core/components/editor/lite-text-editor/toolbar.tsx b/web/core/components/editor/lite-text-editor/toolbar.tsx index 69affb67e..7576a1096 100644 --- a/web/core/components/editor/lite-text-editor/toolbar.tsx +++ b/web/core/components/editor/lite-text-editor/toolbar.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState, useCallback } from "react"; import { Globe2, Lock, LucideIcon } from "lucide-react"; // editor -import { EditorMenuItemNames, EditorRefApi } from "@plane/editor"; +import { EditorRefApi, TEditorCommands } from "@plane/editor"; // ui import { Button, Tooltip } from "@plane/ui"; // constants @@ -14,7 +14,7 @@ import { cn } from "@/helpers/common.helper"; type Props = { accessSpecifier?: EIssueCommentAccessSpecifier; - executeCommand: (commandName: EditorMenuItemNames) => void; + executeCommand: (commandKey: TEditorCommands) => void; handleAccessChange?: (accessKey: EIssueCommentAccessSpecifier) => void; handleSubmit: (event: React.MouseEvent) => void; isCommentEmpty: boolean; diff --git a/web/core/components/pages/editor/header/toolbar.tsx b/web/core/components/pages/editor/header/toolbar.tsx index 9ca72434c..65d484ef1 100644 --- a/web/core/components/pages/editor/header/toolbar.tsx +++ b/web/core/components/pages/editor/header/toolbar.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState, useCallback } from "react"; import { Check, ChevronDown } from "lucide-react"; // editor -import { EditorMenuItemNames, EditorRefApi } from "@plane/editor"; +import { EditorRefApi, TEditorCommands } from "@plane/editor"; // ui import { CustomMenu, Tooltip } from "@plane/ui"; // constants @@ -18,7 +18,7 @@ type Props = { type ToolbarButtonProps = { item: ToolbarMenuItem; isActive: boolean; - executeCommand: (commandName: EditorMenuItemNames) => void; + executeCommand: (commandKey: TEditorCommands) => void; }; const ToolbarButton: React.FC = React.memo((props) => { diff --git a/web/core/constants/editor.ts b/web/core/constants/editor.ts index ea5279233..e3fae96bd 100644 --- a/web/core/constants/editor.ts +++ b/web/core/constants/editor.ts @@ -20,12 +20,12 @@ import { Underline, } from "lucide-react"; // editor -import { EditorMenuItemNames } from "@plane/editor"; +import { TEditorCommands } from "@plane/editor"; type TEditorTypes = "lite" | "document"; export type ToolbarMenuItem = { - key: EditorMenuItemNames; + key: TEditorCommands; name: string; icon: LucideIcon; shortcut?: string[];