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": {
|
||||
"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",
|
||||
|
@ -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()
|
||||
|
@ -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 { 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 };
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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<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() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
|
@ -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",
|
||||
},
|
||||
|
@ -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 : "<p></p>",
|
||||
onCreate: async ({ editor }) => {
|
||||
onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()))
|
||||
onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
|
||||
},
|
||||
onUpdate: async ({ editor }) => {
|
||||
// for instant feedback loop
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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: {
|
||||
|
@ -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;
|
||||
|
@ -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[],
|
||||
|
@ -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) => {
|
||||
|
@ -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 }) =>
|
||||
|
@ -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;
|
||||
|
@ -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<void> {
|
||||
@ -73,3 +73,18 @@ async function onNodeDeleted(
|
||||
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 { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
|
||||
|
||||
|
@ -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(/<img.*?>/g, "");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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[];
|
||||
|
@ -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",
|
||||
|
@ -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";
|
||||
|
@ -3,31 +3,34 @@ import { useState } from "react";
|
||||
import { IMarking } from "..";
|
||||
|
||||
export const useEditorMarkings = () => {
|
||||
|
||||
const [markings, setMarkings] = useState<IMarking[]>([])
|
||||
const [markings, setMarkings] = useState<IMarking[]>([]);
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
@ -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<string>;
|
||||
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
||||
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,
|
||||
|
@ -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;
|
||||
|
@ -6,8 +6,9 @@ type Props = {
|
||||
};
|
||||
|
||||
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}
|
||||
</span>
|
||||
);
|
||||
|
||||
|
@ -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<Props> = ({
|
||||
</div>
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,14 +1,13 @@
|
||||
|
||||
export interface IDuplicationConfig {
|
||||
action: () => Promise<void>
|
||||
action: () => Promise<void>;
|
||||
}
|
||||
export interface IPageLockConfig {
|
||||
is_locked: boolean,
|
||||
action: () => Promise<void>
|
||||
locked_by?: string,
|
||||
is_locked: boolean;
|
||||
action: () => Promise<void>;
|
||||
locked_by?: string;
|
||||
}
|
||||
export interface IPageArchiveConfig {
|
||||
is_archived: boolean,
|
||||
archived_at?: Date,
|
||||
action: () => Promise<void>
|
||||
}
|
||||
is_archived: boolean;
|
||||
archived_at?: Date;
|
||||
action: () => Promise<void>;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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";
|
||||
|
@ -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<string>;
|
||||
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;
|
||||
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,
|
||||
|
@ -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<SVGProps<SVGSVGElement>> {
|
||||
size?: string | number
|
||||
absoluteStrokeWidth?: boolean
|
||||
size?: string | number;
|
||||
absoluteStrokeWidth?: boolean;
|
||||
}
|
||||
|
||||
type LucideIcon = (props: LucideProps) => JSX.Element;
|
||||
|
@ -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"
|
||||
|
@ -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";
|
||||
|
@ -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,
|
||||
|
@ -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<string>;
|
||||
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 {
|
||||
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<EditorHandle>;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
@ -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,
|
||||
|
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}
|
||||
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)
|
||||
|
@ -106,6 +106,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
ref={editorRef}
|
||||
value={value}
|
||||
debouncedUpdatesEnabled={false}
|
||||
|
@ -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<string> {
|
||||
return async (file: File) => {
|
||||
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> {
|
||||
const lastIndex = assetUrl.lastIndexOf("/");
|
||||
const assetId = assetUrl.substring(lastIndex + 1);
|
||||
|
@ -155,6 +155,7 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
ref={editorRef}
|
||||
debouncedUpdatesEnabled={false}
|
||||
value={!value || value === "" ? "<p></p>" : value}
|
||||
|
@ -87,6 +87,7 @@ export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit, showAc
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
ref={editorRef}
|
||||
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
|
||||
customClassName="p-2 h-full"
|
||||
|
@ -108,6 +108,7 @@ export const CommentCard: 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}
|
||||
|
@ -148,6 +148,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
value={value}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
|
@ -425,6 +425,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
ref={editorRef}
|
||||
debouncedUpdatesEnabled={false}
|
||||
value={
|
||||
|
@ -379,6 +379,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
ref={editorRef}
|
||||
debouncedUpdatesEnabled={false}
|
||||
value={
|
||||
|
@ -117,6 +117,7 @@ export const IssueCommentCard: React.FC<IIssueCommentCard> = (props) => {
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
ref={editorRef}
|
||||
value={watch("comment_html")}
|
||||
debouncedUpdatesEnabled={false}
|
||||
|
@ -88,6 +88,7 @@ export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
ref={editorRef}
|
||||
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
|
||||
customClassName="p-2 h-full"
|
||||
|
@ -66,11 +66,15 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (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<IPeekOverviewIssueDetails> = (props) =
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
render={({ field: { onChange } }) => (
|
||||
<TextArea
|
||||
id="name"
|
||||
name="name"
|
||||
value={value}
|
||||
value={localTitleValue}
|
||||
placeholder="Enter issue name"
|
||||
onFocus={() => setCharacterLimit(true)}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setCharacterLimit(false);
|
||||
setIsSubmitting("submitting");
|
||||
debouncedTitleSave();
|
||||
setLocalTitleValue(e.target.value);
|
||||
onChange(e.target.value);
|
||||
debouncedFormSave();
|
||||
}}
|
||||
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"
|
||||
@ -139,20 +144,41 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
||||
)}
|
||||
</div>
|
||||
<span>{errors.name ? errors.name.message : null}</span>
|
||||
<RichTextEditor
|
||||
dragDropEnabled
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
value={issue?.description_html}
|
||||
debouncedUpdatesEnabled={false}
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
debouncedIssueDescription(description_html);
|
||||
}}
|
||||
customClassName="mt-0"
|
||||
mentionSuggestions={editorSuggestions.mentionSuggestions}
|
||||
mentionHighlights={editorSuggestions.mentionHighlights}
|
||||
/>
|
||||
<div className="relative">
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<RichTextEditor
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
value={value}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
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
|
||||
issueReactions={issueReactions}
|
||||
user={user}
|
||||
|
@ -271,6 +271,7 @@ export const CreateUpdateBlockInline: FC<Props> = (props) => {
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
ref={editorRef}
|
||||
value={"<p></p>"}
|
||||
debouncedUpdatesEnabled={false}
|
||||
@ -293,6 +294,7 @@ export const CreateUpdateBlockInline: FC<Props> = (props) => {
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
ref={editorRef}
|
||||
value={
|
||||
value && value !== "" && Object.keys(value).length > 0
|
||||
|
@ -267,6 +267,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
}}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
ref={editorRef}
|
||||
debouncedUpdatesEnabled={false}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
|
@ -35,6 +35,7 @@ export 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);
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
const lastIndex = assetUrl.lastIndexOf("/");
|
||||
const assetId = assetUrl.substring(lastIndex + 1);
|
||||
|
@ -23,6 +23,8 @@
|
||||
/* Custom image styles */
|
||||
|
||||
.ProseMirror img {
|
||||
min-width: 100px;
|
||||
min-height: 100px;
|
||||
transition: filter 0.1s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
|
Loading…
Reference in New Issue
Block a user