mirror of
https://github.com/makeplane/plane.git
synced 2026-04-30 04:59:41 -05:00
Fix: Cycle graphs refactor (#5745)
* fix: community changes for cycle graphs * fix: added dependency from root package.json
This commit is contained in:
Vendored
+2
-1
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
@@ -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": "*",
|
||||
|
||||
Reference in New Issue
Block a user