From f434365e2115da6e9e9fa46cd9a08876a6bbfeca Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 6 Jun 2025 14:41:30 +0530 Subject: [PATCH 01/20] init: page navigation pane --- .../editor/src/ce/constants/extensions.ts | 1 + .../pages/editor/navigation-pane/index.ts | 21 +++ .../components/pages/editor/editor-body.tsx | 110 +++++++------- .../components/pages/editor/page-root.tsx | 97 ++++++++---- .../components/pages/editor/toolbar/index.ts | 1 - .../pages/editor/toolbar/info-popover.tsx | 141 ------------------ web/core/components/pages/header/actions.tsx | 3 +- .../components/pages/navigation-pane/index.ts | 5 + .../components/pages/navigation-pane/root.tsx | 75 ++++++++++ .../pages/navigation-pane/tab-panels/info.tsx | 106 +++++++++++++ .../navigation-pane/tab-panels/outline.tsx | 19 +++ .../pages/navigation-pane/tab-panels/root.tsx | 27 ++++ .../pages/navigation-pane/tabs-list.tsx | 37 +++++ web/core/hooks/use-query-params.ts | 38 +++-- 14 files changed, 433 insertions(+), 248 deletions(-) create mode 100644 packages/editor/src/ce/constants/extensions.ts create mode 100644 web/ce/components/pages/editor/navigation-pane/index.ts delete mode 100644 web/core/components/pages/editor/toolbar/info-popover.tsx create mode 100644 web/core/components/pages/navigation-pane/index.ts create mode 100644 web/core/components/pages/navigation-pane/root.tsx create mode 100644 web/core/components/pages/navigation-pane/tab-panels/info.tsx create mode 100644 web/core/components/pages/navigation-pane/tab-panels/outline.tsx create mode 100644 web/core/components/pages/navigation-pane/tab-panels/root.tsx create mode 100644 web/core/components/pages/navigation-pane/tabs-list.tsx diff --git a/packages/editor/src/ce/constants/extensions.ts b/packages/editor/src/ce/constants/extensions.ts new file mode 100644 index 00000000000..8787ec0c1bb --- /dev/null +++ b/packages/editor/src/ce/constants/extensions.ts @@ -0,0 +1 @@ +export enum ADDITIONAL_EXTENSIONS {} diff --git a/web/ce/components/pages/editor/navigation-pane/index.ts b/web/ce/components/pages/editor/navigation-pane/index.ts new file mode 100644 index 00000000000..2efcca181d8 --- /dev/null +++ b/web/ce/components/pages/editor/navigation-pane/index.ts @@ -0,0 +1,21 @@ +export type TPageNavigationPaneTab = "outline" | "info" | "assets"; + +export const PAGE_NAVIGATION_PANE_TABS_LIST: { + key: TPageNavigationPaneTab; + i18n_label: string; +}[] = [ + { + key: "outline", + i18n_label: "Outline", + }, + { + key: "info", + i18n_label: "Info", + }, + { + key: "assets", + i18n_label: "Assets", + }, +]; + +export const PAGE_NAVIGATION_PANE_TAB_KEYS = PAGE_NAVIGATION_PANE_TABS_LIST.map((tab) => tab.key); diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 5f194818361..05a267ee112 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -162,64 +162,66 @@ export const PageEditorBody: React.FC = observer((props) => { if (pageId === undefined || !realtimeConfig) return ; return ( - -
- {/* table of content */} -
-
-
-
- -
-
- + <> + +
+ {/* table of content */} +
+
+
+
+ +
+
+ +
-
-
-
- - +
+
+ + +
+ { + const res = await fetchMentions(query); + if (!res) throw new Error("Failed in fetching mentions"); + return res; + }, + renderComponent: (props) => , + getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }), + }} + embedHandler={{ + issue: issueEmbedProps, + }} + realtimeConfig={realtimeConfig} + serverHandler={serverHandler} + user={userConfig} + disabledExtensions={disabledExtensions} + aiHandler={{ + menu: getAIMenu, + }} + />
- { - const res = await fetchMentions(query); - if (!res) throw new Error("Failed in fetching mentions"); - return res; - }, - renderComponent: (props) => , - getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }), - }} - embedHandler={{ - issue: issueEmbedProps, - }} - realtimeConfig={realtimeConfig} - serverHandler={serverHandler} - user={userConfig} - disabledExtensions={disabledExtensions} - aiHandler={{ - menu: getAIMenu, - }} - /> -
-
+ + ); }); diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx index 2f1595e3307..47e4ef69421 100644 --- a/web/core/components/pages/editor/page-root.tsx +++ b/web/core/components/pages/editor/page-root.tsx @@ -1,9 +1,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; import { useSearchParams } from "next/navigation"; -// editor +// plane imports import { EditorRefApi } from "@plane/editor"; -// types import { TDocumentPayload, TPage, TPageVersion, TWebhookConnectionQueryParams } from "@plane/types"; // components import { @@ -18,8 +17,15 @@ import { import { useAppRouter } from "@/hooks/use-app-router"; import { usePageFallback } from "@/hooks/use-page-fallback"; import { useQueryParams } from "@/hooks/use-query-params"; +// plane web import +import { + PAGE_NAVIGATION_PANE_TAB_KEYS, + TPageNavigationPaneTab, +} from "@/plane-web/components/pages/editor/navigation-pane"; // store import { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, PageNavigationPaneRoot } from "../navigation-pane"; export type TPageRootHandlers = { create: (payload: Partial) => Promise | undefined>; @@ -89,17 +95,17 @@ export const PageRoot = observer((props: TPageRootProps) => { setIsVersionsOverlayOpen(true); }, [version]); - const handleCloseVersionsOverlay = () => { + const handleCloseVersionsOverlay = useCallback(() => { const updatedRoute = updateQueryParams({ paramsToRemove: ["version"], }); router.push(updatedRoute); - }; + }, [router, updateQueryParams]); - const handleRestoreVersion = async (descriptionHTML: string) => { + const handleRestoreVersion = useCallback(async (descriptionHTML: string) => { editorRef.current?.clearEditor(); editorRef.current?.setEditorValue(descriptionHTML); - }; + }, []); const currentVersionDescription = editorRef.current?.getDocument().html; // reset editor ref on unmount @@ -110,32 +116,63 @@ export const PageRoot = observer((props: TPageRootProps) => { [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 updatedRoute = updateQueryParams({ + paramsToRemove: [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM], + }); + router.push(updatedRoute); + }, [router, updateQueryParams]); + return ( - <> - - - +
+ + + +
+ - +
); }); diff --git a/web/core/components/pages/editor/toolbar/index.ts b/web/core/components/pages/editor/toolbar/index.ts index 66652b2dbd0..2c36785bd3e 100644 --- a/web/core/components/pages/editor/toolbar/index.ts +++ b/web/core/components/pages/editor/toolbar/index.ts @@ -1,5 +1,4 @@ export * from "./color-dropdown"; -export * from "./info-popover"; export * from "./options-dropdown"; export * from "./root"; export * from "./toolbar"; diff --git a/web/core/components/pages/editor/toolbar/info-popover.tsx b/web/core/components/pages/editor/toolbar/info-popover.tsx deleted file mode 100644 index d5a28145949..00000000000 --- a/web/core/components/pages/editor/toolbar/info-popover.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { useParams } from "next/navigation"; -import { usePopper } from "react-popper"; -import { Info } from "lucide-react"; -// plane imports -import { Avatar } from "@plane/ui"; -import { getFileURL, renderFormattedDate } from "@plane/utils"; -// helpers -import { calculateTimeAgoShort, getReadTimeFromWordsCount } from "@/helpers/date-time.helper"; -// hooks -import { useMember } from "@/hooks/store"; -// store types -import { TPageInstance } from "@/store/pages/base-page"; - -type Props = { - page: TPageInstance; -}; - -export const PageInfoPopover: React.FC = observer((props) => { - const { page } = props; - // states - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - // refs - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - // router - const { workspaceSlug } = useParams(); - // popper-js - const { styles: infoPopoverStyles, attributes: infoPopoverAttributes } = usePopper(referenceElement, popperElement, { - placement: "bottom-start", - }); - // store hooks - const { getUserDetails } = useMember(); - // derived values - const editorInformation = page.updated_by ? getUserDetails(page.updated_by) : undefined; - const creatorInformation = page.created_by ? getUserDetails(page.created_by) : undefined; - - const documentsInfo = page.editorRef?.getDocumentInfo() || { words: 0, characters: 0, paragraphs: 0 }; - - const secondsToReadableTime = () => { - const wordsCount = documentsInfo.words; - const readTimeInSeconds = Number(getReadTimeFromWordsCount(wordsCount).toFixed(0)); - return readTimeInSeconds < 60 ? `${readTimeInSeconds}s` : `${Math.ceil(readTimeInSeconds / 60)}m`; - }; - - const documentInfoCards = [ - { - key: "words-count", - title: "Words", - info: documentsInfo.words, - }, - { - key: "characters-count", - title: "Characters", - info: documentsInfo.characters, - }, - { - key: "paragraphs-count", - title: "Paragraphs", - info: documentsInfo.paragraphs, - }, - { - key: "read-time", - title: "Read time", - info: secondsToReadableTime(), - }, - ]; - - return ( -
setIsPopoverOpen(true)} - onMouseLeave={() => setIsPopoverOpen(false)} - > - - {isPopoverOpen && ( -
-
- {documentInfoCards.map((card) => ( -
-
{card.info}
-

{card.title}

-
- ))} -
-
-
-

Edited by

- - - - {editorInformation?.display_name}{" "} - {calculateTimeAgoShort(page.updated_at ?? "")} ago - - -
-
-

Created by

- - - - {creatorInformation?.display_name}{" "} - {renderFormattedDate(page.created_at)} - - -
-
-
- )} -
- ); -}); diff --git a/web/core/components/pages/header/actions.tsx b/web/core/components/pages/header/actions.tsx index 3c73d42efd4..042c1841291 100644 --- a/web/core/components/pages/header/actions.tsx +++ b/web/core/components/pages/header/actions.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react"; // components -import { PageInfoPopover, PageOptionsDropdown } from "@/components/pages"; +import { PageOptionsDropdown } from "@/components/pages"; // plane web components import { PageLockControl } from "@/plane-web/components/pages/header/lock-control"; import { PageMoveControl } from "@/plane-web/components/pages/header/move-control"; @@ -30,7 +30,6 @@ export const PageHeaderActions: React.FC = observer((props) => { - diff --git a/web/core/components/pages/navigation-pane/index.ts b/web/core/components/pages/navigation-pane/index.ts new file mode 100644 index 00000000000..b852c6ab723 --- /dev/null +++ b/web/core/components/pages/navigation-pane/index.ts @@ -0,0 +1,5 @@ +export * from "./root"; + +export const PAGE_NAVIGATION_PANE_WIDTH = 294; + +export const PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM = "sidebarTab"; diff --git a/web/core/components/pages/navigation-pane/root.tsx b/web/core/components/pages/navigation-pane/root.tsx new file mode 100644 index 00000000000..75e2737cb0f --- /dev/null +++ b/web/core/components/pages/navigation-pane/root.tsx @@ -0,0 +1,75 @@ +import { useCallback } from "react"; +import { observer } from "mobx-react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { ArrowRightCircle } from "lucide-react"; +import { Tab } from "@headlessui/react"; +// hooks +import { useQueryParams } from "@/hooks/use-query-params"; +// plane web components +import { + PAGE_NAVIGATION_PANE_TAB_KEYS, + TPageNavigationPaneTab, +} from "@/plane-web/components/pages/editor/navigation-pane"; +// store +import { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { PageNavigationPaneTabPanelsRoot } from "./tab-panels/root"; +import { PageNavigationPaneTabsList } from "./tabs-list"; +import { PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, PAGE_NAVIGATION_PANE_WIDTH } from "./index"; + +type Props = { + handleClose: () => void; + isNavigationPaneOpen: boolean; + page: TPageInstance; +}; + +export const PageNavigationPaneRoot: React.FC = observer((props) => { + const { handleClose, isNavigationPaneOpen, page } = props; + // navigation + const router = useRouter(); + const searchParams = useSearchParams(); + // query params + const { updateQueryParams } = useQueryParams(); + // derived values + const { editorRef } = page; + const navigationPaneQueryParam = searchParams.get( + PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM + ) as TPageNavigationPaneTab | null; + const activeTab: TPageNavigationPaneTab = navigationPaneQueryParam || "outline"; + const selectedIndex = PAGE_NAVIGATION_PANE_TAB_KEYS.indexOf(activeTab); + + const handleTabChange = useCallback( + (index: number) => { + const updatedTab = PAGE_NAVIGATION_PANE_TAB_KEYS[index]; + const updatedRoute = updateQueryParams({ + paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: updatedTab }, + }); + router.push(updatedRoute); + }, + [router, updateQueryParams] + ); + + return ( +
+
+ +
+ + + + +
+ ); +}); diff --git a/web/core/components/pages/navigation-pane/tab-panels/info.tsx b/web/core/components/pages/navigation-pane/tab-panels/info.tsx new file mode 100644 index 00000000000..5838cb2bff6 --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/info.tsx @@ -0,0 +1,106 @@ +import { observer } from "mobx-react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { Avatar } from "@plane/ui"; +import { calculateTimeAgoShort, getFileURL, getReadTimeFromWordsCount, renderFormattedDate } from "@plane/utils"; +// hooks +import { useMember } from "@/hooks/store"; +// store +import { TPageInstance } from "@/store/pages/base-page"; + +type Props = { + page: TPageInstance; +}; + +export const PageNavigationPaneInfoTabPanel: React.FC = observer((props) => { + const { page } = props; + // navigation + const { workspaceSlug } = useParams(); + // store hooks + const { getUserDetails } = useMember(); + // derived values + const { created_by, editorRef, updated_by } = page; + const editorInformation = updated_by ? getUserDetails(updated_by) : undefined; + const creatorInformation = created_by ? getUserDetails(created_by) : undefined; + const documentsInfo = editorRef?.getDocumentInfo() || { words: 0, characters: 0, paragraphs: 0 }; + // translation + const { t } = useTranslation(); + + const secondsToReadableTime = () => { + const wordsCount = documentsInfo.words; + const readTimeInSeconds = Number(getReadTimeFromWordsCount(wordsCount).toFixed(0)); + return readTimeInSeconds < 60 ? `${readTimeInSeconds}s` : `${Math.ceil(readTimeInSeconds / 60)}m`; + }; + + const documentInfoCards = [ + { + key: "words-count", + title: "Words", + info: documentsInfo.words, + }, + { + key: "characters-count", + title: "Characters", + info: documentsInfo.characters, + }, + { + key: "paragraphs-count", + title: "Paragraphs", + info: documentsInfo.paragraphs, + }, + { + key: "read-time", + title: "Read time", + info: secondsToReadableTime(), + }, + ]; + + return ( +
+
+ {documentInfoCards.map((card) => ( +
+
{card.info}
+

{card.title}

+
+ ))} +
+
+
+

Edited by

+
+ + + {editorInformation?.display_name ?? t("common.deactivated_user")} + + + {calculateTimeAgoShort(page.updated_at ?? "")} ago + +
+
+
+

{t("common.created_by")}

+
+ + + {creatorInformation?.display_name ?? t("common.deactivated_user")} + + {renderFormattedDate(page.created_at)} +
+
+
+
+ ); +}); diff --git a/web/core/components/pages/navigation-pane/tab-panels/outline.tsx b/web/core/components/pages/navigation-pane/tab-panels/outline.tsx new file mode 100644 index 00000000000..42259517672 --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/outline.tsx @@ -0,0 +1,19 @@ +// store +import { TPageInstance } from "@/store/pages/base-page"; +import { PageContentBrowser } from "../../editor"; + +type Props = { + page: TPageInstance; +}; + +export const PageNavigationPaneOutlineTabPanel: React.FC = (props) => { + const { page } = props; + // derived values + const { editorRef } = page; + + return ( +
+ +
+ ); +}; diff --git a/web/core/components/pages/navigation-pane/tab-panels/root.tsx b/web/core/components/pages/navigation-pane/tab-panels/root.tsx new file mode 100644 index 00000000000..fdd51d66809 --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/root.tsx @@ -0,0 +1,27 @@ +import { Tab } from "@headlessui/react"; +// plane web imports +import { PAGE_NAVIGATION_PANE_TABS_LIST } from "@/plane-web/components/pages/editor/navigation-pane"; +// store +import { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { PageNavigationPaneInfoTabPanel } from "./info"; +import { PageNavigationPaneOutlineTabPanel } from "./outline"; + +type Props = { + page: TPageInstance; +}; + +export const PageNavigationPaneTabPanelsRoot: React.FC = (props) => { + const { page } = props; + + return ( + + {PAGE_NAVIGATION_PANE_TABS_LIST.map((tab) => ( + + {tab.key === "outline" && } + {tab.key === "info" && } + + ))} + + ); +}; diff --git a/web/core/components/pages/navigation-pane/tabs-list.tsx b/web/core/components/pages/navigation-pane/tabs-list.tsx new file mode 100644 index 00000000000..bd826b2cdf1 --- /dev/null +++ b/web/core/components/pages/navigation-pane/tabs-list.tsx @@ -0,0 +1,37 @@ +import { Tab } from "@headlessui/react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// plane web components +import { PAGE_NAVIGATION_PANE_TABS_LIST } from "@/plane-web/components/pages/editor/navigation-pane"; + +export const PageNavigationPaneTabsList = () => { + // translation + const { t } = useTranslation(); + + return ( + + {({ selectedIndex }) => ( + <> + {PAGE_NAVIGATION_PANE_TABS_LIST.map((tab) => ( + + {t(tab.i18n_label)} + + ))} + {/* active tab indicator */} +
+ + )} + + ); +}; diff --git a/web/core/hooks/use-query-params.ts b/web/core/hooks/use-query-params.ts index 8b689f0cbe0..579022493d0 100644 --- a/web/core/hooks/use-query-params.ts +++ b/web/core/hooks/use-query-params.ts @@ -1,3 +1,4 @@ +import { useCallback } from "react"; import { useSearchParams, usePathname } from "next/navigation"; type TParamsToAdd = { @@ -9,29 +10,26 @@ export const useQueryParams = () => { const searchParams = useSearchParams(); const pathname = usePathname(); - const updateQueryParams = ({ - paramsToAdd = {}, - paramsToRemove = [], - }: { - paramsToAdd?: TParamsToAdd; - paramsToRemove?: string[]; - }) => { - const currentParams = new URLSearchParams(searchParams.toString()); + const updateQueryParams = useCallback( + ({ paramsToAdd = {}, paramsToRemove = [] }: { paramsToAdd?: TParamsToAdd; paramsToRemove?: string[] }) => { + const currentParams = new URLSearchParams(searchParams.toString()); - // add or update query parameters - Object.keys(paramsToAdd).forEach((key) => { - currentParams.set(key, paramsToAdd[key]); - }); + // add or update query parameters + Object.keys(paramsToAdd).forEach((key) => { + currentParams.set(key, paramsToAdd[key]); + }); - // remove specified query parameters - paramsToRemove.forEach((key) => { - currentParams.delete(key); - }); + // remove specified query parameters + paramsToRemove.forEach((key) => { + currentParams.delete(key); + }); - // construct the new route with the updated query parameters - const newRoute = `${pathname}?${currentParams.toString()}`; - return newRoute; - }; + // construct the new route with the updated query parameters + const newRoute = `${pathname}?${currentParams.toString()}`; + return newRoute; + }, + [pathname, searchParams] + ); return { updateQueryParams, From 40a723b329221ab911e5cc62eeabc4c00bddf791 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 10 Jun 2025 13:57:37 +0530 Subject: [PATCH 02/20] chore: outline and info tabs --- .../components/pages/editor/page-root.tsx | 35 ++--- .../pages/editor/summary/content-browser.tsx | 22 ++- .../editor/summary/heading-components.tsx | 24 ++-- .../components/pages/navigation-pane/index.ts | 1 + .../components/pages/navigation-pane/root.tsx | 18 +-- .../pages/navigation-pane/tab-panels/info.tsx | 106 -------------- .../tab-panels/info/actors-info.tsx | 64 +++++++++ .../navigation-pane/tab-panels/info/root.tsx | 67 +++++++++ .../tab-panels/info/version-history.tsx | 136 ++++++++++++++++++ .../navigation-pane/tab-panels/outline.tsx | 4 +- .../pages/navigation-pane/tab-panels/root.tsx | 20 ++- .../pages/navigation-pane/tabs-list.tsx | 2 +- web/core/components/pages/version/editor.tsx | 8 +- web/core/components/pages/version/index.ts | 3 - .../components/pages/version/main-content.tsx | 44 +++--- web/core/components/pages/version/root.tsx | 56 ++++---- .../pages/version/sidebar-list-item.tsx | 49 ------- .../components/pages/version/sidebar-list.tsx | 99 ------------- .../components/pages/version/sidebar-root.tsx | 38 ----- web/styles/globals.css | 1 + 20 files changed, 376 insertions(+), 421 deletions(-) delete mode 100644 web/core/components/pages/navigation-pane/tab-panels/info.tsx create mode 100644 web/core/components/pages/navigation-pane/tab-panels/info/actors-info.tsx create mode 100644 web/core/components/pages/navigation-pane/tab-panels/info/root.tsx create mode 100644 web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx delete mode 100644 web/core/components/pages/version/sidebar-list-item.tsx delete mode 100644 web/core/components/pages/version/sidebar-list.tsx delete mode 100644 web/core/components/pages/version/sidebar-root.tsx diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx index 47e4ef69421..699cbdde62f 100644 --- a/web/core/components/pages/editor/page-root.tsx +++ b/web/core/components/pages/editor/page-root.tsx @@ -25,7 +25,11 @@ import { // store import { TPageInstance } from "@/store/pages/base-page"; // local imports -import { PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, PageNavigationPaneRoot } from "../navigation-pane"; +import { + PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, + PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM, + PageNavigationPaneRoot, +} from "../navigation-pane"; export type TPageRootHandlers = { create: (payload: Partial) => Promise | undefined>; @@ -51,7 +55,6 @@ export const PageRoot = observer((props: TPageRootProps) => { // states const [editorReady, setEditorReady] = useState(false); const [hasConnectionFailed, setHasConnectionFailed] = useState(false); - const [isVersionsOverlayOpen, setIsVersionsOverlayOpen] = useState(false); // refs const editorRef = useRef(null); // router @@ -86,27 +89,10 @@ export const PageRoot = observer((props: TPageRootProps) => { }, 0); }, [isContentEditable, setEditorRef]); - const version = searchParams.get("version"); - useEffect(() => { - if (!version) { - setIsVersionsOverlayOpen(false); - return; - } - setIsVersionsOverlayOpen(true); - }, [version]); - - const handleCloseVersionsOverlay = useCallback(() => { - const updatedRoute = updateQueryParams({ - paramsToRemove: ["version"], - }); - router.push(updatedRoute); - }, [router, updateQueryParams]); - const handleRestoreVersion = useCallback(async (descriptionHTML: string) => { editorRef.current?.clearEditor(); editorRef.current?.setEditorValue(descriptionHTML); }, []); - const currentVersionDescription = editorRef.current?.getDocument().html; // reset editor ref on unmount useEffect( @@ -131,7 +117,7 @@ export const PageRoot = observer((props: TPageRootProps) => { const handleCloseNavigationPane = useCallback(() => { const updatedRoute = updateQueryParams({ - paramsToRemove: [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM], + paramsToRemove: [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM], }); router.push(updatedRoute); }, [router, updateQueryParams]); @@ -140,14 +126,9 @@ export const PageRoot = observer((props: TPageRootProps) => {
@@ -172,6 +153,10 @@ export const PageRoot = observer((props: TPageRootProps) => { handleClose={handleCloseNavigationPane} isNavigationPaneOpen={isValidNavigationPaneTab} page={page} + versionHistory={{ + fetchAllVersions: handlers.fetchAllVersions, + fetchVersionDetails: handlers.fetchVersionDetails, + }} />
); diff --git a/web/core/components/pages/editor/summary/content-browser.tsx b/web/core/components/pages/editor/summary/content-browser.tsx index e0ef271168c..3d7ced77fdb 100644 --- a/web/core/components/pages/editor/summary/content-browser.tsx +++ b/web/core/components/pages/editor/summary/content-browser.tsx @@ -1,17 +1,19 @@ import { useState, useEffect, useCallback } from "react"; -// plane editor +// plane imports import { EditorRefApi, IMarking } from "@plane/editor"; +import { cn } from "@plane/utils"; // components -import { OutlineHeading1, OutlineHeading2, OutlineHeading3 } from "./heading-components"; +import { OutlineHeading1, OutlineHeading2, OutlineHeading3, THeadingComponentProps } from "./heading-components"; type Props = { + className?: string; editorRef: EditorRefApi | null; setSidePeekVisible?: (sidePeekState: boolean) => void; showOutline?: boolean; }; export const PageContentBrowser: React.FC = (props) => { - const { editorRef, setSidePeekVisible, showOutline = false } = props; + const { className, editorRef, setSidePeekVisible, showOutline = false } = props; // states const [headings, setHeadings] = useState([]); @@ -20,7 +22,7 @@ export const PageContentBrowser: React.FC = (props) => { // for initial render of this component to get the editor headings setHeadings(editorRef?.getHeadings() ?? []); return () => { - if (unsubscribe) unsubscribe(); + unsubscribe?.(); }; }, [editorRef]); @@ -33,7 +35,7 @@ export const PageContentBrowser: React.FC = (props) => { ); const HeadingComponent: { - [key: number]: React.FC<{ marking: IMarking; onClick: () => void }>; + [key: number]: React.FC; } = { 1: OutlineHeading1, 2: OutlineHeading2, @@ -41,7 +43,15 @@ export const PageContentBrowser: React.FC = (props) => { }; return ( -
+
{headings.map((marking) => { const Component = HeadingComponent[marking.level]; if (!Component) return null; diff --git a/web/core/components/pages/editor/summary/heading-components.tsx b/web/core/components/pages/editor/summary/heading-components.tsx index c2e78dd67f1..d06eaded41c 100644 --- a/web/core/components/pages/editor/summary/heading-components.tsx +++ b/web/core/components/pages/editor/summary/heading-components.tsx @@ -1,37 +1,29 @@ -// plane editor +// plane imports import type { IMarking } from "@plane/editor"; +import { cn } from "@plane/utils"; export type THeadingComponentProps = { marking: IMarking; onClick: (event: React.MouseEvent) => void; }; +const COMMON_CLASSNAME = + "w-full py-1 text-left font-medium text-custom-text-300 hover:text-custom-primary-100 truncate transition-colors"; + export const OutlineHeading1 = ({ marking, onClick }: THeadingComponentProps) => ( - ); export const OutlineHeading2 = ({ marking, onClick }: THeadingComponentProps) => ( - ); export const OutlineHeading3 = ({ marking, onClick }: THeadingComponentProps) => ( - ); diff --git a/web/core/components/pages/navigation-pane/index.ts b/web/core/components/pages/navigation-pane/index.ts index b852c6ab723..0dd3c4bd581 100644 --- a/web/core/components/pages/navigation-pane/index.ts +++ b/web/core/components/pages/navigation-pane/index.ts @@ -3,3 +3,4 @@ export * from "./root"; export const PAGE_NAVIGATION_PANE_WIDTH = 294; export const PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM = "sidebarTab"; +export const PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM = "version"; diff --git a/web/core/components/pages/navigation-pane/root.tsx b/web/core/components/pages/navigation-pane/root.tsx index 75e2737cb0f..286d2786eaf 100644 --- a/web/core/components/pages/navigation-pane/root.tsx +++ b/web/core/components/pages/navigation-pane/root.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import React, { useCallback } from "react"; import { observer } from "mobx-react"; import { useRouter, useSearchParams } from "next/navigation"; import { ArrowRightCircle } from "lucide-react"; @@ -13,6 +13,7 @@ import { // store import { TPageInstance } from "@/store/pages/base-page"; // local imports +import { TPageRootHandlers } from "../editor"; import { PageNavigationPaneTabPanelsRoot } from "./tab-panels/root"; import { PageNavigationPaneTabsList } from "./tabs-list"; import { PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, PAGE_NAVIGATION_PANE_WIDTH } from "./index"; @@ -21,10 +22,11 @@ type Props = { handleClose: () => void; isNavigationPaneOpen: boolean; page: TPageInstance; + versionHistory: Pick; }; export const PageNavigationPaneRoot: React.FC = observer((props) => { - const { handleClose, isNavigationPaneOpen, page } = props; + const { handleClose, isNavigationPaneOpen, page, versionHistory } = props; // navigation const router = useRouter(); const searchParams = useSearchParams(); @@ -50,14 +52,14 @@ export const PageNavigationPaneRoot: React.FC = observer((props) => { ); return ( -
-
+
- + - + -
+ ); }); diff --git a/web/core/components/pages/navigation-pane/tab-panels/info.tsx b/web/core/components/pages/navigation-pane/tab-panels/info.tsx deleted file mode 100644 index 5838cb2bff6..00000000000 --- a/web/core/components/pages/navigation-pane/tab-panels/info.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { observer } from "mobx-react"; -import Link from "next/link"; -import { useParams } from "next/navigation"; -// plane imports -import { useTranslation } from "@plane/i18n"; -import { Avatar } from "@plane/ui"; -import { calculateTimeAgoShort, getFileURL, getReadTimeFromWordsCount, renderFormattedDate } from "@plane/utils"; -// hooks -import { useMember } from "@/hooks/store"; -// store -import { TPageInstance } from "@/store/pages/base-page"; - -type Props = { - page: TPageInstance; -}; - -export const PageNavigationPaneInfoTabPanel: React.FC = observer((props) => { - const { page } = props; - // navigation - const { workspaceSlug } = useParams(); - // store hooks - const { getUserDetails } = useMember(); - // derived values - const { created_by, editorRef, updated_by } = page; - const editorInformation = updated_by ? getUserDetails(updated_by) : undefined; - const creatorInformation = created_by ? getUserDetails(created_by) : undefined; - const documentsInfo = editorRef?.getDocumentInfo() || { words: 0, characters: 0, paragraphs: 0 }; - // translation - const { t } = useTranslation(); - - const secondsToReadableTime = () => { - const wordsCount = documentsInfo.words; - const readTimeInSeconds = Number(getReadTimeFromWordsCount(wordsCount).toFixed(0)); - return readTimeInSeconds < 60 ? `${readTimeInSeconds}s` : `${Math.ceil(readTimeInSeconds / 60)}m`; - }; - - const documentInfoCards = [ - { - key: "words-count", - title: "Words", - info: documentsInfo.words, - }, - { - key: "characters-count", - title: "Characters", - info: documentsInfo.characters, - }, - { - key: "paragraphs-count", - title: "Paragraphs", - info: documentsInfo.paragraphs, - }, - { - key: "read-time", - title: "Read time", - info: secondsToReadableTime(), - }, - ]; - - return ( -
-
- {documentInfoCards.map((card) => ( -
-
{card.info}
-

{card.title}

-
- ))} -
-
-
-

Edited by

-
- - - {editorInformation?.display_name ?? t("common.deactivated_user")} - - - {calculateTimeAgoShort(page.updated_at ?? "")} ago - -
-
-
-

{t("common.created_by")}

-
- - - {creatorInformation?.display_name ?? t("common.deactivated_user")} - - {renderFormattedDate(page.created_at)} -
-
-
-
- ); -}); diff --git a/web/core/components/pages/navigation-pane/tab-panels/info/actors-info.tsx b/web/core/components/pages/navigation-pane/tab-panels/info/actors-info.tsx new file mode 100644 index 00000000000..aabad54fc72 --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/info/actors-info.tsx @@ -0,0 +1,64 @@ +import { observer } from "mobx-react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { Avatar } from "@plane/ui"; +import { calculateTimeAgoShort, getFileURL, renderFormattedDate } from "@plane/utils"; +// hooks +import { useMember } from "@/hooks/store"; +// store +import { TPageInstance } from "@/store/pages/base-page"; + +type Props = { + page: TPageInstance; +}; + +export const PageNavigationPaneInfoTabActorsInfo: React.FC = observer((props) => { + const { page } = props; + // navigation + const { workspaceSlug } = useParams(); + // store hooks + const { getUserDetails } = useMember(); + // derived values + const { created_by, updated_by } = page; + const editorInformation = updated_by ? getUserDetails(updated_by) : undefined; + const creatorInformation = created_by ? getUserDetails(created_by) : undefined; + // translation + const { t } = useTranslation(); + + return ( +
+
+

Edited by

+
+ + + {editorInformation?.display_name ?? t("common.deactivated_user")} + + {calculateTimeAgoShort(page.updated_at ?? "")} ago +
+
+
+

{t("common.created_by")}

+
+ + + {creatorInformation?.display_name ?? t("common.deactivated_user")} + + {renderFormattedDate(page.created_at)} +
+
+
+ ); +}); diff --git a/web/core/components/pages/navigation-pane/tab-panels/info/root.tsx b/web/core/components/pages/navigation-pane/tab-panels/info/root.tsx new file mode 100644 index 00000000000..09901d672ee --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/info/root.tsx @@ -0,0 +1,67 @@ +import { observer } from "mobx-react"; +// plane imports +import { getReadTimeFromWordsCount } from "@plane/utils"; +// components +import { TPageRootHandlers } from "@/components/pages/editor"; +// store +import { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { PageNavigationPaneInfoTabActorsInfo } from "./actors-info"; +import { PageNavigationPaneInfoTabVersionHistory } from "./version-history"; + +type Props = { + page: TPageInstance; + versionHistory: Pick; +}; + +export const PageNavigationPaneInfoTabPanel: React.FC = observer((props) => { + const { page, versionHistory } = props; + // derived values + const { editorRef } = page; + const documentsInfo = editorRef?.getDocumentInfo() || { words: 0, characters: 0, paragraphs: 0 }; + + const secondsToReadableTime = () => { + const wordsCount = documentsInfo.words; + const readTimeInSeconds = Number(getReadTimeFromWordsCount(wordsCount).toFixed(0)); + return readTimeInSeconds < 60 ? `${readTimeInSeconds}s` : `${Math.ceil(readTimeInSeconds / 60)}m`; + }; + + const documentInfoCards = [ + { + key: "words-count", + title: "Words", + info: documentsInfo.words, + }, + { + key: "characters-count", + title: "Characters", + info: documentsInfo.characters, + }, + { + key: "paragraphs-count", + title: "Paragraphs", + info: documentsInfo.paragraphs, + }, + { + key: "read-time", + title: "Read time", + info: secondsToReadableTime(), + }, + ]; + + return ( +
+
+ {documentInfoCards.map((card) => ( +
+
{card.info}
+

{card.title}

+
+ ))} +
+ +
+ +
+ ); +}); diff --git a/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx b/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx new file mode 100644 index 00000000000..882dc16dfc9 --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx @@ -0,0 +1,136 @@ +import { useCallback } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +import useSWR from "swr"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { TPageVersion } from "@plane/types"; +import { Avatar } from "@plane/ui"; +import { cn, getFileURL, renderFormattedDate } from "@plane/utils"; +// components +import { TPageRootHandlers } from "@/components/pages/editor"; +// helpers +import { renderFormattedTime } from "@/helpers/date-time.helper"; +// hooks +import { useMember } from "@/hooks/store"; +import { useQueryParams } from "@/hooks/use-query-params"; +// store +import { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM } from "../.."; + +type Props = { + page: TPageInstance; + versionHistory: Pick; +}; + +const VersionHistoryItem = observer( + (props: { getVersionLink: (versionID: string) => string; isVersionActive: boolean; version: TPageVersion }) => { + const { getVersionLink, isVersionActive, version } = props; + // store hooks + const { getUserDetails } = useMember(); + // derived values + const versionCreator = getUserDetails(version.created_by); + // translation + const { t } = useTranslation(); + + return ( +
  • + {/* timeline icon */} +
    +
    +
    + {/* end timeline icon */} + +

    + {renderFormattedDate(version.last_saved_at)}, {renderFormattedTime(version.last_saved_at)} +

    +

    + + {versionCreator?.display_name ?? t("common.deactivated_user")} +

    + +
  • + ); + } +); + +export const PageNavigationPaneInfoTabVersionHistory: React.FC = observer((props) => { + const { page, versionHistory } = props; + // navigation + const searchParams = useSearchParams(); + const activeVersion = searchParams.get(PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM); + // derived values + const { id } = page; + // query params + const { updateQueryParams } = useQueryParams(); + // fetch all versions + const { data: versionsList } = useSWR( + id ? `PAGE_VERSIONS_LIST_${id}` : null, + id ? () => versionHistory.fetchAllVersions(id) : null + ); + + const getVersionLink = useCallback( + (versionID?: string) => { + if (versionID) { + return updateQueryParams({ + paramsToAdd: { [PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM]: versionID }, + }); + } else { + return updateQueryParams({ + paramsToRemove: [PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM], + }); + } + }, + [updateQueryParams] + ); + + return ( +
    +

    Version history

    +
    +
      + {/* timeline line */} +
      +
      +
      + {/* end timeline line */} +
    • + {/* timeline icon */} +
      +
      +
      + {/* end timeline icon */} + + Current version + +
    • + {versionsList?.map((version) => ( + + ))} +
    +
    +
    + ); +}); diff --git a/web/core/components/pages/navigation-pane/tab-panels/outline.tsx b/web/core/components/pages/navigation-pane/tab-panels/outline.tsx index 42259517672..df8ce02954e 100644 --- a/web/core/components/pages/navigation-pane/tab-panels/outline.tsx +++ b/web/core/components/pages/navigation-pane/tab-panels/outline.tsx @@ -12,8 +12,8 @@ export const PageNavigationPaneOutlineTabPanel: React.FC = (props) => { const { editorRef } = page; return ( -
    - +
    +
    ); }; diff --git a/web/core/components/pages/navigation-pane/tab-panels/root.tsx b/web/core/components/pages/navigation-pane/tab-panels/root.tsx index fdd51d66809..ebab53bf8e7 100644 --- a/web/core/components/pages/navigation-pane/tab-panels/root.tsx +++ b/web/core/components/pages/navigation-pane/tab-panels/root.tsx @@ -1,25 +1,35 @@ +import React from "react"; import { Tab } from "@headlessui/react"; +// components +import { TPageRootHandlers } from "@/components/pages/editor"; // plane web imports import { PAGE_NAVIGATION_PANE_TABS_LIST } from "@/plane-web/components/pages/editor/navigation-pane"; // store import { TPageInstance } from "@/store/pages/base-page"; // local imports -import { PageNavigationPaneInfoTabPanel } from "./info"; +import { PageNavigationPaneAssetsTabPanel } from "./assets"; +import { PageNavigationPaneInfoTabPanel } from "./info/root"; import { PageNavigationPaneOutlineTabPanel } from "./outline"; type Props = { page: TPageInstance; + versionHistory: Pick; }; export const PageNavigationPaneTabPanelsRoot: React.FC = (props) => { - const { page } = props; + const { page, versionHistory } = props; return ( - + {PAGE_NAVIGATION_PANE_TABS_LIST.map((tab) => ( - + {tab.key === "outline" && } - {tab.key === "info" && } + {tab.key === "info" && } + {tab.key === "assets" && } ))} diff --git a/web/core/components/pages/navigation-pane/tabs-list.tsx b/web/core/components/pages/navigation-pane/tabs-list.tsx index bd826b2cdf1..6c089016d92 100644 --- a/web/core/components/pages/navigation-pane/tabs-list.tsx +++ b/web/core/components/pages/navigation-pane/tabs-list.tsx @@ -9,7 +9,7 @@ export const PageNavigationPaneTabsList = () => { const { t } = useTranslation(); return ( - + {({ selectedIndex }) => ( <> {PAGE_NAVIGATION_PANE_TABS_LIST.map((tab) => ( diff --git a/web/core/components/pages/version/editor.tsx b/web/core/components/pages/version/editor.tsx index f0b28e24e59..783e4c7e893 100644 --- a/web/core/components/pages/version/editor.tsx +++ b/web/core/components/pages/version/editor.tsx @@ -16,13 +16,11 @@ import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed"; export type TVersionEditorProps = { activeVersion: string | null; - currentVersionDescription: string | null; - isCurrentVersionActive: boolean; versionDetails: TPageVersion | undefined; }; export const PagesVersionEditor: React.FC = observer((props) => { - const { activeVersion, currentVersionDescription, isCurrentVersionActive, versionDetails } = props; + const { activeVersion, versionDetails } = props; // store hooks const { getUserDetails } = useMember(); // params @@ -49,7 +47,7 @@ export const PagesVersionEditor: React.FC = observer((props wideLayout: true, }; - if (!isCurrentVersionActive && !versionDetails) + if (!versionDetails) return (
    @@ -91,7 +89,7 @@ export const PagesVersionEditor: React.FC = observer((props
    ); - const description = isCurrentVersionActive ? currentVersionDescription : versionDetails?.description_html; + const description = versionDetails?.description_html; if (description === undefined || description?.trim() === "") return null; return ( diff --git a/web/core/components/pages/version/index.ts b/web/core/components/pages/version/index.ts index 8e04e4de9e0..5da43e95910 100644 --- a/web/core/components/pages/version/index.ts +++ b/web/core/components/pages/version/index.ts @@ -1,6 +1,3 @@ export * from "./editor"; export * from "./main-content"; export * from "./root"; -export * from "./sidebar-list-item"; -export * from "./sidebar-list"; -export * from "./sidebar-root"; diff --git a/web/core/components/pages/version/main-content.tsx b/web/core/components/pages/version/main-content.tsx index b36820fbbf6..7a26f671452 100644 --- a/web/core/components/pages/version/main-content.tsx +++ b/web/core/components/pages/version/main-content.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; -import { TriangleAlert } from "lucide-react"; +import { EyeIcon, TriangleAlert } from "lucide-react"; // plane types import { TPageVersion } from "@plane/types"; // plane ui @@ -13,7 +13,6 @@ import { renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.he type Props = { activeVersion: string | null; - currentVersionDescription: string | null; editorComponent: React.FC; fetchVersionDetails: (pageId: string, versionId: string) => Promise; handleClose: () => void; @@ -23,16 +22,8 @@ type Props = { }; export const PageVersionsMainContent: React.FC = observer((props) => { - const { - activeVersion, - currentVersionDescription, - editorComponent, - fetchVersionDetails, - handleClose, - handleRestore, - pageId, - restoreEnabled, - } = props; + const { activeVersion, editorComponent, fetchVersionDetails, handleClose, handleRestore, pageId, restoreEnabled } = + props; // states const [isRestoring, setIsRestoring] = useState(false); const [isRetrying, setIsRetrying] = useState(false); @@ -42,12 +33,10 @@ export const PageVersionsMainContent: React.FC = observer((props) => { error: versionDetailsError, mutate: mutateVersionDetails, } = useSWR( - pageId && activeVersion && activeVersion !== "current" ? `PAGE_VERSION_${activeVersion}` : null, - pageId && activeVersion && activeVersion !== "current" ? () => fetchVersionDetails(pageId, activeVersion) : null + pageId && activeVersion ? `PAGE_VERSION_${activeVersion}` : null, + pageId && activeVersion ? () => fetchVersionDetails(pageId, activeVersion) : null ); - const isCurrentVersionActive = activeVersion === "current"; - const handleRestoreVersion = async () => { if (!restoreEnabled) return; setIsRestoring(true); @@ -96,14 +85,18 @@ export const PageVersionsMainContent: React.FC = observer((props) => { ) : ( <>
    -
    - {isCurrentVersionActive - ? "Current version" - : versionDetails +
    +
    + {versionDetails ? `${renderFormattedDate(versionDetails.last_saved_at)} ${renderFormattedTime(versionDetails.last_saved_at)}` : "Loading version details"} -
    - {!isCurrentVersionActive && restoreEnabled && ( +
    + + + View only + +
    + {restoreEnabled && (
    - +
    )} diff --git a/web/core/components/pages/version/root.tsx b/web/core/components/pages/version/root.tsx index 0717be012e9..fc391976244 100644 --- a/web/core/components/pages/version/root.tsx +++ b/web/core/components/pages/version/root.tsx @@ -1,54 +1,57 @@ +import { useCallback } from "react"; import { observer } from "mobx-react"; +import { useRouter, useSearchParams } from "next/navigation"; // plane types import { TPageVersion } from "@plane/types"; // components -import { PageVersionsMainContent, PageVersionsSidebarRoot, TVersionEditorProps } from "@/components/pages"; +import { PageVersionsMainContent, TVersionEditorProps } from "@/components/pages"; // helpers import { cn } from "@/helpers/common.helper"; +// hooks +import { useQueryParams } from "@/hooks/use-query-params"; +// local imports +import { PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM, PAGE_NAVIGATION_PANE_WIDTH } from "../navigation-pane"; type Props = { - activeVersion: string | null; - currentVersionDescription: string | null; editorComponent: React.FC; - fetchAllVersions: (pageId: string) => Promise; fetchVersionDetails: (pageId: string, versionId: string) => Promise; handleRestore: (descriptionHTML: string) => Promise; - isOpen: boolean; - onClose: () => void; pageId: string; restoreEnabled: boolean; }; export const PageVersionsOverlay: React.FC = observer((props) => { - const { - activeVersion, - currentVersionDescription, - editorComponent, - fetchAllVersions, - fetchVersionDetails, - handleRestore, - isOpen, - onClose, - pageId, - restoreEnabled, - } = props; + const { editorComponent, fetchVersionDetails, handleRestore, pageId, restoreEnabled } = props; + // navigation + const router = useRouter(); + const searchParams = useSearchParams(); + // query params + const { updateQueryParams } = useQueryParams(); + // derived values + const activeVersion = searchParams.get(PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM); + const isOpen = !!activeVersion; - const handleClose = () => { - onClose(); - }; + const handleClose = useCallback(() => { + const updatedRoute = updateQueryParams({ + paramsToRemove: [PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM], + }); + router.push(updatedRoute); + }, [router, updateQueryParams]); return (
    = observer((props) => { pageId={pageId} restoreEnabled={restoreEnabled} /> -
    ); }); diff --git a/web/core/components/pages/version/sidebar-list-item.tsx b/web/core/components/pages/version/sidebar-list-item.tsx deleted file mode 100644 index be11f57250c..00000000000 --- a/web/core/components/pages/version/sidebar-list-item.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { observer } from "mobx-react"; -import Link from "next/link"; -// plane types -import { TPageVersion } from "@plane/types"; -// plane ui -import { Avatar } from "@plane/ui"; -// helpers -import { cn } from "@/helpers/common.helper"; -import { renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; -// hooks -import { useMember } from "@/hooks/store"; - -type Props = { - href: string; - isActive: boolean; - version: TPageVersion; -}; - -export const PlaneVersionsSidebarListItem: React.FC = observer((props) => { - const { href, isActive, version } = props; - // store hooks - const { getUserDetails } = useMember(); - // derived values - const ownerDetails = getUserDetails(version.owned_by); - - return ( - -

    - {renderFormattedDate(version.last_saved_at)} {renderFormattedTime(version.last_saved_at)} -

    -

    - - {ownerDetails?.display_name} -

    - - ); -}); diff --git a/web/core/components/pages/version/sidebar-list.tsx b/web/core/components/pages/version/sidebar-list.tsx deleted file mode 100644 index cf276742b02..00000000000 --- a/web/core/components/pages/version/sidebar-list.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useState } from "react"; -import Link from "next/link"; -import useSWR from "swr"; -import { TriangleAlert } from "lucide-react"; -// plane types -import { TPageVersion } from "@plane/types"; -// plane ui -import { Button, Loader } from "@plane/ui"; -// components -import { PlaneVersionsSidebarListItem } from "@/components/pages"; -// helpers -import { cn } from "@/helpers/common.helper"; -// hooks -import { useQueryParams } from "@/hooks/use-query-params"; - -type Props = { - activeVersion: string | null; - fetchAllVersions: (pageId: string) => Promise; - isOpen: boolean; - pageId: string; -}; - -export const PageVersionsSidebarList: React.FC = (props) => { - const { activeVersion, fetchAllVersions, isOpen, pageId } = props; - // states - const [isRetrying, setIsRetrying] = useState(false); - // update query params - const { updateQueryParams } = useQueryParams(); - - const { - data: versionsList, - error: versionsListError, - mutate: mutateVersionsList, - } = useSWR( - pageId && isOpen ? `PAGE_VERSIONS_LIST_${pageId}` : null, - pageId && isOpen ? () => fetchAllVersions(pageId) : null - ); - - const handleRetry = async () => { - setIsRetrying(true); - await mutateVersionsList(); - setIsRetrying(false); - }; - - const getVersionLink = (versionID: string) => - updateQueryParams({ - paramsToAdd: { version: versionID }, - }); - - return ( -
    - -

    Current version

    - - {versionsListError ? ( -
    -
    - - - -
    -
    Something went wrong!
    -

    - There was a problem while loading previous -
    - versions, please try again. -

    -
    - -
    -
    - ) : versionsList ? ( - versionsList.map((version) => ( - - )) - ) : ( - - - - - - - - )} -
    - ); -}; diff --git a/web/core/components/pages/version/sidebar-root.tsx b/web/core/components/pages/version/sidebar-root.tsx deleted file mode 100644 index 793d7fed90f..00000000000 --- a/web/core/components/pages/version/sidebar-root.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { X } from "lucide-react"; -// plane types -import { TPageVersion } from "@plane/types"; -// components -import { PageVersionsSidebarList } from "@/components/pages"; - -type Props = { - activeVersion: string | null; - fetchAllVersions: (pageId: string) => Promise; - handleClose: () => void; - isOpen: boolean; - pageId: string; -}; - -export const PageVersionsSidebarRoot: React.FC = (props) => { - const { activeVersion, fetchAllVersions, handleClose, isOpen, pageId } = props; - - return ( -
    -
    -
    Version history
    - -
    - -
    - ); -}; diff --git a/web/styles/globals.css b/web/styles/globals.css index c6e4654d077..51c4419b86f 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -497,6 +497,7 @@ text-rendering: optimizeLegibility; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; + scroll-behavior: smooth; } body { From cf463a4ce995ccd6bc3d40134a6aeea5b96a41ec Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 12 Jun 2025 16:44:00 +0530 Subject: [PATCH 03/20] chore: asset download endpoint --- apiserver/plane/app/urls/asset.py | 12 ++ apiserver/plane/app/views/__init__.py | 2 + apiserver/plane/app/views/asset/v2.py | 35 +++++ packages/editor/src/ce/constants/assets.ts | 6 + .../custom-image/components/image-block.tsx | 1 + packages/editor/src/core/helpers/assets.ts | 52 ++++++++ packages/editor/src/core/hooks/use-editor.ts | 65 +++++---- .../src/core/hooks/use-read-only-editor.ts | 26 +++- packages/editor/src/core/types/editor.ts | 14 ++ .../components/pages/editor/editor-body.tsx | 22 +-- .../components/pages/editor/page-root.tsx | 1 + .../pages/editor/summary/content-browser.tsx | 5 +- .../components/pages/editor/toolbar/root.tsx | 63 ++++++--- .../navigation-pane/tab-panels/assets.tsx | 125 ++++++++++++++++++ .../tab-panels/info/version-history.tsx | 82 ++++++------ .../navigation-pane/tab-panels/outline.tsx | 24 +++- .../pages/navigation-pane/tab-panels/root.tsx | 2 +- web/helpers/editor.helper.ts | 15 +++ .../pages/navigation-pane/outline-dark.webp | Bin 0 -> 18104 bytes .../pages/navigation-pane/outline-light.webp | Bin 0 -> 18782 bytes 20 files changed, 452 insertions(+), 100 deletions(-) create mode 100644 packages/editor/src/ce/constants/assets.ts create mode 100644 packages/editor/src/core/helpers/assets.ts create mode 100644 web/core/components/pages/navigation-pane/tab-panels/assets.tsx create mode 100644 web/public/empty-state/pages/navigation-pane/outline-dark.webp create mode 100644 web/public/empty-state/pages/navigation-pane/outline-light.webp diff --git a/apiserver/plane/app/urls/asset.py b/apiserver/plane/app/urls/asset.py index 77dd3d00efa..93356b04cb4 100644 --- a/apiserver/plane/app/urls/asset.py +++ b/apiserver/plane/app/urls/asset.py @@ -13,6 +13,8 @@ ProjectAssetEndpoint, ProjectBulkAssetEndpoint, AssetCheckEndpoint, + WorkspaceAssetDownloadEndpoint, + ProjectAssetDownloadEndpoint, ) @@ -89,4 +91,14 @@ AssetCheckEndpoint.as_view(), name="asset-check", ), + path( + "assets/v2/workspaces//download//", + WorkspaceAssetDownloadEndpoint.as_view(), + name="workspace-asset-download", + ), + path( + "assets/v2/workspaces//projects//download//", + ProjectAssetDownloadEndpoint.as_view(), + name="project-asset-download", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 55642a53358..6d56473e3f1 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -107,6 +107,8 @@ ProjectAssetEndpoint, ProjectBulkAssetEndpoint, AssetCheckEndpoint, + WorkspaceAssetDownloadEndpoint, + ProjectAssetDownloadEndpoint, ) from .issue.base import ( IssueListEndpoint, diff --git a/apiserver/plane/app/views/asset/v2.py b/apiserver/plane/app/views/asset/v2.py index aecba04b8c3..f0e583f91b7 100644 --- a/apiserver/plane/app/views/asset/v2.py +++ b/apiserver/plane/app/views/asset/v2.py @@ -718,3 +718,38 @@ def get(self, request, slug, asset_id): id=asset_id, workspace__slug=slug, deleted_at__isnull=True ).exists() return Response({"exists": asset}, status=status.HTTP_200_OK) + + +class WorkspaceAssetDownloadEndpoint(BaseAPIView): + """Endpoint to generate a download link for an asset with content-disposition=attachment.""" + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") + def get(self, request, slug, asset_id): + asset = FileAsset.all_objects.get( + id=asset_id, workspace__slug=slug, deleted_at__isnull=True + ) + storage = S3Storage(request=request) + signed_url = storage.generate_presigned_url( + object_name=asset.asset.name, + disposition=f"attachment; filename={asset.asset.name}", + ) + return HttpResponseRedirect(signed_url) + + +class ProjectAssetDownloadEndpoint(BaseAPIView): + """Endpoint to generate a download link for an asset with content-disposition=attachment.""" + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="PROJECT") + def get(self, request, slug, project_id, asset_id): + asset = FileAsset.all_objects.get( + id=asset_id, + workspace__slug=slug, + project_id=project_id, + deleted_at__isnull=True, + ) + storage = S3Storage(request=request) + signed_url = storage.generate_presigned_url( + object_name=asset.asset.name, + disposition=f"attachment; filename={asset.asset.name}", + ) + return HttpResponseRedirect(signed_url) diff --git a/packages/editor/src/ce/constants/assets.ts b/packages/editor/src/ce/constants/assets.ts new file mode 100644 index 00000000000..12b89b3abaa --- /dev/null +++ b/packages/editor/src/ce/constants/assets.ts @@ -0,0 +1,6 @@ +// helpers +import { TAssetMetaDataRecord } from "@/helpers/assets"; +// local imports +import { ADDITIONAL_EXTENSIONS } from "./extensions"; + +export const ADDITIONAL_ASSETS_META_DATA_RECORD: Partial> = {}; diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx index 5dfbad01294..280d3dce662 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx @@ -222,6 +222,7 @@ export const CustomImageBlock: React.FC = (props) => { return (
    TEditorAsset | undefined; + +const ASSETS_META_DATA_RECORD: Partial> = { + [CORE_EXTENSIONS.IMAGE]: (node: Node, index) => { + if (!node.attrs?.src) return; + return { + id: node.attrs?.id, + name: `image-${index + 1}`, + scrollId: `#image-block-${node.attrs?.id}`, + size: 0, + src: node.attrs?.src, + type: "IMAGE", + }; + }, + [CORE_EXTENSIONS.CUSTOM_IMAGE]: (node: Node, index) => { + if (!node.attrs?.src) return; + return { + id: node.attrs?.id, + name: `image-${index + 1}`, + scrollId: `#image-block-${node.attrs?.id}`, + size: 0, + src: node.attrs?.src, + type: "CUSTOM_IMAGE", + }; + }, +}; + +export const getAllEditorAssets = (editor: Editor): TEditorAsset[] => { + const assets: TEditorAsset[] = []; + editor.state.doc.descendants((node, _pos, _parent, index) => { + const assetMetaData = ASSETS_META_DATA_RECORD[node.type.name as keyof typeof ASSETS_META_DATA_RECORD]; + const additionalAssetMetaData = + ADDITIONAL_ASSETS_META_DATA_RECORD[node.type.name as unknown as keyof typeof ADDITIONAL_ASSETS_META_DATA_RECORD]; + let asset: TEditorAsset | undefined = undefined; + if (assetMetaData) { + asset = assetMetaData(node, index); + } else if (additionalAssetMetaData) { + asset = additionalAssetMetaData(node, index); + } + if (asset) assets.push(asset); + }); + return assets; +}; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index ce3cdbe5fb5..e3cd769f805 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -8,25 +8,19 @@ import * as Y from "yjs"; import { getEditorMenuItems } from "@/components/menus"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; +import { CORE_EDITOR_META } from "@/constants/meta"; // extensions import { CoreEditorExtensions } from "@/extensions"; // helpers +import { getAllEditorAssets } from "@/helpers/assets"; import { getParagraphCount } from "@/helpers/common"; import { getExtensionStorage } from "@/helpers/get-extension-storage"; import { insertContentAtSavedSelection } from "@/helpers/insert-content-at-cursor-position"; -import { IMarking, scrollSummary, scrollToNodeViaDOMCoordinates } from "@/helpers/scroll-to-node"; +import { scrollSummary, scrollToNodeViaDOMCoordinates } from "@/helpers/scroll-to-node"; // props import { CoreEditorProps } from "@/props"; // types -import type { - TDocumentEventsServer, - EditorRefApi, - TEditorCommands, - TFileHandler, - TExtensions, - TMentionHandler, -} from "@/types"; -import { CORE_EDITOR_META } from "@/constants/meta"; +import type { EditorRefApi, TEditorCommands, TFileHandler, TExtensions, TMentionHandler } from "@/types"; export interface CustomEditorProps { editable: boolean; @@ -144,7 +138,7 @@ export const useEditor = (props: CustomEditorProps) => { forwardedRef, () => ({ blur: () => editor?.commands.blur(), - scrollToNodeViaDOMCoordinates(behavior?: ScrollBehavior, pos?: number) { + scrollToNodeViaDOMCoordinates(behavior, pos) { const resolvedPos = pos ?? editor?.state.selection.from; if (!editor || !resolvedPos) return; scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior); @@ -153,10 +147,10 @@ export const useEditor = (props: CustomEditorProps) => { clearEditor: (emitUpdate = false) => { editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); }, - setEditorValue: (content: string, emitUpdate = false) => { + setEditorValue: (content, emitUpdate = false) => { editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true }); }, - setEditorValueAtCursorPosition: (content: string) => { + setEditorValueAtCursorPosition: (content) => { if (editor?.state.selection) { insertContentAtSavedSelection(editor, content); } @@ -184,36 +178,53 @@ export const useEditor = (props: CustomEditorProps) => { return item.isActive(props); }, - onHeadingChange: (callback: (headings: IMarking[]) => void) => { - // Subscribe to update event emitted from headers extension - editor?.on("update", () => { + 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); + }; + }, + onAssetChange: (callback) => { + const handleAssetChange = () => { + if (!editor) return; + const assets = getAllEditorAssets(editor); + callback(assets); + }; + + // Subscribe to update assets + editor?.on("update", handleAssetChange); // 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"); + editor?.off("update", handleAssetChange); }; }, getHeadings: () => (editor ? getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings : []), - onStateChange: (callback: () => void) => { + onStateChange: (callback) => { // Subscribe to editor state changes - editor?.on("transaction", () => { - callback(); - }); + 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"); + editor?.off("transaction", callback); }; }, - getMarkDown: (): string => { + getMarkDown: () => { const markdownOutput = editor?.storage.markdown.getMarkdown(); return markdownOutput; }, @@ -228,13 +239,13 @@ export const useEditor = (props: CustomEditorProps) => { json: documentJSON, }; }, - scrollSummary: (marking: IMarking): void => { + scrollSummary: (marking) => { if (!editor) return; scrollSummary(editor, marking); }, isEditorReadyToDiscard: () => !!editor && getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress === false, - setFocusAtPosition: (position: number) => { + setFocusAtPosition: (position) => { if (!editor || editor.isDestroyed) { console.error("Editor reference is not available or has been destroyed."); return; @@ -294,7 +305,7 @@ export const useEditor = (props: CustomEditorProps) => { if (!document) return; Y.applyUpdate(document, value); }, - emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message), + emitRealTimeUpdate: (message) => provider?.sendStateless(message), listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) }, }), [editor] diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index 6a6e25d9fd5..497a6260655 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -3,16 +3,18 @@ import { EditorProps } from "@tiptap/pm/view"; import { useEditor as useTiptapEditor, Extensions } from "@tiptap/react"; import { useImperativeHandle, MutableRefObject, useEffect } from "react"; import * as Y from "yjs"; +// constants +import { CORE_EDITOR_META } from "@/constants/meta"; // extensions import { CoreReadOnlyEditorExtensions } from "@/extensions"; // helpers +import { getAllEditorAssets } from "@/helpers/assets"; import { getParagraphCount } from "@/helpers/common"; import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; // props import { CoreReadOnlyEditorProps } from "@/props"; // types import type { EditorReadOnlyRefApi, TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types"; -import { CORE_EDITOR_META } from "@/constants/meta"; interface CustomReadOnlyEditorProps { disabledExtensions: TExtensions[]; @@ -79,10 +81,10 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { clearEditor: (emitUpdate = false) => { editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); }, - setEditorValue: (content: string, emitUpdate = false) => { + setEditorValue: (content, emitUpdate = false) => { editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true }); }, - getMarkDown: (): string => { + getMarkDown: () => { const markdownOutput = editor?.storage.markdown.getMarkdown(); return markdownOutput; }, @@ -97,7 +99,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { json: documentJSON, }; }, - scrollSummary: (marking: IMarking): void => { + scrollSummary: (marking) => { if (!editor) return; scrollSummary(editor, marking); }, @@ -106,6 +108,22 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { paragraphs: getParagraphCount(editor.state), words: editor.storage?.characterCount?.words?.() ?? 0, }), + onAssetChange: (callback) => { + const handleAssetChange = () => { + if (!editor) return; + const assets = getAllEditorAssets(editor); + callback(assets); + }; + + // Subscribe to update assets + editor?.on("update", handleAssetChange); + // 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", handleAssetChange); + }; + }, })); if (!editor) { diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index ace1048ce8c..2039f14ea8e 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -2,10 +2,14 @@ import { Extensions, JSONContent } from "@tiptap/core"; import { Selection } from "@tiptap/pm/state"; // plane types import { TWebhookConnectionQueryParams } from "@plane/types"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extension types import { TTextAlign } from "@/extensions"; // helpers import { IMarking } from "@/helpers/scroll-to-node"; +// plane editor imports +import { ADDITIONAL_EXTENSIONS } from "@/plane-editor/constants/extensions"; // types import { TAIHandler, @@ -75,6 +79,15 @@ type TCommandWithPropsWithItemKey = T extends keyof T ? { itemKey: T } & TCommandExtraProps[T] : { itemKey: T }; +export type TEditorAsset = { + id: string; + name: string; + scrollId: string; + size: number; + src: string; + type: keyof typeof CORE_EXTENSIONS | keyof typeof ADDITIONAL_EXTENSIONS; +}; + // editor refs export type EditorReadOnlyRefApi = { getMarkDown: () => string; @@ -91,6 +104,7 @@ export type EditorReadOnlyRefApi = { paragraphs: number; words: number; }; + onAssetChange: (callback: (assets: TEditorAsset[]) => void) => () => void; }; export interface EditorRefApi extends EditorReadOnlyRefApi { diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 05a267ee112..483d3ab888b 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -48,6 +48,7 @@ type Props = { handleConnectionStatus: Dispatch>; handleEditorReady: (status: boolean) => void; handlers: TEditorBodyHandlers; + isNavigationPaneOpen: boolean; page: TPageInstance; webhookConnectionParams: TWebhookConnectionQueryParams; workspaceSlug: string; @@ -60,6 +61,7 @@ export const PageEditorBody: React.FC = observer((props) => { handleConnectionStatus, handleEditorReady, handlers, + isNavigationPaneOpen, page, webhookConnectionParams, workspaceSlug, @@ -169,18 +171,20 @@ export const PageEditorBody: React.FC = observer((props) => { >
    {/* table of content */} -
    -
    -
    -
    - -
    -
    - + {!isNavigationPaneOpen && ( +
    +
    +
    +
    + +
    +
    + +
    -
    + )}
    diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx index 699cbdde62f..a079564b23c 100644 --- a/web/core/components/pages/editor/page-root.tsx +++ b/web/core/components/pages/editor/page-root.tsx @@ -144,6 +144,7 @@ export const PageRoot = observer((props: TPageRootProps) => { handleConnectionStatus={setHasConnectionFailed} handleEditorReady={handleEditorReady} handlers={handlers} + isNavigationPaneOpen={isValidNavigationPaneTab} page={page} webhookConnectionParams={webhookConnectionParams} workspaceSlug={workspaceSlug} diff --git a/web/core/components/pages/editor/summary/content-browser.tsx b/web/core/components/pages/editor/summary/content-browser.tsx index 3d7ced77fdb..d22a1ec2cc5 100644 --- a/web/core/components/pages/editor/summary/content-browser.tsx +++ b/web/core/components/pages/editor/summary/content-browser.tsx @@ -7,13 +7,14 @@ import { OutlineHeading1, OutlineHeading2, OutlineHeading3, THeadingComponentPro type Props = { className?: string; + emptyState?: React.ReactNode; editorRef: EditorRefApi | null; setSidePeekVisible?: (sidePeekState: boolean) => void; showOutline?: boolean; }; export const PageContentBrowser: React.FC = (props) => { - const { className, editorRef, setSidePeekVisible, showOutline = false } = props; + const { className, editorRef, emptyState, setSidePeekVisible, showOutline = false } = props; // states const [headings, setHeadings] = useState([]); @@ -42,6 +43,8 @@ export const PageContentBrowser: React.FC = (props) => { 3: OutlineHeading3, }; + if (headings.length === 0) return emptyState ?? null; + return (
    void; + isNavigationPaneOpen: boolean; page: TPageInstance; }; export const PageEditorToolbarRoot: React.FC = observer((props) => { - const { page } = props; + const { handleOpenNavigationPane, isNavigationPaneOpen, page } = props; // derived values const { isContentEditable, editorRef } = page; // page filters @@ -24,25 +27,51 @@ export const PageEditorToolbarRoot: React.FC = observer((props) => { const shouldHideToolbar = !isStickyToolbarEnabled || !isContentEditable; return ( -
    + <>
    -
    - {editorRef && } - +
    +
    + {editorRef && } +
    + + {!isNavigationPaneOpen && ( + + )} +
    +
    -
    + {shouldHideToolbar && ( +
    + {!isNavigationPaneOpen && ( + + )} +
    + )} + ); }); diff --git a/web/core/components/pages/navigation-pane/tab-panels/assets.tsx b/web/core/components/pages/navigation-pane/tab-panels/assets.tsx new file mode 100644 index 00000000000..82a7fd646b3 --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/assets.tsx @@ -0,0 +1,125 @@ +import { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import Image from "next/image"; +import { useParams } from "next/navigation"; +import { Download } from "lucide-react"; +// plane imports +import type { TEditorAsset } from "@plane/editor"; +import { convertBytesToSize } from "@plane/utils"; +// helpers +import { getEditorAssetDownloadSrc, getEditorAssetSrc } from "@/helpers/editor.helper"; +// hooks +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +// store +import { TPageInstance } from "@/store/pages/base-page"; + +type Props = { + page: TPageInstance; +}; + +type AssetItemProps = { + asset: TEditorAsset; + page: TPageInstance; +}; + +const AssetItem = observer((props: AssetItemProps) => { + const { asset, page } = props; + // navigation + const { workspaceSlug } = useParams(); + // derived values + const { project_ids } = page; + + const getAssetSrc = (path: string) => { + if (!path || !workspaceSlug) return ""; + if (path.startsWith("http")) { + return path; + } else { + return ( + getEditorAssetSrc({ + assetId: path, + projectId: project_ids?.[0], + workspaceSlug: workspaceSlug.toString(), + }) ?? "" + ); + } + }; + + const getAssetDownloadSrc = (path: string) => { + if (!path || !workspaceSlug) return ""; + if (path.startsWith("http")) { + return path; + } else { + return ( + getEditorAssetDownloadSrc({ + assetId: path, + projectId: project_ids?.[0], + workspaceSlug: workspaceSlug.toString(), + }) ?? "" + ); + } + }; + + return ( + +
    +
    +

    {asset.name}

    +

    {convertBytesToSize(Number(asset.size || 0))}

    +
    +
    + + Download + + + ); +}); + +export const PageNavigationPaneAssetsTabPanel: React.FC = (props) => { + const { page } = props; + // states + const [assets, setAssets] = useState([]); + // derived values + const { editorRef } = page; + // subscribe to asset changes + useEffect(() => { + const unsubscribe = editorRef?.onAssetChange(setAssets); + return () => { + unsubscribe?.(); + }; + }, [editorRef]); + + // asset resolved path + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/pages/navigation-pane/outline" }); + + if (assets.length === 0) + return ( +
    +
    + An image depicting the outline of a page +
    +

    Missing images

    +

    Add images to see them here.

    +
    +
    +
    + ); + + return ( +
    + {assets?.map((asset) => )} +
    + ); +}; diff --git a/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx b/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx index 882dc16dfc9..f99bd1c97e4 100644 --- a/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx +++ b/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx @@ -25,46 +25,50 @@ type Props = { versionHistory: Pick; }; -const VersionHistoryItem = observer( - (props: { getVersionLink: (versionID: string) => string; isVersionActive: boolean; version: TPageVersion }) => { - const { getVersionLink, isVersionActive, version } = props; - // store hooks - const { getUserDetails } = useMember(); - // derived values - const versionCreator = getUserDetails(version.created_by); - // translation - const { t } = useTranslation(); +type VersionHistoryItemProps = { + getVersionLink: (versionID: string) => string; + isVersionActive: boolean; + version: TPageVersion; +}; - return ( -
  • - {/* timeline icon */} -
    -
    -
    - {/* end timeline icon */} - -

    - {renderFormattedDate(version.last_saved_at)}, {renderFormattedTime(version.last_saved_at)} -

    -

    - - {versionCreator?.display_name ?? t("common.deactivated_user")} -

    - -
  • - ); - } -); +const VersionHistoryItem = observer((props: VersionHistoryItemProps) => { + const { getVersionLink, isVersionActive, version } = props; + // store hooks + const { getUserDetails } = useMember(); + // derived values + const versionCreator = getUserDetails(version.created_by); + // translation + const { t } = useTranslation(); + + return ( +
  • + {/* timeline icon */} +
    +
    +
    + {/* end timeline icon */} + +

    + {renderFormattedDate(version.last_saved_at)}, {renderFormattedTime(version.last_saved_at)} +

    +

    + + {versionCreator?.display_name ?? t("common.deactivated_user")} +

    + +
  • + ); +}); export const PageNavigationPaneInfoTabVersionHistory: React.FC = observer((props) => { const { page, versionHistory } = props; diff --git a/web/core/components/pages/navigation-pane/tab-panels/outline.tsx b/web/core/components/pages/navigation-pane/tab-panels/outline.tsx index df8ce02954e..1d0179c75c9 100644 --- a/web/core/components/pages/navigation-pane/tab-panels/outline.tsx +++ b/web/core/components/pages/navigation-pane/tab-panels/outline.tsx @@ -1,5 +1,9 @@ +import Image from "next/image"; +// hooks +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // store import { TPageInstance } from "@/store/pages/base-page"; +// local imports import { PageContentBrowser } from "../../editor"; type Props = { @@ -10,10 +14,26 @@ export const PageNavigationPaneOutlineTabPanel: React.FC = (props) => { const { page } = props; // derived values const { editorRef } = page; + // asset resolved path + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/pages/navigation-pane/outline" }); + + const EmptyState = ( +
    +
    + An image depicting the outline of a page +
    +

    Missing headings

    +

    + Let{"'"}s put some headings in this page to see them here +

    +
    +
    +
    + ); return ( -
    - +
    +
    ); }; diff --git a/web/core/components/pages/navigation-pane/tab-panels/root.tsx b/web/core/components/pages/navigation-pane/tab-panels/root.tsx index ebab53bf8e7..d27884ffe91 100644 --- a/web/core/components/pages/navigation-pane/tab-panels/root.tsx +++ b/web/core/components/pages/navigation-pane/tab-panels/root.tsx @@ -25,7 +25,7 @@ export const PageNavigationPaneTabPanelsRoot: React.FC = (props) => { {tab.key === "outline" && } {tab.key === "info" && } diff --git a/web/helpers/editor.helper.ts b/web/helpers/editor.helper.ts index 17e170493a1..2064d13b2d5 100644 --- a/web/helpers/editor.helper.ts +++ b/web/helpers/editor.helper.ts @@ -22,6 +22,21 @@ export const getEditorAssetSrc = (args: TEditorSrcArgs): string | undefined => { return url; }; +/** + * @description generate the file source using assetId + * @param {TEditorSrcArgs} args + */ +export const getEditorAssetDownloadSrc = (args: TEditorSrcArgs): string | undefined => { + const { assetId, projectId, workspaceSlug } = args; + let url: string | undefined = ""; + if (projectId) { + url = getFileURL(`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/download/${assetId}/`); + } else { + url = getFileURL(`/api/assets/v2/workspaces/${workspaceSlug}/download/${assetId}/`); + } + return url; +}; + export const getTextContent = (jsx: JSX.Element | React.ReactNode | null | undefined): string => { if (!jsx) return ""; diff --git a/web/public/empty-state/pages/navigation-pane/outline-dark.webp b/web/public/empty-state/pages/navigation-pane/outline-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..8b77d305859b49cc243f63bb30911a44257b706b GIT binary patch literal 18104 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 literal 0 HcmV?d00001 diff --git a/web/public/empty-state/pages/navigation-pane/outline-light.webp b/web/public/empty-state/pages/navigation-pane/outline-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..e92cc6ae7bb14165b36527fc388ba37c952c9791 GIT binary patch literal 18782 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 Z*A=aXJM{{G%bCUr@ Date: Thu, 12 Jun 2025 18:41:05 +0530 Subject: [PATCH 04/20] chore: realtime document info updates --- packages/editor/src/ce/types/storage.ts | 2 + .../editor/src/core/helpers/editor-ref.ts | 57 ++++++ packages/editor/src/core/hooks/use-editor.ts | 179 ++++++++---------- .../src/core/hooks/use-read-only-editor.ts | 58 +----- packages/editor/src/core/types/editor.ts | 42 ++-- .../navigation-pane/tab-panels/assets.tsx | 2 + .../tab-panels/info/document-info.tsx | 77 ++++++++ .../navigation-pane/tab-panels/info/root.tsx | 44 +---- 8 files changed, 250 insertions(+), 211 deletions(-) create mode 100644 packages/editor/src/core/helpers/editor-ref.ts create mode 100644 web/core/components/pages/navigation-pane/tab-panels/info/document-info.tsx diff --git a/packages/editor/src/ce/types/storage.ts b/packages/editor/src/ce/types/storage.ts index 5f576df5090..380ade4475d 100644 --- a/packages/editor/src/ce/types/storage.ts +++ b/packages/editor/src/ce/types/storage.ts @@ -1,3 +1,4 @@ +import { CharacterCountStorage } from "@tiptap/extension-character-count"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions @@ -15,6 +16,7 @@ export type ExtensionStorageMap = { [CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage; [CORE_EXTENSIONS.MENTION]: MentionExtensionStorage; [CORE_EXTENSIONS.UTILITY]: UtilityExtensionStorage; + [CORE_EXTENSIONS.CHARACTER_COUNT]: CharacterCountStorage; }; export type ExtensionFileSetStorageKey = Extract; diff --git a/packages/editor/src/core/helpers/editor-ref.ts b/packages/editor/src/core/helpers/editor-ref.ts new file mode 100644 index 00000000000..2a47f47d7af --- /dev/null +++ b/packages/editor/src/core/helpers/editor-ref.ts @@ -0,0 +1,57 @@ +import { HocuspocusProvider } from "@hocuspocus/provider"; +import { Editor } from "@tiptap/core"; +import * as Y from "yjs"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +import { CORE_EDITOR_META } from "@/constants/meta"; +// types +import { EditorReadOnlyRefApi } from "@/types"; +// local imports +import { getAllEditorAssets } from "./assets"; +import { getParagraphCount } from "./common"; +import { getExtensionStorage } from "./get-extension-storage"; +import { scrollSummary } from "./scroll-to-node"; + +type TArgs = { + editor: Editor | null; + provider: HocuspocusProvider | undefined; +}; + +export const getEditorRefHelpers = (args: TArgs): EditorReadOnlyRefApi => { + const { editor, provider } = args; + + return { + clearEditor: (emitUpdate = false) => { + editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); + }, + getAssets: () => (editor ? getAllEditorAssets(editor) : []), + getDocument: () => { + const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null; + const documentHTML = editor?.getHTML() ?? "

    "; + const documentJSON = editor?.getJSON() ?? null; + + return { + binary: documentBinary, + html: documentHTML, + json: documentJSON, + }; + }, + getDocumentInfo: () => ({ + characters: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.characters?.() : 0, + paragraphs: getParagraphCount(editor?.state), + words: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.words?.() : 0, + }), + getHeadings: () => (editor ? getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings : []), + getMarkDown: () => { + const markdownOutput = editor?.storage.markdown.getMarkdown(); + return markdownOutput; + }, + scrollSummary: (marking) => { + if (!editor) return; + scrollSummary(editor, marking); + }, + setEditorValue: (content, emitUpdate = false) => { + editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true }); + }, + }; +}; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index e3cd769f805..1a1d6c8ad1b 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -8,15 +8,15 @@ import * as Y from "yjs"; import { getEditorMenuItems } from "@/components/menus"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; -import { CORE_EDITOR_META } from "@/constants/meta"; // extensions import { CoreEditorExtensions } from "@/extensions"; // helpers import { getAllEditorAssets } from "@/helpers/assets"; import { getParagraphCount } from "@/helpers/common"; +import { getEditorRefHelpers } from "@/helpers/editor-ref"; import { getExtensionStorage } from "@/helpers/get-extension-storage"; import { insertContentAtSavedSelection } from "@/helpers/insert-content-at-cursor-position"; -import { scrollSummary, scrollToNodeViaDOMCoordinates } from "@/helpers/scroll-to-node"; +import { scrollToNodeViaDOMCoordinates } from "@/helpers/scroll-to-node"; // props import { CoreEditorProps } from "@/props"; // types @@ -137,24 +137,9 @@ export const useEditor = (props: CustomEditorProps) => { useImperativeHandle( forwardedRef, () => ({ + ...getEditorRefHelpers({ editor, provider }), blur: () => editor?.commands.blur(), - scrollToNodeViaDOMCoordinates(behavior, pos) { - const resolvedPos = pos ?? editor?.state.selection.from; - if (!editor || !resolvedPos) return; - scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior); - }, - getCurrentCursorPosition: () => editor?.state.selection.from, - clearEditor: (emitUpdate = false) => { - editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); - }, - setEditorValue: (content, emitUpdate = false) => { - editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true }); - }, - setEditorValueAtCursorPosition: (content) => { - if (editor?.state.selection) { - insertContentAtSavedSelection(editor, content); - } - }, + emitRealTimeUpdate: (message) => provider?.sendStateless(message), executeMenuItemCommand: (props) => { const { itemKey } = props; const editorItems = getEditorMenuItems(editor); @@ -168,6 +153,42 @@ export const useEditor = (props: CustomEditorProps) => { 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("
    ").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); @@ -178,41 +199,60 @@ export const useEditor = (props: CustomEditorProps) => { return item.isActive(props); }, - onHeadingChange: (callback) => { - const handleHeadingChange = () => { + listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) }, + onAssetChange: (callback) => { + const handleAssetChange = () => { if (!editor) return; - const headings = getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings; - if (headings) { - callback(headings); - } + const assets = getAllEditorAssets(editor); + callback(assets); }; - // Subscribe to update event emitted from headers extension - editor?.on("update", handleHeadingChange); + // Subscribe to update assets + editor?.on("update", handleAssetChange); // 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); + editor?.off("update", handleAssetChange); }; }, - onAssetChange: (callback) => { - const handleAssetChange = () => { + onDocumentInfoChange: (callback) => { + const handleDocumentInfoChange = () => { if (!editor) return; - const assets = getAllEditorAssets(editor); - callback(assets); + 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 assets - editor?.on("update", handleAssetChange); + // 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", handleAssetChange); + 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); }; }, - getHeadings: () => (editor ? getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings : []), onStateChange: (callback) => { // Subscribe to editor state changes editor?.on("transaction", callback); @@ -224,27 +264,16 @@ export const useEditor = (props: CustomEditorProps) => { editor?.off("transaction", callback); }; }, - getMarkDown: () => { - const markdownOutput = editor?.storage.markdown.getMarkdown(); - return markdownOutput; - }, - getDocument: () => { - const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null; - const documentHTML = editor?.getHTML() ?? "

    "; - const documentJSON = editor?.getJSON() ?? null; - - return { - binary: documentBinary, - html: documentHTML, - json: documentJSON, - }; + scrollToNodeViaDOMCoordinates(behavior, pos) { + const resolvedPos = pos ?? editor?.state.selection.from; + if (!editor || !resolvedPos) return; + scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior); }, - scrollSummary: (marking) => { - if (!editor) return; - scrollSummary(editor, marking); + setEditorValueAtCursorPosition: (content) => { + if (editor?.state.selection) { + insertContentAtSavedSelection(editor, content); + } }, - isEditorReadyToDiscard: () => - !!editor && getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress === false, setFocusAtPosition: (position) => { if (!editor || editor.isDestroyed) { console.error("Editor reference is not available or has been destroyed."); @@ -262,51 +291,11 @@ export const useEditor = (props: CustomEditorProps) => { console.error("An error occurred while setting focus at position:", error); } }, - 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("
    ").insertContent(contentHTML).run(); - } else { - // replace selected text with the content provided - editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run(); - } - }, - getDocumentInfo: () => ({ - characters: editor?.storage?.characterCount?.characters?.() ?? 0, - paragraphs: getParagraphCount(editor?.state), - words: editor?.storage?.characterCount?.words?.() ?? 0, - }), setProviderDocument: (value) => { const document = provider?.document; if (!document) return; Y.applyUpdate(document, value); }, - emitRealTimeUpdate: (message) => provider?.sendStateless(message), - listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) }, }), [editor] ); diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index 497a6260655..af0e07ccd47 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -2,15 +2,10 @@ import { HocuspocusProvider } from "@hocuspocus/provider"; import { EditorProps } from "@tiptap/pm/view"; import { useEditor as useTiptapEditor, Extensions } from "@tiptap/react"; import { useImperativeHandle, MutableRefObject, useEffect } from "react"; -import * as Y from "yjs"; -// constants -import { CORE_EDITOR_META } from "@/constants/meta"; // extensions import { CoreReadOnlyEditorExtensions } from "@/extensions"; // helpers -import { getAllEditorAssets } from "@/helpers/assets"; -import { getParagraphCount } from "@/helpers/common"; -import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; +import { getEditorRefHelpers } from "@/helpers/editor-ref"; // props import { CoreReadOnlyEditorProps } from "@/props"; // types @@ -45,7 +40,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { const editor = useTiptapEditor({ editable: false, - immediatelyRender: true, + immediatelyRender: false, shouldRerenderOnTransaction: false, content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "

    ", parseOptions: { preserveWhitespace: true }, @@ -77,54 +72,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: true }); }, [editor, initialValue]); - useImperativeHandle(forwardedRef, () => ({ - clearEditor: (emitUpdate = false) => { - editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); - }, - setEditorValue: (content, emitUpdate = false) => { - editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true }); - }, - getMarkDown: () => { - const markdownOutput = editor?.storage.markdown.getMarkdown(); - return markdownOutput; - }, - getDocument: () => { - const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null; - const documentHTML = editor?.getHTML() ?? "

    "; - const documentJSON = editor?.getJSON() ?? null; - - return { - binary: documentBinary, - html: documentHTML, - json: documentJSON, - }; - }, - scrollSummary: (marking) => { - if (!editor) return; - scrollSummary(editor, marking); - }, - getDocumentInfo: () => ({ - characters: editor.storage?.characterCount?.characters?.() ?? 0, - paragraphs: getParagraphCount(editor.state), - words: editor.storage?.characterCount?.words?.() ?? 0, - }), - onAssetChange: (callback) => { - const handleAssetChange = () => { - if (!editor) return; - const assets = getAllEditorAssets(editor); - callback(assets); - }; - - // Subscribe to update assets - editor?.on("update", handleAssetChange); - // 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", handleAssetChange); - }; - }, - })); + useImperativeHandle(forwardedRef, () => getEditorRefHelpers({ editor, provider })); if (!editor) { return null; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 2039f14ea8e..4c3ad14d45f 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -88,42 +88,46 @@ export type TEditorAsset = { type: keyof typeof CORE_EXTENSIONS | keyof typeof ADDITIONAL_EXTENSIONS; }; +export type TDocumentInfo = { + characters: number; + paragraphs: number; + words: number; +}; + // editor refs export type EditorReadOnlyRefApi = { - getMarkDown: () => string; + clearEditor: (emitUpdate?: boolean) => void; + getAssets: () => TEditorAsset[]; getDocument: () => { binary: Uint8Array | null; html: string; json: JSONContent | null; }; - clearEditor: (emitUpdate?: boolean) => void; - setEditorValue: (content: string, emitUpdate?: boolean) => void; + getDocumentInfo: () => TDocumentInfo; + getHeadings: () => IMarking[]; + getMarkDown: () => string; scrollSummary: (marking: IMarking) => void; - getDocumentInfo: () => { - characters: number; - paragraphs: number; - words: number; - }; - onAssetChange: (callback: (assets: TEditorAsset[]) => void) => () => void; + setEditorValue: (content: string, emitUpdate?: boolean) => void; }; export interface EditorRefApi extends EditorReadOnlyRefApi { blur: () => void; - scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void; - getCurrentCursorPosition: () => number | undefined; - setEditorValueAtCursorPosition: (content: string) => void; + emitRealTimeUpdate: (action: TDocumentEventsServer) => void; executeMenuItemCommand: (props: TCommandWithPropsWithItemKey) => void; + getCurrentCursorPosition: () => number | undefined; + getSelectedText: () => string | null; + insertText: (contentHTML: string, insertOnNextLine?: boolean) => void; + isEditorReadyToDiscard: () => boolean; isMenuItemActive: (props: TCommandWithPropsWithItemKey) => boolean; + listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined; + onAssetChange: (callback: (assets: TEditorAsset[]) => void) => () => void; + onDocumentInfoChange: (callback: (documentInfo: TDocumentInfo) => void) => () => void; + onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void; onStateChange: (callback: () => void) => () => void; + scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void; + setEditorValueAtCursorPosition: (content: string) => void; setFocusAtPosition: (position: number) => void; - isEditorReadyToDiscard: () => boolean; - getSelectedText: () => string | null; - insertText: (contentHTML: string, insertOnNextLine?: boolean) => void; setProviderDocument: (value: Uint8Array) => void; - onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void; - getHeadings: () => IMarking[]; - emitRealTimeUpdate: (action: TDocumentEventsServer) => void; - listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined; } // editor props diff --git a/web/core/components/pages/navigation-pane/tab-panels/assets.tsx b/web/core/components/pages/navigation-pane/tab-panels/assets.tsx index 82a7fd646b3..f6a9c98eac0 100644 --- a/web/core/components/pages/navigation-pane/tab-panels/assets.tsx +++ b/web/core/components/pages/navigation-pane/tab-panels/assets.tsx @@ -96,6 +96,8 @@ export const PageNavigationPaneAssetsTabPanel: React.FC = (props) => { // subscribe to asset changes useEffect(() => { const unsubscribe = editorRef?.onAssetChange(setAssets); + // for initial render of this component to get the editor assets + setAssets(editorRef?.getAssets() ?? []); return () => { unsubscribe?.(); }; diff --git a/web/core/components/pages/navigation-pane/tab-panels/info/document-info.tsx b/web/core/components/pages/navigation-pane/tab-panels/info/document-info.tsx new file mode 100644 index 00000000000..3692faa0ff5 --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/info/document-info.tsx @@ -0,0 +1,77 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { observer } from "mobx-react"; +// plane imports +import type { TDocumentInfo } from "@plane/editor"; +import { getReadTimeFromWordsCount } from "@plane/utils"; +// store +import { TPageInstance } from "@/store/pages/base-page"; + +type Props = { + page: TPageInstance; +}; + +const DEFAULT_DOCUMENT_INFO: TDocumentInfo = { + words: 0, + characters: 0, + paragraphs: 0, +}; + +export const PageNavigationPaneInfoTabDocumentInfo: React.FC = observer((props) => { + const { page } = props; + // states + const [documentInfo, setDocumentInfo] = useState(DEFAULT_DOCUMENT_INFO); + // derived values + const { editorRef } = page; + // subscribe to asset changes + useEffect(() => { + const unsubscribe = editorRef?.onDocumentInfoChange(setDocumentInfo); + // for initial render of this component to get the editor assets + setDocumentInfo(editorRef?.getDocumentInfo() ?? DEFAULT_DOCUMENT_INFO); + return () => { + unsubscribe?.(); + }; + }, [editorRef]); + + const secondsToReadableTime = useCallback(() => { + const wordsCount = documentInfo.words; + const readTimeInSeconds = Number(getReadTimeFromWordsCount(wordsCount).toFixed(0)); + return readTimeInSeconds < 60 ? `${readTimeInSeconds}s` : `${Math.ceil(readTimeInSeconds / 60)}m`; + }, [documentInfo.words]); + + const documentInfoCards = useMemo( + () => [ + { + key: "words-count", + title: "Words", + info: documentInfo.words, + }, + { + key: "characters-count", + title: "Characters", + info: documentInfo.characters, + }, + { + key: "paragraphs-count", + title: "Paragraphs", + info: documentInfo.paragraphs, + }, + { + key: "read-time", + title: "Read time", + info: secondsToReadableTime(), + }, + ], + [documentInfo, secondsToReadableTime] + ); + + return ( +
    + {documentInfoCards.map((card) => ( +
    +
    {card.info}
    +

    {card.title}

    +
    + ))} +
    + ); +}); diff --git a/web/core/components/pages/navigation-pane/tab-panels/info/root.tsx b/web/core/components/pages/navigation-pane/tab-panels/info/root.tsx index 09901d672ee..77edc24e046 100644 --- a/web/core/components/pages/navigation-pane/tab-panels/info/root.tsx +++ b/web/core/components/pages/navigation-pane/tab-panels/info/root.tsx @@ -1,12 +1,11 @@ import { observer } from "mobx-react"; -// plane imports -import { getReadTimeFromWordsCount } from "@plane/utils"; // components import { TPageRootHandlers } from "@/components/pages/editor"; // store import { TPageInstance } from "@/store/pages/base-page"; // local imports import { PageNavigationPaneInfoTabActorsInfo } from "./actors-info"; +import { PageNavigationPaneInfoTabDocumentInfo } from "./document-info"; import { PageNavigationPaneInfoTabVersionHistory } from "./version-history"; type Props = { @@ -16,49 +15,10 @@ type Props = { export const PageNavigationPaneInfoTabPanel: React.FC = observer((props) => { const { page, versionHistory } = props; - // derived values - const { editorRef } = page; - const documentsInfo = editorRef?.getDocumentInfo() || { words: 0, characters: 0, paragraphs: 0 }; - - const secondsToReadableTime = () => { - const wordsCount = documentsInfo.words; - const readTimeInSeconds = Number(getReadTimeFromWordsCount(wordsCount).toFixed(0)); - return readTimeInSeconds < 60 ? `${readTimeInSeconds}s` : `${Math.ceil(readTimeInSeconds / 60)}m`; - }; - - const documentInfoCards = [ - { - key: "words-count", - title: "Words", - info: documentsInfo.words, - }, - { - key: "characters-count", - title: "Characters", - info: documentsInfo.characters, - }, - { - key: "paragraphs-count", - title: "Paragraphs", - info: documentsInfo.paragraphs, - }, - { - key: "read-time", - title: "Read time", - info: secondsToReadableTime(), - }, - ]; return (
    -
    - {documentInfoCards.map((card) => ( -
    -
    {card.info}
    -

    {card.title}

    -
    - ))} -
    +
    From 78a4e38331c3da177d594922b5583a630dfafac4 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 13 Jun 2025 14:20:23 +0530 Subject: [PATCH 05/20] chore: add support for code splitting --- .../pages/editor/navigation-pane/index.ts | 21 ------------ .../components/pages/navigation-pane/index.ts | 31 ++++++++++++++++++ .../pages/navigation-pane/tab-panels/root.tsx | 11 +++++++ .../components/pages/editor/page-root.tsx | 6 ++-- .../components/pages/navigation-pane/index.ts | 5 +++ .../components/pages/navigation-pane/root.tsx | 12 +++---- .../navigation-pane/tab-panels/assets.tsx | 4 +-- .../pages/navigation-pane/tab-panels/root.tsx | 6 ++-- .../pages/navigation-pane/tabs-list.tsx | 8 ++--- .../pages/navigation-pane/assets-dark.webp | Bin 0 -> 21086 bytes .../pages/navigation-pane/assets-light.webp | Bin 0 -> 21472 bytes .../pages/navigation-pane/outline-dark.webp | Bin 18104 -> 18452 bytes 12 files changed, 65 insertions(+), 39 deletions(-) delete mode 100644 web/ce/components/pages/editor/navigation-pane/index.ts create mode 100644 web/ce/components/pages/navigation-pane/index.ts create mode 100644 web/ce/components/pages/navigation-pane/tab-panels/root.tsx create mode 100644 web/public/empty-state/pages/navigation-pane/assets-dark.webp create mode 100644 web/public/empty-state/pages/navigation-pane/assets-light.webp diff --git a/web/ce/components/pages/editor/navigation-pane/index.ts b/web/ce/components/pages/editor/navigation-pane/index.ts deleted file mode 100644 index 2efcca181d8..00000000000 --- a/web/ce/components/pages/editor/navigation-pane/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type TPageNavigationPaneTab = "outline" | "info" | "assets"; - -export const PAGE_NAVIGATION_PANE_TABS_LIST: { - key: TPageNavigationPaneTab; - i18n_label: string; -}[] = [ - { - key: "outline", - i18n_label: "Outline", - }, - { - key: "info", - i18n_label: "Info", - }, - { - key: "assets", - i18n_label: "Assets", - }, -]; - -export const PAGE_NAVIGATION_PANE_TAB_KEYS = PAGE_NAVIGATION_PANE_TABS_LIST.map((tab) => tab.key); diff --git a/web/ce/components/pages/navigation-pane/index.ts b/web/ce/components/pages/navigation-pane/index.ts new file mode 100644 index 00000000000..54a645a7362 --- /dev/null +++ b/web/ce/components/pages/navigation-pane/index.ts @@ -0,0 +1,31 @@ +export type TPageNavigationPaneTab = "outline" | "info" | "assets"; + +export const PAGE_NAVIGATION_PANE_TABS_LIST: Record< + TPageNavigationPaneTab, + { + key: TPageNavigationPaneTab; + i18n_label: string; + } +> = { + outline: { + key: "outline", + i18n_label: "Outline", + }, + info: { + key: "info", + i18n_label: "Info", + }, + assets: { + key: "assets", + i18n_label: "Assets", + }, +}; + +export const ORDERED_PAGE_NAVIGATION_TABS_LIST: { + key: TPageNavigationPaneTab; + i18n_label: string; +}[] = [ + PAGE_NAVIGATION_PANE_TABS_LIST.outline, + PAGE_NAVIGATION_PANE_TABS_LIST.info, + PAGE_NAVIGATION_PANE_TABS_LIST.assets, +]; diff --git a/web/ce/components/pages/navigation-pane/tab-panels/root.tsx b/web/ce/components/pages/navigation-pane/tab-panels/root.tsx new file mode 100644 index 00000000000..2a854d086a9 --- /dev/null +++ b/web/ce/components/pages/navigation-pane/tab-panels/root.tsx @@ -0,0 +1,11 @@ +// store +import type { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { TPageNavigationPaneTab } from ".."; + +export type Props = { + activeTab: TPageNavigationPaneTab; + page: TPageInstance; +}; + +export const PageNavigationPaneAdditionalTabPanelsRoot: React.FC = () => null; diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx index a079564b23c..1e5cdc45351 100644 --- a/web/core/components/pages/editor/page-root.tsx +++ b/web/core/components/pages/editor/page-root.tsx @@ -18,14 +18,12 @@ import { useAppRouter } from "@/hooks/use-app-router"; import { usePageFallback } from "@/hooks/use-page-fallback"; import { useQueryParams } from "@/hooks/use-query-params"; // plane web import -import { - PAGE_NAVIGATION_PANE_TAB_KEYS, - TPageNavigationPaneTab, -} from "@/plane-web/components/pages/editor/navigation-pane"; +import { TPageNavigationPaneTab } from "@/plane-web/components/pages/navigation-pane"; // store import { TPageInstance } from "@/store/pages/base-page"; // local imports import { + PAGE_NAVIGATION_PANE_TAB_KEYS, PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM, PageNavigationPaneRoot, diff --git a/web/core/components/pages/navigation-pane/index.ts b/web/core/components/pages/navigation-pane/index.ts index 0dd3c4bd581..52026510632 100644 --- a/web/core/components/pages/navigation-pane/index.ts +++ b/web/core/components/pages/navigation-pane/index.ts @@ -1,6 +1,11 @@ +// plane web imports +import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane"; + export * from "./root"; export const PAGE_NAVIGATION_PANE_WIDTH = 294; export const PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM = "sidebarTab"; export const PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM = "version"; + +export const PAGE_NAVIGATION_PANE_TAB_KEYS = ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => tab.key); diff --git a/web/core/components/pages/navigation-pane/root.tsx b/web/core/components/pages/navigation-pane/root.tsx index 286d2786eaf..d0d99d9b320 100644 --- a/web/core/components/pages/navigation-pane/root.tsx +++ b/web/core/components/pages/navigation-pane/root.tsx @@ -6,17 +6,18 @@ import { Tab } from "@headlessui/react"; // hooks import { useQueryParams } from "@/hooks/use-query-params"; // plane web components -import { - PAGE_NAVIGATION_PANE_TAB_KEYS, - TPageNavigationPaneTab, -} from "@/plane-web/components/pages/editor/navigation-pane"; +import { TPageNavigationPaneTab } from "@/plane-web/components/pages/navigation-pane"; // store import { TPageInstance } from "@/store/pages/base-page"; // local imports import { TPageRootHandlers } from "../editor"; import { PageNavigationPaneTabPanelsRoot } from "./tab-panels/root"; import { PageNavigationPaneTabsList } from "./tabs-list"; -import { PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, PAGE_NAVIGATION_PANE_WIDTH } from "./index"; +import { + PAGE_NAVIGATION_PANE_TAB_KEYS, + PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, + PAGE_NAVIGATION_PANE_WIDTH, +} from "./index"; type Props = { handleClose: () => void; @@ -33,7 +34,6 @@ export const PageNavigationPaneRoot: React.FC = observer((props) => { // query params const { updateQueryParams } = useQueryParams(); // derived values - const { editorRef } = page; const navigationPaneQueryParam = searchParams.get( PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM ) as TPageNavigationPaneTab | null; diff --git a/web/core/components/pages/navigation-pane/tab-panels/assets.tsx b/web/core/components/pages/navigation-pane/tab-panels/assets.tsx index f6a9c98eac0..ed834aeb9b5 100644 --- a/web/core/components/pages/navigation-pane/tab-panels/assets.tsx +++ b/web/core/components/pages/navigation-pane/tab-panels/assets.tsx @@ -104,13 +104,13 @@ export const PageNavigationPaneAssetsTabPanel: React.FC = (props) => { }, [editorRef]); // asset resolved path - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/pages/navigation-pane/outline" }); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/pages/navigation-pane/assets" }); if (assets.length === 0) return (
    - An image depicting the outline of a page + An image depicting the assets of a page

    Missing images

    Add images to see them here.

    diff --git a/web/core/components/pages/navigation-pane/tab-panels/root.tsx b/web/core/components/pages/navigation-pane/tab-panels/root.tsx index d27884ffe91..c9880f0d58c 100644 --- a/web/core/components/pages/navigation-pane/tab-panels/root.tsx +++ b/web/core/components/pages/navigation-pane/tab-panels/root.tsx @@ -3,7 +3,8 @@ import { Tab } from "@headlessui/react"; // components import { TPageRootHandlers } from "@/components/pages/editor"; // plane web imports -import { PAGE_NAVIGATION_PANE_TABS_LIST } from "@/plane-web/components/pages/editor/navigation-pane"; +import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane"; +import { PageNavigationPaneAdditionalTabPanelsRoot } from "@/plane-web/components/pages/navigation-pane/tab-panels/root"; // store import { TPageInstance } from "@/store/pages/base-page"; // local imports @@ -21,7 +22,7 @@ export const PageNavigationPaneTabPanelsRoot: React.FC = (props) => { return ( - {PAGE_NAVIGATION_PANE_TABS_LIST.map((tab) => ( + {ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => ( = (props) => { {tab.key === "outline" && } {tab.key === "info" && } {tab.key === "assets" && } + ))} diff --git a/web/core/components/pages/navigation-pane/tabs-list.tsx b/web/core/components/pages/navigation-pane/tabs-list.tsx index 6c089016d92..bf438321683 100644 --- a/web/core/components/pages/navigation-pane/tabs-list.tsx +++ b/web/core/components/pages/navigation-pane/tabs-list.tsx @@ -2,7 +2,7 @@ import { Tab } from "@headlessui/react"; // plane imports import { useTranslation } from "@plane/i18n"; // plane web components -import { PAGE_NAVIGATION_PANE_TABS_LIST } from "@/plane-web/components/pages/editor/navigation-pane"; +import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane"; export const PageNavigationPaneTabsList = () => { // translation @@ -12,7 +12,7 @@ export const PageNavigationPaneTabsList = () => { {({ selectedIndex }) => ( <> - {PAGE_NAVIGATION_PANE_TABS_LIST.map((tab) => ( + {ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => ( {
    diff --git a/web/public/empty-state/pages/navigation-pane/assets-dark.webp b/web/public/empty-state/pages/navigation-pane/assets-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..e454d6dc17d77d199edb1cb18bf0e097fa5e1fab GIT binary patch literal 21086 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 KzUb%x0000IRB(p? literal 0 HcmV?d00001 diff --git a/web/public/empty-state/pages/navigation-pane/outline-dark.webp b/web/public/empty-state/pages/navigation-pane/outline-dark.webp index 8b77d305859b49cc243f63bb30911a44257b706b..dcd26c3950b75609dbf14a2f9abbe1247739da8e 100644 GIT binary patch literal 18452 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 From aa8ae78ff793ab168c548b5a2861222c0d0f6305 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 13 Jun 2025 15:51:35 +0530 Subject: [PATCH 06/20] fix: formatting --- apiserver/plane/app/views/asset/v2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apiserver/plane/app/views/asset/v2.py b/apiserver/plane/app/views/asset/v2.py index 925a3c331ee..5994ffd8c16 100644 --- a/apiserver/plane/app/views/asset/v2.py +++ b/apiserver/plane/app/views/asset/v2.py @@ -742,6 +742,7 @@ def get(self, request, slug, asset_id): object_name=asset.asset.name, disposition=f"attachment; filename={asset.asset.name}", ) + return HttpResponseRedirect(signed_url) @@ -768,4 +769,5 @@ def get(self, request, slug, project_id, asset_id): object_name=asset.asset.name, disposition=f"attachment; filename={asset.asset.name}", ) + return HttpResponseRedirect(signed_url) From 3a7f84b51a41656d8f9c3d1626728f91ac5eec28 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 13 Jun 2025 15:58:03 +0530 Subject: [PATCH 07/20] refactor: image block id generation --- .../custom-image/components/image-block.tsx | 4 +- packages/editor/src/core/helpers/assets.ts | 6 +- .../components/pages/editor/editor-body.tsx | 114 +++++++++--------- 3 files changed, 63 insertions(+), 61 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx index 280d3dce662..d6e70804935 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx @@ -38,6 +38,8 @@ const ensurePixelString = (value: Pixel | TDefault | number | undefin return value; }; +export const getImageBlockId = (id: string) => `editor-image-block-${id}`; + type CustomImageBlockProps = CustomBaseImageNodeViewProps & { imageFromFileSystem: string | undefined; setFailedToLoadImage: (isError: boolean) => void; @@ -222,7 +224,7 @@ export const CustomImageBlock: React.FC = (props) => { return (
    = observer((props) => { if (pageId === undefined || !realtimeConfig) return ; return ( - <> - -
    - {/* table of content */} - {!isNavigationPaneOpen && ( -
    -
    -
    -
    - -
    -
    - -
    + +
    + {/* table of content */} + {!isNavigationPaneOpen && ( +
    +
    +
    +
    + +
    +
    +
    - )} -
    -
    - - -
    - { - const res = await fetchMentions(query); - if (!res) throw new Error("Failed in fetching mentions"); - return res; - }, - renderComponent: (props) => , - getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }), - }} - embedHandler={{ - issue: issueEmbedProps, - }} - realtimeConfig={realtimeConfig} - serverHandler={serverHandler} - user={userConfig} - disabledExtensions={disabledExtensions} - aiHandler={{ - menu: getAIMenu, - }} - /> + )} +
    +
    + + +
    - - + { + const res = await fetchMentions(query); + if (!res) throw new Error("Failed in fetching mentions"); + return res; + }, + renderComponent: (props) => , + getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }), + }} + embedHandler={{ + issue: issueEmbedProps, + }} + realtimeConfig={realtimeConfig} + serverHandler={serverHandler} + user={userConfig} + disabledExtensions={disabledExtensions} + aiHandler={{ + menu: getAIMenu, + }} + /> +
    + ); }); From 553aa62fa7e170bab59dff9b78a905684ab67387 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 13 Jun 2025 16:26:30 +0530 Subject: [PATCH 08/20] chore: implement translation --- .../editor/src/core/plugins/drag-handle.ts | 7 +++--- .../components/pages/navigation-pane/index.ts | 6 ++--- .../components/pages/editor/editor-body.tsx | 13 +++++++++-- .../components/pages/editor/page-root.tsx | 1 + .../components/pages/editor/toolbar/root.tsx | 5 +++++ .../components/pages/navigation-pane/root.tsx | 22 +++++++++++++------ .../navigation-pane/tab-panels/assets.tsx | 13 ++++++++--- .../tab-panels/info/actors-info.tsx | 8 +++++-- .../tab-panels/info/document-info.tsx | 13 ++++++----- .../tab-panels/info/version-history.tsx | 8 +++++-- .../navigation-pane/tab-panels/outline.tsx | 8 +++++-- 11 files changed, 74 insertions(+), 30 deletions(-) diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts index aa00fa32d90..e04bbaba47e 100644 --- a/packages/editor/src/core/plugins/drag-handle.ts +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -1,7 +1,6 @@ import { Fragment, Slice, Node, Schema } from "@tiptap/pm/model"; import { NodeSelection } from "@tiptap/pm/state"; -// @ts-expect-error __serializeForClipboard's is not exported -import { __serializeForClipboard, EditorView } from "@tiptap/pm/view"; +import { EditorView } from "@tiptap/pm/view"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions @@ -171,7 +170,7 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp return; } - const scrollableParent = getScrollParent(dragHandleElement); + const scrollableParent = getScrollParent(dragHandleElement!); if (!scrollableParent) return; const scrollRegionUp = options.scrollThreshold.up; @@ -417,7 +416,7 @@ const handleNodeSelection = ( } const slice = view.state.selection.content(); - const { dom, text } = __serializeForClipboard(view, slice); + const { dom, text } = view.serializeForClipboard(slice); if (event instanceof DragEvent && event.dataTransfer) { event.dataTransfer.clearData(); diff --git a/web/ce/components/pages/navigation-pane/index.ts b/web/ce/components/pages/navigation-pane/index.ts index 54a645a7362..79ee20c26dd 100644 --- a/web/ce/components/pages/navigation-pane/index.ts +++ b/web/ce/components/pages/navigation-pane/index.ts @@ -9,15 +9,15 @@ export const PAGE_NAVIGATION_PANE_TABS_LIST: Record< > = { outline: { key: "outline", - i18n_label: "Outline", + i18n_label: "page_navigation_pane.tabs.outline.label", }, info: { key: "info", - i18n_label: "Info", + i18n_label: "page_navigation_pane.tabs.info.label", }, assets: { key: "assets", - i18n_label: "Assets", + i18n_label: "page_navigation_pane.tabs.assets.label", }, }; diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index b671405bf90..9b1edba8a52 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -10,6 +10,7 @@ import { TRealtimeConfig, TServerHandler, } from "@plane/editor"; +import { useTranslation } from "@plane/i18n"; import { TSearchEntityRequestPayload, TSearchResponse, TWebhookConnectionQueryParams } from "@plane/types"; import { ERowVariant, Row } from "@plane/ui"; import { cn } from "@plane/utils"; @@ -47,6 +48,7 @@ type Props = { editorForwardRef: React.RefObject; handleConnectionStatus: Dispatch>; handleEditorReady: (status: boolean) => void; + handleOpenNavigationPane: () => void; handlers: TEditorBodyHandlers; isNavigationPaneOpen: boolean; page: TPageInstance; @@ -60,6 +62,7 @@ export const PageEditorBody: React.FC = observer((props) => { editorForwardRef, handleConnectionStatus, handleEditorReady, + handleOpenNavigationPane, handlers, isNavigationPaneOpen, page, @@ -70,7 +73,6 @@ export const PageEditorBody: React.FC = observer((props) => { const { data: currentUser } = useUser(); const { getWorkspaceBySlug } = useWorkspace(); const { getUserDetails } = useMember(); - // derived values const { id: pageId, name: pageTitle, isContentEditable, updateTitle, editorRef } = page; const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? ""; @@ -87,6 +89,8 @@ export const PageEditorBody: React.FC = observer((props) => { const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug); // page filters const { fontSize, fontStyle, isFullWidth } = usePageFilters(); + // translation + const { t } = useTranslation(); // derived values const displayConfig: TDisplayConfig = useMemo( () => ({ @@ -174,7 +178,12 @@ export const PageEditorBody: React.FC = observer((props) => {
    -
    +
    diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx index 1e5cdc45351..07bbf887ca6 100644 --- a/web/core/components/pages/editor/page-root.tsx +++ b/web/core/components/pages/editor/page-root.tsx @@ -141,6 +141,7 @@ export const PageRoot = observer((props: TPageRootProps) => { editorForwardRef={editorRef} handleConnectionStatus={setHasConnectionFailed} handleEditorReady={handleEditorReady} + handleOpenNavigationPane={handleOpenNavigationPane} handlers={handlers} isNavigationPaneOpen={isValidNavigationPaneTab} page={page} diff --git a/web/core/components/pages/editor/toolbar/root.tsx b/web/core/components/pages/editor/toolbar/root.tsx index 4e11979efc5..a1d928543dc 100644 --- a/web/core/components/pages/editor/toolbar/root.tsx +++ b/web/core/components/pages/editor/toolbar/root.tsx @@ -1,5 +1,7 @@ import { observer } from "mobx-react"; import { PanelRight } from "lucide-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; // components import { PageToolbar } from "@/components/pages"; // helpers @@ -19,6 +21,8 @@ type Props = { export const PageEditorToolbarRoot: React.FC = observer((props) => { const { handleOpenNavigationPane, isNavigationPaneOpen, page } = props; + // translation + const { t } = useTranslation(); // derived values const { isContentEditable, editorRef } = page; // page filters @@ -66,6 +70,7 @@ export const PageEditorToolbarRoot: React.FC = observer((props) => { type="button" className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80 transition-colors" onClick={handleOpenNavigationPane} + aria-label={t("page_navigation_pane.open_button")} > diff --git a/web/core/components/pages/navigation-pane/root.tsx b/web/core/components/pages/navigation-pane/root.tsx index d0d99d9b320..df693e23763 100644 --- a/web/core/components/pages/navigation-pane/root.tsx +++ b/web/core/components/pages/navigation-pane/root.tsx @@ -3,6 +3,9 @@ import { observer } from "mobx-react"; import { useRouter, useSearchParams } from "next/navigation"; import { ArrowRightCircle } from "lucide-react"; import { Tab } from "@headlessui/react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { Tooltip } from "@plane/ui"; // hooks import { useQueryParams } from "@/hooks/use-query-params"; // plane web components @@ -39,6 +42,8 @@ export const PageNavigationPaneRoot: React.FC = observer((props) => { ) as TPageNavigationPaneTab | null; const activeTab: TPageNavigationPaneTab = navigationPaneQueryParam || "outline"; const selectedIndex = PAGE_NAVIGATION_PANE_TAB_KEYS.indexOf(activeTab); + // translation + const { t } = useTranslation(); const handleTabChange = useCallback( (index: number) => { @@ -60,13 +65,16 @@ export const PageNavigationPaneRoot: React.FC = observer((props) => { }} >
    - + + +
    diff --git a/web/core/components/pages/navigation-pane/tab-panels/assets.tsx b/web/core/components/pages/navigation-pane/tab-panels/assets.tsx index ed834aeb9b5..ae7f76b847e 100644 --- a/web/core/components/pages/navigation-pane/tab-panels/assets.tsx +++ b/web/core/components/pages/navigation-pane/tab-panels/assets.tsx @@ -5,6 +5,7 @@ import { useParams } from "next/navigation"; import { Download } from "lucide-react"; // plane imports import type { TEditorAsset } from "@plane/editor"; +import { useTranslation } from "@plane/i18n"; import { convertBytesToSize } from "@plane/utils"; // helpers import { getEditorAssetDownloadSrc, getEditorAssetSrc } from "@/helpers/editor.helper"; @@ -28,6 +29,8 @@ const AssetItem = observer((props: AssetItemProps) => { const { workspaceSlug } = useParams(); // derived values const { project_ids } = page; + // translation + const { t } = useTranslation(); const getAssetSrc = (path: string) => { if (!path || !workspaceSlug) return ""; @@ -81,7 +84,7 @@ const AssetItem = observer((props: AssetItemProps) => { className="shrink-0 self-end mb-1 mr-1 py-0.5 px-1 flex items-center gap-1 rounded text-custom-text-200 hover:text-custom-text-100 opacity-0 pointer-events-none group-hover/asset-item:opacity-100 group-hover/asset-item:pointer-events-auto transition-all" > - Download + {t("page_navigation_pane.tabs.assets.download_button")} ); @@ -93,6 +96,8 @@ export const PageNavigationPaneAssetsTabPanel: React.FC = (props) => { const [assets, setAssets] = useState([]); // derived values const { editorRef } = page; + // translation + const { t } = useTranslation(); // subscribe to asset changes useEffect(() => { const unsubscribe = editorRef?.onAssetChange(setAssets); @@ -112,8 +117,10 @@ export const PageNavigationPaneAssetsTabPanel: React.FC = (props) => {
    An image depicting the assets of a page
    -

    Missing images

    -

    Add images to see them here.

    +

    {t("page_navigation_pane.tabs.assets.empty_state.title")}

    +

    + {t("page_navigation_pane.tabs.assets.empty_state.description")} +

    diff --git a/web/core/components/pages/navigation-pane/tab-panels/info/actors-info.tsx b/web/core/components/pages/navigation-pane/tab-panels/info/actors-info.tsx index aabad54fc72..d4c166ddff0 100644 --- a/web/core/components/pages/navigation-pane/tab-panels/info/actors-info.tsx +++ b/web/core/components/pages/navigation-pane/tab-panels/info/actors-info.tsx @@ -30,7 +30,9 @@ export const PageNavigationPaneInfoTabActorsInfo: React.FC = observer((pr return (
    -

    Edited by

    +

    + {t("page_navigation_pane.tabs.info.actors_info.edited_by")} +

    = observer((pr
    -

    {t("common.created_by")}

    +

    + {t("page_navigation_pane.tabs.info.actors_info.created_by")} +

    = observer(( const [documentInfo, setDocumentInfo] = useState(DEFAULT_DOCUMENT_INFO); // derived values const { editorRef } = page; + // translation + const { t } = useTranslation(); // subscribe to asset changes useEffect(() => { const unsubscribe = editorRef?.onDocumentInfoChange(setDocumentInfo); @@ -42,26 +45,26 @@ export const PageNavigationPaneInfoTabDocumentInfo: React.FC = observer(( () => [ { key: "words-count", - title: "Words", + title: t("page_navigation_pane.tabs.info.document_info.words"), info: documentInfo.words, }, { key: "characters-count", - title: "Characters", + title: t("page_navigation_pane.tabs.info.document_info.characters"), info: documentInfo.characters, }, { key: "paragraphs-count", - title: "Paragraphs", + title: t("page_navigation_pane.tabs.info.document_info.paragraphs"), info: documentInfo.paragraphs, }, { key: "read-time", - title: "Read time", + title: t("page_navigation_pane.tabs.info.document_info.read_time"), info: secondsToReadableTime(), }, ], - [documentInfo, secondsToReadableTime] + [documentInfo, secondsToReadableTime, t] ); return ( diff --git a/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx b/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx index f99bd1c97e4..831455b78e9 100644 --- a/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx +++ b/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx @@ -77,6 +77,8 @@ export const PageNavigationPaneInfoTabVersionHistory: React.FC = observer const activeVersion = searchParams.get(PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM); // derived values const { id } = page; + // translation + const { t } = useTranslation(); // query params const { updateQueryParams } = useQueryParams(); // fetch all versions @@ -102,7 +104,9 @@ export const PageNavigationPaneInfoTabVersionHistory: React.FC = observer return (
    -

    Version history

    +

    + {t("page_navigation_pane.tabs.info.version_history.label")} +