Fix: Cycle graphs refactor (#5745)

* fix: community changes for cycle graphs

* fix: added dependency from root package.json
This commit is contained in:
Akshita Goyal
2024-10-03 19:25:53 +05:30
committed by GitHub
parent ee0dce46de
commit f1a0a8d925
8 changed files with 91 additions and 176 deletions
+2 -1
View File
@@ -46,11 +46,11 @@ export type TCycleEstimateDistribution = {
export type TCycleProgress = {
date: string;
started: number;
actual: number;
pending: number;
ideal: number | null;
scope: number;
completed: number;
actual: number;
unstarted: number;
backlog: number;
cancelled: number;
@@ -103,6 +103,7 @@ export interface ICycle extends TProgressSnapshot {
workspace_id: string;
project_detail: IProjectDetails;
progress: any[];
version: number;
}
export interface CycleIssueResponse {
@@ -1,89 +0,0 @@
"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 { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { useCycle } from "@/hooks/store";
import { ActiveCycleIssueDetails } from "@/store/issue/cycle";
import useCyclesDetails from "./use-cycles-details";
interface IActiveCycleDetails {
workspaceSlug: string;
projectId: string;
}
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
const { workspaceSlug, projectId } = props;
const { currentProjectActiveCycle, currentProjectActiveCycleId } = useCycle();
const {
handleFiltersUpdate,
cycle: activeCycle,
cycleIssueDetails,
} = useCyclesDetails({ workspaceSlug, projectId, cycleId: currentProjectActiveCycleId });
return (
<>
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
{({ open }) => (
<>
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 cursor-pointer">
<CycleListGroupHeader title="Active cycle" type="current" isExpanded={open} />
</Disclosure.Button>
<Disclosure.Panel>
{!currentProjectActiveCycle ? (
<EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />
) : (
<div className="flex flex-col border-b border-custom-border-200">
{currentProjectActiveCycleId && (
<CyclesListItem
key={currentProjectActiveCycleId}
cycleId={currentProjectActiveCycleId}
workspaceSlug={workspaceSlug}
projectId={projectId}
className="!border-b-transparent"
/>
)}
<Row className="bg-custom-background-100 pt-3 pb-6">
<div className="grid grid-cols-1 bg-custom-background-100 gap-3 lg:grid-cols-2 xl:grid-cols-3">
<ActiveCycleProgress
handleFiltersUpdate={handleFiltersUpdate}
projectId={projectId}
workspaceSlug={workspaceSlug}
cycle={activeCycle}
/>
<ActiveCycleProductivity
workspaceSlug={workspaceSlug}
projectId={projectId}
cycle={activeCycle}
/>
<ActiveCycleStats
workspaceSlug={workspaceSlug}
projectId={projectId}
cycle={activeCycle}
cycleId={currentProjectActiveCycleId}
handleFiltersUpdate={handleFiltersUpdate}
cycleIssueDetails={cycleIssueDetails as ActiveCycleIssueDetails}
/>
</div>
</Row>
</div>
)}
</Disclosure.Panel>
</>
)}
</Disclosure>
</>
);
});
+1 -1
View File
@@ -28,7 +28,7 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
</>
) : (
<>
<ActiveCycleRoot workspaceSlug={workspaceSlug} projectId={projectId} />
<ActiveCycleRoot />
{upcomingCycleIds && (
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
+1 -1
View File
@@ -60,7 +60,7 @@ export const CYCLE_STATUS: {
{
label: "day left",
value: "current",
title: "Active",
title: "In progress",
color: "#F59E0B",
textColor: "text-amber-500",
bgColor: "bg-amber-50",
+7 -12
View File
@@ -71,11 +71,7 @@ export interface ICycleStore {
fetchArchivedCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>;
fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>;
fetchActiveCycleProgress: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<TProgressSnapshot>;
fetchActiveCycleProgressPro: (
workspaceSlug: string,
projectId: string,
cycleId: string
) => Promise<TProgressSnapshot> | Promise<null>;
fetchActiveCycleProgressPro: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
fetchActiveCycleAnalytics: (
workspaceSlug: string,
projectId: string,
@@ -146,7 +142,6 @@ export class CycleStore implements ICycleStore {
fetchArchivedCycles: action,
fetchArchivedCycleDetails: action,
fetchActiveCycleProgress: action,
fetchActiveCycleProgressPro: action,
fetchActiveCycleAnalytics: action,
fetchCycleDetails: action,
createCycle: action,
@@ -282,7 +277,7 @@ export class CycleStore implements ICycleStore {
*/
getActiveCycleProgress = computedFn((cycleId?: string) => {
const cycle = cycleId ? this.cycleMap[cycleId] : this.currentProjectActiveCycle;
if (!cycle?.progress) return null;
if (!cycle) return null;
const isTypeIssue = this.getEstimateTypeByCycleId(cycle.id) === "issues";
const isBurnDown = this.getPlotTypeByCycleId(cycle.id) === "burndown";
@@ -403,13 +398,13 @@ export class CycleStore implements ICycleStore {
await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload);
/**
* @description gets the plot type for the module store
* @description gets the plot type for the cycle store
* @param {TCyclePlotType} plotType
*/
getPlotTypeByCycleId = computedFn((cycleId: string) => this.plotType[cycleId] || "burndown");
/**
* @description gets the estimate type for the module store
* @description gets the estimate type for the cycle store
* @param {TCycleEstimateType} estimateType
*/
getEstimateTypeByCycleId = computedFn((cycleId: string) => {
@@ -421,7 +416,7 @@ export class CycleStore implements ICycleStore {
});
/**
* @description updates the plot type for the module store
* @description updates the plot type for the cycle store
* @param {TCyclePlotType} plotType
*/
setPlotType = (cycleId: string, plotType: TCyclePlotType) => {
@@ -429,7 +424,7 @@ export class CycleStore implements ICycleStore {
};
/**
* @description updates the estimate type for the module store
* @description updates the estimate type for the cycle store
* @param {TCycleEstimateType} estimateType
*/
setEstimateType = (cycleId: string, estimateType: TCycleEstimateType) => {
@@ -545,7 +540,7 @@ export class CycleStore implements ICycleStore {
* @param cycleId
* @returns
*/
fetchActiveCycleProgressPro = async (workspaceSlug: string, projectId: string, cycleId: string) => null;
fetchActiveCycleProgressPro = action(async (workspaceSlug: string, projectId: string, cycleId: string) => {});
/**
* @description fetches active cycle analytics
+77 -26
View File
@@ -1,8 +1,9 @@
import { startOfToday, format } from "date-fns";
import { isEmpty, orderBy, uniqBy } from "lodash";
import sortBy from "lodash/sortBy";
import { ICycle, TCycleFilters } from "@plane/types";
// helpers
import { generateDateArray, getDate, getToday } from "@/helpers/date-time.helper";
import { findTotalDaysInRange, generateDateArray, getDate } from "@/helpers/date-time.helper";
import { satisfiesDateFilter } from "@/helpers/filter.helper";
export type TProgressChartData = {
@@ -75,47 +76,97 @@ 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 scope = (p: any, isTypeIssue: boolean) => (isTypeIssue ? p.total_issues : p.total_estimate_points);
const ideal = (date: string, scope: number, cycle: ICycle) =>
Math.floor(
((findTotalDaysInRange(date, cycle.end_date) || 0) /
(findTotalDaysInRange(cycle.start_date, cycle.end_date) || 0)) *
scope
);
const formatV1Data = (isTypeIssue: boolean, cycle: ICycle, isBurnDown: boolean, endDate: Date | string) => {
const today = format(startOfToday(), "yyyy-MM-dd");
const data = isTypeIssue ? cycle.distribution : cycle.estimate_distribution;
const extendedArray = generateDateArray(endDate, endDate).map((d) => d.date);
if (isEmpty(data)) return [];
const progress = [...Object.keys(data.completion_chart), ...extendedArray].map((p) => {
const pending = data.completion_chart[p] || 0;
const total = isTypeIssue ? cycle.total_issues : cycle.total_estimate_points;
const completed = scope(cycle, isTypeIssue) - pending;
return {
date: p,
scope: p! < today ? scope(cycle, isTypeIssue) : null,
completed,
backlog: isTypeIssue ? cycle.backlog_issues : cycle.backlog_estimate_points,
started: p === today ? cycle[isTypeIssue ? "started_issues" : "started_estimate_points"] : undefined,
unstarted: p === today ? cycle[isTypeIssue ? "unstarted_issues" : "unstarted_estimate_points"] : undefined,
cancelled: p === today ? cycle[isTypeIssue ? "cancelled_issues" : "cancelled_estimate_points"] : undefined,
pending: Math.abs(pending || 0),
ideal:
p < today
? ideal(p, total || 0, cycle)
: p <= cycle.end_date!
? ideal(today as string, total || 0, cycle)
: null,
actual: p <= today ? (isBurnDown ? Math.abs(pending) : completed) : undefined,
};
});
return progress;
};
const formatV2Data = (isTypeIssue: boolean, cycle: ICycle, isBurnDown: boolean, endDate: Date | string) => {
if (!cycle.progress) return [];
let today: Date | string = startOfToday();
const extendedArray = endDate > today ? generateDateArray(today as Date, endDate) : [];
if (isEmpty(cycle.progress)) return extendedArray;
today = getToday(true);
today = format(startOfToday(), "yyyy-MM-dd");
const todaysData = cycle?.progress[cycle?.progress.length - 1];
const scopeToday = scope(todaysData, isTypeIssue);
const idealToday = ideal(todaysData.date, scopeToday, cycle);
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) => {
let 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;
const dataDate = p.progress_date ? format(new Date(p.progress_date), "yyyy-MM-dd") : p.date;
return {
date: p.date,
scope: p.date! < today ? scope(p) : p.date! < cycle.end_date! ? scopeToday : null,
date: dataDate,
scope: dataDate! < today ? scope(p, isTypeIssue) : dataDate! <= 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,
ideal:
dataDate! < today
? ideal(dataDate, scope(p, isTypeIssue), cycle)
: dataDate! < cycle.end_date!
? idealToday
: null,
actual: dataDate! <= today ? (isBurnDown ? Math.abs(pending) : completed) : undefined,
};
});
return uniqBy(progress, "date");
progress = uniqBy(progress, "date");
return progress;
};
export const formatActiveCycle = (args: {
cycle: ICycle;
isBurnDown?: boolean | undefined;
isTypeIssue?: boolean | undefined;
}) => {
const { cycle, isBurnDown, isTypeIssue } = args;
const endDate: Date | string = new Date(cycle.end_date!);
return cycle.version === 1
? formatV1Data(isTypeIssue!, cycle, isBurnDown!, endDate)
: formatV2Data(isTypeIssue!, cycle, isBurnDown!, endDate);
};
+1 -45
View File
@@ -358,57 +358,13 @@ export const getReadTimeFromWordsCount = (wordsCount: number): number => {
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) => {
export const generateDateArray = (startDate: string | Date, endDate: string | 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);
+2 -1
View File
@@ -65,7 +65,8 @@
"use-debounce": "^9.0.4",
"use-font-face-observer": "^1.2.2",
"uuid": "^9.0.0",
"zxcvbn": "^4.4.2"
"zxcvbn": "^4.4.2",
"recharts": "^2.12.7"
},
"devDependencies": {
"@plane/eslint-config": "*",