-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[WEB-413] chore: project active cycle UI revamp (#3997)
* chore: project active cycle ui revamp * chore: resolved liniting issues --------- Co-authored-by: gurusainath <gurusainath007@gmail.com>
- Loading branch information
1 parent
bca3e13
commit 7142889
Showing
8 changed files
with
530 additions
and
368 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,260 @@ | ||
import { FC, Fragment } from "react"; | ||
import { observer } from "mobx-react"; | ||
import Link from "next/link"; | ||
import useSWR from "swr"; | ||
import { CalendarCheck } from "lucide-react"; | ||
import { Tab } from "@headlessui/react"; | ||
// types | ||
import { ICycle, TIssue } from "@plane/types"; | ||
// ui | ||
import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui"; | ||
// components | ||
import { SingleProgressStats } from "@/components/core"; | ||
import { StateDropdown } from "@/components/dropdowns"; | ||
// constants | ||
import { CYCLE_ISSUES_WITH_PARAMS } from "@/constants/fetch-keys"; | ||
import { EIssuesStoreType } from "@/constants/issue"; | ||
// helper | ||
import { cn } from "@/helpers/common.helper"; | ||
import { renderFormattedDate, renderFormattedDateWithoutYear } from "@/helpers/date-time.helper"; | ||
// hooks | ||
import { useIssues, useProject } from "@/hooks/store"; | ||
import useLocalStorage from "@/hooks/use-local-storage"; | ||
|
||
export type ActiveCycleStatsProps = { | ||
workspaceSlug: string; | ||
projectId: string; | ||
cycle: ICycle; | ||
}; | ||
|
||
export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => { | ||
const { workspaceSlug, projectId, cycle } = props; | ||
|
||
const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees"); | ||
|
||
const currentValue = (tab: string | null) => { | ||
switch (tab) { | ||
case "Priority-Issues": | ||
return 0; | ||
case "Assignees": | ||
return 1; | ||
case "Labels": | ||
return 2; | ||
default: | ||
return 0; | ||
} | ||
}; | ||
const { | ||
issues: { fetchActiveCycleIssues }, | ||
} = useIssues(EIssuesStoreType.CYCLE); | ||
|
||
const { currentProjectDetails } = useProject(); | ||
|
||
const { data: activeCycleIssues } = useSWR( | ||
workspaceSlug && projectId && cycle.id ? CYCLE_ISSUES_WITH_PARAMS(cycle.id, { priority: "urgent,high" }) : null, | ||
workspaceSlug && projectId && cycle.id ? () => fetchActiveCycleIssues(workspaceSlug, projectId, cycle.id) : null | ||
); | ||
|
||
const cycleIssues = activeCycleIssues ?? []; | ||
|
||
return ( | ||
<div className="flex flex-col gap-4 p-4 min-h-[17rem] overflow-hidden col-span-1 lg:col-span-2 xl:col-span-1 border border-custom-border-200 rounded-lg"> | ||
<Tab.Group | ||
as={Fragment} | ||
defaultIndex={currentValue(tab)} | ||
onChange={(i) => { | ||
switch (i) { | ||
case 0: | ||
return setTab("Priority-Issues"); | ||
case 1: | ||
return setTab("Assignees"); | ||
case 2: | ||
return setTab("Labels"); | ||
|
||
default: | ||
return setTab("Priority-Issues"); | ||
} | ||
}} | ||
> | ||
<Tab.List | ||
as="div" | ||
className="relative border-[0.5px] border-custom-border-200 rounded bg-custom-background-80 p-[1px] grid" | ||
style={{ | ||
gridTemplateColumns: `repeat(3, 1fr)`, | ||
}} | ||
> | ||
<Tab | ||
className={({ selected }) => | ||
cn( | ||
"relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500", | ||
{ | ||
"text-custom-text-300 bg-custom-background-100": selected, | ||
"hover:text-custom-text-300": !selected, | ||
} | ||
) | ||
} | ||
> | ||
Priority Issues | ||
</Tab> | ||
<Tab | ||
className={({ selected }) => | ||
cn( | ||
"relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500", | ||
{ | ||
"text-custom-text-300 bg-custom-background-100": selected, | ||
"hover:text-custom-text-300": !selected, | ||
} | ||
) | ||
} | ||
> | ||
Assignees | ||
</Tab> | ||
<Tab | ||
className={({ selected }) => | ||
cn( | ||
"relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500", | ||
{ | ||
"text-custom-text-300 bg-custom-background-100": selected, | ||
"hover:text-custom-text-300": !selected, | ||
} | ||
) | ||
} | ||
> | ||
Labels | ||
</Tab> | ||
</Tab.List> | ||
|
||
<Tab.Panels as={Fragment}> | ||
<Tab.Panel | ||
as="div" | ||
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm" | ||
> | ||
<div className="flex flex-col gap-1 h-full w-full overflow-y-auto vertical-scrollbar scrollbar-sm"> | ||
{cycleIssues ? ( | ||
cycleIssues.length > 0 ? ( | ||
cycleIssues.map((issue: TIssue) => ( | ||
<Link | ||
key={issue.id} | ||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`} | ||
className="group flex cursor-pointer items-center justify-between gap-2 rounded-md hover:bg-custom-background-90 p-1" | ||
> | ||
<div className="flex items-center gap-1.5 flex-grow w-full min-w-24 truncate"> | ||
<PriorityIcon priority={issue.priority} withContainer size={12} /> | ||
|
||
<Tooltip | ||
tooltipHeading="Issue ID" | ||
tooltipContent={`${currentProjectDetails?.identifier}-${issue.sequence_id}`} | ||
> | ||
<span className="flex-shrink-0 text-xs text-custom-text-200"> | ||
{currentProjectDetails?.identifier}-{issue.sequence_id} | ||
</span> | ||
</Tooltip> | ||
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}> | ||
<span className="text-[0.825rem] text-custom-text-100 truncate">{issue.name}</span> | ||
</Tooltip> | ||
</div> | ||
<div className="flex items-center gap-1.5 flex-shrink-0"> | ||
<StateDropdown | ||
value={issue.state_id ?? undefined} | ||
onChange={() => {}} | ||
projectId={projectId?.toString() ?? ""} | ||
disabled | ||
buttonVariant="background-with-text" | ||
buttonContainerClassName="cursor-pointer max-w-24" | ||
showTooltip | ||
/> | ||
{issue.target_date && ( | ||
<Tooltip tooltipHeading="Target Date" tooltipContent={renderFormattedDate(issue.target_date)}> | ||
<div className="h-full flex truncate items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80 group-hover:bg-custom-background-100 cursor-pointer"> | ||
<CalendarCheck className="h-3 w-3 flex-shrink-0" /> | ||
<span className="text-xs truncate"> | ||
{renderFormattedDateWithoutYear(issue.target_date)} | ||
</span> | ||
</div> | ||
</Tooltip> | ||
)} | ||
</div> | ||
</Link> | ||
)) | ||
) : ( | ||
<div className="flex items-center justify-center text-center h-full text-sm text-custom-text-200"> | ||
<span>There are no high priority issues present in this cycle.</span> | ||
</div> | ||
) | ||
) : ( | ||
<Loader className="space-y-3"> | ||
<Loader.Item height="50px" /> | ||
<Loader.Item height="50px" /> | ||
<Loader.Item height="50px" /> | ||
</Loader> | ||
)} | ||
</div> | ||
</Tab.Panel> | ||
|
||
<Tab.Panel | ||
as="div" | ||
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm" | ||
> | ||
{cycle.distribution?.assignees?.map((assignee, index) => { | ||
if (assignee.assignee_id) | ||
return ( | ||
<SingleProgressStats | ||
key={assignee.assignee_id} | ||
title={ | ||
<div className="flex items-center gap-2"> | ||
<Avatar name={assignee?.display_name ?? undefined} src={assignee?.avatar ?? undefined} /> | ||
|
||
<span>{assignee.display_name}</span> | ||
</div> | ||
} | ||
completed={assignee.completed_issues} | ||
total={assignee.total_issues} | ||
/> | ||
); | ||
else | ||
return ( | ||
<SingleProgressStats | ||
key={`unassigned-${index}`} | ||
title={ | ||
<div className="flex items-center gap-2"> | ||
<div className="h-5 w-5 rounded-full border-2 border-custom-border-200 bg-custom-background-80"> | ||
<img src="/user.png" height="100%" width="100%" className="rounded-full" alt="User" /> | ||
</div> | ||
<span>No assignee</span> | ||
</div> | ||
} | ||
completed={assignee.completed_issues} | ||
total={assignee.total_issues} | ||
/> | ||
); | ||
})} | ||
</Tab.Panel> | ||
|
||
<Tab.Panel | ||
as="div" | ||
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm" | ||
> | ||
{cycle.distribution?.labels?.map((label, index) => ( | ||
<SingleProgressStats | ||
key={label.label_id ?? `no-label-${index}`} | ||
title={ | ||
<div className="flex items-center gap-2"> | ||
<span | ||
className="block h-3 w-3 rounded-full" | ||
style={{ | ||
backgroundColor: label.color ?? "#000000", | ||
}} | ||
/> | ||
<span className="text-xs">{label.label_name ?? "No labels"}</span> | ||
</div> | ||
} | ||
completed={label.completed_issues} | ||
total={label.total_issues} | ||
/> | ||
))} | ||
</Tab.Panel> | ||
</Tab.Panels> | ||
</Tab.Group> | ||
</div> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { FC } from "react"; | ||
import Link from "next/link"; | ||
// types | ||
import { ICycle, TCycleGroups } from "@plane/types"; | ||
// ui | ||
import { Tooltip, CycleGroupIcon, getButtonStyling, Avatar, AvatarGroup } from "@plane/ui"; | ||
// helpers | ||
import { renderFormattedDate, findHowManyDaysLeft } from "@/helpers/date-time.helper"; | ||
import { truncateText } from "@/helpers/string.helper"; | ||
// hooks | ||
import { useMember } from "@/hooks/store"; | ||
|
||
export type ActiveCycleHeaderProps = { | ||
cycle: ICycle; | ||
workspaceSlug: string; | ||
projectId: string; | ||
}; | ||
|
||
export const ActiveCycleHeader: FC<ActiveCycleHeaderProps> = (props) => { | ||
const { cycle, workspaceSlug, projectId } = props; | ||
// store | ||
const { getUserDetails } = useMember(); | ||
const cycleOwnerDetails = cycle && cycle.owned_by_id ? getUserDetails(cycle.owned_by_id) : undefined; | ||
|
||
const daysLeft = findHowManyDaysLeft(cycle.end_date) ?? 0; | ||
const currentCycleStatus = cycle.status.toLocaleLowerCase() as TCycleGroups; | ||
|
||
const cycleAssignee = (cycle.distribution?.assignees ?? []).filter((assignee) => assignee.display_name); | ||
|
||
return ( | ||
<div className="flex items-center justify-between px-3 py-1.5 rounded border-[0.5px] border-custom-border-100 bg-custom-background-90"> | ||
<div className="flex items-center gap-2 cursor-default"> | ||
<CycleGroupIcon cycleGroup={currentCycleStatus} className="h-4 w-4" /> | ||
<Tooltip tooltipContent={cycle.name} position="top-left"> | ||
<h3 className="break-words text-lg font-medium">{truncateText(cycle.name, 70)}</h3> | ||
</Tooltip> | ||
<Tooltip | ||
tooltipContent={`Start date: ${renderFormattedDate(cycle.start_date ?? "")} Due Date: ${renderFormattedDate( | ||
cycle.end_date ?? "" | ||
)}`} | ||
position="top-left" | ||
> | ||
<span className="flex gap-1 whitespace-nowrap rounded-sm text-custom-text-400 font-semibold text-sm leading-5"> | ||
{`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`} | ||
</span> | ||
</Tooltip> | ||
</div> | ||
<div className="flex items-center gap-4"> | ||
<div className="rounded-sm text-sm"> | ||
<div className="flex gap-2 divide-x spac divide-x-border-300 text-sm whitespace-nowrap text-custom-text-300 font-medium"> | ||
<Avatar name={cycleOwnerDetails?.display_name} src={cycleOwnerDetails?.avatar} /> | ||
{cycleAssignee.length > 0 && ( | ||
<span className="pl-2"> | ||
<AvatarGroup showTooltip> | ||
{cycleAssignee.map((member) => ( | ||
<Avatar | ||
key={member.assignee_id} | ||
name={member?.display_name ?? ""} | ||
src={member?.avatar ?? ""} | ||
showTooltip={false} | ||
/> | ||
))} | ||
</AvatarGroup> | ||
</span> | ||
)} | ||
</div> | ||
</div> | ||
<Link | ||
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`} | ||
className={`${getButtonStyling("outline-primary", "sm")} cursor-pointer`} | ||
> | ||
View Cycle | ||
</Link> | ||
</div> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,8 @@ | ||
export * from "./root"; | ||
export * from "./header"; | ||
export * from "./stats"; | ||
export * from "./upcoming-cycles-list-item"; | ||
export * from "./upcoming-cycles-list"; | ||
export * from "./cycle-stats"; | ||
export * from "./progress"; | ||
export * from "./productivity"; |
Oops, something went wrong.