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/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..035a5858543 100644 --- a/web/core/components/cycles/analytics-sidebar/index.ts +++ b/web/core/components/cycles/analytics-sidebar/index.ts @@ -1,3 +1,5 @@ export * from "./root"; export * from "./issue-progress"; export * from "./progress-stats"; +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 ? (