forked from github/plane
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
This commit is contained in:
parent
0fcadca53a
commit
e01ca97fc9
@ -19,7 +19,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"dev": "tsup --watch",
|
"dev": "tsup --watch",
|
||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit",
|
||||||
|
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"next": "12.3.2",
|
"next": "12.3.2",
|
||||||
@ -28,6 +29,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tiptap/core": "^2.1.7",
|
"@tiptap/core": "^2.1.7",
|
||||||
|
"@plane/editor-types": "*",
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.1.12",
|
"@tiptap/extension-code-block-lowlight": "^2.1.12",
|
||||||
"@tiptap/extension-color": "^2.1.11",
|
"@tiptap/extension-color": "^2.1.11",
|
||||||
"@tiptap/extension-image": "^2.1.7",
|
"@tiptap/extension-image": "^2.1.7",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import { UploadImage } from "@plane/editor-types";
|
||||||
import { Editor, Range } from "@tiptap/core";
|
import { Editor, Range } from "@tiptap/core";
|
||||||
import { UploadImage } from "../types/upload-image";
|
|
||||||
import { startImageUpload } from "../ui/plugins/upload-image";
|
import { startImageUpload } from "../ui/plugins/upload-image";
|
||||||
|
import { findTableAncestor } from "./utils";
|
||||||
|
|
||||||
export const toggleHeadingOne = (editor: Editor, range?: Range) => {
|
export const toggleHeadingOne = (editor: Editor, range?: Range) => {
|
||||||
if (range)
|
if (range)
|
||||||
@ -95,6 +96,15 @@ export const toggleBlockquote = (editor: Editor, range?: Range) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const insertTableCommand = (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)
|
if (range)
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
|
@ -1,10 +0,0 @@
|
|||||||
export type IMentionSuggestion = {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
avatar: string;
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
redirect_uri: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IMentionHighlight = string;
|
|
@ -1 +0,0 @@
|
|||||||
export type UploadImage = (file: File) => Promise<string>;
|
|
@ -1,30 +1,33 @@
|
|||||||
import { getNodeType } from '@tiptap/core'
|
import { getNodeType } from "@tiptap/core";
|
||||||
import { NodeType } from '@tiptap/pm/model'
|
import { NodeType } from "@tiptap/pm/model";
|
||||||
import { EditorState } from '@tiptap/pm/state'
|
import { EditorState } from "@tiptap/pm/state";
|
||||||
|
|
||||||
export const findListItemPos = (typeOrName: string | NodeType, state: EditorState) => {
|
export const findListItemPos = (
|
||||||
const { $from } = state.selection
|
typeOrName: string | NodeType,
|
||||||
const nodeType = getNodeType(typeOrName, state.schema)
|
state: EditorState,
|
||||||
|
) => {
|
||||||
|
const { $from } = state.selection;
|
||||||
|
const nodeType = getNodeType(typeOrName, state.schema);
|
||||||
|
|
||||||
let currentNode = null
|
let currentNode = null;
|
||||||
let currentDepth = $from.depth
|
let currentDepth = $from.depth;
|
||||||
let currentPos = $from.pos
|
let currentPos = $from.pos;
|
||||||
let targetDepth: number | null = null
|
let targetDepth: number | null = null;
|
||||||
|
|
||||||
while (currentDepth > 0 && targetDepth === null) {
|
while (currentDepth > 0 && targetDepth === null) {
|
||||||
currentNode = $from.node(currentDepth)
|
currentNode = $from.node(currentDepth);
|
||||||
|
|
||||||
if (currentNode.type === nodeType) {
|
if (currentNode.type === nodeType) {
|
||||||
targetDepth = currentDepth
|
targetDepth = currentDepth;
|
||||||
} else {
|
} else {
|
||||||
currentDepth -= 1
|
currentDepth -= 1;
|
||||||
currentPos -= 1
|
currentPos -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetDepth === null) {
|
if (targetDepth === null) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { $pos: state.doc.resolve(currentPos), depth: targetDepth }
|
return { $pos: state.doc.resolve(currentPos), depth: targetDepth };
|
||||||
}
|
};
|
||||||
|
@ -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[]) => {
|
export const hasListBefore = (
|
||||||
const { $anchor } = editorState.selection
|
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)) {
|
if (!previousNode || !parentListTypes.includes(previousNode.type.name)) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
};
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import { EditorState } from '@tiptap/pm/state'
|
import { EditorState } from "@tiptap/pm/state";
|
||||||
|
|
||||||
export const hasListItemAfter = (typeOrName: string, state: EditorState): boolean => {
|
export const hasListItemAfter = (
|
||||||
const { $anchor } = state.selection
|
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) {
|
if ($targetPos.index() === $targetPos.parent.childCount - 1) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($targetPos.nodeAfter?.type.name !== typeOrName) {
|
if ($targetPos.nodeAfter?.type.name !== typeOrName) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
};
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import { EditorState } from '@tiptap/pm/state'
|
import { EditorState } from "@tiptap/pm/state";
|
||||||
|
|
||||||
export const hasListItemBefore = (typeOrName: string, state: EditorState): boolean => {
|
export const hasListItemBefore = (
|
||||||
const { $anchor } = state.selection
|
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) {
|
if ($targetPos.index() === 0) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($targetPos.nodeBefore?.type.name !== typeOrName) {
|
if ($targetPos.nodeBefore?.type.name !== typeOrName) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
};
|
||||||
|
@ -1,19 +1,135 @@
|
|||||||
import Image from "@tiptap/extension-image";
|
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
|
||||||
import TrackImageDeletionPlugin from "../../plugins/delete-image";
|
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||||
import UploadImagesPlugin from "../../plugins/upload-image";
|
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 = (
|
const ImageExtension = (
|
||||||
deleteImage: DeleteImage,
|
deleteImage: DeleteImage,
|
||||||
|
restoreFile: RestoreImage,
|
||||||
cancelUploadImage?: () => any,
|
cancelUploadImage?: () => any,
|
||||||
) =>
|
) =>
|
||||||
Image.extend({
|
ImageExt.extend({
|
||||||
addProseMirrorPlugins() {
|
addProseMirrorPlugins() {
|
||||||
return [
|
return [
|
||||||
UploadImagesPlugin(cancelUploadImage),
|
UploadImagesPlugin(cancelUploadImage),
|
||||||
TrackImageDeletionPlugin(deleteImage),
|
new Plugin({
|
||||||
|
key: deleteKey,
|
||||||
|
appendTransaction: (
|
||||||
|
transactions: readonly Transaction[],
|
||||||
|
oldState: EditorState,
|
||||||
|
newState: EditorState,
|
||||||
|
) => {
|
||||||
|
const newImageSources = new Set<string>();
|
||||||
|
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<string>();
|
||||||
|
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<string>();
|
||||||
|
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<src, isDeleted>
|
||||||
|
addStorage() {
|
||||||
|
return {
|
||||||
|
images: new Map<string, boolean>(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
...this.parent?.(),
|
...this.parent?.(),
|
||||||
|
@ -15,14 +15,17 @@ import HorizontalRule from "./horizontal-rule";
|
|||||||
|
|
||||||
import ImageExtension from "./image";
|
import ImageExtension from "./image";
|
||||||
|
|
||||||
import { DeleteImage } from "../../types/delete-image";
|
|
||||||
import { isValidHttpUrl } from "../../lib/utils";
|
import { isValidHttpUrl } from "../../lib/utils";
|
||||||
import { IMentionSuggestion } from "../../types/mention-suggestion";
|
|
||||||
import { Mentions } from "../mentions";
|
import { Mentions } from "../mentions";
|
||||||
|
|
||||||
import { CustomKeymap } from "./keymap";
|
import { CustomKeymap } from "./keymap";
|
||||||
import { CustomCodeBlock } from "./code";
|
import { CustomCodeBlock } from "./code";
|
||||||
import { ListKeymap } from "./custom-list-keymap";
|
import { ListKeymap } from "./custom-list-keymap";
|
||||||
|
import {
|
||||||
|
IMentionSuggestion,
|
||||||
|
DeleteImage,
|
||||||
|
RestoreImage,
|
||||||
|
} from "@plane/editor-types";
|
||||||
|
|
||||||
export const CoreEditorExtensions = (
|
export const CoreEditorExtensions = (
|
||||||
mentionConfig: {
|
mentionConfig: {
|
||||||
@ -30,6 +33,7 @@ export const CoreEditorExtensions = (
|
|||||||
mentionHighlights: string[];
|
mentionHighlights: string[];
|
||||||
},
|
},
|
||||||
deleteFile: DeleteImage,
|
deleteFile: DeleteImage,
|
||||||
|
restoreFile: RestoreImage,
|
||||||
cancelUploadImage?: () => any,
|
cancelUploadImage?: () => any,
|
||||||
) => [
|
) => [
|
||||||
StarterKit.configure({
|
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",
|
"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: {
|
HTMLAttributes: {
|
||||||
class: "rounded-lg border border-custom-border-300",
|
class: "rounded-lg border border-custom-border-300",
|
||||||
},
|
},
|
||||||
|
@ -1,22 +1,27 @@
|
|||||||
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
||||||
import { useImperativeHandle, useRef, MutableRefObject } from "react";
|
import { useImperativeHandle, useRef, MutableRefObject } from "react";
|
||||||
import { DeleteImage } from "../../types/delete-image";
|
|
||||||
import { CoreEditorProps } from "../props";
|
import { CoreEditorProps } from "../props";
|
||||||
import { CoreEditorExtensions } from "../extensions";
|
import { CoreEditorExtensions } from "../extensions";
|
||||||
import { EditorProps } from "@tiptap/pm/view";
|
import { EditorProps } from "@tiptap/pm/view";
|
||||||
import { getTrimmedHTML } from "../../lib/utils";
|
import { getTrimmedHTML } from "../../lib/utils";
|
||||||
import { UploadImage } from "../../types/upload-image";
|
|
||||||
import { useInitializedContent } from "./useInitializedContent";
|
import { useInitializedContent } from "./useInitializedContent";
|
||||||
import { IMentionSuggestion } from "../../types/mention-suggestion";
|
import {
|
||||||
|
DeleteImage,
|
||||||
|
IMentionSuggestion,
|
||||||
|
RestoreImage,
|
||||||
|
UploadImage,
|
||||||
|
} from "@plane/editor-types";
|
||||||
|
|
||||||
interface CustomEditorProps {
|
interface CustomEditorProps {
|
||||||
uploadFile: UploadImage;
|
uploadFile: UploadImage;
|
||||||
|
restoreFile: RestoreImage;
|
||||||
|
deleteFile: DeleteImage;
|
||||||
|
cancelUploadImage?: () => any;
|
||||||
setIsSubmitting?: (
|
setIsSubmitting?: (
|
||||||
isSubmitting: "submitting" | "submitted" | "saved",
|
isSubmitting: "submitting" | "submitted" | "saved",
|
||||||
) => void;
|
) => void;
|
||||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||||
value: string;
|
value: string;
|
||||||
deleteFile: DeleteImage;
|
|
||||||
debouncedUpdatesEnabled?: boolean;
|
debouncedUpdatesEnabled?: boolean;
|
||||||
onStart?: (json: any, html: string) => void;
|
onStart?: (json: any, html: string) => void;
|
||||||
onChange?: (json: any, html: string) => void;
|
onChange?: (json: any, html: string) => void;
|
||||||
@ -25,7 +30,6 @@ interface CustomEditorProps {
|
|||||||
forwardedRef?: any;
|
forwardedRef?: any;
|
||||||
mentionHighlights?: string[];
|
mentionHighlights?: string[];
|
||||||
mentionSuggestions?: IMentionSuggestion[];
|
mentionSuggestions?: IMentionSuggestion[];
|
||||||
cancelUploadImage?: () => any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useEditor = ({
|
export const useEditor = ({
|
||||||
@ -39,6 +43,7 @@ export const useEditor = ({
|
|||||||
onChange,
|
onChange,
|
||||||
setIsSubmitting,
|
setIsSubmitting,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
|
restoreFile,
|
||||||
setShouldShowAlert,
|
setShouldShowAlert,
|
||||||
mentionHighlights,
|
mentionHighlights,
|
||||||
mentionSuggestions,
|
mentionSuggestions,
|
||||||
@ -56,6 +61,7 @@ export const useEditor = ({
|
|||||||
mentionHighlights: mentionHighlights ?? [],
|
mentionHighlights: mentionHighlights ?? [],
|
||||||
},
|
},
|
||||||
deleteFile,
|
deleteFile,
|
||||||
|
restoreFile,
|
||||||
cancelUploadImage,
|
cancelUploadImage,
|
||||||
),
|
),
|
||||||
...extensions,
|
...extensions,
|
||||||
@ -63,7 +69,7 @@ export const useEditor = ({
|
|||||||
content:
|
content:
|
||||||
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
|
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
|
||||||
onCreate: async ({ editor }) => {
|
onCreate: async ({ editor }) => {
|
||||||
onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()))
|
onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
|
||||||
},
|
},
|
||||||
onUpdate: async ({ editor }) => {
|
onUpdate: async ({ editor }) => {
|
||||||
// for instant feedback loop
|
// for instant feedback loop
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
import { CoreReadOnlyEditorExtensions } from "../../ui/read-only/extensions";
|
import { CoreReadOnlyEditorExtensions } from "../../ui/read-only/extensions";
|
||||||
import { CoreReadOnlyEditorProps } from "../../ui/read-only/props";
|
import { CoreReadOnlyEditorProps } from "../../ui/read-only/props";
|
||||||
import { EditorProps } from "@tiptap/pm/view";
|
import { EditorProps } from "@tiptap/pm/view";
|
||||||
import { IMentionSuggestion } from "../../types/mention-suggestion";
|
import { IMentionSuggestion } from "@plane/editor-types";
|
||||||
|
|
||||||
interface CustomReadOnlyEditorProps {
|
interface CustomReadOnlyEditorProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Extension } from "@tiptap/react";
|
import { Extension } from "@tiptap/react";
|
||||||
import { UploadImage } from "../types/upload-image";
|
|
||||||
import { DeleteImage } from "../types/delete-image";
|
|
||||||
import { getEditorClassNames } from "../lib/utils";
|
import { getEditorClassNames } from "../lib/utils";
|
||||||
import { EditorProps } from "@tiptap/pm/view";
|
import { EditorProps } from "@tiptap/pm/view";
|
||||||
import { useEditor } from "./hooks/useEditor";
|
import { useEditor } from "./hooks/useEditor";
|
||||||
import { EditorContainer } from "../ui/components/editor-container";
|
import { EditorContainer } from "../ui/components/editor-container";
|
||||||
import { EditorContentWrapper } from "../ui/components/editor-content";
|
import { EditorContentWrapper } from "../ui/components/editor-content";
|
||||||
import { IMentionSuggestion } from "../types/mention-suggestion";
|
import {
|
||||||
|
UploadImage,
|
||||||
|
DeleteImage,
|
||||||
|
IMentionSuggestion,
|
||||||
|
} from "@plane/editor-types";
|
||||||
|
|
||||||
interface ICoreEditor {
|
interface ICoreEditor {
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { IMentionSuggestion } from "@plane/editor-types";
|
||||||
import { Editor } from "@tiptap/react";
|
import { Editor } from "@tiptap/react";
|
||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
@ -7,8 +8,6 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { IMentionSuggestion } from "../../types/mention-suggestion";
|
|
||||||
|
|
||||||
interface MentionListProps {
|
interface MentionListProps {
|
||||||
items: IMentionSuggestion[];
|
items: IMentionSuggestion[];
|
||||||
command: (item: {
|
command: (item: {
|
||||||
|
@ -2,7 +2,8 @@ import { Mention, MentionOptions } from "@tiptap/extension-mention";
|
|||||||
import { mergeAttributes } from "@tiptap/core";
|
import { mergeAttributes } from "@tiptap/core";
|
||||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
import mentionNodeView from "./mentionNodeView";
|
import mentionNodeView from "./mentionNodeView";
|
||||||
import { IMentionHighlight } from "../../types/mention-suggestion";
|
import { IMentionHighlight } from "@plane/editor-types";
|
||||||
|
|
||||||
export interface CustomMentionOptions extends MentionOptions {
|
export interface CustomMentionOptions extends MentionOptions {
|
||||||
mentionHighlights: IMentionHighlight[];
|
mentionHighlights: IMentionHighlight[];
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
@ -2,10 +2,7 @@
|
|||||||
|
|
||||||
import suggestion from "./suggestion";
|
import suggestion from "./suggestion";
|
||||||
import { CustomMention } from "./custom";
|
import { CustomMention } from "./custom";
|
||||||
import {
|
import { IMentionHighlight, IMentionSuggestion } from "@plane/editor-types";
|
||||||
IMentionHighlight,
|
|
||||||
IMentionSuggestion,
|
|
||||||
} from "../../types/mention-suggestion";
|
|
||||||
|
|
||||||
export const Mentions = (
|
export const Mentions = (
|
||||||
mentionSuggestions: IMentionSuggestion[],
|
mentionSuggestions: IMentionSuggestion[],
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import { NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewWrapper } from "@tiptap/react";
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
import { useRouter } from "next/router";
|
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
|
// eslint-disable-next-line import/no-anonymous-default-export
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
|
@ -3,7 +3,7 @@ import { Editor } from "@tiptap/core";
|
|||||||
import tippy from "tippy.js";
|
import tippy from "tippy.js";
|
||||||
|
|
||||||
import MentionList from "./MentionList";
|
import MentionList from "./MentionList";
|
||||||
import { IMentionSuggestion } from "../../types/mention-suggestion";
|
import { IMentionSuggestion } from "@plane/editor-types";
|
||||||
|
|
||||||
const Suggestion = (suggestions: IMentionSuggestion[]) => ({
|
const Suggestion = (suggestions: IMentionSuggestion[]) => ({
|
||||||
items: ({ query }: { query: string }) =>
|
items: ({ query }: { query: string }) =>
|
||||||
|
@ -15,7 +15,6 @@ import {
|
|||||||
CodeIcon,
|
CodeIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Editor } from "@tiptap/react";
|
import { Editor } from "@tiptap/react";
|
||||||
import { UploadImage } from "../../../types/upload-image";
|
|
||||||
import {
|
import {
|
||||||
insertImageCommand,
|
insertImageCommand,
|
||||||
insertTableCommand,
|
insertTableCommand,
|
||||||
@ -32,6 +31,7 @@ import {
|
|||||||
toggleTaskList,
|
toggleTaskList,
|
||||||
toggleUnderline,
|
toggleUnderline,
|
||||||
} from "../../../lib/editor-commands";
|
} from "../../../lib/editor-commands";
|
||||||
|
import { UploadImage } from "@plane/editor-types";
|
||||||
|
|
||||||
export interface EditorMenuItem {
|
export interface EditorMenuItem {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
|
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
|
||||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
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 deleteKey = new PluginKey("delete-image");
|
||||||
const IMAGE_NODE_TYPE = "image";
|
const IMAGE_NODE_TYPE = "image";
|
||||||
@ -59,7 +59,7 @@ const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
|
|||||||
|
|
||||||
export default TrackImageDeletionPlugin;
|
export default TrackImageDeletionPlugin;
|
||||||
|
|
||||||
async function onNodeDeleted(
|
export async function onNodeDeleted(
|
||||||
src: string,
|
src: string,
|
||||||
deleteImage: DeleteImage,
|
deleteImage: DeleteImage,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -73,3 +73,18 @@ async function onNodeDeleted(
|
|||||||
console.error("Error deleting image: ", error);
|
console.error("Error deleting image: ", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function onNodeRestored(
|
||||||
|
src: string,
|
||||||
|
restoreImage: RestoreImage,
|
||||||
|
): Promise<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
|
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import { UploadImage } from "@plane/editor-types";
|
||||||
import { EditorProps } from "@tiptap/pm/view";
|
import { EditorProps } from "@tiptap/pm/view";
|
||||||
import { findTableAncestor } from "../lib/utils";
|
import { findTableAncestor } from "../lib/utils";
|
||||||
import { startImageUpload } from "./plugins/upload-image";
|
import { startImageUpload } from "./plugins/upload-image";
|
||||||
import { UploadImage } from "../types/upload-image";
|
|
||||||
|
|
||||||
export function CoreEditorProps(
|
export function CoreEditorProps(
|
||||||
uploadFile: UploadImage,
|
uploadFile: UploadImage,
|
||||||
@ -82,5 +82,8 @@ export function CoreEditorProps(
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
transformPastedHTML(html) {
|
||||||
|
return html.replace(/<img.*?>/g, "");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import TableRow from "../extensions/table/table-row/table-row";
|
|||||||
import ReadOnlyImageExtension from "../extensions/image/read-only-image";
|
import ReadOnlyImageExtension from "../extensions/image/read-only-image";
|
||||||
import { isValidHttpUrl } from "../../lib/utils";
|
import { isValidHttpUrl } from "../../lib/utils";
|
||||||
import { Mentions } from "../mentions";
|
import { Mentions } from "../mentions";
|
||||||
import { IMentionSuggestion } from "../../types/mention-suggestion";
|
import { IMentionSuggestion } from "@plane/editor-types";
|
||||||
|
|
||||||
export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
||||||
mentionSuggestions: IMentionSuggestion[];
|
mentionSuggestions: IMentionSuggestion[];
|
||||||
|
@ -1 +1 @@
|
|||||||
# Document Editor
|
# Document Editor
|
||||||
|
@ -18,7 +18,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"dev": "tsup --watch",
|
"dev": "tsup --watch",
|
||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit",
|
||||||
|
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"next": "12.3.2",
|
"next": "12.3.2",
|
||||||
@ -30,6 +31,7 @@
|
|||||||
"@plane/ui": "*",
|
"@plane/ui": "*",
|
||||||
"@plane/editor-core": "*",
|
"@plane/editor-core": "*",
|
||||||
"@plane/editor-extensions": "*",
|
"@plane/editor-extensions": "*",
|
||||||
|
"@plane/editor-types": "*",
|
||||||
"@tiptap/core": "^2.1.7",
|
"@tiptap/core": "^2.1.7",
|
||||||
"@tiptap/extension-placeholder": "^2.1.11",
|
"@tiptap/extension-placeholder": "^2.1.11",
|
||||||
"@types/node": "18.15.3",
|
"@types/node": "18.15.3",
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
export { DocumentEditor, DocumentEditorWithRef } from "./ui"
|
export { DocumentEditor, DocumentEditorWithRef } from "./ui";
|
||||||
export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef } from "./ui/readonly"
|
export {
|
||||||
export { FixedMenu } from "./ui/menu/fixed-menu"
|
DocumentReadOnlyEditor,
|
||||||
|
DocumentReadOnlyEditorWithRef,
|
||||||
|
} from "./ui/readonly";
|
||||||
|
export { FixedMenu } from "./ui/menu/fixed-menu";
|
||||||
|
@ -3,31 +3,34 @@ import { useState } from "react";
|
|||||||
import { IMarking } from "..";
|
import { IMarking } from "..";
|
||||||
|
|
||||||
export const useEditorMarkings = () => {
|
export const useEditorMarkings = () => {
|
||||||
|
const [markings, setMarkings] = useState<IMarking[]>([]);
|
||||||
const [markings, setMarkings] = useState<IMarking[]>([])
|
|
||||||
|
|
||||||
const updateMarkings = (json: any) => {
|
const updateMarkings = (json: any) => {
|
||||||
const nodes = json.content as any[]
|
const nodes = json.content as any[];
|
||||||
const tempMarkings: IMarking[] = []
|
const tempMarkings: IMarking[] = [];
|
||||||
let h1Sequence: number = 0
|
let h1Sequence: number = 0;
|
||||||
let h2Sequence: number = 0
|
let h2Sequence: number = 0;
|
||||||
if (nodes) {
|
if (nodes) {
|
||||||
nodes.forEach((node) => {
|
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({
|
tempMarkings.push({
|
||||||
type: "heading",
|
type: "heading",
|
||||||
level: node.attrs.level,
|
level: node.attrs.level,
|
||||||
text: node.content[0].text,
|
text: node.content[0].text,
|
||||||
sequence: node.attrs.level === 1 ? ++h1Sequence : ++h2Sequence
|
sequence: node.attrs.level === 1 ? ++h1Sequence : ++h2Sequence,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
setMarkings(tempMarkings)
|
setMarkings(tempMarkings);
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateMarkings,
|
updateMarkings,
|
||||||
markings,
|
markings,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
@ -14,15 +14,14 @@ import { DocumentDetails } from "./types/editor-types";
|
|||||||
import { PageRenderer } from "./components/page-renderer";
|
import { PageRenderer } from "./components/page-renderer";
|
||||||
import { getMenuOptions } from "./utils/menu-options";
|
import { getMenuOptions } from "./utils/menu-options";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { UploadImage, DeleteImage, RestoreImage } from "@plane/editor-types";
|
||||||
export type UploadImage = (file: File) => Promise<string>;
|
|
||||||
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
|
||||||
|
|
||||||
interface IDocumentEditor {
|
interface IDocumentEditor {
|
||||||
documentDetails: DocumentDetails;
|
documentDetails: DocumentDetails;
|
||||||
value: string;
|
value: string;
|
||||||
uploadFile: UploadImage;
|
uploadFile: UploadImage;
|
||||||
deleteFile: DeleteImage;
|
deleteFile: DeleteImage;
|
||||||
|
restoreFile: RestoreImage;
|
||||||
customClassName?: string;
|
customClassName?: string;
|
||||||
editorContentCustomClassNames?: string;
|
editorContentCustomClassNames?: string;
|
||||||
onChange: (json: any, html: string) => void;
|
onChange: (json: any, html: string) => void;
|
||||||
@ -62,6 +61,7 @@ const DocumentEditor = ({
|
|||||||
value,
|
value,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
|
restoreFile,
|
||||||
customClassName,
|
customClassName,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
duplicationConfig,
|
duplicationConfig,
|
||||||
@ -82,6 +82,7 @@ const DocumentEditor = ({
|
|||||||
updateMarkings(json);
|
updateMarkings(json);
|
||||||
},
|
},
|
||||||
debouncedUpdatesEnabled,
|
debouncedUpdatesEnabled,
|
||||||
|
restoreFile,
|
||||||
setIsSubmitting,
|
setIsSubmitting,
|
||||||
setShouldShowAlert,
|
setShouldShowAlert,
|
||||||
value,
|
value,
|
||||||
|
@ -19,7 +19,7 @@ import {
|
|||||||
HeadingThreeItem,
|
HeadingThreeItem,
|
||||||
findTableAncestor,
|
findTableAncestor,
|
||||||
} from "@plane/editor-core";
|
} from "@plane/editor-core";
|
||||||
import { UploadImage } from "..";
|
import { UploadImage } from "@plane/editor-types";
|
||||||
|
|
||||||
export interface BubbleMenuItem {
|
export interface BubbleMenuItem {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -6,8 +6,9 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
|
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
|
||||||
<span className={`material-symbols-rounded text-sm leading-5 font-light ${className}`}>
|
<span
|
||||||
|
className={`material-symbols-rounded text-sm leading-5 font-light ${className}`}
|
||||||
|
>
|
||||||
{iconName}
|
{iconName}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
export { FixedMenu } from "./fixed-menu";
|
export { FixedMenu } from "./fixed-menu";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
// next-themes
|
// next-themes
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
@ -69,8 +69,16 @@ export const Tooltip: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
position={position}
|
position={position}
|
||||||
renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) =>
|
renderTarget={({
|
||||||
React.cloneElement(children, { ref: eleReference, ...tooltipProps, ...children.props })
|
isOpen: isTooltipOpen,
|
||||||
|
ref: eleReference,
|
||||||
|
...tooltipProps
|
||||||
|
}) =>
|
||||||
|
React.cloneElement(children, {
|
||||||
|
ref: eleReference,
|
||||||
|
...tooltipProps,
|
||||||
|
...children.props,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
|
|
||||||
export interface DocumentDetails {
|
export interface DocumentDetails {
|
||||||
title: string;
|
title: string;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
created_on: Date;
|
created_on: Date;
|
||||||
last_updated_by: string;
|
last_updated_by: string;
|
||||||
last_updated_at: Date;
|
last_updated_at: Date;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
|
|
||||||
export interface IDuplicationConfig {
|
export interface IDuplicationConfig {
|
||||||
action: () => Promise<void>
|
action: () => Promise<void>;
|
||||||
}
|
}
|
||||||
export interface IPageLockConfig {
|
export interface IPageLockConfig {
|
||||||
is_locked: boolean,
|
is_locked: boolean;
|
||||||
action: () => Promise<void>
|
action: () => Promise<void>;
|
||||||
locked_by?: string,
|
locked_by?: string;
|
||||||
}
|
}
|
||||||
export interface IPageArchiveConfig {
|
export interface IPageArchiveConfig {
|
||||||
is_archived: boolean,
|
is_archived: boolean;
|
||||||
archived_at?: Date,
|
archived_at?: Date;
|
||||||
action: () => Promise<void>
|
action: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -2,34 +2,33 @@ import { Editor } from "@tiptap/react";
|
|||||||
import { IMarking } from "..";
|
import { IMarking } from "..";
|
||||||
|
|
||||||
function findNthH1(editor: Editor, n: number, level: number): number {
|
function findNthH1(editor: Editor, n: number, level: number): number {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
editor.state.doc.descendants((node, position) => {
|
editor.state.doc.descendants((node, position) => {
|
||||||
if (node.type.name === 'heading' && node.attrs.level === level) {
|
if (node.type.name === "heading" && node.attrs.level === level) {
|
||||||
count++;
|
count++;
|
||||||
if (count === n) {
|
if (count === n) {
|
||||||
pos = position;
|
pos = position;
|
||||||
return false;
|
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' });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
export function scrollSummary(editor: Editor, marking: IMarking) {
|
function scrollToNode(editor: Editor, pos: number): void {
|
||||||
if (editor) {
|
const headingNode = editor.state.doc.nodeAt(pos);
|
||||||
const pos = findNthH1(editor, marking.sequence, marking.level)
|
if (headingNode) {
|
||||||
scrollToNode(editor, pos)
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Editor } from "@tiptap/core"
|
import { Editor } from "@tiptap/core";
|
||||||
|
|
||||||
export const copyMarkdownToClipboard = (editor: Editor | null) => {
|
export const copyMarkdownToClipboard = (editor: Editor | null) => {
|
||||||
const markdownOutput = editor?.storage.markdown.getMarkdown();
|
const markdownOutput = editor?.storage.markdown.getMarkdown();
|
||||||
navigator.clipboard.writeText(markdownOutput)
|
navigator.clipboard.writeText(markdownOutput);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const CopyPageLink = () => {
|
export const CopyPageLink = () => {
|
||||||
if (window){
|
if (window) {
|
||||||
navigator.clipboard.writeText(window.location.toString())
|
navigator.clipboard.writeText(window.location.toString());
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
@ -19,7 +19,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"dev": "tsup --watch",
|
"dev": "tsup --watch",
|
||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit",
|
||||||
|
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"next": "12.3.2",
|
"next": "12.3.2",
|
||||||
@ -29,7 +30,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@plane/editor-core": "*",
|
"@plane/editor-core": "*",
|
||||||
"@plane/ui": "*"
|
"@plane/ui": "*",
|
||||||
|
"@plane/editor-types": "*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "18.15.3",
|
"@types/node": "18.15.3",
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
export { LiteTextEditor, LiteTextEditorWithRef } from "./ui";
|
export { LiteTextEditor, LiteTextEditorWithRef } from "./ui";
|
||||||
export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef } from "./ui/read-only";
|
export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef } from "./ui/read-only";
|
||||||
export type { IMentionSuggestion, IMentionHighlight } from "./ui";
|
export type {
|
||||||
|
IMentionSuggestion,
|
||||||
|
IMentionHighlight,
|
||||||
|
} from "@plane/editor-types";
|
||||||
|
@ -7,24 +7,19 @@ import {
|
|||||||
} from "@plane/editor-core";
|
} from "@plane/editor-core";
|
||||||
import { FixedMenu } from "./menus/fixed-menu";
|
import { FixedMenu } from "./menus/fixed-menu";
|
||||||
import { LiteTextEditorExtensions } from "./extensions";
|
import { LiteTextEditorExtensions } from "./extensions";
|
||||||
|
import {
|
||||||
export type UploadImage = (file: File) => Promise<string>;
|
UploadImage,
|
||||||
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
DeleteImage,
|
||||||
export type IMentionSuggestion = {
|
IMentionSuggestion,
|
||||||
id: string;
|
RestoreImage,
|
||||||
type: string;
|
} from "@plane/editor-types";
|
||||||
avatar: string;
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
redirect_uri: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IMentionHighlight = string;
|
|
||||||
|
|
||||||
interface ILiteTextEditor {
|
interface ILiteTextEditor {
|
||||||
value: string;
|
value: string;
|
||||||
uploadFile: UploadImage;
|
uploadFile: UploadImage;
|
||||||
deleteFile: DeleteImage;
|
deleteFile: DeleteImage;
|
||||||
|
restoreFile: RestoreImage;
|
||||||
|
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
borderOnFocus?: boolean;
|
borderOnFocus?: boolean;
|
||||||
customClassName?: string;
|
customClassName?: string;
|
||||||
@ -73,6 +68,7 @@ const LiteTextEditor = (props: LiteTextEditorProps) => {
|
|||||||
value,
|
value,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
|
restoreFile,
|
||||||
noBorder,
|
noBorder,
|
||||||
borderOnFocus,
|
borderOnFocus,
|
||||||
customClassName,
|
customClassName,
|
||||||
@ -93,6 +89,7 @@ const LiteTextEditor = (props: LiteTextEditorProps) => {
|
|||||||
value,
|
value,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
|
restoreFile,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
extensions: LiteTextEditorExtensions(onEnterKeyPress),
|
extensions: LiteTextEditorExtensions(onEnterKeyPress),
|
||||||
mentionHighlights,
|
mentionHighlights,
|
||||||
|
@ -16,11 +16,12 @@ import {
|
|||||||
UnderLineItem,
|
UnderLineItem,
|
||||||
} from "@plane/editor-core";
|
} from "@plane/editor-core";
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
import { UploadImage } from "../../";
|
|
||||||
import type { SVGProps } from "react";
|
import type { SVGProps } from "react";
|
||||||
|
import { UploadImage } from "@plane/editor-types";
|
||||||
|
|
||||||
interface LucideProps extends Partial<SVGProps<SVGSVGElement>> {
|
interface LucideProps extends Partial<SVGProps<SVGSVGElement>> {
|
||||||
size?: string | number
|
size?: string | number;
|
||||||
absoluteStrokeWidth?: boolean
|
absoluteStrokeWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type LucideIcon = (props: LucideProps) => JSX.Element;
|
type LucideIcon = (props: LucideProps) => JSX.Element;
|
||||||
|
@ -19,7 +19,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"dev": "tsup --watch",
|
"dev": "tsup --watch",
|
||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit",
|
||||||
|
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"next": "12.3.2",
|
"next": "12.3.2",
|
||||||
@ -30,6 +31,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@plane/editor-core": "*",
|
"@plane/editor-core": "*",
|
||||||
"@tiptap/core": "^2.1.11",
|
"@tiptap/core": "^2.1.11",
|
||||||
|
"@plane/editor-types": "*",
|
||||||
"@plane/editor-extensions": "*",
|
"@plane/editor-extensions": "*",
|
||||||
"@tiptap/extension-placeholder": "^2.1.11",
|
"@tiptap/extension-placeholder": "^2.1.11",
|
||||||
"lucide-react": "^0.244.0"
|
"lucide-react": "^0.244.0"
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
export { RichTextEditor, RichTextEditorWithRef } from "./ui";
|
export { RichTextEditor, RichTextEditorWithRef } from "./ui";
|
||||||
export { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "./ui/read-only";
|
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";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { SlashCommand } from "@plane/editor-extensions";
|
import { SlashCommand } from "@plane/editor-extensions";
|
||||||
import Placeholder from "@tiptap/extension-placeholder";
|
import Placeholder from "@tiptap/extension-placeholder";
|
||||||
import { DragAndDrop } from "@plane/editor-extensions";
|
import { DragAndDrop } from "@plane/editor-extensions";
|
||||||
import { UploadImage } from "../";
|
import { UploadImage } from "@plane/editor-types";
|
||||||
|
|
||||||
export const RichTextEditorExtensions = (
|
export const RichTextEditorExtensions = (
|
||||||
uploadFile: UploadImage,
|
uploadFile: UploadImage,
|
||||||
|
@ -8,25 +8,18 @@ import {
|
|||||||
} from "@plane/editor-core";
|
} from "@plane/editor-core";
|
||||||
import { EditorBubbleMenu } from "./menus/bubble-menu";
|
import { EditorBubbleMenu } from "./menus/bubble-menu";
|
||||||
import { RichTextEditorExtensions } from "./extensions";
|
import { RichTextEditorExtensions } from "./extensions";
|
||||||
|
import {
|
||||||
|
DeleteImage,
|
||||||
|
IMentionSuggestion,
|
||||||
|
RestoreImage,
|
||||||
|
UploadImage,
|
||||||
|
} from "@plane/editor-types";
|
||||||
|
|
||||||
export type UploadImage = (file: File) => Promise<string>;
|
export type IRichTextEditor = {
|
||||||
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
|
||||||
|
|
||||||
export type IMentionSuggestion = {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
avatar: string;
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
redirect_uri: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IMentionHighlight = string;
|
|
||||||
|
|
||||||
interface IRichTextEditor {
|
|
||||||
value: string;
|
value: string;
|
||||||
dragDropEnabled?: boolean;
|
dragDropEnabled?: boolean;
|
||||||
uploadFile: UploadImage;
|
uploadFile: UploadImage;
|
||||||
|
restoreFile: RestoreImage;
|
||||||
deleteFile: DeleteImage;
|
deleteFile: DeleteImage;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
borderOnFocus?: boolean;
|
borderOnFocus?: boolean;
|
||||||
@ -42,9 +35,9 @@ interface IRichTextEditor {
|
|||||||
debouncedUpdatesEnabled?: boolean;
|
debouncedUpdatesEnabled?: boolean;
|
||||||
mentionHighlights?: string[];
|
mentionHighlights?: string[];
|
||||||
mentionSuggestions?: IMentionSuggestion[];
|
mentionSuggestions?: IMentionSuggestion[];
|
||||||
}
|
};
|
||||||
|
|
||||||
interface RichTextEditorProps extends IRichTextEditor {
|
export interface RichTextEditorProps extends IRichTextEditor {
|
||||||
forwardedRef?: React.Ref<EditorHandle>;
|
forwardedRef?: React.Ref<EditorHandle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +60,7 @@ const RichTextEditor = ({
|
|||||||
cancelUploadImage,
|
cancelUploadImage,
|
||||||
borderOnFocus,
|
borderOnFocus,
|
||||||
customClassName,
|
customClassName,
|
||||||
|
restoreFile,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
mentionHighlights,
|
mentionHighlights,
|
||||||
mentionSuggestions,
|
mentionSuggestions,
|
||||||
@ -80,6 +74,7 @@ const RichTextEditor = ({
|
|||||||
uploadFile,
|
uploadFile,
|
||||||
cancelUploadImage,
|
cancelUploadImage,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
|
restoreFile,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
extensions: RichTextEditorExtensions(
|
extensions: RichTextEditorExtensions(
|
||||||
uploadFile,
|
uploadFile,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export type { DeleteImage } from "./types/delete-image";
|
export type { DeleteImage } from "./types/delete-image";
|
||||||
export type { UploadImage } from "./types/upload-image";
|
export type { UploadImage } from "./types/upload-image";
|
||||||
|
export type { RestoreImage } from "./types/restore-image";
|
||||||
export type {
|
export type {
|
||||||
IMentionHighlight,
|
IMentionHighlight,
|
||||||
IMentionSuggestion,
|
IMentionSuggestion,
|
||||||
|
1
packages/editor/types/src/types/restore-image.ts
Normal file
1
packages/editor/types/src/types/restore-image.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type RestoreImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
@ -79,6 +79,7 @@ export const AddComment: React.FC<Props> = observer((props) => {
|
|||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspace_slug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspace_slug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
restoreFile={fileService.restoreImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={
|
value={
|
||||||
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
|
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
|
||||||
|
@ -106,6 +106,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
|||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
restoreFile={fileService.restoreImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={value}
|
value={value}
|
||||||
debouncedUpdatesEnabled={false}
|
debouncedUpdatesEnabled={false}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
// helpers
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
@ -33,6 +35,7 @@ class FileService extends APIService {
|
|||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
this.uploadFile = this.uploadFile.bind(this);
|
this.uploadFile = this.uploadFile.bind(this);
|
||||||
this.deleteImage = this.deleteImage.bind(this);
|
this.deleteImage = this.deleteImage.bind(this);
|
||||||
|
this.restoreImage = this.restoreImage.bind(this);
|
||||||
this.cancelUpload = this.cancelUpload.bind(this);
|
this.cancelUpload = this.cancelUpload.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +53,7 @@ class FileService extends APIService {
|
|||||||
if (axios.isCancel(error)) {
|
if (axios.isCancel(error)) {
|
||||||
console.log(error.message);
|
console.log(error.message);
|
||||||
} else {
|
} else {
|
||||||
|
console.log(error);
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -58,6 +62,7 @@ class FileService extends APIService {
|
|||||||
cancelUpload() {
|
cancelUpload() {
|
||||||
this.cancelSource.cancel("Upload cancelled");
|
this.cancelSource.cancel("Upload cancelled");
|
||||||
}
|
}
|
||||||
|
|
||||||
getUploadFileFunction(workspaceSlug: string): (file: File) => Promise<string> {
|
getUploadFileFunction(workspaceSlug: string): (file: File) => Promise<string> {
|
||||||
return async (file: File) => {
|
return async (file: File) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@ -77,6 +82,17 @@ class FileService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async restoreImage(assetUrlWithWorkspaceId: string): Promise<any> {
|
||||||
|
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<any> {
|
async deleteFile(workspaceId: string, assetUrl: string): Promise<any> {
|
||||||
const lastIndex = assetUrl.lastIndexOf("/");
|
const lastIndex = assetUrl.lastIndexOf("/");
|
||||||
const assetId = assetUrl.substring(lastIndex + 1);
|
const assetId = assetUrl.substring(lastIndex + 1);
|
||||||
|
@ -155,6 +155,7 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
|||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
restoreFile={fileService.restoreImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
debouncedUpdatesEnabled={false}
|
debouncedUpdatesEnabled={false}
|
||||||
value={!value || value === "" ? "<p></p>" : value}
|
value={!value || value === "" ? "<p></p>" : value}
|
||||||
|
@ -87,6 +87,7 @@ export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit, showAc
|
|||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
restoreFile={fileService.restoreImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
|
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
|
||||||
customClassName="p-2 h-full"
|
customClassName="p-2 h-full"
|
||||||
|
@ -108,6 +108,7 @@ export const CommentCard: React.FC<Props> = ({
|
|||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
restoreFile={fileService.restoreImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={watch("comment_html")}
|
value={watch("comment_html")}
|
||||||
debouncedUpdatesEnabled={false}
|
debouncedUpdatesEnabled={false}
|
||||||
|
@ -148,6 +148,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
|||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
restoreFile={fileService.restoreImage}
|
||||||
value={value}
|
value={value}
|
||||||
setShouldShowAlert={setShowAlert}
|
setShouldShowAlert={setShowAlert}
|
||||||
setIsSubmitting={setIsSubmitting}
|
setIsSubmitting={setIsSubmitting}
|
||||||
|
@ -425,6 +425,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
restoreFile={fileService.restoreImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
debouncedUpdatesEnabled={false}
|
debouncedUpdatesEnabled={false}
|
||||||
value={
|
value={
|
||||||
|
@ -379,6 +379,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
restoreFile={fileService.restoreImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
debouncedUpdatesEnabled={false}
|
debouncedUpdatesEnabled={false}
|
||||||
value={
|
value={
|
||||||
|
@ -117,6 +117,7 @@ export const IssueCommentCard: React.FC<IIssueCommentCard> = (props) => {
|
|||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
restoreFile={fileService.restoreImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={watch("comment_html")}
|
value={watch("comment_html")}
|
||||||
debouncedUpdatesEnabled={false}
|
debouncedUpdatesEnabled={false}
|
||||||
|
@ -88,6 +88,7 @@ export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
|
|||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
restoreFile={fileService.restoreImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
|
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
|
||||||
customClassName="p-2 h-full"
|
customClassName="p-2 h-full"
|
||||||
|
@ -66,11 +66,15 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
[issue, issueUpdate]
|
[issue, issueUpdate]
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedIssueDescription = useDebouncedCallback(async (_data: any) => {
|
const [localTitleValue, setLocalTitleValue] = useState("");
|
||||||
issueUpdate({ ...issue, description_html: _data });
|
const issueTitleCurrentValue = watch("name");
|
||||||
}, 1500);
|
useEffect(() => {
|
||||||
|
if (localTitleValue === "" && issueTitleCurrentValue !== "") {
|
||||||
|
setLocalTitleValue(issueTitleCurrentValue);
|
||||||
|
}
|
||||||
|
}, [issueTitleCurrentValue, localTitleValue]);
|
||||||
|
|
||||||
const debouncedTitleSave = useDebouncedCallback(async () => {
|
const debouncedFormSave = useDebouncedCallback(async () => {
|
||||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
|
||||||
@ -105,18 +109,19 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
<Controller
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { onChange } }) => (
|
||||||
<TextArea
|
<TextArea
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
value={value}
|
value={localTitleValue}
|
||||||
placeholder="Enter issue name"
|
placeholder="Enter issue name"
|
||||||
onFocus={() => setCharacterLimit(true)}
|
onFocus={() => setCharacterLimit(true)}
|
||||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
|
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setCharacterLimit(false);
|
setCharacterLimit(false);
|
||||||
setIsSubmitting("submitting");
|
setIsSubmitting("submitting");
|
||||||
debouncedTitleSave();
|
setLocalTitleValue(e.target.value);
|
||||||
onChange(e.target.value);
|
onChange(e.target.value);
|
||||||
|
debouncedFormSave();
|
||||||
}}
|
}}
|
||||||
required={true}
|
required={true}
|
||||||
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary !p-0 focus:!px-3 focus:!py-2"
|
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary !p-0 focus:!px-3 focus:!py-2"
|
||||||
@ -139,20 +144,41 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span>{errors.name ? errors.name.message : null}</span>
|
<span>{errors.name ? errors.name.message : null}</span>
|
||||||
<RichTextEditor
|
<div className="relative">
|
||||||
dragDropEnabled
|
<Controller
|
||||||
cancelUploadImage={fileService.cancelUpload}
|
name="description_html"
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
control={control}
|
||||||
deleteFile={fileService.deleteImage}
|
render={({ field: { value, onChange } }) => (
|
||||||
value={issue?.description_html}
|
<RichTextEditor
|
||||||
debouncedUpdatesEnabled={false}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
onChange={(description: Object, description_html: string) => {
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||||
debouncedIssueDescription(description_html);
|
deleteFile={fileService.deleteImage}
|
||||||
}}
|
restoreFile={fileService.restoreImage}
|
||||||
customClassName="mt-0"
|
value={value}
|
||||||
mentionSuggestions={editorSuggestions.mentionSuggestions}
|
setShouldShowAlert={setShowAlert}
|
||||||
mentionHighlights={editorSuggestions.mentionHighlights}
|
setIsSubmitting={setIsSubmitting}
|
||||||
/>
|
dragDropEnabled
|
||||||
|
customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"}
|
||||||
|
noBorder={!isAllowed}
|
||||||
|
onChange={(description: Object, description_html: string) => {
|
||||||
|
setShowAlert(true);
|
||||||
|
setIsSubmitting("submitting");
|
||||||
|
onChange(description_html);
|
||||||
|
debouncedFormSave();
|
||||||
|
}}
|
||||||
|
mentionSuggestions={editorSuggestions.mentionSuggestions}
|
||||||
|
mentionHighlights={editorSuggestions.mentionHighlights}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
|
||||||
|
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<IssueReaction
|
<IssueReaction
|
||||||
issueReactions={issueReactions}
|
issueReactions={issueReactions}
|
||||||
user={user}
|
user={user}
|
||||||
|
@ -271,6 +271,7 @@ export const CreateUpdateBlockInline: FC<Props> = (props) => {
|
|||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
restoreFile={fileService.restoreImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={"<p></p>"}
|
value={"<p></p>"}
|
||||||
debouncedUpdatesEnabled={false}
|
debouncedUpdatesEnabled={false}
|
||||||
@ -293,6 +294,7 @@ export const CreateUpdateBlockInline: FC<Props> = (props) => {
|
|||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
restoreFile={fileService.restoreImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={
|
value={
|
||||||
value && value !== "" && Object.keys(value).length > 0
|
value && value !== "" && Object.keys(value).length > 0
|
||||||
|
@ -267,6 +267,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
}}
|
}}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
restoreFile={fileService.restoreImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
debouncedUpdatesEnabled={false}
|
debouncedUpdatesEnabled={false}
|
||||||
setIsSubmitting={setIsSubmitting}
|
setIsSubmitting={setIsSubmitting}
|
||||||
|
@ -35,6 +35,7 @@ export class FileService extends APIService {
|
|||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
this.uploadFile = this.uploadFile.bind(this);
|
this.uploadFile = this.uploadFile.bind(this);
|
||||||
this.deleteImage = this.deleteImage.bind(this);
|
this.deleteImage = this.deleteImage.bind(this);
|
||||||
|
this.restoreImage = this.restoreImage.bind(this);
|
||||||
this.cancelUpload = this.cancelUpload.bind(this);
|
this.cancelUpload = this.cancelUpload.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +82,17 @@ export class FileService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async restoreImage(assetUrlWithWorkspaceId: string): Promise<any> {
|
||||||
|
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<any> {
|
async deleteFile(workspaceId: string, assetUrl: string): Promise<any> {
|
||||||
const lastIndex = assetUrl.lastIndexOf("/");
|
const lastIndex = assetUrl.lastIndexOf("/");
|
||||||
const assetId = assetUrl.substring(lastIndex + 1);
|
const assetId = assetUrl.substring(lastIndex + 1);
|
||||||
|
@ -23,6 +23,8 @@
|
|||||||
/* Custom image styles */
|
/* Custom image styles */
|
||||||
|
|
||||||
.ProseMirror img {
|
.ProseMirror img {
|
||||||
|
min-width: 100px;
|
||||||
|
min-height: 100px;
|
||||||
transition: filter 0.1s ease-in-out;
|
transition: filter 0.1s ease-in-out;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
Loading…
Reference in New Issue
Block a user