feat: sidebar progress (#252)

* feat: cycle assignees and labels progress added

* fix: build fix

* feat: sidebar progress stats added and refactor

* refactor: progress stats and cycle sidebar

* feat: module sidebar progress added

* feat: sidebar progress no assignee added

* feat: states tab added

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>
This commit is contained in:
Anmol Singh Bhatia 2023-02-08 18:50:08 +05:30 committed by GitHub
parent 76cc634a46
commit c978632938
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 260 additions and 35 deletions

View File

@ -0,0 +1,179 @@
import React from "react";
import { Tab } from "@headlessui/react";
import { useRouter } from "next/router";
import useSWR from "swr";
import Image from "next/image";
// ui
import SingleProgressStats from "./single-progress-stats";
import { Avatar } from "components/ui";
// icons
import User from "public/user.png";
// types
import { IIssue, IIssueLabels } from "types";
// constants
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
// services
import issuesServices from "services/issues.service";
import projectService from "services/project.service";
type Props = {
groupedIssues: any;
issues: IIssue[];
};
const stateGroupColours: {
[key: string]: string;
} = {
backlog: "#3f76ff",
unstarted: "#ff9e9e",
started: "#d687ff",
cancelled: "#ff5353",
completed: "#096e8d",
};
const SidebarProgressStats: React.FC<Props> = ({ groupedIssues, issues }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
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
);
return (
<div className="flex flex-col items-center justify-center w-full gap-2 ">
<Tab.Group>
<Tab.List
as="div"
className="flex items-center justify-between w-full rounded bg-gray-100 text-xs"
>
<Tab
className={({ selected }) =>
`w-1/2 rounded py-1 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
}
>
Assignees
</Tab>
<Tab
className={({ selected }) =>
`w-1/2 rounded py-1 ${selected ? "bg-gray-300 font-semibold" : "hover:bg-gray-200 "}`
}
>
Labels
</Tab>
<Tab
className={({ selected }) =>
`w-1/2 rounded py-1 ${selected ? "bg-gray-300 font-semibold" : "hover:bg-gray-200 "}`
}
>
States
</Tab>
</Tab.List>
<Tab.Panels className="flex items-center justify-between w-full">
<Tab.Panel as="div" className="w-full flex flex-col ">
{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}
/>
);
}
})}
{issues?.filter((i) => i.assignees?.length === 0).length > 0 ? (
<SingleProgressStats
title={
<>
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
<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 flex flex-col ">
{issueLabels?.map((issue, index) => {
const totalArray = issues?.filter((i) => i.labels?.includes(issue.id));
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
if (totalArray.length > 0) {
return (
<SingleProgressStats
key={index}
title={
<>
<span
className="block h-2 w-2 rounded-full "
style={{
backgroundColor: issue.color,
}}
/>
<span className="text-xs capitalize">{issue.name}</span>
</>
}
completed={completeArray.length}
total={totalArray.length}
/>
);
}
})}
</Tab.Panel>
<Tab.Panel as="div" className="w-full flex flex-col ">
{Object.keys(groupedIssues).map((group, index) => (
<SingleProgressStats
key={index}
title={
<>
<span
className="block h-2 w-2 rounded-full "
style={{
backgroundColor: stateGroupColours[group],
}}
/>
<span className="text-xs capitalize">{group}</span>
</>
}
completed={groupedIssues[group].length}
total={issues.length}
/>
))}
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
);
};
export default SidebarProgressStats;

View File

@ -0,0 +1,29 @@
import React from "react";
import { CircularProgressbar } from "react-circular-progressbar";
type TSingleProgressStatsProps = {
title: any;
completed: number;
total: number;
};
const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({ title, completed, total }) => (
<>
<div className="flex items-center justify-between w-full py-3 text-xs border-b-[1px] border-gray-200">
<div className="flex items-center justify-start w-1/2 gap-2">{title}</div>
<div className="flex items-center justify-end w-1/2 gap-1 px-2">
<div className="flex h-5 justify-center items-center gap-1 ">
<span className="h-4 w-4 ">
<CircularProgressbar value={completed} maxValue={total} strokeWidth={10} />
</span>
<span className="w-8 text-right">{Math.floor((completed / total) * 100)}%</span>
</div>
<span>of</span>
<span>{total}</span>
</div>
</div>
</>
);
export default SingleProgressStats;

View File

@ -7,6 +7,16 @@ import { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// icons
import {
CalendarDaysIcon,
ChartPieIcon,
LinkIcon,
PlusIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
// progress-bar
import { CircularProgressbar } from "react-circular-progressbar";
// services
import modulesService from "services/modules.service";
// hooks
@ -18,27 +28,19 @@ import {
SidebarMembersSelect,
SidebarStatusSelect,
} from "components/modules";
// progress-bar
import { CircularProgressbar } from "react-circular-progressbar";
import "react-circular-progressbar/dist/styles.css";
// ui
import { CustomDatePicker, Loader } from "components/ui";
// icons
import {
CalendarDaysIcon,
ChartPieIcon,
LinkIcon,
PlusIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
// helpers
import { timeAgo } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
import { groupBy } from "helpers/array.helper";
// types
import { IModule, ModuleIssueResponse } from "types";
import { IIssue, IModule, ModuleIssueResponse } from "types";
// fetch-keys
import { MODULE_DETAILS } from "constants/fetch-keys";
import SidebarProgressStats from "components/core/sidebar/sidebar-progress-stats";
const defaultValues: Partial<IModule> = {
lead: "",
@ -49,6 +51,7 @@ const defaultValues: Partial<IModule> = {
};
type Props = {
issues: IIssue[];
module?: IModule;
isOpen: boolean;
moduleIssues: ModuleIssueResponse[] | undefined;
@ -56,6 +59,7 @@ type Props = {
};
export const ModuleDetailsSidebar: React.FC<Props> = ({
issues,
module,
isOpen,
moduleIssues,
@ -290,6 +294,9 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
</div>
</div>
</div>
<div className="w-full">
<SidebarProgressStats issues={issues} groupedIssues={groupedIssues} />
</div>
</>
) : (
<Loader>

View File

@ -9,24 +9,27 @@ import { mutate } from "swr";
import { Controller, useForm } from "react-hook-form";
// icons
import { CalendarDaysIcon, ChartPieIcon, LinkIcon, UserIcon } from "@heroicons/react/24/outline";
// services
import cyclesService from "services/cycles.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Loader, CustomDatePicker } from "components/ui";
// progress-bar
import { CircularProgressbar } from "react-circular-progressbar";
// ui
import { Loader, CustomDatePicker } from "components/ui";
import "react-circular-progressbar/dist/styles.css";
// hooks
import useToast from "hooks/use-toast";
// services
import cyclesService from "services/cycles.service";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
import { groupBy } from "helpers/array.helper";
// types
import { CycleIssueResponse, ICycle } from "types";
import { CycleIssueResponse, ICycle, IIssue } from "types";
// fetch-keys
import { CYCLE_DETAILS } from "constants/fetch-keys";
import SidebarProgressStats from "components/core/sidebar/sidebar-progress-stats";
type Props = {
issues: IIssue[];
cycle: ICycle | undefined;
isOpen: boolean;
cycleIssues: CycleIssueResponse[];
@ -37,7 +40,7 @@ const defaultValues: Partial<ICycle> = {
end_date: new Date().toString(),
};
const CycleDetailSidebar: React.FC<Props> = ({ cycle, isOpen, cycleIssues }) => {
const CycleDetailSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cycleIssues }) => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;
@ -219,6 +222,9 @@ const CycleDetailSidebar: React.FC<Props> = ({ cycle, isOpen, cycleIssues }) =>
</div>
<div className="py-1" />
</div>
<div className="w-full">
<SidebarProgressStats issues={issues} groupedIssues={groupedIssues} />
</div>
</>
) : (
<Loader>

View File

@ -3,7 +3,10 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { NextPageContext } from "next";
// icons
import { ArrowLeftIcon, ListBulletIcon, PlusIcon } from "@heroicons/react/24/outline";
import { CyclesIcon } from "components/icons";
// lib
import { requiredAdmin, requiredAuth } from "lib/auth";
// layouts
@ -21,19 +24,17 @@ import projectService from "services/project.service";
// ui
import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { ArrowLeftIcon, ListBulletIcon, PlusIcon } from "@heroicons/react/24/outline";
import { CyclesIcon } from "components/icons";
// types
import { CycleIssueResponse, IIssue, SelectIssue, UserAuth } from "types";
import { NextPageContext } from "next";
import { CycleIssueResponse, IIssue, IIssueLabels, SelectIssue, UserAuth } from "types";
// fetch-keys
import {
CYCLE_ISSUES,
CYCLE_LIST,
PROJECT_ISSUES_LIST,
PROJECT_DETAILS,
PROJECT_ISSUE_LABELS,
CYCLE_DETAILS,
PROJECT_MEMBERS,
} from "constants/fetch-keys";
const SingleCycle: React.FC<UserAuth> = (props) => {
@ -95,6 +96,7 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
)
: null
);
const cycleIssuesArray = cycleIssues?.map((issue) => ({
...issue.issue_detail,
sub_issues_count: issue.sub_issues_count,
@ -241,6 +243,7 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
</div>
)}
<CycleDetailSidebar
issues={cycleIssuesArray ?? []}
cycle={cycleDetails}
isOpen={cycleSidebar}
cycleIssues={cycleIssues ?? []}

View File

@ -1,9 +1,17 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { NextPageContext } from "next";
import useSWR, { mutate } from "swr";
// icons
import {
ArrowLeftIcon,
ListBulletIcon,
PlusIcon,
RectangleGroupIcon,
RectangleStackIcon,
} from "@heroicons/react/24/outline";
// lib
import { requiredAdmin, requiredAuth } from "lib/auth";
// services
@ -20,14 +28,6 @@ import { DeleteModuleModal, ModuleDetailsSidebar } from "components/modules";
// ui
import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import {
ArrowLeftIcon,
ListBulletIcon,
PlusIcon,
RectangleGroupIcon,
RectangleStackIcon,
} from "@heroicons/react/24/outline";
// types
import {
IIssue,
@ -37,7 +37,7 @@ import {
SelectModuleType,
UserAuth,
} from "types";
import { NextPageContext } from "next";
// fetch-keys
import {
MODULE_DETAILS,
@ -262,6 +262,7 @@ const SingleModule: React.FC<UserAuth> = (props) => {
</div>
)}
<ModuleDetailsSidebar
issues={moduleIssuesArray ?? []}
module={moduleDetails}
isOpen={moduleSidebar}
moduleIssues={moduleIssues}