Skip to content

Commit 0bfb74d

Browse files
authored
[WIKI-830] fix: copy clipboard functionality in the editor (#8229)
* feat: enhance clipboard functionality for markdown and HTML content * fix: improve error handling and state management in CustomImageNodeView component * fix: correct asset retrieval query by removing workspace filter in DuplicateAssetEndpoint * fix: update meta tag creation in PasteAssetPlugin for clipboard HTML content * feat: implement copyMarkdownToClipboard utility for enhanced clipboard functionality * refactor: replace copyMarkdownToClipboard utility with copyTextToClipboard for simplified clipboard operations * refactor: streamline clipboard operations by replacing copyTextToClipboard with copyMarkdownToClipboard in editor components * refactor: simplify PasteAssetPlugin by removing unnecessary meta tag handling and streamlining HTML processing * feat: implement asset duplication processing on paste for enhanced clipboard functionality * chore:remove async from copy markdown method * chore: add paste html * remove:prevent default * refactor: remove hasChanges from processAssetDuplication return type for simplified asset processing * fix: format options-dropdown.tsx
1 parent 362d29c commit 0bfb74d

File tree

11 files changed

+96
-107
lines changed

11 files changed

+96
-107
lines changed

apps/api/plane/app/views/asset/v2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@ def get_entity_id_field(self, entity_type, entity_id):
766766

767767
return {}
768768

769-
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
769+
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
770770
def post(self, request, slug, asset_id):
771771
project_id = request.data.get("project_id", None)
772772
entity_id = request.data.get("entity_id", None)
@@ -792,7 +792,7 @@ def post(self, request, slug, asset_id):
792792

793793
storage = S3Storage(request=request)
794794
original_asset = FileAsset.objects.filter(
795-
workspace=workspace, id=asset_id, is_uploaded=True
795+
id=asset_id, is_uploaded=True
796796
).first()
797797

798798
if not original_asset:

apps/web/core/components/core/description-versions/modal.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,12 @@ export const DescriptionVersionsModal = observer(function DescriptionVersionsMod
5959

6060
const handleCopyMarkdown = useCallback(() => {
6161
if (!editorRef.current) return;
62-
copyTextToClipboard(editorRef.current.getMarkDown()).then(() =>
63-
setToast({
64-
type: TOAST_TYPE.SUCCESS,
65-
title: t("toast.success"),
66-
message: "Markdown copied to clipboard.",
67-
})
68-
);
62+
editorRef.current.copyMarkdownToClipboard();
63+
setToast({
64+
type: TOAST_TYPE.SUCCESS,
65+
title: t("toast.success"),
66+
message: "Markdown copied to clipboard.",
67+
});
6968
}, [t]);
7069

7170
if (!workspaceId) return null;

apps/web/core/components/pages/editor/toolbar/options-dropdown.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,12 @@ export const PageOptionsDropdown = observer(function PageOptionsDropdown(props:
7171
key: "copy-markdown",
7272
action: () => {
7373
if (!editorRef) return;
74-
copyTextToClipboard(editorRef.getMarkDown()).then(() =>
75-
setToast({
76-
type: TOAST_TYPE.SUCCESS,
77-
title: "Success!",
78-
message: "Markdown copied to clipboard.",
79-
})
80-
);
74+
editorRef.copyMarkdownToClipboard();
75+
setToast({
76+
type: TOAST_TYPE.SUCCESS,
77+
title: "Success!",
78+
message: "Markdown copied to clipboard.",
79+
});
8180
},
8281
title: "Copy markdown",
8382
icon: Clipboard,

packages/editor/src/core/extensions/custom-image/components/node-view.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,24 @@ export function CustomImageNodeView(props: CustomImageNodeViewProps) {
5656
return;
5757
}
5858

59+
setResolvedSrc(undefined);
60+
setResolvedDownloadSrc(undefined);
61+
setFailedToLoadImage(false);
62+
5963
const getImageSource = async () => {
60-
const url = await extension.options.getImageSource?.(imgNodeSrc);
61-
setResolvedSrc(url);
62-
const downloadUrl = await extension.options.getImageDownloadSource?.(imgNodeSrc);
63-
setResolvedDownloadSrc(downloadUrl);
64+
try {
65+
const url = await extension.options.getImageSource?.(imgNodeSrc);
66+
setResolvedSrc(url);
67+
const downloadUrl = await extension.options.getImageDownloadSource?.(imgNodeSrc);
68+
setResolvedDownloadSrc(downloadUrl);
69+
} catch (error) {
70+
console.error("Error fetching image source:", error);
71+
setFailedToLoadImage(true);
72+
}
6473
};
6574
getImageSource();
6675
}, [imgNodeSrc, extension.options]);
6776

68-
// Handle image duplication when status is duplicating
6977
useEffect(() => {
7078
const handleDuplication = async () => {
7179
if (status !== ECustomImageStatus.DUPLICATING || !extension.options.duplicateImage || !imgNodeSrc) {
@@ -87,11 +95,8 @@ export function CustomImageNodeView(props: CustomImageNodeViewProps) {
8795
throw new Error("Duplication returned invalid asset ID");
8896
}
8997

90-
// Update node with new source and success status
91-
updateAttributes({
92-
src: newAssetId,
93-
status: ECustomImageStatus.UPLOADED,
94-
});
98+
setFailedToLoadImage(false);
99+
updateAttributes({ src: newAssetId, status: ECustomImageStatus.UPLOADED });
95100
} catch (error: unknown) {
96101
console.error("Failed to duplicate image:", error);
97102
// Update status to failed
@@ -115,11 +120,13 @@ export function CustomImageNodeView(props: CustomImageNodeViewProps) {
115120
useEffect(() => {
116121
if (status === ECustomImageStatus.UPLOADED) {
117122
hasRetriedOnMount.current = false;
123+
setFailedToLoadImage(false);
118124
}
119125
}, [status]);
120126

121127
const hasDuplicationFailed = hasImageDuplicationFailed(status);
122-
const shouldShowBlock = (isUploaded || imageFromFileSystem) && !failedToLoadImage;
128+
const hasValidImageSource = imageFromFileSystem || (isUploaded && resolvedSrc);
129+
const shouldShowBlock = hasValidImageSource && !failedToLoadImage && !hasDuplicationFailed;
123130

124131
return (
125132
<NodeViewWrapper>

packages/editor/src/core/extensions/utility.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import type { TAdditionalActiveDropbarExtensions } from "@/plane-editor/types/ut
88
import { DropHandlerPlugin } from "@/plugins/drop";
99
import { FilePlugins } from "@/plugins/file/root";
1010
import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
11-
// types
12-
import { PasteAssetPlugin } from "@/plugins/paste-asset";
1311
import type { IEditorProps, TEditorAsset, TFileHandler } from "@/types";
1412

1513
type TActiveDropbarExtensions =
@@ -82,7 +80,6 @@ export const UtilityExtension = (props: Props) => {
8280
flaggedExtensions,
8381
editor: this.editor,
8482
}),
85-
PasteAssetPlugin(),
8683
];
8784
},
8885

packages/editor/src/core/helpers/editor-ref.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,27 @@ export const getEditorRefHelpers = (args: TArgs): EditorRefApi => {
8989
});
9090
return markdown;
9191
},
92+
copyMarkdownToClipboard: () => {
93+
if (!editor) return;
94+
95+
const html = editor.getHTML();
96+
const metaData = getEditorMetaData(html);
97+
const markdown = convertHTMLToMarkdown({
98+
description_html: html,
99+
metaData,
100+
});
101+
102+
const copyHandler = (event: ClipboardEvent) => {
103+
event.preventDefault();
104+
event.clipboardData?.setData("text/plain", markdown);
105+
event.clipboardData?.setData("text/html", html);
106+
event.clipboardData?.setData("text/plane-editor-html", html);
107+
document.removeEventListener("copy", copyHandler);
108+
};
109+
110+
document.addEventListener("copy", copyHandler);
111+
document.execCommand("copy");
112+
},
92113
isAnyDropbarOpen: () => {
93114
if (!editor) return false;
94115
const utilityStorage = editor.storage.utility;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { assetDuplicationHandlers } from "@/plane-editor/helpers/asset-duplication";
2+
3+
// Utility function to process HTML content with all registered handlers
4+
export const processAssetDuplication = (htmlContent: string): { processedHtml: string } => {
5+
const tempDiv = document.createElement("div");
6+
tempDiv.innerHTML = htmlContent;
7+
8+
let processedHtml = htmlContent;
9+
10+
// Process each registered component type
11+
for (const [componentName, handler] of Object.entries(assetDuplicationHandlers)) {
12+
const elements = tempDiv.querySelectorAll(componentName);
13+
14+
if (elements.length > 0) {
15+
elements.forEach((element) => {
16+
const result = handler({ element, originalHtml: processedHtml });
17+
if (result.shouldProcess) {
18+
processedHtml = result.modifiedHtml;
19+
}
20+
});
21+
22+
// Update tempDiv with processed HTML for next iteration
23+
tempDiv.innerHTML = processedHtml;
24+
}
25+
}
26+
27+
return { processedHtml };
28+
};

packages/editor/src/core/plugins/markdown-clipboard.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const MarkdownClipboardPlugin = (args: TArgs): Plugin => {
3232
});
3333
event.clipboardData?.setData("text/plain", markdown);
3434
event.clipboardData?.setData("text/html", clipboardHTML);
35+
event.clipboardData?.setData("text/plane-editor-html", clipboardHTML);
3536
return true;
3637
} catch (error) {
3738
console.error("Failed to copy markdown content to clipboard:", error);

packages/editor/src/core/plugins/paste-asset.ts

Lines changed: 0 additions & 77 deletions
This file was deleted.

packages/editor/src/core/props.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { DOMParser } from "@tiptap/pm/model";
12
import type { EditorProps } from "@tiptap/pm/view";
23
// plane utils
34
import { cn } from "@plane/utils";
5+
// helpers
6+
import { processAssetDuplication } from "@/helpers/paste-asset";
47

58
type TArgs = {
69
editorClassName: string;
@@ -27,5 +30,15 @@ export const CoreEditorProps = (props: TArgs): EditorProps => {
2730
}
2831
},
2932
},
33+
handlePaste: (view, event) => {
34+
if (!event.clipboardData) return false;
35+
36+
const htmlContent = event.clipboardData.getData("text/plane-editor-html");
37+
if (!htmlContent) return false;
38+
39+
const { processedHtml } = processAssetDuplication(htmlContent);
40+
view.pasteHTML(processedHtml);
41+
return true;
42+
},
3043
};
3144
};

0 commit comments

Comments
 (0)