From 6c1818c0147687dcf3aa090e4a573e22da4c5f01 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 23 Aug 2024 12:34:54 +0530 Subject: [PATCH 1/5] chore: project page version --- apiserver/plane/app/serializers/__init__.py | 1 + apiserver/plane/app/serializers/page.py | 35 ++++++++++++++++++++- apiserver/plane/app/views/page/version.py | 16 +++++----- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 83981ab53c6..618a9ec20f1 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -92,6 +92,7 @@ SubPageSerializer, PageDetailSerializer, PageVersionSerializer, + PageVersionDetailSerializer, ) from .estimate import ( diff --git a/apiserver/plane/app/serializers/page.py b/apiserver/plane/app/serializers/page.py index c754bd431cc..e7f273d408a 100644 --- a/apiserver/plane/app/serializers/page.py +++ b/apiserver/plane/app/serializers/page.py @@ -167,7 +167,40 @@ class Meta: class PageVersionSerializer(BaseSerializer): class Meta: model = PageVersion - fields = "__all__" + fields = [ + "id", + "workspace", + "page", + "last_saved_at", + "owned_by", + "created_at", + "updated_at", + "created_by", + "updated_by", + ] + read_only_fields = [ + "workspace", + "page", + ] + + +class PageVersionDetailSerializer(BaseSerializer): + class Meta: + model = PageVersion + fields = [ + "id", + "workspace", + "page", + "last_saved_at", + "description_binary", + "description_html", + "description_json", + "owned_by", + "created_at", + "updated_at", + "created_by", + "updated_by", + ] read_only_fields = [ "workspace", "page", diff --git a/apiserver/plane/app/views/page/version.py b/apiserver/plane/app/views/page/version.py index 70f6bd978f4..995a0626362 100644 --- a/apiserver/plane/app/views/page/version.py +++ b/apiserver/plane/app/views/page/version.py @@ -5,16 +5,18 @@ # Module imports from plane.db.models import PageVersion from ..base import BaseAPIView -from plane.app.permissions import ProjectEntityPermission -from plane.app.serializers import PageVersionSerializer +from plane.app.serializers import ( + PageVersionSerializer, + PageVersionDetailSerializer, +) +from plane.app.permissions import allow_permission, ROLE class PageVersionEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST] + ) def get(self, request, slug, project_id, page_id, pk=None): # Check if pk is provided if pk: @@ -25,7 +27,7 @@ def get(self, request, slug, project_id, page_id, pk=None): pk=pk, ) # Serialize the page version - serializer = PageVersionSerializer(page_version) + serializer = PageVersionDetailSerializer(page_version) return Response(serializer.data, status=status.HTTP_200_OK) # Return all page versions page_versions = PageVersion.objects.filter( From 861756cbc6702ec9db82ce5939231c6a67eb97b9 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 23 Aug 2024 16:32:32 +0530 Subject: [PATCH 2/5] feat: page version history implemented --- packages/editor/src/core/hooks/use-editor.ts | 4 +- packages/editor/src/core/types/editor.ts | 2 +- packages/types/src/pages.d.ts | 16 +++ .../pages/(detail)/[pageId]/page.tsx | 2 +- .../pages/editor/header/options-dropdown.tsx | 20 +++- .../components/pages/editor/page-root.tsx | 92 +++++++++++---- web/core/components/pages/index.ts | 1 + web/core/components/pages/version/editor.tsx | 111 ++++++++++++++++++ web/core/components/pages/version/index.ts | 5 + .../components/pages/version/main-content.tsx | 83 +++++++++++++ web/core/components/pages/version/root.tsx | 50 ++++++++ .../pages/version/sidebar-list-item.tsx | 48 ++++++++ web/core/components/pages/version/sidebar.tsx | 74 ++++++++++++ web/core/hooks/use-page-description.ts | 19 ++- web/core/services/page/index.ts | 1 + .../page/project-page-version.service.ts | 33 ++++++ 16 files changed, 532 insertions(+), 29 deletions(-) create mode 100644 web/core/components/pages/version/editor.tsx create mode 100644 web/core/components/pages/version/index.ts create mode 100644 web/core/components/pages/version/main-content.tsx create mode 100644 web/core/components/pages/version/root.tsx create mode 100644 web/core/components/pages/version/sidebar-list-item.tsx create mode 100644 web/core/components/pages/version/sidebar.tsx create mode 100644 web/core/services/page/project-page-version.service.ts diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index f9e8fdd609f..523c4be0fa1 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -126,8 +126,8 @@ export const useEditor = (props: CustomEditorProps) => { useImperativeHandle( forwardedRef, () => ({ - clearEditor: () => { - editorRef.current?.commands.clearContent(); + clearEditor: (emitUpdate = false) => { + editorRef.current?.commands.clearContent(emitUpdate); }, setEditorValue: (content: string) => { editorRef.current?.commands.setContent(content); diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index ac804b9b14f..b26fac38445 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -6,7 +6,7 @@ import { IMentionHighlight, IMentionSuggestion, TDisplayConfig, TEditorCommands, export type EditorReadOnlyRefApi = { getMarkDown: () => string; getHTML: () => string; - clearEditor: () => void; + clearEditor: (emitUpdate?: boolean) => void; setEditorValue: (content: string) => void; scrollSummary: (marking: IMarking) => void; }; diff --git a/packages/types/src/pages.d.ts b/packages/types/src/pages.d.ts index ea9b8b8ea5b..a78ff30568b 100644 --- a/packages/types/src/pages.d.ts +++ b/packages/types/src/pages.d.ts @@ -48,3 +48,19 @@ export type TPageFilters = { }; export type TPageEmbedType = "mention" | "issue"; + +export type TPageVersion = { + created_at: string; + created_by: string; + deleted_at: string | null; + description_binary?: string | null; + description_html?: string | null; + description_json?: object; + id: string; + last_saved_at: string; + owned_by: string; + page: string; + updated_at: string; + updated_by: string; + workspace: string; +} \ No newline at end of file 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 8c96f2bcf80..e9debb2bcf4 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 @@ -64,7 +64,7 @@ const PageDetailsPage = observer(() => { <>
-
+
diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 016ca8eb570..abeb6c94302 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -1,8 +1,8 @@ "use client"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import { ArchiveRestoreIcon, Clipboard, Copy, Link, Lock, LockOpen } from "lucide-react"; +import { useParams, usePathname, useRouter, useSearchParams } from "next/navigation"; +import { ArchiveRestoreIcon, Clipboard, Copy, History, Link, Lock, LockOpen } from "lucide-react"; // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; // ui @@ -23,6 +23,10 @@ type Props = { export const PageOptionsDropdown: React.FC = observer((props) => { const { editorRef, handleDuplicatePage, page, handleSaveDescription } = props; + // router + const router = useRouter(); + const pathname = usePathname(); + const currentSearchParams = useSearchParams(); // store values const { archived_at, @@ -145,6 +149,18 @@ export const PageOptionsDropdown: React.FC = observer((props) => { icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, shouldRender: canCurrentUserArchivePage, }, + { + key: "version-history", + action: () => { + // add query param, version=current to the route + const updatedSearchParams = new URLSearchParams(currentSearchParams.toString()); + updatedSearchParams.set("version", "current"); + router.push(pathname + "?" + updatedSearchParams.toString()); + }, + label: "Version history", + icon: History, + shouldRender: true, + }, ]; return ( diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx index 7ca023261af..063df3fbcb9 100644 --- a/web/core/components/pages/editor/page-root.tsx +++ b/web/core/components/pages/editor/page-root.tsx @@ -1,12 +1,22 @@ -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; +import { useSearchParams } from "next/navigation"; +// plane editor import { EditorRefApi, useEditorMarkings } from "@plane/editor"; +// plane types import { TPage } from "@plane/types"; +// plane ui import { setToast, TOAST_TYPE } from "@plane/ui"; -import { PageEditorHeaderRoot, PageEditorBody } from "@/components/pages"; +// components +import { PageEditorHeaderRoot, PageEditorBody, PageVersionsOverlay } from "@/components/pages"; +// hooks import { useProjectPages } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePageDescription } from "@/hooks/use-page-description"; +// services +import { ProjectPageVersionService } from "@/services/page"; +const projectPageVersionService = new ProjectPageVersionService(); +// store import { IPage } from "@/store/pages/page"; type TPageRootProps = { @@ -16,34 +26,38 @@ type TPageRootProps = { }; export const PageRoot = observer((props: TPageRootProps) => { - // router - const router = useAppRouter(); const { projectId, workspaceSlug, page } = props; - const { createPage } = useProjectPages(); - const { access, description_html, name } = page; - // states const [editorReady, setEditorReady] = useState(false); const [readOnlyEditorReady, setReadOnlyEditorReady] = useState(false); - + const [sidePeekVisible, setSidePeekVisible] = useState(window.innerWidth >= 768); + const [isVersionsOverlayOpen, setIsVersionsOverlayOpen] = useState(false); // refs const editorRef = useRef(null); const readOnlyEditorRef = useRef(null); - + // router + const router = useAppRouter(); + // search params + const searchParams = useSearchParams(); + // store hooks + const { createPage } = useProjectPages(); + // derived values + const { access, description_html, name } = page; // editor markings hook const { markings, updateMarkings } = useEditorMarkings(); - - const [sidePeekVisible, setSidePeekVisible] = useState(window.innerWidth >= 768 ? true : false); - // project-description - const { handleDescriptionChange, isDescriptionReady, pageDescriptionYJS, handleSaveDescription } = usePageDescription( - { - editorRef, - page, - projectId, - workspaceSlug, - } - ); + const { + handleDescriptionChange, + isDescriptionReady, + pageDescriptionYJS, + handleSaveDescription, + manuallyUpdateDescription, + } = usePageDescription({ + editorRef, + page, + projectId, + workspaceSlug, + }); const handleCreatePage = async (payload: Partial) => await createPage(payload); @@ -65,8 +79,46 @@ export const PageRoot = observer((props: TPageRootProps) => { ); }; + const version = searchParams.get("version"); + useEffect(() => { + if (!version) { + setIsVersionsOverlayOpen(false); + return; + } + setIsVersionsOverlayOpen(true); + }, [version]); + + const handleCloseVersionsOverlay = () => { + setIsVersionsOverlayOpen(false); + router.push(`/${workspaceSlug}/projects/${projectId}/pages/${page.id}`); + }; + return ( <> + { + if (!workspaceSlug || !projectId) return; + return await projectPageVersionService.fetchAllVersions( + workspaceSlug.toString(), + projectId.toString(), + pageId + ); + }} + fetchVersionDetails={async (pageId, versionId) => { + if (!workspaceSlug || !projectId) return; + return await projectPageVersionService.fetchVersionById( + workspaceSlug.toString(), + projectId.toString(), + pageId, + versionId + ); + }} + handleRestore={manuallyUpdateDescription} + isOpen={isVersionsOverlayOpen} + onClose={handleCloseVersionsOverlay} + pageId={page.id ?? ""} + /> = observer((props) => { + const { activeVersion, isCurrentVersionActive, versionDetails } = props; + // params + const { workspaceSlug, projectId, pageId } = useParams(); + // store hooks + const { data: currentUser } = useUser(); + const { + getUserDetails, + project: { getProjectMemberIds }, + } = useMember(); + const { description_html } = usePage(pageId.toString() ?? ""); + // derived values + const projectMemberIds = projectId ? getProjectMemberIds(projectId.toString()) : []; + const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite); + // issue-embed + const { issueEmbedProps } = useIssueEmbed(workspaceSlug?.toString() ?? "", projectId?.toString() ?? ""); + // use-mention + const { mentionHighlights } = useMention({ + workspaceSlug: workspaceSlug?.toString() ?? "", + projectId: projectId?.toString() ?? "", + members: projectMemberDetails, + user: currentUser ?? undefined, + }); + // page filters + const { fontSize, fontStyle } = usePageFilters(); + + const displayConfig: TDisplayConfig = { + fontSize, + fontStyle, + }; + + if (!isCurrentVersionActive && !versionDetails) + return ( +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ + +
+
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+
+
+ ); + + return ( +

"} + containerClassName="p-0 pb-64 border-none" + displayConfig={displayConfig} + editorClassName="pl-10" + mentionHandler={{ + highlights: mentionHighlights, + }} + embedHandler={{ + issue: { + widgetCallback: issueEmbedProps.widgetCallback, + }, + }} + /> + ); +}); diff --git a/web/core/components/pages/version/index.ts b/web/core/components/pages/version/index.ts new file mode 100644 index 00000000000..ab07dfa8fe7 --- /dev/null +++ b/web/core/components/pages/version/index.ts @@ -0,0 +1,5 @@ +export * from "./editor"; +export * from "./main-content"; +export * from "./root"; +export * from "./sidebar-list-item"; +export * from "./sidebar"; diff --git a/web/core/components/pages/version/main-content.tsx b/web/core/components/pages/version/main-content.tsx new file mode 100644 index 00000000000..2f8b50e3b4f --- /dev/null +++ b/web/core/components/pages/version/main-content.tsx @@ -0,0 +1,83 @@ +import { useState } from "react"; +import { observer } from "mobx-react"; +import useSWR from "swr"; +// plane types +import { TPageVersion } from "@plane/types"; +// plane ui +import { Button, setToast, TOAST_TYPE } from "@plane/ui"; +// components +import { PagesVersionEditor } from "@/components/pages"; +// helpers +import { renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper"; + +type Props = { + activeVersion: string | null; + fetchVersionDetails: (pageId: string, versionId: string) => Promise; + handleClose: () => void; + handleRestore: (descriptionHTML: string) => Promise; + pageId: string; +}; + +export const PageVersionsMainContent: React.FC = observer((props) => { + const { activeVersion, fetchVersionDetails, handleClose, handleRestore, pageId } = props; + // states + const [isRestoring, setIsRestoring] = useState(false); + + const { data: versionDetails } = useSWR( + pageId && activeVersion && activeVersion !== "current" ? `PAGE_VERSION_${activeVersion}` : null, + pageId && activeVersion && activeVersion !== "current" ? () => fetchVersionDetails(pageId, activeVersion) : null + ); + + const isCurrentVersionActive = activeVersion === "current"; + + const handleRestoreVersion = async () => { + setIsRestoring(true); + await handleRestore(versionDetails?.description_html ?? "

") + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Page version restored.", + }); + handleClose(); + }) + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Failed to restore page version.", + }) + ) + .finally(() => setIsRestoring(false)); + }; + + return ( +
+
+
+ {isCurrentVersionActive + ? "Current version" + : versionDetails + ? `${renderFormattedDate(versionDetails.last_saved_at)} ${renderFormattedTime(versionDetails.last_saved_at)}` + : "Loading version details"} +
+ {!isCurrentVersionActive && ( + + )} +
+
+ +
+
+ ); +}); diff --git a/web/core/components/pages/version/root.tsx b/web/core/components/pages/version/root.tsx new file mode 100644 index 00000000000..015f5f5d78f --- /dev/null +++ b/web/core/components/pages/version/root.tsx @@ -0,0 +1,50 @@ +import useSWR from "swr"; +// plane types +import { TPageVersion } from "@plane/types"; +// components +import { PageVersionsMainContent, PageVersionsSidebar } from "@/components/pages"; +// helpers +import { cn } from "@/helpers/common.helper"; + +type Props = { + activeVersion: string | null; + fetchAllVersions: (pageId: string) => Promise; + fetchVersionDetails: (pageId: string, versionId: string) => Promise; + handleRestore: (descriptionHTML: string) => Promise; + isOpen: boolean; + onClose: () => void; + pageId: string; +}; + +export const PageVersionsOverlay: React.FC = (props) => { + const { activeVersion, fetchAllVersions, fetchVersionDetails, handleRestore, isOpen, onClose, pageId } = props; + + const { data: versionsList } = useSWR( + pageId && isOpen ? `PAGE_VERSIONS_LIST_${pageId}` : null, + pageId && isOpen ? () => fetchAllVersions(pageId) : null + ); + + const handleClose = () => { + onClose(); + }; + + return ( +
+ + +
+ ); +}; diff --git a/web/core/components/pages/version/sidebar-list-item.tsx b/web/core/components/pages/version/sidebar-list-item.tsx new file mode 100644 index 00000000000..9c1c13e0de8 --- /dev/null +++ b/web/core/components/pages/version/sidebar-list-item.tsx @@ -0,0 +1,48 @@ +import { observer } from "mobx-react"; +import Link from "next/link"; +// plane types +import { TPageVersion } from "@plane/types"; +// plane ui +import { Avatar } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper"; +// hooks +import { useMember } from "@/hooks/store"; + +type Props = { + href: string; + isActive: boolean; + version: TPageVersion; +}; + +export const PlaneVersionsSidebarListItem: React.FC = observer((props) => { + const { href, isActive, version } = props; + // store hooks + const { getUserDetails } = useMember(); + // derived values + const ownerDetails = getUserDetails(version.owned_by); + + return ( + +

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

+

+ + {ownerDetails?.display_name} +

+ + ); +}); diff --git a/web/core/components/pages/version/sidebar.tsx b/web/core/components/pages/version/sidebar.tsx new file mode 100644 index 00000000000..65ae2da9718 --- /dev/null +++ b/web/core/components/pages/version/sidebar.tsx @@ -0,0 +1,74 @@ +import Link from "next/link"; +import { usePathname, useSearchParams } from "next/navigation"; +import { X } from "lucide-react"; +// plane types +import { TPageVersion } from "@plane/types"; +// plane ui +import { Loader } from "@plane/ui"; +// components +import { PlaneVersionsSidebarListItem } from "@/components/pages"; +// helpers +import { cn } from "@/helpers/common.helper"; + +type Props = { + activeVersion: string | null; + handleClose: () => void; + versions: TPageVersion[] | undefined; +}; + +export const PageVersionsSidebar: React.FC = (props) => { + const { activeVersion, handleClose, versions } = props; + // params + const pathname = usePathname(); + const currentSearchParams = useSearchParams(); + + const getVersionLink = (versionID: string) => { + // add query param, version=current to the route + const updatedSearchParams = new URLSearchParams(currentSearchParams.toString()); + updatedSearchParams.set("version", versionID); + return pathname + "?" + updatedSearchParams.toString(); + }; + + return ( +
+
+
Version history
+ +
+
+ +

Current version

+ + {versions ? ( + versions.map((version) => ( + + )) + ) : ( + + + + + + + + )} +
+
+ ); +}; diff --git a/web/core/hooks/use-page-description.ts b/web/core/hooks/use-page-description.ts index f7b467d4d0b..4273694505d 100644 --- a/web/core/hooks/use-page-description.ts +++ b/web/core/hooks/use-page-description.ts @@ -1,20 +1,19 @@ import React, { useCallback, useEffect, useState } from "react"; import useSWR from "swr"; - +// plane editor import { EditorRefApi, proseMirrorJSONToBinaryString, applyUpdates, generateJSONfromHTMLForDocumentEditor, } from "@plane/editor"; - // hooks import { setToast, TOAST_TYPE } from "@plane/ui"; import useAutoSave from "@/hooks/use-auto-save"; import useReloadConfirmations from "@/hooks/use-reload-confirmation"; - // services import { ProjectPageService } from "@/services/page"; +// store import { IPage } from "@/store/pages/page"; const projectPageService = new ProjectPageService(); @@ -183,6 +182,19 @@ export const usePageDescription = (props: Props) => { ] ); + const manuallyUpdateDescription = async (descriptionHTML: string) => { + const { contentJSON, editorSchema } = generateJSONfromHTMLForDocumentEditor(descriptionHTML ?? "

"); + const yDocBinaryString = proseMirrorJSONToBinaryString(contentJSON, "default", editorSchema); + + try { + editorRef.current?.clearEditor(true); + await updateDescription(yDocBinaryString, descriptionHTML ?? "

"); + await mutateDescriptionYJS(); + } catch (error) { + console.log("error", error); + } + }; + useAutoSave(handleSaveDescription); return { @@ -190,5 +202,6 @@ export const usePageDescription = (props: Props) => { isDescriptionReady, pageDescriptionYJS, handleSaveDescription, + manuallyUpdateDescription, }; }; diff --git a/web/core/services/page/index.ts b/web/core/services/page/index.ts index d89b175d633..b25199e7f3a 100644 --- a/web/core/services/page/index.ts +++ b/web/core/services/page/index.ts @@ -1 +1,2 @@ +export * from "./project-page-version.service"; export * from "./project-page.service"; diff --git a/web/core/services/page/project-page-version.service.ts b/web/core/services/page/project-page-version.service.ts new file mode 100644 index 00000000000..05732e3d225 --- /dev/null +++ b/web/core/services/page/project-page-version.service.ts @@ -0,0 +1,33 @@ +// plane types +import { TPageVersion } from "@plane/types"; +// helpers +import { API_BASE_URL } from "@/helpers/common.helper"; +// services +import { APIService } from "@/services/api.service"; + +export class ProjectPageVersionService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async fetchAllVersions(workspaceSlug: string, projectId: string, pageId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/versions/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async fetchVersionById( + workspaceSlug: string, + projectId: string, + pageId: string, + versionId: string + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/versions/${versionId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} From 2ea7220612b9acf8680fb977f8aa2e1a9a080c39 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 23 Aug 2024 16:34:30 +0530 Subject: [PATCH 3/5] chore: hide save button when version history overlay is active --- .../(detail)/[projectId]/pages/(detail)/header.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx index c04c6d94484..4cfe11d353d 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; +import { useParams, useSearchParams } from "next/navigation"; import { FileText } from "lucide-react"; // types import { TLogoProps } from "@plane/types"; @@ -25,6 +25,7 @@ export interface IPagesHeaderProps { export const PageDetailsHeader = observer(() => { // router const { workspaceSlug, pageId } = useParams(); + const searchParams = useSearchParams(); // state const [isOpen, setIsOpen] = useState(false); // store hooks @@ -55,6 +56,8 @@ export const PageDetailsHeader = observer(() => { } }; + const isVersionHistoryOverlayActive = !!searchParams.get("version"); + return (
@@ -157,7 +160,7 @@ export const PageDetailsHeader = observer(() => {
- {isContentEditable && ( + {isContentEditable && !isVersionHistoryOverlayActive && ( - )} -
-
- -
+ {versionDetailsError ? ( +
+
+ + + +
+
Something went wrong!
+

The version could not be loaded, please try again.

+
+ +
+
+ ) : ( + <> +
+
+ {isCurrentVersionActive + ? "Current version" + : versionDetails + ? `${renderFormattedDate(versionDetails.last_saved_at)} ${renderFormattedTime(versionDetails.last_saved_at)}` + : "Loading version details"} +
+ {!isCurrentVersionActive && ( + + )} +
+
+ +
+ + )}
); }); diff --git a/web/core/components/pages/version/root.tsx b/web/core/components/pages/version/root.tsx index 015f5f5d78f..443053d56db 100644 --- a/web/core/components/pages/version/root.tsx +++ b/web/core/components/pages/version/root.tsx @@ -1,8 +1,7 @@ -import useSWR from "swr"; // plane types import { TPageVersion } from "@plane/types"; // components -import { PageVersionsMainContent, PageVersionsSidebar } from "@/components/pages"; +import { PageVersionsMainContent, PageVersionsSidebarRoot } from "@/components/pages"; // helpers import { cn } from "@/helpers/common.helper"; @@ -19,11 +18,6 @@ type Props = { export const PageVersionsOverlay: React.FC = (props) => { const { activeVersion, fetchAllVersions, fetchVersionDetails, handleRestore, isOpen, onClose, pageId } = props; - const { data: versionsList } = useSWR( - pageId && isOpen ? `PAGE_VERSIONS_LIST_${pageId}` : null, - pageId && isOpen ? () => fetchAllVersions(pageId) : null - ); - const handleClose = () => { onClose(); }; @@ -44,7 +38,13 @@ export const PageVersionsOverlay: React.FC = (props) => { handleRestore={handleRestore} pageId={pageId} /> - + ); }; diff --git a/web/core/components/pages/version/sidebar-list.tsx b/web/core/components/pages/version/sidebar-list.tsx new file mode 100644 index 00000000000..cf276742b02 --- /dev/null +++ b/web/core/components/pages/version/sidebar-list.tsx @@ -0,0 +1,99 @@ +import { useState } from "react"; +import Link from "next/link"; +import useSWR from "swr"; +import { TriangleAlert } from "lucide-react"; +// plane types +import { TPageVersion } from "@plane/types"; +// plane ui +import { Button, Loader } from "@plane/ui"; +// components +import { PlaneVersionsSidebarListItem } from "@/components/pages"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useQueryParams } from "@/hooks/use-query-params"; + +type Props = { + activeVersion: string | null; + fetchAllVersions: (pageId: string) => Promise; + isOpen: boolean; + pageId: string; +}; + +export const PageVersionsSidebarList: React.FC = (props) => { + const { activeVersion, fetchAllVersions, isOpen, pageId } = props; + // states + const [isRetrying, setIsRetrying] = useState(false); + // update query params + const { updateQueryParams } = useQueryParams(); + + const { + data: versionsList, + error: versionsListError, + mutate: mutateVersionsList, + } = useSWR( + pageId && isOpen ? `PAGE_VERSIONS_LIST_${pageId}` : null, + pageId && isOpen ? () => fetchAllVersions(pageId) : null + ); + + const handleRetry = async () => { + setIsRetrying(true); + await mutateVersionsList(); + setIsRetrying(false); + }; + + const getVersionLink = (versionID: string) => + updateQueryParams({ + paramsToAdd: { version: versionID }, + }); + + return ( +
+ +

Current version

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

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

+
+ +
+
+ ) : versionsList ? ( + versionsList.map((version) => ( + + )) + ) : ( + + + + + + + + )} +
+ ); +}; diff --git a/web/core/components/pages/version/sidebar-root.tsx b/web/core/components/pages/version/sidebar-root.tsx new file mode 100644 index 00000000000..793d7fed90f --- /dev/null +++ b/web/core/components/pages/version/sidebar-root.tsx @@ -0,0 +1,38 @@ +import { X } from "lucide-react"; +// plane types +import { TPageVersion } from "@plane/types"; +// components +import { PageVersionsSidebarList } from "@/components/pages"; + +type Props = { + activeVersion: string | null; + fetchAllVersions: (pageId: string) => Promise; + handleClose: () => void; + isOpen: boolean; + pageId: string; +}; + +export const PageVersionsSidebarRoot: React.FC = (props) => { + const { activeVersion, fetchAllVersions, handleClose, isOpen, pageId } = props; + + return ( +
+
+
Version history
+ +
+ +
+ ); +}; diff --git a/web/core/components/pages/version/sidebar.tsx b/web/core/components/pages/version/sidebar.tsx deleted file mode 100644 index 8acadd0dad8..00000000000 --- a/web/core/components/pages/version/sidebar.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import Link from "next/link"; -import { X } from "lucide-react"; -// plane types -import { TPageVersion } from "@plane/types"; -// plane ui -import { Loader } from "@plane/ui"; -// components -import { PlaneVersionsSidebarListItem } from "@/components/pages"; -// helpers -import { cn } from "@/helpers/common.helper"; -// hooks -import { useQueryParams } from "@/hooks/use-query-params"; - -type Props = { - activeVersion: string | null; - handleClose: () => void; - versions: TPageVersion[] | undefined; -}; - -export const PageVersionsSidebar: React.FC = (props) => { - const { activeVersion, handleClose, versions } = props; - // update query params - const { updateQueryParams } = useQueryParams(); - - const getVersionLink = (versionID: string) => - updateQueryParams({ - paramsToAdd: { version: versionID }, - }); - - return ( -
-
-
Version history
- -
-
- -

Current version

- - {versions ? ( - versions.map((version) => ( - - )) - ) : ( - - - - - - - - )} -
-
- ); -};