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;
|
||||
members: IProjectMember[] | undefined;
|
||||
addIssueToState: (groupTitle: string, stateId: string | null) => void;
|
||||
makeIssueCopy: (issue: IIssue) => void;
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
@ -25,6 +26,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
states,
|
||||
members,
|
||||
addIssueToState,
|
||||
makeIssueCopy,
|
||||
handleEditIssue,
|
||||
openIssuesListModal,
|
||||
handleDeleteIssue,
|
||||
@ -66,6 +68,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
selectedGroup={selectedGroup}
|
||||
members={members}
|
||||
handleEditIssue={handleEditIssue}
|
||||
makeIssueCopy={makeIssueCopy}
|
||||
addIssueToState={() => addIssueToState(singleGroup, stateId)}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
openIssuesListModal={openIssuesListModal ?? null}
|
||||
|
@ -29,6 +29,7 @@ type Props = {
|
||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||
members: IProjectMember[] | undefined;
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
makeIssueCopy: (issue: IIssue) => void;
|
||||
addIssueToState: () => void;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
@ -47,6 +48,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
selectedGroup,
|
||||
members,
|
||||
handleEditIssue,
|
||||
makeIssueCopy,
|
||||
addIssueToState,
|
||||
handleDeleteIssue,
|
||||
openIssuesListModal,
|
||||
@ -132,6 +134,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
selectedGroup={selectedGroup}
|
||||
properties={properties}
|
||||
editIssue={() => handleEditIssue(issue)}
|
||||
makeIssueCopy={() => makeIssueCopy(issue)}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
orderBy={orderBy}
|
||||
handleTrashBox={handleTrashBox}
|
||||
|
@ -24,7 +24,14 @@ import {
|
||||
ViewStateSelect,
|
||||
} from "components/issues/view-select";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
import { ContextMenu, CustomMenu } from "components/ui";
|
||||
// icons
|
||||
import {
|
||||
ClipboardDocumentCheckIcon,
|
||||
LinkIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
@ -47,6 +54,7 @@ type Props = {
|
||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||
properties: Properties;
|
||||
editIssue: () => void;
|
||||
makeIssueCopy: () => void;
|
||||
removeIssue?: (() => void) | null;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
orderBy: NestedKeyOf<IIssue> | null;
|
||||
@ -62,12 +70,14 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
selectedGroup,
|
||||
properties,
|
||||
editIssue,
|
||||
makeIssueCopy,
|
||||
removeIssue,
|
||||
handleDeleteIssue,
|
||||
orderBy,
|
||||
handleTrashBox,
|
||||
userAuth,
|
||||
}) => {
|
||||
// context menu
|
||||
const [contextMenu, setContextMenu] = useState(false);
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
@ -183,19 +193,29 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
if (snapshot.isDragging) handleTrashBox(snapshot.isDragging);
|
||||
}, [snapshot, handleTrashBox]);
|
||||
|
||||
useEffect(() => {
|
||||
const hideContextMenu = () => setContextMenu(false);
|
||||
|
||||
window.addEventListener("click", hideContextMenu);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("click", hideContextMenu);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
|
||||
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
|
||||
className={`mb-3 rounded bg-white shadow ${
|
||||
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 });
|
||||
}}
|
||||
>
|
||||
{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">
|
||||
{!isNotAllowed && (
|
||||
<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)}>
|
||||
Delete issue
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>Copy issue link</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
Copy issue link
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
@ -332,5 +325,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -269,6 +269,15 @@ export const IssuesView: React.FC<Props> = ({
|
||||
[setCreateIssueModal, setPreloadedData, selectedGroup]
|
||||
);
|
||||
|
||||
const makeIssueCopy = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setCreateIssueModal(true);
|
||||
|
||||
setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" });
|
||||
},
|
||||
[setCreateIssueModal, setPreloadedData]
|
||||
);
|
||||
|
||||
const handleEditIssue = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setEditIssueModal(true);
|
||||
@ -370,8 +379,8 @@ export const IssuesView: React.FC<Props> = ({
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`${
|
||||
trashBox ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
|
||||
} 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 ${
|
||||
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
||||
} 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" : ""
|
||||
} duration-200`}
|
||||
ref={provided.innerRef}
|
||||
@ -408,6 +417,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
states={states}
|
||||
members={members}
|
||||
addIssueToState={addIssueToState}
|
||||
makeIssueCopy={makeIssueCopy}
|
||||
handleEditIssue={handleEditIssue}
|
||||
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
|
@ -77,17 +77,6 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
|
||||
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(
|
||||
workspaceSlug && projectId
|
||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
||||
@ -228,7 +217,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
isOpen={deleteIssueModal}
|
||||
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">
|
||||
<h4 className="text-sm font-medium">
|
||||
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
|
||||
@ -453,8 +442,8 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
);
|
||||
} else
|
||||
return (
|
||||
<div className="bg-gray-50 border-y border-gray-400">
|
||||
<div className="flex select-none font-medium items-center gap-2 truncate p-2 text-gray-900">
|
||||
<div className="border-y border-gray-400 bg-gray-50">
|
||||
<div className="flex select-none items-center gap-2 truncate p-2 font-medium text-gray-900">
|
||||
<RectangleGroupIcon className="h-3 w-3" />{" "}
|
||||
{label.name}
|
||||
</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 "./avatar";
|
||||
export * from "./button";
|
||||
export * from "./context-menu";
|
||||
export * from "./custom-menu";
|
||||
export * from "./custom-search-select";
|
||||
export * from "./custom-select";
|
||||
|
Loading…
Reference in New Issue
Block a user