mirror of
https://github.com/makeplane/plane.git
synced 2026-07-12 13:29:56 +02:00
feat: version history for issues
This commit is contained in:
@@ -61,8 +61,7 @@ export const IssueVersionEditor: React.FC<TVersionEditorProps> = observer((props
|
||||
<RichTextReadOnlyEditor
|
||||
id={activeVersion ?? ""}
|
||||
initialValue={description ?? "<p></p>"}
|
||||
containerClassName="p-0 pb-64 border-none"
|
||||
editorClassName="pl-10"
|
||||
containerClassName="!px-5 pb-64 border-none"
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
/>
|
||||
|
||||
@@ -4,13 +4,14 @@ import { FC, useCallback, useRef } from "react";
|
||||
import debounce from "lodash/debounce";
|
||||
import { observer } from "mobx-react";
|
||||
// plane editor
|
||||
import { convertBinaryDataToBase64String, EditorRefApi } from "@plane/editor";
|
||||
import { convertBinaryDataToBase64String, EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
|
||||
// types
|
||||
import { EFileAssetType } from "@plane/types/src/enums";
|
||||
// plane ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { CollaborativeRichTextEditor, CollaborativeRichTextReadOnlyEditor } from "@/components/editor";
|
||||
import { IssueVersionHistory } from "@/components/issues";
|
||||
// helpers
|
||||
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
@@ -49,6 +50,7 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
||||
} = props;
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
const readOnlyEditorRef = useRef<EditorReadOnlyRefApi>(null);
|
||||
// store hooks
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
@@ -95,7 +97,7 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{!disabled ? (
|
||||
<CollaborativeRichTextEditor
|
||||
key={issueId}
|
||||
@@ -132,6 +134,7 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
||||
/>
|
||||
) : (
|
||||
<CollaborativeRichTextReadOnlyEditor
|
||||
ref={readOnlyEditorRef}
|
||||
containerClassName={containerClassName}
|
||||
descriptionBinary={savedDescriptionBinary}
|
||||
descriptionHTML={descriptionHTML}
|
||||
@@ -140,6 +143,13 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{/* version history overlay */}
|
||||
<IssueVersionHistory
|
||||
disabled={!!disabled}
|
||||
editorRef={editorRef.current}
|
||||
issueId={issueId}
|
||||
readOnlyEditorRef={readOnlyEditorRef.current}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ArchiveIcon, ArchiveRestoreIcon, LinkIcon, Trash2 } from "lucide-react";
|
||||
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
import { ArchiveIcon, ArchiveRestoreIcon, History, LinkIcon, Trash2 } from "lucide-react";
|
||||
// plane ui
|
||||
import { CustomMenu, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { ArchiveIssueModal, DeleteIssueModal, IssueSubscription } from "@/components/issues";
|
||||
// constants
|
||||
@@ -25,6 +26,8 @@ import {
|
||||
} from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { useQueryParams } from "@/hooks/use-query-params";
|
||||
// plane web constants
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
type Props = {
|
||||
@@ -35,15 +38,12 @@ type Props = {
|
||||
|
||||
export const IssueDetailQuickActions: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId } = props;
|
||||
|
||||
// states
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const [archiveIssueModal, setArchiveIssueModal] = useState(false);
|
||||
const [isRestoring, setIsRestoring] = useState(false);
|
||||
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
|
||||
// hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
@@ -62,13 +62,11 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
|
||||
} = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const pathname = usePathname();
|
||||
|
||||
const { updateQueryParams } = useQueryParams();
|
||||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
if (!issue) return <></>;
|
||||
|
||||
const stateDetails = getStateById(issue.state_id);
|
||||
|
||||
// handlers
|
||||
const handleCopyText = () => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
@@ -80,7 +78,6 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteIssue = async () => {
|
||||
try {
|
||||
const deleteIssue = issue?.archived_at ? removeArchivedIssue : removeIssue;
|
||||
@@ -109,7 +106,6 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleArchiveIssue = async () => {
|
||||
try {
|
||||
await archiveIssue(workspaceSlug, projectId, issueId).then(() => {
|
||||
@@ -128,7 +124,6 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestore = async () => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
@@ -153,6 +148,21 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
|
||||
.finally(() => setIsRestoring(false));
|
||||
};
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
// auth
|
||||
const isEditable = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT);
|
||||
const canRestoreIssue = allowPermissions(
|
||||
@@ -239,7 +249,6 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isEditable && (
|
||||
<Tooltip tooltipContent="Delete" isMobile={isMobile}>
|
||||
<button
|
||||
@@ -251,6 +260,17 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<CustomMenu maxHeight="lg" placement="bottom-start" verticalEllipsis closeOnSelect>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
if (!item.shouldRender) return null;
|
||||
return (
|
||||
<CustomMenu.MenuItem key={item.key} onClick={item.action} className="flex items-center gap-2">
|
||||
<item.icon className="h-3 w-3" />
|
||||
{item.label}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TIssue } from "@plane/types";
|
||||
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { EmptyState } from "@/components/common";
|
||||
import { IssuePeekOverview, IssueVersionHistory } from "@/components/issues";
|
||||
import { IssuePeekOverview } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED } from "@/constants/event-tracker";
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
@@ -344,7 +344,6 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
pathname,
|
||||
]
|
||||
);
|
||||
|
||||
// issue details
|
||||
const issue = getIssueById(issueId);
|
||||
// checking if issue is editable, based on user role
|
||||
@@ -390,8 +389,6 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
)}
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
{/* version history overlay */}
|
||||
<IssueVersionHistory />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -14,9 +14,9 @@ const issueVersionService = new IssueVersionService();
|
||||
|
||||
type Props = {
|
||||
disabled: boolean;
|
||||
editorRef: EditorRefApi;
|
||||
editorRef: EditorRefApi | null;
|
||||
issueId: string;
|
||||
readOnlyEditorRef: EditorReadOnlyRefApi;
|
||||
readOnlyEditorRef: EditorReadOnlyRefApi | null;
|
||||
};
|
||||
|
||||
export const IssueVersionHistory: React.FC<Props> = (props) => {
|
||||
|
||||
@@ -4,6 +4,5 @@ export * from "./header";
|
||||
export * from "./list";
|
||||
export * from "./loaders";
|
||||
export * from "./modals";
|
||||
export * from "./version";
|
||||
export * from "./pages-list-main-content";
|
||||
export * from "./pages-list-view";
|
||||
|
||||
Reference in New Issue
Block a user