- <>Commented {timeAgo(comment.created_at)}> + <>commented {timeAgo(comment.created_at)}>
,
- command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
+ command: ({ editor, range }: CommandProps) =>
+ editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
},
{
title: "Image",
@@ -190,7 +218,15 @@ export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
}
};
-const CommandList = ({ items, command }: { items: CommandItemProps[]; command: any; editor: any; range: any }) => {
+const CommandList = ({
+ items,
+ command,
+}: {
+ items: CommandItemProps[];
+ command: any;
+ editor: any;
+ range: any;
+}) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = useCallback(
diff --git a/space/components/tiptap/table-menu/InsertBottomTableIcon.tsx b/space/components/tiptap/table-menu/InsertBottomTableIcon.tsx
new file mode 100644
index 000000000..0e42ba648
--- /dev/null
+++ b/space/components/tiptap/table-menu/InsertBottomTableIcon.tsx
@@ -0,0 +1,16 @@
+const InsertBottomTableIcon = (props: any) => (
+
+);
+
+export default InsertBottomTableIcon;
diff --git a/space/components/tiptap/table-menu/InsertLeftTableIcon.tsx b/space/components/tiptap/table-menu/InsertLeftTableIcon.tsx
new file mode 100644
index 000000000..1fd75fe87
--- /dev/null
+++ b/space/components/tiptap/table-menu/InsertLeftTableIcon.tsx
@@ -0,0 +1,15 @@
+const InsertLeftTableIcon = (props: any) => (
+
+);
+export default InsertLeftTableIcon;
diff --git a/space/components/tiptap/table-menu/InsertRightTableIcon.tsx b/space/components/tiptap/table-menu/InsertRightTableIcon.tsx
new file mode 100644
index 000000000..1a6570969
--- /dev/null
+++ b/space/components/tiptap/table-menu/InsertRightTableIcon.tsx
@@ -0,0 +1,16 @@
+const InsertRightTableIcon = (props: any) => (
+
+);
+
+export default InsertRightTableIcon;
diff --git a/space/components/tiptap/table-menu/InsertTopTableIcon.tsx b/space/components/tiptap/table-menu/InsertTopTableIcon.tsx
new file mode 100644
index 000000000..8f04f4f61
--- /dev/null
+++ b/space/components/tiptap/table-menu/InsertTopTableIcon.tsx
@@ -0,0 +1,15 @@
+const InsertTopTableIcon = (props: any) => (
+
+);
+export default InsertTopTableIcon;
diff --git a/space/components/tiptap/table-menu/index.tsx b/space/components/tiptap/table-menu/index.tsx
new file mode 100644
index 000000000..94f9c0f8d
--- /dev/null
+++ b/space/components/tiptap/table-menu/index.tsx
@@ -0,0 +1,143 @@
+import { useState, useEffect } from "react";
+import { Rows, Columns, ToggleRight } from "lucide-react";
+import { cn } from "../utils";
+import { Tooltip } from "components/ui";
+import InsertLeftTableIcon from "./InsertLeftTableIcon";
+import InsertRightTableIcon from "./InsertRightTableIcon";
+import InsertTopTableIcon from "./InsertTopTableIcon";
+import InsertBottomTableIcon from "./InsertBottomTableIcon";
+
+interface TableMenuItem {
+ command: () => void;
+ icon: any;
+ key: string;
+ name: string;
+}
+
+export const findTableAncestor = (node: Node | null): HTMLTableElement | null => {
+ while (node !== null && node.nodeName !== "TABLE") {
+ node = node.parentNode;
+ }
+ return node as HTMLTableElement;
+};
+
+export const TableMenu = ({ editor }: { editor: any }) => {
+ const [tableLocation, setTableLocation] = useState({ bottom: 0, left: 0 });
+ const isOpen = editor?.isActive("table");
+
+ const items: TableMenuItem[] = [
+ {
+ command: () => editor.chain().focus().addColumnBefore().run(),
+ icon: InsertLeftTableIcon,
+ key: "insert-column-left",
+ name: "Insert 1 column left",
+ },
+ {
+ command: () => editor.chain().focus().addColumnAfter().run(),
+ icon: InsertRightTableIcon,
+ key: "insert-column-right",
+ name: "Insert 1 column right",
+ },
+ {
+ command: () => editor.chain().focus().addRowBefore().run(),
+ icon: InsertTopTableIcon,
+ key: "insert-row-above",
+ name: "Insert 1 row above",
+ },
+ {
+ command: () => editor.chain().focus().addRowAfter().run(),
+ icon: InsertBottomTableIcon,
+ key: "insert-row-below",
+ name: "Insert 1 row below",
+ },
+ {
+ command: () => editor.chain().focus().deleteColumn().run(),
+ icon: Columns,
+ key: "delete-column",
+ name: "Delete column",
+ },
+ {
+ command: () => editor.chain().focus().deleteRow().run(),
+ icon: Rows,
+ key: "delete-row",
+ name: "Delete row",
+ },
+ {
+ command: () => editor.chain().focus().toggleHeaderRow().run(),
+ icon: ToggleRight,
+ key: "toggle-header-row",
+ name: "Toggle header row",
+ },
+ ];
+
+ useEffect(() => {
+ if (!window) return;
+
+ const handleWindowClick = () => {
+ const selection: any = window?.getSelection();
+
+ if (selection.rangeCount !== 0) {
+ const range = selection.getRangeAt(0);
+ const tableNode = findTableAncestor(range.startContainer);
+
+ let parent = tableNode?.parentElement;
+
+ if (tableNode) {
+ const tableRect = tableNode.getBoundingClientRect();
+ const tableCenter = tableRect.left + tableRect.width / 2;
+ const menuWidth = 45;
+ const menuLeft = tableCenter - menuWidth / 2;
+ const tableBottom = tableRect.bottom;
+
+ setTableLocation({ bottom: tableBottom, left: menuLeft });
+
+ while (parent) {
+ if (!parent.classList.contains("disable-scroll"))
+ parent.classList.add("disable-scroll");
+ parent = parent.parentElement;
+ }
+ } else {
+ const scrollDisabledContainers = document.querySelectorAll(".disable-scroll");
+
+ scrollDisabledContainers.forEach((container) => {
+ container.classList.remove("disable-scroll");
+ });
+ }
+ }
+ };
+
+ window.addEventListener("click", handleWindowClick);
+
+ return () => {
+ window.removeEventListener("click", handleWindowClick);
+ };
+ }, [tableLocation, editor]);
+
+ return (
+ {state.name}
- Commented {timeAgo(comment.created_at)} + commented {timeAgo(comment.created_at)}
{state.description}
diff --git a/web/components/tiptap/extensions/index.tsx b/web/components/tiptap/extensions/index.tsx index ea5d421a6..f5dc11384 100644 --- a/web/components/tiptap/extensions/index.tsx +++ b/web/components/tiptap/extensions/index.tsx @@ -32,122 +32,122 @@ export const TiptapExtensions = ( workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void ) => [ - StarterKit.configure({ - bulletList: { - HTMLAttributes: { - class: "list-disc list-outside leading-3 -mt-2", + StarterKit.configure({ + bulletList: { + HTMLAttributes: { + class: "list-disc list-outside leading-3 -mt-2", + }, }, - }, - orderedList: { - HTMLAttributes: { - class: "list-decimal list-outside leading-3 -mt-2", + orderedList: { + HTMLAttributes: { + class: "list-decimal list-outside leading-3 -mt-2", + }, }, - }, - listItem: { - HTMLAttributes: { - class: "leading-normal -mb-2", + listItem: { + HTMLAttributes: { + class: "leading-normal -mb-2", + }, }, - }, - blockquote: { - HTMLAttributes: { - class: "border-l-4 border-custom-border-300", + blockquote: { + HTMLAttributes: { + class: "border-l-4 border-custom-border-300", + }, }, - }, - code: { + code: { + HTMLAttributes: { + class: + "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", + spellcheck: "false", + }, + }, + codeBlock: false, + horizontalRule: false, + dropcursor: { + color: "rgba(var(--color-text-100))", + width: 2, + }, + gapcursor: false, + }), + CodeBlockLowlight.configure({ + lowlight, + }), + HorizontalRule.extend({ + addInputRules() { + return [ + new InputRule({ + find: /^(?:---|—-|___\s|\*\*\*\s)$/, + handler: ({ state, range, commands }) => { + commands.splitBlock(); + + const attributes = {}; + const { tr } = state; + const start = range.from; + const end = range.to; + // @ts-ignore + tr.replaceWith(start - 1, end, this.type.create(attributes)); + }, + }), + ]; + }, + }).configure({ + HTMLAttributes: { + class: "mb-6 border-t border-custom-border-300", + }, + }), + Gapcursor, + TiptapLink.configure({ + protocols: ["http", "https"], + validate: (url) => isValidHttpUrl(url), HTMLAttributes: { class: - "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", - spellcheck: "false", + "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", }, - }, - codeBlock: false, - horizontalRule: false, - dropcursor: { - color: "rgba(var(--color-text-100))", - width: 2, - }, - gapcursor: false, - }), - CodeBlockLowlight.configure({ - lowlight, - }), - HorizontalRule.extend({ - addInputRules() { - return [ - new InputRule({ - find: /^(?:---|—-|___\s|\*\*\*\s)$/, - handler: ({ state, range, commands }) => { - commands.splitBlock(); + }), + UpdatedImage.configure({ + HTMLAttributes: { + class: "rounded-lg border border-custom-border-300", + }, + }), + Placeholder.configure({ + placeholder: ({ node }) => { + if (node.type.name === "heading") { + return `Heading ${node.attrs.level}`; + } + if (node.type.name === "image" || node.type.name === "table") { + return ""; + } - const attributes = {}; - const { tr } = state; - const start = range.from; - const end = range.to; - // @ts-ignore - tr.replaceWith(start - 1, end, this.type.create(attributes)); - }, - }), - ]; - }, - }).configure({ - HTMLAttributes: { - class: "mb-6 border-t border-custom-border-300", - }, - }), - Gapcursor, - TiptapLink.configure({ - protocols: ["http", "https"], - validate: (url) => isValidHttpUrl(url), - HTMLAttributes: { - class: - "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", - }, - }), - UpdatedImage.configure({ - HTMLAttributes: { - class: "rounded-lg border border-custom-border-300", - }, - }), - Placeholder.configure({ - placeholder: ({ node }) => { - if (node.type.name === "heading") { - return `Heading ${node.attrs.level}`; - } - if (node.type.name === "image" || node.type.name === "table") { - return ""; - } - - return "Press '/' for commands..."; - }, - includeChildren: true, - }), - UniqueID.configure({ - types: ["image"], - }), - SlashCommand(workspaceSlug, setIsSubmitting), - TiptapUnderline, - TextStyle, - Color, - Highlight.configure({ - multicolor: true, - }), - TaskList.configure({ - HTMLAttributes: { - class: "not-prose pl-2", - }, - }), - TaskItem.configure({ - HTMLAttributes: { - class: "flex items-start my-4", - }, - nested: true, - }), - Markdown.configure({ - html: true, - transformCopiedText: true, - }), - Table, - TableHeader, - CustomTableCell, - TableRow, -]; + return "Press '/' for commands..."; + }, + includeChildren: true, + }), + UniqueID.configure({ + types: ["image"], + }), + SlashCommand(workspaceSlug, setIsSubmitting), + TiptapUnderline, + TextStyle, + Color, + Highlight.configure({ + multicolor: true, + }), + TaskList.configure({ + HTMLAttributes: { + class: "not-prose pl-2", + }, + }), + TaskItem.configure({ + HTMLAttributes: { + class: "flex items-start my-4", + }, + nested: true, + }), + Markdown.configure({ + html: true, + transformCopiedText: true, + }), + Table, + TableHeader, + CustomTableCell, + TableRow, + ]; diff --git a/web/components/tiptap/index.tsx b/web/components/tiptap/index.tsx index 82304bfb0..84f691c35 100644 --- a/web/components/tiptap/index.tsx +++ b/web/components/tiptap/index.tsx @@ -1,9 +1,10 @@ +import { useImperativeHandle, useRef, forwardRef, useEffect } from "react"; import { useEditor, EditorContent, Editor } from "@tiptap/react"; import { useDebouncedCallback } from "use-debounce"; +// components import { EditorBubbleMenu } from "./bubble-menu"; import { TiptapExtensions } from "./extensions"; import { TiptapEditorProps } from "./props"; -import { useImperativeHandle, useRef, forwardRef } from "react"; import { ImageResizer } from "./extensions/image-resize"; import { TableMenu } from "./table-menu"; @@ -76,8 +77,8 @@ const Tiptap = (props: ITipTapRichTextEditor) => { const editorClassNames = `relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md ${noBorder ? "" : "border border-custom-border-200"} ${ - borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" - } ${customClassName}`; + borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" + } ${customClassName}`; if (!editor) return null; editorRef.current = editor; diff --git a/web/components/tiptap/plugins/delete-image.tsx b/web/components/tiptap/plugins/delete-image.tsx index 568e4b748..fdf515ccc 100644 --- a/web/components/tiptap/plugins/delete-image.tsx +++ b/web/components/tiptap/plugins/delete-image.tsx @@ -1,43 +1,51 @@ -import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; import { Node as ProseMirrorNode } from "@tiptap/pm/model"; import fileService from "services/file.service"; const deleteKey = new PluginKey("delete-image"); +const IMAGE_NODE_TYPE = "image"; -const TrackImageDeletionPlugin = () => +interface ImageNode extends ProseMirrorNode { + attrs: { + src: string; + id: string; + }; +} + +const TrackImageDeletionPlugin = (): Plugin => new Plugin({ key: deleteKey, - appendTransaction: (transactions, oldState, newState) => { + appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { + const newImageSources = new Set(); + newState.doc.descendants((node) => { + if (node.type.name === IMAGE_NODE_TYPE) { + newImageSources.add(node.attrs.src); + } + }); + transactions.forEach((transaction) => { if (!transaction.docChanged) return; - const removedImages: ProseMirrorNode[] = []; + const removedImages: ImageNode[] = []; oldState.doc.descendants((oldNode, oldPos) => { - if (oldNode.type.name !== "image") return; - + if (oldNode.type.name !== IMAGE_NODE_TYPE) return; if (oldPos < 0 || oldPos > newState.doc.content.size) return; if (!newState.doc.resolve(oldPos).parent) return; + const newNode = newState.doc.nodeAt(oldPos); // Check if the node has been deleted or replaced - if (!newNode || newNode.type.name !== "image") { - // Check if the node still exists elsewhere in the document - let nodeExists = false; - newState.doc.descendants((node) => { - if (node.attrs.id === oldNode.attrs.id) { - nodeExists = true; - } - }); - if (!nodeExists) { - removedImages.push(oldNode as ProseMirrorNode); + if (!newNode || newNode.type.name !== IMAGE_NODE_TYPE) { + if (!newImageSources.has(oldNode.attrs.src)) { + removedImages.push(oldNode as ImageNode); } } }); - removedImages.forEach((node) => { + removedImages.forEach(async (node) => { const src = node.attrs.src; - onNodeDeleted(src); + await onNodeDeleted(src); }); }); @@ -47,10 +55,14 @@ const TrackImageDeletionPlugin = () => export default TrackImageDeletionPlugin; -async function onNodeDeleted(src: string) { - const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); - const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId); - if (resStatus === 204) { - console.log("Image deleted successfully"); +async function onNodeDeleted(src: string): PromiseUploading...
+ ) : ( + <> +No sub issues
++ {subIssue.project_detail.identifier}-{subIssue.sequence_id} +
+{subIssue.name}
+{option.label}
+