From d238bac3872382e670a05d04f4950708968fb913 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 14 Nov 2024 12:55:27 +0530 Subject: [PATCH 01/32] dev: support for edition specific options in pages --- .../components/pages/header/extra-options.ts | 13 +++++++ web/ce/components/pages/header/index.ts | 1 + web/ce/components/pages/index.ts | 1 + .../pages/editor/header/options-dropdown.tsx | 36 ++++++++++--------- 4 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 web/ce/components/pages/header/extra-options.ts create mode 100644 web/ce/components/pages/header/index.ts diff --git a/web/ce/components/pages/header/extra-options.ts b/web/ce/components/pages/header/extra-options.ts new file mode 100644 index 00000000000..c663fa7cd6b --- /dev/null +++ b/web/ce/components/pages/header/extra-options.ts @@ -0,0 +1,13 @@ +import { TContextMenuItem } from "@plane/ui"; + +export const PAGE_OPTIONS_DROPDOWN_EXTRA_OPTIONS: (TContextMenuItem & { + pushAfter?: string; +})[] = [ + { + key: "move-page", + title: "Move page", + action: () => {}, + pushAfter: "make-a-copy", + shouldRender: false, + }, +]; diff --git a/web/ce/components/pages/header/index.ts b/web/ce/components/pages/header/index.ts new file mode 100644 index 00000000000..af0d9904202 --- /dev/null +++ b/web/ce/components/pages/header/index.ts @@ -0,0 +1 @@ +export * from "./extra-options"; diff --git a/web/ce/components/pages/index.ts b/web/ce/components/pages/index.ts index 6f3d30c9a95..a9d45938ad8 100644 --- a/web/ce/components/pages/index.ts +++ b/web/ce/components/pages/index.ts @@ -1,2 +1,3 @@ export * from "./editor"; +export * from "./header"; export * from "./extra-actions"; diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index c7cf53a5f50..908b2923195 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -7,7 +7,7 @@ import { ArchiveRestoreIcon, ArrowUpToLine, Clipboard, Copy, History, Link, Lock // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; // ui -import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; +import { ArchiveIcon, CustomMenu, TContextMenuItem, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; // components import { ExportPageModal } from "@/components/pages"; // helpers @@ -15,6 +15,8 @@ import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper // hooks import { usePageFilters } from "@/hooks/use-page-filters"; import { useQueryParams } from "@/hooks/use-query-params"; +// plane web components +import { PAGE_OPTIONS_DROPDOWN_EXTRA_OPTIONS } from "@/plane-web/components/pages"; // store import { IPage } from "@/store/pages/page"; @@ -88,13 +90,7 @@ export const PageOptionsDropdown: React.FC = observer((props) => { ); // menu items list - const MENU_ITEMS: { - key: string; - action: () => void; - label: string; - icon: React.FC; - shouldRender: boolean; - }[] = [ + const MENU_ITEMS: TContextMenuItem[] = [ { key: "copy-markdown", action: () => { @@ -107,7 +103,7 @@ export const PageOptionsDropdown: React.FC = observer((props) => { }) ); }, - label: "Copy markdown", + title: "Copy markdown", icon: Clipboard, shouldRender: true, }, @@ -125,28 +121,28 @@ export const PageOptionsDropdown: React.FC = observer((props) => { }) ); }, - label: "Copy page link", + title: "Copy page link", icon: Link, shouldRender: true, }, { key: "make-a-copy", action: handleDuplicatePage, - label: "Make a copy", + title: "Make a copy", icon: Copy, shouldRender: canCurrentUserDuplicatePage, }, { key: "lock-unlock-page", action: is_locked ? handleUnlockPage : handleLockPage, - label: is_locked ? "Unlock page" : "Lock page", + title: is_locked ? "Unlock page" : "Lock page", icon: is_locked ? LockOpen : Lock, shouldRender: canCurrentUserLockPage, }, { key: "archive-restore-page", action: archived_at ? handleRestorePage : handleArchivePage, - label: archived_at ? "Restore page" : "Archive page", + title: archived_at ? "Restore page" : "Archive page", icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, shouldRender: canCurrentUserArchivePage, }, @@ -159,18 +155,24 @@ export const PageOptionsDropdown: React.FC = observer((props) => { }); router.push(updatedRoute); }, - label: "Version history", + title: "Version history", icon: History, shouldRender: true, }, { key: "export", action: () => setIsExportModalOpen(true), - label: "Export", + title: "Export", icon: ArrowUpToLine, shouldRender: true, }, ]; + // add extra options + PAGE_OPTIONS_DROPDOWN_EXTRA_OPTIONS.forEach((item) => { + const index = MENU_ITEMS.findIndex((i) => i.key === item.pushAfter); + if (index !== -1) MENU_ITEMS.splice(index + 1, 0, item); + else MENU_ITEMS.push(item); + }); return ( <> @@ -192,8 +194,8 @@ export const PageOptionsDropdown: React.FC = observer((props) => { if (!item.shouldRender) return null; return ( - - {item.label} + {item.icon && } + {item.title} ); })} From 9ceb91c2079ee0777067333c38d95dbfd7aa2540 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 18 Nov 2024 17:33:15 +0530 Subject: [PATCH 02/32] refactor: page quick actions --- apiserver/plane/app/serializers/page.py | 4 + apiserver/plane/app/urls/page.py | 6 + apiserver/plane/app/views/__init__.py | 1 + apiserver/plane/app/views/page/base.py | 39 ++- .../ui/src/dropdowns/context-menu/item.tsx | 32 ++- .../ui/src/dropdowns/context-menu/root.tsx | 3 +- .../components/pages/header/extra-options.ts | 13 - web/ce/components/pages/header/index.ts | 1 - web/ce/components/pages/index.ts | 1 - web/ce/types/page.ts | 1 + .../components/pages/dropdowns/actions.tsx | 222 ++++++++++++++++++ web/core/components/pages/dropdowns/index.ts | 2 +- .../pages/dropdowns/quick-actions.tsx | 131 ----------- .../pages/editor/header/extra-options.tsx | 9 +- .../pages/editor/header/mobile-root.tsx | 22 +- .../pages/editor/header/options-dropdown.tsx | 158 +++---------- .../components/pages/editor/header/root.tsx | 21 +- .../components/pages/editor/page-root.tsx | 30 +-- .../pages/list/block-item-action.tsx | 20 +- .../services/page/project-page.service.ts | 8 + web/core/store/pages/page.ts | 11 + 21 files changed, 366 insertions(+), 369 deletions(-) delete mode 100644 web/ce/components/pages/header/extra-options.ts delete mode 100644 web/ce/components/pages/header/index.ts create mode 100644 web/ce/types/page.ts create mode 100644 web/core/components/pages/dropdowns/actions.tsx delete mode 100644 web/core/components/pages/dropdowns/quick-actions.tsx diff --git a/apiserver/plane/app/serializers/page.py b/apiserver/plane/app/serializers/page.py index e7f273d408a..e1c13528e2c 100644 --- a/apiserver/plane/app/serializers/page.py +++ b/apiserver/plane/app/serializers/page.py @@ -63,6 +63,8 @@ def create(self, validated_data): labels = validated_data.pop("labels", None) project_id = self.context["project_id"] owned_by_id = self.context["owned_by_id"] + description = self.context["description"] + description_binary = self.context["description_binary"] description_html = self.context["description_html"] # Get the workspace id from the project @@ -71,6 +73,8 @@ def create(self, validated_data): # Create the page page = Page.objects.create( **validated_data, + description=description, + description_binary=description_binary, description_html=description_html, owned_by_id=owned_by_id, workspace_id=project.workspace_id, diff --git a/apiserver/plane/app/urls/page.py b/apiserver/plane/app/urls/page.py index 7c1ac5dfee3..e79e9090f4e 100644 --- a/apiserver/plane/app/urls/page.py +++ b/apiserver/plane/app/urls/page.py @@ -8,6 +8,7 @@ SubPagesEndpoint, PagesDescriptionViewSet, PageVersionEndpoint, + PageDuplicateEndpoint, ) @@ -111,4 +112,9 @@ PageVersionEndpoint.as_view(), name="page-versions", ), + path( + "workspaces//projects//pages//duplicate/", + PageDuplicateEndpoint.as_view(), + name="page-duplicate", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 159686fa3ac..f61f7ce3178 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -202,6 +202,7 @@ PageLogEndpoint, SubPagesEndpoint, PagesDescriptionViewSet, + PageDuplicateEndpoint, ) from .page.version import PageVersionEndpoint diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py index 5e56cc7036f..83ae2180df5 100644 --- a/apiserver/plane/app/views/page/base.py +++ b/apiserver/plane/app/views/page/base.py @@ -126,6 +126,10 @@ def create(self, request, slug, project_id): context={ "project_id": project_id, "owned_by_id": request.user.id, + "description": request.data.get("description", {}), + "description_binary": request.data.get( + "description_binary", None + ), "description_html": request.data.get( "description_html", "

" ), @@ -438,7 +442,6 @@ def destroy(self, request, slug, project_id, pk): class PageFavoriteViewSet(BaseViewSet): - model = UserFavorite @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @@ -465,7 +468,6 @@ def destroy(self, request, slug, project_id, pk): class PageLogEndpoint(BaseAPIView): - serializer_class = PageLogSerializer model = PageLog @@ -504,7 +506,6 @@ def delete(self, request, slug, project_id, page_id, transaction): class SubPagesEndpoint(BaseAPIView): - @method_decorator(gzip_page) def get(self, request, slug, project_id, page_id): pages = ( @@ -522,7 +523,6 @@ def get(self, request, slug, project_id, page_id): class PagesDescriptionViewSet(BaseViewSet): - @allow_permission( [ ROLE.ADMIN, @@ -629,3 +629,34 @@ def partial_update(self, request, slug, project_id, pk): return Response({"message": "Updated successfully"}) else: return Response({"error": "No binary data provided"}) + + +class PageDuplicateEndpoint(BaseAPIView): + def post(self, request, slug, project_id, page_id): + page = Page.objects.filter( + pk=page_id, + workspace__slug=slug, + projects__id=project_id, + ).values() + new_page_data = list(page)[0] + new_page_data.name = f"{new_page_data.name} (Copy)" + + serializer = PageSerializer( + data=new_page_data, + context={ + "project_id": project_id, + "owned_by_id": request.user.id, + "description": new_page_data.description, + "description_binary": new_page_data.description_binary, + "description_html": new_page_data.description_html, + }, + ) + + if serializer.is_valid(): + serializer.save() + # capture the page transaction + page_transaction.delay(request.data, None, serializer.data["id"]) + page = Page.objects.get(pk=serializer.data["id"]) + serializer = PageDetailSerializer(page) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/packages/ui/src/dropdowns/context-menu/item.tsx b/packages/ui/src/dropdowns/context-menu/item.tsx index 99ef790e3f6..6f332d0569c 100644 --- a/packages/ui/src/dropdowns/context-menu/item.tsx +++ b/packages/ui/src/dropdowns/context-menu/item.tsx @@ -36,19 +36,25 @@ export const ContextMenuItem: React.FC = (props) => { onMouseEnter={handleActiveItem} disabled={item.disabled} > - {item.icon && } -
-
{item.title}
- {item.description && ( -

- {item.description} -

- )} -
+ {item.customContent ? ( + item.customContent + ) : ( + <> + {item.icon && } +
+
{item.title}
+ {item.description && ( +

+ {item.description} +

+ )} +
+ + )} ); }; diff --git a/packages/ui/src/dropdowns/context-menu/root.tsx b/packages/ui/src/dropdowns/context-menu/root.tsx index 03fe0cf7bc5..ba41d5c4667 100644 --- a/packages/ui/src/dropdowns/context-menu/root.tsx +++ b/packages/ui/src/dropdowns/context-menu/root.tsx @@ -11,7 +11,8 @@ import { usePlatformOS } from "../../hooks/use-platform-os"; export type TContextMenuItem = { key: string; - title: string; + customContent?: React.ReactNode; + title?: string; description?: string; icon?: React.FC; action: () => void; diff --git a/web/ce/components/pages/header/extra-options.ts b/web/ce/components/pages/header/extra-options.ts deleted file mode 100644 index c663fa7cd6b..00000000000 --- a/web/ce/components/pages/header/extra-options.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { TContextMenuItem } from "@plane/ui"; - -export const PAGE_OPTIONS_DROPDOWN_EXTRA_OPTIONS: (TContextMenuItem & { - pushAfter?: string; -})[] = [ - { - key: "move-page", - title: "Move page", - action: () => {}, - pushAfter: "make-a-copy", - shouldRender: false, - }, -]; diff --git a/web/ce/components/pages/header/index.ts b/web/ce/components/pages/header/index.ts deleted file mode 100644 index af0d9904202..00000000000 --- a/web/ce/components/pages/header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./extra-options"; diff --git a/web/ce/components/pages/index.ts b/web/ce/components/pages/index.ts index a9d45938ad8..6f3d30c9a95 100644 --- a/web/ce/components/pages/index.ts +++ b/web/ce/components/pages/index.ts @@ -1,3 +1,2 @@ export * from "./editor"; -export * from "./header"; export * from "./extra-actions"; diff --git a/web/ce/types/page.ts b/web/ce/types/page.ts new file mode 100644 index 00000000000..4f43a07c1f9 --- /dev/null +++ b/web/ce/types/page.ts @@ -0,0 +1 @@ +export type TPageExtraActions = never; diff --git a/web/core/components/pages/dropdowns/actions.tsx b/web/core/components/pages/dropdowns/actions.tsx new file mode 100644 index 00000000000..5cd5029f06b --- /dev/null +++ b/web/core/components/pages/dropdowns/actions.tsx @@ -0,0 +1,222 @@ +"use client"; + +import { useState } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { + ArchiveRestoreIcon, + Copy, + ExternalLink, + Globe2, + Link, + Lock, + LockKeyhole, + LockKeyholeOpen, + Trash2, +} from "lucide-react"; +// plane ui +import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { DeletePageModal } from "@/components/pages"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { copyUrlToClipboard } from "@/helpers/string.helper"; +// hooks +import { useProjectPages } from "@/hooks/store"; +// plane web types +import { TPageExtraActions } from "@/plane-web/types/page"; + +export type TPageActions = + | "full-screen" + | "copy-markdown" + | "toggle-lock" + | "toggle-privacy" + | "open-in-new-tab" + | "copy-link" + | "make-a-copy" + | "archive-restore" + | "delete" + | "version-history" + | "export" + | TPageExtraActions; + +type Props = { + extraOptions?: TContextMenuItem[]; + optionsOrder: TPageActions[]; + pageId: string; + parentRef?: React.RefObject; +}; + +export const PageActions: React.FC = observer((props) => { + const { extraOptions, optionsOrder, pageId, parentRef } = props; + // states + const [deletePageModal, setDeletePageModal] = useState(false); + // params + const { workspaceSlug, projectId } = useParams(); + // store hooks + const { pageById } = useProjectPages(); + const page = pageById(pageId); + if (!page) return null; + const { + access, + archived_at, + archive, + is_locked, + restore, + lock, + unlock, + makePublic, + makePrivate, + duplicate, + canCurrentUserArchivePage, + canCurrentUserDuplicatePage, + canCurrentUserChangeAccess, + canCurrentUserDeletePage, + canCurrentUserLockPage, + } = page; + // derived values + const pageLink = projectId + ? `${workspaceSlug}/projects/${projectId}/pages/${pageId}` + : `${workspaceSlug}/pages/${pageId}`; + + const handleCopyText = () => + copyUrlToClipboard(pageLink).then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Link Copied!", + message: "Page link copied to clipboard.", + }); + }); + + const handleOpenInNewTab = () => window.open(`/${pageLink}`, "_blank"); + + const handleLockPage = async () => + await lock().catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Page could not be locked. Please try again later.", + }) + ); + + const handleUnlockPage = async () => + await unlock().catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Page could not be unlocked. Please try again later.", + }) + ); + + const MENU_ITEMS: (TContextMenuItem & { key: TPageActions })[] = [ + { + key: "toggle-lock", + action: is_locked ? handleUnlockPage : handleLockPage, + title: is_locked ? "Unlock page" : "Lock page", + icon: is_locked ? LockKeyholeOpen : LockKeyhole, + shouldRender: canCurrentUserLockPage, + }, + { + key: "toggle-privacy", + action: async () => { + const changedPageType = access === 0 ? "private" : "public"; + + try { + if (access === 0) await makePrivate(); + else await makePublic(); + + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: `The page has been marked ${changedPageType} and moved to the ${changedPageType} section.`, + }); + } catch { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: `The page couldn't be marked ${changedPageType}. Please try again.`, + }); + } + }, + title: access === 0 ? "Make private" : "Make public", + icon: access === 0 ? Lock : Globe2, + shouldRender: canCurrentUserChangeAccess && !archived_at, + }, + { + key: "open-in-new-tab", + action: handleOpenInNewTab, + title: "Open in new tab", + icon: ExternalLink, + shouldRender: true, + }, + { + key: "copy-link", + action: handleCopyText, + title: "Copy link", + icon: Link, + shouldRender: true, + }, + { + key: "make-a-copy", + action: duplicate, + title: "Make a copy", + icon: Copy, + shouldRender: canCurrentUserDuplicatePage, + }, + { + key: "archive-restore", + action: archived_at ? restore : archive, + title: archived_at ? "Restore" : "Archive", + icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, + shouldRender: canCurrentUserArchivePage, + }, + { + key: "delete", + action: () => setDeletePageModal(true), + title: "Delete", + icon: Trash2, + shouldRender: canCurrentUserDeletePage && !!archived_at, + }, + ]; + if (extraOptions) { + // @ts-expect-error type mismatch, not necessary to fix + MENU_ITEMS.push(...extraOptions); + } + // arrange options + const arrangedOptions = optionsOrder + .map((key) => MENU_ITEMS.find((item) => item.key === key)) + .filter((item) => !!item); + + return ( + <> + setDeletePageModal(false)} pageId={page.id ?? ""} /> + {parentRef && } + + {arrangedOptions.map((item) => { + if (item.shouldRender === false) return null; + return ( + { + e.preventDefault(); + e.stopPropagation(); + item.action(); + }} + className={cn("flex items-center gap-2", item.className)} + disabled={item.disabled} + > + {item.customContent ? ( + item.customContent + ) : ( + <> + {item.icon && } + {item.title} + + )} + + ); + })} + + + ); +}); diff --git a/web/core/components/pages/dropdowns/index.ts b/web/core/components/pages/dropdowns/index.ts index 16d9c337209..74ebad1d675 100644 --- a/web/core/components/pages/dropdowns/index.ts +++ b/web/core/components/pages/dropdowns/index.ts @@ -1,2 +1,2 @@ +export * from "./actions"; export * from "./edit-information-popover"; -export * from "./quick-actions"; diff --git a/web/core/components/pages/dropdowns/quick-actions.tsx b/web/core/components/pages/dropdowns/quick-actions.tsx deleted file mode 100644 index 6bed6be2c65..00000000000 --- a/web/core/components/pages/dropdowns/quick-actions.tsx +++ /dev/null @@ -1,131 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { observer } from "mobx-react"; -import { ArchiveRestoreIcon, ExternalLink, Link, Lock, Trash2, UsersRound } from "lucide-react"; -import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; -// components -import { DeletePageModal } from "@/components/pages"; -// helpers -import { copyUrlToClipboard } from "@/helpers/string.helper"; -// store -import { IPage } from "@/store/pages/page"; - -type Props = { - page: IPage; - pageLink: string; - parentRef: React.RefObject; -}; - -export const PageQuickActions: React.FC = observer((props) => { - const { page, pageLink, parentRef } = props; - // states - const [deletePageModal, setDeletePageModal] = useState(false); - // store hooks - const { - access, - archive, - archived_at, - makePublic, - makePrivate, - restore, - canCurrentUserArchivePage, - canCurrentUserChangeAccess, - canCurrentUserDeletePage, - } = page; - - const handleCopyText = () => - copyUrlToClipboard(pageLink).then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Link Copied!", - message: "Page link copied to clipboard.", - }); - }); - - const handleOpenInNewTab = () => window.open(`/${pageLink}`, "_blank"); - - const MENU_ITEMS: TContextMenuItem[] = [ - { - key: "make-public-private", - action: async () => { - const changedPageType = access === 0 ? "private" : "public"; - - try { - if (access === 0) await makePrivate(); - else await makePublic(); - - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: `The page has been marked ${changedPageType} and moved to the ${changedPageType} section.`, - }); - } catch (err) { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: `The page couldn't be marked ${changedPageType}. Please try again.`, - }); - } - }, - title: access === 0 ? "Make private" : "Make public", - icon: access === 0 ? Lock : UsersRound, - shouldRender: canCurrentUserChangeAccess && !archived_at, - }, - { - key: "open-new-tab", - action: handleOpenInNewTab, - title: "Open in new tab", - icon: ExternalLink, - shouldRender: true, - }, - { - key: "copy-link", - action: handleCopyText, - title: "Copy link", - icon: Link, - shouldRender: true, - }, - { - key: "archive-restore", - action: archived_at ? restore : archive, - title: archived_at ? "Restore" : "Archive", - icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, - shouldRender: canCurrentUserArchivePage, - }, - { - key: "delete", - action: () => setDeletePageModal(true), - title: "Delete", - icon: Trash2, - shouldRender: canCurrentUserDeletePage && !!archived_at, - }, - ]; - - return ( - <> - setDeletePageModal(false)} pageId={page.id ?? ""} /> - - - {MENU_ITEMS.map((item) => { - if (!item.shouldRender) return null; - return ( - { - e.preventDefault(); - e.stopPropagation(); - item.action(); - }} - className="flex items-center gap-2" - disabled={item.disabled} - > - {item.icon && } - {item.title} - - ); - })} - - - ); -}); diff --git a/web/core/components/pages/editor/header/extra-options.tsx b/web/core/components/pages/editor/header/extra-options.tsx index f21959cd93d..58fa60705bb 100644 --- a/web/core/components/pages/editor/header/extra-options.tsx +++ b/web/core/components/pages/editor/header/extra-options.tsx @@ -17,13 +17,12 @@ import { IPage } from "@/store/pages/page"; type Props = { editorRef: React.RefObject; - handleDuplicatePage: () => void; page: IPage; readOnlyEditorRef: React.RefObject; }; export const PageExtraOptions: React.FC = observer((props) => { - const { editorRef, handleDuplicatePage, page, readOnlyEditorRef } = props; + const { editorRef, page, readOnlyEditorRef } = props; // derived values const { archived_at, @@ -86,11 +85,7 @@ export const PageExtraOptions: React.FC = observer((props) => { /> )} - + ); }); diff --git a/web/core/components/pages/editor/header/mobile-root.tsx b/web/core/components/pages/editor/header/mobile-root.tsx index ac831796cbe..0da8613aa0b 100644 --- a/web/core/components/pages/editor/header/mobile-root.tsx +++ b/web/core/components/pages/editor/header/mobile-root.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react"; -import { EditorReadOnlyRefApi, EditorRefApi, IMarking } from "@plane/editor"; +import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; // components import { Header, EHeaderVariant } from "@plane/ui"; import { PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages"; @@ -11,7 +11,6 @@ import { IPage } from "@/store/pages/page"; type Props = { editorReady: boolean; editorRef: React.RefObject; - handleDuplicatePage: () => void; page: IPage; readOnlyEditorReady: boolean; readOnlyEditorRef: React.RefObject; @@ -20,16 +19,8 @@ type Props = { }; export const PageEditorMobileHeaderRoot: React.FC = observer((props) => { - const { - editorReady, - editorRef, - handleDuplicatePage, - page, - readOnlyEditorReady, - readOnlyEditorRef, - setSidePeekVisible, - sidePeekVisible, - } = props; + const { editorReady, editorRef, page, readOnlyEditorReady, readOnlyEditorRef, setSidePeekVisible, sidePeekVisible } = + props; // derived values const { isContentEditable } = page; // page filters @@ -48,12 +39,7 @@ export const PageEditorMobileHeaderRoot: React.FC = observer((props) => { setSidePeekVisible={setSidePeekVisible} /> - +
{(editorReady || readOnlyEditorReady) && isContentEditable && editorRef.current && ( diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 908b2923195..b7e54e45448 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -2,95 +2,52 @@ import { useState } from "react"; import { observer } from "mobx-react"; -import { useParams, useRouter } from "next/navigation"; -import { ArchiveRestoreIcon, ArrowUpToLine, Clipboard, Copy, History, Link, Lock, LockOpen } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { ArrowUpToLine, Clipboard, History } from "lucide-react"; // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; // ui -import { ArchiveIcon, CustomMenu, TContextMenuItem, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; +import { TContextMenuItem, TOAST_TYPE, setToast, ToggleSwitch } from "@plane/ui"; // components -import { ExportPageModal } from "@/components/pages"; +import { ExportPageModal, PageActions, TPageActions } from "@/components/pages"; // helpers -import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper"; +import { copyTextToClipboard } from "@/helpers/string.helper"; // hooks import { usePageFilters } from "@/hooks/use-page-filters"; import { useQueryParams } from "@/hooks/use-query-params"; -// plane web components -import { PAGE_OPTIONS_DROPDOWN_EXTRA_OPTIONS } from "@/plane-web/components/pages"; // store import { IPage } from "@/store/pages/page"; type Props = { editorRef: EditorRefApi | EditorReadOnlyRefApi | null; - handleDuplicatePage: () => void; page: IPage; }; export const PageOptionsDropdown: React.FC = observer((props) => { - const { editorRef, handleDuplicatePage, page } = props; + const { editorRef, page } = props; // router const router = useRouter(); // store values - const { - name, - archived_at, - is_locked, - id, - archive, - lock, - unlock, - canCurrentUserArchivePage, - canCurrentUserDuplicatePage, - canCurrentUserLockPage, - restore, - } = page; + const { name } = page; // states const [isExportModalOpen, setIsExportModalOpen] = useState(false); - // store hooks - const { workspaceSlug, projectId } = useParams(); // page filters const { isFullWidth, handleFullWidth } = usePageFilters(); // update query params const { updateQueryParams } = useQueryParams(); - - const handleArchivePage = async () => - await archive().catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be archived. Please try again later.", - }) - ); - - const handleRestorePage = async () => - await restore().catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be restored. Please try again later.", - }) - ); - - const handleLockPage = async () => - await lock().catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be locked. Please try again later.", - }) - ); - - const handleUnlockPage = async () => - await unlock().catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be unlocked. Please try again later.", - }) - ); - // menu items list - const MENU_ITEMS: TContextMenuItem[] = [ + const EXTRA_MENU_OPTIONS: (TContextMenuItem & { key: TPageActions })[] = [ + { + key: "full-screen", + action: () => handleFullWidth(!isFullWidth), + customContent: ( + <> + Full width + {}} /> + + ), + className: "flex items-center justify-between gap-2", + }, { key: "copy-markdown", action: () => { @@ -107,45 +64,6 @@ export const PageOptionsDropdown: React.FC = observer((props) => { icon: Clipboard, shouldRender: true, }, - { - key: "copy-page-link", - action: () => { - const pageLink = projectId - ? `${workspaceSlug?.toString()}/projects/${projectId?.toString()}/pages/${id}` - : `${workspaceSlug?.toString()}/pages/${id}`; - copyUrlToClipboard(pageLink).then(() => - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: "Page link copied to clipboard.", - }) - ); - }, - title: "Copy page link", - icon: Link, - shouldRender: true, - }, - { - key: "make-a-copy", - action: handleDuplicatePage, - title: "Make a copy", - icon: Copy, - shouldRender: canCurrentUserDuplicatePage, - }, - { - key: "lock-unlock-page", - action: is_locked ? handleUnlockPage : handleLockPage, - title: is_locked ? "Unlock page" : "Lock page", - icon: is_locked ? LockOpen : Lock, - shouldRender: canCurrentUserLockPage, - }, - { - key: "archive-restore-page", - action: archived_at ? handleRestorePage : handleArchivePage, - title: archived_at ? "Restore page" : "Archive page", - icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, - shouldRender: canCurrentUserArchivePage, - }, { key: "version-history", action: () => { @@ -167,12 +85,6 @@ export const PageOptionsDropdown: React.FC = observer((props) => { shouldRender: true, }, ]; - // add extra options - PAGE_OPTIONS_DROPDOWN_EXTRA_OPTIONS.forEach((item) => { - const index = MENU_ITEMS.findIndex((i) => i.key === item.pushAfter); - if (index !== -1) MENU_ITEMS.splice(index + 1, 0, item); - else MENU_ITEMS.push(item); - }); return ( <> @@ -182,24 +94,20 @@ export const PageOptionsDropdown: React.FC = observer((props) => { onClose={() => setIsExportModalOpen(false)} pageTitle={name ?? ""} /> - - handleFullWidth(!isFullWidth)} - > - Full width - {}} /> - - {MENU_ITEMS.map((item) => { - if (!item.shouldRender) return null; - return ( - - {item.icon && } - {item.title} - - ); - })} - + ); }); diff --git a/web/core/components/pages/editor/header/root.tsx b/web/core/components/pages/editor/header/root.tsx index 9640f4e43b6..42fb017fb79 100644 --- a/web/core/components/pages/editor/header/root.tsx +++ b/web/core/components/pages/editor/header/root.tsx @@ -13,7 +13,6 @@ import { IPage } from "@/store/pages/page"; type Props = { editorReady: boolean; editorRef: React.RefObject; - handleDuplicatePage: () => void; page: IPage; readOnlyEditorReady: boolean; readOnlyEditorRef: React.RefObject; @@ -22,16 +21,8 @@ type Props = { }; export const PageEditorHeaderRoot: React.FC = observer((props) => { - const { - editorReady, - editorRef, - handleDuplicatePage, - page, - readOnlyEditorReady, - readOnlyEditorRef, - setSidePeekVisible, - sidePeekVisible, - } = props; + const { editorReady, editorRef, page, readOnlyEditorReady, readOnlyEditorRef, setSidePeekVisible, sidePeekVisible } = + props; // derived values const { isContentEditable } = page; // page filters @@ -62,12 +53,7 @@ export const PageEditorHeaderRoot: React.FC = observer((props) => { )} - +
= observer((props) => { readOnlyEditorRef={readOnlyEditorRef} editorReady={editorReady} readOnlyEditorReady={readOnlyEditorReady} - handleDuplicatePage={handleDuplicatePage} page={page} sidePeekVisible={sidePeekVisible} setSidePeekVisible={setSidePeekVisible} diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx index ff1f3519e93..6cc353a709a 100644 --- a/web/core/components/pages/editor/page-root.tsx +++ b/web/core/components/pages/editor/page-root.tsx @@ -3,14 +3,9 @@ import { observer } from "mobx-react"; import { useSearchParams } from "next/navigation"; // editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; -// types -import { TPage } from "@plane/types"; -// ui -import { setToast, TOAST_TYPE } from "@plane/ui"; // components import { PageEditorHeaderRoot, PageEditorBody, PageVersionsOverlay, PagesVersionEditor } from "@/components/pages"; // hooks -import { useProjectPages } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePageFallback } from "@/hooks/use-page-fallback"; import { useQueryParams } from "@/hooks/use-query-params"; @@ -42,10 +37,8 @@ export const PageRoot = observer((props: TPageRootProps) => { const router = useAppRouter(); // search params const searchParams = useSearchParams(); - // store hooks - const { createPage } = useProjectPages(); // derived values - const { access, description_html, name, isContentEditable, updateDescription } = page; + const { isContentEditable, updateDescription } = page; // page fallback usePageFallback({ editorRef, @@ -59,26 +52,6 @@ export const PageRoot = observer((props: TPageRootProps) => { // update query params const { updateQueryParams } = useQueryParams(); - const handleCreatePage = async (payload: Partial) => await createPage(payload); - - const handleDuplicatePage = async () => { - const formData: Partial = { - name: "Copy of " + name, - description_html: editorRef.current?.getDocument().html ?? description_html ?? "

", - access, - }; - - await handleCreatePage(formData) - .then((res) => router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res?.id}`)) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be duplicated. Please try again later.", - }) - ); - }; - const version = searchParams.get("version"); useEffect(() => { if (!version) { @@ -135,7 +108,6 @@ export const PageRoot = observer((props: TPageRootProps) => { ; }; export const BlockItemAction: FC = observer((props) => { - const { workspaceSlug, projectId, pageId, parentRef } = props; + const { pageId, parentRef } = props; // store hooks const page = usePage(pageId); const { getUserDetails } = useMember(); @@ -94,10 +92,18 @@ export const BlockItemAction: FC = observer((props) => { )} {/* quick actions dropdown */} - ); diff --git a/web/core/services/page/project-page.service.ts b/web/core/services/page/project-page.service.ts index 00d9401a69a..b0583742eda 100644 --- a/web/core/services/page/project-page.service.ts +++ b/web/core/services/page/project-page.service.ts @@ -158,4 +158,12 @@ export class ProjectPageService extends APIService { throw error; }); } + + async duplicate(workspaceSlug: string, projectId: string, pageId: string): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/duplicate/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts index ee4c499b80c..07ebd6f165e 100644 --- a/web/core/store/pages/page.ts +++ b/web/core/store/pages/page.ts @@ -43,6 +43,7 @@ export interface IPage extends TPage { updatePageLogo: (logo_props: TLogoProps) => Promise; addToFavorites: () => Promise; removePageFromFavorites: () => Promise; + duplicate: () => Promise; } export class Page implements IPage { @@ -149,6 +150,7 @@ export class Page implements IPage { updatePageLogo: action, addToFavorites: action, removePageFromFavorites: action, + duplicate: action, }); this.pageService = new ProjectPageService(); @@ -549,4 +551,13 @@ export class Page implements IPage { throw error; }); }; + + /** + * @description duplicate the page + */ + duplicate = async () => { + const { workspaceSlug, projectId } = this.store.router; + if (!workspaceSlug || !projectId || !this.id) return undefined; + await this.pageService.duplicate(workspaceSlug, projectId, this.id); + }; } From bb00042bff0fc6ab00cf0a7e674d0cd90b4261fc Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Wed, 20 Nov 2024 13:03:27 +0530 Subject: [PATCH 03/32] chore: add customizable page actions --- .../ui/src/dropdowns/context-menu/item.tsx | 4 +- packages/ui/src/dropdowns/custom-menu.tsx | 4 +- .../[projectId]/pages/(list)/header.tsx | 15 +- web/ce/components/pages/index.ts | 1 + web/ce/components/pages/modals/index.ts | 1 + .../pages/modals/move-page-modal.tsx | 10 + web/ce/types/page.ts | 1 - .../components/pages/dropdowns/actions.tsx | 241 +++++++-------- .../pages/editor/header/options-dropdown.tsx | 277 ++++++++++++++---- .../pages/list/block-item-action.tsx | 190 +++++++++++- web/core/components/pages/list/block.tsx | 4 +- web/core/store/pages/page.ts | 15 + web/core/store/pages/project-page.store.ts | 37 ++- 13 files changed, 581 insertions(+), 219 deletions(-) create mode 100644 web/ce/components/pages/modals/index.ts create mode 100644 web/ce/components/pages/modals/move-page-modal.tsx delete mode 100644 web/ce/types/page.ts diff --git a/packages/ui/src/dropdowns/context-menu/item.tsx b/packages/ui/src/dropdowns/context-menu/item.tsx index 6f332d0569c..83124392082 100644 --- a/packages/ui/src/dropdowns/context-menu/item.tsx +++ b/packages/ui/src/dropdowns/context-menu/item.tsx @@ -36,9 +36,7 @@ export const ContextMenuItem: React.FC = (props) => { onMouseEnter={handleActiveItem} disabled={item.disabled} > - {item.customContent ? ( - item.customContent - ) : ( + {item.customContent ?? ( <> {item.icon && }
diff --git a/packages/ui/src/dropdowns/custom-menu.tsx b/packages/ui/src/dropdowns/custom-menu.tsx index 274601822ea..e75c43230c3 100644 --- a/packages/ui/src/dropdowns/custom-menu.tsx +++ b/packages/ui/src/dropdowns/custom-menu.tsx @@ -54,7 +54,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { if (referenceElement) referenceElement.focus(); }; const closeDropdown = () => { - isOpen && onMenuClose && onMenuClose(); + if (isOpen) onMenuClose?.(); setIsOpen(false); }; @@ -209,7 +209,7 @@ const MenuItem: React.FC = (props) => { )} onClick={(e) => { close(); - onClick && onClick(e); + onClick?.(e); }} disabled={disabled} > diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx index f98bd9db47c..b5fc0f8caf5 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx @@ -13,9 +13,7 @@ import { BreadcrumbLink, Logo } from "@/components/common"; // constants import { EPageAccess } from "@/constants/page"; // hooks -import { useEventTracker, useProject, useProjectPages, useUserPermissions } from "@/hooks/store"; -// plane web hooks -import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; +import { useEventTracker, useProject, useProjectPages } from "@/hooks/store"; export const PagesListHeader = observer(() => { // states @@ -25,17 +23,10 @@ export const PagesListHeader = observer(() => { const { workspaceSlug } = useParams(); const searchParams = useSearchParams(); const pageType = searchParams.get("type"); - // store hooks - const { allowPermissions } = useUserPermissions(); const { currentProjectDetails, loader } = useProject(); - const { createPage } = useProjectPages(); + const { canCurrentUserCreatePage, createPage } = useProjectPages(); const { setTrackElement } = useEventTracker(); - // auth - const canUserCreatePage = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - EUserPermissionsLevel.PROJECT - ); // handle page create const handleCreatePage = async () => { setIsCreatingPage(true); @@ -88,7 +79,7 @@ export const PagesListHeader = observer(() => {
- {canUserCreatePage ? ( + {canCurrentUserCreatePage ? (
; handleDuplicatePage: () => void; page: IPage; - readOnlyEditorRef: React.RefObject; + syncState: boolean | null; }; export const PageExtraOptions: React.FC = observer((props) => { - const { editorRef, handleDuplicatePage, page, readOnlyEditorRef } = props; + const { editorRef, syncState, handleDuplicatePage, page } = props; // derived values const { archived_at, @@ -60,6 +61,7 @@ export const PageExtraOptions: React.FC = observer((props) => { return (
{is_locked && } + {!syncState && } {archived_at && (
@@ -85,12 +87,8 @@ export const PageExtraOptions: React.FC = observer((props) => { iconClassName="text-custom-text-100" /> )} - - + +
); }); diff --git a/web/core/components/pages/editor/header/info-popover.tsx b/web/core/components/pages/editor/header/info-popover.tsx index e295d8ea278..13a1b17666a 100644 --- a/web/core/components/pages/editor/header/info-popover.tsx +++ b/web/core/components/pages/editor/header/info-popover.tsx @@ -2,12 +2,12 @@ import { useState } from "react"; import { usePopper } from "react-popper"; import { Info } from "lucide-react"; // plane editor -import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; +import { EditorRefApi } from "@plane/editor"; // helpers import { getReadTimeFromWordsCount } from "@/helpers/date-time.helper"; type Props = { - editorRef: EditorRefApi | EditorReadOnlyRefApi | null; + editorRef: EditorRefApi | null; }; export const PageInfoPopover: React.FC = (props) => { diff --git a/web/core/components/pages/editor/header/mobile-root.tsx b/web/core/components/pages/editor/header/mobile-root.tsx index ac831796cbe..df93a70e922 100644 --- a/web/core/components/pages/editor/header/mobile-root.tsx +++ b/web/core/components/pages/editor/header/mobile-root.tsx @@ -13,52 +13,34 @@ type Props = { editorRef: React.RefObject; handleDuplicatePage: () => void; page: IPage; - readOnlyEditorReady: boolean; - readOnlyEditorRef: React.RefObject; setSidePeekVisible: (sidePeekState: boolean) => void; sidePeekVisible: boolean; }; export const PageEditorMobileHeaderRoot: React.FC = observer((props) => { - const { - editorReady, - editorRef, - handleDuplicatePage, - page, - readOnlyEditorReady, - readOnlyEditorRef, - setSidePeekVisible, - sidePeekVisible, - } = props; + const { editorReady, editorRef, handleDuplicatePage, page, setSidePeekVisible, sidePeekVisible } = props; // derived values const { isContentEditable } = page; // page filters const { isFullWidth } = usePageFilters(); - if (!editorRef.current && !readOnlyEditorRef.current) return null; + if (!editorRef.current) return null; return ( <>
- +
- {(editorReady || readOnlyEditorReady) && isContentEditable && editorRef.current && ( - - )} + {editorReady && isContentEditable && editorRef.current && }
); diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index ff0987a9dc2..a1db6f97241 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -15,7 +15,7 @@ import { LucideIcon, } from "lucide-react"; // document editor -import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; +import { EditorRefApi } from "@plane/editor"; // ui import { ArchiveIcon, CustomMenu, type ISvgIcons, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; // components @@ -30,7 +30,7 @@ import { useQueryParams } from "@/hooks/use-query-params"; import { IPage } from "@/store/pages/page"; type Props = { - editorRef: EditorRefApi | EditorReadOnlyRefApi | null; + editorRef: EditorRefApi | null; handleDuplicatePage: () => void; page: IPage; }; diff --git a/web/core/components/pages/editor/header/root.tsx b/web/core/components/pages/editor/header/root.tsx index 9640f4e43b6..d8001faf000 100644 --- a/web/core/components/pages/editor/header/root.tsx +++ b/web/core/components/pages/editor/header/root.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react"; -import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; +import { EditorRefApi } from "@plane/editor"; // components import { Header, EHeaderVariant } from "@plane/ui"; import { PageEditorMobileHeaderRoot, PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages"; @@ -15,35 +15,25 @@ type Props = { editorRef: React.RefObject; handleDuplicatePage: () => void; page: IPage; - readOnlyEditorReady: boolean; - readOnlyEditorRef: React.RefObject; setSidePeekVisible: (sidePeekState: boolean) => void; sidePeekVisible: boolean; + syncState: boolean | null; }; export const PageEditorHeaderRoot: React.FC = observer((props) => { - const { - editorReady, - editorRef, - handleDuplicatePage, - page, - readOnlyEditorReady, - readOnlyEditorRef, - setSidePeekVisible, - sidePeekVisible, - } = props; + const { editorReady, editorRef, setSidePeekVisible, sidePeekVisible, handleDuplicatePage, page, syncState } = props; // derived values const { isContentEditable } = page; // page filters const { isFullWidth } = usePageFilters(); - if (!editorRef.current && !readOnlyEditorRef.current) return null; + if (!editorRef.current) return null; return ( <>
- {(editorReady || readOnlyEditorReady) && ( + {editorReady && (
= observer((props) => { })} >
)} - {(editorReady || readOnlyEditorReady) && isContentEditable && editorRef.current && ( - - )} + {editorReady && isContentEditable && editorRef.current && }
{ // states const [editorReady, setEditorReady] = useState(false); const [hasConnectionFailed, setHasConnectionFailed] = useState(false); - const [readOnlyEditorReady, setReadOnlyEditorReady] = useState(false); const [sidePeekVisible, setSidePeekVisible] = useState(window.innerWidth >= 768); + const [syncState, setSyncing] = useState(null); const [isVersionsOverlayOpen, setIsVersionsOverlayOpen] = useState(false); // refs const editorRef = useRef(null); - const readOnlyEditorRef = useRef(null); // router const router = useAppRouter(); // search params @@ -99,9 +98,7 @@ export const PageRoot = observer((props: TPageRootProps) => { editorRef.current?.clearEditor(); editorRef.current?.setEditorValue(descriptionHTML); }; - const currentVersionDescription = isContentEditable - ? editorRef.current?.getDocument().html - : readOnlyEditorRef.current?.getDocument().html; + const currentVersionDescription = editorRef.current?.getDocument().html; return ( <> @@ -137,20 +134,18 @@ export const PageRoot = observer((props: TPageRootProps) => { editorRef={editorRef} handleDuplicatePage={handleDuplicatePage} page={page} - readOnlyEditorReady={readOnlyEditorReady} - readOnlyEditorRef={readOnlyEditorRef} setSidePeekVisible={(state) => setSidePeekVisible(state)} sidePeekVisible={sidePeekVisible} + syncState={syncState} /> setHasConnectionFailed(status)} - handleEditorReady={(val) => setEditorReady(val)} - handleReadOnlyEditorReady={() => setReadOnlyEditorReady(true)} + handleConnectionStatus={setHasConnectionFailed} + handleEditorReady={setEditorReady} page={page} - readOnlyEditorRef={readOnlyEditorRef} sidePeekVisible={sidePeekVisible} + setSyncing={setSyncing} /> ); diff --git a/web/core/components/pages/editor/summary/content-browser.tsx b/web/core/components/pages/editor/summary/content-browser.tsx index 669d2e978c8..16d818aaeb7 100644 --- a/web/core/components/pages/editor/summary/content-browser.tsx +++ b/web/core/components/pages/editor/summary/content-browser.tsx @@ -1,11 +1,11 @@ import { useState, useEffect } from "react"; // plane editor -import { EditorReadOnlyRefApi, EditorRefApi, IMarking } from "@plane/editor"; +import { EditorRefApi, IMarking } from "@plane/editor"; // components import { OutlineHeading1, OutlineHeading2, OutlineHeading3 } from "./heading-components"; type Props = { - editorRef: EditorRefApi | EditorReadOnlyRefApi | null; + editorRef: EditorRefApi | null; setSidePeekVisible?: (sidePeekState: boolean) => void; }; diff --git a/web/core/components/pages/editor/summary/popover.tsx b/web/core/components/pages/editor/summary/popover.tsx index 5d14234f037..9acc4a7cc0c 100644 --- a/web/core/components/pages/editor/summary/popover.tsx +++ b/web/core/components/pages/editor/summary/popover.tsx @@ -2,14 +2,14 @@ import { useState } from "react"; import { usePopper } from "react-popper"; import { List } from "lucide-react"; // document editor -import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; +import { EditorRefApi } from "@plane/editor"; // helpers import { cn } from "@/helpers/common.helper"; // components import { PageContentBrowser } from "./content-browser"; type Props = { - editorRef: EditorRefApi | EditorReadOnlyRefApi | null; + editorRef: EditorRefApi | null; isFullWidth: boolean; sidePeekVisible: boolean; setSidePeekVisible: (sidePeekState: boolean) => void; diff --git a/web/core/components/pages/modals/export-page-modal.tsx b/web/core/components/pages/modals/export-page-modal.tsx index cf4f0a0f4b5..acf4ff08311 100644 --- a/web/core/components/pages/modals/export-page-modal.tsx +++ b/web/core/components/pages/modals/export-page-modal.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { PageProps, pdf } from "@react-pdf/renderer"; import { Controller, useForm } from "react-hook-form"; // plane editor -import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; +import { EditorRefApi } from "@plane/editor"; // plane ui import { Button, CustomSelect, EModalPosition, EModalWidth, ModalCore, setToast, TOAST_TYPE } from "@plane/ui"; // components @@ -16,7 +16,7 @@ import { } from "@/helpers/editor.helper"; type Props = { - editorRef: EditorRefApi | EditorReadOnlyRefApi | null; + editorRef: EditorRefApi | null; isOpen: boolean; onClose: () => void; pageTitle: string; diff --git a/web/core/hooks/use-collaborative-page-actions.tsx b/web/core/hooks/use-collaborative-page-actions.tsx index 6ec9f799050..6196929b6a4 100644 --- a/web/core/hooks/use-collaborative-page-actions.tsx +++ b/web/core/hooks/use-collaborative-page-actions.tsx @@ -50,6 +50,10 @@ export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorRead try { await actionDetails.execute(isPerformedByCurrentUser); if (isPerformedByCurrentUser) { + const serverEventName = getServerEventName(clientAction); + if (serverEventName) { + editorRef?.emitRealTimeUpdate(serverEventName); + } setCurrentActionBeingProcessed(clientAction); } } catch { @@ -60,18 +64,9 @@ export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorRead }); } }, - [actionHandlerMap] + [actionHandlerMap, editorRef] ); - useEffect(() => { - if (currentActionBeingProcessed) { - const serverEventName = getServerEventName(currentActionBeingProcessed); - if (serverEventName) { - editorRef?.emitRealTimeUpdate(serverEventName); - } - } - }, [currentActionBeingProcessed, editorRef]); - useEffect(() => { const realTimeStatelessMessageListener = editorRef?.listenToRealTimeUpdate(); @@ -95,6 +90,5 @@ export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorRead return { executeCollaborativeAction, - EVENT_ACTION_DETAILS_MAP: actionHandlerMap, }; }; From d52b7475be582bd510ce83079f1754046d953d75 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 16 Dec 2024 16:35:44 +0530 Subject: [PATCH 13/32] chore: add sync for page access cahnge --- .../document-collaborative-events.ts | 2 + .../components/pages/dropdowns/actions.tsx | 10 +++- .../pages/editor/header/extra-options.tsx | 9 ++-- .../pages/editor/header/mobile-root.tsx | 18 ++----- .../pages/editor/header/options-dropdown.tsx | 2 + .../components/pages/editor/header/root.tsx | 11 ++--- .../pages/list/block-item-action.tsx | 4 +- .../hooks/use-collaborative-page-actions.tsx | 22 ++++++++- web/core/hooks/use-page-operations.ts | 16 ++++--- web/core/store/pages/page.ts | 48 ++++++++++--------- 10 files changed, 85 insertions(+), 57 deletions(-) diff --git a/packages/editor/src/core/constants/document-collaborative-events.ts b/packages/editor/src/core/constants/document-collaborative-events.ts index 5e79efc7a71..72e8b1dbded 100644 --- a/packages/editor/src/core/constants/document-collaborative-events.ts +++ b/packages/editor/src/core/constants/document-collaborative-events.ts @@ -3,4 +3,6 @@ export const DocumentCollaborativeEvents = { unlock: { client: "unlocked", server: "unlock" }, archive: { client: "archived", server: "archive" }, unarchive: { client: "unarchived", server: "unarchive" }, + "make-public": { client: "made-public", server: "make-public" }, + "make-private": { client: "made-private", server: "make-private" }, } as const; diff --git a/web/core/components/pages/dropdowns/actions.tsx b/web/core/components/pages/dropdowns/actions.tsx index 16914f370b6..f979d7f43bd 100644 --- a/web/core/components/pages/dropdowns/actions.tsx +++ b/web/core/components/pages/dropdowns/actions.tsx @@ -14,6 +14,8 @@ import { LockKeyholeOpen, Trash2, } from "lucide-react"; +// plane editor +import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; // plane ui import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui"; // components @@ -44,6 +46,7 @@ export type TPageActions = | "move"; type Props = { + editorRef?: EditorRefApi | EditorReadOnlyRefApi | null; extraOptions?: (TContextMenuItem & { key: TPageActions })[]; optionsOrder: TPageActions[]; page: IPage; @@ -51,12 +54,15 @@ type Props = { }; export const PageActions: React.FC = observer((props) => { - const { extraOptions, optionsOrder, page, parentRef } = props; + const { editorRef, extraOptions, optionsOrder, page, parentRef } = props; // states const [deletePageModal, setDeletePageModal] = useState(false); const [movePageModal, setMovePageModal] = useState(false); // page operations - const { pageOperations } = usePageOperations(page); + const { pageOperations } = usePageOperations({ + editorRef, + page, + }); // derived values const { access, diff --git a/web/core/components/pages/editor/header/extra-options.tsx b/web/core/components/pages/editor/header/extra-options.tsx index 58fa60705bb..bfc6d35eaf7 100644 --- a/web/core/components/pages/editor/header/extra-options.tsx +++ b/web/core/components/pages/editor/header/extra-options.tsx @@ -16,13 +16,12 @@ import useOnlineStatus from "@/hooks/use-online-status"; import { IPage } from "@/store/pages/page"; type Props = { - editorRef: React.RefObject; + editorRef: EditorRefApi | EditorReadOnlyRefApi | null; page: IPage; - readOnlyEditorRef: React.RefObject; }; export const PageExtraOptions: React.FC = observer((props) => { - const { editorRef, page, readOnlyEditorRef } = props; + const { editorRef, page } = props; // derived values const { archived_at, @@ -84,8 +83,8 @@ export const PageExtraOptions: React.FC = observer((props) => { iconClassName="text-custom-text-100" /> )} - - + +
); }); diff --git a/web/core/components/pages/editor/header/mobile-root.tsx b/web/core/components/pages/editor/header/mobile-root.tsx index 0da8613aa0b..003237d915a 100644 --- a/web/core/components/pages/editor/header/mobile-root.tsx +++ b/web/core/components/pages/editor/header/mobile-root.tsx @@ -9,42 +9,34 @@ import { usePageFilters } from "@/hooks/use-page-filters"; import { IPage } from "@/store/pages/page"; type Props = { - editorReady: boolean; - editorRef: React.RefObject; + editorRef: EditorRefApi | EditorReadOnlyRefApi | null; page: IPage; - readOnlyEditorReady: boolean; - readOnlyEditorRef: React.RefObject; setSidePeekVisible: (sidePeekState: boolean) => void; sidePeekVisible: boolean; }; export const PageEditorMobileHeaderRoot: React.FC = observer((props) => { - const { editorReady, editorRef, page, readOnlyEditorReady, readOnlyEditorRef, setSidePeekVisible, sidePeekVisible } = - props; + const { editorRef, page, setSidePeekVisible, sidePeekVisible } = props; // derived values const { isContentEditable } = page; // page filters const { isFullWidth } = usePageFilters(); - if (!editorRef.current && !readOnlyEditorRef.current) return null; - return ( <>
- +
- {(editorReady || readOnlyEditorReady) && isContentEditable && editorRef.current && ( - - )} + {isContentEditable && editorRef && }
); diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 58d89b57b17..3793eee9d28 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -98,11 +98,13 @@ export const PageOptionsDropdown: React.FC = observer((props) => { pageTitle={name ?? ""} /> = observer((props) => { const { isContentEditable } = page; // page filters const { isFullWidth } = usePageFilters(); + // derived values + const resolvedEditorRef = isContentEditable ? editorRef.current : readOnlyEditorRef.current; - if (!editorRef.current && !readOnlyEditorRef.current) return null; + if (!resolvedEditorRef) return null; return ( <> @@ -53,14 +55,11 @@ export const PageEditorHeaderRoot: React.FC = observer((props) => { )} - +
= observer((props) => { const page = usePage(pageId); const { getUserDetails } = useMember(); // page operations - const { pageOperations } = usePageOperations(page); + const { pageOperations } = usePageOperations({ + page, + }); // derived values const { access, created_at, is_favorite, owned_by, canCurrentUserFavoritePage } = page; const ownerDetails = owned_by ? getUserDetails(owned_by) : undefined; diff --git a/web/core/hooks/use-collaborative-page-actions.tsx b/web/core/hooks/use-collaborative-page-actions.tsx index 6ec9f799050..cd89607d644 100644 --- a/web/core/hooks/use-collaborative-page-actions.tsx +++ b/web/core/hooks/use-collaborative-page-actions.tsx @@ -14,7 +14,13 @@ type CollaborativeActionEvent = | { type: "sendMessageToServer"; message: TDocumentEventsServer } | { type: "receivedMessageFromServer"; message: TDocumentEventsClient }; -export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorReadOnlyRefApi | null, page: IPage) => { +type Props = { + editorRef?: EditorRefApi | EditorReadOnlyRefApi | null; + page: IPage; +}; + +export const useCollaborativePageActions = (props: Props) => { + const { editorRef, page } = props; // currentUserAction local state to track if the current action is being processed, a // local action is basically the action performed by the current user to avoid double operations const [currentActionBeingProcessed, setCurrentActionBeingProcessed] = useState(null); @@ -37,6 +43,14 @@ export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorRead execute: (shouldSync) => page.restore(shouldSync), errorMessage: "Page could not be restored. Please try again later.", }, + [DocumentCollaborativeEvents["make-public"].client]: { + execute: (shouldSync) => page.makePublic(shouldSync), + errorMessage: "Page could not be made public. Please try again later.", + }, + [DocumentCollaborativeEvents["make-private"].client]: { + execute: (shouldSync) => page.makePrivate(shouldSync), + errorMessage: "Page could not be made private. Please try again later.", + }, }), [page] ); @@ -64,6 +78,7 @@ export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorRead ); useEffect(() => { + if (!editorRef) return; if (currentActionBeingProcessed) { const serverEventName = getServerEventName(currentActionBeingProcessed); if (serverEventName) { @@ -73,9 +88,12 @@ export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorRead }, [currentActionBeingProcessed, editorRef]); useEffect(() => { - const realTimeStatelessMessageListener = editorRef?.listenToRealTimeUpdate(); + if (!editorRef) return; + const realTimeStatelessMessageListener = editorRef?.listenToRealTimeUpdate(); + console.log(realTimeStatelessMessageListener); const handleStatelessMessage = (message: { payload: TDocumentEventsClient }) => { + console.log("aaa", message); if (currentActionBeingProcessed === message.payload) { setCurrentActionBeingProcessed(null); return; diff --git a/web/core/hooks/use-page-operations.ts b/web/core/hooks/use-page-operations.ts index 1493aadb1cd..7990252a6b5 100644 --- a/web/core/hooks/use-page-operations.ts +++ b/web/core/hooks/use-page-operations.ts @@ -1,5 +1,7 @@ import { useMemo } from "react"; import { useParams } from "next/navigation"; +// plane editor +import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; // plane ui import { setToast, TOAST_TYPE } from "@plane/ui"; // helpers @@ -19,32 +21,34 @@ export type TPageOperations = { toggleArchive: () => void; }; +type Props = { + editorRef?: EditorRefApi | EditorReadOnlyRefApi | null; + page: IPage; +}; + export const usePageOperations = ( - page: IPage + props: Props ): { pageOperations: TPageOperations; } => { + const { page } = props; // params const { workspaceSlug, projectId } = useParams(); // derived values const { access, addToFavorites, - archive, archived_at, duplicate, id, is_favorite, is_locked, - lock, makePrivate, makePublic, removePageFromFavorites, - restore, - unlock, } = page; // collaborative actions - const { executeCollaborativeAction } = useCollaborativePageActions(undefined, page); + const { executeCollaborativeAction } = useCollaborativePageActions(props); // page operations const pageOperations: TPageOperations = useMemo(() => { const pageLink = projectId ? `${workspaceSlug}/projects/${projectId}/pages/${id}` : `${workspaceSlug}/pages/${id}`; diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts index bbb8dfd8eab..383e6d8ef19 100644 --- a/web/core/store/pages/page.ts +++ b/web/core/store/pages/page.ts @@ -33,8 +33,8 @@ export interface IPage extends TPage { update: (pageData: Partial) => Promise; updateTitle: (title: string) => void; updateDescription: (document: TDocumentPayload) => Promise; - makePublic: () => Promise; - makePrivate: () => Promise; + makePublic: (shouldSync?: boolean) => Promise; + makePrivate: (shouldSync?: boolean) => Promise; lock: (shouldSync?: boolean) => Promise; unlock: (shouldSync?: boolean) => Promise; archive: (shouldSync?: boolean) => Promise; @@ -415,44 +415,48 @@ export class Page implements IPage { /** * @description make the page public */ - makePublic = async () => { + makePublic = async (shouldSync: boolean = true) => { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId || !this.id) return undefined; const pageAccess = this.access; runInAction(() => (this.access = EPageAccess.PUBLIC)); - try { - await this.pageService.updateAccess(workspaceSlug, projectId, this.id, { - access: EPageAccess.PUBLIC, - }); - } catch (error) { - runInAction(() => { - this.access = pageAccess; - }); - throw error; + if (shouldSync) { + try { + await this.pageService.updateAccess(workspaceSlug, projectId, this.id, { + access: EPageAccess.PUBLIC, + }); + } catch (error) { + runInAction(() => { + this.access = pageAccess; + }); + throw error; + } } }; /** * @description make the page private */ - makePrivate = async () => { + makePrivate = async (shouldSync: boolean = true) => { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId || !this.id) return undefined; const pageAccess = this.access; runInAction(() => (this.access = EPageAccess.PRIVATE)); - try { - await this.pageService.updateAccess(workspaceSlug, projectId, this.id, { - access: EPageAccess.PRIVATE, - }); - } catch (error) { - runInAction(() => { - this.access = pageAccess; - }); - throw error; + if (shouldSync) { + try { + await this.pageService.updateAccess(workspaceSlug, projectId, this.id, { + access: EPageAccess.PRIVATE, + }); + } catch (error) { + runInAction(() => { + this.access = pageAccess; + }); + throw error; + } } }; From 2b06dfa3f68a69aea81491bae10ca73516d150f6 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 16 Dec 2024 19:47:23 +0530 Subject: [PATCH 14/32] fix: sync state --- web/core/components/pages/editor/editor-body.tsx | 8 ++++---- .../components/pages/editor/header/extra-options.tsx | 2 +- .../components/pages/editor/header/mobile-root.tsx | 10 ++++++++-- web/core/components/pages/editor/header/root.tsx | 3 ++- web/core/components/pages/editor/page-root.tsx | 2 +- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 7b729477075..8cf794d9b4a 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -101,15 +101,15 @@ export const PageEditorBody: React.FC = observer((props) => { const handleServerConnect = useCallback(() => { handleConnectionStatus(false); - }, []); + }, [handleConnectionStatus]); const handleServerError = useCallback(() => { handleConnectionStatus(true); - }, []); + }, [handleConnectionStatus]); const handleServerSynced = useCallback(() => { - setSyncing(true); - }, []); + setSyncing(false); + }, [setSyncing]); const serverHandler: TServerHandler = useMemo( () => ({ diff --git a/web/core/components/pages/editor/header/extra-options.tsx b/web/core/components/pages/editor/header/extra-options.tsx index 50dd1eda239..2eb76659fe6 100644 --- a/web/core/components/pages/editor/header/extra-options.tsx +++ b/web/core/components/pages/editor/header/extra-options.tsx @@ -61,7 +61,7 @@ export const PageExtraOptions: React.FC = observer((props) => { return (
{is_locked && } - {!syncState && } + {syncState && } {archived_at && (
diff --git a/web/core/components/pages/editor/header/mobile-root.tsx b/web/core/components/pages/editor/header/mobile-root.tsx index df93a70e922..0987d17d6fe 100644 --- a/web/core/components/pages/editor/header/mobile-root.tsx +++ b/web/core/components/pages/editor/header/mobile-root.tsx @@ -15,10 +15,11 @@ type Props = { page: IPage; setSidePeekVisible: (sidePeekState: boolean) => void; sidePeekVisible: boolean; + syncState: boolean; }; export const PageEditorMobileHeaderRoot: React.FC = observer((props) => { - const { editorReady, editorRef, handleDuplicatePage, page, setSidePeekVisible, sidePeekVisible } = props; + const { editorReady, editorRef, handleDuplicatePage, page, setSidePeekVisible, sidePeekVisible, syncState } = props; // derived values const { isContentEditable } = page; // page filters @@ -37,7 +38,12 @@ export const PageEditorMobileHeaderRoot: React.FC = observer((props) => { setSidePeekVisible={setSidePeekVisible} />
- +
{editorReady && isContentEditable && editorRef.current && } diff --git a/web/core/components/pages/editor/header/root.tsx b/web/core/components/pages/editor/header/root.tsx index d8001faf000..8c88b83f6ee 100644 --- a/web/core/components/pages/editor/header/root.tsx +++ b/web/core/components/pages/editor/header/root.tsx @@ -17,7 +17,7 @@ type Props = { page: IPage; setSidePeekVisible: (sidePeekState: boolean) => void; sidePeekVisible: boolean; - syncState: boolean | null; + syncState: boolean; }; export const PageEditorHeaderRoot: React.FC = observer((props) => { @@ -65,6 +65,7 @@ export const PageEditorHeaderRoot: React.FC = observer((props) => { page={page} sidePeekVisible={sidePeekVisible} setSidePeekVisible={setSidePeekVisible} + syncState={syncState} />
diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx index 6a300d68891..19c0779e9ff 100644 --- a/web/core/components/pages/editor/page-root.tsx +++ b/web/core/components/pages/editor/page-root.tsx @@ -33,7 +33,7 @@ export const PageRoot = observer((props: TPageRootProps) => { const [editorReady, setEditorReady] = useState(false); const [hasConnectionFailed, setHasConnectionFailed] = useState(false); const [sidePeekVisible, setSidePeekVisible] = useState(window.innerWidth >= 768); - const [syncState, setSyncing] = useState(null); + const [syncState, setSyncing] = useState(true); const [isVersionsOverlayOpen, setIsVersionsOverlayOpen] = useState(false); // refs const editorRef = useRef(null); From 09f2be4d63d9a6f4c7c4e0527552b2d7d7253fed Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 16 Dec 2024 20:02:59 +0530 Subject: [PATCH 15/32] fix: indexeddb sync loader added --- .../editors/document/collaborative-editor.tsx | 11 +++++++---- .../components/editors/document/page-renderer.tsx | 1 - web/core/components/pages/editor/editor-body.tsx | 1 - 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx index 5a7ff1840a2..5accb9133dc 100644 --- a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx +++ b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx @@ -88,10 +88,13 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { if (!editor) return null; - // Wait until we know about IndexedDB status - if (hasIndexedDbEntry === null) return null; - - if (hasServerConnectionFailed || (!hasIndexedDbEntry && !hasServerSynced) || !hasIndexedDbSynced) { + if ( + hasServerConnectionFailed || + (!hasIndexedDbEntry && !hasServerSynced) || + !hasIndexedDbSynced || + !hasIndexedDbEntry + ) { + console.log("syncing indexedDB"); return ; } diff --git a/packages/editor/src/core/components/editors/document/page-renderer.tsx b/packages/editor/src/core/components/editors/document/page-renderer.tsx index 7e190a640ee..15fe8e7bbc6 100644 --- a/packages/editor/src/core/components/editors/document/page-renderer.tsx +++ b/packages/editor/src/core/components/editors/document/page-renderer.tsx @@ -129,7 +129,6 @@ export const PageRenderer = (props: IPageRenderer) => { [editor, cleanup] ); - console.log("rendered"); return ( <>
diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 8cf794d9b4a..41066a57c30 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -221,7 +221,6 @@ export const PageEditorBody: React.FC = observer((props) => { menu: getAIMenu, }} /> - )
Date: Mon, 16 Dec 2024 21:02:52 +0530 Subject: [PATCH 16/32] fix: remove node error fixed --- .../components/editors/document/page-renderer.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/core/components/editors/document/page-renderer.tsx b/packages/editor/src/core/components/editors/document/page-renderer.tsx index 15fe8e7bbc6..f291c8b3a3f 100644 --- a/packages/editor/src/core/components/editors/document/page-renderer.tsx +++ b/packages/editor/src/core/components/editors/document/page-renderer.tsx @@ -139,12 +139,12 @@ export const PageRenderer = (props: IPageRenderer) => { id={id} > - {/* {editor.isEditable && ( */} - {/* <> */} - {/* */} - {/* */} - {/* */} - {/* )} */} + {editor.isEditable && ( +
+ + +
+ )}
{isOpen && linkViewProps && coordinates && ( From a7ddf3dc155902bb55f2ac84382ec97507798773 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 17 Dec 2024 13:46:21 +0530 Subject: [PATCH 17/32] style: page title and checkbox --- .../collaborative-read-only-editor.tsx | 81 ---------------- .../core/components/editors/document/index.ts | 1 - .../editor/src/core/extensions/extensions.tsx | 3 + .../core/extensions/read-only-extensions.tsx | 2 - .../use-read-only-collaborative-editor.ts | 92 ------------------- packages/editor/src/index.ts | 1 - packages/editor/src/styles/editor.css | 12 +-- .../components/pages/editor/editor-body.tsx | 6 +- 8 files changed, 12 insertions(+), 186 deletions(-) delete mode 100644 packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx delete mode 100644 packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts diff --git a/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx deleted file mode 100644 index 89acace7b70..00000000000 --- a/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { forwardRef, MutableRefObject } from "react"; -// components -import { DocumentContentLoader, PageRenderer } from "@/components/editors"; -// constants -import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; -// extensions -import { IssueWidget } from "@/extensions"; -// helpers -import { getEditorClassNames } from "@/helpers/common"; -// hooks -import { useReadOnlyCollaborativeEditor } from "@/hooks/use-read-only-collaborative-editor"; -// types -import { EditorReadOnlyRefApi, ICollaborativeDocumentReadOnlyEditor } from "@/types"; - -const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOnlyEditor) => { - const { - containerClassName, - disabledExtensions, - displayConfig = DEFAULT_DISPLAY_CONFIG, - editorClassName = "", - embedHandler, - fileHandler, - forwardedRef, - handleEditorReady, - id, - mentionHandler, - realtimeConfig, - serverHandler, - user, - } = props; - const extensions = []; - if (embedHandler?.issue) { - extensions.push( - IssueWidget({ - widgetCallback: embedHandler.issue.widgetCallback, - }) - ); - } - - const { editor, hasServerConnectionFailed, hasServerSynced } = useReadOnlyCollaborativeEditor({ - disabledExtensions, - editorClassName, - extensions, - fileHandler, - forwardedRef, - handleEditorReady, - id, - mentionHandler, - realtimeConfig, - serverHandler, - user, - }); - - const editorContainerClassName = getEditorClassNames({ - containerClassName, - }); - - if (!editor) return null; - - if (!hasServerSynced && !hasServerConnectionFailed) return ; - - return ( - - ); -}; - -const CollaborativeDocumentReadOnlyEditorWithRef = forwardRef< - EditorReadOnlyRefApi, - ICollaborativeDocumentReadOnlyEditor ->((props, ref) => ( - } /> -)); - -CollaborativeDocumentReadOnlyEditorWithRef.displayName = "CollaborativeDocumentReadOnlyEditorWithRef"; - -export { CollaborativeDocumentReadOnlyEditorWithRef }; diff --git a/packages/editor/src/core/components/editors/document/index.ts b/packages/editor/src/core/components/editors/document/index.ts index 514b620e3a2..571cb7e9a1f 100644 --- a/packages/editor/src/core/components/editors/document/index.ts +++ b/packages/editor/src/core/components/editors/document/index.ts @@ -1,5 +1,4 @@ export * from "./collaborative-editor"; -export * from "./collaborative-read-only-editor"; export * from "./loader"; export * from "./page-renderer"; export * from "./read-only-editor"; diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 4f166ea2a8e..706c56143d1 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -138,6 +138,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { CustomCodeInlineExtension, Markdown.configure({ html: true, + transformCopiedText: true, transformPastedText: true, breaks: true, }), @@ -152,6 +153,8 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { }), Placeholder.configure({ placeholder: ({ editor, node }) => { + if (!editor.isEditable) return; + if (node.type.name === "heading") return `Heading ${node.attrs.level}`; if (editor.storage.imageComponent.uploadInProgress) return ""; diff --git a/packages/editor/src/core/extensions/read-only-extensions.tsx b/packages/editor/src/core/extensions/read-only-extensions.tsx index 4debda019c1..7e9b055210b 100644 --- a/packages/editor/src/core/extensions/read-only-extensions.tsx +++ b/packages/editor/src/core/extensions/read-only-extensions.tsx @@ -20,7 +20,6 @@ import { TableRow, Table, CustomMention, - HeadingListExtension, CustomReadOnlyImageExtension, CustomTextAlignExtension, CustomCalloutReadOnlyExtension, @@ -139,7 +138,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { }), CharacterCount, CustomColorExtension, - HeadingListExtension, CustomTextAlignExtension, CustomCalloutReadOnlyExtension, ...CoreReadOnlyEditorAdditionalExtensions({ diff --git a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts deleted file mode 100644 index 01ca19b8148..00000000000 --- a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { HocuspocusProvider } from "@hocuspocus/provider"; -import Collaboration from "@tiptap/extension-collaboration"; -import { IndexeddbPersistence } from "y-indexeddb"; -// extensions -import { HeadingListExtension } from "@/extensions"; -// hooks -import { useReadOnlyEditor } from "@/hooks/use-read-only-editor"; -// types -import { TReadOnlyCollaborativeEditorProps } from "@/types"; - -export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEditorProps) => { - const { - disabledExtensions, - editorClassName, - editorProps = {}, - extensions, - fileHandler, - forwardedRef, - handleEditorReady, - id, - mentionHandler, - realtimeConfig, - serverHandler, - user, - } = props; - // states - const [hasServerConnectionFailed, setHasServerConnectionFailed] = useState(false); - const [hasServerSynced, setHasServerSynced] = useState(false); - // initialize Hocuspocus provider - const provider = useMemo( - () => - new HocuspocusProvider({ - name: id, - url: realtimeConfig.url, - token: JSON.stringify(user), - parameters: realtimeConfig.queryParams, - onAuthenticationFailed: () => { - serverHandler?.onServerError?.(); - setHasServerConnectionFailed(true); - }, - onConnect: () => serverHandler?.onConnect?.(), - onClose: (data) => { - if (data.event.code === 1006) { - serverHandler?.onServerError?.(); - setHasServerConnectionFailed(true); - } - }, - onSynced: () => setHasServerSynced(true), - }), - [id, realtimeConfig, serverHandler, user] - ); - - // indexed db integration for offline support - const localProvider = useMemo( - () => (id ? new IndexeddbPersistence(id, provider.document) : undefined), - [id, provider] - ); - - // destroy and disconnect connection on unmount - useEffect( - () => () => { - provider.destroy(); - localProvider?.destroy(); - }, - [provider, localProvider] - ); - - const editor = useReadOnlyEditor({ - disabledExtensions, - editorProps, - editorClassName, - extensions: [ - ...(extensions ?? []), - HeadingListExtension, - Collaboration.configure({ - document: provider.document, - }), - ], - fileHandler, - forwardedRef, - handleEditorReady, - mentionHandler, - provider, - }); - - return { - editor, - hasServerConnectionFailed, - hasServerSynced, - }; -}; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index ed7d9134698..9dd0db267f2 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -9,7 +9,6 @@ import "./styles/drag-drop.css"; // editors export { CollaborativeDocumentEditorWithRef, - CollaborativeDocumentReadOnlyEditorWithRef, DocumentReadOnlyEditorWithRef, LiteTextEditorWithRef, LiteTextReadOnlyEditorWithRef, diff --git a/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css index db60c7cf5f5..e234f87cf86 100644 --- a/packages/editor/src/styles/editor.css +++ b/packages/editor/src/styles/editor.css @@ -111,8 +111,12 @@ ul[data-type="taskList"] li > label input[type="checkbox"] { transform: scale(1.05); } -ul[data-type="taskList"] li > label input[type="checkbox"]:hover { - background-color: rgba(var(--color-background-80)) !important; +.ProseMirror[contenteditable="true"] input[type="checkbox"]:hover { + background-color: rgba(var(--color-background-80)); +} + +.ProseMirror[contenteditable="false"] input[type="checkbox"] { + pointer-events: none; } ul[data-type="taskList"] li > label input[type="checkbox"][checked] { @@ -151,10 +155,6 @@ ul[data-type="taskList"] li > label input[type="checkbox"] { margin-right: 0.2rem; margin-top: 0.15rem; - &:hover { - background-color: rgb(var(--color-background-80)); - } - &:active { background-color: rgb(var(--color-background-90)); } diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 41066a57c30..876a3db7f27 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -166,13 +166,13 @@ export const PageEditorBody: React.FC = observer((props) => { {!isFullWidth && }
-
-
+
+
Date: Tue, 17 Dec 2024 14:27:25 +0530 Subject: [PATCH 18/32] chore: removing the syncing logic --- .../editors/document/collaborative-editor.tsx | 65 ++++++------------- .../core/hooks/use-collaborative-editor.ts | 12 +--- .../editor/src/core/types/collaboration.ts | 1 - .../components/icons/syncing-component.tsx | 18 ----- .../components/pages/editor/editor-body.tsx | 10 +-- .../pages/editor/header/extra-options.tsx | 4 +- .../pages/editor/header/mobile-root.tsx | 10 +-- .../components/pages/editor/header/root.tsx | 11 +--- .../components/pages/editor/page-root.tsx | 3 - 9 files changed, 28 insertions(+), 106 deletions(-) delete mode 100644 web/core/components/icons/syncing-component.tsx diff --git a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx index 5accb9133dc..44c18c2f680 100644 --- a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx +++ b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; // components import { DocumentContentLoader, PageRenderer } from "@/components/editors"; // constants @@ -44,25 +44,24 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { } // use document editor - const { editor, hasServerConnectionFailed, hasServerSynced, localProvider, hasIndexedDbSynced } = - useCollaborativeEditor({ - disabledExtensions, - editable, - editorClassName, - embedHandler, - extensions, - fileHandler, - forwardedRef, - handleEditorReady, - id, - mentionHandler, - onTransaction, - placeholder, - realtimeConfig, - serverHandler, - tabIndex, - user, - }); + const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({ + disabledExtensions, + editable, + editorClassName, + embedHandler, + extensions, + fileHandler, + forwardedRef, + handleEditorReady, + id, + mentionHandler, + onTransaction, + placeholder, + realtimeConfig, + serverHandler, + tabIndex, + user, + }); const editorContainerClassNames = getEditorClassNames({ noBorder: true, @@ -70,33 +69,9 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { containerClassName, }); - const [hasIndexedDbEntry, setHasIndexedDbEntry] = useState(null); - - useEffect(() => { - async function documentIndexedDbEntry(dbName: string) { - try { - const databases = await indexedDB.databases(); - const hasEntry = databases.some((db) => db.name === dbName); - setHasIndexedDbEntry(hasEntry); - } catch (error) { - console.error("Error checking database existence:", error); - return false; - } - } - documentIndexedDbEntry(id); - }, [id, localProvider]); - if (!editor) return null; - if ( - hasServerConnectionFailed || - (!hasIndexedDbEntry && !hasServerSynced) || - !hasIndexedDbSynced || - !hasIndexedDbEntry - ) { - console.log("syncing indexedDB"); - return ; - } + if (!hasServerSynced && !hasServerConnectionFailed) return ; return ( { // states const [hasServerConnectionFailed, setHasServerConnectionFailed] = useState(false); const [hasServerSynced, setHasServerSynced] = useState(false); - const [hasIndexedDbSynced, setHasIndexedDbSynced] = useState(false); // initialize Hocuspocus provider const provider = useMemo( () => @@ -55,10 +54,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { setHasServerConnectionFailed(true); } }, - onSynced: () => { - serverHandler?.onServerSync?.(); - setHasServerSynced(true); - }, + onSynced: () => setHasServerSynced(true), }), [id, realtimeConfig, serverHandler, user] ); @@ -68,10 +64,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { [id, provider] ); - localProvider?.on("synced", () => { - setHasIndexedDbSynced(true); - }); - // destroy and disconnect all providers connection on unmount useEffect( () => () => { @@ -119,7 +111,5 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { editor, hasServerConnectionFailed, hasServerSynced, - hasIndexedDbSynced, - localProvider, }; }; diff --git a/packages/editor/src/core/types/collaboration.ts b/packages/editor/src/core/types/collaboration.ts index 0c0079b00b3..b95f7283eb5 100644 --- a/packages/editor/src/core/types/collaboration.ts +++ b/packages/editor/src/core/types/collaboration.ts @@ -17,7 +17,6 @@ import { export type TServerHandler = { onConnect?: () => void; onServerError?: () => void; - onServerSync?: () => void; }; type TCollaborativeEditorHookProps = { diff --git a/web/core/components/icons/syncing-component.tsx b/web/core/components/icons/syncing-component.tsx deleted file mode 100644 index 1397c9cf23c..00000000000 --- a/web/core/components/icons/syncing-component.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { RefreshCcw } from "lucide-react"; -import { Tooltip } from "@plane/ui"; - -export const SyncingComponent = (props: { toolTipContent?: string }) => { - const { toolTipContent } = props; - const lockedComponent = ( -
- - Syncing -
- ); - - return ( - <> - {toolTipContent ? {lockedComponent} : <>{lockedComponent}} - - ); -}; diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 876a3db7f27..adbfd63680d 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -44,11 +44,10 @@ type Props = { handleEditorReady: Dispatch>; page: IPage; sidePeekVisible: boolean; - setSyncing: (value: boolean) => void; }; export const PageEditorBody: React.FC = observer((props) => { - const { editorRef, handleConnectionStatus, handleEditorReady, page, sidePeekVisible, setSyncing } = props; + const { editorRef, handleConnectionStatus, handleEditorReady, page, sidePeekVisible } = props; // router const { workspaceSlug, projectId } = useParams(); // store hooks @@ -107,17 +106,12 @@ export const PageEditorBody: React.FC = observer((props) => { handleConnectionStatus(true); }, [handleConnectionStatus]); - const handleServerSynced = useCallback(() => { - setSyncing(false); - }, [setSyncing]); - const serverHandler: TServerHandler = useMemo( () => ({ onConnect: handleServerConnect, onServerError: handleServerError, - onServerSync: handleServerSynced, }), - [handleServerConnect, handleServerError, handleServerSynced] + [handleServerConnect, handleServerError] ); const realtimeConfig: TRealtimeConfig | undefined = useMemo(() => { diff --git a/web/core/components/pages/editor/header/extra-options.tsx b/web/core/components/pages/editor/header/extra-options.tsx index 2eb76659fe6..9ecc2998f92 100644 --- a/web/core/components/pages/editor/header/extra-options.tsx +++ b/web/core/components/pages/editor/header/extra-options.tsx @@ -20,11 +20,10 @@ type Props = { editorRef: React.RefObject; handleDuplicatePage: () => void; page: IPage; - syncState: boolean | null; }; export const PageExtraOptions: React.FC = observer((props) => { - const { editorRef, syncState, handleDuplicatePage, page } = props; + const { editorRef, handleDuplicatePage, page } = props; // derived values const { archived_at, @@ -61,7 +60,6 @@ export const PageExtraOptions: React.FC = observer((props) => { return (
{is_locked && } - {syncState && } {archived_at && (
diff --git a/web/core/components/pages/editor/header/mobile-root.tsx b/web/core/components/pages/editor/header/mobile-root.tsx index 0987d17d6fe..df93a70e922 100644 --- a/web/core/components/pages/editor/header/mobile-root.tsx +++ b/web/core/components/pages/editor/header/mobile-root.tsx @@ -15,11 +15,10 @@ type Props = { page: IPage; setSidePeekVisible: (sidePeekState: boolean) => void; sidePeekVisible: boolean; - syncState: boolean; }; export const PageEditorMobileHeaderRoot: React.FC = observer((props) => { - const { editorReady, editorRef, handleDuplicatePage, page, setSidePeekVisible, sidePeekVisible, syncState } = props; + const { editorReady, editorRef, handleDuplicatePage, page, setSidePeekVisible, sidePeekVisible } = props; // derived values const { isContentEditable } = page; // page filters @@ -38,12 +37,7 @@ export const PageEditorMobileHeaderRoot: React.FC = observer((props) => { setSidePeekVisible={setSidePeekVisible} />
- +
{editorReady && isContentEditable && editorRef.current && } diff --git a/web/core/components/pages/editor/header/root.tsx b/web/core/components/pages/editor/header/root.tsx index 8c88b83f6ee..41933feb725 100644 --- a/web/core/components/pages/editor/header/root.tsx +++ b/web/core/components/pages/editor/header/root.tsx @@ -17,11 +17,10 @@ type Props = { page: IPage; setSidePeekVisible: (sidePeekState: boolean) => void; sidePeekVisible: boolean; - syncState: boolean; }; export const PageEditorHeaderRoot: React.FC = observer((props) => { - const { editorReady, editorRef, setSidePeekVisible, sidePeekVisible, handleDuplicatePage, page, syncState } = props; + const { editorReady, editorRef, setSidePeekVisible, sidePeekVisible, handleDuplicatePage, page } = props; // derived values const { isContentEditable } = page; // page filters @@ -50,12 +49,7 @@ export const PageEditorHeaderRoot: React.FC = observer((props) => { )} {editorReady && isContentEditable && editorRef.current && } - +
= observer((props) => { page={page} sidePeekVisible={sidePeekVisible} setSidePeekVisible={setSidePeekVisible} - syncState={syncState} />
diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx index 19c0779e9ff..6f4418fe4d1 100644 --- a/web/core/components/pages/editor/page-root.tsx +++ b/web/core/components/pages/editor/page-root.tsx @@ -33,7 +33,6 @@ export const PageRoot = observer((props: TPageRootProps) => { const [editorReady, setEditorReady] = useState(false); const [hasConnectionFailed, setHasConnectionFailed] = useState(false); const [sidePeekVisible, setSidePeekVisible] = useState(window.innerWidth >= 768); - const [syncState, setSyncing] = useState(true); const [isVersionsOverlayOpen, setIsVersionsOverlayOpen] = useState(false); // refs const editorRef = useRef(null); @@ -136,7 +135,6 @@ export const PageRoot = observer((props: TPageRootProps) => { page={page} setSidePeekVisible={(state) => setSidePeekVisible(state)} sidePeekVisible={sidePeekVisible} - syncState={syncState} /> { handleEditorReady={setEditorReady} page={page} sidePeekVisible={sidePeekVisible} - setSyncing={setSyncing} /> ); From 2fc1ac74420ebf4f9d5c34c1ff016a56c72fe56a Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 17 Dec 2024 15:43:30 +0530 Subject: [PATCH 19/32] revert: is editable check removed in display message --- .../extensions/custom-image/components/image-uploader.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index ef6c28fa65c..d1af3831588 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -131,11 +131,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { return "Drop image here"; } - if (!editor.isEditable) { - return "Viewing Mode: Image Upload Disabled"; - } else { - return "Add an image"; - } + return "Add an image"; }, [draggedInside, failedToLoadImage, isImageBeingUploaded]); return ( From 25abb9ea8c11a7c97d7d4995ee3af8d643e6fb3d Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 17 Dec 2024 16:11:27 +0530 Subject: [PATCH 20/32] fix: editable field optional --- packages/editor/src/core/hooks/use-editor.ts | 4 ++-- web/core/components/pages/editor/header/extra-options.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 05e48e93acf..a366adcf499 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -27,7 +27,7 @@ import type { } from "@/types"; export interface CustomEditorProps { - editable: boolean; + editable?: boolean; editorClassName: string; editorProps?: EditorProps; enableHistory: boolean; @@ -56,7 +56,7 @@ export interface CustomEditorProps { export const useEditor = (props: CustomEditorProps) => { const { disabledExtensions, - editable, + editable = true, editorClassName, editorProps = {}, enableHistory, diff --git a/web/core/components/pages/editor/header/extra-options.tsx b/web/core/components/pages/editor/header/extra-options.tsx index 9ecc2998f92..ed615d23af9 100644 --- a/web/core/components/pages/editor/header/extra-options.tsx +++ b/web/core/components/pages/editor/header/extra-options.tsx @@ -85,8 +85,8 @@ export const PageExtraOptions: React.FC = observer((props) => { iconClassName="text-custom-text-100" /> )} - - + +
); }); From 61894116b6ea00715465b767e3c8fecb10338e0f Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 17 Dec 2024 17:48:39 +0530 Subject: [PATCH 21/32] fix: editable removed as optional prop --- packages/editor/src/core/components/editors/editor-wrapper.tsx | 3 +-- packages/editor/src/core/extensions/extensions.tsx | 2 +- packages/editor/src/core/hooks/use-editor.ts | 2 +- packages/editor/src/core/types/editor.ts | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/core/components/editors/editor-wrapper.tsx b/packages/editor/src/core/components/editors/editor-wrapper.tsx index 7090569625f..50898b097af 100644 --- a/packages/editor/src/core/components/editors/editor-wrapper.tsx +++ b/packages/editor/src/core/components/editors/editor-wrapper.tsx @@ -21,7 +21,6 @@ export const EditorWrapper: React.FC = (props) => { containerClassName, disabledExtensions, displayConfig = DEFAULT_DISPLAY_CONFIG, - editable, editorClassName = "", extensions, id, @@ -39,7 +38,7 @@ export const EditorWrapper: React.FC = (props) => { } = props; const editor = useEditor({ - editable, + editable: true, disabledExtensions, editorClassName, enableHistory: true, diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 706c56143d1..71126c57664 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -47,7 +47,7 @@ type TArguments = { }; placeholder?: string | ((isFocused: boolean, value: string) => string); tabIndex?: number; - editable?: boolean; + editable: boolean; }; export const CoreEditorExtensions = (args: TArguments): Extensions => { diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index a366adcf499..dd17373f76e 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -27,7 +27,7 @@ import type { } from "@/types"; export interface CustomEditorProps { - editable?: boolean; + editable: boolean; editorClassName: string; editorProps?: EditorProps; enableHistory: boolean; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 25d22e439d1..404a3021d12 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -106,7 +106,6 @@ export interface EditorRefApi extends EditorReadOnlyRefApi { // editor props export interface IEditorProps { - editable: boolean; containerClassName?: string; displayConfig?: TDisplayConfig; disabledExtensions: TExtensions[]; @@ -139,6 +138,7 @@ export interface IRichTextEditor extends IEditorProps { export interface ICollaborativeDocumentEditor extends Omit { + editable: boolean; aiHandler?: TAIHandler; embedHandler: TEmbedConfig; handleEditorReady?: (value: boolean) => void; From f24d8a11ca35067a7cea5ceffd0e564cce695022 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 17 Dec 2024 17:51:04 +0530 Subject: [PATCH 22/32] fix: extra options import fix --- web/core/components/pages/editor/header/extra-options.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/core/components/pages/editor/header/extra-options.tsx b/web/core/components/pages/editor/header/extra-options.tsx index ed615d23af9..924e28d05e4 100644 --- a/web/core/components/pages/editor/header/extra-options.tsx +++ b/web/core/components/pages/editor/header/extra-options.tsx @@ -7,7 +7,6 @@ import { EditorRefApi } from "@plane/editor"; import { ArchiveIcon, FavoriteStar, setToast, TOAST_TYPE, Tooltip } from "@plane/ui"; // components import { LockedComponent } from "@/components/icons/locked-component"; -import { SyncingComponent } from "@/components/icons/syncing-component"; import { PageInfoPopover, PageOptionsDropdown } from "@/components/pages"; // helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; From 6fe2eef63d5c78715a74015556baa195b8b2c6cf Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 17 Dec 2024 18:06:17 +0530 Subject: [PATCH 23/32] fix: remove readonly stuff --- web/core/components/pages/dropdowns/actions.tsx | 4 ++-- web/core/components/pages/editor/header/extra-options.tsx | 2 +- web/core/components/pages/editor/header/mobile-root.tsx | 4 ++-- .../components/pages/editor/header/options-dropdown.tsx | 2 +- web/core/components/pages/editor/header/root.tsx | 5 ++--- web/core/components/pages/editor/page-root.tsx | 2 +- web/core/hooks/use-collaborative-page-actions.tsx | 6 ++---- web/core/hooks/use-page-operations.ts | 4 ++-- 8 files changed, 13 insertions(+), 16 deletions(-) diff --git a/web/core/components/pages/dropdowns/actions.tsx b/web/core/components/pages/dropdowns/actions.tsx index f979d7f43bd..f49292c1685 100644 --- a/web/core/components/pages/dropdowns/actions.tsx +++ b/web/core/components/pages/dropdowns/actions.tsx @@ -15,7 +15,7 @@ import { Trash2, } from "lucide-react"; // plane editor -import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; +import { EditorRefApi } from "@plane/editor"; // plane ui import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui"; // components @@ -46,7 +46,7 @@ export type TPageActions = | "move"; type Props = { - editorRef?: EditorRefApi | EditorReadOnlyRefApi | null; + editorRef?: EditorRefApi | null; extraOptions?: (TContextMenuItem & { key: TPageActions })[]; optionsOrder: TPageActions[]; page: IPage; diff --git a/web/core/components/pages/editor/header/extra-options.tsx b/web/core/components/pages/editor/header/extra-options.tsx index 42676f275ca..e0064e3683e 100644 --- a/web/core/components/pages/editor/header/extra-options.tsx +++ b/web/core/components/pages/editor/header/extra-options.tsx @@ -16,7 +16,7 @@ import useOnlineStatus from "@/hooks/use-online-status"; import { IPage } from "@/store/pages/page"; type Props = { - editorRef: EditorRefApi | EditorReadOnlyRefApi | null; + editorRef: EditorRefApi | null; page: IPage; }; diff --git a/web/core/components/pages/editor/header/mobile-root.tsx b/web/core/components/pages/editor/header/mobile-root.tsx index 003237d915a..57c9d237817 100644 --- a/web/core/components/pages/editor/header/mobile-root.tsx +++ b/web/core/components/pages/editor/header/mobile-root.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react"; -import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; +import { EditorRefApi } from "@plane/editor"; // components import { Header, EHeaderVariant } from "@plane/ui"; import { PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages"; @@ -9,7 +9,7 @@ import { usePageFilters } from "@/hooks/use-page-filters"; import { IPage } from "@/store/pages/page"; type Props = { - editorRef: EditorRefApi | EditorReadOnlyRefApi | null; + editorRef: EditorRefApi | null; page: IPage; setSidePeekVisible: (sidePeekState: boolean) => void; sidePeekVisible: boolean; diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 89e9f315cee..30149544436 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -19,7 +19,7 @@ import { useQueryParams } from "@/hooks/use-query-params"; import { IPage } from "@/store/pages/page"; type Props = { - editorRef: EditorRefApi | EditorReadOnlyRefApi | null; + editorRef: EditorRefApi | null; page: IPage; }; diff --git a/web/core/components/pages/editor/header/root.tsx b/web/core/components/pages/editor/header/root.tsx index ba11eb87d13..b731d86d21e 100644 --- a/web/core/components/pages/editor/header/root.tsx +++ b/web/core/components/pages/editor/header/root.tsx @@ -19,14 +19,13 @@ type Props = { }; export const PageEditorHeaderRoot: React.FC = observer((props) => { - const { editorReady, editorRef, page, readOnlyEditorReady, readOnlyEditorRef, setSidePeekVisible, sidePeekVisible } = - props; + const { editorReady, editorRef, page, setSidePeekVisible, sidePeekVisible } = props; // derived values const { isContentEditable } = page; // page filters const { isFullWidth } = usePageFilters(); // derived values - const resolvedEditorRef = isContentEditable ? editorRef.current : readOnlyEditorRef.current; + const resolvedEditorRef = editorRef.current; if (!resolvedEditorRef) return null; diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx index 3a8c3ebd2be..b4b4f0e4960 100644 --- a/web/core/components/pages/editor/page-root.tsx +++ b/web/core/components/pages/editor/page-root.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; import { useSearchParams } from "next/navigation"; // editor -import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; +import { EditorRefApi } from "@plane/editor"; // components import { PageEditorHeaderRoot, PageEditorBody, PageVersionsOverlay, PagesVersionEditor } from "@/components/pages"; // hooks diff --git a/web/core/hooks/use-collaborative-page-actions.tsx b/web/core/hooks/use-collaborative-page-actions.tsx index fc1dde9f734..64f1af27f87 100644 --- a/web/core/hooks/use-collaborative-page-actions.tsx +++ b/web/core/hooks/use-collaborative-page-actions.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useMemo } from "react"; -import { EditorReadOnlyRefApi, EditorRefApi, TDocumentEventsServer } from "@plane/editor"; +import { EditorRefApi, TDocumentEventsServer } from "@plane/editor"; import { DocumentCollaborativeEvents, TDocumentEventsClient, getServerEventName } from "@plane/editor/lib"; import { TOAST_TYPE, setToast } from "@plane/ui"; import { IPage } from "@/store/pages/page"; @@ -15,7 +15,7 @@ type CollaborativeActionEvent = | { type: "receivedMessageFromServer"; message: TDocumentEventsClient }; type Props = { - editorRef?: EditorRefApi | EditorReadOnlyRefApi | null; + editorRef?: EditorRefApi | null; page: IPage; }; @@ -82,8 +82,6 @@ export const useCollaborativePageActions = (props: Props) => { ); useEffect(() => { - const realTimeStatelessMessageListener = editorRef?.listenToRealTimeUpdate(); - const realTimeStatelessMessageListener = editorRef?.listenToRealTimeUpdate(); console.log(realTimeStatelessMessageListener); const handleStatelessMessage = (message: { payload: TDocumentEventsClient }) => { diff --git a/web/core/hooks/use-page-operations.ts b/web/core/hooks/use-page-operations.ts index 7990252a6b5..949b36b65c2 100644 --- a/web/core/hooks/use-page-operations.ts +++ b/web/core/hooks/use-page-operations.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { useParams } from "next/navigation"; // plane editor -import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; +import { EditorRefApi } from "@plane/editor"; // plane ui import { setToast, TOAST_TYPE } from "@plane/ui"; // helpers @@ -22,7 +22,7 @@ export type TPageOperations = { }; type Props = { - editorRef?: EditorRefApi | EditorReadOnlyRefApi | null; + editorRef?: EditorRefApi | null; page: IPage; }; From 43db52882e34e2608241df627ef24294e68a8cf9 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 17 Dec 2024 19:42:47 +0530 Subject: [PATCH 24/32] fix: added toggle access --- web/core/components/pages/editor/header/options-dropdown.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 30149544436..6e426cd858c 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -105,6 +105,7 @@ export const PageOptionsDropdown: React.FC = observer((props) => { "copy-markdown", "copy-link", "toggle-lock", + "toggle-access", "make-a-copy", "move", "archive-restore", From 3992bd24c1b3ca24569723b8d2ce0d8a559a1ce4 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Wed, 18 Dec 2024 13:59:19 +0530 Subject: [PATCH 25/32] chore: add access change sync --- .../pages/(detail)/[pageId]/page.tsx | 4 ++-- web/core/hooks/use-page-operations.ts | 4 ++-- web/core/store/pages/page.ts | 20 ++++++------------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx index 4d3f395ea00..3db12d140da 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx @@ -22,7 +22,7 @@ const PageDetailsPage = observer(() => { // store hooks const { getPageById } = useProjectPages(); const page = usePage(pageId?.toString() ?? ""); - const { id, name } = page; + const { canCurrentUserAccessPage, id, name } = page; // fetch page details const { error: pageDetailsError } = useSWR( @@ -44,7 +44,7 @@ const PageDetailsPage = observer(() => {
); - if (pageDetailsError) + if (pageDetailsError || !canCurrentUserAccessPage) return (

Page not found

diff --git a/web/core/hooks/use-page-operations.ts b/web/core/hooks/use-page-operations.ts index 949b36b65c2..5e9620a9580 100644 --- a/web/core/hooks/use-page-operations.ts +++ b/web/core/hooks/use-page-operations.ts @@ -84,8 +84,8 @@ export const usePageOperations = ( toggleAccess: async () => { const changedPageType = access === 0 ? "private" : "public"; try { - if (access === 0) await makePrivate(); - else await makePublic(); + if (access === 0) await executeCollaborativeAction({ type: "sendMessageToServer", message: "make-private" }); + else await executeCollaborativeAction({ type: "sendMessageToServer", message: "make-public" }); setToast({ type: TOAST_TYPE.SUCCESS, diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts index 383e6d8ef19..e4b3b747457 100644 --- a/web/core/store/pages/page.ts +++ b/web/core/store/pages/page.ts @@ -15,8 +15,8 @@ export interface IPage extends TPage { isSubmitting: TNameDescriptionLoader; // computed asJSON: TPage | undefined; - isCurrentUserOwner: boolean; // it will give the user is the owner of the page or not - canCurrentUserEditPage: boolean; // it will give the user permission to read the page or write the page + isCurrentUserOwner: boolean; + canCurrentUserAccessPage: boolean; canCurrentUserDuplicatePage: boolean; canCurrentUserLockPage: boolean; canCurrentUserChangeAccess: boolean; @@ -128,7 +128,7 @@ export class Page implements IPage { // computed asJSON: computed, isCurrentUserOwner: computed, - canCurrentUserEditPage: computed, + canCurrentUserAccessPage: computed, canCurrentUserDuplicatePage: computed, canCurrentUserLockPage: computed, canCurrentUserChangeAccess: computed, @@ -213,19 +213,11 @@ export class Page implements IPage { } /** - * @description returns true if the current logged in user can edit the page + * @description returns true if the current logged in user can access the page */ - get canCurrentUserEditPage() { - const { workspaceSlug, projectId } = this.store.router; - const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId( - workspaceSlug?.toString() || "", - projectId?.toString() || "" - ); + get canCurrentUserAccessPage() { const isPagePublic = this.access === EPageAccess.PUBLIC; - return ( - (isPagePublic && !!currentUserProjectRole && currentUserProjectRole >= EUserPermissions.MEMBER) || - (!isPagePublic && this.isCurrentUserOwner) - ); + return isPagePublic || this.isCurrentUserOwner; } /** From 63c59df8d35b4e44efc47ecc5349fd8a3a19c69a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 19 Dec 2024 18:55:23 +0530 Subject: [PATCH 26/32] fix: full width toggle --- .../components/pages/dropdowns/actions.tsx | 40 ++++++------ .../pages/editor/header/options-dropdown.tsx | 1 - web/core/hooks/use-page-filters.ts | 63 +++++++++++++------ web/core/hooks/use-page-operations.ts | 15 +---- 4 files changed, 64 insertions(+), 55 deletions(-) diff --git a/web/core/components/pages/dropdowns/actions.tsx b/web/core/components/pages/dropdowns/actions.tsx index f49292c1685..1fe0c240e25 100644 --- a/web/core/components/pages/dropdowns/actions.tsx +++ b/web/core/components/pages/dropdowns/actions.tsx @@ -76,8 +76,8 @@ export const PageActions: React.FC = observer((props) => { canCurrentUserMovePage, } = page; // menu items - const MENU_ITEMS: (TContextMenuItem & { key: TPageActions })[] = useMemo( - () => [ + const MENU_ITEMS: (TContextMenuItem & { key: TPageActions })[] = useMemo(() => { + const menuItems: (TContextMenuItem & { key: TPageActions })[] = [ { key: "toggle-lock", action: pageOperations.toggleLock, @@ -127,7 +127,6 @@ export const PageActions: React.FC = observer((props) => { icon: Trash2, shouldRender: canCurrentUserDeletePage && !!archived_at, }, - { key: "move", action: () => setMovePageModal(true), @@ -135,23 +134,24 @@ export const PageActions: React.FC = observer((props) => { icon: FileOutput, shouldRender: canCurrentUserMovePage, }, - ], - [ - access, - archived_at, - is_locked, - canCurrentUserArchivePage, - canCurrentUserChangeAccess, - canCurrentUserDeletePage, - canCurrentUserDuplicatePage, - canCurrentUserLockPage, - canCurrentUserMovePage, - pageOperations, - ] - ); - if (extraOptions) { - MENU_ITEMS.push(...extraOptions); - } + ]; + if (extraOptions) { + menuItems.push(...extraOptions); + } + return menuItems; + }, [ + access, + archived_at, + extraOptions, + is_locked, + canCurrentUserArchivePage, + canCurrentUserChangeAccess, + canCurrentUserDeletePage, + canCurrentUserDuplicatePage, + canCurrentUserLockPage, + canCurrentUserMovePage, + pageOperations, + ]); // arrange options const arrangedOptions = useMemo( () => diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 6e426cd858c..e2acf1ccac4 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -107,7 +107,6 @@ export const PageOptionsDropdown: React.FC = observer((props) => { "toggle-lock", "toggle-access", "make-a-copy", - "move", "archive-restore", "delete", "version-history", diff --git a/web/core/hooks/use-page-filters.ts b/web/core/hooks/use-page-filters.ts index 309cb5f462e..e7698a93405 100644 --- a/web/core/hooks/use-page-filters.ts +++ b/web/core/hooks/use-page-filters.ts @@ -1,3 +1,4 @@ +import { useCallback, useMemo } from "react"; // plane editor import { TEditorFontSize, TEditorFontStyle } from "@plane/editor"; // hooks @@ -22,39 +23,61 @@ export const usePageFilters = () => { DEFAULT_PERSONALIZATION_VALUES ); // stored values - const isFullWidth = !!pagesConfig?.full_width; - const fontSize = pagesConfig?.font_size ?? DEFAULT_PERSONALIZATION_VALUES.font_size; - const fontStyle = pagesConfig?.font_style ?? DEFAULT_PERSONALIZATION_VALUES.font_style; + const isFullWidth = useMemo(() => !!pagesConfig?.full_width, [pagesConfig?.full_width]); + const fontSize = useMemo( + () => pagesConfig?.font_size ?? DEFAULT_PERSONALIZATION_VALUES.font_size, + [pagesConfig?.font_size] + ); + const fontStyle = useMemo( + () => pagesConfig?.font_style ?? DEFAULT_PERSONALIZATION_VALUES.font_style, + [pagesConfig?.font_style] + ); // update action - const handleUpdateConfig = (payload: Partial) => - setPagesConfig({ - ...(pagesConfig ?? DEFAULT_PERSONALIZATION_VALUES), - ...payload, - }); + const handleUpdateConfig = useCallback( + (payload: Partial) => { + setPagesConfig({ + ...(pagesConfig ?? DEFAULT_PERSONALIZATION_VALUES), + ...payload, + }); + }, + [pagesConfig, setPagesConfig] + ); /** * @description action to update full_width value * @param {boolean} value */ - const handleFullWidth = (value: boolean) => - handleUpdateConfig({ - full_width: value, - }); + const handleFullWidth = useCallback( + (value: boolean) => { + handleUpdateConfig({ + full_width: value, + }); + }, + [handleUpdateConfig] + ); /** * @description action to update font_size value * @param {TEditorFontSize} value */ - const handleFontSize = (value: TEditorFontSize) => - handleUpdateConfig({ - font_size: value, - }); + const handleFontSize = useCallback( + (value: TEditorFontSize) => { + handleUpdateConfig({ + font_size: value, + }); + }, + [handleUpdateConfig] + ); /** * @description action to update font_size value * @param {TEditorFontSize} value */ - const handleFontStyle = (value: TEditorFontStyle) => - handleUpdateConfig({ - font_style: value, - }); + const handleFontStyle = useCallback( + (value: TEditorFontStyle) => { + handleUpdateConfig({ + font_style: value, + }); + }, + [handleUpdateConfig] + ); return { fontSize, diff --git a/web/core/hooks/use-page-operations.ts b/web/core/hooks/use-page-operations.ts index 5e9620a9580..05444c10ea9 100644 --- a/web/core/hooks/use-page-operations.ts +++ b/web/core/hooks/use-page-operations.ts @@ -35,18 +35,7 @@ export const usePageOperations = ( // params const { workspaceSlug, projectId } = useParams(); // derived values - const { - access, - addToFavorites, - archived_at, - duplicate, - id, - is_favorite, - is_locked, - makePrivate, - makePublic, - removePageFromFavorites, - } = page; + const { access, addToFavorites, archived_at, duplicate, id, is_favorite, is_locked, removePageFromFavorites } = page; // collaborative actions const { executeCollaborativeAction } = useCollaborativePageActions(props); // page operations @@ -195,8 +184,6 @@ export const usePageOperations = ( id, is_favorite, is_locked, - makePrivate, - makePublic, projectId, removePageFromFavorites, workspaceSlug, From 732da3ee1374294bcdf2aa2c6f20c52440b62f66 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 19 Dec 2024 20:40:19 +0530 Subject: [PATCH 27/32] refactor: types and enums added --- web/core/components/pages/dropdowns/actions.tsx | 4 ++-- web/core/components/pages/editor/header/mobile-root.tsx | 2 +- web/core/hooks/use-page-operations.ts | 7 +++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/web/core/components/pages/dropdowns/actions.tsx b/web/core/components/pages/dropdowns/actions.tsx index 1fe0c240e25..2c6a403ad5e 100644 --- a/web/core/components/pages/dropdowns/actions.tsx +++ b/web/core/components/pages/dropdowns/actions.tsx @@ -116,8 +116,8 @@ export const PageActions: React.FC = observer((props) => { { key: "archive-restore", action: pageOperations.toggleArchive, - title: !!archived_at ? "Restore" : "Archive", - icon: !!archived_at ? ArchiveRestoreIcon : ArchiveIcon, + title: archived_at ? "Restore" : "Archive", + icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, shouldRender: canCurrentUserArchivePage, }, { diff --git a/web/core/components/pages/editor/header/mobile-root.tsx b/web/core/components/pages/editor/header/mobile-root.tsx index 57c9d237817..073c698140a 100644 --- a/web/core/components/pages/editor/header/mobile-root.tsx +++ b/web/core/components/pages/editor/header/mobile-root.tsx @@ -36,7 +36,7 @@ export const PageEditorMobileHeaderRoot: React.FC = observer((props) => {
- {isContentEditable && editorRef && } + {isContentEditable && editorRef && }
); diff --git a/web/core/hooks/use-page-operations.ts b/web/core/hooks/use-page-operations.ts index 05444c10ea9..12a08dd46d2 100644 --- a/web/core/hooks/use-page-operations.ts +++ b/web/core/hooks/use-page-operations.ts @@ -2,6 +2,8 @@ import { useMemo } from "react"; import { useParams } from "next/navigation"; // plane editor import { EditorRefApi } from "@plane/editor"; +// plane types +import { EPageAccess } from "@plane/types/src/enums"; // plane ui import { setToast, TOAST_TYPE } from "@plane/ui"; // helpers @@ -71,9 +73,10 @@ export const usePageOperations = ( move: async () => {}, openInNewTab: () => window.open(`/${pageLink}`, "_blank"), toggleAccess: async () => { - const changedPageType = access === 0 ? "private" : "public"; + const changedPageType = access === EPageAccess.PUBLIC ? "private" : "public"; try { - if (access === 0) await executeCollaborativeAction({ type: "sendMessageToServer", message: "make-private" }); + if (access === EPageAccess.PUBLIC) + await executeCollaborativeAction({ type: "sendMessageToServer", message: "make-private" }); else await executeCollaborativeAction({ type: "sendMessageToServer", message: "make-public" }); setToast({ From a5bae5289cdd0f35bfa817289ee3daf8854a7916 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 19 Dec 2024 20:41:52 +0530 Subject: [PATCH 28/32] refactore: update store action --- web/core/store/pages/page.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts index e4b3b747457..eb94b238537 100644 --- a/web/core/store/pages/page.ts +++ b/web/core/store/pages/page.ts @@ -412,7 +412,9 @@ export class Page implements IPage { if (!workspaceSlug || !projectId || !this.id) return undefined; const pageAccess = this.access; - runInAction(() => (this.access = EPageAccess.PUBLIC)); + runInAction(() => { + this.access = EPageAccess.PUBLIC; + }); if (shouldSync) { try { @@ -436,7 +438,9 @@ export class Page implements IPage { if (!workspaceSlug || !projectId || !this.id) return undefined; const pageAccess = this.access; - runInAction(() => (this.access = EPageAccess.PRIVATE)); + runInAction(() => { + this.access = EPageAccess.PRIVATE; + }); if (shouldSync) { try { From ba90bfc938053dc482522835858467afd3b0acaf Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 30 Dec 2024 12:57:06 +0530 Subject: [PATCH 29/32] chore: changed the duplicate viewset --- apiserver/plane/app/views/page/base.py | 44 ++++++++++++++------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py index 8f22845a227..12643f8e66a 100644 --- a/apiserver/plane/app/views/page/base.py +++ b/apiserver/plane/app/views/page/base.py @@ -562,26 +562,30 @@ class PageDuplicateEndpoint(BaseAPIView): def post(self, request, slug, project_id, page_id): page = Page.objects.filter( pk=page_id, workspace__slug=slug, projects__id=project_id - ).values() - new_page_data = list(page)[0] - new_page_data.name = f"{new_page_data.name} (Copy)" + ).first() - serializer = PageSerializer( - data=new_page_data, - context={ - "project_id": project_id, - "owned_by_id": request.user.id, - "description": new_page_data.description, - "description_binary": new_page_data.description_binary, - "description_html": new_page_data.description_html, - }, + # get all the project ids where page is present + project_ids = ProjectPage.objects.filter(page_id=page_id).values_list( + "project_id", flat=True ) - if serializer.is_valid(): - serializer.save() - # capture the page transaction - page_transaction.delay(request.data, None, serializer.data["id"]) - page = Page.objects.get(pk=serializer.data["id"]) - serializer = PageDetailSerializer(page) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + page.pk = None + page.name = f"{page.name} (Copy)" + page.save() + + for project_id in project_ids: + ProjectPage.objects.create( + workspace_id=page.workspace_id, + project_id=project_id, + page_id=page.id, + created_by_id=page.created_by_id, + updated_by_id=page.updated_by_id, + ) + + + page_transaction.delay( + {"description_html": page.description_html}, None, page.id + ) + page = Page.objects.get(pk=page.id) + serializer = PageDetailSerializer(page) + return Response(serializer.data, status=status.HTTP_201_CREATED) From 3527e6df7209a6134fce439844dae3d5f5d080c3 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 30 Dec 2024 15:53:08 +0530 Subject: [PATCH 30/32] fix: remove the page binary --- apiserver/plane/app/views/page/base.py | 2 +- web/core/store/pages/base-page.ts | 6 +++--- web/core/store/pages/project-page.ts | 13 +++++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py index 12643f8e66a..9243a3f9fdf 100644 --- a/apiserver/plane/app/views/page/base.py +++ b/apiserver/plane/app/views/page/base.py @@ -571,6 +571,7 @@ def post(self, request, slug, project_id, page_id): page.pk = None page.name = f"{page.name} (Copy)" + page.description_binary = None page.save() for project_id in project_ids: @@ -582,7 +583,6 @@ def post(self, request, slug, project_id, page_id): updated_by_id=page.updated_by_id, ) - page_transaction.delay( {"description_html": page.description_html}, None, page.id ) diff --git a/web/core/store/pages/base-page.ts b/web/core/store/pages/base-page.ts index da5e6d7831d..7b4e86429d6 100644 --- a/web/core/store/pages/base-page.ts +++ b/web/core/store/pages/base-page.ts @@ -30,7 +30,7 @@ export type TBasePage = TPage & { updatePageLogo: (logo_props: TLogoProps) => Promise; addToFavorites: () => Promise; removePageFromFavorites: () => Promise; - duplicate: () => Promise; + duplicate: () => Promise; }; export type TBasePagePermissions = { @@ -55,7 +55,7 @@ export type TBasePageServices = { archived_at: string; }>; restore: () => Promise; - duplicate: () => Promise; + duplicate: () => Promise; }; export type TPageInstance = TBasePage & @@ -487,6 +487,6 @@ export class BasePage implements TBasePage { duplicate = async () => { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId || !this.id) return undefined; - await this.services.duplicate(); + return await this.services.duplicate(); }; } diff --git a/web/core/store/pages/project-page.ts b/web/core/store/pages/project-page.ts index b03e391e184..f3459be7838 100644 --- a/web/core/store/pages/project-page.ts +++ b/web/core/store/pages/project-page.ts @@ -50,9 +50,14 @@ export class ProjectPage extends BasePage implements TProjectPage { if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields."); await projectPageService.restore(workspaceSlug, projectId, page.id); }, + duplicate: async () => { + if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields."); + return await projectPageService.duplicate(workspaceSlug, projectId, page.id); + }, }); makeObservable(this, { // computed + canCurrentUserAccessPage: computed, canCurrentUserEditPage: computed, canCurrentUserDuplicatePage: computed, canCurrentUserLockPage: computed, @@ -81,6 +86,14 @@ export class ProjectPage extends BasePage implements TProjectPage { return highestRole; }); + /** + * @description returns true if the current logged in user can access the page + */ + get canCurrentUserAccessPage() { + const isPagePublic = this.access === EPageAccess.PUBLIC; + return isPagePublic || this.isCurrentUserOwner; + } + /** * @description returns true if the current logged in user can edit the page */ From 190d0c6d748f7a8e828e3edd212caa45387bed9b Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 30 Dec 2024 16:35:23 +0530 Subject: [PATCH 31/32] fix: duplicate page action --- web/core/store/pages/base-page.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/web/core/store/pages/base-page.ts b/web/core/store/pages/base-page.ts index 7b4e86429d6..d8d88ac99f4 100644 --- a/web/core/store/pages/base-page.ts +++ b/web/core/store/pages/base-page.ts @@ -484,9 +484,5 @@ export class BasePage implements TBasePage { /** * @description duplicate the page */ - duplicate = async () => { - const { workspaceSlug, projectId } = this.store.router; - if (!workspaceSlug || !projectId || !this.id) return undefined; - return await this.services.duplicate(); - }; + duplicate = async () => await this.services.duplicate(); } From 941d4bb525cc23d84ee69dc6f6026bd6dc2b15aa Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 30 Dec 2024 21:14:12 +0530 Subject: [PATCH 32/32] fix: merge conflicts --- web/ce/components/pages/modals/move-page-modal.tsx | 4 ++-- web/core/components/pages/dropdowns/actions.tsx | 6 +++--- web/core/hooks/use-collaborative-page-actions.tsx | 13 +++++++------ web/core/hooks/use-page-operations.ts | 4 ++-- web/core/store/pages/base-page.ts | 1 + web/core/store/pages/project-page.ts | 9 +++++++++ 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/web/ce/components/pages/modals/move-page-modal.tsx b/web/ce/components/pages/modals/move-page-modal.tsx index 3fac0f6a042..d1aeb7b5313 100644 --- a/web/ce/components/pages/modals/move-page-modal.tsx +++ b/web/ce/components/pages/modals/move-page-modal.tsx @@ -1,10 +1,10 @@ // store types -import { IPage } from "@/store/pages/page"; +import { TPageInstance } from "@/store/pages/base-page"; export type TMovePageModalProps = { isOpen: boolean; onClose: () => void; - page: IPage; + page: TPageInstance; }; export const MovePageModal: React.FC = () => null; diff --git a/web/core/components/pages/dropdowns/actions.tsx b/web/core/components/pages/dropdowns/actions.tsx index 2c6a403ad5e..496ee14c9cd 100644 --- a/web/core/components/pages/dropdowns/actions.tsx +++ b/web/core/components/pages/dropdowns/actions.tsx @@ -29,7 +29,7 @@ import { usePageOperations } from "@/hooks/use-page-operations"; // plane web components import { MovePageModal } from "@/plane-web/components/pages"; // store types -import { IPage } from "@/store/pages/page"; +import { TPageInstance } from "@/store/pages/base-page"; export type TPageActions = | "full-screen" @@ -49,7 +49,7 @@ type Props = { editorRef?: EditorRefApi | null; extraOptions?: (TContextMenuItem & { key: TPageActions })[]; optionsOrder: TPageActions[]; - page: IPage; + page: TPageInstance; parentRef?: React.RefObject; }; @@ -164,7 +164,7 @@ export const PageActions: React.FC = observer((props) => { return ( <> setMovePageModal(false)} page={page} /> - setDeletePageModal(false)} pageId={page.id ?? ""} /> + setDeletePageModal(false)} page={page} /> {parentRef && } {arrangedOptions.map((item) => { diff --git a/web/core/hooks/use-collaborative-page-actions.tsx b/web/core/hooks/use-collaborative-page-actions.tsx index e0e662073ae..5ef1bfec9f1 100644 --- a/web/core/hooks/use-collaborative-page-actions.tsx +++ b/web/core/hooks/use-collaborative-page-actions.tsx @@ -16,10 +16,13 @@ type CollaborativeActionEvent = | { type: "sendMessageToServer"; message: TDocumentEventsServer } | { type: "receivedMessageFromServer"; message: TDocumentEventsClient }; -export const useCollaborativePageActions = ( - editorRef: EditorRefApi | null, - page: TPageInstance -) => { +type Props = { + editorRef?: EditorRefApi | null; + page: TPageInstance; +}; + +export const useCollaborativePageActions = (props: Props) => { + const { editorRef, page } = props; // currentUserAction local state to track if the current action is being processed, a // local action is basically the action performed by the current user to avoid double operations const [currentActionBeingProcessed, setCurrentActionBeingProcessed] = useState(null); @@ -82,9 +85,7 @@ export const useCollaborativePageActions = ( useEffect(() => { const realTimeStatelessMessageListener = editorRef?.listenToRealTimeUpdate(); - console.log(realTimeStatelessMessageListener); const handleStatelessMessage = (message: { payload: TDocumentEventsClient }) => { - console.log("aaa", message); if (currentActionBeingProcessed === message.payload) { setCurrentActionBeingProcessed(null); return; diff --git a/web/core/hooks/use-page-operations.ts b/web/core/hooks/use-page-operations.ts index 12a08dd46d2..efe973ad7d2 100644 --- a/web/core/hooks/use-page-operations.ts +++ b/web/core/hooks/use-page-operations.ts @@ -11,7 +11,7 @@ import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks import { useCollaborativePageActions } from "@/hooks/use-collaborative-page-actions"; // store types -import { IPage } from "@/store/pages/page"; +import { TPageInstance } from "@/store/pages/base-page"; export type TPageOperations = { toggleLock: () => void; @@ -25,7 +25,7 @@ export type TPageOperations = { type Props = { editorRef?: EditorRefApi | null; - page: IPage; + page: TPageInstance; }; export const usePageOperations = ( diff --git a/web/core/store/pages/base-page.ts b/web/core/store/pages/base-page.ts index d8d88ac99f4..e7338ff3c5f 100644 --- a/web/core/store/pages/base-page.ts +++ b/web/core/store/pages/base-page.ts @@ -42,6 +42,7 @@ export type TBasePagePermissions = { canCurrentUserArchivePage: boolean; canCurrentUserDeletePage: boolean; canCurrentUserFavoritePage: boolean; + canCurrentUserMovePage: boolean; isContentEditable: boolean; }; diff --git a/web/core/store/pages/project-page.ts b/web/core/store/pages/project-page.ts index f3459be7838..a7201687d0f 100644 --- a/web/core/store/pages/project-page.ts +++ b/web/core/store/pages/project-page.ts @@ -65,6 +65,7 @@ export class ProjectPage extends BasePage implements TProjectPage { canCurrentUserArchivePage: computed, canCurrentUserDeletePage: computed, canCurrentUserFavoritePage: computed, + canCurrentUserMovePage: computed, isContentEditable: computed, }); } @@ -154,6 +155,14 @@ export class ProjectPage extends BasePage implements TProjectPage { return !!highestRole && highestRole >= EUserPermissions.MEMBER; } + /** + * @description returns true if the current logged in user can move the page + */ + get canCurrentUserMovePage() { + const highestRole = this.getHighestRoleAcrossProjects(); + return this.isCurrentUserOwner || highestRole === EUserPermissions.ADMIN; + } + /** * @description returns true if the page can be edited */