mirror of
https://github.com/gosticks/plane.git
synced 2025-10-16 12:45:33 +00:00
[WIKI-511] [WIKI-572] fix: extended page root and editor body (#7661)
* fix: better refactoring of page root and editor body * fix: editor sideeffects * fix: add comments json field * fix: props name * fix: review changes * fix: extra checks * fix: type changes * fix: remove editor ref current * fix: renaming * fix: link check
This commit is contained in:
parent
f42eeec2c0
commit
fd5ba6c7b8
@ -2,17 +2,16 @@ import React from "react";
|
|||||||
// plane imports
|
// plane imports
|
||||||
import { type EditorRefApi, type ILiteTextEditorProps, LiteTextEditorWithRef, type TFileHandler } from "@plane/editor";
|
import { type EditorRefApi, type ILiteTextEditorProps, LiteTextEditorWithRef, type TFileHandler } from "@plane/editor";
|
||||||
import type { MakeOptional } from "@plane/types";
|
import type { MakeOptional } from "@plane/types";
|
||||||
import { cn } from "@plane/utils";
|
import { cn, isCommentEmpty } from "@plane/utils";
|
||||||
// helpers
|
// helpers
|
||||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||||
import { isCommentEmpty } from "@/helpers/string.helper";
|
|
||||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||||
// local imports
|
// local imports
|
||||||
import { EditorMentionsRoot } from "./embeds/mentions";
|
import { EditorMentionsRoot } from "./embeds/mentions";
|
||||||
import { IssueCommentToolbar } from "./toolbar";
|
import { IssueCommentToolbar } from "./toolbar";
|
||||||
|
|
||||||
type LiteTextEditorWrapperProps = MakeOptional<
|
type LiteTextEditorWrapperProps = MakeOptional<
|
||||||
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler">,
|
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler" | "extendedEditorProps">,
|
||||||
"disabledExtensions" | "flaggedExtensions"
|
"disabledExtensions" | "flaggedExtensions"
|
||||||
> & {
|
> & {
|
||||||
anchor: string;
|
anchor: string;
|
||||||
@ -63,6 +62,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
|||||||
mentionHandler={{
|
mentionHandler={{
|
||||||
renderComponent: (props) => <EditorMentionsRoot {...props} />,
|
renderComponent: (props) => <EditorMentionsRoot {...props} />,
|
||||||
}}
|
}}
|
||||||
|
extendedEditorProps={{}}
|
||||||
{...rest}
|
{...rest}
|
||||||
// overriding the containerClassName to add relative class passed
|
// overriding the containerClassName to add relative class passed
|
||||||
containerClassName={cn(containerClassName, "relative")}
|
containerClassName={cn(containerClassName, "relative")}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
|||||||
import { EditorMentionsRoot } from "./embeds/mentions";
|
import { EditorMentionsRoot } from "./embeds/mentions";
|
||||||
|
|
||||||
type RichTextEditorWrapperProps = MakeOptional<
|
type RichTextEditorWrapperProps = MakeOptional<
|
||||||
Omit<IRichTextEditorProps, "editable" | "fileHandler" | "mentionHandler">,
|
Omit<IRichTextEditorProps, "editable" | "fileHandler" | "mentionHandler" | "extendedEditorProps">,
|
||||||
"disabledExtensions" | "flaggedExtensions"
|
"disabledExtensions" | "flaggedExtensions"
|
||||||
> & {
|
> & {
|
||||||
anchor: string;
|
anchor: string;
|
||||||
@ -56,6 +56,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
})}
|
})}
|
||||||
flaggedExtensions={richTextEditorExtensions.flagged}
|
flaggedExtensions={richTextEditorExtensions.flagged}
|
||||||
|
extendedEditorProps={{}}
|
||||||
{...rest}
|
{...rest}
|
||||||
containerClassName={containerClassName}
|
containerClassName={containerClassName}
|
||||||
editorClassName="min-h-[100px] py-2 overflow-hidden"
|
editorClassName="min-h-[100px] py-2 overflow-hidden"
|
||||||
|
|||||||
@ -57,24 +57,6 @@ export const isEmptyHtmlString = (htmlString: string, allowedHTMLTags: string[]
|
|||||||
return cleanText.trim() === "";
|
return cleanText.trim() === "";
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @description this function returns whether a comment is empty or not by checking for the following conditions-
|
|
||||||
* 1. If comment is undefined
|
|
||||||
* 2. If comment is an empty string
|
|
||||||
* 3. If comment is "<p></p>"
|
|
||||||
* @param {string | undefined} comment
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
export const isCommentEmpty = (comment: string | undefined): boolean => {
|
|
||||||
// return true if comment is undefined
|
|
||||||
if (!comment) return true;
|
|
||||||
return (
|
|
||||||
comment?.trim() === "" ||
|
|
||||||
comment === "<p></p>" ||
|
|
||||||
isEmptyHtmlString(comment ?? "", ["img", "mention-component", "image-component", "embed-component"])
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
|
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
|
||||||
|
|
||||||
export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
@ -20,6 +20,7 @@ import { PageRoot, TPageRootConfig, TPageRootHandlers } from "@/components/pages
|
|||||||
import { useEditorConfig } from "@/hooks/editor";
|
import { useEditorConfig } from "@/hooks/editor";
|
||||||
import { useEditorAsset } from "@/hooks/store/use-editor-asset";
|
import { useEditorAsset } from "@/hooks/store/use-editor-asset";
|
||||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||||
|
import { useAppRouter } from "@/hooks/use-app-router";
|
||||||
// plane web hooks
|
// plane web hooks
|
||||||
import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store";
|
import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store";
|
||||||
// plane web services
|
// plane web services
|
||||||
@ -30,13 +31,17 @@ const workspaceService = new WorkspaceService();
|
|||||||
const projectPageService = new ProjectPageService();
|
const projectPageService = new ProjectPageService();
|
||||||
const projectPageVersionService = new ProjectPageVersionService();
|
const projectPageVersionService = new ProjectPageVersionService();
|
||||||
|
|
||||||
|
const storeType = EPageStoreType.PROJECT;
|
||||||
|
|
||||||
const PageDetailsPage = observer(() => {
|
const PageDetailsPage = observer(() => {
|
||||||
|
// router
|
||||||
|
const router = useAppRouter();
|
||||||
const { workspaceSlug, projectId, pageId } = useParams();
|
const { workspaceSlug, projectId, pageId } = useParams();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { createPage, fetchPageDetails } = usePageStore(EPageStoreType.PROJECT);
|
const { createPage, fetchPageDetails } = usePageStore(storeType);
|
||||||
const page = usePage({
|
const page = usePage({
|
||||||
pageId: pageId?.toString() ?? "",
|
pageId: pageId?.toString() ?? "",
|
||||||
storeType: EPageStoreType.PROJECT,
|
storeType,
|
||||||
});
|
});
|
||||||
const { getWorkspaceBySlug } = useWorkspace();
|
const { getWorkspaceBySlug } = useWorkspace();
|
||||||
const { uploadEditorAsset } = useEditorAsset();
|
const { uploadEditorAsset } = useEditorAsset();
|
||||||
@ -88,10 +93,25 @@ const PageDetailsPage = observer(() => {
|
|||||||
versionId
|
versionId
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getRedirectionLink: (pageId) => `/${workspaceSlug}/projects/${projectId}/pages/${pageId}`,
|
restoreVersion: async (pageId, versionId) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
await projectPageVersionService.restoreVersion(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
pageId,
|
||||||
|
versionId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getRedirectionLink: (pageId) => {
|
||||||
|
if (pageId) {
|
||||||
|
return `/${workspaceSlug}/projects/${projectId}/pages/${pageId}`;
|
||||||
|
} else {
|
||||||
|
return `/${workspaceSlug}/projects/${projectId}/pages`;
|
||||||
|
}
|
||||||
|
},
|
||||||
updateDescription: updateDescription ?? (async () => {}),
|
updateDescription: updateDescription ?? (async () => {}),
|
||||||
}),
|
}),
|
||||||
[createPage, fetchEntityCallback, id, projectId, updateDescription, workspaceSlug]
|
[createPage, fetchEntityCallback, id, updateDescription, workspaceSlug, projectId]
|
||||||
);
|
);
|
||||||
// page root config
|
// page root config
|
||||||
const pageRootConfig: TPageRootConfig = useMemo(
|
const pageRootConfig: TPageRootConfig = useMemo(
|
||||||
@ -115,7 +135,7 @@ const PageDetailsPage = observer(() => {
|
|||||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
[getEditorFileHandlers, id, projectId, uploadEditorAsset, workspaceId, workspaceSlug]
|
[getEditorFileHandlers, id, uploadEditorAsset, projectId, workspaceId, workspaceSlug]
|
||||||
);
|
);
|
||||||
|
|
||||||
const webhookConnectionParams: TWebhookConnectionQueryParams = useMemo(
|
const webhookConnectionParams: TWebhookConnectionQueryParams = useMemo(
|
||||||
@ -127,6 +147,12 @@ const PageDetailsPage = observer(() => {
|
|||||||
[projectId, workspaceSlug]
|
[projectId, workspaceSlug]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (page?.deleted_at && page?.id) {
|
||||||
|
router.push(pageRootHandlers.getRedirectionLink());
|
||||||
|
}
|
||||||
|
}, [page?.deleted_at, page?.id, router, pageRootHandlers]);
|
||||||
|
|
||||||
if ((!page || !id) && !pageDetailsError)
|
if ((!page || !id) && !pageDetailsError)
|
||||||
return (
|
return (
|
||||||
<div className="size-full grid place-items-center">
|
<div className="size-full grid place-items-center">
|
||||||
@ -150,7 +176,7 @@ const PageDetailsPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!page) return null;
|
if (!page || !workspaceSlug || !projectId) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -160,9 +186,11 @@ const PageDetailsPage = observer(() => {
|
|||||||
<PageRoot
|
<PageRoot
|
||||||
config={pageRootConfig}
|
config={pageRootConfig}
|
||||||
handlers={pageRootHandlers}
|
handlers={pageRootHandlers}
|
||||||
|
storeType={storeType}
|
||||||
page={page}
|
page={page}
|
||||||
webhookConnectionParams={webhookConnectionParams}
|
webhookConnectionParams={webhookConnectionParams}
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId?.toString()}
|
||||||
/>
|
/>
|
||||||
<IssuePeekOverview />
|
<IssuePeekOverview />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export * from "./move-page-modal";
|
export * from "./move-page-modal";
|
||||||
|
export * from "./modals";
|
||||||
|
|||||||
15
apps/web/ce/components/pages/modals/modals.tsx
Normal file
15
apps/web/ce/components/pages/modals/modals.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
// components
|
||||||
|
import { EPageStoreType } from "@/plane-web/hooks/store";
|
||||||
|
// store
|
||||||
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
|
export type TPageModalsProps = {
|
||||||
|
page: TPageInstance;
|
||||||
|
storeType: EPageStoreType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageModals: React.FC<TPageModalsProps> = observer((props) => null);
|
||||||
2
apps/web/ce/hooks/pages/index.ts
Normal file
2
apps/web/ce/hooks/pages/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./use-pages-pane-extensions";
|
||||||
|
export * from "./use-extended-editor-extensions";
|
||||||
20
apps/web/ce/hooks/pages/use-extended-editor-extensions.ts
Normal file
20
apps/web/ce/hooks/pages/use-extended-editor-extensions.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { IEditorPropsExtended } from "@plane/editor";
|
||||||
|
import type { TSearchEntityRequestPayload, TSearchResponse } from "@plane/types";
|
||||||
|
import type { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
import { EPageStoreType } from "../store";
|
||||||
|
|
||||||
|
export type TExtendedEditorExtensionsHookParams = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
page: TPageInstance;
|
||||||
|
storeType: EPageStoreType;
|
||||||
|
fetchEntity: (payload: TSearchEntityRequestPayload) => Promise<TSearchResponse>;
|
||||||
|
getRedirectionLink: (pageId?: string) => string;
|
||||||
|
extensionHandlers?: Map<string, unknown>;
|
||||||
|
projectId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TExtendedEditorExtensionsConfig = IEditorPropsExtended;
|
||||||
|
|
||||||
|
export const useExtendedEditorProps = (
|
||||||
|
_params: TExtendedEditorExtensionsHookParams
|
||||||
|
): TExtendedEditorExtensionsConfig => ({});
|
||||||
52
apps/web/ce/hooks/pages/use-pages-pane-extensions.ts
Normal file
52
apps/web/ce/hooks/pages/use-pages-pane-extensions.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useCallback, useMemo, type RefObject } from "react";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import type { EditorRefApi } from "@plane/editor";
|
||||||
|
import {
|
||||||
|
PAGE_NAVIGATION_PANE_TAB_KEYS,
|
||||||
|
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM,
|
||||||
|
} from "@/components/pages/navigation-pane";
|
||||||
|
import { useAppRouter } from "@/hooks/use-app-router";
|
||||||
|
import { useQueryParams } from "@/hooks/use-query-params";
|
||||||
|
import type { TPageNavigationPaneTab } from "@/plane-web/components/pages/navigation-pane";
|
||||||
|
import { INavigationPaneExtension } from "@/plane-web/types/pages/pane-extensions";
|
||||||
|
import type { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
|
export type TPageExtensionHookParams = {
|
||||||
|
page: TPageInstance;
|
||||||
|
editorRef: RefObject<EditorRefApi>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePagesPaneExtensions = (_params: TPageExtensionHookParams) => {
|
||||||
|
const router = useAppRouter();
|
||||||
|
const { updateQueryParams } = useQueryParams();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// Generic navigation pane logic - hook manages feature-specific routing
|
||||||
|
const navigationPaneQueryParam = searchParams.get(
|
||||||
|
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM
|
||||||
|
) as TPageNavigationPaneTab | null;
|
||||||
|
|
||||||
|
const isNavigationPaneOpen =
|
||||||
|
!!navigationPaneQueryParam && PAGE_NAVIGATION_PANE_TAB_KEYS.includes(navigationPaneQueryParam);
|
||||||
|
|
||||||
|
const handleOpenNavigationPane = useCallback(() => {
|
||||||
|
const updatedRoute = updateQueryParams({
|
||||||
|
paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: "outline" },
|
||||||
|
});
|
||||||
|
router.push(updatedRoute);
|
||||||
|
}, [router, updateQueryParams]);
|
||||||
|
|
||||||
|
const editorExtensionHandlers: Map<string, unknown> = useMemo(() => {
|
||||||
|
const map: Map<string, unknown> = new Map();
|
||||||
|
return map;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const navigationPaneExtensions: INavigationPaneExtension[] = [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
editorExtensionHandlers,
|
||||||
|
navigationPaneExtensions,
|
||||||
|
handleOpenNavigationPane,
|
||||||
|
isNavigationPaneOpen,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
// editor
|
// editor
|
||||||
import type { TExtensions } from "@plane/editor";
|
import type { TExtensions } from "@plane/editor";
|
||||||
|
import { EPageStoreType } from "@/plane-web/hooks/store";
|
||||||
|
|
||||||
export type TEditorFlaggingHookReturnType = {
|
export type TEditorFlaggingHookReturnType = {
|
||||||
document: {
|
document: {
|
||||||
@ -16,10 +17,15 @@ export type TEditorFlaggingHookReturnType = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TEditorFlaggingHookProps = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
storeType?: EPageStoreType;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description extensions disabled in various editors
|
* @description extensions disabled in various editors
|
||||||
*/
|
*/
|
||||||
export const useEditorFlagging = (workspaceSlug: string): TEditorFlaggingHookReturnType => ({
|
export const useEditorFlagging = (props: TEditorFlaggingHookProps): TEditorFlaggingHookReturnType => ({
|
||||||
document: {
|
document: {
|
||||||
disabled: ["ai", "collaboration-cursor"],
|
disabled: ["ai", "collaboration-cursor"],
|
||||||
flagged: [],
|
flagged: [],
|
||||||
|
|||||||
16
apps/web/ce/types/pages/pane-extensions.ts
Normal file
16
apps/web/ce/types/pages/pane-extensions.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import {
|
||||||
|
type INavigationPaneExtension as ICoreNavigationPaneExtension,
|
||||||
|
type INavigationPaneExtensionComponent,
|
||||||
|
} from "@/components/pages/navigation-pane";
|
||||||
|
|
||||||
|
// EE Union/map of extension data types (keyed by extension id)
|
||||||
|
export type TNavigationPaneExtensionData = Record<string, unknown>;
|
||||||
|
|
||||||
|
// EE Navigation pane extension configuration
|
||||||
|
export interface INavigationPaneExtension<
|
||||||
|
T extends keyof TNavigationPaneExtensionData = keyof TNavigationPaneExtensionData,
|
||||||
|
> extends Omit<ICoreNavigationPaneExtension<TNavigationPaneExtensionData[T]>, "id" | "data" | "component"> {
|
||||||
|
id: T;
|
||||||
|
component: INavigationPaneExtensionComponent<TNavigationPaneExtensionData[T]>;
|
||||||
|
data?: TNavigationPaneExtensionData[T];
|
||||||
|
}
|
||||||
@ -13,7 +13,7 @@ import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed";
|
|||||||
import { EditorMentionsRoot } from "../embeds/mentions";
|
import { EditorMentionsRoot } from "../embeds/mentions";
|
||||||
|
|
||||||
type DocumentEditorWrapperProps = MakeOptional<
|
type DocumentEditorWrapperProps = MakeOptional<
|
||||||
Omit<IDocumentEditorProps, "fileHandler" | "mentionHandler" | "embedHandler" | "user">,
|
Omit<IDocumentEditorProps, "fileHandler" | "mentionHandler" | "embedHandler" | "user" | "extendedEditorProps">,
|
||||||
"disabledExtensions" | "editable" | "flaggedExtensions"
|
"disabledExtensions" | "editable" | "flaggedExtensions"
|
||||||
> & {
|
> & {
|
||||||
embedHandler?: Partial<IDocumentEditorProps["embedHandler"]>;
|
embedHandler?: Partial<IDocumentEditorProps["embedHandler"]>;
|
||||||
@ -45,7 +45,9 @@ export const DocumentEditor = forwardRef<EditorRefApi, DocumentEditorWrapperProp
|
|||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
// editor flaggings
|
// editor flaggings
|
||||||
const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug);
|
const { document: documentEditorExtensions } = useEditorFlagging({
|
||||||
|
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||||
|
});
|
||||||
// use editor mention
|
// use editor mention
|
||||||
const { fetchMentions } = useEditorMention({
|
const { fetchMentions } = useEditorMention({
|
||||||
searchEntity: editable ? async (payload) => await props.searchMentionCallback(payload) : async () => ({}),
|
searchEntity: editable ? async (payload) => await props.searchMentionCallback(payload) : async () => ({}),
|
||||||
@ -83,6 +85,7 @@ export const DocumentEditor = forwardRef<EditorRefApi, DocumentEditorWrapperProp
|
|||||||
issue: issueEmbedProps,
|
issue: issueEmbedProps,
|
||||||
...embedHandler,
|
...embedHandler,
|
||||||
}}
|
}}
|
||||||
|
extendedEditorProps={{}}
|
||||||
{...rest}
|
{...rest}
|
||||||
containerClassName={cn("relative pl-3 pb-3", containerClassName)}
|
containerClassName={cn("relative pl-3 pb-3", containerClassName)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { WorkspaceService } from "@/plane-web/services";
|
|||||||
const workspaceService = new WorkspaceService();
|
const workspaceService = new WorkspaceService();
|
||||||
|
|
||||||
type LiteTextEditorWrapperProps = MakeOptional<
|
type LiteTextEditorWrapperProps = MakeOptional<
|
||||||
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler">,
|
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler" | "extendedEditorProps">,
|
||||||
"disabledExtensions" | "flaggedExtensions"
|
"disabledExtensions" | "flaggedExtensions"
|
||||||
> & {
|
> & {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -68,7 +68,9 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
|||||||
// states
|
// states
|
||||||
const [isFocused, setIsFocused] = useState(showToolbarInitially);
|
const [isFocused, setIsFocused] = useState(showToolbarInitially);
|
||||||
// editor flaggings
|
// editor flaggings
|
||||||
const { liteText: liteTextEditorExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
const { liteText: liteTextEditorExtensions } = useEditorFlagging({
|
||||||
|
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||||
|
});
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
// use editor mention
|
// use editor mention
|
||||||
@ -126,6 +128,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
|||||||
containerClassName={cn(containerClassName, "relative", {
|
containerClassName={cn(containerClassName, "relative", {
|
||||||
"p-2": !editable,
|
"p-2": !editable,
|
||||||
})}
|
})}
|
||||||
|
extendedEditorProps={{}}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
{showToolbar && editable && (
|
{showToolbar && editable && (
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { useMember } from "@/hooks/store/use-member";
|
|||||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||||
|
|
||||||
type RichTextEditorWrapperProps = MakeOptional<
|
type RichTextEditorWrapperProps = MakeOptional<
|
||||||
Omit<IRichTextEditorProps, "fileHandler" | "mentionHandler">,
|
Omit<IRichTextEditorProps, "fileHandler" | "mentionHandler" | "extendedEditorProps">,
|
||||||
"disabledExtensions" | "editable" | "flaggedExtensions"
|
"disabledExtensions" | "editable" | "flaggedExtensions"
|
||||||
> & {
|
> & {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -42,7 +42,9 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
|||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
// editor flaggings
|
// editor flaggings
|
||||||
const { richText: richTextEditorExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
const { richText: richTextEditorExtensions } = useEditorFlagging({
|
||||||
|
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||||
|
});
|
||||||
// use editor mention
|
// use editor mention
|
||||||
const { fetchMentions } = useEditorMention({
|
const { fetchMentions } = useEditorMention({
|
||||||
searchEntity: editable ? async (payload) => await props.searchMentionCallback(payload) : async () => ({}),
|
searchEntity: editable ? async (payload) => await props.searchMentionCallback(payload) : async () => ({}),
|
||||||
@ -73,6 +75,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
|||||||
display_name: getUserDetails(id)?.display_name ?? "",
|
display_name: getUserDetails(id)?.display_name ?? "",
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
|
extendedEditorProps={{}}
|
||||||
{...rest}
|
{...rest}
|
||||||
containerClassName={cn("relative pl-3 pb-3", containerClassName)}
|
containerClassName={cn("relative pl-3 pb-3", containerClassName)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { StickyEditorToolbar } from "./toolbar";
|
|||||||
|
|
||||||
interface StickyEditorWrapperProps
|
interface StickyEditorWrapperProps
|
||||||
extends Omit<
|
extends Omit<
|
||||||
ILiteTextEditorProps,
|
Omit<ILiteTextEditorProps, "extendedEditorProps">,
|
||||||
"disabledExtensions" | "editable" | "flaggedExtensions" | "fileHandler" | "mentionHandler"
|
"disabledExtensions" | "editable" | "flaggedExtensions" | "fileHandler" | "mentionHandler"
|
||||||
> {
|
> {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -51,7 +51,9 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
|||||||
// states
|
// states
|
||||||
const [isFocused, setIsFocused] = useState(showToolbarInitially);
|
const [isFocused, setIsFocused] = useState(showToolbarInitially);
|
||||||
// editor flaggings
|
// editor flaggings
|
||||||
const { liteText: liteTextEditorExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
const { liteText: liteTextEditorExtensions } = useEditorFlagging({
|
||||||
|
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||||
|
});
|
||||||
// editor config
|
// editor config
|
||||||
const { getEditorFileHandlers } = useEditorConfig();
|
const { getEditorFileHandlers } = useEditorConfig();
|
||||||
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> {
|
||||||
@ -79,6 +81,7 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
|||||||
mentionHandler={{
|
mentionHandler={{
|
||||||
renderComponent: () => <></>,
|
renderComponent: () => <></>,
|
||||||
}}
|
}}
|
||||||
|
extendedEditorProps={{}}
|
||||||
containerClassName={cn(containerClassName, "relative")}
|
containerClassName={cn(containerClassName, "relative")}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -24,8 +24,13 @@ import { useUser } from "@/hooks/store/user";
|
|||||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { EditorAIMenu } from "@/plane-web/components/pages";
|
import { EditorAIMenu } from "@/plane-web/components/pages";
|
||||||
|
// plane web types
|
||||||
|
import type { TExtendedEditorExtensionsConfig } from "@/plane-web/hooks/pages";
|
||||||
|
// plane web store
|
||||||
|
import { EPageStoreType } from "@/plane-web/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";
|
||||||
|
|
||||||
import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed";
|
import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed";
|
||||||
// store
|
// store
|
||||||
import type { TPageInstance } from "@/store/pages/base-page";
|
import type { TPageInstance } from "@/store/pages/base-page";
|
||||||
@ -41,6 +46,7 @@ export type TEditorBodyConfig = {
|
|||||||
|
|
||||||
export type TEditorBodyHandlers = {
|
export type TEditorBodyHandlers = {
|
||||||
fetchEntity: (payload: TSearchEntityRequestPayload) => Promise<TSearchResponse>;
|
fetchEntity: (payload: TSearchEntityRequestPayload) => Promise<TSearchResponse>;
|
||||||
|
getRedirectionLink: (pageId?: string) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -54,7 +60,11 @@ type Props = {
|
|||||||
isNavigationPaneOpen: boolean;
|
isNavigationPaneOpen: boolean;
|
||||||
page: TPageInstance;
|
page: TPageInstance;
|
||||||
webhookConnectionParams: TWebhookConnectionQueryParams;
|
webhookConnectionParams: TWebhookConnectionQueryParams;
|
||||||
|
projectId: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
storeType: EPageStoreType;
|
||||||
|
|
||||||
|
extendedEditorProps: TExtendedEditorExtensionsConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageEditorBody: React.FC<Props> = observer((props) => {
|
export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||||
@ -67,8 +77,11 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||||||
handlers,
|
handlers,
|
||||||
isNavigationPaneOpen,
|
isNavigationPaneOpen,
|
||||||
page,
|
page,
|
||||||
|
storeType,
|
||||||
webhookConnectionParams,
|
webhookConnectionParams,
|
||||||
|
projectId,
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
|
extendedEditorProps,
|
||||||
} = props;
|
} = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
@ -93,7 +106,10 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||||||
searchEntity: handlers.fetchEntity,
|
searchEntity: handlers.fetchEntity,
|
||||||
});
|
});
|
||||||
// editor flaggings
|
// editor flaggings
|
||||||
const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug);
|
const { document: documentEditorExtensions } = useEditorFlagging({
|
||||||
|
workspaceSlug,
|
||||||
|
storeType,
|
||||||
|
});
|
||||||
// page filters
|
// page filters
|
||||||
const { fontSize, fontStyle, isFullWidth } = usePageFilters();
|
const { fontSize, fontStyle, isFullWidth } = usePageFilters();
|
||||||
// translation
|
// translation
|
||||||
@ -115,7 +131,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
workspaceSlug={workspaceSlug}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[editorRef, workspaceId, workspaceSlug]
|
[editorRef, workspaceId, workspaceSlug]
|
||||||
@ -202,7 +218,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
<div className="page-header-container group/page-header">
|
<div className="page-header-container group/page-header">
|
||||||
<div className={blockWidthClassName}>
|
<div className={blockWidthClassName}>
|
||||||
<PageEditorHeaderRoot page={page} />
|
<PageEditorHeaderRoot page={page} projectId={projectId} />
|
||||||
<PageEditorTitle
|
<PageEditorTitle
|
||||||
editorRef={editorRef}
|
editorRef={editorRef}
|
||||||
readOnly={!isContentEditable}
|
readOnly={!isContentEditable}
|
||||||
@ -240,6 +256,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||||||
menu: getAIMenu,
|
menu: getAIMenu,
|
||||||
}}
|
}}
|
||||||
onAssetChange={updateAssetsList}
|
onAssetChange={updateAssetsList}
|
||||||
|
extendedEditorProps={extendedEditorProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { PageEditorHeaderLogoPicker } from "./logo-picker";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
page: TPageInstance;
|
page: TPageInstance;
|
||||||
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useSearchParams } from "next/navigation";
|
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { EditorRefApi } from "@plane/editor";
|
import type { EditorRefApi } from "@plane/editor";
|
||||||
import type { TDocumentPayload, TPage, TPageVersion, TWebhookConnectionQueryParams } from "@plane/types";
|
import type { TDocumentPayload, TPage, TPageVersion, TWebhookConnectionQueryParams } from "@plane/types";
|
||||||
@ -9,19 +8,20 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
|||||||
import { usePageFallback } from "@/hooks/use-page-fallback";
|
import { usePageFallback } from "@/hooks/use-page-fallback";
|
||||||
import { useQueryParams } from "@/hooks/use-query-params";
|
import { useQueryParams } from "@/hooks/use-query-params";
|
||||||
// plane web import
|
// plane web import
|
||||||
import type { TPageNavigationPaneTab } from "@/plane-web/components/pages/navigation-pane";
|
import { PageModals } from "@/plane-web/components/pages";
|
||||||
|
import { usePagesPaneExtensions, useExtendedEditorProps } from "@/plane-web/hooks/pages";
|
||||||
|
import { EPageStoreType } from "@/plane-web/hooks/store";
|
||||||
// store
|
// store
|
||||||
import type { TPageInstance } from "@/store/pages/base-page";
|
import type { TPageInstance } from "@/store/pages/base-page";
|
||||||
// local imports
|
// local imports
|
||||||
import {
|
import {
|
||||||
PAGE_NAVIGATION_PANE_TAB_KEYS,
|
|
||||||
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM,
|
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM,
|
||||||
PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM,
|
PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM,
|
||||||
PageNavigationPaneRoot,
|
PageNavigationPaneRoot,
|
||||||
} from "../navigation-pane";
|
} from "../navigation-pane";
|
||||||
import { PageVersionsOverlay } from "../version";
|
import { PageVersionsOverlay } from "../version";
|
||||||
import { PagesVersionEditor } from "../version/editor";
|
import { PagesVersionEditor } from "../version/editor";
|
||||||
import { PageEditorBody, TEditorBodyConfig, TEditorBodyHandlers } from "./editor-body";
|
import { PageEditorBody, type TEditorBodyConfig, type TEditorBodyHandlers } from "./editor-body";
|
||||||
import { PageEditorToolbarRoot } from "./toolbar";
|
import { PageEditorToolbarRoot } from "./toolbar";
|
||||||
|
|
||||||
export type TPageRootHandlers = {
|
export type TPageRootHandlers = {
|
||||||
@ -29,7 +29,7 @@ export type TPageRootHandlers = {
|
|||||||
fetchAllVersions: (pageId: string) => Promise<TPageVersion[] | undefined>;
|
fetchAllVersions: (pageId: string) => Promise<TPageVersion[] | undefined>;
|
||||||
fetchDescriptionBinary: () => Promise<any>;
|
fetchDescriptionBinary: () => Promise<any>;
|
||||||
fetchVersionDetails: (pageId: string, versionId: string) => Promise<TPageVersion | undefined>;
|
fetchVersionDetails: (pageId: string, versionId: string) => Promise<TPageVersion | undefined>;
|
||||||
getRedirectionLink: (pageId: string) => string;
|
restoreVersion: (pageId: string, versionId: string) => Promise<void>;
|
||||||
updateDescription: (document: TDocumentPayload) => Promise<void>;
|
updateDescription: (document: TDocumentPayload) => Promise<void>;
|
||||||
} & TEditorBodyHandlers;
|
} & TEditorBodyHandlers;
|
||||||
|
|
||||||
@ -39,12 +39,14 @@ type TPageRootProps = {
|
|||||||
config: TPageRootConfig;
|
config: TPageRootConfig;
|
||||||
handlers: TPageRootHandlers;
|
handlers: TPageRootHandlers;
|
||||||
page: TPageInstance;
|
page: TPageInstance;
|
||||||
|
storeType: EPageStoreType;
|
||||||
webhookConnectionParams: TWebhookConnectionQueryParams;
|
webhookConnectionParams: TWebhookConnectionQueryParams;
|
||||||
|
projectId: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageRoot = observer((props: TPageRootProps) => {
|
export const PageRoot = observer((props: TPageRootProps) => {
|
||||||
const { config, handlers, page, webhookConnectionParams, workspaceSlug } = props;
|
const { config, handlers, page, projectId, storeType, webhookConnectionParams, workspaceSlug } = props;
|
||||||
// states
|
// states
|
||||||
const [editorReady, setEditorReady] = useState(false);
|
const [editorReady, setEditorReady] = useState(false);
|
||||||
const [hasConnectionFailed, setHasConnectionFailed] = useState(false);
|
const [hasConnectionFailed, setHasConnectionFailed] = useState(false);
|
||||||
@ -52,8 +54,6 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||||||
const editorRef = useRef<EditorRefApi>(null);
|
const editorRef = useRef<EditorRefApi>(null);
|
||||||
// router
|
// router
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
// search params
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
// derived values
|
// derived values
|
||||||
const {
|
const {
|
||||||
isContentEditable,
|
isContentEditable,
|
||||||
@ -66,7 +66,6 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||||||
hasConnectionFailed,
|
hasConnectionFailed,
|
||||||
updatePageDescription: handlers.updateDescription,
|
updatePageDescription: handlers.updateDescription,
|
||||||
});
|
});
|
||||||
// update query params
|
|
||||||
const { updateQueryParams } = useQueryParams();
|
const { updateQueryParams } = useQueryParams();
|
||||||
|
|
||||||
const handleEditorReady = useCallback(
|
const handleEditorReady = useCallback(
|
||||||
@ -85,10 +84,31 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}, [isContentEditable, setEditorRef]);
|
}, [isContentEditable, setEditorRef]);
|
||||||
|
|
||||||
const handleRestoreVersion = useCallback(async (descriptionHTML: string) => {
|
// Get extensions and navigation logic from hook
|
||||||
editorRef.current?.clearEditor();
|
const { editorExtensionHandlers, navigationPaneExtensions, handleOpenNavigationPane, isNavigationPaneOpen } =
|
||||||
editorRef.current?.setEditorValue(descriptionHTML);
|
usePagesPaneExtensions({
|
||||||
}, []);
|
page,
|
||||||
|
editorRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get extended editor extensions configuration
|
||||||
|
const extendedEditorProps = useExtendedEditorProps({
|
||||||
|
workspaceSlug,
|
||||||
|
page,
|
||||||
|
storeType,
|
||||||
|
fetchEntity: handlers.fetchEntity,
|
||||||
|
getRedirectionLink: handlers.getRedirectionLink,
|
||||||
|
extensionHandlers: editorExtensionHandlers,
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRestoreVersion = useCallback(
|
||||||
|
async (descriptionHTML: string) => {
|
||||||
|
editorRef.current?.clearEditor();
|
||||||
|
editorRef.current?.setEditorValue(descriptionHTML);
|
||||||
|
},
|
||||||
|
[editorRef]
|
||||||
|
);
|
||||||
|
|
||||||
// reset editor ref on unmount
|
// reset editor ref on unmount
|
||||||
useEffect(
|
useEffect(
|
||||||
@ -98,19 +118,6 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||||||
[setEditorRef]
|
[setEditorRef]
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigationPaneQueryParam = searchParams.get(
|
|
||||||
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM
|
|
||||||
) as TPageNavigationPaneTab | null;
|
|
||||||
const isValidNavigationPaneTab =
|
|
||||||
!!navigationPaneQueryParam && PAGE_NAVIGATION_PANE_TAB_KEYS.includes(navigationPaneQueryParam);
|
|
||||||
|
|
||||||
const handleOpenNavigationPane = useCallback(() => {
|
|
||||||
const updatedRoute = updateQueryParams({
|
|
||||||
paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: "outline" },
|
|
||||||
});
|
|
||||||
router.push(updatedRoute);
|
|
||||||
}, [router, updateQueryParams]);
|
|
||||||
|
|
||||||
const handleCloseNavigationPane = useCallback(() => {
|
const handleCloseNavigationPane = useCallback(() => {
|
||||||
const updatedRoute = updateQueryParams({
|
const updatedRoute = updateQueryParams({
|
||||||
paramsToRemove: [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM],
|
paramsToRemove: [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM],
|
||||||
@ -127,10 +134,11 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||||||
handleRestore={handleRestoreVersion}
|
handleRestore={handleRestoreVersion}
|
||||||
pageId={page.id ?? ""}
|
pageId={page.id ?? ""}
|
||||||
restoreEnabled={isContentEditable}
|
restoreEnabled={isContentEditable}
|
||||||
|
storeType={storeType}
|
||||||
/>
|
/>
|
||||||
<PageEditorToolbarRoot
|
<PageEditorToolbarRoot
|
||||||
handleOpenNavigationPane={handleOpenNavigationPane}
|
handleOpenNavigationPane={handleOpenNavigationPane}
|
||||||
isNavigationPaneOpen={isValidNavigationPaneTab}
|
isNavigationPaneOpen={isNavigationPaneOpen}
|
||||||
page={page}
|
page={page}
|
||||||
/>
|
/>
|
||||||
<PageEditorBody
|
<PageEditorBody
|
||||||
@ -141,21 +149,27 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||||||
handleEditorReady={handleEditorReady}
|
handleEditorReady={handleEditorReady}
|
||||||
handleOpenNavigationPane={handleOpenNavigationPane}
|
handleOpenNavigationPane={handleOpenNavigationPane}
|
||||||
handlers={handlers}
|
handlers={handlers}
|
||||||
isNavigationPaneOpen={isValidNavigationPaneTab}
|
isNavigationPaneOpen={isNavigationPaneOpen}
|
||||||
page={page}
|
page={page}
|
||||||
|
projectId={projectId}
|
||||||
|
storeType={storeType}
|
||||||
webhookConnectionParams={webhookConnectionParams}
|
webhookConnectionParams={webhookConnectionParams}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
|
extendedEditorProps={extendedEditorProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<PageNavigationPaneRoot
|
<PageNavigationPaneRoot
|
||||||
|
storeType={storeType}
|
||||||
handleClose={handleCloseNavigationPane}
|
handleClose={handleCloseNavigationPane}
|
||||||
isNavigationPaneOpen={isValidNavigationPaneTab}
|
isNavigationPaneOpen={isNavigationPaneOpen}
|
||||||
page={page}
|
page={page}
|
||||||
versionHistory={{
|
versionHistory={{
|
||||||
fetchAllVersions: handlers.fetchAllVersions,
|
fetchAllVersions: handlers.fetchAllVersions,
|
||||||
fetchVersionDetails: handlers.fetchVersionDetails,
|
fetchVersionDetails: handlers.fetchVersionDetails,
|
||||||
}}
|
}}
|
||||||
|
extensions={navigationPaneExtensions}
|
||||||
/>
|
/>
|
||||||
|
<PageModals page={page} storeType={storeType} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
|
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
|
||||||
|
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
export * from "./types";
|
||||||
|
|
||||||
export const PAGE_NAVIGATION_PANE_WIDTH = 294;
|
export const PAGE_NAVIGATION_PANE_WIDTH = 294;
|
||||||
|
|
||||||
|
|||||||
@ -11,11 +11,14 @@ import { useQueryParams } from "@/hooks/use-query-params";
|
|||||||
// plane web components
|
// plane web components
|
||||||
import { TPageNavigationPaneTab } from "@/plane-web/components/pages/navigation-pane";
|
import { TPageNavigationPaneTab } from "@/plane-web/components/pages/navigation-pane";
|
||||||
// store
|
// store
|
||||||
|
import { type EPageStoreType } from "@/plane-web/hooks/store";
|
||||||
import type { TPageInstance } from "@/store/pages/base-page";
|
import type { TPageInstance } from "@/store/pages/base-page";
|
||||||
// local imports
|
// local imports
|
||||||
import { TPageRootHandlers } from "../editor/page-root";
|
import { TPageRootHandlers } from "../editor/page-root";
|
||||||
import { PageNavigationPaneTabPanelsRoot } from "./tab-panels/root";
|
import { PageNavigationPaneTabPanelsRoot } from "./tab-panels/root";
|
||||||
import { PageNavigationPaneTabsList } from "./tabs-list";
|
import { PageNavigationPaneTabsList } from "./tabs-list";
|
||||||
|
import { INavigationPaneExtension } from "./types/extensions";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PAGE_NAVIGATION_PANE_TAB_KEYS,
|
PAGE_NAVIGATION_PANE_TAB_KEYS,
|
||||||
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM,
|
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM,
|
||||||
@ -28,10 +31,14 @@ type Props = {
|
|||||||
isNavigationPaneOpen: boolean;
|
isNavigationPaneOpen: boolean;
|
||||||
page: TPageInstance;
|
page: TPageInstance;
|
||||||
versionHistory: Pick<TPageRootHandlers, "fetchAllVersions" | "fetchVersionDetails">;
|
versionHistory: Pick<TPageRootHandlers, "fetchAllVersions" | "fetchVersionDetails">;
|
||||||
|
// Generic extension system for additional navigation pane content
|
||||||
|
extensions?: INavigationPaneExtension[];
|
||||||
|
storeType: EPageStoreType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageNavigationPaneRoot: React.FC<Props> = observer((props) => {
|
export const PageNavigationPaneRoot: React.FC<Props> = observer((props) => {
|
||||||
const { handleClose, isNavigationPaneOpen, page, versionHistory } = props;
|
const { handleClose, isNavigationPaneOpen, page, versionHistory, extensions = [], storeType } = props;
|
||||||
|
|
||||||
// navigation
|
// navigation
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@ -43,6 +50,21 @@ export const PageNavigationPaneRoot: React.FC<Props> = observer((props) => {
|
|||||||
) as TPageNavigationPaneTab | null;
|
) as TPageNavigationPaneTab | null;
|
||||||
const activeTab: TPageNavigationPaneTab = navigationPaneQueryParam || "outline";
|
const activeTab: TPageNavigationPaneTab = navigationPaneQueryParam || "outline";
|
||||||
const selectedIndex = PAGE_NAVIGATION_PANE_TAB_KEYS.indexOf(activeTab);
|
const selectedIndex = PAGE_NAVIGATION_PANE_TAB_KEYS.indexOf(activeTab);
|
||||||
|
|
||||||
|
// Check if any extension is currently active based on query parameters
|
||||||
|
const ActiveExtension = extensions.find((extension) => {
|
||||||
|
const paneTabValue = searchParams.get(PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM);
|
||||||
|
const hasVersionParam = searchParams.get(PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM);
|
||||||
|
|
||||||
|
// Extension is active ONLY when paneTab matches AND no regular navigation params are present
|
||||||
|
return paneTabValue === extension.triggerParam && !hasVersionParam;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't show tabs when an extension is active
|
||||||
|
const showNavigationTabs = !ActiveExtension && isNavigationPaneOpen;
|
||||||
|
|
||||||
|
// Use extension width if available, otherwise fall back to default
|
||||||
|
const paneWidth = ActiveExtension?.width ?? PAGE_NAVIGATION_PANE_WIDTH;
|
||||||
// translation
|
// translation
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -61,10 +83,10 @@ export const PageNavigationPaneRoot: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className="flex-shrink-0 h-full flex flex-col bg-custom-background-100 pt-3.5 border-l border-custom-border-200 transition-all duration-300 ease-in-out"
|
className="flex-shrink-0 h-full flex flex-col bg-custom-background-100 pt-3.5 border-l border-custom-border-200 transition-all duration-300 ease-out"
|
||||||
style={{
|
style={{
|
||||||
width: `${PAGE_NAVIGATION_PANE_WIDTH}px`,
|
width: `${paneWidth}px`,
|
||||||
marginRight: isNavigationPaneOpen ? "0px" : `-${PAGE_NAVIGATION_PANE_WIDTH}px`,
|
marginRight: isNavigationPaneOpen ? "0px" : `-${paneWidth}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mb-3.5 px-3.5">
|
<div className="mb-3.5 px-3.5">
|
||||||
@ -79,10 +101,17 @@ export const PageNavigationPaneRoot: React.FC<Props> = observer((props) => {
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Tab.Group as={React.Fragment} selectedIndex={selectedIndex} onChange={handleTabChange}>
|
|
||||||
<PageNavigationPaneTabsList />
|
<div className="flex-1 flex flex-col overflow-hidden animate-slide-in-right">
|
||||||
<PageNavigationPaneTabPanelsRoot page={page} versionHistory={versionHistory} />
|
{ActiveExtension ? (
|
||||||
</Tab.Group>
|
<ActiveExtension.component page={page} extensionData={ActiveExtension.data} storeType={storeType} />
|
||||||
|
) : showNavigationTabs ? (
|
||||||
|
<Tab.Group as={React.Fragment} selectedIndex={selectedIndex} onChange={handleTabChange}>
|
||||||
|
<PageNavigationPaneTabsList />
|
||||||
|
<PageNavigationPaneTabPanelsRoot page={page} versionHistory={versionHistory} />
|
||||||
|
</Tab.Group>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import { EPageStoreType } from "@/plane-web/hooks/store";
|
||||||
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
|
export interface INavigationPaneExtensionProps<T = any> {
|
||||||
|
page: TPageInstance;
|
||||||
|
extensionData?: T;
|
||||||
|
storeType: EPageStoreType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INavigationPaneExtensionComponent<T = any> {
|
||||||
|
(props: INavigationPaneExtensionProps<T>): ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INavigationPaneExtension<T = any> {
|
||||||
|
id: string;
|
||||||
|
triggerParam: string;
|
||||||
|
component: INavigationPaneExtensionComponent<T>;
|
||||||
|
data?: T;
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
// Export generic extension system interfaces
|
||||||
|
export type {
|
||||||
|
INavigationPaneExtensionProps,
|
||||||
|
INavigationPaneExtensionComponent,
|
||||||
|
INavigationPaneExtension,
|
||||||
|
} from "./extensions";
|
||||||
@ -9,10 +9,13 @@ import { DocumentEditor } from "@/components/editor/document/editor";
|
|||||||
// hooks
|
// hooks
|
||||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||||
|
// plane web hooks
|
||||||
|
import { EPageStoreType } from "@/plane-web/hooks/store";
|
||||||
|
|
||||||
export type TVersionEditorProps = {
|
export type TVersionEditorProps = {
|
||||||
activeVersion: string | null;
|
activeVersion: string | null;
|
||||||
versionDetails: TPageVersion | undefined;
|
versionDetails: TPageVersion | undefined;
|
||||||
|
storeType: EPageStoreType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props) => {
|
export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props) => {
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { EyeIcon, TriangleAlert } from "lucide-react";
|
|||||||
import { TPageVersion } from "@plane/types";
|
import { TPageVersion } from "@plane/types";
|
||||||
import { Button, setToast, TOAST_TYPE } from "@plane/ui";
|
import { Button, setToast, TOAST_TYPE } from "@plane/ui";
|
||||||
import { renderFormattedDate, renderFormattedTime } from "@plane/utils";
|
import { renderFormattedDate, renderFormattedTime } from "@plane/utils";
|
||||||
|
// helpers
|
||||||
|
import { EPageStoreType } from "@/plane-web/hooks/store";
|
||||||
// local imports
|
// local imports
|
||||||
import { TVersionEditorProps } from "./editor";
|
import { TVersionEditorProps } from "./editor";
|
||||||
|
|
||||||
@ -17,11 +19,20 @@ type Props = {
|
|||||||
handleRestore: (descriptionHTML: string) => Promise<void>;
|
handleRestore: (descriptionHTML: string) => Promise<void>;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
restoreEnabled: boolean;
|
restoreEnabled: boolean;
|
||||||
|
storeType: EPageStoreType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageVersionsMainContent: React.FC<Props> = observer((props) => {
|
export const PageVersionsMainContent: React.FC<Props> = observer((props) => {
|
||||||
const { activeVersion, editorComponent, fetchVersionDetails, handleClose, handleRestore, pageId, restoreEnabled } =
|
const {
|
||||||
props;
|
activeVersion,
|
||||||
|
editorComponent,
|
||||||
|
fetchVersionDetails,
|
||||||
|
handleClose,
|
||||||
|
handleRestore,
|
||||||
|
pageId,
|
||||||
|
restoreEnabled,
|
||||||
|
storeType,
|
||||||
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [isRestoring, setIsRestoring] = useState(false);
|
const [isRestoring, setIsRestoring] = useState(false);
|
||||||
const [isRetrying, setIsRetrying] = useState(false);
|
const [isRetrying, setIsRetrying] = useState(false);
|
||||||
@ -107,7 +118,7 @@ export const PageVersionsMainContent: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-8 h-full overflow-y-scroll vertical-scrollbar scrollbar-sm">
|
<div className="pt-8 h-full overflow-y-scroll vertical-scrollbar scrollbar-sm">
|
||||||
<VersionEditor activeVersion={activeVersion} versionDetails={versionDetails} />
|
<VersionEditor activeVersion={activeVersion} storeType={storeType} versionDetails={versionDetails} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { TPageVersion } from "@plane/types";
|
|||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
import { useQueryParams } from "@/hooks/use-query-params";
|
import { useQueryParams } from "@/hooks/use-query-params";
|
||||||
|
// plane web imports
|
||||||
|
import { EPageStoreType } from "@/plane-web/hooks/store";
|
||||||
// local imports
|
// local imports
|
||||||
import { PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM, PAGE_NAVIGATION_PANE_WIDTH } from "../navigation-pane";
|
import { PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM, PAGE_NAVIGATION_PANE_WIDTH } from "../navigation-pane";
|
||||||
import { TVersionEditorProps } from "./editor";
|
import { TVersionEditorProps } from "./editor";
|
||||||
@ -17,10 +19,11 @@ type Props = {
|
|||||||
handleRestore: (descriptionHTML: string) => Promise<void>;
|
handleRestore: (descriptionHTML: string) => Promise<void>;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
restoreEnabled: boolean;
|
restoreEnabled: boolean;
|
||||||
|
storeType: EPageStoreType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageVersionsOverlay: React.FC<Props> = observer((props) => {
|
export const PageVersionsOverlay: React.FC<Props> = observer((props) => {
|
||||||
const { editorComponent, fetchVersionDetails, handleRestore, pageId, restoreEnabled } = props;
|
const { editorComponent, fetchVersionDetails, handleRestore, pageId, restoreEnabled, storeType } = props;
|
||||||
// navigation
|
// navigation
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@ -57,6 +60,7 @@ export const PageVersionsOverlay: React.FC<Props> = observer((props) => {
|
|||||||
handleRestore={handleRestore}
|
handleRestore={handleRestore}
|
||||||
pageId={pageId}
|
pageId={pageId}
|
||||||
restoreEnabled={restoreEnabled}
|
restoreEnabled={restoreEnabled}
|
||||||
|
storeType={storeType}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -30,4 +30,14 @@ export class ProjectPageVersionService extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async restoreVersion(workspaceSlug: string, projectId: string, pageId: string, versionId: string): Promise<void> {
|
||||||
|
return this.post(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/versions/${versionId}/restore/`
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,6 +93,7 @@ export class BasePage extends ExtendedBasePage implements TBasePage {
|
|||||||
updated_by: string | undefined;
|
updated_by: string | undefined;
|
||||||
created_at: Date | undefined;
|
created_at: Date | undefined;
|
||||||
updated_at: Date | undefined;
|
updated_at: Date | undefined;
|
||||||
|
deleted_at: Date | undefined;
|
||||||
// helpers
|
// helpers
|
||||||
oldName: string = "";
|
oldName: string = "";
|
||||||
// services
|
// services
|
||||||
@ -130,6 +131,7 @@ export class BasePage extends ExtendedBasePage implements TBasePage {
|
|||||||
this.created_at = page?.created_at || undefined;
|
this.created_at = page?.created_at || undefined;
|
||||||
this.updated_at = page?.updated_at || undefined;
|
this.updated_at = page?.updated_at || undefined;
|
||||||
this.oldName = page?.name || "";
|
this.oldName = page?.name || "";
|
||||||
|
this.deleted_at = page?.deleted_at || undefined;
|
||||||
|
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// loaders
|
// loaders
|
||||||
@ -153,6 +155,7 @@ export class BasePage extends ExtendedBasePage implements TBasePage {
|
|||||||
updated_by: observable.ref,
|
updated_by: observable.ref,
|
||||||
created_at: observable.ref,
|
created_at: observable.ref,
|
||||||
updated_at: observable.ref,
|
updated_at: observable.ref,
|
||||||
|
deleted_at: observable.ref,
|
||||||
// helpers
|
// helpers
|
||||||
oldName: observable.ref,
|
oldName: observable.ref,
|
||||||
setIsSubmitting: action,
|
setIsSubmitting: action,
|
||||||
@ -227,6 +230,7 @@ export class BasePage extends ExtendedBasePage implements TBasePage {
|
|||||||
updated_by: this.updated_by,
|
updated_by: this.updated_by,
|
||||||
created_at: this.created_at,
|
created_at: this.created_at,
|
||||||
updated_at: this.updated_at,
|
updated_at: this.updated_at,
|
||||||
|
deleted_at: this.deleted_at,
|
||||||
...this.asJSONExtended,
|
...this.asJSONExtended,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { type Editor } from "@tiptap/core";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import type { IEditorPropsExtended } from "@/types";
|
||||||
|
|
||||||
|
export type DocumentEditorSideEffectsProps = {
|
||||||
|
editor: Editor;
|
||||||
|
id: string;
|
||||||
|
updatePageProperties?: unknown;
|
||||||
|
extendedEditorProps?: IEditorPropsExtended;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocumentEditorSideEffects = (_props: DocumentEditorSideEffectsProps): ReactElement | null => null;
|
||||||
9
packages/editor/src/ce/types/editor-extended.ts
Normal file
9
packages/editor/src/ce/types/editor-extended.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export type IEditorExtensionOptions = unknown;
|
||||||
|
|
||||||
|
export type IEditorPropsExtended = unknown;
|
||||||
|
|
||||||
|
export type TExtendedEditorCommands = never;
|
||||||
|
|
||||||
|
export type TExtendedCommandExtraProps = unknown;
|
||||||
|
|
||||||
|
export type TExtendedEditorRefApi = unknown;
|
||||||
@ -1 +1,2 @@
|
|||||||
export * from "./issue-embed";
|
export * from "./issue-embed";
|
||||||
|
export * from "./editor-extended";
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import { WorkItemEmbedExtension } from "@/extensions";
|
|||||||
import { getEditorClassNames } from "@/helpers/common";
|
import { getEditorClassNames } from "@/helpers/common";
|
||||||
// hooks
|
// hooks
|
||||||
import { useCollaborativeEditor } from "@/hooks/use-collaborative-editor";
|
import { useCollaborativeEditor } from "@/hooks/use-collaborative-editor";
|
||||||
|
// constants
|
||||||
|
import { DocumentEditorSideEffects } from "@/plane-editor/components/document-editor-side-effects";
|
||||||
// types
|
// types
|
||||||
import type { EditorRefApi, ICollaborativeDocumentEditorProps } from "@/types";
|
import type { EditorRefApi, ICollaborativeDocumentEditorProps } from "@/types";
|
||||||
|
|
||||||
@ -27,6 +29,7 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
|
|||||||
editable,
|
editable,
|
||||||
editorClassName = "",
|
editorClassName = "",
|
||||||
editorProps,
|
editorProps,
|
||||||
|
extendedEditorProps,
|
||||||
embedHandler,
|
embedHandler,
|
||||||
fileHandler,
|
fileHandler,
|
||||||
flaggedExtensions,
|
flaggedExtensions,
|
||||||
@ -97,20 +100,23 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
|
|||||||
if (!editor) return null;
|
if (!editor) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageRenderer
|
<>
|
||||||
aiHandler={aiHandler}
|
<DocumentEditorSideEffects editor={editor} id={id} extendedEditorProps={extendedEditorProps} />
|
||||||
bubbleMenuEnabled={bubbleMenuEnabled}
|
<PageRenderer
|
||||||
displayConfig={displayConfig}
|
aiHandler={aiHandler}
|
||||||
documentLoaderClassName={documentLoaderClassName}
|
bubbleMenuEnabled={bubbleMenuEnabled}
|
||||||
editor={editor}
|
displayConfig={displayConfig}
|
||||||
editorContainerClassName={cn(editorContainerClassNames, "document-editor")}
|
documentLoaderClassName={documentLoaderClassName}
|
||||||
id={id}
|
editor={editor}
|
||||||
isTouchDevice={!!isTouchDevice}
|
editorContainerClassName={cn(editorContainerClassNames, "document-editor")}
|
||||||
isLoading={!hasServerSynced && !hasServerConnectionFailed}
|
id={id}
|
||||||
tabIndex={tabIndex}
|
isTouchDevice={!!isTouchDevice}
|
||||||
flaggedExtensions={flaggedExtensions}
|
isLoading={!hasServerSynced && !hasServerConnectionFailed}
|
||||||
disabledExtensions={disabledExtensions}
|
tabIndex={tabIndex}
|
||||||
/>
|
flaggedExtensions={flaggedExtensions}
|
||||||
|
disabledExtensions={disabledExtensions}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import type { EditorProps, EditorView } from "@tiptap/pm/view";
|
|||||||
import type { NodeViewProps as TNodeViewProps } from "@tiptap/react";
|
import type { NodeViewProps as TNodeViewProps } from "@tiptap/react";
|
||||||
// extension types
|
// extension types
|
||||||
import type { TTextAlign } from "@/extensions";
|
import type { TTextAlign } from "@/extensions";
|
||||||
|
// plane editor imports
|
||||||
|
import type { IEditorPropsExtended, TExtendedEditorCommands } from "@/plane-editor/types/editor-extended";
|
||||||
// types
|
// types
|
||||||
import type {
|
import type {
|
||||||
IMarking,
|
IMarking,
|
||||||
@ -50,7 +52,8 @@ export type TEditorCommands =
|
|||||||
| "callout"
|
| "callout"
|
||||||
| "attachment"
|
| "attachment"
|
||||||
| "emoji"
|
| "emoji"
|
||||||
| "external-embed";
|
| "external-embed"
|
||||||
|
| TExtendedEditorCommands;
|
||||||
|
|
||||||
export type TCommandExtraProps = {
|
export type TCommandExtraProps = {
|
||||||
image: {
|
image: {
|
||||||
@ -157,6 +160,7 @@ export type IEditorProps = {
|
|||||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
|
extendedEditorProps: IEditorPropsExtended;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ILiteTextEditorProps = IEditorProps;
|
export type ILiteTextEditorProps = IEditorProps;
|
||||||
|
|||||||
@ -8,5 +8,6 @@ export * from "./extensions";
|
|||||||
export * from "./hook";
|
export * from "./hook";
|
||||||
export * from "./mention";
|
export * from "./mention";
|
||||||
export * from "./slash-commands-suggestion";
|
export * from "./slash-commands-suggestion";
|
||||||
export * from "@/plane-editor/types";
|
|
||||||
export * from "./document-collaborative-events";
|
export * from "./document-collaborative-events";
|
||||||
|
|
||||||
|
export * from "@/plane-editor/types";
|
||||||
|
|||||||
20
packages/types/src/editor/editor-content.ts
Normal file
20
packages/types/src/editor/editor-content.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Editor content types - locally defined to avoid external dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type JSONContent = {
|
||||||
|
type?: string;
|
||||||
|
attrs?: Record<string, unknown>;
|
||||||
|
content?: JSONContent[];
|
||||||
|
marks?: {
|
||||||
|
type: string;
|
||||||
|
attrs?: Record<string, unknown>;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}[];
|
||||||
|
text?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HTMLContent = string;
|
||||||
|
|
||||||
|
export type Content = HTMLContent | JSONContent | JSONContent[] | null;
|
||||||
1
packages/types/src/editor/index.ts
Normal file
1
packages/types/src/editor/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type { JSONContent, HTMLContent, Content } from "./editor-content";
|
||||||
@ -28,6 +28,7 @@ export * from "./waitlist";
|
|||||||
export * from "./webhook";
|
export * from "./webhook";
|
||||||
export * from "./workspace-views";
|
export * from "./workspace-views";
|
||||||
export * from "./common";
|
export * from "./common";
|
||||||
|
export * from "./editor";
|
||||||
export * from "./pragmatic";
|
export * from "./pragmatic";
|
||||||
export * from "./publish";
|
export * from "./publish";
|
||||||
export * from "./search";
|
export * from "./search";
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { JSONContent } from "../../editor";
|
||||||
import { EIssueCommentAccessSpecifier } from "../../enums";
|
import { EIssueCommentAccessSpecifier } from "../../enums";
|
||||||
import { TFileSignedURLResponse } from "../../file";
|
import { TFileSignedURLResponse } from "../../file";
|
||||||
import { IUserLite } from "../../users";
|
import { IUserLite } from "../../users";
|
||||||
@ -34,7 +35,7 @@ export type TIssueComment = {
|
|||||||
comment_reactions: any[];
|
comment_reactions: any[];
|
||||||
comment_stripped: string;
|
comment_stripped: string;
|
||||||
comment_html: string;
|
comment_html: string;
|
||||||
comment_json: object;
|
comment_json: JSONContent;
|
||||||
external_id: string | undefined;
|
external_id: string | undefined;
|
||||||
external_source: string | undefined;
|
external_source: string | undefined;
|
||||||
access: EIssueCommentAccessSpecifier;
|
access: EIssueCommentAccessSpecifier;
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export type TPage = {
|
|||||||
updated_by: string | undefined;
|
updated_by: string | undefined;
|
||||||
workspace: string | undefined;
|
workspace: string | undefined;
|
||||||
logo_props: TLogoProps | undefined;
|
logo_props: TLogoProps | undefined;
|
||||||
|
deleted_at: Date | undefined;
|
||||||
} & TPageExtended;
|
} & TPageExtended;
|
||||||
|
|
||||||
// page filters
|
// page filters
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { Content, JSONContent } from "@plane/types";
|
||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -161,14 +162,86 @@ export const isEmptyHtmlString = (htmlString: string, allowedHTMLTags: string[]
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description this function returns whether a comment is empty or not by checking for the following conditions-
|
* @description
|
||||||
* 1. If comment is undefined
|
* Check if a JSONContent object is empty
|
||||||
* 2. If comment is an empty string
|
* @param {JSONContent} content
|
||||||
* 3. If comment is "<p></p>"
|
|
||||||
* @param {string | undefined} comment
|
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export const isCommentEmpty = (comment: string | undefined): boolean => {
|
const isJSONContentEmpty = (content: JSONContent): boolean => {
|
||||||
|
// If it has text, check if text is meaningful
|
||||||
|
if (content.text !== undefined) {
|
||||||
|
return !content.text || content.text.trim() === "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it has no content array, consider it empty
|
||||||
|
if (!content.content || content.content.length === 0) {
|
||||||
|
// Special case: empty paragraph nodes should be considered empty
|
||||||
|
if (content.type === "paragraph" || content.type === "doc") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// For other node types without content (like hard breaks), check if they're meaningful
|
||||||
|
return (
|
||||||
|
content.type !== "hardBreak" &&
|
||||||
|
content.type !== "image" &&
|
||||||
|
content.type !== "mention-component" &&
|
||||||
|
content.type !== "image-component"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all nested content is empty
|
||||||
|
return content.content.every(isJSONContentEmpty);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description
|
||||||
|
* This function will check if the comment is empty or not.
|
||||||
|
* It returns true if comment is empty.
|
||||||
|
* Now supports TipTap Content types (HTMLContent, JSONContent, JSONContent[], null)
|
||||||
|
*
|
||||||
|
* For HTML content:
|
||||||
|
* 1. If comment is undefined/null
|
||||||
|
* 2. If comment is an empty string
|
||||||
|
* 3. If comment is "<p></p>"
|
||||||
|
* 4. If comment contains only empty HTML tags
|
||||||
|
*
|
||||||
|
* For JSON content:
|
||||||
|
* 1. If content is null/undefined
|
||||||
|
* 2. If content has no meaningful text or nested content
|
||||||
|
* 3. If all nested content is empty
|
||||||
|
*
|
||||||
|
* @param {Content} comment - TipTap Content type
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const isCommentEmpty = (comment: Content | undefined): boolean => {
|
||||||
|
// Handle null/undefined
|
||||||
|
if (!comment) return true;
|
||||||
|
|
||||||
|
// Handle HTMLContent (string)
|
||||||
|
if (typeof comment === "string") {
|
||||||
|
return (
|
||||||
|
comment.trim() === "" ||
|
||||||
|
comment === "<p></p>" ||
|
||||||
|
isEmptyHtmlString(comment, ["img", "mention-component", "image-component"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JSONContent[] (array)
|
||||||
|
if (Array.isArray(comment)) {
|
||||||
|
return comment.length === 0 || comment.every(isJSONContentEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JSONContent (object)
|
||||||
|
return isJSONContentEmpty(comment);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description
|
||||||
|
* Legacy function for backward compatibility with string comments
|
||||||
|
* @param {string | undefined} comment
|
||||||
|
* @returns {boolean}
|
||||||
|
* @deprecated Use isCommentEmpty with Content type instead
|
||||||
|
*/
|
||||||
|
export const isStringCommentEmpty = (comment: string | undefined): boolean => {
|
||||||
// return true if comment is undefined
|
// return true if comment is undefined
|
||||||
if (!comment) return true;
|
if (!comment) return true;
|
||||||
return (
|
return (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user