[WEB-2007] fix: cycles loading optimization (#5207)
* fix: cycles loading optimization * fix: ts error * fix: types added along with apis * fix: formatting * fix: removed bottom border * fix: fixed loading state for cycle-stats --------- Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
This commit is contained in:
parent
2978593c63
commit
31fe9a1a02
8 changed files with 223 additions and 100 deletions
9
packages/types/src/cycle/cycle.d.ts
vendored
9
packages/types/src/cycle/cycle.d.ts
vendored
|
|
@ -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";
|
export type TCycleGroups = "current" | "upcoming" | "completed" | "draft";
|
||||||
|
|
||||||
|
|
@ -61,6 +61,10 @@ export type TProgressSnapshot = {
|
||||||
estimate_distribution?: TCycleEstimateDistribution;
|
estimate_distribution?: TCycleEstimateDistribution;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface IProjectDetails {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICycle extends TProgressSnapshot {
|
export interface ICycle extends TProgressSnapshot {
|
||||||
progress_snapshot: TProgressSnapshot | undefined;
|
progress_snapshot: TProgressSnapshot | undefined;
|
||||||
|
|
||||||
|
|
@ -85,6 +89,7 @@ export interface ICycle extends TProgressSnapshot {
|
||||||
filters: IIssueFilterOptions;
|
filters: IIssueFilterOptions;
|
||||||
};
|
};
|
||||||
workspace_id: string;
|
workspace_id: string;
|
||||||
|
project_detail: IProjectDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CycleIssueResponse {
|
export interface CycleIssueResponse {
|
||||||
|
|
@ -102,7 +107,7 @@ export interface CycleIssueResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SelectCycleType =
|
export type SelectCycleType =
|
||||||
| (ICycle & { actionType: "edit" | "delete" | "create-issue" })
|
| (ICycle & {actionType: "edit" | "delete" | "create-issue"})
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
export type CycleDateCheckData = {
|
export type CycleDateCheckData = {
|
||||||
|
|
|
||||||
27
packages/types/src/workspace.d.ts
vendored
27
packages/types/src/workspace.d.ts
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
import {EUserWorkspaceRoles} from "@/constants/workspace";
|
||||||
import type {
|
import type {
|
||||||
|
ICycle,
|
||||||
IProjectMember,
|
IProjectMember,
|
||||||
IUser,
|
IUser,
|
||||||
IUserLite,
|
IUserLite,
|
||||||
|
|
@ -46,7 +47,7 @@ export interface IWorkspaceMemberInvitation {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkspaceBulkInviteFormData {
|
export interface IWorkspaceBulkInviteFormData {
|
||||||
emails: { email: string; role: EUserWorkspaceRoles }[];
|
emails: {email: string; role: EUserWorkspaceRoles}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Properties = {
|
export type Properties = {
|
||||||
|
|
@ -197,3 +198,25 @@ export interface IProductUpdateResponse {
|
||||||
eyes: number;
|
eyes: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IWorkspaceActiveCyclesResponse {
|
||||||
|
count: number;
|
||||||
|
extra_stats: null;
|
||||||
|
next_cursor: string;
|
||||||
|
next_page_results: boolean;
|
||||||
|
prev_cursor: string;
|
||||||
|
prev_page_results: boolean;
|
||||||
|
results: ICycle[];
|
||||||
|
total_pages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWorkspaceProgressResponse {
|
||||||
|
completed_issues: number;
|
||||||
|
total_issues: number;
|
||||||
|
started_issues: number;
|
||||||
|
cancelled_issues: number;
|
||||||
|
unstarted_issues: number;
|
||||||
|
}
|
||||||
|
export interface IWorkspaceAnalyticsResponse {
|
||||||
|
completion_chart: any;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,12 @@ import useLocalStorage from "@/hooks/use-local-storage";
|
||||||
export type ActiveCycleStatsProps = {
|
export type ActiveCycleStatsProps = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
cycle: ICycle;
|
cycle: ICycle | null;
|
||||||
|
cycleId?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, cycle } = props;
|
const { workspaceSlug, projectId, cycle, cycleId } = props;
|
||||||
|
|
||||||
const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");
|
const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");
|
||||||
|
|
||||||
|
|
@ -63,22 +64,29 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && cycle.id ? CYCLE_ISSUES_WITH_PARAMS(cycle.id, { priority: "urgent,high" }) : null,
|
workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId, { priority: "urgent,high" }) : null,
|
||||||
workspaceSlug && projectId && cycle.id
|
workspaceSlug && projectId && cycleId ? () => fetchActiveCycleIssues(workspaceSlug, projectId, 30, cycleId) : null,
|
||||||
? () => fetchActiveCycleIssues(workspaceSlug, projectId, 30, cycle.id)
|
|
||||||
: null,
|
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const cycleIssueDetails = getActiveCycleById(cycle.id);
|
const cycleIssueDetails = cycleId ? getActiveCycleById(cycleId) : { nextPageResults: false };
|
||||||
|
|
||||||
const loadMoreIssues = useCallback(() => {
|
const loadMoreIssues = useCallback(() => {
|
||||||
fetchNextActiveCycleIssues(workspaceSlug, projectId, cycle.id);
|
if (!cycleId) return;
|
||||||
}, [workspaceSlug, projectId, cycle.id, issuesLoaderElement, cycleIssueDetails?.nextPageResults]);
|
fetchNextActiveCycleIssues(workspaceSlug, projectId, cycleId);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [workspaceSlug, projectId, cycleId, issuesLoaderElement, cycleIssueDetails?.nextPageResults]);
|
||||||
|
|
||||||
useIntersectionObserver(issuesContainerRef, issuesLoaderElement, loadMoreIssues, `0% 0% 100% 0%`);
|
useIntersectionObserver(issuesContainerRef, issuesLoaderElement, loadMoreIssues, `0% 0% 100% 0%`);
|
||||||
|
|
||||||
return (
|
const loaders = (
|
||||||
|
<Loader className="space-y-3">
|
||||||
|
<Loader.Item height="30px" />
|
||||||
|
<Loader.Item height="30px" />
|
||||||
|
<Loader.Item height="30px" />
|
||||||
|
</Loader>
|
||||||
|
);
|
||||||
|
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
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
|
|
@ -154,7 +162,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||||
ref={issuesContainerRef}
|
ref={issuesContainerRef}
|
||||||
className="flex flex-col gap-1 h-full w-full overflow-y-auto vertical-scrollbar scrollbar-sm"
|
className="flex flex-col gap-1 h-full w-full overflow-y-auto vertical-scrollbar scrollbar-sm"
|
||||||
>
|
>
|
||||||
{cycleIssueDetails && cycleIssueDetails.issueIds ? (
|
{cycleIssueDetails && "issueIds" in cycleIssueDetails ? (
|
||||||
cycleIssueDetails.issueCount > 0 ? (
|
cycleIssueDetails.issueCount > 0 ? (
|
||||||
<>
|
<>
|
||||||
{cycleIssueDetails.issueIds.map((issueId: string) => {
|
{cycleIssueDetails.issueIds.map((issueId: string) => {
|
||||||
|
|
@ -229,11 +237,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Loader className="space-y-3">
|
loaders
|
||||||
<Loader.Item height="50px" />
|
|
||||||
<Loader.Item height="50px" />
|
|
||||||
<Loader.Item height="50px" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
|
@ -242,44 +246,52 @@ 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?.distribution?.assignees && cycle.distribution.assignees.length > 0 ? (
|
{cycle ? (
|
||||||
cycle.distribution?.assignees?.map((assignee, index) => {
|
cycle?.distribution?.assignees && cycle.distribution.assignees.length > 0 ? (
|
||||||
if (assignee.assignee_id)
|
cycle.distribution?.assignees?.map((assignee, index) => {
|
||||||
return (
|
if (assignee.assignee_id)
|
||||||
<SingleProgressStats
|
return (
|
||||||
key={assignee.assignee_id}
|
<SingleProgressStats
|
||||||
title={
|
key={assignee.assignee_id}
|
||||||
<div className="flex items-center gap-2">
|
title={
|
||||||
<Avatar name={assignee?.display_name ?? undefined} src={assignee?.avatar ?? undefined} />
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar name={assignee?.display_name ?? undefined} src={assignee?.avatar ?? undefined} />
|
||||||
|
|
||||||
<span>{assignee.display_name}</span>
|
<span>{assignee.display_name}</span>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
completed={assignee.completed_issues}
|
|
||||||
total={assignee.total_issues}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
else
|
|
||||||
return (
|
|
||||||
<SingleProgressStats
|
|
||||||
key={`unassigned-${index}`}
|
|
||||||
title={
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-5 w-5 rounded-full border-2 border-custom-border-200 bg-custom-background-80">
|
|
||||||
<img src="/user.png" height="100%" width="100%" className="rounded-full" alt="User" />
|
|
||||||
</div>
|
</div>
|
||||||
<span>No assignee</span>
|
}
|
||||||
</div>
|
completed={assignee.completed_issues}
|
||||||
}
|
total={assignee.total_issues}
|
||||||
completed={assignee.completed_issues}
|
/>
|
||||||
total={assignee.total_issues}
|
);
|
||||||
/>
|
else
|
||||||
);
|
return (
|
||||||
})
|
<SingleProgressStats
|
||||||
|
key={`unassigned-${index}`}
|
||||||
|
title={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full border-2 border-custom-border-200 bg-custom-background-80">
|
||||||
|
<img src="/user.png" height="100%" width="100%" className="rounded-full" alt="User" />
|
||||||
|
</div>
|
||||||
|
<span>No assignee</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
completed={assignee.completed_issues}
|
||||||
|
total={assignee.total_issues}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full w-full">
|
||||||
|
<EmptyState
|
||||||
|
type={EmptyStateType.ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE}
|
||||||
|
layout="screen-simple"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full w-full">
|
loaders
|
||||||
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE} layout="screen-simple" size="sm" />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
|
||||||
|
|
@ -287,33 +299,41 @@ 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?.distribution?.labels && cycle.distribution.labels.length > 0 ? (
|
{cycle ? (
|
||||||
cycle.distribution.labels?.map((label, index) => (
|
cycle?.distribution?.labels && cycle.distribution.labels.length > 0 ? (
|
||||||
<SingleProgressStats
|
cycle.distribution.labels?.map((label, index) => (
|
||||||
key={label.label_id ?? `no-label-${index}`}
|
<SingleProgressStats
|
||||||
title={
|
key={label.label_id ?? `no-label-${index}`}
|
||||||
<div className="flex items-center gap-2">
|
title={
|
||||||
<span
|
<div className="flex items-center gap-2">
|
||||||
className="block h-3 w-3 rounded-full"
|
<span
|
||||||
style={{
|
className="block h-3 w-3 rounded-full"
|
||||||
backgroundColor: label.color ?? "#000000",
|
style={{
|
||||||
}}
|
backgroundColor: label.color ?? "#000000",
|
||||||
/>
|
}}
|
||||||
<span className="text-xs">{label.label_name ?? "No labels"}</span>
|
/>
|
||||||
</div>
|
<span className="text-xs">{label.label_name ?? "No labels"}</span>
|
||||||
}
|
</div>
|
||||||
completed={label.completed_issues}
|
}
|
||||||
total={label.total_issues}
|
completed={label.completed_issues}
|
||||||
/>
|
total={label.total_issues}
|
||||||
))
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full w-full">
|
||||||
|
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_LABEL_EMPTY_STATE} layout="screen-simple" size="sm" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full w-full">
|
loaders
|
||||||
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_LABEL_EMPTY_STATE} layout="screen-simple" size="sm" />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Loader className="flex flex-col gap-4 min-h-[17rem] overflow-hidden bg-custom-background-100 col-span-1 lg:col-span-2 xl:col-span-1">
|
||||||
|
<Loader.Item width="100%" height="17rem" />
|
||||||
|
</Loader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { FC, Fragment, useState } 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, 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 { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
|
|
@ -15,7 +15,7 @@ import { EEstimateSystem } from "@/plane-web/constants/estimates";
|
||||||
export type ActiveCycleProductivityProps = {
|
export type ActiveCycleProductivityProps = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
cycle: ICycle;
|
cycle: ICycle | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cycleBurnDownChartOptions = [
|
const cycleBurnDownChartOptions = [
|
||||||
|
|
@ -51,10 +51,11 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observe
|
||||||
isCurrentProjectEstimateEnabled && currentActiveEstimateId && estimateById(currentActiveEstimateId);
|
isCurrentProjectEstimateEnabled && currentActiveEstimateId && estimateById(currentActiveEstimateId);
|
||||||
const isCurrentEstimateTypeIsPoints = estimateDetails && estimateDetails?.type === EEstimateSystem.POINTS;
|
const isCurrentEstimateTypeIsPoints = estimateDetails && estimateDetails?.type === EEstimateSystem.POINTS;
|
||||||
|
|
||||||
const chartDistributionData = plotType === "points" ? cycle?.estimate_distribution : cycle?.distribution || undefined;
|
const chartDistributionData =
|
||||||
|
cycle && plotType === "points" ? cycle?.estimate_distribution : cycle?.distribution || undefined;
|
||||||
const completionChartDistributionData = chartDistributionData?.completion_chart || undefined;
|
const completionChartDistributionData = chartDistributionData?.completion_chart || undefined;
|
||||||
|
|
||||||
return (
|
return cycle ? (
|
||||||
<div className="flex flex-col justify-center 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 justify-center 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 -mt-7">
|
<div className="relative flex items-center justify-between gap-4 -mt-7">
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}>
|
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}>
|
||||||
|
|
@ -135,5 +136,9 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observe
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Loader className="flex flex-col min-h-[17rem] gap-5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
|
||||||
|
<Loader.Item width="100%" height="100%" />
|
||||||
|
</Loader>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import Link from "next/link";
|
||||||
// types
|
// types
|
||||||
import { ICycle } from "@plane/types";
|
import { ICycle } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { LinearProgressIndicator } from "@plane/ui";
|
import { LinearProgressIndicator, Loader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
// constants
|
// constants
|
||||||
|
|
@ -15,7 +15,7 @@ import { EmptyStateType } from "@/constants/empty-state";
|
||||||
export type ActiveCycleProgressProps = {
|
export type ActiveCycleProgressProps = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
cycle: ICycle;
|
cycle: ICycle | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
|
export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
|
||||||
|
|
@ -24,18 +24,20 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
|
||||||
const progressIndicatorData = CYCLE_STATE_GROUPS_DETAILS.map((group, index) => ({
|
const progressIndicatorData = CYCLE_STATE_GROUPS_DETAILS.map((group, index) => ({
|
||||||
id: index,
|
id: index,
|
||||||
name: group.title,
|
name: group.title,
|
||||||
value: 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 = {
|
const groupedIssues: any = cycle
|
||||||
completed: cycle.completed_issues,
|
? {
|
||||||
started: cycle.started_issues,
|
completed: cycle.completed_issues,
|
||||||
unstarted: cycle.unstarted_issues,
|
started: cycle.started_issues,
|
||||||
backlog: cycle.backlog_issues,
|
unstarted: cycle.unstarted_issues,
|
||||||
};
|
backlog: cycle.backlog_issues,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
return (
|
return cycle ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}
|
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}
|
||||||
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"
|
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"
|
||||||
|
|
@ -94,5 +96,9 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Loader className="flex flex-col min-h-[17rem] gap-5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
|
||||||
|
<Loader.Item width="100%" height="100%" />
|
||||||
|
</Loader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
||||||
// props
|
// props
|
||||||
const { workspaceSlug, projectId } = props;
|
const { workspaceSlug, projectId } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { fetchActiveCycle, currentProjectActiveCycleId, getActiveCycleById } = useCycle();
|
const { currentProjectActiveCycle, fetchActiveCycle, currentProjectActiveCycleId, getActiveCycleById } = useCycle();
|
||||||
// derived values
|
// derived values
|
||||||
const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
|
const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
|
||||||
// fetch active cycle details
|
// fetch active cycle details
|
||||||
|
|
@ -38,7 +38,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
||||||
);
|
);
|
||||||
|
|
||||||
// show loader if active cycle is loading
|
// show loader if active cycle is loading
|
||||||
if (!activeCycle && isLoading)
|
if (!currentProjectActiveCycle && isLoading)
|
||||||
return (
|
return (
|
||||||
<Loader>
|
<Loader>
|
||||||
<Loader.Item height="250px" />
|
<Loader.Item height="250px" />
|
||||||
|
|
@ -54,10 +54,10 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
||||||
<CycleListGroupHeader title="Active cycle" type="current" isExpanded={open} />
|
<CycleListGroupHeader title="Active cycle" type="current" isExpanded={open} />
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
{!activeCycle ? (
|
{!currentProjectActiveCycle ? (
|
||||||
<EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />
|
<EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col bg-custom-background-90 border-b">
|
<div className="flex flex-col bg-custom-background-90">
|
||||||
{currentProjectActiveCycleId && (
|
{currentProjectActiveCycleId && (
|
||||||
<CyclesListItem
|
<CyclesListItem
|
||||||
key={currentProjectActiveCycleId}
|
key={currentProjectActiveCycleId}
|
||||||
|
|
@ -75,7 +75,12 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
cycle={activeCycle}
|
cycle={activeCycle}
|
||||||
/>
|
/>
|
||||||
<ActiveCycleStats workspaceSlug={workspaceSlug} projectId={projectId} cycle={activeCycle} />
|
<ActiveCycleStats
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
cycle={activeCycle}
|
||||||
|
cycleId={currentProjectActiveCycleId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,64 @@
|
||||||
// services
|
// services
|
||||||
import type { CycleDateCheckData, ICycle, TIssuesResponse } from "@plane/types";
|
import type {
|
||||||
|
CycleDateCheckData,
|
||||||
|
ICycle,
|
||||||
|
TIssuesResponse,
|
||||||
|
IWorkspaceActiveCyclesResponse,
|
||||||
|
IWorkspaceProgressResponse,
|
||||||
|
IWorkspaceAnalyticsResponse,
|
||||||
|
} 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";
|
||||||
// types
|
|
||||||
// helpers
|
|
||||||
|
|
||||||
export class CycleService extends APIService {
|
export class CycleService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async workspaceActiveCyclesAnalytics(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string,
|
||||||
|
analytic_type: string = "points"
|
||||||
|
): Promise<IWorkspaceAnalyticsResponse> {
|
||||||
|
return this.get(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/analytics?type=${analytic_type}`
|
||||||
|
)
|
||||||
|
.then((res) => res?.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async workspaceActiveCyclesProgress(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string
|
||||||
|
): Promise<IWorkspaceProgressResponse> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/progress/`)
|
||||||
|
.then((res) => res?.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async workspaceActiveCycles(
|
||||||
|
workspaceSlug: string,
|
||||||
|
cursor: string,
|
||||||
|
per_page: number
|
||||||
|
): Promise<IWorkspaceActiveCyclesResponse> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/active-cycles/`, {
|
||||||
|
params: {
|
||||||
|
per_page,
|
||||||
|
cursor,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res?.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getWorkspaceCycles(workspaceSlug: string): Promise<ICycle[]> {
|
async getWorkspaceCycles(workspaceSlug: string): Promise<ICycle[]> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/cycles/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/cycles/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ export interface ICycleStore {
|
||||||
currentProjectDraftCycleIds: string[] | null;
|
currentProjectDraftCycleIds: string[] | null;
|
||||||
currentProjectActiveCycleId: string | null;
|
currentProjectActiveCycleId: string | null;
|
||||||
currentProjectArchivedCycleIds: string[] | null;
|
currentProjectArchivedCycleIds: string[] | null;
|
||||||
|
currentProjectActiveCycle: ICycle | null;
|
||||||
|
|
||||||
// computed actions
|
// computed actions
|
||||||
getFilteredCycleIds: (projectId: string, sortByManual: boolean) => string[] | null;
|
getFilteredCycleIds: (projectId: string, sortByManual: boolean) => string[] | null;
|
||||||
getFilteredCompletedCycleIds: (projectId: string) => string[] | null;
|
getFilteredCompletedCycleIds: (projectId: string) => string[] | null;
|
||||||
|
|
@ -100,6 +102,8 @@ export class CycleStore implements ICycleStore {
|
||||||
currentProjectDraftCycleIds: computed,
|
currentProjectDraftCycleIds: computed,
|
||||||
currentProjectActiveCycleId: computed,
|
currentProjectActiveCycleId: computed,
|
||||||
currentProjectArchivedCycleIds: computed,
|
currentProjectArchivedCycleIds: computed,
|
||||||
|
currentProjectActiveCycle: computed,
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
setPlotType: action,
|
setPlotType: action,
|
||||||
fetchWorkspaceCycles: action,
|
fetchWorkspaceCycles: action,
|
||||||
|
|
@ -208,7 +212,7 @@ export class CycleStore implements ICycleStore {
|
||||||
get currentProjectActiveCycleId() {
|
get currentProjectActiveCycleId() {
|
||||||
const projectId = this.rootStore.router.projectId;
|
const projectId = this.rootStore.router.projectId;
|
||||||
if (!projectId) return null;
|
if (!projectId) return null;
|
||||||
const activeCycle = Object.keys(this.activeCycleIdMap ?? {}).find(
|
const activeCycle = Object.keys(this.cycleMap ?? {}).find(
|
||||||
(cycleId) => this.cycleMap?.[cycleId]?.project_id === projectId
|
(cycleId) => this.cycleMap?.[cycleId]?.project_id === projectId
|
||||||
);
|
);
|
||||||
return activeCycle || null;
|
return activeCycle || null;
|
||||||
|
|
@ -228,6 +232,12 @@ export class CycleStore implements ICycleStore {
|
||||||
return archivedCycleIds;
|
return archivedCycleIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get currentProjectActiveCycle() {
|
||||||
|
const projectId = this.rootStore.router.projectId;
|
||||||
|
if (!projectId && !this.currentProjectActiveCycleId) return null;
|
||||||
|
return this.cycleMap?.[this.currentProjectActiveCycleId!] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description returns filtered cycle ids based on display filters and filters
|
* @description returns filtered cycle ids based on display filters and filters
|
||||||
* @param {TCycleDisplayFilters} displayFilters
|
* @param {TCycleDisplayFilters} displayFilters
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue