From c99579cddc8a673052ffc02ce4b7b2b0fc75bea8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Sat, 15 Jun 2024 18:12:18 +0530 Subject: [PATCH] [WEB-1600] chore: issue detail ui enhancement (#4832) * chore: archived issue header consistency * chore: restor banner removed from issue detail page * chore: issue detail quick action component added * chore: moved sidebar issue quick action to app header --- .../(detail)/[projectId]/archives/header.tsx | 2 +- .../(detail)/[archivedIssueId]/page.tsx | 65 +---- .../archives/issues/(detail)/header.tsx | 8 +- .../[projectId]/issues/(detail)/header.tsx | 6 + .../components/issues/issue-detail/index.ts | 1 + .../issue-detail-quick-actions.tsx | 250 ++++++++++++++++++ .../components/issues/issue-detail/root.tsx | 3 +- .../issues/issue-detail/sidebar.tsx | 125 +-------- 8 files changed, 276 insertions(+), 184 deletions(-) create mode 100644 web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/header.tsx index 2d8f80e2a3..43119ccf83 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/header.tsx @@ -38,7 +38,7 @@ export const ProjectArchivesHeader: FC = observer(() => { PROJECT_ARCHIVES_BREADCRUMB_LIST[activeTab as keyof typeof PROJECT_ARCHIVES_BREADCRUMB_LIST]; return ( -
+
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx index bc578bf5b4..74dbb76491 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx @@ -1,40 +1,28 @@ "use client"; -import { useState } from "react"; import { observer } from "mobx-react"; -import { useParams, useRouter } from "next/navigation"; +import { useParams } from "next/navigation"; import useSWR from "swr"; -// icons -import { ArchiveRestoreIcon } from "lucide-react"; // ui -import { ArchiveIcon, Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; +import { Loader } from "@plane/ui"; // components import { PageHead } from "@/components/core"; import { IssueDetailRoot } from "@/components/issues"; // constants -import { EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useIssueDetail, useIssues, useProject, useUser } from "@/hooks/store"; +import { useIssueDetail, useProject } from "@/hooks/store"; const ArchivedIssueDetailsPage = observer(() => { // router - const router = useRouter(); const { workspaceSlug, projectId, archivedIssueId } = useParams(); // states - const [isRestoring, setIsRestoring] = useState(false); // hooks const { fetchIssue, issue: { getIssueById }, } = useIssueDetail(); - const { - issues: { restoreIssue }, - } = useIssues(EIssuesStoreType.ARCHIVED); + const { getProjectById } = useProject(); - const { - membership: { currentProjectRole }, - } = useUser(); const { isLoading, data: swrArchivedIssueDetails } = useSWR( workspaceSlug && projectId && archivedIssueId @@ -49,35 +37,9 @@ const ArchivedIssueDetailsPage = observer(() => { const issue = archivedIssueId ? getIssueById(archivedIssueId.toString()) : undefined; const project = issue ? getProjectById(issue?.project_id ?? "") : undefined; const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined; - // auth - const canRestoreIssue = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; if (!issue) return <>; - const handleRestore = async () => { - if (!workspaceSlug || !projectId || !archivedIssueId) return; - - setIsRestoring(true); - - await restoreIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString()) - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Restore success", - message: "Your issue can be found in project issues.", - }); - router.push(`/${workspaceSlug}/projects/${projectId}/issues/${archivedIssueId}`); - }) - .catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Issue could not be restored. Please try again.", - }); - }) - .finally(() => setIsRestoring(false)); - }; - const issueLoader = !issue || isLoading ? true : false; return ( @@ -101,23 +63,6 @@ const ArchivedIssueDetailsPage = observer(() => { ) : (
- {issue?.archived_at && canRestoreIssue && ( -
-
- -

This issue has been archived.

-
- -
- )} {workspaceSlug && projectId && archivedIssueId && ( { ); }); -export default ArchivedIssueDetailsPage; \ No newline at end of file +export default ArchivedIssueDetailsPage; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/header.tsx index 1ea1839264..9190a7c0bd 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/header.tsx @@ -7,6 +7,7 @@ import useSWR from "swr"; import { ArchiveIcon, Breadcrumbs, LayersIcon } from "@plane/ui"; // components import { BreadcrumbLink, Logo } from "@/components/common"; +import { IssueDetailQuickActions } from "@/components/issues"; // constants import { ISSUE_DETAILS } from "@/constants/fetch-keys"; // hooks @@ -35,7 +36,7 @@ export const ProjectArchivedIssueDetailsHeader = observer(() => { ); return ( -
+
@@ -90,6 +91,11 @@ export const ProjectArchivedIssueDetailsHeader = observer(() => {
+
); }); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/header.tsx index 645013378e..ab6d3be7cd 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/header.tsx @@ -7,6 +7,7 @@ import { PanelRight } from "lucide-react"; import { Breadcrumbs, LayersIcon } from "@plane/ui"; // components import { BreadcrumbLink, Logo } from "@/components/common"; +import { IssueDetailQuickActions } from "@/components/issues"; // helpers import { cn } from "@/helpers/common.helper"; // hooks @@ -74,6 +75,11 @@ export const ProjectIssueDetailsHeader = observer(() => {
+ + + {issue?.archived_at && canRestoreIssue ? ( + <> + + + + + ) : ( + <> + {isArchivingAllowed && ( + + + + )} + + )} + + {isEditable && ( + + + + )} +
+
+
+ + ); +}); diff --git a/web/core/components/issues/issue-detail/root.tsx b/web/core/components/issues/issue-detail/root.tsx index 1d528b200e..9c6d0abbed 100644 --- a/web/core/components/issues/issue-detail/root.tsx +++ b/web/core/components/issues/issue-detail/root.tsx @@ -391,7 +391,7 @@ export const IssueDetailRoot: FC = observer((props) => { />
= observer((props) => { projectId={projectId} issueId={issueId} issueOperations={issueOperations} - is_archived={is_archived} isEditable={!is_archived && isEditable} />
diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index d92cbfbbe7..d8ee6f77eb 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -1,34 +1,22 @@ "use client"; -import React, { useState } from "react"; +import React from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/navigation"; import { CalendarCheck2, CalendarClock, CircleDot, CopyPlus, LayoutPanelTop, - LinkIcon, Signal, Tag, - Trash2, Triangle, Users, XCircle, } from "lucide-react"; // hooks // components -import { - ArchiveIcon, - ContrastIcon, - DiceIcon, - DoubleCircleIcon, - RelatedIcon, - TOAST_TYPE, - Tooltip, - setToast, -} from "@plane/ui"; +import { ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon } from "@plane/ui"; import { DateDropdown, EstimateDropdown, @@ -39,8 +27,6 @@ import { // ui // helpers import { - ArchiveIssueModal, - DeleteIssueModal, IssueCycleSelect, IssueLabel, IssueLinkRoot, @@ -50,17 +36,13 @@ import { } from "@/components/issues"; // helpers // types -import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state"; import { cn } from "@/helpers/common.helper"; import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; -import { copyTextToClipboard } from "@/helpers/string.helper"; // types -import { useProjectEstimates, useIssueDetail, useProject, useProjectState, useUser } from "@/hooks/store"; -import { usePlatformOS } from "@/hooks/use-platform-os"; +import { useProjectEstimates, useIssueDetail, useProject, useProjectState } from "@/hooks/store"; // components import type { TIssueOperations } from "./root"; -import { IssueSubscription } from "./subscription"; // icons // helpers // types @@ -70,56 +52,24 @@ type Props = { projectId: string; issueId: string; issueOperations: TIssueOperations; - is_archived: boolean; isEditable: boolean; }; export const IssueDetailsSidebar: React.FC = observer((props) => { - const { workspaceSlug, projectId, issueId, issueOperations, is_archived, isEditable } = props; - // states - const [deleteIssueModal, setDeleteIssueModal] = useState(false); - const [archiveIssueModal, setArchiveIssueModal] = useState(false); - // router - const router = useRouter(); + const { workspaceSlug, projectId, issueId, issueOperations, isEditable } = props; // store hooks const { getProjectById } = useProject(); - const { data: currentUser } = useUser(); const { areEstimateEnabledByProjectId } = useProjectEstimates(); const { issue: { getIssueById }, } = useIssueDetail(); const { getStateById } = useProjectState(); - const { isMobile } = usePlatformOS(); const issue = getIssueById(issueId); if (!issue) return <>; - const handleCopyText = () => { - const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Link Copied!", - message: "Issue link copied to clipboard.", - }); - }); - }; - - const handleDeleteIssue = async () => { - await issueOperations.remove(workspaceSlug, projectId, issueId); - router.push(`/${workspaceSlug}/projects/${projectId}/issues`); - }; - - const handleArchiveIssue = async () => { - if (!issueOperations.archive) return; - await issueOperations.archive(workspaceSlug, projectId, issueId); - router.push(`/${workspaceSlug}/projects/${projectId}/archives/issues/${issue.id}`); - }; // derived values const projectDetails = getProjectById(issue.project_id); const stateDetails = getStateById(issue.state_id); - // auth - const isArchivingAllowed = !is_archived && issueOperations.archive && isEditable; - const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group); const minDate = issue.start_date ? getDate(issue.start_date) : null; minDate?.setDate(minDate.getDate()); @@ -129,72 +79,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { return ( <> - setDeleteIssueModal(false)} - isOpen={deleteIssueModal} - data={issue} - onSubmit={handleDeleteIssue} - /> - setArchiveIssueModal(false)} - data={issue} - onSubmit={handleArchiveIssue} - /> -
-
-
- {currentUser && !is_archived && ( - - )} -
- - - - {isArchivingAllowed && ( - - - - )} - {isEditable && ( - - - - )} -
-
-
- +
Properties
{/* TODO: render properties using a common component */}