[WIKI-788] fix: editor markdown copy rules (#8140)
This commit is contained in:
parent
f510020daa
commit
d462546055
32 changed files with 1467 additions and 189 deletions
|
|
@ -7,6 +7,7 @@ import { cn } from "@plane/utils";
|
|||
// hooks
|
||||
import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
// local imports
|
||||
|
|
@ -14,7 +15,7 @@ import { EditorMentionsRoot } from "../embeds/mentions";
|
|||
|
||||
type DocumentEditorWrapperProps = MakeOptional<
|
||||
Omit<IDocumentEditorProps, "fileHandler" | "mentionHandler" | "user" | "extendedEditorProps">,
|
||||
"disabledExtensions" | "editable" | "flaggedExtensions"
|
||||
"disabledExtensions" | "editable" | "flaggedExtensions" | "getEditorMetaData"
|
||||
> & {
|
||||
extendedEditorProps?: Partial<IEditorPropsExtended>;
|
||||
workspaceSlug: string;
|
||||
|
|
@ -44,6 +45,11 @@ export const DocumentEditor = forwardRef<EditorRefApi, DocumentEditorWrapperProp
|
|||
} = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
// parse content
|
||||
const { getEditorMetaData } = useParseEditorContent({
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
// editor flaggings
|
||||
const { document: documentEditorExtensions } = useEditorFlagging({
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
|
|
@ -68,6 +74,7 @@ export const DocumentEditor = forwardRef<EditorRefApi, DocumentEditorWrapperProp
|
|||
workspaceId,
|
||||
workspaceSlug,
|
||||
})}
|
||||
getEditorMetaData={getEditorMetaData}
|
||||
mentionHandler={{
|
||||
searchCallback: async (query) => {
|
||||
const res = await fetchMentions(query);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { IssueCommentToolbar } from "@/components/editor/lite-text/toolbar";
|
|||
// hooks
|
||||
import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
// plane web service
|
||||
|
|
@ -22,7 +23,7 @@ const workspaceService = new WorkspaceService();
|
|||
|
||||
type LiteTextEditorWrapperProps = MakeOptional<
|
||||
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler" | "extendedEditorProps">,
|
||||
"disabledExtensions" | "flaggedExtensions"
|
||||
"disabledExtensions" | "flaggedExtensions" | "getEditorMetaData"
|
||||
> & {
|
||||
workspaceSlug: string;
|
||||
workspaceId: string;
|
||||
|
|
@ -80,6 +81,11 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
|||
});
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
// parse content
|
||||
const { getEditorMetaData } = useParseEditorContent({
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
// use editor mention
|
||||
const { fetchMentions } = useEditorMention({
|
||||
searchEntity: async (payload) =>
|
||||
|
|
@ -124,6 +130,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
|||
workspaceId,
|
||||
workspaceSlug,
|
||||
})}
|
||||
getEditorMetaData={getEditorMetaData}
|
||||
handleEditorReady={(ready) => {
|
||||
if (ready) {
|
||||
setEditorRef(isMutableRefObject<EditorRefApi>(ref) ? ref.current : null);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import { forwardRef } from "react";
|
||||
// plane imports
|
||||
import { RichTextEditorWithRef } from "@plane/editor";
|
||||
import type { EditorRefApi, IRichTextEditorProps, TFileHandler } from "@plane/editor";
|
||||
|
|
@ -9,12 +9,13 @@ import { EditorMentionsRoot } from "@/components/editor/embeds/mentions";
|
|||
// hooks
|
||||
import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
|
||||
type RichTextEditorWrapperProps = MakeOptional<
|
||||
Omit<IRichTextEditorProps, "fileHandler" | "mentionHandler" | "extendedEditorProps">,
|
||||
"disabledExtensions" | "editable" | "flaggedExtensions"
|
||||
"disabledExtensions" | "editable" | "flaggedExtensions" | "getEditorMetaData"
|
||||
> & {
|
||||
workspaceSlug: string;
|
||||
workspaceId: string;
|
||||
|
|
@ -53,6 +54,11 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
|||
});
|
||||
// editor config
|
||||
const { getEditorFileHandlers } = useEditorConfig();
|
||||
// parse content
|
||||
const { getEditorMetaData } = useParseEditorContent({
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
|
||||
return (
|
||||
<RichTextEditorWithRef
|
||||
|
|
@ -66,6 +72,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
|||
workspaceId,
|
||||
workspaceSlug,
|
||||
})}
|
||||
getEditorMetaData={getEditorMetaData}
|
||||
mentionHandler={{
|
||||
searchCallback: async (query) => {
|
||||
const res = await fetchMentions(query);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type { TSticky } from "@plane/types";
|
|||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useEditorConfig } from "@/hooks/editor";
|
||||
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { StickyEditorToolbar } from "./toolbar";
|
||||
|
|
@ -17,7 +18,7 @@ import { StickyEditorToolbar } from "./toolbar";
|
|||
interface StickyEditorWrapperProps
|
||||
extends Omit<
|
||||
Omit<ILiteTextEditorProps, "extendedEditorProps">,
|
||||
"disabledExtensions" | "editable" | "flaggedExtensions" | "fileHandler" | "mentionHandler"
|
||||
"disabledExtensions" | "editable" | "flaggedExtensions" | "fileHandler" | "mentionHandler" | "getEditorMetaData"
|
||||
> {
|
||||
workspaceSlug: string;
|
||||
workspaceId: string;
|
||||
|
|
@ -55,6 +56,11 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
|||
const { liteText: liteTextEditorExtensions } = useEditorFlagging({
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
});
|
||||
// parse content
|
||||
const { getEditorMetaData } = useParseEditorContent({
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
// editor config
|
||||
const { getEditorFileHandlers } = useEditorConfig();
|
||||
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
|
||||
|
|
@ -62,6 +68,7 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
|||
}
|
||||
// derived values
|
||||
const editorRef = isMutableRefObject<EditorRefApi>(ref) ? ref.current : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("relative border border-custom-border-200 rounded", parentClassName)}
|
||||
|
|
@ -79,6 +86,7 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
|||
workspaceId,
|
||||
workspaceSlug,
|
||||
})}
|
||||
getEditorMetaData={getEditorMetaData}
|
||||
mentionHandler={{
|
||||
renderComponent: () => <></>,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { useMember } from "@/hooks/store/use-member";
|
|||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
// plane web imports
|
||||
import { EditorAIMenu } from "@/plane-web/components/pages";
|
||||
import type { TExtendedEditorExtensionsConfig } from "@/plane-web/hooks/pages";
|
||||
|
|
@ -57,10 +58,9 @@ type Props = {
|
|||
isNavigationPaneOpen: boolean;
|
||||
page: TPageInstance;
|
||||
webhookConnectionParams: TWebhookConnectionQueryParams;
|
||||
projectId: string;
|
||||
projectId?: string;
|
||||
workspaceSlug: string;
|
||||
storeType: EPageStoreType;
|
||||
|
||||
extendedEditorProps: TExtendedEditorExtensionsConfig;
|
||||
};
|
||||
|
||||
|
|
@ -103,6 +103,11 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||
workspaceSlug,
|
||||
storeType,
|
||||
});
|
||||
// parse content
|
||||
const { getEditorMetaData } = useParseEditorContent({
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
// page filters
|
||||
const { fontSize, fontStyle, isFullWidth } = usePageFilters();
|
||||
// translation
|
||||
|
|
@ -235,6 +240,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||
ref={editorForwardRef}
|
||||
containerClassName="h-full p-0 pb-64"
|
||||
displayConfig={displayConfig}
|
||||
getEditorMetaData={getEditorMetaData}
|
||||
mentionHandler={{
|
||||
searchCallback: async (query) => {
|
||||
const res = await fetchMentions(query);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { PageEditorHeaderLogoPicker } from "./logo-picker";
|
|||
|
||||
type Props = {
|
||||
page: TPageInstance;
|
||||
projectId: string;
|
||||
projectId?: string;
|
||||
};
|
||||
|
||||
export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ type TPageRootProps = {
|
|||
page: TPageInstance;
|
||||
storeType: EPageStoreType;
|
||||
webhookConnectionParams: TWebhookConnectionQueryParams;
|
||||
projectId: string;
|
||||
projectId?: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useState } from "react";
|
|||
import type { PageProps } from "@react-pdf/renderer";
|
||||
import { pdf } from "@react-pdf/renderer";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useParams } from "react-router";
|
||||
// plane editor
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
// plane ui
|
||||
|
|
@ -100,13 +101,17 @@ export const ExportPageModal: React.FC<Props> = (props) => {
|
|||
const { editorRef, isOpen, onClose, pageTitle } = props;
|
||||
// states
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
// params
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// form info
|
||||
const { control, reset, watch } = useForm<TFormValues>({
|
||||
defaultValues,
|
||||
});
|
||||
// parse editor content
|
||||
const { replaceCustomComponentsFromHTMLContent, replaceCustomComponentsFromMarkdownContent } =
|
||||
useParseEditorContent();
|
||||
const { replaceCustomComponentsFromHTMLContent, replaceCustomComponentsFromMarkdownContent } = useParseEditorContent({
|
||||
projectId,
|
||||
workspaceSlug: workspaceSlug ?? "",
|
||||
});
|
||||
// derived values
|
||||
const selectedExportFormat = watch("export_format");
|
||||
const selectedPageFormat = watch("page_format");
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
import { useCallback } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane types
|
||||
import type { TSearchEntities } from "@plane/types";
|
||||
// helpers
|
||||
import { getBase64Image } from "@plane/utils";
|
||||
import { getBase64Image, getEditorAssetSrc } from "@plane/utils";
|
||||
import type { TCustomComponentsMetaData } from "@plane/utils";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
// plane web hooks
|
||||
import { useAdditionalEditorMention } from "@/plane-web/hooks/use-additional-editor-mention";
|
||||
|
||||
export const useParseEditorContent = () => {
|
||||
// params
|
||||
const { workspaceSlug } = useParams();
|
||||
type TArgs = {
|
||||
projectId?: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const useParseEditorContent = (args: TArgs) => {
|
||||
const { projectId, workspaceSlug } = args;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
// parse additional content
|
||||
|
|
@ -150,7 +154,7 @@ export const useParseEditorContent = () => {
|
|||
serializedDoc = serializedDoc.replace(/background-color: null/g, "").replace(/color: null/g, "");
|
||||
return serializedDoc;
|
||||
},
|
||||
[getUserDetails]
|
||||
[getUserDetails, parseAdditionalEditorContent]
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
@ -160,7 +164,6 @@ export const useParseEditorContent = () => {
|
|||
*/
|
||||
const replaceCustomComponentsFromMarkdownContent = useCallback(
|
||||
(props: { markdownContent: string; noAssets?: boolean }): string => {
|
||||
const start = performance.now();
|
||||
const { markdownContent, noAssets = false } = props;
|
||||
let parsedMarkdownContent = markdownContent;
|
||||
// replace the matched mention components with [display_name](redirect_url)
|
||||
|
|
@ -203,15 +206,68 @@ export const useParseEditorContent = () => {
|
|||
// remove all issue-embed components
|
||||
const issueEmbedRegex = /<issue-embed-component[^>]*>[^]*<\/issue-embed-component>/g;
|
||||
parsedMarkdownContent = parsedMarkdownContent.replace(issueEmbedRegex, "");
|
||||
const end = performance.now();
|
||||
console.log("Exec time:", end - start);
|
||||
return parsedMarkdownContent;
|
||||
},
|
||||
[getUserDetails, workspaceSlug]
|
||||
[getUserDetails, parseAdditionalEditorContent, workspaceSlug]
|
||||
);
|
||||
|
||||
const getEditorMetaData = useCallback(
|
||||
(htmlContent: string): TCustomComponentsMetaData => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlContent, "text/html");
|
||||
const imageMetaData: TCustomComponentsMetaData["file_assets"] = [];
|
||||
// process image components
|
||||
const imageComponents = doc.querySelectorAll("image-component");
|
||||
imageComponents.forEach((element) => {
|
||||
const src = element.getAttribute("src");
|
||||
if (src) {
|
||||
const assetSrc = src.startsWith("http")
|
||||
? src
|
||||
: getEditorAssetSrc({
|
||||
assetId: src,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
if (assetSrc) {
|
||||
imageMetaData.push({
|
||||
id: src,
|
||||
name: src,
|
||||
url: assetSrc,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
// process user mentions
|
||||
const userMentions: TCustomComponentsMetaData["user_mentions"] = [];
|
||||
const mentionComponents = doc.querySelectorAll("mention-component");
|
||||
mentionComponents.forEach((element) => {
|
||||
const id = element.getAttribute("entity_identifier");
|
||||
if (id) {
|
||||
const userDetails = getUserDetails(id);
|
||||
const originUrl = typeof window !== "undefined" && (window.location.origin ?? "");
|
||||
const path = `${workspaceSlug}/profile/${id}`;
|
||||
const url = `${originUrl}/${path}`;
|
||||
if (userDetails) {
|
||||
userMentions.push({
|
||||
id,
|
||||
display_name: userDetails.display_name,
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
file_assets: imageMetaData,
|
||||
user_mentions: userMentions,
|
||||
};
|
||||
},
|
||||
[getUserDetails, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
return {
|
||||
replaceCustomComponentsFromHTMLContent,
|
||||
replaceCustomComponentsFromMarkdownContent,
|
||||
getEditorMetaData,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue