diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index dba7a7a2f..36b3411fc 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -15,6 +15,7 @@ from django.db.models import ( Value, CharField, When, + Max, ) from django.core.serializers.json import DjangoJSONEncoder from django.utils.decorators import method_decorator @@ -195,7 +196,7 @@ class IssueViewSet(BaseViewSet): output_field=CharField(), ) ).order_by("priority_order") - + # State Ordering elif order_by_param in [ "state__name", @@ -218,6 +219,22 @@ class IssueViewSet(BaseViewSet): output_field=CharField(), ) ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" + ) else: issue_queryset = issue_queryset.order_by(order_by_param) @@ -239,7 +256,7 @@ class IssueViewSet(BaseViewSet): return Response(issues, status=status.HTTP_200_OK) except Exception as e: - print(e) + capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index f348f642a..74acb2044 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -1,7 +1,6 @@ from django.utils.timezone import make_aware from django.utils.dateparse import parse_datetime - def filter_state(params, filter, method): if method == "GET": states = params.get("state").split(",") @@ -26,12 +25,27 @@ def filter_estimate_point(params, filter, method): def filter_priority(params, filter, method): if method == "GET": - priorties = params.get("priority").split(",") - if len(priorties) and "" not in priorties: - filter["priority__in"] = priorties + priorities = params.get("priority").split(",") + if len(priorities) and "" not in priorities: + if len(priorities) == 1 and "null" in priorities: + filter["priority__isnull"] = True + elif len(priorities) > 1 and "null" in priorities: + filter["priority__isnull"] = True + filter["priority__in"] = [p for p in priorities if p != "null"] + else: + filter["priority__in"] = [p for p in priorities if p != "null"] + else: if params.get("priority", None) and len(params.get("priority")): - filter["priority__in"] = params.get("priority") + priorities = params.get("priority") + if len(priorities) == 1 and "null" in priorities: + filter["priority__isnull"] = True + elif len(priorities) > 1 and "null" in priorities: + filter["priority__isnull"] = True + filter["priority__in"] = [p for p in priorities if p != "null"] + else: + filter["priority__in"] = [p for p in priorities if p != "null"] + return filter diff --git a/apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx b/apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx index 863eb664b..a37518cba 100644 --- a/apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx +++ b/apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx @@ -41,6 +41,14 @@ export const AnalyticsYearWiseIssues: React.FC = ({ defaultAnalytics }) = colors={(datum) => datum.color} curve="monotoneX" margin={{ top: 20 }} + enableSlices="x" + sliceTooltip={(datum) => ( +
+ {datum.slice.points[0].data.yFormatted} + issues closed in + {datum.slice.points[0].data.xFormatted} +
+ )} theme={{ background: "rgb(var(--color-bg-base))", }} diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx index 5c0cc5102..cde1b2e38 100644 --- a/apps/app/components/core/board-view/single-issue.tsx +++ b/apps/app/components/core/board-view/single-issue.tsx @@ -23,6 +23,7 @@ import { ViewAssigneeSelect, ViewDueDateSelect, ViewEstimateSelect, + ViewLabelSelect, ViewPrioritySelect, ViewStateSelect, } from "components/issues"; @@ -44,7 +45,14 @@ import { LayerDiagonalIcon } from "components/icons"; import { handleIssuesMutation } from "constants/issue"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // types -import { ICurrentUserResponse, IIssue, Properties, TIssueGroupByOptions, UserAuth } from "types"; +import { + ICurrentUserResponse, + IIssue, + ISubIssueResponse, + Properties, + TIssueGroupByOptions, + UserAuth, +} from "types"; // fetch-keys import { CYCLE_DETAILS, @@ -52,6 +60,8 @@ import { MODULE_DETAILS, MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, + SUB_ISSUES, + VIEW_ISSUES, } from "constants/fetch-keys"; type Props = { @@ -101,86 +111,68 @@ export const SingleBoardIssue: React.FC = ({ const { orderBy, params } = useIssuesView(); const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { setToastAlert } = useToast(); const partialUpdateIssue = useCallback( - (formData: Partial, issueId: string) => { + (formData: Partial, issue: IIssue) => { if (!workspaceSlug || !projectId) return; - if (cycleId) - mutate< - | { - [key: string]: IIssue[]; - } - | IIssue[] - >( - CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params), - (prevData) => - handleIssuesMutation( - formData, - groupTitle ?? "", - selectedGroup, - index, - orderBy, - prevData - ), - false - ); - else if (moduleId) - mutate< - | { - [key: string]: IIssue[]; - } - | IIssue[] - >( - MODULE_ISSUES_WITH_PARAMS(moduleId as string), - (prevData) => - handleIssuesMutation( - formData, - groupTitle ?? "", - selectedGroup, - index, - orderBy, - prevData - ), - false - ); - else { - mutate< - | { - [key: string]: IIssue[]; - } - | IIssue[] - >( - PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params), + const fetchKey = cycleId + ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) + : moduleId + ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) + : viewId + ? VIEW_ISSUES(viewId.toString(), params) + : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); + + if (issue.parent) { + mutate( + SUB_ISSUES(issue.parent.toString()), (prevData) => { if (!prevData) return prevData; - return handleIssuesMutation( + return { + ...prevData, + sub_issues: (prevData.sub_issues ?? []).map((i) => { + if (i.id === issue.id) { + return { + ...i, + ...formData, + }; + } + return i; + }), + }; + }, + false + ); + } else { + mutate< + | { + [key: string]: IIssue[]; + } + | IIssue[] + >( + fetchKey, + (prevData) => + handleIssuesMutation( formData, groupTitle ?? "", selectedGroup, index, orderBy, prevData - ); - }, + ), false ); } issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) + .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user) .then(() => { - if (cycleId) { - mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); - mutate(CYCLE_DETAILS(cycleId as string)); - } else if (moduleId) { - mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); - mutate(MODULE_DETAILS(moduleId as string)); - } else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params)); + mutate(fetchKey); }); }, [ @@ -188,6 +180,7 @@ export const SingleBoardIssue: React.FC = ({ projectId, cycleId, moduleId, + viewId, groupTitle, index, selectedGroup, @@ -370,23 +363,15 @@ export const SingleBoardIssue: React.FC = ({ isNotAllowed={isNotAllowed} /> )} - {properties.labels && issue.label_details.length > 0 && ( -
- {issue.label_details.map((label) => ( -
- - {label.name} -
- ))} -
+ {properties.labels && ( + )} {properties.assignee && ( = ({ const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); const partialUpdateIssue = useCallback( - (formData: Partial, issueId: string) => { + (formData: Partial, issue: IIssue) => { if (!workspaceSlug || !projectId) return; const fetchKey = cycleId @@ -79,25 +81,54 @@ export const SingleCalendarIssue: React.FC = ({ ? VIEW_ISSUES(viewId.toString(), params) : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); - mutate( - fetchKey, - (prevData) => - (prevData ?? []).map((p) => { - if (p.id === issueId) { - return { - ...p, - ...formData, - assignees: formData?.assignees_list ?? p.assignees, - }; - } + if (issue.parent) { + mutate( + SUB_ISSUES(issue.parent.toString()), + (prevData) => { + if (!prevData) return prevData; - return p; - }), - false - ); + return { + ...prevData, + sub_issues: (prevData.sub_issues ?? []).map((i) => { + if (i.id === issue.id) { + return { + ...i, + ...formData, + }; + } + return i; + }), + }; + }, + false + ); + } else { + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((p) => { + if (p.id === issue.id) { + return { + ...p, + ...formData, + assignees: formData?.assignees_list ?? p.assignees, + }; + } + + return p; + }), + false + ); + } issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, formData, user) + .patchIssue( + workspaceSlug as string, + projectId as string, + issue.id as string, + formData, + user + ) .then(() => { mutate(fetchKey); }) @@ -207,25 +238,14 @@ export const SingleCalendarIssue: React.FC = ({ isNotAllowed={isNotAllowed} /> )} - {properties.labels && issue.label_details.length > 0 ? ( -
- {issue.label_details.map((label) => ( - - - {label.name} - - ))} -
- ) : ( - "" + {properties.labels && ( + )} {properties.assignee && ( = ({ /> ) : issueView === "spreadsheet" ? ( diff --git a/apps/app/components/core/list-view/single-issue.tsx b/apps/app/components/core/list-view/single-issue.tsx index f2a587899..f4d749452 100644 --- a/apps/app/components/core/list-view/single-issue.tsx +++ b/apps/app/components/core/list-view/single-issue.tsx @@ -14,6 +14,7 @@ import { ViewAssigneeSelect, ViewDueDateSelect, ViewEstimateSelect, + ViewLabelSelect, ViewPrioritySelect, ViewStateSelect, } from "components/issues/view-select"; @@ -36,7 +37,7 @@ import { LayerDiagonalIcon } from "components/icons"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { handleIssuesMutation } from "constants/issue"; // types -import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types"; // fetch-keys import { CYCLE_DETAILS, @@ -44,6 +45,8 @@ import { MODULE_DETAILS, MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, + SUB_ISSUES, + VIEW_ISSUES, } from "constants/fetch-keys"; type Props = { @@ -80,24 +83,53 @@ export const SingleListIssue: React.FC = ({ const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { setToastAlert } = useToast(); const { groupByProperty: selectedGroup, orderBy, params } = useIssueView(); const partialUpdateIssue = useCallback( - (formData: Partial, issueId: string) => { + (formData: Partial, issue: IIssue) => { if (!workspaceSlug || !projectId) return; - if (cycleId) + const fetchKey = cycleId + ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) + : moduleId + ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) + : viewId + ? VIEW_ISSUES(viewId.toString(), params) + : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); + + if (issue.parent) { + mutate( + SUB_ISSUES(issue.parent.toString()), + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + sub_issues: (prevData.sub_issues ?? []).map((i) => { + if (i.id === issue.id) { + return { + ...i, + ...formData, + }; + } + return i; + }), + }; + }, + false + ); + } else { mutate< | { [key: string]: IIssue[]; } | IIssue[] >( - CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params), + fetchKey, (prevData) => handleIssuesMutation( formData, @@ -109,49 +141,12 @@ export const SingleListIssue: React.FC = ({ ), false ); - - if (moduleId) - mutate< - | { - [key: string]: IIssue[]; - } - | IIssue[] - >( - MODULE_ISSUES_WITH_PARAMS(moduleId as string, params), - (prevData) => - handleIssuesMutation( - formData, - groupTitle ?? "", - selectedGroup, - index, - orderBy, - prevData - ), - false - ); - - mutate< - | { - [key: string]: IIssue[]; - } - | IIssue[] - >( - PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params), - (prevData) => - handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, orderBy, prevData), - false - ); + } issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) + .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user) .then(() => { - if (cycleId) { - mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); - mutate(CYCLE_DETAILS(cycleId as string)); - } else if (moduleId) { - mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); - mutate(MODULE_DETAILS(moduleId as string)); - } else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params)); + mutate(fetchKey); }); }, [ @@ -159,6 +154,7 @@ export const SingleListIssue: React.FC = ({ projectId, cycleId, moduleId, + viewId, groupTitle, index, selectedGroup, @@ -275,25 +271,14 @@ export const SingleListIssue: React.FC = ({ isNotAllowed={isNotAllowed} /> )} - {properties.labels && issue.label_details.length > 0 ? ( -
- {issue.label_details.map((label) => ( - - - {label.name} - - ))} -
- ) : ( - "" + {properties.labels && ( + )} {properties.assignee && ( = ({ distribution, startDate, endDate, tota colors={(datum) => datum.color ?? "#3F76FF"} customYAxisTickValues={[0, totalIssues]} gridXValues={chartData.map((item, index) => (index % 2 === 0 ? item.currentDate : ""))} + enableSlices="x" + sliceTooltip={(datum) => ( +
+ {datum.slice.points[0].data.yFormatted} + issues pending on + {datum.slice.points[0].data.xFormatted} +
+ )} theme={{ background: "transparent", axis: { diff --git a/apps/app/components/core/spreadsheet-view/single-issue.tsx b/apps/app/components/core/spreadsheet-view/single-issue.tsx index bae89d8bb..2aabbef93 100644 --- a/apps/app/components/core/spreadsheet-view/single-issue.tsx +++ b/apps/app/components/core/spreadsheet-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"; @@ -10,12 +10,19 @@ import { ViewAssigneeSelect, ViewDueDateSelect, ViewEstimateSelect, + ViewLabelSelect, ViewPrioritySelect, ViewStateSelect, } from "components/issues"; +import { Popover2 } from "@blueprintjs/popover2"; // icons -import { CustomMenu, Icon } from "components/ui"; -import { LinkIcon, PencilIcon, TrashIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { Icon } from "components/ui"; +import { + EllipsisHorizontalIcon, + LinkIcon, + PencilIcon, + TrashIcon, +} from "@heroicons/react/24/outline"; // hooks import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import useToast from "hooks/use-toast"; @@ -26,10 +33,11 @@ import { CYCLE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, + SUB_ISSUES, VIEW_ISSUES, } from "constants/fetch-keys"; // types -import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types"; // helper import { copyTextToClipboard } from "helpers/string.helper"; @@ -58,6 +66,7 @@ export const SingleSpreadsheetIssue: React.FC = ({ userAuth, nestingLevel, }) => { + const [isOpen, setIsOpen] = useState(false); const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; @@ -67,7 +76,7 @@ export const SingleSpreadsheetIssue: React.FC = ({ const { setToastAlert } = useToast(); const partialUpdateIssue = useCallback( - (formData: Partial, issueId: string) => { + (formData: Partial, issue: IIssue) => { if (!workspaceSlug || !projectId) return; const fetchKey = cycleId @@ -78,25 +87,58 @@ export const SingleSpreadsheetIssue: React.FC = ({ ? VIEW_ISSUES(viewId.toString(), params) : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); - mutate( - fetchKey, - (prevData) => - (prevData ?? []).map((p) => { - if (p.id === issueId) { - return { - ...p, - ...formData, - }; - } - return p; - }), - false - ); + if (issue.parent) { + mutate( + SUB_ISSUES(issue.parent.toString()), + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + sub_issues: (prevData.sub_issues ?? []).map((i) => { + if (i.id === issue.id) { + return { + ...i, + ...formData, + }; + } + return i; + }), + }; + }, + false + ); + } else { + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((p) => { + if (p.id === issue.id) { + return { + ...p, + ...formData, + }; + } + return p; + }), + false + ); + } issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, formData, user) + .patchIssue( + workspaceSlug as string, + projectId as string, + issue.id as string, + formData, + user + ) .then(() => { - mutate(fetchKey); + if (issue.parent) { + mutate(SUB_ISSUES(issue.parent as string)); + } else { + mutate(fetchKey); + } }) .catch((error) => { console.log(error); @@ -128,27 +170,87 @@ export const SingleSpreadsheetIssue: React.FC = ({ className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-brand-surface-2 border-b border-brand-base w-full min-w-max" style={{ gridTemplateColumns }} > -
- -
+
+
+
{properties.key && ( - + {issue.project_detail?.identifier}-{issue.sequence_id} )} + {!isNotAllowed && ( +
+ setIsOpen(nextOpenState)} + content={ +
+ + + + + +
+ } + placement="bottom-start" + > + +
+
+ )}
-
+
{issue.sub_issues_count > 0 && ( )}
- +
+ {issue.name} @@ -191,30 +293,19 @@ export const SingleSpreadsheetIssue: React.FC = ({ />
)} - {properties.labels ? ( - issue.label_details.length > 0 ? ( -
- {issue.label_details.slice(0, 4).map((label, index) => ( -
- -
- ))} - {issue.label_details.length > 4 ? +{issue.label_details.length - 4} : null} -
- ) : ( -
- No Labels -
- ) - ) : ( - "" + {properties.labels && ( +
+ +
)} + {properties.due_date && (
= ({ />
)} -
- {!isNotAllowed && ( - - handleEditIssue(issue)}> -
- - Edit issue -
-
- handleDeleteIssue(issue)}> -
- - Delete issue -
-
- -
- - Copy issue link -
-
-
- )} -
); }; diff --git a/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx b/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx index 85d05a288..a0f404fba 100644 --- a/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx +++ b/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx @@ -40,7 +40,7 @@ export const SpreadsheetColumns: React.FC = ({ columnData, gridTemplateCo return (
{col.propertyName === "title" ? ( diff --git a/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx b/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx index c47ddd805..5655d11de 100644 --- a/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx +++ b/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx @@ -5,7 +5,7 @@ import { useRouter } from "next/router"; // components import { SpreadsheetColumns, SpreadsheetIssues } from "components/core"; -import { Icon, Spinner } from "components/ui"; +import { CustomMenu, Icon, Spinner } from "components/ui"; // hooks import useIssuesProperties from "hooks/use-issue-properties"; import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; @@ -17,15 +17,21 @@ import { SPREADSHEET_COLUMN } from "constants/spreadsheet"; import { PlusIcon } from "@heroicons/react/24/outline"; type Props = { + type: "issue" | "cycle" | "module"; handleEditIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void; + openIssuesListModal?: (() => void) | null; + isCompleted?: boolean; user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; export const SpreadsheetView: React.FC = ({ + type, handleEditIssue, handleDeleteIssue, + openIssuesListModal, + isCompleted = false, user, userAuth, }) => { @@ -56,7 +62,7 @@ export const SpreadsheetView: React.FC = ({ return (
-
+
{spreadsheetIssues ? ( @@ -75,16 +81,55 @@ export const SpreadsheetView: React.FC = ({ userAuth={userAuth} /> ))} - + {type === "issue" ? ( + + ) : ( + !isCompleted && ( + + + Add Issue + + } + position="left" + menuItemsClassName="left-5 !w-36" + noBorder + > + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + Create new + + {openIssuesListModal && ( + + Add an existing issue + + )} + + ) + )} +
) : ( diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx index e7be252b4..8bf64e6ff 100644 --- a/apps/app/components/issues/form.tsx +++ b/apps/app/components/issues/form.tsx @@ -23,7 +23,6 @@ import { IssueStateSelect, } from "components/issues/select"; import { CreateStateModal } from "components/states"; -import { CreateUpdateCycleModal } from "components/cycles"; import { CreateLabelModal } from "components/labels"; // ui import { @@ -73,7 +72,6 @@ const defaultValues: Partial = { description_html: "

", estimate_point: null, state: "", - cycle: null, priority: null, assignees: [], assignees_list: [], @@ -122,7 +120,6 @@ export const IssueForm: FC = ({ }) => { // states const [mostSimilarIssue, setMostSimilarIssue] = useState(); - const [cycleModal, setCycleModal] = useState(false); const [stateModal, setStateModal] = useState(false); const [labelModal, setLabelModal] = useState(false); const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); @@ -252,11 +249,6 @@ export const IssueForm: FC = ({ projectId={projectId} user={user} /> - setCycleModal(false)} - user={user} - /> setLabelModal(false)} diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx index dd5262453..b83bbc480 100644 --- a/apps/app/components/issues/modal.tsx +++ b/apps/app/components/issues/modal.tsx @@ -82,12 +82,17 @@ export const CreateUpdateIssueModal: React.FC = ({ const { params: inboxParams } = useInboxView(); const { params: spreadsheetParams } = useSpreadsheetIssuesView(); - if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string }; - if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string }; - const { user } = useUser(); const { setToastAlert } = useToast(); + if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string }; + if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string }; + if (router.asPath.includes("my-issues")) + prePopulateData = { + ...prePopulateData, + assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""], + }; + const { data: issues } = useSWR( workspaceSlug && activeProject ? PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? "") @@ -121,7 +126,7 @@ export const CreateUpdateIssueModal: React.FC = ({ }, [handleClose]); const addIssueToCycle = async (issueId: string, cycleId: string) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !activeProject) return; await issuesService .addIssueToCycle( @@ -142,7 +147,7 @@ export const CreateUpdateIssueModal: React.FC = ({ }; const addIssueToModule = async (issueId: string, moduleId: string) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !activeProject) return; await modulesService .addIssuesToModule( @@ -163,7 +168,7 @@ export const CreateUpdateIssueModal: React.FC = ({ }; const addIssueToInbox = async (formData: Partial) => { - if (!workspaceSlug || !projectId || !inboxId) return; + if (!workspaceSlug || !activeProject || !inboxId) return; const payload = { issue: { @@ -178,7 +183,7 @@ export const CreateUpdateIssueModal: React.FC = ({ await inboxServices .createInboxIssue( workspaceSlug.toString(), - projectId.toString(), + activeProject.toString(), inboxId.toString(), payload, user @@ -191,7 +196,7 @@ export const CreateUpdateIssueModal: React.FC = ({ }); router.push( - `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${res.issue_inbox[0].id}` + `/${workspaceSlug}/projects/${activeProject}/inbox/${inboxId}?inboxIssueId=${res.issue_inbox[0].id}` ); mutate(INBOX_ISSUES(inboxId.toString(), inboxParams)); @@ -211,7 +216,7 @@ export const CreateUpdateIssueModal: React.FC = ({ ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams) : viewId ? VIEW_ISSUES(viewId.toString(), calendarParams) - : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", calendarParams); + : PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", calendarParams); const spreadsheetFetchKey = cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams) @@ -219,7 +224,7 @@ export const CreateUpdateIssueModal: React.FC = ({ ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams) : viewId ? VIEW_ISSUES(viewId.toString(), spreadsheetParams) - : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", spreadsheetParams); + : PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", spreadsheetParams); const ganttFetchKey = cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString()) @@ -227,10 +232,10 @@ export const CreateUpdateIssueModal: React.FC = ({ ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString()) : viewId ? VIEW_ISSUES(viewId.toString(), viewGanttParams) - : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? ""); + : PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? ""); const createIssue = async (payload: Partial) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !activeProject) return; if (inboxId) await addIssueToInbox(payload); else @@ -252,7 +257,8 @@ export const CreateUpdateIssueModal: React.FC = ({ message: "Issue created successfully.", }); - if (payload.assignees_list?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE); + if (payload.assignees_list?.some((assignee) => assignee === user?.id)) + mutate(USER_ISSUE(workspaceSlug as string)); if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); }) diff --git a/apps/app/components/issues/my-issues-list-item.tsx b/apps/app/components/issues/my-issues-list-item.tsx index 5cff38478..790570cce 100644 --- a/apps/app/components/issues/my-issues-list-item.tsx +++ b/apps/app/components/issues/my-issues-list-item.tsx @@ -44,14 +44,14 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId const { setToastAlert } = useToast(); const partialUpdateIssue = useCallback( - (formData: Partial, issueId: string) => { + (formData: Partial, issue: IIssue) => { if (!workspaceSlug) return; mutate( USER_ISSUE(workspaceSlug as string), (prevData) => prevData?.map((p) => { - if (p.id === issueId) return { ...p, ...formData }; + if (p.id === issue.id) return { ...p, ...formData }; return p; }), @@ -59,7 +59,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId ); issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) + .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user) .then((res) => { mutate(USER_ISSUE(workspaceSlug as string)); }) diff --git a/apps/app/components/issues/view-select/assignee.tsx b/apps/app/components/issues/view-select/assignee.tsx index 1dbfbabba..8bfa797fe 100644 --- a/apps/app/components/issues/view-select/assignee.tsx +++ b/apps/app/components/issues/view-select/assignee.tsx @@ -18,7 +18,7 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys"; type Props = { issue: IIssue; - partialUpdateIssue: (formData: Partial, issueId: string) => void; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; position?: "left" | "right"; selfPositioned?: boolean; tooltipPosition?: "left" | "right"; @@ -108,7 +108,7 @@ export const ViewAssigneeSelect: React.FC = ({ if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); else newData.push(data); - partialUpdateIssue({ assignees_list: data }, issue.id); + partialUpdateIssue({ assignees_list: data }, issue); trackEventServices.trackIssuePartialPropertyUpdateEvent( { diff --git a/apps/app/components/issues/view-select/due-date.tsx b/apps/app/components/issues/view-select/due-date.tsx index f74b62689..163816a99 100644 --- a/apps/app/components/issues/view-select/due-date.tsx +++ b/apps/app/components/issues/view-select/due-date.tsx @@ -11,7 +11,7 @@ import { ICurrentUserResponse, IIssue } from "types"; type Props = { issue: IIssue; - partialUpdateIssue: (formData: Partial, issueId: string) => void; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; noBorder?: boolean; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; @@ -48,7 +48,7 @@ export const ViewDueDateSelect: React.FC = ({ priority: issue.priority, state: issue.state, }, - issue.id + issue ); trackEventServices.trackIssuePartialPropertyUpdateEvent( { diff --git a/apps/app/components/issues/view-select/estimate.tsx b/apps/app/components/issues/view-select/estimate.tsx index 02a3e0710..0f932405f 100644 --- a/apps/app/components/issues/view-select/estimate.tsx +++ b/apps/app/components/issues/view-select/estimate.tsx @@ -15,7 +15,7 @@ import { ICurrentUserResponse, IIssue } from "types"; type Props = { issue: IIssue; - partialUpdateIssue: (formData: Partial, issueId: string) => void; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; position?: "left" | "right"; selfPositioned?: boolean; customButton?: boolean; @@ -54,7 +54,7 @@ export const ViewEstimateSelect: React.FC = ({ { - partialUpdateIssue({ estimate_point: val }, issue.id); + partialUpdateIssue({ estimate_point: val }, issue); trackEventServices.trackIssuePartialPropertyUpdateEvent( { workspaceSlug, diff --git a/apps/app/components/issues/view-select/index.ts b/apps/app/components/issues/view-select/index.ts index 55ecfcbdb..a05cf61b6 100644 --- a/apps/app/components/issues/view-select/index.ts +++ b/apps/app/components/issues/view-select/index.ts @@ -3,3 +3,4 @@ export * from "./due-date"; export * from "./estimate"; export * from "./priority"; export * from "./state"; +export * from "./label"; diff --git a/apps/app/components/issues/view-select/label.tsx b/apps/app/components/issues/view-select/label.tsx new file mode 100644 index 000000000..33df5cf9f --- /dev/null +++ b/apps/app/components/issues/view-select/label.tsx @@ -0,0 +1,148 @@ +import React, { useState } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import issuesService from "services/issues.service"; +// component +import { CreateLabelModal } from "components/labels"; +// ui +import { CustomSearchSelect, Tooltip } from "components/ui"; +// icons +import { PlusIcon, TagIcon } from "@heroicons/react/24/outline"; +// types +import { ICurrentUserResponse, IIssue, IIssueLabels } from "types"; +// fetch-keys +import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; + +type Props = { + issue: IIssue; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + position?: "left" | "right"; + selfPositioned?: boolean; + tooltipPosition?: "left" | "right"; + customButton?: boolean; + user: ICurrentUserResponse | undefined; + isNotAllowed: boolean; +}; + +export const ViewLabelSelect: React.FC = ({ + issue, + partialUpdateIssue, + position = "left", + selfPositioned = false, + tooltipPosition = "right", + user, + isNotAllowed, + customButton = false, +}) => { + const [labelModal, setLabelModal] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: issueLabels } = useSWR( + projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null, + workspaceSlug && projectId + ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) + : null + ); + + const options = issueLabels?.map((label) => ({ + value: label.id, + query: label.name, + content: ( +
+ + {label.name} +
+ ), + })); + + const labelsLabel = ( + 0 + ? issue.label_details.map((label) => label.name ?? "").join(", ") + : "No Label" + } + > +
+ {issue.label_details.length > 0 ? ( + <> + {issue.label_details.slice(0, 4).map((label, index) => ( +
+ +
+ ))} + {issue.label_details.length > 4 ? +{issue.label_details.length - 4} : null} + + ) : ( + <> + + + )} +
+
+ ); + + const footerOption = ( + + ); + + return ( + <> + {projectId && ( + setLabelModal(false)} + projectId={projectId.toString()} + user={user} + /> + )} + { + partialUpdateIssue({ labels_list: data }, issue); + }} + options={options} + {...(customButton ? { customButton: labelsLabel } : { label: labelsLabel })} + multiple + noChevron + position={position} + disabled={isNotAllowed} + selfPositioned={selfPositioned} + footerOption={footerOption} + dropdownWidth="w-full min-w-[12rem]" + /> + + ); +}; diff --git a/apps/app/components/issues/view-select/priority.tsx b/apps/app/components/issues/view-select/priority.tsx index 499546931..0afd2dd98 100644 --- a/apps/app/components/issues/view-select/priority.tsx +++ b/apps/app/components/issues/view-select/priority.tsx @@ -17,7 +17,7 @@ import { capitalizeFirstLetter } from "helpers/string.helper"; type Props = { issue: IIssue; - partialUpdateIssue: (formData: Partial, issueId: string) => void; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; position?: "left" | "right"; selfPositioned?: boolean; noBorder?: boolean; @@ -41,7 +41,7 @@ export const ViewPrioritySelect: React.FC = ({ { - partialUpdateIssue({ priority: data }, issue.id); + partialUpdateIssue({ priority: data }, issue); trackEventServices.trackIssuePartialPropertyUpdateEvent( { workspaceSlug, diff --git a/apps/app/components/issues/view-select/state.tsx b/apps/app/components/issues/view-select/state.tsx index c097c7326..5ec0f71c7 100644 --- a/apps/app/components/issues/view-select/state.tsx +++ b/apps/app/components/issues/view-select/state.tsx @@ -19,7 +19,7 @@ import { STATES_LIST } from "constants/fetch-keys"; type Props = { issue: IIssue; - partialUpdateIssue: (formData: Partial, issueId: string) => void; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; position?: "left" | "right"; selfPositioned?: boolean; customButton?: boolean; @@ -83,7 +83,7 @@ export const ViewStateSelect: React.FC = ({ priority: issue.priority, target_date: issue.target_date, }, - issue.id + issue ); trackEventServices.trackIssuePartialPropertyUpdateEvent( { diff --git a/apps/app/components/states/create-update-state-inline.tsx b/apps/app/components/states/create-update-state-inline.tsx index 8a9d81968..94062878f 100644 --- a/apps/app/components/states/create-update-state-inline.tsx +++ b/apps/app/components/states/create-update-state-inline.tsx @@ -15,7 +15,7 @@ import stateService from "services/state.service"; // hooks import useToast from "hooks/use-toast"; // ui -import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui"; +import { CustomSelect, Input, PrimaryButton, SecondaryButton, Tooltip } from "components/ui"; // types import type { ICurrentUserResponse, IState, IStateResponse } from "types"; // fetch-keys @@ -28,6 +28,7 @@ type Props = { onClose: () => void; selectedGroup: StateGroup | null; user: ICurrentUserResponse | undefined; + groupLength: number; }; export type StateGroup = "backlog" | "unstarted" | "started" | "completed" | "cancelled" | null; @@ -43,6 +44,7 @@ export const CreateUpdateStateInline: React.FC = ({ onClose, selectedGroup, user, + groupLength, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -174,9 +176,8 @@ export const CreateUpdateStateInline: React.FC = ({ {({ open }) => ( <> {watch("color") && watch("color") !== "" && ( = ({ name="group" control={control} render={({ field: { value, onChange } }) => ( - k === value.toString()) - ? GROUP_CHOICES[value.toString() as keyof typeof GROUP_CHOICES] - : "Select group" - } - input - > - {Object.keys(GROUP_CHOICES).map((key) => ( - - {GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]} - - ))} - + +
+ k === value.toString()) + ? GROUP_CHOICES[value.toString() as keyof typeof GROUP_CHOICES] + : "Select group" + } + input + > + {Object.keys(GROUP_CHOICES).map((key) => ( + + {GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]} + + ))} + +
+
)} /> )} diff --git a/apps/app/components/ui/custom-menu.tsx b/apps/app/components/ui/custom-menu.tsx index 006802b79..958453b28 100644 --- a/apps/app/components/ui/custom-menu.tsx +++ b/apps/app/components/ui/custom-menu.tsx @@ -19,6 +19,7 @@ type Props = { noChevron?: boolean; position?: "left" | "right"; verticalPosition?: "top" | "bottom"; + menuItemsClassName?: string; customButton?: JSX.Element; menuItemsWhiteBg?: boolean; }; @@ -44,6 +45,7 @@ const CustomMenu = ({ noChevron = false, position = "right", verticalPosition = "bottom", + menuItemsClassName = "", customButton, menuItemsWhiteBg = false, }: Props) => ( @@ -133,7 +135,7 @@ const CustomMenu = ({ menuItemsWhiteBg ? "border-brand-surface-1 bg-brand-base" : "border-brand-base bg-brand-surface-1" - }`} + } ${menuItemsClassName}`} >
{children}
diff --git a/apps/app/components/ui/custom-select.tsx b/apps/app/components/ui/custom-select.tsx index 86196a63e..ec55bf29a 100644 --- a/apps/app/components/ui/custom-select.tsx +++ b/apps/app/components/ui/custom-select.tsx @@ -54,7 +54,7 @@ const CustomSelect = ({ ) : ( = ({ }} className={`${ child.selected ? "bg-brand-surface-2" : "" - } flex w-full items-center whitespace-nowrap break-words rounded px-1 py-1.5 text-left capitalize text-brand-secondary hover:bg-brand-surface-2`} + } flex w-full items-center justify-between whitespace-nowrap break-words rounded px-1 py-1.5 text-left capitalize text-brand-secondary hover:bg-brand-surface-2`} > {child.label} + ))}
diff --git a/apps/app/components/views/select-filters.tsx b/apps/app/components/views/select-filters.tsx index 3351be667..164c4f58c 100644 --- a/apps/app/components/views/select-filters.tsx +++ b/apps/app/components/views/select-filters.tsx @@ -70,7 +70,7 @@ export const SelectFilters: React.FC = ({ value: PRIORITIES, children: [ ...PRIORITIES.map((priority) => ({ - id: priority ?? "none", + id: priority === null ? "null" : priority, label: (
{getPriorityIcon(priority)} {priority ?? "None"} @@ -78,9 +78,9 @@ export const SelectFilters: React.FC = ({ ), value: { key: "priority", - value: priority, + value: priority === null ? "null" : priority, }, - selected: filters?.priority?.includes(priority ?? "none"), + selected: filters?.priority?.includes(priority === null ? "null" : priority), })), ], }, diff --git a/apps/app/components/workspace/completed-issues-graph.tsx b/apps/app/components/workspace/completed-issues-graph.tsx index 9a1ada618..4c7e1edb6 100644 --- a/apps/app/components/workspace/completed-issues-graph.tsx +++ b/apps/app/components/workspace/completed-issues-graph.tsx @@ -60,6 +60,14 @@ export const CompletedIssuesGraph: React.FC = ({ month, issues, setMonth margin={{ top: 20, right: 20, bottom: 20, left: 20 }} customYAxisTickValues={data.map((item) => item.completed_count)} colors={(datum) => datum.color} + enableSlices="x" + sliceTooltip={(datum) => ( +
+ {datum.slice.points[0].data.yFormatted} + issues closed in + {datum.slice.points[0].data.xFormatted} +
+ )} theme={{ background: "rgb(var(--color-bg-base))", }} diff --git a/apps/app/layouts/app-layout/app-header.tsx b/apps/app/layouts/app-layout/app-header.tsx index 95f998dec..1f99e044e 100644 --- a/apps/app/layouts/app-layout/app-header.tsx +++ b/apps/app/layouts/app-layout/app-header.tsx @@ -6,10 +6,15 @@ type Props = { left?: JSX.Element; right?: JSX.Element; setToggleSidebar: React.Dispatch>; + noHeader: boolean; }; -const Header: React.FC = ({ breadcrumbs, left, right, setToggleSidebar }) => ( -
+const Header: React.FC = ({ breadcrumbs, left, right, setToggleSidebar, noHeader }) => ( +