mirror of
https://github.com/makeplane/plane.git
synced 2025-12-23 15:19:37 +01:00
[WEB-2293] feat: pages version history (#5417)
* chore: project page version * feat: page version history implemented * chore: hide save button when version history overlay is active * refactor: updated navigation logic * chore: added error states --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
committed by
GitHub
parent
d802316c5c
commit
a0ed51c845
@@ -92,6 +92,7 @@ from .page import (
|
|||||||
SubPageSerializer,
|
SubPageSerializer,
|
||||||
PageDetailSerializer,
|
PageDetailSerializer,
|
||||||
PageVersionSerializer,
|
PageVersionSerializer,
|
||||||
|
PageVersionDetailSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .estimate import (
|
from .estimate import (
|
||||||
|
|||||||
@@ -167,7 +167,40 @@ class PageLogSerializer(BaseSerializer):
|
|||||||
class PageVersionSerializer(BaseSerializer):
|
class PageVersionSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PageVersion
|
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 = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
"page",
|
"page",
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ from rest_framework.response import Response
|
|||||||
# Module imports
|
# Module imports
|
||||||
from plane.db.models import PageVersion
|
from plane.db.models import PageVersion
|
||||||
from ..base import BaseAPIView
|
from ..base import BaseAPIView
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.serializers import (
|
||||||
from plane.app.serializers import PageVersionSerializer
|
PageVersionSerializer,
|
||||||
|
PageVersionDetailSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import allow_permission, ROLE
|
||||||
|
|
||||||
|
|
||||||
class PageVersionEndpoint(BaseAPIView):
|
class PageVersionEndpoint(BaseAPIView):
|
||||||
|
|
||||||
permission_classes = [
|
@allow_permission(
|
||||||
ProjectEntityPermission,
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]
|
||||||
]
|
)
|
||||||
|
|
||||||
def get(self, request, slug, project_id, page_id, pk=None):
|
def get(self, request, slug, project_id, page_id, pk=None):
|
||||||
# Check if pk is provided
|
# Check if pk is provided
|
||||||
if pk:
|
if pk:
|
||||||
@@ -25,7 +27,7 @@ class PageVersionEndpoint(BaseAPIView):
|
|||||||
pk=pk,
|
pk=pk,
|
||||||
)
|
)
|
||||||
# Serialize the page version
|
# Serialize the page version
|
||||||
serializer = PageVersionSerializer(page_version)
|
serializer = PageVersionDetailSerializer(page_version)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
# Return all page versions
|
# Return all page versions
|
||||||
page_versions = PageVersion.objects.filter(
|
page_versions = PageVersion.objects.filter(
|
||||||
|
|||||||
@@ -126,8 +126,8 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
() => ({
|
() => ({
|
||||||
clearEditor: () => {
|
clearEditor: (emitUpdate = false) => {
|
||||||
editorRef.current?.commands.clearContent();
|
editorRef.current?.commands.clearContent(emitUpdate);
|
||||||
},
|
},
|
||||||
setEditorValue: (content: string) => {
|
setEditorValue: (content: string) => {
|
||||||
editorRef.current?.commands.setContent(content);
|
editorRef.current?.commands.setContent(content);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { IMentionHighlight, IMentionSuggestion, TDisplayConfig, TEditorCommands,
|
|||||||
export type EditorReadOnlyRefApi = {
|
export type EditorReadOnlyRefApi = {
|
||||||
getMarkDown: () => string;
|
getMarkDown: () => string;
|
||||||
getHTML: () => string;
|
getHTML: () => string;
|
||||||
clearEditor: () => void;
|
clearEditor: (emitUpdate?: boolean) => void;
|
||||||
setEditorValue: (content: string) => void;
|
setEditorValue: (content: string) => void;
|
||||||
scrollSummary: (marking: IMarking) => void;
|
scrollSummary: (marking: IMarking) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
16
packages/types/src/pages.d.ts
vendored
16
packages/types/src/pages.d.ts
vendored
@@ -48,3 +48,19 @@ export type TPageFilters = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TPageEmbedType = "mention" | "issue";
|
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;
|
||||||
|
}
|
||||||
@@ -64,7 +64,7 @@ const PageDetailsPage = observer(() => {
|
|||||||
<>
|
<>
|
||||||
<PageHead title={name} />
|
<PageHead title={name} />
|
||||||
<div className="flex h-full flex-col justify-between">
|
<div className="flex h-full flex-col justify-between">
|
||||||
<div className="h-full w-full flex-shrink-0 flex flex-col overflow-hidden">
|
<div className="relative h-full w-full flex-shrink-0 flex flex-col overflow-hidden">
|
||||||
<PageRoot page={page} projectId={projectId.toString()} workspaceSlug={workspaceSlug.toString()} />
|
<PageRoot page={page} projectId={projectId.toString()} workspaceSlug={workspaceSlug.toString()} />
|
||||||
<IssuePeekOverview />
|
<IssuePeekOverview />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams, useSearchParams } from "next/navigation";
|
||||||
import { FileText } from "lucide-react";
|
import { FileText } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { TLogoProps } from "@plane/types";
|
import { TLogoProps } from "@plane/types";
|
||||||
@@ -25,6 +25,7 @@ export interface IPagesHeaderProps {
|
|||||||
export const PageDetailsHeader = observer(() => {
|
export const PageDetailsHeader = observer(() => {
|
||||||
// router
|
// router
|
||||||
const { workspaceSlug, pageId } = useParams();
|
const { workspaceSlug, pageId } = useParams();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
// state
|
// state
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
@@ -55,6 +56,8 @@ export const PageDetailsHeader = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isVersionHistoryOverlayActive = !!searchParams.get("version");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
@@ -157,7 +160,7 @@ export const PageDetailsHeader = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PageDetailsHeaderExtraActions />
|
<PageDetailsHeaderExtraActions />
|
||||||
{isContentEditable && (
|
{isContentEditable && !isVersionHistoryOverlayActive && (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { ArchiveRestoreIcon, Clipboard, Copy, Link, Lock, LockOpen } from "lucide-react";
|
import { ArchiveRestoreIcon, Clipboard, Copy, History, Link, Lock, LockOpen } from "lucide-react";
|
||||||
// document editor
|
// document editor
|
||||||
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
|
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
|
||||||
// ui
|
// ui
|
||||||
@@ -11,6 +11,7 @@ import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@pl
|
|||||||
import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper";
|
import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||||
|
import { useQueryParams } from "@/hooks/use-query-params";
|
||||||
// store
|
// store
|
||||||
import { IPage } from "@/store/pages/page";
|
import { IPage } from "@/store/pages/page";
|
||||||
|
|
||||||
@@ -23,6 +24,8 @@ type Props = {
|
|||||||
|
|
||||||
export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
||||||
const { editorRef, handleDuplicatePage, page, handleSaveDescription } = props;
|
const { editorRef, handleDuplicatePage, page, handleSaveDescription } = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
// store values
|
// store values
|
||||||
const {
|
const {
|
||||||
archived_at,
|
archived_at,
|
||||||
@@ -40,6 +43,8 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
|||||||
const { workspaceSlug, projectId } = useParams();
|
const { workspaceSlug, projectId } = useParams();
|
||||||
// page filters
|
// page filters
|
||||||
const { isFullWidth, handleFullWidth } = usePageFilters();
|
const { isFullWidth, handleFullWidth } = usePageFilters();
|
||||||
|
// update query params
|
||||||
|
const { updateQueryParams } = useQueryParams();
|
||||||
|
|
||||||
const handleArchivePage = async () =>
|
const handleArchivePage = async () =>
|
||||||
await archive().catch(() =>
|
await archive().catch(() =>
|
||||||
@@ -145,6 +150,19 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
|||||||
icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon,
|
icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon,
|
||||||
shouldRender: canCurrentUserArchivePage,
|
shouldRender: canCurrentUserArchivePage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "version-history",
|
||||||
|
action: () => {
|
||||||
|
// add query param, version=current to the route
|
||||||
|
const updatedRoute = updateQueryParams({
|
||||||
|
paramsToAdd: { version: "current" },
|
||||||
|
});
|
||||||
|
router.push(updatedRoute);
|
||||||
|
},
|
||||||
|
label: "Version history",
|
||||||
|
icon: History,
|
||||||
|
shouldRender: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
// plane editor
|
||||||
import { EditorRefApi, useEditorMarkings } from "@plane/editor";
|
import { EditorRefApi, useEditorMarkings } from "@plane/editor";
|
||||||
|
// plane types
|
||||||
import { TPage } from "@plane/types";
|
import { TPage } from "@plane/types";
|
||||||
|
// plane ui
|
||||||
import { setToast, TOAST_TYPE } from "@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 { useProjectPages } from "@/hooks/store";
|
||||||
import { useAppRouter } from "@/hooks/use-app-router";
|
import { useAppRouter } from "@/hooks/use-app-router";
|
||||||
import { usePageDescription } from "@/hooks/use-page-description";
|
import { usePageDescription } from "@/hooks/use-page-description";
|
||||||
|
import { useQueryParams } from "@/hooks/use-query-params";
|
||||||
|
// services
|
||||||
|
import { ProjectPageVersionService } from "@/services/page";
|
||||||
|
const projectPageVersionService = new ProjectPageVersionService();
|
||||||
|
// store
|
||||||
import { IPage } from "@/store/pages/page";
|
import { IPage } from "@/store/pages/page";
|
||||||
|
|
||||||
type TPageRootProps = {
|
type TPageRootProps = {
|
||||||
@@ -16,34 +27,40 @@ type TPageRootProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PageRoot = observer((props: TPageRootProps) => {
|
export const PageRoot = observer((props: TPageRootProps) => {
|
||||||
// router
|
|
||||||
const router = useAppRouter();
|
|
||||||
const { projectId, workspaceSlug, page } = props;
|
const { projectId, workspaceSlug, page } = props;
|
||||||
const { createPage } = useProjectPages();
|
|
||||||
const { access, description_html, name } = page;
|
|
||||||
|
|
||||||
// states
|
// states
|
||||||
const [editorReady, setEditorReady] = useState(false);
|
const [editorReady, setEditorReady] = useState(false);
|
||||||
const [readOnlyEditorReady, setReadOnlyEditorReady] = useState(false);
|
const [readOnlyEditorReady, setReadOnlyEditorReady] = useState(false);
|
||||||
|
const [sidePeekVisible, setSidePeekVisible] = useState(window.innerWidth >= 768);
|
||||||
|
const [isVersionsOverlayOpen, setIsVersionsOverlayOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
const editorRef = useRef<EditorRefApi>(null);
|
const editorRef = useRef<EditorRefApi>(null);
|
||||||
const readOnlyEditorRef = useRef<EditorRefApi>(null);
|
const readOnlyEditorRef = useRef<EditorRefApi>(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
|
// editor markings hook
|
||||||
const { markings, updateMarkings } = useEditorMarkings();
|
const { markings, updateMarkings } = useEditorMarkings();
|
||||||
|
|
||||||
const [sidePeekVisible, setSidePeekVisible] = useState(window.innerWidth >= 768 ? true : false);
|
|
||||||
|
|
||||||
// project-description
|
// project-description
|
||||||
const { handleDescriptionChange, isDescriptionReady, pageDescriptionYJS, handleSaveDescription } = usePageDescription(
|
const {
|
||||||
{
|
handleDescriptionChange,
|
||||||
|
isDescriptionReady,
|
||||||
|
pageDescriptionYJS,
|
||||||
|
handleSaveDescription,
|
||||||
|
manuallyUpdateDescription,
|
||||||
|
} = usePageDescription({
|
||||||
editorRef,
|
editorRef,
|
||||||
page,
|
page,
|
||||||
projectId,
|
projectId,
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
}
|
});
|
||||||
);
|
// update query params
|
||||||
|
const { updateQueryParams } = useQueryParams();
|
||||||
|
|
||||||
const handleCreatePage = async (payload: Partial<TPage>) => await createPage(payload);
|
const handleCreatePage = async (payload: Partial<TPage>) => await createPage(payload);
|
||||||
|
|
||||||
@@ -65,8 +82,48 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const version = searchParams.get("version");
|
||||||
|
useEffect(() => {
|
||||||
|
if (!version) {
|
||||||
|
setIsVersionsOverlayOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsVersionsOverlayOpen(true);
|
||||||
|
}, [version]);
|
||||||
|
|
||||||
|
const handleCloseVersionsOverlay = () => {
|
||||||
|
const updatedRoute = updateQueryParams({
|
||||||
|
paramsToRemove: ["version"],
|
||||||
|
});
|
||||||
|
router.push(updatedRoute);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PageVersionsOverlay
|
||||||
|
activeVersion={version}
|
||||||
|
fetchAllVersions={async (pageId) => {
|
||||||
|
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 ?? ""}
|
||||||
|
/>
|
||||||
<PageEditorHeaderRoot
|
<PageEditorHeaderRoot
|
||||||
editorRef={editorRef}
|
editorRef={editorRef}
|
||||||
readOnlyEditorRef={readOnlyEditorRef}
|
readOnlyEditorRef={readOnlyEditorRef}
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ export * from "./header";
|
|||||||
export * from "./list";
|
export * from "./list";
|
||||||
export * from "./loaders";
|
export * from "./loaders";
|
||||||
export * from "./modals";
|
export * from "./modals";
|
||||||
|
export * from "./version";
|
||||||
export * from "./pages-list-main-content";
|
export * from "./pages-list-main-content";
|
||||||
export * from "./pages-list-view";
|
export * from "./pages-list-view";
|
||||||
|
|||||||
111
web/core/components/pages/version/editor.tsx
Normal file
111
web/core/components/pages/version/editor.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
// plane editor
|
||||||
|
import { DocumentReadOnlyEditorWithRef, TDisplayConfig } from "@plane/editor";
|
||||||
|
// plane types
|
||||||
|
import { IUserLite, TPageVersion } from "@plane/types";
|
||||||
|
// plane ui
|
||||||
|
import { Loader } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import { useMember, useMention, usePage, useUser } from "@/hooks/store";
|
||||||
|
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||||
|
// plane web hooks
|
||||||
|
import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
activeVersion: string | null;
|
||||||
|
isCurrentVersionActive: boolean;
|
||||||
|
versionDetails: TPageVersion | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PagesVersionEditor: React.FC<Props> = 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 (
|
||||||
|
<div className="size-full px-5">
|
||||||
|
<Loader className="relative space-y-4">
|
||||||
|
<Loader.Item width="50%" height="36px" />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="py-2">
|
||||||
|
<Loader.Item width="100%" height="36px" />
|
||||||
|
</div>
|
||||||
|
<Loader.Item width="80%" height="22px" />
|
||||||
|
<div className="relative flex items-center gap-2">
|
||||||
|
<Loader.Item width="30px" height="30px" />
|
||||||
|
<Loader.Item width="30%" height="22px" />
|
||||||
|
</div>
|
||||||
|
<div className="py-2">
|
||||||
|
<Loader.Item width="60%" height="36px" />
|
||||||
|
</div>
|
||||||
|
<Loader.Item width="70%" height="22px" />
|
||||||
|
<Loader.Item width="30%" height="22px" />
|
||||||
|
<div className="relative flex items-center gap-2">
|
||||||
|
<Loader.Item width="30px" height="30px" />
|
||||||
|
<Loader.Item width="30%" height="22px" />
|
||||||
|
</div>
|
||||||
|
<div className="py-2">
|
||||||
|
<Loader.Item width="50%" height="30px" />
|
||||||
|
</div>
|
||||||
|
<Loader.Item width="100%" height="22px" />
|
||||||
|
<div className="py-2">
|
||||||
|
<Loader.Item width="30%" height="30px" />
|
||||||
|
</div>
|
||||||
|
<Loader.Item width="30%" height="22px" />
|
||||||
|
<div className="relative flex items-center gap-2">
|
||||||
|
<div className="py-2">
|
||||||
|
<Loader.Item width="30px" height="30px" />
|
||||||
|
</div>
|
||||||
|
<Loader.Item width="30%" height="22px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Loader>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentReadOnlyEditorWithRef
|
||||||
|
id={activeVersion ?? ""}
|
||||||
|
initialValue={(isCurrentVersionActive ? description_html : versionDetails?.description_html) ?? "<p></p>"}
|
||||||
|
containerClassName="p-0 pb-64 border-none"
|
||||||
|
displayConfig={displayConfig}
|
||||||
|
editorClassName="pl-10"
|
||||||
|
mentionHandler={{
|
||||||
|
highlights: mentionHighlights,
|
||||||
|
}}
|
||||||
|
embedHandler={{
|
||||||
|
issue: {
|
||||||
|
widgetCallback: issueEmbedProps.widgetCallback,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
6
web/core/components/pages/version/index.ts
Normal file
6
web/core/components/pages/version/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export * from "./editor";
|
||||||
|
export * from "./main-content";
|
||||||
|
export * from "./root";
|
||||||
|
export * from "./sidebar-list-item";
|
||||||
|
export * from "./sidebar-list";
|
||||||
|
export * from "./sidebar-root";
|
||||||
114
web/core/components/pages/version/main-content.tsx
Normal file
114
web/core/components/pages/version/main-content.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { TriangleAlert } from "lucide-react";
|
||||||
|
// 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<TPageVersion | undefined>;
|
||||||
|
handleClose: () => void;
|
||||||
|
handleRestore: (descriptionHTML: string) => Promise<void>;
|
||||||
|
pageId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageVersionsMainContent: React.FC<Props> = observer((props) => {
|
||||||
|
const { activeVersion, fetchVersionDetails, handleClose, handleRestore, pageId } = props;
|
||||||
|
// states
|
||||||
|
const [isRestoring, setIsRestoring] = useState(false);
|
||||||
|
const [isRetrying, setIsRetrying] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: versionDetails,
|
||||||
|
error: versionDetailsError,
|
||||||
|
mutate: mutateVersionDetails,
|
||||||
|
} = 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 ?? "<p></p>")
|
||||||
|
.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));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRetry = async () => {
|
||||||
|
setIsRetrying(true);
|
||||||
|
await mutateVersionDetails();
|
||||||
|
setIsRetrying(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-grow flex flex-col">
|
||||||
|
{versionDetailsError ? (
|
||||||
|
<div className="flex-grow grid place-items-center">
|
||||||
|
<div className="flex flex-col items-center gap-4 text-center">
|
||||||
|
<span className="flex-shrink-0 grid place-items-center size-11 text-custom-text-300">
|
||||||
|
<TriangleAlert className="size-10" />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h6 className="text-lg font-semibold">Something went wrong!</h6>
|
||||||
|
<p className="text-sm text-custom-text-300">The version could not be loaded, please try again.</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="link-primary" onClick={handleRetry} loading={isRetrying}>
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="min-h-14 py-3 px-5 border-b border-custom-border-200 flex items-center justify-between gap-2">
|
||||||
|
<h6 className="text-base font-medium">
|
||||||
|
{isCurrentVersionActive
|
||||||
|
? "Current version"
|
||||||
|
: versionDetails
|
||||||
|
? `${renderFormattedDate(versionDetails.last_saved_at)} ${renderFormattedTime(versionDetails.last_saved_at)}`
|
||||||
|
: "Loading version details"}
|
||||||
|
</h6>
|
||||||
|
{!isCurrentVersionActive && (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
className="flex-shrink-0"
|
||||||
|
onClick={handleRestoreVersion}
|
||||||
|
loading={isRestoring}
|
||||||
|
>
|
||||||
|
{isRestoring ? "Restoring" : "Restore"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="pt-8 h-full overflow-y-scroll vertical-scrollbar scrollbar-sm">
|
||||||
|
<PagesVersionEditor
|
||||||
|
activeVersion={activeVersion}
|
||||||
|
isCurrentVersionActive={isCurrentVersionActive}
|
||||||
|
versionDetails={versionDetails}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
50
web/core/components/pages/version/root.tsx
Normal file
50
web/core/components/pages/version/root.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// plane types
|
||||||
|
import { TPageVersion } from "@plane/types";
|
||||||
|
// components
|
||||||
|
import { PageVersionsMainContent, PageVersionsSidebarRoot } from "@/components/pages";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
activeVersion: string | null;
|
||||||
|
fetchAllVersions: (pageId: string) => Promise<TPageVersion[] | undefined>;
|
||||||
|
fetchVersionDetails: (pageId: string, versionId: string) => Promise<TPageVersion | undefined>;
|
||||||
|
handleRestore: (descriptionHTML: string) => Promise<void>;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
pageId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageVersionsOverlay: React.FC<Props> = (props) => {
|
||||||
|
const { activeVersion, fetchAllVersions, fetchVersionDetails, handleRestore, isOpen, onClose, pageId } = props;
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 z-10 size-full bg-custom-background-100 flex overflow-hidden opacity-0 pointer-events-none transition-opacity",
|
||||||
|
{
|
||||||
|
"opacity-100 pointer-events-auto": isOpen,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PageVersionsMainContent
|
||||||
|
activeVersion={activeVersion}
|
||||||
|
fetchVersionDetails={fetchVersionDetails}
|
||||||
|
handleClose={handleClose}
|
||||||
|
handleRestore={handleRestore}
|
||||||
|
pageId={pageId}
|
||||||
|
/>
|
||||||
|
<PageVersionsSidebarRoot
|
||||||
|
activeVersion={activeVersion}
|
||||||
|
fetchAllVersions={fetchAllVersions}
|
||||||
|
handleClose={handleClose}
|
||||||
|
isOpen={isOpen}
|
||||||
|
pageId={pageId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
48
web/core/components/pages/version/sidebar-list-item.tsx
Normal file
48
web/core/components/pages/version/sidebar-list-item.tsx
Normal file
@@ -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<Props> = observer((props) => {
|
||||||
|
const { href, isActive, version } = props;
|
||||||
|
// store hooks
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
|
// derived values
|
||||||
|
const ownerDetails = getUserDetails(version.owned_by);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className={cn("block p-2 rounded-md w-72 hover:bg-custom-background-80 transition-colors", {
|
||||||
|
"bg-custom-background-80": isActive,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<p className="text-sm font-medium truncate">
|
||||||
|
{renderFormattedDate(version.last_saved_at)} {renderFormattedTime(version.last_saved_at)}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 flex items-center gap-1 text-xs">
|
||||||
|
<Avatar
|
||||||
|
src={ownerDetails?.avatar}
|
||||||
|
name={ownerDetails?.display_name}
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span className="text-custom-text-300">{ownerDetails?.display_name}</span>
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
});
|
||||||
99
web/core/components/pages/version/sidebar-list.tsx
Normal file
99
web/core/components/pages/version/sidebar-list.tsx
Normal file
@@ -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<TPageVersion[] | undefined>;
|
||||||
|
isOpen: boolean;
|
||||||
|
pageId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageVersionsSidebarList: React.FC<Props> = (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 (
|
||||||
|
<div className="mt-4 px-4 h-full flex flex-col space-y-2 overflow-y-scroll vertical-scrollbar scrollbar-sm">
|
||||||
|
<Link
|
||||||
|
href={getVersionLink("current")}
|
||||||
|
className={cn("block p-2 rounded-md w-72 hover:bg-custom-background-80 transition-colors", {
|
||||||
|
"bg-custom-background-80": activeVersion === "current",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<p className="text-sm font-medium">Current version</p>
|
||||||
|
</Link>
|
||||||
|
{versionsListError ? (
|
||||||
|
<div className="h-full grid place-items-center">
|
||||||
|
<div className="flex flex-col items-center gap-4 text-center">
|
||||||
|
<span className="flex-shrink-0 grid place-items-center size-11 text-custom-text-300">
|
||||||
|
<TriangleAlert className="size-10" />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h6 className="text-base font-semibold">Something went wrong!</h6>
|
||||||
|
<p className="text-xs text-custom-text-300">
|
||||||
|
There was a problem while loading previous
|
||||||
|
<br />
|
||||||
|
versions, please try again.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="link-primary" onClick={handleRetry} loading={isRetrying}>
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : versionsList ? (
|
||||||
|
versionsList.map((version) => (
|
||||||
|
<PlaneVersionsSidebarListItem
|
||||||
|
key={version.id}
|
||||||
|
href={getVersionLink(version.id)}
|
||||||
|
isActive={activeVersion === version.id}
|
||||||
|
version={version}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Loader className="space-y-4">
|
||||||
|
<Loader.Item height="56px" />
|
||||||
|
<Loader.Item height="56px" />
|
||||||
|
<Loader.Item height="56px" />
|
||||||
|
<Loader.Item height="56px" />
|
||||||
|
<Loader.Item height="56px" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
38
web/core/components/pages/version/sidebar-root.tsx
Normal file
38
web/core/components/pages/version/sidebar-root.tsx
Normal file
@@ -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<TPageVersion[] | undefined>;
|
||||||
|
handleClose: () => void;
|
||||||
|
isOpen: boolean;
|
||||||
|
pageId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageVersionsSidebarRoot: React.FC<Props> = (props) => {
|
||||||
|
const { activeVersion, fetchAllVersions, handleClose, isOpen, pageId } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-shrink-0 py-4 border-l border-custom-border-200 flex flex-col">
|
||||||
|
<div className="px-6 flex items-center justify-between gap-2">
|
||||||
|
<h5 className="text-base font-semibold">Version history</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
className="flex-shrink-0 size-6 grid place-items-center text-custom-text-300 hover:text-custom-text-100 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<PageVersionsSidebarList
|
||||||
|
activeVersion={activeVersion}
|
||||||
|
fetchAllVersions={fetchAllVersions}
|
||||||
|
isOpen={isOpen}
|
||||||
|
pageId={pageId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
// plane editor
|
||||||
import {
|
import {
|
||||||
EditorRefApi,
|
EditorRefApi,
|
||||||
proseMirrorJSONToBinaryString,
|
proseMirrorJSONToBinaryString,
|
||||||
applyUpdates,
|
applyUpdates,
|
||||||
generateJSONfromHTMLForDocumentEditor,
|
generateJSONfromHTMLForDocumentEditor,
|
||||||
} from "@plane/editor";
|
} from "@plane/editor";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||||
import useAutoSave from "@/hooks/use-auto-save";
|
import useAutoSave from "@/hooks/use-auto-save";
|
||||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import { ProjectPageService } from "@/services/page";
|
import { ProjectPageService } from "@/services/page";
|
||||||
|
// store
|
||||||
import { IPage } from "@/store/pages/page";
|
import { IPage } from "@/store/pages/page";
|
||||||
|
|
||||||
const projectPageService = new ProjectPageService();
|
const projectPageService = new ProjectPageService();
|
||||||
@@ -183,6 +182,19 @@ export const usePageDescription = (props: Props) => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const manuallyUpdateDescription = async (descriptionHTML: string) => {
|
||||||
|
const { contentJSON, editorSchema } = generateJSONfromHTMLForDocumentEditor(descriptionHTML ?? "<p></p>");
|
||||||
|
const yDocBinaryString = proseMirrorJSONToBinaryString(contentJSON, "default", editorSchema);
|
||||||
|
|
||||||
|
try {
|
||||||
|
editorRef.current?.clearEditor(true);
|
||||||
|
await updateDescription(yDocBinaryString, descriptionHTML ?? "<p></p>");
|
||||||
|
await mutateDescriptionYJS();
|
||||||
|
} catch (error) {
|
||||||
|
console.log("error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useAutoSave(handleSaveDescription);
|
useAutoSave(handleSaveDescription);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -190,5 +202,6 @@ export const usePageDescription = (props: Props) => {
|
|||||||
isDescriptionReady,
|
isDescriptionReady,
|
||||||
pageDescriptionYJS,
|
pageDescriptionYJS,
|
||||||
handleSaveDescription,
|
handleSaveDescription,
|
||||||
|
manuallyUpdateDescription,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
39
web/core/hooks/use-query-params.ts
Normal file
39
web/core/hooks/use-query-params.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { useSearchParams, usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
type TParamsToAdd = {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useQueryParams = () => {
|
||||||
|
// next navigation
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const updateQueryParams = ({
|
||||||
|
paramsToAdd = {},
|
||||||
|
paramsToRemove = [],
|
||||||
|
}: {
|
||||||
|
paramsToAdd?: TParamsToAdd;
|
||||||
|
paramsToRemove?: string[];
|
||||||
|
}) => {
|
||||||
|
const currentParams = new URLSearchParams(searchParams.toString());
|
||||||
|
|
||||||
|
// add or update query parameters
|
||||||
|
Object.keys(paramsToAdd).forEach((key) => {
|
||||||
|
currentParams.set(key, paramsToAdd[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove specified query parameters
|
||||||
|
paramsToRemove.forEach((key) => {
|
||||||
|
currentParams.delete(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
// construct the new route with the updated query parameters
|
||||||
|
const newRoute = `${pathname}?${currentParams.toString()}`;
|
||||||
|
return newRoute;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateQueryParams,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
export * from "./project-page-version.service";
|
||||||
export * from "./project-page.service";
|
export * from "./project-page.service";
|
||||||
|
|||||||
33
web/core/services/page/project-page-version.service.ts
Normal file
33
web/core/services/page/project-page-version.service.ts
Normal file
@@ -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<TPageVersion[]> {
|
||||||
|
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<TPageVersion> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/versions/${versionId}/`)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user