Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/types/src/cycle/cycle.d.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -61,6 +61,10 @@ export type TProgressSnapshot = {
estimate_distribution?: TCycleEstimateDistribution;
};

export interface IProjectDetails {
id: string;
}

export interface ICycle extends TProgressSnapshot {
progress_snapshot: TProgressSnapshot | undefined;

Expand All @@ -85,6 +89,7 @@ export interface ICycle extends TProgressSnapshot {
filters: IIssueFilterOptions;
};
workspace_id: string;
project_detail: IProjectDetails;
}

export interface CycleIssueResponse {
Expand All @@ -102,7 +107,7 @@ export interface CycleIssueResponse {
}

export type SelectCycleType =
| (ICycle & { actionType: "edit" | "delete" | "create-issue" })
| (ICycle & {actionType: "edit" | "delete" | "create-issue"})
| undefined;

export type CycleDateCheckData = {
Expand Down
27 changes: 25 additions & 2 deletions packages/types/src/workspace.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EUserWorkspaceRoles } from "@/constants/workspace";
import {EUserWorkspaceRoles} from "@/constants/workspace";
import type {
ICycle,
IProjectMember,
IUser,
IUserLite,
Expand Down Expand Up @@ -46,7 +47,7 @@ export interface IWorkspaceMemberInvitation {
}

export interface IWorkspaceBulkInviteFormData {
emails: { email: string; role: EUserWorkspaceRoles }[];
emails: {email: string; role: EUserWorkspaceRoles}[];
}

export type Properties = {
Expand Down Expand Up @@ -197,3 +198,25 @@ export interface IProductUpdateResponse {
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;
}
166 changes: 93 additions & 73 deletions web/core/components/cycles/active-cycle/cycle-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ import useLocalStorage from "@/hooks/use-local-storage";
export type ActiveCycleStatsProps = {
workspaceSlug: string;
projectId: string;
cycle: ICycle;
cycle: ICycle | null;
cycleId?: string | null;
};

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");

Expand Down Expand Up @@ -63,22 +64,29 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
const { currentProjectDetails } = useProject();

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,
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 = getActiveCycleById(cycle.id);
const cycleIssueDetails = cycleId ? getActiveCycleById(cycleId) : { nextPageResults: false };

const loadMoreIssues = useCallback(() => {
fetchNextActiveCycleIssues(workspaceSlug, projectId, cycle.id);
}, [workspaceSlug, projectId, cycle.id, issuesLoaderElement, cycleIssueDetails?.nextPageResults]);
if (!cycleId) return;
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%`);

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">
<Tab.Group
as={Fragment}
Expand Down Expand Up @@ -154,7 +162,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
ref={issuesContainerRef}
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.issueIds.map((issueId: string) => {
Expand Down Expand Up @@ -229,11 +237,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
</div>
)
) : (
<Loader className="space-y-3">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</Loader>
loaders
)}
</div>
</Tab.Panel>
Expand All @@ -242,78 +246,94 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
as="div"
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.distribution?.assignees?.map((assignee, index) => {
if (assignee.assignee_id)
return (
<SingleProgressStats
key={assignee.assignee_id}
title={
<div className="flex items-center gap-2">
<Avatar name={assignee?.display_name ?? undefined} src={assignee?.avatar ?? undefined} />
{cycle ? (
cycle?.distribution?.assignees && cycle.distribution.assignees.length > 0 ? (
cycle.distribution?.assignees?.map((assignee, index) => {
if (assignee.assignee_id)
return (
<SingleProgressStats
key={assignee.assignee_id}
title={
<div className="flex items-center gap-2">
<Avatar name={assignee?.display_name ?? undefined} src={assignee?.avatar ?? undefined} />

<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" />
<span>{assignee.display_name}</span>
</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">
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE} layout="screen-simple" size="sm" />
</div>
loaders
)}
</Tab.Panel>

<Tab.Panel
as="div"
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.distribution.labels?.map((label, index) => (
<SingleProgressStats
key={label.label_id ?? `no-label-${index}`}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full"
style={{
backgroundColor: label.color ?? "#000000",
}}
/>
<span className="text-xs">{label.label_name ?? "No labels"}</span>
</div>
}
completed={label.completed_issues}
total={label.total_issues}
/>
))
{cycle ? (
cycle?.distribution?.labels && cycle.distribution.labels.length > 0 ? (
cycle.distribution.labels?.map((label, index) => (
<SingleProgressStats
key={label.label_id ?? `no-label-${index}`}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full"
style={{
backgroundColor: label.color ?? "#000000",
}}
/>
<span className="text-xs">{label.label_name ?? "No labels"}</span>
</div>
}
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">
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_LABEL_EMPTY_STATE} layout="screen-simple" size="sm" />
</div>
loaders
)}
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</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>
);
});
13 changes: 9 additions & 4 deletions web/core/components/cycles/active-cycle/productivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FC, Fragment, useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { ICycle, TCyclePlotType } from "@plane/types";
import { CustomSelect, Spinner } from "@plane/ui";
import { CustomSelect, Loader, Spinner } from "@plane/ui";
// components
import ProgressChart from "@/components/core/sidebar/progress-chart";
import { EmptyState } from "@/components/empty-state";
Expand All @@ -15,7 +15,7 @@ import { EEstimateSystem } from "@/plane-web/constants/estimates";
export type ActiveCycleProductivityProps = {
workspaceSlug: string;
projectId: string;
cycle: ICycle;
cycle: ICycle | null;
};

const cycleBurnDownChartOptions = [
Expand Down Expand Up @@ -51,10 +51,11 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observe
isCurrentProjectEstimateEnabled && currentActiveEstimateId && estimateById(currentActiveEstimateId);
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;

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="relative flex items-center justify-between gap-4 -mt-7">
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}>
Expand Down Expand Up @@ -135,5 +136,9 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observe
)}
</Link>
</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>
);
});
Loading