diff --git a/apiserver/plane/app/views/asset/v2.py b/apiserver/plane/app/views/asset/v2.py
index 6979cec6df..b42653c80d 100644
--- a/apiserver/plane/app/views/asset/v2.py
+++ b/apiserver/plane/app/views/asset/v2.py
@@ -385,12 +385,26 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
"image/jpg",
"image/gif",
]
- if type not in allowed_types:
+
+ # Define the set of entity types that use settings.ATTACHMENT_MIME_TYPES
+ special_entity_types = {
+ FileAsset.EntityTypeContext.PAGE_DESCRIPTION,
+ FileAsset.EntityTypeContext.WORKITEM_TEMPLATE_DESCRIPTION,
+ FileAsset.EntityTypeContext.PAGE_TEMPLATE_DESCRIPTION,
+ FileAsset.EntityTypeContext.INITIATIVE_DESCRIPTION,
+ }
+
+ # Map entity type category to allowed types and error message
+ if entity_type in special_entity_types:
+ valid_types = settings.ATTACHMENT_MIME_TYPES
+ error_message = "Invalid file type."
+ else:
+ valid_types = allowed_types
+ error_message = "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed."
+
+ if type not in valid_types:
return Response(
- {
- "error": "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed.",
- "status": False,
- },
+ {"error": error_message, "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -608,12 +622,28 @@ class ProjectAssetEndpoint(BaseAPIView):
"image/jpg",
"image/gif",
]
- if type not in allowed_types:
+
+ # Define the set of entity types that use settings.ATTACHMENT_MIME_TYPES
+ special_entity_types = {
+ FileAsset.EntityTypeContext.ISSUE_DESCRIPTION,
+ FileAsset.EntityTypeContext.PAGE_DESCRIPTION,
+ FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION,
+ FileAsset.EntityTypeContext.WORKITEM_TEMPLATE_DESCRIPTION,
+ FileAsset.EntityTypeContext.PAGE_TEMPLATE_DESCRIPTION,
+ FileAsset.EntityTypeContext.PROJECT_DESCRIPTION,
+ }
+
+ # Map entity type category to allowed types and error message
+ if entity_type in special_entity_types:
+ valid_types = settings.ATTACHMENT_MIME_TYPES
+ error_message = "Invalid file type."
+ else:
+ valid_types = allowed_types
+ error_message = "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed."
+
+ if type not in valid_types:
return Response(
- {
- "error": "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed.",
- "status": False,
- },
+ {"error": error_message, "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
diff --git a/packages/constants/src/feature-flag.ts b/packages/constants/src/feature-flag.ts
index f6069f4e1f..115b60f9a2 100644
--- a/packages/constants/src/feature-flag.ts
+++ b/packages/constants/src/feature-flag.ts
@@ -3,23 +3,15 @@ import { EProductSubscriptionEnum } from "./payment";
export enum E_FEATURE_FLAGS {
BULK_OPS_ONE = "BULK_OPS_ONE",
BULK_OPS_PRO = "BULK_OPS_PRO",
- COLLABORATION_CURSOR = "COLLABORATION_CURSOR",
- EDITOR_AI_OPS = "EDITOR_AI_OPS",
ESTIMATE_WITH_TIME = "ESTIMATE_WITH_TIME",
ISSUE_TYPES = "ISSUE_TYPES",
EPICS = "EPICS",
WORK_ITEM_CONVERSION = "WORK_ITEM_CONVERSION",
OIDC_SAML_AUTH = "OIDC_SAML_AUTH",
- PAGE_ISSUE_EMBEDS = "PAGE_ISSUE_EMBEDS",
- PAGE_PUBLISH = "PAGE_PUBLISH",
- MOVE_PAGES = "MOVE_PAGES",
- NESTED_PAGES = "NESTED_PAGES",
- SHARED_PAGES = "SHARED_PAGES",
VIEW_ACCESS_PRIVATE = "VIEW_ACCESS_PRIVATE",
VIEW_LOCK = "VIEW_LOCK",
VIEW_PUBLISH = "VIEW_PUBLISH",
WORKSPACE_ACTIVE_CYCLES = "WORKSPACE_ACTIVE_CYCLES",
- WORKSPACE_PAGES = "WORKSPACE_PAGES",
ISSUE_WORKLOG = "ISSUE_WORKLOG",
PROJECT_GROUPING = "PROJECT_GROUPING",
CYCLE_PROGRESS_CHARTS = "CYCLE_PROGRESS_CHARTS",
@@ -92,7 +84,16 @@ export enum E_FEATURE_FLAGS {
DASHBOARDS_ADVANCED = "DASHBOARDS_ADVANCED",
// applications
APPLICATIONS = "APPLICATIONS",
-
+ // pages and editor
+ COLLABORATION_CURSOR = "COLLABORATION_CURSOR",
+ EDITOR_AI_OPS = "EDITOR_AI_OPS",
+ PAGE_ISSUE_EMBEDS = "PAGE_ISSUE_EMBEDS",
+ PAGE_PUBLISH = "PAGE_PUBLISH",
+ MOVE_PAGES = "MOVE_PAGES",
+ NESTED_PAGES = "NESTED_PAGES",
+ WORKSPACE_PAGES = "WORKSPACE_PAGES",
+ SHARED_PAGES = "SHARED_PAGES",
+ EDITOR_ATTACHMENTS = "EDITOR_ATTACHMENTS",
// analytics
ANALYTICS_ADVANCED = "ANALYTICS_ADVANCED",
}
diff --git a/packages/editor/src/ce/extensions/core/extensions.ts b/packages/editor/src/ce/extensions/core/extensions.ts
index cecfb38b47..a72bcc2159 100644
--- a/packages/editor/src/ce/extensions/core/extensions.ts
+++ b/packages/editor/src/ce/extensions/core/extensions.ts
@@ -1,13 +1,13 @@
-import { Extensions } from "@tiptap/core";
+import type { Extensions } from "@tiptap/core";
// types
-import { TExtensions, TFileHandler } from "@/types";
+import type { IEditorProps } from "@/types";
-type Props = {
- disabledExtensions: TExtensions[];
- fileHandler: TFileHandler;
-};
+export type TCoreAdditionalExtensionsProps = Pick<
+ IEditorProps,
+ "disabledExtensions" | "flaggedExtensions" | "fileHandler"
+>;
-export const CoreEditorAdditionalExtensions = (props: Props): Extensions => {
+export const CoreEditorAdditionalExtensions = (props: TCoreAdditionalExtensionsProps): Extensions => {
const {} = props;
return [];
};
diff --git a/packages/editor/src/ce/extensions/core/read-only-extensions.ts b/packages/editor/src/ce/extensions/core/read-only-extensions.ts
index 398848e31d..4f9306da30 100644
--- a/packages/editor/src/ce/extensions/core/read-only-extensions.ts
+++ b/packages/editor/src/ce/extensions/core/read-only-extensions.ts
@@ -1,12 +1,15 @@
-import { Extensions } from "@tiptap/core";
+import type { Extensions } from "@tiptap/core";
// types
-import { TExtensions } from "@/types";
+import type { IReadOnlyEditorProps } from "@/types";
-type Props = {
- disabledExtensions: TExtensions[];
-};
+export type TCoreReadOnlyEditorAdditionalExtensionsProps = Pick<
+ IReadOnlyEditorProps,
+ "disabledExtensions" | "flaggedExtensions"
+>;
-export const CoreReadOnlyEditorAdditionalExtensions = (props: Props): Extensions => {
+export const CoreReadOnlyEditorAdditionalExtensions = (
+ props: TCoreReadOnlyEditorAdditionalExtensionsProps
+): Extensions => {
const {} = props;
return [];
};
diff --git a/packages/editor/src/ce/extensions/document-extensions.tsx b/packages/editor/src/ce/extensions/document-extensions.tsx
index d13e2d6969..55c908faae 100644
--- a/packages/editor/src/ce/extensions/document-extensions.tsx
+++ b/packages/editor/src/ce/extensions/document-extensions.tsx
@@ -1,36 +1,39 @@
-import { HocuspocusProvider } from "@hocuspocus/provider";
-import { AnyExtension, Extensions } from "@tiptap/core";
+import type { HocuspocusProvider } from "@hocuspocus/provider";
+import type { AnyExtension, Extensions } from "@tiptap/core";
import { SlashCommands } from "@/extensions";
// plane editor types
-import { TEmbedConfig } from "@/plane-editor/types";
+import type { TEmbedConfig } from "@/plane-editor/types";
// types
-import { TExtensions, TFileHandler, TUserDetails } from "@/types";
+import type { IEditorProps, TExtensions, TUserDetails } from "@/types";
-export type TDocumentEditorAdditionalExtensionsProps = {
- disabledExtensions: TExtensions[];
+export type TDocumentEditorAdditionalExtensionsProps = Pick<
+ IEditorProps,
+ "disabledExtensions" | "flaggedExtensions" | "fileHandler"
+> & {
embedConfig: TEmbedConfig | undefined;
- fileHandler: TFileHandler;
+ isEditable: boolean;
provider?: HocuspocusProvider;
userDetails: TUserDetails;
};
export type TDocumentEditorAdditionalExtensionsRegistry = {
- isEnabled: (disabledExtensions: TExtensions[]) => boolean;
+ isEnabled: (disabledExtensions: TExtensions[], flaggedExtensions: TExtensions[]) => boolean;
getExtension: (props: TDocumentEditorAdditionalExtensionsProps) => AnyExtension | undefined;
};
const extensionRegistry: TDocumentEditorAdditionalExtensionsRegistry[] = [
{
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
- getExtension: ({ disabledExtensions }) => SlashCommands({ disabledExtensions }),
+ getExtension: ({ disabledExtensions, flaggedExtensions }) =>
+ SlashCommands({ disabledExtensions, flaggedExtensions }),
},
];
export const DocumentEditorAdditionalExtensions = (props: TDocumentEditorAdditionalExtensionsProps) => {
- const { disabledExtensions = [] } = props;
+ const { disabledExtensions, flaggedExtensions } = props;
const documentExtensions: Extensions = extensionRegistry
- .filter((config) => config.isEnabled(disabledExtensions))
+ .filter((config) => config.isEnabled(disabledExtensions, flaggedExtensions))
.map((config) => config.getExtension(props))
.filter((extension): extension is AnyExtension => extension !== undefined);
diff --git a/packages/editor/src/ce/extensions/rich-text/extensions.tsx b/packages/editor/src/ce/extensions/rich-text/extensions.tsx
index 0eedd1e87f..520dfa10e8 100644
--- a/packages/editor/src/ce/extensions/rich-text/extensions.tsx
+++ b/packages/editor/src/ce/extensions/rich-text/extensions.tsx
@@ -2,19 +2,19 @@ import { AnyExtension, Extensions } from "@tiptap/core";
// extensions
import { SlashCommands } from "@/extensions/slash-commands/root";
// types
-import { TExtensions, TFileHandler } from "@/types";
+import { IEditorProps, TExtensions } from "@/types";
-export type TRichTextEditorAdditionalExtensionsProps = {
- disabledExtensions: TExtensions[];
- fileHandler: TFileHandler;
-};
+export type TRichTextEditorAdditionalExtensionsProps = Pick<
+ IEditorProps,
+ "disabledExtensions" | "flaggedExtensions" | "fileHandler"
+>;
/**
* Registry entry configuration for extensions
*/
export type TRichTextEditorAdditionalExtensionsRegistry = {
/** Determines if the extension should be enabled based on disabled extensions */
- isEnabled: (disabledExtensions: TExtensions[]) => boolean;
+ isEnabled: (disabledExtensions: TExtensions[], flaggedExtensions: TExtensions[]) => boolean;
/** Returns the extension instance(s) when enabled */
getExtension: (props: TRichTextEditorAdditionalExtensionsProps) => AnyExtension | undefined;
};
@@ -22,18 +22,19 @@ export type TRichTextEditorAdditionalExtensionsRegistry = {
const extensionRegistry: TRichTextEditorAdditionalExtensionsRegistry[] = [
{
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
- getExtension: ({ disabledExtensions }) =>
+ getExtension: ({ disabledExtensions, flaggedExtensions }) =>
SlashCommands({
disabledExtensions,
+ flaggedExtensions,
}),
},
];
export const RichTextEditorAdditionalExtensions = (props: TRichTextEditorAdditionalExtensionsProps) => {
- const { disabledExtensions } = props;
+ const { disabledExtensions, flaggedExtensions } = props;
const extensions: Extensions = extensionRegistry
- .filter((config) => config.isEnabled(disabledExtensions))
+ .filter((config) => config.isEnabled(disabledExtensions, flaggedExtensions))
.map((config) => config.getExtension(props))
.filter((extension): extension is AnyExtension => extension !== undefined);
diff --git a/packages/editor/src/ce/extensions/rich-text/read-only-extensions.tsx b/packages/editor/src/ce/extensions/rich-text/read-only-extensions.tsx
index 0156301170..0b7cbc7306 100644
--- a/packages/editor/src/ce/extensions/rich-text/read-only-extensions.tsx
+++ b/packages/editor/src/ce/extensions/rich-text/read-only-extensions.tsx
@@ -1,11 +1,11 @@
import { AnyExtension, Extensions } from "@tiptap/core";
// types
-import { TExtensions, TReadOnlyFileHandler } from "@/types";
+import { IReadOnlyEditorProps, TExtensions } from "@/types";
-export type TRichTextReadOnlyEditorAdditionalExtensionsProps = {
- disabledExtensions: TExtensions[];
- fileHandler: TReadOnlyFileHandler;
-};
+export type TRichTextReadOnlyEditorAdditionalExtensionsProps = Pick<
+ IReadOnlyEditorProps,
+ "disabledExtensions" | "flaggedExtensions" | "fileHandler"
+>;
/**
* Registry entry configuration for extensions
diff --git a/packages/editor/src/ce/extensions/slash-commands.tsx b/packages/editor/src/ce/extensions/slash-commands.tsx
index faefa74523..d61d056c8d 100644
--- a/packages/editor/src/ce/extensions/slash-commands.tsx
+++ b/packages/editor/src/ce/extensions/slash-commands.tsx
@@ -1,11 +1,9 @@
// extensions
-import { TSlashCommandAdditionalOption } from "@/extensions";
+import type { TSlashCommandAdditionalOption } from "@/extensions";
// types
-import { TExtensions } from "@/types";
+import type { IEditorProps } from "@/types";
-type Props = {
- disabledExtensions?: TExtensions[];
-};
+type Props = Pick;
export const coreEditorAdditionalSlashCommandOptions = (props: Props): TSlashCommandAdditionalOption[] => {
const {} = props;
diff --git a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx
index 3262634c2e..56dfe7e6cf 100644
--- a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx
+++ b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx
@@ -11,10 +11,11 @@ import { getEditorClassNames } from "@/helpers/common";
// hooks
import { useCollaborativeEditor } from "@/hooks/use-collaborative-editor";
// types
-import { EditorRefApi, ICollaborativeDocumentEditor } from "@/types";
+import { EditorRefApi, ICollaborativeDocumentEditorProps } from "@/types";
-const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
+const CollaborativeDocumentEditor: React.FC = (props) => {
const {
+ onChange,
onTransaction,
aiHandler,
bubbleMenuEnabled = true,
@@ -25,11 +26,11 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
editorClassName = "",
embedHandler,
fileHandler,
+ flaggedExtensions,
forwardedRef,
handleEditorReady,
id,
mentionHandler,
- onChange,
pageRestorationInProgress,
placeholder,
realtimeConfig,
@@ -62,9 +63,11 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
embedHandler,
extensions,
fileHandler,
+ flaggedExtensions,
forwardedRef,
handleEditorReady,
id,
+ isSmoothCursorEnabled,
mentionHandler,
onChange,
onTransaction,
@@ -73,9 +76,8 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
serverHandler,
tabIndex,
titleRef,
- user,
updatePageProperties,
- isSmoothCursorEnabled,
+ user,
});
const editorContainerClassNames = getEditorClassNames({
@@ -105,7 +107,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
);
};
-const CollaborativeDocumentEditorWithRef = React.forwardRef(
+const CollaborativeDocumentEditorWithRef = React.forwardRef(
(props, ref) => (
} />
)
diff --git a/packages/editor/src/core/components/editors/document/editor.tsx b/packages/editor/src/core/components/editors/document/editor.tsx
index 53c9ac9f5d..29690b9d5f 100644
--- a/packages/editor/src/core/components/editors/document/editor.tsx
+++ b/packages/editor/src/core/components/editors/document/editor.tsx
@@ -24,8 +24,9 @@ const DocumentEditor = (props: IDocumentEditor) => {
editorClassName = "",
embedHandler,
fileHandler,
- id,
+ flaggedExtensions,
forwardedRef,
+ id,
handleEditorReady,
initialValue,
isSmoothCursorEnabled,
@@ -51,6 +52,8 @@ const DocumentEditor = (props: IDocumentEditor) => {
...DocumentEditorAdditionalExtensions({
disabledExtensions,
embedConfig: embedHandler,
+ flaggedExtensions,
+ isEditable: true,
fileHandler,
userDetails: user,
})
@@ -65,8 +68,10 @@ const DocumentEditor = (props: IDocumentEditor) => {
enableHistory: true,
extensions,
fileHandler,
+ flaggedExtensions,
forwardedRef,
handleEditorReady,
+ id,
initialValue,
isSmoothCursorEnabled,
mentionHandler,
diff --git a/packages/editor/src/core/components/editors/document/page-renderer.tsx b/packages/editor/src/core/components/editors/document/page-renderer.tsx
index 9dfafec519..d5e3532c03 100644
--- a/packages/editor/src/core/components/editors/document/page-renderer.tsx
+++ b/packages/editor/src/core/components/editors/document/page-renderer.tsx
@@ -7,7 +7,7 @@ import { AIFeaturesMenu, BlockMenu, EditorBubbleMenu } from "@/components/menus"
// types
import { TAIHandler, TDisplayConfig } from "@/types";
-type IPageRenderer = {
+type Props = {
aiHandler?: TAIHandler;
bubbleMenuEnabled: boolean;
displayConfig: TDisplayConfig;
@@ -19,7 +19,7 @@ type IPageRenderer = {
tabIndex?: number;
};
-export const PageRenderer = (props: IPageRenderer) => {
+export const PageRenderer = (props: Props) => {
const {
aiHandler,
bubbleMenuEnabled,
diff --git a/packages/editor/src/core/components/editors/document/read-only-editor.tsx b/packages/editor/src/core/components/editors/document/read-only-editor.tsx
index 79e994a772..ba04bda453 100644
--- a/packages/editor/src/core/components/editors/document/read-only-editor.tsx
+++ b/packages/editor/src/core/components/editors/document/read-only-editor.tsx
@@ -1,5 +1,5 @@
import { Extensions } from "@tiptap/core";
-import { forwardRef, MutableRefObject } from "react";
+import React, { forwardRef, MutableRefObject } from "react";
// components
import { PageRenderer } from "@/components/editors";
// constants
@@ -12,33 +12,11 @@ import { getEditorClassNames } from "@/helpers/common";
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// plane editor extensions
import { PageEmbedReadOnlyExtension } from "@/plane-editor/extensions";
-// plane web types
-import { TReadOnlyEmbedConfig } from "@/plane-editor/types";
+import { CustomAttachmentExtension } from "@/plane-editor/extensions/attachments/extension";
// types
-import {
- EditorReadOnlyRefApi,
- TDisplayConfig,
- TExtensions,
- TReadOnlyFileHandler,
- TReadOnlyMentionHandler,
-} from "@/types";
+import { EditorReadOnlyRefApi, IDocumentReadOnlyEditorProps } from "@/types";
-interface IDocumentReadOnlyEditor {
- disabledExtensions: TExtensions[];
- id: string;
- initialValue: string;
- containerClassName: string;
- displayConfig?: TDisplayConfig;
- editorClassName?: string;
- embedHandler: TReadOnlyEmbedConfig;
- fileHandler: TReadOnlyFileHandler;
- tabIndex?: number;
- handleEditorReady?: (value: boolean) => void;
- mentionHandler: TReadOnlyMentionHandler;
- forwardedRef?: React.MutableRefObject;
-}
-
-const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
+const DocumentReadOnlyEditor: React.FC = (props) => {
const {
containerClassName,
disabledExtensions,
@@ -46,13 +24,20 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
editorClassName = "",
embedHandler,
fileHandler,
+ flaggedExtensions,
id,
forwardedRef,
handleEditorReady,
initialValue,
mentionHandler,
} = props;
- const extensions: Extensions = [];
+ const extensions: Extensions = [
+ CustomAttachmentExtension({
+ fileHandler,
+ isFlagged: flaggedExtensions.includes("attachments"),
+ isEditable: false,
+ }),
+ ];
if (embedHandler?.issue) {
extensions.push(
WorkItemEmbedExtension({
@@ -74,6 +59,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
editorClassName,
extensions,
fileHandler,
+ flaggedExtensions,
forwardedRef,
handleEditorReady,
initialValue,
@@ -97,7 +83,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
);
};
-const DocumentReadOnlyEditorWithRef = forwardRef((props, ref) => (
+const DocumentReadOnlyEditorWithRef = forwardRef((props, ref) => (
} />
));
diff --git a/packages/editor/src/core/components/editors/editor-wrapper.tsx b/packages/editor/src/core/components/editors/editor-wrapper.tsx
index 514866fb28..c19d202d79 100644
--- a/packages/editor/src/core/components/editors/editor-wrapper.tsx
+++ b/packages/editor/src/core/components/editors/editor-wrapper.tsx
@@ -27,6 +27,7 @@ export const EditorWrapper: React.FC = (props) => {
id,
initialValue,
fileHandler,
+ flaggedExtensions,
forwardedRef,
handleEditorReady,
isSmoothCursorEnabled,
@@ -46,6 +47,7 @@ export const EditorWrapper: React.FC = (props) => {
enableHistory: true,
extensions,
fileHandler,
+ flaggedExtensions,
forwardedRef,
handleEditorReady,
isSmoothCursorEnabled,
diff --git a/packages/editor/src/core/components/editors/lite-text/editor.tsx b/packages/editor/src/core/components/editors/lite-text/editor.tsx
index 527ef5b79d..c10782a8dd 100644
--- a/packages/editor/src/core/components/editors/lite-text/editor.tsx
+++ b/packages/editor/src/core/components/editors/lite-text/editor.tsx
@@ -4,23 +4,25 @@ import { EditorWrapper } from "@/components/editors/editor-wrapper";
// extensions
import { EnterKeyExtension } from "@/extensions";
// types
-import { EditorRefApi, ILiteTextEditor } from "@/types";
+import { EditorRefApi, ILiteTextEditorProps } from "@/types";
-const LiteTextEditor = (props: ILiteTextEditor) => {
+const LiteTextEditor: React.FC = (props) => {
const { onEnterKeyPress, disabledExtensions, extensions: externalExtensions = [], isSmoothCursorEnabled } = props;
- const extensions = useMemo(
- () => [
- ...externalExtensions,
- ...(disabledExtensions?.includes("enter-key") ? [] : [EnterKeyExtension(onEnterKeyPress)]),
- ],
- [externalExtensions, disabledExtensions, onEnterKeyPress]
- );
+ const extensions = useMemo(() => {
+ const resolvedExtensions = [...externalExtensions];
+
+ if (!disabledExtensions?.includes("enter-key")) {
+ resolvedExtensions.push(EnterKeyExtension(onEnterKeyPress));
+ }
+
+ return resolvedExtensions;
+ }, [externalExtensions, disabledExtensions, onEnterKeyPress]);
return ;
};
-const LiteTextEditorWithRef = forwardRef((props, ref) => (
+const LiteTextEditorWithRef = forwardRef((props, ref) => (
} />
));
diff --git a/packages/editor/src/core/components/editors/lite-text/read-only-editor.tsx b/packages/editor/src/core/components/editors/lite-text/read-only-editor.tsx
index b721c84c5d..75e02791de 100644
--- a/packages/editor/src/core/components/editors/lite-text/read-only-editor.tsx
+++ b/packages/editor/src/core/components/editors/lite-text/read-only-editor.tsx
@@ -2,9 +2,9 @@ import { forwardRef } from "react";
// components
import { ReadOnlyEditorWrapper } from "@/components/editors";
// types
-import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditor } from "@/types";
+import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditorProps } from "@/types";
-const LiteTextReadOnlyEditorWithRef = forwardRef((props, ref) => (
+const LiteTextReadOnlyEditorWithRef = forwardRef((props, ref) => (
} />
));
diff --git a/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx b/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx
index ffed8ba8bd..b6abd1a6a5 100644
--- a/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx
+++ b/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx
@@ -17,6 +17,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
editorClassName = "",
extensions,
fileHandler,
+ flaggedExtensions,
forwardedRef,
id,
initialValue,
@@ -28,6 +29,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
editorClassName,
extensions,
fileHandler,
+ flaggedExtensions,
forwardedRef,
initialValue,
mentionHandler,
diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx
index c1c216e258..f17a72df4c 100644
--- a/packages/editor/src/core/components/editors/rich-text/editor.tsx
+++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx
@@ -7,15 +7,16 @@ import { SideMenuExtension } from "@/extensions";
// plane editor imports
import { RichTextEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/extensions";
// types
-import { EditorRefApi, IRichTextEditor } from "@/types";
+import { EditorRefApi, IRichTextEditorProps } from "@/types";
-const RichTextEditor = (props: IRichTextEditor) => {
+const RichTextEditor: React.FC = (props) => {
const {
+ bubbleMenuEnabled = true,
disabledExtensions,
dragDropEnabled,
- fileHandler,
- bubbleMenuEnabled = true,
extensions: externalExtensions = [],
+ fileHandler,
+ flaggedExtensions,
isSmoothCursorEnabled,
} = props;
@@ -29,11 +30,12 @@ const RichTextEditor = (props: IRichTextEditor) => {
...RichTextEditorAdditionalExtensions({
disabledExtensions,
fileHandler,
+ flaggedExtensions,
}),
];
return extensions;
- }, [dragDropEnabled, disabledExtensions, externalExtensions, fileHandler]);
+ }, [dragDropEnabled, disabledExtensions, externalExtensions, fileHandler, flaggedExtensions]);
return (
@@ -42,7 +44,7 @@ const RichTextEditor = (props: IRichTextEditor) => {
);
};
-const RichTextEditorWithRef = forwardRef((props, ref) => (
+const RichTextEditorWithRef = forwardRef((props, ref) => (
} />
));
diff --git a/packages/editor/src/core/components/editors/rich-text/read-only-editor.tsx b/packages/editor/src/core/components/editors/rich-text/read-only-editor.tsx
index 18d960ca53..efad3d6ac1 100644
--- a/packages/editor/src/core/components/editors/rich-text/read-only-editor.tsx
+++ b/packages/editor/src/core/components/editors/rich-text/read-only-editor.tsx
@@ -2,23 +2,22 @@ import { forwardRef, useCallback } from "react";
// plane editor extensions
import { RichTextReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/read-only-extensions";
// types
-import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor } from "@/types";
+import { EditorReadOnlyRefApi, IRichTextReadOnlyEditorProps } from "@/types";
// local imports
import { ReadOnlyEditorWrapper } from "../read-only-editor-wrapper";
-const RichTextReadOnlyEditorWithRef = forwardRef((props, ref) => {
- const { disabledExtensions, fileHandler } = props;
+const RichTextReadOnlyEditorWithRef = forwardRef((props, ref) => {
+ const { disabledExtensions, fileHandler, flaggedExtensions } = props;
const getExtensions = useCallback(() => {
- const extensions = [
- ...RichTextReadOnlyEditorAdditionalExtensions({
- disabledExtensions,
- fileHandler,
- }),
- ];
+ const extensions = RichTextReadOnlyEditorAdditionalExtensions({
+ disabledExtensions,
+ fileHandler,
+ flaggedExtensions,
+ });
return extensions;
- }, [disabledExtensions, fileHandler]);
+ }, [disabledExtensions, fileHandler, flaggedExtensions]);
return (
& {
enableHistory: boolean;
isSmoothCursorEnabled: boolean;
- fileHandler: TFileHandler;
- mentionHandler: TMentionHandler;
- placeholder?: string | ((isFocused: boolean, value: string) => string);
- tabIndex?: number;
editable: boolean;
};
export const CoreEditorExtensions = (args: TArguments): Extensions => {
- const { disabledExtensions, enableHistory, fileHandler, mentionHandler, placeholder, tabIndex, editable,isSmoothCursorEnabled } = args;
+ const {
+ disabledExtensions,
+ enableHistory,
+ fileHandler,
+ flaggedExtensions,
+ isSmoothCursorEnabled,
+ mentionHandler,
+ placeholder,
+ tabIndex,
+ editable,
+ } = args;
const extensions = [
StarterKit.configure({
@@ -179,6 +187,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
CustomColorExtension,
...CoreEditorAdditionalExtensions({
disabledExtensions,
+ flaggedExtensions,
fileHandler,
}),
];
diff --git a/packages/editor/src/core/extensions/read-only-extensions.ts b/packages/editor/src/core/extensions/read-only-extensions.ts
index b28e8a67ab..0f422c620b 100644
--- a/packages/editor/src/core/extensions/read-only-extensions.ts
+++ b/packages/editor/src/core/extensions/read-only-extensions.ts
@@ -31,16 +31,12 @@ import { isValidHttpUrl } from "@/helpers/common";
// plane editor extensions
import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions";
// types
-import { TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types";
+import type { IReadOnlyEditorProps } from "@/types";
-type Props = {
- disabledExtensions: TExtensions[];
- fileHandler: TReadOnlyFileHandler;
- mentionHandler: TReadOnlyMentionHandler;
-};
+type Props = Pick;
export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
- const { disabledExtensions, fileHandler, mentionHandler } = props;
+ const { disabledExtensions, fileHandler, flaggedExtensions, mentionHandler } = props;
const extensions = [
StarterKit.configure({
@@ -133,6 +129,7 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
}),
...CoreReadOnlyEditorAdditionalExtensions({
disabledExtensions,
+ flaggedExtensions,
}),
];
diff --git a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx
index 993282c892..c65de3b0a2 100644
--- a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx
+++ b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx
@@ -112,7 +112,7 @@ const fuzzySearch = (query: string, text: string): { match: boolean; score: numb
export const getSlashCommandFilteredSections =
(args: TExtensionProps) =>
({ query }: { query: string }): TSlashCommandSection[] => {
- const { additionalOptions: externalAdditionalOptions, disabledExtensions } = args;
+ const { additionalOptions: externalAdditionalOptions, disabledExtensions, flaggedExtensions } = args;
const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [
{
key: "general",
@@ -353,6 +353,7 @@ export const getSlashCommandFilteredSections =
...(externalAdditionalOptions ?? []),
...coreEditorAdditionalSlashCommandOptions({
disabledExtensions,
+ flaggedExtensions,
}),
]?.forEach((item) => {
const sectionToPushTo = SLASH_COMMAND_SECTIONS.find((s) => s.key === item.section) ?? SLASH_COMMAND_SECTIONS[0];
diff --git a/packages/editor/src/core/extensions/slash-commands/root.tsx b/packages/editor/src/core/extensions/slash-commands/root.tsx
index d7fc6d9c07..fc444c42ca 100644
--- a/packages/editor/src/core/extensions/slash-commands/root.tsx
+++ b/packages/editor/src/core/extensions/slash-commands/root.tsx
@@ -1,14 +1,14 @@
+import { computePosition, flip, shift } from "@floating-ui/dom";
import { Editor, Range, Extension } from "@tiptap/core";
import { ReactRenderer, posToDOMRect } from "@tiptap/react";
import Suggestion, { SuggestionOptions } from "@tiptap/suggestion";
-import { computePosition, flip, shift } from "@floating-ui/dom";
import { FC } from "react";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// helpers
import { CommandListInstance } from "@/helpers/tippy";
// types
-import { ISlashCommandItem, TEditorCommands, TExtensions, TSlashCommandSectionKeys } from "@/types";
+import { IEditorProps, ISlashCommandItem, TEditorCommands, TSlashCommandSectionKeys } from "@/types";
// components
import { getSlashCommandFilteredSections } from "./command-items-list";
import { SlashCommandsMenu, SlashCommandsMenuProps } from "./command-menu";
@@ -132,9 +132,8 @@ const Command = Extension.create({
},
});
-export type TExtensionProps = {
+export type TExtensionProps = Pick & {
additionalOptions?: TSlashCommandAdditionalOption[];
- disabledExtensions?: TExtensions[];
};
export const SlashCommands = (props: TExtensionProps) =>
diff --git a/packages/editor/src/core/extensions/utility.ts b/packages/editor/src/core/extensions/utility.ts
index 1354a5fc51..b4c82ee4b7 100644
--- a/packages/editor/src/core/extensions/utility.ts
+++ b/packages/editor/src/core/extensions/utility.ts
@@ -6,7 +6,7 @@ import { DropHandlerPlugin } from "@/plugins/drop";
import { FilePlugins } from "@/plugins/file/root";
import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
// types
-import { TExtensions, TFileHandler, TReadOnlyFileHandler } from "@/types";
+import type { IEditorProps, TFileHandler, TReadOnlyFileHandler } from "@/types";
// prosemirror plugins
import { codemark } from "./code-mark";
@@ -23,8 +23,7 @@ export interface UtilityExtensionStorage {
uploadInProgress: boolean;
}
-type Props = {
- disabledExtensions: TExtensions[];
+type Props = Pick & {
fileHandler: TFileHandler | TReadOnlyFileHandler;
isEditable: boolean;
};
diff --git a/packages/editor/src/core/helpers/file.ts b/packages/editor/src/core/helpers/file.ts
index 33d3c7d781..a7e0a071b8 100644
--- a/packages/editor/src/core/helpers/file.ts
+++ b/packages/editor/src/core/helpers/file.ts
@@ -2,6 +2,7 @@ export enum EFileError {
INVALID_FILE_TYPE = "INVALID_FILE_TYPE",
FILE_SIZE_TOO_LARGE = "FILE_SIZE_TOO_LARGE",
NO_FILE_SELECTED = "NO_FILE_SELECTED",
+ UPLOAD_FAILED = "UPLOAD_FAILED",
}
type TArgs = {
diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts
index e7e18e2270..eb0893d92e 100644
--- a/packages/editor/src/core/hooks/use-collaborative-editor.ts
+++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts
@@ -13,26 +13,23 @@ import { useRealtimeEvents } from "@/hooks/use-realtime-events";
// plane editor extensions
import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions";
// types
-import { TCollaborativeEditorProps } from "@/types";
+import { TCollaborativeEditorHookProps } from "@/types";
+// local imports
import { useEditorNavigation } from "./use-editor-navigation";
import { useTitleEditor } from "./use-title-editor";
-/**
- * Hook that creates a collaborative editor with title and main editor components
- * Handles real-time collaboration, local persistence, and keyboard navigation between editors
- */
-export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
+export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) => {
const {
- onChange,
onTransaction,
isSmoothCursorEnabled,
disabledExtensions,
editable,
- editorClassName,
+ editorClassName = "",
editorProps = {},
embedHandler,
- extensions,
+ extensions = [],
fileHandler,
+ flaggedExtensions,
forwardedRef,
handleEditorReady,
id,
@@ -125,7 +122,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
// Initialize main document editor
const editor = useEditor({
- embedConfig: embedHandler,
disabledExtensions,
id,
editable,
@@ -145,15 +141,13 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
document: provider.document,
field: "default",
}),
-
- // User-provided extensions
- ...(extensions ?? []),
-
- // Additional document editor extensions
+ ...extensions,
...DocumentEditorAdditionalExtensions({
disabledExtensions,
embedConfig: embedHandler,
fileHandler,
+ flaggedExtensions,
+ isEditable: editable,
provider,
userDetails: user,
}),
@@ -162,11 +156,11 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
mainNavigationExtension,
],
fileHandler,
+ flaggedExtensions,
+ forwardedRef,
handleEditorReady,
isSmoothCursorEnabled,
- forwardedRef,
mentionHandler,
- onChange,
onTransaction,
placeholder,
provider,
diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts
index c7825d637c..022c9a0e26 100644
--- a/packages/editor/src/core/hooks/use-editor.ts
+++ b/packages/editor/src/core/hooks/use-editor.ts
@@ -1,8 +1,6 @@
-import { HocuspocusProvider } from "@hocuspocus/provider";
import { DOMSerializer } from "@tiptap/pm/model";
-import { EditorProps } from "@tiptap/pm/view";
-import { useEditor as useTiptapEditor, Extensions, findChildren } from "@tiptap/react";
-import { useImperativeHandle, MutableRefObject, useEffect } from "react";
+import { useEditor as useTiptapEditor, findChildren } from "@tiptap/react";
+import { useImperativeHandle, useEffect } from "react";
import * as Y from "yjs";
// components
import { getEditorMenuItems } from "@/components/menus";
@@ -19,64 +17,31 @@ import { IMarking, scrollSummary, scrollToNodeViaDOMCoordinates } from "@/helper
// props
import { CoreEditorProps } from "@/props";
// types
-import type {
- TDocumentEventsServer,
- EditorRefApi,
- TEditorCommands,
- TFileHandler,
- TExtensions,
- TMentionHandler,
- TEmbedConfig,
-} from "@/types";
+import type { TDocumentEventsServer, TEditorCommands, TEditorHookProps } from "@/types";
-export interface CustomEditorProps {
- editable?: boolean;
- editorClassName: string;
- editorProps?: EditorProps;
- embedConfig?: TEmbedConfig | undefined;
- enableHistory: boolean;
- disabledExtensions: TExtensions[];
- extensions?: Extensions;
- fileHandler: TFileHandler;
- forwardedRef?: MutableRefObject;
- handleEditorReady?: (value: boolean) => void;
- isSmoothCursorEnabled: boolean;
- id?: string;
- initialValue?: string;
- mentionHandler: TMentionHandler;
- onChange?: (json: object, html: string) => void;
- onTransaction?: () => void;
- autofocus?: boolean;
- placeholder?: string | ((isFocused: boolean, value: string) => string);
- provider?: HocuspocusProvider;
- tabIndex?: number;
- // undefined when prop is not passed, null if intentionally passed to stop
- // swr syncing
- value?: string | null | undefined;
-}
-
-export const useEditor = (props: CustomEditorProps) => {
+export const useEditor = (props: TEditorHookProps) => {
const {
+ autofocus = false,
disabledExtensions,
editable = true,
- editorClassName,
+ editorClassName = "",
editorProps = {},
enableHistory,
extensions = [],
fileHandler,
+ flaggedExtensions,
forwardedRef,
handleEditorReady,
id = "",
initialValue,
+ isSmoothCursorEnabled = false,
mentionHandler,
onChange,
onTransaction,
placeholder,
+ provider,
tabIndex,
value,
- provider,
- autofocus = false,
- isSmoothCursorEnabled = false,
} = props;
const editor = useTiptapEditor(
@@ -99,6 +64,7 @@ export const useEditor = (props: CustomEditorProps) => {
disabledExtensions,
enableHistory,
fileHandler,
+ flaggedExtensions,
mentionHandler,
placeholder,
tabIndex,
diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts
index 6a6e25d9fd..d259470ac9 100644
--- a/packages/editor/src/core/hooks/use-read-only-editor.ts
+++ b/packages/editor/src/core/hooks/use-read-only-editor.ts
@@ -1,8 +1,8 @@
-import { HocuspocusProvider } from "@hocuspocus/provider";
-import { EditorProps } from "@tiptap/pm/view";
-import { useEditor as useTiptapEditor, Extensions } from "@tiptap/react";
-import { useImperativeHandle, MutableRefObject, useEffect } from "react";
+import { useEditor as useTiptapEditor } from "@tiptap/react";
+import { useImperativeHandle, useEffect } from "react";
import * as Y from "yjs";
+// constants
+import { CORE_EDITOR_META } from "@/constants/meta";
// extensions
import { CoreReadOnlyEditorExtensions } from "@/extensions";
// helpers
@@ -11,32 +11,19 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
// props
import { CoreReadOnlyEditorProps } from "@/props";
// types
-import type { EditorReadOnlyRefApi, TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types";
-import { CORE_EDITOR_META } from "@/constants/meta";
+import type { TReadOnlyEditorHookProps } from "@/types";
-interface CustomReadOnlyEditorProps {
- disabledExtensions: TExtensions[];
- editorClassName: string;
- editorProps?: EditorProps;
- extensions?: Extensions;
- forwardedRef?: MutableRefObject;
- initialValue?: string;
- fileHandler: TReadOnlyFileHandler;
- handleEditorReady?: (value: boolean) => void;
- mentionHandler: TReadOnlyMentionHandler;
- provider?: HocuspocusProvider;
-}
-
-export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
+export const useReadOnlyEditor = (props: TReadOnlyEditorHookProps) => {
const {
disabledExtensions,
- initialValue,
- editorClassName,
- forwardedRef,
- extensions = [],
+ editorClassName = "",
editorProps = {},
+ extensions = [],
fileHandler,
+ flaggedExtensions,
+ forwardedRef,
handleEditorReady,
+ initialValue,
mentionHandler,
provider,
} = props;
@@ -59,8 +46,9 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
extensions: [
...CoreReadOnlyEditorExtensions({
disabledExtensions,
- mentionHandler,
fileHandler,
+ flaggedExtensions,
+ mentionHandler,
}),
...extensions,
],
diff --git a/packages/editor/src/core/hooks/use-title-editor.ts b/packages/editor/src/core/hooks/use-title-editor.ts
index f9ab760dc5..48ff2d2755 100644
--- a/packages/editor/src/core/hooks/use-title-editor.ts
+++ b/packages/editor/src/core/hooks/use-title-editor.ts
@@ -5,6 +5,8 @@ import { DOMSerializer } from "@tiptap/pm/model";
import { useEditor } from "@tiptap/react";
import { useImperativeHandle } from "react";
import * as Y from "yjs";
+// constants
+import { CORE_EDITOR_META } from "@/constants/meta";
// extensions
import { IMarking, SmoothCursorExtension } from "@/extensions";
import { TitleExtensions } from "@/extensions/title-extension";
@@ -70,7 +72,7 @@ export const useTitleEditor = (props: TitleEditorProps) => {
clearEditor: (emitUpdate = false) => {
editor
?.chain()
- .setMeta("skipImageDeletion", true)
+ .setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true)
.setMeta("intentionalDeletion", true)
.clearContent(emitUpdate)
.run();
diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts
index 406fb023e3..35d688977e 100644
--- a/packages/editor/src/core/plugins/drag-handle.ts
+++ b/packages/editor/src/core/plugins/drag-handle.ts
@@ -21,6 +21,7 @@ const generalSelectors = [
".image-component",
".image-upload-component",
".editor-callout-component",
+ ".editor-attachment-component",
".page-embed-component",
].join(", ");
diff --git a/packages/editor/src/core/plugins/drop.ts b/packages/editor/src/core/plugins/drop.ts
index ad8070924b..fdca8bbb6d 100644
--- a/packages/editor/src/core/plugins/drop.ts
+++ b/packages/editor/src/core/plugins/drop.ts
@@ -117,7 +117,12 @@ export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => {
pos,
event,
});
- } else if (fileType === "attachment") {
+ } else if (fileType === "attachment" && !disabledExtensions?.includes("attachments")) {
+ editor.commands.insertAttachmentComponent({
+ file,
+ pos,
+ event,
+ });
}
} catch (error) {
console.error(`Error while ${event}ing file:`, error);
diff --git a/packages/editor/src/core/types/collaboration.ts b/packages/editor/src/core/types/collaboration.ts
index 579bb383a8..bd033a0ec2 100644
--- a/packages/editor/src/core/types/collaboration.ts
+++ b/packages/editor/src/core/types/collaboration.ts
@@ -1,56 +1,5 @@
-import { Extensions } from "@tiptap/core";
-import { EditorProps } from "@tiptap/pm/view";
-// plane editor types
-import { TEmbedConfig } from "@/plane-editor/types";
-// types
-import {
- EditorReadOnlyRefApi,
- EditorRefApi,
- EditorTitleRefApi,
- TExtensions,
- TFileHandler,
- TMentionHandler,
- TReadOnlyFileHandler,
- TReadOnlyMentionHandler,
- TRealtimeConfig,
- TUserDetails,
-} from "@/types";
-
export type TServerHandler = {
onConnect?: () => void;
onServerError?: () => void;
onServerSynced?: () => void;
};
-
-type TCollaborativeEditorHookProps = {
- disabledExtensions: TExtensions[];
- editable: boolean;
- editorClassName: string;
- editorProps?: EditorProps;
- extensions?: Extensions;
- handleEditorReady?: (value: boolean) => void;
- id: string;
- realtimeConfig: TRealtimeConfig;
- serverHandler?: TServerHandler;
- user: TUserDetails;
-};
-
-export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & {
- isSmoothCursorEnabled: boolean;
- onTransaction?: () => void;
- embedHandler?: TEmbedConfig;
- fileHandler: TFileHandler;
- forwardedRef?: React.MutableRefObject;
- mentionHandler: TMentionHandler;
- onChange?: (description_json: object, description_html: string) => void;
- placeholder?: string | ((isFocused: boolean, value: string) => string);
- tabIndex?: number;
- titleRef?: React.MutableRefObject;
- updatePageProperties?: (pageId: string, messageType: string, payload?: any, performAction?: boolean) => void;
-};
-
-export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & {
- fileHandler: TReadOnlyFileHandler;
- forwardedRef?: React.MutableRefObject;
- mentionHandler: TReadOnlyMentionHandler;
-};
diff --git a/packages/editor/src/core/types/config.ts b/packages/editor/src/core/types/config.ts
index ace2220ed0..60ccfa8412 100644
--- a/packages/editor/src/core/types/config.ts
+++ b/packages/editor/src/core/types/config.ts
@@ -1,3 +1,6 @@
+// plane imports
+import { TWebhookConnectionQueryParams } from "@plane/types";
+
export type TReadOnlyFileHandler = {
checkIfAssetExists: (assetId: string) => Promise;
getAssetSrc: (path: string) => Promise;
@@ -30,3 +33,15 @@ export type TDisplayConfig = {
lineSpacing?: TEditorLineSpacing;
wideLayout?: boolean;
};
+
+export type TUserDetails = {
+ color: string;
+ id: string;
+ name: string;
+ cookie?: string;
+};
+
+export type TRealtimeConfig = {
+ url: string;
+ queryParams: TWebhookConnectionQueryParams;
+};
diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts
index cc52ca9da1..763502e942 100644
--- a/packages/editor/src/core/types/editor.ts
+++ b/packages/editor/src/core/types/editor.ts
@@ -1,13 +1,11 @@
-import { Extensions, JSONContent } from "@tiptap/core";
-import { Selection } from "@tiptap/pm/state";
-// plane types
-import { TWebhookConnectionQueryParams } from "@plane/types";
+import type { Extensions, JSONContent } from "@tiptap/core";
+import type { Selection } from "@tiptap/pm/state";
// extension types
-import { TTextAlign } from "@/extensions";
+import type { TTextAlign } from "@/extensions";
// helpers
-import { IMarking } from "@/helpers/scroll-to-node";
+import type { IMarking } from "@/helpers/scroll-to-node";
// types
-import {
+import type {
EventToPayloadMap,
TAIHandler,
TDisplayConfig,
@@ -19,7 +17,9 @@ import {
TMentionHandler,
TReadOnlyFileHandler,
TReadOnlyMentionHandler,
+ TRealtimeConfig,
TServerHandler,
+ TUserDetails,
} from "@/types";
export type TEditorCommands =
@@ -133,41 +133,39 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
// editor props
export interface IEditorProps {
+ autofocus?: boolean;
+ bubbleMenuEnabled?: boolean;
containerClassName?: string;
displayConfig?: TDisplayConfig;
disabledExtensions: TExtensions[];
editorClassName?: string;
+ extensions?: Extensions;
+ flaggedExtensions: TExtensions[];
fileHandler: TFileHandler;
forwardedRef?: React.MutableRefObject;
+ handleEditorReady?: (value: boolean) => void;
id: string;
initialValue: string;
mentionHandler: TMentionHandler;
onChange?: (json: object, html: string) => void;
- onTransaction?: () => void;
- handleEditorReady?: (value: boolean) => void;
isSmoothCursorEnabled: boolean;
- autofocus?: boolean;
onEnterKeyPress?: (e?: any) => void;
+ onTransaction?: () => void;
placeholder?: string | ((isFocused: boolean, value: string) => string);
tabIndex?: number;
value?: string | null;
- bubbleMenuEnabled?: boolean;
}
-export interface ILiteTextEditor extends IEditorProps {
- extensions?: Extensions;
-}
-export interface IRichTextEditor extends IEditorProps {
- extensions?: Extensions;
+
+export type ILiteTextEditorProps = IEditorProps;
+export interface IRichTextEditorProps extends IEditorProps {
dragDropEnabled?: boolean;
}
-export interface ICollaborativeDocumentEditor extends Omit {
+export interface ICollaborativeDocumentEditorProps
+ extends Omit {
aiHandler?: TAIHandler;
- bubbleMenuEnabled?: boolean;
editable: boolean;
embedHandler: TEmbedConfig;
- handleEditorReady?: (value: boolean) => void;
- id: string;
realtimeConfig: TRealtimeConfig;
serverHandler?: TServerHandler;
user: TUserDetails;
@@ -183,57 +181,37 @@ export interface ICollaborativeDocumentEditor extends Omit {
aiHandler?: TAIHandler;
- bubbleMenuEnabled?: boolean;
embedHandler: TEmbedConfig;
- handleEditorReady?: (value: boolean) => void;
- id: string;
user: TUserDetails;
}
// read only editor props
-export interface IReadOnlyEditorProps {
- containerClassName?: string;
- disabledExtensions: TExtensions[];
- displayConfig?: TDisplayConfig;
- editorClassName?: string;
- extensions?: Extensions;
+export interface IReadOnlyEditorProps
+ extends Pick<
+ IEditorProps,
+ | "containerClassName"
+ | "disabledExtensions"
+ | "flaggedExtensions"
+ | "displayConfig"
+ | "editorClassName"
+ | "extensions"
+ | "handleEditorReady"
+ | "id"
+ | "initialValue"
+ > {
fileHandler: TReadOnlyFileHandler;
forwardedRef?: React.MutableRefObject;
- id: string;
- initialValue: string;
mentionHandler: TReadOnlyMentionHandler;
}
-export type ILiteTextReadOnlyEditor = IReadOnlyEditorProps;
+export type ILiteTextReadOnlyEditorProps = IReadOnlyEditorProps;
-export type IRichTextReadOnlyEditor = IReadOnlyEditorProps;
+export type IRichTextReadOnlyEditorProps = IReadOnlyEditorProps;
-export interface ICollaborativeDocumentReadOnlyEditor extends Omit {
+export interface IDocumentReadOnlyEditorProps extends IReadOnlyEditorProps {
embedHandler: TEmbedConfig;
- handleEditorReady?: (value: boolean) => void;
- id: string;
- realtimeConfig: TRealtimeConfig;
- serverHandler?: TServerHandler;
- user: TUserDetails;
}
-export interface IDocumentReadOnlyEditor extends IReadOnlyEditorProps {
- embedHandler: TEmbedConfig;
- handleEditorReady?: (value: boolean) => void;
-}
-
-export type TUserDetails = {
- color: string;
- id: string;
- name: string;
- cookie?: string;
-};
-
-export type TRealtimeConfig = {
- url: string;
- queryParams: TWebhookConnectionQueryParams;
-};
-
export interface EditorEvents {
beforeCreate: never;
create: never;
diff --git a/packages/editor/src/core/types/extensions.ts b/packages/editor/src/core/types/extensions.ts
index a2fcebc0eb..33aee7a9f7 100644
--- a/packages/editor/src/core/types/extensions.ts
+++ b/packages/editor/src/core/types/extensions.ts
@@ -6,4 +6,5 @@ export type TExtensions =
| "slash-commands"
| "enter-key"
| "image"
- | "nested-pages";
+ | "nested-pages"
+ | "attachments";
diff --git a/packages/editor/src/core/types/hook.ts b/packages/editor/src/core/types/hook.ts
new file mode 100644
index 0000000000..4ac9e40863
--- /dev/null
+++ b/packages/editor/src/core/types/hook.ts
@@ -0,0 +1,63 @@
+import type { HocuspocusProvider } from "@hocuspocus/provider";
+import type { EditorProps } from "@tiptap/pm/view";
+// local imports
+import type {
+ EditorTitleRefApi,
+ ICollaborativeDocumentEditorProps,
+ IEditorProps,
+ IReadOnlyEditorProps,
+} from "./editor";
+
+type TCoreHookProps = Pick<
+ IEditorProps,
+ | "disabledExtensions"
+ | "editorClassName"
+ | "extensions"
+ | "flaggedExtensions"
+ | "handleEditorReady"
+ | "isSmoothCursorEnabled"
+> & {
+ editorProps?: EditorProps;
+};
+
+export type TEditorHookProps = TCoreHookProps &
+ Pick<
+ IEditorProps,
+ | "autofocus"
+ | "fileHandler"
+ | "forwardedRef"
+ | "id"
+ | "mentionHandler"
+ | "onChange"
+ | "onTransaction"
+ | "placeholder"
+ | "tabIndex"
+ | "value"
+ > & {
+ editable: boolean;
+ enableHistory: boolean;
+ initialValue?: string;
+ provider?: HocuspocusProvider;
+ };
+
+export type TCollaborativeEditorHookProps = TCoreHookProps &
+ Pick<
+ TEditorHookProps,
+ | "editable"
+ | "fileHandler"
+ | "forwardedRef"
+ | "id"
+ | "mentionHandler"
+ | "onChange"
+ | "onTransaction"
+ | "placeholder"
+ | "tabIndex"
+ > &
+ Pick & {
+ titleRef?: React.MutableRefObject;
+ updatePageProperties?: (pageId: string, messageType: string, payload?: any, performAction?: boolean) => void;
+ };
+
+export type TReadOnlyEditorHookProps = Omit &
+ Pick &
+ Pick;
diff --git a/packages/editor/src/core/types/index.ts b/packages/editor/src/core/types/index.ts
index 66cb249425..619fa0c784 100644
--- a/packages/editor/src/core/types/index.ts
+++ b/packages/editor/src/core/types/index.ts
@@ -4,6 +4,7 @@ export * from "./config";
export * from "./editor";
export * from "./embed";
export * from "./extensions";
+export * from "./hook";
export * from "./mention";
export * from "./slash-commands-suggestion";
export * from "@/plane-editor/types";
diff --git a/packages/editor/src/ee/constants/extensions.ts b/packages/editor/src/ee/constants/extensions.ts
index b06a641307..9ede403f2a 100644
--- a/packages/editor/src/ee/constants/extensions.ts
+++ b/packages/editor/src/ee/constants/extensions.ts
@@ -1,3 +1,4 @@
export enum ADDITIONAL_EXTENSIONS {
- PAGE_EMBED_COMPONENT = "page-embed-component",
-}
\ No newline at end of file
+ PAGE_EMBED_COMPONENT = "pageEmbedComponent",
+ ATTACHMENT = "attachmentComponent",
+}
diff --git a/packages/editor/src/ee/constants/utility.ts b/packages/editor/src/ee/constants/utility.ts
index feb62b3529..f53e2b6fbd 100644
--- a/packages/editor/src/ee/constants/utility.ts
+++ b/packages/editor/src/ee/constants/utility.ts
@@ -1 +1,16 @@
-export { NODE_FILE_MAP } from "../../ce/constants/utility";
+// ce imports
+import { NODE_FILE_MAP as CORE_NODE_FILE_MAP } from "src/ce/constants/utility";
+// local imports
+import { ExtensionFileSetStorageKey } from "../types/storage";
+import { ADDITIONAL_EXTENSIONS } from "./extensions";
+
+export const NODE_FILE_MAP: {
+ [key: string]: {
+ fileSetName: ExtensionFileSetStorageKey;
+ };
+} = {
+ ...CORE_NODE_FILE_MAP,
+ [ADDITIONAL_EXTENSIONS.ATTACHMENT]: {
+ fileSetName: "deletedAttachmentSet",
+ },
+};
diff --git a/packages/editor/src/ee/extensions/attachments/components/block.tsx b/packages/editor/src/ee/extensions/attachments/components/block.tsx
new file mode 100644
index 0000000000..75f971e35f
--- /dev/null
+++ b/packages/editor/src/ee/extensions/attachments/components/block.tsx
@@ -0,0 +1,56 @@
+import { File } from "lucide-react";
+import { useEffect, useState } from "react";
+// plane imports
+import { convertBytesToSize } from "@plane/utils";
+// local imports
+import { EAttachmentBlockAttributeNames } from "../types";
+import { CustomAttachmentNodeViewProps } from "./node-view";
+
+type Props = CustomAttachmentNodeViewProps & {
+ resolvedSource: string;
+};
+
+export const CustomAttachmentBlock: React.FC = (props) => {
+ const { extension, node, resolvedSource } = props;
+ // states
+ const [hasCheckedExistence, setHasCheckedExistence] = useState(false);
+ // derived values
+ const { src } = node.attrs;
+
+ useEffect(() => {
+ if (hasCheckedExistence || !src) return;
+ const checkExistence = async () => {
+ try {
+ const doesAttachmentExist = await extension.options.checkIfAttachmentExists?.(src);
+ if (!doesAttachmentExist) {
+ await extension.options.restoreAttachment?.(src);
+ }
+ } catch (error) {
+ console.error("Error in checking attachment existence", error);
+ } finally {
+ setHasCheckedExistence(true);
+ }
+ };
+ checkExistence();
+ }, [extension.options, hasCheckedExistence, src]);
+
+ return (
+
+
+
+
+
+
{node.attrs[EAttachmentBlockAttributeNames.FILE_NAME]}
+
+ {convertBytesToSize(Number(node.attrs[EAttachmentBlockAttributeNames.FILE_SIZE] || 0))}
+
+
+
+ );
+};
diff --git a/packages/editor/src/ee/extensions/attachments/components/flagged-state.tsx b/packages/editor/src/ee/extensions/attachments/components/flagged-state.tsx
new file mode 100644
index 0000000000..67506579d2
--- /dev/null
+++ b/packages/editor/src/ee/extensions/attachments/components/flagged-state.tsx
@@ -0,0 +1,19 @@
+import { FileKey2 } from "lucide-react";
+
+export const CustomAttachmentFlaggedState = () => (
+
+
+
+
+
+ {/* {t("attachmentComponent.upgrade.description")} */}
+ Upgrade your plan to view this attachment.
+
+
+);
diff --git a/packages/editor/src/ee/extensions/attachments/components/node-view.tsx b/packages/editor/src/ee/extensions/attachments/components/node-view.tsx
new file mode 100644
index 0000000000..c511f4fd5e
--- /dev/null
+++ b/packages/editor/src/ee/extensions/attachments/components/node-view.tsx
@@ -0,0 +1,54 @@
+import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
+import { useEffect, useRef, useState } from "react";
+// local imports
+import { type AttachmentExtension, type TAttachmentBlockAttributes } from "../types";
+import { CustomAttachmentBlock } from "./block";
+import { CustomAttachmentFlaggedState } from "./flagged-state";
+import { CustomAttachmentUploader } from "./uploader";
+
+export type CustomAttachmentNodeViewProps = Omit & {
+ extension: AttachmentExtension;
+ node: NodeViewProps["node"] & {
+ attrs: TAttachmentBlockAttributes;
+ };
+ updateAttributes: (attrs: Partial) => void;
+};
+
+export const CustomAttachmentNodeView: React.FC = (props) => {
+ const { extension, node } = props;
+ // states
+ const [resolvedSource, setResolvedSource] = useState(null);
+ // refs
+ const attachmentComponentRef = useRef(null);
+ // derived values
+ const { src } = node.attrs;
+ const isAttachmentUploaded = !!src;
+ const isExtensionFlagged = extension.options.isFlagged;
+
+ useEffect(() => {
+ if (!src || resolvedSource) return;
+ const getAttachmentSource = async () => {
+ const source = await extension.options.getAttachmentSource?.(src);
+ setResolvedSource(source);
+ };
+ getAttachmentSource();
+ }, [extension.options, resolvedSource, src]);
+
+ return (
+
+ {isExtensionFlagged ? (
+
+
+
+ ) : (
+
+ {isAttachmentUploaded ? (
+ <>{resolvedSource && }>
+ ) : (
+
+ )}
+
+ )}
+
+ );
+};
diff --git a/packages/editor/src/ee/extensions/attachments/components/upload-status.tsx b/packages/editor/src/ee/extensions/attachments/components/upload-status.tsx
new file mode 100644
index 0000000000..b8741e5350
--- /dev/null
+++ b/packages/editor/src/ee/extensions/attachments/components/upload-status.tsx
@@ -0,0 +1,60 @@
+import { Editor } from "@tiptap/core";
+import { useEditorState } from "@tiptap/react";
+import { useEffect, useRef, useState } from "react";
+// constants
+import { CORE_EXTENSIONS } from "@/constants/extension";
+// helpers
+import { getExtensionStorage } from "@/helpers/get-extension-storage";
+
+type Props = {
+ editor: Editor;
+ nodeId: string;
+};
+
+export const CustomAttachmentUploadStatus: React.FC = (props) => {
+ const { editor, nodeId } = props;
+ // Displayed status that will animate smoothly
+ const [displayStatus, setDisplayStatus] = useState(0);
+ // Animation frame ID for cleanup
+ const animationFrameRef = useRef(null);
+ // subscribe to image upload status
+ const uploadStatus: number | undefined = useEditorState({
+ editor,
+ selector: ({ editor }) => getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.assetsUploadStatus?.[nodeId],
+ });
+
+ useEffect(() => {
+ const animateToValue = (start: number, end: number, startTime: number) => {
+ const duration = 200;
+
+ const animation = (currentTime: number) => {
+ const elapsed = currentTime - startTime;
+ const progress = Math.min(elapsed / duration, 1);
+
+ // Easing function for smooth animation
+ const easeOutCubic = 1 - Math.pow(1 - progress, 3);
+
+ // Calculate current display value
+ const currentValue = Math.floor(start + (end - start) * easeOutCubic);
+ setDisplayStatus(currentValue);
+
+ // Continue animation if not complete
+ if (progress < 1) {
+ animationFrameRef.current = requestAnimationFrame((time) => animation(time));
+ }
+ };
+ animationFrameRef.current = requestAnimationFrame((time) => animation(time));
+ };
+ animateToValue(displayStatus, uploadStatus == undefined ? 100 : uploadStatus, performance.now());
+
+ return () => {
+ if (animationFrameRef.current) {
+ cancelAnimationFrame(animationFrameRef.current);
+ }
+ };
+ }, [displayStatus, uploadStatus]);
+
+ if (uploadStatus === undefined) return null;
+
+ return <>{displayStatus}%>;
+};
diff --git a/packages/editor/src/ee/extensions/attachments/components/uploader-details.tsx b/packages/editor/src/ee/extensions/attachments/components/uploader-details.tsx
new file mode 100644
index 0000000000..2bed571b3d
--- /dev/null
+++ b/packages/editor/src/ee/extensions/attachments/components/uploader-details.tsx
@@ -0,0 +1,100 @@
+import { Editor } from "@tiptap/core";
+import { useEditorState } from "@tiptap/react";
+import { useMemo } from "react";
+// plane imports
+import { cn } from "@plane/utils";
+// helpers
+import { EFileError } from "@/helpers/file";
+// local imports
+import { getAttachmentExtensionErrorMap } from "../utils";
+import { CustomAttachmentUploadStatus } from "./upload-status";
+
+type Props = {
+ blockId: string;
+ editor: Editor;
+ fileBeingUploaded: File | null;
+ maxFileSize: number;
+};
+
+export const CustomAttachmentUploaderDetails: React.FC = (props) => {
+ const { blockId, editor, fileBeingUploaded, maxFileSize } = props;
+ // subscribe to editor storage
+ const fileUploadErrorMap = useEditorState({
+ editor,
+ selector: ({ editor }) => getAttachmentExtensionErrorMap(editor),
+ });
+ const fileUploadError = fileUploadErrorMap?.get(blockId);
+
+ const errorMessage = useMemo(() => {
+ let title = "";
+ let description: React.ReactNode = "";
+
+ switch (fileUploadError?.error) {
+ case EFileError.FILE_SIZE_TOO_LARGE:
+ // title = t("attachmentComponent.errors.file_too_large.title");
+ title = "File too large.";
+ // description = t("attachmentComponent.errors.file_too_large.description", {
+ // maxFileSize: maxFileSize / 1024 / 1024,
+ // });
+ description = `Maximum size per file is ${maxFileSize / 1024 / 1024}MB`;
+ break;
+ case EFileError.INVALID_FILE_TYPE:
+ // title = t("attachmentComponent.errors.unsupported_file_type.title");
+ title = "Unsupported file type.";
+ description = (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ >
+ {/* {t("attachmentComponent.errors.unsupported_file_type.description")} */}
+ See supported formats
+
+ );
+ break;
+ default:
+ // title = t("attachmentComponent.errors.default.title");
+ title = "Upload failed.";
+ // description = t("attachmentComponent.errors.default.description");
+ description = "Something went wrong. Please try again.";
+ }
+ return {
+ title,
+ description,
+ };
+ }, [fileUploadError, maxFileSize]);
+
+ return (
+
+
+ {fileBeingUploaded
+ ? fileBeingUploaded?.name
+ : fileUploadError
+ ? `${fileUploadError.file.name}- ${errorMessage.title}`
+ : // : t("attachmentComponent.uploader.drag_and_drop")
+ "Drop files here or click to upload"}
+
+
+ {fileBeingUploaded ? (
+
+ ) : fileUploadError ? (
+ errorMessage.description
+ ) : (
+ // t("attachmentComponent.errors.file_too_large.description", {
+ // maxFileSize: maxFileSize / 1024 / 1024,
+ // })
+ `Maximum size per file is ${maxFileSize / 1024 / 1024}MB`
+ )}
+
+
+ );
+};
diff --git a/packages/editor/src/ee/extensions/attachments/components/uploader.tsx b/packages/editor/src/ee/extensions/attachments/components/uploader.tsx
new file mode 100644
index 0000000000..f161f6e6ea
--- /dev/null
+++ b/packages/editor/src/ee/extensions/attachments/components/uploader.tsx
@@ -0,0 +1,207 @@
+import { FileUp } from "lucide-react";
+import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
+// plane imports
+import { cn } from "@plane/utils";
+// constants
+import { ACCEPTED_ATTACHMENT_MIME_TYPES } from "@/constants/config";
+import { CORE_EXTENSIONS } from "@/constants/extension";
+// helpers
+import { EFileError } from "@/helpers/file";
+import { getExtensionStorage } from "@/helpers/get-extension-storage";
+// hooks
+import { uploadFirstFileAndInsertRemaining, useDropZone, useUploader } from "@/hooks/use-file-upload";
+// plane editor imports
+import { ADDITIONAL_EXTENSIONS } from "@/plane-editor/constants/extensions";
+// local imports
+import { EAttachmentBlockAttributeNames } from "../types";
+import { getAttachmentExtensionErrorMap, getAttachmentExtensionFileMap } from "../utils";
+import { CustomAttachmentNodeViewProps } from "./node-view";
+import { CustomAttachmentUploaderDetails } from "./uploader-details";
+
+export const CustomAttachmentUploader: React.FC = (props) => {
+ const { editor, extension, getPos, node, updateAttributes } = props;
+ // states
+ const [fileBeingUploaded, setFileBeingUploaded] = useState(null);
+ // refs
+ const fileInputRef = useRef(null);
+ const hasTriggeredFilePickerRef = useRef(false);
+ // derived values
+ const { id: attachmentBlockId } = node.attrs;
+ const maxFileSize = getExtensionStorage(editor, ADDITIONAL_EXTENSIONS.ATTACHMENT)?.maxFileSize;
+ const attachmentExtensionFileMap = useMemo(() => getAttachmentExtensionFileMap(editor), [editor]);
+ const attachmentExtensionErrorMap = useMemo(() => getAttachmentExtensionErrorMap(editor), [editor]);
+
+ // upload handler
+ const onUpload = useCallback(
+ (url: string, file: File) => {
+ if (!url || !file || !attachmentBlockId) return;
+ // update the node view's attributes post upload
+ updateAttributes({
+ [EAttachmentBlockAttributeNames.SOURCE]: url,
+ [EAttachmentBlockAttributeNames.FILE_NAME]: file.name,
+ [EAttachmentBlockAttributeNames.FILE_TYPE]: file.type,
+ [EAttachmentBlockAttributeNames.FILE_SIZE]: file.size,
+ });
+ // delete from the attachment file map
+ attachmentExtensionFileMap?.delete(attachmentBlockId);
+
+ const pos = getPos();
+ // get current node
+ const getCurrentSelection = editor.state.selection;
+ const currentNode = editor.state.doc.nodeAt(getCurrentSelection.from);
+
+ // only if the cursor is at the current image component, manipulate
+ // the cursor position
+ if (currentNode && currentNode.type.name === node.type.name && currentNode.attrs.src === url) {
+ // control cursor position after upload
+ const nextNode = editor.state.doc.nodeAt(pos + 1);
+
+ if (nextNode && nextNode.type.name === CORE_EXTENSIONS.PARAGRAPH) {
+ // If there is a paragraph node after the image component, move the focus to the next node
+ editor.commands.setTextSelection(pos + 1);
+ } else {
+ // create a new paragraph after the image component post upload
+ editor.commands.createParagraphNear();
+ }
+ }
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [attachmentBlockId, attachmentExtensionFileMap, updateAttributes, getPos]
+ );
+
+ const uploadAttachmentEditorCommand = useCallback(
+ async (file: File) => {
+ try {
+ setFileBeingUploaded(file);
+ return await extension.options.uploadAttachment?.(attachmentBlockId ?? "", file);
+ } catch (error) {
+ attachmentExtensionErrorMap?.set(attachmentBlockId ?? "", {
+ error: EFileError.UPLOAD_FAILED,
+ file,
+ });
+ console.error("Error in uploading attachment via uploader:", error);
+ } finally {
+ setFileBeingUploaded(null);
+ }
+ },
+ [attachmentBlockId, attachmentExtensionErrorMap, extension.options]
+ );
+
+ const handleProgressStatus = useCallback(
+ (isUploading: boolean) => {
+ getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY).uploadInProgress = isUploading;
+ },
+ [editor]
+ );
+
+ const handleInvalidFile = useCallback(
+ (error: EFileError, file: File) => {
+ attachmentExtensionErrorMap?.set(attachmentBlockId ?? "", {
+ error,
+ file,
+ });
+ setFileBeingUploaded(null);
+ },
+ [attachmentBlockId, attachmentExtensionErrorMap]
+ );
+
+ // file upload
+ const { uploadFile } = useUploader({
+ acceptedMimeTypes: ACCEPTED_ATTACHMENT_MIME_TYPES,
+ editorCommand: uploadAttachmentEditorCommand,
+ handleProgressStatus,
+ maxFileSize,
+ onInvalidFile: handleInvalidFile,
+ onUpload,
+ });
+ // dropzone
+ const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({
+ editor,
+ pos: getPos(),
+ type: "attachment",
+ uploader: uploadFile,
+ });
+
+ const handleFileChange = useCallback(
+ async (e: React.ChangeEvent) => {
+ e.preventDefault();
+ const filesList = e.target.files;
+ if (!filesList) return;
+
+ await uploadFirstFileAndInsertRemaining({
+ editor,
+ filesList,
+ pos: getPos(),
+ type: "attachment",
+ uploader: uploadFile,
+ });
+ },
+ [editor, getPos, uploadFile]
+ );
+
+ // the meta data of the image component
+ const meta = useMemo(
+ () => attachmentExtensionFileMap?.get(attachmentBlockId ?? ""),
+ [attachmentBlockId, attachmentExtensionFileMap]
+ );
+
+ // after the attachment component is mounted we start the upload process based on
+ // its upload status
+ useEffect(() => {
+ if (!meta || !attachmentBlockId) return;
+
+ if (meta.event === "drop" && "file" in meta) {
+ uploadFile(meta.file);
+ } else if (meta.event === "insert" && fileInputRef.current && !hasTriggeredFilePickerRef.current) {
+ if (meta.hasOpenedFileInputOnce) return;
+ fileInputRef.current.click();
+ hasTriggeredFilePickerRef.current = true;
+ attachmentExtensionFileMap?.set(attachmentBlockId, {
+ ...meta,
+ hasOpenedFileInputOnce: true,
+ });
+ }
+ }, [attachmentBlockId, attachmentExtensionFileMap, meta, uploadFile]);
+
+ return (
+ {
+ if (editor.isEditable && !fileBeingUploaded) fileInputRef.current?.click();
+ }}
+ role="button"
+ // aria-label={t("attachmentComponent.aria.click_to_upload")}
+ aria-label="Click to upload attachment"
+ aria-disabled={!editor.isEditable}
+ >
+
+
+
+
+
+
+ );
+};
diff --git a/packages/editor/src/ee/extensions/attachments/extension-config.ts b/packages/editor/src/ee/extensions/attachments/extension-config.ts
new file mode 100644
index 0000000000..a32c82fe12
--- /dev/null
+++ b/packages/editor/src/ee/extensions/attachments/extension-config.ts
@@ -0,0 +1,44 @@
+import { mergeAttributes, Node } from "@tiptap/core";
+// plane editor imports
+import { ADDITIONAL_EXTENSIONS } from "@/plane-editor/constants/extensions";
+// local imports
+import { type AttachmentExtension, EAttachmentBlockAttributeNames, type InsertAttachmentComponentProps } from "./types";
+import { DEFAULT_ATTACHMENT_BLOCK_ATTRIBUTES } from "./utils";
+
+declare module "@tiptap/core" {
+ interface Commands {
+ [ADDITIONAL_EXTENSIONS.ATTACHMENT]: {
+ insertAttachmentComponent: (props: InsertAttachmentComponentProps) => ReturnType;
+ };
+ }
+}
+
+export const CustomAttachmentExtensionConfig: AttachmentExtension = Node.create({
+ name: ADDITIONAL_EXTENSIONS.ATTACHMENT,
+ group: "block",
+ atom: true,
+
+ addAttributes() {
+ const attributes = {
+ ...Object.values(EAttachmentBlockAttributeNames).reduce((acc, value) => {
+ acc[value] = {
+ default: DEFAULT_ATTACHMENT_BLOCK_ATTRIBUTES[value],
+ };
+ return acc;
+ }, {}),
+ };
+ return attributes;
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: "attachment-component",
+ },
+ ];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ["attachment-component", mergeAttributes(HTMLAttributes)];
+ },
+});
diff --git a/packages/editor/src/ee/extensions/attachments/extension.ts b/packages/editor/src/ee/extensions/attachments/extension.ts
new file mode 100644
index 0000000000..a67831afb3
--- /dev/null
+++ b/packages/editor/src/ee/extensions/attachments/extension.ts
@@ -0,0 +1,113 @@
+import { ReactNodeViewRenderer } from "@tiptap/react";
+import { v4 as uuidv4 } from "uuid";
+// helpers
+import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
+// types
+import { TFileHandler, TReadOnlyFileHandler } from "@/types";
+// block
+import { CustomAttachmentNodeView } from "./components/node-view";
+// config
+import { CustomAttachmentExtensionConfig } from "./extension-config";
+// types
+import { TAttachmentBlockAttributes } from "./types";
+// utils
+import { DEFAULT_ATTACHMENT_BLOCK_ATTRIBUTES, getAttachmentExtensionFileMap } from "./utils";
+
+type Props = {
+ fileHandler: TReadOnlyFileHandler | TFileHandler;
+ isEditable: boolean;
+ isFlagged: boolean;
+};
+
+export const CustomAttachmentExtension = (props: Props) => {
+ const { fileHandler, isEditable, isFlagged } = props;
+ // derived values
+ const { checkIfAssetExists, getAssetSrc, restore } = fileHandler;
+
+ return CustomAttachmentExtensionConfig.extend({
+ selectable: isEditable,
+ draggable: isEditable,
+
+ addOptions() {
+ const upload = "upload" in fileHandler ? fileHandler.upload : undefined;
+
+ return {
+ checkIfAttachmentExists: checkIfAssetExists,
+ getAttachmentSource: getAssetSrc,
+ isFlagged,
+ restoreAttachment: restore,
+ uploadAttachment: upload,
+ };
+ },
+
+ addStorage() {
+ const maxFileSize = "validation" in fileHandler ? fileHandler.validation?.maxFileSize : 0;
+
+ return {
+ fileMap: new Map(),
+ deletedAttachmentSet: new Map(),
+ errorMap: new Map(),
+ maxFileSize,
+ // escape markdown for attachments
+ markdown: {
+ serialize() {},
+ },
+ };
+ },
+
+ addCommands() {
+ return {
+ insertAttachmentComponent:
+ (props) =>
+ ({ commands }) => {
+ const { event, file, pos } = props;
+ // generate a unique id to keep track of dropped
+ // files' data and for logging transactions
+ const fileId = uuidv4();
+
+ const attachmentExtensionFileMap = getAttachmentExtensionFileMap(this.editor);
+ if (attachmentExtensionFileMap) {
+ if (event === "drop" && file) {
+ attachmentExtensionFileMap.set(fileId, {
+ file,
+ event,
+ });
+ } else if (event === "insert") {
+ attachmentExtensionFileMap.set(fileId, {
+ event,
+ hasOpenedFileInputOnce: false,
+ });
+ }
+ }
+ // create default attributes
+ const attributes: TAttachmentBlockAttributes = {
+ ...DEFAULT_ATTACHMENT_BLOCK_ATTRIBUTES,
+ id: fileId,
+ };
+
+ if (pos) {
+ return commands.insertContentAt(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(CustomAttachmentNodeView);
+ },
+ });
+};
diff --git a/packages/editor/src/ee/extensions/attachments/types.ts b/packages/editor/src/ee/extensions/attachments/types.ts
new file mode 100644
index 0000000000..ed44f021fa
--- /dev/null
+++ b/packages/editor/src/ee/extensions/attachments/types.ts
@@ -0,0 +1,42 @@
+import { Node } from "@tiptap/core";
+// extensions
+import { InsertImageComponentProps, UploadEntity } from "@/extensions/custom-image/custom-image";
+// helpers
+import { EFileError } from "@/helpers/file";
+// types
+import { TFileHandler } from "@/types";
+
+export enum EAttachmentBlockAttributeNames {
+ ID = "id",
+ SOURCE = "src",
+ FILE_NAME = "data-name",
+ FILE_TYPE = "data-file-type",
+ FILE_SIZE = "data-file-size",
+}
+
+export type TAttachmentBlockAttributes = {
+ [EAttachmentBlockAttributeNames.SOURCE]: string | null;
+ [EAttachmentBlockAttributeNames.ID]: string | null;
+ [EAttachmentBlockAttributeNames.FILE_NAME]: string | null;
+ [EAttachmentBlockAttributeNames.FILE_TYPE]: string | null;
+ [EAttachmentBlockAttributeNames.FILE_SIZE]: number | string | null;
+};
+
+export type InsertAttachmentComponentProps = InsertImageComponentProps;
+
+export type AttachmentExtensionOptions = {
+ checkIfAttachmentExists: TFileHandler["checkIfAssetExists"];
+ getAttachmentSource: TFileHandler["getAssetSrc"];
+ isFlagged: boolean;
+ restoreAttachment: TFileHandler["restore"];
+ uploadAttachment?: TFileHandler["upload"];
+};
+
+export type AttachmentExtensionStorage = {
+ deletedAttachmentSet: Map;
+ fileMap: Map;
+ errorMap: Map;
+ maxFileSize: number;
+};
+
+export type AttachmentExtension = Node;
diff --git a/packages/editor/src/ee/extensions/attachments/utils.ts b/packages/editor/src/ee/extensions/attachments/utils.ts
new file mode 100644
index 0000000000..bea1b81f2a
--- /dev/null
+++ b/packages/editor/src/ee/extensions/attachments/utils.ts
@@ -0,0 +1,21 @@
+import { Editor } from "@tiptap/core";
+// helpers
+import { getExtensionStorage } from "@/helpers/get-extension-storage";
+// plane editor imports
+import { ADDITIONAL_EXTENSIONS } from "@/plane-editor/constants/extensions";
+// local imports
+import { EAttachmentBlockAttributeNames, TAttachmentBlockAttributes } from "./types";
+
+export const DEFAULT_ATTACHMENT_BLOCK_ATTRIBUTES: TAttachmentBlockAttributes = {
+ [EAttachmentBlockAttributeNames.SOURCE]: null,
+ [EAttachmentBlockAttributeNames.ID]: null,
+ [EAttachmentBlockAttributeNames.FILE_NAME]: null,
+ [EAttachmentBlockAttributeNames.FILE_TYPE]: null,
+ [EAttachmentBlockAttributeNames.FILE_SIZE]: null,
+};
+
+export const getAttachmentExtensionFileMap = (editor: Editor) =>
+ getExtensionStorage(editor, ADDITIONAL_EXTENSIONS.ATTACHMENT)?.fileMap;
+
+export const getAttachmentExtensionErrorMap = (editor: Editor) =>
+ getExtensionStorage(editor, ADDITIONAL_EXTENSIONS.ATTACHMENT)?.errorMap;
diff --git a/packages/editor/src/ee/extensions/core/extensions.ts b/packages/editor/src/ee/extensions/core/extensions.ts
index cecfb38b47..eb34b8269c 100644
--- a/packages/editor/src/ee/extensions/core/extensions.ts
+++ b/packages/editor/src/ee/extensions/core/extensions.ts
@@ -1,13 +1,8 @@
import { Extensions } from "@tiptap/core";
-// types
-import { TExtensions, TFileHandler } from "@/types";
+// ce imports
+import { TCoreAdditionalExtensionsProps } from "src/ce/extensions";
-type Props = {
- disabledExtensions: TExtensions[];
- fileHandler: TFileHandler;
-};
-
-export const CoreEditorAdditionalExtensions = (props: Props): Extensions => {
+export const CoreEditorAdditionalExtensions = (props: TCoreAdditionalExtensionsProps): Extensions => {
const {} = props;
return [];
};
diff --git a/packages/editor/src/ee/extensions/core/read-only-extensions.ts b/packages/editor/src/ee/extensions/core/read-only-extensions.ts
index 398848e31d..fa699dd1be 100644
--- a/packages/editor/src/ee/extensions/core/read-only-extensions.ts
+++ b/packages/editor/src/ee/extensions/core/read-only-extensions.ts
@@ -1,12 +1,10 @@
import { Extensions } from "@tiptap/core";
-// types
-import { TExtensions } from "@/types";
+// ce imports
+import { TCoreReadOnlyEditorAdditionalExtensionsProps } from "src/ce/extensions";
-type Props = {
- disabledExtensions: TExtensions[];
-};
-
-export const CoreReadOnlyEditorAdditionalExtensions = (props: Props): Extensions => {
+export const CoreReadOnlyEditorAdditionalExtensions = (
+ props: TCoreReadOnlyEditorAdditionalExtensionsProps
+): Extensions => {
const {} = props;
return [];
};
diff --git a/packages/editor/src/ee/extensions/core/without-props.ts b/packages/editor/src/ee/extensions/core/without-props.ts
index a7f8bdbff5..542c484853 100644
--- a/packages/editor/src/ee/extensions/core/without-props.ts
+++ b/packages/editor/src/ee/extensions/core/without-props.ts
@@ -1,6 +1,7 @@
import { Extensions } from "@tiptap/core";
+import { CustomAttachmentExtensionConfig } from "../attachments/extension-config";
import { PageEmbedExtensionConfig } from "../page-embed/extension-config";
-export const CoreEditorAdditionalExtensionsWithoutProps: Extensions = [];
+export const CoreEditorAdditionalExtensionsWithoutProps: Extensions = [CustomAttachmentExtensionConfig];
export const DocumentEditorAdditionalExtensionsWithoutProps: Extensions = [PageEmbedExtensionConfig];
diff --git a/packages/editor/src/ee/extensions/document-extensions.tsx b/packages/editor/src/ee/extensions/document-extensions.tsx
index 1049adf708..125fb52ade 100644
--- a/packages/editor/src/ee/extensions/document-extensions.tsx
+++ b/packages/editor/src/ee/extensions/document-extensions.tsx
@@ -1,5 +1,5 @@
-import { Extension } from "@tiptap/core";
-import { FileText } from "lucide-react";
+import { AnyExtension, Extensions } from "@tiptap/core";
+import { FileText, Paperclip } from "lucide-react";
// core imports
import {
TDocumentEditorAdditionalExtensionsProps,
@@ -16,17 +16,22 @@ import { IssueEmbedSuggestions, IssueListRenderer, PageEmbedExtension } from "@/
// types
import { TExtensions } from "@/types";
// local imports
+import { insertAttachment } from "../helpers/editor-commands";
+import { CustomAttachmentExtension } from "./attachments/extension";
import { CustomCollaborationCursor } from "./collaboration-cursor";
/**
* Registry for slash commands
* Each entry defines a single slash command option with its own enabling logic
*/
-const slashCommandRegistry = [
+const slashCommandRegistry: {
+ isEnabled: (disabledExtensions: TExtensions[]) => boolean;
+ getOption: (props: TDocumentEditorAdditionalExtensionsProps) => TSlashCommandAdditionalOption | null;
+}[] = [
{
// Work item embed slash command
- isEnabled: (disabledExtensions: TExtensions[]) => !disabledExtensions.includes("issue-embed"),
- getOption: (): TSlashCommandAdditionalOption => ({
+ isEnabled: (disabledExtensions) => !disabledExtensions.includes("issue-embed"),
+ getOption: () => ({
commandKey: "issue-embed",
key: "issue-embed",
title: "Work item",
@@ -42,8 +47,8 @@ const slashCommandRegistry = [
},
{
// Page embed slash command
- isEnabled: (disabledExtensions: TExtensions[]) => !disabledExtensions.includes("nested-pages"),
- getOption: ({ embedConfig }: TDocumentEditorAdditionalExtensionsProps): TSlashCommandAdditionalOption | null => {
+ isEnabled: (disabledExtensions) => !disabledExtensions.includes("nested-pages"),
+ getOption: ({ embedConfig }) => {
// Only enable if page config with createCallback exists
const pageConfig = embedConfig?.page;
if (!pageConfig?.createCallback) return null;
@@ -73,6 +78,21 @@ const slashCommandRegistry = [
};
},
},
+ {
+ // Attachment slash command
+ isEnabled: (disabledExtensions) => !disabledExtensions.includes("attachments"),
+ getOption: () => ({
+ commandKey: "attachment",
+ key: "attachment",
+ title: "Attachment",
+ description: "Insert a file",
+ searchTerms: ["image", "photo", "picture", "pdf", "media", "upload", "audio", "video", "file", "attachment"],
+ icon: ,
+ command: ({ editor, range }) => insertAttachment({ editor, event: "insert", range }),
+ section: "general",
+ pushAfter: "image",
+ }),
+ },
];
/**
@@ -93,11 +113,12 @@ const extensionRegistry: TDocumentEditorAdditionalExtensionsRegistry[] = [
return SlashCommands({
additionalOptions: slashCommandOptions,
disabledExtensions: props.disabledExtensions,
+ flaggedExtensions: props.flaggedExtensions,
});
},
},
{
- // Issue embed suggestions extension
+ // Work item embed suggestions extension
isEnabled: (disabledExtensions) => !disabledExtensions.includes("issue-embed"),
getExtension: ({ embedConfig }) => {
const issueConfig = embedConfig?.issue;
@@ -125,7 +146,7 @@ const extensionRegistry: TDocumentEditorAdditionalExtensionsRegistry[] = [
},
{
// Page embed extension
- isEnabled: () => true,
+ isEnabled: (disabledExtensions) => !disabledExtensions.includes("nested-pages"),
getExtension: ({ embedConfig }) => {
const pageConfig = embedConfig?.page;
@@ -141,19 +162,29 @@ const extensionRegistry: TDocumentEditorAdditionalExtensionsRegistry[] = [
});
},
},
+ {
+ // Attachment extension
+ isEnabled: (disabledExtensions) => !disabledExtensions.includes("attachments"),
+ getExtension: ({ flaggedExtensions, fileHandler, isEditable }) =>
+ CustomAttachmentExtension({
+ fileHandler,
+ isFlagged: flaggedExtensions.includes("attachments"),
+ isEditable,
+ }),
+ },
];
/**
* Returns all enabled extensions for the document editor
*/
export const DocumentEditorAdditionalExtensions = (props: TDocumentEditorAdditionalExtensionsProps) => {
- const { disabledExtensions = [] } = props;
+ const { disabledExtensions, flaggedExtensions } = props;
// Filter enabled extensions and flatten the result
- const extensions: Extension[] = extensionRegistry
- .filter((config) => config.isEnabled(disabledExtensions))
+ const extensions: Extensions = extensionRegistry
+ .filter((config) => config.isEnabled(disabledExtensions, flaggedExtensions))
.map((config) => config.getExtension(props))
- .filter((extension): extension is Extension => extension !== undefined);
+ .filter((extension): extension is AnyExtension => extension !== undefined);
return extensions;
};
diff --git a/packages/editor/src/ee/extensions/rich-text/extensions.tsx b/packages/editor/src/ee/extensions/rich-text/extensions.tsx
index 54d83e7d41..3d55a593f2 100644
--- a/packages/editor/src/ee/extensions/rich-text/extensions.tsx
+++ b/packages/editor/src/ee/extensions/rich-text/extensions.tsx
@@ -1 +1,78 @@
-export * from "src/ce/extensions/rich-text/extensions";
+import { AnyExtension, Extensions } from "@tiptap/core";
+import { Paperclip } from "lucide-react";
+// core imports
+import {
+ TRichTextEditorAdditionalExtensionsProps,
+ TRichTextEditorAdditionalExtensionsRegistry,
+} from "src/ce/extensions/rich-text/extensions";
+// extensions
+import { SlashCommands, TSlashCommandAdditionalOption } from "@/extensions/slash-commands/root";
+// types
+import { TExtensions } from "@/types";
+// local imports
+import { insertAttachment } from "../../helpers/editor-commands";
+import { CustomAttachmentExtension } from "../attachments/extension";
+
+/**
+ * Registry for slash commands
+ * Each entry defines a single slash command option with its own enabling logic
+ */
+const slashCommandRegistry: {
+ isEnabled: (disabledExtensions: TExtensions[]) => boolean;
+ getOption: (props: TRichTextEditorAdditionalExtensionsProps) => TSlashCommandAdditionalOption | null;
+}[] = [
+ {
+ isEnabled: (disabledExtensions) => !disabledExtensions.includes("attachments"),
+ getOption: () => ({
+ commandKey: "attachment",
+ key: "attachment",
+ title: "Attachment",
+ description: "Insert a file",
+ searchTerms: ["image", "photo", "picture", "pdf", "media", "upload", "audio", "video", "file", "attachment"],
+ icon: ,
+ command: ({ editor, range }) => insertAttachment({ editor, event: "insert", range }),
+ section: "general",
+ pushAfter: "image",
+ }),
+ },
+];
+
+const extensionRegistry: TRichTextEditorAdditionalExtensionsRegistry[] = [
+ {
+ isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
+ getExtension: (props) => {
+ const { disabledExtensions, flaggedExtensions } = props;
+ // Get enabled slash command options from the registry
+ const slashCommandOptions = slashCommandRegistry
+ .filter((command) => command.isEnabled(disabledExtensions || []))
+ .map((command) => command.getOption(props))
+ .filter((option): option is TSlashCommandAdditionalOption => option !== null);
+
+ return SlashCommands({
+ additionalOptions: slashCommandOptions,
+ disabledExtensions,
+ flaggedExtensions,
+ });
+ },
+ },
+ {
+ isEnabled: (disabledExtensions) => !disabledExtensions.includes("attachments"),
+ getExtension: ({ flaggedExtensions, fileHandler }) =>
+ CustomAttachmentExtension({
+ fileHandler,
+ isFlagged: flaggedExtensions.includes("attachments"),
+ isEditable: true,
+ }),
+ },
+];
+
+export const RichTextEditorAdditionalExtensions = (props: TRichTextEditorAdditionalExtensionsProps) => {
+ const { disabledExtensions, flaggedExtensions } = props;
+
+ const extensions: Extensions = extensionRegistry
+ .filter((config) => config.isEnabled(disabledExtensions, flaggedExtensions))
+ .map((config) => config.getExtension(props))
+ .filter((extension): extension is AnyExtension => extension !== undefined);
+
+ return extensions;
+};
diff --git a/packages/editor/src/ee/extensions/rich-text/read-only-extensions.tsx b/packages/editor/src/ee/extensions/rich-text/read-only-extensions.tsx
index e05745154f..fb73d07e99 100644
--- a/packages/editor/src/ee/extensions/rich-text/read-only-extensions.tsx
+++ b/packages/editor/src/ee/extensions/rich-text/read-only-extensions.tsx
@@ -1 +1,31 @@
-export * from "src/ce/extensions/rich-text/read-only-extensions";
+import { AnyExtension, Extensions } from "@tiptap/core";
+// core imports
+import {
+ TRichTextReadOnlyEditorAdditionalExtensionsProps,
+ TRichTextReadOnlyEditorAdditionalExtensionsRegistry,
+} from "src/ce/extensions/rich-text/read-only-extensions";
+// local imports
+import { CustomAttachmentExtension } from "../attachments/extension";
+
+const extensionRegistry: TRichTextReadOnlyEditorAdditionalExtensionsRegistry[] = [
+ {
+ isEnabled: (disabledExtensions) => !disabledExtensions.includes("attachments"),
+ getExtension: ({ fileHandler, flaggedExtensions }) =>
+ CustomAttachmentExtension({
+ fileHandler,
+ isFlagged: flaggedExtensions.includes("attachments"),
+ isEditable: false,
+ }),
+ },
+];
+
+export const RichTextReadOnlyEditorAdditionalExtensions = (props: TRichTextReadOnlyEditorAdditionalExtensionsProps) => {
+ const { disabledExtensions } = props;
+
+ const extensions: Extensions = extensionRegistry
+ .filter((config) => config.isEnabled(disabledExtensions))
+ .map((config) => config.getExtension(props))
+ .filter((extension): extension is AnyExtension => extension !== undefined);
+
+ return extensions;
+};
diff --git a/packages/editor/src/ee/helpers/editor-commands.ts b/packages/editor/src/ee/helpers/editor-commands.ts
new file mode 100644
index 0000000000..0180445719
--- /dev/null
+++ b/packages/editor/src/ee/helpers/editor-commands.ts
@@ -0,0 +1,24 @@
+import { Editor, Range } from "@tiptap/core";
+// plane editor extensions
+import { type InsertAttachmentComponentProps } from "@/plane-editor/extensions/attachments/types";
+
+export const insertAttachment = ({
+ editor,
+ event,
+ pos,
+ file,
+ range,
+}: {
+ editor: Editor;
+ event: "insert" | "drop";
+ pos?: number | null;
+ file?: File;
+ range?: Range;
+}) => {
+ if (range) editor.chain().focus().deleteRange(range).run();
+
+ const attachmentOptions: InsertAttachmentComponentProps = { event };
+ if (pos) attachmentOptions.pos = pos;
+ if (file) attachmentOptions.file = file;
+ return editor?.chain().focus().insertAttachmentComponent(attachmentOptions).run();
+};
diff --git a/packages/editor/src/ee/types/storage.ts b/packages/editor/src/ee/types/storage.ts
new file mode 100644
index 0000000000..c2db9f25d9
--- /dev/null
+++ b/packages/editor/src/ee/types/storage.ts
@@ -0,0 +1,15 @@
+import {
+ ExtensionStorageMap as CoreExtensionStorageMap,
+ ExtensionFileSetStorageKey as CoreExtensionFileSetStorageKey,
+} from "src/ce/types/storage";
+// local imports
+import { ADDITIONAL_EXTENSIONS } from "../constants/extensions";
+import { type AttachmentExtensionStorage } from "../extensions/attachments/types";
+
+export type ExtensionStorageMap = CoreExtensionStorageMap & {
+ [ADDITIONAL_EXTENSIONS.ATTACHMENT]: AttachmentExtensionStorage;
+};
+
+export type ExtensionFileSetStorageKey =
+ | CoreExtensionFileSetStorageKey
+ | Extract;
diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts
index 02e272e5e0..090e1a8872 100644
--- a/packages/editor/src/index.ts
+++ b/packages/editor/src/index.ts
@@ -40,5 +40,4 @@ export { type IMarking, useEditorMarkings } from "@/hooks/use-editor-markings";
export { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// types
-export type { CustomEditorProps } from "@/hooks/use-editor";
export * from "@/types";
diff --git a/packages/i18n/src/locales/cs/editor-extended.json b/packages/i18n/src/locales/cs/editor-extended.json
index 0967ef424b..ec39e323f9 100644
--- a/packages/i18n/src/locales/cs/editor-extended.json
+++ b/packages/i18n/src/locales/cs/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Přetáhněte soubory pro nahrání externích souborů"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "Soubor je příliš velký.",
+ "description": "Maximální velikost na soubor je {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "Nepodporovaný typ souboru.",
+ "description": "Zobrazit podporované formáty"
+ },
+ "default": {
+ "title": "Nahrávání selhalo.",
+ "description": "Něco se pokazilo. Zkuste to znovu."
+ }
+ },
+ "upgrade": {
+ "description": "Upgradujte svůj plán pro zobrazení této přílohy."
+ },
+ "aria": {
+ "click_to_upload": "Klikněte pro nahrání přílohy"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/de/editor-extended.json b/packages/i18n/src/locales/de/editor-extended.json
index 0967ef424b..c0ecc74d31 100644
--- a/packages/i18n/src/locales/de/editor-extended.json
+++ b/packages/i18n/src/locales/de/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Zum Hochladen externe Dateien ziehen und ablegen"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "Datei zu groß.",
+ "description": "Maximale Größe pro Datei ist {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "Nicht unterstützter Dateityp.",
+ "description": "Unterstützte Formate anzeigen"
+ },
+ "default": {
+ "title": "Upload fehlgeschlagen.",
+ "description": "Etwas ist schief gelaufen. Bitte versuchen Sie es erneut."
+ }
+ },
+ "upgrade": {
+ "description": "Aktualisieren Sie Ihren Plan, um diesen Anhang anzuzeigen."
+ },
+ "aria": {
+ "click_to_upload": "Klicken Sie, um Anhang hochzuladen"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/en/editor-extended.json b/packages/i18n/src/locales/en/editor-extended.json
index 0967ef424b..88aa7778c4 100644
--- a/packages/i18n/src/locales/en/editor-extended.json
+++ b/packages/i18n/src/locales/en/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Drop files here or click to upload"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "File too large.",
+ "description": "Maximum size per file is {maxFileSize}MB"
+ },
+ "unsupported_file_type": {
+ "title": "Unsupported file type.",
+ "description": "See supported formats"
+ },
+ "default": {
+ "title": "Upload failed.",
+ "description": "Something went wrong. Please try again."
+ }
+ },
+ "upgrade": {
+ "description": "Upgrade your plan to view this attachment."
+ },
+ "aria": {
+ "click_to_upload": "Click to upload attachment"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/es/editor-extended.json b/packages/i18n/src/locales/es/editor-extended.json
index 0967ef424b..672b4fc212 100644
--- a/packages/i18n/src/locales/es/editor-extended.json
+++ b/packages/i18n/src/locales/es/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Arrastra y suelta para subir archivos externos"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "Archivo demasiado grande.",
+ "description": "El tamaño máximo por archivo es {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "Tipo de archivo no compatible.",
+ "description": "Ver formatos compatibles"
+ },
+ "default": {
+ "title": "Error al subir.",
+ "description": "Algo salió mal. Por favor, inténtelo de nuevo."
+ }
+ },
+ "upgrade": {
+ "description": "Actualice su plan para ver este archivo adjunto."
+ },
+ "aria": {
+ "click_to_upload": "Haga clic para subir archivo adjunto"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/fr/editor-extended.json b/packages/i18n/src/locales/fr/editor-extended.json
index 0967ef424b..4a278f0a57 100644
--- a/packages/i18n/src/locales/fr/editor-extended.json
+++ b/packages/i18n/src/locales/fr/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Glissez-déposez pour télécharger des fichiers externes"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "Fichier trop volumineux.",
+ "description": "La taille maximale par fichier est de {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "Type de fichier non pris en charge.",
+ "description": "Voir les formats pris en charge"
+ },
+ "default": {
+ "title": "Échec du téléchargement.",
+ "description": "Quelque chose s'est mal passé. Veuillez réessayer."
+ }
+ },
+ "upgrade": {
+ "description": "Mettez à niveau votre plan pour voir cette pièce jointe."
+ },
+ "aria": {
+ "click_to_upload": "Cliquez pour télécharger la pièce jointe"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/id/editor-extended.json b/packages/i18n/src/locales/id/editor-extended.json
index 0967ef424b..ad0c526dfd 100644
--- a/packages/i18n/src/locales/id/editor-extended.json
+++ b/packages/i18n/src/locales/id/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Seret dan lepas untuk mengunggah file eksternal"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "File terlalu besar.",
+ "description": "Ukuran maksimum per file adalah {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "Jenis file tidak didukung.",
+ "description": "Lihat format yang didukung"
+ },
+ "default": {
+ "title": "Upload gagal.",
+ "description": "Terjadi kesalahan. Silakan coba lagi."
+ }
+ },
+ "upgrade": {
+ "description": "Tingkatkan paket Anda untuk melihat lampiran ini."
+ },
+ "aria": {
+ "click_to_upload": "Klik untuk mengunggah lampiran"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/it/editor-extended.json b/packages/i18n/src/locales/it/editor-extended.json
index 0967ef424b..c71d52c4e0 100644
--- a/packages/i18n/src/locales/it/editor-extended.json
+++ b/packages/i18n/src/locales/it/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Trascina e rilascia per caricare file esterni"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "File troppo grande.",
+ "description": "La dimensione massima per file è {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "Tipo di file non supportato.",
+ "description": "Vedi formati supportati"
+ },
+ "default": {
+ "title": "Caricamento fallito.",
+ "description": "Qualcosa è andato storto. Riprova."
+ }
+ },
+ "upgrade": {
+ "description": "Aggiorna il tuo piano per visualizzare questo allegato."
+ },
+ "aria": {
+ "click_to_upload": "Clicca per caricare allegato"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/ja/editor-extended.json b/packages/i18n/src/locales/ja/editor-extended.json
index 0967ef424b..a8d6d2c480 100644
--- a/packages/i18n/src/locales/ja/editor-extended.json
+++ b/packages/i18n/src/locales/ja/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "外部ファイルをアップロードするにはドラッグ&ドロップしてください"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "ファイルが大きすぎます。",
+ "description": "ファイルあたりの最大サイズは {maxFileSize} MB です"
+ },
+ "unsupported_file_type": {
+ "title": "サポートされていないファイル形式です。",
+ "description": "サポートされている形式を確認"
+ },
+ "default": {
+ "title": "アップロードに失敗しました。",
+ "description": "問題が発生しました。もう一度お試しください。"
+ }
+ },
+ "upgrade": {
+ "description": "この添付ファイルを表示するにはプランをアップグレードしてください。"
+ },
+ "aria": {
+ "click_to_upload": "クリックして添付ファイルをアップロード"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/ko/editor-extended.json b/packages/i18n/src/locales/ko/editor-extended.json
index 0967ef424b..147a7101a3 100644
--- a/packages/i18n/src/locales/ko/editor-extended.json
+++ b/packages/i18n/src/locales/ko/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "외부 파일을 업로드하려면 드래그 앤 드롭하세요"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "파일이 너무 큽니다.",
+ "description": "파일당 최대 크기는 {maxFileSize} MB입니다"
+ },
+ "unsupported_file_type": {
+ "title": "지원되지 않는 파일 형식입니다.",
+ "description": "지원되는 형식 보기"
+ },
+ "default": {
+ "title": "업로드 실패.",
+ "description": "문제가 발생했습니다. 다시 시도해 주세요."
+ }
+ },
+ "upgrade": {
+ "description": "이 첨부 파일을 보려면 플랜을 업그레이드하세요."
+ },
+ "aria": {
+ "click_to_upload": "첨부 파일을 업로드하려면 클릭하세요"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/pl/editor-extended.json b/packages/i18n/src/locales/pl/editor-extended.json
index 0967ef424b..484bb5da04 100644
--- a/packages/i18n/src/locales/pl/editor-extended.json
+++ b/packages/i18n/src/locales/pl/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Przeciągnij i upuść, aby przesłać pliki zewnętrzne"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "Plik za duży.",
+ "description": "Maksymalny rozmiar na plik to {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "Nieobsługiwany typ pliku.",
+ "description": "Zobacz obsługiwane formaty"
+ },
+ "default": {
+ "title": "Przesyłanie nie powiodło się.",
+ "description": "Coś poszło nie tak. Spróbuj ponownie."
+ }
+ },
+ "upgrade": {
+ "description": "Uaktualnij swój plan, aby wyświetlić ten załącznik."
+ },
+ "aria": {
+ "click_to_upload": "Kliknij, aby przesłać załącznik"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/pt-BR/editor-extended.json b/packages/i18n/src/locales/pt-BR/editor-extended.json
index 0967ef424b..0d8121184b 100644
--- a/packages/i18n/src/locales/pt-BR/editor-extended.json
+++ b/packages/i18n/src/locales/pt-BR/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Arraste e solte para fazer upload de arquivos externos"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "Arquivo muito grande.",
+ "description": "O tamanho máximo por arquivo é {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "Tipo de arquivo não suportado.",
+ "description": "Ver formatos suportados"
+ },
+ "default": {
+ "title": "Falha no upload.",
+ "description": "Algo deu errado. Tente novamente."
+ }
+ },
+ "upgrade": {
+ "description": "Atualize seu plano para visualizar este anexo."
+ },
+ "aria": {
+ "click_to_upload": "Clique para fazer upload do anexo"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/ro/editor-extended.json b/packages/i18n/src/locales/ro/editor-extended.json
index 0967ef424b..6f97833600 100644
--- a/packages/i18n/src/locales/ro/editor-extended.json
+++ b/packages/i18n/src/locales/ro/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Trageți și plasați pentru a încărca fișiere externe"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "Fișier prea mare.",
+ "description": "Dimensiunea maximă per fișier este {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "Tip de fișier neacceptat.",
+ "description": "Vezi formatele acceptate"
+ },
+ "default": {
+ "title": "Încărcarea a eșuat.",
+ "description": "Ceva a mers prost. Vă rugăm să încercați din nou."
+ }
+ },
+ "upgrade": {
+ "description": "Actualizați planul pentru a vizualiza această atașare."
+ },
+ "aria": {
+ "click_to_upload": "Faceți clic pentru a încărca atașamentul"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/ru/editor-extended.json b/packages/i18n/src/locales/ru/editor-extended.json
index 0967ef424b..bc2b1fbd38 100644
--- a/packages/i18n/src/locales/ru/editor-extended.json
+++ b/packages/i18n/src/locales/ru/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Перетащите для загрузки внешних файлов"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "Файл слишком большой.",
+ "description": "Максимальный размер файла {maxFileSize} МБ"
+ },
+ "unsupported_file_type": {
+ "title": "Неподдерживаемый тип файла.",
+ "description": "Посмотреть поддерживаемые форматы"
+ },
+ "default": {
+ "title": "Загрузка не удалась.",
+ "description": "Что-то пошло не так. Попробуйте еще раз."
+ }
+ },
+ "upgrade": {
+ "description": "Обновите план для просмотра этого вложения."
+ },
+ "aria": {
+ "click_to_upload": "Нажмите для загрузки вложения"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/sk/editor-extended.json b/packages/i18n/src/locales/sk/editor-extended.json
index 0967ef424b..34e8f9f949 100644
--- a/packages/i18n/src/locales/sk/editor-extended.json
+++ b/packages/i18n/src/locales/sk/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Pretiahni a pusť pre nahranie externých súborov"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "Súbor je príliš veľký.",
+ "description": "Maximálna veľkosť na súbor je {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "Nepodporovaný typ súboru.",
+ "description": "Zobraziť podporované formáty"
+ },
+ "default": {
+ "title": "Nahrávanie zlyhalo.",
+ "description": "Niečo sa pokazilo. Skúste to znovu."
+ }
+ },
+ "upgrade": {
+ "description": "Upgradujte svoj plán pre zobrazenie tejto prílohy."
+ },
+ "aria": {
+ "click_to_upload": "Kliknite pre nahranie prílohy"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/tr-TR/editor-extended.json b/packages/i18n/src/locales/tr-TR/editor-extended.json
index 0967ef424b..fe0dbc7b47 100644
--- a/packages/i18n/src/locales/tr-TR/editor-extended.json
+++ b/packages/i18n/src/locales/tr-TR/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Harici dosyaları yüklemek için sürükle ve bırak"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "Dosya çok büyük.",
+ "description": "Dosya başına maksimum boyut {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "Desteklenmeyen dosya türü.",
+ "description": "Desteklenen formatları gör"
+ },
+ "default": {
+ "title": "Yükleme başarısız.",
+ "description": "Bir şeyler ters gitti. Lütfen tekrar deneyin."
+ }
+ },
+ "upgrade": {
+ "description": "Bu eki görüntülemek için planınızı yükseltin."
+ },
+ "aria": {
+ "click_to_upload": "Ek yüklemek için tıklayın"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/ua/editor-extended.json b/packages/i18n/src/locales/ua/editor-extended.json
index 0967ef424b..869c6250ef 100644
--- a/packages/i18n/src/locales/ua/editor-extended.json
+++ b/packages/i18n/src/locales/ua/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Перетягніть для завантаження зовнішніх файлів"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "Файл занадто великий.",
+ "description": "Максимальний розмір файлу {maxFileSize} МБ"
+ },
+ "unsupported_file_type": {
+ "title": "Непідтримуваний тип файлу.",
+ "description": "Переглянути підтримувані формати"
+ },
+ "default": {
+ "title": "Завантаження не вдалося.",
+ "description": "Щось пішло не так. Спробуйте ще раз."
+ }
+ },
+ "upgrade": {
+ "description": "Оновіть план для перегляду цього вкладення."
+ },
+ "aria": {
+ "click_to_upload": "Натисніть для завантаження вкладення"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/vi-VN/editor-extended.json b/packages/i18n/src/locales/vi-VN/editor-extended.json
index 0967ef424b..3ffd1170ea 100644
--- a/packages/i18n/src/locales/vi-VN/editor-extended.json
+++ b/packages/i18n/src/locales/vi-VN/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "Kéo và thả để tải lên tệp bên ngoài"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "Tệp quá lớn.",
+ "description": "Kích thước tối đa mỗi tệp là {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "Loại tệp không được hỗ trợ.",
+ "description": "Xem các định dạng được hỗ trợ"
+ },
+ "default": {
+ "title": "Tải lên thất bại.",
+ "description": "Đã xảy ra lỗi. Vui lòng thử lại."
+ }
+ },
+ "upgrade": {
+ "description": "Nâng cấp gói của bạn để xem tệp đính kèm này."
+ },
+ "aria": {
+ "click_to_upload": "Nhấp để tải lên tệp đính kèm"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/zh-CN/editor-extended.json b/packages/i18n/src/locales/zh-CN/editor-extended.json
index 0967ef424b..ae92dd95cd 100644
--- a/packages/i18n/src/locales/zh-CN/editor-extended.json
+++ b/packages/i18n/src/locales/zh-CN/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "拖拽上传外部文件"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "文件过大。",
+ "description": "每个文件的最大大小为 {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "不支持的文件类型。",
+ "description": "查看支持的格式"
+ },
+ "default": {
+ "title": "上传失败。",
+ "description": "出现错误。请重试。"
+ }
+ },
+ "upgrade": {
+ "description": "升级您的计划以查看此附件。"
+ },
+ "aria": {
+ "click_to_upload": "点击上传附件"
+ }
+ }
+}
diff --git a/packages/i18n/src/locales/zh-TW/editor-extended.json b/packages/i18n/src/locales/zh-TW/editor-extended.json
index 0967ef424b..ff944dd120 100644
--- a/packages/i18n/src/locales/zh-TW/editor-extended.json
+++ b/packages/i18n/src/locales/zh-TW/editor-extended.json
@@ -1 +1,27 @@
-{}
+{
+ "attachmentComponent": {
+ "uploader": {
+ "drag_and_drop": "拖拽上傳外部檔案"
+ },
+ "errors": {
+ "file_too_large": {
+ "title": "檔案過大。",
+ "description": "每個檔案的最大大小為 {maxFileSize} MB"
+ },
+ "unsupported_file_type": {
+ "title": "不支援的檔案類型。",
+ "description": "查看支援的格式"
+ },
+ "default": {
+ "title": "上傳失敗。",
+ "description": "出現錯誤。請重試。"
+ }
+ },
+ "upgrade": {
+ "description": "升級您的計劃以查看此附件。"
+ },
+ "aria": {
+ "click_to_upload": "點擊上傳附件"
+ }
+ }
+}
diff --git a/space/core/components/editor/lite-text-editor.tsx b/space/core/components/editor/lite-text-editor.tsx
index 2663e0ca20..82da909b3e 100644
--- a/space/core/components/editor/lite-text-editor.tsx
+++ b/space/core/components/editor/lite-text-editor.tsx
@@ -1,6 +1,6 @@
import React from "react";
// plane imports
-import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
+import { EditorRefApi, ILiteTextEditorProps, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
import { MakeOptional } from "@plane/types";
import { cn } from "@plane/utils";
// components
@@ -11,8 +11,8 @@ import { isCommentEmpty } from "@/helpers/string.helper";
interface LiteTextEditorWrapperProps
extends MakeOptional<
- Omit,
- "disabledExtensions"
+ Omit,
+ "disabledExtensions" | "flaggedExtensions"
> {
anchor: string;
workspaceId: string;
@@ -30,6 +30,7 @@ export const LiteTextEditor = React.forwardRef(ref: React.ForwardedRef): ref is React.MutableRefObject {
@@ -44,6 +45,7 @@ export const LiteTextEditor = React.forwardRef,
- "disabledExtensions"
+ Omit,
+ "disabledExtensions" | "flaggedExtensions"
> & {
anchor: string;
workspaceId: string;
};
export const LiteTextReadOnlyEditor = React.forwardRef(
- ({ anchor, workspaceId, disabledExtensions, ...props }, ref) => {
+ ({ anchor, workspaceId, disabledExtensions, flaggedExtensions, ...props }, ref) => {
const { getMemberById } = useMember();
return (
,
- "disabledExtensions"
+ Omit,
+ "disabledExtensions" | "flaggedExtensions"
> {
anchor: string;
uploadFile: TFileHandler["upload"];
@@ -20,7 +20,7 @@ interface RichTextEditorWrapperProps
}
export const RichTextEditor = forwardRef((props, ref) => {
- const { anchor, containerClassName, uploadFile, workspaceId, disabledExtensions, ...rest } = props;
+ const { anchor, containerClassName, uploadFile, workspaceId, disabledExtensions, flaggedExtensions, ...rest } = props;
const { getMemberById } = useMember();
return (
,
- "disabledExtensions"
+ Omit,
+ "disabledExtensions" | "flaggedExtensions"
> & {
anchor: string;
workspaceId: string;
};
export const RichTextReadOnlyEditor = React.forwardRef(
- ({ anchor, workspaceId, disabledExtensions, ...props }, ref) => {
+ ({ anchor, workspaceId, disabledExtensions, flaggedExtensions, ...props }, ref) => {
const { getMemberById } = useMember();
return (
= observer((props) => {
ref={editorRef}
id={pageDetails.id}
disabledExtensions={[]}
+ flaggedExtensions={[]}
initialValue={pageDetails.description_html ?? ""}
containerClassName="p-0 pb-64 border-none"
fileHandler={getReadOnlyEditorFileHandlers({
diff --git a/web/ce/hooks/use-editor-flagging.ts b/web/ce/hooks/use-editor-flagging.ts
index 05fdff91fa..7e04919ce6 100644
--- a/web/ce/hooks/use-editor-flagging.ts
+++ b/web/ce/hooks/use-editor-flagging.ts
@@ -1,17 +1,35 @@
// editor
import { TExtensions } from "@plane/editor";
+export type TEditorFlaggingHookReturnType = {
+ document: {
+ disabled: TExtensions[];
+ flagged: TExtensions[];
+ };
+ liteText: {
+ disabled: TExtensions[];
+ flagged: TExtensions[];
+ };
+ richText: {
+ disabled: TExtensions[];
+ flagged: TExtensions[];
+ };
+};
+
/**
* @description extensions disabled in various editors
*/
-export const useEditorFlagging = (
- workspaceSlug: string
-): {
- documentEditor: TExtensions[];
- liteTextEditor: TExtensions[];
- richTextEditor: TExtensions[];
-} => ({
- documentEditor: ["ai", "collaboration-cursor"],
- liteTextEditor: ["ai", "collaboration-cursor"],
- richTextEditor: ["ai", "collaboration-cursor"],
+export const useEditorFlagging = (workspaceSlug: string): TEditorFlaggingHookReturnType => ({
+ document: {
+ disabled: ["ai", "collaboration-cursor"],
+ flagged: [],
+ },
+ liteText: {
+ disabled: ["ai", "collaboration-cursor"],
+ flagged: [],
+ },
+ richText: {
+ disabled: ["ai", "collaboration-cursor"],
+ flagged: [],
+ },
});
diff --git a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx
index 03e55956b4..ef7ec1defb 100644
--- a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx
+++ b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx
@@ -2,7 +2,7 @@ import React, { useState } from "react";
// plane constants
import { EIssueCommentAccessSpecifier } from "@plane/constants";
// plane editor
-import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
+import { EditorRefApi, ILiteTextEditorProps, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
// i18n
import { useTranslation } from "@plane/i18n";
// components
@@ -22,8 +22,8 @@ const workspaceService = new WorkspaceService();
interface LiteTextEditorWrapperProps
extends MakeOptional<
- Omit,
- "disabledExtensions"
+ Omit,
+ "disabledExtensions" | "flaggedExtensions" | "isSmoothCursorEnabled"
> {
workspaceSlug: string;
workspaceId: string;
@@ -58,13 +58,13 @@ export const LiteTextEditor = React.forwardRef
,
- "disabledExtensions"
+ Omit,
+ "disabledExtensions" | "flaggedExtensions"
> & {
workspaceId: string;
workspaceSlug: string;
@@ -28,14 +28,15 @@ export const LiteTextReadOnlyEditor = React.forwardRef,
- "disabledExtensions"
+ Omit,
+ "disabledExtensions" | "flaggedExtensions" | "isSmoothCursorEnabled"
> {
searchMentionCallback: (payload: TSearchEntityRequestPayload) => Promise;
workspaceSlug: string;
@@ -42,7 +42,7 @@ export const RichTextEditor = forwardRef await searchMentionCallback(payload),
@@ -53,7 +53,8 @@ export const RichTextEditor = forwardRef,
- "disabledExtensions"
+ Omit,
+ "disabledExtensions" | "flaggedExtensions"
> & {
workspaceId: string;
workspaceSlug: string;
@@ -30,14 +30,15 @@ export const RichTextReadOnlyEditor = React.forwardRef {
+ extends Omit<
+ ILiteTextEditorProps,
+ "disabledExtensions" | "flaggedExtensions" | "fileHandler" | "isSmoothCursorEnabled" | "mentionHandler"
+ > {
workspaceSlug: string;
workspaceId: string;
projectId?: string;
@@ -49,7 +52,7 @@ export const StickyEditor = React.forwardRef
= observer((props) => {
searchEntity: handlers.fetchEntity,
});
// editor flaggings
- const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString(), storeType);
+ const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug?.toString(), storeType);
// page filters
const { fontSize, fontStyle, isFullWidth } = usePageFilters();
// derived values
@@ -289,7 +289,8 @@ export const PageEditorBody: React.FC = observer((props) => {
realtimeConfig={realtimeConfig}
serverHandler={serverHandler}
user={userConfig}
- disabledExtensions={disabledExtensions}
+ disabledExtensions={documentEditorExtensions.disabled}
+ flaggedExtensions={documentEditorExtensions.flagged}
aiHandler={{
menu: getAIMenu,
}}
diff --git a/web/core/components/pages/version/editor.tsx b/web/core/components/pages/version/editor.tsx
index 347ea4c577..ac72fb5afa 100644
--- a/web/core/components/pages/version/editor.tsx
+++ b/web/core/components/pages/version/editor.tsx
@@ -38,7 +38,7 @@ export const PagesVersionEditor: React.FC = observer((props
// derived values
const workspaceDetails = getWorkspaceBySlug(workspaceSlug?.toString() ?? "");
// editor flaggings
- const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? "");
+ const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? "");
// editor config
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
// issue-embed
@@ -111,7 +111,8 @@ export const PagesVersionEditor: React.FC = observer((props
id={activeVersion ?? ""}
initialValue={description ?? ""}
containerClassName="p-0 pb-64 border-none"
- disabledExtensions={disabledExtensions}
+ disabledExtensions={documentEditorExtensions.disabled}
+ flaggedExtensions={documentEditorExtensions.flagged}
displayConfig={displayConfig}
editorClassName="pl-10"
fileHandler={getReadOnlyEditorFileHandlers({
diff --git a/web/ee/components/common/input/description-input.tsx b/web/ee/components/common/input/description-input.tsx
index 4ec4cb9e33..d71dcf75ec 100644
--- a/web/ee/components/common/input/description-input.tsx
+++ b/web/ee/components/common/input/description-input.tsx
@@ -5,15 +5,15 @@ import debounce from "lodash/debounce";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
// plane imports
-import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
+import type { EditorReadOnlyRefApi, EditorRefApi, TExtensions } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { TNameDescriptionLoader } from "@plane/types";
import { EFileAssetType } from "@plane/types/src/enums";
import { Loader } from "@plane/ui";
+import { getDescriptionPlaceholderI18n } from "@plane/utils";
// components
import { RichTextEditor, RichTextReadOnlyEditor } from "@/components/editor";
// helpers
-import { getDescriptionPlaceholderI18n } from "@plane/utils";
// hooks
import { useEditorAsset, useWorkspace } from "@/hooks/store";
// plane web services
@@ -26,6 +26,7 @@ interface IFormData {
}
export type DescriptionInputProps = {
+ disabledExtensions?: TExtensions[];
editorReadOnlyRef?: React.RefObject;
editorRef?: React.RefObject;
workspaceSlug: string;
@@ -56,6 +57,7 @@ export const DescriptionInput: FC = observer((props) => {
swrDescription,
containerClassName,
disabled,
+ disabledExtensions,
} = props;
// states
const [localDescription, setLocalDescription] = useState({
@@ -117,6 +119,7 @@ export const DescriptionInput: FC = observer((props) => {
"}
value={swrDescription ?? null}
workspaceSlug={workspaceSlug}
@@ -161,6 +164,7 @@ export const DescriptionInput: FC = observer((props) => {
= (props) => {
})
}
containerClassName="pt-3 min-h-[120px] border-[0.5px] border-custom-border-200 rounded-lg relative focus:ring-1 focus:ring-custom-primary"
+ disabledExtensions={["attachments"]}
uploadFile={async (blockId, file) => {
try {
const { asset_id } = await uploadEditorAsset({
diff --git a/web/ee/components/customers/detail/main.tsx b/web/ee/components/customers/detail/main.tsx
index d73ecc874f..759bae4076 100644
--- a/web/ee/components/customers/detail/main.tsx
+++ b/web/ee/components/customers/detail/main.tsx
@@ -168,6 +168,7 @@ export const CustomerMainRoot: FC = observer((props) => {
setIsSubmitting={setIsSubmitting}
containerClassName="border-none min-h-[88px]"
disabled={!isEditable}
+ disabledExtensions={["attachments"]}
/>
= observer((props) => {
onChange={(_description: object, description_html: string) => {
onChange(description_html);
}}
+ disabledExtensions={["attachments"]}
displayConfig={{ fontSize: "small-font" }}
placeholder={(isFocused, description) => t(getDescriptionPlaceholderI18n(isFocused, description))}
searchMentionCallback={async (payload) =>
diff --git a/web/ee/components/pages/embed/preview.tsx b/web/ee/components/pages/embed/preview.tsx
index 89c8f7a3f8..c0d87dc9a8 100644
--- a/web/ee/components/pages/embed/preview.tsx
+++ b/web/ee/components/pages/embed/preview.tsx
@@ -33,7 +33,7 @@ export const PageEmbedPreview: React.FC = observer((props) => {
const { workspaceSlug, projectId } = useParams();
const { fetchPageDetails } = usePageStore(storeType);
// editor flaggings
- const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? "");
+ const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? "");
// issue-embed
const { issueEmbedProps } = useIssueEmbed({
workspaceSlug: workspaceSlug?.toString() ?? "",
@@ -168,7 +168,8 @@ export const PageEmbedPreview: React.FC = observer((props) => {
initialValue={description_html ?? ""}
containerClassName="p-0 pl-3 border-none"
editorClassName="p-2.5 text-xs"
- disabledExtensions={disabledExtensions}
+ disabledExtensions={documentEditorExtensions.disabled}
+ flaggedExtensions={documentEditorExtensions.flagged}
displayConfig={{
fontSize: "small-font",
}}
diff --git a/web/ee/components/pages/version/editor.tsx b/web/ee/components/pages/version/editor.tsx
index 523571abde..57063ea8e5 100644
--- a/web/ee/components/pages/version/editor.tsx
+++ b/web/ee/components/pages/version/editor.tsx
@@ -27,7 +27,7 @@ export const WorkspacePagesVersionEditor: React.FC = observ
// editor config
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
// editor flagging
- const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? "");
+ const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? "");
// page filters
const { fontSize, fontStyle } = usePageFilters();
@@ -86,7 +86,8 @@ export const WorkspacePagesVersionEditor: React.FC = observ
id={activeVersion ?? ""}
initialValue={description ?? ""}
containerClassName="p-0 pb-64 border-none"
- disabledExtensions={disabledExtensions}
+ disabledExtensions={documentEditorExtensions.disabled}
+ flaggedExtensions={documentEditorExtensions.flagged}
displayConfig={displayConfig}
editorClassName="pl-10"
fileHandler={getReadOnlyEditorFileHandlers({
diff --git a/web/ee/components/templates/settings/page/form/editor.tsx b/web/ee/components/templates/settings/page/form/editor.tsx
index 416b420986..1c613f2384 100644
--- a/web/ee/components/templates/settings/page/form/editor.tsx
+++ b/web/ee/components/templates/settings/page/form/editor.tsx
@@ -36,10 +36,10 @@ export const PageTemplateEditor = observer((props: Props) => {
const { getWorkspaceBySlug } = useWorkspace();
const { uploadEditorAsset } = useEditorAsset();
const { getEditorFileHandlers } = useEditorConfig();
- const { documentEditor: documentEditorDisabledExtensions } = useEditorFlagging(workspaceSlug);
+ const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug);
// derived values
const additionalDisabledExtensions: TExtensions[] = ["issue-embed"];
- const disabledExtensions = [...documentEditorDisabledExtensions, ...additionalDisabledExtensions];
+ const disabledExtensions = [...documentEditorExtensions.disabled, ...additionalDisabledExtensions];
const workspaceId = useMemo(
() => (workspaceSlug ? (getWorkspaceBySlug(workspaceSlug)?.id ?? "") : ""),
[getWorkspaceBySlug, workspaceSlug]
@@ -97,6 +97,7 @@ export const PageTemplateEditor = observer((props: Props) => {
return (
{
- const isIssueEmbedEnabled = useFlag(workspaceSlug, "PAGE_ISSUE_EMBEDS");
+export const useEditorFlagging = (workspaceSlug: string, storeType?: EPageStoreType): TEditorFlaggingHookReturnType => {
+ const isWorkItemEmbedEnabled = useFlag(workspaceSlug, "PAGE_ISSUE_EMBEDS");
const isEditorAIOpsEnabled = useFlag(workspaceSlug, "EDITOR_AI_OPS");
const isCollaborationCursorEnabled = useFlag(workspaceSlug, "COLLABORATION_CURSOR");
- // extensions disabled in the document editor
- const documentEditor: TExtensions[] = [];
- if (!isIssueEmbedEnabled) documentEditor.push("issue-embed");
- if (!isEditorAIOpsEnabled) documentEditor.push("ai");
- if (!isCollaborationCursorEnabled) documentEditor.push("collaboration-cursor");
-
const { isNestedPagesEnabled } = usePageStore(storeType || EPageStoreType.WORKSPACE);
+ const isEditorAttachmentsEnabled = useFlag(workspaceSlug, "EDITOR_ATTACHMENTS");
+ // disabled and flagged in the document editor
+ const documentDisabled: TExtensions[] = [];
+ const documentFlagged: TExtensions[] = [];
+ // disabled and flagged in the rich text editor
+ const richTextDisabled: TExtensions[] = [];
+ const richTextFlagged: TExtensions[] = [];
+ if (!isWorkItemEmbedEnabled) {
+ documentFlagged.push("issue-embed");
+ }
+ if (!isEditorAIOpsEnabled) {
+ documentDisabled.push("ai");
+ }
+ if (!isCollaborationCursorEnabled) {
+ documentDisabled.push("collaboration-cursor");
+ }
if (storeType && !isNestedPagesEnabled(workspaceSlug)) {
- documentEditor.push("nested-pages");
+ documentFlagged.push("nested-pages");
+ }
+ if (!isEditorAttachmentsEnabled) {
+ documentFlagged.push("attachments");
+ richTextFlagged.push("attachments");
}
return {
- documentEditor,
- liteTextEditor: [],
- richTextEditor: [],
+ document: {
+ disabled: documentDisabled,
+ flagged: documentFlagged,
+ },
+ liteText: {
+ disabled: [],
+ flagged: [],
+ },
+ richText: {
+ disabled: richTextDisabled,
+ flagged: richTextFlagged,
+ },
};
};