From 31a5211d2cc6db97428acec8c19a49d8256b341f Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:33:10 +0530 Subject: [PATCH] chore: workspace active cycle revamp, project active cycle bug fixes and improvement. (#3407) * chore:progress component improvement * fix: completed at for done state * fix: urgent and high issues in cycle * fix: added pagination for active cycles * fix: load more pagination * chore: cycle types, constant and service updated * chore: linear progress indicator component improvement * chore: project cycle bug fixes and improvement * chore: workspace active cycles revamp * fix: module and cycle modal start date validation * chore: workspace active cycle improvement * chore: workspace active cycles imrprovement * chore: workspace active cycles imrprovement --------- Co-authored-by: NarayanBavisetti --- apiserver/plane/app/views/cycle.py | 214 +++++----- apiserver/plane/db/models/issue.py | 12 + apiserver/plane/utils/analytics_plot.py | 6 +- packages/types/src/cycles.d.ts | 2 +- packages/types/src/workspace.d.ts | 12 + .../progress/linear-progress-indicator.tsx | 14 +- .../core/sidebar/single-progress-stats.tsx | 2 +- .../cycles/active-cycle-details.tsx | 287 ++++--------- web/components/cycles/active-cycle-info.tsx | 380 +++++++++--------- web/components/cycles/active-cycle-stats.tsx | 2 +- web/components/cycles/form.tsx | 1 + .../headers/workspace-active-cycle.tsx | 49 +-- web/components/modules/form.tsx | 3 +- .../workspace-active-cycles-list.tsx | 105 +++-- web/constants/cycle.ts | 26 +- web/constants/fetch-keys.ts | 2 + web/services/cycle.service.ts | 15 +- web/store/workspace/index.ts | 14 - 18 files changed, 546 insertions(+), 600 deletions(-) diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle.py index 4db0ec565..b11b7a3a7 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle.py @@ -916,6 +916,115 @@ class ActiveCycleEndpoint(BaseAPIView): permission_classes = [ WorkspaceUserPermission, ] + + def get_results_controller(self, results, active_cycles=None): + for cycle in results: + assignee_distribution = ( + Issue.objects.filter( + issue_cycle__cycle_id=cycle["id"], + project_id=cycle["project"], + workspace__slug=self.kwargs.get("slug"), + ) + .annotate(display_name=F("assignees__display_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate(avatar=F("assignees__avatar")) + .values("display_name", "assignee_id", "avatar") + .annotate( + total_issues=Count( + "assignee_id", + filter=Q(archived_at__isnull=True, is_draft=False), + ), + ) + .annotate( + completed_issues=Count( + "assignee_id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "assignee_id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("display_name") + ) + + label_distribution = ( + Issue.objects.filter( + issue_cycle__cycle_id=cycle["id"], + project_id=cycle["project"], + workspace__slug=self.kwargs.get("slug"), + ) + .annotate(label_name=F("labels__name")) + .annotate(color=F("labels__color")) + .annotate(label_id=F("labels__id")) + .values("label_name", "color", "label_id") + .annotate( + total_issues=Count( + "label_id", + filter=Q(archived_at__isnull=True, is_draft=False), + ) + ) + .annotate( + completed_issues=Count( + "label_id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "label_id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("label_name") + ) + cycle["distribution"] = { + "assignees": assignee_distribution, + "labels": label_distribution, + "completion_chart": {}, + } + if cycle["start_date"] and cycle["end_date"]: + cycle["distribution"][ + "completion_chart" + ] = burndown_plot( + queryset=active_cycles.get(pk=cycle["id"]), + slug=self.kwargs.get("slug"), + project_id=cycle["project"], + cycle_id=cycle["id"], + ) + + priority_issues = Issue.objects.filter(issue_cycle__cycle_id=cycle["id"], priority__in=["urgent", "high"]) + # Priority Ordering + priority_order = ["urgent", "high"] + priority_issues = priority_issues.annotate( + priority_order=Case( + *[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)], + output_field=CharField(), + ) + ).order_by("priority_order")[:5] + + cycle["issues"] = IssueSerializer(priority_issues, many=True).data + + return results + def get(self, request, slug): subquery = CycleFavorite.objects.filter( user=self.request.user, @@ -1045,100 +1154,11 @@ class ActiveCycleEndpoint(BaseAPIView): ) .order_by("-created_at") ) - - cycles = CycleSerializer(active_cycles, many=True).data - - for cycle in cycles: - assignee_distribution = ( - Issue.objects.filter( - issue_cycle__cycle_id=cycle["id"], - project_id=cycle["project"], - workspace__slug=slug, - ) - .annotate(display_name=F("assignees__display_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate(avatar=F("assignees__avatar")) - .values("display_name", "assignee_id", "avatar") - .annotate( - total_issues=Count( - "assignee_id", - filter=Q(archived_at__isnull=True, is_draft=False), - ), - ) - .annotate( - completed_issues=Count( - "assignee_id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "assignee_id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("display_name") - ) - - label_distribution = ( - Issue.objects.filter( - issue_cycle__cycle_id=cycle["id"], - project_id=cycle["project"], - workspace__slug=slug, - ) - .annotate(label_name=F("labels__name")) - .annotate(color=F("labels__color")) - .annotate(label_id=F("labels__id")) - .values("label_name", "color", "label_id") - .annotate( - total_issues=Count( - "label_id", - filter=Q(archived_at__isnull=True, is_draft=False), - ) - ) - .annotate( - completed_issues=Count( - "label_id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "label_id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("label_name") - ) - cycle["distribution"] = { - "assignees": assignee_distribution, - "labels": label_distribution, - "completion_chart": {}, - } - if cycle["start_date"] and cycle["end_date"]: - cycle["distribution"][ - "completion_chart" - ] = burndown_plot( - queryset=active_cycles.get(pk=cycle["id"]), - slug=slug, - project_id=cycle["project"], - cycle_id=cycle["id"], - ) - - return Response(cycles, status=status.HTTP_200_OK) + + return self.paginate( + request=request, + queryset=active_cycles, + on_results=lambda active_cycles: CycleSerializer(active_cycles, many=True).data, + controller=lambda results: self.get_results_controller(results, active_cycles), + default_per_page=int(request.GET.get("per_page", 3)) + ) diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 43274ea13..d5ed4247a 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -9,6 +9,7 @@ from django.db.models.signals import post_save from django.dispatch import receiver from django.core.validators import MinValueValidator, MaxValueValidator from django.core.exceptions import ValidationError +from django.utils import timezone # Module imports from . import ProjectBaseModel @@ -183,6 +184,17 @@ class Issue(ProjectBaseModel): self.state = default_state except ImportError: pass + else: + try: + from plane.db.models import State + + # Check if the current issue state group is completed or not + if self.state.group == "completed": + self.completed_at = timezone.now() + else: + self.completed_at = None + except ImportError: + pass if self._state.adding: # Get the maximum display_id value from the database diff --git a/apiserver/plane/utils/analytics_plot.py b/apiserver/plane/utils/analytics_plot.py index 07d456a1d..2a2a11fd8 100644 --- a/apiserver/plane/utils/analytics_plot.py +++ b/apiserver/plane/utils/analytics_plot.py @@ -12,6 +12,7 @@ from django.db.models.functions import ( ExtractYear, Concat, ) +from django.utils import timezone # Module imports from plane.db.models import Issue @@ -168,6 +169,9 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): if item["date"] is not None and item["date"] <= date ) cumulative_pending_issues -= total_completed - chart_data[str(date)] = cumulative_pending_issues + if date > timezone.now().date(): + chart_data[str(date)] = None + else: + chart_data[str(date)] = cumulative_pending_issues return chart_data diff --git a/packages/types/src/cycles.d.ts b/packages/types/src/cycles.d.ts index 92ee18a42..b1efb48c5 100644 --- a/packages/types/src/cycles.d.ts +++ b/packages/types/src/cycles.d.ts @@ -62,7 +62,7 @@ export type TAssigneesDistribution = { }; export type TCompletionChartDistribution = { - [key: string]: number; + [key: string]: number | null; }; export type TLabelsDistribution = { diff --git a/packages/types/src/workspace.d.ts b/packages/types/src/workspace.d.ts index 2d7e94d95..426183860 100644 --- a/packages/types/src/workspace.d.ts +++ b/packages/types/src/workspace.d.ts @@ -1,5 +1,6 @@ import { EUserWorkspaceRoles } from "constants/workspace"; import type { + ICycle, IProjectMember, IUser, IUserLite, @@ -182,3 +183,14 @@ export interface IProductUpdateResponse { eyes: number; }; } + +export interface IWorkspaceActiveCyclesResponse { + count: number; + extra_stats: null; + next_cursor: string; + next_page_results: boolean; + prev_cursor: string; + prev_page_results: boolean; + results: ICycle[]; + total_pages: number; +} diff --git a/packages/ui/src/progress/linear-progress-indicator.tsx b/packages/ui/src/progress/linear-progress-indicator.tsx index 471015406..f1f3e5d17 100644 --- a/packages/ui/src/progress/linear-progress-indicator.tsx +++ b/packages/ui/src/progress/linear-progress-indicator.tsx @@ -4,31 +4,37 @@ import { Tooltip } from "../tooltip"; type Props = { data: any; noTooltip?: boolean; + inPercentage?: boolean; }; -export const LinearProgressIndicator: React.FC = ({ data, noTooltip = false }) => { +export const LinearProgressIndicator: React.FC = ({ data, noTooltip = false, inPercentage = false }) => { const total = data.reduce((acc: any, cur: any) => acc + cur.value, 0); // eslint-disable-next-line @typescript-eslint/no-unused-vars let progress = 0; - const bars = data.map((item: any) => { + const bars = data.map((item: any, index: Number) => { const width = `${(item.value / total) * 100}%`; const style = { width, backgroundColor: item.color, + borderTopLeftRadius: index === 0 ? "99px" : 0, + borderBottomLeftRadius: index === 0 ? "99px" : 0, + borderTopRightRadius: index === data.length - 1 ? "99px" : 0, + borderBottomRightRadius: index === data.length - 1 ? "99px" : 0, }; progress += item.value; if (noTooltip) return
; + if (width === "0%") return <>; else return ( - +
); }); return ( -
+
{total === 0 ? (
{bars}
) : ( diff --git a/web/components/core/sidebar/single-progress-stats.tsx b/web/components/core/sidebar/single-progress-stats.tsx index f58bbc2c3..4d926285b 100644 --- a/web/components/core/sidebar/single-progress-stats.tsx +++ b/web/components/core/sidebar/single-progress-stats.tsx @@ -30,7 +30,7 @@ export const SingleProgressStats: React.FC = ({ - {isNaN(Math.floor((completed / total) * 100)) ? "0" : Math.floor((completed / total) * 100)}% + {isNaN(Math.round((completed / total) * 100)) ? "0" : Math.round((completed / total) * 100)}%
of {total} diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index 56c5e1bc9..931147102 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; // hooks -import { useApplication, useCycle, useIssues, useProjectState } from "hooks/store"; +import { useApplication, useCycle, useIssues, useProject } from "hooks/store"; import useToast from "hooks/use-toast"; // ui import { SingleProgressStats } from "components/core"; @@ -12,55 +12,27 @@ import { Loader, Tooltip, LinearProgressIndicator, - ContrastIcon, - RunningIcon, LayersIcon, StateGroupIcon, PriorityIcon, Avatar, + CycleGroupIcon, } from "@plane/ui"; // components import ProgressChart from "components/core/sidebar/progress-chart"; import { ActiveCycleProgressStats } from "components/cycles"; -import { ViewIssueLabel } from "components/issues"; +import { StateDropdown } from "components/dropdowns"; // icons -import { AlarmClock, AlertTriangle, ArrowRight, CalendarDays, Star, Target } from "lucide-react"; +import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react"; // helpers -import { renderFormattedDate, findHowManyDaysLeft } from "helpers/date-time.helper"; +import { renderFormattedDate, findHowManyDaysLeft, renderFormattedDateWithoutYear } from "helpers/date-time.helper"; import { truncateText } from "helpers/string.helper"; // types -import { ICycle } from "@plane/types"; +import { ICycle, TCycleGroups } from "@plane/types"; +// constants import { EIssuesStoreType } from "constants/issue"; -import { ACTIVE_CYCLE_ISSUES } from "store/issue/cycle"; import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; - -const stateGroups = [ - { - key: "backlog_issues", - title: "Backlog", - color: "#dee2e6", - }, - { - key: "unstarted_issues", - title: "Unstarted", - color: "#26b5ce", - }, - { - key: "started_issues", - title: "Started", - color: "#f7ae59", - }, - { - key: "cancelled_issues", - title: "Cancelled", - color: "#d687ff", - }, - { - key: "completed_issues", - title: "Completed", - color: "#09a953", - }, -]; +import { STATE_GROUPS_DETAILS } from "constants/cycle"; interface IActiveCycleDetails { workspaceSlug: string; @@ -72,8 +44,7 @@ export const ActiveCycleDetails: React.FC = observer((props const { workspaceSlug, projectId } = props; // store hooks const { - issues: { issues, fetchActiveCycleIssues }, - issueMap, + issues: { fetchActiveCycleIssues }, } = useIssues(EIssuesStoreType.CYCLE); const { commandPalette: { toggleCreateCycleModal }, @@ -85,7 +56,7 @@ export const ActiveCycleDetails: React.FC = observer((props addCycleToFavorites, removeCycleFromFavorites, } = useCycle(); - const { getProjectStates } = useProjectState(); + const { currentProjectDetails } = useProject(); // toast alert const { setToastAlert } = useToast(); @@ -95,9 +66,8 @@ export const ActiveCycleDetails: React.FC = observer((props ); const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null; - const issueIds = issues?.[ACTIVE_CYCLE_ISSUES]; - useSWR( + const { data: activeCycleIssues } = useSWR( workspaceSlug && projectId && currentProjectActiveCycleId ? CYCLE_ISSUES_WITH_PARAMS(currentProjectActiveCycleId, { priority: "urgent,high" }) : null, @@ -142,14 +112,13 @@ export const ActiveCycleDetails: React.FC = observer((props const startDate = new Date(activeCycle.start_date ?? ""); const groupedIssues: any = { - backlog: activeCycle.backlog_issues, - unstarted: activeCycle.unstarted_issues, - started: activeCycle.started_issues, completed: activeCycle.completed_issues, - cancelled: activeCycle.cancelled_issues, + started: activeCycle.started_issues, + unstarted: activeCycle.unstarted_issues, + backlog: activeCycle.backlog_issues, }; - const cycleStatus = activeCycle.status.toLocaleLowerCase(); + const cycleStatus = activeCycle.status.toLowerCase() as TCycleGroups; const handleAddToFavorites = (e: MouseEvent) => { e.preventDefault(); @@ -177,7 +146,7 @@ export const ActiveCycleDetails: React.FC = observer((props }); }; - const progressIndicatorData = stateGroups.map((group, index) => ({ + const progressIndicatorData = STATE_GROUPS_DETAILS.map((group, index) => ({ id: index, name: group.title, value: @@ -187,6 +156,8 @@ export const ActiveCycleDetails: React.FC = observer((props color: group.color, })); + const daysLeft = findHowManyDaysLeft(activeCycle.end_date ?? new Date()); + return (
@@ -196,68 +167,15 @@ export const ActiveCycleDetails: React.FC = observer((props
- +

{truncateText(activeCycle.name, 70)}

- - {cycleStatus === "current" ? ( - - - {findHowManyDaysLeft(activeCycle.end_date ?? new Date())} Days Left - - ) : cycleStatus === "upcoming" ? ( - - - {findHowManyDaysLeft(activeCycle.start_date ?? new Date())} Days Left - - ) : cycleStatus === "completed" ? ( - - {activeCycle.total_issues - activeCycle.completed_issues > 0 && ( - - - - - - )}{" "} - Completed - - ) : ( - cycleStatus - )} + + {`${daysLeft} ${daysLeft > 1 ? "Days" : "Day"} Left`} {activeCycle.is_favorite ? ( - ) : ( - - )} - -
- -
-
- - {cycle?.start_date && {renderFormattedDate(cycle?.start_date)}} -
- -
- - {cycle?.end_date && {renderFormattedDate(cycle?.end_date)}} -
-
- -
-
+ <> +
+ {cycle.project_detail.emoji ? ( + + {renderEmoji(cycle.project_detail.emoji)} + + ) : cycle.project_detail.icon_prop ? ( +
+ {renderEmoji(cycle.project_detail.icon_prop)} +
+ ) : ( + + {cycle.project_detail?.name.charAt(0)} + + )} +

{cycle.project_detail.name}

+
+
+
+
+ + +

{truncateText(cycle.name, 70)}

+
+ + + {`${daysLeft} ${daysLeft > 1 ? "Days" : "Day"} Left`} + + +
+
+ + + Lead: +
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( {cycle.owned_by.display_name} @@ -170,85 +118,143 @@ export const ActiveCycleInfo: FC = (props) => { {cycle.owned_by.display_name.charAt(0)} )} - {cycle.owned_by.display_name} + {cycle.owned_by.display_name}
- - {cycle.assignees.length > 0 && ( -
- - {cycle.assignees.map((assignee: any) => ( - - ))} - -
- )} -
- -
-
- - {cycle.total_issues} issues -
-
- - {cycle.completed_issues} issues -
-
-
- { - handleCurrentView("active"); - }} - > - - View Cycle - - - - - - View Cycle Issues - - -
-
+ + + + +
-
-
-
-
- Progress - -
-
+
+
+
+

Progress

+ + {`${cycle.completed_issues + cycle.cancelled_issues}/${cycle.total_issues - cycle.cancelled_issues} ${ + cycle.completed_issues + cycle.cancelled_issues > 1 ? "Issues" : "Issue" + } closed`} + +
+ +
+
{Object.keys(groupedIssues).map((group, index) => ( - - - {group} + <> + {groupedIssues[group] > 0 && ( +
+
+ + {group} +
+ {`: ${groupedIssues[group]} ${groupedIssues[group] > 1 ? "Issues" : "Issue"}`}
- } - completed={groupedIssues[group]} - total={cycle.total_issues} - /> + )} + ))} + {cycle.cancelled_issues > 0 && ( + + + {`${cycle.cancelled_issues} cancelled ${ + cycle.cancelled_issues > 1 ? "issues are" : "issue is" + } excluded from this report.`}{" "} + + + )}
-
- + +
+
+

Issue Burndown

+
+ +
+ +
+
+
+
+

Priority

+
+
+ {cycleIssues ? ( + cycleIssues.length > 0 ? ( + cycleIssues.map((issue: any) => ( + +
+ + + + {cycle.project_detail?.identifier}-{issue.sequence_id} + + + + {issue.name} + +
+
+ {}} + projectId={projectId?.toString() ?? ""} + disabled={true} + buttonVariant="background-with-text" + /> + {issue.target_date && ( + +
+ + {renderFormattedDateWithoutYear(issue.target_date)} +
+
+ )} +
+ + )) + ) : ( +
+ There are no high priority issues present in this cycle. +
+ ) + ) : ( + + + + + + )} +
-
+ ); }; diff --git a/web/components/cycles/active-cycle-stats.tsx b/web/components/cycles/active-cycle-stats.tsx index 524b02dd0..1ffe19260 100644 --- a/web/components/cycles/active-cycle-stats.tsx +++ b/web/components/cycles/active-cycle-stats.tsx @@ -127,7 +127,7 @@ export const ActiveCycleProgressStats: React.FC = ({ cycle }) => { ) : (
- No issues present in the cycle. + There are no high priority issues present in this cycle.
)} diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index bab31d93f..e648a158e 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -126,6 +126,7 @@ export const CycleForm: React.FC = (props) => { onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)} buttonVariant="border-with-text" placeholder="Start date" + minDate={new Date()} maxDate={maxDate ?? undefined} tabIndex={3} /> diff --git a/web/components/headers/workspace-active-cycle.tsx b/web/components/headers/workspace-active-cycle.tsx index f5cf00010..79e6bcaf3 100644 --- a/web/components/headers/workspace-active-cycle.tsx +++ b/web/components/headers/workspace-active-cycle.tsx @@ -1,40 +1,23 @@ import { observer } from "mobx-react-lite"; -import { Search, SendToBack } from "lucide-react"; -// hooks -import { useWorkspace } from "hooks/store"; +import { SendToBack } from "lucide-react"; // ui import { Breadcrumbs } from "@plane/ui"; -export const WorkspaceActiveCycleHeader = observer(() => { - // store hooks - const { workspaceActiveCyclesSearchQuery, setWorkspaceActiveCyclesSearchQuery } = useWorkspace(); - return ( -
-
-
- - } - label="Active Cycles" - /> - - - Beta - -
-
-
-
- - setWorkspaceActiveCyclesSearchQuery(e.target.value)} - placeholder="Search" +export const WorkspaceActiveCycleHeader = observer(() => ( +
+
+
+ + } + label="Active Cycles" /> -
+ + + Beta +
- ); -}); +
+)); diff --git a/web/components/modules/form.tsx b/web/components/modules/form.tsx index be0792caa..661d31ef5 100644 --- a/web/components/modules/form.tsx +++ b/web/components/modules/form.tsx @@ -70,7 +70,7 @@ export const ModuleForm: React.FC = ({ const startDate = watch("start_date"); const targetDate = watch("target_date"); - const minDate = startDate ? new Date(startDate) : null; + const minDate = startDate ? new Date(startDate) : new Date(); minDate?.setDate(minDate.getDate()); const maxDate = targetDate ? new Date(targetDate) : null; @@ -159,6 +159,7 @@ export const ModuleForm: React.FC = ({ onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)} buttonVariant="border-with-text" placeholder="Start date" + minDate={new Date()} maxDate={maxDate ?? undefined} tabIndex={3} /> diff --git a/web/components/workspace/workspace-active-cycles-list.tsx b/web/components/workspace/workspace-active-cycles-list.tsx index 85740f6c3..922df4d9f 100644 --- a/web/components/workspace/workspace-active-cycles-list.tsx +++ b/web/components/workspace/workspace-active-cycles-list.tsx @@ -1,57 +1,90 @@ +import { useEffect, useState } from "react"; +import useSWR from "swr"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import useSWR from "swr"; +import isEqual from "lodash/isEqual"; // components import { ActiveCycleInfo } from "components/cycles"; +import { Button, ContrastIcon, Spinner } from "@plane/ui"; // services import { CycleService } from "services/cycle.service"; const cycleService = new CycleService(); -// hooks -import { useWorkspace } from "hooks/store"; -// helpers -import { renderEmoji } from "helpers/emoji.helper"; +// constants +import { WORKSPACE_ACTIVE_CYCLES_LIST } from "constants/fetch-keys"; +// types +import { ICycle } from "@plane/types"; + +const per_page = 3; export const WorkspaceActiveCyclesList = observer(() => { + // state + const [cursor, setCursor] = useState(`3:0:0`); + const [allCyclesData, setAllCyclesData] = useState([]); + const [hasMoreResults, setHasMoreResults] = useState(true); // router const router = useRouter(); const { workspaceSlug } = router.query; + // fetching active cycles in workspace - const { data } = useSWR("WORKSPACE_ACTIVE_CYCLES", () => cycleService.workspaceActiveCycles(workspaceSlug as string)); - // store - const { workspaceActiveCyclesSearchQuery } = useWorkspace(); - // filter cycles based on search query - const filteredCycles = data?.filter( - (cycle) => - cycle.project_detail.name.toLowerCase().includes(workspaceActiveCyclesSearchQuery.toLowerCase()) || - cycle.project_detail.identifier?.toLowerCase().includes(workspaceActiveCyclesSearchQuery.toLowerCase()) || - cycle.name.toLowerCase().includes(workspaceActiveCyclesSearchQuery.toLowerCase()) + const { data: workspaceActiveCycles, isLoading } = useSWR( + workspaceSlug && cursor ? WORKSPACE_ACTIVE_CYCLES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null, + workspaceSlug && cursor + ? () => cycleService.workspaceActiveCycles(workspaceSlug.toString(), cursor, per_page) + : null ); + useEffect(() => { + if (workspaceActiveCycles && !isEqual(workspaceActiveCycles.results, allCyclesData)) { + setAllCyclesData((prevData) => [...prevData, ...workspaceActiveCycles.results]); + setHasMoreResults(workspaceActiveCycles.next_page_results); + } + }, [workspaceActiveCycles]); + + const handleLoadMore = () => { + if (hasMoreResults) { + setCursor(workspaceActiveCycles?.next_cursor); + } + }; + + if (allCyclesData.length === 0 && !workspaceActiveCycles) { + return ( +
+ +
+ ); + } + return ( -
- {workspaceSlug && - filteredCycles && - filteredCycles.map((cycle) => ( -
-
- {cycle.project_detail.emoji ? ( - - {renderEmoji(cycle.project_detail.emoji)} - - ) : cycle.project_detail.icon_prop ? ( -
- {renderEmoji(cycle.project_detail.icon_prop)} -
- ) : ( - - {cycle.project_detail?.name.charAt(0)} - - )} -

{cycle.project_detail.name}

+
+ {allCyclesData.length > 0 ? ( + <> + {workspaceSlug && + allCyclesData.map((cycle) => ( +
+ +
+ ))} + + {hasMoreResults && ( +
+
- + )} + + ) : ( +
+
+
+ +
+

+ No ongoing cycles are currently active in any of the projects. +

- ))} +
+ )}
); }); diff --git a/web/constants/cycle.ts b/web/constants/cycle.ts index 1b1453503..179420aae 100644 --- a/web/constants/cycle.ts +++ b/web/constants/cycle.ts @@ -87,31 +87,25 @@ export const CYCLE_STATUS: { }, ]; - export const STATE_GROUPS_DETAILS = [ { - key: "backlog_issues", - title: "Backlog", - color: "#dee2e6", - }, - { - key: "unstarted_issues", - title: "Unstarted", - color: "#26b5ce", + key: "completed_issues", + title: "Completed", + color: "#46A758", }, { key: "started_issues", title: "Started", - color: "#f7ae59", + color: "#FFC53D", }, { - key: "cancelled_issues", - title: "Cancelled", - color: "#d687ff", + key: "unstarted_issues", + title: "Unstarted", + color: "#FB923C", }, { - key: "completed_issues", - title: "Completed", - color: "#09a953", + key: "backlog_issues", + title: "Backlog", + color: "#F0F0F3", }, ]; diff --git a/web/constants/fetch-keys.ts b/web/constants/fetch-keys.ts index ec88c8c87..719d889f4 100644 --- a/web/constants/fetch-keys.ts +++ b/web/constants/fetch-keys.ts @@ -142,6 +142,8 @@ export const WORKSPACE_LABELS = (workspaceSlug: string) => `WORKSPACE_LABELS_${w export const PROJECT_GITHUB_REPOSITORY = (projectId: string) => `PROJECT_GITHUB_REPOSITORY_${projectId.toUpperCase()}`; // cycles +export const WORKSPACE_ACTIVE_CYCLES_LIST = (workspaceSlug: string, cursor: string, per_page: string) => + `WORKSPACE_ACTIVE_CYCLES_LIST_${workspaceSlug.toUpperCase()}_${cursor.toUpperCase()}_${per_page.toUpperCase()}`; export const CYCLES_LIST = (projectId: string) => `CYCLE_LIST_${projectId.toUpperCase()}`; export const INCOMPLETE_CYCLES_LIST = (projectId: string) => `INCOMPLETE_CYCLES_LIST_${projectId.toUpperCase()}`; export const CURRENT_CYCLE_LIST = (projectId: string) => `CURRENT_CYCLE_LIST_${projectId.toUpperCase()}`; diff --git a/web/services/cycle.service.ts b/web/services/cycle.service.ts index 7c22f34a6..f624aa271 100644 --- a/web/services/cycle.service.ts +++ b/web/services/cycle.service.ts @@ -1,7 +1,7 @@ // services import { APIService } from "services/api.service"; // types -import type { CycleDateCheckData, ICycle, TIssue, TIssueMap } from "@plane/types"; +import type { CycleDateCheckData, ICycle, IWorkspaceActiveCyclesResponse, TIssue } from "@plane/types"; // helpers import { API_BASE_URL } from "helpers/common.helper"; @@ -10,8 +10,17 @@ export class CycleService extends APIService { super(API_BASE_URL); } - async workspaceActiveCycles(workspaceSlug: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/active-cycles/`) + async workspaceActiveCycles( + workspaceSlug: string, + cursor: string, + per_page: number + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/active-cycles/`, { + params: { + per_page, + cursor, + }, + }) .then((res) => res?.data) .catch((err) => { throw err?.response?.data; diff --git a/web/store/workspace/index.ts b/web/store/workspace/index.ts index d3d7b58ac..4020aaef7 100644 --- a/web/store/workspace/index.ts +++ b/web/store/workspace/index.ts @@ -12,12 +12,9 @@ import { ApiTokenStore, IApiTokenStore } from "./api-token.store"; export interface IWorkspaceRootStore { // observables workspaces: Record; - workspaceActiveCyclesSearchQuery: string; // computed currentWorkspace: IWorkspace | null; workspacesCreatedByCurrentUser: IWorkspace[] | null; - // actions - setWorkspaceActiveCyclesSearchQuery: (query: string) => void; // computed actions getWorkspaceBySlug: (workspaceSlug: string) => IWorkspace | null; getWorkspaceById: (workspaceId: string) => IWorkspace | null; @@ -34,7 +31,6 @@ export interface IWorkspaceRootStore { export class WorkspaceRootStore implements IWorkspaceRootStore { // observables - workspaceActiveCyclesSearchQuery: string = ""; workspaces: Record = {}; // services workspaceService; @@ -49,7 +45,6 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { makeObservable(this, { // observables workspaces: observable, - workspaceActiveCyclesSearchQuery: observable.ref, // computed currentWorkspace: computed, workspacesCreatedByCurrentUser: computed, @@ -57,7 +52,6 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { getWorkspaceBySlug: action, getWorkspaceById: action, // actions - setWorkspaceActiveCyclesSearchQuery: action, fetchWorkspaces: action, createWorkspace: action, updateWorkspace: action, @@ -108,14 +102,6 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { */ getWorkspaceById = (workspaceId: string) => this.workspaces?.[workspaceId] || null; // TODO: use undefined instead of null - /** - * Sets search query - * @param query - */ - setWorkspaceActiveCyclesSearchQuery = (query: string) => { - this.workspaceActiveCyclesSearchQuery = query; - }; - /** * fetch user workspaces from API */