mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
feat: editor attachment paste handling (#1975)
* reupload attachments if uploaded to a different page * use image dimensions on paste/DnD * tooltips withinPortal:false * isolating attribute
This commit is contained in:
@@ -132,7 +132,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<div className={classes.toolbar}>
|
<div className={classes.toolbar}>
|
||||||
<Tooltip position="top" label={t("Info")}>
|
<Tooltip position="top" label={t("Info")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => setCalloutType("info")}
|
onClick={() => setCalloutType("info")}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -147,7 +147,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Note")}>
|
<Tooltip position="top" label={t("Note")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => setCalloutType("note")}
|
onClick={() => setCalloutType("note")}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -159,7 +159,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Success")}>
|
<Tooltip position="top" label={t("Success")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => setCalloutType("success")}
|
onClick={() => setCalloutType("success")}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -174,7 +174,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Warning")}>
|
<Tooltip position="top" label={t("Warning")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => setCalloutType("warning")}
|
onClick={() => setCalloutType("warning")}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -189,7 +189,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Danger")}>
|
<Tooltip position="top" label={t("Danger")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => setCalloutType("danger")}
|
onClick={() => setCalloutType("danger")}
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|||||||
@@ -4,6 +4,20 @@ import { uploadAttachmentAction } from "../attachment/upload-attachment-action";
|
|||||||
import { createMentionAction } from "@/features/editor/components/link/internal-link-paste.ts";
|
import { createMentionAction } from "@/features/editor/components/link/internal-link-paste.ts";
|
||||||
import { INTERNAL_LINK_REGEX } from "@/lib/constants.ts";
|
import { INTERNAL_LINK_REGEX } from "@/lib/constants.ts";
|
||||||
import { Editor } from "@tiptap/core";
|
import { Editor } from "@tiptap/core";
|
||||||
|
import {
|
||||||
|
getAttachmentInfo,
|
||||||
|
uploadFile,
|
||||||
|
} from "@/features/page/services/page-service.ts";
|
||||||
|
|
||||||
|
const ATTACHMENT_NODE_TYPES = [
|
||||||
|
"image",
|
||||||
|
"video",
|
||||||
|
"attachment",
|
||||||
|
"excalidraw",
|
||||||
|
"drawio",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ATTACHMENT_URL_RE = /\/api\/files\/([0-9a-f-]+)\//;
|
||||||
|
|
||||||
export const handlePaste = (
|
export const handlePaste = (
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
@@ -57,9 +71,152 @@ export const handlePaste = (
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const htmlData = event.clipboardData?.getData("text/html");
|
||||||
|
if (htmlData && ATTACHMENT_URL_RE.test(htmlData)) {
|
||||||
|
const pasteFrom = editor.state.selection.from;
|
||||||
|
setTimeout(() => {
|
||||||
|
reuploadPastedAttachments(editor, pageId, pasteFrom);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function reuploadPastedAttachments(
|
||||||
|
editor: Editor,
|
||||||
|
pageId: string,
|
||||||
|
pasteFrom: number,
|
||||||
|
) {
|
||||||
|
const pasteEnd = editor.state.selection.from;
|
||||||
|
if (pasteEnd <= pasteFrom) return;
|
||||||
|
|
||||||
|
type PastedNode = {
|
||||||
|
pos: number;
|
||||||
|
attachmentId: string;
|
||||||
|
nodeTypeName: string;
|
||||||
|
src?: string;
|
||||||
|
url?: string;
|
||||||
|
fileName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pastedNodes: PastedNode[] = [];
|
||||||
|
const seenAttachmentIds = new Set<string>();
|
||||||
|
|
||||||
|
editor.state.doc.nodesBetween(pasteFrom, pasteEnd, (node, pos) => {
|
||||||
|
if (!ATTACHMENT_NODE_TYPES.includes(node.type.name)) return;
|
||||||
|
const attachmentId = node.attrs.attachmentId;
|
||||||
|
if (!attachmentId) return;
|
||||||
|
|
||||||
|
const src = node.attrs.src || node.attrs.url || "";
|
||||||
|
const match = ATTACHMENT_URL_RE.exec(src);
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
const fileName =
|
||||||
|
node.attrs.name || src.split("/").pop() || "file";
|
||||||
|
|
||||||
|
pastedNodes.push({
|
||||||
|
pos,
|
||||||
|
attachmentId,
|
||||||
|
nodeTypeName: node.type.name,
|
||||||
|
src: node.attrs.src,
|
||||||
|
url: node.attrs.url,
|
||||||
|
fileName,
|
||||||
|
});
|
||||||
|
seenAttachmentIds.add(attachmentId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pastedNodes.length === 0) return;
|
||||||
|
|
||||||
|
const attachmentPageMap = new Map<string, string | null>();
|
||||||
|
await Promise.all(
|
||||||
|
[...seenAttachmentIds].map(async (id) => {
|
||||||
|
try {
|
||||||
|
const info = await getAttachmentInfo(id);
|
||||||
|
attachmentPageMap.set(id, info.pageId);
|
||||||
|
} catch {
|
||||||
|
attachmentPageMap.set(id, null);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodesToReupload = pastedNodes.filter((n) => {
|
||||||
|
const ownerPageId = attachmentPageMap.get(n.attachmentId);
|
||||||
|
return ownerPageId !== null && ownerPageId !== pageId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nodesToReupload.length === 0) return;
|
||||||
|
|
||||||
|
const uniqueNodes = new Map<string, (typeof nodesToReupload)[0]>();
|
||||||
|
for (const node of nodesToReupload) {
|
||||||
|
if (!uniqueNodes.has(node.attachmentId)) {
|
||||||
|
uniqueNodes.set(node.attachmentId, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reuploadResults = new Map<
|
||||||
|
string,
|
||||||
|
{ id: string; fileName: string; fileSize: number; mimeType: string }
|
||||||
|
>();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[...uniqueNodes.values()].map(async (node) => {
|
||||||
|
const fileUrl = node.src || node.url;
|
||||||
|
if (!fileUrl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(fileUrl, { credentials: "include" });
|
||||||
|
if (!response.ok) return;
|
||||||
|
const blob = await response.blob();
|
||||||
|
const file = new File([blob], node.fileName, { type: blob.type });
|
||||||
|
const newAttachment = await uploadFile(file, pageId);
|
||||||
|
reuploadResults.set(node.attachmentId, {
|
||||||
|
id: newAttachment.id,
|
||||||
|
fileName: newAttachment.fileName,
|
||||||
|
fileSize: newAttachment.fileSize,
|
||||||
|
mimeType: newAttachment.mimeType,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// keep original reference on failure
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (reuploadResults.size === 0) return;
|
||||||
|
|
||||||
|
editor.chain().command(({ tr }) => {
|
||||||
|
const sorted = [...nodesToReupload].sort((a, b) => b.pos - a.pos);
|
||||||
|
|
||||||
|
for (const pastedNode of sorted) {
|
||||||
|
const result = reuploadResults.get(pastedNode.attachmentId);
|
||||||
|
if (!result) continue;
|
||||||
|
|
||||||
|
const node = tr.doc.nodeAt(pastedNode.pos);
|
||||||
|
if (!node || node.attrs.attachmentId !== pastedNode.attachmentId)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const newAttrs = { ...node.attrs };
|
||||||
|
newAttrs.attachmentId = result.id;
|
||||||
|
|
||||||
|
if (newAttrs.src) {
|
||||||
|
newAttrs.src = `/api/files/${result.id}/${result.fileName}`;
|
||||||
|
}
|
||||||
|
if (newAttrs.url) {
|
||||||
|
newAttrs.url = `/api/files/${result.id}/${result.fileName}`;
|
||||||
|
}
|
||||||
|
if (pastedNode.nodeTypeName === "attachment") {
|
||||||
|
newAttrs.name = result.fileName;
|
||||||
|
newAttrs.mime = result.mimeType;
|
||||||
|
newAttrs.size = result.fileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.setNodeMarkup(pastedNode.pos, undefined, newAttrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}).run();
|
||||||
|
}
|
||||||
|
|
||||||
export const handleFileDrop = (
|
export const handleFileDrop = (
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
event: DragEvent,
|
event: DragEvent,
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import {
|
|||||||
EditorMenuProps,
|
EditorMenuProps,
|
||||||
ShouldShowProps,
|
ShouldShowProps,
|
||||||
} from "@/features/editor/components/table/types/types.ts";
|
} from "@/features/editor/components/table/types/types.ts";
|
||||||
import { ActionIcon, Modal, Tooltip, useComputedColorScheme } from "@mantine/core";
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Modal,
|
||||||
|
Tooltip,
|
||||||
|
useComputedColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
@@ -194,7 +199,7 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<div className={classes.toolbar}>
|
<div className={classes.toolbar}>
|
||||||
<Tooltip position="top" label={t("Align left")}>
|
<Tooltip position="top" label={t("Align left")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignLeft}
|
onClick={alignLeft}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -206,7 +211,11 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Align center")}>
|
<Tooltip
|
||||||
|
position="top"
|
||||||
|
label={t("Align center")}
|
||||||
|
withinPortal={false}
|
||||||
|
>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignCenter}
|
onClick={alignCenter}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -232,7 +241,7 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
|
|
||||||
<div className={classes.divider} />
|
<div className={classes.divider} />
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Edit")}>
|
<Tooltip position="top" label={t("Edit")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -243,7 +252,7 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Download")}>
|
<Tooltip position="top" label={t("Download")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -254,7 +263,7 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Delete")}>
|
<Tooltip position="top" label={t("Delete")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|||||||
@@ -79,8 +79,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
editor.isActive("excalidraw") &&
|
editor.isActive("excalidraw") && editor.getAttributes("excalidraw")?.src
|
||||||
editor.getAttributes("excalidraw")?.src
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[editor],
|
[editor],
|
||||||
@@ -228,7 +227,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<div className={classes.toolbar}>
|
<div className={classes.toolbar}>
|
||||||
<Tooltip position="top" label={t("Align left")}>
|
<Tooltip position="top" label={t("Align left")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignLeft}
|
onClick={alignLeft}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -242,7 +241,11 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Align center")}>
|
<Tooltip
|
||||||
|
position="top"
|
||||||
|
label={t("Align center")}
|
||||||
|
withinPortal={false}
|
||||||
|
>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignCenter}
|
onClick={alignCenter}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -256,7 +259,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Align right")}>
|
<Tooltip position="top" label={t("Align right")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignRight}
|
onClick={alignRight}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -272,7 +275,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
|
|
||||||
<div className={classes.divider} />
|
<div className={classes.divider} />
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Edit")}>
|
<Tooltip position="top" label={t("Edit")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -283,7 +286,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Download")}>
|
<Tooltip position="top" label={t("Download")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -294,7 +297,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Delete")}>
|
<Tooltip position="top" label={t("Delete")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<div className={classes.toolbar}>
|
<div className={classes.toolbar}>
|
||||||
<Tooltip position="top" label={t("Align left")}>
|
<Tooltip position="top" label={t("Align left")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignImageLeft}
|
onClick={alignImageLeft}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -161,7 +161,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Align center")}>
|
<Tooltip position="top" label={t("Align center")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignImageCenter}
|
onClick={alignImageCenter}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -173,7 +173,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Align right")}>
|
<Tooltip position="top" label={t("Align right")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignImageRight}
|
onClick={alignImageRight}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -187,7 +187,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
|
|
||||||
<div className={classes.divider} />
|
<div className={classes.divider} />
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Download")}>
|
<Tooltip position="top" label={t("Download")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -198,7 +198,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Replace image")}>
|
<Tooltip position="top" label={t("Replace image")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={handleReplace}
|
onClick={handleReplace}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -209,7 +209,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Delete")}>
|
<Tooltip position="top" label={t("Delete")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<div className={classes.toolbar}>
|
<div className={classes.toolbar}>
|
||||||
<Tooltip position="top" label={t("Align left")}>
|
<Tooltip position="top" label={t("Align left")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignLeft}
|
onClick={alignLeft}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -137,7 +137,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Align center")}>
|
<Tooltip position="top" label={t("Align center")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignCenter}
|
onClick={alignCenter}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -149,7 +149,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Align right")}>
|
<Tooltip position="top" label={t("Align right")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignRight}
|
onClick={alignRight}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -163,7 +163,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
|
|
||||||
<div className={classes.divider} />
|
<div className={classes.divider} />
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Download")}>
|
<Tooltip position="top" label={t("Download")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -174,7 +174,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Delete")}>
|
<Tooltip position="top" label={t("Delete")} withinPortal={false}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|||||||
@@ -158,6 +158,15 @@ export async function importZip(
|
|||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAttachmentInfo(
|
||||||
|
attachmentId: string,
|
||||||
|
): Promise<IAttachment> {
|
||||||
|
const req = await api.post<IAttachment>("/files/info", {
|
||||||
|
attachmentId,
|
||||||
|
});
|
||||||
|
return req.data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function uploadFile(
|
export async function uploadFile(
|
||||||
file: File,
|
file: File,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import { EnvironmentService } from '../../integrations/environment/environment.s
|
|||||||
import { TokenService } from '../auth/services/token.service';
|
import { TokenService } from '../auth/services/token.service';
|
||||||
import { JwtAttachmentPayload, JwtType } from '../auth/dto/jwt-payload';
|
import { JwtAttachmentPayload, JwtType } from '../auth/dto/jwt-payload';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { RemoveIconDto } from './dto/attachment.dto';
|
import { AttachmentInfoDto, RemoveIconDto } from './dto/attachment.dto';
|
||||||
import { PageAccessService } from '../page/page-access/page-access.service';
|
import { PageAccessService } from '../page/page-access/page-access.service';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
@@ -349,6 +349,34 @@ export class AttachmentController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('files/info')
|
||||||
|
async getAttachmentInfo(
|
||||||
|
@Body() dto: AttachmentInfoDto,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
) {
|
||||||
|
const attachment = await this.attachmentRepo.findById(dto.attachmentId);
|
||||||
|
if (
|
||||||
|
!attachment ||
|
||||||
|
!attachment.pageId ||
|
||||||
|
attachment.workspaceId !== workspace.id ||
|
||||||
|
attachment.type !== AttachmentType.File
|
||||||
|
) {
|
||||||
|
throw new NotFoundException('File not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = await this.pageRepo.findById(attachment.pageId);
|
||||||
|
if (!page) {
|
||||||
|
throw new NotFoundException('File not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.pageAccessService.validateCanView(page, user);
|
||||||
|
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('attachments/remove-icon')
|
@Post('attachments/remove-icon')
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { IsEnum, IsIn, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
|
import { IsEnum, IsIn, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
|
||||||
import { AttachmentType } from '../attachment.constants';
|
import { AttachmentType } from '../attachment.constants';
|
||||||
|
|
||||||
|
export class AttachmentInfoDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsUUID()
|
||||||
|
attachmentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class RemoveIconDto {
|
export class RemoveIconDto {
|
||||||
@IsEnum(AttachmentType)
|
@IsEnum(AttachmentType)
|
||||||
@IsIn([
|
@IsIn([
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: dc8da28f24...9e493d75f5
@@ -53,6 +53,7 @@ export const Callout = Node.create<CalloutOptions>({
|
|||||||
content: "block+",
|
content: "block+",
|
||||||
group: "block",
|
group: "block",
|
||||||
defining: true,
|
defining: true,
|
||||||
|
isolating: true,
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ const handleImageUpload =
|
|||||||
);
|
);
|
||||||
|
|
||||||
const placeholderId = generateNodeId();
|
const placeholderId = generateNodeId();
|
||||||
|
const width = imageDimensions?.width ?? undefined;
|
||||||
|
const height = imageDimensions?.height ?? undefined;
|
||||||
const aspectRatio = imageDimensions
|
const aspectRatio = imageDimensions
|
||||||
? imageDimensions.width / imageDimensions.height
|
? imageDimensions.width / imageDimensions.height
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -57,6 +59,8 @@ const handleImageUpload =
|
|||||||
id: placeholderId,
|
id: placeholderId,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
},
|
},
|
||||||
|
width,
|
||||||
|
height,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,6 +92,8 @@ const handleImageUpload =
|
|||||||
src: `/api/files/${attachment.id}/${attachment.fileName}`,
|
src: `/api/files/${attachment.id}/${attachment.fileName}`,
|
||||||
attachmentId: attachment.id,
|
attachmentId: attachment.id,
|
||||||
size: attachment.fileSize,
|
size: attachment.fileSize,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const MathBlock = Node.create({
|
|||||||
name: "mathBlock",
|
name: "mathBlock",
|
||||||
group: "block",
|
group: "block",
|
||||||
atom: true,
|
atom: true,
|
||||||
|
isolating: true,
|
||||||
|
|
||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const Subpages = Node.create<SubpagesOptions>({
|
|||||||
group: "block",
|
group: "block",
|
||||||
atom: true,
|
atom: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
|
isolating: true,
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ const handleVideoUpload =
|
|||||||
const objectUrl = URL.createObjectURL(file);
|
const objectUrl = URL.createObjectURL(file);
|
||||||
const videoDimensions = await getVideoDimensions(objectUrl);
|
const videoDimensions = await getVideoDimensions(objectUrl);
|
||||||
const placeholderId = generateNodeId();
|
const placeholderId = generateNodeId();
|
||||||
const aspectRatio = videoDimensions.aspectRatio;
|
const width = videoDimensions?.width ?? undefined;
|
||||||
|
const height = videoDimensions?.height ?? undefined;
|
||||||
|
const aspectRatio = videoDimensions?.aspectRatio;
|
||||||
|
|
||||||
let placeholderInserted = false;
|
let placeholderInserted = false;
|
||||||
|
|
||||||
@@ -76,6 +78,8 @@ const handleVideoUpload =
|
|||||||
id: placeholderId,
|
id: placeholderId,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
},
|
},
|
||||||
|
width,
|
||||||
|
height,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,6 +112,8 @@ const handleVideoUpload =
|
|||||||
attachmentId: attachment.id,
|
attachmentId: attachment.id,
|
||||||
title: attachment.fileName,
|
title: attachment.fileName,
|
||||||
size: attachment.fileSize,
|
size: attachment.fileSize,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user