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 ( + +
+ User +
+ 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 ? ( - - ) : ( - - )} - -
- -
-
- - {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{" "} + +

+
+
+ )}
); });