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
This commit is contained in:
sriram veeraghanta
2024-01-28 16:46:32 +05:30
committed by GitHub
parent 74069425ea
commit 910ac659fa
10 changed files with 471 additions and 295 deletions

View File

@@ -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<ActiveCycleInfoProps> = (props) => {
const { cycle, workspaceSlug, projectId } = props;
// local storage
const { setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active");
const { setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("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 (
<>
<div className="flex items-center gap-1.5 px-3 py-1.5">
{cycle.project_detail.emoji ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
{renderEmoji(cycle.project_detail.emoji)}
</span>
) : cycle.project_detail.icon_prop ? (
<div className="grid h-7 w-7 flex-shrink-0 place-items-center">
{renderEmoji(cycle.project_detail.icon_prop)}
</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{cycle.project_detail?.name.charAt(0)}
</span>
)}
<h2 className="text-xl font-semibold">{cycle.project_detail.name}</h2>
</div>
<ActiveCyclesProjectTitle project={cycle.project_detail} />
<div className="flex flex-col gap-2 rounded border border-custom-border-200">
<div className="flex items-center justify-between px-3 pt-3 pb-1">
<div className="flex items-center gap-2 cursor-default">
<CycleGroupIcon cycleGroup={cuurentCycle} className="h-4 w-4" />
<Tooltip tooltipContent={cycle.name} position="top-left">
<h3 className="break-words text-lg font-medium">{truncateText(cycle.name, 70)}</h3>
</Tooltip>
<Tooltip
tooltipContent={`Start date: ${renderFormattedDate(
cycle.start_date ?? ""
)} Due Date: ${renderFormattedDate(cycle.end_date ?? "")}`}
position="top-left"
>
<span className="flex gap-1 whitespace-nowrap rounded-sm text-sm px-3 py-0.5 bg-amber-500/10 text-amber-500">
{`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`}
</span>
</Tooltip>
</div>
<div className="flex items-center gap-2.5">
<span className="rounded-sm text-sm px-3 py-1 bg-custom-background-80">
<span className="flex gap-2 text-sm whitespace-nowrap font-medium">
<span>Lead:</span>
<div className="flex items-center gap-1.5">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<img
src={cycle.owned_by.avatar}
height={18}
width={18}
className="rounded-full"
alt={cycle.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize">
{cycle.owned_by.display_name.charAt(0)}
</span>
)}
<span>{cycle.owned_by.display_name}</span>
</div>
</span>
</span>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles`}>
<Button
variant="primary"
size="sm"
onClick={() => {
handleCurrentView("active");
}}
>
View Cycle
</Button>
</Link>
</div>
</div>
<ActiveCycleHeader cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
<div className="flex flex-col gap-4 px-3 pt-2 min-h-52 border-r-0 border-t border-custom-border-300 lg:border-r">
<div className="flex items-center justify-between gap-4">
<h3 className="text-xl font-medium">Progress</h3>
<span className="flex gap-1 text-sm whitespace-nowrap rounded-sm px-3 py-1 ">
{`${cycle.completed_issues + cycle.cancelled_issues}/${cycle.total_issues - cycle.cancelled_issues} ${
cycle.completed_issues + cycle.cancelled_issues > 1 ? "Issues" : "Issue"
} closed`}
</span>
</div>
<LinearProgressIndicator data={progressIndicatorData} />
<div>
<div className="flex flex-col gap-2">
{Object.keys(groupedIssues).map((group, index) => (
<>
{groupedIssues[group] > 0 && (
<div className="flex items-center justify-start gap-2 text-sm">
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full"
style={{
backgroundColor: WORKSPACE_ACTIVE_CYCLE_STATE_GROUPS_DETAILS[index].color,
}}
/>
<span className="capitalize font-medium w-16">{group}</span>
</div>
<span>{`: ${groupedIssues[group]} ${groupedIssues[group] > 1 ? "Issues" : "Issue"}`}</span>
</div>
)}
</>
))}
{cycle.cancelled_issues > 0 && (
<span className="flex items-center gap-2 text-sm text-custom-text-300">
<span>
{`${cycle.cancelled_issues} cancelled ${
cycle.cancelled_issues > 1 ? "issues are" : "issue is"
} excluded from this report.`}{" "}
</span>
</span>
)}
</div>
</div>
</div>
<div className="flex flex-col gap-4 px-3 pt-2 min-h-52 border-r-0 border-t border-custom-border-300 lg:border-r">
<div className="flex items-center justify-between gap-4">
<h3 className="text-xl font-medium">Issue Burndown</h3>
</div>
<div className="relative ">
<ProgressChart
distribution={cycle.distribution?.completion_chart ?? {}}
startDate={cycle.start_date ?? ""}
endDate={cycle.end_date ?? ""}
totalIssues={cycle.total_issues}
/>
</div>
</div>
<div className="flex flex-col gap-4 px-3 pt-2 min-h-52 overflow-hidden col-span-1 lg:col-span-2 xl:col-span-1 border-t border-custom-border-300">
<div className="flex items-center justify-between gap-4">
<h3 className="text-xl font-medium">Priority</h3>
</div>
<div className="flex flex-col gap-4 h-full w-full max-h-40 overflow-y-auto pb-3">
{cycleIssues ? (
cycleIssues.length > 0 ? (
cycleIssues.map((issue: any) => (
<Link
key={issue.id}
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
className="flex cursor-pointer items-center justify-between gap-2 rounded-md border border-custom-border-200 px-3 py-1.5"
>
<div className="flex items-center gap-1.5 flex-grow w-full truncate">
<PriorityIcon priority={issue.priority} withContainer size={12} />
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${cycle.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{cycle.project_detail?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-[0.825rem] text-custom-text-100 truncate">{issue.name}</span>
</Tooltip>
</div>
<div className="flex items-center gap-1.5 flex-shrink-0">
<StateDropdown
value={issue.state_id ?? undefined}
onChange={() => {}}
projectId={projectId?.toString() ?? ""}
disabled={true}
buttonVariant="background-with-text"
/>
{issue.target_date && (
<Tooltip tooltipHeading="Target Date" tooltipContent={renderFormattedDate(issue.target_date)}>
<div className="h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80 cursor-not-allowed">
<CalendarCheck className="h-3 w-3 flex-shrink-0" />
<span className="text-xs">{renderFormattedDateWithoutYear(issue.target_date)}</span>
</div>
</Tooltip>
)}
</div>
</Link>
))
) : (
<div className="flex items-center justify-center h-full text-sm text-custom-text-200">
<span>There are no high priority issues present in this cycle.</span>
</div>
)
) : (
<Loader className="space-y-3">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</Loader>
)}
</div>
</div>
<ActiveCycleProgress cycle={cycle} />
<ActiveCycleProductivity cycle={cycle} />
<ActiveCyclePriorityIssues cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
</div>
</>

View File

@@ -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<ActiveCycleInfoCardProps> = (props) => {
const { cycle, workspaceSlug, projectId } = props;
return (
<>
<ActiveCyclesProjectTitle project={cycle.project_detail} />
<div className="flex flex-col gap-2 rounded border border-custom-border-200">
<ActiveCycleHeader cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
<ActiveCycleProgress cycle={cycle} />
<ActiveCycleProductivity cycle={cycle} />
<ActiveCyclePriorityIssues cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
</div>
</>
);
};

View File

@@ -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<ActiveCycleHeaderProps> = (props) => {
const { cycle, workspaceSlug, projectId } = props;
// local storage
const { setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active");
const { setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("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 (
<div className="flex items-center justify-between px-3 pt-3 pb-1">
<div className="flex items-center gap-2 cursor-default">
<CycleGroupIcon cycleGroup={currentCycleStatus} className="h-4 w-4" />
<Tooltip tooltipContent={cycle.name} position="top-left">
<h3 className="break-words text-lg font-medium">{truncateText(cycle.name, 70)}</h3>
</Tooltip>
<Tooltip
tooltipContent={`Start date: ${renderFormattedDate(cycle.start_date ?? "")} Due Date: ${renderFormattedDate(
cycle.end_date ?? ""
)}`}
position="top-left"
>
<span className="flex gap-1 whitespace-nowrap rounded-sm text-sm px-3 py-0.5 bg-amber-500/10 text-amber-500">
{`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`}
</span>
</Tooltip>
</div>
<div className="flex items-center gap-2.5">
<span className="rounded-sm text-sm px-3 py-1 bg-custom-background-80">
<span className="flex gap-2 text-sm whitespace-nowrap font-medium">
<span>Lead:</span>
<div className="flex items-center gap-1.5">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<img
src={cycle.owned_by.avatar}
height={18}
width={18}
className="rounded-full"
alt={cycle.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize">
{cycle.owned_by.display_name.charAt(0)}
</span>
)}
<span>{cycle.owned_by.display_name}</span>
</div>
</span>
</span>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles`}>
<Button
variant="primary"
size="sm"
onClick={() => {
handleCurrentView("active");
}}
>
View Cycle
</Button>
</Link>
</div>
</div>
);
};

View File

@@ -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";

View File

@@ -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<ActiveCyclesListPageProps> = (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 (
<div className="flex items-center justify-center h-full w-full">
<Spinner />
</div>
);
}
return (
<>
{workspaceActiveCycles.results.map((cycle: any) => (
<div key={cycle.id} className="px-5 py-5">
<ActiveCycleInfoCard workspaceSlug={workspaceSlug?.toString()} projectId={cycle.project} cycle={cycle} />
</div>
))}
</>
);
};

View File

@@ -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<ActiveCyclePriorityIssuesProps> = (props) => {
const { workspaceSlug, projectId, cycle } = props;
const cycleIssues = cycle.issues ?? [];
return (
<div className="flex flex-col gap-4 px-3 pt-2 min-h-52 overflow-hidden col-span-1 lg:col-span-2 xl:col-span-1 border-t border-custom-border-300">
<div className="flex items-center justify-between gap-4">
<h3 className="text-lg font-medium">Priority</h3>
</div>
<div className="flex flex-col gap-4 h-full w-full max-h-40 overflow-y-auto pb-3">
{cycleIssues ? (
cycleIssues.length > 0 ? (
cycleIssues.map((issue: any) => (
<Link
key={issue.id}
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
className="flex cursor-pointer items-center justify-between gap-2 rounded-md border border-custom-border-200 px-3 py-1.5"
>
<div className="flex items-center gap-1.5 flex-grow w-full truncate">
<PriorityIcon priority={issue.priority} withContainer size={12} />
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${cycle.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{cycle.project_detail?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-[0.825rem] text-custom-text-100 truncate">{issue.name}</span>
</Tooltip>
</div>
<div className="flex items-center gap-1.5 flex-shrink-0">
<StateDropdown
value={issue.state_id ?? undefined}
onChange={() => {}}
projectId={projectId?.toString() ?? ""}
disabled={true}
buttonVariant="background-with-text"
/>
{issue.target_date && (
<Tooltip tooltipHeading="Target Date" tooltipContent={renderFormattedDate(issue.target_date)}>
<div className="h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80 cursor-not-allowed">
<CalendarCheck className="h-3 w-3 flex-shrink-0" />
<span className="text-xs">{renderFormattedDateWithoutYear(issue.target_date)}</span>
</div>
</Tooltip>
)}
</div>
</Link>
))
) : (
<div className="flex items-center justify-center h-full text-sm text-custom-text-200">
<span>There are no high priority issues present in this cycle.</span>
</div>
)
) : (
<Loader className="space-y-3">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</Loader>
)}
</div>
</div>
);
};

View File

@@ -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<ActiveCycleProductivityProps> = (props) => {
const { cycle } = props;
return (
<div className="flex flex-col gap-4 px-3 pt-2 min-h-52 border-r-0 border-t border-custom-border-300 lg:border-r">
<div className="flex items-center justify-between gap-4">
<h3 className="text-lg font-medium">Issue Burndown</h3>
</div>
<div className="relative ">
<ProgressChart
distribution={cycle.distribution?.completion_chart ?? {}}
startDate={cycle.start_date ?? ""}
endDate={cycle.end_date ?? ""}
totalIssues={cycle.total_issues}
/>
</div>
</div>
);
};

View File

@@ -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<ActiveCycleProgressProps> = (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 (
<div className="flex flex-col gap-4 px-3 pt-2 min-h-52 border-r-0 border-t border-custom-border-300 lg:border-r">
<div className="flex items-center justify-between gap-4">
<h3 className="text-lg font-medium">Progress</h3>
<span className="flex gap-1 text-sm whitespace-nowrap rounded-sm px-3 py-1 ">
{`${cycle.completed_issues + cycle.cancelled_issues}/${cycle.total_issues - cycle.cancelled_issues} ${
cycle.completed_issues + cycle.cancelled_issues > 1 ? "Issues" : "Issue"
} closed`}
</span>
</div>
<LinearProgressIndicator data={progressIndicatorData} />
<div>
<div className="flex flex-col gap-2">
{Object.keys(groupedIssues).map((group, index) => (
<div key={index}>
{groupedIssues[group] > 0 && (
<div className="flex items-center justify-start gap-2 text-sm">
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full"
style={{
backgroundColor: WORKSPACE_ACTIVE_CYCLE_STATE_GROUPS_DETAILS[index].color,
}}
/>
<span className="capitalize font-medium w-16">{group}</span>
</div>
<span>{`: ${groupedIssues[group]} ${groupedIssues[group] > 1 ? "Issues" : "Issue"}`}</span>
</div>
)}
</div>
))}
{cycle.cancelled_issues > 0 && (
<span className="flex items-center gap-2 text-sm text-custom-text-300">
<span>
{`${cycle.cancelled_issues} cancelled ${
cycle.cancelled_issues > 1 ? "issues are" : "issue is"
} excluded from this report.`}{" "}
</span>
</span>
)}
</div>
</div>
</div>
);
};

View File

@@ -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<ActiveCyclesProjectTitleProps> = (props) => {
const { project } = props;
return (
<div className="flex items-center gap-1.5 px-3 py-1.5">
{project?.emoji ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
{renderEmoji(project.emoji)}
</span>
) : project?.icon_prop ? (
<div className="grid h-7 w-7 flex-shrink-0 place-items-center">{renderEmoji(project.icon_prop)}</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{project?.name.charAt(0)}
</span>
)}
<h2 className="text-xl font-semibold">{project?.name}</h2>
</div>
);
};

View File

@@ -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<string | undefined>(`3:0:0`);
const [allCyclesData, setAllCyclesData] = useState<ICycle[]>([]);
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 (
<div className="flex items-center justify-center h-full w-full">
<Spinner />
</div>
const updateResultsCount = (count: number) => {
setResultsCount(count);
};
const handleLoadMore = () => {
setPageCount(pageCount + 1);
};
if (!workspaceSlug) {
return null;
}
for (let i = 1; i <= pageCount; i++) {
activeCyclesPages.push(
<ActiveCyclesListPage
cursor={`${perPage}:${i - 1}:0`}
perPage={perPage}
workspaceSlug={workspaceSlug.toString()}
updateTotalPages={updateTotalPages}
updateResultsCount={updateResultsCount}
key={i}
/>
);
}
@@ -68,28 +62,22 @@ export const WorkspaceActiveCyclesList = observer(() => {
"workspace-active-cycles",
currentUser?.theme.theme === "light"
);
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
return (
<div className="h-full w-full">
{allCyclesData.length > 0 ? (
<>
{workspaceSlug &&
allCyclesData.map((cycle) => (
<div key={cycle.id} className="px-5 py-5">
<ActiveCycleInfo workspaceSlug={workspaceSlug?.toString()} projectId={cycle.project} cycle={cycle} />
</div>
))}
{activeCyclesPages}
{hasMoreResults && (
<div className="flex items-center justify-center gap-4 text-xs w-full py-5">
<Button variant="outline-primary" size="sm" onClick={handleLoadMore}>
{isLoading ? "Loading..." : "Load More"}
</Button>
</div>
)}
</>
) : (
{pageCount < totalPages && resultsCount !== 0 && (
<div className="flex items-center justify-center gap-4 text-xs w-full py-5">
<Button variant="outline-primary" size="sm" onClick={handleLoadMore}>
Load More
</Button>
</div>
)}
{resultsCount === 0 && (
<EmptyState
image={EmptyStateImagePath}
title="No active cycles"