[WIKI-181] feat: editor attachments (#2238)

* feat: attachment extension

* chore: slash command option added for attachments

* chore: add upload logic

* chore: add missing plugins to attachments extension

* refactor: file handling

* fix: attachment upload

* fix: attachment upload

* restore unnecessary files

* chore: added more types for work item and pages

* refactor: attachment extension

* chore: added conditions for custom entities

* chore: add upload status

* refactor: onCreate and upload logic

* fix: extension without props

* fix: restore deleted assets logic

* refactor: attachment extension types and utils

* chore: add editor extensions and translations

* chore: add disabled state and translations

* chore: update container elements

* fix: pageEmbedComponent correct node name

* chore: add missing translations

* refactor: remove read only extension

* chore: handle api call error

* fix: lint error

* fix: drop handlerr

* chore: extension in the read only document editor

* chore: add support for initiative and project overview

* fix: liver server runtime error

* refactor: editor props structure

* chore: add missing prop

* chore: update editor flag hook

* fix: space app build

* fix: customers attachment extension

* fix: customer requests description

* refactor: additional extension types

* chore: remove unnecessary comments

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
This commit is contained in:
Aaryan Khandelwal
2025-06-19 17:22:24 +05:30
committed by GitHub
parent bad6e0ba95
commit 28edf03a94
97 changed files with 1947 additions and 484 deletions

View File

@@ -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,
)

View File

@@ -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",
}

View File

@@ -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 [];
};

View File

@@ -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 [];
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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<IEditorProps, "disabledExtensions" | "flaggedExtensions">;
export const coreEditorAdditionalSlashCommandOptions = (props: Props): TSlashCommandAdditionalOption[] => {
const {} = props;

View File

@@ -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<ICollaborativeDocumentEditorProps> = (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<EditorRefApi, ICollaborativeDocumentEditor>(
const CollaborativeDocumentEditorWithRef = React.forwardRef<EditorRefApi, ICollaborativeDocumentEditorProps>(
(props, ref) => (
<CollaborativeDocumentEditor {...props} forwardedRef={ref as React.MutableRefObject<EditorRefApi | null>} />
)

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<EditorReadOnlyRefApi | null>;
}
const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
const DocumentReadOnlyEditor: React.FC<IDocumentReadOnlyEditorProps> = (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<EditorReadOnlyRefApi, IDocumentReadOnlyEditor>((props, ref) => (
const DocumentReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IDocumentReadOnlyEditorProps>((props, ref) => (
<DocumentReadOnlyEditor {...props} forwardedRef={ref as MutableRefObject<EditorReadOnlyRefApi | null>} />
));

View File

@@ -27,6 +27,7 @@ export const EditorWrapper: React.FC<Props> = (props) => {
id,
initialValue,
fileHandler,
flaggedExtensions,
forwardedRef,
handleEditorReady,
isSmoothCursorEnabled,
@@ -46,6 +47,7 @@ export const EditorWrapper: React.FC<Props> = (props) => {
enableHistory: true,
extensions,
fileHandler,
flaggedExtensions,
forwardedRef,
handleEditorReady,
isSmoothCursorEnabled,

View File

@@ -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<ILiteTextEditorProps> = (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 <EditorWrapper {...props} extensions={extensions} isSmoothCursorEnabled={isSmoothCursorEnabled} />;
};
const LiteTextEditorWithRef = forwardRef<EditorRefApi, ILiteTextEditor>((props, ref) => (
const LiteTextEditorWithRef = forwardRef<EditorRefApi, ILiteTextEditorProps>((props, ref) => (
<LiteTextEditor {...props} forwardedRef={ref as React.MutableRefObject<EditorRefApi | null>} />
));

View File

@@ -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<EditorReadOnlyRefApi, ILiteTextReadOnlyEditor>((props, ref) => (
const LiteTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, ILiteTextReadOnlyEditorProps>((props, ref) => (
<ReadOnlyEditorWrapper {...props} forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>} />
));

View File

@@ -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,

View File

@@ -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<IRichTextEditorProps> = (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 (
<EditorWrapper {...props} extensions={getExtensions()} isSmoothCursorEnabled={isSmoothCursorEnabled}>
@@ -42,7 +44,7 @@ const RichTextEditor = (props: IRichTextEditor) => {
);
};
const RichTextEditorWithRef = forwardRef<EditorRefApi, IRichTextEditor>((props, ref) => (
const RichTextEditorWithRef = forwardRef<EditorRefApi, IRichTextEditorProps>((props, ref) => (
<RichTextEditor {...props} forwardedRef={ref as React.MutableRefObject<EditorRefApi | null>} />
));

View File

@@ -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<EditorReadOnlyRefApi, IRichTextReadOnlyEditor>((props, ref) => {
const { disabledExtensions, fileHandler } = props;
const RichTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IRichTextReadOnlyEditorProps>((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 (
<ReadOnlyEditorWrapper

View File

@@ -38,21 +38,29 @@ import { getExtensionStorage } from "@/helpers/get-extension-storage";
// plane editor extensions
import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions";
// types
import { TExtensions, TFileHandler, TMentionHandler } from "@/types";
import type { IEditorProps } from "@/types";
type TArguments = {
disabledExtensions: TExtensions[];
type TArguments = Pick<
IEditorProps,
"disabledExtensions" | "flaggedExtensions" | "fileHandler" | "mentionHandler" | "placeholder" | "tabIndex"
> & {
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,
}),
];

View File

@@ -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<IReadOnlyEditorProps, "disabledExtensions" | "flaggedExtensions" | "fileHandler" | "mentionHandler">;
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,
}),
];

View File

@@ -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];

View File

@@ -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<SlashCommandOptions>({
},
});
export type TExtensionProps = {
export type TExtensionProps = Pick<IEditorProps, "disabledExtensions" | "flaggedExtensions"> & {
additionalOptions?: TSlashCommandAdditionalOption[];
disabledExtensions?: TExtensions[];
};
export const SlashCommands = (props: TExtensionProps) =>

View File

@@ -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<IEditorProps, "disabledExtensions"> & {
fileHandler: TFileHandler | TReadOnlyFileHandler;
isEditable: boolean;
};

View File

@@ -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 = {

View File

@@ -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,

View File

@@ -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<EditorRefApi | null>;
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,

View File

@@ -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<EditorReadOnlyRefApi | null>;
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,
],

View File

@@ -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();

View File

@@ -21,6 +21,7 @@ const generalSelectors = [
".image-component",
".image-upload-component",
".editor-callout-component",
".editor-attachment-component",
".page-embed-component",
].join(", ");

View File

@@ -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);

View File

@@ -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<EditorRefApi | null>;
mentionHandler: TMentionHandler;
onChange?: (description_json: object, description_html: string) => void;
placeholder?: string | ((isFocused: boolean, value: string) => string);
tabIndex?: number;
titleRef?: React.MutableRefObject<EditorTitleRefApi | null>;
updatePageProperties?: (pageId: string, messageType: string, payload?: any, performAction?: boolean) => void;
};
export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & {
fileHandler: TReadOnlyFileHandler;
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
mentionHandler: TReadOnlyMentionHandler;
};

View File

@@ -1,3 +1,6 @@
// plane imports
import { TWebhookConnectionQueryParams } from "@plane/types";
export type TReadOnlyFileHandler = {
checkIfAssetExists: (assetId: string) => Promise<boolean>;
getAssetSrc: (path: string) => Promise<string>;
@@ -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;
};

View File

@@ -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<EditorRefApi | null>;
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<IEditorProps, "initialValue" | "onEnterKeyPress" | "value"> {
export interface ICollaborativeDocumentEditorProps
extends Omit<IEditorProps, "extensions" | "initialValue" | "onEnterKeyPress" | "value"> {
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<IEditorProps, "initia
export interface IDocumentEditor extends Omit<IEditorProps, "onEnterKeyPress" | "value"> {
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<EditorReadOnlyRefApi | null>;
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<IReadOnlyEditorProps, "initialValue"> {
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;

View File

@@ -6,4 +6,5 @@ export type TExtensions =
| "slash-commands"
| "enter-key"
| "image"
| "nested-pages";
| "nested-pages"
| "attachments";

View File

@@ -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<ICollaborativeDocumentEditorProps, "embedHandler" | "realtimeConfig" | "serverHandler" | "user"> & {
titleRef?: React.MutableRefObject<EditorTitleRefApi | null>;
updatePageProperties?: (pageId: string, messageType: string, payload?: any, performAction?: boolean) => void;
};
export type TReadOnlyEditorHookProps = Omit<TCoreHookProps, "isSmoothCursorEnabled"> &
Pick<TEditorHookProps, "initialValue" | "provider"> &
Pick<IReadOnlyEditorProps, "fileHandler" | "forwardedRef" | "mentionHandler">;

View File

@@ -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";

View File

@@ -1,3 +1,4 @@
export enum ADDITIONAL_EXTENSIONS {
PAGE_EMBED_COMPONENT = "page-embed-component",
}
PAGE_EMBED_COMPONENT = "pageEmbedComponent",
ATTACHMENT = "attachmentComponent",
}

View File

@@ -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",
},
};

View File

@@ -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> = (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 (
<a
href={resolvedSource}
className="py-3 px-2 rounded-lg bg-custom-background-90 hover:bg-custom-background-80 border border-custom-border-300 flex items-start gap-2 transition-colors"
contentEditable={false}
target="_blank"
rel="noopener noreferrer"
>
<div className="flex-shrink-0 mt-1 size-8 grid place-items-center">
<File className="flex-shrink-0 size-8 text-custom-text-300" />
</div>
<div className="truncate">
<p className="not-prose text-sm truncate">{node.attrs[EAttachmentBlockAttributeNames.FILE_NAME]}</p>
<p className="not-prose text-xs text-custom-text-300">
{convertBytesToSize(Number(node.attrs[EAttachmentBlockAttributeNames.FILE_SIZE] || 0))}
</p>
</div>
</a>
);
};

View File

@@ -0,0 +1,19 @@
import { FileKey2 } from "lucide-react";
export const CustomAttachmentFlaggedState = () => (
<a
href="https://plane.so/pro"
className="py-3 px-2 rounded-lg bg-custom-background-90 hover:bg-custom-background-80 border border-custom-border-300 flex items-start gap-2 transition-colors"
contentEditable={false}
target="_blank"
rel="noopener noreferrer"
>
<span className="flex-shrink-0 mt-0.5 size-4 grid place-items-center">
<FileKey2 className="size-4" />
</span>
<p className="not-prose text-sm">
{/* {t("attachmentComponent.upgrade.description")} */}
Upgrade your plan to view this attachment.
</p>
</a>
);

View File

@@ -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<NodeViewProps, "extension"> & {
extension: AttachmentExtension;
node: NodeViewProps["node"] & {
attrs: TAttachmentBlockAttributes;
};
updateAttributes: (attrs: Partial<TAttachmentBlockAttributes>) => void;
};
export const CustomAttachmentNodeView: React.FC<CustomAttachmentNodeViewProps> = (props) => {
const { extension, node } = props;
// states
const [resolvedSource, setResolvedSource] = useState<string | null>(null);
// refs
const attachmentComponentRef = useRef<HTMLDivElement>(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 (
<NodeViewWrapper className="editor-attachment-component">
{isExtensionFlagged ? (
<div className="p-0 mx-0 py-2 not-prose">
<CustomAttachmentFlaggedState />
</div>
) : (
<div className="p-0 mx-0 py-2 not-prose" ref={attachmentComponentRef} contentEditable={false}>
{isAttachmentUploaded ? (
<>{resolvedSource && <CustomAttachmentBlock {...props} resolvedSource={resolvedSource} />}</>
) : (
<CustomAttachmentUploader {...props} />
)}
</div>
)}
</NodeViewWrapper>
);
};

View File

@@ -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> = (props) => {
const { editor, nodeId } = props;
// Displayed status that will animate smoothly
const [displayStatus, setDisplayStatus] = useState(0);
// Animation frame ID for cleanup
const animationFrameRef = useRef<number | null>(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}%</>;
};

View File

@@ -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> = (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 = (
<a
href="https://docs.plane.so/core-concepts/issues/overview#supported-file-types"
target="_blank"
rel="noopener noreferrer"
className="text-custom-primary-100 hover:underline"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{/* {t("attachmentComponent.errors.unsupported_file_type.description")} */}
See supported formats
</a>
);
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 (
<div className="truncate">
<p
className={cn("not-prose text-sm truncate", {
"text-red-500": !fileBeingUploaded && fileUploadError,
})}
>
{fileBeingUploaded
? fileBeingUploaded?.name
: fileUploadError
? `${fileUploadError.file.name}- ${errorMessage.title}`
: // : t("attachmentComponent.uploader.drag_and_drop")
"Drop files here or click to upload"}
</p>
<p className="not-prose text-xs text-custom-text-300">
{fileBeingUploaded ? (
<CustomAttachmentUploadStatus editor={editor} nodeId={blockId} />
) : fileUploadError ? (
errorMessage.description
) : (
// t("attachmentComponent.errors.file_too_large.description", {
// maxFileSize: maxFileSize / 1024 / 1024,
// })
`Maximum size per file is ${maxFileSize / 1024 / 1024}MB`
)}
</p>
</div>
);
};

View File

@@ -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<CustomAttachmentNodeViewProps> = (props) => {
const { editor, extension, getPos, node, updateAttributes } = props;
// states
const [fileBeingUploaded, setFileBeingUploaded] = useState<File | null>(null);
// refs
const fileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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 (
<div
className={cn(
"py-3 px-2 rounded-lg bg-custom-background-90 border border-dashed border-custom-border-300 flex items-center gap-2 transition-colors cursor-default",
{
"hover:bg-custom-background-80 cursor-pointer": editor.isEditable,
"bg-custom-background-80": editor.isEditable && draggedInside,
}
)}
onDrop={onDrop}
onDragOver={onDragEnter}
onDragLeave={onDragLeave}
contentEditable={false}
onClick={() => {
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}
>
<div className="flex-shrink-0 mt-1 size-8 grid place-items-center">
<FileUp className="flex-shrink-0 size-8 text-custom-text-300" />
</div>
<CustomAttachmentUploaderDetails
blockId={attachmentBlockId ?? ""}
editor={editor}
fileBeingUploaded={fileBeingUploaded}
maxFileSize={maxFileSize}
/>
<input
className="size-0 overflow-hidden"
ref={fileInputRef}
hidden
type="file"
accept={ACCEPTED_ATTACHMENT_MIME_TYPES.join(",")}
onChange={handleFileChange}
multiple
/>
</div>
);
};

View File

@@ -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<ReturnType> {
[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)];
},
});

View File

@@ -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);
},
});
};

View File

@@ -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<string, boolean>;
fileMap: Map<string, UploadEntity>;
errorMap: Map<string, { error: EFileError; file: File }>;
maxFileSize: number;
};
export type AttachmentExtension = Node<AttachmentExtensionOptions, AttachmentExtensionStorage>;

View File

@@ -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;

View File

@@ -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 [];
};

View File

@@ -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 [];
};

View File

@@ -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];

View File

@@ -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: <Paperclip className="size-3.5" />,
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;
};

View File

@@ -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: <Paperclip className="size-3.5" />,
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;
};

View File

@@ -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;
};

View File

@@ -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();
};

View File

@@ -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<keyof AttachmentExtensionStorage, "deletedAttachmentSet">;

View File

@@ -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";

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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": "クリックして添付ファイルをアップロード"
}
}
}

View File

@@ -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": "첨부 파일을 업로드하려면 클릭하세요"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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": "Нажмите для загрузки вложения"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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": "Натисніть для завантаження вкладення"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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": "点击上传附件"
}
}
}

View File

@@ -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": "點擊上傳附件"
}
}
}

View File

@@ -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<ILiteTextEditor, "fileHandler" | "mentionHandler" | "isSmoothCursorEnabled">,
"disabledExtensions"
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler" | "isSmoothCursorEnabled">,
"disabledExtensions" | "flaggedExtensions"
> {
anchor: string;
workspaceId: string;
@@ -30,6 +30,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
showSubmitButton = true,
uploadFile,
disabledExtensions,
flaggedExtensions,
...rest
} = props;
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
@@ -44,6 +45,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
<LiteTextEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions ?? []}
flaggedExtensions={flaggedExtensions ?? []}
fileHandler={getEditorFileHandlers({
anchor,
uploadFile,

View File

@@ -1,6 +1,6 @@
import React from "react";
// plane imports
import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditor, LiteTextReadOnlyEditorWithRef } from "@plane/editor";
import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditorProps, LiteTextReadOnlyEditorWithRef } from "@plane/editor";
import { MakeOptional } from "@plane/types";
import { cn } from "@plane/utils";
// components
@@ -11,21 +11,22 @@ import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
import { useMember } from "@/hooks/store";
type LiteTextReadOnlyEditorWrapperProps = MakeOptional<
Omit<ILiteTextReadOnlyEditor, "fileHandler" | "mentionHandler">,
"disabledExtensions"
Omit<ILiteTextReadOnlyEditorProps, "fileHandler" | "mentionHandler">,
"disabledExtensions" | "flaggedExtensions"
> & {
anchor: string;
workspaceId: string;
};
export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
({ anchor, workspaceId, disabledExtensions, ...props }, ref) => {
({ anchor, workspaceId, disabledExtensions, flaggedExtensions, ...props }, ref) => {
const { getMemberById } = useMember();
return (
<LiteTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions ?? []}
flaggedExtensions={flaggedExtensions ?? []}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
workspaceId,

View File

@@ -1,6 +1,6 @@
import React, { forwardRef } from "react";
// plane imports
import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef, TFileHandler } from "@plane/editor";
import { EditorRefApi, IRichTextEditorProps, RichTextEditorWithRef, TFileHandler } from "@plane/editor";
import { MakeOptional } from "@plane/types";
// components
import { EditorMentionsRoot } from "@/components/editor";
@@ -11,8 +11,8 @@ import { useMember } from "@/hooks/store";
interface RichTextEditorWrapperProps
extends MakeOptional<
Omit<IRichTextEditor, "fileHandler" | "mentionHandler" | "isSmoothCursorEnabled">,
"disabledExtensions"
Omit<IRichTextEditorProps, "fileHandler" | "mentionHandler" | "isSmoothCursorEnabled">,
"disabledExtensions" | "flaggedExtensions"
> {
anchor: string;
uploadFile: TFileHandler["upload"];
@@ -20,7 +20,7 @@ interface RichTextEditorWrapperProps
}
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
const { anchor, containerClassName, uploadFile, workspaceId, disabledExtensions, ...rest } = props;
const { anchor, containerClassName, uploadFile, workspaceId, disabledExtensions, flaggedExtensions, ...rest } = props;
const { getMemberById } = useMember();
return (
<RichTextEditorWithRef
@@ -37,6 +37,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
uploadFile,
workspaceId,
})}
flaggedExtensions={flaggedExtensions ?? []}
{...rest}
containerClassName={containerClassName}
editorClassName="min-h-[100px] max-h-[200px] border-[0.5px] border-custom-border-300 rounded-md pl-3 py-2 overflow-hidden"

View File

@@ -1,6 +1,6 @@
import React from "react";
// plane imports
import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor, RichTextReadOnlyEditorWithRef } from "@plane/editor";
import { EditorReadOnlyRefApi, IRichTextReadOnlyEditorProps, RichTextReadOnlyEditorWithRef } from "@plane/editor";
import { MakeOptional } from "@plane/types";
import { cn } from "@plane/utils";
// components
@@ -11,21 +11,22 @@ import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
import { useMember } from "@/hooks/store";
type RichTextReadOnlyEditorWrapperProps = MakeOptional<
Omit<IRichTextReadOnlyEditor, "fileHandler" | "mentionHandler">,
"disabledExtensions"
Omit<IRichTextReadOnlyEditorProps, "fileHandler" | "mentionHandler">,
"disabledExtensions" | "flaggedExtensions"
> & {
anchor: string;
workspaceId: string;
};
export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
({ anchor, workspaceId, disabledExtensions, ...props }, ref) => {
({ anchor, workspaceId, disabledExtensions, flaggedExtensions, ...props }, ref) => {
const { getMemberById } = useMember();
return (
<RichTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions ?? []}
flaggedExtensions={flaggedExtensions ?? []}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
workspaceId,

View File

@@ -70,6 +70,7 @@ export const PageDetailsMainContent: React.FC<Props> = observer((props) => {
ref={editorRef}
id={pageDetails.id}
disabledExtensions={[]}
flaggedExtensions={[]}
initialValue={pageDetails.description_html ?? "<p></p>"}
containerClassName="p-0 pb-64 border-none"
fileHandler={getReadOnlyEditorFileHandlers({

View File

@@ -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: [],
},
});

View File

@@ -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<ILiteTextEditor, "fileHandler" | "mentionHandler" | "isSmoothCursorEnabled">,
"disabledExtensions"
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler">,
"disabledExtensions" | "flaggedExtensions" | "isSmoothCursorEnabled"
> {
workspaceSlug: string;
workspaceId: string;
@@ -58,13 +58,13 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
parentClassName = "",
placeholder = t("issue.comments.placeholder"),
uploadFile,
disabledExtensions: additionalDisabledExtensions,
disabledExtensions: additionalDisabledExtensions = [],
...rest
} = props;
// states
const [isFocused, setIsFocused] = useState(showToolbarInitially);
// editor flaggings
const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
const { liteText: liteTextEditorExtensions } = useEditorFlagging(workspaceSlug?.toString());
// store hooks
const { getUserDetails } = useMember();
// use editor mention
@@ -96,7 +96,8 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
>
<LiteTextEditorWithRef
ref={ref}
disabledExtensions={[...disabledExtensions, ...(additionalDisabledExtensions ?? [])]}
disabledExtensions={[...liteTextEditorExtensions.disabled, ...additionalDisabledExtensions]}
flaggedExtensions={liteTextEditorExtensions.flagged}
fileHandler={getEditorFileHandlers({
projectId,
uploadFile,

View File

@@ -1,6 +1,6 @@
import React from "react";
// plane imports
import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditor, LiteTextReadOnlyEditorWithRef } from "@plane/editor";
import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditorProps, LiteTextReadOnlyEditorWithRef } from "@plane/editor";
import { MakeOptional } from "@plane/types";
// components
import { cn } from "@plane/utils";
@@ -14,8 +14,8 @@ import { useMember } from "@/hooks/store";
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
type LiteTextReadOnlyEditorWrapperProps = MakeOptional<
Omit<ILiteTextReadOnlyEditor, "fileHandler" | "mentionHandler">,
"disabledExtensions"
Omit<ILiteTextReadOnlyEditorProps, "fileHandler" | "mentionHandler">,
"disabledExtensions" | "flaggedExtensions"
> & {
workspaceId: string;
workspaceSlug: string;
@@ -28,14 +28,15 @@ export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Lit
const { getUserDetails } = useMember();
// editor flaggings
const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
const { liteText: liteTextEditorExtensions } = useEditorFlagging(workspaceSlug?.toString());
// editor config
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
return (
<LiteTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={[...disabledExtensions, ...(additionalDisabledExtensions ?? [])]}
disabledExtensions={[...liteTextEditorExtensions.disabled, ...(additionalDisabledExtensions ?? [])]}
flaggedExtensions={liteTextEditorExtensions.flagged}
fileHandler={getReadOnlyEditorFileHandlers({
projectId,
workspaceId,

View File

@@ -1,6 +1,6 @@
import React, { forwardRef } from "react";
// plane imports
import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef, TFileHandler } from "@plane/editor";
import { EditorRefApi, IRichTextEditorProps, RichTextEditorWithRef, TFileHandler } from "@plane/editor";
import { MakeOptional, TSearchEntityRequestPayload, TSearchResponse } from "@plane/types";
// components
import { cn } from "@plane/utils";
@@ -15,8 +15,8 @@ import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
interface RichTextEditorWrapperProps
extends MakeOptional<
Omit<IRichTextEditor, "fileHandler" | "mentionHandler" | "isSmoothCursorEnabled">,
"disabledExtensions"
Omit<IRichTextEditorProps, "fileHandler" | "mentionHandler">,
"disabledExtensions" | "flaggedExtensions" | "isSmoothCursorEnabled"
> {
searchMentionCallback: (payload: TSearchEntityRequestPayload) => Promise<TSearchResponse>;
workspaceSlug: string;
@@ -42,7 +42,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
data: { is_smooth_cursor_enabled },
} = useUserProfile();
// editor flaggings
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
const { richText: richTextEditorExtensions } = useEditorFlagging(workspaceSlug?.toString());
// use editor mention
const { fetchMentions } = useEditorMention({
searchEntity: async (payload) => await searchMentionCallback(payload),
@@ -53,7 +53,8 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
return (
<RichTextEditorWithRef
ref={ref}
disabledExtensions={[...disabledExtensions, ...(additionalDisabledExtensions ?? [])]}
disabledExtensions={[...richTextEditorExtensions.disabled, ...(additionalDisabledExtensions ?? [])]}
flaggedExtensions={richTextEditorExtensions.flagged}
fileHandler={getEditorFileHandlers({
projectId,
uploadFile,

View File

@@ -2,7 +2,7 @@
import React from "react";
// plane imports
import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor, RichTextReadOnlyEditorWithRef } from "@plane/editor";
import { EditorReadOnlyRefApi, IRichTextReadOnlyEditorProps, RichTextReadOnlyEditorWithRef } from "@plane/editor";
import { MakeOptional } from "@plane/types";
// components
import { cn } from "@plane/utils";
@@ -16,8 +16,8 @@ import { useMember } from "@/hooks/store";
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
type RichTextReadOnlyEditorWrapperProps = MakeOptional<
Omit<IRichTextReadOnlyEditor, "fileHandler" | "mentionHandler">,
"disabledExtensions"
Omit<IRichTextReadOnlyEditorProps, "fileHandler" | "mentionHandler">,
"disabledExtensions" | "flaggedExtensions"
> & {
workspaceId: string;
workspaceSlug: string;
@@ -30,14 +30,15 @@ export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Ric
const { getUserDetails } = useMember();
// editor flaggings
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
const { richText: richTextEditorExtensions } = useEditorFlagging(workspaceSlug?.toString());
// editor config
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
return (
<RichTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={[...disabledExtensions, ...(additionalDisabledExtensions ?? [])]}
disabledExtensions={[...richTextEditorExtensions.disabled, ...(additionalDisabledExtensions ?? [])]}
flaggedExtensions={richTextEditorExtensions.flagged}
fileHandler={getReadOnlyEditorFileHandlers({
projectId,
workspaceId,

View File

@@ -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";
// components
import { TSticky } from "@plane/types";
// helpers
@@ -15,7 +15,10 @@ import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { StickyEditorToolbar } from "./toolbar";
interface StickyEditorWrapperProps
extends Omit<ILiteTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler" | "isSmoothCursorEnabled"> {
extends Omit<
ILiteTextEditorProps,
"disabledExtensions" | "flaggedExtensions" | "fileHandler" | "isSmoothCursorEnabled" | "mentionHandler"
> {
workspaceSlug: string;
workspaceId: string;
projectId?: string;
@@ -49,7 +52,7 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
// states
const [isFocused, setIsFocused] = useState(showToolbarInitially);
// editor flaggings
const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
const { liteText: liteTextEditorExtensions } = useEditorFlagging(workspaceSlug?.toString());
// store hooks
const {
data: { is_smooth_cursor_enabled },
@@ -70,7 +73,8 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
>
<LiteTextEditorWithRef
ref={ref}
disabledExtensions={[...disabledExtensions, "enter-key"]}
disabledExtensions={[...liteTextEditorExtensions.disabled, "enter-key"]}
flaggedExtensions={liteTextEditorExtensions.flagged}
fileHandler={getEditorFileHandlers({
projectId,
uploadFile,

View File

@@ -122,7 +122,7 @@ export const PageEditorBody: React.FC<Props> = 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<Props> = observer((props) => {
realtimeConfig={realtimeConfig}
serverHandler={serverHandler}
user={userConfig}
disabledExtensions={disabledExtensions}
disabledExtensions={documentEditorExtensions.disabled}
flaggedExtensions={documentEditorExtensions.flagged}
aiHandler={{
menu: getAIMenu,
}}

View File

@@ -38,7 +38,7 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = 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<TVersionEditorProps> = observer((props
id={activeVersion ?? ""}
initialValue={description ?? "<p></p>"}
containerClassName="p-0 pb-64 border-none"
disabledExtensions={disabledExtensions}
disabledExtensions={documentEditorExtensions.disabled}
flaggedExtensions={documentEditorExtensions.flagged}
displayConfig={displayConfig}
editorClassName="pl-10"
fileHandler={getReadOnlyEditorFileHandlers({

View File

@@ -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<EditorReadOnlyRefApi>;
editorRef?: React.RefObject<EditorRefApi>;
workspaceSlug: string;
@@ -56,6 +57,7 @@ export const DescriptionInput: FC<DescriptionInputProps> = observer((props) => {
swrDescription,
containerClassName,
disabled,
disabledExtensions,
} = props;
// states
const [localDescription, setLocalDescription] = useState({
@@ -117,6 +119,7 @@ export const DescriptionInput: FC<DescriptionInputProps> = observer((props) => {
<RichTextEditor
ref={editorRef}
id={itemId}
disabledExtensions={disabledExtensions}
initialValue={localDescription.description_html ?? "<p></p>"}
value={swrDescription ?? null}
workspaceSlug={workspaceSlug}
@@ -161,6 +164,7 @@ export const DescriptionInput: FC<DescriptionInputProps> = observer((props) => {
<RichTextReadOnlyEditor
ref={editorReadOnlyRef}
id={itemId}
disabledExtensions={disabledExtensions}
initialValue={localDescription.description_html ?? ""}
containerClassName={containerClassName}
workspaceId={workspaceDetails?.id ?? ""}

View File

@@ -226,6 +226,7 @@ export const DefaultProperties: FC<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({

View File

@@ -168,6 +168,7 @@ export const CustomerMainRoot: FC<TProps> = observer((props) => {
setIsSubmitting={setIsSubmitting}
containerClassName="border-none min-h-[88px]"
disabled={!isEditable}
disabledExtensions={["attachments"]}
/>
</div>
<Tabs

View File

@@ -8,10 +8,10 @@ import { CustomerService } from "@plane/services";
import { ISearchIssueResponse, TCustomerRequest, TProjectIssuesSearchParams } from "@plane/types";
import { EFileAssetType } from "@plane/types/src/enums";
import { Button, EModalPosition, EModalWidth, Input, LayersIcon, setToast, TOAST_TYPE, ModalCore } from "@plane/ui";
import { getDescriptionPlaceholderI18n } from "@plane/utils";
import { ExistingIssuesListModal } from "@/components/core";
// plane web imports
import { RichTextEditor } from "@/components/editor";
import { getDescriptionPlaceholderI18n } from "@plane/utils";
import { useEditorAsset, useWorkspace } from "@/hooks/store";
import { RequestAttachmentsList, SourceCreateUpdateModal, SourceItem } from "@/plane-web/components/customers";
import { useCustomers } from "@/plane-web/hooks/store";
@@ -220,6 +220,7 @@ export const CustomerRequestForm: FC<TProps> = 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) =>

View File

@@ -33,7 +33,7 @@ export const PageEmbedPreview: React.FC<Props> = 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<Props> = observer((props) => {
initialValue={description_html ?? "<p></p>"}
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",
}}

View File

@@ -27,7 +27,7 @@ export const WorkspacePagesVersionEditor: React.FC<TVersionEditorProps> = 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<TVersionEditorProps> = observ
id={activeVersion ?? ""}
initialValue={description ?? "<p></p>"}
containerClassName="p-0 pb-64 border-none"
disabledExtensions={disabledExtensions}
disabledExtensions={documentEditorExtensions.disabled}
flaggedExtensions={documentEditorExtensions.flagged}
displayConfig={displayConfig}
editorClassName="pl-10"
fileHandler={getReadOnlyEditorFileHandlers({

View File

@@ -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 (
<DocumentEditorWithRef
disabledExtensions={disabledExtensions}
flaggedExtensions={documentEditorExtensions.flagged}
fileHandler={fileHandler}
id="page-template-editor"
initialValue={initialValue}

View File

@@ -1,36 +1,54 @@
// editor
import { TExtensions } from "@plane/editor";
// plane imports
import type { TExtensions } from "@plane/editor";
// ce imports
import { TEditorFlaggingHookReturnType } from "@/ce/hooks/use-editor-flagging";
// plane web hooks
import { EPageStoreType, useFlag, usePageStore } from "@/plane-web/hooks/store";
/**
* @description extensions disabled in various editors
*/
export const useEditorFlagging = (
workspaceSlug: string,
storeType?: EPageStoreType
): {
documentEditor: TExtensions[];
liteTextEditor: TExtensions[];
richTextEditor: TExtensions[];
} => {
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,
},
};
};