mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 22:09:12 +02:00
[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:
committed by
GitHub
parent
bad6e0ba95
commit
28edf03a94
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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 [];
|
||||
};
|
||||
|
||||
@@ -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 [];
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>} />
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>} />
|
||||
));
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>} />
|
||||
));
|
||||
|
||||
|
||||
@@ -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>} />
|
||||
));
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>} />
|
||||
));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -21,6 +21,7 @@ const generalSelectors = [
|
||||
".image-component",
|
||||
".image-upload-component",
|
||||
".editor-callout-component",
|
||||
".editor-attachment-component",
|
||||
".page-embed-component",
|
||||
].join(", ");
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,4 +6,5 @@ export type TExtensions =
|
||||
| "slash-commands"
|
||||
| "enter-key"
|
||||
| "image"
|
||||
| "nested-pages";
|
||||
| "nested-pages"
|
||||
| "attachments";
|
||||
|
||||
63
packages/editor/src/core/types/hook.ts
Normal file
63
packages/editor/src/core/types/hook.ts
Normal 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">;
|
||||
@@ -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";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export enum ADDITIONAL_EXTENSIONS {
|
||||
PAGE_EMBED_COMPONENT = "page-embed-component",
|
||||
}
|
||||
PAGE_EMBED_COMPONENT = "pageEmbedComponent",
|
||||
ATTACHMENT = "attachmentComponent",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}%</>;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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)];
|
||||
},
|
||||
});
|
||||
113
packages/editor/src/ee/extensions/attachments/extension.ts
Normal file
113
packages/editor/src/ee/extensions/attachments/extension.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
};
|
||||
42
packages/editor/src/ee/extensions/attachments/types.ts
Normal file
42
packages/editor/src/ee/extensions/attachments/types.ts
Normal 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>;
|
||||
21
packages/editor/src/ee/extensions/attachments/utils.ts
Normal file
21
packages/editor/src/ee/extensions/attachments/utils.ts
Normal 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;
|
||||
@@ -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 [];
|
||||
};
|
||||
|
||||
@@ -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 [];
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
24
packages/editor/src/ee/helpers/editor-commands.ts
Normal file
24
packages/editor/src/ee/helpers/editor-commands.ts
Normal 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();
|
||||
};
|
||||
15
packages/editor/src/ee/types/storage.ts
Normal file
15
packages/editor/src/ee/types/storage.ts
Normal 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">;
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "クリックして添付ファイルをアップロード"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "첨부 파일을 업로드하려면 클릭하세요"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "Нажмите для загрузки вложения"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "Натисніть для завантаження вкладення"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "点击上传附件"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "點擊上傳附件"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 ?? ""}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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",
|
||||
}}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user