mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
76cc634a46
commit
c978632938
179
apps/app/components/core/sidebar/sidebar-progress-stats.tsx
Normal file
179
apps/app/components/core/sidebar/sidebar-progress-stats.tsx
Normal 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;
|
29
apps/app/components/core/sidebar/single-progress-stats.tsx
Normal file
29
apps/app/components/core/sidebar/single-progress-stats.tsx
Normal 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;
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 ?? []}
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user