[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
@ -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,7 +201,8 @@ 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 ? (
|
||||||
|
cycle.distribution?.assignees?.map((assignee, index) => {
|
||||||
if (assignee.assignee_id)
|
if (assignee.assignee_id)
|
||||||
return (
|
return (
|
||||||
<SingleProgressStats
|
<SingleProgressStats
|
||||||
@ -227,14 +234,20 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
|||||||
total={assignee.total_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 ? (
|
||||||
|
cycle.distribution?.labels?.map((label, index) => (
|
||||||
<SingleProgressStats
|
<SingleProgressStats
|
||||||
key={label.label_id ?? `no-label-${index}`}
|
key={label.label_id ?? `no-label-${index}`}
|
||||||
title={
|
title={
|
||||||
@ -251,7 +264,12 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
|||||||
completed={label.completed_issues}
|
completed={label.completed_issues}
|
||||||
total={label.total_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>
|
||||||
|
@ -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,7 +19,8 @@ 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="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="flex items-center justify-between gap-4 py-1 text-xs text-custom-text-300">
|
||||||
<div className="flex items-center gap-3 text-custom-text-300">
|
<div className="flex items-center gap-3 text-custom-text-300">
|
||||||
@ -41,6 +45,14 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props)
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-center h-full w-full">
|
||||||
|
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_CHART_EMPTY_STATE} layout="screen-simple" size="sm" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,15 +35,18 @@ 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>
|
||||||
|
{cycle.total_issues > 0 && (
|
||||||
<span className="flex gap-1 text-sm text-custom-text-400 font-medium whitespace-nowrap rounded-sm px-3 py-1 ">
|
<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}/${cycle.total_issues - cycle.cancelled_issues} ${
|
{`${cycle.completed_issues + cycle.cancelled_issues}/${cycle.total_issues - cycle.cancelled_issues} ${
|
||||||
cycle.completed_issues + cycle.cancelled_issues > 1 ? "Issues" : "Issue"
|
cycle.completed_issues + cycle.cancelled_issues > 1 ? "Issues" : "Issue"
|
||||||
} closed`}
|
} closed`}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<LinearProgressIndicator size="lg" data={progressIndicatorData} />
|
{cycle.total_issues > 0 && <LinearProgressIndicator size="lg" data={progressIndicatorData} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{cycle.total_issues > 0 ? (
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
{Object.keys(groupedIssues).map((group, index) => (
|
{Object.keys(groupedIssues).map((group, index) => (
|
||||||
<>
|
<>
|
||||||
@ -74,6 +80,11 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
|
|||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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%"
|
||||||
/>
|
/>
|
||||||
|
@ -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;
|
||||||
|
BIN
web/public/empty-state/active-cycle/assignee-dark.webp
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
web/public/empty-state/active-cycle/assignee-light.webp
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/public/empty-state/active-cycle/chart-dark.webp
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/public/empty-state/active-cycle/chart-light.webp
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/public/empty-state/active-cycle/cycle-dark.webp
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
web/public/empty-state/active-cycle/cycle-light.webp
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
web/public/empty-state/active-cycle/label-dark.webp
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/public/empty-state/active-cycle/label-light.webp
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
web/public/empty-state/active-cycle/priority-dark.webp
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
web/public/empty-state/active-cycle/priority-light.webp
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/public/empty-state/active-cycle/progress-dark.webp
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
web/public/empty-state/active-cycle/progress-light.webp
Normal file
After Width: | Height: | Size: 1.1 KiB |