diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/teamspaces/(detail)/[teamId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/teamspaces/(detail)/[teamId]/page.tsx
index 1cdf032d94e..da784069c18 100644
--- a/apps/web/app/(all)/[workspaceSlug]/(projects)/teamspaces/(detail)/[teamId]/page.tsx
+++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/teamspaces/(detail)/[teamId]/page.tsx
@@ -7,6 +7,8 @@
// Teamspaces — mote.
// See docs/mote-design/05-teamspaces-access.md, section 1.
+"use client";
+
import { useParams } from "next/navigation";
import { TeamspaceDetailRoot } from "@/plane-web/components/teamspaces";
diff --git a/packages/editor/src/core/constants/config.ts b/packages/editor/src/core/constants/config.ts
index 9e126fa2a7e..7f8cf5d3ea5 100644
--- a/packages/editor/src/core/constants/config.ts
+++ b/packages/editor/src/core/constants/config.ts
@@ -16,6 +16,8 @@ export const DEFAULT_DISPLAY_CONFIG: TDisplayConfig = {
export const ACCEPTED_IMAGE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"];
+export const ACCEPTED_VIDEO_MIME_TYPES = ["video/mp4", "video/webm", "video/quicktime"];
+
export const ACCEPTED_ATTACHMENT_MIME_TYPES = [
"image/jpeg",
"image/png",
diff --git a/packages/editor/src/core/constants/extension.ts b/packages/editor/src/core/constants/extension.ts
index 3226cfd8d15..35ffb03420f 100644
--- a/packages/editor/src/core/constants/extension.ts
+++ b/packages/editor/src/core/constants/extension.ts
@@ -14,6 +14,7 @@ export enum CORE_EXTENSIONS {
CODE_INLINE = "code",
CUSTOM_COLOR = "customColor",
CUSTOM_IMAGE = "imageComponent",
+ CUSTOM_VIDEO = "videoComponent",
CUSTOM_LINK = "link",
DOCUMENT = "doc",
DROP_CURSOR = "dropCursor",
@@ -72,6 +73,7 @@ export const BLOCK_NODE_TYPES = [
// Media and embed nodes
CORE_EXTENSIONS.IMAGE,
CORE_EXTENSIONS.CUSTOM_IMAGE,
+ CORE_EXTENSIONS.CUSTOM_VIDEO,
CORE_EXTENSIONS.CALLOUT,
CORE_EXTENSIONS.WORK_ITEM_EMBED,
];
diff --git a/packages/editor/src/core/extensions/custom-video/components/block.tsx b/packages/editor/src/core/extensions/custom-video/components/block.tsx
new file mode 100644
index 00000000000..823167d275a
--- /dev/null
+++ b/packages/editor/src/core/extensions/custom-video/components/block.tsx
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+import { Download } from "lucide-react";
+import { useCallback } from "react";
+// plane imports
+import { cn } from "@plane/utils";
+// local imports
+import type { CustomVideoNodeViewProps } from "./node-view";
+
+type CustomVideoBlockProps = CustomVideoNodeViewProps & {
+ videoFromFileSystem: string | undefined;
+ setFailedToLoadVideo: (isError: boolean) => void;
+ src: string | undefined;
+ downloadSrc: string | undefined;
+};
+
+export function CustomVideoBlock(props: CustomVideoBlockProps) {
+ const { selected, setFailedToLoadVideo, src: resolvedVideoSrc, downloadSrc, videoFromFileSystem } = props;
+
+ const videoSrc = videoFromFileSystem || resolvedVideoSrc;
+
+ const handleError = useCallback(() => {
+ setFailedToLoadVideo(true);
+ }, [setFailedToLoadVideo]);
+
+ const handleDownload = useCallback(() => {
+ if (!downloadSrc) return;
+ window.open(downloadSrc, "_blank", "noopener,noreferrer");
+ }, [downloadSrc]);
+
+ return (
+
+
+ {downloadSrc && (
+
+ )}
+
+ );
+}
diff --git a/packages/editor/src/core/extensions/custom-video/components/node-view.tsx b/packages/editor/src/core/extensions/custom-video/components/node-view.tsx
new file mode 100644
index 00000000000..b8614401bf4
--- /dev/null
+++ b/packages/editor/src/core/extensions/custom-video/components/node-view.tsx
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+import { NodeViewWrapper } from "@tiptap/react";
+import type { NodeViewProps } from "@tiptap/react";
+import { useEffect, useRef, useState } from "react";
+// local imports
+import type { CustomVideoExtensionType, TCustomVideoAttributes } from "../types";
+import { ECustomVideoAttributeNames } from "../types";
+import { CustomVideoBlock } from "./block";
+import { CustomVideoUploader } from "./uploader";
+
+export type CustomVideoNodeViewProps = Omit & {
+ extension: CustomVideoExtensionType;
+ node: NodeViewProps["node"] & {
+ attrs: TCustomVideoAttributes;
+ };
+ updateAttributes: (attrs: Partial) => void;
+};
+
+export function CustomVideoNodeView(props: CustomVideoNodeViewProps) {
+ const { extension, node } = props;
+ const { src: videoNodeSrc } = node.attrs;
+
+ const [isUploaded, setIsUploaded] = useState(!!videoNodeSrc);
+ const [resolvedSrc, setResolvedSrc] = useState(undefined);
+ const [resolvedDownloadSrc, setResolvedDownloadSrc] = useState(undefined);
+ const [videoFromFileSystem, setVideoFromFileSystem] = useState(undefined);
+ const [failedToLoadVideo, setFailedToLoadVideo] = useState(false);
+
+ const videoComponentRef = useRef(null);
+
+ // the video is already uploaded if the video-component node has src attribute
+ // and we need to remove the blob from our file system
+ useEffect(() => {
+ if (resolvedSrc || videoNodeSrc) {
+ setIsUploaded(true);
+ setVideoFromFileSystem(undefined);
+ } else {
+ setIsUploaded(false);
+ }
+ }, [resolvedSrc, videoNodeSrc]);
+
+ useEffect(() => {
+ if (!videoNodeSrc) {
+ setResolvedSrc(undefined);
+ setResolvedDownloadSrc(undefined);
+ return;
+ }
+
+ setResolvedSrc(undefined);
+ setResolvedDownloadSrc(undefined);
+ setFailedToLoadVideo(false);
+
+ const getVideoSource = async () => {
+ try {
+ const url = await extension.options.getVideoSource?.(videoNodeSrc);
+ setResolvedSrc(url);
+ const downloadUrl = await extension.options.getVideoDownloadSource?.(videoNodeSrc);
+ setResolvedDownloadSrc(downloadUrl);
+ } catch (error) {
+ console.error("Error fetching video source:", error);
+ setFailedToLoadVideo(true);
+ }
+ };
+ void getVideoSource();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [videoNodeSrc, extension.options.getVideoSource, extension.options.getVideoDownloadSource]);
+
+ const hasValidVideoSource = videoFromFileSystem || (isUploaded && resolvedSrc);
+ const shouldShowBlock = hasValidVideoSource && !failedToLoadVideo;
+
+ return (
+
+
+ {shouldShowBlock ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/packages/editor/src/core/extensions/custom-video/components/uploader.tsx b/packages/editor/src/core/extensions/custom-video/components/uploader.tsx
new file mode 100644
index 00000000000..fc04f19fda1
--- /dev/null
+++ b/packages/editor/src/core/extensions/custom-video/components/uploader.tsx
@@ -0,0 +1,195 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+import { VideoIcon } from "lucide-react";
+import type { ChangeEvent } from "react";
+import { useCallback, useEffect, useMemo, useRef } from "react";
+// plane imports
+import { cn } from "@plane/utils";
+// constants
+import { ACCEPTED_VIDEO_MIME_TYPES } from "@/constants/config";
+// helpers
+import type { EFileError } from "@/helpers/file";
+// hooks
+import { useUploader, useDropZone, uploadFirstFileAndInsertRemaining } from "@/hooks/use-file-upload";
+// local imports
+import { ECustomVideoStatus } from "../types";
+import { getVideoComponentFileMap } from "../utils";
+import type { CustomVideoNodeViewProps } from "./node-view";
+
+type CustomVideoUploaderProps = CustomVideoNodeViewProps & {
+ failedToLoadVideo: boolean;
+ loadVideoFromFileSystem: (file: string) => void;
+ maxFileSize: number;
+ setIsUploaded: (isUploaded: boolean) => void;
+};
+
+export function CustomVideoUploader(props: CustomVideoUploaderProps) {
+ const {
+ editor,
+ extension,
+ failedToLoadVideo,
+ getPos,
+ loadVideoFromFileSystem,
+ maxFileSize,
+ node,
+ selected,
+ setIsUploaded,
+ updateAttributes,
+ } = props;
+ // refs
+ const fileInputRef = useRef(null);
+ const hasTriggeredFilePickerRef = useRef(false);
+ const hasTriedUploadingOnMountRef = useRef(false);
+ const { id: videoEntityId } = node.attrs;
+ // derived values
+ const videoComponentFileMap = useMemo(() => getVideoComponentFileMap(editor), [editor]);
+ const isTouchDevice = !!(editor.storage.utility as { isTouchDevice?: boolean } | undefined)?.isTouchDevice;
+
+ const onUpload = useCallback(
+ (url: string) => {
+ if (url) {
+ if (!videoEntityId) return;
+ setIsUploaded(true);
+ updateAttributes({
+ src: url,
+ status: ECustomVideoStatus.UPLOADED,
+ });
+ videoComponentFileMap?.delete(videoEntityId);
+ }
+ },
+ [videoComponentFileMap, videoEntityId, updateAttributes, setIsUploaded]
+ );
+
+ const uploadVideoEditorCommand = useCallback(
+ async (file: File) => {
+ updateAttributes({ status: ECustomVideoStatus.UPLOADING });
+ return await extension.options.uploadVideo?.(videoEntityId ?? "", file);
+ },
+ [extension.options, videoEntityId, updateAttributes]
+ );
+
+ const handleProgressStatus = useCallback(
+ (isUploading: boolean) => {
+ editor.storage.utility.uploadInProgress = isUploading;
+ },
+ [editor]
+ );
+
+ const handleInvalidFile = useCallback((_error: EFileError, _file: File, message: string) => {
+ alert(message);
+ }, []);
+
+ // hooks
+ const { isUploading: isVideoBeingUploaded, uploadFile } = useUploader({
+ acceptedMimeTypes: ACCEPTED_VIDEO_MIME_TYPES,
+ editorCommand: uploadVideoEditorCommand,
+ handleProgressStatus,
+ loadFileFromFileSystem: loadVideoFromFileSystem,
+ maxFileSize,
+ onInvalidFile: handleInvalidFile,
+ onUpload,
+ });
+
+ const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({
+ editor,
+ getPos,
+ type: "attachment",
+ uploader: uploadFile,
+ });
+
+ // after the video component is mounted we start the upload process based on
+ // it's uploaded
+ useEffect(() => {
+ if (hasTriedUploadingOnMountRef.current) return;
+
+ const meta = videoComponentFileMap?.get(videoEntityId ?? "");
+ if (meta) {
+ if (meta.event === "drop" && "file" in meta) {
+ hasTriedUploadingOnMountRef.current = true;
+ uploadFile(meta.file);
+ } else if (meta.event === "insert" && fileInputRef.current && !hasTriggeredFilePickerRef.current) {
+ if (meta.hasOpenedFileInputOnce) return;
+ if (!isTouchDevice) {
+ fileInputRef.current.click();
+ }
+ hasTriggeredFilePickerRef.current = true;
+ videoComponentFileMap?.set(videoEntityId ?? "", { ...meta, hasOpenedFileInputOnce: true });
+ }
+ } else {
+ hasTriedUploadingOnMountRef.current = true;
+ }
+ }, [videoEntityId, isTouchDevice, uploadFile, videoComponentFileMap]);
+
+ const onFileChange = useCallback(
+ async (e: ChangeEvent) => {
+ e.preventDefault();
+ const filesList = e.target.files;
+ const pos = getPos();
+ if (!filesList || pos === undefined) {
+ return;
+ }
+ await uploadFirstFileAndInsertRemaining({
+ editor,
+ filesList,
+ pos,
+ type: "attachment",
+ uploader: uploadFile,
+ });
+ },
+ [uploadFile, editor, getPos]
+ );
+
+ const getDisplayMessage = useCallback(() => {
+ if (failedToLoadVideo) {
+ return "Error loading video";
+ }
+ if (isVideoBeingUploaded) {
+ return "Uploading...";
+ }
+ if (draggedInside && editor.isEditable) {
+ return "Drop video here";
+ }
+ return "Add a video";
+ }, [draggedInside, editor.isEditable, failedToLoadVideo, isVideoBeingUploaded]);
+
+ return (
+ {
+ if (!failedToLoadVideo && editor.isEditable) {
+ fileInputRef.current?.click();
+ }
+ }}
+ >
+
+
{getDisplayMessage()}
+
+
+ );
+}
diff --git a/packages/editor/src/core/extensions/custom-video/extension-config.ts b/packages/editor/src/core/extensions/custom-video/extension-config.ts
new file mode 100644
index 00000000000..91f6518ad80
--- /dev/null
+++ b/packages/editor/src/core/extensions/custom-video/extension-config.ts
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+import { Node, mergeAttributes } from "@tiptap/core";
+// constants
+import { CORE_EXTENSIONS } from "@/constants/extension";
+// local imports
+import { ECustomVideoAttributeNames } from "./types";
+import type {
+ CustomVideoExtensionOptions,
+ TCustomVideoAttributes,
+ CustomVideoExtensionType,
+ CustomVideoExtensionStorage,
+ InsertVideoComponentProps,
+} from "./types";
+import { DEFAULT_CUSTOM_VIDEO_ATTRIBUTES } from "./utils";
+
+declare module "@tiptap/core" {
+ interface Commands {
+ [CORE_EXTENSIONS.CUSTOM_VIDEO]: {
+ insertVideoComponent: ({ file, pos, event }: InsertVideoComponentProps) => ReturnType;
+ };
+ }
+ interface Storage {
+ [CORE_EXTENSIONS.CUSTOM_VIDEO]: CustomVideoExtensionStorage;
+ }
+}
+
+export const CustomVideoExtensionConfig: CustomVideoExtensionType = Node.create<
+ CustomVideoExtensionOptions,
+ CustomVideoExtensionStorage
+>({
+ name: CORE_EXTENSIONS.CUSTOM_VIDEO,
+ group: "block",
+ atom: true,
+
+ addAttributes() {
+ return Object.values(ECustomVideoAttributeNames).reduce(
+ (acc, value) => {
+ acc[value] = {
+ default: DEFAULT_CUSTOM_VIDEO_ATTRIBUTES[value],
+ };
+ return acc;
+ },
+ {} as Record
+ );
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: "video-component",
+ },
+ ];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ["video-component", mergeAttributes(HTMLAttributes)];
+ },
+});
diff --git a/packages/editor/src/core/extensions/custom-video/extension.tsx b/packages/editor/src/core/extensions/custom-video/extension.tsx
new file mode 100644
index 00000000000..63099c69a43
--- /dev/null
+++ b/packages/editor/src/core/extensions/custom-video/extension.tsx
@@ -0,0 +1,130 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+import { ReactNodeViewRenderer } from "@tiptap/react";
+import { v4 as uuidv4 } from "uuid";
+// constants
+import { ACCEPTED_VIDEO_MIME_TYPES } from "@/constants/config";
+// helpers
+import { isFileValid } from "@/helpers/file";
+import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
+// types
+import type { TFileHandler } from "@/types";
+// local imports
+import type { CustomVideoNodeViewProps } from "./components/node-view";
+import { CustomVideoNodeView } from "./components/node-view";
+import { CustomVideoExtensionConfig } from "./extension-config";
+import type { CustomVideoExtensionOptions, CustomVideoExtensionStorage } from "./types";
+import { ECustomVideoAttributeNames, ECustomVideoStatus } from "./types";
+import { getVideoComponentFileMap } from "./utils";
+
+type Props = {
+ fileHandler: TFileHandler;
+ isEditable: boolean;
+};
+
+export function CustomVideoExtension(props: Props) {
+ const { fileHandler, isEditable } = props;
+ // derived values
+ const { getAssetSrc, getAssetDownloadSrc } = fileHandler;
+
+ return CustomVideoExtensionConfig.extend({
+ selectable: isEditable,
+ draggable: isEditable,
+
+ addOptions() {
+ const upload = "upload" in fileHandler ? fileHandler.upload : undefined;
+ return {
+ ...this.parent?.(),
+ getVideoDownloadSource: getAssetDownloadSrc,
+ getVideoSource: getAssetSrc,
+ uploadVideo: upload,
+ };
+ },
+
+ addStorage() {
+ const maxFileSize = "validation" in fileHandler ? fileHandler.validation?.maxFileSize : 0;
+
+ return {
+ fileMap: new Map(),
+ maxFileSize,
+ // escape markdown for videos
+ markdown: {
+ serialize() {},
+ },
+ };
+ },
+
+ addCommands() {
+ return {
+ insertVideoComponent:
+ (props) =>
+ ({ commands }) => {
+ // Early return if there's an invalid file being dropped
+ if (
+ props?.file &&
+ !isFileValid({
+ acceptedMimeTypes: ACCEPTED_VIDEO_MIME_TYPES,
+ file: props.file,
+ maxFileSize: this.storage.maxFileSize,
+ onError: (_error, message) => alert(message),
+ })
+ ) {
+ return false;
+ }
+
+ // generate a unique id for the video to keep track of dropped
+ // files' file data
+ const fileId = uuidv4();
+
+ const videoComponentFileMap = getVideoComponentFileMap(this.editor);
+
+ if (videoComponentFileMap) {
+ if (props?.event === "drop" && props.file) {
+ videoComponentFileMap.set(fileId, {
+ file: props.file,
+ event: props.event,
+ });
+ } else if (props.event === "insert") {
+ videoComponentFileMap.set(fileId, {
+ event: props.event,
+ hasOpenedFileInputOnce: false,
+ });
+ }
+ }
+
+ const attributes = {
+ [ECustomVideoAttributeNames.ID]: fileId,
+ [ECustomVideoAttributeNames.STATUS]: ECustomVideoStatus.PENDING,
+ };
+
+ if (props.pos) {
+ return commands.insertContentAt(props.pos, {
+ type: this.name,
+ attrs: attributes,
+ });
+ }
+ return commands.insertContent({
+ type: this.name,
+ attrs: attributes,
+ });
+ },
+ };
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name),
+ ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name),
+ };
+ },
+ addNodeView() {
+ return ReactNodeViewRenderer((props) => (
+
+ ));
+ },
+ });
+}
diff --git a/packages/editor/src/core/extensions/custom-video/types.ts b/packages/editor/src/core/extensions/custom-video/types.ts
new file mode 100644
index 00000000000..453e119c0e4
--- /dev/null
+++ b/packages/editor/src/core/extensions/custom-video/types.ts
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+import type { Node } from "@tiptap/core";
+// types
+import type { TFileHandler } from "@/types";
+
+export enum ECustomVideoAttributeNames {
+ ID = "id",
+ SOURCE = "src",
+ STATUS = "status",
+}
+
+export enum ECustomVideoStatus {
+ PENDING = "pending",
+ UPLOADING = "uploading",
+ UPLOADED = "uploaded",
+}
+
+export type TCustomVideoAttributes = {
+ [ECustomVideoAttributeNames.ID]: string | null;
+ [ECustomVideoAttributeNames.SOURCE]: string | null;
+ [ECustomVideoAttributeNames.STATUS]: ECustomVideoStatus;
+};
+
+export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { hasOpenedFileInputOnce?: boolean };
+
+export type InsertVideoComponentProps = {
+ file?: File;
+ pos?: number;
+ event: "insert" | "drop";
+};
+
+export type CustomVideoExtensionOptions = {
+ getVideoSource: TFileHandler["getAssetSrc"];
+ getVideoDownloadSource: TFileHandler["getAssetDownloadSrc"];
+ uploadVideo?: TFileHandler["upload"];
+};
+
+export type CustomVideoExtensionStorage = {
+ fileMap: Map;
+ maxFileSize: number;
+};
+
+export type CustomVideoExtensionType = Node;
diff --git a/packages/editor/src/core/extensions/custom-video/utils.ts b/packages/editor/src/core/extensions/custom-video/utils.ts
new file mode 100644
index 00000000000..bef331562f0
--- /dev/null
+++ b/packages/editor/src/core/extensions/custom-video/utils.ts
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+import type { Editor } from "@tiptap/core";
+// local imports
+import { ECustomVideoAttributeNames, ECustomVideoStatus } from "./types";
+import type { TCustomVideoAttributes } from "./types";
+
+export const DEFAULT_CUSTOM_VIDEO_ATTRIBUTES: TCustomVideoAttributes = {
+ [ECustomVideoAttributeNames.SOURCE]: null,
+ [ECustomVideoAttributeNames.ID]: null,
+ [ECustomVideoAttributeNames.STATUS]: ECustomVideoStatus.PENDING,
+};
+
+export const getVideoComponentFileMap = (editor: Editor) => editor.storage.videoComponent?.fileMap;
diff --git a/packages/editor/src/core/extensions/extensions.ts b/packages/editor/src/core/extensions/extensions.ts
index 420e749507f..ac1017a3f8c 100644
--- a/packages/editor/src/core/extensions/extensions.ts
+++ b/packages/editor/src/core/extensions/extensions.ts
@@ -39,6 +39,7 @@ import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions";
import type { IEditorProps } from "@/types";
// local imports
import { CustomImageExtension } from "./custom-image/extension";
+import { CustomVideoExtension } from "./custom-video/extension";
import { EmojiExtension } from "./emoji/extension";
import { CustomPlaceholderExtension } from "./placeholder";
import { CustomStarterKitExtension } from "./starter-kit";
@@ -152,5 +153,14 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
);
}
+ if (!disabledExtensions.includes("video")) {
+ extensions.push(
+ CustomVideoExtension({
+ fileHandler,
+ isEditable: editable,
+ })
+ );
+ }
+
return extensions;
};
diff --git a/packages/editor/src/core/plugins/drop.ts b/packages/editor/src/core/plugins/drop.ts
index e0e9057a5c2..7076482ab50 100644
--- a/packages/editor/src/core/plugins/drop.ts
+++ b/packages/editor/src/core/plugins/drop.ts
@@ -7,7 +7,11 @@
import type { Editor } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
// constants
-import { ACCEPTED_ATTACHMENT_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config";
+import {
+ ACCEPTED_ATTACHMENT_MIME_TYPES,
+ ACCEPTED_IMAGE_MIME_TYPES,
+ ACCEPTED_VIDEO_MIME_TYPES,
+} from "@/constants/config";
// types
import type { TEditorCommands, TExtensions } from "@/types";
@@ -126,7 +130,12 @@ export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => {
pos,
event,
});
- } else if (fileType === "attachment") {
+ } else if (fileType === "attachment" && ACCEPTED_VIDEO_MIME_TYPES.includes(file.type) && !disabledExtensions?.includes("video")) {
+ editor.commands.insertVideoComponent({
+ file,
+ pos,
+ event,
+ });
}
} catch (error) {
console.error(`Error while ${event}ing file:`, error);
diff --git a/packages/editor/src/core/types/extensions.ts b/packages/editor/src/core/types/extensions.ts
index 2670b6c0398..82d80ee1490 100644
--- a/packages/editor/src/core/types/extensions.ts
+++ b/packages/editor/src/core/types/extensions.ts
@@ -4,4 +4,4 @@
* See the LICENSE file for details.
*/
-export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed" | "slash-commands" | "enter-key" | "image";
+export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed" | "slash-commands" | "enter-key" | "image" | "video";