[WEB-812] chore: project active cycle stats empty state (#4053)

* chore: empty state asset and config file updated

* chore: empty state asset and config file updated

* chore: active cycle empty state implementation
This commit is contained in:
Anmol Singh Bhatia 2024-03-25 13:16:53 +05:30 committed by GitHub
parent 2429ac053d
commit 9f9e508bb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 203 additions and 114 deletions

View File

@ -11,7 +11,9 @@ import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui";
// components // components
import { SingleProgressStats } from "@/components/core"; import { SingleProgressStats } from "@/components/core";
import { StateDropdown } from "@/components/dropdowns"; import { StateDropdown } from "@/components/dropdowns";
import { EmptyState } from "@/components/empty-state";
// constants // constants
import { EmptyStateType } from "@/constants/empty-state";
import { CYCLE_ISSUES_WITH_PARAMS } from "@/constants/fetch-keys"; import { CYCLE_ISSUES_WITH_PARAMS } from "@/constants/fetch-keys";
import { EIssuesStoreType } from "@/constants/issue"; import { EIssuesStoreType } from "@/constants/issue";
// helper // helper
@ -177,8 +179,12 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
</Link> </Link>
)) ))
) : ( ) : (
<div className="flex items-center justify-center text-center h-full text-sm text-custom-text-200"> <div className="flex items-center justify-center h-full w-full">
<span>There are no high priority issues present in this cycle.</span> <EmptyState
type={EmptyStateType.ACTIVE_CYCLE_PRIORITY_ISSUE_EMPTY_STATE}
layout="screen-simple"
size="sm"
/>
</div> </div>
) )
) : ( ) : (
@ -195,63 +201,75 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
as="div" as="div"
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm" 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) => { {cycleIssues.length > 0 ? (
if (assignee.assignee_id) cycle.distribution?.assignees?.map((assignee, index) => {
return ( if (assignee.assignee_id)
<SingleProgressStats return (
key={assignee.assignee_id} <SingleProgressStats
title={ key={assignee.assignee_id}
<div className="flex items-center gap-2"> title={
<Avatar name={assignee?.display_name ?? undefined} src={assignee?.avatar ?? undefined} /> <div className="flex items-center gap-2">
<Avatar name={assignee?.display_name ?? undefined} src={assignee?.avatar ?? undefined} />
<span>{assignee.display_name}</span> <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> </div>
<span>No assignee</span> }
</div> completed={assignee.completed_issues}
} total={assignee.total_issues}
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}
/>
);
})
) : (
<div className="flex items-center justify-center h-full w-full">
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE} layout="screen-simple" size="sm" />
</div>
)}
</Tab.Panel> </Tab.Panel>
<Tab.Panel <Tab.Panel
as="div" as="div"
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm" 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) => ( {cycleIssues.length > 0 ? (
<SingleProgressStats cycle.distribution?.labels?.map((label, index) => (
key={label.label_id ?? `no-label-${index}`} <SingleProgressStats
title={ key={label.label_id ?? `no-label-${index}`}
<div className="flex items-center gap-2"> title={
<span <div className="flex items-center gap-2">
className="block h-3 w-3 rounded-full" <span
style={{ className="block h-3 w-3 rounded-full"
backgroundColor: label.color ?? "#000000", style={{
}} backgroundColor: label.color ?? "#000000",
/> }}
<span className="text-xs">{label.label_name ?? "No labels"}</span> />
</div> <span className="text-xs">{label.label_name ?? "No labels"}</span>
} </div>
completed={label.completed_issues} }
total={label.total_issues} completed={label.completed_issues}
/> total={label.total_issues}
))} />
))
) : (
<div className="flex items-center justify-center h-full w-full">
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_LABEL_EMPTY_STATE} layout="screen-simple" size="sm" />
</div>
)}
</Tab.Panel> </Tab.Panel>
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>

View File

@ -3,6 +3,9 @@ import { FC } from "react";
import { ICycle } from "@plane/types"; import { ICycle } from "@plane/types";
// components // components
import ProgressChart from "@/components/core/sidebar/progress-chart"; import ProgressChart from "@/components/core/sidebar/progress-chart";
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
export type ActiveCycleProductivityProps = { export type ActiveCycleProductivityProps = {
cycle: ICycle; cycle: ICycle;
@ -16,31 +19,40 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props)
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<h3 className="text-base text-custom-text-300 font-semibold">Issue burndown</h3> <h3 className="text-base text-custom-text-300 font-semibold">Issue burndown</h3>
</div> </div>
{cycle.total_issues > 0 ? (
<div className="h-full w-full px-2"> <>
<div className="flex items-center justify-between gap-4 py-1 text-xs text-custom-text-300"> <div className="h-full w-full px-2">
<div className="flex items-center gap-3 text-custom-text-300"> <div className="flex items-center justify-between gap-4 py-1 text-xs text-custom-text-300">
<div className="flex items-center justify-center gap-1"> <div className="flex items-center gap-3 text-custom-text-300">
<span className="h-2 w-2 rounded-full bg-[#A9BBD0]" /> <div className="flex items-center justify-center gap-1">
<span>Ideal</span> <span className="h-2 w-2 rounded-full bg-[#A9BBD0]" />
<span>Ideal</span>
</div>
<div className="flex items-center justify-center gap-1">
<span className="h-2 w-2 rounded-full bg-[#4C8FFF]" />
<span>Current</span>
</div>
</div>
<span>{`Pending issues - ${cycle.backlog_issues + cycle.unstarted_issues + cycle.started_issues}`}</span>
</div> </div>
<div className="flex items-center justify-center gap-1"> <div className="relative h-full">
<span className="h-2 w-2 rounded-full bg-[#4C8FFF]" /> <ProgressChart
<span>Current</span> className="h-full"
distribution={cycle.distribution?.completion_chart ?? {}}
startDate={cycle.start_date ?? ""}
endDate={cycle.end_date ?? ""}
totalIssues={cycle.total_issues}
/>
</div> </div>
</div> </div>
<span>{`Pending issues - ${cycle.backlog_issues + cycle.unstarted_issues + cycle.started_issues}`}</span> </>
</div> ) : (
<div className="relative h-full"> <>
<ProgressChart <div className="flex items-center justify-center h-full w-full">
className="h-full" <EmptyState type={EmptyStateType.ACTIVE_CYCLE_CHART_EMPTY_STATE} layout="screen-simple" size="sm" />
distribution={cycle.distribution?.completion_chart ?? {}} </div>
startDate={cycle.start_date ?? ""} </>
endDate={cycle.end_date ?? ""} )}
totalIssues={cycle.total_issues}
/>
</div>
</div>
</div> </div>
); );
}; };

View File

@ -3,8 +3,11 @@ import { FC } from "react";
import { ICycle } from "@plane/types"; import { ICycle } from "@plane/types";
// ui // ui
import { LinearProgressIndicator } from "@plane/ui"; import { LinearProgressIndicator } from "@plane/ui";
// components
import { EmptyState } from "@/components/empty-state";
// constants // constants
import { CYCLE_STATE_GROUPS_DETAILS } from "@/constants/cycle"; import { CYCLE_STATE_GROUPS_DETAILS } from "@/constants/cycle";
import { EmptyStateType } from "@/constants/empty-state";
export type ActiveCycleProgressProps = { export type ActiveCycleProgressProps = {
cycle: ICycle; cycle: ICycle;
@ -32,48 +35,56 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<h3 className="text-base text-custom-text-300 font-semibold">Progress</h3> <h3 className="text-base text-custom-text-300 font-semibold">Progress</h3>
<span className="flex gap-1 text-sm text-custom-text-400 font-medium whitespace-nowrap rounded-sm px-3 py-1 "> {cycle.total_issues > 0 && (
{`${cycle.completed_issues + cycle.cancelled_issues}/${cycle.total_issues - cycle.cancelled_issues} ${ <span className="flex gap-1 text-sm text-custom-text-400 font-medium whitespace-nowrap rounded-sm px-3 py-1 ">
cycle.completed_issues + cycle.cancelled_issues > 1 ? "Issues" : "Issue" {`${cycle.completed_issues + cycle.cancelled_issues}/${cycle.total_issues - cycle.cancelled_issues} ${
} closed`} cycle.completed_issues + cycle.cancelled_issues > 1 ? "Issues" : "Issue"
</span> } closed`}
</span>
)}
</div> </div>
<LinearProgressIndicator size="lg" data={progressIndicatorData} /> {cycle.total_issues > 0 && <LinearProgressIndicator size="lg" data={progressIndicatorData} />}
</div> </div>
<div className="flex flex-col gap-5"> {cycle.total_issues > 0 ? (
{Object.keys(groupedIssues).map((group, index) => ( <div className="flex flex-col gap-5">
<> {Object.keys(groupedIssues).map((group, index) => (
{groupedIssues[group] > 0 && ( <>
<div key={index}> {groupedIssues[group] > 0 && (
<div className="flex items-center justify-between gap-2 text-sm"> <div key={index}>
<div className="flex items-center gap-1.5"> <div className="flex items-center justify-between gap-2 text-sm">
<span <div className="flex items-center gap-1.5">
className="block h-3 w-3 rounded-full" <span
style={{ className="block h-3 w-3 rounded-full"
backgroundColor: CYCLE_STATE_GROUPS_DETAILS[index].color, style={{
}} backgroundColor: CYCLE_STATE_GROUPS_DETAILS[index].color,
/> }}
<span className="text-custom-text-300 capitalize font-medium w-16">{group}</span> />
<span className="text-custom-text-300 capitalize font-medium w-16">{group}</span>
</div>
<span className="text-custom-text-300">{`${groupedIssues[group]} ${
groupedIssues[group] > 1 ? "Issues" : "Issue"
}`}</span>
</div> </div>
<span className="text-custom-text-300">{`${groupedIssues[group]} ${
groupedIssues[group] > 1 ? "Issues" : "Issue"
}`}</span>
</div> </div>
</div> )}
)} </>
</> ))}
))} {cycle.cancelled_issues > 0 && (
{cycle.cancelled_issues > 0 && ( <span className="flex items-center gap-2 text-sm text-custom-text-300">
<span className="flex items-center gap-2 text-sm text-custom-text-300"> <span>
<span> {`${cycle.cancelled_issues} cancelled ${
{`${cycle.cancelled_issues} cancelled ${ cycle.cancelled_issues > 1 ? "issues are" : "issue is"
cycle.cancelled_issues > 1 ? "issues are" : "issue is" } excluded from this report.`}{" "}
} excluded from this report.`}{" "} </span>
</span> </span>
</span> )}
)} </div>
</div> ) : (
<div className="flex items-center justify-center h-full w-full">
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE} layout="screen-simple" size="sm" />
</div>
)}
</div> </div>
); );
}; };

View File

@ -1,5 +1,7 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
// components // components
import { UpcomingCycleListItem } from "@/components/cycles"; import { UpcomingCycleListItem } from "@/components/cycles";
// hooks // hooks
@ -14,6 +16,11 @@ export const UpcomingCyclesList: FC<Props> = observer((props) => {
// store hooks // store hooks
const { currentProjectUpcomingCycleIds } = useCycle(); const { currentProjectUpcomingCycleIds } = useCycle();
// theme
const { resolvedTheme } = useTheme();
const resolvedEmptyStatePath = `/empty-state/active-cycle/cycle-${resolvedTheme === "light" ? "light" : "dark"}.webp`;
if (!currentProjectUpcomingCycleIds) return null; if (!currentProjectUpcomingCycleIds) return null;
return ( return (
@ -28,8 +35,18 @@ export const UpcomingCyclesList: FC<Props> = observer((props) => {
))} ))}
</div> </div>
) : ( ) : (
<div className="w-full grid place-items-center py-20"> <div className="flex items-center justify-center h-full w-full py-20">
<div className="text-center"> <div className="text-center flex flex-col gap-2.5 items-center">
<div className="h-24 w-24">
<Image
src={resolvedEmptyStatePath}
alt="button image"
width={78}
height={78}
layout="responsive"
lazyBoundary="100%"
/>
</div>
<h5 className="text-xl font-medium mb-1">No upcoming cycles</h5> <h5 className="text-xl font-medium mb-1">No upcoming cycles</h5>
<p className="text-custom-text-400 text-base"> <p className="text-custom-text-400 text-base">
Create new cycles to find them here or check Create new cycles to find them here or check

View File

@ -151,12 +151,12 @@ export const EmptyState: React.FC<EmptyStateProps> = (props) => {
)} )}
{layout === "screen-simple" && ( {layout === "screen-simple" && (
<div className="text-center flex flex-col gap-2.5 items-center"> <div className="text-center flex flex-col gap-2.5 items-center">
<div className="h-28 w-28"> <div className={`${size === "sm" ? "h-24 w-24" : "h-28 w-28"}`}>
<Image <Image
src={resolvedEmptyStatePath} src={resolvedEmptyStatePath}
alt={key || "button image"} alt={key || "button image"}
width={96} width={size === "sm" ? 78 : 96}
height={96} height={size === "sm" ? 78 : 96}
layout="responsive" layout="responsive"
lazyBoundary="100%" lazyBoundary="100%"
/> />

View File

@ -84,6 +84,12 @@ export enum EmptyStateType {
NOTIFICATION_ARCHIVED_EMPTY_STATE = "notification-archived-empty-state", NOTIFICATION_ARCHIVED_EMPTY_STATE = "notification-archived-empty-state",
NOTIFICATION_SNOOZED_EMPTY_STATE = "notification-snoozed-empty-state", NOTIFICATION_SNOOZED_EMPTY_STATE = "notification-snoozed-empty-state",
NOTIFICATION_UNREAD_EMPTY_STATE = "notification-unread-empty-state", NOTIFICATION_UNREAD_EMPTY_STATE = "notification-unread-empty-state",
ACTIVE_CYCLE_PROGRESS_EMPTY_STATE = "active-cycle-progress-empty-state",
ACTIVE_CYCLE_CHART_EMPTY_STATE = "active-cycle-chart-empty-state",
ACTIVE_CYCLE_PRIORITY_ISSUE_EMPTY_STATE = "active-cycle-priority-issue-empty-state",
ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE = "active-cycle-assignee-empty-state",
ACTIVE_CYCLE_LABEL_EMPTY_STATE = "active-cycle-label-empty-state",
} }
const emptyStateDetails = { const emptyStateDetails = {
@ -584,6 +590,31 @@ const emptyStateDetails = {
description: "Any notification you archive will be \n available here to help you focus", description: "Any notification you archive will be \n available here to help you focus",
path: "/empty-state/search/archive", path: "/empty-state/search/archive",
}, },
[EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE]: {
key: EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE,
title: "Add issues to the cycle to view it's \n progress",
path: "/empty-state/active-cycle/progress",
},
[EmptyStateType.ACTIVE_CYCLE_CHART_EMPTY_STATE]: {
key: EmptyStateType.ACTIVE_CYCLE_CHART_EMPTY_STATE,
title: "Add issues to the cycle to view the \n burndown chart.",
path: "/empty-state/active-cycle/chart",
},
[EmptyStateType.ACTIVE_CYCLE_PRIORITY_ISSUE_EMPTY_STATE]: {
key: EmptyStateType.ACTIVE_CYCLE_PRIORITY_ISSUE_EMPTY_STATE,
title: "Observe high priority issues tackled in \n the cycle at a glance.",
path: "/empty-state/active-cycle/priority",
},
[EmptyStateType.ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE]: {
key: EmptyStateType.ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE,
title: "Add assignees to issues to see a \n breakdown of work by assignees.",
path: "/empty-state/active-cycle/assignee",
},
[EmptyStateType.ACTIVE_CYCLE_LABEL_EMPTY_STATE]: {
key: EmptyStateType.ACTIVE_CYCLE_LABEL_EMPTY_STATE,
title: "Add labels to issues to see the \n breakdown of work by labels.",
path: "/empty-state/active-cycle/label",
},
} as const; } as const;
export const EMPTY_STATE_DETAILS: Record<EmptyStateType, EmptyStateDetails> = emptyStateDetails; export const EMPTY_STATE_DETAILS: Record<EmptyStateType, EmptyStateDetails> = emptyStateDetails;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB