From 910ac659fa47ecb7d83da8b56ca5580358c0af2b Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Sun, 28 Jan 2024 16:46:32 +0530 Subject: [PATCH] fix: active cycles pagination issue fixes (#97) * fix: breaking down active cycles info component into multiple components * fix: pagination fixes * fix: minor bugfixes and edge handling --- web/components/cycles/active-cycle-info.tsx | 253 +----------------- web/components/cycles/active-cycles/card.tsx | 35 +++ .../cycles/active-cycles/header.tsx | 97 +++++++ web/components/cycles/active-cycles/index.ts | 7 + .../cycles/active-cycles/list-page.tsx | 54 ++++ .../cycles/active-cycles/priority-issues.tsx | 87 ++++++ .../cycles/active-cycles/productivity.tsx | 30 +++ .../cycles/active-cycles/progress.tsx | 74 +++++ .../cycles/active-cycles/project-title.tsx | 29 ++ .../workspace-active-cycles-list.tsx | 100 +++---- 10 files changed, 471 insertions(+), 295 deletions(-) create mode 100644 web/components/cycles/active-cycles/card.tsx create mode 100644 web/components/cycles/active-cycles/header.tsx create mode 100644 web/components/cycles/active-cycles/index.ts create mode 100644 web/components/cycles/active-cycles/list-page.tsx create mode 100644 web/components/cycles/active-cycles/priority-issues.tsx create mode 100644 web/components/cycles/active-cycles/productivity.tsx create mode 100644 web/components/cycles/active-cycles/progress.tsx create mode 100644 web/components/cycles/active-cycles/project-title.tsx diff --git a/web/components/cycles/active-cycle-info.tsx b/web/components/cycles/active-cycle-info.tsx index 63d003323a..684e7362ad 100644 --- a/web/components/cycles/active-cycle-info.tsx +++ b/web/components/cycles/active-cycle-info.tsx @@ -1,21 +1,14 @@ -import { FC, useCallback } from "react"; -import Link from "next/link"; -// hooks -import useLocalStorage from "hooks/use-local-storage"; -// ui -import { Tooltip, LinearProgressIndicator, Loader, PriorityIcon, Button, CycleGroupIcon } from "@plane/ui"; -import { CalendarCheck } from "lucide-react"; +import { FC } from "react"; // components -import ProgressChart from "components/core/sidebar/progress-chart"; -import { StateDropdown } from "components/dropdowns"; +import { + ActiveCyclesProjectTitle, + ActiveCycleHeader, + ActiveCycleProgress, + ActiveCycleProductivity, + ActiveCyclePriorityIssues, +} from "components/cycles/active-cycles"; // types -import { ICycle, TCycleGroups, TCycleLayout, TCycleView } from "@plane/types"; -// helpers -import { renderFormattedDate, findHowManyDaysLeft, renderFormattedDateWithoutYear } from "helpers/date-time.helper"; -import { truncateText } from "helpers/string.helper"; -import { renderEmoji } from "helpers/emoji.helper"; -// constants -import { WORKSPACE_ACTIVE_CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle"; +import { ICycle } from "@plane/types"; export type ActiveCycleInfoProps = { cycle: ICycle; @@ -25,234 +18,16 @@ export type ActiveCycleInfoProps = { export const ActiveCycleInfo: FC = (props) => { const { cycle, workspaceSlug, projectId } = props; - // local storage - const { setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); - const { setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); - - const cycleIssues = cycle.issues ?? []; - - const handleCurrentLayout = useCallback( - (_layout: TCycleLayout) => { - setCycleLayout(_layout); - }, - [setCycleLayout] - ); - - const handleCurrentView = useCallback( - (_view: TCycleView) => { - setCycleTab(_view); - if (_view === "draft") handleCurrentLayout("list"); - }, - [handleCurrentLayout, setCycleTab] - ); - - const groupedIssues: any = { - completed: cycle.completed_issues, - started: cycle.started_issues, - unstarted: cycle.unstarted_issues, - backlog: cycle.backlog_issues, - }; - - const progressIndicatorData = WORKSPACE_ACTIVE_CYCLE_STATE_GROUPS_DETAILS.map((group, index) => ({ - id: index, - name: group.title, - value: cycle.total_issues > 0 ? (cycle[group.key as keyof ICycle] as number) : 0, - color: group.color, - })); - - const cuurentCycle = cycle.status.toLowerCase() as TCycleGroups; - - const daysLeft = findHowManyDaysLeft(cycle.end_date ?? new Date()); return ( <> -
- {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} - ) : ( - - {cycle.owned_by.display_name.charAt(0)} - - )} - {cycle.owned_by.display_name} -
-
-
- - - -
-
+
-
-
-

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) => ( - <> - {groupedIssues[group] > 0 && ( -
-
- - {group} -
- {`: ${groupedIssues[group]} ${groupedIssues[group] > 1 ? "Issues" : "Issue"}`} -
- )} - - ))} - {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-cycles/card.tsx b/web/components/cycles/active-cycles/card.tsx new file mode 100644 index 0000000000..ef7d8dec2c --- /dev/null +++ b/web/components/cycles/active-cycles/card.tsx @@ -0,0 +1,35 @@ +import { FC } from "react"; +// components +import { + ActiveCyclesProjectTitle, + ActiveCycleHeader, + ActiveCycleProgress, + ActiveCycleProductivity, + ActiveCyclePriorityIssues, +} from "components/cycles/active-cycles"; +// types +import { ICycle } from "@plane/types"; + +export type ActiveCycleInfoCardProps = { + cycle: ICycle; + workspaceSlug: string; + projectId: string; +}; + +export const ActiveCycleInfoCard: FC = (props) => { + const { cycle, workspaceSlug, projectId } = props; + + return ( + <> + +
+ +
+ + + +
+
+ + ); +}; diff --git a/web/components/cycles/active-cycles/header.tsx b/web/components/cycles/active-cycles/header.tsx new file mode 100644 index 0000000000..e09c0aa2df --- /dev/null +++ b/web/components/cycles/active-cycles/header.tsx @@ -0,0 +1,97 @@ +import { FC, useCallback } from "react"; +import Link from "next/link"; +// hooks +import useLocalStorage from "hooks/use-local-storage"; +// ui +import { Tooltip, Button, CycleGroupIcon } from "@plane/ui"; +// types +import { ICycle, TCycleGroups, TCycleLayout, TCycleView } from "@plane/types"; +// helpers +import { truncateText } from "helpers/string.helper"; +import { renderFormattedDate, findHowManyDaysLeft } from "helpers/date-time.helper"; + +export type ActiveCycleHeaderProps = { + cycle: ICycle; + workspaceSlug: string; + projectId: string; +}; + +export const ActiveCycleHeader: FC = (props) => { + const { cycle, workspaceSlug, projectId } = props; + // local storage + const { setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); + const { setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); + + const handleCurrentLayout = useCallback( + (_layout: TCycleLayout) => { + setCycleLayout(_layout); + }, + [setCycleLayout] + ); + + const handleCurrentView = useCallback( + (_view: TCycleView) => { + setCycleTab(_view); + if (_view === "draft") handleCurrentLayout("list"); + }, + [handleCurrentLayout, setCycleTab] + ); + + const daysLeft = findHowManyDaysLeft(cycle.end_date ?? new Date()); + const currentCycleStatus = cycle.status.toLocaleLowerCase() as TCycleGroups; + + return ( +
+
+ + +

{truncateText(cycle.name, 70)}

+
+ + + {`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`} + + +
+
+ + + Lead: +
+ {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( + {cycle.owned_by.display_name} + ) : ( + + {cycle.owned_by.display_name.charAt(0)} + + )} + {cycle.owned_by.display_name} +
+
+
+ + + +
+
+ ); +}; diff --git a/web/components/cycles/active-cycles/index.ts b/web/components/cycles/active-cycles/index.ts new file mode 100644 index 0000000000..e703d8df03 --- /dev/null +++ b/web/components/cycles/active-cycles/index.ts @@ -0,0 +1,7 @@ +export * from "./header"; +export * from "./progress"; +export * from "./project-title"; +export * from "./productivity"; +export * from "./priority-issues"; +export * from "./card"; +export * from "./list-page"; diff --git a/web/components/cycles/active-cycles/list-page.tsx b/web/components/cycles/active-cycles/list-page.tsx new file mode 100644 index 0000000000..a39f8ea9a2 --- /dev/null +++ b/web/components/cycles/active-cycles/list-page.tsx @@ -0,0 +1,54 @@ +import { FC, useEffect } from "react"; +import useSWR from "swr"; +// components +import { ActiveCycleInfoCard } from "components/cycles/active-cycles"; +// constants +import { WORKSPACE_ACTIVE_CYCLES_LIST } from "constants/fetch-keys"; +// services +import { CycleService } from "services/cycle.service"; +const cycleService = new CycleService(); +// ui +import { Spinner } from "@plane/ui"; + +export type ActiveCyclesListPageProps = { + workspaceSlug: string; + cursor: string; + perPage: number; + updateTotalPages: (count: number) => void; + updateResultsCount: (count: number) => void; +}; + +export const ActiveCyclesListPage: FC = (props) => { + const { workspaceSlug, cursor, perPage, updateTotalPages, updateResultsCount } = props; + + // fetching active cycles in workspace + const { data: workspaceActiveCycles } = useSWR( + workspaceSlug && cursor ? WORKSPACE_ACTIVE_CYCLES_LIST(workspaceSlug as string, cursor, `${perPage}`) : null, + workspaceSlug && cursor ? () => cycleService.workspaceActiveCycles(workspaceSlug.toString(), cursor, perPage) : null + ); + + useEffect(() => { + if (workspaceActiveCycles) { + updateTotalPages(workspaceActiveCycles.total_pages); + updateResultsCount(workspaceActiveCycles.results.length); + } + }, [updateTotalPages, updateResultsCount, workspaceActiveCycles]); + + if (!workspaceActiveCycles) { + return ( +
+ +
+ ); + } + + return ( + <> + {workspaceActiveCycles.results.map((cycle: any) => ( +
+ +
+ ))} + + ); +}; diff --git a/web/components/cycles/active-cycles/priority-issues.tsx b/web/components/cycles/active-cycles/priority-issues.tsx new file mode 100644 index 0000000000..b2f05e4fc5 --- /dev/null +++ b/web/components/cycles/active-cycles/priority-issues.tsx @@ -0,0 +1,87 @@ +import { FC } from "react"; +import Link from "next/link"; +// ui +import { Tooltip, Loader, PriorityIcon } from "@plane/ui"; +// icons +import { CalendarCheck } from "lucide-react"; +// types +import { ICycle } from "@plane/types"; +// components +import { StateDropdown } from "components/dropdowns"; +// helpers +import { renderFormattedDate, renderFormattedDateWithoutYear } from "helpers/date-time.helper"; + +export type ActiveCyclePriorityIssuesProps = { + workspaceSlug: string; + projectId: string; + cycle: ICycle; +}; + +export const ActiveCyclePriorityIssues: FC = (props) => { + const { workspaceSlug, projectId, cycle } = props; + + const cycleIssues = cycle.issues ?? []; + + return ( +
+
+

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-cycles/productivity.tsx b/web/components/cycles/active-cycles/productivity.tsx new file mode 100644 index 0000000000..22363c4ab0 --- /dev/null +++ b/web/components/cycles/active-cycles/productivity.tsx @@ -0,0 +1,30 @@ +import { FC } from "react"; +// components +import ProgressChart from "components/core/sidebar/progress-chart"; +// types +import { ICycle } from "@plane/types"; + +export type ActiveCycleProductivityProps = { + cycle: ICycle; +}; + +export const ActiveCycleProductivity: FC = (props) => { + const { cycle } = props; + + return ( +
+
+

Issue Burndown

+
+ +
+ +
+
+ ); +}; diff --git a/web/components/cycles/active-cycles/progress.tsx b/web/components/cycles/active-cycles/progress.tsx new file mode 100644 index 0000000000..e5e520ad07 --- /dev/null +++ b/web/components/cycles/active-cycles/progress.tsx @@ -0,0 +1,74 @@ +import { FC } from "react"; +// ui +import { LinearProgressIndicator } from "@plane/ui"; +// types +import { ICycle } from "@plane/types"; +// constants +import { WORKSPACE_ACTIVE_CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle"; + +export type ActiveCycleProgressProps = { + cycle: ICycle; +}; + +export const ActiveCycleProgress: FC = (props) => { + const { cycle } = props; + + const progressIndicatorData = WORKSPACE_ACTIVE_CYCLE_STATE_GROUPS_DETAILS.map((group, index) => ({ + id: index, + name: group.title, + value: cycle.total_issues > 0 ? (cycle[group.key as keyof ICycle] as number) : 0, + color: group.color, + })); + + const groupedIssues: any = { + completed: cycle.completed_issues, + started: cycle.started_issues, + unstarted: cycle.unstarted_issues, + backlog: cycle.backlog_issues, + }; + + return ( +
+
+

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) => ( +
+ {groupedIssues[group] > 0 && ( +
+
+ + {group} +
+ {`: ${groupedIssues[group]} ${groupedIssues[group] > 1 ? "Issues" : "Issue"}`} +
+ )} +
+ ))} + {cycle.cancelled_issues > 0 && ( + + + {`${cycle.cancelled_issues} cancelled ${ + cycle.cancelled_issues > 1 ? "issues are" : "issue is" + } excluded from this report.`}{" "} + + + )} +
+
+
+ ); +}; diff --git a/web/components/cycles/active-cycles/project-title.tsx b/web/components/cycles/active-cycles/project-title.tsx new file mode 100644 index 0000000000..1225d24fce --- /dev/null +++ b/web/components/cycles/active-cycles/project-title.tsx @@ -0,0 +1,29 @@ +import { FC } from "react"; +// helpers +import { renderEmoji } from "helpers/emoji.helper"; +// types +import { IProjectLite } from "@plane/types"; + +export type ActiveCyclesProjectTitleProps = { + project: IProjectLite | undefined; +}; + +export const ActiveCyclesProjectTitle: FC = (props) => { + const { project } = props; + return ( +
+ {project?.emoji ? ( + + {renderEmoji(project.emoji)} + + ) : project?.icon_prop ? ( +
{renderEmoji(project.icon_prop)}
+ ) : ( + + {project?.name.charAt(0)} + + )} +

{project?.name}

+
+ ); +}; diff --git a/web/components/workspace/workspace-active-cycles-list.tsx b/web/components/workspace/workspace-active-cycles-list.tsx index 99d0bb1bab..2f6caccf02 100644 --- a/web/components/workspace/workspace-active-cycles-list.tsx +++ b/web/components/workspace/workspace-active-cycles-list.tsx @@ -1,65 +1,59 @@ -import { useEffect, useState } from "react"; -import useSWR from "swr"; +import { useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import isEqual from "lodash/isEqual"; // hooks import { useUser } from "hooks/store"; // components -import { ActiveCycleInfo } from "components/cycles"; -import { Button, Spinner } from "@plane/ui"; +import { ActiveCyclesListPage } from "components/cycles/active-cycles"; +import { Button } from "@plane/ui"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -// services -import { CycleService } from "services/cycle.service"; -const cycleService = new CycleService(); // constants -import { WORKSPACE_ACTIVE_CYCLES_LIST } from "constants/fetch-keys"; import { EUserWorkspaceRoles } from "constants/workspace"; -// types -import { ICycle } from "@plane/types"; -const per_page = 3; +const perPage = 3; export const WorkspaceActiveCyclesList = observer(() => { // state - const [cursor, setCursor] = useState(`3:0:0`); - const [allCyclesData, setAllCyclesData] = useState([]); - const [hasMoreResults, setHasMoreResults] = useState(true); + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); // workspaceActiveCycles.results.length // router const router = useRouter(); const { workspaceSlug } = router.query; - + // store const { membership: { currentWorkspaceRole }, currentUser, } = useUser(); - // fetching active cycles in workspace - 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 - ); + const activeCyclesPages = []; - 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); - } + const updateTotalPages = (count: number) => { + setTotalPages(count); }; - if (allCyclesData.length === 0 && !workspaceActiveCycles) { - return ( -
- -
+ const updateResultsCount = (count: number) => { + setResultsCount(count); + }; + + const handleLoadMore = () => { + setPageCount(pageCount + 1); + }; + + if (!workspaceSlug) { + return null; + } + + for (let i = 1; i <= pageCount; i++) { + activeCyclesPages.push( + ); } @@ -68,28 +62,22 @@ export const WorkspaceActiveCyclesList = observer(() => { "workspace-active-cycles", currentUser?.theme.theme === "light" ); + const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; return (
- {allCyclesData.length > 0 ? ( - <> - {workspaceSlug && - allCyclesData.map((cycle) => ( -
- -
- ))} + {activeCyclesPages} - {hasMoreResults && ( -
- -
- )} - - ) : ( + {pageCount < totalPages && resultsCount !== 0 && ( +
+ +
+ )} + + {resultsCount === 0 && (