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 = ({ issue_detail: { ...p.issue_detail, ...formData, + assignees: formData.assignees_list ?? p.issue_detail.assignees_list, }, }; } @@ -122,6 +123,7 @@ export const SingleBoardIssue: React.FC = ({ issue_detail: { ...p.issue_detail, ...formData, + assignees: formData.assignees_list ?? p.issue_detail.assignees_list, }, }; } @@ -136,7 +138,8 @@ export const SingleBoardIssue: React.FC = ({ 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 = ({ [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 = ({ ...style, transitionDuration: `0.001s`, }; - } + }; const handleCopyText = () => { const originURL = @@ -295,7 +298,7 @@ export const SingleBoardIssue: React.FC = ({ {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} )} - {properties.labels && ( + {properties.labels && issue.label_details.length > 0 && (
{issue.label_details.map((label) => ( = ({ 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 = ({ states, members, addIssueToState, + makeIssueCopy, openIssuesListModal, handleEditIssue, handleDeleteIssue, @@ -50,6 +52,7 @@ export const AllLists: React.FC = ({ 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 = ({ 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) => { if (!workspaceSlug || !projectId) return; @@ -63,6 +78,7 @@ export const SingleListIssue: React.FC = ({ issue_detail: { ...p.issue_detail, ...formData, + assignees: formData.assignees_list ?? p.issue_detail.assignees_list, }, }; } @@ -84,6 +100,7 @@ export const SingleListIssue: React.FC = ({ issue_detail: { ...p.issue_detail, ...formData, + assignees: formData.assignees_list ?? p.issue_detail.assignees_list, }, }; } @@ -98,7 +115,8 @@ export const SingleListIssue: React.FC = ({ 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 = ({ }); }); }; + const isNotAllowed = userAuth.isGuest || userAuth.isViewer; return ( -
-
- - - - {properties.key && ( - - - {issue.project_detail?.identifier}-{issue.sequence_id} + <> + + + Edit issue + + + Make a copy... + + handleDeleteIssue(issue)}> + Delete issue + + + Copy issue link + + +
{ + e.preventDefault(); + setContextMenu(true); + setContextMenuPosition({ x: e.pageX, y: e.pageY }); + }} + > + -
- {properties.priority && ( - - )} - {properties.state && ( - - )} - {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.priority && ( + + )} + {properties.state && ( + + )} + {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) => ( - {label.name} - - ))} -
- )} - {properties.assignee && ( - - )} - {type && !isNotAllowed && ( - - Edit issue - {type !== "issue" && removeIssue && ( - - <>Remove from {type} + key={label.id} + className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs" + > + + {label.name} + + ))} +
+ )} + {properties.assignee && ( + + )} + {type && !isNotAllowed && ( + + Edit issue + {type !== "issue" && removeIssue && ( + + <>Remove from {type} + + )} + handleDeleteIssue(issue)}> + Delete issue - )} - handleDeleteIssue(issue)}> - Delete issue - - Copy issue link - - )} + Copy issue link + + )} +
-
+ ); }; 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 | 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 = ({ selectedGroup, members, addIssueToState, + makeIssueCopy, handleEditIssue, handleDeleteIssue, openIssuesListModal, @@ -113,6 +115,7 @@ export const SingleList: React.FC = ({ 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 = ({ 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 = ({ 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: (
- - {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}
), })) ?? []; @@ -54,19 +56,20 @@ export const IssueAssigneeSelect: React.FC = ({ projectId, value = [], on label={
{value && value.length > 0 && Array.isArray(value) ? ( - +
- {value.length} Assignees - + {value.length} Assignees +
) : ( - - - Assignee - +
+ + Assignee +
)}
} 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 = ({ value, onChange }) => (
} onChange={onChange} + noChevron > {PRIORITIES.map((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 = ({ 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 = ({ setIsOpen, value, onChange, Create New State } + 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) => void; + position?: "left" | "right"; selfPositioned?: boolean; tooltipPosition?: "left" | "right"; isNotAllowed: boolean; @@ -26,6 +28,7 @@ type Props = { export const ViewAssigneeSelect: React.FC = ({ issue, partialUpdateIssue, + position = "left", selfPositioned = false, tooltipPosition = "right", isNotAllowed, @@ -40,9 +43,27 @@ export const ViewAssigneeSelect: React.FC = ({ : 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: ( +
+ + {member.member.first_name && member.member.first_name !== "" + ? member.member.first_name + : member.member.email} +
+ ), + })) ?? []; + return ( - { const newData = issue.assignees ?? []; @@ -50,69 +71,119 @@ export const ViewAssigneeSelect: React.FC = ({ 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 }) => ( -
- - 0 - ? issue.assignee_details - .map((assignee) => - assignee?.first_name !== "" ? assignee?.first_name : assignee?.email - ) - .join(", ") - : "No Assignee" - } - > -
- -
-
-
- - 0 + ? issue.assignee_details + .map((assignee) => + assignee?.first_name !== "" ? assignee?.first_name : assignee?.email + ) + .join(", ") + : "No Assignee" + } + > +
- - {members?.map((member) => ( - - `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} - > - - {member.member.first_name && member.member.first_name !== "" - ? member.member.first_name - : member.member.email} - - ))} - - -
- )} - + {issue.assignees && issue.assignees.length > 0 && Array.isArray(issue.assignees) ? ( +
+ + {issue.assignees.length} Assignees +
+ ) : ( +
+ + Assignee +
+ )} +
+ + } + multiple + noChevron + position={position} + disabled={isNotAllowed} + /> + // { + // 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 }) => ( + //
+ // + // 0 + // ? issue.assignee_details + // .map((assignee) => + // assignee?.first_name !== "" ? assignee?.first_name : assignee?.email + // ) + // .join(", ") + // : "No Assignee" + // } + // > + //
+ // + //
+ //
+ //
+ + // + // + // {members?.map((member) => ( + // + // `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} + // > + // + // {member.member.first_name && member.member.first_name !== "" + // ? member.member.first_name + // : member.member.email} + // + // ))} + // + // + //
+ // )} + //
); }; 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) => void; + position?: "left" | "right"; selfPositioned?: boolean; isNotAllowed: boolean; }; @@ -19,19 +20,18 @@ type Props = { export const ViewPrioritySelect: React.FC = ({ issue, partialUpdateIssue, + position = "left", selfPositioned = false, isNotAllowed, }) => ( { - partialUpdateIssue({ priority: data }); - }} + onChange={(data: string) => partialUpdateIssue({ priority: data })} maxHeight="md" customButton={ )} -
+ ); 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} /> -
+
{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 && } +
+ +
)} @@ -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 = ({ : "" }`} > -
{children}
+
{children}
@@ -112,13 +112,14 @@ const Option: React.FC = ({ 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 && } - +
+
{children}
+ {selected && } +
)} ); 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 = ({ className={`h-4 w-4 flex-shrink-0 rounded-full border border-white `} style={{ - backgroundColor: color, + backgroundColor: color && color !== "" ? color : "#000000", }} />