Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sidebar progress #252

Merged
merged 7 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions apps/app/components/core/sidebar/sidebar-progress-stats.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 29 additions & 0 deletions apps/app/components/core/sidebar/single-progress-stats.tsx
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion apps/app/components/issues/select/label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const IssueLabelSelect: React.FC<Props> = ({ value, onChange, projectId }
const options = issueLabels?.map((label) => ({
value: label.id,
display: label.name,
color: label.colour,
color: label.color,
}));

const filteredOptions =
Expand Down
12 changes: 6 additions & 6 deletions apps/app/components/issues/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ type Props = {

const defaultValues: Partial<IIssueLabels> = {
name: "",
colour: "#ff0000",
color: "#ff0000",
};

export const IssueDetailsSidebar: React.FC<Props> = ({
Expand Down Expand Up @@ -310,7 +310,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{ backgroundColor: singleLabel?.colour ?? "green" }}
style={{ backgroundColor: singleLabel?.color ?? "green" }}
/>
{singleLabel.name}
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
Expand Down Expand Up @@ -366,7 +366,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{ backgroundColor: label.colour ?? "green" }}
style={{ backgroundColor: label.color ?? "green" }}
/>
{label.name}
</Listbox.Option>
Expand Down Expand Up @@ -416,11 +416,11 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<Popover.Button
className={`flex items-center gap-1 rounded-md bg-white p-1 outline-none focus:ring-2 focus:ring-indigo-500`}
>
{watch("colour") && watch("colour") !== "" && (
{watch("color") && watch("color") !== "" && (
<span
className="h-5 w-5 rounded"
style={{
backgroundColor: watch("colour") ?? "green",
backgroundColor: watch("color") ?? "green",
}}
/>
)}
Expand All @@ -438,7 +438,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
>
<Popover.Panel className="absolute right-0 bottom-8 z-10 mt-1 max-w-xs transform px-2 sm:px-0">
<Controller
name="colour"
name="color"
control={controlLabel}
render={({ field: { value, onChange } }) => (
<TwitterPicker
Expand Down
29 changes: 18 additions & 11 deletions apps/app/components/modules/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: "",
Expand All @@ -49,13 +51,15 @@ const defaultValues: Partial<IModule> = {
};

type Props = {
issues: IIssue[];
module?: IModule;
isOpen: boolean;
moduleIssues: ModuleIssueResponse[] | undefined;
handleDeleteModule: () => void;
};

export const ModuleDetailsSidebar: React.FC<Props> = ({
issues,
module,
isOpen,
moduleIssues,
Expand Down Expand Up @@ -290,6 +294,9 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
</div>
</div>
</div>
<div className="w-full">
<SidebarProgressStats issues={issues} groupedIssues={groupedIssues} />
</div>
</>
) : (
<Loader>
Expand Down
22 changes: 14 additions & 8 deletions apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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;

Expand Down Expand Up @@ -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>
Expand Down
Loading