diff --git a/apps/app/components/core/board-view/all-boards.tsx b/apps/app/components/core/board-view/all-boards.tsx index 1b3c31716..2c3c46462 100644 --- a/apps/app/components/core/board-view/all-boards.tsx +++ b/apps/app/components/core/board-view/all-boards.tsx @@ -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 = ({ states, members, addIssueToState, + makeIssueCopy, handleEditIssue, openIssuesListModal, handleDeleteIssue, @@ -66,6 +68,7 @@ export const AllBoards: React.FC = ({ selectedGroup={selectedGroup} members={members} handleEditIssue={handleEditIssue} + makeIssueCopy={makeIssueCopy} addIssueToState={() => addIssueToState(singleGroup, stateId)} handleDeleteIssue={handleDeleteIssue} openIssuesListModal={openIssuesListModal ?? null} diff --git a/apps/app/components/core/board-view/single-board.tsx b/apps/app/components/core/board-view/single-board.tsx index 8ed5f573a..d1d3154c5 100644 --- a/apps/app/components/core/board-view/single-board.tsx +++ b/apps/app/components/core/board-view/single-board.tsx @@ -29,6 +29,7 @@ type Props = { selectedGroup: NestedKeyOf | 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 = ({ selectedGroup, members, handleEditIssue, + makeIssueCopy, addIssueToState, handleDeleteIssue, openIssuesListModal, @@ -132,6 +134,7 @@ export const SingleBoard: React.FC = ({ selectedGroup={selectedGroup} properties={properties} editIssue={() => handleEditIssue(issue)} + makeIssueCopy={() => makeIssueCopy(issue)} handleDeleteIssue={handleDeleteIssue} orderBy={orderBy} handleTrashBox={handleTrashBox} diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx index 7ef8e0211..a187b104f 100644 --- a/apps/app/components/core/board-view/single-issue.tsx +++ b/apps/app/components/core/board-view/single-issue.tsx @@ -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 | null; properties: Properties; editIssue: () => void; + makeIssueCopy: () => void; removeIssue?: (() => void) | null; handleDeleteIssue: (issue: IIssue) => void; orderBy: NestedKeyOf | null; @@ -62,12 +70,14 @@ export const SingleBoardIssue: React.FC = ({ 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,154 +193,138 @@ export const SingleBoardIssue: React.FC = ({ 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 ( -
{ - e.preventDefault(); - setContextMenu(true); - setContextMenuPosition({ x: e.pageX, y: e.pageY }); - }} - > - {contextMenu && ( -
-
- - Open issue - - - -
-
- )} -
- {!isNotAllowed && ( -
- {type && !isNotAllowed && ( - - Edit issue - {type !== "issue" && removeIssue && ( - - <>Remove from {type} + <> + + + Edit issue + + + Make a copy... + + handleDeleteIssue(issue)}> + Delete issue + + + Copy issue link + + +
{ + e.preventDefault(); + setContextMenu(true); + setContextMenuPosition({ x: e.pageX, y: e.pageY }); + }} + > +
+ {!isNotAllowed && ( +
+ {type && !isNotAllowed && ( + + Edit issue + {type !== "issue" && removeIssue && ( + + <>Remove from {type} + + )} + handleDeleteIssue(issue)}> + Delete issue - )} - handleDeleteIssue(issue)}> - Delete issue - - Copy issue link - + + Copy issue link + + + )} +
+ )} + + + {properties.key && ( +
+ {issue.project_detail.identifier}-{issue.sequence_id} +
+ )} +
+ {issue.name} +
+
+ +
+ {properties.priority && selectedGroup !== "priority" && ( + )} -
- )} - - - {properties.key && ( -
- {issue.project_detail.identifier}-{issue.sequence_id} + {properties.state && selectedGroup !== "state_detail.name" && ( + + )} + {properties.due_date && ( + + )} + {properties.sub_issue_count && ( +
+ {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
)} -
- {issue.name} -
-
- -
- {properties.priority && selectedGroup !== "priority" && ( - - )} - {properties.state && selectedGroup !== "state_detail.name" && ( - - )} - {properties.due_date && ( - - )} - {properties.sub_issue_count && ( -
- {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} -
- )} - {properties.labels && ( -
- {issue.label_details.map((label) => ( - + {properties.labels && ( +
+ {issue.label_details.map((label) => ( - {label.name} - - ))} -
- )} - {properties.assignee && ( - - )} + key={label.id} + className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs" + > + + {label.name} + + ))} +
+ )} + {properties.assignee && ( + + )} +
-
+ ); }; diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index 2cefcfcf5..762506499 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -269,6 +269,15 @@ export const IssuesView: React.FC = ({ [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 = ({ {(provided, snapshot) => (
= ({ states={states} members={members} addIssueToState={addIssueToState} + makeIssueCopy={makeIssueCopy} handleEditIssue={handleEditIssue} openIssuesListModal={type !== "issue" ? openIssuesListModal : null} handleDeleteIssue={handleDeleteIssue} diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx index 249927f25..9158b7738 100644 --- a/apps/app/components/issues/sidebar.tsx +++ b/apps/app/components/issues/sidebar.tsx @@ -77,17 +77,6 @@ export const IssueDetailsSidebar: React.FC = ({ 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 = ({ isOpen={deleteIssueModal} data={issueDetail ?? null} /> -
+

{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id} @@ -453,8 +442,8 @@ export const IssueDetailsSidebar: React.FC = ({ ); } else return ( -
-
+
+
{" "} {label.name}
diff --git a/apps/app/components/ui/context-menu.tsx b/apps/app/components/ui/context-menu.tsx new file mode 100644 index 000000000..749a986dd --- /dev/null +++ b/apps/app/components/ui/context-menu.tsx @@ -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>; +}; + +const ContextMenu = ({ position, children, title, isOpen, setIsOpen }: Props) => { + useEffect(() => { + const hideContextMenu = () => setIsOpen(false); + + window.addEventListener("click", hideContextMenu); + + return () => { + window.removeEventListener("click", hideContextMenu); + }; + }, [setIsOpen]); + + return ( +
+
+ {title &&

{title}

} + {children} +
+
+ ); +}; + +type MenuItemProps = { + children: JSX.Element | string; + renderAs?: "button" | "a"; + href?: string; + onClick?: () => void; + className?: string; + Icon?: any; +}; + +const MenuItem: React.FC = ({ + children, + renderAs, + href = "", + onClick, + className = "", + Icon, +}) => ( +
+ {renderAs === "a" ? ( + + + <> + {Icon && } + {children} + + + + ) : ( + + )} +
+); + +ContextMenu.Item = MenuItem; + +export { ContextMenu }; diff --git a/apps/app/components/ui/index.ts b/apps/app/components/ui/index.ts index 66f0ea1e5..607caecd5 100644 --- a/apps/app/components/ui/index.ts +++ b/apps/app/components/ui/index.ts @@ -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";