diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx index a187b104f..2d2c34626 100644 --- a/apps/app/components/core/board-view/single-issue.tsx +++ b/apps/app/components/core/board-view/single-issue.tsx @@ -101,6 +101,7 @@ export const SingleBoardIssue: React.FC<Props> = ({ issue_detail: { ...p.issue_detail, ...formData, + assignees: formData.assignees_list ?? p.issue_detail.assignees_list, }, }; } @@ -122,6 +123,7 @@ export const SingleBoardIssue: React.FC<Props> = ({ issue_detail: { ...p.issue_detail, ...formData, + assignees: formData.assignees_list ?? p.issue_detail.assignees_list, }, }; } @@ -136,7 +138,8 @@ export const SingleBoardIssue: React.FC<Props> = ({ PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), (prevData) => (prevData ?? []).map((p) => { - if (p.id === issue.id) return { ...p, ...formData }; + if (p.id === issue.id) + return { ...p, ...formData, assignees: formData.assignees_list ?? p.assignees_list }; return p; }), @@ -159,10 +162,10 @@ export const SingleBoardIssue: React.FC<Props> = ({ [workspaceSlug, projectId, cycleId, moduleId, issue] ); - function getStyle( + const getStyle = ( style: DraggingStyle | NotDraggingStyle | undefined, snapshot: DraggableStateSnapshot - ) { + ) => { if (orderBy === "sort_order") return style; if (!snapshot.isDragging) return {}; if (!snapshot.isDropAnimating) { @@ -173,7 +176,7 @@ export const SingleBoardIssue: React.FC<Props> = ({ ...style, transitionDuration: `0.001s`, }; - } + }; const handleCopyText = () => { const originURL = @@ -295,7 +298,7 @@ export const SingleBoardIssue: React.FC<Props> = ({ {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} </div> )} - {properties.labels && ( + {properties.labels && issue.label_details.length > 0 && ( <div className="flex flex-wrap gap-1"> {issue.label_details.map((label) => ( <span diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index 762506499..779338321 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -398,6 +398,7 @@ export const IssuesView: React.FC<Props> = ({ states={states} members={members} addIssueToState={addIssueToState} + makeIssueCopy={makeIssueCopy} handleEditIssue={handleEditIssue} handleDeleteIssue={handleDeleteIssue} openIssuesListModal={type !== "issue" ? openIssuesListModal : null} diff --git a/apps/app/components/core/list-view/all-lists.tsx b/apps/app/components/core/list-view/all-lists.tsx index c2b6c498a..d7c93cb78 100644 --- a/apps/app/components/core/list-view/all-lists.tsx +++ b/apps/app/components/core/list-view/all-lists.tsx @@ -12,6 +12,7 @@ type Props = { states: IState[] | undefined; members: IProjectMember[] | undefined; addIssueToState: (groupTitle: string, stateId: string | null) => void; + makeIssueCopy: (issue: IIssue) => void; handleEditIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void; openIssuesListModal?: (() => void) | null; @@ -25,6 +26,7 @@ export const AllLists: React.FC<Props> = ({ states, members, addIssueToState, + makeIssueCopy, openIssuesListModal, handleEditIssue, handleDeleteIssue, @@ -50,6 +52,7 @@ export const AllLists: React.FC<Props> = ({ selectedGroup={selectedGroup} members={members} addIssueToState={() => addIssueToState(singleGroup, stateId)} + makeIssueCopy={makeIssueCopy} handleEditIssue={handleEditIssue} handleDeleteIssue={handleDeleteIssue} openIssuesListModal={type !== "issue" ? openIssuesListModal : null} diff --git a/apps/app/components/core/list-view/single-issue.tsx b/apps/app/components/core/list-view/single-issue.tsx index 0dea00020..134fffa1c 100644 --- a/apps/app/components/core/list-view/single-issue.tsx +++ b/apps/app/components/core/list-view/single-issue.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -18,7 +18,14 @@ import { } from "components/issues/view-select"; // ui -import { Tooltip, CustomMenu } from "components/ui"; +import { Tooltip, CustomMenu, ContextMenu } from "components/ui"; +// icons +import { + ClipboardDocumentCheckIcon, + LinkIcon, + PencilIcon, + TrashIcon, +} from "@heroicons/react/24/outline"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types @@ -31,6 +38,7 @@ type Props = { issue: IIssue; properties: Properties; editIssue: () => void; + makeIssueCopy: () => void; removeIssue?: (() => void) | null; handleDeleteIssue: (issue: IIssue) => void; userAuth: UserAuth; @@ -41,13 +49,20 @@ export const SingleListIssue: React.FC<Props> = ({ issue, properties, editIssue, + makeIssueCopy, removeIssue, handleDeleteIssue, userAuth, }) => { + // context menu + const [contextMenu, setContextMenu] = useState(false); + const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); + const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { setToastAlert } = useToast(); + const partialUpdateIssue = useCallback( (formData: Partial<IIssue>) => { if (!workspaceSlug || !projectId) return; @@ -63,6 +78,7 @@ export const SingleListIssue: React.FC<Props> = ({ issue_detail: { ...p.issue_detail, ...formData, + assignees: formData.assignees_list ?? p.issue_detail.assignees_list, }, }; } @@ -84,6 +100,7 @@ export const SingleListIssue: React.FC<Props> = ({ issue_detail: { ...p.issue_detail, ...formData, + assignees: formData.assignees_list ?? p.issue_detail.assignees_list, }, }; } @@ -98,7 +115,8 @@ export const SingleListIssue: React.FC<Props> = ({ PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), (prevData) => (prevData ?? []).map((p) => { - if (p.id === issue.id) return { ...p, ...formData }; + if (p.id === issue.id) + return { ...p, ...formData, assignees: formData.assignees_list ?? p.assignees_list }; return p; }), @@ -134,104 +152,136 @@ export const SingleListIssue: React.FC<Props> = ({ }); }); }; + const isNotAllowed = userAuth.isGuest || userAuth.isViewer; return ( - <div className="flex items-center justify-between gap-2 px-4 py-3 text-sm"> - <div className="flex items-center gap-2"> - <span - className="block h-1.5 w-1.5 flex-shrink-0 rounded-full" - style={{ - backgroundColor: issue.state_detail.color, - }} - /> - <Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}> - <a className="group relative flex items-center gap-2"> - {properties.key && ( - <Tooltip - tooltipHeading="ID" - tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`} - > - <span className="flex-shrink-0 text-xs text-gray-500"> - {issue.project_detail?.identifier}-{issue.sequence_id} + <> + <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="flex items-center justify-between gap-2 px-4 py-3 text-sm" + onContextMenu={(e) => { + e.preventDefault(); + setContextMenu(true); + setContextMenuPosition({ x: e.pageX, y: e.pageY }); + }} + > + <div className="flex items-center gap-2"> + <span + className="block h-1.5 w-1.5 flex-shrink-0 rounded-full" + style={{ + backgroundColor: issue.state_detail.color, + }} + /> + <Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}> + <a className="group relative flex items-center gap-2"> + {properties.key && ( + <Tooltip + tooltipHeading="ID" + tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`} + > + <span className="flex-shrink-0 text-xs text-gray-500"> + {issue.project_detail?.identifier}-{issue.sequence_id} + </span> + </Tooltip> + )} + <Tooltip tooltipHeading="Title" tooltipContent={issue.name}> + <span className="w-auto max-w-lg overflow-hidden text-ellipsis whitespace-nowrap"> + {issue.name} </span> </Tooltip> - )} - <Tooltip tooltipHeading="Title" tooltipContent={issue.name}> - <span className="w-auto max-w-lg text-ellipsis overflow-hidden whitespace-nowrap"> - {issue.name} - </span> - </Tooltip> - </a> - </Link> - </div> - <div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs"> - {properties.priority && ( - <ViewPrioritySelect - issue={issue} - partialUpdateIssue={partialUpdateIssue} - isNotAllowed={isNotAllowed} - /> - )} - {properties.state && ( - <ViewStateSelect - issue={issue} - partialUpdateIssue={partialUpdateIssue} - isNotAllowed={isNotAllowed} - /> - )} - {properties.due_date && ( - <ViewDueDateSelect - issue={issue} - partialUpdateIssue={partialUpdateIssue} - isNotAllowed={isNotAllowed} - /> - )} - {properties.sub_issue_count && ( - <div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm"> - {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} - </div> - )} - {properties.labels && ( - <div className="flex flex-wrap gap-1"> - {issue.label_details.map((label) => ( - <span - key={label.id} - className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs" - > + </a> + </Link> + </div> + <div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs"> + {properties.priority && ( + <ViewPrioritySelect + issue={issue} + partialUpdateIssue={partialUpdateIssue} + position="right" + isNotAllowed={isNotAllowed} + /> + )} + {properties.state && ( + <ViewStateSelect + issue={issue} + partialUpdateIssue={partialUpdateIssue} + position="right" + isNotAllowed={isNotAllowed} + /> + )} + {properties.due_date && ( + <ViewDueDateSelect + issue={issue} + partialUpdateIssue={partialUpdateIssue} + isNotAllowed={isNotAllowed} + /> + )} + {properties.sub_issue_count && ( + <div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm"> + {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} + </div> + )} + {properties.labels && ( + <div className="flex flex-wrap gap-1"> + {issue.label_details.map((label) => ( <span - className="h-1.5 w-1.5 flex-shrink-0 rounded-full" - style={{ - backgroundColor: label?.color && label.color !== "" ? label.color : "#000", - }} - /> - {label.name} - </span> - ))} - </div> - )} - {properties.assignee && ( - <ViewAssigneeSelect - issue={issue} - partialUpdateIssue={partialUpdateIssue} - isNotAllowed={isNotAllowed} - /> - )} - {type && !isNotAllowed && ( - <CustomMenu width="auto" ellipsis> - <CustomMenu.MenuItem onClick={editIssue}>Edit issue</CustomMenu.MenuItem> - {type !== "issue" && removeIssue && ( - <CustomMenu.MenuItem onClick={removeIssue}> - <>Remove from {type}</> + key={label.id} + className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs" + > + <span + className="h-1.5 w-1.5 flex-shrink-0 rounded-full" + style={{ + backgroundColor: label?.color && label.color !== "" ? label.color : "#000", + }} + /> + {label.name} + </span> + ))} + </div> + )} + {properties.assignee && ( + <ViewAssigneeSelect + issue={issue} + partialUpdateIssue={partialUpdateIssue} + position="right" + isNotAllowed={isNotAllowed} + /> + )} + {type && !isNotAllowed && ( + <CustomMenu width="auto" ellipsis> + <CustomMenu.MenuItem onClick={editIssue}>Edit issue</CustomMenu.MenuItem> + {type !== "issue" && removeIssue && ( + <CustomMenu.MenuItem onClick={removeIssue}> + <>Remove from {type}</> + </CustomMenu.MenuItem> + )} + <CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}> + Delete issue </CustomMenu.MenuItem> - )} - <CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}> - Delete issue - </CustomMenu.MenuItem> - <CustomMenu.MenuItem onClick={handleCopyText}>Copy issue link</CustomMenu.MenuItem> - </CustomMenu> - )} + <CustomMenu.MenuItem onClick={handleCopyText}>Copy issue link</CustomMenu.MenuItem> + </CustomMenu> + )} + </div> </div> - </div> + </> ); }; diff --git a/apps/app/components/core/list-view/single-list.tsx b/apps/app/components/core/list-view/single-list.tsx index 9c3a7ac0f..478b27380 100644 --- a/apps/app/components/core/list-view/single-list.tsx +++ b/apps/app/components/core/list-view/single-list.tsx @@ -23,6 +23,7 @@ type Props = { selectedGroup: NestedKeyOf<IIssue> | null; members: IProjectMember[] | undefined; addIssueToState: () => void; + makeIssueCopy: (issue: IIssue) => void; handleEditIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void; openIssuesListModal?: (() => void) | null; @@ -37,6 +38,7 @@ export const SingleList: React.FC<Props> = ({ selectedGroup, members, addIssueToState, + makeIssueCopy, handleEditIssue, handleDeleteIssue, openIssuesListModal, @@ -113,6 +115,7 @@ export const SingleList: React.FC<Props> = ({ issue={issue} properties={properties} editIssue={() => handleEditIssue(issue)} + makeIssueCopy={() => makeIssueCopy(issue)} handleDeleteIssue={handleDeleteIssue} removeIssue={() => { removeIssue && removeIssue(issue.bridge); diff --git a/apps/app/components/issues/select/assignee.tsx b/apps/app/components/issues/select/assignee.tsx index f932e0e6c..d47d4b178 100644 --- a/apps/app/components/issues/select/assignee.tsx +++ b/apps/app/components/issues/select/assignee.tsx @@ -22,7 +22,7 @@ export const IssueAssigneeSelect: React.FC<Props> = ({ projectId, value = [], on const { workspaceSlug } = router.query; // fetching project members - const { data: people } = useSWR( + const { data: members } = useSWR( workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId ? () => projectServices.projectMembers(workspaceSlug as string, projectId as string) @@ -30,18 +30,20 @@ export const IssueAssigneeSelect: React.FC<Props> = ({ projectId, value = [], on ); const options = - people?.map((person) => ({ - value: person.member.id, + members?.map((member) => ({ + value: member.member.id, query: - person.member.first_name && person.member.first_name !== "" - ? person.member.first_name - : person.member.email, + (member.member.first_name && member.member.first_name !== "" + ? member.member.first_name + : member.member.email) + + " " + + member.member.last_name ?? "", content: ( <div className="flex items-center gap-2"> - <Avatar user={person.member} /> - {person.member.first_name && person.member.first_name !== "" - ? person.member.first_name - : person.member.email} + <Avatar user={member.member} /> + {member.member.first_name && member.member.first_name !== "" + ? member.member.first_name + : member.member.email} </div> ), })) ?? []; @@ -54,19 +56,20 @@ export const IssueAssigneeSelect: React.FC<Props> = ({ projectId, value = [], on label={ <div className="flex items-center gap-2 text-gray-500"> {value && value.length > 0 && Array.isArray(value) ? ( - <span className="flex items-center justify-center gap-2"> + <div className="flex items-center justify-center gap-2"> <AssigneesList userIds={value} length={3} showLength={false} /> - <span className=" text-gray-500">{value.length} Assignees</span> - </span> + <span className="text-gray-500">{value.length} Assignees</span> + </div> ) : ( - <span className="flex items-center justify-center gap-2"> - <UserGroupIcon className="h-4 w-4 text-gray-500 " /> - <span className=" text-gray-500">Assignee</span> - </span> + <div className="flex items-center justify-center gap-2"> + <UserGroupIcon className="h-4 w-4 text-gray-500" /> + <span className="text-gray-500">Assignee</span> + </div> )} </div> } multiple + noChevron /> ); }; diff --git a/apps/app/components/issues/select/priority.tsx b/apps/app/components/issues/select/priority.tsx index 710b35d14..d184ad0e5 100644 --- a/apps/app/components/issues/select/priority.tsx +++ b/apps/app/components/issues/select/priority.tsx @@ -26,6 +26,7 @@ export const IssuePrioritySelect: React.FC<Props> = ({ value, onChange }) => ( </div> } onChange={onChange} + noChevron > {PRIORITIES.map((priority) => ( <CustomSelect.Option key={priority} value={priority}> diff --git a/apps/app/components/issues/select/project.tsx b/apps/app/components/issues/select/project.tsx index 1e09d0a35..be35098e0 100644 --- a/apps/app/components/issues/select/project.tsx +++ b/apps/app/components/issues/select/project.tsx @@ -46,6 +46,7 @@ export const IssueProjectSelect: React.FC<IssueProjectSelectProps> = ({ onChange(val); setActiveProject(val); }} + noChevron > {projects ? ( projects.length > 0 ? ( diff --git a/apps/app/components/issues/select/state.tsx b/apps/app/components/issues/select/state.tsx index f8a0c5e15..eca350fe6 100644 --- a/apps/app/components/issues/select/state.tsx +++ b/apps/app/components/issues/select/state.tsx @@ -72,6 +72,7 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange, Create New State </button> } + noChevron /> ); }; diff --git a/apps/app/components/issues/view-select/assignee.tsx b/apps/app/components/issues/view-select/assignee.tsx index cb482dfa5..2ab96ab08 100644 --- a/apps/app/components/issues/view-select/assignee.tsx +++ b/apps/app/components/issues/view-select/assignee.tsx @@ -9,15 +9,17 @@ import { Listbox, Transition } from "@headlessui/react"; // services import projectService from "services/project.service"; // ui -import { AssigneesList, Avatar, Tooltip } from "components/ui"; +import { AssigneesList, Avatar, CustomSearchSelect, Tooltip } from "components/ui"; // types import { IIssue } from "types"; // fetch-keys import { PROJECT_MEMBERS } from "constants/fetch-keys"; +import { UserGroupIcon } from "@heroicons/react/24/outline"; type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial<IIssue>) => void; + position?: "left" | "right"; selfPositioned?: boolean; tooltipPosition?: "left" | "right"; isNotAllowed: boolean; @@ -26,6 +28,7 @@ type Props = { export const ViewAssigneeSelect: React.FC<Props> = ({ issue, partialUpdateIssue, + position = "left", selfPositioned = false, tooltipPosition = "right", isNotAllowed, @@ -40,9 +43,27 @@ export const ViewAssigneeSelect: React.FC<Props> = ({ : null ); + const options = + members?.map((member) => ({ + value: member.member.id, + query: + (member.member.first_name && member.member.first_name !== "" + ? member.member.first_name + : member.member.email) + + " " + + member.member.last_name ?? "", + content: ( + <div className="flex items-center gap-2"> + <Avatar user={member.member} /> + {member.member.first_name && member.member.first_name !== "" + ? member.member.first_name + : member.member.email} + </div> + ), + })) ?? []; + return ( - <Listbox - as="div" + <CustomSearchSelect value={issue.assignees} onChange={(data: any) => { const newData = issue.assignees ?? []; @@ -50,69 +71,119 @@ export const ViewAssigneeSelect: React.FC<Props> = ({ if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); else newData.push(data); - partialUpdateIssue({ assignees_list: newData }); + partialUpdateIssue({ assignees_list: data }); }} - className={`group ${!selfPositioned ? "relative" : ""} flex-shrink-0`} - disabled={isNotAllowed} - > - {({ open }) => ( - <div> - <Listbox.Button> - <Tooltip - position={`top-${tooltipPosition}`} - tooltipHeading="Assignees" - tooltipContent={ - issue.assignee_details.length > 0 - ? issue.assignee_details - .map((assignee) => - assignee?.first_name !== "" ? assignee?.first_name : assignee?.email - ) - .join(", ") - : "No Assignee" - } - > - <div - className={`flex ${ - isNotAllowed ? "cursor-not-allowed" : "cursor-pointer" - } items-center gap-1 text-xs`} - > - <AssigneesList userIds={issue.assignees ?? []} /> - </div> - </Tooltip> - </Listbox.Button> - - <Transition - show={open} - as={React.Fragment} - leave="transition ease-in duration-100" - leaveFrom="opacity-100" - leaveTo="opacity-0" + options={options} + label={ + <Tooltip + position={`top-${tooltipPosition}`} + tooltipHeading="Assignees" + tooltipContent={ + issue.assignee_details.length > 0 + ? issue.assignee_details + .map((assignee) => + assignee?.first_name !== "" ? assignee?.first_name : assignee?.email + ) + .join(", ") + : "No Assignee" + } + > + <div + className={`flex ${ + isNotAllowed ? "cursor-not-allowed" : "cursor-pointer" + } items-center gap-2 text-gray-500`} > - <Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg min-w-full ring-1 ring-black ring-opacity-5 focus:outline-none"> - {members?.map((member) => ( - <Listbox.Option - key={member.member.id} - className={({ active, selected }) => - `flex items-center gap-x-1 cursor-pointer select-none p-2 whitespace-nowrap ${ - active ? "bg-indigo-50" : "" - } ${ - selected || issue.assignees?.includes(member.member.id) - ? "bg-indigo-50 font-medium" - : "font-normal" - }` - } - value={member.member.id} - > - <Avatar user={member.member} /> - {member.member.first_name && member.member.first_name !== "" - ? member.member.first_name - : member.member.email} - </Listbox.Option> - ))} - </Listbox.Options> - </Transition> - </div> - )} - </Listbox> + {issue.assignees && issue.assignees.length > 0 && Array.isArray(issue.assignees) ? ( + <div className="flex items-center justify-center gap-2"> + <AssigneesList userIds={issue.assignees} length={3} showLength={false} /> + <span className="text-gray-500">{issue.assignees.length} Assignees</span> + </div> + ) : ( + <div className="flex items-center justify-center gap-2"> + <UserGroupIcon className="h-4 w-4 text-gray-500" /> + <span className="text-gray-500">Assignee</span> + </div> + )} + </div> + </Tooltip> + } + multiple + noChevron + position={position} + disabled={isNotAllowed} + /> + // <Listbox + // as="div" + // value={issue.assignees} + // onChange={(data: any) => { + // const newData = issue.assignees ?? []; + + // if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); + // else newData.push(data); + + // partialUpdateIssue({ assignees_list: newData }); + // }} + // className={`group ${!selfPositioned ? "relative" : ""} flex-shrink-0`} + // disabled={isNotAllowed} + // > + // {({ open }) => ( + // <div> + // <Listbox.Button> + // <Tooltip + // position={`top-${tooltipPosition}`} + // tooltipHeading="Assignees" + // tooltipContent={ + // issue.assignee_details.length > 0 + // ? issue.assignee_details + // .map((assignee) => + // assignee?.first_name !== "" ? assignee?.first_name : assignee?.email + // ) + // .join(", ") + // : "No Assignee" + // } + // > + // <div + // className={`flex ${ + // isNotAllowed ? "cursor-not-allowed" : "cursor-pointer" + // } items-center gap-1 text-xs`} + // > + // <AssigneesList userIds={issue.assignees ?? []} /> + // </div> + // </Tooltip> + // </Listbox.Button> + + // <Transition + // show={open} + // as={React.Fragment} + // leave="transition ease-in duration-100" + // leaveFrom="opacity-100" + // leaveTo="opacity-0" + // > + // <Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 min-w-full overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> + // {members?.map((member) => ( + // <Listbox.Option + // key={member.member.id} + // className={({ active, selected }) => + // `flex cursor-pointer select-none items-center gap-x-1 whitespace-nowrap p-2 ${ + // active ? "bg-indigo-50" : "" + // } ${ + // selected || issue.assignees?.includes(member.member.id) + // ? "bg-indigo-50 font-medium" + // : "font-normal" + // }` + // } + // value={member.member.id} + // > + // <Avatar user={member.member} /> + // {member.member.first_name && member.member.first_name !== "" + // ? member.member.first_name + // : member.member.email} + // </Listbox.Option> + // ))} + // </Listbox.Options> + // </Transition> + // </div> + // )} + // </Listbox> ); }; diff --git a/apps/app/components/issues/view-select/priority.tsx b/apps/app/components/issues/view-select/priority.tsx index f47ea934d..6ca81037e 100644 --- a/apps/app/components/issues/view-select/priority.tsx +++ b/apps/app/components/issues/view-select/priority.tsx @@ -12,6 +12,7 @@ import { PRIORITIES } from "constants/project"; type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial<IIssue>) => void; + position?: "left" | "right"; selfPositioned?: boolean; isNotAllowed: boolean; }; @@ -19,19 +20,18 @@ type Props = { export const ViewPrioritySelect: React.FC<Props> = ({ issue, partialUpdateIssue, + position = "left", selfPositioned = false, isNotAllowed, }) => ( <CustomSelect value={issue.state} - onChange={(data: string) => { - partialUpdateIssue({ priority: data }); - }} + onChange={(data: string) => partialUpdateIssue({ priority: data })} maxHeight="md" customButton={ <button type="button" - className={`grid place-items-center rounded w-6 h-6 ${ + className={`grid h-6 w-6 place-items-center rounded ${ isNotAllowed ? "cursor-not-allowed" : "cursor-pointer" } items-center shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${ issue.priority === "urgent" @@ -57,6 +57,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({ } noChevron disabled={isNotAllowed} + position={position} selfPositioned={selfPositioned} > {PRIORITIES?.map((priority) => ( diff --git a/apps/app/components/issues/view-select/state.tsx b/apps/app/components/issues/view-select/state.tsx index 704bef426..b7e10847c 100644 --- a/apps/app/components/issues/view-select/state.tsx +++ b/apps/app/components/issues/view-select/state.tsx @@ -13,10 +13,12 @@ import { getStatesList } from "helpers/state.helper"; import { IIssue } from "types"; // fetch-keys import { STATE_LIST } from "constants/fetch-keys"; +import { getStateGroupIcon } from "components/icons"; type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial<IIssue>) => void; + position?: "left" | "right"; selfPositioned?: boolean; isNotAllowed: boolean; }; @@ -24,6 +26,7 @@ type Props = { export const ViewStateSelect: React.FC<Props> = ({ issue, partialUpdateIssue, + position = "left", selfPositioned = false, isNotAllowed, }) => { @@ -38,46 +41,38 @@ export const ViewStateSelect: React.FC<Props> = ({ ); const states = getStatesList(stateGroups ?? {}); + const currentState = states?.find((s) => s.id === issue.state); + return ( <CustomSelect label={ <> - <span - className="h-1.5 w-1.5 flex-shrink-0 rounded-full" - style={{ - backgroundColor: states?.find((s) => s.id === issue.state)?.color, - }} - /> + {getStateGroupIcon( + currentState?.group ?? "backlog", + "16", + "16", + currentState?.color ?? "" + )} <Tooltip tooltipHeading="State" - tooltipContent={addSpaceIfCamelCase( - states?.find((s) => s.id === issue.state)?.name ?? "" - )} + tooltipContent={addSpaceIfCamelCase(currentState?.name ?? "")} > - <span> - {addSpaceIfCamelCase(states?.find((s) => s.id === issue.state)?.name ?? "")} - </span> + <span>{addSpaceIfCamelCase(currentState?.name ?? "")}</span> </Tooltip> </> } value={issue.state} - onChange={(data: string) => { - partialUpdateIssue({ state: data }); - }} + onChange={(data: string) => partialUpdateIssue({ state: data })} maxHeight="md" noChevron disabled={isNotAllowed} + position={position} selfPositioned={selfPositioned} > {states?.map((state) => ( <CustomSelect.Option key={state.id} value={state.id}> <> - <span - className="h-1.5 w-1.5 flex-shrink-0 rounded-full" - style={{ - backgroundColor: state.color, - }} - /> + {getStateGroupIcon(state.group, "16", "16", state.color)} {addSpaceIfCamelCase(state.name)} </> </CustomSelect.Option> diff --git a/apps/app/components/ui/context-menu.tsx b/apps/app/components/ui/context-menu.tsx index 749a986dd..70d1e1265 100644 --- a/apps/app/components/ui/context-menu.tsx +++ b/apps/app/components/ui/context-menu.tsx @@ -15,14 +15,28 @@ type Props = { const ContextMenu = ({ position, children, title, isOpen, setIsOpen }: Props) => { useEffect(() => { - const hideContextMenu = () => setIsOpen(false); + const hideContextMenu = () => { + if (isOpen) setIsOpen(false); + }; window.addEventListener("click", hideContextMenu); return () => { window.removeEventListener("click", hideContextMenu); }; - }, [setIsOpen]); + }, [isOpen, setIsOpen]); + + useEffect(() => { + const hideContextMenu = (e: KeyboardEvent) => { + if (e.key === "Escape" && isOpen) setIsOpen(false); + }; + + window.addEventListener("keydown", hideContextMenu); + + return () => { + window.removeEventListener("keydown", hideContextMenu); + }; + }, [isOpen, setIsOpen]); return ( <div @@ -61,10 +75,12 @@ const MenuItem: React.FC<MenuItemProps> = ({ 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"> + <a + className={`${className} flex w-full items-center gap-2 rounded px-1 py-1.5 text-left hover:bg-hover-gray`} + > <> {Icon && <Icon />} {children} @@ -72,14 +88,18 @@ const MenuItem: React.FC<MenuItemProps> = ({ </a> </Link> ) : ( - <button type="button" className="flex items-center gap-2" onClick={onClick}> + <button + type="button" + className={`${className} flex w-full items-center gap-2 rounded px-1 py-1.5 text-left hover:bg-hover-gray`} + onClick={onClick} + > <> {Icon && <Icon height={12} width={12} />} {children} </> </button> )} - </div> + </> ); ContextMenu.Item = MenuItem; diff --git a/apps/app/components/ui/custom-search-select.tsx b/apps/app/components/ui/custom-search-select.tsx index b9266a8af..7356aa50d 100644 --- a/apps/app/components/ui/custom-search-select.tsx +++ b/apps/app/components/ui/custom-search-select.tsx @@ -58,6 +58,7 @@ export const CustomSearchSelect = ({ value={value} onChange={onChange} className={`${!selfPositioned ? "relative" : ""} flex-shrink-0 text-left`} + disabled={disabled} multiple > {({ open }: any) => ( @@ -111,7 +112,7 @@ export const CustomSearchSelect = ({ displayValue={(assigned: any) => assigned?.name} /> </div> - <div className="mt-2"> + <div className="mt-2 space-y-1"> {filteredOptions ? ( filteredOptions.length > 0 ? ( filteredOptions.map((option) => ( @@ -124,10 +125,18 @@ export const CustomSearchSelect = ({ } flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 text-gray-500` } > - {({ selected }) => ( + {({ active, selected }) => ( <> {option.content} - {selected && <CheckIcon className="h-4 w-4" />} + <div + className={`flex items-center justify-center rounded border border-gray-500 p-0.5 ${ + active || selected ? "opacity-100" : "opacity-0" + }`} + > + <CheckIcon + className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} + /> + </div> </> )} </Combobox.Option> @@ -151,6 +160,7 @@ export const CustomSearchSelect = ({ value={value} onChange={onChange} className={`${!selfPositioned ? "relative" : ""} flex-shrink-0 text-left`} + disabled={disabled} > {({ open }: any) => ( <> diff --git a/apps/app/components/ui/custom-select.tsx b/apps/app/components/ui/custom-select.tsx index 0a0eda305..6c81efd5c 100644 --- a/apps/app/components/ui/custom-select.tsx +++ b/apps/app/components/ui/custom-select.tsx @@ -94,7 +94,7 @@ const CustomSelect = ({ : "" }`} > - <div className="p-2">{children}</div> + <div className="space-y-1 p-2">{children}</div> </Listbox.Options> </Transition> </Listbox> @@ -112,13 +112,14 @@ const Option: React.FC<OptionProps> = ({ children, value, className }) => ( className={({ active, selected }) => `${className} ${active || selected ? "bg-hover-gray" : ""} ${ selected ? "font-medium" : "" - } flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 text-gray-500` + } cursor-pointer select-none truncate rounded px-1 py-1.5 text-gray-500` } > {({ selected }) => ( - <> - {children} {selected && <CheckIcon className="h-4 w-4" />} - </> + <div className="flex items-center justify-between gap-2"> + <div className="flex items-center gap-2">{children}</div> + {selected && <CheckIcon className="h-4 w-4 flex-shrink-0" />} + </div> )} </Listbox.Option> ); diff --git a/apps/app/components/ui/labels-list.tsx b/apps/app/components/ui/labels-list.tsx index 26257549b..2b296d624 100644 --- a/apps/app/components/ui/labels-list.tsx +++ b/apps/app/components/ui/labels-list.tsx @@ -20,7 +20,7 @@ export const IssueLabelsList: React.FC<IssueLabelsListProps> = ({ className={`h-4 w-4 flex-shrink-0 rounded-full border border-white `} style={{ - backgroundColor: color, + backgroundColor: color && color !== "" ? color : "#000000", }} /> </div>