mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
style: cycle new ui (#1052)
* style: cycles new ui * chore: added progress bar for the high priority issues * fix: build fix * style: active cycle details, theming , padding and layout * style: cycle list and card styling * style: cycle card * fix: tooltip text overflow * fix: cycle mutation fix * style: cycle list and card view improvement, chore: code refactor * feat: view cycle button * style: cycle list and board view improvement * style: responsiveness added * feat: active cycle stats component, chore: code refactor * fix: active cycle divider fix, style: stats font color * fix: tooltip fix --------- Co-authored-by: kunal_17 <kunalvish17360@gmail.com>
This commit is contained in:
parent
c49b0d6151
commit
559b0cc9c8
@ -12,9 +12,11 @@ type Props = {
|
||||
issues: IIssue[];
|
||||
start: string;
|
||||
end: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
const ProgressChart: React.FC<Props> = ({ issues, start, end }) => {
|
||||
const ProgressChart: React.FC<Props> = ({ issues, start, end, width = 360, height = 160 }) => {
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
const getChartData = () => {
|
||||
@ -51,8 +53,8 @@ const ProgressChart: React.FC<Props> = ({ issues, start, end }) => {
|
||||
return (
|
||||
<div className="absolute -left-4 flex h-full w-full items-center justify-center text-xs">
|
||||
<AreaChart
|
||||
width={360}
|
||||
height={160}
|
||||
width={width}
|
||||
height={height}
|
||||
data={ChartData}
|
||||
margin={{
|
||||
top: 12,
|
||||
|
@ -29,6 +29,8 @@ type Props = {
|
||||
issues: IIssue[];
|
||||
module?: IModule;
|
||||
userAuth?: UserAuth;
|
||||
roundedTab?: boolean;
|
||||
noBackground?: boolean;
|
||||
};
|
||||
|
||||
const stateGroupColours: {
|
||||
@ -46,6 +48,8 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
issues,
|
||||
module,
|
||||
userAuth,
|
||||
roundedTab,
|
||||
noBackground,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
@ -100,12 +104,16 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
>
|
||||
<Tab.List
|
||||
as="div"
|
||||
className={`flex w-full items-center justify-between rounded-md bg-brand-surface-1 px-1 py-1.5
|
||||
className={`flex w-full items-center gap-2 justify-between rounded-md ${
|
||||
noBackground ? "" : "bg-brand-surface-1"
|
||||
} px-1 py-1.5
|
||||
${module ? "text-xs" : "text-sm"} `}
|
||||
>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-full rounded px-3 py-1 text-brand-base ${
|
||||
`w-full ${
|
||||
roundedTab ? "rounded-3xl border border-brand-base" : "rounded"
|
||||
} px-3 py-1 text-brand-base ${
|
||||
selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
|
||||
}`
|
||||
}
|
||||
@ -114,7 +122,9 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-full rounded px-3 py-1 text-brand-base ${
|
||||
`w-full ${
|
||||
roundedTab ? "rounded-3xl border border-brand-base" : "rounded"
|
||||
} px-3 py-1 text-brand-base ${
|
||||
selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
|
||||
}`
|
||||
}
|
||||
@ -123,7 +133,9 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-full rounded px-3 py-1 text-brand-base ${
|
||||
`w-full ${
|
||||
roundedTab ? "rounded-3xl border border-brand-base" : "rounded"
|
||||
} px-3 py-1 text-brand-base ${
|
||||
selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
|
||||
}`
|
||||
}
|
||||
@ -131,10 +143,10 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
States
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels className="flex w-full items-center justify-between pt-1">
|
||||
<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 totalArray = issues?.filter((i) => i?.assignees?.includes(member.member.id));
|
||||
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
|
||||
|
||||
if (totalArray.length > 0) {
|
||||
@ -150,19 +162,19 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
completed={completeArray.length}
|
||||
total={totalArray.length}
|
||||
onClick={() => {
|
||||
if (filters.assignees?.includes(member.member.id))
|
||||
if (filters?.assignees?.includes(member.member.id))
|
||||
setFilters({
|
||||
assignees: filters.assignees?.filter((a) => a !== member.member.id),
|
||||
assignees: filters?.assignees?.filter((a) => a !== member.member.id),
|
||||
});
|
||||
else
|
||||
setFilters({ assignees: [...(filters?.assignees ?? []), member.member.id] });
|
||||
}}
|
||||
selected={filters.assignees?.includes(member.member.id)}
|
||||
selected={filters?.assignees?.includes(member.member.id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
{issues?.filter((i) => i.assignees?.length === 0).length > 0 ? (
|
||||
{issues?.filter((i) => i?.assignees?.length === 0).length > 0 ? (
|
||||
<SingleProgressStats
|
||||
title={
|
||||
<>
|
||||
@ -180,10 +192,10 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
}
|
||||
completed={
|
||||
issues?.filter(
|
||||
(i) => i.state_detail.group === "completed" && i.assignees?.length === 0
|
||||
(i) => i?.state_detail.group === "completed" && i.assignees?.length === 0
|
||||
).length
|
||||
}
|
||||
total={issues?.filter((i) => i.assignees?.length === 0).length}
|
||||
total={issues?.filter((i) => i?.assignees?.length === 0).length}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
@ -191,8 +203,8 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
</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");
|
||||
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 (
|
||||
@ -207,7 +219,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
label.color && label.color !== "" ? label.color : "#000000",
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs capitalize">{label.name}</span>
|
||||
<span className="text-xs capitalize">{label?.name}</span>
|
||||
</div>
|
||||
}
|
||||
completed={completeArray.length}
|
||||
@ -215,11 +227,11 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
onClick={() => {
|
||||
if (filters.labels?.includes(label.id))
|
||||
setFilters({
|
||||
labels: filters.labels?.filter((l) => l !== label.id),
|
||||
labels: filters?.labels?.filter((l) => l !== label.id),
|
||||
});
|
||||
else setFilters({ labels: [...(filters?.labels ?? []), label.id] });
|
||||
}}
|
||||
selected={filters.labels?.includes(label.id)}
|
||||
selected={filters?.labels?.includes(label.id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
|
||||
selected = false,
|
||||
}) => (
|
||||
<div
|
||||
className={`flex w-full items-center justify-between rounded p-2 text-xs ${
|
||||
className={`flex w-full items-center gap-4 justify-between rounded-sm p-1 text-xs ${
|
||||
onClick ? "cursor-pointer hover:bg-brand-surface-1" : ""
|
||||
} ${selected ? "bg-brand-surface-1" : ""}`}
|
||||
onClick={onClick}
|
||||
|
602
apps/app/components/cycles/active-cycle-details.tsx
Normal file
602
apps/app/components/cycles/active-cycle-details.tsx
Normal file
@ -0,0 +1,602 @@
|
||||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { LinearProgressIndicator, Tooltip } from "components/ui";
|
||||
import { AssigneesList } from "components/ui/avatar";
|
||||
import { SingleProgressStats } from "components/core";
|
||||
// components
|
||||
import ProgressChart from "components/core/sidebar/progress-chart";
|
||||
import { ActiveCycleProgressStats } from "components/cycles";
|
||||
|
||||
// icons
|
||||
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
|
||||
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||
import {
|
||||
TargetIcon,
|
||||
ContrastIcon,
|
||||
PersonRunningIcon,
|
||||
ArrowRightIcon,
|
||||
TriangleExclamationIcon,
|
||||
AlarmClockIcon,
|
||||
LayerDiagonalIcon,
|
||||
CompletedStateIcon,
|
||||
} from "components/icons";
|
||||
import { StarIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import {
|
||||
getDateRangeStatus,
|
||||
renderShortDateWithYearFormat,
|
||||
findHowManyDaysLeft,
|
||||
} from "helpers/date-time.helper";
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import {
|
||||
CompletedCyclesResponse,
|
||||
CurrentAndUpcomingCyclesResponse,
|
||||
DraftCyclesResponse,
|
||||
ICycle,
|
||||
IIssue,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST,
|
||||
CYCLE_DRAFT_LIST,
|
||||
CYCLE_ISSUES,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
type TSingleStatProps = {
|
||||
cycle: ICycle;
|
||||
isCompleted?: boolean;
|
||||
};
|
||||
|
||||
const stateGroups = [
|
||||
{
|
||||
key: "backlog_issues",
|
||||
title: "Backlog",
|
||||
color: "#dee2e6",
|
||||
},
|
||||
{
|
||||
key: "unstarted_issues",
|
||||
title: "Unstarted",
|
||||
color: "#26b5ce",
|
||||
},
|
||||
{
|
||||
key: "started_issues",
|
||||
title: "Started",
|
||||
color: "#f7ae59",
|
||||
},
|
||||
{
|
||||
key: "cancelled_issues",
|
||||
title: "Cancelled",
|
||||
color: "#d687ff",
|
||||
},
|
||||
{
|
||||
key: "completed_issues",
|
||||
title: "Completed",
|
||||
color: "#09a953",
|
||||
},
|
||||
];
|
||||
|
||||
export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle, isCompleted = false }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const endDate = new Date(cycle.end_date ?? "");
|
||||
const startDate = new Date(cycle.start_date ?? "");
|
||||
|
||||
const groupedIssues: any = {
|
||||
backlog: cycle.backlog_issues,
|
||||
unstarted: cycle.unstarted_issues,
|
||||
started: cycle.started_issues,
|
||||
completed: cycle.completed_issues,
|
||||
cancelled: cycle.cancelled_issues,
|
||||
};
|
||||
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
|
||||
const handleAddToFavorites = () => {
|
||||
if (!workspaceSlug || !projectId || !cycle) return;
|
||||
|
||||
switch (cycleStatus) {
|
||||
case "current":
|
||||
case "upcoming":
|
||||
mutate<CurrentAndUpcomingCyclesResponse>(
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
current_cycle: (prevData?.current_cycle ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
}),
|
||||
false
|
||||
);
|
||||
break;
|
||||
case "completed":
|
||||
mutate<CompletedCyclesResponse>(
|
||||
CYCLE_COMPLETE_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
}),
|
||||
false
|
||||
);
|
||||
break;
|
||||
case "draft":
|
||||
mutate<DraftCyclesResponse>(
|
||||
CYCLE_DRAFT_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
}),
|
||||
false
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
cyclesService
|
||||
.addCycleToFavorites(workspaceSlug as string, projectId as string, {
|
||||
cycle: cycle.id,
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = () => {
|
||||
if (!workspaceSlug || !projectId || !cycle) return;
|
||||
|
||||
switch (cycleStatus) {
|
||||
case "current":
|
||||
case "upcoming":
|
||||
mutate<CurrentAndUpcomingCyclesResponse>(
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
current_cycle: (prevData?.current_cycle ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
}),
|
||||
false
|
||||
);
|
||||
break;
|
||||
case "completed":
|
||||
mutate<CompletedCyclesResponse>(
|
||||
CYCLE_COMPLETE_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
}),
|
||||
false
|
||||
);
|
||||
break;
|
||||
case "draft":
|
||||
mutate<DraftCyclesResponse>(
|
||||
CYCLE_DRAFT_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
}),
|
||||
false
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
cyclesService
|
||||
.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id)
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't remove the cycle from favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const { data: issues } = useSWR<IIssue[]>(
|
||||
workspaceSlug && projectId && cycle.id ? CYCLE_ISSUES(cycle.id as string) : null,
|
||||
workspaceSlug && projectId && cycle.id
|
||||
? () =>
|
||||
cyclesService.getCycleIssues(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
cycle.id as string
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const progressIndicatorData = stateGroups.map((group, index) => ({
|
||||
id: index,
|
||||
name: group.title,
|
||||
value:
|
||||
cycle.total_issues > 0
|
||||
? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100
|
||||
: 0,
|
||||
color: group.color,
|
||||
}));
|
||||
|
||||
return (
|
||||
<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="w-full">
|
||||
<div className="flex h-full flex-col gap-5 rounded-b-[10px] p-4">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="flex items-center gap-1">
|
||||
<ContrastIcon
|
||||
className="h-5 w-5"
|
||||
color={`${
|
||||
cycleStatus === "current"
|
||||
? "#09A953"
|
||||
: cycleStatus === "upcoming"
|
||||
? "#F7AE59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3F76FF"
|
||||
: cycleStatus === "draft"
|
||||
? "#858E96"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
<Tooltip tooltipContent={cycle.name} position="top-left">
|
||||
<h3 className="break-all text-lg font-semibold">
|
||||
{truncateText(cycle.name, 70)}
|
||||
</h3>
|
||||
</Tooltip>
|
||||
</span>
|
||||
<span className="flex items-center gap-1 capitalize">
|
||||
<span
|
||||
className={`rounded-full px-1.5 py-0.5
|
||||
${
|
||||
cycleStatus === "current"
|
||||
? "bg-green-600/5 text-green-600"
|
||||
: cycleStatus === "upcoming"
|
||||
? "bg-orange-300/5 text-orange-300"
|
||||
: cycleStatus === "completed"
|
||||
? "bg-blue-500/5 text-blue-500"
|
||||
: cycleStatus === "draft"
|
||||
? "bg-neutral-400/5 text-neutral-400"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{cycleStatus === "current" ? (
|
||||
<span className="flex gap-1">
|
||||
<PersonRunningIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
|
||||
</span>
|
||||
) : cycleStatus === "upcoming" ? (
|
||||
<span className="flex gap-1">
|
||||
<AlarmClockIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left
|
||||
</span>
|
||||
) : cycleStatus === "completed" ? (
|
||||
<span className="flex gap-1">
|
||||
{cycle.total_issues - cycle.completed_issues > 0 && (
|
||||
<Tooltip
|
||||
tooltipContent={`${
|
||||
cycle.total_issues - cycle.completed_issues
|
||||
} more pending ${
|
||||
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
|
||||
}`}
|
||||
>
|
||||
<span>
|
||||
<TriangleExclamationIcon className="h-3.5 w-3.5 fill-current" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}{" "}
|
||||
Completed
|
||||
</span>
|
||||
) : (
|
||||
cycleStatus
|
||||
)}
|
||||
</span>
|
||||
{cycle.is_favorite ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleRemoveFromFavorites();
|
||||
}}
|
||||
>
|
||||
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleAddToFavorites();
|
||||
}}
|
||||
>
|
||||
<StarIcon className="h-4 w-4 " color="#858E96" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-start gap-5 text-brand-secondary">
|
||||
<div className="flex items-start gap-1">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
<span>{renderShortDateWithYearFormat(startDate)}</span>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<div className="flex items-start gap-1">
|
||||
<TargetIcon className="h-4 w-4" />
|
||||
<span>{renderShortDateWithYearFormat(endDate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2.5 text-brand-secondary">
|
||||
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
||||
<Image
|
||||
src={cycle.owned_by.avatar}
|
||||
height={16}
|
||||
width={16}
|
||||
className="rounded-full"
|
||||
alt={cycle.owned_by.first_name}
|
||||
/>
|
||||
) : (
|
||||
<span className="bg-brand-secondary flex h-5 w-5 items-center justify-center rounded-full bg-brand-base capitalize">
|
||||
{cycle.owned_by.first_name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-brand-secondary">{cycle.owned_by.first_name}</span>
|
||||
</div>
|
||||
|
||||
{cycle.assignees.length > 0 && (
|
||||
<div className="flex items-center gap-1 text-brand-secondary">
|
||||
<AssigneesList users={cycle.assignees} length={4} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-brand-secondary">
|
||||
<div className="flex gap-2">
|
||||
<LayerDiagonalIcon className="h-4 w-4 flex-shrink-0" />
|
||||
{cycle.total_issues} issues
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<CompletedStateIcon width={16} height={16} color="#438AF3" />
|
||||
{cycle.completed_issues} issues
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
||||
<a className="bg-brand-accent text-white px-4 rounded-md py-2 text-center text-sm font-medium w-full hover:bg-brand-accent/90">
|
||||
View Cycle
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</a>
|
||||
</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-full flex-col border-brand-base">
|
||||
<div className="flex h-full w-full flex-col text-brand-secondary p-4">
|
||||
<div className="flex w-full items-center gap-2 py-1">
|
||||
<span>Progress</span>
|
||||
<LinearProgressIndicator data={progressIndicatorData} />
|
||||
</div>
|
||||
<div className="flex flex-col mt-2 gap-1 items-center">
|
||||
{Object.keys(groupedIssues).map((group, index) => (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full "
|
||||
style={{
|
||||
backgroundColor: stateGroups[index].color,
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs capitalize">{group}</span>
|
||||
</div>
|
||||
}
|
||||
completed={groupedIssues[group]}
|
||||
total={cycle.total_issues}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-brand-base p-4">
|
||||
<ActiveCycleProgressStats issues={issues ?? []} />
|
||||
</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
|
||||
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"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div>
|
||||
<Tooltip
|
||||
tooltipHeading="Issue ID"
|
||||
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
||||
>
|
||||
<span className="flex-shrink-0 text-xs text-brand-secondary">
|
||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip
|
||||
position="top-left"
|
||||
tooltipHeading="Title"
|
||||
tooltipContent={issue.name}
|
||||
>
|
||||
<span className="text-[0.825rem] text-brand-base">
|
||||
{truncateText(issue.name, 30)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div
|
||||
className={`grid h-6 w-6 place-items-center items-center rounded border shadow-sm ${
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
{getPriorityIcon(issue.priority, "text-sm")}
|
||||
</div>
|
||||
{issue.label_details.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{issue.label_details.map((label) => (
|
||||
<span
|
||||
key={label.id}
|
||||
className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary"
|
||||
>
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor:
|
||||
label?.color && label.color !== "" ? label.color : "#000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<div className={`flex items-center gap-2 text-brand-secondary`}>
|
||||
{issue.assignees &&
|
||||
issue.assignees.length > 0 &&
|
||||
Array.isArray(issue.assignees) ? (
|
||||
<div className="-my-0.5 flex items-center justify-center gap-2">
|
||||
<AssigneesList
|
||||
userIds={issue.assignees}
|
||||
length={3}
|
||||
showLength={false}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="h-1 w-full rounded-full bg-brand-surface-2">
|
||||
<div
|
||||
className="h-1 rounded-full bg-green-600"
|
||||
style={{
|
||||
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) *
|
||||
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
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between border-brand-base p-4">
|
||||
<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>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>Current</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>
|
||||
<LayerDiagonalIcon className="h-5 w-5 flex-shrink-0 text-brand-secondary" />
|
||||
</span>
|
||||
<span>
|
||||
Pending Issues -{" "}
|
||||
{cycle.total_issues - (cycle.completed_issues + cycle.cancelled_issues)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-64">
|
||||
<ProgressChart
|
||||
issues={issues ?? []}
|
||||
start={cycle?.start_date ?? ""}
|
||||
end={cycle?.end_date ?? ""}
|
||||
width={475}
|
||||
height={256}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
182
apps/app/components/cycles/active-cycle-stats.tsx
Normal file
182
apps/app/components/cycles/active-cycle-stats.tsx
Normal file
@ -0,0 +1,182 @@
|
||||
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";
|
||||
// components
|
||||
import { SingleProgressStats } from "components/core";
|
||||
// ui
|
||||
import { Avatar } from "components/ui";
|
||||
// icons
|
||||
import User from "public/user.png";
|
||||
// types
|
||||
import { IIssue, IIssueLabels } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
// types
|
||||
type Props = {
|
||||
issues: IIssue[];
|
||||
};
|
||||
|
||||
export const ActiveCycleProgressStats: React.FC<Props> = ({ issues }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
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":
|
||||
return 0;
|
||||
case "Labels":
|
||||
return 1;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Tab.Group
|
||||
defaultIndex={currentValue(tab)}
|
||||
onChange={(i) => {
|
||||
switch (i) {
|
||||
case 0:
|
||||
return setTab("Assignees");
|
||||
case 1:
|
||||
return setTab("Labels");
|
||||
|
||||
default:
|
||||
return setTab("Assignees");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab.List as="div" className="flex flex-wrap items-center justify-start gap-4 text-sm">
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`px-3 py-1 text-brand-base rounded-3xl border border-brand-base ${
|
||||
selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Assignees
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`px-3 py-1 text-brand-base rounded-3xl border border-brand-base ${
|
||||
selected ? " bg-brand-accent text-white" : " hover:bg-brand-surface-2"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Labels
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels className="flex w-full">
|
||||
<Tab.Panel
|
||||
as="div"
|
||||
className="flex flex-col w-full mt-2 gap-1 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) {
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
<span>{member.member.first_name}</span>
|
||||
</div>
|
||||
}
|
||||
completed={completeArray.length}
|
||||
total={totalArray.length}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
{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-white bg-brand-surface-2">
|
||||
<Image
|
||||
src={User}
|
||||
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 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
};
|
@ -17,7 +17,7 @@ type TCycleStatsViewProps = {
|
||||
type: "current" | "upcoming" | "draft";
|
||||
};
|
||||
|
||||
export const CyclesList: React.FC<TCycleStatsViewProps> = ({
|
||||
export const AllCyclesBoard: React.FC<TCycleStatsViewProps> = ({
|
||||
cycles,
|
||||
setCreateUpdateCycleModal,
|
||||
setSelectedCycle,
|
86
apps/app/components/cycles/all-cycles-list.tsx
Normal file
86
apps/app/components/cycles/all-cycles-list.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { useState } from "react";
|
||||
|
||||
// components
|
||||
import { DeleteCycleModal, SingleCycleList } from "components/cycles";
|
||||
import { EmptyState, Loader } from "components/ui";
|
||||
// image
|
||||
import emptyCycle from "public/empty-state/empty-cycle.svg";
|
||||
// icon
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { ICycle, SelectCycleType } from "types";
|
||||
|
||||
type TCycleStatsViewProps = {
|
||||
cycles: ICycle[] | undefined;
|
||||
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
|
||||
type: "current" | "upcoming" | "draft";
|
||||
};
|
||||
|
||||
export const AllCyclesList: React.FC<TCycleStatsViewProps> = ({
|
||||
cycles,
|
||||
setCreateUpdateCycleModal,
|
||||
setSelectedCycle,
|
||||
type,
|
||||
}) => {
|
||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
|
||||
|
||||
const handleDeleteCycle = (cycle: ICycle) => {
|
||||
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
|
||||
setCycleDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleEditCycle = (cycle: ICycle) => {
|
||||
setSelectedCycle({ ...cycle, actionType: "edit" });
|
||||
setCreateUpdateCycleModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteCycleModal
|
||||
isOpen={
|
||||
cycleDeleteModal &&
|
||||
!!selectedCycleForDelete &&
|
||||
selectedCycleForDelete.actionType === "delete"
|
||||
}
|
||||
setIsOpen={setCycleDeleteModal}
|
||||
data={selectedCycleForDelete}
|
||||
/>
|
||||
{cycles ? (
|
||||
cycles.length > 0 ? (
|
||||
<div>
|
||||
{cycles.map((cycle) => (
|
||||
<div className="hover:bg-brand-surface-2">
|
||||
<div className="flex flex-col border-brand-base">
|
||||
<SingleCycleList
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : type === "current" ? (
|
||||
<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 current cycle is present.</h3>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
type="cycle"
|
||||
title="Create New Cycle"
|
||||
description="Sprint more effectively with Cycles by confining your project
|
||||
to a fixed amount of time. Create new cycle now."
|
||||
imgURL={emptyCycle}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Loader.Item height="200px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -7,9 +7,9 @@ import useSWR from "swr";
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// components
|
||||
import { DeleteCycleModal, SingleCycleCard } from "components/cycles";
|
||||
import { DeleteCycleModal, SingleCycleCard, SingleCycleList } from "components/cycles";
|
||||
// icons
|
||||
import { CompletedCycleIcon, ExclamationIcon } from "components/icons";
|
||||
import { ExclamationIcon } from "components/icons";
|
||||
// types
|
||||
import { ICycle, SelectCycleType } from "types";
|
||||
// fetch-keys
|
||||
@ -19,11 +19,13 @@ import { EmptyState, Loader } from "components/ui";
|
||||
import emptyCycle from "public/empty-state/empty-cycle.svg";
|
||||
|
||||
export interface CompletedCyclesListProps {
|
||||
cycleView: string;
|
||||
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
|
||||
}
|
||||
|
||||
export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
|
||||
export const CompletedCycles: React.FC<CompletedCyclesListProps> = ({
|
||||
cycleView,
|
||||
setCreateUpdateCycleModal,
|
||||
setSelectedCycle,
|
||||
}) => {
|
||||
@ -72,6 +74,23 @@ export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
|
||||
/>
|
||||
<span>Completed cycles are not editable.</span>
|
||||
</div>
|
||||
{cycleView === "list" ? (
|
||||
<div>
|
||||
{completedCycles.completed_cycles.map((cycle) => (
|
||||
<div className="hover:bg-brand-surface-2">
|
||||
<div className="flex flex-col border-brand-base">
|
||||
<SingleCycleList
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
isCompleted
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
{completedCycles.completed_cycles.map((cycle) => (
|
||||
<SingleCycleCard
|
||||
@ -83,6 +102,7 @@ export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
207
apps/app/components/cycles/cycles-view.tsx
Normal file
207
apps/app/components/cycles/cycles-view.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
import React from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
// hooks
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// components
|
||||
import {
|
||||
ActiveCycleDetails,
|
||||
CompletedCyclesListProps,
|
||||
AllCyclesBoard,
|
||||
AllCyclesList,
|
||||
CompletedCycles,
|
||||
} from "components/cycles";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
// icons
|
||||
import { ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import {
|
||||
SelectCycleType,
|
||||
ICycle,
|
||||
CurrentAndUpcomingCyclesResponse,
|
||||
DraftCyclesResponse,
|
||||
} from "types";
|
||||
|
||||
type Props = {
|
||||
cycleView: string;
|
||||
setCycleView: React.Dispatch<React.SetStateAction<string>>;
|
||||
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
|
||||
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
cyclesCompleteList: ICycle[] | undefined;
|
||||
currentAndUpcomingCycles: CurrentAndUpcomingCyclesResponse | undefined;
|
||||
draftCycles: DraftCyclesResponse | undefined;
|
||||
};
|
||||
|
||||
export const CyclesView: React.FC<Props> = ({
|
||||
cycleView,
|
||||
setCycleView,
|
||||
setSelectedCycle,
|
||||
setCreateUpdateCycleModal,
|
||||
cyclesCompleteList,
|
||||
currentAndUpcomingCycles,
|
||||
draftCycles,
|
||||
}) => {
|
||||
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All");
|
||||
|
||||
const currentTabValue = (tab: string | null) => {
|
||||
switch (tab) {
|
||||
case "All":
|
||||
return 0;
|
||||
case "Active":
|
||||
return 1;
|
||||
case "Upcoming":
|
||||
return 2;
|
||||
case "Completed":
|
||||
return 3;
|
||||
case "Drafts":
|
||||
return 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const CompletedCycles = dynamic<CompletedCyclesListProps>(
|
||||
() => import("components/cycles").then((a) => a.CompletedCycles),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<Loader className="mb-5">
|
||||
<Loader.Item height="12rem" width="100%" />
|
||||
</Loader>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tab.Group
|
||||
defaultIndex={currentTabValue(cycleTab)}
|
||||
onChange={(i) => {
|
||||
switch (i) {
|
||||
case 0:
|
||||
return setCycleTab("All");
|
||||
case 1:
|
||||
return setCycleTab("Active");
|
||||
case 2:
|
||||
return setCycleTab("Upcoming");
|
||||
case 3:
|
||||
return setCycleTab("Completed");
|
||||
case 4:
|
||||
return setCycleTab("Drafts");
|
||||
|
||||
default:
|
||||
return setCycleTab("All");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
<div className="flex justify-between">
|
||||
<Tab.List as="div" className="flex flex-wrap items-center justify-start gap-4 text-base">
|
||||
{["All", "Active", "Upcoming", "Completed", "Drafts"].map((tab, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border px-6 py-1 outline-none ${
|
||||
selected
|
||||
? "border-brand-accent bg-brand-accent text-white font-medium"
|
||||
: "border-brand-base bg-brand-base hover:bg-brand-surface-2"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
{cycleTab !== "Active" && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2`}
|
||||
onClick={() => setCycleView("list")}
|
||||
>
|
||||
<ListBulletIcon className="h-4 w-4 text-brand-secondary" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2`}
|
||||
onClick={() => setCycleView("board")}
|
||||
>
|
||||
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
{cycleView === "list" && (
|
||||
<AllCyclesList
|
||||
cycles={cyclesCompleteList}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="current"
|
||||
/>
|
||||
)}
|
||||
{cycleView === "board" && (
|
||||
<AllCyclesBoard
|
||||
cycles={cyclesCompleteList}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="current"
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
{currentAndUpcomingCycles?.current_cycle?.[0] && (
|
||||
<ActiveCycleDetails cycle={currentAndUpcomingCycles?.current_cycle?.[0]} />
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
{cycleView === "list" && (
|
||||
<AllCyclesList
|
||||
cycles={currentAndUpcomingCycles?.upcoming_cycle}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="upcoming"
|
||||
/>
|
||||
)}
|
||||
{cycleView === "board" && (
|
||||
<AllCyclesBoard
|
||||
cycles={currentAndUpcomingCycles?.upcoming_cycle}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="upcoming"
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
<CompletedCycles
|
||||
cycleView={cycleView}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
{cycleView === "list" && (
|
||||
<AllCyclesList
|
||||
cycles={draftCycles?.draft_cycles}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="draft"
|
||||
/>
|
||||
)}
|
||||
{cycleView === "board" && (
|
||||
<AllCyclesBoard
|
||||
cycles={draftCycles?.draft_cycles}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="draft"
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -29,6 +29,7 @@ type TConfirmCycleDeletionProps = {
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST,
|
||||
CYCLE_DETAILS,
|
||||
CYCLE_DRAFT_LIST,
|
||||
CYCLE_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
@ -114,6 +115,14 @@ export const DeleteCycleModal: React.FC<TConfirmCycleDeletionProps> = ({
|
||||
false
|
||||
);
|
||||
}
|
||||
mutate(
|
||||
CYCLE_DETAILS(projectId as string),
|
||||
(prevData: any) => {
|
||||
if (!prevData) return;
|
||||
return prevData.filter((cycle: any) => cycle.id !== data?.id);
|
||||
},
|
||||
false
|
||||
);
|
||||
handleClose();
|
||||
|
||||
setToastAlert({
|
||||
|
@ -1,11 +1,16 @@
|
||||
export * from "./completed-cycles-list";
|
||||
export * from "./cycles-list";
|
||||
export * from "./active-cycle-details";
|
||||
export * from "./cycles-view";
|
||||
export * from "./completed-cycles";
|
||||
export * from "./all-cycles-board";
|
||||
export * from "./all-cycles-list";
|
||||
export * from "./delete-cycle-modal";
|
||||
export * from "./form";
|
||||
export * from "./modal";
|
||||
export * from "./select";
|
||||
export * from "./sidebar";
|
||||
export * from "./single-cycle-list";
|
||||
export * from "./single-cycle-card";
|
||||
export * from "./empty-cycle";
|
||||
export * from "./transfer-issues-modal";
|
||||
export * from "./transfer-issues";
|
||||
export * from "./active-cycle-stats";
|
||||
|
@ -20,6 +20,7 @@ import type { ICycle } from "types";
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST,
|
||||
CYCLE_DETAILS,
|
||||
CYCLE_DRAFT_LIST,
|
||||
CYCLE_INCOMPLETE_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
@ -58,6 +59,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
mutate(CYCLE_DRAFT_LIST(projectId as string));
|
||||
}
|
||||
mutate(CYCLE_INCOMPLETE_LIST(projectId as string));
|
||||
mutate(CYCLE_DETAILS(projectId as string));
|
||||
handleClose();
|
||||
|
||||
setToastAlert({
|
||||
@ -92,6 +94,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
default:
|
||||
mutate(CYCLE_DRAFT_LIST(projectId as string));
|
||||
}
|
||||
mutate(CYCLE_DETAILS(projectId as string));
|
||||
if (
|
||||
getDateRangeStatus(data?.start_date, data?.end_date) !=
|
||||
getDateRangeStatus(res.start_date, res.end_date)
|
||||
|
@ -13,9 +13,19 @@ import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { AssigneesList, Avatar } from "components/ui/avatar";
|
||||
import { SingleProgressStats } from "components/core";
|
||||
|
||||
// icons
|
||||
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
|
||||
import { TargetIcon } from "components/icons";
|
||||
import { CalendarDaysIcon, ExclamationCircleIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
TargetIcon,
|
||||
ContrastIcon,
|
||||
PersonRunningIcon,
|
||||
ArrowRightIcon,
|
||||
TriangleExclamationIcon,
|
||||
AlarmClockIcon,
|
||||
} from "components/icons";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
LinkIcon,
|
||||
@ -24,7 +34,11 @@ import {
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { getDateRangeStatus, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
import {
|
||||
getDateRangeStatus,
|
||||
renderShortDateWithYearFormat,
|
||||
findHowManyDaysLeft,
|
||||
} from "helpers/date-time.helper";
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import {
|
||||
@ -86,14 +100,13 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
const endDate = new Date(cycle.end_date ?? "");
|
||||
const startDate = new Date(cycle.start_date ?? "");
|
||||
|
||||
const handleAddToFavorites = () => {
|
||||
if (!workspaceSlug || !projectId || !cycle) return;
|
||||
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
|
||||
switch (cycleStatus) {
|
||||
case "current":
|
||||
case "upcoming":
|
||||
@ -154,8 +167,6 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
const handleRemoveFromFavorites = () => {
|
||||
if (!workspaceSlug || !projectId || !cycle) return;
|
||||
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
|
||||
switch (cycleStatus) {
|
||||
case "current":
|
||||
case "upcoming":
|
||||
@ -236,18 +247,88 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
color: group.color,
|
||||
}));
|
||||
|
||||
const groupedIssues: any = {
|
||||
backlog: cycle.backlog_issues,
|
||||
unstarted: cycle.unstarted_issues,
|
||||
started: cycle.started_issues,
|
||||
completed: cycle.completed_issues,
|
||||
cancelled: cycle.cancelled_issues,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col rounded-[10px] bg-brand-base text-xs shadow">
|
||||
<div className="flex flex-col rounded-[10px] bg-brand-base border border-brand-base text-xs shadow">
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
||||
<a className="w-full">
|
||||
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
|
||||
<div className="flex items-start justify-between gap-1">
|
||||
<Tooltip tooltipContent={cycle.name} position="top-left">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="flex items-center gap-1">
|
||||
<ContrastIcon
|
||||
className="h-5 w-5"
|
||||
color={`${
|
||||
cycleStatus === "current"
|
||||
? "#09A953"
|
||||
: cycleStatus === "upcoming"
|
||||
? "#F7AE59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3F76FF"
|
||||
: cycleStatus === "draft"
|
||||
? "#858E96"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
<Tooltip tooltipContent={cycle.name} className="break-all" position="top-left">
|
||||
<h3 className="break-all text-lg font-semibold">
|
||||
{truncateText(cycle.name, 75)}
|
||||
{truncateText(cycle.name, 15)}
|
||||
</h3>
|
||||
</Tooltip>
|
||||
</span>
|
||||
<span className="flex items-center gap-1 capitalize">
|
||||
<span
|
||||
className={`rounded-full px-1.5 py-0.5
|
||||
${
|
||||
cycleStatus === "current"
|
||||
? "bg-green-600/5 text-green-600"
|
||||
: cycleStatus === "upcoming"
|
||||
? "bg-orange-300/5 text-orange-300"
|
||||
: cycleStatus === "completed"
|
||||
? "bg-blue-500/5 text-blue-500"
|
||||
: cycleStatus === "draft"
|
||||
? "bg-neutral-400/5 text-neutral-400"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{cycleStatus === "current" ? (
|
||||
<span className="flex gap-1">
|
||||
<PersonRunningIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
|
||||
</span>
|
||||
) : cycleStatus === "upcoming" ? (
|
||||
<span className="flex gap-1">
|
||||
<AlarmClockIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left
|
||||
</span>
|
||||
) : cycleStatus === "completed" ? (
|
||||
<span className="flex gap-1">
|
||||
{cycle.total_issues - cycle.completed_issues > 0 && (
|
||||
<Tooltip
|
||||
tooltipContent={`${
|
||||
cycle.total_issues - cycle.completed_issues
|
||||
} more pending ${
|
||||
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
|
||||
}`}
|
||||
>
|
||||
<span>
|
||||
<TriangleExclamationIcon className="h-3.5 w-3.5 fill-current" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}{" "}
|
||||
Completed
|
||||
</span>
|
||||
) : (
|
||||
cycleStatus
|
||||
)}
|
||||
</span>
|
||||
{cycle.is_favorite ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@ -267,23 +348,29 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
<StarIcon className="h-4 w-4 " color="#858E96" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-start gap-5 text-brand-secondary">
|
||||
<div className="flex h-4 items-center justify-start gap-5 text-brand-secondary">
|
||||
{cycleStatus !== "draft" && (
|
||||
<>
|
||||
<div className="flex items-start gap-1">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
<span>Start :</span>
|
||||
<span>{renderShortDateWithYearFormat(startDate)}</span>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-4 w-4" />
|
||||
<div className="flex items-start gap-1">
|
||||
<TargetIcon className="h-4 w-4" />
|
||||
<span>End :</span>
|
||||
<span>{renderShortDateWithYearFormat(endDate)}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between text-brand-secondary">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex justify-between items-end">
|
||||
<div className="flex flex-col gap-2 text-xs text-brand-secondary">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-16">Creator:</div>
|
||||
<div className="flex items-center gap-2.5 text-brand-secondary">
|
||||
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
||||
<Image
|
||||
src={cycle.owned_by.avatar}
|
||||
@ -293,12 +380,25 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
alt={cycle.owned_by.first_name}
|
||||
/>
|
||||
) : (
|
||||
<span className="bg-brand-secondary flex h-5 w-5 items-center justify-center rounded-full capitalize">
|
||||
<span className="bg-brand-secondary flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
|
||||
{cycle.owned_by.first_name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<span>{cycle.owned_by.first_name}</span>
|
||||
<span className="text-brand-secondary">{cycle.owned_by.first_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-5 items-center gap-2">
|
||||
<div className="w-16">Members:</div>
|
||||
{cycle.assignees.length > 0 ? (
|
||||
<div className="flex items-center gap-1 text-brand-secondary">
|
||||
<AssigneesList users={cycle.assignees} length={4} />
|
||||
</div>
|
||||
) : (
|
||||
"No members"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
{!isCompleted && (
|
||||
<button
|
||||
@ -306,7 +406,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
e.preventDefault();
|
||||
handleEditCycle();
|
||||
}}
|
||||
className="flex cursor-pointer items-center rounded p-1 duration-300 hover:bg-brand-surface-1"
|
||||
className="flex cursor-pointer items-center rounded p-1 text-brand-secondary duration-300 hover:bg-brand-surface-1"
|
||||
>
|
||||
<span>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
@ -356,7 +456,35 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
>
|
||||
<div className="flex w-full items-center gap-2 px-4 py-1">
|
||||
<span>Progress</span>
|
||||
<LinearProgressIndicator data={progressIndicatorData} />
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
<div className="flex w-56 flex-col">
|
||||
{Object.keys(groupedIssues).map((group, index) => (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full "
|
||||
style={{
|
||||
backgroundColor: stateGroups[index].color,
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs capitalize">{group}</span>
|
||||
</div>
|
||||
}
|
||||
completed={groupedIssues[group]}
|
||||
total={cycle.total_issues}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
position="bottom"
|
||||
>
|
||||
<div className="flex w-full items-center">
|
||||
<LinearProgressIndicator data={progressIndicatorData} noTooltip={true} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Disclosure.Button>
|
||||
<span className="p-1">
|
||||
<ChevronDownIcon
|
||||
|
510
apps/app/components/cycles/single-cycle-list.tsx
Normal file
510
apps/app/components/cycles/single-cycle-list.tsx
Normal file
@ -0,0 +1,510 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui";
|
||||
|
||||
// icons
|
||||
import { CalendarDaysIcon, ExclamationCircleIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
TargetIcon,
|
||||
ContrastIcon,
|
||||
PersonRunningIcon,
|
||||
ArrowRightIcon,
|
||||
TriangleExclamationIcon,
|
||||
AlarmClockIcon,
|
||||
} from "components/icons";
|
||||
import { LinkIcon, PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import {
|
||||
getDateRangeStatus,
|
||||
renderShortDateWithYearFormat,
|
||||
findHowManyDaysLeft,
|
||||
} from "helpers/date-time.helper";
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import {
|
||||
CompletedCyclesResponse,
|
||||
CurrentAndUpcomingCyclesResponse,
|
||||
DraftCyclesResponse,
|
||||
ICycle,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST,
|
||||
CYCLE_DRAFT_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
import { type } from "os";
|
||||
|
||||
type TSingleStatProps = {
|
||||
cycle: ICycle;
|
||||
handleEditCycle: () => void;
|
||||
handleDeleteCycle: () => void;
|
||||
isCompleted?: boolean;
|
||||
};
|
||||
|
||||
const stateGroups = [
|
||||
{
|
||||
key: "backlog_issues",
|
||||
title: "Backlog",
|
||||
color: "#dee2e6",
|
||||
},
|
||||
{
|
||||
key: "unstarted_issues",
|
||||
title: "Unstarted",
|
||||
color: "#26b5ce",
|
||||
},
|
||||
{
|
||||
key: "started_issues",
|
||||
title: "Started",
|
||||
color: "#f7ae59",
|
||||
},
|
||||
{
|
||||
key: "cancelled_issues",
|
||||
title: "Cancelled",
|
||||
color: "#d687ff",
|
||||
},
|
||||
{
|
||||
key: "completed_issues",
|
||||
title: "Completed",
|
||||
color: "#09a953",
|
||||
},
|
||||
];
|
||||
|
||||
type progress = {
|
||||
progress: number;
|
||||
};
|
||||
|
||||
function RadialProgressBar({ progress }: progress) {
|
||||
const [circumference, setCircumference] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const radius = 40;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
setCircumference(circumference);
|
||||
}, []);
|
||||
|
||||
const progressOffset = ((100 - progress) / 100) * circumference;
|
||||
|
||||
return (
|
||||
<div className="relative h-4 w-4">
|
||||
<svg className="absolute top-0 left-0" viewBox="0 0 100 100">
|
||||
<circle
|
||||
className={"stroke-current opacity-10"}
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
strokeWidth="12"
|
||||
fill="none"
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
/>
|
||||
<circle
|
||||
className={`stroke-current`}
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
strokeWidth="12"
|
||||
fill="none"
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
strokeDashoffset={progressOffset}
|
||||
transform="rotate(-90 50 50)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
||||
cycle,
|
||||
handleEditCycle,
|
||||
handleDeleteCycle,
|
||||
isCompleted = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
const endDate = new Date(cycle.end_date ?? "");
|
||||
const startDate = new Date(cycle.start_date ?? "");
|
||||
|
||||
const handleAddToFavorites = () => {
|
||||
if (!workspaceSlug || !projectId || !cycle) return;
|
||||
|
||||
switch (cycleStatus) {
|
||||
case "current":
|
||||
case "upcoming":
|
||||
mutate<CurrentAndUpcomingCyclesResponse>(
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
current_cycle: (prevData?.current_cycle ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
}),
|
||||
false
|
||||
);
|
||||
break;
|
||||
case "completed":
|
||||
mutate<CompletedCyclesResponse>(
|
||||
CYCLE_COMPLETE_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
}),
|
||||
false
|
||||
);
|
||||
break;
|
||||
case "draft":
|
||||
mutate<DraftCyclesResponse>(
|
||||
CYCLE_DRAFT_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
}),
|
||||
false
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
cyclesService
|
||||
.addCycleToFavorites(workspaceSlug as string, projectId as string, {
|
||||
cycle: cycle.id,
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = () => {
|
||||
if (!workspaceSlug || !projectId || !cycle) return;
|
||||
|
||||
switch (cycleStatus) {
|
||||
case "current":
|
||||
case "upcoming":
|
||||
mutate<CurrentAndUpcomingCyclesResponse>(
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
current_cycle: (prevData?.current_cycle ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
}),
|
||||
false
|
||||
);
|
||||
break;
|
||||
case "completed":
|
||||
mutate<CompletedCyclesResponse>(
|
||||
CYCLE_COMPLETE_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
}),
|
||||
false
|
||||
);
|
||||
break;
|
||||
case "draft":
|
||||
mutate<DraftCyclesResponse>(
|
||||
CYCLE_DRAFT_LIST(projectId as string),
|
||||
(prevData) => ({
|
||||
draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
}),
|
||||
false
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
cyclesService
|
||||
.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id)
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't remove the cycle from favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Cycle link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const progressIndicatorData = stateGroups.map((group, index) => ({
|
||||
id: index,
|
||||
name: group.title,
|
||||
value:
|
||||
cycle.total_issues > 0
|
||||
? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100
|
||||
: 0,
|
||||
color: group.color,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col border-b border-brand-base text-xs hover:bg-brand-surface-2">
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
||||
<a className="w-full">
|
||||
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="flex items-start gap-2">
|
||||
<ContrastIcon
|
||||
className="mt-1 h-5 w-5"
|
||||
color={`${
|
||||
cycleStatus === "current"
|
||||
? "#09A953"
|
||||
: cycleStatus === "upcoming"
|
||||
? "#F7AE59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3F76FF"
|
||||
: cycleStatus === "draft"
|
||||
? "#858E96"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
<div>
|
||||
<Tooltip tooltipContent={cycle.name} className="break-all" position="top-left">
|
||||
<h3 className="break-all text-base font-semibold">
|
||||
{truncateText(cycle.name, 70)}
|
||||
</h3>
|
||||
</Tooltip>
|
||||
<p className="mt-2 text-brand-secondary">{cycle.description}</p>
|
||||
</div>
|
||||
</span>
|
||||
<span className="flex items-center gap-4 capitalize">
|
||||
<span
|
||||
className={`rounded-full px-1.5 py-0.5
|
||||
${
|
||||
cycleStatus === "current"
|
||||
? "bg-green-600/5 text-green-600"
|
||||
: cycleStatus === "upcoming"
|
||||
? "bg-orange-300/5 text-orange-300"
|
||||
: cycleStatus === "completed"
|
||||
? "bg-blue-500/5 text-blue-500"
|
||||
: cycleStatus === "draft"
|
||||
? "bg-neutral-400/5 text-neutral-400"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{cycleStatus === "current" ? (
|
||||
<span className="flex gap-1">
|
||||
<PersonRunningIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
|
||||
</span>
|
||||
) : cycleStatus === "upcoming" ? (
|
||||
<span className="flex gap-1">
|
||||
<AlarmClockIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left
|
||||
</span>
|
||||
) : cycleStatus === "completed" ? (
|
||||
<span className="flex items-center gap-1">
|
||||
{cycle.total_issues - cycle.completed_issues > 0 && (
|
||||
<Tooltip
|
||||
tooltipContent={`${
|
||||
cycle.total_issues - cycle.completed_issues
|
||||
} more pending ${
|
||||
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
|
||||
}`}
|
||||
>
|
||||
<span>
|
||||
<TriangleExclamationIcon className="h-3.5 w-3.5 fill-current" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}{" "}
|
||||
Completed
|
||||
</span>
|
||||
) : (
|
||||
cycleStatus
|
||||
)}
|
||||
</span>
|
||||
|
||||
{cycleStatus !== "draft" && (
|
||||
<div className="flex items-center justify-start gap-2 text-brand-secondary">
|
||||
<div className="flex items-start gap-1 ">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
<span>{renderShortDateWithYearFormat(startDate)}</span>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-4 w-4" />
|
||||
<div className="flex items-start gap-1 ">
|
||||
<TargetIcon className="h-4 w-4" />
|
||||
<span>{renderShortDateWithYearFormat(endDate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2.5 text-brand-secondary">
|
||||
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
||||
<Image
|
||||
src={cycle.owned_by.avatar}
|
||||
height={16}
|
||||
width={16}
|
||||
className="rounded-full"
|
||||
alt={cycle.owned_by.first_name}
|
||||
/>
|
||||
) : (
|
||||
<span className="bg-brand-secondary flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
|
||||
{cycle.owned_by.first_name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip
|
||||
position="top-right"
|
||||
tooltipContent={
|
||||
<div className="flex w-80 items-center gap-2 px-4 py-1">
|
||||
<span>Progress</span>
|
||||
<LinearProgressIndicator data={progressIndicatorData} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={`rounded-md px-1.5 py-1
|
||||
${
|
||||
cycleStatus === "current"
|
||||
? "border border-green-600 bg-green-600/5 text-green-600"
|
||||
: cycleStatus === "upcoming"
|
||||
? "border border-orange-300 bg-orange-300/5 text-orange-300"
|
||||
: cycleStatus === "completed"
|
||||
? "border border-blue-500 bg-blue-500/5 text-blue-500"
|
||||
: cycleStatus === "draft"
|
||||
? "border border-neutral-400 bg-neutral-400/5 text-neutral-400"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{cycleStatus === "current" ? (
|
||||
<span className="flex gap-1">
|
||||
<RadialProgressBar
|
||||
progress={(cycle.completed_issues / cycle.total_issues) * 100}
|
||||
/>
|
||||
<span>
|
||||
{Math.floor((cycle.completed_issues / cycle.total_issues) * 100)} %
|
||||
</span>
|
||||
</span>
|
||||
) : cycleStatus === "upcoming" ? (
|
||||
<span className="flex gap-1">
|
||||
<RadialProgressBar progress={100} /> Yet to start
|
||||
</span>
|
||||
) : cycleStatus === "completed" ? (
|
||||
<span className="flex gap-1">
|
||||
<RadialProgressBar progress={100} />
|
||||
<span>{100} %</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex gap-1">
|
||||
<RadialProgressBar
|
||||
progress={(cycle.total_issues / cycle.completed_issues) * 100}
|
||||
/>
|
||||
{cycleStatus}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{cycle.is_favorite ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleRemoveFromFavorites();
|
||||
}}
|
||||
>
|
||||
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleAddToFavorites();
|
||||
}}
|
||||
>
|
||||
<StarIcon className="h-4 w-4 " color="#858E96" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<CustomMenu width="auto" verticalEllipsis>
|
||||
{!isCompleted && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleEditCycle();
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit Cycle</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{!isCompleted && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleDeleteCycle();
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete cycle</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleCopyText();
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy cycle link</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
22
apps/app/components/icons/alarm-clock-icon.tsx
Normal file
22
apps/app/components/icons/alarm-clock-icon.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const AlarmClockIcon: React.FC<Props> = ({
|
||||
width = "24",
|
||||
height = "24",
|
||||
color = "#858E96",
|
||||
className,
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 18 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.98125 15.4609C8.05625 15.4609 7.18437 15.2859 6.36562 14.9359C5.54687 14.5859 4.83437 14.1078 4.22812 13.5016C3.62187 12.8953 3.14062 12.1828 2.78437 11.3641C2.42812 10.5453 2.25 9.66886 2.25 8.73469C2.25 7.80053 2.42812 6.92553 2.78437 6.10969C3.14062 5.29386 3.62187 4.57969 4.22812 3.96719C4.83437 3.35469 5.54687 2.87344 6.36562 2.52344C7.18437 2.17344 8.05625 1.99844 8.98125 1.99844C9.90625 1.99844 10.7781 2.17344 11.5969 2.52344C12.4156 2.87344 13.1312 3.35469 13.7437 3.96719C14.3562 4.57969 14.8375 5.29386 15.1875 6.10969C15.5375 6.92553 15.7125 7.80053 15.7125 8.73469C15.7125 9.66886 15.5375 10.5453 15.1875 11.3641C14.8375 12.1828 14.3562 12.8953 13.7437 13.5016C13.1312 14.1078 12.4156 14.5859 11.5969 14.9359C10.7781 15.2859 9.90625 15.4609 8.98125 15.4609ZM11.25 11.7859L12.0375 10.9984L9.6 8.56094V4.99844H8.475V9.01094L11.25 11.7859ZM4.0125 0.742188L4.8 1.52969L1.725 4.49219L0.9375 3.70469L4.0125 0.742188ZM13.95 0.742188L17.025 3.70469L16.2375 4.49219L13.1625 1.52969L13.95 0.742188ZM8.98206 14.3359C10.544 14.3359 11.8687 13.7919 12.9562 12.7039C14.0437 11.6158 14.5875 10.2908 14.5875 8.72888C14.5875 7.16692 14.0435 5.84219 12.9554 4.75469C11.8674 3.66719 10.5424 3.12344 8.98044 3.12344C7.41848 3.12344 6.09375 3.66746 5.00625 4.75549C3.91875 5.84353 3.375 7.16853 3.375 8.73049C3.375 10.2925 3.91902 11.6172 5.00706 12.7047C6.09509 13.7922 7.42009 14.3359 8.98206 14.3359Z"
|
||||
fill="#F7AE59"
|
||||
/>
|
||||
</svg>
|
||||
);
|
@ -1,3 +1,4 @@
|
||||
export * from "./alarm-clock-icon";
|
||||
export * from "./attachment-icon";
|
||||
export * from "./backlog-state-icon";
|
||||
export * from "./blocked-icon";
|
||||
@ -26,6 +27,7 @@ export * from "./lock-icon";
|
||||
export * from "./menu-icon";
|
||||
export * from "./pencil-scribble-icon";
|
||||
export * from "./plus-icon";
|
||||
export * from "./person-running-icon";
|
||||
export * from "./priority-icon";
|
||||
export * from "./question-mark-circle-icon";
|
||||
export * from "./setting-icon";
|
||||
@ -70,6 +72,7 @@ export * from "./png-file-icon";
|
||||
export * from "./jpg-file-icon";
|
||||
export * from "./svg-file-icon";
|
||||
export * from "./txt-file-icon";
|
||||
export * from "./triangle-exclamation-icon";
|
||||
export * from "./default-file-icon";
|
||||
export * from "./video-file-icon";
|
||||
export * from "./audio-file-icon";
|
||||
|
19
apps/app/components/icons/person-running-icon.tsx
Normal file
19
apps/app/components/icons/person-running-icon.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const PersonRunningIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 18 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.05 12L3.2625 11.2125L10.9125 3.5625H8.25V5.0625H7.125V2.4375H11.3063C11.4813 2.4375 11.65 2.46875 11.8125 2.53125C11.975 2.59375 12.1188 2.6875 12.2438 2.8125L14.4938 5.04375C14.8563 5.40625 15.275 5.68125 15.75 5.86875C16.225 6.05625 16.725 6.1625 17.25 6.1875V7.3125C16.6 7.2875 15.975 7.16563 15.375 6.94688C14.775 6.72813 14.25 6.3875 13.8 5.925L12.9375 5.0625L10.8 7.2L12.4125 8.8125L7.8375 11.4563L7.275 10.4813L10.575 8.56875L9.0375 7.03125L4.05 12ZM2.25 6.75V5.625H6V6.75H2.25ZM0.75 4.3125V3.1875H4.5V4.3125H0.75ZM14.8125 2.8125C14.45 2.8125 14.1406 2.68438 13.8844 2.42813C13.6281 2.17188 13.5 1.8625 13.5 1.5C13.5 1.1375 13.6281 0.828125 13.8844 0.571875C14.1406 0.315625 14.45 0.1875 14.8125 0.1875C15.175 0.1875 15.4844 0.315625 15.7406 0.571875C15.9969 0.828125 16.125 1.1375 16.125 1.5C16.125 1.8625 15.9969 2.17188 15.7406 2.42813C15.4844 2.68438 15.175 2.8125 14.8125 2.8125ZM2.25 1.875V0.75H6V1.875H2.25Z"
|
||||
fill="#09A953"
|
||||
/>
|
||||
</svg>
|
||||
);
|
20
apps/app/components/icons/triangle-exclamation-icon.tsx
Normal file
20
apps/app/components/icons/triangle-exclamation-icon.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const TriangleExclamationIcon: React.FC<Props> = ({
|
||||
width = "24",
|
||||
height = "24",
|
||||
className,
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 16 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M0.7254 13.75C0.6129 13.75 0.515075 13.7242 0.431925 13.6727C0.348775 13.6211 0.2841 13.5531 0.2379 13.4688C0.185813 13.3863 0.157169 13.2969 0.151969 13.2006C0.146757 13.1044 0.1754 13.0063 0.2379 12.9063L7.5129 0.34375C7.5754 0.24375 7.64728 0.171875 7.72853 0.128125C7.80978 0.084375 7.9004 0.0625 8.0004 0.0625C8.1004 0.0625 8.19103 0.084375 8.27228 0.128125C8.35353 0.171875 8.4254 0.24375 8.4879 0.34375L15.7629 12.9063C15.8254 13.0063 15.854 13.1044 15.8488 13.2006C15.8436 13.2969 15.815 13.3863 15.7629 13.4688C15.7167 13.5531 15.652 13.6211 15.5689 13.6727C15.4857 13.7242 15.3879 13.75 15.2754 13.75H0.7254ZM1.7004 12.625H14.3004L8.0004 1.75L1.7004 12.625ZM8.07868 11.5563C8.23899 11.5563 8.37228 11.502 8.47853 11.3936C8.58478 11.2851 8.6379 11.1508 8.6379 10.9905C8.6379 10.8302 8.58368 10.6969 8.47524 10.5906C8.36679 10.4844 8.23242 10.4313 8.07212 10.4313C7.91181 10.4313 7.77853 10.4855 7.67228 10.5939C7.56603 10.7024 7.5129 10.8367 7.5129 10.997C7.5129 11.1573 7.56712 11.2906 7.67556 11.3969C7.78401 11.5031 7.91838 11.5563 8.07868 11.5563ZM8.07868 9.475C8.23899 9.475 8.37228 9.42109 8.47853 9.31328C8.58478 9.20547 8.6379 9.07188 8.6379 8.9125V5.8375C8.6379 5.67813 8.58368 5.54453 8.47524 5.43672C8.36679 5.32891 8.23242 5.275 8.07212 5.275C7.91181 5.275 7.77853 5.32891 7.67228 5.43672C7.56603 5.54453 7.5129 5.67813 7.5129 5.8375V8.9125C7.5129 9.07188 7.56712 9.20547 7.67556 9.31328C7.78401 9.42109 7.91838 9.475 8.07868 9.475Z" />
|
||||
</svg>
|
||||
);
|
@ -3,9 +3,10 @@ import { Tooltip } from "./tooltip";
|
||||
|
||||
type Props = {
|
||||
data: any;
|
||||
noTooltip?: boolean
|
||||
};
|
||||
|
||||
export const LinearProgressIndicator: React.FC<Props> = ({ data }) => {
|
||||
export const LinearProgressIndicator: React.FC<Props> = ({ data, noTooltip=false }) => {
|
||||
const total = data.reduce((acc: any, cur: any) => acc + cur.value, 0);
|
||||
let progress = 0;
|
||||
|
||||
@ -16,8 +17,8 @@ export const LinearProgressIndicator: React.FC<Props> = ({ data }) => {
|
||||
backgroundColor: item.color,
|
||||
};
|
||||
progress += item.value;
|
||||
|
||||
return (
|
||||
if (noTooltip) return <div style={style} />
|
||||
else return (
|
||||
<Tooltip key={item.id} tooltipContent={`${item.name} ${Math.round(item.value)}%`}>
|
||||
<div style={style} />
|
||||
</Tooltip>
|
||||
@ -26,7 +27,11 @@ export const LinearProgressIndicator: React.FC<Props> = ({ data }) => {
|
||||
|
||||
return (
|
||||
<div className="flex h-1 w-full items-center justify-between gap-1">
|
||||
{total === 0 ? " - 0%" : <div className="flex h-full w-full gap-1">{bars}</div>}
|
||||
{total === 0 ? (
|
||||
<div className="flex h-full w-full gap-1 bg-neutral-500">{bars}</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full gap-1">{bars}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -4,7 +4,8 @@ import { Tooltip2 } from "@blueprintjs/popover2";
|
||||
|
||||
type Props = {
|
||||
tooltipHeading?: string;
|
||||
tooltipContent: string;
|
||||
tooltipContent: string | JSX.Element;
|
||||
triangle?: boolean;
|
||||
position?:
|
||||
| "top"
|
||||
| "right"
|
||||
@ -35,17 +36,23 @@ export const Tooltip: React.FC<Props> = ({
|
||||
disabled = false,
|
||||
className = "",
|
||||
theme = "light",
|
||||
triangle,
|
||||
}) => (
|
||||
<Tooltip2
|
||||
disabled={disabled}
|
||||
content={
|
||||
<div
|
||||
className={`${className} flex max-w-[600px] flex-col items-start justify-center gap-1 rounded-md p-2 text-left text-xs shadow-md ${
|
||||
theme === "light" ? "bg-brand-surface-2 text-brand-muted-1" : "bg-black text-white"
|
||||
className={`${className} relative flex max-w-[600px] flex-col items-start justify-center gap-1 rounded-md p-2 text-left text-xs shadow-md ${
|
||||
theme === "light" ? "text-brand-muted-1 bg-brand-surface-2" : "bg-black text-white"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute inset-0 left-1/2 -top-1 h-3 w-3 rotate-45 bg-brand-surface-2 ${
|
||||
theme === "light" ? "text-brand-muted-1 bg-brand-surface-2" : "bg-black text-white"
|
||||
}`}
|
||||
/>
|
||||
{tooltipHeading && <h5 className="font-medium">{tooltipHeading}</h5>}
|
||||
<p>{tooltipContent}</p>
|
||||
{tooltipContent}
|
||||
</div>
|
||||
}
|
||||
position={position}
|
||||
|
@ -1,23 +1,18 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
// hooks
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// services
|
||||
import cycleService from "services/cycles.service";
|
||||
import projectService from "services/project.service";
|
||||
// layouts
|
||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
// components
|
||||
import { CompletedCyclesListProps, CreateUpdateCycleModal, CyclesList } from "components/cycles";
|
||||
import { CreateUpdateCycleModal, CyclesView } from "components/cycles";
|
||||
// ui
|
||||
import { Loader, PrimaryButton } from "components/ui";
|
||||
import { PrimaryButton } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
@ -29,25 +24,13 @@ import {
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST,
|
||||
CYCLE_DRAFT_LIST,
|
||||
PROJECT_DETAILS,
|
||||
CYCLE_DETAILS,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
const CompletedCyclesList = dynamic<CompletedCyclesListProps>(
|
||||
() => import("components/cycles").then((a) => a.CompletedCyclesList),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<Loader className="mb-5">
|
||||
<Loader.Item height="12rem" width="100%" />
|
||||
</Loader>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
const ProjectCycles: NextPage = () => {
|
||||
const [selectedCycle, setSelectedCycle] = useState<SelectCycleType>();
|
||||
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
|
||||
|
||||
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "Upcoming");
|
||||
const [cycleView, setCycleView] = useState<string>("list");
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
@ -73,6 +56,13 @@ const ProjectCycles: NextPage = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: cyclesCompleteList } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_DETAILS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => cycleService.getCycles(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (createUpdateCycleModal) return;
|
||||
const timer = setTimeout(() => {
|
||||
@ -83,13 +73,16 @@ const ProjectCycles: NextPage = () => {
|
||||
|
||||
const currentTabValue = (tab: string | null) => {
|
||||
switch (tab) {
|
||||
case "Upcoming":
|
||||
case "All":
|
||||
return 0;
|
||||
case "Completed":
|
||||
case "Active":
|
||||
return 1;
|
||||
case "Drafts":
|
||||
case "Upcoming":
|
||||
return 2;
|
||||
|
||||
case "Completed":
|
||||
return 3;
|
||||
case "Drafts":
|
||||
return 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@ -126,101 +119,16 @@ const ProjectCycles: NextPage = () => {
|
||||
/>
|
||||
<div className="space-y-8 p-8">
|
||||
<div className="flex flex-col gap-5">
|
||||
{currentAndUpcomingCycles && currentAndUpcomingCycles.current_cycle.length > 0 && (
|
||||
<h3 className="text-3xl font-semibold text-brand-base">Current Cycle</h3>
|
||||
)}
|
||||
<div className="space-y-5">
|
||||
<CyclesList
|
||||
cycles={currentAndUpcomingCycles?.current_cycle}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
<h3 className="text-2xl font-semibold text-brand-base">Cycles</h3>
|
||||
<CyclesView
|
||||
cycleView={cycleView}
|
||||
setCycleView={setCycleView}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="current"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-5">
|
||||
<h3 className="text-3xl font-semibold text-brand-base">Other Cycles</h3>
|
||||
<div>
|
||||
<Tab.Group
|
||||
defaultIndex={currentTabValue(cycleTab)}
|
||||
onChange={(i) => {
|
||||
switch (i) {
|
||||
case 0:
|
||||
return setCycleTab("Upcoming");
|
||||
case 1:
|
||||
return setCycleTab("Completed");
|
||||
case 2:
|
||||
return setCycleTab("Drafts");
|
||||
|
||||
default:
|
||||
return setCycleTab("Upcoming");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab.List
|
||||
as="div"
|
||||
className="flex items-center justify-start gap-4 text-base font-medium"
|
||||
>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border px-5 py-1.5 text-sm outline-none sm:px-7 sm:py-2 sm:text-base ${
|
||||
selected
|
||||
? "border-brand-accent bg-brand-accent text-white"
|
||||
: "border-brand-base bg-brand-surface-2 hover:bg-brand-surface-1"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Upcoming
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border px-5 py-1.5 text-sm outline-none sm:px-7 sm:py-2 sm:text-base ${
|
||||
selected
|
||||
? "border-brand-accent bg-brand-accent text-white"
|
||||
: "border-brand-base bg-brand-surface-2 hover:bg-brand-surface-1"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Completed
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border px-5 py-1.5 text-sm outline-none sm:px-7 sm:py-2 sm:text-base ${
|
||||
selected
|
||||
? "border-brand-accent bg-brand-accent text-white"
|
||||
: "border-brand-base bg-brand-surface-2 hover:bg-brand-surface-1"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Drafts
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel as="div" className="mt-8 space-y-5">
|
||||
<CyclesList
|
||||
cycles={currentAndUpcomingCycles?.upcoming_cycle}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="upcoming"
|
||||
cyclesCompleteList={cyclesCompleteList}
|
||||
currentAndUpcomingCycles={currentAndUpcomingCycles}
|
||||
draftCycles={draftCycles}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="mt-8 space-y-5">
|
||||
<CompletedCyclesList
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="mt-8 space-y-5">
|
||||
<CyclesList
|
||||
cycles={draftCycles?.draft_cycles}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
type="draft"
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
|
2
apps/app/types/cycles.d.ts
vendored
2
apps/app/types/cycles.d.ts
vendored
@ -6,6 +6,7 @@ import type {
|
||||
IWorkspace,
|
||||
IWorkspaceLite,
|
||||
IIssueFilterOptions,
|
||||
IUserLite,
|
||||
} from "types";
|
||||
|
||||
export interface ICycle {
|
||||
@ -29,6 +30,7 @@ export interface ICycle {
|
||||
unstarted_issues: number;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
assignees: IUserLite[];
|
||||
view_props: {
|
||||
filters: IIssueFilterOptions;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user