From b420887a9952d944c2451c885a37af138518e202 Mon Sep 17 00:00:00 2001 From: gakshita Date: Thu, 26 Sep 2024 16:43:12 +0530 Subject: [PATCH 1/3] fix: progress chart code splitting --- packages/types/src/cycle/cycle.d.ts | 20 ++++- packages/ui/src/icons/done-icon.tsx | 22 ++++++ packages/ui/src/icons/in-progress-icon.tsx | 17 +++++ packages/ui/src/icons/index.ts | 3 + packages/ui/src/icons/planned-icon.tsx | 40 ++++++++++ packages/ui/src/loader.tsx | 5 +- .../progress/circular-progress-indicator.tsx | 8 +- .../cycles/(detail)/[cycleId]/page.tsx | 4 +- web/helpers/cycle.helper.ts | 61 +++++++++++++++- web/helpers/date-time.helper.ts | 73 +++++++++++++++++++ 10 files changed, 241 insertions(+), 12 deletions(-) create mode 100644 packages/ui/src/icons/done-icon.tsx create mode 100644 packages/ui/src/icons/in-progress-icon.tsx create mode 100644 packages/ui/src/icons/planned-icon.tsx diff --git a/packages/types/src/cycle/cycle.d.ts b/packages/types/src/cycle/cycle.d.ts index a2b3814ed3c..d8a6915a92b 100644 --- a/packages/types/src/cycle/cycle.d.ts +++ b/packages/types/src/cycle/cycle.d.ts @@ -1,4 +1,4 @@ -import type {TIssue, IIssueFilterOptions} from "@plane/types"; +import type { TIssue, IIssueFilterOptions } from "@plane/types"; export type TCycleGroups = "current" | "upcoming" | "completed" | "draft"; @@ -43,6 +43,18 @@ export type TCycleEstimateDistribution = { completion_chart: TCycleCompletionChartDistribution; labels: (TCycleLabelsDistribution & TCycleEstimateDistributionBase)[]; }; +export type TCycleProgress = { + date: string; + started: number; + pending: number; + ideal: number | null; + scope: number; + completed: number; + actual: number; + unstarted: number; + backlog: number; + cancelled: number; +}; export type TProgressSnapshot = { total_issues: number; @@ -90,6 +102,7 @@ export interface ICycle extends TProgressSnapshot { }; workspace_id: string; project_detail: IProjectDetails; + progress: any[]; } export interface CycleIssueResponse { @@ -107,7 +120,7 @@ export interface CycleIssueResponse { } export type SelectCycleType = - | (ICycle & {actionType: "edit" | "delete" | "create-issue"}) + | (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined; export type CycleDateCheckData = { @@ -116,4 +129,5 @@ export type CycleDateCheckData = { cycle_id?: string; }; -export type TCyclePlotType = "burndown" | "points"; +export type TCycleEstimateType = "issues" | "points"; +export type TCyclePlotType = "burndown" | "burnup"; diff --git a/packages/ui/src/icons/done-icon.tsx b/packages/ui/src/icons/done-icon.tsx new file mode 100644 index 00000000000..f8f65aa5ff4 --- /dev/null +++ b/packages/ui/src/icons/done-icon.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const DoneState: React.FC = ({ width = "10", height = "11", className, color }) => ( + + + + +); diff --git a/packages/ui/src/icons/in-progress-icon.tsx b/packages/ui/src/icons/in-progress-icon.tsx new file mode 100644 index 00000000000..085f9d74db4 --- /dev/null +++ b/packages/ui/src/icons/in-progress-icon.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const InProgressState: React.FC = ({ width = "10", height = "11", className, color }) => ( + + + + +); diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts index 660768845b3..69436c2e836 100644 --- a/packages/ui/src/icons/index.ts +++ b/packages/ui/src/icons/index.ts @@ -28,3 +28,6 @@ export * from "./dropdown-icon"; export * from "./intake"; export * from "./user-activity-icon"; export * from "./favorite-folder-icon"; +export * from "./planned-icon"; +export * from "./in-progress-icon"; +export * from "./done-icon"; diff --git a/packages/ui/src/icons/planned-icon.tsx b/packages/ui/src/icons/planned-icon.tsx new file mode 100644 index 00000000000..88aa6bbe375 --- /dev/null +++ b/packages/ui/src/icons/planned-icon.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const PlannedState: React.FC = ({ width = "10", height = "11", className, color }) => ( + + + + + + + + + + + + +); diff --git a/packages/ui/src/loader.tsx b/packages/ui/src/loader.tsx index ed02d5af7bb..f8ca5ea7beb 100644 --- a/packages/ui/src/loader.tsx +++ b/packages/ui/src/loader.tsx @@ -16,10 +16,11 @@ const Loader = ({ children, className = "" }: Props) => ( type ItemProps = { height?: string; width?: string; + className?: string; }; -const Item: React.FC = ({ height = "auto", width = "auto" }) => ( -
+const Item: React.FC = ({ height = "auto", width = "auto", className = "" }) => ( +
); Loader.Item = Item; diff --git a/packages/ui/src/progress/circular-progress-indicator.tsx b/packages/ui/src/progress/circular-progress-indicator.tsx index 66c70475f49..139297c40e9 100644 --- a/packages/ui/src/progress/circular-progress-indicator.tsx +++ b/packages/ui/src/progress/circular-progress-indicator.tsx @@ -9,7 +9,7 @@ interface ICircularProgressIndicator { } export const CircularProgressIndicator: React.FC = (props) => { - const { size = 40, percentage = 25, strokeWidth = 6, children } = props; + const { size = 40, percentage = 25, strokeWidth = 6, strokeColor = "stroke-custom-primary-100", children } = props; const sqSize = size; const radius = (size - strokeWidth) / 2; @@ -27,7 +27,7 @@ export const CircularProgressIndicator: React.FC = ( strokeWidth={`${strokeWidth}px`} style={{ filter: "url(#filter0_bi_377_19141)" }} /> - + {/* = ( - + */} { {cycleId && !isSidebarCollapsed && (
- +
)}
diff --git a/web/helpers/cycle.helper.ts b/web/helpers/cycle.helper.ts index 889487dad47..49725511f09 100644 --- a/web/helpers/cycle.helper.ts +++ b/web/helpers/cycle.helper.ts @@ -1,9 +1,23 @@ +import { isEmpty, orderBy, uniqBy } from "lodash"; import sortBy from "lodash/sortBy"; import { ICycle, TCycleFilters } from "@plane/types"; // helpers -import { getDate } from "@/helpers/date-time.helper"; +import { generateDateArray, getDate, getToday } from "@/helpers/date-time.helper"; import { satisfiesDateFilter } from "@/helpers/filter.helper"; +export type TProgressChartData = { + date: string; + scope: number; + completed: number; + backlog: number; + started: number; + unstarted: number; + cancelled: number; + pending: number; + ideal: number; + actual: number; +}[]; + /** * @description orders cycles based on their status * @param {ICycle[]} cycles @@ -60,3 +74,48 @@ export const shouldFilterCycle = (cycle: ICycle, filter: TCycleFilters): boolean return fallsInFilters; }; + +export const formatActiveCycle = (args: { + cycle: ICycle; + isBurnDown?: boolean | undefined; + isTypeIssue?: boolean | undefined; +}) => { + const { cycle, isBurnDown, isTypeIssue } = args; + let today = getToday(); + const endDate: Date | string = new Date(cycle.end_date!); + + const extendedArray = endDate > today ? generateDateArray(today as Date, endDate) : []; + if (isEmpty(cycle.progress)) return extendedArray; + today = getToday(true); + + const scope = (p: any) => (isTypeIssue ? p.total_issues : p.total_estimate_points); + const ideal = (p: any) => + isTypeIssue + ? Math.abs(p.total_issues - p.completed_issues + (Math.random() < 0.5 ? -1 : 1)) + : Math.abs(p.total_estimate_points - p.completed_estimate_points + (Math.random() < 0.5 ? -1 : 1)); + + const scopeToday = scope(cycle?.progress[cycle?.progress.length - 1]); + const idealToday = ideal(cycle?.progress[cycle?.progress.length - 1]); + + const progress = [...orderBy(cycle?.progress, "date"), ...extendedArray].map((p) => { + const pending = isTypeIssue + ? p.total_issues - p.completed_issues - p.cancelled_issues + : p.total_estimate_points - p.completed_estimate_points - p.cancelled_estimate_points; + const completed = isTypeIssue ? p.completed_issues : p.completed_estimate_points; + + return { + date: p.date, + scope: p.date! < today ? scope(p) : p.date! < cycle.end_date! ? scopeToday : null, + completed, + backlog: isTypeIssue ? p.backlog_issues : p.backlog_estimate_points, + started: isTypeIssue ? p.started_issues : p.started_estimate_points, + unstarted: isTypeIssue ? p.unstarted_issues : p.unstarted_estimate_points, + cancelled: isTypeIssue ? p.cancelled_issues : p.cancelled_estimate_points, + pending: Math.abs(pending), + // TODO: This is a temporary logic to show the ideal line in the cycle chart + ideal: p.date! < today ? ideal(p) : p.date! < cycle.end_date! ? idealToday : null, + actual: p.date! <= today ? (isBurnDown ? Math.abs(pending) : completed) : undefined, + }; + }); + return uniqBy(progress, "date"); +}; diff --git a/web/helpers/date-time.helper.ts b/web/helpers/date-time.helper.ts index 50728182a93..e20dc0637be 100644 --- a/web/helpers/date-time.helper.ts +++ b/web/helpers/date-time.helper.ts @@ -357,3 +357,76 @@ export const getReadTimeFromWordsCount = (wordsCount: number): number => { const minutes = wordsCount / wordsPerMinute; return minutes * 60; }; + +/** + * @description calculates today's date + * @param {boolean} format + * @returns {Date | string} today's date + * @example getToday() // Output: 2024-09-29T00:00:00.000Z + * @example getToday(true) // Output: 2024-09-29 + */ +export const getToday = (format: boolean = false) => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + if (!format) return today; + + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, "0"); // Months are 0-based, so add 1 + const day = String(today.getDate()).padStart(2, "0"); // Add leading zero for single digits + return `${year}-${month}-${day}`; +}; + +/** + * @description calculates the date of the day before today + * @param {boolean} format + * @returns {Date | string} date of the day before today + * @example dateFormatter() // Output: "Sept 20, 2024" + */ +export const dateFormatter = (dateString: string) => { + // Convert to Date object + const date = new Date(dateString); + + // Options for the desired format (Month Day, Year) + const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric" }; + + // Format the date + const formattedDate = date.toLocaleDateString("en-US", options); + + return formattedDate; +}; + +/** + * @description calculates days left from today to the end date + * @returns {Date | string} number of days left + */ +export const daysLeft = (end_date: string) => + end_date ? Math.ceil((new Date(end_date).getTime() - new Date().getTime()) / (1000 * 3600 * 24)) : 0; + +/** + * @description generates an array of dates between the start and end dates + * @param startDate + * @param endDate + * @returns + */ +export const generateDateArray = (startDate: Date, endDate: Date) => { + // Convert the start and end dates to Date objects if they aren't already + const start = new Date(startDate); + // start.setDate(start.getDate() + 1); + const end = new Date(endDate); + end.setDate(end.getDate() + 1); + + // Create an empty array to store the dates + const dateArray = []; + + // Use a while loop to generate dates between the range + while (start <= end) { + // Increment the date by 1 day (86400000 milliseconds) + start.setDate(start.getDate() + 1); + // Push the current date (converted to ISO string for consistency) + dateArray.push({ + date: new Date(start).toISOString().split("T")[0], + }); + } + + return dateArray; +}; From b9e774bd20402c17bd53be452f4637660e280cd9 Mon Sep 17 00:00:00 2001 From: gakshita Date: Thu, 26 Sep 2024 16:43:24 +0530 Subject: [PATCH 2/3] fix: progress chart code splitting --- .../components/cycles/active-cycle/index.ts | 1 + .../components/cycles/active-cycle/root.tsx | 89 ++++ .../cycles/analytics-sidebar/index.ts | 1 + .../analytics-sidebar/sidebar-chart.tsx | 57 +++ web/ce/components/cycles/index.ts | 2 + .../components/cycles/active-cycle/index.ts | 1 - .../cycles/active-cycle/productivity.tsx | 22 +- .../cycles/active-cycle/use-cycles-details.ts | 2 +- .../cycles/analytics-sidebar/index.ts | 3 + .../analytics-sidebar/issue-progress.tsx | 165 +++---- .../analytics-sidebar/progress-stats.tsx | 24 +- .../cycles/analytics-sidebar/root.tsx | 441 ++---------------- .../analytics-sidebar/sidebar-details.tsx | 138 ++++++ .../analytics-sidebar/sidebar-header.tsx | 326 +++++++++++++ .../components/cycles/cycle-peek-overview.tsx | 8 +- .../cycles/list/cycle-list-item-action.tsx | 145 +++--- .../cycles/list/cycles-list-item.tsx | 8 - web/core/components/cycles/list/root.tsx | 8 +- web/core/services/cycle.service.ts | 12 + web/core/store/cycle.store.ts | 72 ++- web/core/store/issue/cycle/issue.store.ts | 4 + 21 files changed, 909 insertions(+), 620 deletions(-) create mode 100644 web/ce/components/cycles/active-cycle/index.ts create mode 100644 web/ce/components/cycles/active-cycle/root.tsx create mode 100644 web/ce/components/cycles/analytics-sidebar/index.ts create mode 100644 web/ce/components/cycles/analytics-sidebar/sidebar-chart.tsx create mode 100644 web/ce/components/cycles/index.ts create mode 100644 web/core/components/cycles/analytics-sidebar/sidebar-details.tsx create mode 100644 web/core/components/cycles/analytics-sidebar/sidebar-header.tsx diff --git a/web/ce/components/cycles/active-cycle/index.ts b/web/ce/components/cycles/active-cycle/index.ts new file mode 100644 index 00000000000..1efe34c51ec --- /dev/null +++ b/web/ce/components/cycles/active-cycle/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/web/ce/components/cycles/active-cycle/root.tsx b/web/ce/components/cycles/active-cycle/root.tsx new file mode 100644 index 00000000000..a173cfda03a --- /dev/null +++ b/web/ce/components/cycles/active-cycle/root.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { observer } from "mobx-react"; +import { Disclosure } from "@headlessui/react"; +// ui +import { Row } from "@plane/ui"; +// components +import { + ActiveCycleProductivity, + ActiveCycleProgress, + ActiveCycleStats, + CycleListGroupHeader, + CyclesListItem, +} from "@/components/cycles"; +import useCyclesDetails from "@/components/cycles/active-cycle/use-cycles-details"; +import { EmptyState } from "@/components/empty-state"; +// constants +import { EmptyStateType } from "@/constants/empty-state"; +import { useCycle } from "@/hooks/store"; +import { ActiveCycleIssueDetails } from "@/store/issue/cycle"; + +interface IActiveCycleDetails { + workspaceSlug: string; + projectId: string; +} + +export const ActiveCycleRoot: React.FC = observer((props) => { + const { workspaceSlug, projectId } = props; + const { currentProjectActiveCycle, currentProjectActiveCycleId } = useCycle(); + const { + handleFiltersUpdate, + cycle: activeCycle, + cycleIssueDetails, + } = useCyclesDetails({ workspaceSlug, projectId, cycleId: currentProjectActiveCycleId }); + + return ( + <> + + {({ open }) => ( + <> + + + + + {!currentProjectActiveCycle ? ( + + ) : ( +
+ {currentProjectActiveCycleId && ( + + )} + +
+ + + +
+
+
+ )} +
+ + )} +
+ + ); +}); diff --git a/web/ce/components/cycles/analytics-sidebar/index.ts b/web/ce/components/cycles/analytics-sidebar/index.ts new file mode 100644 index 00000000000..3ba38c61be5 --- /dev/null +++ b/web/ce/components/cycles/analytics-sidebar/index.ts @@ -0,0 +1 @@ +export * from "./sidebar-chart"; diff --git a/web/ce/components/cycles/analytics-sidebar/sidebar-chart.tsx b/web/ce/components/cycles/analytics-sidebar/sidebar-chart.tsx new file mode 100644 index 00000000000..e5b69ef24b1 --- /dev/null +++ b/web/ce/components/cycles/analytics-sidebar/sidebar-chart.tsx @@ -0,0 +1,57 @@ +import { Fragment } from "react"; +import { TCycleDistribution, TCycleEstimateDistribution } from "@plane/types"; +import { Loader } from "@plane/ui"; +import ProgressChart from "@/components/core/sidebar/progress-chart"; + +type ProgressChartProps = { + chartDistributionData: TCycleEstimateDistribution | TCycleDistribution | undefined; + cycleStartDate: Date | undefined; + cycleEndDate: Date | undefined; + totalEstimatePoints: number; + totalIssues: number; + plotType: string; +}; +export const SidebarBaseChart = (props: ProgressChartProps) => { + const { chartDistributionData, cycleStartDate, cycleEndDate, totalEstimatePoints, totalIssues, plotType } = props; + const completionChartDistributionData = chartDistributionData?.completion_chart || undefined; + + return ( +
+
+
+ + Ideal +
+
+ + Current +
+
+ {cycleStartDate && cycleEndDate && completionChartDistributionData ? ( + + {plotType === "points" ? ( + + ) : ( + + )} + + ) : ( + + + + )} +
+ ); +}; diff --git a/web/ce/components/cycles/index.ts b/web/ce/components/cycles/index.ts new file mode 100644 index 00000000000..89934687567 --- /dev/null +++ b/web/ce/components/cycles/index.ts @@ -0,0 +1,2 @@ +export * from "./active-cycle"; +export * from "./analytics-sidebar"; diff --git a/web/core/components/cycles/active-cycle/index.ts b/web/core/components/cycles/active-cycle/index.ts index d88ccc3e8b6..c2197825207 100644 --- a/web/core/components/cycles/active-cycle/index.ts +++ b/web/core/components/cycles/active-cycle/index.ts @@ -1,4 +1,3 @@ -export * from "./root"; export * from "./header"; export * from "./stats"; export * from "./upcoming-cycles-list-item"; diff --git a/web/core/components/cycles/active-cycle/productivity.tsx b/web/core/components/cycles/active-cycle/productivity.tsx index 31a865eae38..1e70f326f40 100644 --- a/web/core/components/cycles/active-cycle/productivity.tsx +++ b/web/core/components/cycles/active-cycle/productivity.tsx @@ -1,7 +1,7 @@ import { FC, Fragment } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { ICycle, TCyclePlotType } from "@plane/types"; +import { ICycle, TCycleEstimateType, TCyclePlotType } from "@plane/types"; import { CustomSelect, Loader } from "@plane/ui"; // components import ProgressChart from "@/components/core/sidebar/progress-chart"; @@ -19,22 +19,22 @@ export type ActiveCycleProductivityProps = { }; const cycleBurnDownChartOptions = [ - { value: "burndown", label: "Issues" }, + { value: "issues", label: "Issues" }, { value: "points", label: "Points" }, ]; export const ActiveCycleProductivity: FC = observer((props) => { const { workspaceSlug, projectId, cycle } = props; // hooks - const { getPlotTypeByCycleId, setPlotType } = useCycle(); + const { getEstimateTypeByCycleId, setEstimateType } = useCycle(); const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates(); // derived values - const plotType: TCyclePlotType = (cycle && getPlotTypeByCycleId(cycle.id)) || "burndown"; + const estimateType: TCycleEstimateType = (cycle && getEstimateTypeByCycleId(cycle.id)) || "issues"; - const onChange = async (value: TCyclePlotType) => { + const onChange = async (value: TCycleEstimateType) => { if (!workspaceSlug || !projectId || !cycle || !cycle.id) return; - setPlotType(cycle.id, value); + setEstimateType(cycle.id, value); }; const isCurrentProjectEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId) ? true : false; @@ -43,7 +43,7 @@ export const ActiveCycleProductivity: FC = observe const isCurrentEstimateTypeIsPoints = estimateDetails && estimateDetails?.type === EEstimateSystem.POINTS; const chartDistributionData = - cycle && plotType === "points" ? cycle?.estimate_distribution : cycle?.distribution || undefined; + cycle && estimateType === "points" ? cycle?.estimate_distribution : cycle?.distribution || undefined; const completionChartDistributionData = chartDistributionData?.completion_chart || undefined; return cycle && completionChartDistributionData ? ( @@ -55,8 +55,8 @@ export const ActiveCycleProductivity: FC = observe {isCurrentEstimateTypeIsPoints && (
{cycleBurnDownChartOptions.find((v) => v.value === plotType)?.label ?? "None"}} + value={estimateType} + label={{cycleBurnDownChartOptions.find((v) => v.value === estimateType)?.label ?? "None"}} onChange={onChange} maxHeight="lg" > @@ -85,7 +85,7 @@ export const ActiveCycleProductivity: FC = observe Current
- {plotType === "points" ? ( + {estimateType === "points" ? ( {`Pending points - ${cycle.backlog_estimate_points + cycle.unstarted_estimate_points + cycle.started_estimate_points}`} ) : ( {`Pending issues - ${cycle.backlog_issues + cycle.unstarted_issues + cycle.started_issues}`} @@ -95,7 +95,7 @@ export const ActiveCycleProductivity: FC = observe
{completionChartDistributionData && ( - {plotType === "points" ? ( + {estimateType === "points" ? ( { diff --git a/web/core/components/cycles/analytics-sidebar/index.ts b/web/core/components/cycles/analytics-sidebar/index.ts index c509152a2bf..eb3dc868e0a 100644 --- a/web/core/components/cycles/analytics-sidebar/index.ts +++ b/web/core/components/cycles/analytics-sidebar/index.ts @@ -1,3 +1,6 @@ export * from "./root"; export * from "./issue-progress"; export * from "./progress-stats"; +export * from "./root"; +export * from "./sidebar-header"; +export * from "./sidebar-details"; diff --git a/web/core/components/cycles/analytics-sidebar/issue-progress.tsx b/web/core/components/cycles/analytics-sidebar/issue-progress.tsx index ea44cce8156..f88df77b35d 100644 --- a/web/core/components/cycles/analytics-sidebar/issue-progress.tsx +++ b/web/core/components/cycles/analytics-sidebar/issue-progress.tsx @@ -5,12 +5,11 @@ import isEmpty from "lodash/isEmpty"; import isEqual from "lodash/isEqual"; import { observer } from "mobx-react"; import { useSearchParams } from "next/navigation"; -import { AlertCircle, ChevronUp, ChevronDown } from "lucide-react"; +import { ChevronUp, ChevronDown } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; -import { ICycle, IIssueFilterOptions, TCyclePlotType, TProgressSnapshot } from "@plane/types"; -import { CustomSelect, Loader, Spinner } from "@plane/ui"; +import { ICycle, IIssueFilterOptions, TCycleEstimateType, TCyclePlotType, TProgressSnapshot } from "@plane/types"; +import { CustomSelect } from "@plane/ui"; // components -import ProgressChart from "@/components/core/sidebar/progress-chart"; import { CycleProgressStats } from "@/components/cycles"; // constants import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; @@ -19,6 +18,7 @@ import { getDate } from "@/helpers/date-time.helper"; // hooks import { useIssues, useCycle, useProjectEstimates } from "@/hooks/store"; // plane web constants +import { SidebarBaseChart } from "@/plane-web/components/cycles/analytics-sidebar/sidebar-chart"; import { EEstimateSystem } from "@/plane-web/constants/estimates"; type TCycleAnalyticsProgress = { @@ -27,11 +27,6 @@ type TCycleAnalyticsProgress = { cycleId: string; }; -const cycleBurnDownChartOptions = [ - { value: "burndown", label: "Issues" }, - { value: "points", label: "Points" }, -]; - const validateCycleSnapshot = (cycleDetails: ICycle | null): ICycle | null => { if (!cycleDetails || cycleDetails === null) return cycleDetails; @@ -47,6 +42,18 @@ const validateCycleSnapshot = (cycleDetails: ICycle | null): ICycle | null => { return updatedCycleDetails; }; +type options = { + value: string; + label: string; +}; +export const cycleChartOptions: options[] = [ + { value: "burndown", label: "Burn-down" }, + { value: "burnup", label: "Burn-up" }, +]; +export const cycleEstimateOptions: options[] = [ + { value: "issues", label: "issues" }, + { value: "points", label: "points" }, +]; export const CycleAnalyticsProgress: FC = observer((props) => { // props const { workspaceSlug, projectId, cycleId } = props; @@ -55,7 +62,15 @@ export const CycleAnalyticsProgress: FC = observer((pro const peekCycle = searchParams.get("peekCycle") || undefined; // hooks const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates(); - const { getPlotTypeByCycleId, setPlotType, getCycleById, fetchCycleDetails, fetchArchivedCycleDetails } = useCycle(); + const { + getPlotTypeByCycleId, + getEstimateTypeByCycleId, + setPlotType, + getCycleById, + fetchCycleDetails, + fetchArchivedCycleDetails, + setEstimateType, + } = useCycle(); const { issuesFilter: { issueFilters, updateFilters }, } = useIssues(EIssuesStoreType.CYCLE); @@ -65,6 +80,7 @@ export const CycleAnalyticsProgress: FC = observer((pro // derived values const cycleDetails = validateCycleSnapshot(getCycleById(cycleId)); const plotType: TCyclePlotType = getPlotTypeByCycleId(cycleId); + const estimateType = getEstimateTypeByCycleId(cycleId); const isCurrentProjectEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId) ? true : false; const estimateDetails = isCurrentProjectEstimateEnabled && currentActiveEstimateId && estimateById(currentActiveEstimateId); @@ -76,7 +92,7 @@ export const CycleAnalyticsProgress: FC = observer((pro const totalEstimatePoints = cycleDetails?.total_estimate_points || 0; const progressHeaderPercentage = cycleDetails - ? plotType === "points" + ? estimateType === "points" ? completedEstimatePoints != 0 && totalEstimatePoints != 0 ? Math.round((completedEstimatePoints / totalEstimatePoints) * 100) : 0 @@ -86,21 +102,22 @@ export const CycleAnalyticsProgress: FC = observer((pro : 0; const chartDistributionData = - plotType === "points" ? cycleDetails?.estimate_distribution : cycleDetails?.distribution || undefined; - const completionChartDistributionData = chartDistributionData?.completion_chart || undefined; + estimateType === "points" ? cycleDetails?.estimate_distribution : cycleDetails?.distribution || undefined; const groupedIssues = useMemo( () => ({ - backlog: plotType === "points" ? cycleDetails?.backlog_estimate_points || 0 : cycleDetails?.backlog_issues || 0, + backlog: + estimateType === "points" ? cycleDetails?.backlog_estimate_points || 0 : cycleDetails?.backlog_issues || 0, unstarted: - plotType === "points" ? cycleDetails?.unstarted_estimate_points || 0 : cycleDetails?.unstarted_issues || 0, - started: plotType === "points" ? cycleDetails?.started_estimate_points || 0 : cycleDetails?.started_issues || 0, + estimateType === "points" ? cycleDetails?.unstarted_estimate_points || 0 : cycleDetails?.unstarted_issues || 0, + started: + estimateType === "points" ? cycleDetails?.started_estimate_points || 0 : cycleDetails?.started_issues || 0, completed: - plotType === "points" ? cycleDetails?.completed_estimate_points || 0 : cycleDetails?.completed_issues || 0, + estimateType === "points" ? cycleDetails?.completed_estimate_points || 0 : cycleDetails?.completed_issues || 0, cancelled: - plotType === "points" ? cycleDetails?.cancelled_estimate_points || 0 : cycleDetails?.cancelled_issues || 0, + estimateType === "points" ? cycleDetails?.cancelled_estimate_points || 0 : cycleDetails?.cancelled_issues || 0, }), - [plotType, cycleDetails] + [estimateType, cycleDetails] ); const cycleStartDate = getDate(cycleDetails?.start_date); @@ -111,8 +128,8 @@ export const CycleAnalyticsProgress: FC = observer((pro const isArchived = !!cycleDetails?.archived_at; // handlers - const onChange = async (value: TCyclePlotType) => { - setPlotType(cycleId, value); + const onChange = async (value: TCycleEstimateType) => { + setEstimateType(cycleId, value); if (!workspaceSlug || !projectId || !cycleId) return; try { setLoader(true); @@ -124,7 +141,7 @@ export const CycleAnalyticsProgress: FC = observer((pro setLoader(false); } catch (error) { setLoader(false); - setPlotType(cycleId, plotType); + setEstimateType(cycleId, estimateType); } }; @@ -161,40 +178,16 @@ export const CycleAnalyticsProgress: FC = observer((pro if (!cycleDetails) return <>; return ( -
+
{({ open }) => ( -
+
{/* progress bar header */} {isCycleDateValid ? (
Progress
- {progressHeaderPercentage > 0 && ( -
{`${progressHeaderPercentage}%`}
- )}
- {isCurrentEstimateTypeIsPoints && ( - <> -
- {cycleBurnDownChartOptions.find((v) => v.value === plotType)?.label ?? "None"} - } - onChange={onChange} - maxHeight="lg" - > - {cycleBurnDownChartOptions.map((item) => ( - - {item.label} - - ))} - -
- {loader && } - - )} {open ? (