diff --git a/web/components/core/sidebar/progress-chart.tsx b/web/components/core/sidebar/progress-chart.tsx
index a1c5f3a131e..68b1708fe82 100644
--- a/web/components/core/sidebar/progress-chart.tsx
+++ b/web/components/core/sidebar/progress-chart.tsx
@@ -12,6 +12,7 @@ type Props = {
startDate: string | Date;
endDate: string | Date;
totalIssues: number;
+ className?: string;
};
const styleById = {
@@ -40,7 +41,7 @@ const DashedLine = ({ series, lineGenerator, xScale, yScale }: any) =>
/>
));
-const ProgressChart: React.FC = ({ distribution, startDate, endDate, totalIssues }) => {
+const ProgressChart: React.FC = ({ distribution, startDate, endDate, totalIssues, className = "" }) => {
const chartData = Object.keys(distribution ?? []).map((key) => ({
currentDate: renderFormattedDateWithoutYear(key),
pending: distribution[key],
@@ -73,7 +74,7 @@ const ProgressChart: React.FC = ({ distribution, startDate, endDate, tota
};
return (
-
+
= observer((props) => {
+ const { workspaceSlug, projectId, cycle } = props;
+
+ const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");
+
+ const currentValue = (tab: string | null) => {
+ switch (tab) {
+ case "Priority-Issues":
+ return 0;
+ case "Assignees":
+ return 1;
+ case "Labels":
+ return 2;
+ default:
+ return 0;
+ }
+ };
+ const {
+ issues: { fetchActiveCycleIssues },
+ } = useIssues(EIssuesStoreType.CYCLE);
+
+ const { currentProjectDetails } = useProject();
+
+ const { data: activeCycleIssues } = useSWR(
+ workspaceSlug && projectId && cycle.id ? CYCLE_ISSUES_WITH_PARAMS(cycle.id, { priority: "urgent,high" }) : null,
+ workspaceSlug && projectId && cycle.id ? () => fetchActiveCycleIssues(workspaceSlug, projectId, cycle.id) : null
+ );
+
+ const cycleIssues = activeCycleIssues ?? [];
+
+ return (
+
+
{
+ switch (i) {
+ case 0:
+ return setTab("Priority-Issues");
+ case 1:
+ return setTab("Assignees");
+ case 2:
+ return setTab("Labels");
+
+ default:
+ return setTab("Priority-Issues");
+ }
+ }}
+ >
+
+
+ cn(
+ "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500",
+ {
+ "text-custom-text-300 bg-custom-background-100": selected,
+ "hover:text-custom-text-300": !selected,
+ }
+ )
+ }
+ >
+ Priority Issues
+
+
+ cn(
+ "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500",
+ {
+ "text-custom-text-300 bg-custom-background-100": selected,
+ "hover:text-custom-text-300": !selected,
+ }
+ )
+ }
+ >
+ Assignees
+
+
+ cn(
+ "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500",
+ {
+ "text-custom-text-300 bg-custom-background-100": selected,
+ "hover:text-custom-text-300": !selected,
+ }
+ )
+ }
+ >
+ Labels
+
+
+
+
+
+
+ {cycleIssues ? (
+ cycleIssues.length > 0 ? (
+ cycleIssues.map((issue: TIssue) => (
+
+
+
+
+
+
+ {currentProjectDetails?.identifier}-{issue.sequence_id}
+
+
+
+ {issue.name}
+
+
+
+
{}}
+ projectId={projectId?.toString() ?? ""}
+ disabled
+ buttonVariant="background-with-text"
+ buttonContainerClassName="cursor-pointer max-w-24"
+ showTooltip
+ />
+ {issue.target_date && (
+
+
+
+
+ {renderFormattedDateWithoutYear(issue.target_date)}
+
+
+
+ )}
+
+
+ ))
+ ) : (
+
+ There are no high priority issues present in this cycle.
+
+ )
+ ) : (
+
+
+
+
+
+ )}
+
+
+
+
+ {cycle.distribution?.assignees?.map((assignee, index) => {
+ if (assignee.assignee_id)
+ return (
+
+
+
+ {assignee.display_name}
+
+ }
+ completed={assignee.completed_issues}
+ total={assignee.total_issues}
+ />
+ );
+ else
+ return (
+
+
+
+
+ No assignee
+
+ }
+ completed={assignee.completed_issues}
+ total={assignee.total_issues}
+ />
+ );
+ })}
+
+
+
+ {cycle.distribution?.labels?.map((label, index) => (
+
+
+ {label.label_name ?? "No labels"}
+
+ }
+ completed={label.completed_issues}
+ total={label.total_issues}
+ />
+ ))}
+
+
+
+
+ );
+});
diff --git a/web/components/cycles/active-cycle/header.tsx b/web/components/cycles/active-cycle/header.tsx
new file mode 100644
index 00000000000..98ed91c1dd0
--- /dev/null
+++ b/web/components/cycles/active-cycle/header.tsx
@@ -0,0 +1,77 @@
+import { FC } from "react";
+import Link from "next/link";
+// types
+import { ICycle, TCycleGroups } from "@plane/types";
+// ui
+import { Tooltip, CycleGroupIcon, getButtonStyling, Avatar, AvatarGroup } from "@plane/ui";
+// helpers
+import { renderFormattedDate, findHowManyDaysLeft } from "@/helpers/date-time.helper";
+import { truncateText } from "@/helpers/string.helper";
+// hooks
+import { useMember } from "@/hooks/store";
+
+export type ActiveCycleHeaderProps = {
+ cycle: ICycle;
+ workspaceSlug: string;
+ projectId: string;
+};
+
+export const ActiveCycleHeader: FC = (props) => {
+ const { cycle, workspaceSlug, projectId } = props;
+ // store
+ const { getUserDetails } = useMember();
+ const cycleOwnerDetails = cycle && cycle.owned_by_id ? getUserDetails(cycle.owned_by_id) : undefined;
+
+ const daysLeft = findHowManyDaysLeft(cycle.end_date) ?? 0;
+ const currentCycleStatus = cycle.status.toLocaleLowerCase() as TCycleGroups;
+
+ const cycleAssignee = (cycle.distribution?.assignees ?? []).filter((assignee) => assignee.display_name);
+
+ return (
+
+
+
+
+ {truncateText(cycle.name, 70)}
+
+
+
+ {`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`}
+
+
+
+
+
+
+
+ {cycleAssignee.length > 0 && (
+
+
+ {cycleAssignee.map((member) => (
+
+ ))}
+
+
+ )}
+
+
+
+ View Cycle
+
+
+
+ );
+};
diff --git a/web/components/cycles/active-cycle/index.ts b/web/components/cycles/active-cycle/index.ts
index 73d5d1e98ef..d88ccc3e8b6 100644
--- a/web/components/cycles/active-cycle/index.ts
+++ b/web/components/cycles/active-cycle/index.ts
@@ -1,4 +1,8 @@
export * from "./root";
+export * from "./header";
export * from "./stats";
export * from "./upcoming-cycles-list-item";
export * from "./upcoming-cycles-list";
+export * from "./cycle-stats";
+export * from "./progress";
+export * from "./productivity";
diff --git a/web/components/cycles/active-cycle/productivity.tsx b/web/components/cycles/active-cycle/productivity.tsx
new file mode 100644
index 00000000000..59c2ac3c902
--- /dev/null
+++ b/web/components/cycles/active-cycle/productivity.tsx
@@ -0,0 +1,46 @@
+import { FC } from "react";
+// types
+import { ICycle } from "@plane/types";
+// components
+import ProgressChart from "@/components/core/sidebar/progress-chart";
+
+export type ActiveCycleProductivityProps = {
+ cycle: ICycle;
+};
+
+export const ActiveCycleProductivity: FC = (props) => {
+ const { cycle } = props;
+
+ return (
+
+
+
Issue burndown
+
+
+
+
+
+
+
+ Ideal
+
+
+
+ Current
+
+
+
{`Pending issues - ${cycle.backlog_issues + cycle.unstarted_issues + cycle.started_issues}`}
+
+
+
+
+ );
+};
diff --git a/web/components/cycles/active-cycle/progress.tsx b/web/components/cycles/active-cycle/progress.tsx
new file mode 100644
index 00000000000..dea3b496a85
--- /dev/null
+++ b/web/components/cycles/active-cycle/progress.tsx
@@ -0,0 +1,79 @@
+import { FC } from "react";
+// types
+import { ICycle } from "@plane/types";
+// ui
+import { LinearProgressIndicator } from "@plane/ui";
+// constants
+import { CYCLE_STATE_GROUPS_DETAILS } from "@/constants/cycle";
+
+export type ActiveCycleProgressProps = {
+ cycle: ICycle;
+};
+
+export const ActiveCycleProgress: FC = (props) => {
+ const { cycle } = props;
+
+ const progressIndicatorData = 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-cycle/root.tsx b/web/components/cycles/active-cycle/root.tsx
index 83acd152177..bd2c3b61328 100644
--- a/web/components/cycles/active-cycle/root.tsx
+++ b/web/components/cycles/active-cycle/root.tsx
@@ -1,48 +1,20 @@
-import { MouseEvent } from "react";
import { observer } from "mobx-react-lite";
-import Link from "next/link";
import useSWR from "swr";
-// hooks
-import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react";
-import { ICycle, TCycleGroups } from "@plane/types";
-import {
- AvatarGroup,
- Loader,
- Tooltip,
- LinearProgressIndicator,
- LayersIcon,
- StateGroupIcon,
- PriorityIcon,
- Avatar,
- CycleGroupIcon,
- setPromiseToast,
- getButtonStyling,
-} from "@plane/ui";
-import { SingleProgressStats } from "@/components/core";
// ui
+import { Loader } from "@plane/ui";
// components
-import ProgressChart from "@/components/core/sidebar/progress-chart";
-import { ActiveCycleProgressStats, UpcomingCyclesList } from "@/components/cycles";
-import { StateDropdown } from "@/components/dropdowns";
+import {
+ ActiveCycleHeader,
+ ActiveCycleProductivity,
+ ActiveCycleProgress,
+ ActiveCycleStats,
+ UpcomingCyclesList,
+} from "@/components/cycles";
import { EmptyState } from "@/components/empty-state";
-// icons
-// helpers
-// types
// constants
-import { CYCLE_STATE_GROUPS_DETAILS } from "@/constants/cycle";
import { EmptyStateType } from "@/constants/empty-state";
-import { CYCLE_ISSUES_WITH_PARAMS } from "@/constants/fetch-keys";
-import { EIssuesStoreType } from "@/constants/issue";
-import { cn } from "@/helpers/common.helper";
-import {
- renderFormattedDate,
- findHowManyDaysLeft,
- renderFormattedDateWithoutYear,
- getDate,
-} from "@/helpers/date-time.helper";
-import { truncateText } from "@/helpers/string.helper";
-import { useCycle, useCycleFilter, useIssues, useMember, useProject } from "@/hooks/store";
-import { usePlatformOS } from "@/hooks/use-platform-os";
+// hooks
+import { useCycle, useCycleFilter } from "@/hooks/store";
interface IActiveCycleDetails {
workspaceSlug: string;
@@ -52,41 +24,24 @@ interface IActiveCycleDetails {
export const ActiveCycleRoot: React.FC = observer((props) => {
// props
const { workspaceSlug, projectId } = props;
- // hooks
- const { isMobile } = usePlatformOS();
// store hooks
- const {
- issues: { fetchActiveCycleIssues },
- } = useIssues(EIssuesStoreType.CYCLE);
- const {
- currentProjectActiveCycleId,
- currentProjectUpcomingCycleIds,
- fetchActiveCycle,
- getActiveCycleById,
- addCycleToFavorites,
- removeCycleFromFavorites,
- } = useCycle();
- const { currentProjectDetails } = useProject();
- const { getUserDetails } = useMember();
+ const { fetchActiveCycle, currentProjectActiveCycleId, currentProjectUpcomingCycleIds, getActiveCycleById } =
+ useCycle();
// cycle filters hook
const { updateDisplayFilters } = useCycleFilter();
// derived values
const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
- const cycleOwnerDetails = activeCycle ? getUserDetails(activeCycle.owned_by_id) : undefined;
// fetch active cycle details
const { isLoading } = useSWR(
workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null,
workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null
);
- // fetch active cycle issues
- const { data: activeCycleIssues } = useSWR(
- workspaceSlug && projectId && currentProjectActiveCycleId
- ? CYCLE_ISSUES_WITH_PARAMS(currentProjectActiveCycleId, { priority: "urgent,high" })
- : null,
- workspaceSlug && projectId && currentProjectActiveCycleId
- ? () => fetchActiveCycleIssues(workspaceSlug, projectId, currentProjectActiveCycleId)
- : null
- );
+
+ const handleEmptyStateAction = () =>
+ updateDisplayFilters(projectId, {
+ active_tab: "all",
+ });
+
// show loader if active cycle is loading
if (!activeCycle && isLoading)
return (
@@ -110,310 +65,28 @@ export const ActiveCycleRoot: React.FC = observer((props) =
Create new cycles to find them here or check
{"'"}All{"'"} cycles tab to see all cycles or{" "}
-
-
+
>
);
}
- const endDate = getDate(activeCycle.end_date);
- const startDate = getDate(activeCycle.start_date);
- const daysLeft = findHowManyDaysLeft(activeCycle.end_date) ?? 0;
- const cycleStatus = activeCycle.status.toLowerCase() as TCycleGroups;
-
- const groupedIssues: any = {
- backlog: activeCycle.backlog_issues,
- unstarted: activeCycle.unstarted_issues,
- started: activeCycle.started_issues,
- completed: activeCycle.completed_issues,
- cancelled: activeCycle.cancelled_issues,
- };
-
- const handleAddToFavorites = (e: MouseEvent) => {
- e.preventDefault();
- if (!workspaceSlug || !projectId) return;
-
- const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id);
-
- setPromiseToast(addToFavoritePromise, {
- loading: "Adding cycle to favorites...",
- success: {
- title: "Success!",
- message: () => "Cycle added to favorites.",
- },
- error: {
- title: "Error!",
- message: () => "Couldn't add the cycle to favorites. Please try again.",
- },
- });
- };
-
- const handleRemoveFromFavorites = (e: MouseEvent) => {
- e.preventDefault();
- if (!workspaceSlug || !projectId) return;
-
- const removeFromFavoritePromise = removeCycleFromFavorites(
- workspaceSlug?.toString(),
- projectId.toString(),
- activeCycle.id
- );
-
- setPromiseToast(removeFromFavoritePromise, {
- loading: "Removing cycle from favorites...",
- success: {
- title: "Success!",
- message: () => "Cycle removed from favorites.",
- },
- error: {
- title: "Error!",
- message: () => "Couldn't remove the cycle from favorites. Please try again.",
- },
- });
- };
-
- const progressIndicatorData = CYCLE_STATE_GROUPS_DETAILS.map((group, index) => ({
- id: index,
- name: group.title,
- value:
- activeCycle.total_issues > 0
- ? ((activeCycle[group.key as keyof ICycle] as number) / activeCycle.total_issues) * 100
- : 0,
- color: group.color,
- }));
-
return (
-
-
-
-
-
-
-
-
-
-
-
- {truncateText(activeCycle.name, 70)}
-
-
-
-
- {`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`}
-
- {activeCycle.is_favorite ? (
- {
- handleRemoveFromFavorites(e);
- }}
- >
-
-
- ) : (
- {
- handleAddToFavorites(e);
- }}
- >
-
-
- )}
-
-
-
-
-
-
- {renderFormattedDate(startDate)}
-
-
-
-
- {renderFormattedDate(endDate)}
-
-
-
-
-
-
-
{cycleOwnerDetails?.display_name}
-
-
- {activeCycle.assignee_ids.length > 0 && (
-
-
- {activeCycle.assignee_ids.map((assignee_id) => {
- const member = getUserDetails(assignee_id);
- return ;
- })}
-
-
- )}
-
-
-
-
-
- {activeCycle.total_issues} issues
-
-
-
- {activeCycle.completed_issues} issues
-
-
-
-
- View cycle
-
-
-
-
-
-
-
-
- Progress
-
-
-
- {Object.keys(groupedIssues).map((group, index) => (
-
-
- {group}
-
- }
- completed={groupedIssues[group]}
- total={activeCycle.total_issues}
- />
- ))}
-
-
-
-
-
-
-
-
-
High priority issues
-
- {activeCycleIssues ? (
- activeCycleIssues.length > 0 ? (
- activeCycleIssues.map((issue) => (
-
-
-
-
-
-
- {currentProjectDetails?.identifier}-{issue.sequence_id}
-
-
-
- {truncateText(issue.name, 30)}
-
-
-
-
{}}
- projectId={projectId}
- disabled
- buttonVariant="background-with-text"
- />
- {issue.target_date && (
-
-
-
- {renderFormattedDateWithoutYear(issue.target_date)}
-
-
- )}
-
-
- ))
- ) : (
-
- There are no high priority issues present in this cycle.
-
- )
- ) : (
-
-
-
-
-
- )}
-
-
-
-
-
-
-
- Ideal
-
-
-
- Current
-
-
-
-
-
-
-
- Pending issues-{" "}
- {activeCycle.total_issues - (activeCycle.completed_issues + activeCycle.cancelled_issues)}
-
-
-
-
+ <>
+
-
+ {currentProjectUpcomingCycleIds &&
}
+ >
);
});
diff --git a/web/components/cycles/active-cycle/upcoming-cycles-list.tsx b/web/components/cycles/active-cycle/upcoming-cycles-list.tsx
index c2d8b2388fe..f4156f34186 100644
--- a/web/components/cycles/active-cycle/upcoming-cycles-list.tsx
+++ b/web/components/cycles/active-cycle/upcoming-cycles-list.tsx
@@ -1,10 +1,16 @@
+import { FC } from "react";
import { observer } from "mobx-react";
-// hooks
+// components
import { UpcomingCycleListItem } from "@/components/cycles";
+// hooks
import { useCycle } from "@/hooks/store";
-// components
-export const UpcomingCyclesList = observer(() => {
+type Props = {
+ handleEmptyStateAction: () => void;
+};
+
+export const UpcomingCyclesList: FC
= observer((props) => {
+ const { handleEmptyStateAction } = props;
// store hooks
const { currentProjectUpcomingCycleIds } = useCycle();
@@ -12,14 +18,30 @@ export const UpcomingCyclesList = observer(() => {
return (
-
- Upcoming cycles
-
-
- {currentProjectUpcomingCycleIds.map((cycleId) => (
-
- ))}
+
+ Next cycles
+ {currentProjectUpcomingCycleIds.length > 0 ? (
+
+ {currentProjectUpcomingCycleIds.map((cycleId) => (
+
+ ))}
+
+ ) : (
+
+
+
No upcoming cycles
+
+ Create new cycles to find them here or check
+
+ {"'"}All{"'"} cycles tab to see all cycles or{" "}
+
+ click here
+
+
+
+
+ )}
);
});