From d238bac3872382e670a05d04f4950708968fb913 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 14 Nov 2024 12:55:27 +0530 Subject: [PATCH 01/12] 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/12] 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/12] 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 ? (
); }); 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; + } } };