chore: update cycle and module stats logic (#1323)

* refactor: cycles stats

* chore: show assignee avatar in stats

* chore: cycles and modules sidebar stats refactor

* fix: build errors
This commit is contained in:
Aaryan Khandelwal 2023-06-20 16:32:02 +05:30 committed by GitHub
parent d7097330ef
commit cf8c902473
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 297 additions and 401 deletions

View File

@ -80,7 +80,7 @@ export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue,
console.error(e);
});
},
[workspaceSlug, issueId, projectId]
[workspaceSlug, issueId, projectId, user]
);
const handleIssueAssignees = (assignee: string) => {

View File

@ -51,7 +51,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
console.error(e);
});
},
[workspaceSlug, issueId, projectId]
[workspaceSlug, issueId, projectId, user]
);
const handleIssueState = (priority: string | null) => {

View File

@ -63,7 +63,7 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, use
console.error(e);
});
},
[workspaceSlug, issueId, projectId, mutateIssueDetails]
[workspaceSlug, issueId, projectId, mutateIssueDetails, user]
);
const handleIssueState = (stateId: string) => {

View File

@ -181,9 +181,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
mutate(MODULE_DETAILS(moduleId as string));
} else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
})
.catch((error) => {
console.log(error);
});
},
[

View File

@ -3,16 +3,15 @@ import React from "react";
// ui
import { LineGraph } from "components/ui";
// helpers
import { getDatesInRange, renderShortNumericDateFormat } from "helpers/date-time.helper";
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
//types
import { IIssue } from "types";
import { TCompletionChartDistribution } from "types";
type Props = {
issues: IIssue[];
start: string;
end: string;
width?: number;
height?: number;
distribution: TCompletionChartDistribution;
startDate: string | Date;
endDate: string | Date;
totalIssues: number;
};
const styleById = {
@ -41,32 +40,11 @@ const DashedLine = ({ series, lineGenerator, xScale, yScale }: any) =>
/>
));
const ProgressChart: React.FC<Props> = ({ issues, start, end }) => {
const startDate = new Date(start);
const endDate = new Date(end);
const getChartData = () => {
const dateRangeArray = getDatesInRange(startDate, endDate);
let count = 0;
const dateWiseData = dateRangeArray.map((d) => {
const current = d.toISOString().split("T")[0];
const total = issues.length;
const currentData = issues.filter(
(i) => i.completed_at && i.completed_at.toString().split("T")[0] === current
);
count = currentData ? currentData.length + count : count;
return {
currentDate: renderShortNumericDateFormat(current),
currentDateData: currentData,
pending: new Date(current) < new Date() ? total - count : null,
};
});
return dateWiseData;
};
const chartData = getChartData();
const ProgressChart: React.FC<Props> = ({ distribution, startDate, endDate, totalIssues }) => {
const chartData = Object.keys(distribution).map((key) => ({
currentDate: renderShortNumericDateFormat(key),
pending: distribution[key],
}));
return (
<div className="w-full flex justify-center items-center">
@ -74,7 +52,7 @@ const ProgressChart: React.FC<Props> = ({ issues, start, end }) => {
animate
curve="monotoneX"
height="160px"
width="360px"
width="100%"
enableGridY={false}
lineWidth={1}
margin={{ top: 30, right: 30, bottom: 30, left: 30 }}
@ -97,7 +75,7 @@ const ProgressChart: React.FC<Props> = ({ issues, start, end }) => {
data: [
{
x: chartData[0].currentDate,
y: issues.length,
y: totalIssues,
},
{
x: chartData[chartData.length - 1].currentDate,
@ -113,10 +91,10 @@ const ProgressChart: React.FC<Props> = ({ issues, start, end }) => {
enablePoints={false}
enableArea
colors={(datum) => datum.color ?? "#3F76FF"}
customYAxisTickValues={[0, issues.length]}
customYAxisTickValues={[0, totalIssues]}
gridXValues={chartData.map((item, index) => (index % 2 === 0 ? item.currentDate : ""))}
theme={{
background: "rgb(var(--color-bg-sidebar))",
background: "transparent",
axis: {
domain: {
line: {

View File

@ -1,15 +1,7 @@
import React from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Tab } from "@headlessui/react";
// services
import issuesServices from "services/issues.service";
import projectService from "services/project.service";
// hooks
import useLocalStorage from "hooks/use-local-storage";
import useIssuesView from "hooks/use-issues-view";
@ -17,61 +9,43 @@ import useIssuesView from "hooks/use-issues-view";
import { SingleProgressStats } from "components/core";
// ui
import { Avatar } from "components/ui";
// icons
import User from "public/user.png";
// types
import { IIssue, IIssueLabels, IModule, UserAuth } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
import {
IModule,
TAssigneesDistribution,
TCompletionChartDistribution,
TLabelsDistribution,
} from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
// types
type Props = {
groupedIssues: any;
issues: IIssue[];
distribution: {
assignees: TAssigneesDistribution[];
completion_chart: TCompletionChartDistribution;
labels: TLabelsDistribution[];
};
groupedIssues: {
[key: string]: number;
};
totalIssues: number;
module?: IModule;
userAuth?: UserAuth;
roundedTab?: boolean;
noBackground?: boolean;
};
const stateGroupColours: {
[key: string]: string;
} = {
backlog: "#3f76ff",
unstarted: "#ff9e9e",
started: "#d687ff",
cancelled: "#ff5353",
completed: "#096e8d",
};
export const SidebarProgressStats: React.FC<Props> = ({
distribution,
groupedIssues,
issues,
totalIssues,
module,
userAuth,
roundedTab,
noBackground,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { filters, setFilters } = useIssuesView();
const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees");
const { data: issueLabels } = useSWR<IIssueLabels[]>(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
workspaceSlug && projectId
? () => issuesServices.getIssueLabels(workspaceSlug as string, projectId as string)
: null
);
const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(workspaceSlug as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const currentValue = (tab: string | null) => {
switch (tab) {
case "Assignees":
@ -85,6 +59,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
return 0;
}
};
return (
<Tab.Group
defaultIndex={currentValue(tab)}
@ -144,100 +119,93 @@ export const SidebarProgressStats: React.FC<Props> = ({
</Tab>
</Tab.List>
<Tab.Panels className="flex w-full items-center justify-between pt-1 text-brand-secondary">
<Tab.Panel as="div" className="flex w-full flex-col text-xs">
{members?.map((member, index) => {
const totalArray = issues?.filter((i) => i?.assignees?.includes(member.member.id));
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
if (totalArray.length > 0) {
return (
<SingleProgressStats
key={index}
title={
<>
<Avatar user={member.member} />
<span>{member.member.first_name}</span>
</>
}
completed={completeArray.length}
total={totalArray.length}
onClick={() => {
if (filters?.assignees?.includes(member.member.id))
setFilters({
assignees: filters?.assignees?.filter((a) => a !== member.member.id),
});
else
setFilters({ assignees: [...(filters?.assignees ?? []), member.member.id] });
}}
selected={filters?.assignees?.includes(member.member.id)}
/>
);
}
})}
{issues?.filter((i) => i?.assignees?.length === 0).length > 0 ? (
<SingleProgressStats
title={
<>
<div className="h-5 w-5 rounded-full border-2 border-white bg-brand-surface-2">
<Image
src={User}
height="100%"
width="100%"
className="rounded-full"
alt="User"
/>
</div>
<span>No assignee</span>
</>
}
completed={
issues?.filter(
(i) => i?.state_detail.group === "completed" && i.assignees?.length === 0
).length
}
total={issues?.filter((i) => i?.assignees?.length === 0).length}
/>
) : (
""
)}
</Tab.Panel>
<Tab.Panel as="div" className="w-full space-y-1">
{issueLabels?.map((label, index) => {
const totalArray = issues?.filter((i) => i?.labels?.includes(label.id));
const completeArray = totalArray?.filter((i) => i?.state_detail.group === "completed");
if (totalArray.length > 0) {
{distribution.assignees.map((assignee, index) => {
if (assignee.assignee_id)
return (
<SingleProgressStats
key={index}
key={assignee.assignee_id}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full"
style={{
backgroundColor:
label.color && label.color !== "" ? label.color : "#000000",
<Avatar
user={{
id: assignee.assignee_id,
avatar: assignee.avatar ?? "",
first_name: assignee.first_name ?? "",
last_name: assignee.last_name ?? "",
}}
/>
<span className="text-xs capitalize">{label?.name}</span>
<span>{assignee.first_name}</span>
</div>
}
completed={completeArray.length}
total={totalArray.length}
completed={assignee.completed_issues}
total={assignee.total_issues}
onClick={() => {
if (filters.labels?.includes(label.id))
if (filters?.assignees?.includes(assignee.assignee_id ?? ""))
setFilters({
labels: filters?.labels?.filter((l) => l !== label.id),
assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id),
});
else
setFilters({
assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""],
});
else setFilters({ labels: [...(filters?.labels ?? []), label.id] });
}}
selected={filters?.labels?.includes(label.id)}
selected={filters?.assignees?.includes(assignee.assignee_id ?? "")}
/>
);
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-brand-base bg-brand-surface-2">
<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}
/>
);
}
})}
</Tab.Panel>
<Tab.Panel as="div" className="flex w-full flex-col ">
<Tab.Panel as="div" className="w-full space-y-1">
{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 ?? "transparent",
}}
/>
<span className="text-xs">{label.label_name ?? "No labels"}</span>
</div>
}
completed={label.completed_issues}
total={label.total_issues}
onClick={() => {
if (filters.labels?.includes(label.label_id ?? ""))
setFilters({
labels: filters?.labels?.filter((l) => l !== label.label_id),
});
else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] });
}}
selected={filters?.labels?.includes(label.label_id ?? "")}
/>
))}
</Tab.Panel>
<Tab.Panel as="div" className="w-full space-y-1">
{Object.keys(groupedIssues).map((group, index) => (
<SingleProgressStats
key={index}
@ -246,14 +214,14 @@ export const SidebarProgressStats: React.FC<Props> = ({
<span
className="block h-3 w-3 rounded-full "
style={{
backgroundColor: stateGroupColours[group],
backgroundColor: STATE_GROUP_COLORS[group],
}}
/>
<span className="text-xs capitalize">{group}</span>
</div>
}
completed={groupedIssues[group]}
total={issues.length}
total={totalIssues}
/>
))}
</Tab.Panel>

View File

@ -23,10 +23,10 @@ export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
} ${selected ? "bg-brand-surface-1" : ""}`}
onClick={onClick}
>
<div className="flex w-1/2 items-center justify-start gap-2">{title}</div>
<div className="w-1/2">{title}</div>
<div className="flex w-1/2 items-center justify-end gap-1 px-2">
<div className="flex h-5 items-center justify-center gap-1">
<span className="h-4 w-4 ">
<span className="h-4 w-4">
<ProgressBar value={completed} maxValue={total} />
</span>
<span className="w-8 text-right">
@ -36,8 +36,7 @@ export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
%
</span>
</div>
<span>of</span>
<span>{total}</span>
<span>of {total}</span>
</div>
</div>
);

View File

@ -10,7 +10,7 @@ import cyclesService from "services/cycles.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { LinearProgressIndicator, Tooltip } from "components/ui";
import { LinearProgressIndicator, Loader, Tooltip } from "components/ui";
import { AssigneesList } from "components/ui/avatar";
import { SingleProgressStats } from "components/core";
// components
@ -43,10 +43,6 @@ import { ICycle, IIssue } from "types";
// fetch-keys
import { CURRENT_CYCLE_LIST, CYCLES_LIST, CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
type TSingleStatProps = {
cycle: ICycle;
};
const stateGroups = [
{
key: "backlog_issues",
@ -75,12 +71,43 @@ const stateGroups = [
},
];
export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle }) => {
export const ActiveCycleDetails: React.FC = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const { data: currentCycle } = useSWR(
workspaceSlug && projectId ? CURRENT_CYCLE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () =>
cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, "current")
: null
);
const cycle = currentCycle ? currentCycle[0] : null;
const { data: issues } = useSWR(
workspaceSlug && projectId && cycle?.id
? CYCLE_ISSUES_WITH_PARAMS(cycle?.id, { priority: "urgent,high" })
: null,
workspaceSlug && projectId && cycle?.id
? () =>
cyclesService.getCycleIssuesWithParams(
workspaceSlug as string,
projectId as string,
cycle.id,
{ priority: "urgent,high" }
)
: null
) as { data: IIssue[] | undefined };
if (!cycle)
return (
<div className="flex w-full items-center justify-start rounded-[10px] bg-brand-surface-2 px-6 py-4">
<h3 className="text-base font-medium text-brand-base ">No active cycle is present.</h3>
</div>
);
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
@ -164,21 +191,6 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle }) => {
});
};
const { data: issues } = useSWR(
workspaceSlug && projectId && cycle.id
? CYCLE_ISSUES_WITH_PARAMS(cycle.id, { priority: "high" })
: null,
workspaceSlug && projectId && cycle.id
? () =>
cyclesService.getCycleIssuesWithParams(
workspaceSlug as string,
projectId as string,
cycle.id,
{ priority: "high" }
)
: null
) as { data: IIssue[] };
const progressIndicatorData = stateGroups.map((group, index) => ({
id: index,
name: group.title,
@ -193,7 +205,7 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle }) => {
<div className="grid-row-2 grid rounded-[10px] shadow divide-y bg-brand-base border border-brand-base">
<div className="grid grid-cols-1 divide-y border-brand-base lg:divide-y-0 lg:divide-x lg:grid-cols-3">
<div className="flex flex-col text-xs">
<a className="h-full w-full">
<div className="h-full w-full">
<div className="flex h-60 flex-col gap-5 justify-between rounded-b-[10px] p-4">
<div className="flex items-center justify-between gap-1">
<span className="flex items-center gap-1">
@ -341,7 +353,7 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle }) => {
</a>
</Link>
</div>
</a>
</div>
</div>
<div className="grid col-span-2 grid-cols-1 divide-y border-brand-base md:divide-y-0 md:divide-x md:grid-cols-2">
<div className="flex h-60 flex-col border-brand-base">
@ -373,19 +385,17 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle }) => {
</div>
</div>
<div className="border-brand-base h-60 overflow-y-scroll">
<ActiveCycleProgressStats issues={issues ?? []} />
<ActiveCycleProgressStats cycle={cycle} />
</div>
</div>
</div>
<div className="grid grid-cols-1 divide-y border-brand-base lg:divide-y-0 lg:divide-x lg:grid-cols-2">
<div className="flex flex-col justify-between p-4">
<div>
<div className="text-brand-primary mb-2">High Priority Issues</div>
<div className="mb-2 flex max-h-[240px] min-h-[240px] flex-col gap-2.5 overflow-y-scroll rounded-md">
{issues
?.filter((issue) => issue.priority === "urgent" || issue.priority === "high")
.map((issue) => (
<div className="text-brand-primary">High Priority Issues</div>
<div className="my-3 flex max-h-[240px] min-h-[240px] flex-col gap-2.5 overflow-y-scroll rounded-md">
{issues ? (
issues.map((issue) => (
<div
key={issue.id}
className="flex flex-wrap rounded-md items-center justify-between gap-2 border border-brand-base bg-brand-surface-1 px-3 py-1.5"
@ -414,16 +424,10 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle }) => {
<div className="flex items-center gap-1.5">
<div
className={`grid h-6 w-6 place-items-center items-center rounded border shadow-sm ${
className={`grid h-6 w-6 place-items-center items-center rounded border shadow-sm flex-shrink-0 ${
issue.priority === "urgent"
? "border-red-500/20 bg-red-500/20 text-red-500"
: issue.priority === "high"
? "border-orange-500/20 bg-orange-500/20 text-orange-500"
: issue.priority === "medium"
? "border-yellow-500/20 bg-yellow-500/20 text-yellow-500"
: issue.priority === "low"
? "border-green-500/20 bg-green-500/20 text-green-500"
: "border-brand-base"
: "border-orange-500/20 bg-orange-500/20 text-orange-500"
}`}
>
{getPriorityIcon(issue.priority, "text-sm")}
@ -455,7 +459,7 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle }) => {
Array.isArray(issue.assignees) ? (
<div className="-my-0.5 flex items-center justify-center gap-2">
<AssigneesList
userIds={issue.assignees}
users={issue.assignee_details}
length={3}
showLength={false}
/>
@ -466,7 +470,14 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle }) => {
</div>
</div>
</div>
))}
))
) : (
<Loader className="space-y-3">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</Loader>
)}
</div>
</div>
@ -478,33 +489,17 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle }) => {
width:
issues &&
`${
(issues?.filter(
(issue) =>
issue?.state_detail?.group === "completed" &&
(issue?.priority === "urgent" || issue?.priority === "high")
)?.length /
issues?.filter(
(issue) => issue?.priority === "urgent" || issue?.priority === "high"
)?.length) *
(issues.filter((issue) => issue?.state_detail?.group === "completed")
?.length /
issues.length) *
100 ?? 0
}%`,
}}
/>
</div>
<div className="w-16 text-end text-xs text-brand-secondary">
{
issues?.filter(
(issue) =>
issue?.state_detail?.group === "completed" &&
(issue?.priority === "urgent" || issue?.priority === "high")
)?.length
}{" "}
of{" "}
{
issues?.filter(
(issue) => issue?.priority === "urgent" || issue?.priority === "high"
)?.length
}
{issues?.filter((issue) => issue?.state_detail?.group === "completed")?.length} of{" "}
{issues?.length}
</div>
</div>
</div>
@ -512,11 +507,11 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle }) => {
<div className="flex items-start justify-between gap-4 py-1.5 text-xs">
<div className="flex items-center gap-3 text-brand-base">
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
<span className="h-2.5 w-2.5 rounded-full bg-[#a9bbd0]" />
<span>Ideal</span>
</div>
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
<span className="h-2.5 w-2.5 rounded-full bg-[#4c8fff]" />
<span>Current</span>
</div>
</div>
@ -532,11 +527,10 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle }) => {
</div>
<div className="relative h-64">
<ProgressChart
issues={issues ?? []}
start={cycle?.start_date ?? ""}
end={cycle?.end_date ?? ""}
width={475}
height={256}
distribution={cycle.distribution.completion_chart}
startDate={cycle.start_date ?? ""}
endDate={cycle.end_date ?? ""}
totalIssues={cycle.total_issues}
/>
</div>
</div>

View File

@ -1,14 +1,7 @@
import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Tab } from "@headlessui/react";
// services
import issuesServices from "services/issues.service";
import projectService from "services/project.service";
// hooks
import useLocalStorage from "hooks/use-local-storage";
// components
@ -16,34 +9,15 @@ import { SingleProgressStats } from "components/core";
// ui
import { Avatar } from "components/ui";
// types
import { IIssue, IIssueLabels } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
import { ICycle } from "types";
// types
type Props = {
issues: IIssue[];
cycle: ICycle;
};
export const ActiveCycleProgressStats: React.FC<Props> = ({ issues }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
export const ActiveCycleProgressStats: React.FC<Props> = ({ cycle }) => {
const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");
const { data: issueLabels } = useSWR<IIssueLabels[]>(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
workspaceSlug && projectId
? () => issuesServices.getIssueLabels(workspaceSlug as string, projectId as string)
: null
);
const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(workspaceSlug as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const currentValue = (tab: string | null) => {
switch (tab) {
case "Assignees":
@ -55,6 +29,7 @@ export const ActiveCycleProgressStats: React.FC<Props> = ({ issues }) => {
return 0;
}
};
return (
<Tab.Group
defaultIndex={currentValue(tab)}
@ -98,83 +73,74 @@ export const ActiveCycleProgressStats: React.FC<Props> = ({ issues }) => {
as="div"
className="flex flex-col w-full mt-2 gap-1 overflow-y-scroll items-center text-brand-secondary"
>
{members?.map((member, index) => {
const totalArray = issues?.filter((i) => i?.assignees?.includes(member.member.id));
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
if (totalArray.length > 0) {
{cycle.distribution.assignees.map((assignee, index) => {
if (assignee.assignee_id)
return (
<SingleProgressStats
key={index}
key={assignee.assignee_id}
title={
<div className="flex items-center gap-2">
<Avatar user={member.member} />
<span>{member.member.first_name}</span>
<Avatar
user={{
id: assignee.assignee_id,
avatar: assignee.avatar ?? "",
first_name: assignee.first_name ?? "",
last_name: assignee.last_name ?? "",
}}
/>
<span>{assignee.first_name}</span>
</div>
}
completed={completeArray.length}
total={totalArray.length}
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-brand-base bg-brand-surface-2">
<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}
/>
);
}
})}
{issues?.filter((i) => i?.assignees?.length === 0).length > 0 ? (
<SingleProgressStats
title={
<div className="flex items-center gap-2">
<div className="h-5 w-5 rounded-full border-2 border-brand-base bg-brand-surface-2">
<img
src="/user.png"
height="100%"
width="100%"
className="rounded-full"
alt="User"
/>
</div>
<span>No assignee</span>
</div>
}
completed={
issues?.filter(
(i) => i?.state_detail.group === "completed" && i.assignees?.length === 0
).length
}
total={issues?.filter((i) => i?.assignees?.length === 0).length}
/>
) : (
""
)}
</Tab.Panel>
<Tab.Panel
as="div"
className="flex flex-col w-full mt-2 gap-1 overflow-y-scroll items-center text-brand-secondary"
>
{issueLabels?.map((label, index) => {
const totalArray = issues?.filter((i) => i?.labels?.includes(label.id));
const completeArray = totalArray?.filter((i) => i?.state_detail.group === "completed");
if (totalArray.length > 0) {
return (
<SingleProgressStats
key={index}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full"
style={{
backgroundColor:
label.color && label.color !== "" ? label.color : "#000000",
}}
/>
<span className="text-xs capitalize">{label?.name}</span>
</div>
}
completed={completeArray.length}
total={totalArray.length}
/>
);
}
})}
{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 ?? "transparent",
}}
/>
<span className="text-xs">{label.label_name ?? "No labels"}</span>
</div>
}
completed={label.completed_issues}
total={label.total_issues}
/>
))}
</Tab.Panel>
</Tab.Panels>
</Tab.Group>

View File

@ -2,11 +2,22 @@ import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
// headless ui
import { Disclosure, Popover, Transition } from "@headlessui/react";
// services
import cyclesService from "services/cycles.service";
// hooks
import useToast from "hooks/use-toast";
// components
import { SidebarProgressStats } from "components/core";
import ProgressChart from "components/core/sidebar/progress-chart";
import { DeleteCycleModal } from "components/cycles";
// ui
import { CustomMenu, CustomRangeDatePicker, Loader, ProgressBar } from "components/ui";
// icons
import {
CalendarDaysIcon,
@ -18,17 +29,6 @@ import {
DocumentIcon,
LinkIcon,
} from "@heroicons/react/24/outline";
// ui
import { CustomMenu, CustomRangeDatePicker, Loader, ProgressBar } from "components/ui";
// hooks
import useToast from "hooks/use-toast";
// services
import cyclesService from "services/cycles.service";
// components
import { SidebarProgressStats } from "components/core";
import ProgressChart from "components/core/sidebar/progress-chart";
import { DeleteCycleModal } from "components/cycles";
// icons
import { ExclamationIcon } from "components/icons";
// helpers
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
@ -38,9 +38,9 @@ import {
renderShortDate,
} from "helpers/date-time.helper";
// types
import { ICurrentUserResponse, ICycle, IIssue } from "types";
import { ICurrentUserResponse, ICycle } from "types";
// fetch-keys
import { CYCLE_DETAILS, CYCLE_ISSUES } from "constants/fetch-keys";
import { CYCLE_DETAILS } from "constants/fetch-keys";
type Props = {
cycle: ICycle | undefined;
@ -69,18 +69,6 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
end_date: new Date().toString(),
};
const { data: issues } = useSWR<IIssue[]>(
workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null,
workspaceSlug && projectId && cycleId
? () =>
cyclesService.getCycleIssues(
workspaceSlug as string,
projectId as string,
cycleId as string
)
: null
);
const { setValue, reset, watch } = useForm({
defaultValues,
});
@ -553,9 +541,10 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</div>
<div className="relative">
<ProgressChart
issues={issues ?? []}
start={cycle?.start_date ?? ""}
end={cycle?.end_date ?? ""}
distribution={cycle.distribution.completion_chart}
startDate={cycle.start_date ?? ""}
endDate={cycle.end_date ?? ""}
totalIssues={cycle.total_issues}
/>
</div>
</div>
@ -604,7 +593,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
{cycle.total_issues > 0 ? (
<div className="h-full w-full py-4">
<SidebarProgressStats
issues={issues ?? []}
distribution={cycle.distribution}
groupedIssues={{
backlog: cycle.backlog_issues,
unstarted: cycle.unstarted_issues,
@ -612,6 +601,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
completed: cycle.completed_issues,
cancelled: cycle.cancelled_issues,
}}
totalIssues={cycle.total_issues}
/>
</div>
) : (

View File

@ -52,20 +52,13 @@ const defaultValues: Partial<IModule> = {
};
type Props = {
issues: IIssue[];
module?: IModule;
isOpen: boolean;
moduleIssues?: IIssue[];
user: ICurrentUserResponse | undefined;
};
export const ModuleDetailsSidebar: React.FC<Props> = ({
issues,
module,
isOpen,
moduleIssues,
user,
}) => {
export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIssues, user }) => {
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
const [moduleLinkModal, setModuleLinkModal] = useState(false);
@ -464,9 +457,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
</div>
<div className="relative h-40 w-80">
<ProgressChart
issues={issues}
start={module?.start_date ?? ""}
end={module?.target_date ?? ""}
distribution={module.distribution.completion_chart}
startDate={module.start_date ?? ""}
endDate={module.target_date ?? ""}
totalIssues={module.total_issues}
/>
</div>
</div>
@ -517,7 +511,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
<>
<div className=" h-full w-full py-4">
<SidebarProgressStats
issues={issues}
distribution={module.distribution}
groupedIssues={{
backlog: module.backlog_issues,
unstarted: module.unstarted_issues,
@ -525,7 +519,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
completed: module.completed_issues,
cancelled: module.cancelled_issues,
}}
userAuth={memberRole}
totalIssues={module.total_issues}
module={module}
/>
</div>

View File

@ -13,7 +13,7 @@ import { IUser, IUserLite } from "types";
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
type AvatarProps = {
user?: Partial<IUser> | Partial<IUserLite> | IUser | IUserLite | undefined | null;
user?: Partial<IUser> | Partial<IUserLite> | null;
index?: number;
height?: string;
width?: string;

View File

@ -32,7 +32,7 @@ import { ListBulletIcon, PlusIcon, Squares2X2Icon } from "@heroicons/react/24/ou
import { SelectCycleType } from "types";
import type { NextPage } from "next";
// fetch-keys
import { CURRENT_CYCLE_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
import { PROJECT_DETAILS } from "constants/fetch-keys";
const tabsList = ["All", "Active", "Upcoming", "Completed", "Drafts"];
@ -72,14 +72,6 @@ const ProjectCycles: NextPage = () => {
: null
);
const { data: currentCycle } = useSWR(
workspaceSlug && projectId ? CURRENT_CYCLE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () =>
cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, "current")
: null
);
useEffect(() => {
if (createUpdateCycleModal) return;
const timer = setTimeout(() => {
@ -201,15 +193,7 @@ const ProjectCycles: NextPage = () => {
</Tab.Panel>
{cyclesView !== "gantt_chart" && (
<Tab.Panel as="div" className="mt-7 space-y-5 h-full overflow-y-auto">
{currentCycle?.[0] ? (
<ActiveCycleDetails cycle={currentCycle?.[0]} />
) : (
<div className="flex w-full items-center justify-start rounded-[10px] bg-brand-surface-2 px-6 py-4">
<h3 className="text-base font-medium text-brand-base ">
No active cycle is present.
</h3>
</div>
)}
<ActiveCycleDetails />
</Tab.Panel>
)}
<Tab.Panel as="div" className="h-full overflow-y-auto">

View File

@ -5,13 +5,7 @@ import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// icons
import {
ArrowLeftIcon,
ListBulletIcon,
PlusIcon,
RectangleGroupIcon,
RectangleStackIcon,
} from "@heroicons/react/24/outline";
import { ArrowLeftIcon, RectangleGroupIcon } from "@heroicons/react/24/outline";
// services
import modulesService from "services/modules.service";
import issuesService from "services/issues.service";
@ -191,7 +185,6 @@ const SingleModule: React.FC = () => {
</div>
<ModuleDetailsSidebar
issues={moduleIssues ?? []}
module={moduleDetails}
isOpen={moduleSidebar}
moduleIssues={moduleIssues}

View File

@ -16,6 +16,11 @@ export interface ICycle {
created_at: Date;
created_by: string;
description: string;
distribution: {
assignees: TAssigneesDistribution[];
completion_chart: TCompletionChartDistribution;
labels: TLabelsDistribution[];
};
end_date: string | null;
id: string;
is_favorite: boolean;
@ -38,6 +43,29 @@ export interface ICycle {
workspace_detail: IWorkspaceLite;
}
export type TAssigneesDistribution = {
assignee_id: string | null;
avatar: string | null;
completed_issues: number;
first_name: string | null;
last_name: string | null;
pending_issues: number;
total_issues: number;
};
export type TCompletionChartDistribution = {
[key: string]: number;
};
export type TLabelsDistribution = {
color: string | null;
completed_issues: number;
label_id: string | null;
label_name: string | null;
pending_issues: number;
total_issues: number;
};
export interface CycleIssueResponse {
id: string;
issue_detail: IIssue;

View File

@ -18,6 +18,11 @@ export interface IModule {
description: string;
description_text: any;
description_html: any;
distribution: {
assignees: TAssigneesDistribution[];
completion_chart: TCompletionChartDistribution;
labels: TLabelsDistribution[];
};
id: string;
lead: string | null;
lead_detail: IUserLite | null;