[WEB-2114]: Chore: project cycle optimization (#5430)
* chore: project cycle optimization * fix: typo * chore: changed the label typo * feat: intergrated optimized api * chore: added every key as plural * fix: productivity dropdown * fix: removed logging * fix: handled loading * fix: loaders --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
b22bdef9e1
commit
716300d964
12 changed files with 580 additions and 728 deletions
|
|
@ -6,6 +6,8 @@ from plane.app.views import (
|
||||||
CycleIssueViewSet,
|
CycleIssueViewSet,
|
||||||
CycleDateCheckEndpoint,
|
CycleDateCheckEndpoint,
|
||||||
CycleFavoriteViewSet,
|
CycleFavoriteViewSet,
|
||||||
|
CycleProgressEndpoint,
|
||||||
|
CycleAnalyticsEndpoint,
|
||||||
TransferCycleIssueEndpoint,
|
TransferCycleIssueEndpoint,
|
||||||
CycleUserPropertiesEndpoint,
|
CycleUserPropertiesEndpoint,
|
||||||
CycleArchiveUnarchiveEndpoint,
|
CycleArchiveUnarchiveEndpoint,
|
||||||
|
|
@ -106,4 +108,14 @@ urlpatterns = [
|
||||||
CycleArchiveUnarchiveEndpoint.as_view(),
|
CycleArchiveUnarchiveEndpoint.as_view(),
|
||||||
name="cycle-archive-unarchive",
|
name="cycle-archive-unarchive",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/progress/",
|
||||||
|
CycleProgressEndpoint.as_view(),
|
||||||
|
name="project-cycle",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/analytics/",
|
||||||
|
CycleAnalyticsEndpoint.as_view(),
|
||||||
|
name="project-cycle",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,8 @@ from .cycle.base import (
|
||||||
CycleUserPropertiesEndpoint,
|
CycleUserPropertiesEndpoint,
|
||||||
CycleViewSet,
|
CycleViewSet,
|
||||||
TransferCycleIssueEndpoint,
|
TransferCycleIssueEndpoint,
|
||||||
|
CycleAnalyticsEndpoint,
|
||||||
|
CycleProgressEndpoint,
|
||||||
)
|
)
|
||||||
from .cycle.issue import (
|
from .cycle.issue import (
|
||||||
CycleIssueViewSet,
|
CycleIssueViewSet,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import useSWR from "swr";
|
|
||||||
// components
|
// components
|
||||||
import { EmptyState } from "@/components/common";
|
import { EmptyState } from "@/components/common";
|
||||||
import { PageHead } from "@/components/core";
|
import { PageHead } from "@/components/core";
|
||||||
import { CycleDetailsSidebar } from "@/components/cycles";
|
import { CycleDetailsSidebar } from "@/components/cycles";
|
||||||
|
import useCyclesDetails from "@/components/cycles/active-cycle/use-cycles-details";
|
||||||
import { CycleLayoutRoot } from "@/components/issues/issue-layouts";
|
import { CycleLayoutRoot } from "@/components/issues/issue-layouts";
|
||||||
// constants
|
// constants
|
||||||
// import { EIssuesStoreType } from "@/constants/issue";
|
// import { EIssuesStoreType } from "@/constants/issue";
|
||||||
|
|
@ -24,18 +24,17 @@ const CycleDetailPage = observer(() => {
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
const { workspaceSlug, projectId, cycleId } = useParams();
|
const { workspaceSlug, projectId, cycleId } = useParams();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { fetchCycleDetails, getCycleById } = useCycle();
|
const { getCycleById, loader } = useCycle();
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
// const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
|
// const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
|
||||||
// hooks
|
// hooks
|
||||||
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
|
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
|
||||||
// fetching cycle details
|
|
||||||
const { error } = useSWR(
|
useCyclesDetails({
|
||||||
workspaceSlug && projectId && cycleId ? `CYCLE_DETAILS_${cycleId.toString()}` : null,
|
workspaceSlug: workspaceSlug.toString(),
|
||||||
workspaceSlug && projectId && cycleId
|
projectId: projectId.toString(),
|
||||||
? () => fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
|
cycleId: cycleId.toString(),
|
||||||
: null
|
});
|
||||||
);
|
|
||||||
// derived values
|
// derived values
|
||||||
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
||||||
const cycle = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
const cycle = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||||
|
|
@ -52,7 +51,7 @@ const CycleDetailPage = observer(() => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={pageTitle} />
|
<PageHead title={pageTitle} />
|
||||||
{error ? (
|
{!cycle && !loader ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
image={emptyCycle}
|
image={emptyCycle}
|
||||||
title="Cycle does not exist"
|
title="Cycle does not exist"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC, Fragment, useCallback, useRef, useState } from "react";
|
import { FC, Fragment, useCallback, useRef, useState } from "react";
|
||||||
|
import isEmpty from "lodash/isEmpty";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
|
||||||
import { CalendarCheck } from "lucide-react";
|
import { CalendarCheck } from "lucide-react";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
|
|
@ -16,7 +16,6 @@ import { StateDropdown } from "@/components/dropdowns";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
// constants
|
// constants
|
||||||
import { EmptyStateType } from "@/constants/empty-state";
|
import { EmptyStateType } from "@/constants/empty-state";
|
||||||
import { CYCLE_ISSUES_WITH_PARAMS } from "@/constants/fetch-keys";
|
|
||||||
import { EIssuesStoreType } from "@/constants/issue";
|
import { EIssuesStoreType } from "@/constants/issue";
|
||||||
// helper
|
// helper
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
|
@ -27,6 +26,7 @@ import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
|
||||||
import useLocalStorage from "@/hooks/use-local-storage";
|
import useLocalStorage from "@/hooks/use-local-storage";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||||
|
import { ActiveCycleIssueDetails } from "@/store/issue/cycle";
|
||||||
|
|
||||||
export type ActiveCycleStatsProps = {
|
export type ActiveCycleStatsProps = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
|
@ -34,10 +34,11 @@ export type ActiveCycleStatsProps = {
|
||||||
cycle: ICycle | null;
|
cycle: ICycle | null;
|
||||||
cycleId?: string | null;
|
cycleId?: string | null;
|
||||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => void;
|
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => void;
|
||||||
|
cycleIssueDetails: ActiveCycleIssueDetails;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, cycle, cycleId, handleFiltersUpdate } = props;
|
const { workspaceSlug, projectId, cycle, cycleId, handleFiltersUpdate, cycleIssueDetails } = props;
|
||||||
|
|
||||||
const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");
|
const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");
|
||||||
|
|
||||||
|
|
@ -57,21 +58,12 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const {
|
const {
|
||||||
issues: { getActiveCycleById, fetchActiveCycleIssues, fetchNextActiveCycleIssues },
|
issues: { fetchNextActiveCycleIssues },
|
||||||
} = useIssues(EIssuesStoreType.CYCLE);
|
} = useIssues(EIssuesStoreType.CYCLE);
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
setPeekIssue,
|
setPeekIssue,
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
|
||||||
useSWR(
|
|
||||||
workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId, { priority: "urgent,high" }) : null,
|
|
||||||
workspaceSlug && projectId && cycleId ? () => fetchActiveCycleIssues(workspaceSlug, projectId, 30, cycleId) : null,
|
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const cycleIssueDetails = cycleId ? getActiveCycleById(cycleId) : { nextPageResults: false };
|
|
||||||
|
|
||||||
const loadMoreIssues = useCallback(() => {
|
const loadMoreIssues = useCallback(() => {
|
||||||
if (!cycleId) return;
|
if (!cycleId) return;
|
||||||
fetchNextActiveCycleIssues(workspaceSlug, projectId, cycleId);
|
fetchNextActiveCycleIssues(workspaceSlug, projectId, cycleId);
|
||||||
|
|
@ -87,6 +79,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||||
<Loader.Item height="30px" />
|
<Loader.Item height="30px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
);
|
);
|
||||||
|
|
||||||
return cycleId ? (
|
return cycleId ? (
|
||||||
<div className="flex flex-col gap-4 p-4 min-h-[17rem] overflow-hidden bg-custom-background-100 col-span-1 lg:col-span-2 xl:col-span-1 border border-custom-border-200 rounded-lg">
|
<div className="flex flex-col gap-4 p-4 min-h-[17rem] overflow-hidden bg-custom-background-100 col-span-1 lg:col-span-2 xl:col-span-1 border border-custom-border-200 rounded-lg">
|
||||||
<Tab.Group
|
<Tab.Group
|
||||||
|
|
@ -248,7 +241,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||||
as="div"
|
as="div"
|
||||||
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
|
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
|
||||||
>
|
>
|
||||||
{cycle ? (
|
{cycle && !isEmpty(cycle.distribution) ? (
|
||||||
cycle?.distribution?.assignees && cycle.distribution.assignees.length > 0 ? (
|
cycle?.distribution?.assignees && cycle.distribution.assignees.length > 0 ? (
|
||||||
cycle.distribution?.assignees?.map((assignee, index) => {
|
cycle.distribution?.assignees?.map((assignee, index) => {
|
||||||
if (assignee.assignee_id)
|
if (assignee.assignee_id)
|
||||||
|
|
@ -306,7 +299,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||||
as="div"
|
as="div"
|
||||||
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
|
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
|
||||||
>
|
>
|
||||||
{cycle ? (
|
{cycle && !isEmpty(cycle.distribution) ? (
|
||||||
cycle?.distribution?.labels && cycle.distribution.labels.length > 0 ? (
|
cycle?.distribution?.labels && cycle.distribution.labels.length > 0 ? (
|
||||||
cycle.distribution.labels?.map((label, index) => (
|
cycle.distribution.labels?.map((label, index) => (
|
||||||
<SingleProgressStats
|
<SingleProgressStats
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { FC, Fragment, useState } from "react";
|
import { FC, Fragment } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ICycle, TCyclePlotType } from "@plane/types";
|
import { ICycle, TCyclePlotType } from "@plane/types";
|
||||||
import { CustomSelect, Loader, Spinner } from "@plane/ui";
|
import { CustomSelect, Loader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import ProgressChart from "@/components/core/sidebar/progress-chart";
|
import ProgressChart from "@/components/core/sidebar/progress-chart";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
|
|
@ -26,24 +26,15 @@ const cycleBurnDownChartOptions = [
|
||||||
export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observer((props) => {
|
export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, cycle } = props;
|
const { workspaceSlug, projectId, cycle } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { getPlotTypeByCycleId, setPlotType, fetchCycleDetails } = useCycle();
|
const { getPlotTypeByCycleId, setPlotType } = useCycle();
|
||||||
const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates();
|
const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates();
|
||||||
// state
|
|
||||||
const [loader, setLoader] = useState(false);
|
|
||||||
// derived values
|
// derived values
|
||||||
const plotType: TCyclePlotType = (cycle && getPlotTypeByCycleId(cycle.id)) || "burndown";
|
const plotType: TCyclePlotType = (cycle && getPlotTypeByCycleId(cycle.id)) || "burndown";
|
||||||
|
|
||||||
const onChange = async (value: TCyclePlotType) => {
|
const onChange = async (value: TCyclePlotType) => {
|
||||||
if (!workspaceSlug || !projectId || !cycle || !cycle.id) return;
|
if (!workspaceSlug || !projectId || !cycle || !cycle.id) return;
|
||||||
setPlotType(cycle.id, value);
|
setPlotType(cycle.id, value);
|
||||||
try {
|
|
||||||
setLoader(true);
|
|
||||||
await fetchCycleDetails(workspaceSlug, projectId, cycle.id);
|
|
||||||
setLoader(false);
|
|
||||||
} catch (error) {
|
|
||||||
setLoader(false);
|
|
||||||
setPlotType(cycle.id, plotType);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isCurrentProjectEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId) ? true : false;
|
const isCurrentProjectEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId) ? true : false;
|
||||||
|
|
@ -55,7 +46,7 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observe
|
||||||
cycle && plotType === "points" ? cycle?.estimate_distribution : cycle?.distribution || undefined;
|
cycle && plotType === "points" ? cycle?.estimate_distribution : cycle?.distribution || undefined;
|
||||||
const completionChartDistributionData = chartDistributionData?.completion_chart || undefined;
|
const completionChartDistributionData = chartDistributionData?.completion_chart || undefined;
|
||||||
|
|
||||||
return cycle ? (
|
return cycle && completionChartDistributionData ? (
|
||||||
<div className="flex flex-col min-h-[17rem] gap-5 px-3.5 py-4 bg-custom-background-100 border border-custom-border-200 rounded-lg">
|
<div className="flex flex-col min-h-[17rem] gap-5 px-3.5 py-4 bg-custom-background-100 border border-custom-border-200 rounded-lg">
|
||||||
<div className="relative flex items-center justify-between gap-4">
|
<div className="relative flex items-center justify-between gap-4">
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}>
|
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}>
|
||||||
|
|
@ -75,7 +66,6 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observe
|
||||||
</CustomSelect.Option>
|
</CustomSelect.Option>
|
||||||
))}
|
))}
|
||||||
</CustomSelect>
|
</CustomSelect>
|
||||||
{loader && <Spinner className="h-3 w-3" />}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,31 +16,33 @@ import { useProjectState } from "@/hooks/store";
|
||||||
|
|
||||||
export type ActiveCycleProgressProps = {
|
export type ActiveCycleProgressProps = {
|
||||||
cycle: ICycle | null;
|
cycle: ICycle | null;
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => void;
|
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props) => {
|
export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props) => {
|
||||||
const { cycle, handleFiltersUpdate } = props;
|
const { handleFiltersUpdate, cycle } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { groupedProjectStates } = useProjectState();
|
const { groupedProjectStates } = useProjectState();
|
||||||
|
|
||||||
|
// derived values
|
||||||
const progressIndicatorData = PROGRESS_STATE_GROUPS_DETAILS.map((group, index) => ({
|
const progressIndicatorData = PROGRESS_STATE_GROUPS_DETAILS.map((group, index) => ({
|
||||||
id: index,
|
id: index,
|
||||||
name: group.title,
|
name: group.title,
|
||||||
value: cycle && cycle.total_issues > 0 ? (cycle[group.key as keyof ICycle] as number) : 0,
|
value: cycle && cycle.total_issues > 0 ? (cycle[group.key as keyof ICycle] as number) : 0,
|
||||||
color: group.color,
|
color: group.color,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const groupedIssues: any = cycle
|
const groupedIssues: any = cycle
|
||||||
? {
|
? {
|
||||||
completed: cycle.completed_issues,
|
completed: cycle?.completed_issues,
|
||||||
started: cycle.started_issues,
|
started: cycle?.started_issues,
|
||||||
unstarted: cycle.unstarted_issues,
|
unstarted: cycle?.unstarted_issues,
|
||||||
backlog: cycle.backlog_issues,
|
backlog: cycle?.backlog_issues,
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
return cycle ? (
|
return cycle && cycle.hasOwnProperty("started_issues") ? (
|
||||||
<div className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
|
<div className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import isEqual from "lodash/isEqual";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { Disclosure } from "@headlessui/react";
|
import { Disclosure } from "@headlessui/react";
|
||||||
// types
|
|
||||||
import { IIssueFilterOptions } from "@plane/types";
|
|
||||||
// ui
|
|
||||||
import { Loader } from "@plane/ui";
|
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
ActiveCycleProductivity,
|
ActiveCycleProductivity,
|
||||||
|
|
@ -21,9 +13,9 @@ import {
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
// constants
|
// constants
|
||||||
import { EmptyStateType } from "@/constants/empty-state";
|
import { EmptyStateType } from "@/constants/empty-state";
|
||||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
import { useCycle } from "@/hooks/store";
|
||||||
// hooks
|
import { ActiveCycleIssueDetails } from "@/store/issue/cycle";
|
||||||
import { useCycle, useIssues } from "@/hooks/store";
|
import useCyclesDetails from "./use-cycles-details";
|
||||||
|
|
||||||
interface IActiveCycleDetails {
|
interface IActiveCycleDetails {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
|
@ -31,56 +23,13 @@ interface IActiveCycleDetails {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
|
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
|
||||||
// props
|
|
||||||
const { workspaceSlug, projectId } = props;
|
const { workspaceSlug, projectId } = props;
|
||||||
// router
|
const { currentProjectActiveCycle, currentProjectActiveCycleId } = useCycle();
|
||||||
const router = useRouter();
|
|
||||||
// store hooks
|
|
||||||
const {
|
const {
|
||||||
issuesFilter: { issueFilters, updateFilters },
|
handleFiltersUpdate,
|
||||||
} = useIssues(EIssuesStoreType.CYCLE);
|
cycle: activeCycle,
|
||||||
const { currentProjectActiveCycle, fetchActiveCycle, currentProjectActiveCycleId, getActiveCycleById } = useCycle();
|
cycleIssueDetails,
|
||||||
// derived values
|
} = useCyclesDetails({ workspaceSlug, projectId, cycleId: currentProjectActiveCycleId });
|
||||||
const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
|
|
||||||
// fetch active cycle details
|
|
||||||
const { isLoading } = useSWR(
|
|
||||||
workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null,
|
|
||||||
workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFiltersUpdate = useCallback(
|
|
||||||
(key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => {
|
|
||||||
if (!workspaceSlug || !projectId || !currentProjectActiveCycleId) return;
|
|
||||||
|
|
||||||
const newFilters: IIssueFilterOptions = {};
|
|
||||||
Object.keys(issueFilters?.filters ?? {}).forEach((key) => {
|
|
||||||
newFilters[key as keyof IIssueFilterOptions] = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
let newValues: string[] = [];
|
|
||||||
|
|
||||||
if (isEqual(newValues, value)) newValues = [];
|
|
||||||
else newValues = value;
|
|
||||||
|
|
||||||
updateFilters(
|
|
||||||
workspaceSlug.toString(),
|
|
||||||
projectId.toString(),
|
|
||||||
EIssueFilterType.FILTERS,
|
|
||||||
{ ...newFilters, [key]: newValues },
|
|
||||||
currentProjectActiveCycleId.toString()
|
|
||||||
);
|
|
||||||
if (redirect) router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${currentProjectActiveCycleId}`);
|
|
||||||
},
|
|
||||||
[workspaceSlug, projectId, currentProjectActiveCycleId, issueFilters, updateFilters, router]
|
|
||||||
);
|
|
||||||
|
|
||||||
// show loader if active cycle is loading
|
|
||||||
if (!currentProjectActiveCycle && isLoading)
|
|
||||||
return (
|
|
||||||
<Loader>
|
|
||||||
<Loader.Item height="250px" />
|
|
||||||
</Loader>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -106,7 +55,12 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
||||||
)}
|
)}
|
||||||
<div className="bg-custom-background-100 pt-3 pb-6 px-6">
|
<div className="bg-custom-background-100 pt-3 pb-6 px-6">
|
||||||
<div className="grid grid-cols-1 bg-custom-background-100 gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
<div className="grid grid-cols-1 bg-custom-background-100 gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
<ActiveCycleProgress cycle={activeCycle} handleFiltersUpdate={handleFiltersUpdate} />
|
<ActiveCycleProgress
|
||||||
|
handleFiltersUpdate={handleFiltersUpdate}
|
||||||
|
projectId={projectId}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
cycle={activeCycle}
|
||||||
|
/>
|
||||||
<ActiveCycleProductivity
|
<ActiveCycleProductivity
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
|
@ -118,6 +72,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
||||||
cycle={activeCycle}
|
cycle={activeCycle}
|
||||||
cycleId={currentProjectActiveCycleId}
|
cycleId={currentProjectActiveCycleId}
|
||||||
handleFiltersUpdate={handleFiltersUpdate}
|
handleFiltersUpdate={handleFiltersUpdate}
|
||||||
|
cycleIssueDetails={cycleIssueDetails as ActiveCycleIssueDetails}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import isEqual from "lodash/isEqual";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { IIssueFilterOptions } from "@plane/types";
|
||||||
|
import { CYCLE_ISSUES_WITH_PARAMS } from "@/constants/fetch-keys";
|
||||||
|
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||||
|
import { useCycle, useIssues } from "@/hooks/store";
|
||||||
|
|
||||||
|
interface IActiveCycleDetails {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
cycleId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCyclesDetails = (props: IActiveCycleDetails) => {
|
||||||
|
// props
|
||||||
|
const { workspaceSlug, projectId, cycleId } = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
// store hooks
|
||||||
|
const {
|
||||||
|
issuesFilter: { issueFilters, updateFilters },
|
||||||
|
issues: { getActiveCycleById: getActiveCycleByIdFromIssue, fetchActiveCycleIssues },
|
||||||
|
} = useIssues(EIssuesStoreType.CYCLE);
|
||||||
|
|
||||||
|
const { fetchActiveCycleProgress, getCycleById, fetchActiveCycleAnalytics } = useCycle();
|
||||||
|
// derived values
|
||||||
|
const cycle = cycleId ? getCycleById(cycleId) : null;
|
||||||
|
|
||||||
|
// fetch cycle details
|
||||||
|
useSWR(
|
||||||
|
workspaceSlug && projectId && cycle ? `PROJECT_ACTIVE_CYCLE_${projectId}_PROGRESS` : null,
|
||||||
|
workspaceSlug && projectId && cycle ? () => fetchActiveCycleProgress(workspaceSlug, projectId, cycle.id) : null,
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
|
);
|
||||||
|
useSWR(
|
||||||
|
workspaceSlug && projectId && cycle && !cycle?.distribution ? `PROJECT_ACTIVE_CYCLE_${projectId}_DURATION` : null,
|
||||||
|
workspaceSlug && projectId && cycle && !cycle?.distribution
|
||||||
|
? () => fetchActiveCycleAnalytics(workspaceSlug, projectId, cycle.id, "issues")
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
useSWR(
|
||||||
|
workspaceSlug && projectId && cycle && !cycle?.estimate_distribution
|
||||||
|
? `PROJECT_ACTIVE_CYCLE_${projectId}_ESTIMATE_DURATION`
|
||||||
|
: null,
|
||||||
|
workspaceSlug && projectId && cycle && !cycle?.estimate_distribution
|
||||||
|
? () => fetchActiveCycleAnalytics(workspaceSlug, projectId, cycle.id, "points")
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
useSWR(
|
||||||
|
workspaceSlug && projectId && cycle?.id ? CYCLE_ISSUES_WITH_PARAMS(cycle?.id, { priority: "urgent,high" }) : null,
|
||||||
|
workspaceSlug && projectId && cycle?.id
|
||||||
|
? () => fetchActiveCycleIssues(workspaceSlug, projectId, 30, cycle?.id)
|
||||||
|
: null,
|
||||||
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const cycleIssueDetails = cycle?.id ? getActiveCycleByIdFromIssue(cycle?.id) : { nextPageResults: false };
|
||||||
|
|
||||||
|
const handleFiltersUpdate = useCallback(
|
||||||
|
(key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => {
|
||||||
|
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||||
|
|
||||||
|
const newFilters: IIssueFilterOptions = {};
|
||||||
|
Object.keys(issueFilters?.filters ?? {}).forEach((key) => {
|
||||||
|
newFilters[key as keyof IIssueFilterOptions] = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
let newValues: string[] = [];
|
||||||
|
|
||||||
|
if (isEqual(newValues, value)) newValues = [];
|
||||||
|
else newValues = value;
|
||||||
|
|
||||||
|
updateFilters(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
EIssueFilterType.FILTERS,
|
||||||
|
{ ...newFilters, [key]: newValues },
|
||||||
|
cycleId.toString()
|
||||||
|
);
|
||||||
|
if (redirect) router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`);
|
||||||
|
},
|
||||||
|
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters, router]
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
cycle,
|
||||||
|
cycleId,
|
||||||
|
router,
|
||||||
|
handleFiltersUpdate,
|
||||||
|
cycleIssueDetails,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export default useCyclesDetails;
|
||||||
|
|
@ -8,7 +8,7 @@ import { useSearchParams } from "next/navigation";
|
||||||
import { AlertCircle, ChevronUp, ChevronDown } from "lucide-react";
|
import { AlertCircle, ChevronUp, ChevronDown } from "lucide-react";
|
||||||
import { Disclosure, Transition } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
import { ICycle, IIssueFilterOptions, TCyclePlotType, TProgressSnapshot } from "@plane/types";
|
import { ICycle, IIssueFilterOptions, TCyclePlotType, TProgressSnapshot } from "@plane/types";
|
||||||
import { CustomSelect, Spinner } from "@plane/ui";
|
import { CustomSelect, Loader, Spinner } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import ProgressChart from "@/components/core/sidebar/progress-chart";
|
import ProgressChart from "@/components/core/sidebar/progress-chart";
|
||||||
import { CycleProgressStats } from "@/components/cycles";
|
import { CycleProgressStats } from "@/components/cycles";
|
||||||
|
|
@ -231,7 +231,7 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
|
||||||
<span>Current</span>
|
<span>Current</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{cycleStartDate && cycleEndDate && completionChartDistributionData && (
|
{cycleStartDate && cycleEndDate && completionChartDistributionData ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{plotType === "points" ? (
|
{plotType === "points" ? (
|
||||||
<ProgressChart
|
<ProgressChart
|
||||||
|
|
@ -251,6 +251,10 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
) : (
|
||||||
|
<Loader className="w-full h-[160px] mt-4">
|
||||||
|
<Loader.Item width="100%" height="100%" />
|
||||||
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ import type {
|
||||||
ICycle,
|
ICycle,
|
||||||
TIssuesResponse,
|
TIssuesResponse,
|
||||||
IWorkspaceActiveCyclesResponse,
|
IWorkspaceActiveCyclesResponse,
|
||||||
IWorkspaceProgressResponse,
|
TCycleDistribution,
|
||||||
IWorkspaceAnalyticsResponse,
|
TProgressSnapshot,
|
||||||
|
TCycleEstimateDistribution,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||||
import { APIService } from "@/services/api.service";
|
import { APIService } from "@/services/api.service";
|
||||||
|
|
@ -20,7 +21,7 @@ export class CycleService extends APIService {
|
||||||
projectId: string,
|
projectId: string,
|
||||||
cycleId: string,
|
cycleId: string,
|
||||||
analytic_type: string = "points"
|
analytic_type: string = "points"
|
||||||
): Promise<IWorkspaceAnalyticsResponse> {
|
): Promise<TCycleDistribution | TCycleEstimateDistribution> {
|
||||||
return this.get(
|
return this.get(
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/analytics?type=${analytic_type}`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/analytics?type=${analytic_type}`
|
||||||
)
|
)
|
||||||
|
|
@ -34,7 +35,7 @@ export class CycleService extends APIService {
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
cycleId: string
|
cycleId: string
|
||||||
): Promise<IWorkspaceProgressResponse> {
|
): Promise<TProgressSnapshot> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/progress/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/progress/`)
|
||||||
.then((res) => res?.data)
|
.then((res) => res?.data)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,14 @@ import sortBy from "lodash/sortBy";
|
||||||
import { action, computed, observable, makeObservable, runInAction } from "mobx";
|
import { action, computed, observable, makeObservable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
// types
|
// types
|
||||||
import { ICycle, CycleDateCheckData, TCyclePlotType } from "@plane/types";
|
import {
|
||||||
|
ICycle,
|
||||||
|
CycleDateCheckData,
|
||||||
|
TCyclePlotType,
|
||||||
|
TProgressSnapshot,
|
||||||
|
TCycleEstimateDistribution,
|
||||||
|
TCycleDistribution,
|
||||||
|
} from "@plane/types";
|
||||||
// helpers
|
// helpers
|
||||||
import { orderCycles, shouldFilterCycle } from "@/helpers/cycle.helper";
|
import { orderCycles, shouldFilterCycle } from "@/helpers/cycle.helper";
|
||||||
import { getDate } from "@/helpers/date-time.helper";
|
import { getDate } from "@/helpers/date-time.helper";
|
||||||
|
|
@ -55,6 +62,13 @@ export interface ICycleStore {
|
||||||
fetchArchivedCycles: (workspaceSlug: string, projectId: string) => Promise<undefined | ICycle[]>;
|
fetchArchivedCycles: (workspaceSlug: string, projectId: string) => Promise<undefined | ICycle[]>;
|
||||||
fetchArchivedCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>;
|
fetchArchivedCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>;
|
||||||
fetchCycleDetails: (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>;
|
||||||
|
fetchActiveCycleAnalytics: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string,
|
||||||
|
analytic_type: string
|
||||||
|
) => Promise<TCycleDistribution | TCycleEstimateDistribution>;
|
||||||
// crud
|
// crud
|
||||||
createCycle: (workspaceSlug: string, projectId: string, data: Partial<ICycle>) => Promise<ICycle>;
|
createCycle: (workspaceSlug: string, projectId: string, data: Partial<ICycle>) => Promise<ICycle>;
|
||||||
updateCycleDetails: (
|
updateCycleDetails: (
|
||||||
|
|
@ -93,7 +107,7 @@ export class CycleStore implements ICycleStore {
|
||||||
// observables
|
// observables
|
||||||
loader: observable.ref,
|
loader: observable.ref,
|
||||||
cycleMap: observable,
|
cycleMap: observable,
|
||||||
plotType: observable.ref,
|
plotType: observable,
|
||||||
activeCycleIdMap: observable,
|
activeCycleIdMap: observable,
|
||||||
fetchedMap: observable,
|
fetchedMap: observable,
|
||||||
// computed
|
// computed
|
||||||
|
|
@ -113,6 +127,8 @@ export class CycleStore implements ICycleStore {
|
||||||
fetchActiveCycle: action,
|
fetchActiveCycle: action,
|
||||||
fetchArchivedCycles: action,
|
fetchArchivedCycles: action,
|
||||||
fetchArchivedCycleDetails: action,
|
fetchArchivedCycleDetails: action,
|
||||||
|
fetchActiveCycleProgress: action,
|
||||||
|
fetchActiveCycleAnalytics: action,
|
||||||
fetchCycleDetails: action,
|
fetchCycleDetails: action,
|
||||||
createCycle: action,
|
createCycle: action,
|
||||||
updateCycleDetails: action,
|
updateCycleDetails: action,
|
||||||
|
|
@ -403,6 +419,7 @@ export class CycleStore implements ICycleStore {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
response.forEach((cycle) => {
|
response.forEach((cycle) => {
|
||||||
set(this.cycleMap, [cycle.id], cycle);
|
set(this.cycleMap, [cycle.id], cycle);
|
||||||
|
cycle.status?.toLowerCase() === "current" && set(this.activeCycleIdMap, [cycle.id], true);
|
||||||
});
|
});
|
||||||
set(this.fetchedMap, projectId, true);
|
set(this.fetchedMap, projectId, true);
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
|
|
@ -457,6 +474,43 @@ export class CycleStore implements ICycleStore {
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description fetches active cycle progress
|
||||||
|
* @param workspaceSlug
|
||||||
|
* @param projectId
|
||||||
|
* @param cycleId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
fetchActiveCycleProgress = async (workspaceSlug: string, projectId: string, cycleId: string) =>
|
||||||
|
await this.cycleService.workspaceActiveCyclesProgress(workspaceSlug, projectId, cycleId).then((progress) => {
|
||||||
|
runInAction(() => {
|
||||||
|
set(this.cycleMap, [cycleId], { ...this.cycleMap[cycleId], ...progress });
|
||||||
|
});
|
||||||
|
return progress;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description fetches active cycle analytics
|
||||||
|
* @param workspaceSlug
|
||||||
|
* @param projectId
|
||||||
|
* @param cycleId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
fetchActiveCycleAnalytics = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string,
|
||||||
|
analytic_type: string
|
||||||
|
) =>
|
||||||
|
await this.cycleService
|
||||||
|
.workspaceActiveCyclesAnalytics(workspaceSlug, projectId, cycleId, analytic_type)
|
||||||
|
.then((cycle) => {
|
||||||
|
runInAction(() => {
|
||||||
|
set(this.cycleMap, [cycleId, analytic_type === "points" ? "estimate_distribution" : "distribution"], cycle);
|
||||||
|
});
|
||||||
|
return cycle;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description fetches cycle details
|
* @description fetches cycle details
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue