forked from github/plane
refactor: global context menu component
This commit is contained in:
parent
0a681937fd
commit
a4da4bf889
@ -11,6 +11,7 @@ type Props = {
|
|||||||
states: IState[] | undefined;
|
states: IState[] | undefined;
|
||||||
members: IProjectMember[] | undefined;
|
members: IProjectMember[] | undefined;
|
||||||
addIssueToState: (groupTitle: string, stateId: string | null) => void;
|
addIssueToState: (groupTitle: string, stateId: string | null) => void;
|
||||||
|
makeIssueCopy: (issue: IIssue) => void;
|
||||||
handleEditIssue: (issue: IIssue) => void;
|
handleEditIssue: (issue: IIssue) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
@ -25,6 +26,7 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
states,
|
states,
|
||||||
members,
|
members,
|
||||||
addIssueToState,
|
addIssueToState,
|
||||||
|
makeIssueCopy,
|
||||||
handleEditIssue,
|
handleEditIssue,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
@ -66,6 +68,7 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
selectedGroup={selectedGroup}
|
selectedGroup={selectedGroup}
|
||||||
members={members}
|
members={members}
|
||||||
handleEditIssue={handleEditIssue}
|
handleEditIssue={handleEditIssue}
|
||||||
|
makeIssueCopy={makeIssueCopy}
|
||||||
addIssueToState={() => addIssueToState(singleGroup, stateId)}
|
addIssueToState={() => addIssueToState(singleGroup, stateId)}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
openIssuesListModal={openIssuesListModal ?? null}
|
openIssuesListModal={openIssuesListModal ?? null}
|
||||||
|
@ -29,6 +29,7 @@ type Props = {
|
|||||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||||
members: IProjectMember[] | undefined;
|
members: IProjectMember[] | undefined;
|
||||||
handleEditIssue: (issue: IIssue) => void;
|
handleEditIssue: (issue: IIssue) => void;
|
||||||
|
makeIssueCopy: (issue: IIssue) => void;
|
||||||
addIssueToState: () => void;
|
addIssueToState: () => void;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
@ -47,6 +48,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
selectedGroup,
|
selectedGroup,
|
||||||
members,
|
members,
|
||||||
handleEditIssue,
|
handleEditIssue,
|
||||||
|
makeIssueCopy,
|
||||||
addIssueToState,
|
addIssueToState,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
@ -132,6 +134,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
selectedGroup={selectedGroup}
|
selectedGroup={selectedGroup}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
editIssue={() => handleEditIssue(issue)}
|
editIssue={() => handleEditIssue(issue)}
|
||||||
|
makeIssueCopy={() => makeIssueCopy(issue)}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
orderBy={orderBy}
|
orderBy={orderBy}
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
|
@ -24,7 +24,14 @@ import {
|
|||||||
ViewStateSelect,
|
ViewStateSelect,
|
||||||
} from "components/issues/view-select";
|
} from "components/issues/view-select";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "components/ui";
|
import { ContextMenu, CustomMenu } from "components/ui";
|
||||||
|
// icons
|
||||||
|
import {
|
||||||
|
ClipboardDocumentCheckIcon,
|
||||||
|
LinkIcon,
|
||||||
|
PencilIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
@ -47,6 +54,7 @@ type Props = {
|
|||||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
editIssue: () => void;
|
editIssue: () => void;
|
||||||
|
makeIssueCopy: () => void;
|
||||||
removeIssue?: (() => void) | null;
|
removeIssue?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
orderBy: NestedKeyOf<IIssue> | null;
|
orderBy: NestedKeyOf<IIssue> | null;
|
||||||
@ -62,12 +70,14 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
selectedGroup,
|
selectedGroup,
|
||||||
properties,
|
properties,
|
||||||
editIssue,
|
editIssue,
|
||||||
|
makeIssueCopy,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
orderBy,
|
orderBy,
|
||||||
handleTrashBox,
|
handleTrashBox,
|
||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
|
// context menu
|
||||||
const [contextMenu, setContextMenu] = useState(false);
|
const [contextMenu, setContextMenu] = useState(false);
|
||||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
@ -183,19 +193,29 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
if (snapshot.isDragging) handleTrashBox(snapshot.isDragging);
|
if (snapshot.isDragging) handleTrashBox(snapshot.isDragging);
|
||||||
}, [snapshot, handleTrashBox]);
|
}, [snapshot, handleTrashBox]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const hideContextMenu = () => setContextMenu(false);
|
|
||||||
|
|
||||||
window.addEventListener("click", hideContextMenu);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("click", hideContextMenu);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<ContextMenu
|
||||||
|
position={contextMenuPosition}
|
||||||
|
title="Quick actions"
|
||||||
|
isOpen={contextMenu}
|
||||||
|
setIsOpen={setContextMenu}
|
||||||
|
>
|
||||||
|
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}>
|
||||||
|
Edit issue
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
|
||||||
|
Make a copy...
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}>
|
||||||
|
Delete issue
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||||
|
Copy issue link
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu>
|
||||||
<div
|
<div
|
||||||
className={`mb-3 rounded bg-white shadow ${
|
className={`mb-3 rounded bg-white shadow ${
|
||||||
snapshot.isDragging ? "border-2 border-theme shadow-lg" : ""
|
snapshot.isDragging ? "border-2 border-theme shadow-lg" : ""
|
||||||
@ -210,35 +230,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
setContextMenuPosition({ x: e.pageX, y: e.pageY });
|
setContextMenuPosition({ x: e.pageX, y: e.pageY });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{contextMenu && (
|
|
||||||
<div className="fixed z-20 h-full w-full">
|
|
||||||
<div
|
|
||||||
className={`fixed z-20 flex flex-col items-stretch gap-1 rounded-md border bg-white p-2 text-xs shadow-lg`}
|
|
||||||
style={{
|
|
||||||
top: `${contextMenuPosition.y}px`,
|
|
||||||
left: `${contextMenuPosition.x}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
|
|
||||||
<a className="w-full rounded px-1 py-1.5 text-left hover:bg-hover-gray">Open issue</a>
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="rounded px-1 py-1.5 text-left hover:bg-hover-gray"
|
|
||||||
onClick={() => handleDeleteIssue(issue)}
|
|
||||||
>
|
|
||||||
Delete issue
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="rounded px-1 py-1.5 text-left hover:bg-hover-gray"
|
|
||||||
onClick={handleCopyText}
|
|
||||||
>
|
|
||||||
Copy issue link
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="group/card relative select-none p-4">
|
<div className="group/card relative select-none p-4">
|
||||||
{!isNotAllowed && (
|
{!isNotAllowed && (
|
||||||
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100">
|
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100">
|
||||||
@ -253,7 +244,9 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
|
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
|
||||||
Delete issue
|
Delete issue
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem onClick={handleCopyText}>Copy issue link</CustomMenu.MenuItem>
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
|
Copy issue link
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -332,5 +325,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -269,6 +269,15 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
[setCreateIssueModal, setPreloadedData, selectedGroup]
|
[setCreateIssueModal, setPreloadedData, selectedGroup]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const makeIssueCopy = useCallback(
|
||||||
|
(issue: IIssue) => {
|
||||||
|
setCreateIssueModal(true);
|
||||||
|
|
||||||
|
setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" });
|
||||||
|
},
|
||||||
|
[setCreateIssueModal, setPreloadedData]
|
||||||
|
);
|
||||||
|
|
||||||
const handleEditIssue = useCallback(
|
const handleEditIssue = useCallback(
|
||||||
(issue: IIssue) => {
|
(issue: IIssue) => {
|
||||||
setEditIssueModal(true);
|
setEditIssueModal(true);
|
||||||
@ -370,8 +379,8 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
trashBox ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
|
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
||||||
} fixed z-20 top-9 right-9 flex justify-center items-center gap-2 bg-red-100 border-2 border-red-500 p-3 w-96 h-28 text-xs italic text-red-500 font-medium rounded ${
|
} fixed top-9 right-9 z-20 flex h-28 w-96 items-center justify-center gap-2 rounded border-2 border-red-500 bg-red-100 p-3 text-xs font-medium italic text-red-500 ${
|
||||||
snapshot.isDraggingOver ? "bg-red-500 text-white" : ""
|
snapshot.isDraggingOver ? "bg-red-500 text-white" : ""
|
||||||
} duration-200`}
|
} duration-200`}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
@ -408,6 +417,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
states={states}
|
states={states}
|
||||||
members={members}
|
members={members}
|
||||||
addIssueToState={addIssueToState}
|
addIssueToState={addIssueToState}
|
||||||
|
makeIssueCopy={makeIssueCopy}
|
||||||
handleEditIssue={handleEditIssue}
|
handleEditIssue={handleEditIssue}
|
||||||
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
@ -77,17 +77,6 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
console.log("isseu details: ", issueDetail);
|
|
||||||
|
|
||||||
// const { data: issueLinks } = useSWR(
|
|
||||||
// workspaceSlug && projectId
|
|
||||||
// ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
|
||||||
// : null,
|
|
||||||
// workspaceSlug && projectId
|
|
||||||
// ? () => issuesService.getIssues(workspaceSlug as string, projectId as string)
|
|
||||||
// : null
|
|
||||||
// );
|
|
||||||
|
|
||||||
const { data: issues } = useSWR(
|
const { data: issues } = useSWR(
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
||||||
@ -228,7 +217,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
isOpen={deleteIssueModal}
|
isOpen={deleteIssueModal}
|
||||||
data={issueDetail ?? null}
|
data={issueDetail ?? null}
|
||||||
/>
|
/>
|
||||||
<div className="w-full divide-y-2 divide-gray-100 sticky top-5">
|
<div className="sticky top-5 w-full divide-y-2 divide-gray-100">
|
||||||
<div className="flex items-center justify-between pb-3">
|
<div className="flex items-center justify-between pb-3">
|
||||||
<h4 className="text-sm font-medium">
|
<h4 className="text-sm font-medium">
|
||||||
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
|
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
|
||||||
@ -453,8 +442,8 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
} else
|
} else
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-50 border-y border-gray-400">
|
<div className="border-y border-gray-400 bg-gray-50">
|
||||||
<div className="flex select-none font-medium items-center gap-2 truncate p-2 text-gray-900">
|
<div className="flex select-none items-center gap-2 truncate p-2 font-medium text-gray-900">
|
||||||
<RectangleGroupIcon className="h-3 w-3" />{" "}
|
<RectangleGroupIcon className="h-3 w-3" />{" "}
|
||||||
{label.name}
|
{label.name}
|
||||||
</div>
|
</div>
|
||||||
|
87
apps/app/components/ui/context-menu.tsx
Normal file
87
apps/app/components/ui/context-menu.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
children: React.ReactNode;
|
||||||
|
title?: string | JSX.Element;
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContextMenu = ({ position, children, title, isOpen, setIsOpen }: Props) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const hideContextMenu = () => setIsOpen(false);
|
||||||
|
|
||||||
|
window.addEventListener("click", hideContextMenu);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("click", hideContextMenu);
|
||||||
|
};
|
||||||
|
}, [setIsOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`fixed z-20 h-full w-full ${
|
||||||
|
isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`fixed z-20 flex min-w-[8rem] flex-col items-stretch gap-1 rounded-md border bg-white p-2 text-xs shadow-lg`}
|
||||||
|
style={{
|
||||||
|
left: `${position.x}px`,
|
||||||
|
top: `${position.y}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title && <h4 className="border-b px-1 py-1 pb-2 text-[0.8rem] font-medium">{title}</h4>}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type MenuItemProps = {
|
||||||
|
children: JSX.Element | string;
|
||||||
|
renderAs?: "button" | "a";
|
||||||
|
href?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
className?: string;
|
||||||
|
Icon?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MenuItem: React.FC<MenuItemProps> = ({
|
||||||
|
children,
|
||||||
|
renderAs,
|
||||||
|
href = "",
|
||||||
|
onClick,
|
||||||
|
className = "",
|
||||||
|
Icon,
|
||||||
|
}) => (
|
||||||
|
<div className={`${className} w-full rounded px-1 py-1.5 text-left hover:bg-hover-gray`}>
|
||||||
|
{renderAs === "a" ? (
|
||||||
|
<Link href={href}>
|
||||||
|
<a className="flex items-center gap-2">
|
||||||
|
<>
|
||||||
|
{Icon && <Icon />}
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<button type="button" className="flex items-center gap-2" onClick={onClick}>
|
||||||
|
<>
|
||||||
|
{Icon && <Icon height={12} width={12} />}
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
ContextMenu.Item = MenuItem;
|
||||||
|
|
||||||
|
export { ContextMenu };
|
@ -2,6 +2,7 @@ export * from "./input";
|
|||||||
export * from "./text-area";
|
export * from "./text-area";
|
||||||
export * from "./avatar";
|
export * from "./avatar";
|
||||||
export * from "./button";
|
export * from "./button";
|
||||||
|
export * from "./context-menu";
|
||||||
export * from "./custom-menu";
|
export * from "./custom-menu";
|
||||||
export * from "./custom-search-select";
|
export * from "./custom-search-select";
|
||||||
export * from "./custom-select";
|
export * from "./custom-select";
|
||||||
|
Loading…
Reference in New Issue
Block a user