forked from github/plane
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:
parent
d7097330ef
commit
cf8c902473
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
});
|
||||
},
|
||||
[
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
) : (
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -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}
|
||||
|
28
apps/app/types/cycles.d.ts
vendored
28
apps/app/types/cycles.d.ts
vendored
@ -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;
|
||||
|
5
apps/app/types/modules.d.ts
vendored
5
apps/app/types/modules.d.ts
vendored
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user