[WEB-4451] chore: work item header quick action enhancements (#7414)

* chore: work item header quick action enhancements

* chore: code refactor
This commit is contained in:
Anmol Singh Bhatia 2025-07-16 00:52:30 +05:30 committed by GitHub
parent df762afaef
commit ac22df3f88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 486 additions and 194 deletions

View file

@ -1,26 +1,22 @@
"use client";
import React, { FC, useState } from "react";
import React, { FC, useRef } from "react";
import { observer } from "mobx-react";
import { ArchiveIcon, ArchiveRestoreIcon, LinkIcon, Trash2 } from "lucide-react";
import {
ARCHIVABLE_STATE_GROUPS,
EUserPermissions,
EUserPermissionsLevel,
WORK_ITEM_TRACKER_EVENTS,
} from "@plane/constants";
import { LinkIcon } from "lucide-react";
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EIssuesStoreType } from "@plane/types";
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
import { cn, generateWorkItemLink, copyTextToClipboard } from "@plane/utils";
import { generateWorkItemLink, copyTextToClipboard } from "@plane/utils";
// components
import { ArchiveIssueModal, DeleteIssueModal, IssueSubscription } from "@/components/issues";
import { IssueSubscription } from "@/components/issues";
// helpers
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail, useIssues, useProject, useProjectState, useUser, useUserPermissions } from "@/hooks/store";
import { useIssueDetail, useIssues, useProject, useUser } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { WorkItemDetailQuickActions } from "../issue-layouts/quick-action-dropdowns";
type Props = {
workspaceSlug: string;
@ -31,19 +27,16 @@ type Props = {
export const IssueDetailQuickActions: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId } = props;
const { t } = useTranslation();
// states
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [archiveIssueModal, setArchiveIssueModal] = useState(false);
const [isRestoring, setIsRestoring] = useState(false);
// ref
const parentRef = useRef<HTMLDivElement>(null);
// router
const router = useAppRouter();
// hooks
const { data: currentUser } = useUser();
const { allowPermissions } = useUserPermissions();
const { isMobile } = usePlatformOS();
const { getStateById } = useProjectState();
const { getProjectIdentifierById } = useProject();
const {
issue: { getIssueById },
@ -61,7 +54,6 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
const issue = getIssueById(issueId);
if (!issue) return <></>;
const stateDetails = getStateById(issue.state_id);
const projectIdentifier = getProjectIdentifierById(projectId);
const workItemLink = generateWorkItemLink({
@ -133,8 +125,6 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
const handleRestore = async () => {
if (!workspaceSlug || !projectId || !issueId) return;
setIsRestoring(true);
await restoreIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString())
.then(() => {
setToast({
@ -150,40 +140,11 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
title: t("toast.error"),
message: t("issue.restore.failed.message"),
});
})
.finally(() => setIsRestoring(false));
});
};
// auth
const isEditable = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT,
workspaceSlug,
projectId
);
const canRestoreIssue = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT,
workspaceSlug,
projectId
);
const isArchivingAllowed = !issue?.archived_at && isEditable;
const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);
return (
<>
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issue}
onSubmit={handleDeleteIssue}
/>
<ArchiveIssueModal
isOpen={archiveIssueModal}
handleClose={() => setArchiveIssueModal(false)}
data={issue}
onSubmit={handleArchiveIssue}
/>
<div className="flex items-center justify-end flex-shrink-0">
<div className="flex flex-wrap items-center gap-4">
{currentUser && !issue?.archived_at && (
@ -199,64 +160,13 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
<LinkIcon className="h-4 w-4" />
</button>
</Tooltip>
{issue?.archived_at && canRestoreIssue ? (
<>
<Tooltip isMobile={isMobile} tooltipContent="Restore">
<button
type="button"
className={cn(
"grid h-5 w-5 place-items-center rounded focus:outline-none focus:ring-2 focus:ring-custom-primary",
{
"hover:text-custom-text-200": isInArchivableGroup,
"cursor-not-allowed text-custom-text-400": !isInArchivableGroup,
}
)}
onClick={handleRestore}
disabled={isRestoring}
>
<ArchiveRestoreIcon className="h-4 w-4" />
</button>
</Tooltip>
</>
) : (
<>
{isArchivingAllowed && (
<Tooltip
isMobile={isMobile}
tooltipContent={isInArchivableGroup ? t("common.actions.archive") : t("issue.archive.description")}
>
<button
type="button"
className={cn(
"grid h-5 w-5 place-items-center rounded focus:outline-none focus:ring-2 focus:ring-custom-primary",
{
"hover:text-custom-text-200": isInArchivableGroup,
"cursor-not-allowed text-custom-text-400": !isInArchivableGroup,
}
)}
onClick={() => {
if (!isInArchivableGroup) return;
setArchiveIssueModal(true);
}}
>
<ArchiveIcon className="h-4 w-4" />
</button>
</Tooltip>
)}
</>
)}
{isEditable && (
<Tooltip tooltipContent={t("common.actions.delete")} isMobile={isMobile}>
<button
type="button"
className="grid h-5 w-5 place-items-center rounded hover:text-custom-text-200 focus:outline-none focus:ring-2 focus:ring-custom-primary"
onClick={() => setDeleteIssueModal(true)}
>
<Trash2 className="h-4 w-4" />
</button>
</Tooltip>
)}
<WorkItemDetailQuickActions
parentRef={parentRef}
issue={issue}
handleDelete={handleDeleteIssue}
handleArchive={handleArchiveIssue}
handleRestore={handleRestore}
/>
</div>
</div>
</div>

View file

@ -274,6 +274,21 @@ export const useProjectIssueMenuItems = (props: MenuItemFactoryProps): TContextM
);
};
export const useWorkItemDetailMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => {
const factory = useMenuItemFactory(props);
return useMemo(
() => [
factory.createCopyMenuItem(),
factory.createOpenInNewTabMenuItem(),
factory.createArchiveMenuItem(),
factory.createRestoreMenuItem(),
factory.createDeleteMenuItem(),
],
[factory]
);
};
export const useAllIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => {
const factory = useMenuItemFactory(props);

View file

@ -6,3 +6,4 @@ export * from "./module-issue";
export * from "./project-issue";
export * from "./helper";
export * from "../../workspace-draft/quick-action";
export * from "./issue-detail";

View file

@ -0,0 +1,351 @@
"use client";
import { useState } from "react";
import omit from "lodash/omit";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
// plane imports
import {
ARCHIVABLE_STATE_GROUPS,
EUserPermissions,
EUserPermissionsLevel,
WORK_ITEM_TRACKER_ELEMENTS,
} from "@plane/constants";
import { EIssuesStoreType, TIssue } from "@plane/types";
import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useIssues, useProject, useProjectState, useUserPermissions } from "@/hooks/store";
// plane-web components
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns";
import { IQuickActionProps } from "../list/list-view-types";
// helper
import { MenuItemFactoryProps, useWorkItemDetailMenuItems } from "./helper";
type TWorkItemDetailQuickActionProps = IQuickActionProps & {
toggleEditIssueModal?: (value: boolean) => void;
toggleDeleteIssueModal?: (value: boolean) => void;
toggleDuplicateIssueModal?: (value: boolean) => void;
toggleArchiveIssueModal?: (value: boolean) => void;
isPeekMode?: boolean;
};
export const WorkItemDetailQuickActions: React.FC<TWorkItemDetailQuickActionProps> = observer((props) => {
const {
issue,
handleDelete,
handleUpdate,
handleArchive,
handleRestore,
customActionButton,
portalElement,
readOnly = false,
placements = "bottom-end",
parentRef,
toggleEditIssueModal,
toggleDeleteIssueModal,
toggleDuplicateIssueModal,
toggleArchiveIssueModal,
isPeekMode = false,
} = props;
// router
const { workspaceSlug } = useParams();
const pathname = usePathname();
// states
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [archiveIssueModal, setArchiveIssueModal] = useState(false);
const [duplicateWorkItemModal, setDuplicateWorkItemModal] = useState(false);
// store hooks
const { allowPermissions } = useUserPermissions();
const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
const { getStateById } = useProjectState();
const { getProjectIdentifierById } = useProject();
// derived values
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
const stateDetails = getStateById(issue.state_id);
const projectIdentifier = getProjectIdentifierById(issue?.project_id);
// auth
const isEditingAllowed =
allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT,
workspaceSlug?.toString(),
issue.project_id ?? undefined
) && !readOnly;
const isArchivingAllowed = !issue.archived_at && isEditingAllowed;
const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);
const isRestoringAllowed = !!issue.archived_at && isEditingAllowed;
const isDeletingAllowed = isEditingAllowed;
const isDraftIssue = pathname?.includes("draft-issues") || false;
const duplicateIssuePayload = omit(
{
...issue,
name: `${issue.name} (copy)`,
is_draft: isDraftIssue ? false : issue.is_draft,
sourceIssueId: issue.id,
},
["id"]
);
const customEditAction = () => {
setCreateUpdateIssueModal(true);
if (toggleEditIssueModal) toggleEditIssueModal(true);
};
const customDeleteAction = async () => {
setDeleteIssueModal(true);
if (toggleDeleteIssueModal) toggleDeleteIssueModal(true);
};
const customDuplicateAction = async () => {
setDuplicateWorkItemModal(true);
if (toggleDuplicateIssueModal) {
toggleDuplicateIssueModal(true);
}
};
const customArchiveAction = async () => {
setArchiveIssueModal(true);
if (toggleArchiveIssueModal) toggleArchiveIssueModal(true);
};
const customRestoreAction = async () => {
if (handleRestore) await handleRestore();
};
// Menu items and modals using helper
const menuItemProps: MenuItemFactoryProps = {
issue,
workspaceSlug: workspaceSlug?.toString(),
projectIdentifier,
activeLayout,
isEditingAllowed,
isArchivingAllowed,
isRestoringAllowed,
isDeletingAllowed,
isInArchivableGroup,
isDraftIssue,
setIssueToEdit,
setCreateUpdateIssueModal: customEditAction,
setDeleteIssueModal: customDeleteAction,
setArchiveIssueModal: customArchiveAction,
setDuplicateWorkItemModal: customDuplicateAction,
handleDelete: customDeleteAction,
handleUpdate,
handleArchive: customArchiveAction,
handleRestore: customRestoreAction,
storeType: EIssuesStoreType.PROJECT,
};
// const MENU_ITEMS = useWorkItemDetailMenuItems(menuItemProps);
const baseMenuItems = useWorkItemDetailMenuItems(menuItemProps);
const MENU_ITEMS = baseMenuItems
.map((item) => {
// Customize edit action for work item
if (item.key === "edit") {
return {
...item,
shouldRender: isEditingAllowed && !isPeekMode,
};
}
// Customize delete action for work item
if (item.key === "delete") {
return {
...item,
};
}
// Hide copy link in peek mode
if (item.key === "copy-link") {
return {
...item,
shouldRender: !isPeekMode,
};
}
return item;
})
.filter((item) => item.shouldRender !== false);
const CONTEXT_MENU_ITEMS: TContextMenuItem[] = MENU_ITEMS.map((item) => ({
...item,
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.PROJECT_VIEW });
item.action();
},
}));
return (
<>
{/* Modals */}
<ArchiveIssueModal
data={issue}
isOpen={archiveIssueModal}
handleClose={() => {
setArchiveIssueModal(false);
if (toggleArchiveIssueModal) toggleArchiveIssueModal(false);
}}
onSubmit={handleArchive}
/>
<DeleteIssueModal
data={issue}
isOpen={deleteIssueModal}
handleClose={() => {
setDeleteIssueModal(false);
if (toggleDeleteIssueModal) toggleDeleteIssueModal(false);
}}
onSubmit={handleDelete}
/>
<CreateUpdateIssueModal
isOpen={createUpdateIssueModal}
onClose={() => {
setCreateUpdateIssueModal(false);
setIssueToEdit(undefined);
if (toggleEditIssueModal) toggleEditIssueModal(false);
}}
data={issueToEdit ?? duplicateIssuePayload}
onSubmit={async (data) => {
if (issueToEdit && handleUpdate) await handleUpdate(data);
}}
storeType={EIssuesStoreType.PROJECT}
isDraft={isDraftIssue}
/>
{issue.project_id && workspaceSlug && (
<DuplicateWorkItemModal
workItemId={issue.id}
isOpen={duplicateWorkItemModal}
onClose={() => {
setDuplicateWorkItemModal(false);
if (toggleDuplicateIssueModal) toggleDuplicateIssueModal(false);
}}
workspaceSlug={workspaceSlug.toString()}
projectId={issue.project_id}
/>
)}
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu
ellipsis
placement={placements}
customButton={customActionButton}
portalElement={portalElement}
menuItemsClassName="z-[14]"
maxHeight="lg"
closeOnSelect
>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
// Render submenu if nestedMenuItems exist
if (item.nestedMenuItems && item.nestedMenuItems.length > 0) {
return (
<CustomMenu.SubMenu
key={item.key}
trigger={
<div className="flex items-center gap-2">
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<h5>{item.title}</h5>
{item.description && (
<p
className={cn("text-custom-text-300 whitespace-pre-line", {
"text-custom-text-400": item.disabled,
})}
>
{item.description}
</p>
)}
</div>
}
disabled={item.disabled}
className={cn(
"flex items-center gap-2",
{
"text-custom-text-400": item.disabled,
},
item.className
)}
>
{item.nestedMenuItems.map((nestedItem) => (
<CustomMenu.MenuItem
key={nestedItem.key}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.PROJECT_VIEW });
nestedItem.action();
}}
className={cn(
"flex items-center gap-2",
{
"text-custom-text-400": nestedItem.disabled,
},
nestedItem.className
)}
disabled={nestedItem.disabled}
>
{nestedItem.icon && <nestedItem.icon className={cn("h-3 w-3", nestedItem.iconClassName)} />}
<div>
<h5>{nestedItem.title}</h5>
{nestedItem.description && (
<p
className={cn("text-custom-text-300 whitespace-pre-line", {
"text-custom-text-400": nestedItem.disabled,
})}
>
{nestedItem.description}
</p>
)}
</div>
</CustomMenu.MenuItem>
))}
</CustomMenu.SubMenu>
);
}
// Render regular menu item
return (
<CustomMenu.MenuItem
key={item.key}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.PROJECT_VIEW });
item.action();
}}
className={cn(
"flex items-center gap-2",
{
"text-custom-text-400": item.disabled,
},
item.className
)}
disabled={item.disabled}
>
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<div>
<h5>{item.title}</h5>
{item.description && (
<p
className={cn("text-custom-text-300 whitespace-pre-line", {
"text-custom-text-400": item.disabled,
})}
>
{item.description}
</p>
)}
</div>
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</>
);
});

View file

@ -1,15 +1,14 @@
"use client";
import { FC } from "react";
import { FC, useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { ArchiveRestoreIcon, Link2, MoveDiagonal, MoveRight, Trash2 } from "lucide-react";
import { Link2, MoveDiagonal, MoveRight } from "lucide-react";
// plane imports
import { ARCHIVABLE_STATE_GROUPS } from "@plane/constants";
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TNameDescriptionLoader } from "@plane/types";
import { EIssuesStoreType, TNameDescriptionLoader } from "@plane/types";
import {
ArchiveIcon,
CenterPanelIcon,
CustomSelect,
FullScreenPanelIcon,
@ -18,12 +17,15 @@ import {
Tooltip,
setToast,
} from "@plane/ui";
import { copyUrlToClipboard, cn, generateWorkItemLink } from "@plane/utils";
import { copyUrlToClipboard, generateWorkItemLink } from "@plane/utils";
// components
import { IssueSubscription, NameDescriptionUpdateStatus } from "@/components/issues";
import { IssueSubscription, NameDescriptionUpdateStatus, WorkItemDetailQuickActions } from "@/components/issues";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// helpers
// store hooks
import { useIssueDetail, useProject, useProjectState, useUser } from "@/hooks/store";
import { useIssueDetail, useIssues, useProject, useUser } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
export type TPeekModes = "side-peek" | "modal" | "full-screen";
@ -56,9 +58,11 @@ export type PeekOverviewHeaderProps = {
isArchived: boolean;
disabled: boolean;
embedIssue: boolean;
toggleDeleteIssueModal: (issueId: string | null) => void;
toggleArchiveIssueModal: (issueId: string | null) => void;
handleRestoreIssue: () => void;
toggleDeleteIssueModal: (value: boolean) => void;
toggleArchiveIssueModal: (value: boolean) => void;
toggleDuplicateIssueModal: (value: boolean) => void;
toggleEditIssueModal: (value: boolean) => void;
handleRestoreIssue: () => Promise<void>;
isSubmitting: TNameDescriptionLoader;
};
@ -75,23 +79,33 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
removeRoutePeekId,
toggleDeleteIssueModal,
toggleArchiveIssueModal,
toggleDuplicateIssueModal,
toggleEditIssueModal,
handleRestoreIssue,
isSubmitting,
} = props;
// ref
const parentRef = useRef<HTMLDivElement>(null);
// router
const router = useAppRouter();
const { t } = useTranslation();
// store hooks
const { data: currentUser } = useUser();
const {
issue: { getIssueById },
setPeekIssue,
removeIssue,
archiveIssue,
} = useIssueDetail();
const { getStateById } = useProjectState();
const { isMobile } = usePlatformOS();
const { getProjectIdentifierById } = useProject();
// derived values
const issueDetails = getIssueById(issueId);
const stateDetails = issueDetails ? getStateById(issueDetails?.state_id) : undefined;
const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode);
const projectIdentifier = getProjectIdentifierById(issueDetails?.project_id);
const {
issues: { removeIssue: removeArchivedIssue },
} = useIssues(EIssuesStoreType.ARCHIVED);
const workItemLink = generateWorkItemLink({
workspaceSlug,
@ -113,10 +127,49 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
});
});
};
// auth
const isArchivingAllowed = !isArchived && !disabled;
const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);
const isRestoringAllowed = isArchived && !disabled;
const handleDeleteIssue = async () => {
try {
const deleteIssue = issueDetails?.archived_at ? removeArchivedIssue : removeIssue;
return deleteIssue(workspaceSlug, projectId, issueId).then(() => {
setPeekIssue(undefined);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId },
});
});
} catch (error) {
setToast({
title: t("toast.error"),
type: TOAST_TYPE.ERROR,
message: t("entity.delete.failed", { entity: t("issue.label", { count: 1 }) }),
});
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId },
error: error as Error,
});
}
};
const handleArchiveIssue = async () => {
try {
await archiveIssue(workspaceSlug, projectId, issueId).then(() => {
router.push(`/${workspaceSlug}/projects/${projectId}/archives/issues/${issueDetails?.id}`);
});
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId },
});
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId },
error: error as Error,
});
}
};
return (
<div
@ -178,39 +231,20 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
<Link2 className="h-4 w-4 -rotate-45 text-custom-text-300 hover:text-custom-text-200" />
</button>
</Tooltip>
{isArchivingAllowed && (
<Tooltip
isMobile={isMobile}
tooltipContent={isInArchivableGroup ? t("common.actions.archive") : t("issue.archive.description")}
>
<button
type="button"
className={cn("text-custom-text-300", {
"hover:text-custom-text-200": isInArchivableGroup,
"cursor-not-allowed text-custom-text-400": !isInArchivableGroup,
})}
onClick={() => {
if (!isInArchivableGroup) return;
toggleArchiveIssueModal(issueId);
}}
>
<ArchiveIcon className="h-4 w-4" />
</button>
</Tooltip>
)}
{isRestoringAllowed && (
<Tooltip tooltipContent={t("common.actions.restore")} isMobile={isMobile}>
<button type="button" onClick={handleRestoreIssue}>
<ArchiveRestoreIcon className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
</button>
</Tooltip>
)}
{!disabled && (
<Tooltip tooltipContent={t("common.actions.delete")} isMobile={isMobile}>
<button type="button" onClick={() => toggleDeleteIssueModal(issueId)}>
<Trash2 className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
</button>
</Tooltip>
{issueDetails && (
<WorkItemDetailQuickActions
parentRef={parentRef}
issue={issueDetails}
handleDelete={handleDeleteIssue}
handleArchive={handleArchiveIssue}
handleRestore={handleRestoreIssue}
readOnly={disabled}
toggleDeleteIssueModal={toggleDeleteIssueModal}
toggleArchiveIssueModal={toggleArchiveIssueModal}
toggleDuplicateIssueModal={toggleDuplicateIssueModal}
toggleEditIssueModal={toggleEditIssueModal}
isPeekMode
/>
)}
</div>
</div>

View file

@ -1,17 +1,16 @@
import { FC, useRef, useState } from "react";
import { observer } from "mobx-react";
import { createPortal } from "react-dom";
// types
import { EIssueServiceType, TNameDescriptionLoader } from "@plane/types";
// components
import { cn } from "@plane/utils";
import {
DeleteIssueModal,
IssuePeekOverviewHeader,
TPeekModes,
PeekOverviewIssueDetails,
PeekOverviewProperties,
TIssueOperations,
ArchiveIssueModal,
IssuePeekOverviewLoader,
IssuePeekOverviewError,
IssueDetailWidgets,
@ -23,7 +22,6 @@ import useKeypress from "@/hooks/use-keypress";
import usePeekOverviewOutsideClickDetector from "@/hooks/use-peek-overview-outside-click";
// store hooks
import { IssueActivity } from "../issue-detail/issue-activity";
import { createPortal } from "react-dom";
interface IIssueView {
workspaceSlug: string;
@ -54,16 +52,16 @@ export const IssueView: FC<IIssueView> = observer((props) => {
// states
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
const [isSubmitting, setIsSubmitting] = useState<TNameDescriptionLoader>("saved");
const [isDeleteIssueModalOpen, setIsDeleteIssueModalOpen] = useState(false);
const [isArchiveIssueModalOpen, setIsArchiveIssueModalOpen] = useState(false);
const [isDuplicateIssueModalOpen, setIsDuplicateIssueModalOpen] = useState(false);
const [isEditIssueModalOpen, setIsEditIssueModalOpen] = useState(false);
// ref
const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
// store hooks
const {
setPeekIssue,
isAnyModalOpen,
isDeleteIssueModalOpen,
isArchiveIssueModalOpen,
toggleDeleteIssueModal,
toggleArchiveIssueModal,
issue: { getIssueById, getIsLocalDBIssueDescription },
} = useIssueDetail();
const { isAnyModalOpen: isAnyEpicModalOpen } = useIssueDetail(EIssueServiceType.EPICS);
@ -76,11 +74,19 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const isLocalDBIssueDescription = getIsLocalDBIssueDescription(issueId);
const toggleDeleteIssueModal = (value: boolean) => setIsDeleteIssueModalOpen(value);
const toggleArchiveIssueModal = (value: boolean) => setIsArchiveIssueModalOpen(value);
const toggleDuplicateIssueModal = (value: boolean) => setIsDuplicateIssueModalOpen(value);
const toggleEditIssueModal = (value: boolean) => setIsEditIssueModalOpen(value);
const isAnyLocalModalOpen =
isDeleteIssueModalOpen || isArchiveIssueModalOpen || isDuplicateIssueModalOpen || isEditIssueModalOpen;
usePeekOverviewOutsideClickDetector(
issuePeekOverviewRef,
() => {
if (!embedIssue) {
if (!isAnyModalOpen && !isAnyEpicModalOpen) {
if (!isAnyModalOpen && !isAnyEpicModalOpen && !isAnyLocalModalOpen) {
removeRoutePeekId();
}
}
@ -149,6 +155,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
removeRoutePeekId={removeRoutePeekId}
toggleDeleteIssueModal={toggleDeleteIssueModal}
toggleArchiveIssueModal={toggleArchiveIssueModal}
toggleDuplicateIssueModal={toggleDuplicateIssueModal}
toggleEditIssueModal={toggleEditIssueModal}
handleRestoreIssue={handleRestore}
isArchived={is_archived}
issueId={issueId}
@ -254,32 +262,5 @@ export const IssueView: FC<IIssueView> = observer((props) => {
</div>
);
return (
<>
{issue && !is_archived && (
<ArchiveIssueModal
isOpen={isArchiveIssueModalOpen === issueId}
handleClose={() => toggleArchiveIssueModal(null)}
data={issue}
onSubmit={async () => {
if (issueOperations.archive) await issueOperations.archive(workspaceSlug, projectId, issueId);
removeRoutePeekId();
}}
/>
)}
{issue && isDeleteIssueModalOpen === issue.id && (
<DeleteIssueModal
isOpen={!!isDeleteIssueModalOpen}
handleClose={() => {
toggleDeleteIssueModal(null);
}}
data={issue}
onSubmit={async () => issueOperations.remove(workspaceSlug, projectId, issueId)}
/>
)}
{shouldUsePortal && portalContainer ? createPortal(content, portalContainer) : content}
</>
);
return <>{shouldUsePortal && portalContainer ? createPortal(content, portalContainer) : content}</>;
});