mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: cycle stats empty state (#1338)
* chore: active cycle percentage fix * fix: progress chart x-axis values
This commit is contained in:
parent
9c85704be3
commit
d3c56c1765
@ -3,7 +3,7 @@ import React from "react";
|
|||||||
// ui
|
// ui
|
||||||
import { LineGraph } from "components/ui";
|
import { LineGraph } from "components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
import { getDatesInRange, renderShortNumericDateFormat } from "helpers/date-time.helper";
|
||||||
//types
|
//types
|
||||||
import { TCompletionChartDistribution } from "types";
|
import { TCompletionChartDistribution } from "types";
|
||||||
|
|
||||||
@ -46,6 +46,27 @@ const ProgressChart: React.FC<Props> = ({ distribution, startDate, endDate, tota
|
|||||||
pending: distribution[key],
|
pending: distribution[key],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const generateXAxisTickValues = () => {
|
||||||
|
const dates = getDatesInRange(startDate, endDate);
|
||||||
|
|
||||||
|
const maxDates = 4;
|
||||||
|
const totalDates = dates.length;
|
||||||
|
|
||||||
|
if (totalDates <= maxDates) return dates;
|
||||||
|
else {
|
||||||
|
const interval = Math.ceil(totalDates / maxDates);
|
||||||
|
const limitedDates = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < totalDates; i += interval)
|
||||||
|
limitedDates.push(renderShortNumericDateFormat(dates[i]));
|
||||||
|
|
||||||
|
if (!limitedDates.includes(renderShortNumericDateFormat(dates[totalDates - 1])))
|
||||||
|
limitedDates.push(renderShortNumericDateFormat(dates[totalDates - 1]));
|
||||||
|
|
||||||
|
return limitedDates;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex justify-center items-center">
|
<div className="w-full flex justify-center items-center">
|
||||||
<LineGraph
|
<LineGraph
|
||||||
@ -86,7 +107,7 @@ const ProgressChart: React.FC<Props> = ({ distribution, startDate, endDate, tota
|
|||||||
]}
|
]}
|
||||||
layers={["grid", "markers", "areas", DashedLine, "slices", "points", "axes", "legends"]}
|
layers={["grid", "markers", "areas", DashedLine, "slices", "points", "axes", "legends"]}
|
||||||
axisBottom={{
|
axisBottom={{
|
||||||
tickValues: chartData.map((item, index) => (index % 2 === 0 ? item.currentDate : "")),
|
tickValues: generateXAxisTickValues(),
|
||||||
}}
|
}}
|
||||||
enablePoints={false}
|
enablePoints={false}
|
||||||
enableArea
|
enableArea
|
||||||
|
@ -395,82 +395,87 @@ export const ActiveCycleDetails: React.FC = () => {
|
|||||||
<div className="text-brand-primary">High Priority Issues</div>
|
<div className="text-brand-primary">High Priority Issues</div>
|
||||||
<div className="my-3 flex max-h-[240px] min-h-[240px] flex-col gap-2.5 overflow-y-scroll rounded-md">
|
<div className="my-3 flex max-h-[240px] min-h-[240px] flex-col gap-2.5 overflow-y-scroll rounded-md">
|
||||||
{issues ? (
|
{issues ? (
|
||||||
issues.map((issue) => (
|
issues.length > 0 ? (
|
||||||
<div
|
issues.map((issue) => (
|
||||||
key={issue.id}
|
<div
|
||||||
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"
|
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>
|
<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
|
<Tooltip
|
||||||
tooltipHeading="Issue ID"
|
position="top-left"
|
||||||
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
tooltipHeading="Title"
|
||||||
|
tooltipContent={issue.name}
|
||||||
>
|
>
|
||||||
<span className="flex-shrink-0 text-xs text-brand-secondary">
|
<span className="text-[0.825rem] text-brand-base">
|
||||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
{truncateText(issue.name, 30)}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip
|
<div className="flex items-center gap-1.5">
|
||||||
position="top-left"
|
<div
|
||||||
tooltipHeading="Title"
|
className={`grid h-6 w-6 place-items-center items-center rounded border shadow-sm flex-shrink-0 ${
|
||||||
tooltipContent={issue.name}
|
issue.priority === "urgent"
|
||||||
>
|
? "border-red-500/20 bg-red-500/20 text-red-500"
|
||||||
<span className="text-[0.825rem] text-brand-base">
|
: "border-orange-500/20 bg-orange-500/20 text-orange-500"
|
||||||
{truncateText(issue.name, 30)}
|
}`}
|
||||||
</span>
|
>
|
||||||
</Tooltip>
|
{getPriorityIcon(issue.priority, "text-sm")}
|
||||||
</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 flex-shrink-0 ${
|
|
||||||
issue.priority === "urgent"
|
|
||||||
? "border-red-500/20 bg-red-500/20 text-red-500"
|
|
||||||
: "border-orange-500/20 bg-orange-500/20 text-orange-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{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>
|
||||||
) : (
|
{issue.label_details.length > 0 ? (
|
||||||
""
|
<div className="flex flex-wrap gap-1">
|
||||||
)}
|
{issue.label_details.map((label) => (
|
||||||
<div className={`flex items-center gap-2 text-brand-secondary`}>
|
<span
|
||||||
{issue.assignees &&
|
key={label.id}
|
||||||
issue.assignees.length > 0 &&
|
className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary"
|
||||||
Array.isArray(issue.assignees) ? (
|
>
|
||||||
<div className="-my-0.5 flex items-center justify-center gap-2">
|
<span
|
||||||
<AssigneesList
|
className="h-1.5 w-1.5 rounded-full"
|
||||||
users={issue.assignee_details}
|
style={{
|
||||||
length={3}
|
backgroundColor:
|
||||||
showLength={false}
|
label?.color && label.color !== "" ? label.color : "#000",
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
{label.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</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
|
||||||
|
users={issue.assignee_details}
|
||||||
|
length={3}
|
||||||
|
showLength={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="grid place-items-center text-brand-secondary text-sm text-center">
|
||||||
|
No issues present in the cycle.
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
) : (
|
) : (
|
||||||
<Loader className="space-y-3">
|
<Loader className="space-y-3">
|
||||||
<Loader.Item height="50px" />
|
<Loader.Item height="50px" />
|
||||||
@ -481,27 +486,29 @@ export const ActiveCycleDetails: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
{issues && issues.length > 0 && (
|
||||||
<div className="h-1 w-full rounded-full bg-brand-surface-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div
|
<div className="h-1 w-full rounded-full bg-brand-surface-2">
|
||||||
className="h-1 rounded-full bg-green-600"
|
<div
|
||||||
style={{
|
className="h-1 rounded-full bg-green-600"
|
||||||
width:
|
style={{
|
||||||
issues &&
|
width:
|
||||||
`${
|
issues &&
|
||||||
(issues.filter((issue) => issue?.state_detail?.group === "completed")
|
`${
|
||||||
?.length /
|
(issues.filter((issue) => issue?.state_detail?.group === "completed")
|
||||||
issues.length) *
|
?.length /
|
||||||
100 ?? 0
|
issues.length) *
|
||||||
}%`,
|
100 ?? 0
|
||||||
}}
|
}%`,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-16 text-end text-xs text-brand-secondary">
|
||||||
|
{issues?.filter((issue) => issue?.state_detail?.group === "completed")?.length} of{" "}
|
||||||
|
{issues?.length}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-16 text-end text-xs text-brand-secondary">
|
)}
|
||||||
{issues?.filter((issue) => issue?.state_detail?.group === "completed")?.length} of{" "}
|
|
||||||
{issues?.length}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-between border-brand-base p-4">
|
<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-start justify-between gap-4 py-1.5 text-xs">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { Fragment } from "react";
|
||||||
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
@ -32,6 +32,7 @@ export const ActiveCycleProgressStats: React.FC<Props> = ({ cycle }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Group
|
<Tab.Group
|
||||||
|
as={Fragment}
|
||||||
defaultIndex={currentValue(tab)}
|
defaultIndex={currentValue(tab)}
|
||||||
onChange={(i) => {
|
onChange={(i) => {
|
||||||
switch (i) {
|
switch (i) {
|
||||||
@ -68,81 +69,87 @@ export const ActiveCycleProgressStats: React.FC<Props> = ({ cycle }) => {
|
|||||||
Labels
|
Labels
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels className="flex w-full px-4 pb-4">
|
{cycle.total_issues > 0 ? (
|
||||||
<Tab.Panel
|
<Tab.Panels as={Fragment}>
|
||||||
as="div"
|
<Tab.Panel
|
||||||
className="flex flex-col w-full mt-2 gap-1 overflow-y-scroll items-center text-brand-secondary"
|
as="div"
|
||||||
>
|
className="w-full gap-1 overflow-y-scroll items-center text-brand-secondary p-4"
|
||||||
{cycle.distribution.assignees.map((assignee, index) => {
|
>
|
||||||
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
|
<div className="flex items-center gap-2">
|
||||||
user={{
|
<Avatar
|
||||||
id: assignee.assignee_id,
|
user={{
|
||||||
avatar: assignee.avatar ?? "",
|
id: assignee.assignee_id,
|
||||||
first_name: assignee.first_name ?? "",
|
avatar: assignee.avatar ?? "",
|
||||||
last_name: assignee.last_name ?? "",
|
first_name: assignee.first_name ?? "",
|
||||||
}}
|
last_name: assignee.last_name ?? "",
|
||||||
/>
|
}}
|
||||||
<span>{assignee.first_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-brand-base bg-brand-surface-2">
|
|
||||||
<img
|
|
||||||
src="/user.png"
|
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
className="rounded-full"
|
|
||||||
alt="User"
|
|
||||||
/>
|
/>
|
||||||
|
<span>{assignee.first_name}</span>
|
||||||
</div>
|
</div>
|
||||||
<span>No assignee</span>
|
}
|
||||||
</div>
|
completed={assignee.completed_issues}
|
||||||
}
|
total={assignee.total_issues}
|
||||||
completed={assignee.completed_issues}
|
|
||||||
total={assignee.total_issues}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel
|
|
||||||
as="div"
|
|
||||||
className="flex flex-col w-full mt-2 gap-1 overflow-y-scroll items-center text-brand-secondary"
|
|
||||||
>
|
|
||||||
{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 ?? "transparent",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<span className="text-xs">{label.label_name ?? "No labels"}</span>
|
);
|
||||||
</div>
|
else
|
||||||
}
|
return (
|
||||||
completed={label.completed_issues}
|
<SingleProgressStats
|
||||||
total={label.total_issues}
|
key={`unassigned-${index}`}
|
||||||
/>
|
title={
|
||||||
))}
|
<div className="flex items-center gap-2">
|
||||||
</Tab.Panel>
|
<div className="h-5 w-5 rounded-full border-2 border-brand-base bg-brand-surface-2">
|
||||||
</Tab.Panels>
|
<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="w-full gap-1 overflow-y-scroll items-center text-brand-secondary p-4"
|
||||||
|
>
|
||||||
|
{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 ?? "transparent",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">{label.label_name ?? "No labels"}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
completed={label.completed_issues}
|
||||||
|
total={label.total_issues}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
) : (
|
||||||
|
<div className="grid place-items-center text-brand-secondary text-sm text-center mt-4">
|
||||||
|
No issues present in the cycle.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -282,12 +282,18 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
|||||||
>
|
>
|
||||||
{cycleStatus === "current" ? (
|
{cycleStatus === "current" ? (
|
||||||
<span className="flex gap-1">
|
<span className="flex gap-1">
|
||||||
<RadialProgressBar
|
{cycle.total_issues > 0 ? (
|
||||||
progress={(cycle.completed_issues / cycle.total_issues) * 100}
|
<>
|
||||||
/>
|
<RadialProgressBar
|
||||||
<span>
|
progress={(cycle.completed_issues / cycle.total_issues) * 100}
|
||||||
{Math.floor((cycle.completed_issues / cycle.total_issues) * 100)} %
|
/>
|
||||||
</span>
|
<span>
|
||||||
|
{Math.floor((cycle.completed_issues / cycle.total_issues) * 100)} %
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="normal-case">No issues present</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
) : cycleStatus === "upcoming" ? (
|
) : cycleStatus === "upcoming" ? (
|
||||||
<span className="flex gap-1">
|
<span className="flex gap-1">
|
||||||
|
@ -25,13 +25,18 @@ export const findHowManyDaysLeft = (date: string | Date) => {
|
|||||||
return Math.ceil(timeDiff / (1000 * 3600 * 24));
|
return Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDatesInRange = (startDate: Date, endDate: Date) => {
|
export const getDatesInRange = (startDate: string | Date, endDate: string | Date) => {
|
||||||
|
startDate = new Date(startDate);
|
||||||
|
endDate = new Date(endDate);
|
||||||
|
|
||||||
const date = new Date(startDate.getTime());
|
const date = new Date(startDate.getTime());
|
||||||
const dates = [];
|
const dates = [];
|
||||||
|
|
||||||
while (date <= endDate) {
|
while (date <= endDate) {
|
||||||
dates.push(new Date(date));
|
dates.push(new Date(date));
|
||||||
date.setDate(date.getDate() + 1);
|
date.setDate(date.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return dates;
|
return dates;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user