diff --git a/.gitignore b/.gitignore index 921881df4..1e99e102a 100644 --- a/.gitignore +++ b/.gitignore @@ -70,4 +70,6 @@ package-lock.json # lock files package-lock.json pnpm-lock.yaml -pnpm-workspace.yaml \ No newline at end of file +pnpm-workspace.yaml + +.npmrc diff --git a/apps/app/components/issues/description-form.tsx b/apps/app/components/issues/description-form.tsx index d190a4092..fcf10073d 100644 --- a/apps/app/components/issues/description-form.tsx +++ b/apps/app/components/issues/description-form.tsx @@ -7,9 +7,9 @@ import useReloadConfirmations from "hooks/use-reload-confirmation"; // components import { TextArea } from "components/ui"; -import Tiptap from "./tiptap"; // types import { IIssue } from "types"; +import Tiptap from "components/tiptap"; export interface IssueDescriptionFormValues { name: string; @@ -126,7 +126,6 @@ export const IssueDescriptionForm: FC = ({ setIsSubmitting={setIsSubmitting} onChange={(description: Object, description_html: string) => { onChange(description_html); - // setValue("description_html", description_html); setValue("description", description); handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting(false)); }} diff --git a/apps/app/components/issues/plugins/UploadHelper.tsx b/apps/app/components/issues/plugins/UploadHelper.tsx deleted file mode 100644 index 5a64d4c0a..000000000 --- a/apps/app/components/issues/plugins/UploadHelper.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import fileService from 'services/file.service'; - -const UploadImageHandler = (file: File): Promise => { - try { - const formData = new FormData(); - formData.append("asset", file); - formData.append("attributes", JSON.stringify({})); - - return new Promise(async (resolve, reject) => { - const imageUrl = await fileService - .uploadFile("plane", formData) - .then((response) => response.asset); - - console.log(imageUrl, "imageurl") - const image = new Image(); - image.src = imageUrl; - image.onload = () => { - resolve(imageUrl); - }; - }) - } - catch (error) { - console.log(error) - return Promise.reject(error); - } -}; - -export default UploadImageHandler; diff --git a/apps/app/components/issues/tiptap.tsx b/apps/app/components/issues/tiptap.tsx deleted file mode 100644 index ddadc9d13..000000000 --- a/apps/app/components/issues/tiptap.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useEditor, EditorContent } from '@tiptap/react'; -import { useDebouncedCallback } from 'use-debounce'; -import { EditorBubbleMenu } from './EditorBubbleMenu'; -import { TiptapExtensions } from './extensions'; -import { TiptapEditorProps } from './props'; - -type TiptapProps = { - value: string; - noBorder?: boolean; - borderOnFocus?: boolean; - customClassName?: string; - onChange?: (json: any, html: string) => void; - setIsSubmitting: (isSubmitting: boolean) => void; -} - -const Tiptap = ({ onChange, setIsSubmitting, value, noBorder, borderOnFocus, customClassName }: TiptapProps) => { - const editor = useEditor({ - editorProps: TiptapEditorProps, - extensions: TiptapExtensions, - content: value, - onUpdate: async ({ editor }) => { - setIsSubmitting(true); - debouncedUpdates({ onChange, editor }); - } - }); - - const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => { - setTimeout(async () => { - if (onChange) { - onChange(editor.getJSON(), editor.getHTML()); - } - }, 500); - }, 1000); - - const editorClassNames = `mt-2 p-3 relative focus:outline-none rounded-md focus:border-custom-border-200 - ${noBorder ? '' : 'border border-custom-border-200' - } ${borderOnFocus ? 'focus:border border-custom-border-200' : 'focus:border-0' - } ${customClassName}`; - - return ( -
{ - editor?.chain().focus().run(); - }} - className={`tiptap-editor-container relative min-h-[150px] ${editorClassNames}`} - > - {editor && } -
- -
-
- ); -}; - -export default Tiptap; diff --git a/apps/app/components/issues/EditorBubbleMenu.tsx b/apps/app/components/tiptap/bubble-menu/index.tsx similarity index 99% rename from apps/app/components/issues/EditorBubbleMenu.tsx rename to apps/app/components/tiptap/bubble-menu/index.tsx index 32ad3f2a9..00b671393 100644 --- a/apps/app/components/issues/EditorBubbleMenu.tsx +++ b/apps/app/components/tiptap/bubble-menu/index.tsx @@ -10,7 +10,7 @@ import { import { NodeSelector } from "./node-selector"; import { LinkSelector } from "./link-selector"; -import { cn } from "./utils"; +import { cn } from "../utils" export interface BubbleMenuItem { name: string; diff --git a/apps/app/components/issues/link-selector.tsx b/apps/app/components/tiptap/bubble-menu/link-selector.tsx similarity index 98% rename from apps/app/components/issues/link-selector.tsx rename to apps/app/components/tiptap/bubble-menu/link-selector.tsx index 7649c271c..62331ebee 100644 --- a/apps/app/components/issues/link-selector.tsx +++ b/apps/app/components/tiptap/bubble-menu/link-selector.tsx @@ -1,7 +1,7 @@ import { Editor } from "@tiptap/core"; import { Check, Trash } from "lucide-react"; import { Dispatch, FC, SetStateAction, useEffect, useRef } from "react"; -import { cn } from './utils'; +import { cn } from '../utils'; interface LinkSelectorProps { editor: Editor; diff --git a/apps/app/components/issues/node-selector.tsx b/apps/app/components/tiptap/bubble-menu/node-selector.tsx similarity index 97% rename from apps/app/components/issues/node-selector.tsx rename to apps/app/components/tiptap/bubble-menu/node-selector.tsx index ebb9767ec..e74bfa400 100644 --- a/apps/app/components/issues/node-selector.tsx +++ b/apps/app/components/tiptap/bubble-menu/node-selector.tsx @@ -13,8 +13,8 @@ import { } from "lucide-react"; import { Dispatch, FC, SetStateAction } from "react"; -import { BubbleMenuItem } from "./EditorBubbleMenu"; -import { cn } from "./utils"; +import { BubbleMenuItem } from "../bubble-menu"; +import { cn } from "../utils"; interface NodeSelectorProps { editor: Editor; diff --git a/apps/app/components/issues/extensions.tsx b/apps/app/components/tiptap/extensions/index.tsx similarity index 93% rename from apps/app/components/issues/extensions.tsx rename to apps/app/components/tiptap/extensions/index.tsx index f34d32fe4..81a6aa6eb 100644 --- a/apps/app/components/issues/extensions.tsx +++ b/apps/app/components/tiptap/extensions/index.tsx @@ -12,13 +12,14 @@ import { Markdown } from "tiptap-markdown"; import Highlight from "@tiptap/extension-highlight"; import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; import { lowlight } from 'lowlight/lib/core' -import SlashCommand from "./slash-command"; +import SlashCommand from "../slash-command"; import { InputRule } from "@tiptap/core"; import ts from 'highlight.js/lib/languages/typescript' import 'highlight.js/styles/github-dark.css'; -import UploadImagesPlugin from "./plugins/upload-image"; +import UploadImagesPlugin from "../plugins/upload-image"; +import UniqueID from "@tiptap-pro/extension-unique-id"; lowlight.registerLanguage('ts', ts) @@ -115,6 +116,9 @@ export const TiptapExtensions = [ }, includeChildren: true, }), + UniqueID.configure({ + types: ['heading', 'paragraph', 'image'], + }), SlashCommand, TiptapUnderline, TextStyle, diff --git a/apps/app/components/tiptap/index.tsx b/apps/app/components/tiptap/index.tsx new file mode 100644 index 000000000..d0365769d --- /dev/null +++ b/apps/app/components/tiptap/index.tsx @@ -0,0 +1,110 @@ +import { useEditor, EditorContent } from '@tiptap/react'; +import { useDebouncedCallback } from 'use-debounce'; +import { EditorBubbleMenu } from './bubble-menu'; +import { TiptapExtensions } from './extensions'; +import { TiptapEditorProps } from './props'; +import { Node } from "@tiptap/pm/model"; +import { Editor as CoreEditor } from "@tiptap/core"; +import { useCallback, useRef } from 'react'; +import { EditorState } from '@tiptap/pm/state'; +import fileService from 'services/file.service'; + +type TiptapProps = { + value: string; + noBorder?: boolean; + borderOnFocus?: boolean; + customClassName?: string; + onChange?: (json: any, html: string) => void; + setIsSubmitting: (isSubmitting: boolean) => void; +} + +const Tiptap = ({ onChange, setIsSubmitting, value, noBorder, borderOnFocus, customClassName }: TiptapProps) => { + const editor = useEditor({ + editorProps: TiptapEditorProps, + extensions: TiptapExtensions, + content: value, + onUpdate: async ({ editor }) => { + setIsSubmitting(true); + checkForNodeDeletions(editor) + debouncedUpdates({ onChange, editor }); + } + }); + + const previousState = useRef(); + + const extractPath = useCallback((url: string, searchString: string) => { + if (url.startsWith(searchString)) { + console.log("chala", url, searchString) + return url.substring(searchString.length); + } + }, []); + + const onNodeDeleted = useCallback( + async (node: Node) => { + if (node.type.name === 'image') { + const assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1); + const resStatus = await fileService.deleteFile(assetUrlWithWorkspaceId); + if (resStatus === 204) { + console.log("file deleted successfully"); + } + } + }, + [], + ); + + const checkForNodeDeletions = useCallback( + (editor: CoreEditor) => { + const prevNodesById: Record = {}; + previousState.current?.doc.forEach((node) => { + if (node.attrs.id) { + prevNodesById[node.attrs.id] = node; + } + }); + + const nodesById: Record = {}; + editor.state?.doc.forEach((node) => { + if (node.attrs.id) { + nodesById[node.attrs.id] = node; + } + }); + + previousState.current = editor.state; + + for (const [id, node] of Object.entries(prevNodesById)) { + if (nodesById[id] === undefined) { + onNodeDeleted(node); + } + } + }, + [onNodeDeleted], + ); + + const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => { + setTimeout(async () => { + if (onChange) { + onChange(editor.getJSON(), editor.getHTML()); + } + }, 500); + }, 1000); + + const editorClassNames = `mt-2 p-3 relative focus:outline-none rounded-md focus:border-custom-border-200 + ${noBorder ? '' : 'border border-custom-border-200' + } ${borderOnFocus ? 'focus:border border-custom-border-200' : 'focus:border-0' + } ${customClassName}`; + + return ( +
{ + editor?.chain().focus().run(); + }} + className={`tiptap-editor-container relative min-h-[150px] ${editorClassNames}`} + > + {editor && } +
+ +
+
+ ); +}; + +export default Tiptap; diff --git a/apps/app/components/issues/plugins/upload-image.tsx b/apps/app/components/tiptap/plugins/upload-image.tsx similarity index 99% rename from apps/app/components/issues/plugins/upload-image.tsx rename to apps/app/components/tiptap/plugins/upload-image.tsx index b1ea94cc2..d92aa38aa 100644 --- a/apps/app/components/issues/plugins/upload-image.tsx +++ b/apps/app/components/tiptap/plugins/upload-image.tsx @@ -42,7 +42,7 @@ const UploadImagesPlugin = () => props: { decorations(state) { return this.getState(state); - }, + } }, }); diff --git a/apps/app/components/issues/props.ts b/apps/app/components/tiptap/props.tsx similarity index 100% rename from apps/app/components/issues/props.ts rename to apps/app/components/tiptap/props.tsx diff --git a/apps/app/components/issues/slash-command.tsx b/apps/app/components/tiptap/slash-command/index.tsx similarity index 92% rename from apps/app/components/issues/slash-command.tsx rename to apps/app/components/tiptap/slash-command/index.tsx index d869001cd..ab93945a9 100644 --- a/apps/app/components/issues/slash-command.tsx +++ b/apps/app/components/tiptap/slash-command/index.tsx @@ -21,7 +21,9 @@ import { Code, MinusSquare, CheckSquare, + ImageIcon, } from "lucide-react"; +import { startImageUpload } from "../plugins/upload-image"; interface CommandItemProps { title: string; @@ -180,6 +182,27 @@ const getSuggestionItems = ({ query }: { query: string }) => command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), }, + { + title: "Image", + description: "Upload an image from your computer.", + searchTerms: ["photo", "picture", "media"], + icon: , + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).run(); + // upload image + const input = document.createElement("input"); + input.type = "file"; + input.accept = "image/*"; + input.onchange = async () => { + if (input.files?.length) { + const file = input.files[0]; + const pos = editor.view.state.selection.from; + startImageUpload(file, editor.view, pos); + } + }; + input.click(); + }, + }, ].filter((item) => { if (typeof query === "string" && query.length > 0) { const search = query.toLowerCase(); diff --git a/apps/app/components/issues/utils.ts b/apps/app/components/tiptap/utils.ts similarity index 100% rename from apps/app/components/issues/utils.ts rename to apps/app/components/tiptap/utils.ts diff --git a/apps/app/package.json b/apps/app/package.json index 77860d7d0..d9911841c 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -27,6 +27,7 @@ "@nivo/scatterplot": "0.80.0", "@sentry/nextjs": "^7.36.0", "@tailwindcss/typography": "^0.5.9", + "@tiptap-pro/extension-unique-id": "^2.1.0", "@tiptap/extension-code-block-lowlight": "^2.0.4", "@tiptap/extension-color": "^2.0.4", "@tiptap/extension-highlight": "^2.0.4", diff --git a/apps/app/services/file.service.ts b/apps/app/services/file.service.ts index ad87e3a19..a80b2ce80 100644 --- a/apps/app/services/file.service.ts +++ b/apps/app/services/file.service.ts @@ -40,12 +40,9 @@ class FileServices extends APIService { }); } - async deleteFile(workspaceId: string, assetUrl: string): Promise { - const lastIndex = assetUrl.lastIndexOf("/"); - const assetId = assetUrl.substring(lastIndex + 1); - - return this.delete(`/api/workspaces/file-assets/${workspaceId}/${assetId}/`) - .then((response) => response?.data) + async deleteFile(assetUrlWithWorkspaceId: string): Promise { + return this.delete(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/`) + .then((response) => response?.status) .catch((error) => { throw error?.response?.data; });