mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
9065b5d368
* fix: created dashboard, widgets and dashboard widget model * fix: new user home dashboard * chore: recent projects list * chore: recent collaborators * chore: priority order change * chore: payload changes * chore: collaborator's active issue count * chore: all dashboard widgets added with services and typs * chore: centered metric for pie chart * chore: widget filters * chore: created issue filter * fix: created and assigned issues payload change * chore: created issue payload change * fix: date filter change * chore: implement filters * fix: added expansion fields * fix: changed issue structure with relation * chore: new issues response * fix: project member fix * chore: updated issue_relation structure * chore: code cleanup * chore: update issues response and added empty states * fix: button text wrap * chore: update empty state messages * fix: filters * chore: update dark mode empty states * build-error: Type check in the issue relation service * fix: issues redirection * fix: project empty state * chore: project member active check * chore: project member check in state and priority * chore: remove console logs and replace harcoded values with constants * fix: code refactoring * fix: key name changed * refactor: mapping through similar components using an array * fix: build errors --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com> Co-authored-by: gurusainath <gurusainath007@gmail.com>
209 lines
6.6 KiB
TypeScript
209 lines
6.6 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { observer } from "mobx-react-lite";
|
|
// hooks
|
|
import { useDashboard } from "hooks/store";
|
|
// components
|
|
import { MarimekkoGraph } from "components/ui";
|
|
import {
|
|
DurationFilterDropdown,
|
|
IssuesByPriorityEmptyState,
|
|
WidgetLoader,
|
|
WidgetProps,
|
|
} from "components/dashboard/widgets";
|
|
// ui
|
|
import { PriorityIcon } from "@plane/ui";
|
|
// helpers
|
|
import { getCustomDates } from "helpers/dashboard.helper";
|
|
// types
|
|
import { TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types";
|
|
// constants
|
|
import { PRIORITY_GRAPH_GRADIENTS } from "constants/dashboard";
|
|
import { ISSUE_PRIORITIES } from "constants/issue";
|
|
|
|
const TEXT_COLORS = {
|
|
urgent: "#F4A9AA",
|
|
high: "#AB4800",
|
|
medium: "#AB6400",
|
|
low: "#1F2D5C",
|
|
none: "#60646C",
|
|
};
|
|
|
|
const CustomBar = (props: any) => {
|
|
const { bar, workspaceSlug } = props;
|
|
// states
|
|
const [isMouseOver, setIsMouseOver] = useState(false);
|
|
|
|
return (
|
|
<Link href={`/${workspaceSlug}/workspace-views/assigned?priority=${bar?.id}`}>
|
|
<g
|
|
transform={`translate(${bar?.x},${bar?.y})`}
|
|
onMouseEnter={() => setIsMouseOver(true)}
|
|
onMouseLeave={() => setIsMouseOver(false)}
|
|
>
|
|
<rect
|
|
x={0}
|
|
y={isMouseOver ? -6 : 0}
|
|
width={bar?.width}
|
|
height={isMouseOver ? bar?.height + 6 : bar?.height}
|
|
fill={bar?.fill}
|
|
stroke={bar?.borderColor}
|
|
strokeWidth={bar?.borderWidth}
|
|
rx={4}
|
|
ry={4}
|
|
className="duration-300"
|
|
/>
|
|
<text
|
|
x={-bar?.height + 10}
|
|
y={18}
|
|
fill={TEXT_COLORS[bar?.id as keyof typeof TEXT_COLORS]}
|
|
className="capitalize font-medium text-lg -rotate-90"
|
|
dominantBaseline="text-bottom"
|
|
>
|
|
{bar?.id}
|
|
</text>
|
|
</g>
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
const WIDGET_KEY = "issues_by_priority";
|
|
|
|
export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) => {
|
|
const { dashboardId, workspaceSlug } = props;
|
|
// store hooks
|
|
const {
|
|
fetchWidgetStats,
|
|
widgetDetails: allWidgetDetails,
|
|
widgetStats: allWidgetStats,
|
|
updateDashboardWidgetFilters,
|
|
} = useDashboard();
|
|
const widgetDetails = allWidgetDetails?.[workspaceSlug]?.[dashboardId]?.find((w) => w.key === WIDGET_KEY);
|
|
const widgetStats = allWidgetStats?.[workspaceSlug]?.[dashboardId]?.[WIDGET_KEY] as TIssuesByPriorityWidgetResponse[];
|
|
|
|
const handleUpdateFilters = async (filters: Partial<TIssuesByPriorityWidgetFilters>) => {
|
|
if (!widgetDetails) return;
|
|
|
|
await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, {
|
|
widgetKey: WIDGET_KEY,
|
|
filters,
|
|
});
|
|
|
|
fetchWidgetStats(workspaceSlug, dashboardId, {
|
|
widget_key: WIDGET_KEY,
|
|
target_date: getCustomDates(widgetDetails.widget_filters.target_date ?? "this_week"),
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!widgetDetails) return;
|
|
|
|
if (!widgetStats)
|
|
fetchWidgetStats(workspaceSlug, dashboardId, {
|
|
widget_key: WIDGET_KEY,
|
|
target_date: getCustomDates(widgetDetails.widget_filters.target_date ?? "this_week"),
|
|
});
|
|
}, [dashboardId, fetchWidgetStats, widgetDetails, widgetStats, workspaceSlug]);
|
|
|
|
if (!widgetDetails || !widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
|
|
|
|
const totalCount = widgetStats.reduce((acc, item) => acc + item?.count, 0);
|
|
const chartData = widgetStats
|
|
.filter((i) => i.count !== 0)
|
|
.map((item) => ({
|
|
priority: item?.priority,
|
|
percentage: (item?.count / totalCount) * 100,
|
|
urgent: item?.priority === "urgent" ? 1 : 0,
|
|
high: item?.priority === "high" ? 1 : 0,
|
|
medium: item?.priority === "medium" ? 1 : 0,
|
|
low: item?.priority === "low" ? 1 : 0,
|
|
none: item?.priority === "none" ? 1 : 0,
|
|
}));
|
|
|
|
const CustomBarsLayer = (props: any) => {
|
|
const { bars } = props;
|
|
|
|
return (
|
|
<g>
|
|
{bars
|
|
?.filter((b: any) => b?.value === 1) // render only bars with value 1
|
|
.map((bar: any) => (
|
|
<CustomBar key={bar?.key} bar={bar} workspaceSlug={workspaceSlug} />
|
|
))}
|
|
</g>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Link
|
|
href={`/${workspaceSlug}/workspace-views/assigned`}
|
|
className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full py-6 hover:shadow-custom-shadow-4xl duration-300 overflow-hidden"
|
|
>
|
|
<div className="flex items-center justify-between gap-2 pl-7 pr-6">
|
|
<h4 className="text-lg font-semibold text-custom-text-300">Priority of assigned issues</h4>
|
|
<DurationFilterDropdown
|
|
value={widgetDetails.widget_filters.target_date ?? "this_week"}
|
|
onChange={(val) =>
|
|
handleUpdateFilters({
|
|
target_date: val,
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
{totalCount > 0 ? (
|
|
<div className="flex items-center px-11 h-full">
|
|
<div className="w-full -mt-[11px]">
|
|
<MarimekkoGraph
|
|
data={chartData}
|
|
id="priority"
|
|
value="percentage"
|
|
dimensions={ISSUE_PRIORITIES.map((p) => ({
|
|
id: p.key,
|
|
value: p.key,
|
|
}))}
|
|
axisBottom={null}
|
|
axisLeft={null}
|
|
height="119px"
|
|
margin={{
|
|
top: 11,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
}}
|
|
defs={PRIORITY_GRAPH_GRADIENTS}
|
|
fill={ISSUE_PRIORITIES.map((p) => ({
|
|
match: {
|
|
id: p.key,
|
|
},
|
|
id: `gradient${p.title}`,
|
|
}))}
|
|
tooltip={() => <></>}
|
|
enableGridX={false}
|
|
enableGridY={false}
|
|
layers={[CustomBarsLayer]}
|
|
/>
|
|
<div className="flex items-center gap-1 w-full mt-3 text-sm font-semibold text-custom-text-300">
|
|
{chartData.map((item) => (
|
|
<p
|
|
key={item.priority}
|
|
className="flex items-center gap-1 flex-shrink-0"
|
|
style={{
|
|
width: `${item.percentage}%`,
|
|
}}
|
|
>
|
|
<PriorityIcon priority={item.priority} withContainer />
|
|
{item.percentage.toFixed(0)}%
|
|
</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="h-full grid items-end">
|
|
<IssuesByPriorityEmptyState filter={widgetDetails.widget_filters.target_date ?? "this_week"} />
|
|
</div>
|
|
)}
|
|
</Link>
|
|
);
|
|
});
|