From 6607caade7cc72d4f5dd714ce30cb6af69896259 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:34:20 +0530 Subject: [PATCH] fix: Image restoration fixed (marks/unmarks an image to be deleted after a week) (#2859) * image restoration fixed (marks an image to be deleted after a week) * removed clgs * added image constraints * formatted editor-core package using yarn format * lite-text-editor nothing to format * rich-text-editor nothing to format * formatted document-editor with prettier * modified file service to follow api change * fixed more formatting in document editor * fixed all instances of types with that from the package * fixed delete to work consistently (minor optimizations turned off) * stop duplicate images inside editor * restore image on editor creation say if user A deletes image number 2, user B was also in the same issue and in their screen the image was there, if user B makes certain changes and that gets saved in backend, according to user B image 2 should exist but since user A deleted it, it'll not get restored and get deleted in 7 days, hence I've added a check such that whenever a issue loads we restore all images by default * added restore image function with types * replaced all instances to have restore image logic * fixed issue detail for peek view * disabled option to insert table inside a table --- packages/editor/core/package.json | 4 +- .../editor/core/src/lib/editor-commands.ts | 12 +- .../editor/core/src/types/delete-image.ts | 1 - .../core/src/types/mention-suggestion.ts | 10 -- .../editor/core/src/types/upload-image.ts | 1 - .../list-helpers/find-list-item-pos.ts | 37 ++--- .../list-helpers/has-list-before.ts | 20 +-- .../list-helpers/has-list-item-after.ts | 19 +-- .../list-helpers/has-list-item-before.ts | 19 +-- .../core/src/ui/extensions/image/index.tsx | 126 +++++++++++++++++- .../editor/core/src/ui/extensions/index.tsx | 10 +- .../editor/core/src/ui/hooks/useEditor.tsx | 18 ++- .../core/src/ui/hooks/useReadOnlyEditor.tsx | 2 +- packages/editor/core/src/ui/index.tsx | 8 +- .../core/src/ui/mentions/MentionList.tsx | 3 +- .../editor/core/src/ui/mentions/custom.tsx | 3 +- .../editor/core/src/ui/mentions/index.tsx | 5 +- .../core/src/ui/mentions/mentionNodeView.tsx | 2 +- .../editor/core/src/ui/mentions/suggestion.ts | 2 +- .../core/src/ui/menus/menu-items/index.tsx | 2 +- .../core/src/ui/plugins/delete-image.tsx | 19 ++- .../core/src/ui/plugins/upload-image.tsx | 2 +- packages/editor/core/src/ui/props.tsx | 5 +- .../core/src/ui/read-only/extensions.tsx | 2 +- packages/editor/document-editor/Readme.md | 2 +- packages/editor/document-editor/package.json | 4 +- packages/editor/document-editor/src/index.ts | 9 +- .../src/ui/hooks/use-editor-markings.tsx | 37 ++--- .../editor/document-editor/src/ui/index.tsx | 7 +- .../src/ui/menu/fixed-menu.tsx | 2 +- .../document-editor/src/ui/menu/icon.tsx | 5 +- .../document-editor/src/ui/menu/index.tsx | 2 +- .../editor/document-editor/src/ui/tooltip.tsx | 14 +- .../src/ui/types/editor-types.ts | 9 +- .../src/ui/types/menu-actions.d.ts | 17 ++- .../src/ui/utils/editor-summary-utils.ts | 49 ++++--- .../src/ui/utils/menu-actions.ts | 12 +- packages/editor/lite-text-editor/package.json | 6 +- packages/editor/lite-text-editor/src/index.ts | 5 +- .../editor/lite-text-editor/src/ui/index.tsx | 23 ++-- .../src/ui/menus/fixed-menu/index.tsx | 7 +- packages/editor/rich-text-editor/package.json | 4 +- packages/editor/rich-text-editor/src/index.ts | 6 +- .../src/ui/extensions/index.tsx | 2 +- .../editor/rich-text-editor/src/ui/index.tsx | 29 ++-- packages/editor/types/src/index.ts | 1 + .../editor/types/src/types/restore-image.ts | 1 + .../peek-overview/comment/add-comment.tsx | 1 + .../comment/comment-detail-card.tsx | 1 + space/services/file.service.ts | 16 +++ .../inbox/modals/create-issue-modal.tsx | 1 + web/components/issues/comment/add-comment.tsx | 1 + .../issues/comment/comment-card.tsx | 1 + web/components/issues/description-form.tsx | 1 + web/components/issues/draft-issue-form.tsx | 1 + web/components/issues/form.tsx | 1 + .../activity/comment-card.tsx | 1 + .../activity/comment-editor.tsx | 1 + .../issue-peek-overview/issue-detail.tsx | 68 +++++++--- .../pages/create-update-block-inline.tsx | 2 + .../projects/[projectId]/pages/[pageId].tsx | 1 + web/services/file.service.ts | 12 ++ web/styles/editor.css | 2 + 63 files changed, 471 insertions(+), 225 deletions(-) delete mode 100644 packages/editor/core/src/types/delete-image.ts delete mode 100644 packages/editor/core/src/types/mention-suggestion.ts delete mode 100644 packages/editor/core/src/types/upload-image.ts create mode 100644 packages/editor/types/src/types/restore-image.ts diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index 2efb5805e..cb6b73a52 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -19,7 +19,8 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", - "check-types": "tsc --noEmit" + "check-types": "tsc --noEmit", + "format": "prettier --write \"**/*.{ts,tsx,md}\"" }, "peerDependencies": { "next": "12.3.2", @@ -28,6 +29,7 @@ }, "dependencies": { "@tiptap/core": "^2.1.7", + "@plane/editor-types": "*", "@tiptap/extension-code-block-lowlight": "^2.1.12", "@tiptap/extension-color": "^2.1.11", "@tiptap/extension-image": "^2.1.7", diff --git a/packages/editor/core/src/lib/editor-commands.ts b/packages/editor/core/src/lib/editor-commands.ts index 229341f08..725b72b8b 100644 --- a/packages/editor/core/src/lib/editor-commands.ts +++ b/packages/editor/core/src/lib/editor-commands.ts @@ -1,6 +1,7 @@ +import { UploadImage } from "@plane/editor-types"; import { Editor, Range } from "@tiptap/core"; -import { UploadImage } from "../types/upload-image"; import { startImageUpload } from "../ui/plugins/upload-image"; +import { findTableAncestor } from "./utils"; export const toggleHeadingOne = (editor: Editor, range?: Range) => { if (range) @@ -95,6 +96,15 @@ export const toggleBlockquote = (editor: Editor, range?: Range) => { }; export const insertTableCommand = (editor: Editor, range?: Range) => { + if (typeof window !== "undefined") { + const selection: any = window?.getSelection(); + if (selection.rangeCount !== 0) { + const range = selection.getRangeAt(0); + if (findTableAncestor(range.startContainer)) { + return; + } + } + } if (range) editor .chain() diff --git a/packages/editor/core/src/types/delete-image.ts b/packages/editor/core/src/types/delete-image.ts deleted file mode 100644 index 40bfffe2f..000000000 --- a/packages/editor/core/src/types/delete-image.ts +++ /dev/null @@ -1 +0,0 @@ -export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; diff --git a/packages/editor/core/src/types/mention-suggestion.ts b/packages/editor/core/src/types/mention-suggestion.ts deleted file mode 100644 index dcaa3148d..000000000 --- a/packages/editor/core/src/types/mention-suggestion.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type IMentionSuggestion = { - id: string; - type: string; - avatar: string; - title: string; - subtitle: string; - redirect_uri: string; -}; - -export type IMentionHighlight = string; diff --git a/packages/editor/core/src/types/upload-image.ts b/packages/editor/core/src/types/upload-image.ts deleted file mode 100644 index 3cf1408d2..000000000 --- a/packages/editor/core/src/types/upload-image.ts +++ /dev/null @@ -1 +0,0 @@ -export type UploadImage = (file: File) => Promise; diff --git a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos.ts b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos.ts index 17e80b6af..5312cb20e 100644 --- a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos.ts +++ b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos.ts @@ -1,30 +1,33 @@ -import { getNodeType } from '@tiptap/core' -import { NodeType } from '@tiptap/pm/model' -import { EditorState } from '@tiptap/pm/state' +import { getNodeType } from "@tiptap/core"; +import { NodeType } from "@tiptap/pm/model"; +import { EditorState } from "@tiptap/pm/state"; -export const findListItemPos = (typeOrName: string | NodeType, state: EditorState) => { - const { $from } = state.selection - const nodeType = getNodeType(typeOrName, state.schema) +export const findListItemPos = ( + typeOrName: string | NodeType, + state: EditorState, +) => { + const { $from } = state.selection; + const nodeType = getNodeType(typeOrName, state.schema); - let currentNode = null - let currentDepth = $from.depth - let currentPos = $from.pos - let targetDepth: number | null = null + let currentNode = null; + let currentDepth = $from.depth; + let currentPos = $from.pos; + let targetDepth: number | null = null; while (currentDepth > 0 && targetDepth === null) { - currentNode = $from.node(currentDepth) + currentNode = $from.node(currentDepth); if (currentNode.type === nodeType) { - targetDepth = currentDepth + targetDepth = currentDepth; } else { - currentDepth -= 1 - currentPos -= 1 + currentDepth -= 1; + currentPos -= 1; } } if (targetDepth === null) { - return null + return null; } - return { $pos: state.doc.resolve(currentPos), depth: targetDepth } -} + return { $pos: state.doc.resolve(currentPos), depth: targetDepth }; +}; diff --git a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-before.ts b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-before.ts index f8ae97462..99c8ac18b 100644 --- a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-before.ts +++ b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-before.ts @@ -1,15 +1,19 @@ -import { EditorState } from '@tiptap/pm/state' +import { EditorState } from "@tiptap/pm/state"; -export const hasListBefore = (editorState: EditorState, name: string, parentListTypes: string[]) => { - const { $anchor } = editorState.selection +export const hasListBefore = ( + editorState: EditorState, + name: string, + parentListTypes: string[], +) => { + const { $anchor } = editorState.selection; - const previousNodePos = Math.max(0, $anchor.pos - 2) + const previousNodePos = Math.max(0, $anchor.pos - 2); - const previousNode = editorState.doc.resolve(previousNodePos).node() + const previousNode = editorState.doc.resolve(previousNodePos).node(); if (!previousNode || !parentListTypes.includes(previousNode.type.name)) { - return false + return false; } - return true -} + return true; +}; diff --git a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-item-after.ts b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-item-after.ts index 6a4445514..da20516e1 100644 --- a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-item-after.ts +++ b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-item-after.ts @@ -1,17 +1,20 @@ -import { EditorState } from '@tiptap/pm/state' +import { EditorState } from "@tiptap/pm/state"; -export const hasListItemAfter = (typeOrName: string, state: EditorState): boolean => { - const { $anchor } = state.selection +export const hasListItemAfter = ( + typeOrName: string, + state: EditorState, +): boolean => { + const { $anchor } = state.selection; - const $targetPos = state.doc.resolve($anchor.pos - $anchor.parentOffset - 2) + const $targetPos = state.doc.resolve($anchor.pos - $anchor.parentOffset - 2); if ($targetPos.index() === $targetPos.parent.childCount - 1) { - return false + return false; } if ($targetPos.nodeAfter?.type.name !== typeOrName) { - return false + return false; } - return true -} + return true; +}; diff --git a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-item-before.ts b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-item-before.ts index c5d413cb3..4cb1236ab 100644 --- a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-item-before.ts +++ b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/has-list-item-before.ts @@ -1,17 +1,20 @@ -import { EditorState } from '@tiptap/pm/state' +import { EditorState } from "@tiptap/pm/state"; -export const hasListItemBefore = (typeOrName: string, state: EditorState): boolean => { - const { $anchor } = state.selection +export const hasListItemBefore = ( + typeOrName: string, + state: EditorState, +): boolean => { + const { $anchor } = state.selection; - const $targetPos = state.doc.resolve($anchor.pos - 2) + const $targetPos = state.doc.resolve($anchor.pos - 2); if ($targetPos.index() === 0) { - return false + return false; } if ($targetPos.nodeBefore?.type.name !== typeOrName) { - return false + return false; } - return true -} + return true; +}; diff --git a/packages/editor/core/src/ui/extensions/image/index.tsx b/packages/editor/core/src/ui/extensions/image/index.tsx index aea84c6b8..094a198bd 100644 --- a/packages/editor/core/src/ui/extensions/image/index.tsx +++ b/packages/editor/core/src/ui/extensions/image/index.tsx @@ -1,19 +1,135 @@ -import Image from "@tiptap/extension-image"; -import TrackImageDeletionPlugin from "../../plugins/delete-image"; +import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; +import { Node as ProseMirrorNode } from "@tiptap/pm/model"; import UploadImagesPlugin from "../../plugins/upload-image"; -import { DeleteImage } from "../../../types/delete-image"; +import ImageExt from "@tiptap/extension-image"; +import { onNodeDeleted, onNodeRestored } from "../../plugins/delete-image"; +import { DeleteImage, RestoreImage } from "@plane/editor-types"; + +interface ImageNode extends ProseMirrorNode { + attrs: { + src: string; + id: string; + }; +} + +const deleteKey = new PluginKey("delete-image"); +const IMAGE_NODE_TYPE = "image"; const ImageExtension = ( deleteImage: DeleteImage, + restoreFile: RestoreImage, cancelUploadImage?: () => any, ) => - Image.extend({ + ImageExt.extend({ addProseMirrorPlugins() { return [ UploadImagesPlugin(cancelUploadImage), - TrackImageDeletionPlugin(deleteImage), + new Plugin({ + key: deleteKey, + 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) => { + // 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, oldPos) => { + // if the node is not an image, then return as no point in checking + if (oldNode.type.name !== IMAGE_NODE_TYPE) 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; + this.storage.images.set(src, true); + await onNodeDeleted(src, deleteImage); + }); + }); + + return null; + }, + }), + new Plugin({ + key: new PluginKey("imageRestoration"), + appendTransaction: ( + transactions: readonly Transaction[], + oldState: EditorState, + newState: EditorState, + ) => { + const oldImageSources = new Set(); + oldState.doc.descendants((node) => { + if (node.type.name === IMAGE_NODE_TYPE) { + oldImageSources.add(node.attrs.src); + } + }); + + transactions.forEach((transaction) => { + if (!transaction.docChanged) return; + + const addedImages: ImageNode[] = []; + + newState.doc.descendants((node, pos) => { + if (node.type.name !== IMAGE_NODE_TYPE) return; + if (pos < 0 || pos > newState.doc.content.size) return; + if (oldImageSources.has(node.attrs.src)) return; + addedImages.push(node as ImageNode); + }); + + addedImages.forEach(async (image) => { + const wasDeleted = this.storage.images.get(image.attrs.src); + if (wasDeleted === undefined) { + this.storage.images.set(image.attrs.src, false); + } else if (wasDeleted === true) { + await onNodeRestored(image.attrs.src, restoreFile); + } + }); + }); + return null; + }, + }), ]; }, + + onCreate(this) { + const imageSources = new Set(); + this.editor.state.doc.descendants((node) => { + if (node.type.name === IMAGE_NODE_TYPE) { + imageSources.add(node.attrs.src); + } + }); + imageSources.forEach(async (src) => { + try { + const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); + await restoreFile(assetUrlWithWorkspaceId); + } catch (error) { + console.error("Error restoring image: ", error); + } + }); + }, + + // storage to keep track of image states Map + addStorage() { + return { + images: new Map(), + }; + }, + addAttributes() { return { ...this.parent?.(), diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index 2c6d51ad9..03fd508f7 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -15,14 +15,17 @@ import HorizontalRule from "./horizontal-rule"; import ImageExtension from "./image"; -import { DeleteImage } from "../../types/delete-image"; import { isValidHttpUrl } from "../../lib/utils"; -import { IMentionSuggestion } from "../../types/mention-suggestion"; import { Mentions } from "../mentions"; import { CustomKeymap } from "./keymap"; import { CustomCodeBlock } from "./code"; import { ListKeymap } from "./custom-list-keymap"; +import { + IMentionSuggestion, + DeleteImage, + RestoreImage, +} from "@plane/editor-types"; export const CoreEditorExtensions = ( mentionConfig: { @@ -30,6 +33,7 @@ export const CoreEditorExtensions = ( mentionHighlights: string[]; }, deleteFile: DeleteImage, + restoreFile: RestoreImage, cancelUploadImage?: () => any, ) => [ StarterKit.configure({ @@ -71,7 +75,7 @@ export const CoreEditorExtensions = ( "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", }, }), - ImageExtension(deleteFile, cancelUploadImage).configure({ + ImageExtension(deleteFile, restoreFile, cancelUploadImage).configure({ HTMLAttributes: { class: "rounded-lg border border-custom-border-300", }, diff --git a/packages/editor/core/src/ui/hooks/useEditor.tsx b/packages/editor/core/src/ui/hooks/useEditor.tsx index 51bf725ec..0c0a77ab3 100644 --- a/packages/editor/core/src/ui/hooks/useEditor.tsx +++ b/packages/editor/core/src/ui/hooks/useEditor.tsx @@ -1,22 +1,27 @@ import { useEditor as useCustomEditor, Editor } from "@tiptap/react"; import { useImperativeHandle, useRef, MutableRefObject } from "react"; -import { DeleteImage } from "../../types/delete-image"; import { CoreEditorProps } from "../props"; import { CoreEditorExtensions } from "../extensions"; import { EditorProps } from "@tiptap/pm/view"; import { getTrimmedHTML } from "../../lib/utils"; -import { UploadImage } from "../../types/upload-image"; import { useInitializedContent } from "./useInitializedContent"; -import { IMentionSuggestion } from "../../types/mention-suggestion"; +import { + DeleteImage, + IMentionSuggestion, + RestoreImage, + UploadImage, +} from "@plane/editor-types"; interface CustomEditorProps { uploadFile: UploadImage; + restoreFile: RestoreImage; + deleteFile: DeleteImage; + cancelUploadImage?: () => any; setIsSubmitting?: ( isSubmitting: "submitting" | "submitted" | "saved", ) => void; setShouldShowAlert?: (showAlert: boolean) => void; value: string; - deleteFile: DeleteImage; debouncedUpdatesEnabled?: boolean; onStart?: (json: any, html: string) => void; onChange?: (json: any, html: string) => void; @@ -25,7 +30,6 @@ interface CustomEditorProps { forwardedRef?: any; mentionHighlights?: string[]; mentionSuggestions?: IMentionSuggestion[]; - cancelUploadImage?: () => any; } export const useEditor = ({ @@ -39,6 +43,7 @@ export const useEditor = ({ onChange, setIsSubmitting, forwardedRef, + restoreFile, setShouldShowAlert, mentionHighlights, mentionSuggestions, @@ -56,6 +61,7 @@ export const useEditor = ({ mentionHighlights: mentionHighlights ?? [], }, deleteFile, + restoreFile, cancelUploadImage, ), ...extensions, @@ -63,7 +69,7 @@ export const useEditor = ({ content: typeof value === "string" && value.trim() !== "" ? value : "

", onCreate: async ({ editor }) => { - onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())) + onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())); }, onUpdate: async ({ editor }) => { // for instant feedback loop diff --git a/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx b/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx index 75ebddd3c..f9e48dfd3 100644 --- a/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx +++ b/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx @@ -8,7 +8,7 @@ import { import { CoreReadOnlyEditorExtensions } from "../../ui/read-only/extensions"; import { CoreReadOnlyEditorProps } from "../../ui/read-only/props"; import { EditorProps } from "@tiptap/pm/view"; -import { IMentionSuggestion } from "../../types/mention-suggestion"; +import { IMentionSuggestion } from "@plane/editor-types"; interface CustomReadOnlyEditorProps { value: string; diff --git a/packages/editor/core/src/ui/index.tsx b/packages/editor/core/src/ui/index.tsx index a314a2650..79505baa9 100644 --- a/packages/editor/core/src/ui/index.tsx +++ b/packages/editor/core/src/ui/index.tsx @@ -1,14 +1,16 @@ "use client"; import * as React from "react"; import { Extension } from "@tiptap/react"; -import { UploadImage } from "../types/upload-image"; -import { DeleteImage } from "../types/delete-image"; import { getEditorClassNames } from "../lib/utils"; import { EditorProps } from "@tiptap/pm/view"; import { useEditor } from "./hooks/useEditor"; import { EditorContainer } from "../ui/components/editor-container"; import { EditorContentWrapper } from "../ui/components/editor-content"; -import { IMentionSuggestion } from "../types/mention-suggestion"; +import { + UploadImage, + DeleteImage, + IMentionSuggestion, +} from "@plane/editor-types"; interface ICoreEditor { value: string; diff --git a/packages/editor/core/src/ui/mentions/MentionList.tsx b/packages/editor/core/src/ui/mentions/MentionList.tsx index 48aebaa11..1bc4dc4d1 100644 --- a/packages/editor/core/src/ui/mentions/MentionList.tsx +++ b/packages/editor/core/src/ui/mentions/MentionList.tsx @@ -1,3 +1,4 @@ +import { IMentionSuggestion } from "@plane/editor-types"; import { Editor } from "@tiptap/react"; import React, { forwardRef, @@ -7,8 +8,6 @@ import React, { useState, } from "react"; -import { IMentionSuggestion } from "../../types/mention-suggestion"; - interface MentionListProps { items: IMentionSuggestion[]; command: (item: { diff --git a/packages/editor/core/src/ui/mentions/custom.tsx b/packages/editor/core/src/ui/mentions/custom.tsx index dc4ab5aad..e25da6f47 100644 --- a/packages/editor/core/src/ui/mentions/custom.tsx +++ b/packages/editor/core/src/ui/mentions/custom.tsx @@ -2,7 +2,8 @@ import { Mention, MentionOptions } from "@tiptap/extension-mention"; import { mergeAttributes } from "@tiptap/core"; import { ReactNodeViewRenderer } from "@tiptap/react"; import mentionNodeView from "./mentionNodeView"; -import { IMentionHighlight } from "../../types/mention-suggestion"; +import { IMentionHighlight } from "@plane/editor-types"; + export interface CustomMentionOptions extends MentionOptions { mentionHighlights: IMentionHighlight[]; readonly?: boolean; diff --git a/packages/editor/core/src/ui/mentions/index.tsx b/packages/editor/core/src/ui/mentions/index.tsx index 42ec92554..acbea8f59 100644 --- a/packages/editor/core/src/ui/mentions/index.tsx +++ b/packages/editor/core/src/ui/mentions/index.tsx @@ -2,10 +2,7 @@ import suggestion from "./suggestion"; import { CustomMention } from "./custom"; -import { - IMentionHighlight, - IMentionSuggestion, -} from "../../types/mention-suggestion"; +import { IMentionHighlight, IMentionSuggestion } from "@plane/editor-types"; export const Mentions = ( mentionSuggestions: IMentionSuggestion[], diff --git a/packages/editor/core/src/ui/mentions/mentionNodeView.tsx b/packages/editor/core/src/ui/mentions/mentionNodeView.tsx index 331c701e2..1451b0b22 100644 --- a/packages/editor/core/src/ui/mentions/mentionNodeView.tsx +++ b/packages/editor/core/src/ui/mentions/mentionNodeView.tsx @@ -3,7 +3,7 @@ import { NodeViewWrapper } from "@tiptap/react"; import { cn } from "../../lib/utils"; import { useRouter } from "next/router"; -import { IMentionHighlight } from "../../types/mention-suggestion"; +import { IMentionHighlight } from "@plane/editor-types"; // eslint-disable-next-line import/no-anonymous-default-export export default (props) => { diff --git a/packages/editor/core/src/ui/mentions/suggestion.ts b/packages/editor/core/src/ui/mentions/suggestion.ts index ce09cb092..920808c1a 100644 --- a/packages/editor/core/src/ui/mentions/suggestion.ts +++ b/packages/editor/core/src/ui/mentions/suggestion.ts @@ -3,7 +3,7 @@ import { Editor } from "@tiptap/core"; import tippy from "tippy.js"; import MentionList from "./MentionList"; -import { IMentionSuggestion } from "../../types/mention-suggestion"; +import { IMentionSuggestion } from "@plane/editor-types"; const Suggestion = (suggestions: IMentionSuggestion[]) => ({ items: ({ query }: { query: string }) => diff --git a/packages/editor/core/src/ui/menus/menu-items/index.tsx b/packages/editor/core/src/ui/menus/menu-items/index.tsx index a9a724720..3a78c94e3 100644 --- a/packages/editor/core/src/ui/menus/menu-items/index.tsx +++ b/packages/editor/core/src/ui/menus/menu-items/index.tsx @@ -15,7 +15,6 @@ import { CodeIcon, } from "lucide-react"; import { Editor } from "@tiptap/react"; -import { UploadImage } from "../../../types/upload-image"; import { insertImageCommand, insertTableCommand, @@ -32,6 +31,7 @@ import { toggleTaskList, toggleUnderline, } from "../../../lib/editor-commands"; +import { UploadImage } from "@plane/editor-types"; export interface EditorMenuItem { name: string; diff --git a/packages/editor/core/src/ui/plugins/delete-image.tsx b/packages/editor/core/src/ui/plugins/delete-image.tsx index 48ec244fc..6f4bd46be 100644 --- a/packages/editor/core/src/ui/plugins/delete-image.tsx +++ b/packages/editor/core/src/ui/plugins/delete-image.tsx @@ -1,6 +1,6 @@ import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; import { Node as ProseMirrorNode } from "@tiptap/pm/model"; -import { DeleteImage } from "../../types/delete-image"; +import { DeleteImage, RestoreImage } from "@plane/editor-types"; const deleteKey = new PluginKey("delete-image"); const IMAGE_NODE_TYPE = "image"; @@ -59,7 +59,7 @@ const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin => export default TrackImageDeletionPlugin; -async function onNodeDeleted( +export async function onNodeDeleted( src: string, deleteImage: DeleteImage, ): Promise { @@ -73,3 +73,18 @@ async function onNodeDeleted( console.error("Error deleting image: ", error); } } + +export async function onNodeRestored( + src: string, + restoreImage: RestoreImage, +): Promise { + try { + const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); + const resStatus = await restoreImage(assetUrlWithWorkspaceId); + if (resStatus === 204) { + console.log("Image restored successfully"); + } + } catch (error) { + console.error("Error restoring image: ", error); + } +} diff --git a/packages/editor/core/src/ui/plugins/upload-image.tsx b/packages/editor/core/src/ui/plugins/upload-image.tsx index 256460073..ea40c3c71 100644 --- a/packages/editor/core/src/ui/plugins/upload-image.tsx +++ b/packages/editor/core/src/ui/plugins/upload-image.tsx @@ -1,4 +1,4 @@ -import { UploadImage } from "../../types/upload-image"; +import { UploadImage } from "@plane/editor-types"; import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view"; diff --git a/packages/editor/core/src/ui/props.tsx b/packages/editor/core/src/ui/props.tsx index 865e0d2c7..24a08d844 100644 --- a/packages/editor/core/src/ui/props.tsx +++ b/packages/editor/core/src/ui/props.tsx @@ -1,7 +1,7 @@ +import { UploadImage } from "@plane/editor-types"; import { EditorProps } from "@tiptap/pm/view"; import { findTableAncestor } from "../lib/utils"; import { startImageUpload } from "./plugins/upload-image"; -import { UploadImage } from "../types/upload-image"; export function CoreEditorProps( uploadFile: UploadImage, @@ -82,5 +82,8 @@ export function CoreEditorProps( } return false; }, + transformPastedHTML(html) { + return html.replace(//g, ""); + }, }; } diff --git a/packages/editor/core/src/ui/read-only/extensions.tsx b/packages/editor/core/src/ui/read-only/extensions.tsx index b8fc9bb95..f7ebedccc 100644 --- a/packages/editor/core/src/ui/read-only/extensions.tsx +++ b/packages/editor/core/src/ui/read-only/extensions.tsx @@ -16,7 +16,7 @@ import TableRow from "../extensions/table/table-row/table-row"; import ReadOnlyImageExtension from "../extensions/image/read-only-image"; import { isValidHttpUrl } from "../../lib/utils"; import { Mentions } from "../mentions"; -import { IMentionSuggestion } from "../../types/mention-suggestion"; +import { IMentionSuggestion } from "@plane/editor-types"; export const CoreReadOnlyEditorExtensions = (mentionConfig: { mentionSuggestions: IMentionSuggestion[]; diff --git a/packages/editor/document-editor/Readme.md b/packages/editor/document-editor/Readme.md index edbda8ea3..f019d6827 100644 --- a/packages/editor/document-editor/Readme.md +++ b/packages/editor/document-editor/Readme.md @@ -1 +1 @@ -# Document Editor \ No newline at end of file +# Document Editor diff --git a/packages/editor/document-editor/package.json b/packages/editor/document-editor/package.json index 4c7ffb35e..d1ee6ceb0 100644 --- a/packages/editor/document-editor/package.json +++ b/packages/editor/document-editor/package.json @@ -18,7 +18,8 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", - "check-types": "tsc --noEmit" + "check-types": "tsc --noEmit", + "format": "prettier --write \"**/*.{ts,tsx,md}\"" }, "peerDependencies": { "next": "12.3.2", @@ -30,6 +31,7 @@ "@plane/ui": "*", "@plane/editor-core": "*", "@plane/editor-extensions": "*", + "@plane/editor-types": "*", "@tiptap/core": "^2.1.7", "@tiptap/extension-placeholder": "^2.1.11", "@types/node": "18.15.3", diff --git a/packages/editor/document-editor/src/index.ts b/packages/editor/document-editor/src/index.ts index 53ac942e4..ae48a3b47 100644 --- a/packages/editor/document-editor/src/index.ts +++ b/packages/editor/document-editor/src/index.ts @@ -1,3 +1,6 @@ -export { DocumentEditor, DocumentEditorWithRef } from "./ui" -export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef } from "./ui/readonly" -export { FixedMenu } from "./ui/menu/fixed-menu" +export { DocumentEditor, DocumentEditorWithRef } from "./ui"; +export { + DocumentReadOnlyEditor, + DocumentReadOnlyEditorWithRef, +} from "./ui/readonly"; +export { FixedMenu } from "./ui/menu/fixed-menu"; diff --git a/packages/editor/document-editor/src/ui/hooks/use-editor-markings.tsx b/packages/editor/document-editor/src/ui/hooks/use-editor-markings.tsx index 96bd40101..697a7e493 100644 --- a/packages/editor/document-editor/src/ui/hooks/use-editor-markings.tsx +++ b/packages/editor/document-editor/src/ui/hooks/use-editor-markings.tsx @@ -3,31 +3,34 @@ import { useState } from "react"; import { IMarking } from ".."; export const useEditorMarkings = () => { - - const [markings, setMarkings] = useState([]) + const [markings, setMarkings] = useState([]); const updateMarkings = (json: any) => { - const nodes = json.content as any[] - const tempMarkings: IMarking[] = [] - let h1Sequence: number = 0 - let h2Sequence: number = 0 + const nodes = json.content as any[]; + const tempMarkings: IMarking[] = []; + let h1Sequence: number = 0; + let h2Sequence: number = 0; if (nodes) { nodes.forEach((node) => { - if (node.type === "heading" && (node.attrs.level === 1 || node.attrs.level === 2) && node.content) { + if ( + node.type === "heading" && + (node.attrs.level === 1 || node.attrs.level === 2) && + node.content + ) { tempMarkings.push({ type: "heading", level: node.attrs.level, text: node.content[0].text, - sequence: node.attrs.level === 1 ? ++h1Sequence : ++h2Sequence - }) + sequence: node.attrs.level === 1 ? ++h1Sequence : ++h2Sequence, + }); } - }) + }); } - setMarkings(tempMarkings) - } + setMarkings(tempMarkings); + }; - return { - updateMarkings, - markings, - } -} + return { + updateMarkings, + markings, + }; +}; diff --git a/packages/editor/document-editor/src/ui/index.tsx b/packages/editor/document-editor/src/ui/index.tsx index d2f34989a..f841912aa 100644 --- a/packages/editor/document-editor/src/ui/index.tsx +++ b/packages/editor/document-editor/src/ui/index.tsx @@ -14,15 +14,14 @@ import { DocumentDetails } from "./types/editor-types"; import { PageRenderer } from "./components/page-renderer"; import { getMenuOptions } from "./utils/menu-options"; import { useRouter } from "next/router"; - -export type UploadImage = (file: File) => Promise; -export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; +import { UploadImage, DeleteImage, RestoreImage } from "@plane/editor-types"; interface IDocumentEditor { documentDetails: DocumentDetails; value: string; uploadFile: UploadImage; deleteFile: DeleteImage; + restoreFile: RestoreImage; customClassName?: string; editorContentCustomClassNames?: string; onChange: (json: any, html: string) => void; @@ -62,6 +61,7 @@ const DocumentEditor = ({ value, uploadFile, deleteFile, + restoreFile, customClassName, forwardedRef, duplicationConfig, @@ -82,6 +82,7 @@ const DocumentEditor = ({ updateMarkings(json); }, debouncedUpdatesEnabled, + restoreFile, setIsSubmitting, setShouldShowAlert, value, diff --git a/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx b/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx index 9bafc7983..a11f5f358 100644 --- a/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx +++ b/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx @@ -19,7 +19,7 @@ import { HeadingThreeItem, findTableAncestor, } from "@plane/editor-core"; -import { UploadImage } from ".."; +import { UploadImage } from "@plane/editor-types"; export interface BubbleMenuItem { name: string; diff --git a/packages/editor/document-editor/src/ui/menu/icon.tsx b/packages/editor/document-editor/src/ui/menu/icon.tsx index c0006b3f2..60878f9bf 100644 --- a/packages/editor/document-editor/src/ui/menu/icon.tsx +++ b/packages/editor/document-editor/src/ui/menu/icon.tsx @@ -6,8 +6,9 @@ type Props = { }; export const Icon: React.FC = ({ iconName, className = "" }) => ( - + {iconName} ); - diff --git a/packages/editor/document-editor/src/ui/menu/index.tsx b/packages/editor/document-editor/src/ui/menu/index.tsx index 3abc58022..1c411fabf 100644 --- a/packages/editor/document-editor/src/ui/menu/index.tsx +++ b/packages/editor/document-editor/src/ui/menu/index.tsx @@ -1 +1 @@ -export { FixedMenu } from "./fixed-menu"; \ No newline at end of file +export { FixedMenu } from "./fixed-menu"; diff --git a/packages/editor/document-editor/src/ui/tooltip.tsx b/packages/editor/document-editor/src/ui/tooltip.tsx index f29d8a491..d82ed9600 100644 --- a/packages/editor/document-editor/src/ui/tooltip.tsx +++ b/packages/editor/document-editor/src/ui/tooltip.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import * as React from "react"; // next-themes import { useTheme } from "next-themes"; @@ -69,8 +69,16 @@ export const Tooltip: React.FC = ({ } position={position} - renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) => - React.cloneElement(children, { ref: eleReference, ...tooltipProps, ...children.props }) + renderTarget={({ + isOpen: isTooltipOpen, + ref: eleReference, + ...tooltipProps + }) => + React.cloneElement(children, { + ref: eleReference, + ...tooltipProps, + ...children.props, + }) } /> ); diff --git a/packages/editor/document-editor/src/ui/types/editor-types.ts b/packages/editor/document-editor/src/ui/types/editor-types.ts index 4996c0e3b..10e9b16b6 100644 --- a/packages/editor/document-editor/src/ui/types/editor-types.ts +++ b/packages/editor/document-editor/src/ui/types/editor-types.ts @@ -1,8 +1,7 @@ - export interface DocumentDetails { title: string; - created_by: string; - created_on: Date; - last_updated_by: string; - last_updated_at: Date; + created_by: string; + created_on: Date; + last_updated_by: string; + last_updated_at: Date; } diff --git a/packages/editor/document-editor/src/ui/types/menu-actions.d.ts b/packages/editor/document-editor/src/ui/types/menu-actions.d.ts index ebb253312..87e848be7 100644 --- a/packages/editor/document-editor/src/ui/types/menu-actions.d.ts +++ b/packages/editor/document-editor/src/ui/types/menu-actions.d.ts @@ -1,14 +1,13 @@ - export interface IDuplicationConfig { - action: () => Promise + action: () => Promise; } export interface IPageLockConfig { - is_locked: boolean, - action: () => Promise - locked_by?: string, + is_locked: boolean; + action: () => Promise; + locked_by?: string; } export interface IPageArchiveConfig { - is_archived: boolean, - archived_at?: Date, - action: () => Promise - } + is_archived: boolean; + archived_at?: Date; + action: () => Promise; +} diff --git a/packages/editor/document-editor/src/ui/utils/editor-summary-utils.ts b/packages/editor/document-editor/src/ui/utils/editor-summary-utils.ts index 94d0cbbbc..248f439e3 100644 --- a/packages/editor/document-editor/src/ui/utils/editor-summary-utils.ts +++ b/packages/editor/document-editor/src/ui/utils/editor-summary-utils.ts @@ -2,34 +2,33 @@ import { Editor } from "@tiptap/react"; import { IMarking } from ".."; function findNthH1(editor: Editor, n: number, level: number): number { - let count = 0; - let pos = 0; - editor.state.doc.descendants((node, position) => { - if (node.type.name === 'heading' && node.attrs.level === level) { - count++; - if (count === n) { - pos = position; - return false; - } - } - }); - return pos; - } - - function scrollToNode(editor: Editor, pos: number): void { - const headingNode = editor.state.doc.nodeAt(pos); - if (headingNode) { - const headingDOM = editor.view.nodeDOM(pos); - if (headingDOM instanceof HTMLElement) { - headingDOM.scrollIntoView({ behavior: 'smooth' }); + let count = 0; + let pos = 0; + editor.state.doc.descendants((node, position) => { + if (node.type.name === "heading" && node.attrs.level === level) { + count++; + if (count === n) { + pos = position; + return false; } } - } + }); + return pos; +} - export function scrollSummary(editor: Editor, marking: IMarking) { - if (editor) { - const pos = findNthH1(editor, marking.sequence, marking.level) - scrollToNode(editor, pos) +function scrollToNode(editor: Editor, pos: number): void { + const headingNode = editor.state.doc.nodeAt(pos); + if (headingNode) { + const headingDOM = editor.view.nodeDOM(pos); + if (headingDOM instanceof HTMLElement) { + headingDOM.scrollIntoView({ behavior: "smooth" }); } } +} +export function scrollSummary(editor: Editor, marking: IMarking) { + if (editor) { + const pos = findNthH1(editor, marking.sequence, marking.level); + scrollToNode(editor, pos); + } +} diff --git a/packages/editor/document-editor/src/ui/utils/menu-actions.ts b/packages/editor/document-editor/src/ui/utils/menu-actions.ts index c6fd32c21..24eda5a05 100644 --- a/packages/editor/document-editor/src/ui/utils/menu-actions.ts +++ b/packages/editor/document-editor/src/ui/utils/menu-actions.ts @@ -1,12 +1,12 @@ -import { Editor } from "@tiptap/core" +import { Editor } from "@tiptap/core"; export const copyMarkdownToClipboard = (editor: Editor | null) => { const markdownOutput = editor?.storage.markdown.getMarkdown(); - navigator.clipboard.writeText(markdownOutput) -} + navigator.clipboard.writeText(markdownOutput); +}; export const CopyPageLink = () => { - if (window){ - navigator.clipboard.writeText(window.location.toString()) + if (window) { + navigator.clipboard.writeText(window.location.toString()); } -} +}; diff --git a/packages/editor/lite-text-editor/package.json b/packages/editor/lite-text-editor/package.json index d5c0a4a6f..32e4cde27 100644 --- a/packages/editor/lite-text-editor/package.json +++ b/packages/editor/lite-text-editor/package.json @@ -19,7 +19,8 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", - "check-types": "tsc --noEmit" + "check-types": "tsc --noEmit", + "format": "prettier --write \"**/*.{ts,tsx,md}\"" }, "peerDependencies": { "next": "12.3.2", @@ -29,7 +30,8 @@ }, "dependencies": { "@plane/editor-core": "*", - "@plane/ui": "*" + "@plane/ui": "*", + "@plane/editor-types": "*" }, "devDependencies": { "@types/node": "18.15.3", diff --git a/packages/editor/lite-text-editor/src/index.ts b/packages/editor/lite-text-editor/src/index.ts index ba916e666..49001e055 100644 --- a/packages/editor/lite-text-editor/src/index.ts +++ b/packages/editor/lite-text-editor/src/index.ts @@ -1,3 +1,6 @@ export { LiteTextEditor, LiteTextEditorWithRef } from "./ui"; export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef } from "./ui/read-only"; -export type { IMentionSuggestion, IMentionHighlight } from "./ui"; +export type { + IMentionSuggestion, + IMentionHighlight, +} from "@plane/editor-types"; diff --git a/packages/editor/lite-text-editor/src/ui/index.tsx b/packages/editor/lite-text-editor/src/ui/index.tsx index e7decbcac..b0cf3ebbb 100644 --- a/packages/editor/lite-text-editor/src/ui/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/index.tsx @@ -7,24 +7,19 @@ import { } from "@plane/editor-core"; import { FixedMenu } from "./menus/fixed-menu"; import { LiteTextEditorExtensions } from "./extensions"; - -export type UploadImage = (file: File) => Promise; -export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; -export type IMentionSuggestion = { - id: string; - type: string; - avatar: string; - title: string; - subtitle: string; - redirect_uri: string; -}; - -export type IMentionHighlight = string; +import { + UploadImage, + DeleteImage, + IMentionSuggestion, + RestoreImage, +} from "@plane/editor-types"; interface ILiteTextEditor { value: string; uploadFile: UploadImage; deleteFile: DeleteImage; + restoreFile: RestoreImage; + noBorder?: boolean; borderOnFocus?: boolean; customClassName?: string; @@ -73,6 +68,7 @@ const LiteTextEditor = (props: LiteTextEditorProps) => { value, uploadFile, deleteFile, + restoreFile, noBorder, borderOnFocus, customClassName, @@ -93,6 +89,7 @@ const LiteTextEditor = (props: LiteTextEditorProps) => { value, uploadFile, deleteFile, + restoreFile, forwardedRef, extensions: LiteTextEditorExtensions(onEnterKeyPress), mentionHighlights, diff --git a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx index 94d0b2d4e..2f727936c 100644 --- a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx @@ -16,11 +16,12 @@ import { UnderLineItem, } from "@plane/editor-core"; import { Tooltip } from "@plane/ui"; -import { UploadImage } from "../../"; import type { SVGProps } from "react"; +import { UploadImage } from "@plane/editor-types"; + interface LucideProps extends Partial> { - size?: string | number - absoluteStrokeWidth?: boolean + size?: string | number; + absoluteStrokeWidth?: boolean; } type LucideIcon = (props: LucideProps) => JSX.Element; diff --git a/packages/editor/rich-text-editor/package.json b/packages/editor/rich-text-editor/package.json index b79376279..14ef30c3a 100644 --- a/packages/editor/rich-text-editor/package.json +++ b/packages/editor/rich-text-editor/package.json @@ -19,7 +19,8 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", - "check-types": "tsc --noEmit" + "check-types": "tsc --noEmit", + "format": "prettier --write \"**/*.{ts,tsx,md}\"" }, "peerDependencies": { "next": "12.3.2", @@ -30,6 +31,7 @@ "dependencies": { "@plane/editor-core": "*", "@tiptap/core": "^2.1.11", + "@plane/editor-types": "*", "@plane/editor-extensions": "*", "@tiptap/extension-placeholder": "^2.1.11", "lucide-react": "^0.244.0" diff --git a/packages/editor/rich-text-editor/src/index.ts b/packages/editor/rich-text-editor/src/index.ts index 0b854c0ae..e9f536d9a 100644 --- a/packages/editor/rich-text-editor/src/index.ts +++ b/packages/editor/rich-text-editor/src/index.ts @@ -1,3 +1,7 @@ export { RichTextEditor, RichTextEditorWithRef } from "./ui"; export { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "./ui/read-only"; -export type { IMentionSuggestion, IMentionHighlight } from "./ui"; +export type { RichTextEditorProps, IRichTextEditor } from "./ui"; +export type { + IMentionHighlight, + IMentionSuggestion, +} from "@plane/editor-types"; diff --git a/packages/editor/rich-text-editor/src/ui/extensions/index.tsx b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx index 0531e3355..0464034cb 100644 --- a/packages/editor/rich-text-editor/src/ui/extensions/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx @@ -1,7 +1,7 @@ import { SlashCommand } from "@plane/editor-extensions"; import Placeholder from "@tiptap/extension-placeholder"; import { DragAndDrop } from "@plane/editor-extensions"; -import { UploadImage } from "../"; +import { UploadImage } from "@plane/editor-types"; export const RichTextEditorExtensions = ( uploadFile: UploadImage, diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx index 81bbdb597..8757edf13 100644 --- a/packages/editor/rich-text-editor/src/ui/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/index.tsx @@ -8,25 +8,18 @@ import { } from "@plane/editor-core"; import { EditorBubbleMenu } from "./menus/bubble-menu"; import { RichTextEditorExtensions } from "./extensions"; +import { + DeleteImage, + IMentionSuggestion, + RestoreImage, + UploadImage, +} from "@plane/editor-types"; -export type UploadImage = (file: File) => Promise; -export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; - -export type IMentionSuggestion = { - id: string; - type: string; - avatar: string; - title: string; - subtitle: string; - redirect_uri: string; -}; - -export type IMentionHighlight = string; - -interface IRichTextEditor { +export type IRichTextEditor = { value: string; dragDropEnabled?: boolean; uploadFile: UploadImage; + restoreFile: RestoreImage; deleteFile: DeleteImage; noBorder?: boolean; borderOnFocus?: boolean; @@ -42,9 +35,9 @@ interface IRichTextEditor { debouncedUpdatesEnabled?: boolean; mentionHighlights?: string[]; mentionSuggestions?: IMentionSuggestion[]; -} +}; -interface RichTextEditorProps extends IRichTextEditor { +export interface RichTextEditorProps extends IRichTextEditor { forwardedRef?: React.Ref; } @@ -67,6 +60,7 @@ const RichTextEditor = ({ cancelUploadImage, borderOnFocus, customClassName, + restoreFile, forwardedRef, mentionHighlights, mentionSuggestions, @@ -80,6 +74,7 @@ const RichTextEditor = ({ uploadFile, cancelUploadImage, deleteFile, + restoreFile, forwardedRef, extensions: RichTextEditorExtensions( uploadFile, diff --git a/packages/editor/types/src/index.ts b/packages/editor/types/src/index.ts index 3cadcc3d5..58b584977 100644 --- a/packages/editor/types/src/index.ts +++ b/packages/editor/types/src/index.ts @@ -1,5 +1,6 @@ export type { DeleteImage } from "./types/delete-image"; export type { UploadImage } from "./types/upload-image"; +export type { RestoreImage } from "./types/restore-image"; export type { IMentionHighlight, IMentionSuggestion, diff --git a/packages/editor/types/src/types/restore-image.ts b/packages/editor/types/src/types/restore-image.ts new file mode 100644 index 000000000..9b33177b7 --- /dev/null +++ b/packages/editor/types/src/types/restore-image.ts @@ -0,0 +1 @@ +export type RestoreImage = (assetUrlWithWorkspaceId: string) => Promise; diff --git a/space/components/issues/peek-overview/comment/add-comment.tsx b/space/components/issues/peek-overview/comment/add-comment.tsx index 9878fd00a..d6c3ce4e6 100644 --- a/space/components/issues/peek-overview/comment/add-comment.tsx +++ b/space/components/issues/peek-overview/comment/add-comment.tsx @@ -79,6 +79,7 @@ export const AddComment: React.FC = observer((props) => { cancelUploadImage={fileService.cancelUpload} uploadFile={fileService.getUploadFileFunction(workspace_slug as string)} deleteFile={fileService.deleteImage} + restoreFile={fileService.restoreImage} ref={editorRef} value={ !value || value === "" || (typeof value === "object" && Object.keys(value).length === 0) diff --git a/space/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/components/issues/peek-overview/comment/comment-detail-card.tsx index ab09b2490..2f045fa3e 100644 --- a/space/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -106,6 +106,7 @@ export const CommentCard: React.FC = observer((props) => { cancelUploadImage={fileService.cancelUpload} uploadFile={fileService.getUploadFileFunction(workspaceSlug)} deleteFile={fileService.deleteImage} + restoreFile={fileService.restoreImage} ref={editorRef} value={value} debouncedUpdatesEnabled={false} diff --git a/space/services/file.service.ts b/space/services/file.service.ts index ce1f50e70..b2d1f6ccd 100644 --- a/space/services/file.service.ts +++ b/space/services/file.service.ts @@ -1,4 +1,6 @@ +// services import APIService from "services/api.service"; +// helpers import { API_BASE_URL } from "helpers/common.helper"; import axios from "axios"; @@ -33,6 +35,7 @@ class FileService extends APIService { super(API_BASE_URL); this.uploadFile = this.uploadFile.bind(this); this.deleteImage = this.deleteImage.bind(this); + this.restoreImage = this.restoreImage.bind(this); this.cancelUpload = this.cancelUpload.bind(this); } @@ -50,6 +53,7 @@ class FileService extends APIService { if (axios.isCancel(error)) { console.log(error.message); } else { + console.log(error); throw error?.response?.data; } }); @@ -58,6 +62,7 @@ class FileService extends APIService { cancelUpload() { this.cancelSource.cancel("Upload cancelled"); } + getUploadFileFunction(workspaceSlug: string): (file: File) => Promise { return async (file: File) => { const formData = new FormData(); @@ -77,6 +82,17 @@ class FileService extends APIService { }); } + async restoreImage(assetUrlWithWorkspaceId: string): Promise { + return this.post(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/restore/`, { + headers: this.getHeaders(), + "Content-Type": "application/json", + }) + .then((response) => response?.status) + .catch((error) => { + throw error?.response?.data; + }); + } + async deleteFile(workspaceId: string, assetUrl: string): Promise { const lastIndex = assetUrl.lastIndexOf("/"); const assetId = assetUrl.substring(lastIndex + 1); diff --git a/web/components/inbox/modals/create-issue-modal.tsx b/web/components/inbox/modals/create-issue-modal.tsx index 5f8ac3fda..3a0746da6 100644 --- a/web/components/inbox/modals/create-issue-modal.tsx +++ b/web/components/inbox/modals/create-issue-modal.tsx @@ -155,6 +155,7 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { cancelUploadImage={fileService.cancelUpload} uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)} deleteFile={fileService.deleteImage} + restoreFile={fileService.restoreImage} ref={editorRef} debouncedUpdatesEnabled={false} value={!value || value === "" ? "

" : value} diff --git a/web/components/issues/comment/add-comment.tsx b/web/components/issues/comment/add-comment.tsx index ee7805ef7..943b03b91 100644 --- a/web/components/issues/comment/add-comment.tsx +++ b/web/components/issues/comment/add-comment.tsx @@ -87,6 +87,7 @@ export const AddComment: React.FC = ({ disabled = false, onSubmit, showAc cancelUploadImage={fileService.cancelUpload} uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)} deleteFile={fileService.deleteImage} + restoreFile={fileService.restoreImage} ref={editorRef} value={!commentValue || commentValue === "" ? "

" : commentValue} customClassName="p-2 h-full" diff --git a/web/components/issues/comment/comment-card.tsx b/web/components/issues/comment/comment-card.tsx index 577050572..19fbd815d 100644 --- a/web/components/issues/comment/comment-card.tsx +++ b/web/components/issues/comment/comment-card.tsx @@ -108,6 +108,7 @@ export const CommentCard: React.FC = ({ cancelUploadImage={fileService.cancelUpload} uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)} deleteFile={fileService.deleteImage} + restoreFile={fileService.restoreImage} ref={editorRef} value={watch("comment_html")} debouncedUpdatesEnabled={false} diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index 2e59ed91a..9043b857d 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -148,6 +148,7 @@ export const IssueDescriptionForm: FC = (props) => { cancelUploadImage={fileService.cancelUpload} uploadFile={fileService.getUploadFileFunction(workspaceSlug)} deleteFile={fileService.deleteImage} + restoreFile={fileService.restoreImage} value={value} setShouldShowAlert={setShowAlert} setIsSubmitting={setIsSubmitting} diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx index be9857dc8..a342b2d11 100644 --- a/web/components/issues/draft-issue-form.tsx +++ b/web/components/issues/draft-issue-form.tsx @@ -425,6 +425,7 @@ export const DraftIssueForm: FC = observer((props) => { cancelUploadImage={fileService.cancelUpload} uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)} deleteFile={fileService.deleteImage} + restoreFile={fileService.restoreImage} ref={editorRef} debouncedUpdatesEnabled={false} value={ diff --git a/web/components/issues/form.tsx b/web/components/issues/form.tsx index eff3b6193..e58bed089 100644 --- a/web/components/issues/form.tsx +++ b/web/components/issues/form.tsx @@ -379,6 +379,7 @@ export const IssueForm: FC = observer((props) => { cancelUploadImage={fileService.cancelUpload} uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)} deleteFile={fileService.deleteImage} + restoreFile={fileService.restoreImage} ref={editorRef} debouncedUpdatesEnabled={false} value={ diff --git a/web/components/issues/issue-peek-overview/activity/comment-card.tsx b/web/components/issues/issue-peek-overview/activity/comment-card.tsx index e75013516..f576e767e 100644 --- a/web/components/issues/issue-peek-overview/activity/comment-card.tsx +++ b/web/components/issues/issue-peek-overview/activity/comment-card.tsx @@ -117,6 +117,7 @@ export const IssueCommentCard: React.FC = (props) => { cancelUploadImage={fileService.cancelUpload} uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)} deleteFile={fileService.deleteImage} + restoreFile={fileService.restoreImage} ref={editorRef} value={watch("comment_html")} debouncedUpdatesEnabled={false} diff --git a/web/components/issues/issue-peek-overview/activity/comment-editor.tsx b/web/components/issues/issue-peek-overview/activity/comment-editor.tsx index 6b537cb45..6793c2daf 100644 --- a/web/components/issues/issue-peek-overview/activity/comment-editor.tsx +++ b/web/components/issues/issue-peek-overview/activity/comment-editor.tsx @@ -88,6 +88,7 @@ export const IssueCommentEditor: React.FC = (props) => { cancelUploadImage={fileService.cancelUpload} uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)} deleteFile={fileService.deleteImage} + restoreFile={fileService.restoreImage} ref={editorRef} value={!commentValue || commentValue === "" ? "

" : commentValue} customClassName="p-2 h-full" diff --git a/web/components/issues/issue-peek-overview/issue-detail.tsx b/web/components/issues/issue-peek-overview/issue-detail.tsx index 0e3c12c88..746c2592b 100644 --- a/web/components/issues/issue-peek-overview/issue-detail.tsx +++ b/web/components/issues/issue-peek-overview/issue-detail.tsx @@ -66,11 +66,15 @@ export const PeekOverviewIssueDetails: FC = (props) = [issue, issueUpdate] ); - const debouncedIssueDescription = useDebouncedCallback(async (_data: any) => { - issueUpdate({ ...issue, description_html: _data }); - }, 1500); + const [localTitleValue, setLocalTitleValue] = useState(""); + const issueTitleCurrentValue = watch("name"); + useEffect(() => { + if (localTitleValue === "" && issueTitleCurrentValue !== "") { + setLocalTitleValue(issueTitleCurrentValue); + } + }, [issueTitleCurrentValue, localTitleValue]); - const debouncedTitleSave = useDebouncedCallback(async () => { + const debouncedFormSave = useDebouncedCallback(async () => { handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted")); }, 1500); @@ -105,18 +109,19 @@ export const PeekOverviewIssueDetails: FC = (props) = ( + render={({ field: { onChange } }) => (