From 60ae940d40bb3ed3bcb0ca2967c9d9fec8aa8f45 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 20 Sep 2023 17:00:03 +0530 Subject: [PATCH 01/12] chore: sub issues count in individual issue (#2221) --- apiserver/plane/api/views/issue.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 8d2ed9b96..e653f3d44 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -330,7 +330,12 @@ class IssueViewSet(BaseViewSet): def retrieve(self, request, slug, project_id, pk=None): try: - issue = Issue.issue_objects.get( + issue = Issue.issue_objects.annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ).get( workspace__slug=slug, project_id=project_id, pk=pk ) return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) From bd077e6500edefdfc2f0742ff3eeb02504768acf Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 21 Sep 2023 15:39:45 +0530 Subject: [PATCH 02/12] Implemented nested issues in the sub issues section in issue detail page (#2233) * feat: subissues infinte level * feat: updated UI for sub issues * feat: subissues new ui and nested sub issues in issue detail * chore: removed repeated code --- web/components/issues/delete-issue-modal.tsx | 4 +- web/components/issues/index.ts | 1 - web/components/issues/main-content.tsx | 4 +- web/components/issues/sub-issues-list.tsx | 251 ---------------- web/components/issues/sub-issues/index.ts | 1 + web/components/issues/sub-issues/issue.tsx | 171 +++++++++++ .../issues/sub-issues/issues-list.tsx | 84 ++++++ .../issues/sub-issues/progressbar.tsx | 25 ++ .../issues/sub-issues/properties.tsx | 204 +++++++++++++ web/components/issues/sub-issues/root.tsx | 283 ++++++++++++++++++ web/components/project/members-select.tsx | 2 +- 11 files changed, 774 insertions(+), 256 deletions(-) delete mode 100644 web/components/issues/sub-issues-list.tsx create mode 100644 web/components/issues/sub-issues/index.ts create mode 100644 web/components/issues/sub-issues/issue.tsx create mode 100644 web/components/issues/sub-issues/issues-list.tsx create mode 100644 web/components/issues/sub-issues/progressbar.tsx create mode 100644 web/components/issues/sub-issues/properties.tsx create mode 100644 web/components/issues/sub-issues/root.tsx diff --git a/web/components/issues/delete-issue-modal.tsx b/web/components/issues/delete-issue-modal.tsx index d76e5ddd2..62fc04723 100644 --- a/web/components/issues/delete-issue-modal.tsx +++ b/web/components/issues/delete-issue-modal.tsx @@ -35,6 +35,7 @@ type Props = { data: IIssue | null; user: ICurrentUserResponse | undefined; onSubmit?: () => Promise; + redirection?: boolean; }; export const DeleteIssueModal: React.FC = ({ @@ -43,6 +44,7 @@ export const DeleteIssueModal: React.FC = ({ data, user, onSubmit, + redirection = true, }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); @@ -132,7 +134,7 @@ export const DeleteIssueModal: React.FC = ({ message: "Issue deleted successfully", }); - if (issueId) router.back(); + if (issueId && redirection) router.back(); }) .catch((error) => { console.log(error); diff --git a/web/components/issues/index.ts b/web/components/issues/index.ts index 1c51031f3..6b83e7ef4 100644 --- a/web/components/issues/index.ts +++ b/web/components/issues/index.ts @@ -12,7 +12,6 @@ export * from "./main-content"; export * from "./modal"; export * from "./parent-issues-list-modal"; export * from "./sidebar"; -export * from "./sub-issues-list"; export * from "./label"; export * from "./issue-reaction"; export * from "./peek-overview"; diff --git a/web/components/issues/main-content.tsx b/web/components/issues/main-content.tsx index b7b154ce2..5e14ba432 100644 --- a/web/components/issues/main-content.tsx +++ b/web/components/issues/main-content.tsx @@ -18,9 +18,9 @@ import { IssueAttachmentUpload, IssueAttachments, IssueDescriptionForm, - SubIssuesList, IssueReaction, } from "components/issues"; +import { SubIssuesRoot } from "./sub-issues"; // ui import { CustomMenu } from "components/ui"; // icons @@ -206,7 +206,7 @@ export const IssueMainContent: React.FC = ({
- +
diff --git a/web/components/issues/sub-issues-list.tsx b/web/components/issues/sub-issues-list.tsx deleted file mode 100644 index 9ba920ff5..000000000 --- a/web/components/issues/sub-issues-list.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import { FC, useState } from "react"; - -import Link from "next/link"; -import { useRouter } from "next/router"; - -import useSWR, { mutate } from "swr"; - -// headless ui -import { Disclosure, Transition } from "@headlessui/react"; -// services -import issuesService from "services/issues.service"; -// contexts -import { useProjectMyMembership } from "contexts/project-member.context"; -// components -import { ExistingIssuesListModal } from "components/core"; -import { CreateUpdateIssueModal } from "components/issues"; -// ui -import { CustomMenu } from "components/ui"; -// icons -import { ChevronRightIcon, PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; -// types -import { ICurrentUserResponse, IIssue, ISearchIssueResponse, ISubIssueResponse } from "types"; -// fetch-keys -import { SUB_ISSUES } from "constants/fetch-keys"; - -type Props = { - parentIssue: IIssue; - user: ICurrentUserResponse | undefined; - disabled?: boolean; -}; - -export const SubIssuesList: FC = ({ parentIssue, user, disabled = false }) => { - // states - const [createIssueModal, setCreateIssueModal] = useState(false); - const [subIssuesListModal, setSubIssuesListModal] = useState(false); - const [preloadedData, setPreloadedData] = useState | null>(null); - - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { memberRole } = useProjectMyMembership(); - - const { data: subIssuesResponse } = useSWR( - workspaceSlug && parentIssue ? SUB_ISSUES(parentIssue.id) : null, - workspaceSlug && parentIssue - ? () => issuesService.subIssues(workspaceSlug as string, parentIssue.project, parentIssue.id) - : null - ); - - const addAsSubIssue = async (data: ISearchIssueResponse[]) => { - if (!workspaceSlug || !parentIssue) return; - - const payload = { - sub_issue_ids: data.map((i) => i.id), - }; - - await issuesService - .addSubIssues(workspaceSlug as string, parentIssue.project, parentIssue.id, payload) - .finally(() => mutate(SUB_ISSUES(parentIssue.id))); - }; - - const handleSubIssueRemove = (issue: IIssue) => { - if (!workspaceSlug || !parentIssue) return; - - mutate( - SUB_ISSUES(parentIssue.id), - (prevData) => { - if (!prevData) return prevData; - - const stateDistribution = { ...prevData.state_distribution }; - - const issueGroup = issue.state_detail.group; - stateDistribution[issueGroup] = stateDistribution[issueGroup] - 1; - - return { - state_distribution: stateDistribution, - sub_issues: prevData.sub_issues.filter((i) => i.id !== issue.id), - }; - }, - false - ); - - issuesService - .patchIssue(workspaceSlug.toString(), issue.project, issue.id, { parent: null }, user) - .finally(() => mutate(SUB_ISSUES(parentIssue.id))); - }; - - const handleCreateIssueModal = () => { - setCreateIssueModal(true); - - setPreloadedData({ - parent: parentIssue.id, - }); - }; - - const completedSubIssue = subIssuesResponse?.state_distribution.completed ?? 0; - const cancelledSubIssue = subIssuesResponse?.state_distribution.cancelled ?? 0; - - const totalCompletedSubIssues = completedSubIssue + cancelledSubIssue; - - const totalSubIssues = subIssuesResponse ? subIssuesResponse.sub_issues.length : 0; - - const completionPercentage = (totalCompletedSubIssues / totalSubIssues) * 100; - - const isNotAllowed = memberRole.isGuest || memberRole.isViewer || disabled; - - return ( - <> - setCreateIssueModal(false)} - /> - setSubIssuesListModal(false)} - searchParams={{ sub_issue: true, issue_id: parentIssue?.id }} - handleOnSubmit={addAsSubIssue} - workspaceLevelToggle - /> - {subIssuesResponse && subIssuesResponse.sub_issues.length > 0 ? ( - - {({ open }) => ( - <> -
-
- - - Sub-issues{" "} - - {subIssuesResponse.sub_issues.length} - - -
-
-
100 - ? 100 - : completionPercentage.toFixed(0) - }%`, - }} - /> -
- - {isNaN(completionPercentage) - ? 0 - : completionPercentage > 100 - ? 100 - : completionPercentage.toFixed(0)} - % Done - -
-
- - {open && !isNotAllowed ? ( -
- - - - setSubIssuesListModal(true)}> - Add an existing issue - - -
- ) : null} -
- - - {subIssuesResponse.sub_issues.map((issue) => ( - - -
- - - {issue.project_detail.identifier}-{issue.sequence_id} - - {issue.name} -
- - {!isNotAllowed && ( - - )} -
- - ))} -
-
- - )} - - ) : ( - !isNotAllowed && ( - - - Add sub-issue - - } - buttonClassName="whitespace-nowrap" - position="left" - noBorder - noChevron - > - Create new - setSubIssuesListModal(true)}> - Add an existing issue - - - ) - )} - - ); -}; diff --git a/web/components/issues/sub-issues/index.ts b/web/components/issues/sub-issues/index.ts new file mode 100644 index 000000000..1efe34c51 --- /dev/null +++ b/web/components/issues/sub-issues/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/web/components/issues/sub-issues/issue.tsx b/web/components/issues/sub-issues/issue.tsx new file mode 100644 index 000000000..2e3d8acdb --- /dev/null +++ b/web/components/issues/sub-issues/issue.tsx @@ -0,0 +1,171 @@ +import React from "react"; +// next imports +import Link from "next/link"; +// lucide icons +import { + ChevronDown, + ChevronRight, + X, + Pencil, + Trash, + Link as LinkIcon, + Loader, +} from "lucide-react"; +// components +import { SubIssuesRootList } from "./issues-list"; +import { IssueProperty } from "./properties"; +// ui +import { Tooltip, CustomMenu } from "components/ui"; + +// types +import { ICurrentUserResponse, IIssue } from "types"; + +export interface ISubIssues { + workspaceSlug: string; + projectId: string; + parentIssue: IIssue; + issue: any; + spacingLeft?: number; + user: ICurrentUserResponse | undefined; + editable: boolean; + removeIssueFromSubIssues: (parentIssueId: string, issue: IIssue) => void; + issuesVisibility: string[]; + handleIssuesVisibility: (issueId: string) => void; + copyText: (text: string) => void; + handleIssueCrudOperation: ( + key: "create" | "existing" | "edit" | "delete", + issueId: string, + issue?: IIssue | null + ) => void; +} + +export const SubIssues: React.FC = ({ + workspaceSlug, + projectId, + parentIssue, + issue, + spacingLeft = 0, + user, + editable, + removeIssueFromSubIssues, + issuesVisibility, + handleIssuesVisibility, + copyText, + handleIssueCrudOperation, +}) => ( +
+ {issue && ( +
+
+ {issue?.sub_issues_count > 0 && ( + <> + {true ? ( +
handleIssuesVisibility(issue?.id)} + > + {issuesVisibility && issuesVisibility.includes(issue?.id) ? ( + + ) : ( + + )} +
+ ) : ( + + )} + + )} +
+ + + +
+
+ {issue.project_detail.identifier}-{issue?.sequence_id} +
+ +
{issue?.name}
+
+
+ + +
+ +
+ +
+ + {editable && ( + handleIssueCrudOperation("edit", parentIssue?.id, issue)} + > +
+ + Edit issue +
+
+ )} + + {editable && ( + handleIssueCrudOperation("delete", parentIssue?.id, issue)} + > +
+ + Delete issue +
+
+ )} + + +
+ + Copy issue link +
+
+
+
+ + {editable && ( +
removeIssueFromSubIssues(parentIssue?.id, issue)} + > + +
+ )} +
+ )} + + {issuesVisibility.includes(issue?.id) && issue?.sub_issues_count > 0 && ( + + )} +
+); diff --git a/web/components/issues/sub-issues/issues-list.tsx b/web/components/issues/sub-issues/issues-list.tsx new file mode 100644 index 000000000..45c0b1882 --- /dev/null +++ b/web/components/issues/sub-issues/issues-list.tsx @@ -0,0 +1,84 @@ +import React from "react"; +// swr +import useSWR from "swr"; +// components +import { SubIssues } from "./issue"; +// types +import { ICurrentUserResponse, IIssue } from "types"; +// services +import issuesService from "services/issues.service"; +// fetch keys +import { SUB_ISSUES } from "constants/fetch-keys"; + +export interface ISubIssuesRootList { + workspaceSlug: string; + projectId: string; + parentIssue: IIssue; + spacingLeft?: number; + user: ICurrentUserResponse | undefined; + editable: boolean; + removeIssueFromSubIssues: (parentIssueId: string, issue: IIssue) => void; + issuesVisibility: string[]; + handleIssuesVisibility: (issueId: string) => void; + copyText: (text: string) => void; + handleIssueCrudOperation: ( + key: "create" | "existing" | "edit" | "delete", + issueId: string, + issue?: IIssue | null + ) => void; +} + +export const SubIssuesRootList: React.FC = ({ + workspaceSlug, + projectId, + parentIssue, + spacingLeft = 10, + user, + editable, + removeIssueFromSubIssues, + issuesVisibility, + handleIssuesVisibility, + copyText, + handleIssueCrudOperation, +}) => { + const { data: issues, isLoading } = useSWR( + workspaceSlug && projectId && parentIssue && parentIssue?.id + ? SUB_ISSUES(parentIssue?.id) + : null, + workspaceSlug && projectId && parentIssue && parentIssue?.id + ? () => issuesService.subIssues(workspaceSlug, projectId, parentIssue.id) + : null + ); + + return ( +
+ {issues && + issues.sub_issues && + issues.sub_issues.length > 0 && + issues.sub_issues.map((issue: IIssue) => ( + + ))} + +
10 ? `border-l border-custom-border-100` : `` + }`} + style={{ left: `${spacingLeft - 12}px` }} + /> +
+ ); +}; diff --git a/web/components/issues/sub-issues/progressbar.tsx b/web/components/issues/sub-issues/progressbar.tsx new file mode 100644 index 000000000..368078a3d --- /dev/null +++ b/web/components/issues/sub-issues/progressbar.tsx @@ -0,0 +1,25 @@ +export interface IProgressBar { + total: number; + done: number; +} + +export const ProgressBar = ({ total = 0, done = 0 }: IProgressBar) => { + const calPercentage = (doneValue: number, totalValue: number): string => { + if (doneValue === 0 || totalValue === 0) return (0).toFixed(0); + return ((100 * doneValue) / totalValue).toFixed(0); + }; + + return ( +
+
+
+
+
+
+
{calPercentage(done, total)}% Done
+
+ ); +}; diff --git a/web/components/issues/sub-issues/properties.tsx b/web/components/issues/sub-issues/properties.tsx new file mode 100644 index 000000000..a899efdcf --- /dev/null +++ b/web/components/issues/sub-issues/properties.tsx @@ -0,0 +1,204 @@ +import React from "react"; +// swr +import { mutate } from "swr"; +// components +import { ViewDueDateSelect, ViewStartDateSelect } from "components/issues"; +import { MembersSelect, PrioritySelect } from "components/project"; +import { StateSelect } from "components/states"; +// hooks +import useIssuesProperties from "hooks/use-issue-properties"; +// types +import { ICurrentUserResponse, IIssue, IState } from "types"; +// fetch-keys +import { SUB_ISSUES } from "constants/fetch-keys"; +// services +import issuesService from "services/issues.service"; +import trackEventServices from "services/track-event.service"; + +export interface IIssueProperty { + workspaceSlug: string; + projectId: string; + parentIssue: IIssue; + issue: IIssue; + user: ICurrentUserResponse | undefined; + editable: boolean; +} + +export const IssueProperty: React.FC = ({ + workspaceSlug, + projectId, + parentIssue, + issue, + user, + editable, +}) => { + const [properties] = useIssuesProperties(workspaceSlug, projectId); + + const handlePriorityChange = (data: any) => { + partialUpdateIssue({ priority: data }); + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_PRIORITY", + user + ); + }; + + const handleStateChange = (data: string, states: IState[] | undefined) => { + const oldState = states?.find((s) => s.id === issue.state); + const newState = states?.find((s) => s.id === data); + + partialUpdateIssue({ + state: data, + state_detail: newState, + }); + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_STATE", + user + ); + if (oldState?.group !== "completed" && newState?.group !== "completed") { + trackEventServices.trackIssueMarkedAsDoneEvent( + { + workspaceSlug: issue.workspace_detail.slug, + workspaceId: issue.workspace_detail.id, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + user + ); + } + }; + + const handleAssigneeChange = (data: any) => { + const newData = issue.assignees ?? []; + + if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); + else newData.push(data); + + partialUpdateIssue({ assignees_list: data }); + + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_ASSIGNEE", + user + ); + }; + + const partialUpdateIssue = async (data: Partial) => { + mutate( + workspaceSlug && parentIssue ? SUB_ISSUES(parentIssue.id) : null, + (elements: any) => { + const _elements = { ...elements }; + const _issues = _elements.sub_issues.map((element: IIssue) => + element.id === issue.id ? { ...element, ...data } : element + ); + _elements["sub_issues"] = [..._issues]; + return _elements; + }, + false + ); + + const issueResponse = await issuesService.patchIssue( + workspaceSlug as string, + issue.project, + issue.id, + data, + user + ); + + mutate( + SUB_ISSUES(parentIssue.id), + (elements: any) => { + const _elements = elements.sub_issues.map((element: IIssue) => + element.id === issue.id ? issueResponse : element + ); + elements["sub_issues"] = _elements; + return elements; + }, + true + ); + }; + + return ( +
+ {properties.priority && ( +
+ +
+ )} + + {properties.state && ( +
+ +
+ )} + + {properties.start_date && issue.start_date && ( +
+ +
+ )} + + {properties.due_date && issue.target_date && ( +
+ +
+ )} + + {properties.assignee && ( +
+ +
+ )} +
+ ); +}; diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx new file mode 100644 index 000000000..75161e639 --- /dev/null +++ b/web/components/issues/sub-issues/root.tsx @@ -0,0 +1,283 @@ +import React from "react"; +// next imports +import { useRouter } from "next/router"; +// swr +import useSWR, { mutate } from "swr"; +// lucide icons +import { Plus, ChevronRight, ChevronDown } from "lucide-react"; +// components +import { ExistingIssuesListModal } from "components/core"; +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { SubIssuesRootList } from "./issues-list"; +import { ProgressBar } from "./progressbar"; +// ui +import { CustomMenu } from "components/ui"; +// hooks +import { useProjectMyMembership } from "contexts/project-member.context"; +// helpers +import { copyTextToClipboard } from "helpers/string.helper"; +// types +import { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types"; +// services +import issuesService from "services/issues.service"; +// fetch keys +import { SUB_ISSUES } from "constants/fetch-keys"; + +export interface ISubIssuesRoot { + parentIssue: IIssue; + + user: ICurrentUserResponse | undefined; + editable: boolean; +} + +export const SubIssuesRoot: React.FC = ({ parentIssue, user, editable }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + + const { memberRole } = useProjectMyMembership(); + + const { data: issues } = useSWR( + workspaceSlug && projectId && parentIssue && parentIssue?.id + ? SUB_ISSUES(parentIssue?.id) + : null, + workspaceSlug && projectId && parentIssue && parentIssue?.id + ? () => issuesService.subIssues(workspaceSlug, projectId, parentIssue.id) + : null + ); + + const [issuesVisibility, setIssuesVisibility] = React.useState([parentIssue?.id]); + const handleIssuesVisibility = (issueId: string) => { + if (issuesVisibility.includes(issueId)) { + setIssuesVisibility(issuesVisibility.filter((i: string) => i !== issueId)); + } else { + setIssuesVisibility([...issuesVisibility, issueId]); + } + }; + + const [issueCrudOperation, setIssueCrudOperation] = React.useState<{ + create: { toggle: boolean; issueId: string | null }; + existing: { toggle: boolean; issueId: string | null }; + edit: { toggle: boolean; issueId: string | null; issue: IIssue | null }; + delete: { toggle: boolean; issueId: string | null; issue: IIssue | null }; + }>({ + create: { + toggle: false, + issueId: null, + }, + existing: { + toggle: false, + issueId: null, + }, + edit: { + toggle: false, + issueId: null, // parent issue id for mutation + issue: null, + }, + delete: { + toggle: false, + issueId: null, // parent issue id for mutation + issue: null, + }, + }); + const handleIssueCrudOperation = ( + key: "create" | "existing" | "edit" | "delete", + issueId: string | null, + issue: IIssue | null = null + ) => { + setIssueCrudOperation({ + ...issueCrudOperation, + [key]: { + toggle: !issueCrudOperation[key].toggle, + issueId: issueId, + issue: issue, + }, + }); + }; + + const addAsSubIssueFromExistingIssues = async (data: ISearchIssueResponse[]) => { + if (!workspaceSlug || !parentIssue || issueCrudOperation?.existing?.issueId === null) return; + const issueId = issueCrudOperation?.existing?.issueId; + const payload = { + sub_issue_ids: data.map((i) => i.id), + }; + + await issuesService.addSubIssues(workspaceSlug, projectId, issueId, payload).finally(() => { + if (issueId) mutate(SUB_ISSUES(issueId)); + }); + }; + + const removeIssueFromSubIssues = async (parentIssueId: string, issue: IIssue) => { + if (!workspaceSlug || !parentIssue || !issue?.id) return; + issuesService + .patchIssue(workspaceSlug, projectId, issue.id, { parent: null }, user) + .finally(() => { + if (parentIssueId) mutate(SUB_ISSUES(parentIssueId)); + }); + }; + + const copyText = (text: string) => { + const originURL = + typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; + copyTextToClipboard(`${originURL}/${text}`).then(() => { + // setToastAlert({ + // type: "success", + // title: "Link Copied!", + // message: "Issue link copied to clipboard.", + // }); + }); + }; + + const isEditable = memberRole?.isGuest || memberRole?.isViewer ? false : true; + + const mutateSubIssues = (parentIssueId: string | null) => { + if (parentIssueId) mutate(SUB_ISSUES(parentIssueId)); + }; + + return ( +
+ {parentIssue && parentIssue?.sub_issues_count > 0 ? ( + <> + {/* header */} +
+
handleIssuesVisibility(parentIssue?.id)} + > +
+ {issuesVisibility.includes(parentIssue?.id) ? ( + + ) : ( + + )} +
+
Sub-issues
+
({parentIssue?.sub_issues_count})
+
+ +
+ +
+ + {isEditable && issuesVisibility.includes(parentIssue?.id) && ( +
+
handleIssueCrudOperation("create", parentIssue?.id)} + > + Add sub-issue +
+
handleIssueCrudOperation("existing", parentIssue?.id)} + > + Add an existing issue +
+
+ )} +
+ + {/* issues */} + {issuesVisibility.includes(parentIssue?.id) && ( +
+ +
+ )} + + ) : ( + isEditable && ( +
+
No sub issues are available
+ <> + + + Add sub-issue + + } + buttonClassName="whitespace-nowrap" + position="left" + noBorder + noChevron + > + handleIssueCrudOperation("create", parentIssue?.id)} + > + Create new + + handleIssueCrudOperation("existing", parentIssue?.id)} + > + Add an existing issue + + + +
+ ) + )} + + {isEditable && issueCrudOperation?.create?.toggle && ( + handleIssueCrudOperation("create", null)} + /> + )} + + {isEditable && + issueCrudOperation?.existing?.toggle && + issueCrudOperation?.existing?.issueId && ( + handleIssueCrudOperation("existing", null)} + searchParams={{ sub_issue: true, issue_id: issueCrudOperation?.existing?.issueId }} + handleOnSubmit={addAsSubIssueFromExistingIssues} + workspaceLevelToggle + /> + )} + + {isEditable && issueCrudOperation?.edit?.toggle && issueCrudOperation?.edit?.issueId && ( + { + mutateSubIssues(issueCrudOperation?.edit?.issueId); + handleIssueCrudOperation("edit", null, null); + }} + data={issueCrudOperation?.edit?.issue} + /> + )} + + {isEditable && issueCrudOperation?.delete?.toggle && issueCrudOperation?.delete?.issueId && ( + { + mutateSubIssues(issueCrudOperation?.delete?.issueId); + handleIssueCrudOperation("delete", null, null); + }} + data={issueCrudOperation?.delete?.issue} + user={user} + redirection={false} + /> + )} +
+ ); +}; diff --git a/web/components/project/members-select.tsx b/web/components/project/members-select.tsx index f99d85174..4ffad72b9 100644 --- a/web/components/project/members-select.tsx +++ b/web/components/project/members-select.tsx @@ -82,7 +82,7 @@ export const MembersSelect: React.FC = ({ 0 + membersDetails && membersDetails.length > 0 ? membersDetails.map((assignee) => assignee?.display_name).join(", ") : "No Assignee" } From 1621125f6d83058fc7276eb7d3f692db768b4f50 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:03:06 +0530 Subject: [PATCH 03/12] refactor: product updates modal layout (#2225) --- web/components/ui/product-updates-modal.tsx | 110 +++++++++++--------- 1 file changed, 62 insertions(+), 48 deletions(-) diff --git a/web/components/ui/product-updates-modal.tsx b/web/components/ui/product-updates-modal.tsx index b142f8325..4f5bad7b3 100644 --- a/web/components/ui/product-updates-modal.tsx +++ b/web/components/ui/product-updates-modal.tsx @@ -1,15 +1,16 @@ import React from "react"; + import useSWR from "swr"; // headless ui import { Dialog, Transition } from "@headlessui/react"; -// component -import { MarkdownRenderer, Spinner } from "components/ui"; -// icons -import { XMarkIcon } from "@heroicons/react/20/solid"; // services import workspaceService from "services/workspace.service"; -// helper +// components +import { Loader, MarkdownRenderer } from "components/ui"; +// icons +import { XMarkIcon } from "@heroicons/react/20/solid"; +// helpers import { renderLongDateFormat } from "helpers/date-time.helper"; type Props = { @@ -34,8 +35,8 @@ export const ProductUpdatesModal: React.FC = ({ isOpen, setIsOpen }) => {
-
-
+
+
= ({ isOpen, setIsOpen }) => { leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - -
-
-
- - Product Updates - - - - - {updates && updates.length > 0 ? ( - updates.map((item, index) => ( - -
- - {item.tag_name} + +
+ + Product Updates + + + + + {updates && updates.length > 0 ? ( +
+ {updates.map((item, index) => ( + +
+ + {item.tag_name} + + {renderLongDateFormat(item.published_at)} + {index === 0 && ( + + New - {renderLongDateFormat(item.published_at)} - {index === 0 && ( - - New - - )} -
- -
- )) - ) : ( -
- - Loading... -
- )} + )} +
+ + + ))}
-
+ ) : ( +
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+
+ )}
From e3793f446432189118a7b94ae4aab0c986b7a17b Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:03:33 +0530 Subject: [PATCH 04/12] fix: handle no issues in custom analytics (#2226) --- web/components/analytics/custom-analytics/graph/index.tsx | 1 - web/helpers/array.helper.ts | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/web/components/analytics/custom-analytics/graph/index.tsx b/web/components/analytics/custom-analytics/graph/index.tsx index 349f9884d..733d17437 100644 --- a/web/components/analytics/custom-analytics/graph/index.tsx +++ b/web/components/analytics/custom-analytics/graph/index.tsx @@ -9,7 +9,6 @@ import { findStringWithMostCharacters } from "helpers/array.helper"; import { generateBarColor } from "helpers/analytics.helper"; // types import { IAnalyticsParams, IAnalyticsResponse } from "types"; -// constants type Props = { analytics: IAnalyticsResponse; diff --git a/web/helpers/array.helper.ts b/web/helpers/array.helper.ts index bcadbe3c8..a682b0a1c 100644 --- a/web/helpers/array.helper.ts +++ b/web/helpers/array.helper.ts @@ -38,10 +38,13 @@ export const orderArrayBy = ( export const checkDuplicates = (array: any[]) => new Set(array).size !== array.length; -export const findStringWithMostCharacters = (strings: string[]) => - strings.reduce((longestString, currentString) => +export const findStringWithMostCharacters = (strings: string[]): string => { + if (!strings || strings.length === 0) return ""; + + return strings.reduce((longestString, currentString) => currentString.length > longestString.length ? currentString : longestString ); +}; export const checkIfArraysHaveSameElements = (arr1: any[] | null, arr2: any[] | null): boolean => { if (!arr1 || !arr2) return false; From de9f34cac3cfd2cd588bc4eb8efdaa34f06d52cd Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:04:05 +0530 Subject: [PATCH 05/12] fix: activity label color (#2227) --- web/components/core/activity.tsx | 44 +++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 7c2798e7a..c76f1aece 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -1,5 +1,9 @@ import { useRouter } from "next/router"; +import useSWR from "swr"; + +// services +import issuesService from "services/issues.service"; // icons import { Icon, Tooltip } from "components/ui"; import { CopyPlus } from "lucide-react"; @@ -10,6 +14,8 @@ import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { capitalizeFirstLetter } from "helpers/string.helper"; // types import { IIssueActivity } from "types"; +// fetch-keys +import { WORKSPACE_LABELS } from "constants/fetch-keys"; const IssueLink = ({ activity }: { activity: IIssueActivity }) => { const router = useRouter(); @@ -52,6 +58,26 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => { ); }; +const LabelPill = ({ labelId }: { labelId: string }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { data: labels } = useSWR( + workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, + workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null + ); + + return ( + l.id === labelId)?.color ?? "#000000", + }} + aria-hidden="true" + /> + ); +}; + const activityDetails: { [key: string]: { message: ( @@ -325,14 +351,8 @@ const activityDetails: { return ( <> added a new label{" "} - -