[WEB-4208]chore: refactored work item quick actions (#7136)
* chore: refactored work item quick actions * chore: update event handling for menu * chore: reverted unwanted changes * fix: update archive copy link * chore: handled undefined function implementation
This commit is contained in:
parent
14d2d69120
commit
6be3f0ea73
21 changed files with 1602 additions and 517 deletions
|
|
@ -0,0 +1,22 @@
|
|||
import { Copy } from "lucide-react";
|
||||
import { TContextMenuItem } from "@plane/ui";
|
||||
|
||||
export interface CopyMenuHelperProps {
|
||||
baseItem: {
|
||||
key: string;
|
||||
title: string;
|
||||
icon: typeof Copy;
|
||||
action: () => void;
|
||||
shouldRender: boolean;
|
||||
};
|
||||
activeLayout: string;
|
||||
setTrackElement: (element: string) => void;
|
||||
setCreateUpdateIssueModal: (open: boolean) => void;
|
||||
setDuplicateWorkItemModal?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const createCopyMenuWithDuplication = (props: CopyMenuHelperProps): TContextMenuItem => {
|
||||
const { baseItem } = props;
|
||||
|
||||
return baseItem;
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { FC } from "react";
|
||||
|
||||
type TDuplicateWorkItemModalProps = {
|
||||
workItemId: string;
|
||||
onClose: () => void;
|
||||
isOpen: boolean;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const DuplicateWorkItemModal: FC<TDuplicateWorkItemModalProps> = () => <></>;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./duplicate-modal";
|
||||
export * from "./copy-menu-helper";
|
||||
18
web/ce/store/issue/issue-details/root.store.ts
Normal file
18
web/ce/store/issue/issue-details/root.store.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { makeObservable } from "mobx";
|
||||
import { TIssueServiceType } from "@plane/types";
|
||||
import {
|
||||
IssueDetail as IssueDetailCore,
|
||||
IIssueDetail as IIssueDetailCore,
|
||||
} from "@/store/issue/issue-details/root.store";
|
||||
import { IIssueRootStore } from "@/store/issue/root.store";
|
||||
|
||||
export type IIssueDetail = IIssueDetailCore;
|
||||
|
||||
export class IssueDetail extends IssueDetailCore {
|
||||
constructor(rootStore: IIssueRootStore, serviceType: TIssueServiceType) {
|
||||
super(rootStore, serviceType);
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ type Props = TDropdownProps & {
|
|||
onClose?: () => void;
|
||||
renderCondition?: (project: TProject) => boolean;
|
||||
renderByDefault?: boolean;
|
||||
currentProjectId?: string;
|
||||
} & (
|
||||
| {
|
||||
multiple: false;
|
||||
|
|
@ -63,6 +64,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||
tabIndex,
|
||||
value,
|
||||
renderByDefault = true,
|
||||
currentProjectId,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
|
|
@ -108,7 +110,9 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||
});
|
||||
|
||||
const filteredOptions =
|
||||
query === "" ? options : options?.filter((o) => o?.query.toLowerCase().includes(query.toLowerCase()));
|
||||
query === ""
|
||||
? options?.filter((o) => o?.value !== currentProjectId)
|
||||
: options?.filter((o) => o?.value !== currentProjectId && o?.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
|
||||
dropdownRef,
|
||||
|
|
@ -198,7 +202,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||
>
|
||||
{!hideIcon && getProjectIcon(value)}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate max-w-40">{getDisplayName(value, placeholder)}</span>
|
||||
<span className="truncate max-w-40">{getDisplayName(value, placeholder)}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-d
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane helpers
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
// types
|
||||
|
|
@ -60,9 +61,25 @@ interface IssueDetailsBlockProps {
|
|||
|
||||
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props) => {
|
||||
const { cardRef, issue, updateIssue, quickActions, isReadOnly, displayProperties, isEpic = false } = props;
|
||||
// refs
|
||||
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||
// states
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const customActionButton = (
|
||||
<div
|
||||
ref={menuActionRef}
|
||||
className={`flex items-center h-full w-full cursor-pointer rounded p-1 text-custom-sidebar-text-400 hover:bg-custom-background-80 ${
|
||||
isMenuActive ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
>
|
||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
);
|
||||
|
||||
// derived values
|
||||
const subIssueCount = issue?.sub_issues_count ?? 0;
|
||||
|
||||
|
|
@ -71,6 +88,8 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
|
|||
e.preventDefault();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
|
|
@ -85,12 +104,14 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
|
|||
<div
|
||||
className={cn("absolute -top-1 right-0", {
|
||||
"hidden group-hover/kanban-block:block": !isMobile,
|
||||
"!block": isMenuActive,
|
||||
})}
|
||||
onClick={handleEventPropagation}
|
||||
>
|
||||
{quickActions({
|
||||
issue,
|
||||
parentRef: cardRef,
|
||||
customActionButton,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,21 +4,21 @@ import { useState } from "react";
|
|||
import omit from "lodash/omit";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react";
|
||||
// plane imports
|
||||
import { ARCHIVABLE_STATE_GROUPS, EIssuesStoreType } from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
import { ContextMenu, CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { generateWorkItemLink } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useProject, useProjectState } from "@/hooks/store";
|
||||
// types
|
||||
// plane-web components
|
||||
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
// helper
|
||||
import { useAllIssueMenuItems, MenuItemFactoryProps } from "./helper";
|
||||
|
||||
export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
|
||||
const {
|
||||
|
|
@ -37,6 +37,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
|
|||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const [archiveIssueModal, setArchiveIssueModal] = useState(false);
|
||||
const [duplicateWorkItemModal, setDuplicateWorkItemModal] = useState(false);
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
|
|
@ -51,24 +52,6 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
|
|||
const isArchivingAllowed = handleArchive && isEditingAllowed;
|
||||
const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);
|
||||
|
||||
const workItemLink = generateWorkItemLink({
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
projectId: issue?.project_id,
|
||||
issueId: issue?.id,
|
||||
projectIdentifier,
|
||||
sequenceId: issue?.sequence_id,
|
||||
});
|
||||
|
||||
const handleOpenInNewTab = () => window.open(workItemLink, "_blank");
|
||||
const handleCopyIssueLink = () =>
|
||||
copyUrlToClipboard(workItemLink).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link copied",
|
||||
message: "Work item link copied to clipboard",
|
||||
})
|
||||
);
|
||||
|
||||
const duplicateIssuePayload = omit(
|
||||
{
|
||||
...issue,
|
||||
|
|
@ -78,65 +61,33 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
|
|||
["id"]
|
||||
);
|
||||
|
||||
const MENU_ITEMS: TContextMenuItem[] = [
|
||||
{
|
||||
key: "edit",
|
||||
title: "Edit",
|
||||
icon: Pencil,
|
||||
action: () => {
|
||||
setTrackElement("Global issues");
|
||||
setIssueToEdit(issue);
|
||||
setCreateUpdateIssueModal(true);
|
||||
},
|
||||
shouldRender: isEditingAllowed,
|
||||
},
|
||||
{
|
||||
key: "make-a-copy",
|
||||
title: "Make a copy",
|
||||
icon: Copy,
|
||||
action: () => {
|
||||
setTrackElement("Global issues");
|
||||
setCreateUpdateIssueModal(true);
|
||||
},
|
||||
shouldRender: isEditingAllowed,
|
||||
},
|
||||
{
|
||||
key: "open-in-new-tab",
|
||||
title: "Open in new tab",
|
||||
icon: ExternalLink,
|
||||
action: handleOpenInNewTab,
|
||||
},
|
||||
{
|
||||
key: "copy-link",
|
||||
title: "Copy link",
|
||||
icon: Link,
|
||||
action: handleCopyIssueLink,
|
||||
},
|
||||
{
|
||||
key: "archive",
|
||||
title: "Archive",
|
||||
description: isInArchivableGroup ? undefined : "Only completed or canceled\nwork items can be archived",
|
||||
icon: ArchiveIcon,
|
||||
className: "items-start",
|
||||
iconClassName: "mt-1",
|
||||
action: () => setArchiveIssueModal(true),
|
||||
disabled: !isInArchivableGroup,
|
||||
shouldRender: isArchivingAllowed,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
title: "Delete",
|
||||
icon: Trash2,
|
||||
action: () => {
|
||||
setTrackElement("Global issues");
|
||||
setDeleteIssueModal(true);
|
||||
},
|
||||
shouldRender: isEditingAllowed,
|
||||
},
|
||||
];
|
||||
// Menu items and modals using helper
|
||||
const menuItemProps: MenuItemFactoryProps = {
|
||||
issue,
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
projectIdentifier,
|
||||
activeLayout: "Global issues",
|
||||
isEditingAllowed,
|
||||
isArchivingAllowed,
|
||||
isDeletingAllowed: isEditingAllowed,
|
||||
isInArchivableGroup,
|
||||
setTrackElement,
|
||||
setIssueToEdit,
|
||||
setCreateUpdateIssueModal,
|
||||
setDeleteIssueModal,
|
||||
setArchiveIssueModal,
|
||||
setDuplicateWorkItemModal,
|
||||
handleDelete,
|
||||
handleUpdate,
|
||||
handleArchive,
|
||||
storeType: EIssuesStoreType.GLOBAL,
|
||||
};
|
||||
|
||||
const MENU_ITEMS = useAllIssueMenuItems(menuItemProps);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modals */}
|
||||
<ArchiveIssueModal
|
||||
data={issue}
|
||||
isOpen={archiveIssueModal}
|
||||
|
|
@ -160,7 +111,18 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
|
|||
if (issueToEdit && handleUpdate) await handleUpdate(data);
|
||||
}}
|
||||
storeType={EIssuesStoreType.GLOBAL}
|
||||
isDraft={false}
|
||||
/>
|
||||
{issue.project_id && workspaceSlug && (
|
||||
<DuplicateWorkItemModal
|
||||
workItemId={issue.id}
|
||||
isOpen={duplicateWorkItemModal}
|
||||
onClose={() => setDuplicateWorkItemModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={issue.project_id}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
<CustomMenu
|
||||
ellipsis
|
||||
|
|
@ -174,6 +136,73 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
|
|||
>
|
||||
{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();
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -3,21 +3,19 @@
|
|||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { ArchiveRestoreIcon, ExternalLink, Link, Trash2 } from "lucide-react";
|
||||
// ui
|
||||
import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
import { ContextMenu, CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { DeleteIssueModal } from "@/components/issues";
|
||||
// constants
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useIssues, useUserPermissions } from "@/hooks/store";
|
||||
// types
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
// helper
|
||||
import { useArchivedIssueMenuItems, MenuItemFactoryProps } from "./helper";
|
||||
|
||||
export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
|
||||
const {
|
||||
|
|
@ -47,76 +45,34 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = observer((
|
|||
const isRestoringAllowed =
|
||||
handleRestore && allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT);
|
||||
|
||||
const issueLink = `${workspaceSlug}/projects/${issue.project_id}/archives/issues/${issue.id}`;
|
||||
|
||||
const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank");
|
||||
const handleCopyIssueLink = () =>
|
||||
copyUrlToClipboard(issueLink).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link copied",
|
||||
message: "Work item link copied to clipboard",
|
||||
})
|
||||
);
|
||||
const handleIssueRestore = async () => {
|
||||
if (!handleRestore) return;
|
||||
await handleRestore()
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Restore success",
|
||||
message: "Your work item can be found in project work items.",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Work item could not be restored. Please try again.",
|
||||
});
|
||||
});
|
||||
// Menu items and modals using helper
|
||||
const menuItemProps: MenuItemFactoryProps = {
|
||||
issue,
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
activeLayout,
|
||||
isEditingAllowed,
|
||||
isDeletingAllowed: isEditingAllowed,
|
||||
isRestoringAllowed: !!isRestoringAllowed,
|
||||
setTrackElement,
|
||||
setIssueToEdit: () => {},
|
||||
setCreateUpdateIssueModal: () => {},
|
||||
setDeleteIssueModal,
|
||||
handleRestore,
|
||||
handleDelete,
|
||||
};
|
||||
|
||||
const MENU_ITEMS: TContextMenuItem[] = [
|
||||
{
|
||||
key: "restore",
|
||||
title: "Restore",
|
||||
icon: ArchiveRestoreIcon,
|
||||
action: handleIssueRestore,
|
||||
shouldRender: isRestoringAllowed,
|
||||
},
|
||||
{
|
||||
key: "open-in-new-tab",
|
||||
title: "Open in new tab",
|
||||
icon: ExternalLink,
|
||||
action: handleOpenInNewTab,
|
||||
},
|
||||
{
|
||||
key: "copy-link",
|
||||
title: "Copy link",
|
||||
icon: Link,
|
||||
action: handleCopyIssueLink,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
title: "Delete",
|
||||
icon: Trash2,
|
||||
action: () => {
|
||||
setTrackElement(activeLayout);
|
||||
setDeleteIssueModal(true);
|
||||
},
|
||||
shouldRender: isEditingAllowed,
|
||||
},
|
||||
];
|
||||
const MENU_ITEMS = useArchivedIssueMenuItems(menuItemProps);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modals */}
|
||||
<DeleteIssueModal
|
||||
data={issue}
|
||||
isOpen={deleteIssueModal}
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
onSubmit={handleDelete}
|
||||
/>
|
||||
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
<CustomMenu
|
||||
ellipsis
|
||||
|
|
|
|||
|
|
@ -4,21 +4,22 @@ import { useState } from "react";
|
|||
import omit from "lodash/omit";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react";
|
||||
// plane imports
|
||||
import { ARCHIVABLE_STATE_GROUPS, EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
import { ContextMenu, CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { generateWorkItemLink } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useIssues, useProject, useProjectState, useUserPermissions } from "@/hooks/store";
|
||||
// plane-web components
|
||||
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns";
|
||||
// types
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
// helper
|
||||
import { useCycleIssueMenuItems, MenuItemFactoryProps } from "./helper";
|
||||
|
||||
export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
|
||||
const {
|
||||
|
|
@ -38,6 +39,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
|||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const [archiveIssueModal, setArchiveIssueModal] = useState(false);
|
||||
const [duplicateWorkItemModal, setDuplicateWorkItemModal] = useState(false);
|
||||
// router
|
||||
const { workspaceSlug, cycleId } = useParams();
|
||||
// store hooks
|
||||
|
|
@ -58,25 +60,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
|||
|
||||
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
||||
|
||||
const workItemLink = generateWorkItemLink({
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
projectId: issue?.project_id,
|
||||
issueId: issue?.id,
|
||||
projectIdentifier,
|
||||
sequenceId: issue?.sequence_id,
|
||||
});
|
||||
|
||||
const handleOpenInNewTab = () => window.open(workItemLink, "_blank");
|
||||
|
||||
const handleCopyIssueLink = () =>
|
||||
copyUrlToClipboard(workItemLink).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link copied",
|
||||
message: "Work item link copied to clipboard",
|
||||
})
|
||||
);
|
||||
|
||||
const duplicateIssuePayload = omit(
|
||||
{
|
||||
...issue,
|
||||
|
|
@ -86,75 +69,35 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
|||
["id"]
|
||||
);
|
||||
|
||||
const MENU_ITEMS: TContextMenuItem[] = [
|
||||
{
|
||||
key: "edit",
|
||||
title: "Edit",
|
||||
icon: Pencil,
|
||||
action: () => {
|
||||
setIssueToEdit({
|
||||
...issue,
|
||||
cycle_id: cycleId?.toString() ?? null,
|
||||
});
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
},
|
||||
shouldRender: isEditingAllowed,
|
||||
},
|
||||
{
|
||||
key: "make-a-copy",
|
||||
title: "Make a copy",
|
||||
icon: Copy,
|
||||
action: () => {
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
},
|
||||
shouldRender: isEditingAllowed,
|
||||
},
|
||||
{
|
||||
key: "open-in-new-tab",
|
||||
title: "Open in new tab",
|
||||
icon: ExternalLink,
|
||||
action: handleOpenInNewTab,
|
||||
},
|
||||
{
|
||||
key: "copy-link",
|
||||
title: "Copy link",
|
||||
icon: Link,
|
||||
action: handleCopyIssueLink,
|
||||
},
|
||||
{
|
||||
key: "remove-from-cycle",
|
||||
title: "Remove from cycle",
|
||||
icon: XCircle,
|
||||
action: () => handleRemoveFromView?.(),
|
||||
shouldRender: isEditingAllowed,
|
||||
},
|
||||
{
|
||||
key: "archive",
|
||||
title: "Archive",
|
||||
description: isInArchivableGroup ? undefined : "Only completed or canceled\nwork items can be archived",
|
||||
icon: ArchiveIcon,
|
||||
className: "items-start",
|
||||
iconClassName: "mt-1",
|
||||
action: () => setArchiveIssueModal(true),
|
||||
disabled: !isInArchivableGroup,
|
||||
shouldRender: isArchivingAllowed,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
title: "Delete",
|
||||
icon: Trash2,
|
||||
action: () => {
|
||||
setTrackElement(activeLayout);
|
||||
setDeleteIssueModal(true);
|
||||
},
|
||||
shouldRender: isDeletingAllowed,
|
||||
},
|
||||
];
|
||||
// Menu items and modals using helper
|
||||
const menuItemProps: MenuItemFactoryProps = {
|
||||
issue,
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
projectIdentifier,
|
||||
activeLayout,
|
||||
isEditingAllowed,
|
||||
isArchivingAllowed,
|
||||
isDeletingAllowed,
|
||||
isInArchivableGroup,
|
||||
setTrackElement,
|
||||
setIssueToEdit,
|
||||
setCreateUpdateIssueModal,
|
||||
setDeleteIssueModal,
|
||||
setArchiveIssueModal,
|
||||
setDuplicateWorkItemModal,
|
||||
handleRemoveFromView,
|
||||
cycleId: cycleId?.toString(),
|
||||
handleDelete,
|
||||
handleUpdate,
|
||||
handleArchive,
|
||||
storeType: EIssuesStoreType.CYCLE,
|
||||
};
|
||||
|
||||
const MENU_ITEMS = useCycleIssueMenuItems(menuItemProps);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modals */}
|
||||
<ArchiveIssueModal
|
||||
data={issue}
|
||||
isOpen={archiveIssueModal}
|
||||
|
|
@ -178,7 +121,18 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
|||
if (issueToEdit && handleUpdate) await handleUpdate(data);
|
||||
}}
|
||||
storeType={EIssuesStoreType.CYCLE}
|
||||
isDraft={false}
|
||||
/>
|
||||
{issue.project_id && workspaceSlug && (
|
||||
<DuplicateWorkItemModal
|
||||
workItemId={issue.id}
|
||||
isOpen={duplicateWorkItemModal}
|
||||
onClose={() => setDuplicateWorkItemModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={issue.project_id}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
<CustomMenu
|
||||
ellipsis
|
||||
|
|
@ -192,6 +146,73 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
|||
>
|
||||
{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();
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -3,23 +3,21 @@
|
|||
import { useState } from "react";
|
||||
import omit from "lodash/omit";
|
||||
import { observer } from "mobx-react";
|
||||
// icons
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
// types
|
||||
import { EIssuesStoreType,EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui";
|
||||
import { ContextMenu, CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
|
||||
// constant
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useIssues, useUserPermissions } from "@/hooks/store";
|
||||
import { useEventTracker, useUserPermissions } from "@/hooks/store";
|
||||
// types
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
// helper
|
||||
import { useDraftIssueMenuItems, MenuItemFactoryProps } from "./helper";
|
||||
|
||||
export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
|
||||
const {
|
||||
|
|
@ -32,6 +30,9 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
|||
placements = "bottom-end",
|
||||
parentRef,
|
||||
} = props;
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
const pathname = usePathname();
|
||||
// states
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||
|
|
@ -39,56 +40,52 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
|||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
|
||||
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
||||
const activeLayout = "Draft Issues";
|
||||
// auth
|
||||
const isEditingAllowed =
|
||||
allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && !readOnly;
|
||||
allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug?.toString(),
|
||||
issue.project_id ?? undefined
|
||||
) && !readOnly;
|
||||
const isDeletingAllowed = isEditingAllowed;
|
||||
|
||||
const isDraftIssue = pathname?.includes("draft-issues") || false;
|
||||
|
||||
const duplicateIssuePayload = omit(
|
||||
{
|
||||
...issue,
|
||||
name: `${issue.name} (copy)`,
|
||||
is_draft: true,
|
||||
is_draft: isDraftIssue ? false : issue.is_draft,
|
||||
sourceIssueId: issue.id,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
|
||||
const MENU_ITEMS: TContextMenuItem[] = [
|
||||
{
|
||||
key: "edit",
|
||||
title: "edit",
|
||||
icon: Pencil,
|
||||
action: () => {
|
||||
setTrackElement(activeLayout);
|
||||
setIssueToEdit(issue);
|
||||
setCreateUpdateIssueModal(true);
|
||||
},
|
||||
shouldRender: isEditingAllowed,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
title: "delete",
|
||||
icon: Trash2,
|
||||
action: () => {
|
||||
setTrackElement(activeLayout);
|
||||
setDeleteIssueModal(true);
|
||||
},
|
||||
shouldRender: isDeletingAllowed,
|
||||
},
|
||||
];
|
||||
// Menu items and modals using helper
|
||||
const menuItemProps: MenuItemFactoryProps = {
|
||||
issue,
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
activeLayout,
|
||||
isEditingAllowed,
|
||||
isDeletingAllowed,
|
||||
isDraftIssue,
|
||||
setTrackElement,
|
||||
setIssueToEdit,
|
||||
setCreateUpdateIssueModal,
|
||||
setDeleteIssueModal,
|
||||
handleDelete,
|
||||
handleUpdate,
|
||||
storeType: EIssuesStoreType.DRAFT,
|
||||
};
|
||||
|
||||
// check if any of the menu items should render
|
||||
const shouldRenderQuickAction = MENU_ITEMS.some((item) => item.shouldRender);
|
||||
|
||||
if (!shouldRenderQuickAction) return <></>;
|
||||
const MENU_ITEMS = useDraftIssueMenuItems(menuItemProps);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modals */}
|
||||
<DeleteIssueModal
|
||||
data={issue}
|
||||
isOpen={deleteIssueModal}
|
||||
|
|
@ -106,17 +103,17 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
|||
if (issueToEdit && handleUpdate) await handleUpdate(data);
|
||||
}}
|
||||
storeType={EIssuesStoreType.DRAFT}
|
||||
isDraft
|
||||
isDraft={isDraftIssue}
|
||||
/>
|
||||
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
<CustomMenu
|
||||
ellipsis
|
||||
placement={placements}
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
placement={placements}
|
||||
menuItemsClassName="z-[14]"
|
||||
maxHeight="lg"
|
||||
useCaptureForOutsideClick
|
||||
closeOnSelect
|
||||
>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
|
|
@ -140,7 +137,7 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
|||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
<h5>{t(item.title ?? "")}</h5>
|
||||
<h5>{item.title}</h5>
|
||||
{item.description && (
|
||||
<p
|
||||
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,372 @@
|
|||
import { useMemo } from "react";
|
||||
import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle, ArchiveRestoreIcon } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { ArchiveIcon, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
// helpers
|
||||
import { generateWorkItemLink } from "@/helpers/issue.helper";
|
||||
// types
|
||||
import { createCopyMenuWithDuplication } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns";
|
||||
|
||||
// Generic helper function to handle optional function calls gracefully
|
||||
// Overload for functions without parameters
|
||||
export function handleOptionalAction(
|
||||
optionalFn: (() => void) | (() => Promise<void>) | undefined,
|
||||
actionName: string
|
||||
): void;
|
||||
|
||||
// Overload for functions with one parameter
|
||||
export function handleOptionalAction<T>(
|
||||
optionalFn: ((param: T) => void) | ((param: T) => Promise<void>) | undefined,
|
||||
actionName: string,
|
||||
param: T
|
||||
): void;
|
||||
|
||||
// Implementation
|
||||
export function handleOptionalAction<T>(
|
||||
optionalFn: (() => void) | (() => Promise<void>) | ((param: T) => void) | ((param: T) => Promise<void>) | undefined,
|
||||
actionName: string,
|
||||
param?: T
|
||||
): void {
|
||||
if (optionalFn) {
|
||||
if (param !== undefined) {
|
||||
(optionalFn as (param: T) => void | Promise<void>)(param);
|
||||
} else {
|
||||
(optionalFn as () => void | Promise<void>)();
|
||||
}
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Action not available",
|
||||
message: `${actionName} action is not implemented.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface MenuItemFactoryProps {
|
||||
issue: TIssue;
|
||||
workspaceSlug?: string;
|
||||
projectIdentifier?: string;
|
||||
activeLayout?: string;
|
||||
isEditingAllowed: boolean;
|
||||
isArchivingAllowed?: boolean;
|
||||
isDeletingAllowed: boolean;
|
||||
isRestoringAllowed?: boolean;
|
||||
isInArchivableGroup?: boolean;
|
||||
issueTypeDetail?: { is_active?: boolean };
|
||||
isDraftIssue?: boolean;
|
||||
// Action handlers
|
||||
setTrackElement: (element: string) => void;
|
||||
setIssueToEdit: (issue: TIssue | undefined) => void;
|
||||
setCreateUpdateIssueModal: (open: boolean) => void;
|
||||
setDeleteIssueModal: (open: boolean) => void;
|
||||
setArchiveIssueModal?: (open: boolean) => void;
|
||||
setDuplicateWorkItemModal?: (open: boolean) => void;
|
||||
handleRemoveFromView?: () => void;
|
||||
handleRestore?: () => Promise<void>;
|
||||
// External handlers
|
||||
handleDelete?: () => Promise<void>;
|
||||
handleUpdate?: (data: TIssue) => Promise<void>;
|
||||
handleArchive?: () => Promise<void>;
|
||||
// Context-specific data
|
||||
cycleId?: string;
|
||||
moduleId?: string;
|
||||
storeType?: EIssuesStoreType;
|
||||
}
|
||||
|
||||
// Common action handlers hook
|
||||
export const useIssueActionHandlers = (props: MenuItemFactoryProps) => {
|
||||
const { issue, workspaceSlug, projectIdentifier, handleRestore } = props;
|
||||
|
||||
const workItemLink = useMemo(
|
||||
() =>
|
||||
generateWorkItemLink({
|
||||
workspaceSlug,
|
||||
projectId: issue?.project_id,
|
||||
issueId: issue?.id,
|
||||
projectIdentifier,
|
||||
sequenceId: issue?.sequence_id,
|
||||
}),
|
||||
[workspaceSlug, projectIdentifier, issue]
|
||||
);
|
||||
|
||||
const handleCopyIssueLink = () =>
|
||||
copyUrlToClipboard(workItemLink).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link copied",
|
||||
message: "Work item link copied to clipboard",
|
||||
})
|
||||
);
|
||||
|
||||
const handleOpenInNewTab = () => window.open(workItemLink, "_blank");
|
||||
|
||||
const handleIssueRestore = async () => {
|
||||
if (!handleRestore) {
|
||||
handleOptionalAction(handleRestore, "Restore");
|
||||
return;
|
||||
}
|
||||
await handleRestore()
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Restore success",
|
||||
message: "Your work item can be found in project work items.",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Work item could not be restored. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
workItemLink,
|
||||
handleCopyIssueLink,
|
||||
handleOpenInNewTab,
|
||||
handleIssueRestore,
|
||||
};
|
||||
};
|
||||
|
||||
export const useMenuItemFactory = (props: MenuItemFactoryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const actionHandlers = useIssueActionHandlers(props);
|
||||
|
||||
const {
|
||||
issue,
|
||||
activeLayout = "",
|
||||
isEditingAllowed,
|
||||
isArchivingAllowed = false,
|
||||
isDeletingAllowed,
|
||||
isRestoringAllowed = false,
|
||||
isInArchivableGroup = false,
|
||||
issueTypeDetail,
|
||||
setTrackElement,
|
||||
setIssueToEdit,
|
||||
setCreateUpdateIssueModal,
|
||||
setDeleteIssueModal,
|
||||
setArchiveIssueModal,
|
||||
setDuplicateWorkItemModal,
|
||||
handleRemoveFromView,
|
||||
} = props;
|
||||
|
||||
const createEditMenuItem = (customEditAction?: () => void): TContextMenuItem => ({
|
||||
key: "edit",
|
||||
title: t("common.actions.edit"),
|
||||
icon: Pencil,
|
||||
action:
|
||||
customEditAction ||
|
||||
(() => {
|
||||
setTrackElement(activeLayout);
|
||||
setIssueToEdit(issue);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}),
|
||||
shouldRender: isEditingAllowed,
|
||||
});
|
||||
|
||||
const createCopyMenuItem = (): TContextMenuItem => {
|
||||
const baseItem = {
|
||||
key: "make-a-copy",
|
||||
title: t("common.actions.make_a_copy"),
|
||||
icon: Copy,
|
||||
action: () => {
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
},
|
||||
shouldRender: isEditingAllowed && (issueTypeDetail?.is_active ?? true),
|
||||
};
|
||||
|
||||
return createCopyMenuWithDuplication({
|
||||
baseItem,
|
||||
activeLayout,
|
||||
setTrackElement,
|
||||
setCreateUpdateIssueModal,
|
||||
setDuplicateWorkItemModal,
|
||||
});
|
||||
};
|
||||
|
||||
const createOpenInNewTabMenuItem = (): TContextMenuItem => ({
|
||||
key: "open-in-new-tab",
|
||||
title: t("common.actions.open_in_new_tab"),
|
||||
icon: ExternalLink,
|
||||
action: actionHandlers.handleOpenInNewTab,
|
||||
});
|
||||
|
||||
const createCopyLinkMenuItem = (): TContextMenuItem => ({
|
||||
key: "copy-link",
|
||||
title: t("common.actions.copy_link"),
|
||||
icon: Link,
|
||||
action: actionHandlers.handleCopyIssueLink,
|
||||
});
|
||||
|
||||
const createRemoveFromCycleMenuItem = (): TContextMenuItem => ({
|
||||
key: "remove-from-cycle",
|
||||
title: "Remove from cycle",
|
||||
icon: XCircle,
|
||||
action: () => handleOptionalAction(handleRemoveFromView, "Remove from cycle"),
|
||||
shouldRender: isEditingAllowed,
|
||||
});
|
||||
|
||||
const createRemoveFromModuleMenuItem = (): TContextMenuItem => ({
|
||||
key: "remove-from-module",
|
||||
title: "Remove from module",
|
||||
icon: XCircle,
|
||||
action: () => handleOptionalAction(handleRemoveFromView, "Remove from module"),
|
||||
shouldRender: isEditingAllowed,
|
||||
});
|
||||
|
||||
const createArchiveMenuItem = (): TContextMenuItem => ({
|
||||
key: "archive",
|
||||
title: t("common.actions.archive"),
|
||||
description: isInArchivableGroup ? undefined : t("issue.archive.description"),
|
||||
icon: ArchiveIcon,
|
||||
className: "items-start",
|
||||
iconClassName: "mt-1",
|
||||
action: () => handleOptionalAction(setArchiveIssueModal, "Archive", true),
|
||||
disabled: !isInArchivableGroup,
|
||||
shouldRender: isArchivingAllowed,
|
||||
});
|
||||
|
||||
const createRestoreMenuItem = (): TContextMenuItem => ({
|
||||
key: "restore",
|
||||
title: "Restore",
|
||||
icon: ArchiveRestoreIcon,
|
||||
action: actionHandlers.handleIssueRestore,
|
||||
shouldRender: isRestoringAllowed,
|
||||
});
|
||||
|
||||
const createDeleteMenuItem = (): TContextMenuItem => ({
|
||||
key: "delete",
|
||||
title: t("common.actions.delete"),
|
||||
icon: Trash2,
|
||||
action: () => {
|
||||
setTrackElement(activeLayout);
|
||||
setDeleteIssueModal(true);
|
||||
},
|
||||
shouldRender: isDeletingAllowed,
|
||||
});
|
||||
|
||||
return {
|
||||
...actionHandlers,
|
||||
createEditMenuItem,
|
||||
createCopyMenuItem,
|
||||
createOpenInNewTabMenuItem,
|
||||
createCopyLinkMenuItem,
|
||||
createRemoveFromCycleMenuItem,
|
||||
createRemoveFromModuleMenuItem,
|
||||
createArchiveMenuItem,
|
||||
createRestoreMenuItem,
|
||||
createDeleteMenuItem,
|
||||
};
|
||||
};
|
||||
|
||||
// Predefined menu item sets for different contexts
|
||||
export const useProjectIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => {
|
||||
const factory = useMenuItemFactory(props);
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
factory.createEditMenuItem(),
|
||||
factory.createCopyMenuItem(),
|
||||
factory.createOpenInNewTabMenuItem(),
|
||||
factory.createCopyLinkMenuItem(),
|
||||
factory.createArchiveMenuItem(),
|
||||
factory.createDeleteMenuItem(),
|
||||
],
|
||||
[factory]
|
||||
);
|
||||
};
|
||||
|
||||
export const useAllIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => {
|
||||
const factory = useMenuItemFactory(props);
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
factory.createEditMenuItem(),
|
||||
factory.createCopyMenuItem(),
|
||||
factory.createOpenInNewTabMenuItem(),
|
||||
factory.createCopyLinkMenuItem(),
|
||||
factory.createArchiveMenuItem(),
|
||||
factory.createDeleteMenuItem(),
|
||||
],
|
||||
[factory]
|
||||
);
|
||||
};
|
||||
|
||||
export const useCycleIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => {
|
||||
const factory = useMenuItemFactory(props);
|
||||
|
||||
const customEditAction = () => {
|
||||
props.setIssueToEdit({
|
||||
...props.issue,
|
||||
cycle_id: props.cycleId ?? null,
|
||||
});
|
||||
props.setTrackElement(props.activeLayout || "");
|
||||
props.setCreateUpdateIssueModal(true);
|
||||
};
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
factory.createEditMenuItem(customEditAction),
|
||||
factory.createCopyMenuItem(),
|
||||
factory.createOpenInNewTabMenuItem(),
|
||||
factory.createCopyLinkMenuItem(),
|
||||
factory.createRemoveFromCycleMenuItem(),
|
||||
factory.createArchiveMenuItem(),
|
||||
factory.createDeleteMenuItem(),
|
||||
],
|
||||
[factory, props.cycleId]
|
||||
);
|
||||
};
|
||||
|
||||
export const useModuleIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => {
|
||||
const factory = useMenuItemFactory(props);
|
||||
|
||||
const customEditAction = () => {
|
||||
props.setIssueToEdit({
|
||||
...props.issue,
|
||||
module_ids: props.moduleId ? [props.moduleId] : [],
|
||||
});
|
||||
props.setTrackElement(props.activeLayout || "");
|
||||
props.setCreateUpdateIssueModal(true);
|
||||
};
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
factory.createEditMenuItem(customEditAction),
|
||||
factory.createCopyMenuItem(),
|
||||
factory.createOpenInNewTabMenuItem(),
|
||||
factory.createCopyLinkMenuItem(),
|
||||
factory.createRemoveFromModuleMenuItem(),
|
||||
factory.createArchiveMenuItem(),
|
||||
factory.createDeleteMenuItem(),
|
||||
],
|
||||
[factory, props.moduleId]
|
||||
);
|
||||
};
|
||||
|
||||
export const useArchivedIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => {
|
||||
const factory = useMenuItemFactory(props);
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
factory.createRestoreMenuItem(),
|
||||
factory.createOpenInNewTabMenuItem(),
|
||||
factory.createCopyLinkMenuItem(),
|
||||
factory.createDeleteMenuItem(),
|
||||
],
|
||||
[factory]
|
||||
);
|
||||
};
|
||||
|
||||
export const useDraftIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => {
|
||||
const factory = useMenuItemFactory(props);
|
||||
|
||||
return useMemo(() => [factory.createEditMenuItem(), factory.createDeleteMenuItem()], [factory]);
|
||||
};
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
export * from "./all-issue";
|
||||
export * from "./archived-issue";
|
||||
export * from "./cycle-issue";
|
||||
export * from "./draft-issue";
|
||||
export * from "./module-issue";
|
||||
export * from "./project-issue";
|
||||
export * from "./archived-issue";
|
||||
export * from "./draft-issue";
|
||||
export * from "./all-issue";
|
||||
export * from "./helper";
|
||||
export * from "../../workspace-draft/quick-action";
|
||||
|
|
|
|||
|
|
@ -4,21 +4,21 @@ import { useState } from "react";
|
|||
import omit from "lodash/omit";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react";
|
||||
// plane imports
|
||||
import { ARCHIVABLE_STATE_GROUPS, EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
import { ContextMenu, CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { generateWorkItemLink } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useIssues, useEventTracker, useProjectState, useUserPermissions, useProject } from "@/hooks/store";
|
||||
// types
|
||||
// plane-web components
|
||||
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
// helper
|
||||
import { useModuleIssueMenuItems, MenuItemFactoryProps } from "./helper";
|
||||
|
||||
export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
|
||||
const {
|
||||
|
|
@ -38,6 +38,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
|
|||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const [archiveIssueModal, setArchiveIssueModal] = useState(false);
|
||||
const [duplicateWorkItemModal, setDuplicateWorkItemModal] = useState(false);
|
||||
// router
|
||||
const { workspaceSlug, moduleId } = useParams();
|
||||
// store hooks
|
||||
|
|
@ -58,25 +59,6 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
|
|||
|
||||
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
||||
|
||||
const workItemLink = generateWorkItemLink({
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
projectId: issue?.project_id,
|
||||
issueId: issue?.id,
|
||||
projectIdentifier,
|
||||
sequenceId: issue?.sequence_id,
|
||||
});
|
||||
|
||||
const handleOpenInNewTab = () => window.open(workItemLink, "_blank");
|
||||
|
||||
const handleCopyIssueLink = () =>
|
||||
copyUrlToClipboard(workItemLink).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link copied",
|
||||
message: "Work item link copied to clipboard",
|
||||
})
|
||||
);
|
||||
|
||||
const duplicateIssuePayload = omit(
|
||||
{
|
||||
...issue,
|
||||
|
|
@ -86,72 +68,35 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
|
|||
["id"]
|
||||
);
|
||||
|
||||
const MENU_ITEMS: TContextMenuItem[] = [
|
||||
{
|
||||
key: "edit",
|
||||
title: "Edit",
|
||||
icon: Pencil,
|
||||
action: () => {
|
||||
setIssueToEdit({ ...issue, module_ids: moduleId ? [moduleId.toString()] : [] });
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
},
|
||||
shouldRender: isEditingAllowed,
|
||||
},
|
||||
{
|
||||
key: "make-a-copy",
|
||||
title: "Make a copy",
|
||||
icon: Copy,
|
||||
action: () => {
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
},
|
||||
shouldRender: isEditingAllowed,
|
||||
},
|
||||
{
|
||||
key: "open-in-new-tab",
|
||||
title: "Open in new tab",
|
||||
icon: ExternalLink,
|
||||
action: handleOpenInNewTab,
|
||||
},
|
||||
{
|
||||
key: "copy-link",
|
||||
title: "Copy link",
|
||||
icon: Link,
|
||||
action: handleCopyIssueLink,
|
||||
},
|
||||
{
|
||||
key: "remove-from-module",
|
||||
title: "Remove from module",
|
||||
icon: XCircle,
|
||||
action: () => handleRemoveFromView?.(),
|
||||
shouldRender: isEditingAllowed,
|
||||
},
|
||||
{
|
||||
key: "archive",
|
||||
title: "Archive",
|
||||
description: isInArchivableGroup ? undefined : "Only completed or canceled\nwork items can be archived",
|
||||
icon: ArchiveIcon,
|
||||
className: "items-start",
|
||||
iconClassName: "mt-1",
|
||||
action: () => setArchiveIssueModal(true),
|
||||
disabled: !isInArchivableGroup,
|
||||
shouldRender: isArchivingAllowed,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
title: "Delete",
|
||||
icon: Trash2,
|
||||
action: () => {
|
||||
setTrackElement(activeLayout);
|
||||
setDeleteIssueModal(true);
|
||||
},
|
||||
shouldRender: isDeletingAllowed,
|
||||
},
|
||||
];
|
||||
// Menu items and modals using helper
|
||||
const menuItemProps: MenuItemFactoryProps = {
|
||||
issue,
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
projectIdentifier,
|
||||
activeLayout,
|
||||
isEditingAllowed,
|
||||
isArchivingAllowed,
|
||||
isDeletingAllowed,
|
||||
isInArchivableGroup,
|
||||
setTrackElement,
|
||||
setIssueToEdit,
|
||||
setCreateUpdateIssueModal,
|
||||
setDeleteIssueModal,
|
||||
setArchiveIssueModal,
|
||||
setDuplicateWorkItemModal,
|
||||
handleRemoveFromView,
|
||||
moduleId: moduleId?.toString(),
|
||||
handleDelete,
|
||||
handleUpdate,
|
||||
handleArchive,
|
||||
storeType: EIssuesStoreType.MODULE,
|
||||
};
|
||||
|
||||
const MENU_ITEMS = useModuleIssueMenuItems(menuItemProps);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modals */}
|
||||
<ArchiveIssueModal
|
||||
data={issue}
|
||||
isOpen={archiveIssueModal}
|
||||
|
|
@ -175,7 +120,18 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
|
|||
if (issueToEdit && handleUpdate) await handleUpdate(data);
|
||||
}}
|
||||
storeType={EIssuesStoreType.MODULE}
|
||||
isDraft={false}
|
||||
/>
|
||||
{issue.project_id && workspaceSlug && (
|
||||
<DuplicateWorkItemModal
|
||||
workItemId={issue.id}
|
||||
isOpen={duplicateWorkItemModal}
|
||||
onClose={() => setDuplicateWorkItemModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={issue.project_id}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
<CustomMenu
|
||||
ellipsis
|
||||
|
|
@ -189,6 +145,73 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
|
|||
>
|
||||
{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();
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,24 @@
|
|||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import omit from "lodash/omit";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react";
|
||||
// plane imports
|
||||
import { ARCHIVABLE_STATE_GROUPS, EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
import { ContextMenu, CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { generateWorkItemLink } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useIssues, useProject, useProjectState, useUserPermissions } from "@/hooks/store";
|
||||
// types
|
||||
// plane-web components
|
||||
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
// helper
|
||||
import { useProjectIssueMenuItems, MenuItemFactoryProps } from "./helper";
|
||||
|
||||
export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
|
||||
const {
|
||||
|
|
@ -33,8 +32,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
|
|||
placements = "bottom-end",
|
||||
parentRef,
|
||||
} = props;
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
const pathname = usePathname();
|
||||
|
|
@ -43,6 +40,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
|
|||
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 { setTrackElement } = useEventTracker();
|
||||
|
|
@ -65,24 +63,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
|
|||
const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);
|
||||
const isDeletingAllowed = isEditingAllowed;
|
||||
|
||||
const workItemLink = generateWorkItemLink({
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
projectId: issue?.project_id,
|
||||
issueId: issue?.id,
|
||||
projectIdentifier,
|
||||
sequenceId: issue?.sequence_id,
|
||||
});
|
||||
|
||||
const handleCopyIssueLink = () =>
|
||||
copyUrlToClipboard(workItemLink).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link copied",
|
||||
message: "Work item link copied to clipboard",
|
||||
})
|
||||
);
|
||||
const handleOpenInNewTab = () => window.open(workItemLink, "_blank");
|
||||
|
||||
const isDraftIssue = pathname?.includes("draft-issues") || false;
|
||||
|
||||
const duplicateIssuePayload = omit(
|
||||
|
|
@ -95,68 +75,34 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
|
|||
["id"]
|
||||
);
|
||||
|
||||
const MENU_ITEMS: TContextMenuItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "edit",
|
||||
title: t("common.actions.edit"),
|
||||
icon: Pencil,
|
||||
action: () => {
|
||||
setTrackElement(activeLayout);
|
||||
setIssueToEdit(issue);
|
||||
setCreateUpdateIssueModal(true);
|
||||
},
|
||||
shouldRender: isEditingAllowed,
|
||||
},
|
||||
{
|
||||
key: "make-a-copy",
|
||||
title: t("common.actions.make_a_copy"),
|
||||
icon: Copy,
|
||||
action: () => {
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
},
|
||||
shouldRender: isEditingAllowed,
|
||||
},
|
||||
{
|
||||
key: "open-in-new-tab",
|
||||
title: t("common.actions.open_in_new_tab"),
|
||||
icon: ExternalLink,
|
||||
action: handleOpenInNewTab,
|
||||
},
|
||||
{
|
||||
key: "copy-link",
|
||||
title: t("common.actions.copy_link"),
|
||||
icon: Link,
|
||||
action: handleCopyIssueLink,
|
||||
},
|
||||
{
|
||||
key: "archive",
|
||||
title: t("common.actions.archive"),
|
||||
description: isInArchivableGroup ? undefined : t("issue.archive.description"),
|
||||
icon: ArchiveIcon,
|
||||
className: "items-start",
|
||||
iconClassName: "mt-1",
|
||||
action: () => setArchiveIssueModal(true),
|
||||
disabled: !isInArchivableGroup,
|
||||
shouldRender: isArchivingAllowed,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
title: t("common.actions.delete"),
|
||||
icon: Trash2,
|
||||
action: () => {
|
||||
setTrackElement(activeLayout);
|
||||
setDeleteIssueModal(true);
|
||||
},
|
||||
shouldRender: isDeletingAllowed,
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
// Menu items and modals using helper
|
||||
const menuItemProps: MenuItemFactoryProps = {
|
||||
issue,
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
projectIdentifier,
|
||||
activeLayout,
|
||||
isEditingAllowed,
|
||||
isArchivingAllowed,
|
||||
isDeletingAllowed,
|
||||
isInArchivableGroup,
|
||||
isDraftIssue,
|
||||
setTrackElement,
|
||||
setIssueToEdit,
|
||||
setCreateUpdateIssueModal,
|
||||
setDeleteIssueModal,
|
||||
setArchiveIssueModal,
|
||||
setDuplicateWorkItemModal,
|
||||
handleDelete,
|
||||
handleUpdate,
|
||||
handleArchive,
|
||||
storeType: EIssuesStoreType.PROJECT,
|
||||
};
|
||||
|
||||
const MENU_ITEMS = useProjectIssueMenuItems(menuItemProps);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modals */}
|
||||
<ArchiveIssueModal
|
||||
data={issue}
|
||||
isOpen={archiveIssueModal}
|
||||
|
|
@ -182,6 +128,16 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
|
|||
storeType={EIssuesStoreType.PROJECT}
|
||||
isDraft={isDraftIssue}
|
||||
/>
|
||||
{issue.project_id && workspaceSlug && (
|
||||
<DuplicateWorkItemModal
|
||||
workItemId={issue.id}
|
||||
isOpen={duplicateWorkItemModal}
|
||||
onClose={() => setDuplicateWorkItemModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={issue.project_id}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
<CustomMenu
|
||||
ellipsis
|
||||
|
|
@ -190,11 +146,77 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
|
|||
portalElement={portalElement}
|
||||
menuItemsClassName="z-[14]"
|
||||
maxHeight="lg"
|
||||
useCaptureForOutsideClick
|
||||
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();
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { EIssueServiceType } from "@plane/constants";
|
|||
import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite, TIssueServiceType } from "@plane/types";
|
||||
// plane web store
|
||||
import { IProjectEpics, IProjectEpicsFilter, ProjectEpics, ProjectEpicsFilter } from "@/plane-web/store/issue/epic";
|
||||
import { IIssueDetail, IssueDetail } from "@/plane-web/store/issue/issue-details/root.store";
|
||||
import { ITeamIssuesFilter, ITeamIssues, TeamIssues, TeamIssuesFilter } from "@/plane-web/store/issue/team";
|
||||
import {
|
||||
ITeamViewIssues,
|
||||
|
|
@ -19,7 +20,6 @@ import { IWorkspaceMembership } from "@/store/member/workspace-member.store";
|
|||
import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived";
|
||||
import { ICycleIssuesFilter, CycleIssuesFilter, ICycleIssues, CycleIssues } from "./cycle";
|
||||
import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft";
|
||||
import { IIssueDetail, IssueDetail } from "./issue-details/root.store";
|
||||
import { IIssueStore, IssueStore } from "./issue.store";
|
||||
import { ICalendarStore, CalendarStore } from "./issue_calendar_view.store";
|
||||
import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store";
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/components/issues/issue-layouts/quick-action-dropdowns"
|
||||
1
web/ee/store/issue/issue-details/root.store.ts
Normal file
1
web/ee/store/issue/issue-details/root.store.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/store/issue/issue-details/root.store";
|
||||
Loading…
Add table
Add a link
Reference in a new issue