Compare commits

...

3 Commits

Author SHA1 Message Date
Aaryan Khandelwal
afdb3214c5 refactor: issue details root 2024-11-28 16:00:50 +05:30
Aaryan Khandelwal
3b7fbde6b9 Merge branch 'preview' of https://github.com/makeplane/plane into feat/issue-version-history 2024-11-28 13:52:48 +05:30
Aaryan Khandelwal
eb9d84a65a feat: issue version history 2024-11-14 20:03:52 +05:30
18 changed files with 264 additions and 80 deletions

View File

@@ -49,7 +49,7 @@ export type TPageFilters = {
export type TPageEmbedType = "mention" | "issue";
export type TPageVersion = {
export type TEditorVersion = {
created_at: string;
created_by: string;
deleted_at: string | null;
@@ -59,7 +59,6 @@ export type TPageVersion = {
id: string;
last_saved_at: string;
owned_by: string;
page: string;
updated_at: string;
updated_by: string;
workspace: string;

View File

@@ -1,3 +1,4 @@
export * from "./lite-text-editor";
export * from "./pdf";
export * from "./rich-text-editor";
export * from "./version-history";

View File

@@ -0,0 +1,11 @@
import { TEditorVersion } from "@plane/types";
export type TVersionEditorProps = {
activeVersion: string | null;
currentVersionDescription: string | null;
isCurrentVersionActive: boolean;
versionDetails: TEditorVersion | undefined;
};
export * from "./issue-version-editor";
export * from "./page-version-editor";

View File

@@ -0,0 +1,70 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane ui
import { Loader } from "@plane/ui";
// components
import { RichTextReadOnlyEditor } from "@/components/editor";
// local types
import { TVersionEditorProps } from ".";
export const IssueVersionEditor: React.FC<TVersionEditorProps> = observer((props) => {
const { activeVersion, currentVersionDescription, isCurrentVersionActive, versionDetails } = props;
// params
const { workspaceSlug, projectId } = useParams();
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>
);
const description = isCurrentVersionActive ? currentVersionDescription : versionDetails?.description_html;
if (description === undefined || description?.trim() === "") return null;
return (
<RichTextReadOnlyEditor
id={activeVersion ?? ""}
initialValue={description ?? "<p></p>"}
containerClassName="p-0 pb-64 border-none"
editorClassName="pl-10"
workspaceSlug={workspaceSlug?.toString() ?? ""}
projectId={projectId?.toString() ?? ""}
/>
);
});

View File

@@ -1,27 +1,21 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane editor
import { DocumentReadOnlyEditorWithRef, TDisplayConfig } from "@plane/editor";
import { DocumentReadOnlyEditorWithRef } from "@plane/editor";
// plane types
import { IUserLite, TPageVersion } from "@plane/types";
import { IUserLite } from "@plane/types";
// plane ui
import { Loader } from "@plane/ui";
// helpers
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
// hooks
import { useMember, useMention, useUser } from "@/hooks/store";
import { usePageFilters } from "@/hooks/use-page-filters";
// plane web hooks
import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed";
// local types
import { TVersionEditorProps } from ".";
export type TVersionEditorProps = {
activeVersion: string | null;
currentVersionDescription: string | null;
isCurrentVersionActive: boolean;
versionDetails: TPageVersion | undefined;
};
export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props) => {
export const PageVersionEditor: React.FC<TVersionEditorProps> = observer((props) => {
const { activeVersion, currentVersionDescription, isCurrentVersionActive, versionDetails } = props;
// params
const { workspaceSlug, projectId } = useParams();
@@ -43,13 +37,6 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
members: projectMemberDetails,
user: currentUser ?? undefined,
});
// page filters
const { fontSize, fontStyle } = usePageFilters();
const displayConfig: TDisplayConfig = {
fontSize,
fontStyle,
};
if (!isCurrentVersionActive && !versionDetails)
return (
@@ -101,7 +88,6 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
id={activeVersion ?? ""}
initialValue={description ?? "<p></p>"}
containerClassName="p-0 pb-64 border-none"
displayConfig={displayConfig}
editorClassName="pl-10"
fileHandler={getReadOnlyEditorFileHandlers({
projectId: projectId?.toString() ?? "",

View File

@@ -1,4 +1,4 @@
export * from "./editor";
export * from "./editors";
export * from "./main-content";
export * from "./root";
export * from "./sidebar-list-item";

View File

@@ -3,34 +3,34 @@ import { observer } from "mobx-react";
import useSWR from "swr";
import { TriangleAlert } from "lucide-react";
// plane types
import { TPageVersion } from "@plane/types";
import { TEditorVersion } from "@plane/types";
// plane ui
import { Button, setToast, TOAST_TYPE } from "@plane/ui";
// components
import { TVersionEditorProps } from "@/components/pages";
// helpers
import { renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper";
// local types
import { TVersionEditorProps } from ".";
type Props = {
activeVersion: string | null;
currentVersionDescription: string | null;
editorComponent: React.FC<TVersionEditorProps>;
fetchVersionDetails: (pageId: string, versionId: string) => Promise<TPageVersion | undefined>;
entityId: string;
fetchVersionDetails: (entityId: string, versionId: string) => Promise<TEditorVersion | undefined>;
handleClose: () => void;
handleRestore: (descriptionHTML: string) => Promise<void>;
pageId: string;
restoreEnabled: boolean;
};
export const PageVersionsMainContent: React.FC<Props> = observer((props) => {
export const EditorVersionHistoryMainContent: React.FC<Props> = observer((props) => {
const {
activeVersion,
currentVersionDescription,
editorComponent,
entityId,
fetchVersionDetails,
handleClose,
handleRestore,
pageId,
restoreEnabled,
} = props;
// states
@@ -42,8 +42,8 @@ export const PageVersionsMainContent: React.FC<Props> = observer((props) => {
error: versionDetailsError,
mutate: mutateVersionDetails,
} = useSWR(
pageId && activeVersion && activeVersion !== "current" ? `PAGE_VERSION_${activeVersion}` : null,
pageId && activeVersion && activeVersion !== "current" ? () => fetchVersionDetails(pageId, activeVersion) : null
entityId && activeVersion && activeVersion !== "current" ? `EDITOR_VERSION_${activeVersion}` : null,
entityId && activeVersion && activeVersion !== "current" ? () => fetchVersionDetails(entityId, activeVersion) : null
);
const isCurrentVersionActive = activeVersion === "current";
@@ -55,14 +55,14 @@ export const PageVersionsMainContent: React.FC<Props> = observer((props) => {
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Page version restored.",
title: "Version restored.",
});
handleClose();
})
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Failed to restore page version.",
title: "Failed to restore version.",
})
)
.finally(() => setIsRestoring(false));

View File

@@ -1,35 +1,35 @@
import { observer } from "mobx-react";
// plane types
import { TPageVersion } from "@plane/types";
// components
import { PageVersionsMainContent, PageVersionsSidebarRoot, TVersionEditorProps } from "@/components/pages";
import { TEditorVersion } from "@plane/types";
// helpers
import { cn } from "@/helpers/common.helper";
// local components
import { EditorVersionHistoryMainContent, EditorVersionHistorySidebarRoot, TVersionEditorProps } from ".";
type Props = {
activeVersion: string | null;
currentVersionDescription: string | null;
editorComponent: React.FC<TVersionEditorProps>;
fetchAllVersions: (pageId: string) => Promise<TPageVersion[] | undefined>;
fetchVersionDetails: (pageId: string, versionId: string) => Promise<TPageVersion | undefined>;
entityId: string;
fetchAllVersions: (entityId: string) => Promise<TEditorVersion[] | undefined>;
fetchVersionDetails: (entityId: string, versionId: string) => Promise<TEditorVersion | undefined>;
handleRestore: (descriptionHTML: string) => Promise<void>;
isOpen: boolean;
onClose: () => void;
pageId: string;
restoreEnabled: boolean;
};
export const PageVersionsOverlay: React.FC<Props> = observer((props) => {
export const EditorVersionHistoryOverlay: React.FC<Props> = observer((props) => {
const {
activeVersion,
currentVersionDescription,
editorComponent,
entityId,
fetchAllVersions,
fetchVersionDetails,
handleRestore,
isOpen,
onClose,
pageId,
restoreEnabled,
} = props;
@@ -46,22 +46,22 @@ export const PageVersionsOverlay: React.FC<Props> = observer((props) => {
}
)}
>
<PageVersionsMainContent
<EditorVersionHistoryMainContent
activeVersion={activeVersion}
currentVersionDescription={currentVersionDescription}
editorComponent={editorComponent}
entityId={entityId}
fetchVersionDetails={fetchVersionDetails}
handleClose={handleClose}
handleRestore={handleRestore}
pageId={pageId}
restoreEnabled={restoreEnabled}
/>
<PageVersionsSidebarRoot
<EditorVersionHistorySidebarRoot
activeVersion={activeVersion}
entityId={entityId}
fetchAllVersions={fetchAllVersions}
handleClose={handleClose}
isOpen={isOpen}
pageId={pageId}
/>
</div>
);

View File

@@ -1,7 +1,7 @@
import { observer } from "mobx-react";
import Link from "next/link";
// plane types
import { TPageVersion } from "@plane/types";
import { TEditorVersion } from "@plane/types";
// plane ui
import { Avatar } from "@plane/ui";
// helpers
@@ -14,10 +14,10 @@ import { useMember } from "@/hooks/store";
type Props = {
href: string;
isActive: boolean;
version: TPageVersion;
version: TEditorVersion;
};
export const PlaneVersionsSidebarListItem: React.FC<Props> = observer((props) => {
export const EditorVersionHistorySidebarListItem: React.FC<Props> = observer((props) => {
const { href, isActive, version } = props;
// store hooks
const { getUserDetails } = useMember();

View File

@@ -3,25 +3,25 @@ import Link from "next/link";
import useSWR from "swr";
import { TriangleAlert } from "lucide-react";
// plane types
import { TPageVersion } from "@plane/types";
import { TEditorVersion } 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";
// local components
import { EditorVersionHistorySidebarListItem } from ".";
type Props = {
activeVersion: string | null;
fetchAllVersions: (pageId: string) => Promise<TPageVersion[] | undefined>;
entityId: string;
fetchAllVersions: (entityId: string) => Promise<TEditorVersion[] | undefined>;
isOpen: boolean;
pageId: string;
};
export const PageVersionsSidebarList: React.FC<Props> = (props) => {
const { activeVersion, fetchAllVersions, isOpen, pageId } = props;
export const EditorVersionHistorySidebarList: React.FC<Props> = (props) => {
const { activeVersion, entityId, fetchAllVersions, isOpen } = props;
// states
const [isRetrying, setIsRetrying] = useState(false);
// update query params
@@ -32,8 +32,8 @@ export const PageVersionsSidebarList: React.FC<Props> = (props) => {
error: versionsListError,
mutate: mutateVersionsList,
} = useSWR(
pageId && isOpen ? `PAGE_VERSIONS_LIST_${pageId}` : null,
pageId && isOpen ? () => fetchAllVersions(pageId) : null
entityId && isOpen ? `EDITOR_VERSIONS_LIST_${entityId}` : null,
entityId && isOpen ? () => fetchAllVersions(entityId) : null
);
const handleRetry = async () => {
@@ -78,7 +78,7 @@ export const PageVersionsSidebarList: React.FC<Props> = (props) => {
</div>
) : versionsList ? (
versionsList.map((version) => (
<PlaneVersionsSidebarListItem
<EditorVersionHistorySidebarListItem
key={version.id}
href={getVersionLink(version.id)}
isActive={activeVersion === version.id}

View File

@@ -1,19 +1,19 @@
import { X } from "lucide-react";
// plane types
import { TPageVersion } from "@plane/types";
// components
import { PageVersionsSidebarList } from "@/components/pages";
import { TEditorVersion } from "@plane/types";
// local components
import { EditorVersionHistorySidebarList } from ".";
type Props = {
activeVersion: string | null;
fetchAllVersions: (pageId: string) => Promise<TPageVersion[] | undefined>;
entityId: string;
fetchAllVersions: (entityId: string) => Promise<TEditorVersion[] | undefined>;
handleClose: () => void;
isOpen: boolean;
pageId: string;
};
export const PageVersionsSidebarRoot: React.FC<Props> = (props) => {
const { activeVersion, fetchAllVersions, handleClose, isOpen, pageId } = props;
export const EditorVersionHistorySidebarRoot: React.FC<Props> = (props) => {
const { activeVersion, entityId, fetchAllVersions, handleClose, isOpen } = props;
return (
<div className="flex-shrink-0 py-4 border-l border-custom-border-200 flex flex-col">
@@ -27,11 +27,11 @@ export const PageVersionsSidebarRoot: React.FC<Props> = (props) => {
<X className="size-4" />
</button>
</div>
<PageVersionsSidebarList
<EditorVersionHistorySidebarList
activeVersion={activeVersion}
entityId={entityId}
fetchAllVersions={fetchAllVersions}
isOpen={isOpen}
pageId={pageId}
/>
</div>
);

View File

@@ -4,6 +4,7 @@ export * from "./links";
export * from "./parent";
export * from "./reactions";
export * from "./cycle-select";
export * from "./issue-detail-quick-actions";
export * from "./main-content";
export * from "./module-select";
export * from "./parent-select";
@@ -11,4 +12,4 @@ export * from "./relation-select";
export * from "./root";
export * from "./sidebar";
export * from "./subscription";
export * from "./issue-detail-quick-actions";
export * from "./version-history";

View File

@@ -175,7 +175,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
},
path: pathname,
});
} catch (error) {
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -204,7 +204,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
},
path: pathname,
});
} catch (error) {
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -245,7 +245,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
},
path: pathname,
});
} catch (error) {
} catch {
captureIssueEvent({
eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue detail page" },
@@ -281,7 +281,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
},
path: pathname,
});
} catch (error) {
} catch {
captureIssueEvent({
eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
@@ -373,7 +373,6 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
</div>
</div>
)}
{/* peek overview */}
<IssuePeekOverview />
</>

View File

@@ -0,0 +1,82 @@
"use client";
import { useEffect, useState } from "react";
import { useParams, useRouter, useSearchParams } from "next/navigation";
// plane editor
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
// components
import { EditorVersionHistoryOverlay, IssueVersionEditor } from "@/components/editor";
// hooks
import { useQueryParams } from "@/hooks/use-query-params";
// services
import { IssueVersionService } from "@/services/issue";
const issueVersionService = new IssueVersionService();
type Props = {
disabled: boolean;
editorRef: EditorRefApi;
issueId: string;
readOnlyEditorRef: EditorReadOnlyRefApi;
};
export const IssueVersionHistory: React.FC<Props> = (props) => {
const { disabled, editorRef, issueId, readOnlyEditorRef } = props;
// states
const [isVersionsOverlayOpen, setIsVersionsOverlayOpen] = useState(false);
// search params
const searchParams = useSearchParams();
// params
const { projectId, workspaceSlug } = useParams();
// router
const router = useRouter();
// update query params
const { updateQueryParams } = useQueryParams();
const version = searchParams.get("version");
useEffect(() => {
if (!version) {
setIsVersionsOverlayOpen(false);
return;
}
setIsVersionsOverlayOpen(true);
}, [version]);
const handleCloseVersionsOverlay = () => {
const updatedRoute = updateQueryParams({
paramsToRemove: ["version"],
});
router.push(updatedRoute);
};
const handleRestoreVersion = async (descriptionHTML: string) => {
editorRef?.clearEditor();
editorRef?.setEditorValue(descriptionHTML);
};
const currentVersionDescription = disabled ? readOnlyEditorRef?.getDocument().html : editorRef?.getDocument().html;
return (
<EditorVersionHistoryOverlay
activeVersion={version}
currentVersionDescription={currentVersionDescription ?? null}
editorComponent={IssueVersionEditor}
entityId={issueId}
fetchAllVersions={async (issueId) => {
if (!workspaceSlug || !projectId) return;
return await issueVersionService.fetchAllVersions(workspaceSlug.toString(), projectId.toString(), issueId);
}}
fetchVersionDetails={async (issueId, versionId) => {
if (!workspaceSlug || !projectId) return;
return await issueVersionService.fetchVersionById(
workspaceSlug.toString(),
projectId.toString(),
issueId,
versionId
);
}}
handleRestore={handleRestoreVersion}
isOpen={isVersionsOverlayOpen}
onClose={handleCloseVersionsOverlay}
restoreEnabled={!disabled}
/>
);
};

View File

@@ -8,7 +8,8 @@ import { TPage } from "@plane/types";
// ui
import { setToast, TOAST_TYPE } from "@plane/ui";
// components
import { PageEditorHeaderRoot, PageEditorBody, PageVersionsOverlay, PagesVersionEditor } from "@/components/pages";
import { EditorVersionHistoryOverlay, PageVersionEditor } from "@/components/editor";
import { PageEditorHeaderRoot, PageEditorBody } from "@/components/pages";
// hooks
import { useProjectPages } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
@@ -105,10 +106,11 @@ export const PageRoot = observer((props: TPageRootProps) => {
return (
<>
<PageVersionsOverlay
<EditorVersionHistoryOverlay
activeVersion={version}
currentVersionDescription={currentVersionDescription ?? null}
editorComponent={PagesVersionEditor}
editorComponent={PageVersionEditor}
entityId={page.id ?? ""}
fetchAllVersions={async (pageId) => {
if (!workspaceSlug || !projectId) return;
return await projectPageVersionService.fetchAllVersions(
@@ -129,7 +131,6 @@ export const PageRoot = observer((props: TPageRootProps) => {
handleRestore={handleRestoreVersion}
isOpen={isVersionsOverlayOpen}
onClose={handleCloseVersionsOverlay}
pageId={page.id ?? ""}
restoreEnabled={isContentEditable}
/>
<PageEditorHeaderRoot

View File

@@ -7,4 +7,5 @@ export * from "./issue_attachment.service";
export * from "./issue_activity.service";
export * from "./issue_comment.service";
export * from "./issue_relation.service";
export * from "./issue_version.service";
export * from "./workspace_draft.service";

View File

@@ -0,0 +1,33 @@
// plane types
import { TEditorVersion } from "@plane/types";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { APIService } from "@/services/api.service";
export class IssueVersionService extends APIService {
constructor() {
super(API_BASE_URL);
}
async fetchAllVersions(workspaceSlug: string, projectId: string, issueId: string): Promise<TEditorVersion[]> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/versions/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async fetchVersionById(
workspaceSlug: string,
projectId: string,
issueId: string,
versionId: string
): Promise<TEditorVersion> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/versions/${versionId}/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}

View File

@@ -1,5 +1,5 @@
// plane types
import { TPageVersion } from "@plane/types";
import { TEditorVersion } from "@plane/types";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// services
@@ -10,7 +10,7 @@ export class ProjectPageVersionService extends APIService {
super(API_BASE_URL);
}
async fetchAllVersions(workspaceSlug: string, projectId: string, pageId: string): Promise<TPageVersion[]> {
async fetchAllVersions(workspaceSlug: string, projectId: string, pageId: string): Promise<TEditorVersion[]> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/versions/`)
.then((response) => response?.data)
.catch((error) => {
@@ -23,7 +23,7 @@ export class ProjectPageVersionService extends APIService {
projectId: string,
pageId: string,
versionId: string
): Promise<TPageVersion> {
): Promise<TEditorVersion> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/versions/${versionId}/`)
.then((response) => response?.data)
.catch((error) => {