[WIKI-539] refactor: remove lite text read only editor (#7481)
* refactor: remove lite text read only editor * chore: update types
This commit is contained in:
parent
55f06cf546
commit
e0fa6553ae
45 changed files with 280 additions and 758 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
export * from "./embeds";
|
export * from "./embeds";
|
||||||
export * from "./lite-text-editor";
|
export * from "./lite-text-editor";
|
||||||
export * from "./lite-text-read-only-editor";
|
|
||||||
export * from "./rich-text-editor";
|
export * from "./rich-text-editor";
|
||||||
export * from "./toolbar";
|
export * from "./toolbar";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { EditorRefApi, ILiteTextEditorProps, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
|
import { type EditorRefApi, type ILiteTextEditorProps, LiteTextEditorWithRef, type TFileHandler } from "@plane/editor";
|
||||||
import { MakeOptional } from "@plane/types";
|
import type { MakeOptional } from "@plane/types";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
|
import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
|
||||||
|
|
@ -9,28 +9,34 @@ import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
|
||||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||||
import { isCommentEmpty } from "@/helpers/string.helper";
|
import { isCommentEmpty } from "@/helpers/string.helper";
|
||||||
|
|
||||||
interface LiteTextEditorWrapperProps
|
type LiteTextEditorWrapperProps = MakeOptional<
|
||||||
extends MakeOptional<
|
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler">,
|
||||||
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler">,
|
"disabledExtensions" | "flaggedExtensions"
|
||||||
"disabledExtensions" | "flaggedExtensions"
|
> & {
|
||||||
> {
|
|
||||||
anchor: string;
|
anchor: string;
|
||||||
workspaceId: string;
|
|
||||||
isSubmitting?: boolean;
|
isSubmitting?: boolean;
|
||||||
showSubmitButton?: boolean;
|
showSubmitButton?: boolean;
|
||||||
uploadFile: TFileHandler["upload"];
|
workspaceId: string;
|
||||||
}
|
} & (
|
||||||
|
| {
|
||||||
|
editable: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
editable: true;
|
||||||
|
uploadFile: TFileHandler["upload"];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapperProps>((props, ref) => {
|
export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapperProps>((props, ref) => {
|
||||||
const {
|
const {
|
||||||
anchor,
|
anchor,
|
||||||
containerClassName,
|
containerClassName,
|
||||||
workspaceId,
|
disabledExtensions,
|
||||||
|
editable,
|
||||||
|
flaggedExtensions,
|
||||||
isSubmitting = false,
|
isSubmitting = false,
|
||||||
showSubmitButton = true,
|
showSubmitButton = true,
|
||||||
uploadFile,
|
workspaceId,
|
||||||
disabledExtensions,
|
|
||||||
flaggedExtensions,
|
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
|
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
|
||||||
|
|
@ -46,9 +52,10 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
||||||
ref={ref}
|
ref={ref}
|
||||||
disabledExtensions={disabledExtensions ?? []}
|
disabledExtensions={disabledExtensions ?? []}
|
||||||
flaggedExtensions={flaggedExtensions ?? []}
|
flaggedExtensions={flaggedExtensions ?? []}
|
||||||
|
editable={editable}
|
||||||
fileHandler={getEditorFileHandlers({
|
fileHandler={getEditorFileHandlers({
|
||||||
anchor,
|
anchor,
|
||||||
uploadFile,
|
uploadFile: editable ? props.uploadFile : async () => "",
|
||||||
workspaceId,
|
workspaceId,
|
||||||
})}
|
})}
|
||||||
mentionHandler={{
|
mentionHandler={{
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
// plane imports
|
|
||||||
import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditorProps, LiteTextReadOnlyEditorWithRef } from "@plane/editor";
|
|
||||||
import { MakeOptional } from "@plane/types";
|
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
// components
|
|
||||||
import { EditorMentionsRoot } from "@/components/editor";
|
|
||||||
// helpers
|
|
||||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
|
||||||
// store hooks
|
|
||||||
import { useMember } from "@/hooks/store";
|
|
||||||
|
|
||||||
type LiteTextReadOnlyEditorWrapperProps = MakeOptional<
|
|
||||||
Omit<ILiteTextReadOnlyEditorProps, "fileHandler" | "mentionHandler">,
|
|
||||||
"disabledExtensions" | "flaggedExtensions"
|
|
||||||
> & {
|
|
||||||
anchor: string;
|
|
||||||
workspaceId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
|
|
||||||
({ anchor, workspaceId, disabledExtensions, flaggedExtensions, ...props }, ref) => {
|
|
||||||
const { getMemberById } = useMember();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LiteTextReadOnlyEditorWithRef
|
|
||||||
ref={ref}
|
|
||||||
disabledExtensions={disabledExtensions ?? []}
|
|
||||||
flaggedExtensions={flaggedExtensions ?? []}
|
|
||||||
fileHandler={getReadOnlyEditorFileHandlers({
|
|
||||||
anchor,
|
|
||||||
workspaceId,
|
|
||||||
})}
|
|
||||||
mentionHandler={{
|
|
||||||
renderComponent: (props) => <EditorMentionsRoot {...props} />,
|
|
||||||
getMentionedEntityDetails: (id: string) => ({
|
|
||||||
display_name: getMemberById(id)?.member__display_name ?? "",
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
// overriding the customClassName to add relative class passed
|
|
||||||
containerClassName={cn(props.containerClassName, "relative p-2")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
LiteTextReadOnlyEditor.displayName = "LiteTextReadOnlyEditor";
|
|
||||||
|
|
@ -75,6 +75,7 @@ export const AddComment: React.FC<Props> = observer((props) => {
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<LiteTextEditor
|
<LiteTextEditor
|
||||||
|
editable
|
||||||
onEnterKeyPress={(e) => {
|
onEnterKeyPress={(e) => {
|
||||||
if (currentUser) handleSubmit(onSubmit)(e);
|
if (currentUser) handleSubmit(onSubmit)(e);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { EditorRefApi } from "@plane/editor";
|
||||||
import { TIssuePublicComment } from "@plane/types";
|
import { TIssuePublicComment } from "@plane/types";
|
||||||
import { getFileURL } from "@plane/utils";
|
import { getFileURL } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { LiteTextEditor, LiteTextReadOnlyEditor } from "@/components/editor";
|
import { LiteTextEditor } from "@/components/editor";
|
||||||
import { CommentReactions } from "@/components/issues/peek-overview";
|
import { CommentReactions } from "@/components/issues/peek-overview";
|
||||||
// helpers
|
// helpers
|
||||||
import { timeAgo } from "@/helpers/date-time.helper";
|
import { timeAgo } from "@/helpers/date-time.helper";
|
||||||
|
|
@ -102,6 +102,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||||
name="comment_html"
|
name="comment_html"
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<LiteTextEditor
|
<LiteTextEditor
|
||||||
|
editable
|
||||||
anchor={anchor}
|
anchor={anchor}
|
||||||
workspaceId={workspaceID?.toString() ?? ""}
|
workspaceId={workspaceID?.toString() ?? ""}
|
||||||
onEnterKeyPress={handleSubmit(handleCommentUpdate)}
|
onEnterKeyPress={handleSubmit(handleCommentUpdate)}
|
||||||
|
|
@ -138,7 +139,8 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div className={`${isEditing ? "hidden" : ""}`}>
|
<div className={`${isEditing ? "hidden" : ""}`}>
|
||||||
<LiteTextReadOnlyEditor
|
<LiteTextEditor
|
||||||
|
editable={false}
|
||||||
anchor={anchor}
|
anchor={anchor}
|
||||||
workspaceId={workspaceID?.toString() ?? ""}
|
workspaceId={workspaceID?.toString() ?? ""}
|
||||||
ref={showEditorRef}
|
ref={showEditorRef}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ import { observer } from "mobx-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { Globe2, Lock } from "lucide-react";
|
import { Globe2, Lock } from "lucide-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { EditorReadOnlyRefApi } from "@plane/editor";
|
import type { EditorRefApi } from "@plane/editor";
|
||||||
import { useHashScroll } from "@plane/hooks";
|
import { useHashScroll } from "@plane/hooks";
|
||||||
import { EIssueCommentAccessSpecifier, type TCommentsOperations, type TIssueComment } from "@plane/types";
|
import { EIssueCommentAccessSpecifier, type TCommentsOperations, type TIssueComment } from "@plane/types";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { LiteTextReadOnlyEditor } from "@/components/editor";
|
import { LiteTextEditor } from "@/components/editor/lite-text";
|
||||||
// local imports
|
// local imports
|
||||||
import { CommentReactions } from "../comment-reaction";
|
import { CommentReactions } from "../comment-reaction";
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ type Props = {
|
||||||
comment: TIssueComment;
|
comment: TIssueComment;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
|
readOnlyEditorRef: React.RefObject<EditorRefApi>;
|
||||||
showAccessSpecifier: boolean;
|
showAccessSpecifier: boolean;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
|
@ -67,7 +67,8 @@ export const CommentCardDisplay: React.FC<Props> = observer((props) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<LiteTextReadOnlyEditor
|
<LiteTextEditor
|
||||||
|
editable={false}
|
||||||
ref={readOnlyEditorRef}
|
ref={readOnlyEditorRef}
|
||||||
id={comment.id}
|
id={comment.id}
|
||||||
initialValue={comment.comment_html ?? ""}
|
initialValue={comment.comment_html ?? ""}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { observer } from "mobx-react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Check, X } from "lucide-react";
|
import { Check, X } from "lucide-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
|
import type { EditorRefApi } from "@plane/editor";
|
||||||
import type { TCommentsOperations, TIssueComment } from "@plane/types";
|
import type { TCommentsOperations, TIssueComment } from "@plane/types";
|
||||||
import { isCommentEmpty } from "@plane/utils";
|
import { isCommentEmpty } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
|
|
@ -14,7 +14,7 @@ type Props = {
|
||||||
comment: TIssueComment;
|
comment: TIssueComment;
|
||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
readOnlyEditorRef: EditorReadOnlyRefApi | null;
|
readOnlyEditorRef: EditorRefApi | null;
|
||||||
setIsEditing: (isEditing: boolean) => void;
|
setIsEditing: (isEditing: boolean) => void;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
|
@ -75,6 +75,7 @@ export const CommentCardEditForm: React.FC<Props> = observer((props) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LiteTextEditor
|
<LiteTextEditor
|
||||||
|
editable
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { FC, useRef, useState } from "react";
|
import { FC, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { EditorReadOnlyRefApi } from "@plane/editor";
|
import type { EditorRefApi } from "@plane/editor";
|
||||||
import type { TIssueComment, TCommentsOperations } from "@plane/types";
|
import type { TIssueComment, TCommentsOperations } from "@plane/types";
|
||||||
// plane web imports
|
// plane web imports
|
||||||
import { CommentBlock } from "@/plane-web/components/comments";
|
import { CommentBlock } from "@/plane-web/components/comments";
|
||||||
|
|
@ -34,9 +34,10 @@ export const CommentCard: FC<TCommentCard> = observer((props) => {
|
||||||
disabled = false,
|
disabled = false,
|
||||||
projectId,
|
projectId,
|
||||||
} = props;
|
} = props;
|
||||||
const readOnlyEditorRef = useRef<EditorReadOnlyRefApi>(null);
|
|
||||||
// states
|
// states
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
// refs
|
||||||
|
const readOnlyEditorRef = useRef<EditorRefApi>(null);
|
||||||
// derived values
|
// derived values
|
||||||
const workspaceId = comment?.workspace;
|
const workspaceId = comment?.workspace;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ export const CommentCreate: FC<TCommentCreate> = observer((props) => {
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<LiteTextEditor
|
<LiteTextEditor
|
||||||
|
editable
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
id={"add_comment_" + entityId}
|
id={"add_comment_" + entityId}
|
||||||
value={"<p></p>"}
|
value={"<p></p>"}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,14 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
// plane constants
|
// plane imports
|
||||||
import { EIssueCommentAccessSpecifier } from "@plane/constants";
|
import { EIssueCommentAccessSpecifier } from "@plane/constants";
|
||||||
// plane editor
|
import { type EditorRefApi, type ILiteTextEditorProps, LiteTextEditorWithRef, type TFileHandler } from "@plane/editor";
|
||||||
import { EditorRefApi, ILiteTextEditorProps, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
|
|
||||||
// i18n
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
// components
|
import type { MakeOptional } from "@plane/types";
|
||||||
import { MakeOptional } from "@plane/types";
|
|
||||||
import { cn, isCommentEmpty } from "@plane/utils";
|
import { cn, isCommentEmpty } from "@plane/utils";
|
||||||
|
// components
|
||||||
import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
|
import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
|
||||||
// helpers
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
||||||
// store hooks
|
|
||||||
import { useMember } from "@/hooks/store";
|
import { useMember } from "@/hooks/store";
|
||||||
// plane web hooks
|
// plane web hooks
|
||||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||||
|
|
@ -20,11 +16,10 @@ import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||||
import { WorkspaceService } from "@/plane-web/services";
|
import { WorkspaceService } from "@/plane-web/services";
|
||||||
const workspaceService = new WorkspaceService();
|
const workspaceService = new WorkspaceService();
|
||||||
|
|
||||||
interface LiteTextEditorWrapperProps
|
type LiteTextEditorWrapperProps = MakeOptional<
|
||||||
extends MakeOptional<
|
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler">,
|
||||||
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler">,
|
"disabledExtensions" | "flaggedExtensions"
|
||||||
"disabledExtensions" | "flaggedExtensions"
|
> & {
|
||||||
> {
|
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
|
@ -35,15 +30,23 @@ interface LiteTextEditorWrapperProps
|
||||||
isSubmitting?: boolean;
|
isSubmitting?: boolean;
|
||||||
showToolbarInitially?: boolean;
|
showToolbarInitially?: boolean;
|
||||||
showToolbar?: boolean;
|
showToolbar?: boolean;
|
||||||
uploadFile: TFileHandler["upload"];
|
|
||||||
issue_id?: string;
|
issue_id?: string;
|
||||||
parentClassName?: string;
|
parentClassName?: string;
|
||||||
}
|
} & (
|
||||||
|
| {
|
||||||
|
editable: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
editable: true;
|
||||||
|
uploadFile: TFileHandler["upload"];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapperProps>((props, ref) => {
|
export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapperProps>((props, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
containerClassName,
|
containerClassName,
|
||||||
|
editable,
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
projectId,
|
projectId,
|
||||||
|
|
@ -57,7 +60,6 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
||||||
showToolbar = true,
|
showToolbar = true,
|
||||||
parentClassName = "",
|
parentClassName = "",
|
||||||
placeholder = t("issue.comments.placeholder"),
|
placeholder = t("issue.comments.placeholder"),
|
||||||
uploadFile,
|
|
||||||
disabledExtensions: additionalDisabledExtensions = [],
|
disabledExtensions: additionalDisabledExtensions = [],
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
@ -70,10 +72,10 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
||||||
// use editor mention
|
// use editor mention
|
||||||
const { fetchMentions } = useEditorMention({
|
const { fetchMentions } = useEditorMention({
|
||||||
searchEntity: async (payload) =>
|
searchEntity: async (payload) =>
|
||||||
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
|
await workspaceService.searchEntity(workspaceSlug, {
|
||||||
...payload,
|
...payload,
|
||||||
project_id: projectId?.toString() ?? "",
|
project_id: projectId,
|
||||||
issue_id: issue_id,
|
issue_id,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
// editor config
|
// editor config
|
||||||
|
|
@ -94,10 +96,11 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
||||||
<LiteTextEditorWithRef
|
<LiteTextEditorWithRef
|
||||||
ref={ref}
|
ref={ref}
|
||||||
disabledExtensions={[...liteTextEditorExtensions.disabled, ...additionalDisabledExtensions]}
|
disabledExtensions={[...liteTextEditorExtensions.disabled, ...additionalDisabledExtensions]}
|
||||||
|
editable={editable}
|
||||||
flaggedExtensions={liteTextEditorExtensions.flagged}
|
flaggedExtensions={liteTextEditorExtensions.flagged}
|
||||||
fileHandler={getEditorFileHandlers({
|
fileHandler={getEditorFileHandlers({
|
||||||
projectId,
|
projectId,
|
||||||
uploadFile,
|
uploadFile: editable ? props.uploadFile : async () => "",
|
||||||
workspaceId,
|
workspaceId,
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
})}
|
})}
|
||||||
|
|
@ -107,8 +110,10 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
||||||
if (!res) throw new Error("Failed in fetching mentions");
|
if (!res) throw new Error("Failed in fetching mentions");
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
renderComponent: (props) => <EditorMentionsRoot {...props} />,
|
renderComponent: EditorMentionsRoot,
|
||||||
getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }),
|
getMentionedEntityDetails: (id) => ({
|
||||||
|
display_name: getUserDetails(id)?.display_name ?? "",
|
||||||
|
}),
|
||||||
}}
|
}}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
containerClassName={cn(containerClassName, "relative")}
|
containerClassName={cn(containerClassName, "relative")}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
export * from "./editor";
|
export * from "./editor";
|
||||||
export * from "./read-only-editor";
|
|
||||||
export * from "./toolbar";
|
export * from "./toolbar";
|
||||||
|
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
// plane imports
|
|
||||||
import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditorProps, LiteTextReadOnlyEditorWithRef } from "@plane/editor";
|
|
||||||
import { MakeOptional } from "@plane/types";
|
|
||||||
// components
|
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
import { EditorMentionsRoot } from "@/components/editor";
|
|
||||||
// helpers
|
|
||||||
// hooks
|
|
||||||
import { useEditorConfig } from "@/hooks/editor";
|
|
||||||
// store hooks
|
|
||||||
import { useMember } from "@/hooks/store";
|
|
||||||
// plane web hooks
|
|
||||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
|
||||||
|
|
||||||
type LiteTextReadOnlyEditorWrapperProps = MakeOptional<
|
|
||||||
Omit<ILiteTextReadOnlyEditorProps, "fileHandler" | "mentionHandler">,
|
|
||||||
"disabledExtensions" | "flaggedExtensions"
|
|
||||||
> & {
|
|
||||||
workspaceId: string;
|
|
||||||
workspaceSlug: string;
|
|
||||||
projectId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
|
|
||||||
({ workspaceId, workspaceSlug, projectId, disabledExtensions: additionalDisabledExtensions, ...props }, ref) => {
|
|
||||||
// store hooks
|
|
||||||
const { getUserDetails } = useMember();
|
|
||||||
|
|
||||||
// editor flaggings
|
|
||||||
const { liteText: liteTextEditorExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
|
||||||
// editor config
|
|
||||||
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LiteTextReadOnlyEditorWithRef
|
|
||||||
ref={ref}
|
|
||||||
disabledExtensions={[...liteTextEditorExtensions.disabled, ...(additionalDisabledExtensions ?? [])]}
|
|
||||||
flaggedExtensions={liteTextEditorExtensions.flagged}
|
|
||||||
fileHandler={getReadOnlyEditorFileHandlers({
|
|
||||||
projectId,
|
|
||||||
workspaceId,
|
|
||||||
workspaceSlug,
|
|
||||||
})}
|
|
||||||
mentionHandler={{
|
|
||||||
renderComponent: (props) => <EditorMentionsRoot {...props} />,
|
|
||||||
getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }),
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
// overriding the containerClassName to add relative class passed
|
|
||||||
containerClassName={cn(props.containerClassName, "relative p-2")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
LiteTextReadOnlyEditor.displayName = "LiteTextReadOnlyEditor";
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef } from "react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { EditorRefApi, IRichTextEditorProps, RichTextEditorWithRef, TFileHandler } from "@plane/editor";
|
import { type EditorRefApi, type IRichTextEditorProps, RichTextEditorWithRef, type TFileHandler } from "@plane/editor";
|
||||||
import { MakeOptional, TSearchEntityRequestPayload, TSearchResponse } from "@plane/types";
|
import type { MakeOptional, TSearchEntityRequestPayload, TSearchResponse } from "@plane/types";
|
||||||
// components
|
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
|
// components
|
||||||
import { EditorMentionsRoot } from "@/components/editor";
|
import { EditorMentionsRoot } from "@/components/editor";
|
||||||
// helpers
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
||||||
// store hooks
|
|
||||||
import { useMember } from "@/hooks/store";
|
import { useMember } from "@/hooks/store";
|
||||||
// plane web hooks
|
// plane web hooks
|
||||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||||
|
|
@ -38,7 +36,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
projectId,
|
projectId,
|
||||||
disabledExtensions: additionalDisabledExtensions,
|
disabledExtensions: additionalDisabledExtensions = [],
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
|
|
@ -70,8 +68,10 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
||||||
if (!res) throw new Error("Failed in fetching mentions");
|
if (!res) throw new Error("Failed in fetching mentions");
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
renderComponent: (props) => <EditorMentionsRoot {...props} />,
|
renderComponent: EditorMentionsRoot,
|
||||||
getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }),
|
getMentionedEntityDetails: (id) => ({
|
||||||
|
display_name: getUserDetails(id)?.display_name ?? "",
|
||||||
|
}),
|
||||||
}}
|
}}
|
||||||
{...rest}
|
{...rest}
|
||||||
containerClassName={cn("relative pl-3 pb-3", containerClassName)}
|
containerClassName={cn("relative pl-3 pb-3", containerClassName)}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,10 @@ import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||||
import { StickyEditorToolbar } from "./toolbar";
|
import { StickyEditorToolbar } from "./toolbar";
|
||||||
|
|
||||||
interface StickyEditorWrapperProps
|
interface StickyEditorWrapperProps
|
||||||
extends Omit<ILiteTextEditorProps, "disabledExtensions" | "flaggedExtensions" | "fileHandler" | "mentionHandler"> {
|
extends Omit<
|
||||||
|
ILiteTextEditorProps,
|
||||||
|
"disabledExtensions" | "editable" | "flaggedExtensions" | "fileHandler" | "mentionHandler"
|
||||||
|
> {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
|
@ -67,6 +70,7 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
||||||
ref={ref}
|
ref={ref}
|
||||||
disabledExtensions={[...liteTextEditorExtensions.disabled, "enter-key"]}
|
disabledExtensions={[...liteTextEditorExtensions.disabled, "enter-key"]}
|
||||||
flaggedExtensions={liteTextEditorExtensions.flagged}
|
flaggedExtensions={liteTextEditorExtensions.flagged}
|
||||||
|
editable
|
||||||
fileHandler={getEditorFileHandlers({
|
fileHandler={getEditorFileHandlers({
|
||||||
projectId,
|
projectId,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
// plane editor
|
// plane imports
|
||||||
import { TFileHandler, TReadOnlyFileHandler } from "@plane/editor";
|
import type { TFileHandler } from "@plane/editor";
|
||||||
// helpers
|
|
||||||
import { getEditorAssetDownloadSrc, getEditorAssetSrc } from "@plane/utils";
|
import { getEditorAssetDownloadSrc, getEditorAssetSrc } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEditorAsset } from "@/hooks/store";
|
import { useEditorAsset } from "@/hooks/store";
|
||||||
|
|
@ -24,15 +23,30 @@ export const useEditorConfig = () => {
|
||||||
// file size
|
// file size
|
||||||
const { maxFileSize } = useFileSize();
|
const { maxFileSize } = useFileSize();
|
||||||
|
|
||||||
const getReadOnlyEditorFileHandlers = useCallback(
|
const getEditorFileHandlers = useCallback(
|
||||||
(args: Pick<TArgs, "projectId" | "workspaceId" | "workspaceSlug">): TReadOnlyFileHandler => {
|
(args: TArgs): TFileHandler => {
|
||||||
const { projectId, workspaceId, workspaceSlug } = args;
|
const { projectId, uploadFile, workspaceId, workspaceSlug } = args;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
assetsUploadStatus: assetsUploadPercentage,
|
||||||
|
cancel: fileService.cancelUpload,
|
||||||
checkIfAssetExists: async (assetId: string) => {
|
checkIfAssetExists: async (assetId: string) => {
|
||||||
const res = await fileService.checkIfAssetExists(workspaceSlug, assetId);
|
const res = await fileService.checkIfAssetExists(workspaceSlug, assetId);
|
||||||
return res?.exists ?? false;
|
return res?.exists ?? false;
|
||||||
},
|
},
|
||||||
|
delete: async (src: string) => {
|
||||||
|
if (src?.startsWith("http")) {
|
||||||
|
await fileService.deleteOldWorkspaceAsset(workspaceId, src);
|
||||||
|
} else {
|
||||||
|
await fileService.deleteNewAsset(
|
||||||
|
getEditorAssetSrc({
|
||||||
|
assetId: src,
|
||||||
|
projectId,
|
||||||
|
workspaceSlug,
|
||||||
|
}) ?? ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
getAssetDownloadSrc: async (path) => {
|
getAssetDownloadSrc: async (path) => {
|
||||||
if (!path) return "";
|
if (!path) return "";
|
||||||
if (path?.startsWith("http")) {
|
if (path?.startsWith("http")) {
|
||||||
|
|
@ -68,47 +82,16 @@ export const useEditorConfig = () => {
|
||||||
await fileService.restoreNewAsset(workspaceSlug, src);
|
await fileService.restoreNewAsset(workspaceSlug, src);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getEditorFileHandlers = useCallback(
|
|
||||||
(args: TArgs): TFileHandler => {
|
|
||||||
const { projectId, uploadFile, workspaceId, workspaceSlug } = args;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...getReadOnlyEditorFileHandlers({
|
|
||||||
projectId,
|
|
||||||
workspaceId,
|
|
||||||
workspaceSlug,
|
|
||||||
}),
|
|
||||||
assetsUploadStatus: assetsUploadPercentage,
|
|
||||||
upload: uploadFile,
|
upload: uploadFile,
|
||||||
delete: async (src: string) => {
|
|
||||||
if (src?.startsWith("http")) {
|
|
||||||
await fileService.deleteOldWorkspaceAsset(workspaceId, src);
|
|
||||||
} else {
|
|
||||||
await fileService.deleteNewAsset(
|
|
||||||
getEditorAssetSrc({
|
|
||||||
assetId: src,
|
|
||||||
projectId,
|
|
||||||
workspaceSlug,
|
|
||||||
}) ?? ""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cancel: fileService.cancelUpload,
|
|
||||||
validation: {
|
validation: {
|
||||||
maxFileSize,
|
maxFileSize,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[assetsUploadPercentage, getReadOnlyEditorFileHandlers, maxFileSize]
|
[assetsUploadPercentage, maxFileSize]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getEditorFileHandlers,
|
getEditorFileHandlers,
|
||||||
getReadOnlyEditorFileHandlers,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
export * from "./extensions";
|
export * from "./extensions";
|
||||||
export * from "./read-only-extensions";
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import type { Extensions } from "@tiptap/core";
|
|
||||||
// types
|
|
||||||
import type { IReadOnlyEditorProps } from "@/types";
|
|
||||||
|
|
||||||
export type TCoreReadOnlyEditorAdditionalExtensionsProps = Pick<
|
|
||||||
IReadOnlyEditorProps,
|
|
||||||
"disabledExtensions" | "flaggedExtensions"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const CoreReadOnlyEditorAdditionalExtensions = (
|
|
||||||
props: TCoreReadOnlyEditorAdditionalExtensionsProps
|
|
||||||
): Extensions => {
|
|
||||||
const {} = props;
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { AnyExtension, Extensions } from "@tiptap/core";
|
|
||||||
// types
|
|
||||||
import { IReadOnlyEditorProps, TExtensions } from "@/types";
|
|
||||||
|
|
||||||
export type TRichTextReadOnlyEditorAdditionalExtensionsProps = Pick<
|
|
||||||
IReadOnlyEditorProps,
|
|
||||||
"disabledExtensions" | "flaggedExtensions" | "fileHandler"
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registry entry configuration for extensions
|
|
||||||
*/
|
|
||||||
export type TRichTextReadOnlyEditorAdditionalExtensionsRegistry = {
|
|
||||||
/** Determines if the extension should be enabled based on disabled extensions */
|
|
||||||
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
|
||||||
/** Returns the extension instance(s) when enabled */
|
|
||||||
getExtension: (props: TRichTextReadOnlyEditorAdditionalExtensionsProps) => AnyExtension | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const extensionRegistry: TRichTextReadOnlyEditorAdditionalExtensionsRegistry[] = [];
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
@ -4,4 +4,3 @@ export * from "./rich-text";
|
||||||
export * from "./editor-container";
|
export * from "./editor-container";
|
||||||
export * from "./editor-content";
|
export * from "./editor-content";
|
||||||
export * from "./editor-wrapper";
|
export * from "./editor-wrapper";
|
||||||
export * from "./read-only-editor-wrapper";
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const LiteTextEditor: React.FC<ILiteTextEditorProps> = (props) => {
|
||||||
return resolvedExtensions;
|
return resolvedExtensions;
|
||||||
}, [externalExtensions, disabledExtensions, onEnterKeyPress]);
|
}, [externalExtensions, disabledExtensions, onEnterKeyPress]);
|
||||||
|
|
||||||
return <EditorWrapper {...props} editable extensions={extensions} />;
|
return <EditorWrapper {...props} extensions={extensions} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LiteTextEditorWithRef = forwardRef<EditorRefApi, ILiteTextEditorProps>((props, ref) => (
|
const LiteTextEditorWithRef = forwardRef<EditorRefApi, ILiteTextEditorProps>((props, ref) => (
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
export * from "./editor";
|
export * from "./editor";
|
||||||
export * from "./read-only-editor";
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { forwardRef } from "react";
|
|
||||||
// components
|
|
||||||
import { ReadOnlyEditorWrapper } from "@/components/editors";
|
|
||||||
// types
|
|
||||||
import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditorProps } from "@/types";
|
|
||||||
|
|
||||||
const LiteTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, ILiteTextReadOnlyEditorProps>((props, ref) => (
|
|
||||||
<ReadOnlyEditorWrapper {...props} forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>} />
|
|
||||||
));
|
|
||||||
|
|
||||||
LiteTextReadOnlyEditorWithRef.displayName = "LiteReadOnlyEditorWithRef";
|
|
||||||
|
|
||||||
export { LiteTextReadOnlyEditorWithRef };
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
// components
|
|
||||||
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
|
|
||||||
// constants
|
|
||||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
|
||||||
// helpers
|
|
||||||
import { getEditorClassNames } from "@/helpers/common";
|
|
||||||
// hooks
|
|
||||||
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
|
||||||
// types
|
|
||||||
import { IReadOnlyEditorProps } from "@/types";
|
|
||||||
|
|
||||||
export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
|
||||||
const {
|
|
||||||
containerClassName,
|
|
||||||
disabledExtensions,
|
|
||||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
|
||||||
editorClassName = "",
|
|
||||||
extensions,
|
|
||||||
fileHandler,
|
|
||||||
flaggedExtensions,
|
|
||||||
forwardedRef,
|
|
||||||
id,
|
|
||||||
initialValue,
|
|
||||||
mentionHandler,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const editor = useReadOnlyEditor({
|
|
||||||
disabledExtensions,
|
|
||||||
editorClassName,
|
|
||||||
extensions,
|
|
||||||
fileHandler,
|
|
||||||
flaggedExtensions,
|
|
||||||
forwardedRef,
|
|
||||||
initialValue,
|
|
||||||
mentionHandler,
|
|
||||||
});
|
|
||||||
|
|
||||||
const editorContainerClassName = getEditorClassNames({
|
|
||||||
containerClassName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!editor) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EditorContainer
|
|
||||||
displayConfig={displayConfig}
|
|
||||||
editor={editor}
|
|
||||||
editorContainerClassName={editorContainerClassName}
|
|
||||||
id={id}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<EditorContentWrapper editor={editor} id={id} />
|
|
||||||
</div>
|
|
||||||
</EditorContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { EditorBubbleMenu } from "@/components/menus";
|
||||||
// extensions
|
// extensions
|
||||||
import { SideMenuExtension } from "@/extensions";
|
import { SideMenuExtension } from "@/extensions";
|
||||||
// plane editor imports
|
// plane editor imports
|
||||||
import { RichTextEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/extensions";
|
import { RichTextEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text-extensions";
|
||||||
// types
|
// types
|
||||||
import { EditorRefApi, IRichTextEditorProps } from "@/types";
|
import { EditorRefApi, IRichTextEditorProps } from "@/types";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { findParentNodeClosestToPos, Predicate, ReactNodeViewRenderer } from "@tiptap/react";
|
import { findParentNodeClosestToPos, Predicate, ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
// extensions
|
// extensions
|
||||||
import { CustomCalloutBlock, CustomCalloutNodeViewProps } from "@/extensions/callout";
|
import { CustomCalloutBlock, CustomCalloutNodeViewProps } from "@/extensions/callout/block";
|
||||||
// helpers
|
// helpers
|
||||||
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
|
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
|
||||||
// config
|
// config
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1 @@
|
||||||
export * from "./block";
|
|
||||||
export * from "./extension";
|
export * from "./extension";
|
||||||
export * from "./read-only-extension";
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
|
||||||
// extensions
|
|
||||||
import { CustomCalloutBlock, CustomCalloutNodeViewProps } from "@/extensions/callout";
|
|
||||||
// config
|
|
||||||
import { CustomCalloutExtensionConfig } from "./extension-config";
|
|
||||||
|
|
||||||
export const CustomCalloutReadOnlyExtension = CustomCalloutExtensionConfig.extend({
|
|
||||||
selectable: false,
|
|
||||||
draggable: false,
|
|
||||||
|
|
||||||
addNodeView() {
|
|
||||||
return ReactNodeViewRenderer((props) => (
|
|
||||||
<CustomCalloutBlock {...props} node={props.node as CustomCalloutNodeViewProps["node"]} />
|
|
||||||
));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -6,14 +6,14 @@ import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config";
|
||||||
import { isFileValid } from "@/helpers/file";
|
import { isFileValid } from "@/helpers/file";
|
||||||
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
|
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
|
||||||
// types
|
// types
|
||||||
import type { TFileHandler, TReadOnlyFileHandler } from "@/types";
|
import type { TFileHandler } from "@/types";
|
||||||
// local imports
|
// local imports
|
||||||
import { CustomImageNodeView, CustomImageNodeViewProps } from "./components/node-view";
|
import { CustomImageNodeView, CustomImageNodeViewProps } from "./components/node-view";
|
||||||
import { CustomImageExtensionConfig } from "./extension-config";
|
import { CustomImageExtensionConfig } from "./extension-config";
|
||||||
import { getImageComponentImageFileMap } from "./utils";
|
import { getImageComponentImageFileMap } from "./utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
fileHandler: TFileHandler | TReadOnlyFileHandler;
|
fileHandler: TFileHandler;
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
// helpers
|
// helpers
|
||||||
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
|
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
|
||||||
// types
|
// types
|
||||||
import type { TFileHandler, TReadOnlyFileHandler } from "@/types";
|
import type { TFileHandler } from "@/types";
|
||||||
// local imports
|
// local imports
|
||||||
import { CustomImageNodeView, CustomImageNodeViewProps } from "../custom-image/components/node-view";
|
import { CustomImageNodeView, CustomImageNodeViewProps } from "../custom-image/components/node-view";
|
||||||
import { ImageExtensionConfig } from "./extension-config";
|
import { ImageExtensionConfig } from "./extension-config";
|
||||||
|
|
@ -12,7 +12,7 @@ export type ImageExtensionStorage = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
fileHandler: TFileHandler | TReadOnlyFileHandler;
|
fileHandler: TFileHandler;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImageExtension = (props: Props) => {
|
export const ImageExtension = (props: Props) => {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ export * from "./headings-list";
|
||||||
export * from "./horizontal-rule";
|
export * from "./horizontal-rule";
|
||||||
export * from "./keymap";
|
export * from "./keymap";
|
||||||
export * from "./quote";
|
export * from "./quote";
|
||||||
export * from "./read-only-extensions";
|
|
||||||
export * from "./side-menu";
|
export * from "./side-menu";
|
||||||
export * from "./text-align";
|
export * from "./text-align";
|
||||||
export * from "./utility";
|
export * from "./utility";
|
||||||
|
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
import { Extensions } from "@tiptap/core";
|
|
||||||
import CharacterCount from "@tiptap/extension-character-count";
|
|
||||||
import TaskItem from "@tiptap/extension-task-item";
|
|
||||||
import TaskList from "@tiptap/extension-task-list";
|
|
||||||
import TextStyle from "@tiptap/extension-text-style";
|
|
||||||
import TiptapUnderline from "@tiptap/extension-underline";
|
|
||||||
import { Markdown } from "tiptap-markdown";
|
|
||||||
// extensions
|
|
||||||
import {
|
|
||||||
CustomQuoteExtension,
|
|
||||||
CustomHorizontalRule,
|
|
||||||
CustomLinkExtension,
|
|
||||||
CustomTypographyExtension,
|
|
||||||
CustomCodeBlockExtension,
|
|
||||||
CustomCodeInlineExtension,
|
|
||||||
TableHeader,
|
|
||||||
TableCell,
|
|
||||||
TableRow,
|
|
||||||
Table,
|
|
||||||
CustomMentionExtension,
|
|
||||||
CustomTextAlignExtension,
|
|
||||||
CustomCalloutReadOnlyExtension,
|
|
||||||
CustomColorExtension,
|
|
||||||
UtilityExtension,
|
|
||||||
ImageExtension,
|
|
||||||
} from "@/extensions";
|
|
||||||
// plane editor extensions
|
|
||||||
import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
|
||||||
// types
|
|
||||||
import type { IReadOnlyEditorProps } from "@/types";
|
|
||||||
// local imports
|
|
||||||
import { CustomImageExtension } from "./custom-image/extension";
|
|
||||||
import { EmojiExtension } from "./emoji/extension";
|
|
||||||
import { CustomStarterKitExtension } from "./starter-kit";
|
|
||||||
|
|
||||||
type Props = Pick<IReadOnlyEditorProps, "disabledExtensions" | "flaggedExtensions" | "fileHandler" | "mentionHandler">;
|
|
||||||
|
|
||||||
export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
|
||||||
const { disabledExtensions, fileHandler, flaggedExtensions, mentionHandler } = props;
|
|
||||||
|
|
||||||
const extensions = [
|
|
||||||
CustomStarterKitExtension({
|
|
||||||
enableHistory: false,
|
|
||||||
}),
|
|
||||||
EmojiExtension,
|
|
||||||
CustomQuoteExtension,
|
|
||||||
CustomHorizontalRule,
|
|
||||||
CustomLinkExtension,
|
|
||||||
CustomTypographyExtension,
|
|
||||||
TiptapUnderline,
|
|
||||||
TextStyle,
|
|
||||||
TaskList.configure({
|
|
||||||
HTMLAttributes: {
|
|
||||||
class: "not-prose pl-2 space-y-2",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
TaskItem.configure({
|
|
||||||
HTMLAttributes: {
|
|
||||||
class: "relative pointer-events-none",
|
|
||||||
},
|
|
||||||
nested: true,
|
|
||||||
}),
|
|
||||||
CustomCodeBlockExtension,
|
|
||||||
CustomCodeInlineExtension,
|
|
||||||
Markdown.configure({
|
|
||||||
html: true,
|
|
||||||
transformCopiedText: false,
|
|
||||||
}),
|
|
||||||
Table,
|
|
||||||
TableHeader,
|
|
||||||
TableCell,
|
|
||||||
TableRow,
|
|
||||||
CustomMentionExtension(mentionHandler),
|
|
||||||
CharacterCount,
|
|
||||||
CustomColorExtension,
|
|
||||||
CustomTextAlignExtension,
|
|
||||||
CustomCalloutReadOnlyExtension,
|
|
||||||
UtilityExtension({
|
|
||||||
disabledExtensions,
|
|
||||||
fileHandler,
|
|
||||||
isEditable: false,
|
|
||||||
}),
|
|
||||||
...CoreReadOnlyEditorAdditionalExtensions({
|
|
||||||
disabledExtensions,
|
|
||||||
flaggedExtensions,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!disabledExtensions.includes("image")) {
|
|
||||||
extensions.push(
|
|
||||||
ImageExtension({
|
|
||||||
fileHandler,
|
|
||||||
}),
|
|
||||||
CustomImageExtension({
|
|
||||||
fileHandler,
|
|
||||||
isEditable: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return extensions;
|
|
||||||
};
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { FilePlugins } from "@/plugins/file/root";
|
||||||
import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
|
import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
|
||||||
// types
|
// types
|
||||||
|
|
||||||
import type { IEditorProps, TEditorAsset, TFileHandler, TReadOnlyFileHandler } from "@/types";
|
import type { IEditorProps, TEditorAsset, TFileHandler } from "@/types";
|
||||||
type TActiveDropbarExtensions = CORE_EXTENSIONS.MENTION | CORE_EXTENSIONS.EMOJI | TAdditionalActiveDropbarExtensions;
|
type TActiveDropbarExtensions = CORE_EXTENSIONS.MENTION | CORE_EXTENSIONS.EMOJI | TAdditionalActiveDropbarExtensions;
|
||||||
|
|
||||||
declare module "@tiptap/core" {
|
declare module "@tiptap/core" {
|
||||||
|
|
@ -38,7 +38,7 @@ export interface UtilityExtensionStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = Pick<IEditorProps, "disabledExtensions"> & {
|
type Props = Pick<IEditorProps, "disabledExtensions"> & {
|
||||||
fileHandler: TFileHandler | TReadOnlyFileHandler;
|
fileHandler: TFileHandler;
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,26 @@
|
||||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||||
import { Editor } from "@tiptap/core";
|
import { Editor } from "@tiptap/core";
|
||||||
|
import { DOMSerializer } from "@tiptap/pm/model";
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs";
|
||||||
|
// components
|
||||||
|
import { getEditorMenuItems } from "@/components/menus";
|
||||||
// constants
|
// constants
|
||||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||||
import { CORE_EDITOR_META } from "@/constants/meta";
|
import { CORE_EDITOR_META } from "@/constants/meta";
|
||||||
// types
|
// types
|
||||||
import { EditorReadOnlyRefApi } from "@/types";
|
import type { EditorRefApi, TEditorCommands } from "@/types";
|
||||||
// local imports
|
// local imports
|
||||||
import { getParagraphCount } from "./common";
|
import { getParagraphCount } from "./common";
|
||||||
import { getExtensionStorage } from "./get-extension-storage";
|
import { getExtensionStorage } from "./get-extension-storage";
|
||||||
import { scrollSummary } from "./scroll-to-node";
|
import { insertContentAtSavedSelection } from "./insert-content-at-cursor-position";
|
||||||
|
import { scrollSummary, scrollToNodeViaDOMCoordinates } from "./scroll-to-node";
|
||||||
|
|
||||||
type TArgs = {
|
type TArgs = {
|
||||||
editor: Editor | null;
|
editor: Editor | null;
|
||||||
provider: HocuspocusProvider | undefined;
|
provider: HocuspocusProvider | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getEditorRefHelpers = (args: TArgs): EditorReadOnlyRefApi => {
|
export const getEditorRefHelpers = (args: TArgs): EditorRefApi => {
|
||||||
const { editor, provider } = args;
|
const { editor, provider } = args;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -51,5 +55,147 @@ export const getEditorRefHelpers = (args: TArgs): EditorReadOnlyRefApi => {
|
||||||
setEditorValue: (content, emitUpdate = false) => {
|
setEditorValue: (content, emitUpdate = false) => {
|
||||||
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true });
|
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true });
|
||||||
},
|
},
|
||||||
|
blur: () => editor?.commands.blur(),
|
||||||
|
emitRealTimeUpdate: (message) => provider?.sendStateless(message),
|
||||||
|
executeMenuItemCommand: (props) => {
|
||||||
|
const { itemKey } = props;
|
||||||
|
const editorItems = getEditorMenuItems(editor);
|
||||||
|
|
||||||
|
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
|
||||||
|
|
||||||
|
const item = getEditorMenuItem(itemKey);
|
||||||
|
if (item) {
|
||||||
|
item.command(props);
|
||||||
|
} else {
|
||||||
|
console.warn(`No command found for item: ${itemKey}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getCurrentCursorPosition: () => editor?.state.selection.from,
|
||||||
|
getSelectedText: () => {
|
||||||
|
if (!editor) return null;
|
||||||
|
|
||||||
|
const { state } = editor;
|
||||||
|
const { from, to, empty } = state.selection;
|
||||||
|
|
||||||
|
if (empty) return null;
|
||||||
|
|
||||||
|
const nodesArray: string[] = [];
|
||||||
|
state.doc.nodesBetween(from, to, (node, _pos, parent) => {
|
||||||
|
if (parent === state.doc && editor) {
|
||||||
|
const serializer = DOMSerializer.fromSchema(editor.schema);
|
||||||
|
const dom = serializer.serializeNode(node);
|
||||||
|
const tempDiv = document.createElement("div");
|
||||||
|
tempDiv.appendChild(dom);
|
||||||
|
nodesArray.push(tempDiv.innerHTML);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const selection = nodesArray.join("");
|
||||||
|
return selection;
|
||||||
|
},
|
||||||
|
insertText: (contentHTML, insertOnNextLine) => {
|
||||||
|
if (!editor) return;
|
||||||
|
const { from, to, empty } = editor.state.selection;
|
||||||
|
if (empty) return;
|
||||||
|
if (insertOnNextLine) {
|
||||||
|
// move cursor to the end of the selection and insert a new line
|
||||||
|
editor.chain().focus().setTextSelection(to).insertContent("<br />").insertContent(contentHTML).run();
|
||||||
|
} else {
|
||||||
|
// replace selected text with the content provided
|
||||||
|
editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEditorReadyToDiscard: () =>
|
||||||
|
!!editor && getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress === false,
|
||||||
|
isMenuItemActive: (props) => {
|
||||||
|
const { itemKey } = props;
|
||||||
|
const editorItems = getEditorMenuItems(editor);
|
||||||
|
|
||||||
|
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
|
||||||
|
const item = getEditorMenuItem(itemKey);
|
||||||
|
if (!item) return false;
|
||||||
|
|
||||||
|
return item.isActive(props);
|
||||||
|
},
|
||||||
|
listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
|
||||||
|
onDocumentInfoChange: (callback) => {
|
||||||
|
const handleDocumentInfoChange = () => {
|
||||||
|
if (!editor) return;
|
||||||
|
callback({
|
||||||
|
characters: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.characters?.() : 0,
|
||||||
|
paragraphs: getParagraphCount(editor?.state),
|
||||||
|
words: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.words?.() : 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subscribe to update event emitted from character count extension
|
||||||
|
editor?.on("update", handleDocumentInfoChange);
|
||||||
|
// Return a function to unsubscribe to the continuous transactions of
|
||||||
|
// the editor on unmounting the component that has subscribed to this
|
||||||
|
// method
|
||||||
|
return () => {
|
||||||
|
editor?.off("update", handleDocumentInfoChange);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onHeadingChange: (callback) => {
|
||||||
|
const handleHeadingChange = () => {
|
||||||
|
if (!editor) return;
|
||||||
|
const headings = getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings;
|
||||||
|
if (headings) {
|
||||||
|
callback(headings);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subscribe to update event emitted from headers extension
|
||||||
|
editor?.on("update", handleHeadingChange);
|
||||||
|
// Return a function to unsubscribe to the continuous transactions of
|
||||||
|
// the editor on unmounting the component that has subscribed to this
|
||||||
|
// method
|
||||||
|
return () => {
|
||||||
|
editor?.off("update", handleHeadingChange);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onStateChange: (callback) => {
|
||||||
|
// Subscribe to editor state changes
|
||||||
|
editor?.on("transaction", callback);
|
||||||
|
|
||||||
|
// Return a function to unsubscribe to the continuous transactions of
|
||||||
|
// the editor on unmounting the component that has subscribed to this
|
||||||
|
// method
|
||||||
|
return () => {
|
||||||
|
editor?.off("transaction", callback);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
scrollToNodeViaDOMCoordinates(behavior, pos) {
|
||||||
|
const resolvedPos = pos ?? editor?.state.selection.from;
|
||||||
|
if (!editor || !resolvedPos) return;
|
||||||
|
scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior);
|
||||||
|
},
|
||||||
|
setEditorValueAtCursorPosition: (content) => {
|
||||||
|
if (editor?.state.selection) {
|
||||||
|
insertContentAtSavedSelection(editor, content);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setFocusAtPosition: (position) => {
|
||||||
|
if (!editor || editor.isDestroyed) {
|
||||||
|
console.error("Editor reference is not available or has been destroyed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const docSize = editor.state.doc.content.size;
|
||||||
|
const safePosition = Math.max(0, Math.min(position, docSize));
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.insertContentAt(safePosition, [{ type: CORE_EXTENSIONS.PARAGRAPH }])
|
||||||
|
.focus()
|
||||||
|
.run();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("An error occurred while setting focus at position:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setProviderDocument: (value) => {
|
||||||
|
const document = provider?.document;
|
||||||
|
if (!document) return;
|
||||||
|
Y.applyUpdate(document, value);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,16 @@
|
||||||
import { DOMSerializer } from "@tiptap/pm/model";
|
|
||||||
import { useEditorState, useEditor as useTiptapEditor } from "@tiptap/react";
|
import { useEditorState, useEditor as useTiptapEditor } from "@tiptap/react";
|
||||||
import { useImperativeHandle, useEffect } from "react";
|
import { useImperativeHandle, useEffect } from "react";
|
||||||
import * as Y from "yjs";
|
|
||||||
// components
|
|
||||||
import { getEditorMenuItems } from "@/components/menus";
|
|
||||||
// constants
|
// constants
|
||||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||||
// extensions
|
// extensions
|
||||||
import { CoreEditorExtensions } from "@/extensions";
|
import { CoreEditorExtensions } from "@/extensions";
|
||||||
// helpers
|
// helpers
|
||||||
import { getParagraphCount } from "@/helpers/common";
|
|
||||||
import { getEditorRefHelpers } from "@/helpers/editor-ref";
|
import { getEditorRefHelpers } from "@/helpers/editor-ref";
|
||||||
import { getExtensionStorage } from "@/helpers/get-extension-storage";
|
import { getExtensionStorage } from "@/helpers/get-extension-storage";
|
||||||
import { insertContentAtSavedSelection } from "@/helpers/insert-content-at-cursor-position";
|
|
||||||
import { scrollToNodeViaDOMCoordinates } from "@/helpers/scroll-to-node";
|
|
||||||
// props
|
// props
|
||||||
import { CoreEditorProps } from "@/props";
|
import { CoreEditorProps } from "@/props";
|
||||||
// types
|
// types
|
||||||
import type { TEditorCommands, TEditorHookProps } from "@/types";
|
import type { TEditorHookProps } from "@/types";
|
||||||
|
|
||||||
export const useEditor = (props: TEditorHookProps) => {
|
export const useEditor = (props: TEditorHookProps) => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -124,155 +117,7 @@ export const useEditor = (props: TEditorHookProps) => {
|
||||||
onAssetChange(assets);
|
onAssetChange(assets);
|
||||||
}, [assetsList?.assets, onAssetChange]);
|
}, [assetsList?.assets, onAssetChange]);
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(forwardedRef, () => getEditorRefHelpers({ editor, provider }), [editor, provider]);
|
||||||
forwardedRef,
|
|
||||||
() => ({
|
|
||||||
...getEditorRefHelpers({ editor, provider }),
|
|
||||||
blur: () => editor?.commands.blur(),
|
|
||||||
emitRealTimeUpdate: (message) => provider?.sendStateless(message),
|
|
||||||
executeMenuItemCommand: (props) => {
|
|
||||||
const { itemKey } = props;
|
|
||||||
const editorItems = getEditorMenuItems(editor);
|
|
||||||
|
|
||||||
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
|
|
||||||
|
|
||||||
const item = getEditorMenuItem(itemKey);
|
|
||||||
if (item) {
|
|
||||||
item.command(props);
|
|
||||||
} else {
|
|
||||||
console.warn(`No command found for item: ${itemKey}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getCurrentCursorPosition: () => editor?.state.selection.from,
|
|
||||||
getSelectedText: () => {
|
|
||||||
if (!editor) return null;
|
|
||||||
|
|
||||||
const { state } = editor;
|
|
||||||
const { from, to, empty } = state.selection;
|
|
||||||
|
|
||||||
if (empty) return null;
|
|
||||||
|
|
||||||
const nodesArray: string[] = [];
|
|
||||||
state.doc.nodesBetween(from, to, (node, _pos, parent) => {
|
|
||||||
if (parent === state.doc && editor) {
|
|
||||||
const serializer = DOMSerializer.fromSchema(editor.schema);
|
|
||||||
const dom = serializer.serializeNode(node);
|
|
||||||
const tempDiv = document.createElement("div");
|
|
||||||
tempDiv.appendChild(dom);
|
|
||||||
nodesArray.push(tempDiv.innerHTML);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const selection = nodesArray.join("");
|
|
||||||
return selection;
|
|
||||||
},
|
|
||||||
insertText: (contentHTML, insertOnNextLine) => {
|
|
||||||
if (!editor) return;
|
|
||||||
const { from, to, empty } = editor.state.selection;
|
|
||||||
if (empty) return;
|
|
||||||
if (insertOnNextLine) {
|
|
||||||
// move cursor to the end of the selection and insert a new line
|
|
||||||
editor.chain().focus().setTextSelection(to).insertContent("<br />").insertContent(contentHTML).run();
|
|
||||||
} else {
|
|
||||||
// replace selected text with the content provided
|
|
||||||
editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isEditorReadyToDiscard: () =>
|
|
||||||
!!editor && getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress === false,
|
|
||||||
isMenuItemActive: (props) => {
|
|
||||||
const { itemKey } = props;
|
|
||||||
const editorItems = getEditorMenuItems(editor);
|
|
||||||
|
|
||||||
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
|
|
||||||
const item = getEditorMenuItem(itemKey);
|
|
||||||
if (!item) return false;
|
|
||||||
|
|
||||||
return item.isActive(props);
|
|
||||||
},
|
|
||||||
listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
|
|
||||||
onDocumentInfoChange: (callback) => {
|
|
||||||
const handleDocumentInfoChange = () => {
|
|
||||||
if (!editor) return;
|
|
||||||
callback({
|
|
||||||
characters: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.characters?.() : 0,
|
|
||||||
paragraphs: getParagraphCount(editor?.state),
|
|
||||||
words: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.words?.() : 0,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Subscribe to update event emitted from character count extension
|
|
||||||
editor?.on("update", handleDocumentInfoChange);
|
|
||||||
// Return a function to unsubscribe to the continuous transactions of
|
|
||||||
// the editor on unmounting the component that has subscribed to this
|
|
||||||
// method
|
|
||||||
return () => {
|
|
||||||
editor?.off("update", handleDocumentInfoChange);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
onHeadingChange: (callback) => {
|
|
||||||
const handleHeadingChange = () => {
|
|
||||||
if (!editor) return;
|
|
||||||
const headings = getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings;
|
|
||||||
if (headings) {
|
|
||||||
callback(headings);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Subscribe to update event emitted from headers extension
|
|
||||||
editor?.on("update", handleHeadingChange);
|
|
||||||
// Return a function to unsubscribe to the continuous transactions of
|
|
||||||
// the editor on unmounting the component that has subscribed to this
|
|
||||||
// method
|
|
||||||
return () => {
|
|
||||||
editor?.off("update", handleHeadingChange);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
onStateChange: (callback) => {
|
|
||||||
// Subscribe to editor state changes
|
|
||||||
editor?.on("transaction", callback);
|
|
||||||
|
|
||||||
// Return a function to unsubscribe to the continuous transactions of
|
|
||||||
// the editor on unmounting the component that has subscribed to this
|
|
||||||
// method
|
|
||||||
return () => {
|
|
||||||
editor?.off("transaction", callback);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
scrollToNodeViaDOMCoordinates(behavior, pos) {
|
|
||||||
const resolvedPos = pos ?? editor?.state.selection.from;
|
|
||||||
if (!editor || !resolvedPos) return;
|
|
||||||
scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior);
|
|
||||||
},
|
|
||||||
setEditorValueAtCursorPosition: (content) => {
|
|
||||||
if (editor?.state.selection) {
|
|
||||||
insertContentAtSavedSelection(editor, content);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setFocusAtPosition: (position) => {
|
|
||||||
if (!editor || editor.isDestroyed) {
|
|
||||||
console.error("Editor reference is not available or has been destroyed.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const docSize = editor.state.doc.content.size;
|
|
||||||
const safePosition = Math.max(0, Math.min(position, docSize));
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.insertContentAt(safePosition, [{ type: CORE_EXTENSIONS.PARAGRAPH }])
|
|
||||||
.focus()
|
|
||||||
.run();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("An error occurred while setting focus at position:", error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setProviderDocument: (value) => {
|
|
||||||
const document = provider?.document;
|
|
||||||
if (!document) return;
|
|
||||||
Y.applyUpdate(document, value);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[editor, provider]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
import { useEditor as useTiptapEditor } from "@tiptap/react";
|
|
||||||
import { useImperativeHandle, useEffect } from "react";
|
|
||||||
// extensions
|
|
||||||
import { CoreReadOnlyEditorExtensions } from "@/extensions";
|
|
||||||
// helpers
|
|
||||||
import { getEditorRefHelpers } from "@/helpers/editor-ref";
|
|
||||||
// props
|
|
||||||
import { CoreReadOnlyEditorProps } from "@/props";
|
|
||||||
// types
|
|
||||||
import type { TReadOnlyEditorHookProps } from "@/types";
|
|
||||||
|
|
||||||
export const useReadOnlyEditor = (props: TReadOnlyEditorHookProps) => {
|
|
||||||
const {
|
|
||||||
disabledExtensions,
|
|
||||||
editorClassName = "",
|
|
||||||
editorProps = {},
|
|
||||||
extensions = [],
|
|
||||||
fileHandler,
|
|
||||||
flaggedExtensions,
|
|
||||||
forwardedRef,
|
|
||||||
handleEditorReady,
|
|
||||||
initialValue,
|
|
||||||
mentionHandler,
|
|
||||||
provider,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const editor = useTiptapEditor({
|
|
||||||
editable: false,
|
|
||||||
immediatelyRender: false,
|
|
||||||
shouldRerenderOnTransaction: false,
|
|
||||||
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
|
|
||||||
parseOptions: { preserveWhitespace: true },
|
|
||||||
editorProps: {
|
|
||||||
...CoreReadOnlyEditorProps({
|
|
||||||
editorClassName,
|
|
||||||
}),
|
|
||||||
...editorProps,
|
|
||||||
},
|
|
||||||
onCreate: async () => {
|
|
||||||
handleEditorReady?.(true);
|
|
||||||
},
|
|
||||||
extensions: [
|
|
||||||
...CoreReadOnlyEditorExtensions({
|
|
||||||
disabledExtensions,
|
|
||||||
fileHandler,
|
|
||||||
flaggedExtensions,
|
|
||||||
mentionHandler,
|
|
||||||
}),
|
|
||||||
...extensions,
|
|
||||||
],
|
|
||||||
onDestroy: () => {
|
|
||||||
handleEditorReady?.(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// for syncing swr data on tab refocus etc
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialValue === null || initialValue === undefined) return;
|
|
||||||
if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: true });
|
|
||||||
}, [editor, initialValue]);
|
|
||||||
|
|
||||||
useImperativeHandle(forwardedRef, () => getEditorRefHelpers({ editor, provider }));
|
|
||||||
|
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return editor;
|
|
||||||
};
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { Editor } from "@tiptap/core";
|
import { Editor } from "@tiptap/core";
|
||||||
import { Plugin } from "@tiptap/pm/state";
|
import { Plugin } from "@tiptap/pm/state";
|
||||||
// types
|
// types
|
||||||
import { TFileHandler, TReadOnlyFileHandler } from "@/types";
|
import { TFileHandler } from "@/types";
|
||||||
// local imports
|
// local imports
|
||||||
import { TrackFileDeletionPlugin } from "./delete";
|
import { TrackFileDeletionPlugin } from "./delete";
|
||||||
import { TrackFileRestorationPlugin } from "./restore";
|
import { TrackFileRestorationPlugin } from "./restore";
|
||||||
|
|
||||||
type TArgs = {
|
type TArgs = {
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
fileHandler: TFileHandler | TReadOnlyFileHandler;
|
fileHandler: TFileHandler;
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import { EditorProps } from "@tiptap/pm/view";
|
||||||
// plane utils
|
// plane utils
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
|
|
||||||
export type TCoreEditorProps = {
|
type TArgs = {
|
||||||
editorClassName: string;
|
editorClassName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CoreEditorProps = (props: TCoreEditorProps): EditorProps => {
|
export const CoreEditorProps = (props: TArgs): EditorProps => {
|
||||||
const { editorClassName } = props;
|
const { editorClassName } = props;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export * from "./props";
|
|
||||||
export * from "./read-only";
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { EditorProps } from "@tiptap/pm/view";
|
|
||||||
// plane utils
|
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
// props
|
|
||||||
import { TCoreEditorProps } from "@/props";
|
|
||||||
|
|
||||||
export const CoreReadOnlyEditorProps = (props: TCoreEditorProps): EditorProps => {
|
|
||||||
const { editorClassName } = props;
|
|
||||||
|
|
||||||
return {
|
|
||||||
attributes: {
|
|
||||||
class: cn(
|
|
||||||
"prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none",
|
|
||||||
editorClassName
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
// plane imports
|
// plane imports
|
||||||
import { TWebhookConnectionQueryParams } from "@plane/types";
|
import { TWebhookConnectionQueryParams } from "@plane/types";
|
||||||
|
|
||||||
export type TReadOnlyFileHandler = {
|
export type TFileHandler = {
|
||||||
|
assetsUploadStatus: Record<string, number>; // blockId => progress percentage
|
||||||
|
cancel: () => void;
|
||||||
checkIfAssetExists: (assetId: string) => Promise<boolean>;
|
checkIfAssetExists: (assetId: string) => Promise<boolean>;
|
||||||
|
delete: (assetSrc: string) => Promise<void>;
|
||||||
getAssetDownloadSrc: (path: string) => Promise<string>;
|
getAssetDownloadSrc: (path: string) => Promise<string>;
|
||||||
getAssetSrc: (path: string) => Promise<string>;
|
getAssetSrc: (path: string) => Promise<string>;
|
||||||
restore: (assetSrc: string) => Promise<void>;
|
restore: (assetSrc: string) => Promise<void>;
|
||||||
};
|
|
||||||
|
|
||||||
export type TFileHandler = TReadOnlyFileHandler & {
|
|
||||||
assetsUploadStatus: Record<string, number>; // blockId => progress percentage
|
|
||||||
cancel: () => void;
|
|
||||||
delete: (assetSrc: string) => Promise<void>;
|
|
||||||
upload: (blockId: string, file: File) => Promise<string>;
|
upload: (blockId: string, file: File) => Promise<string>;
|
||||||
validation: {
|
validation: {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,6 @@ import type {
|
||||||
TExtensions,
|
TExtensions,
|
||||||
TFileHandler,
|
TFileHandler,
|
||||||
TMentionHandler,
|
TMentionHandler,
|
||||||
TReadOnlyFileHandler,
|
|
||||||
TReadOnlyMentionHandler,
|
|
||||||
TRealtimeConfig,
|
TRealtimeConfig,
|
||||||
TServerHandler,
|
TServerHandler,
|
||||||
TUserDetails,
|
TUserDetails,
|
||||||
|
|
@ -83,9 +81,12 @@ export type TDocumentInfo = {
|
||||||
words: number;
|
words: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// editor refs
|
export type EditorRefApi = {
|
||||||
export type EditorReadOnlyRefApi = {
|
blur: () => void;
|
||||||
clearEditor: (emitUpdate?: boolean) => void;
|
clearEditor: (emitUpdate?: boolean) => void;
|
||||||
|
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
|
||||||
|
executeMenuItemCommand: <T extends TEditorCommands>(props: TCommandWithPropsWithItemKey<T>) => void;
|
||||||
|
getCurrentCursorPosition: () => number | undefined;
|
||||||
getDocument: () => {
|
getDocument: () => {
|
||||||
binary: Uint8Array | null;
|
binary: Uint8Array | null;
|
||||||
html: string;
|
html: string;
|
||||||
|
|
@ -94,15 +95,6 @@ export type EditorReadOnlyRefApi = {
|
||||||
getDocumentInfo: () => TDocumentInfo;
|
getDocumentInfo: () => TDocumentInfo;
|
||||||
getHeadings: () => IMarking[];
|
getHeadings: () => IMarking[];
|
||||||
getMarkDown: () => string;
|
getMarkDown: () => string;
|
||||||
scrollSummary: (marking: IMarking) => void;
|
|
||||||
setEditorValue: (content: string, emitUpdate?: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface EditorRefApi extends EditorReadOnlyRefApi {
|
|
||||||
blur: () => void;
|
|
||||||
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
|
|
||||||
executeMenuItemCommand: <T extends TEditorCommands>(props: TCommandWithPropsWithItemKey<T>) => void;
|
|
||||||
getCurrentCursorPosition: () => number | undefined;
|
|
||||||
getSelectedText: () => string | null;
|
getSelectedText: () => string | null;
|
||||||
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
|
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
|
||||||
isEditorReadyToDiscard: () => boolean;
|
isEditorReadyToDiscard: () => boolean;
|
||||||
|
|
@ -111,12 +103,14 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
|
||||||
onDocumentInfoChange: (callback: (documentInfo: TDocumentInfo) => void) => () => void;
|
onDocumentInfoChange: (callback: (documentInfo: TDocumentInfo) => void) => () => void;
|
||||||
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
|
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
|
||||||
onStateChange: (callback: () => void) => () => void;
|
onStateChange: (callback: () => void) => () => void;
|
||||||
|
scrollSummary: (marking: IMarking) => void;
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void;
|
scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void;
|
||||||
|
setEditorValue: (content: string, emitUpdate?: boolean) => void;
|
||||||
setEditorValueAtCursorPosition: (content: string) => void;
|
setEditorValueAtCursorPosition: (content: string) => void;
|
||||||
setFocusAtPosition: (position: number) => void;
|
setFocusAtPosition: (position: number) => void;
|
||||||
setProviderDocument: (value: Uint8Array) => void;
|
setProviderDocument: (value: Uint8Array) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
// editor props
|
// editor props
|
||||||
export interface IEditorProps {
|
export interface IEditorProps {
|
||||||
|
|
@ -125,6 +119,7 @@ export interface IEditorProps {
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
displayConfig?: TDisplayConfig;
|
displayConfig?: TDisplayConfig;
|
||||||
disabledExtensions: TExtensions[];
|
disabledExtensions: TExtensions[];
|
||||||
|
editable: boolean;
|
||||||
editorClassName?: string;
|
editorClassName?: string;
|
||||||
extensions?: Extensions;
|
extensions?: Extensions;
|
||||||
flaggedExtensions: TExtensions[];
|
flaggedExtensions: TExtensions[];
|
||||||
|
|
@ -147,13 +142,11 @@ export type ILiteTextEditorProps = IEditorProps;
|
||||||
|
|
||||||
export type IRichTextEditorProps = IEditorProps & {
|
export type IRichTextEditorProps = IEditorProps & {
|
||||||
dragDropEnabled?: boolean;
|
dragDropEnabled?: boolean;
|
||||||
editable: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ICollaborativeDocumentEditorProps
|
export interface ICollaborativeDocumentEditorProps
|
||||||
extends Omit<IEditorProps, "extensions" | "initialValue" | "onEnterKeyPress" | "value"> {
|
extends Omit<IEditorProps, "extensions" | "initialValue" | "onEnterKeyPress" | "value"> {
|
||||||
aiHandler?: TAIHandler;
|
aiHandler?: TAIHandler;
|
||||||
editable: boolean;
|
|
||||||
embedHandler: TEmbedConfig;
|
embedHandler: TEmbedConfig;
|
||||||
realtimeConfig: TRealtimeConfig;
|
realtimeConfig: TRealtimeConfig;
|
||||||
serverHandler?: TServerHandler;
|
serverHandler?: TServerHandler;
|
||||||
|
|
@ -162,33 +155,11 @@ export interface ICollaborativeDocumentEditorProps
|
||||||
|
|
||||||
export interface IDocumentEditorProps extends Omit<IEditorProps, "initialValue" | "onEnterKeyPress" | "value"> {
|
export interface IDocumentEditorProps extends Omit<IEditorProps, "initialValue" | "onEnterKeyPress" | "value"> {
|
||||||
aiHandler?: TAIHandler;
|
aiHandler?: TAIHandler;
|
||||||
editable: boolean;
|
|
||||||
embedHandler: TEmbedConfig;
|
embedHandler: TEmbedConfig;
|
||||||
user?: TUserDetails;
|
user?: TUserDetails;
|
||||||
value: Content;
|
value: Content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// read only editor props
|
|
||||||
export interface IReadOnlyEditorProps
|
|
||||||
extends Pick<
|
|
||||||
IEditorProps,
|
|
||||||
| "containerClassName"
|
|
||||||
| "disabledExtensions"
|
|
||||||
| "flaggedExtensions"
|
|
||||||
| "displayConfig"
|
|
||||||
| "editorClassName"
|
|
||||||
| "extensions"
|
|
||||||
| "handleEditorReady"
|
|
||||||
| "id"
|
|
||||||
| "initialValue"
|
|
||||||
> {
|
|
||||||
fileHandler: TReadOnlyFileHandler;
|
|
||||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
|
||||||
mentionHandler: TReadOnlyMentionHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ILiteTextReadOnlyEditorProps = IReadOnlyEditorProps;
|
|
||||||
|
|
||||||
export interface EditorEvents {
|
export interface EditorEvents {
|
||||||
beforeCreate: never;
|
beforeCreate: never;
|
||||||
create: never;
|
create: never;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import type { HocuspocusProvider } from "@hocuspocus/provider";
|
||||||
import type { Content } from "@tiptap/core";
|
import type { Content } from "@tiptap/core";
|
||||||
import type { EditorProps } from "@tiptap/pm/view";
|
import type { EditorProps } from "@tiptap/pm/view";
|
||||||
// local imports
|
// local imports
|
||||||
import type { ICollaborativeDocumentEditorProps, IEditorProps, IReadOnlyEditorProps } from "./editor";
|
import type { ICollaborativeDocumentEditorProps, IEditorProps } from "./editor";
|
||||||
|
|
||||||
type TCoreHookProps = Pick<
|
type TCoreHookProps = Pick<
|
||||||
IEditorProps,
|
IEditorProps,
|
||||||
|
|
@ -47,7 +47,3 @@ export type TCollaborativeEditorHookProps = TCoreHookProps &
|
||||||
| "tabIndex"
|
| "tabIndex"
|
||||||
> &
|
> &
|
||||||
Pick<ICollaborativeDocumentEditorProps, "embedHandler" | "realtimeConfig" | "serverHandler" | "user">;
|
Pick<ICollaborativeDocumentEditorProps, "embedHandler" | "realtimeConfig" | "serverHandler" | "user">;
|
||||||
|
|
||||||
export type TReadOnlyEditorHookProps = TCoreHookProps &
|
|
||||||
Pick<TEditorHookProps, "initialValue" | "provider"> &
|
|
||||||
Pick<IReadOnlyEditorProps, "fileHandler" | "forwardedRef" | "mentionHandler">;
|
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,8 @@ export type TMentionSection = {
|
||||||
|
|
||||||
export type TMentionComponentProps = Pick<TMentionSuggestion, "entity_identifier" | "entity_name">;
|
export type TMentionComponentProps = Pick<TMentionSuggestion, "entity_identifier" | "entity_name">;
|
||||||
|
|
||||||
export type TReadOnlyMentionHandler = {
|
export type TMentionHandler = {
|
||||||
renderComponent: (props: TMentionComponentProps) => React.ReactNode;
|
|
||||||
getMentionedEntityDetails?: (entity_identifier: string) => { display_name: string } | undefined;
|
getMentionedEntityDetails?: (entity_identifier: string) => { display_name: string } | undefined;
|
||||||
};
|
renderComponent: (props: TMentionComponentProps) => React.ReactNode;
|
||||||
|
|
||||||
export type TMentionHandler = TReadOnlyMentionHandler & {
|
|
||||||
searchCallback?: (query: string) => Promise<TMentionSection[]>;
|
searchCallback?: (query: string) => Promise<TMentionSection[]>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ export {
|
||||||
CollaborativeDocumentEditorWithRef,
|
CollaborativeDocumentEditorWithRef,
|
||||||
DocumentEditorWithRef,
|
DocumentEditorWithRef,
|
||||||
LiteTextEditorWithRef,
|
LiteTextEditorWithRef,
|
||||||
LiteTextReadOnlyEditorWithRef,
|
|
||||||
RichTextEditorWithRef,
|
RichTextEditorWithRef,
|
||||||
} from "@/components/editors";
|
} from "@/components/editors";
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue