mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
added delete plugin with loading indicator
This commit is contained in:
parent
9bca3ac48d
commit
36e885dab8
@ -1,9 +1,10 @@
|
|||||||
import Image from "@tiptap/extension-image";
|
import Image from "@tiptap/extension-image";
|
||||||
|
import TrackImageDeletionPlugin from "../plugins/delete-image";
|
||||||
import UploadImagesPlugin from "../plugins/upload-image";
|
import UploadImagesPlugin from "../plugins/upload-image";
|
||||||
|
|
||||||
const UpdatedImage = Image.extend({
|
const UpdatedImage = Image.extend({
|
||||||
addProseMirrorPlugins() {
|
addProseMirrorPlugins() {
|
||||||
return [UploadImagesPlugin()];
|
return [UploadImagesPlugin(), TrackImageDeletionPlugin()];
|
||||||
},
|
},
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import { useEditor, EditorContent, Editor } from "@tiptap/react";
|
import { useEditor, EditorContent, Editor } from "@tiptap/react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import { EditorBubbleMenu } from "./bubble-menu";
|
import { EditorBubbleMenu } from "./bubble-menu";
|
||||||
import { TiptapExtensions } from "./extensions";
|
import { TiptapExtensions } from "./extensions";
|
||||||
import { TiptapEditorProps } from "./props";
|
import { TiptapEditorProps } from "./props";
|
||||||
import { Node } from "@tiptap/pm/model";
|
import { useImperativeHandle, useRef } from "react";
|
||||||
import { Editor as CoreEditor } from "@tiptap/core";
|
|
||||||
import { useCallback, useImperativeHandle, useRef } from "react";
|
|
||||||
import { EditorState } from "@tiptap/pm/state";
|
|
||||||
import fileService from "services/file.service";
|
|
||||||
import { ImageResizer } from "./extensions/image-resize";
|
import { ImageResizer } from "./extensions/image-resize";
|
||||||
|
|
||||||
export interface ITiptapRichTextEditor {
|
export interface ITiptapRichTextEditor {
|
||||||
@ -51,7 +46,6 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
|||||||
// for instant feedback loop
|
// for instant feedback loop
|
||||||
setIsSubmitting?.("submitting");
|
setIsSubmitting?.("submitting");
|
||||||
setShouldShowAlert?.(true);
|
setShouldShowAlert?.(true);
|
||||||
checkForNodeDeletions(editor);
|
|
||||||
if (debouncedUpdatesEnabled) {
|
if (debouncedUpdatesEnabled) {
|
||||||
debouncedUpdates({ onChange, editor });
|
debouncedUpdates({ onChange, editor });
|
||||||
} else {
|
} else {
|
||||||
@ -71,45 +65,6 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const previousState = useRef<EditorState>();
|
|
||||||
|
|
||||||
const onNodeDeleted = useCallback(async (node: Node) => {
|
|
||||||
if (node.type.name === "image") {
|
|
||||||
const assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1);
|
|
||||||
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
|
|
||||||
if (resStatus === 204) {
|
|
||||||
console.log("file deleted successfully");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const checkForNodeDeletions = useCallback(
|
|
||||||
(editor: CoreEditor) => {
|
|
||||||
const prevNodesById: Record<string, Node> = {};
|
|
||||||
previousState.current?.doc.forEach((node) => {
|
|
||||||
if (node.attrs.id) {
|
|
||||||
prevNodesById[node.attrs.id] = node;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const nodesById: Record<string, Node> = {};
|
|
||||||
editor.state?.doc.forEach((node) => {
|
|
||||||
if (node.attrs.id) {
|
|
||||||
nodesById[node.attrs.id] = node;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
previousState.current = editor.state;
|
|
||||||
|
|
||||||
for (const [id, node] of Object.entries(prevNodesById)) {
|
|
||||||
if (nodesById[id] === undefined) {
|
|
||||||
onNodeDeleted(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onNodeDeleted]
|
|
||||||
);
|
|
||||||
|
|
||||||
const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
|
const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
|
57
apps/app/components/tiptap/plugins/delete-image.tsx
Normal file
57
apps/app/components/tiptap/plugins/delete-image.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
|
import { Node as ProseMirrorNode } from '@tiptap/pm/model';
|
||||||
|
import fileService from "services/file.service";
|
||||||
|
|
||||||
|
const deleteKey = new PluginKey("delete-image");
|
||||||
|
|
||||||
|
const TrackImageDeletionPlugin = () =>
|
||||||
|
new Plugin({
|
||||||
|
key: deleteKey,
|
||||||
|
appendTransaction: (transactions, oldState, newState) => {
|
||||||
|
transactions.forEach((transaction) => {
|
||||||
|
if (!transaction.docChanged) return;
|
||||||
|
|
||||||
|
const removedImages: ProseMirrorNode[] = [];
|
||||||
|
|
||||||
|
oldState.doc.descendants((oldNode, oldPos) => {
|
||||||
|
console.log(oldNode.type.name)
|
||||||
|
if (oldNode.type.name !== 'image') return;
|
||||||
|
|
||||||
|
if (!newState.doc.resolve(oldPos).parent) return;
|
||||||
|
const newNode = newState.doc.nodeAt(oldPos);
|
||||||
|
|
||||||
|
// Check if the node has been deleted or replaced
|
||||||
|
if (!newNode || newNode.type.name !== 'image') {
|
||||||
|
// Check if the node still exists elsewhere in the document
|
||||||
|
let nodeExists = false;
|
||||||
|
newState.doc.descendants((node) => {
|
||||||
|
if (node.attrs.id === oldNode.attrs.id) {
|
||||||
|
nodeExists = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!nodeExists) {
|
||||||
|
removedImages.push(oldNode as ProseMirrorNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
removedImages.forEach((node) => {
|
||||||
|
const src = node.attrs.src;
|
||||||
|
onNodeDeleted(src);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TrackImageDeletionPlugin;
|
||||||
|
|
||||||
|
async function onNodeDeleted(src: string) {
|
||||||
|
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||||
|
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
|
||||||
|
if (resStatus === 204) {
|
||||||
|
console.log("Image deleted successfully");
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,7 @@ const UploadImagesPlugin = () =>
|
|||||||
const image = document.createElement("img");
|
const image = document.createElement("img");
|
||||||
image.setAttribute(
|
image.setAttribute(
|
||||||
"class",
|
"class",
|
||||||
"w-[35%] opacity-10 rounded-lg border border-custom-border-300",
|
"opacity-10 rounded-lg border border-custom-border-300",
|
||||||
);
|
);
|
||||||
image.src = src;
|
image.src = src;
|
||||||
placeholder.appendChild(image);
|
placeholder.appendChild(image);
|
||||||
|
@ -126,3 +126,27 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
|||||||
transition: opacity 0.2s ease-out;
|
transition: opacity 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.img-placeholder {
|
||||||
|
position: relative;
|
||||||
|
width: 35%;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid rgba(var(--color-text-200));
|
||||||
|
border-top-color: rgba(var(--color-text-800));
|
||||||
|
animation: spinning 0.6s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinning {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user